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
+5
View File
@@ -11,6 +11,7 @@
import { execFileSync, spawnSync } from "child_process";
import { withErrorContext } from "./gstack-memory-helpers";
import { NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
export interface SourceState {
/** "absent" — id not registered. "match" — id at expected path. "drift" — id at different path. */
@@ -87,6 +88,7 @@ export function probeSource(id: string, env?: NodeJS.ProcessEnv): SourceState {
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
} catch (err) {
const e = err as NodeJS.ErrnoException & { stderr?: Buffer };
@@ -160,6 +162,7 @@ export async function ensureSourceRegistered(
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
if (rm.status !== 0) {
throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);
@@ -173,6 +176,7 @@ export async function ensureSourceRegistered(
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
if (add.status !== 0) {
throw new Error(`gbrain sources add ${id} failed: ${add.stderr || add.stdout || `exit ${add.status}`}`);
@@ -198,6 +202,7 @@ export function sourcePageCount(id: string, env?: NodeJS.ProcessEnv): number | n
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
} catch {
return null;