feat(installer): migrate shims to runtime-managed targets

This commit is contained in:
ZenchantLive 2026-03-02 20:42:35 -08:00
parent 205f9500ec
commit 7945ee8d3c
6 changed files with 271 additions and 1 deletions

64
install/install.ps1 Normal file
View file

@ -0,0 +1,64 @@
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = Split-Path -Parent $ScriptDir
$InstallHome = if ($env:BB_INSTALL_HOME) { $env:BB_INSTALL_HOME } else { $HOME }
$BbHome = Join-Path $InstallHome '.beadboard'
$TargetDir = Join-Path $BbHome 'bin'
$RuntimeDir = Join-Path $BbHome 'runtime'
$CurrentJson = Join-Path $RuntimeDir 'current.json'
$Version = if ($env:BB_RUNTIME_VERSION) { $env:BB_RUNTIME_VERSION } else { '0.1.0' }
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
New-Item -ItemType Directory -Path $RuntimeDir -Force | Out-Null
$BeadboardShim = Join-Path $TargetDir 'beadboard.cmd'
$BbShim = Join-Path $TargetDir 'bb.cmd'
$runtimeMetadata = @{
version = $Version
runtimeRoot = $RepoRoot
installMode = 'repo-shim-fallback'
shimTarget = (Join-Path $RepoRoot 'install\beadboard.mjs')
} | ConvertTo-Json -Depth 4
[System.IO.File]::WriteAllText($CurrentJson, "$runtimeMetadata`n")
$beadboardContent = @"
@echo off
setlocal
set "BB_HOME=%BB_INSTALL_HOME%"
if "%BB_HOME%"=="" set "BB_HOME=%USERPROFILE%"
set "CURRENT_JSON=%BB_HOME%\.beadboard\runtime\current.json"
set "RUNTIME_ROOT="
for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "$p='%CURRENT_JSON%'; if (Test-Path $p) { try { (Get-Content -Raw $p | ConvertFrom-Json).runtimeRoot } catch {} }"`) do set "RUNTIME_ROOT=%%I"
if "%RUNTIME_ROOT%"=="" set "RUNTIME_ROOT=$RepoRoot"
node "%RUNTIME_ROOT%\install\beadboard.mjs" %*
"@
$bbContent = @"
@echo off
setlocal
set "BB_HOME=%BB_INSTALL_HOME%"
if "%BB_HOME%"=="" set "BB_HOME=%USERPROFILE%"
set "CURRENT_JSON=%BB_HOME%\.beadboard\runtime\current.json"
set "RUNTIME_ROOT="
for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "$p='%CURRENT_JSON%'; if (Test-Path $p) { try { (Get-Content -Raw $p | ConvertFrom-Json).runtimeRoot } catch {} }"`) do set "RUNTIME_ROOT=%%I"
if "%RUNTIME_ROOT%"=="" set "RUNTIME_ROOT=$RepoRoot"
npx --yes tsx "%RUNTIME_ROOT%\tools\bb.ts" %*
"@
$beadboardTemp = "$BeadboardShim.tmp"
$bbTemp = "$BbShim.tmp"
[System.IO.File]::WriteAllText($beadboardTemp, $beadboardContent)
[System.IO.File]::WriteAllText($bbTemp, $bbContent)
Move-Item -Path $beadboardTemp -Destination $BeadboardShim -Force
Move-Item -Path $bbTemp -Destination $BbShim -Force
Write-Output "Installed BeadBoard shims:"
Write-Output "- $BeadboardShim"
Write-Output "- $BbShim"
Write-Output "- $CurrentJson"
Write-Output ""
Write-Output "Add to PATH if needed:"
Write-Output " setx PATH ""$TargetDir;%PATH%"""

81
install/install.sh Normal file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
INSTALL_HOME="${BB_INSTALL_HOME:-$HOME}"
BB_HOME="${INSTALL_HOME}/.beadboard"
TARGET_DIR="${BB_HOME}/bin"
RUNTIME_DIR="${BB_HOME}/runtime"
CURRENT_JSON="${RUNTIME_DIR}/current.json"
VERSION="${BB_RUNTIME_VERSION:-0.1.0}"
write_file_atomic() {
local target="$1"
local tmp="${target}.tmp.$$"
cat > "${tmp}"
mv "${tmp}" "${target}"
}
mkdir -p "${TARGET_DIR}" "${RUNTIME_DIR}"
write_file_atomic "${CURRENT_JSON}" <<EOF
{
"version": "${VERSION}",
"runtimeRoot": "${REPO_ROOT}",
"installMode": "repo-shim-fallback",
"shimTarget": "${REPO_ROOT}/install/beadboard.mjs"
}
EOF
write_file_atomic "${TARGET_DIR}/beadboard" <<EOF
#!/usr/bin/env bash
set -euo pipefail
BB_HOME="\${BB_INSTALL_HOME:-\$HOME}/.beadboard"
CURRENT_JSON="\${BB_HOME}/runtime/current.json"
resolve_runtime_root() {
if [ -f "\${CURRENT_JSON}" ]; then
local root
root="\$(node -e "const fs=require('fs');try{const j=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(j&&typeof j.runtimeRoot==='string')process.stdout.write(j.runtimeRoot)}catch{}" "\${CURRENT_JSON}")"
if [ -n "\${root}" ]; then
printf '%s' "\${root}"
return 0
fi
fi
printf '%s' "${REPO_ROOT}"
}
RUNTIME_ROOT="\$(resolve_runtime_root)"
exec node "\${RUNTIME_ROOT}/install/beadboard.mjs" "\$@"
EOF
write_file_atomic "${TARGET_DIR}/bb" <<EOF
#!/usr/bin/env bash
set -euo pipefail
BB_HOME="\${BB_INSTALL_HOME:-\$HOME}/.beadboard"
CURRENT_JSON="\${BB_HOME}/runtime/current.json"
resolve_runtime_root() {
if [ -f "\${CURRENT_JSON}" ]; then
local root
root="\$(node -e "const fs=require('fs');try{const j=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(j&&typeof j.runtimeRoot==='string')process.stdout.write(j.runtimeRoot)}catch{}" "\${CURRENT_JSON}")"
if [ -n "\${root}" ]; then
printf '%s' "\${root}"
return 0
fi
fi
printf '%s' "${REPO_ROOT}"
}
RUNTIME_ROOT="\$(resolve_runtime_root)"
exec npx --yes tsx "\${RUNTIME_ROOT}/tools/bb.ts" "\$@"
EOF
chmod +x "${TARGET_DIR}/beadboard" "${TARGET_DIR}/bb"
cat <<MSG
Installed BeadBoard shims:
- ${TARGET_DIR}/beadboard
- ${TARGET_DIR}/bb
- ${CURRENT_JSON}
Add to PATH if needed:
export PATH="${TARGET_DIR}:\$PATH"
MSG

