#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { pipeline } from 'node:stream/promises'; import { Readable } from 'node:stream'; import { fileURLToPath } from 'node:url'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const runtimeLockPath = path.join(repoRoot, 'runtime.lock.json'); const runtimeDir = path.join(repoRoot, 'resources', 'runtime'); const downloadRoot = path.join(repoRoot, '.runtime-download'); function printUsage() { process.stdout.write(`Usage: node scripts/stage-runtime.mjs [options] Options: --platform Runtime platform key. Defaults to the current platform. --release-tag Release tag to download from. Defaults to runtime.lock.json. --clean Remove staged runtime files and keep resources/runtime/.gitkeep. --help Show this message. `); } function parseArgs(argv) { const parsed = { platform: null, releaseTag: null, clean: false, help: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') { parsed.help = true; continue; } if (arg === '--clean') { parsed.clean = true; continue; } if (arg === '--platform') { parsed.platform = argv[index + 1] ?? null; index += 1; continue; } if (arg === '--release-tag') { parsed.releaseTag = argv[index + 1] ?? null; index += 1; continue; } throw new Error(`Unknown argument: ${arg}`); } return parsed; } function runOrThrow(command, args) { const result = spawnSync(command, args, { cwd: repoRoot, stdio: 'inherit', shell: false, }); if (result.error) { throw new Error(`Failed to run ${command}: ${result.error.message}`); } if (result.status !== 0) { throw new Error(`Command failed: ${command} ${args.join(' ')}`); } } function readRuntimeLock() { return JSON.parse(fs.readFileSync(runtimeLockPath, 'utf8')); } function getDefaultPlatformKey() { const key = `${process.platform}-${process.arch}`; if ( key === 'darwin-arm64' || key === 'darwin-x64' || key === 'linux-x64' || key === 'win32-x64' ) { return key; } throw new Error(`No bundled runtime asset is configured for ${key}`); } function getReleaseTag(runtimeLock, override) { const tag = override?.trim() || runtimeLock.releaseTag?.trim() || runtimeLock.sourceRef?.trim(); if (!tag) { throw new Error('runtime.lock.json does not define releaseTag or sourceRef'); } return tag; } function getReleaseAssetUrl(runtimeLock, releaseTag, asset) { return `https://github.com/${runtimeLock.releaseRepository}/releases/download/${releaseTag}/${encodeURIComponent(asset.file)}`; } function cleanRuntimeDir() { fs.mkdirSync(runtimeDir, { recursive: true }); for (const entry of fs.readdirSync(runtimeDir, { withFileTypes: true })) { if (entry.name === '.gitkeep') { continue; } fs.rmSync(path.join(runtimeDir, entry.name), { recursive: true, force: true }); } } async function downloadFile(url, destinationPath) { fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); const response = await fetch(url, { headers: { 'user-agent': 'agent-teams-runtime-stager', ...(process.env.GH_TOKEN ? { authorization: `Bearer ${process.env.GH_TOKEN}` } : {}), }, redirect: 'follow', }); if (!response.ok || !response.body) { throw new Error(`Failed to download runtime asset: ${response.status} ${response.statusText}`); } await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(destinationPath)); } function extractArchive(archivePath, extractDir, archiveKind) { fs.mkdirSync(extractDir, { recursive: true }); if (archiveKind === 'tar.gz') { runOrThrow('tar', ['-xzf', archivePath, '-C', extractDir]); return; } if (archiveKind === 'zip') { if (process.platform === 'win32') { runOrThrow('powershell', [ '-NoProfile', '-Command', `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${extractDir.replace(/'/g, "''")}' -Force`, ]); return; } runOrThrow('unzip', ['-oq', archivePath, '-d', extractDir]); return; } throw new Error(`Unsupported runtime archive kind: ${archiveKind}`); } function findRuntimePayloadDir(extractDir, binaryName) { const candidates = [path.join(extractDir, 'runtime'), extractDir]; for (const candidate of candidates) { if ( fs.existsSync(path.join(candidate, 'VERSION')) && fs.existsSync(path.join(candidate, binaryName)) ) { return candidate; } } throw new Error(`Extracted runtime archive does not contain runtime/VERSION and ${binaryName}`); } function verifyStagedRuntime(runtimeLock, asset, platformKey) { const versionPath = path.join(runtimeDir, 'VERSION'); const binaryPath = path.join(runtimeDir, asset.binaryName); if (!fs.existsSync(versionPath)) { throw new Error('Staged runtime is missing resources/runtime/VERSION'); } if (!fs.existsSync(binaryPath)) { throw new Error(`Staged runtime is missing resources/runtime/${asset.binaryName}`); } const versionText = fs.readFileSync(versionPath, 'utf8').trim(); if (!versionText.includes(runtimeLock.version)) { throw new Error( `Staged runtime version mismatch for ${platformKey}. Expected ${runtimeLock.version}, got ${versionText}` ); } } async function stageRuntime(options) { const runtimeLock = readRuntimeLock(); const platformKey = options.platform?.trim() || getDefaultPlatformKey(); const asset = runtimeLock.assets?.[platformKey]; if (!asset) { throw new Error(`runtime.lock.json has no asset for ${platformKey}`); } const releaseTag = getReleaseTag(runtimeLock, options.releaseTag); const workDir = path.join(downloadRoot, `stage-${platformKey}-${process.pid}-${Date.now()}`); const archivePath = path.join(workDir, asset.file); const extractDir = path.join(workDir, 'extracted'); fs.rmSync(workDir, { recursive: true, force: true }); fs.mkdirSync(workDir, { recursive: true }); try { const url = getReleaseAssetUrl(runtimeLock, releaseTag, asset); process.stdout.write( `Downloading ${asset.file} from ${runtimeLock.releaseRepository}@${releaseTag}\n` ); await downloadFile(url, archivePath); process.stdout.write(`Extracting ${asset.file}\n`); extractArchive(archivePath, extractDir, asset.archiveKind); const payloadDir = findRuntimePayloadDir(extractDir, asset.binaryName); cleanRuntimeDir(); fs.cpSync(payloadDir, runtimeDir, { recursive: true }); if (process.platform !== 'win32' && platformKey !== 'win32-x64') { fs.chmodSync(path.join(runtimeDir, asset.binaryName), 0o755); } verifyStagedRuntime(runtimeLock, asset, platformKey); process.stdout.write(`Staged runtime ${runtimeLock.version} for ${platformKey}\n`); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { printUsage(); return; } if (options.clean) { cleanRuntimeDir(); process.stdout.write('Cleaned resources/runtime\n'); return; } await stageRuntime(options); } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });