refactor(gsd): extract safeCopy/safeMkdir helpers to replace repetitive try/catch FS patterns (#1043)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-17 18:36:17 -06:00 committed by GitHub
parent 4f10e9bdc4
commit 665121537d
3 changed files with 55 additions and 41 deletions

View file

@ -13,6 +13,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync, readdirSync } from "node:fs";
import { join, sep as pathSep } from "node:path";
import { homedir } from "node:os";
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
@ -32,14 +33,7 @@ export function syncProjectRootToWorktree(projectRoot: string, worktreePath: str
// Copy milestone directory from project root to worktree if the project root
// has newer artifacts (e.g. slices that don't exist in the worktree yet)
try {
const srcMilestone = join(prGsd, "milestones", milestoneId);
const dstMilestone = join(wtGsd, "milestones", milestoneId);
if (existsSync(srcMilestone)) {
mkdirSync(dstMilestone, { recursive: true });
cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
}
} catch { /* non-fatal */ }
safeCopyRecursive(join(prGsd, "milestones", milestoneId), join(wtGsd, "milestones", milestoneId))
// Delete worktree gsd.db so it rebuilds from the freshly synced files.
// Stale DB rows are the root cause of the infinite skip loop (#853).
@ -67,22 +61,11 @@ export function syncStateToProjectRoot(worktreePath: string, projectRoot: string
const prGsd = join(projectRoot, ".gsd");
// 1. STATE.md — the quick-glance status used by initial deriveState()
try {
const src = join(wtGsd, "STATE.md");
const dst = join(prGsd, "STATE.md");
if (existsSync(src)) cpSync(src, dst, { force: true });
} catch { /* non-fatal */ }
safeCopy(join(wtGsd, "STATE.md"), join(prGsd, "STATE.md"), { force: true })
// 2. Milestone directory — ROADMAP, slice PLANs, task summaries
// Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
try {
const srcMilestone = join(wtGsd, "milestones", milestoneId);
const dstMilestone = join(prGsd, "milestones", milestoneId);
if (existsSync(srcMilestone)) {
mkdirSync(dstMilestone, { recursive: true });
cpSync(srcMilestone, dstMilestone, { recursive: true, force: true });
}
} catch { /* non-fatal */ }
safeCopyRecursive(join(wtGsd, "milestones", milestoneId), join(prGsd, "milestones", milestoneId), { force: true })
// 3. Merge completed-units.json (set-union of both locations)
// Prevents already-completed units from being re-dispatched after crash/restart.
@ -104,14 +87,7 @@ export function syncStateToProjectRoot(worktreePath: string, projectRoot: string
// Without this, a crash during a unit leaves the runtime record only in the
// worktree. If the next session resolves basePath before worktree re-entry,
// selfHeal can't find or clear the stale record (#769).
try {
const srcRuntime = join(wtGsd, "runtime", "units");
const dstRuntime = join(prGsd, "runtime", "units");
if (existsSync(srcRuntime)) {
mkdirSync(dstRuntime, { recursive: true });
cpSync(srcRuntime, dstRuntime, { recursive: true, force: true });
}
} catch { /* non-fatal */ }
safeCopyRecursive(join(wtGsd, "runtime", "units"), join(prGsd, "runtime", "units"), { force: true })
}
// ─── Resource Staleness ───────────────────────────────────────────────────

View file

@ -11,6 +11,7 @@ import { isAbsolute, join } from "node:path";
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
import { execSync, execFileSync } from "node:child_process";
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
import {
createWorktree,
removeWorktree,
@ -290,21 +291,11 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
if (!existsSync(srcGsd)) return;
// Copy milestones/ directory (planning files, roadmaps, plans, research)
const srcMilestones = join(srcGsd, "milestones");
if (existsSync(srcMilestones)) {
try {
cpSync(srcMilestones, join(dstGsd, "milestones"), { recursive: true, force: true });
} catch { /* non-fatal */ }
}
safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), { force: true });
// Copy top-level planning files
for (const file of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "QUEUE.md", "STATE.md", "KNOWLEDGE.md", "OVERRIDES.md"]) {
const src = join(srcGsd, file);
if (existsSync(src)) {
try {
cpSync(src, join(dstGsd, file), { force: true });
} catch { /* non-fatal */ }
}
safeCopy(join(srcGsd, file), join(dstGsd, file), { force: true });
}
// Copy gsd.db if present in source

View file

@ -0,0 +1,47 @@
import { existsSync, mkdirSync, cpSync, type CopySyncOptions } from "node:fs"
import { dirname } from "node:path"
/**
* Safely creates a directory. Returns true if successful, false on error.
* Logs to stderr when GSD_DEBUG is set.
*/
export function safeMkdir(dirPath: string): boolean {
try {
mkdirSync(dirPath, { recursive: true })
return true
} catch (err) {
if (process.env.GSD_DEBUG) console.error(`[gsd] mkdir failed: ${dirPath}`, err)
return false
}
}
/**
* Safely copies src to dst. Returns true if successful, false if src doesn't exist or copy fails.
* Logs to stderr when GSD_DEBUG is set.
*/
export function safeCopy(src: string, dst: string, opts?: CopySyncOptions): boolean {
if (!existsSync(src)) return false
try {
cpSync(src, dst, opts)
return true
} catch (err) {
if (process.env.GSD_DEBUG) console.error(`[gsd] copy failed: ${src}${dst}`, err)
return false
}
}
/**
* Safely copies a directory recursively, creating the parent of dst if needed.
* Returns true if successful.
*/
export function safeCopyRecursive(src: string, dst: string, opts?: Omit<CopySyncOptions, 'recursive'>): boolean {
if (!existsSync(src)) return false
try {
mkdirSync(dirname(dst), { recursive: true })
cpSync(src, dst, { ...opts, recursive: true })
return true
} catch (err) {
if (process.env.GSD_DEBUG) console.error(`[gsd] recursive copy failed: ${src}${dst}`, err)
return false
}
}