#!/usr/bin/env bun /** * Run the platform from a worktree against an isolated workdir. * * Use case: smoke-test or QA against a feature branch without colliding * with a primary `~/.nimblebrain` dev or another worktree's state. Runs * in dev mode (no `instance.json` → no auth gate), so it's suitable for * Chrome DevTools-driven E2E against `/v1/*` endpoints with no login * dance. * * Convention (per worktree root): * - Workdir: `/.nimblebrain-worktree/` * - Config: `/.nimblebrain-worktree/nimblebrain.json` (auto-seeded on first run) * - Ports: API 27271, Web 27270 (override via `NB_API_PORT` / `NB_WEB_PORT`) * - Auth: none (dev mode — no `instance.json`) * * Set `ANTHROPIC_API_KEY` (or other provider keys) in your shell to * unlock real LLM calls. As a convenience, this script also auto-loads * `/.env` if present, otherwise the main repo's `.env` * (discovered via `git rev-parse --git-common-dir`). Shell-exported * values always win over the file. Without any keys set, everything * but model invocation still works — uploads, MCP resources, tool * calls, conversation log. * * Reset state: `rm -rf .nimblebrain-worktree && bun run dev:worktree` * Share state across worktrees: `NB_WORK_DIR=/abs/path bun run dev:worktree` */ import { spawn } from "node:child_process"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { loadDotenvIntoProcess } from "./lib/dev-env.ts"; import { installIfMissing } from "./lib/dev-prepare.ts"; import { buildDevWorktreeSeed, WORKDIR_NAME } from "./lib/dev-worktree-config.ts"; // Anchor the worktree root from the script's location, not `process.cwd()`, // so `bun run scripts/dev-worktree.ts` from a subdirectory still resolves // the right place. The script lives at `/scripts/dev-worktree.ts`, // so the parent of its containing directory is the worktree root. const WORKTREE_ROOT = dirname(import.meta.dir); // `||` (not `??`) so an accidentally-exported empty `NB_WORK_DIR=""` from a // misconfigured shell or direnv doesn't slip through and produce nonsense // paths. Same for the port vars below. const WORKDIR = process.env.NB_WORK_DIR || join(WORKTREE_ROOT, WORKDIR_NAME); const CONFIG_PATH = join(WORKDIR, "nimblebrain.json"); const API_PORT = process.env.NB_API_PORT || "27271"; const WEB_PORT = process.env.NB_WEB_PORT || "27270"; function seedConfigIfMissing(): void { if (existsSync(CONFIG_PATH)) return; mkdirSync(WORKDIR, { recursive: true }); const seed = buildDevWorktreeSeed(WORKTREE_ROOT, WORKDIR); writeFileSync(CONFIG_PATH, `${JSON.stringify(seed, null, 2)}\n`); console.log(`[dev:worktree] Seeded ${CONFIG_PATH}`); } seedConfigIfMissing(); // Auto-load .env BEFORE the spawn so the child process inherits keys. // Discovery is worktree-local first, then the main repo (shared `.env` // via `git rev-parse --git-common-dir`). Shell exports always win, so // `direnv` / `mise` workflows aren't disrupted. Silent when no .env // found — the prior "set in your shell" contract still works. const envResult = loadDotenvIntoProcess(WORKTREE_ROOT); // Root ONLY. `scripts/dev.ts` imports from `src/`, so it cannot install the // deps it needs to be loaded — that has to happen before it is spawned. Web // deps and app UI builds live in `scripts/lib/dev-prepare.ts`, which dev.ts // runs, so every launcher gets them and not just this one. installIfMissing("root", WORKTREE_ROOT, "[dev:worktree]"); console.log("[dev:worktree] Starting"); console.log(`[dev:worktree] Worktree: ${WORKTREE_ROOT}`); console.log(`[dev:worktree] Workdir: ${WORKDIR}`); console.log(`[dev:worktree] API: http://localhost:${API_PORT}`); console.log(`[dev:worktree] Web: http://localhost:${WEB_PORT}`); console.log("[dev:worktree] Auth: none (dev mode)"); if (envResult.path) { const note = envResult.skipped.length > 0 ? ` (applied ${envResult.applied.length}, ${envResult.skipped.length} skipped — shell already set)` : ` (applied ${envResult.applied.length})`; console.log(`[dev:worktree] Loaded .env from ${envResult.path}${note}`); } // Delegate to the shared dev orchestrator (serve API + Vite web). The runtime // binary is serve-only — it no longer has a `dev` subcommand — so worktree dev // goes through `scripts/dev.ts`, the same launcher `bun run dev` uses, with the // worktree's ports/workdir threaded via env below. const child = spawn("bun", ["run", "scripts/dev.ts", "--port", API_PORT, "--config", CONFIG_PATH], { stdio: "inherit", cwd: WORKTREE_ROOT, env: { ...process.env, NB_API_PORT: API_PORT, NB_WEB_PORT: WEB_PORT, NB_WORK_DIR: WORKDIR, }, }); child.on("error", (err) => { console.error(`[dev:worktree] Failed to spawn bun: ${err.message}`); process.exit(1); }); child.on("exit", (code) => process.exit(code ?? 0));