Skip to content

v0.13.0 migration orchestrator uses process.execPath → bun-runtime installs permanently 'partial' #332

@ChenyqThu

Description

@ChenyqThu

Summary

On installs that run via bun run src/cli.ts (symlink from ~/.bun/bin/gbrain
pointing at the source file, not the compiled binary), the v0.13.0 migration
orchestrator always finishes as partial because frontmatter_backfill never
succeeds.

Root cause: src/commands/migrations/v0_13_0.ts:44 has

const GBRAIN = process.execPath;

Under compiled-binary installs (bun build --compile --outfile bin/gbrain),
process.execPath is the gbrain binary, so this works. Under bun-runtime
installs, process.execPath is /Users/.../.bun/bin/bun. Phase B then
executes:

execSync(`${GBRAIN} extract links --source db --include-frontmatter`)
// → bun extract links --source db --include-frontmatter

Bun interprets this as bun <script> and tries to resolve extract as an
npm script or local binary. It fails with error: Script not found "extract",
and as a side effect, it runs bun init in the worktree, polluting
package.json with "private": true + a typescript peerDep and creating a
.cursor/rules/ directory.

Reproduction

  1. Install gbrain via git clone + bun link (the docs/INSTALL_FOR_AGENTS.md
    path), so which gbrain~/.bun/bin/gbrain symlink → src/cli.ts.
  2. Pre-existing brain at schema_version <= 11 (so v0.13.0 is pending, not skipped).
  3. Run gbrain apply-migrations --yes --non-interactive.

Expected: v0.13.0 completes.
Observed: v0.13.0 → partial with frontmatter_backfill failure. gbrain doctor permanently reports MINIONS HALF-INSTALLED (partial migration: 0.13.0). package.json silently gains bogus fields. Re-running keeps
re-triggering the same failure.

Workaround

Run gbrain extract links --source db --include-frontmatter manually and
restore package.json / delete .cursor/ afterward. Doctor still reports
the partial state because the orchestrator ledger in
~/.gbrain/migrations/completed.jsonl doesn't pick up the out-of-band run.

Suggested fix

Resolve GBRAIN by preference order (most specific → least):

import { dirname, join } from 'node:path';
import { existsSync } from 'node:fs';

// 1. Compiled-binary case: process.execPath IS the gbrain binary.
// 2. bun-runtime case: argv[1] is src/cli.ts under the project; use
//    `gbrain` from PATH so the public CLI contract is honored.
function resolveGbrain(): string {
  // If argv[1] is a script path (compiled binaries don't have this), fall
  // back to the `gbrain` symlink on PATH.
  const isScript = process.argv[1]?.endsWith('.ts') || process.argv[1]?.endsWith('.js');
  if (isScript) return 'gbrain';
  return process.execPath;
}
const GBRAIN = resolveGbrain();

Or, more conservatively, always use gbrain from PATH in
src/commands/migrations/*.ts — matches user expectation that the installed
CLI is callable by name, and sidesteps both cases.

The same pattern appears in other migration orchestrators (grep for
process.execPath under src/commands/migrations/); worth a sweep.

Environment

  • gbrain 0.15.1 (synced today from upstream master)
  • Install path: ~/.bun/bin/gbrain symlink → src/cli.ts
  • Runtime: Bun v1.3.10
  • macOS 25.3.0 (Darwin arm64)
  • Engine: pglite

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions