fix(gbrain): spawn gbrain + brain-sync through a shell on Windows (#1731)

On Windows, bun/npm install gbrain as a gbrain.cmd/.ps1 shim and gstack-brain-sync
is a bash shebang script. spawnSync/spawn/execFileSync resolve neither without a
shell, so the child spawn failed ENOENT — on the sync orchestrator this surfaced
as 'brain-sync exited undefined' (#1731).

Add NEEDS_SHELL_ON_WINDOWS (process.platform === 'win32') in gbrain-exec and pass
it as shell: to every gbrain/brain-sync child spawn: spawnGbrain, spawnGbrainAsync,
execGbrainText (gbrain-exec), the two sources-list/remove/add spawns (gbrain-sources),
the version + probe spawns (gbrain-local-status), and the two brain-sync spawns in
the orchestrator. POSIX keeps the cheaper no-shell path.

macOS/Linux CI can't exercise the Windows path, so test/gbrain-spawn-windows-shell.ts
is a static-grep tripwire: it fails CI if a gbrain/brain-sync spawn is added without
the shell flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-30 10:42:15 -07:00
parent c87e57e150
commit fb3103237a
5 changed files with 74 additions and 2 deletions
+15
View File
@@ -137,6 +137,18 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
return out;
}
/**
* Windows can't directly spawn the `gbrain` launcher (bun/npm install it as a
* `gbrain.cmd`/`.ps1` shim) or a shebang script like the bash `gstack-brain-sync`
* — `spawnSync`/`spawn` resolve those only through a shell's PATHEXT + interpreter
* lookup. Without `shell: true` the child spawn fails ENOENT, which on the sync
* orchestrator surfaced as "brain-sync exited undefined" (#1731). Gate on platform
* so POSIX keeps the cheaper no-shell path. Exported so the static-grep tripwire
* (test/gbrain-spawn-windows-shell.test.ts) can assert every gbrain/brain-sync
* spawn carries it.
*/
export const NEEDS_SHELL_ON_WINDOWS = process.platform === "win32";
export interface SpawnGbrainOptions {
/** Timeout in milliseconds. Defaults to 30s. */
timeout?: number;
@@ -166,6 +178,7 @@ export function spawnGbrain(args: string[], opts: SpawnGbrainOptions = {}): Spaw
cwd: opts.cwd,
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: opts.announce }),
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
}
@@ -198,6 +211,7 @@ export function spawnGbrainAsync(
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
cwd: opts.cwd,
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: false }),
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
}
@@ -212,5 +226,6 @@ export function execGbrainText(args: string[], opts: SpawnGbrainOptions = {}): s
cwd: opts.cwd,
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: opts.announce }),
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
}