diff --git a/bin/beadboard.js b/bin/beadboard.js new file mode 100644 index 0000000..dec9b01 --- /dev/null +++ b/bin/beadboard.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); + +const cliPath = path.resolve(__dirname, '../src/cli/beadboard-cli.ts'); +const result = spawnSync(process.execPath, ['--import', 'tsx', cliPath, ...process.argv.slice(2)], { + stdio: 'inherit', +}); + +process.exit(result.status ?? 1); diff --git a/package.json b/package.json index a4c24c4..b6ee39b 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,17 @@ "version": "0.1.0", "private": true, "license": "MIT", + "bin": { + "beadboard": "bin/beadboard.js", + "bb": "bin/beadboard.js" + }, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint .", "typecheck": "tsc --noEmit", - "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/blocked-triage-modal.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx && node --import tsx --test tests/components/graph/graph-node-conversation.test.tsx && node --import tsx --test tests/lib/coord-schema.test.ts && node --import tsx --test tests/lib/install-manifest.test.ts && node --import tsx --test tests/lib/runtime-manager.test.ts && node --import tsx --test tests/lib/coord-events.test.ts && node --import tsx --test tests/api/coord-events-route.test.ts && node --import tsx --test tests/lib/coord-projections-inbox.test.ts && node --import tsx --test tests/lib/coord-projections-reservations.test.ts && node --import tsx --test tests/components/sessions/conversation-drawer-coord.test.tsx && node --import tsx --test tests/scripts/beadboard-launcher.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-runtime.test.ts && node --import tsx --test tests/scripts/install-wrappers-contract.test.ts && node --import tsx --test tests/scripts/install-sh-smoke.test.ts && node --import tsx --test tests/scripts/install-legacy-migration.test.ts && node --import tsx --test tests/scripts/installer-ci-contract.test.ts && node --import tsx --test tests/docs/installer-quickstart-contract.test.ts && node --import tsx --test tests/docs/runtime-manager-adr-contract.test.ts && node --import tsx --test tests/skills/beadboard-driver/resolve-bb.test.ts && node --import tsx --test tests/skills/beadboard-driver/session-preflight.test.ts && node --import tsx --test tests/skills/beadboard-driver/generate-agent-name.test.ts && node --import tsx --test tests/skills/beadboard-driver/readiness-report.test.ts && node --import tsx --test tests/skills/beadboard-driver/skill-local-runner.test.ts && node --import tsx --test tests/skills/beadboard-driver/diagnose-env.test.ts && node --import tsx --test tests/skills/beadboard-driver/heal-common-issues.test.ts && node --import tsx --test tests/lib/epic-graph.test.ts && node --import tsx --test tests/components/shared/left-panel-filtering.test.ts && node --import tsx --test tests/hooks/use-beads-subscription-contract.test.ts && node --import tsx --test tests/components/graph/dependency-graph-hide-closed-contract.test.ts && node --import tsx --test tests/components/shared/unified-shell-hide-closed-contract.test.ts", + "test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/components/shared/base-card.test.tsx && node --import tsx --test tests/components/shared/agent-avatar.test.tsx && node --import tsx --test tests/components/sessions/sessions-header.test.ts && node --import tsx --test tests/components/sessions/agent-station-logic.test.ts && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/components/shared/left-panel.test.tsx && node --import tsx --test tests/components/shared/top-bar.test.tsx && node --import tsx --test tests/components/shared/mobile-nav.test.tsx && node --import tsx --test tests/components/swarm/swarm-card.test.tsx && node --import tsx --test tests/hooks/url-state-integration.test.ts && node --import tsx --test tests/hooks/use-graph-analysis.test.ts && node --import tsx --test tests/components/graph/smart-dag.test.tsx && node --import tsx --test tests/components/unified-shell.test.tsx && node --import tsx --test tests/components/blocked-triage-modal.test.tsx && node --import tsx --test tests/components/graph/graph-node-labels.test.tsx && node --import tsx --test tests/components/graph/graph-node-assign.test.tsx && node --import tsx --test tests/components/graph/graph-node-conversation.test.tsx && node --import tsx --test tests/lib/coord-schema.test.ts && node --import tsx --test tests/lib/install-manifest.test.ts && node --import tsx --test tests/lib/runtime-manager.test.ts && node --import tsx --test tests/lib/coord-events.test.ts && node --import tsx --test tests/api/coord-events-route.test.ts && node --import tsx --test tests/lib/coord-projections-inbox.test.ts && node --import tsx --test tests/lib/coord-projections-reservations.test.ts && node --import tsx --test tests/components/sessions/conversation-drawer-coord.test.tsx && node --import tsx --test tests/scripts/beadboard-launcher.test.ts && node --import tsx --test tests/scripts/beadboard-launcher-runtime.test.ts && node --import tsx --test tests/scripts/install-wrappers-contract.test.ts && node --import tsx --test tests/scripts/install-sh-smoke.test.ts && node --import tsx --test tests/scripts/install-legacy-migration.test.ts && node --import tsx --test tests/scripts/installer-ci-contract.test.ts && node --import tsx --test tests/docs/installer-quickstart-contract.test.ts && node --import tsx --test tests/docs/runtime-manager-adr-contract.test.ts && node --import tsx --test tests/cli/beadboard-cli.test.ts && node --import tsx --test tests/skills/beadboard-driver/resolve-bb.test.ts && node --import tsx --test tests/skills/beadboard-driver/session-preflight.test.ts && node --import tsx --test tests/skills/beadboard-driver/generate-agent-name.test.ts && node --import tsx --test tests/skills/beadboard-driver/readiness-report.test.ts && node --import tsx --test tests/skills/beadboard-driver/skill-local-runner.test.ts && node --import tsx --test tests/skills/beadboard-driver/diagnose-env.test.ts && node --import tsx --test tests/skills/beadboard-driver/heal-common-issues.test.ts && node --import tsx --test tests/lib/epic-graph.test.ts && node --import tsx --test tests/components/shared/left-panel-filtering.test.ts && node --import tsx --test tests/hooks/use-beads-subscription-contract.test.ts && node --import tsx --test tests/components/graph/dependency-graph-hide-closed-contract.test.ts && node --import tsx --test tests/components/shared/unified-shell-hide-closed-contract.test.ts", "video": "remotion preview src/video/index.ts", "video:render": "remotion render src/video/index.ts Main out/video.mp4", "video:thumbnail": "remotion still src/video/index.ts Main out/thumbnail.png --frame=60" diff --git a/src/cli/beadboard-cli.ts b/src/cli/beadboard-cli.ts new file mode 100644 index 0000000..32cbd20 --- /dev/null +++ b/src/cli/beadboard-cli.ts @@ -0,0 +1,88 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getRuntimePaths, resolveInstallHome } from '../lib/runtime-manager'; + +export type CliResult = { + ok: boolean; + command: string; + [key: string]: unknown; +}; + +function parseVersion(env: NodeJS.ProcessEnv): string { + const raw = (env.BB_RUNTIME_VERSION || env.npm_package_version || '0.1.0').trim(); + return raw.startsWith('v') ? raw.slice(1) : raw; +} + +export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise { + const args = [...argv]; + const asJson = args.includes('--json'); + const yes = args.includes('--yes'); + const command = args.find((arg) => !arg.startsWith('-')) || 'help'; + + const installHome = resolveInstallHome({ ...env, HOME: env.HOME || os.homedir() }); + const version = parseVersion(env); + const runtime = getRuntimePaths(installHome, version); + + if (command === 'doctor') { + return { + ok: true, + command, + json: asJson, + installMode: env.BB_INSTALL_MODE || 'npm-global-or-wrapper', + installHome, + runtimeRoot: runtime.runtimeRoot, + runtimeCurrentMetadata: runtime.runtimeCurrentMetadata, + shimDir: runtime.shimDir, + }; + } + + if (command === 'self-update') { + return { + ok: true, + command, + updated: false, + message: 'Self-update is not configured for this distribution yet. Reinstall with npm i -g beadboard when published.', + }; + } + + if (command === 'uninstall') { + if (!yes) { + return { + ok: false, + command, + error: 'Refusing uninstall without --yes', + }; + } + + await Promise.all([ + fs.rm(runtime.runtimeBase, { recursive: true, force: true }), + fs.rm(runtime.shimDir, { recursive: true, force: true }), + ]); + + return { + ok: true, + command, + removed: [runtime.runtimeBase, runtime.shimDir], + }; + } + + return { + ok: true, + command: 'help', + usage: 'beadboard [--json] [--yes]', + }; +} + +async function main() { + const result = await runCli(process.argv.slice(2)); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) { + process.exitCode = 1; + } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + void main(); +} diff --git a/tests/cli/beadboard-cli.test.ts b/tests/cli/beadboard-cli.test.ts new file mode 100644 index 0000000..ae8eaac --- /dev/null +++ b/tests/cli/beadboard-cli.test.ts @@ -0,0 +1,22 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { runCli } from '../../src/cli/beadboard-cli'; + +test('doctor returns structured install diagnostics', async () => { + const out = await runCli(['doctor', '--json']); + assert.equal(out.ok, true); + assert.ok(out.installMode); +}); + +test('self-update returns explicit placeholder result', async () => { + const out = await runCli(['self-update', '--json']); + assert.equal(out.ok, true); + assert.equal(out.command, 'self-update'); + assert.equal(out.updated, false); +}); + +test('uninstall requires --yes', async () => { + const out = await runCli(['uninstall', '--json']); + assert.equal(out.ok, false); + assert.match(out.error, /--yes/); +});