From 665121537d83ff55341250910d2f18a76c74933d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 17 Mar 2026 18:36:17 -0600 Subject: [PATCH] refactor(gsd): extract safeCopy/safeMkdir helpers to replace repetitive try/catch FS patterns (#1043) Co-authored-by: Claude Opus 4.6 (1M context) --- .../extensions/gsd/auto-worktree-sync.ts | 34 ++------------ src/resources/extensions/gsd/auto-worktree.ts | 15 ++---- src/resources/extensions/gsd/safe-fs.ts | 47 +++++++++++++++++++ 3 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 src/resources/extensions/gsd/safe-fs.ts diff --git a/src/resources/extensions/gsd/auto-worktree-sync.ts b/src/resources/extensions/gsd/auto-worktree-sync.ts index 9e948e498..b1545026d 100644 --- a/src/resources/extensions/gsd/auto-worktree-sync.ts +++ b/src/resources/extensions/gsd/auto-worktree-sync.ts @@ -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 ─────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 2177216f1..c91c7de5e 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -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 diff --git a/src/resources/extensions/gsd/safe-fs.ts b/src/resources/extensions/gsd/safe-fs.ts new file mode 100644 index 000000000..8872b8b28 --- /dev/null +++ b/src/resources/extensions/gsd/safe-fs.ts @@ -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): 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 + } +}