fix(gbrain): quote cmd.exe arguments at a single gbrain invocation seam

Fixes #2471. With shell:true on Windows, node/bun join argv into one cmd.exe
string without quoting, so a repo path with a space — the default
C:\Users\First Last\ layout — split into two arguments and every gbrain call
carrying a path silently targeted the wrong location (worst: `sources add
--path`). All gbrain CLI invocations now build their (cmd, argv, shell)
triple through gbrainInvocation(), which quotes risky arguments for cmd.exe's
re-parse (embedded quotes doubled). The four direct spawn sites in
lib/gbrain-sources.ts route through the seam; the #1731 static invariant is
upgraded for seamed files (any direct "gbrain" opener is the violation) and
kept as-is for lib/gbrain-local-status.ts. POSIX behavior unchanged
(shell:false, passthrough argv).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 08:54:06 -07:00
co-authored by Claude Fable 5
parent ea780fed61
commit 4047e52bc6
3 changed files with 119 additions and 39 deletions
+19 -18
View File
@@ -12,7 +12,7 @@
import { execFileSync, spawnSync } from "child_process";
import { realpathSync } from "fs";
import { withErrorContext } from "./gstack-memory-helpers";
import { execGbrainJson, NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
import { execGbrainJson, gbrainInvocation } from "./gbrain-exec";
import {
detectAutopilot,
decideSourceRemove,
@@ -117,12 +117,13 @@ function samePath(registered: string | undefined, requested: string): boolean {
export function probeSource(id: string, env?: NodeJS.ProcessEnv): SourceState {
let stdout: string;
try {
stdout = execFileSync("gbrain", ["sources", "list", "--json"], {
const inv = gbrainInvocation(["sources", "list", "--json"]);
stdout = execFileSync(inv.cmd, inv.argv, {
encoding: "utf-8",
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
} catch (err) {
const e = err as NodeJS.ErrnoException & { stderr?: Buffer };
@@ -224,29 +225,28 @@ export async function ensureSourceRegistered(
throw new Error(`refusing drift re-register of ${id}: ${decision.reason}`);
}
const rm = spawnSync(
"gbrain",
["sources", "remove", id, "--yes", "--confirm-destructive", ...decision.extraArgs],
{
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
},
);
const rmInv = gbrainInvocation(["sources", "remove", id, "--yes", "--confirm-destructive", ...decision.extraArgs]);
const rm = spawnSync(rmInv.cmd, rmInv.argv, {
encoding: "utf-8",
timeout: 30_000,
env,
shell: rmInv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
if (rm.status !== 0) {
throw new Error(`gbrain sources remove ${id} failed: ${rm.stderr || rm.stdout || `exit ${rm.status}`}`);
}
}
// Add.
// Add. `path` is a user repo path — the #2471 space-in-path victim; the
// invocation seam quotes it for cmd.exe's re-parse.
const addArgs = ["sources", "add", id, "--path", path];
if (federated) addArgs.push("--federated");
const add = spawnSync("gbrain", addArgs, {
const addInv = gbrainInvocation(addArgs);
const add = spawnSync(addInv.cmd, addInv.argv, {
encoding: "utf-8",
timeout: 30_000,
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
shell: addInv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
if (add.status !== 0) {
throw new Error(`gbrain sources add ${id} failed: ${add.stderr || add.stdout || `exit ${add.status}`}`);
@@ -267,12 +267,13 @@ export async function ensureSourceRegistered(
export function sourcePageCount(id: string, env?: NodeJS.ProcessEnv): number | null {
let stdout: string;
try {
stdout = execFileSync("gbrain", ["sources", "list", "--json"], {
const inv = gbrainInvocation(["sources", "list", "--json"]);
stdout = execFileSync(inv.cmd, inv.argv, {
encoding: "utf-8",
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
} catch {
return null;