From cea78ceb1b989f7f1bf5b4c20b50d67601cc29f3 Mon Sep 17 00:00:00 2001 From: csw-chen <66931558+csw-chen@users.noreply.github.com> Date: Fri, 29 May 2026 02:12:54 +0800 Subject: [PATCH] fix(windows): suppress console popup on child_process calls (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 14 ++++++++++++++ src/extraction/index.ts | 8 ++++---- src/extraction/wasm-runtime-flags.ts | 1 + src/installer/index.ts | 2 +- src/installer/targets/antigravity.ts | 1 + src/mcp/engine.ts | 3 +-- src/sync/git-hooks.ts | 2 ++ src/sync/worktree.ts | 1 + 8 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ecf14e..8d3bb14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. +- **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.** The per-file counter in the orchestrator only saw extraction-phase edges, so resolution and synthesizer edges (often >50% of the graph on diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 95e47e8..db69d25 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -191,7 +191,7 @@ export function buildDefaultIgnore(rootDir: string): Ignore { * (See issue #193.) */ function collectGitFiles(repoDir: string, prefix: string, files: Set): 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, // which the index would otherwise represent only as a commit pointer. @@ -241,7 +241,7 @@ function getGitVisibleFiles(rootDir: string): Set | null { const gitRoot = execFileSync( 'git', ['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(); if (path.resolve(gitRoot) !== path.resolve(rootDir)) { @@ -250,7 +250,7 @@ function getGitVisibleFiles(rootDir: string): Set | null { execFileSync( 'git', ['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 return null; @@ -291,7 +291,7 @@ function getGitChangedFiles(rootDir: string): GitChanges | null { const output = execFileSync( 'git', ['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[] = []; diff --git a/src/extraction/wasm-runtime-flags.ts b/src/extraction/wasm-runtime-flags.ts index e44c84d..c1b30a6 100644 --- a/src/extraction/wasm-runtime-flags.ts +++ b/src/extraction/wasm-runtime-flags.ts @@ -98,6 +98,7 @@ export function relaunchWithWasmRuntimeFlagsIfNeeded(scriptPath: string): void { const result = spawnSync(process.execPath, argv, { stdio: 'inherit', env: { ...process.env, [RELAUNCH_GUARD_ENV]: '1', [HOST_PPID_ENV]: String(process.ppid) }, + windowsHide: true, }); if (result.error) { diff --git a/src/installer/index.ts b/src/installer/index.ts index ce102aa..5893f7b 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -119,7 +119,7 @@ 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' }); + execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe', windowsHide: true }); s.stop('Installed codegraph CLI on PATH'); } catch { s.stop('Could not install (permission denied)'); diff --git a/src/installer/targets/antigravity.ts b/src/installer/targets/antigravity.ts index 9ecc4bc..1c12849 100644 --- a/src/installer/targets/antigravity.ts +++ b/src/installer/targets/antigravity.ts @@ -124,6 +124,7 @@ function resolveCodegraphCommand(): string { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], shell: '/bin/bash', + windowsHide: true, }).trim(); if (resolved && fs.existsSync(resolved)) return resolved; } catch { diff --git a/src/mcp/engine.ts b/src/mcp/engine.ts index 9ba89da..193f2bb 100644 --- a/src/mcp/engine.ts +++ b/src/mcp/engine.ts @@ -147,8 +147,7 @@ export class MCPEngine { const resolvedRoot = findNearestCodeGraphRoot(searchFrom); if (!resolvedRoot) { - // No .codegraph/ above searchFrom — that's not an error, sessions may - // still discover one later via roots/list. + // No .codegraph/ above searchFrom. Sessions may still discover one later via roots/list this.projectPath = searchFrom; return; } diff --git a/src/sync/git-hooks.ts b/src/sync/git-hooks.ts index 3344c5f..a657d75 100644 --- a/src/sync/git-hooks.ts +++ b/src/sync/git-hooks.ts @@ -44,6 +44,7 @@ export function isGitRepo(projectRoot: string): boolean { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, }).trim(); return out === 'true'; } catch { @@ -61,6 +62,7 @@ function gitHooksDir(projectRoot: string): string | null { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, }).trim(); if (!out) return null; return path.isAbsolute(out) ? out : path.resolve(projectRoot, out); diff --git a/src/sync/worktree.ts b/src/sync/worktree.ts index 27bfca5..bf370b1 100644 --- a/src/sync/worktree.ts +++ b/src/sync/worktree.ts @@ -35,6 +35,7 @@ export function gitWorktreeRoot(dir: string): string | null { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, }).trim(); return out ? realpath(out) : null; } catch {