import { spawnSync } from 'node:child_process'; import { createWriteStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs'; import { chmod, copyFile } from 'node:fs/promises'; import { get } from 'node:https'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; export const NATS_SERVER_VERSION = '2.14.1'; const thisFile = fileURLToPath(import.meta.url); export const repoRoot = resolve(dirname(thisFile), '..', '..'); function platformName() { if (process.platform === 'linux') return 'linux'; if (process.platform === 'darwin') return 'darwin'; if (process.platform === 'win32') return 'windows'; throw new Error(`Unsupported platform for managed nats-server install: ${process.platform}`); } function archName() { if (process.arch === 'x64') return 'amd64'; if (process.arch === 'arm64') return 'arm64'; throw new Error(`Unsupported architecture for managed nats-server install: ${process.arch}`); } function executableName() { return process.platform === 'win32' ? 'nats-server.exe' : 'nats-server'; } export function managedNatsServerPath(root = repoRoot) { return join(root, '.tools', 'nats-server', `v${NATS_SERVER_VERSION}`, `${platformName()}-${archName()}`, 'bin', executableName()); } function pathEntries() { return (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean); } function executableExists(path) { try { return existsSync(path) && statSync(path).isFile(); } catch { return false; } } function findOnPath(name) { for (const entry of pathEntries()) { const candidate = join(entry, name); if (executableExists(candidate)) return candidate; } return null; } export function resolveNatsServer(root = repoRoot) { const explicit = process.env.MATRIX_NATS_SERVER; if (explicit) { const resolved = resolve(explicit); if (!executableExists(resolved)) { throw new Error(`MATRIX_NATS_SERVER points to a missing file: ${resolved}`); } return { path: resolved, source: 'MATRIX_NATS_SERVER' }; } const managed = managedNatsServerPath(root); if (executableExists(managed)) return { path: managed, source: 'repo-managed' }; const onPath = findOnPath(executableName()) ?? findOnPath('nats-server'); if (onPath) return { path: onPath, source: 'PATH' }; return null; } export function natsServerInstallUrl(version = NATS_SERVER_VERSION) { const format = process.platform === 'win32' ? 'zip' : 'tar.gz'; const asset = `nats-server-v${version}-${platformName()}-${archName()}.${format}`; return { asset, url: `https://github.com/nats-io/nats-server/releases/download/v${version}/${asset}`, format, }; } function download(url, destination) { return new Promise((resolveDownload, rejectDownload) => { const request = get(url, (response) => { if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { response.resume(); download(response.headers.location, destination).then(resolveDownload, rejectDownload); return; } if (response.statusCode !== 200) { response.resume(); rejectDownload(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`)); return; } const file = createWriteStream(destination); response.pipe(file); file.on('finish', () => { file.close(resolveDownload); }); file.on('error', rejectDownload); }); request.on('error', rejectDownload); }); } function walkFiles(dir) { const out = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isDirectory()) out.push(...walkFiles(path)); else out.push(path); } return out; } function extractArchive(archive, destination, format) { mkdirSync(destination, { recursive: true }); if (format === 'tar.gz') { const result = spawnSync('tar', ['-xzf', archive, '-C', destination], { stdio: 'inherit' }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(`tar extraction failed with exit ${result.status}`); return; } const script = `Expand-Archive -LiteralPath ${JSON.stringify(archive)} -DestinationPath ${JSON.stringify(destination)} -Force`; const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'inherit' }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(`zip extraction failed with exit ${result.status}`); } export async function installManagedNatsServer(root = repoRoot) { const existing = resolveNatsServer(root); if (existing?.source === 'repo-managed') { return { ...existing, installed: false }; } const target = managedNatsServerPath(root); const installRoot = dirname(dirname(target)); const tmpRoot = join(root, '.tools', 'tmp', `nats-server-v${NATS_SERVER_VERSION}-${Date.now()}`); const { asset, url, format } = natsServerInstallUrl(); const archive = join(tmpRoot, asset); const extractDir = join(tmpRoot, 'extract'); rmSync(installRoot, { recursive: true, force: true }); mkdirSync(tmpRoot, { recursive: true }); try { await download(url, archive); extractArchive(archive, extractDir, format); const binary = walkFiles(extractDir).find((path) => path.endsWith(executableName()) || path.endsWith('/nats-server')); if (!binary) throw new Error(`Downloaded ${asset} did not contain ${executableName()}`); mkdirSync(dirname(target), { recursive: true }); await copyFile(binary, target); if (process.platform !== 'win32') await chmod(target, 0o755); return { path: target, source: 'repo-managed', installed: true, url }; } finally { rmSync(tmpRoot, { recursive: true, force: true }); } } export function readNatsServerVersion(binaryPath) { const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(`${binaryPath} --version failed with exit ${result.status}`); return `${result.stdout}${result.stderr}`.trim(); }