|
1 | | -import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "fs"; |
2 | | -import { join } from "path"; |
| 1 | +import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs"; |
| 2 | +import { basename, dirname, join } from "path"; |
3 | 3 | import { SensitiveEntry } from "./config"; |
4 | 4 | import { jailbreakDir, sensitiveFactsDir } from "./paths"; |
5 | 5 | import { ensureQmdIndex, resolveQmdCommand, runQmd } from "./qmd"; |
@@ -67,17 +67,76 @@ export function ensureJailbreakFiles(): void { |
67 | 67 |
|
68 | 68 | export function syncSensitiveFactsFiles(profile: string, entries: SensitiveEntry[]): void { |
69 | 69 | const dir = sensitiveFactsDir(profile); |
70 | | - ensureDir(dir); |
71 | | - const existing = readdirSync(dir).filter((f) => f.endsWith(".md")); |
72 | | - for (const file of existing) { |
73 | | - unlinkSync(join(dir, file)); |
| 70 | + const parent = dirname(dir); |
| 71 | + ensureDir(parent); |
| 72 | + |
| 73 | + const writeFiles = (target: string) => { |
| 74 | + ensureDir(target); |
| 75 | + entries.forEach((entry, idx) => { |
| 76 | + const safeLabel = entry.label.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase(); |
| 77 | + const filename = join(target, `${idx + 1}-${safeLabel || "fact"}.md`); |
| 78 | + const body = entry.pattern; |
| 79 | + writeFileSync(filename, `${body}\n`, { mode: 0o600 }); |
| 80 | + }); |
| 81 | + }; |
| 82 | + |
| 83 | + const dirName = basename(dir); |
| 84 | + const tmpDir = join(parent, `${dirName}.tmp-${Date.now()}`); |
| 85 | + let tmpReady = false; |
| 86 | + try { |
| 87 | + writeFiles(tmpDir); |
| 88 | + tmpReady = true; |
| 89 | + } catch { |
| 90 | + // fall back to in-place writes if temp dir cannot be created |
| 91 | + try { |
| 92 | + writeFiles(dir); |
| 93 | + } catch { |
| 94 | + // ignore write failures |
| 95 | + } |
| 96 | + return; |
| 97 | + } |
| 98 | + |
| 99 | + const backupDir = join(parent, `${dirName}.bak-${Date.now()}`); |
| 100 | + let backupMoved = false; |
| 101 | + let swapped = false; |
| 102 | + |
| 103 | + try { |
| 104 | + if (existsSync(dir)) { |
| 105 | + renameSync(dir, backupDir); |
| 106 | + backupMoved = true; |
| 107 | + } |
| 108 | + renameSync(tmpDir, dir); |
| 109 | + swapped = true; |
| 110 | + } catch { |
| 111 | + if (backupMoved && !existsSync(dir)) { |
| 112 | + try { |
| 113 | + renameSync(backupDir, dir); |
| 114 | + backupMoved = false; |
| 115 | + } catch { |
| 116 | + // ignore restore failures |
| 117 | + } |
| 118 | + } |
| 119 | + try { |
| 120 | + writeFiles(dir); |
| 121 | + } catch { |
| 122 | + // ignore write failures |
| 123 | + } |
| 124 | + } finally { |
| 125 | + if (!swapped) { |
| 126 | + try { |
| 127 | + rmSync(tmpDir, { recursive: true, force: true }); |
| 128 | + } catch { |
| 129 | + // ignore cleanup failures |
| 130 | + } |
| 131 | + } |
| 132 | + if (backupMoved) { |
| 133 | + try { |
| 134 | + rmSync(backupDir, { recursive: true, force: true }); |
| 135 | + } catch { |
| 136 | + // ignore cleanup failures |
| 137 | + } |
| 138 | + } |
74 | 139 | } |
75 | | - entries.forEach((entry, idx) => { |
76 | | - const safeLabel = entry.label.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase(); |
77 | | - const filename = join(dir, `${idx + 1}-${safeLabel || "fact"}.md`); |
78 | | - const body = entry.pattern; |
79 | | - writeFileSync(filename, `${body}\n`, { mode: 0o600 }); |
80 | | - }); |
81 | 140 | } |
82 | 141 |
|
83 | 142 | export type SafetyMatch = { |
|
0 commit comments