From 7945ee8d3cbe7f220b7bea5e1c6e9475698750c9 Mon Sep 17 00:00:00 2001 From: ZenchantLive Date: Mon, 2 Mar 2026 20:42:35 -0800 Subject: [PATCH] feat(installer): migrate shims to runtime-managed targets --- install/install.ps1 | 64 +++++++++++++++ install/install.sh | 81 +++++++++++++++++++ package.json | 2 +- .../scripts/install-legacy-migration.test.ts | 45 +++++++++++ tests/scripts/install-sh-smoke.test.ts | 41 ++++++++++ .../scripts/install-wrappers-contract.test.ts | 39 +++++++++ 6 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 install/install.ps1 create mode 100644 install/install.sh create mode 100644 tests/scripts/install-legacy-migration.test.ts create mode 100644 tests/scripts/install-sh-smoke.test.ts create mode 100644 tests/scripts/install-wrappers-contract.test.ts diff --git a/install/install.ps1 b/install/install.ps1 new file mode 100644 index 0000000..485fdbc --- /dev/null +++ b/install/install.ps1 @@ -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%""" diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 0000000..efdd859 --- /dev/null +++ b/install/install.sh @@ -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}" < { + 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 }); + } +}); diff --git a/tests/scripts/install-sh-smoke.test.ts b/tests/scripts/install-sh-smoke.test.ts new file mode 100644 index 0000000..b4bc461 --- /dev/null +++ b/tests/scripts/install-sh-smoke.test.ts @@ -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 }); + } +}); diff --git a/tests/scripts/install-wrappers-contract.test.ts b/tests/scripts/install-wrappers-contract.test.ts new file mode 100644 index 0000000..1323a4f --- /dev/null +++ b/tests/scripts/install-wrappers-contract.test.ts @@ -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 { + 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); +});