fix(autopilot): derive bun runtime dir for cron PATH; detect wrapper in --status (#3397)

Two robustness fixes to `gbrain autopilot --install`/`--status`, hardening #3305.

1. Universal bun PATH (extends #3305). The install-generated wrapper
   (~/.gbrain/autopilot-run.sh) execs the `#!/usr/bin/env bun` gbrain shim, so
   bun must be on PATH under cron/systemd/launchd's minimal env. #3305 hardcodes
   `$HOME/.bun/bin`, which only covers the default bun.sh installer. Hosts where
   bun lives elsewhere (Homebrew, npm -g, Docker /usr/local/bin, custom
   BUN_INSTALL, nix) still die with `env: bun: No such file or directory`,
   leaving a stale lock that stalls the nightly cycle. Fix: bake the dir of the
   actually-running bun (dirname(process.execPath)) onto PATH at install time,
   ~/.bun/bin kept as fallback, single-quote-escaped, empty execPath guarded.

2. `--status` false negative. showStatus() checked crontab.includes('gbrain
   autopilot'), but --install writes a line calling the wrapper
   `.../autopilot-run.sh` — no such substring. So `--status` reported
   installed:false on every wrapper-based Linux host. Fix: also match
   'autopilot-run.sh'.

Tests: test/autopilot-install.test.ts — universal-form + runtime-derivation +
wrapper-detection assertions (fail-before/pass-after verified).
This commit is contained in:
Anton Senkovskiy
2026-07-27 14:11:05 -07:00
committed by GitHub
parent 5ecab70a21
commit f0a28eb276
2 changed files with 41 additions and 9 deletions
+20 -3
View File
@@ -115,13 +115,30 @@ describe('autopilot wrapper script — bun PATH export (v0.42.x regression)', ()
test('wrapper exports ~/.bun/bin onto PATH before the exec', async () => {
const { readFileSync } = await import('fs');
const src = readFileSync('src/commands/autopilot.ts', 'utf8');
// The export line must appear inside the writeWrapperScript heredoc.
expect(src).toMatch(/export\s+PATH="\$HOME\/\.bun\/bin:\$PATH"/);
// The export line must appear inside the writeWrapperScript heredoc, now
// prefixed with the runtime dir derived at install time (universal), with
// ~/.bun/bin retained as a fallback.
expect(src).toMatch(/export PATH=\$\{runtimePathPrefix\}"\$HOME\/\.bun\/bin:\$PATH"/);
// The runtime dir is derived from the actually-running bun (covers Homebrew /
// npm -g / Docker / custom BUN_INSTALL / nix), not hardcoded to ~/.bun/bin.
expect(src).toMatch(/const runtimeDir = dirname\(process\.execPath/);
// The export must precede the exec line, otherwise env never sees it.
const exportIdx = src.search(/export\s+PATH="\$HOME\/\.bun\/bin/);
const exportIdx = src.search(/export PATH=\$\{runtimePathPrefix\}/);
const execIdx = src.search(/exec\s+'\${safeGbrainPath}'/);
expect(exportIdx).toBeGreaterThan(0);
expect(execIdx).toBeGreaterThan(0);
expect(exportIdx).toBeLessThan(execIdx);
});
});
// Status detection must recognize the wrapper-based cron line that --install
// actually writes (…/autopilot-run.sh), not just the legacy `gbrain autopilot`
// invocation — otherwise `--status` reports installed:false on every Linux host
// that installed via the wrapper indirection.
describe('autopilot showStatus — wrapper-path detection', () => {
test('status detects the autopilot-run.sh wrapper line', async () => {
const { readFileSync } = await import('fs');
const src = readFileSync('src/commands/autopilot.ts', 'utf8');
expect(src).toMatch(/crontab\.includes\('autopilot-run\.sh'\)/);
});
});