#!/usr/bin/env node /** * verae-fleet — catalog, monitor, restart, pause. * * Local (no daemon): list * Daemon: serve | monitor * Against daemon: status | start | stop | pause | resume | restart */ import http from 'node:http'; import { loadFleet, listServices } from './load.js'; import { Supervisor } from './supervisor.js'; import { Monitor } from './monitor.js'; import { startControlServer } from './server.js'; const loaded = loadFleet(); const [cmd = 'help', target] = process.argv.slice(2); const BASE = process.env.FLEET_URL || `http://127.0.0.1:${loaded.control.port}`; function print(obj) { process.stdout.write(`${typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)}\n`); } function help() { print(`verae-fleet list services, config files, replica min/max serve start floors + monitor + UI ${BASE}/ monitor same without printing extra status GET daemon status start turn on, spawn up to min stop turn off (service disable) or stop one replica pause pause (does not count toward tree-node floor) resume restart kill + respawn reconcile force floor check Central spec: fleet.json Machines: machines.json (add hosts to spread replicas) Per service: services/.json Remote SSH: machines.json user + host + identityFile (path only) ssh-check [id] test SSH login (default ns1) `); } function api(method, pathname) { return new Promise((resolve, reject) => { const u = new URL(pathname, BASE); const req = http.request( { hostname: u.hostname, port: u.port, path: u.pathname, method, timeout: 8000 }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8'); try { resolve({ status: res.statusCode, body: JSON.parse(raw) }); } catch { resolve({ status: res.statusCode, body: raw }); } }); }, ); req.on('error', reject); req.end(); }); } async function main() { if (cmd === 'help' || cmd === '-h' || cmd === '--help') return help(); if (cmd === 'list') { for (const r of listServices(loaded)) { print( `${r.id.padEnd(22)} min=${String(r.min).padStart(2)} max=${String(r.max).padStart(2)} floor=${r.keepFloor ? 'yes' : 'no '} managed=${r.managed ? 'yes' : 'no '} ${r.configPath}`, ); } print(`\ncentral replica spec: ${loaded.fleetPath}`); print(`tree-node floor: min=${loaded.services['tree-node'].min} keepFloor=${loaded.services['tree-node'].keepFloor}`); print('\nmachines:'); for (const m of loaded.machines) { const ssh = m.kind === 'ssh' ? `${m.user}@${m.host} key=${m.identityFile}` : m.host; print(` ${m.id.padEnd(12)} ${m.kind.padEnd(6)} ${m.enabled ? 'on ' : 'off'} cap=${m.capacity} ${ssh}`); } return; } if (cmd === 'ssh-check') { const { sshCheck } = await import('./ssh.js'); const id = target || 'ns1'; const m = loaded.machines.find((x) => x.id === id); if (!m) throw new Error(`unknown machine ${id}`); print(await sshCheck(m)); return; } if (cmd === 'serve' || cmd === 'monitor') { const sup = new Supervisor({ loaded }); for (const id of Object.keys(loaded.services)) { const s = loaded.services[id]; if (s.managed && s.enabled && s.min > 0) await sup.startService(id); } const mon = new Monitor(sup); await mon.tick(); mon.start(); startControlServer(sup, mon); print(`fleet ${cmd} pid=${process.pid}\nUI ${BASE}/\nSIGINT stops all managed replicas`); process.on('SIGINT', async () => { mon.stop(); await sup.stopAll(); process.exit(0); }); process.on('SIGTERM', async () => { mon.stop(); await sup.stopAll(); process.exit(0); }); return; } const instance = target && /-\d+$/.test(target); const map = { status: ['GET', '/api/status'], reconcile: ['POST', '/api/reconcile'], }; if (map[cmd] && !target) { const [m, p] = map[cmd]; const r = await api(m, p); print(r.body); if (r.status >= 400) process.exitCode = 1; return; } if (!target) { help(); process.exitCode = 1; return; } const pathName = instance ? `/api/instances/${target}/${cmd}` : `/api/services/${target}/${cmd}`; const r = await api('POST', pathName); print(r.body); if (r.status >= 400) process.exitCode = 1; } main().catch((err) => { print({ error: err.message, hint: 'Is `verae-fleet serve` running?' }); process.exitCode = 1; });