fix(windows): suppress console popup on child_process calls (#498)

On Windows, v0.9.5's detached shared daemon (#411) has no inherited console,
so any console-subsystem child it spawns gets a fresh visible console window
unless the spawn passes `windowsHide: true`. The fix adds the flag to all
ten `spawnSync` / `execFileSync` / `execSync` call sites across extraction,
sync, installer, and the WASM-flags relaunch. macOS/Linux ignore the option,
so this is a no-op elsewhere.

Fixes #485, #510, #530.

Co-authored work:
- #498 (csw-chen) — full sweep across extraction, sync, installer, and wasm-runtime. **This is the change being merged.**
- #505 (yushengruohui) — independently identified and fixed the 7 git execFileSync sites. Superseded by #498's broader sweep; same diagnosis.
- #521 (JirA44) — independently identified and fixed the WASM-runtime spawnSync re-exec. Superseded by #498's broader sweep; same diagnosis.

Validated on Windows 11 ARM64 (Parallels): a detached parent's 15 git spawns produce 15 visible black flash-windows without the fix and 0 with it.
This commit is contained in:
csw-chen
2026-05-28 13:12:54 -05:00
committed by GitHub
parent 71935e37c2
commit cea78ceb1b
8 changed files with 25 additions and 7 deletions
+14
View File
@@ -167,6 +167,20 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
calls pay nothing. Most visible on the "deleted everything between calls pay nothing. Most visible on the "deleted everything between
sessions" case, where MCP now returns the correct empty index instead sessions" case, where MCP now returns the correct empty index instead
of stale rows. Validated end-to-end on a 10,640-file VS Code index. of stale rows. Validated end-to-end on a 10,640-file VS Code index.
- **Windows: black console windows no longer flash on every file save / MCP
reconnect (#485, #510, #530).** v0.9.5 moved the MCP server to a detached
shared daemon (#411). Detached processes have no inherited console on
Windows, so any console-subsystem child they spawn (the daemon's `git`
invocations during auto-sync, the WASM-runtime `node` re-exec, the
installer's `npm` shell-out) is created with a fresh console window
visible to the user unless the spawn passes `windowsHide: true` (which
libuv translates to `STARTF_USESHOWWINDOW | SW_HIDE`, so the window is
created hidden and never flashes). All ten `spawnSync` / `execFileSync` /
`execSync` call sites across extraction, sync, installer, and the
WASM-flags relaunch now pass `windowsHide: true`. macOS/Linux ignore the
option, so this is a no-op elsewhere. The daemon launcher itself
(`src/mcp/index.ts`) already passed the flag — these children had been
missed.
- **`codegraph index` / `init -i` summary now reports the true edge count.** - **`codegraph index` / `init -i` summary now reports the true edge count.**
The per-file counter in the orchestrator only saw extraction-phase edges, The per-file counter in the orchestrator only saw extraction-phase edges,
so resolution and synthesizer edges (often >50% of the graph on so resolution and synthesizer edges (often >50% of the graph on
+4 -4
View File
@@ -191,7 +191,7 @@ export function buildDefaultIgnore(rootDir: string): Ignore {
* (See issue #193.) * (See issue #193.)
*/ */
function collectGitFiles(repoDir: string, prefix: string, files: Set<string>): void { function collectGitFiles(repoDir: string, prefix: string, files: Set<string>): void {
const gitOpts = { cwd: repoDir, encoding: 'utf-8' as const, timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'] }; const gitOpts = { cwd: repoDir, encoding: 'utf-8' as const, timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'], windowsHide: true };
// Tracked files. --recurse-submodules pulls in files from active submodules, // Tracked files. --recurse-submodules pulls in files from active submodules,
// which the index would otherwise represent only as a commit pointer. // which the index would otherwise represent only as a commit pointer.
@@ -241,7 +241,7 @@ function getGitVisibleFiles(rootDir: string): Set<string> | null {
const gitRoot = execFileSync( const gitRoot = execFileSync(
'git', 'git',
['rev-parse', '--show-toplevel'], ['rev-parse', '--show-toplevel'],
{ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
).trim(); ).trim();
if (path.resolve(gitRoot) !== path.resolve(rootDir)) { if (path.resolve(gitRoot) !== path.resolve(rootDir)) {
@@ -250,7 +250,7 @@ function getGitVisibleFiles(rootDir: string): Set<string> | null {
execFileSync( execFileSync(
'git', 'git',
['check-ignore', '-q', path.resolve(rootDir)], ['check-ignore', '-q', path.resolve(rootDir)],
{ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] } { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
); );
// Directory is gitignored by parent repo — fall back to filesystem walk // Directory is gitignored by parent repo — fall back to filesystem walk
return null; return null;
@@ -291,7 +291,7 @@ function getGitChangedFiles(rootDir: string): GitChanges | null {
const output = execFileSync( const output = execFileSync(
'git', 'git',
['status', '--porcelain', '--no-renames'], ['status', '--porcelain', '--no-renames'],
{ cwd: rootDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] } { cwd: rootDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
); );
const modified: string[] = []; const modified: string[] = [];
+1
View File
@@ -98,6 +98,7 @@ export function relaunchWithWasmRuntimeFlagsIfNeeded(scriptPath: string): void {
const result = spawnSync(process.execPath, argv, { const result = spawnSync(process.execPath, argv, {
stdio: 'inherit', stdio: 'inherit',
env: { ...process.env, [RELAUNCH_GUARD_ENV]: '1', [HOST_PPID_ENV]: String(process.ppid) }, env: { ...process.env, [RELAUNCH_GUARD_ENV]: '1', [HOST_PPID_ENV]: String(process.ppid) },
windowsHide: true,
}); });
if (result.error) { if (result.error) {
+1 -1
View File
@@ -119,7 +119,7 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
const s = clack.spinner(); const s = clack.spinner();
s.start('Installing codegraph CLI...'); s.start('Installing codegraph CLI...');
try { try {
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' }); execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe', windowsHide: true });
s.stop('Installed codegraph CLI on PATH'); s.stop('Installed codegraph CLI on PATH');
} catch { } catch {
s.stop('Could not install (permission denied)'); s.stop('Could not install (permission denied)');
+1
View File
@@ -124,6 +124,7 @@ function resolveCodegraphCommand(): string {
encoding: 'utf-8', encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'], stdio: ['ignore', 'pipe', 'ignore'],
shell: '/bin/bash', shell: '/bin/bash',
windowsHide: true,
}).trim(); }).trim();
if (resolved && fs.existsSync(resolved)) return resolved; if (resolved && fs.existsSync(resolved)) return resolved;
} catch { } catch {
+1 -2
View File
@@ -147,8 +147,7 @@ export class MCPEngine {
const resolvedRoot = findNearestCodeGraphRoot(searchFrom); const resolvedRoot = findNearestCodeGraphRoot(searchFrom);
if (!resolvedRoot) { if (!resolvedRoot) {
// No .codegraph/ above searchFrom — that's not an error, sessions may // No .codegraph/ above searchFrom. Sessions may still discover one later via roots/list
// still discover one later via roots/list.
this.projectPath = searchFrom; this.projectPath = searchFrom;
return; return;
} }
+2
View File
@@ -44,6 +44,7 @@ export function isGitRepo(projectRoot: string): boolean {
cwd: projectRoot, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'], stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
}).trim(); }).trim();
return out === 'true'; return out === 'true';
} catch { } catch {
@@ -61,6 +62,7 @@ function gitHooksDir(projectRoot: string): string | null {
cwd: projectRoot, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'], stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
}).trim(); }).trim();
if (!out) return null; if (!out) return null;
return path.isAbsolute(out) ? out : path.resolve(projectRoot, out); return path.isAbsolute(out) ? out : path.resolve(projectRoot, out);
+1
View File
@@ -35,6 +35,7 @@ export function gitWorktreeRoot(dir: string): string | null {
cwd: dir, cwd: dir,
encoding: 'utf8', encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'], stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
}).trim(); }).trim();
return out ? realpath(out) : null; return out ? realpath(out) : null;
} catch { } catch {