From 4c2ae2e5b7766c68c128d214a6623eaef6c2c445 Mon Sep 17 00:00:00 2001 From: ZenchantLive Date: Tue, 3 Mar 2026 16:19:01 -0800 Subject: [PATCH] launcher: add start --dolt and startup guidance --- README.md | 5 ++ install/beadboard.mjs | 90 ++++++++++++++++++++++-- src/cli/beadboard-cli.ts | 2 +- tests/cli/beadboard-help-output.test.ts | 1 + tests/scripts/beadboard-launcher.test.ts | 42 +++++++++++ 5 files changed, 132 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cf7d86d..406aac0 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,14 @@ Runtime home: Launcher commands: - `beadboard start` +- `beadboard start --dolt` (runs `bd dolt start` in the current project folder, then starts BeadBoard) - `beadboard open` - `beadboard status` +Startup note: +- In project repositories, run `bd dolt start` from the project directory (the folder with `.beads`). +- If you want one command for both steps, use `beadboard start --dolt`. + --- ## Quick Start diff --git a/install/beadboard.mjs b/install/beadboard.mjs index 1a79ca8..bd355ae 100644 --- a/install/beadboard.mjs +++ b/install/beadboard.mjs @@ -4,7 +4,7 @@ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { spawn } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -29,9 +29,13 @@ function parseArgs(argv) { const jsonIndex = args.indexOf('--json'); const json = jsonIndex !== -1; if (json) args.splice(jsonIndex, 1); + const doltIndex = args.indexOf('--dolt'); + const dolt = doltIndex !== -1; + if (dolt) args.splice(doltIndex, 1); return { command: args[0] || 'help', json, + dolt, }; } @@ -57,7 +61,9 @@ function output(payload, asJson) { `bd Path: ${payload.bd?.path || '(not found)'}`, `Project CWD: ${payload.bd?.project?.cwd || process.cwd()}`, `.beads Dir: ${payload.bd?.project?.hasBeadsDir ? 'yes' : 'no'}`, - `.beads DB: ${payload.bd?.project?.hasBeadsDb ? 'yes' : 'no'}`, + `SQLite Legacy DB: ${payload.bd?.backend?.sqliteLegacyDb ? 'yes' : 'no'}`, + `SQLite Migrated DB: ${payload.bd?.backend?.sqliteMigratedDb ? 'yes' : 'no'}`, + `Dolt Repo: ${payload.bd?.backend?.doltRepo ? 'yes' : 'no'}`, ]; process.stdout.write(`${statusLines.join('\n')}\n`); return; @@ -67,10 +73,21 @@ function output(payload, asJson) { return; } if (payload.command === 'start') { - process.stdout.write('Starting BeadBoard dev server...\n'); + const startLines = [ + 'Starting BeadBoard dev server...', + 'Tip: In your project folder, run `bd dolt start` first.', + 'Shortcut: `beadboard start --dolt` runs Dolt + BeadBoard startup together.', + ]; + if (payload.doltRequested) { + startLines.push(`Dolt startup: ${payload.doltStarted ? 'started' : 'not started'}`); + if (payload.doltMessage) { + startLines.push(`Dolt detail: ${payload.doltMessage}`); + } + } + process.stdout.write(`${startLines.join('\n')}\n`); return; } - process.stdout.write('Usage: beadboard [--json]\n'); + process.stdout.write('Usage: beadboard [--json] [--dolt]\n'); } function getPort() { @@ -103,6 +120,8 @@ function resolveBdPath() { function getBdDiagnostics() { const beadsDir = path.resolve(process.cwd(), '.beads'); const dbPath = path.join(beadsDir, 'beads.db'); + const migratedDbPath = path.join(beadsDir, 'beads.db.migrated'); + const doltRepoPath = path.join(beadsDir, 'dolt'); const bdPath = resolveBdPath(); return { available: Boolean(bdPath), @@ -112,6 +131,11 @@ function getBdDiagnostics() { hasBeadsDir: fs.existsSync(beadsDir), hasBeadsDb: fs.existsSync(dbPath), }, + backend: { + sqliteLegacyDb: fs.existsSync(dbPath), + sqliteMigratedDb: fs.existsSync(migratedDbPath), + doltRepo: fs.existsSync(doltRepoPath), + }, }; } @@ -151,13 +175,55 @@ function openUrl(url) { spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref(); } +function startDoltInProject(cwd) { + const bdPath = resolveBdPath(); + if (!bdPath) { + return { + attempted: false, + started: false, + message: 'bd not found on PATH; run `bd dolt start` manually in your project folder.', + }; + } + + const dolt = spawnSync(bdPath, ['dolt', 'start'], { + cwd, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + const started = (dolt.status ?? 1) === 0; + return { + attempted: true, + started, + message: started + ? 'bd dolt start completed.' + : 'bd dolt start failed; continuing with BeadBoard startup.', + }; +} + async function main() { - const { command, json } = parseArgs(process.argv.slice(2)); + const { command, json, dolt } = parseArgs(process.argv.slice(2)); const port = getPort(); const url = `http://127.0.0.1:${port}`; const runtime = getRuntimeMetadata(); if (command === 'start') { + const doltState = dolt + ? startDoltInProject(process.cwd()) + : { attempted: false, started: false, message: null }; + if (process.env.BB_START_NOOP === '1') { + output( + { + ok: true, + command: 'start', + doltRequested: dolt, + doltAttempted: doltState.attempted, + doltStarted: doltState.started, + doltMessage: doltState.message, + }, + json, + ); + return; + } const startRoot = fs.existsSync(runtime.runtimeRoot) ? runtime.runtimeRoot : repoRoot; const child = spawn('npm', ['run', 'dev'], { cwd: startRoot, @@ -165,7 +231,17 @@ async function main() { shell: process.platform === 'win32', }); child.on('exit', (code) => process.exit(code ?? 0)); - output({ ok: true, command: 'start' }, json); + output( + { + ok: true, + command: 'start', + doltRequested: dolt, + doltAttempted: doltState.attempted, + doltStarted: doltState.started, + doltMessage: doltState.message, + }, + json, + ); return; } @@ -190,7 +266,7 @@ async function main() { bd: getBdDiagnostics(), }; output(payload, json); - process.exit(probe.running ? 0 : 1); + process.exit(json ? (probe.running ? 0 : 1) : 0); return; } diff --git a/src/cli/beadboard-cli.ts b/src/cli/beadboard-cli.ts index f00b194..f743bfc 100644 --- a/src/cli/beadboard-cli.ts +++ b/src/cli/beadboard-cli.ts @@ -81,7 +81,7 @@ function renderHelpText(): string { ' beadboard [options]', '', 'Runtime Commands:', - ' beadboard start Start BeadBoard runtime', + ' beadboard start [--dolt] Start BeadBoard runtime (optionally start Dolt first)', ' beadboard open Open BeadBoard in browser', ' beadboard status [--json] Show runtime + bd diagnostics', '', diff --git a/tests/cli/beadboard-help-output.test.ts b/tests/cli/beadboard-help-output.test.ts index 058ce7d..e410463 100644 --- a/tests/cli/beadboard-help-output.test.ts +++ b/tests/cli/beadboard-help-output.test.ts @@ -11,6 +11,7 @@ test('bb --help prints human-readable usage by default', async () => { const { stdout } = await execFileAsync(process.execPath, [binPath, '--help']); assert.match(stdout, /Usage:/i); assert.match(stdout, /Runtime Commands:/i); + assert.match(stdout, /start \[--dolt\]/i); assert.match(stdout, /Management Commands:/i); assert.doesNotMatch(stdout, /^\s*\{/); }); diff --git a/tests/scripts/beadboard-launcher.test.ts b/tests/scripts/beadboard-launcher.test.ts index b987352..21d6100 100644 --- a/tests/scripts/beadboard-launcher.test.ts +++ b/tests/scripts/beadboard-launcher.test.ts @@ -4,6 +4,8 @@ import path from 'node:path'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import http from 'node:http'; +import fs from 'node:fs'; +import os from 'node:os'; const execFileAsync = promisify(execFile); const launcherPath = path.resolve('install/beadboard.mjs'); @@ -61,3 +63,43 @@ test('beadboard launcher open --json supports noop mode', async () => { assert.equal(payload.command, 'open'); assert.match(payload.url, /3456/); }); + +test('beadboard launcher start text includes dolt guidance', async () => { + const { stdout } = await execFileAsync(process.execPath, [launcherPath, 'start'], { + env: { ...process.env, BB_START_NOOP: '1' }, + }); + assert.match(stdout, /Starting BeadBoard dev server/i); + assert.match(stdout, /bd dolt start/i); + assert.match(stdout, /beadboard start --dolt/i); +}); + +test('beadboard launcher start --dolt runs bd dolt start in cwd', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'beadboard-start-dolt-')); + const binDir = path.join(tmpDir, 'bin'); + fs.mkdirSync(binDir, { recursive: true }); + const logPath = path.join(tmpDir, 'bd.log'); + const fakeBdPath = path.join(binDir, 'bd'); + fs.writeFileSync( + fakeBdPath, + '#!/usr/bin/env bash\nprintf "%s|%s\\n" "$PWD" "$*" > "$BB_FAKE_BD_LOG"\n', + 'utf8', + ); + fs.chmodSync(fakeBdPath, 0o755); + + const { stdout } = await execFileAsync(process.execPath, [launcherPath, 'start', '--dolt', '--json'], { + cwd: tmpDir, + env: { + ...process.env, + BB_START_NOOP: '1', + BB_FAKE_BD_LOG: logPath, + PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`, + }, + }); + + const payload = JSON.parse(stdout); + assert.equal(payload.ok, true); + assert.equal(payload.command, 'start'); + assert.equal(payload.doltRequested, true); + const bdInvocation = fs.readFileSync(logPath, 'utf8').trim(); + assert.equal(bdInvocation, `${tmpDir}|dolt start`); +});