refactor(config): extract the safe-write primitives cc-mutate guards with
This commit is contained in:
+9
-72
@@ -31,6 +31,7 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { getClaudeHome } = require("./claude-home");
|
||||
const { isUnder, MAX_FILE_BYTES } = require("./cc-discovery");
|
||||
const { timestamp, createBackup, atomicWriteFile } = require("./config-write");
|
||||
|
||||
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
||||
// Auto-memory files are arbitrary flat *.md filenames inside a project's
|
||||
@@ -173,84 +174,22 @@ function autoMemoryBackupRoot(memDir) {
|
||||
return path.join(memDir, ".cc-config-backups", "auto-memory");
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
return new Date().toISOString().replace(/[:]/g, "-");
|
||||
}
|
||||
|
||||
function copyDirSync(src, dst) {
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, ent.name);
|
||||
const d = path.join(dst, ent.name);
|
||||
if (ent.isDirectory()) copyDirSync(s, d);
|
||||
else if (ent.isFile()) fs.copyFileSync(s, d);
|
||||
// symlinks/sockets/etc skipped intentionally — these surfaces are
|
||||
// text-file-only by spec
|
||||
}
|
||||
}
|
||||
|
||||
function rmTreeSync(p) {
|
||||
fs.rmSync(p, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Always-on backup. For files, copies to <backupRoot>/<name>.<ts>.bak. For
|
||||
* dirs (skills), copies the whole tree. Returns the backup path (or null
|
||||
* if there was nothing to back up — e.g. brand-new file).
|
||||
* Always-on backup, routed to the bucket the (scope, type) pair backs up
|
||||
* into. For files, copies to <bucket>/<name>.<ts>.bak. For dirs (skills),
|
||||
* copies the whole tree. Returns the backup path (or null if there was
|
||||
* nothing to back up — e.g. brand-new file).
|
||||
*/
|
||||
function createBackup({ scope, type, target, kind, opts }) {
|
||||
if (!fs.existsSync(target)) return null;
|
||||
function backupTarget({ scope, type, target, opts }) {
|
||||
let root;
|
||||
if (type === "memory") root = memoryBackupRoot(scope, opts);
|
||||
else if (type === "auto-memory") root = autoMemoryBackupRoot(path.dirname(target));
|
||||
else root = backupRoot(scope, type, opts);
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const base = path.basename(target);
|
||||
const stamp = timestamp();
|
||||
if (kind === "dir") {
|
||||
const dst = path.join(root, `${base}.${stamp}.bak`);
|
||||
copyDirSync(target, dst);
|
||||
return dst;
|
||||
}
|
||||
// file
|
||||
const dst = path.join(root, `${base}.${stamp}.bak`);
|
||||
fs.copyFileSync(target, dst);
|
||||
return dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomic write: tmp file → fsync (best-effort) → rename. Tmp is unlinked
|
||||
* on any failure path. Caller is responsible for ensuring parent dir exists.
|
||||
*/
|
||||
function atomicWriteFile(filePath, content) {
|
||||
const dir = path.dirname(filePath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
||||
let fd;
|
||||
try {
|
||||
fd = fs.openSync(tmp, "wx");
|
||||
fs.writeSync(fd, content);
|
||||
try {
|
||||
fs.fsyncSync(fd);
|
||||
} catch {
|
||||
// fsync may fail on some filesystems / tmpfs — non-fatal
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
fd = null;
|
||||
fs.renameSync(tmp, filePath);
|
||||
} catch (err) {
|
||||
try {
|
||||
if (fd != null) fs.closeSync(fd);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return createBackup({ source: target, backupRoot: root, type, name: path.basename(target) });
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
@@ -278,11 +217,10 @@ function writeArtifact(args) {
|
||||
|
||||
const existedBefore = fs.existsSync(r.filePath);
|
||||
const backupPath = existedBefore
|
||||
? createBackup({
|
||||
? backupTarget({
|
||||
scope,
|
||||
type,
|
||||
target: r.kind === "dir" ? r.target : r.filePath,
|
||||
kind: r.kind,
|
||||
opts: { cwd },
|
||||
})
|
||||
: null;
|
||||
@@ -317,11 +255,10 @@ function deleteArtifact(args) {
|
||||
throw makeError("ENOTFOUND", `${type}/${name || "CLAUDE.md"} does not exist`);
|
||||
}
|
||||
|
||||
const backupPath = createBackup({
|
||||
const backupPath = backupTarget({
|
||||
scope,
|
||||
type,
|
||||
target: r.target,
|
||||
kind: r.kind === "memoryFile" ? "file" : r.kind,
|
||||
opts: { cwd },
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user