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:
parent
4f10e9bdc4
commit
665121537d
3 changed files with 55 additions and 41 deletions
|
|
@ -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 ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
47
src/resources/extensions/gsd/safe-fs.ts
Normal file
47
src/resources/extensions/gsd/safe-fs.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue