#!/usr/bin/env node // Usage: // node packages/control-board/src/cli.mjs scan [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print] // node packages/control-board/src/cli.mjs serve [same flags] [--port N] [--host 127.0.0.1] // Exit 0 on success, 2 on a config refusal or bad usage. import { join } from "node:path"; import { homedir } from "node:os"; import { loadConfig, defaultConfigPath, discoverRepoAgents, discoverFleetAgents, scan, tmuxInspect, ConfigError } from "./scan.mjs"; import { startServer } from "./serve.mjs"; const USAGE = "usage: mosaic-board scan|serve [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print] [--port N] [--host 127.0.0.1]"; function parseArgs(argv) { const opts = { command: argv[0], config: defaultConfigPath(), repo: process.cwd(), fleet: join(homedir(), ".mosaic", "fleet", "agents"), liveness: "tmux", print: false, port: 7331, host: "127.0.0.1" }; for (let i = 1; i < argv.length; i++) { const a = argv[i]; const next = () => { if (i + 1 >= argv.length) throw new ConfigError(`missing value for ${a}`); return argv[++i]; }; if (a === "--config") opts.config = next(); else if (a === "--repo") opts.repo = next(); else if (a === "--fleet") opts.fleet = next(); else if (a === "--liveness") opts.liveness = next(); else if (a === "--print") opts.print = true; else if (a === "--port") { opts.port = Number(next()); if (!Number.isInteger(opts.port) || opts.port < 0 || opts.port > 65535) throw new ConfigError("--port must be an integer 0..65535"); } else if (a === "--host") opts.host = next(); else throw new ConfigError(`unknown argument: ${a}`); } if (opts.command !== "scan" && opts.command !== "serve") throw new ConfigError(USAGE); if (!["tmux", "assume-alive"].includes(opts.liveness)) throw new ConfigError(`unknown liveness mode: ${opts.liveness}`); return opts; } async function main() { const opts = parseArgs(process.argv.slice(2)); const { dataRoot } = loadConfig(opts.config); const specs = [...discoverRepoAgents(opts.repo), ...(opts.fleet === "none" ? [] : discoverFleetAgents(opts.fleet))]; const isAlive = opts.liveness === "tmux" ? tmuxInspect : () => true; const boardDir = join(dataRoot, "board"); const seatsDir = join(dataRoot, "seats"); if (opts.command === "serve") { const server = await startServer({ host: opts.host, port: opts.port, specs, boardDir, isAlive, seatsDir, discordDataRoot: dataRoot }); const addr = server.address(); process.stdout.write(`control board: http://${opts.host}:${addr.port}/ (${specs.length} agents; board files in ${boardDir}; Ctrl-C to stop)\n`); const stop = () => server.close(() => process.exit(0)); process.on("SIGINT", stop); process.on("SIGTERM", stop); return; } const index = scan(specs, { boardDir, isAlive, seatsDir, discordDataRoot: dataRoot }); for (const line of index.discoveryErrors) process.stderr.write(`${line}\n`); for (const line of index.registrationErrors) process.stderr.write(`registration skipped: ${line}\n`); if (opts.print) { for (const s of index.sessions) { const flag = s.waitingOnYou ? "*" : s.seen ? "s" : " "; const age = s.relaunchedAt ? "relaunch" : s.ageSeconds == null ? "-" : `${Math.round(s.ageSeconds / 60)}m`; const preview = s.relaunchedAt ? `relaunched at ${s.relaunchedAt}, no messages since` : s.lastAssistantText ? s.lastAssistantText.slice(0, 80) : ""; process.stdout.write(`${flag} ${s.state.padEnd(8)} ${s.project.padEnd(14)} ${s.agent.padEnd(16)} ${age.padStart(7)} ${preview}\n`); } } process.stdout.write(`board: ${boardDir} (${index.sessions.length} sessions, ${index.waitingOnYou.length} waiting on you, ${index.seen.length} seen, ${index.registered.length} registered, ${index.registrationStale.length} stale)\n`); } main().catch((err) => { if (err instanceof ConfigError) { process.stderr.write(`refused: ${err.message}\n`); process.exit(2); } throw err; });