fix(sync,installer): time-bound the git/npm subprocess calls that had no timeout (#1139) (#1148)

extraction/index.ts bounds every git call it makes; worktree.ts,
git-hooks.ts, and the installer's npm install -g did not, so a stuck
subprocess blocked the caller indefinitely. Worst case was the daemon:
gitWorktreeRoot/gitCommonDir run (memoized) on the main event loop while
serving MCP clients, where an unbounded git hang would trip the 60s
liveness watchdog and SIGKILL a healthy daemon. git calls get 5s, the
interactive npm install 120s. Regression tests assert the option through
a mocked child_process plus a per-file call-site sweep.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-02 17:11:19 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 713ab7af43
commit 2f70eb3d32
5 changed files with 81 additions and 1 deletions
+3 -1
View File
@@ -115,7 +115,9 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
const s = clack.spinner();
s.start('Installing codegraph CLI...');
try {
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe', windowsHide: true });
// Generous bound (slow networks / cold npm cache) — but bounded, so a
// wedged npm can't hang the interactive installer forever (#1139).
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe', windowsHide: true, timeout: 120_000 });
s.stop('Installed codegraph CLI on PATH');
} catch {
s.stop('Could not install (permission denied)');
+2
View File
@@ -45,6 +45,7 @@ export function isGitRepo(projectRoot: string): boolean {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
timeout: 5000, // fail fast instead of hanging init/sync on a stuck git (#1139)
}).trim();
return out === 'true';
} catch {
@@ -63,6 +64,7 @@ function gitHooksDir(projectRoot: string): string | null {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
timeout: 5000, // same rationale as isGitRepo
}).trim();
if (!out) return null;
return path.isAbsolute(out) ? out : path.resolve(projectRoot, out);
+5
View File
@@ -36,6 +36,10 @@ export function gitWorktreeRoot(dir: string): string | null {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
// Bounded like every git call in extraction/: this runs (memoized) on
// the daemon's main event loop, where an unbounded hang would trip the
// 60s liveness watchdog and SIGKILL a healthy daemon (#1139).
timeout: 5000,
}).trim();
return out ? realpath(out) : null;
} catch {
@@ -58,6 +62,7 @@ export function gitCommonDir(dir: string): string | null {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
timeout: 5000, // same rationale as gitWorktreeRoot
}).trim();
if (!out) return null;
// `--git-common-dir` is relative to cwd unless already absolute.