diff --git a/package.json b/package.json index 47f8054..59a818b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "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/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", + "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/install-wrappers-contract.test.ts && node --import tsx --test tests/scripts/install-sh-smoke.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", "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/lib/runtime-manager.ts b/src/lib/runtime-manager.ts new file mode 100644 index 0000000..535167f --- /dev/null +++ b/src/lib/runtime-manager.ts @@ -0,0 +1,41 @@ +import path from 'node:path'; + +const SEMVER_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; + +function ensureNonEmpty(value: string, field: string): string { + const normalized = value.trim(); + if (!normalized) { + throw new Error(`${field} must be a non-empty string`); + } + return normalized; +} + +export function normalizeVersion(version: string): string { + const normalized = ensureNonEmpty(version, 'version'); + if (!SEMVER_PATTERN.test(normalized)) { + throw new Error(`version must be semver-compatible: ${version}`); + } + return normalized.startsWith('v') ? normalized.slice(1) : normalized; +} + +export function resolveInstallHome(env: NodeJS.ProcessEnv): string { + const home = env.BB_INSTALL_HOME ?? env.HOME ?? env.USERPROFILE; + return ensureNonEmpty(home ?? '', 'install home'); +} + +export function getRuntimePaths(home: string, version: string) { + const installHome = ensureNonEmpty(home, 'home'); + const normalizedVersion = normalizeVersion(version); + const beadboardHome = path.join(installHome, '.beadboard'); + const runtimeBase = path.join(beadboardHome, 'runtime'); + + return { + installHome, + beadboardHome, + runtimeBase, + runtimeRoot: path.join(runtimeBase, normalizedVersion), + runtimeCurrentMetadata: path.join(runtimeBase, 'current.json'), + shimDir: path.join(beadboardHome, 'bin'), + shimNames: ['bb', 'beadboard'] as const, + }; +} diff --git a/tests/lib/runtime-manager.test.ts b/tests/lib/runtime-manager.test.ts new file mode 100644 index 0000000..3c6a093 --- /dev/null +++ b/tests/lib/runtime-manager.test.ts @@ -0,0 +1,14 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { getRuntimePaths, normalizeVersion } from '../../src/lib/runtime-manager'; + +test('normalizeVersion supports semver and rejects empty', () => { + assert.equal(normalizeVersion('1.2.3'), '1.2.3'); + assert.throws(() => normalizeVersion('')); +}); + +test('getRuntimePaths builds ~/.beadboard/runtime/ layout', () => { + const p = getRuntimePaths('/tmp/home', '1.2.3'); + assert.match(p.runtimeRoot, /runtime\/1\.2\.3$/); + assert.match(p.shimDir, /\.beadboard\/bin$/); +});