Bläddra i källkod

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.
csw-chen 3 veckor sedan
förälder
incheckning
cea78ceb1b

+ 14 - 0
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
   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
src/extraction/index.ts

@@ -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 - 0
src/extraction/wasm-runtime-flags.ts

@@ -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
src/installer/index.ts

@@ -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 - 0
src/installer/targets/antigravity.ts

@@ -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
src/mcp/engine.ts

@@ -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
-      // still discover one later via roots/list.
+      // No .codegraph/ above searchFrom. Sessions may still discover one later via roots/list
       this.projectPath = searchFrom;
       this.projectPath = searchFrom;
       return;
       return;
     }
     }

+ 2 - 0
src/sync/git-hooks.ts

@@ -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 - 0
src/sync/worktree.ts

@@ -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 {