#!/usr/bin/env node /** * Remote host agent: spawn/kill fleet workers so the control plane can spread replicas. * * FLEET_AGENT_PORT=3851 node src/agent.js * * Bind loopback or a private interface. Do not publish NATS. */ import http from 'node:http'; import { spawn } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js'); const PORT = Number(process.env.FLEET_AGENT_PORT || 3851); const BIND = process.env.FLEET_AGENT_BIND || '127.0.0.1'; const children = new Map(); function json(res, code, obj) { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); } async function readBody(req) { const chunks = []; for await (const c of req) chunks.push(c); const raw = Buffer.concat(chunks).toString('utf8'); return raw ? JSON.parse(raw) : {}; } export function startAgent({ port = PORT, bind = BIND } = {}) { const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://${bind}:${port}`); try { if (req.method === 'GET' && url.pathname === '/health') { return json(res, 200, { ok: true, role: 'fleet-agent', workers: [...children.keys()], }); } if (req.method === 'POST' && url.pathname === '/spawn') { const body = await readBody(req); const instance = body.instance; if (!instance) return json(res, 400, { error: 'instance required' }); if (children.has(instance)) { return json(res, 200, { pid: children.get(instance).pid, instance, existing: true }); } const child = spawn(process.execPath, [WORKER], { env: { ...process.env, ...(body.env || {}) }, stdio: ['ignore', 'pipe', 'pipe'], }); children.set(instance, child); child.on('exit', () => children.delete(instance)); return json(res, 200, { pid: child.pid, instance }); } if (req.method === 'POST' && url.pathname === '/kill') { const body = await readBody(req); const child = children.get(body.instance); if (child) { child.kill('SIGTERM'); children.delete(body.instance); } return json(res, 200, { killed: Boolean(child), instance: body.instance }); } json(res, 404, { error: 'not found' }); } catch (err) { json(res, 400, { error: err.message }); } }); server.listen(port, bind, () => { process.stdout.write(`fleet-agent http://${bind}:${port}/\n`); }); return { server, async close() { for (const c of children.values()) c.kill('SIGTERM'); children.clear(); await new Promise((r) => server.close(r)); }, }; } if (process.argv[1] && path.basename(process.argv[1]) === 'agent.js') { startAgent(); }