From 6860e4ce6d56e992971f93717e1f54c93420dd22 Mon Sep 17 00:00:00 2001 From: Jayesh Betala Date: Sat, 16 May 2026 21:00:32 +0530 Subject: [PATCH] fix(gbrain): probe CLI without command builtin --- bin/gstack-gbrain-sync.ts | 12 ------------ bin/gstack-memory-ingest.ts | 3 +-- lib/gbrain-local-status.ts | 7 +++---- test/gbrain-local-status.test.ts | 11 +++++++++++ test/gstack-gbrain-sync.test.ts | 7 +++++++ test/gstack-memory-ingest.test.ts | 7 +++++++ 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/bin/gstack-gbrain-sync.ts b/bin/gstack-gbrain-sync.ts index 732ee430c4..8fc5c7cad8 100644 --- a/bin/gstack-gbrain-sync.ts +++ b/bin/gstack-gbrain-sync.ts @@ -233,15 +233,6 @@ function constrainSourceId(prefix: string, raw: string): string { return tail ? `${prefix}-${tail}-${hash}` : `${prefix}-${hash}`; } -function gbrainAvailable(): boolean { - try { - execSync("command -v gbrain", { stdio: "ignore" }); - return true; - } catch { - return false; - } -} - // ── Lock file (D1) ───────────────────────────────────────────────────────── interface LockInfo { @@ -333,9 +324,6 @@ async function runCodeImport(args: CliArgs): Promise { if (!root) { return { name: "code", ran: false, ok: true, duration_ms: 0, summary: "skipped (not in git repo)" }; } - if (!gbrainAvailable()) { - return { name: "code", ran: false, ok: false, duration_ms: 0, summary: "skipped (gbrain CLI not in PATH)" }; - } const sourceId = deriveCodeSourceId(root); diff --git a/bin/gstack-memory-ingest.ts b/bin/gstack-memory-ingest.ts index b1169ae693..f18c91a282 100644 --- a/bin/gstack-memory-ingest.ts +++ b/bin/gstack-memory-ingest.ts @@ -54,7 +54,7 @@ import { rmSync, } from "fs"; import { join, basename, dirname } from "path"; -import { execSync, execFileSync, spawnSync, spawn, type ChildProcess } from "child_process"; +import { execFileSync, spawnSync, spawn, type ChildProcess } from "child_process"; import { homedir } from "os"; import { createHash } from "crypto"; @@ -809,7 +809,6 @@ let _gbrainAvailability: boolean | null = null; function gbrainAvailable(): boolean { if (_gbrainAvailability !== null) return _gbrainAvailability; try { - execSync("command -v gbrain", { stdio: "ignore" }); // Probe `--help` for the `import` subcommand. gbrain v0.20.0+ ships // `import ` (batch markdown import via path-authoritative slugs). // If absent, we surface a single clean error here rather than failing diff --git a/lib/gbrain-local-status.ts b/lib/gbrain-local-status.ts index e646abd611..f546a93bc7 100644 --- a/lib/gbrain-local-status.ts +++ b/lib/gbrain-local-status.ts @@ -101,13 +101,13 @@ export function resolveGbrainBin(env?: NodeJS.ProcessEnv): string | null { if (_gbrainBinCache.has(key)) return _gbrainBinCache.get(key)!; let result: string | null = null; try { - const out = execFileSync("sh", ["-c", "command -v gbrain"], { + execFileSync("gbrain", ["--version"], { encoding: "utf-8", timeout: 2_000, - stdio: ["ignore", "pipe", "ignore"], + stdio: ["ignore", "ignore", "ignore"], env: e, }); - result = out.trim() || null; + result = "gbrain"; } catch { result = null; } @@ -266,4 +266,3 @@ export function localEngineStatus(opts: ClassifyOptions = {}): LocalEngineStatus writeCache(fresh, key); return fresh; } - diff --git a/test/gbrain-local-status.test.ts b/test/gbrain-local-status.test.ts index 272a99289a..90744bb2c4 100644 --- a/test/gbrain-local-status.test.ts +++ b/test/gbrain-local-status.test.ts @@ -21,6 +21,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { mkdtempSync, writeFileSync, + readFileSync, mkdirSync, rmSync, chmodSync, @@ -160,6 +161,16 @@ describe("lib/gbrain-local-status — five status cases", () => { restoreEnv = null; }); + it("probes the gbrain executable directly instead of shelling through command -v", () => { + const source = readFileSync( + join(import.meta.dir, "..", "lib", "gbrain-local-status.ts"), + "utf-8", + ); + + expect(source).not.toContain('command -v gbrain'); + expect(source).toContain('execFileSync("gbrain", ["--version"]'); + }); + it("returns 'no-cli' when gbrain is not on PATH", () => { env = makeEnv({ withGbrain: false }); restoreEnv = applyEnv(env); diff --git a/test/gstack-gbrain-sync.test.ts b/test/gstack-gbrain-sync.test.ts index 528d6deed7..7fefa2b4c0 100644 --- a/test/gstack-gbrain-sync.test.ts +++ b/test/gstack-gbrain-sync.test.ts @@ -48,6 +48,13 @@ describe("gstack-gbrain-sync CLI", () => { expect(r.stderr).toContain("Unknown argument: --bogus"); }); + it("uses the shared local gbrain status classifier instead of shelling through command -v", () => { + const source = readFileSync(SCRIPT, "utf-8"); + + expect(source).not.toContain('command -v gbrain'); + expect(source).toContain("localEngineStatus"); + }); + it("--dry-run with --code-only reports the code import preview only", () => { const home = makeTestHome(); const gstackHome = join(home, ".gstack"); diff --git a/test/gstack-memory-ingest.test.ts b/test/gstack-memory-ingest.test.ts index 638a2a6d54..105c6d7870 100644 --- a/test/gstack-memory-ingest.test.ts +++ b/test/gstack-memory-ingest.test.ts @@ -421,6 +421,13 @@ esac } describe("gstack-memory-ingest writer (gbrain v0.20+ batch `import` interface)", () => { + it("probes the gbrain executable directly instead of shelling through command -v", () => { + const source = readFileSync(SCRIPT, "utf-8"); + + expect(source).not.toContain('command -v gbrain'); + expect(source).toContain('execFileSync("gbrain", ["--help"]'); + }); + it("invokes `gbrain import --no-embed --json` exactly once with hierarchical staging", () => { const home = makeTestHome(); const gstackHome = join(home, ".gstack");