B8: read gbrain JSON with bun instead of jq in install and wireup

gstack-gbrain-install read package.json with jq but never checked for it,
so on a machine without jq a successful install failed with "cannot read
version". gstack-gbrain-source-wireup hard-failed without jq ("brew
install jq" is not actionable on git-bash), so the wireup was skipped on
every run. Both now read the JSON with bun, which they already require.

Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com>
This commit is contained in:
garrytanandbenjamin beres committed 2026-10-03 21:34:15 +00:00
1 parent c612f28339
commit 6c0917aed2
4 files changed
+108 -17

No files matched your search

+36
View File
@@ -324,3 +324,39 @@ describe('gstack-gbrain-install argument handling', () => {
expect(r.stderr).toContain('unknown flag');
});
});
// B8 (#1675): the installer reads package.json with bun; it used to need jq,
// never checked for it, and reported a successful install as failed.
describe('gstack-gbrain-install without jq (B8)', () => {
function pathWithoutJq(extra: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-jq-bin-'));
for (const base of ['/usr/bin', '/bin', '/usr/sbin', '/sbin']) {
if (!fs.existsSync(base)) continue;
for (const name of fs.readdirSync(base)) {
if (name === 'jq' || name === 'gbrain' || fs.existsSync(path.join(dir, name))) continue;
try { fs.symlinkSync(path.join(base, name), path.join(dir, name)); } catch {}
}
}
return `${extra}:${dir}:${BUN_ONLY_DIR}`;
}
test('validates a matching install and reuses a detected clone with no jq on PATH', async () => {
const installDir = path.join(tmpHomeReal, 'gbrain-install');
fs.mkdirSync(installDir, { recursive: true });
fs.writeFileSync(path.join(installDir, 'package.json'), JSON.stringify({ name: 'gbrain', version: '0.60.37', bin: { gbrain: 'src/cli.ts' } }));
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-gbrain-'));
fs.writeFileSync(path.join(fakeBin, 'gbrain'), '#!/bin/sh\necho "gbrain 0.60.37"\n', { mode: 0o755 });
const PATH = pathWithoutJq(fakeBin);
const ok = await run(INSTALL, ['--validate-only', '--install-dir', installDir], { env: { PATH } });
expect(ok.stderr).not.toContain('cannot read version');
expect(ok.status).toBe(0);
const clone = path.join(tmpHomeReal, 'git', 'gbrain');
fs.mkdirSync(clone, { recursive: true });
fs.writeFileSync(path.join(clone, 'package.json'), JSON.stringify({ name: 'gbrain', version: '0.60.37', bin: { gbrain: 'src/cli.ts' } }));
const dry = await run(INSTALL, ['--dry-run'], { env: { PATH } });
expect(dry.stdout).toContain(`detected existing gbrain clone at ${clone}`);
fs.rmSync(fakeBin, { recursive: true, force: true });
});
});
+31
View File
@@ -565,3 +565,34 @@ describe('gstack-gbrain-source-wireup — defensive paths', () => {
expect(fs.existsSync(path.join(worktreeDir, 'unrelated.txt'))).toBe(false); // stray gone
});
});
// B8 (#2369): the wireup reads `gbrain sources list --json` with bun, so a
// machine without jq (git-bash on Windows, minimal Linux) still wires up.
describe('gstack-gbrain-source-wireup without jq (B8)', () => {
function pathWithoutJq(): string {
const dir = path.join(tmpHome, 'no-jq-bin');
fs.mkdirSync(dir, { recursive: true });
for (const base of ['/usr/bin', '/bin', '/usr/sbin', '/sbin']) {
if (!fs.existsSync(base)) continue;
for (const name of fs.readdirSync(base)) {
if (name === 'jq' || fs.existsSync(path.join(dir, name))) continue;
try { fs.symlinkSync(path.join(base, name), path.join(dir, name)); } catch {}
}
}
return `${fakeBinDir}:${dir}:${BUN_ONLY_DIR}`;
}
test('detects an already-registered source and re-runs cleanly with no jq on PATH', () => {
setupGstackRepo('git@github.com:user/gstack-brain-user.git');
makeFakeGbrain({});
const PATH = pathWithoutJq();
expect(spawnSync('sh', ['-c', 'command -v jq'], { env: { PATH }, timeout: 10_000 }).status).not.toBe(0);
const first = run([], { env: { PATH, GSTACK_BRAIN_NO_SYNC: '1' } });
expect(first.stderr).not.toContain('jq required');
expect(first.status).toBe(0);
const second = run([], { env: { PATH, GSTACK_BRAIN_NO_SYNC: '1' } });
expect(second.status).toBe(0);
expect(readState().sources).toHaveLength(1);
});
});