View file

@ -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/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/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/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"

View file

@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
test('installer migrates legacy repo-bound shim to runtime-managed shim', async () => {
if (process.platform === 'win32') {
return;
}
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-install-migrate-'));
const installHome = path.join(root, 'home').replace(/\\/g, '/');
try {
const shimDir = path.join(installHome, '.beadboard', 'bin');
await fs.mkdir(shimDir, { recursive: true });
await fs.writeFile(
path.join(shimDir, 'beadboard'),
'#!/usr/bin/env bash\nexec node "/legacy/repo/install/beadboard.mjs" "$@"\n',
'utf8',
);
await execFileAsync('bash', ['install/install.sh'], {
env: { ...process.env, BB_INSTALL_HOME: installHome },
});
const shimRaw = await fs.readFile(path.join(shimDir, 'beadboard'), 'utf8');
const metadataRaw = await fs.readFile(
path.join(installHome, '.beadboard', 'runtime', 'current.json'),
'utf8',
);
assert.doesNotMatch(shimRaw, /\/legacy\/repo\/install\/beadboard\.mjs/);
assert.match(shimRaw, /runtime\/current\.json/);
assert.match(shimRaw, /RUNTIME_ROOT/);
assert.match(metadataRaw, /"runtimeRoot"/);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});

View file

@ -0,0 +1,41 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const scriptPath = 'install/install.sh';
test('install.sh supports install and reinstall into BB_INSTALL_HOME', { skip: os.platform() === 'win32' ? 'Bash pathing issues on Windows' : false }, async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-install-smoke-'));
const installHome = path.join(root, 'home').replace(/\\/g, '/');
try {
await execFileAsync('bash', [scriptPath], {
env: { ...process.env, BB_INSTALL_HOME: installHome },
});
await execFileAsync('bash', [scriptPath], {
env: { ...process.env, BB_INSTALL_HOME: installHome },
});
const beadboardShim = path.join(installHome, '.beadboard', 'bin', 'beadboard');
const bbShim = path.join(installHome, '.beadboard', 'bin', 'bb');
const runtimeMetadata = path.join(installHome, '.beadboard', 'runtime', 'current.json');
const [beadboardRaw, bbRaw, metadataRaw] = await Promise.all([
fs.readFile(beadboardShim, 'utf8'),
fs.readFile(bbShim, 'utf8'),
fs.readFile(runtimeMetadata, 'utf8'),
]);
assert.match(beadboardRaw, /runtime\/current\.json/);
assert.match(bbRaw, /runtime\/current\.json/);
assert.match(metadataRaw, /"runtimeRoot"/);
assert.match(metadataRaw, /"installMode"/);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});

View file

@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
async function read(relativePath: string): Promise<string> {
return fs.readFile(path.resolve(relativePath), 'utf8');
}
test('install wrapper scripts exist with canonical filenames', async () => {
const [ps1, sh] = await Promise.all([
read('install/install.ps1'),
read('install/install.sh'),
]);
assert.match(ps1, /beadboard/i);
assert.match(sh, /beadboard/i);
});
test('install wrappers provision both bb and beadboard shims', async () => {
const [ps1, sh] = await Promise.all([
read('install/install.ps1'),
read('install/install.sh'),
]);
assert.match(ps1, /\bbb\b/i);
assert.match(ps1, /\bbeadboard\b/i);
assert.match(sh, /\bbb\b/i);
assert.match(sh, /\bbeadboard\b/i);
});
test('install wrappers write runtime metadata and resolve runtime targets first', async () => {
const [ps1, sh] = await Promise.all([
read('install/install.ps1'),
read('install/install.sh'),
]);
assert.match(ps1, /runtime\\current\.json/i);
assert.match(ps1, /RUNTIME_ROOT/i);
assert.match(sh, /runtime\/current\.json/i);
assert.match(sh, /RUNTIME_ROOT/i);
});