hivecast-sdk/scripts/lib/nats-server-tool.mjs

171 lines
6.1 KiB
JavaScript
Raw Permalink Normal View History

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();
}