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:
@@ -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
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)');
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user