feat(launcher): add runtime-aware status metadata
This commit is contained in:
parent
0f33a653d3
commit
205f9500ec
4 changed files with 231 additions and 1 deletions
152
install/beadboard.mjs
Normal file
152
install/beadboard.mjs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
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';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
function getRuntimeMetadata() {
|
||||
const installHome = process.env.BB_INSTALL_HOME || process.env.HOME || process.env.USERPROFILE || '';
|
||||
const version = (process.env.BB_RUNTIME_VERSION || '0.1.0').trim();
|
||||
const runtimeRoot =
|
||||
process.env.BB_RUNTIME_ROOT || path.join(installHome, '.beadboard', 'runtime', version);
|
||||
const shimTarget = process.env.BB_SHIM_TARGET || __filename;
|
||||
const installMode =
|
||||
process.env.BB_INSTALL_MODE ||
|
||||
(__dirname.includes(`${path.sep}.beadboard${path.sep}runtime${path.sep}`)
|
||||
? 'runtime-managed'
|
||||
: 'repo-shim');
|
||||
return { runtimeRoot, shimTarget, installMode };
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = [...argv];
|
||||
const jsonIndex = args.indexOf('--json');
|
||||
const json = jsonIndex !== -1;
|
||||
if (json) args.splice(jsonIndex, 1);
|
||||
return {
|
||||
command: args[0] || 'help',
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
function output(payload, asJson) {
|
||||
if (asJson) {
|
||||
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (payload.ok === false) {
|
||||
process.stderr.write(`${payload.error}\n`);
|
||||
return;
|
||||
}
|
||||
if (payload.command === 'status') {
|
||||
process.stdout.write(
|
||||
payload.running
|
||||
? `BeadBoard is running at ${payload.url}\n`
|
||||
: `BeadBoard is not running at ${payload.url}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (payload.command === 'open') {
|
||||
process.stdout.write(`Open ${payload.url}\n`);
|
||||
return;
|
||||
}
|
||||
if (payload.command === 'start') {
|
||||
process.stdout.write('Starting BeadBoard dev server...\n');
|
||||
return;
|
||||
}
|
||||
process.stdout.write('Usage: beadboard <start|open|status> [--json]\n');
|
||||
}
|
||||
|
||||
function getPort() {
|
||||
const value = Number.parseInt(process.env.BB_PORT || '3000', 10);
|
||||
return Number.isFinite(value) ? value : 3000;
|
||||
}
|
||||
|
||||
function statusRequest(port) {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
path: '/',
|
||||
timeout: 1200,
|
||||
},
|
||||
(res) => {
|
||||
res.resume();
|
||||
resolve({ running: true, statusCode: res.statusCode || 200 });
|
||||
},
|
||||
);
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve({ running: false, statusCode: null });
|
||||
});
|
||||
req.on('error', () => resolve({ running: false, statusCode: null }));
|
||||
});
|
||||
}
|
||||
|
||||
function openUrl(url) {
|
||||
if (process.env.BB_OPEN_NOOP === '1') return;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
||||
return;
|
||||
}
|
||||
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { command, json } = parseArgs(process.argv.slice(2));
|
||||
const port = getPort();
|
||||
const url = `http://127.0.0.1:${port}`;
|
||||
const runtime = getRuntimeMetadata();
|
||||
|
||||
if (command === 'start') {
|
||||
const startRoot = fs.existsSync(runtime.runtimeRoot) ? runtime.runtimeRoot : repoRoot;
|
||||
const child = spawn('npm', ['run', 'dev'], {
|
||||
cwd: startRoot,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
child.on('exit', (code) => process.exit(code ?? 0));
|
||||
output({ ok: true, command: 'start' }, json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'open') {
|
||||
openUrl(url);
|
||||
output({ ok: true, command: 'open', url }, json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'status') {
|
||||
const probe = await statusRequest(port);
|
||||
const payload = {
|
||||
ok: true,
|
||||
command: 'status',
|
||||
running: probe.running,
|
||||
statusCode: probe.statusCode,
|
||||
url,
|
||||
port,
|
||||
runtimeRoot: runtime.runtimeRoot,
|
||||
installMode: runtime.installMode,
|
||||
shimTarget: runtime.shimTarget,
|
||||
};
|
||||
output(payload, json);
|
||||
process.exit(probe.running ? 0 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
output({ ok: true, command: 'help' }, json);
|
||||
}
|
||||
|
||||
void main();
|
||||
|
|
@ -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/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/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"
|
||||
|
|
|
|||
15
tests/scripts/beadboard-launcher-runtime.test.ts
Normal file
15
tests/scripts/beadboard-launcher-runtime.test.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import path from 'node:path';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const launcherPath = path.resolve('install/beadboard.mjs');
|
||||
|
||||
test('status --json reports runtime root and install mode', async () => {
|
||||
const { stdout } = await execFileAsync(process.execPath, [launcherPath, 'status', '--json']);
|
||||
const payload = JSON.parse(stdout);
|
||||
assert.ok(payload.runtimeRoot);
|
||||
assert.ok(payload.installMode);
|
||||
});
|
||||
63
tests/scripts/beadboard-launcher.test.ts
Normal file
63
tests/scripts/beadboard-launcher.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import http from 'node:http';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const launcherPath = path.resolve('install/beadboard.mjs');
|
||||
|
||||
function getFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = http.createServer();
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('failed to resolve free port'));
|
||||
return;
|
||||
}
|
||||
const { port } = address;
|
||||
server.close((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('beadboard launcher status --json reports running server', async () => {
|
||||
const port = await getFreePort();
|
||||
const server = http.createServer((_req, res) => {
|
||||
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||
res.end('ok');
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(port, '127.0.0.1', () => resolve()));
|
||||
|
||||
try {
|
||||
const { stdout } = await execFileAsync(process.execPath, [launcherPath, 'status', '--json'], {
|
||||
env: { ...process.env, BB_PORT: String(port) },
|
||||
});
|
||||
const payload = JSON.parse(stdout);
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.running, true);
|
||||
assert.equal(payload.port, port);
|
||||
assert.ok(payload.runtimeRoot);
|
||||
assert.ok(payload.installMode);
|
||||
assert.ok(payload.shimTarget);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve())),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('beadboard launcher open --json supports noop mode', async () => {
|
||||
const { stdout } = await execFileAsync(process.execPath, [launcherPath, 'open', '--json'], {
|
||||
env: { ...process.env, BB_OPEN_NOOP: '1', BB_PORT: '3456' },
|
||||
});
|
||||
const payload = JSON.parse(stdout);
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.command, 'open');
|
||||
assert.match(payload.url, /3456/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue