#!/usr/bin/env node // Usage: // mosaic launch [--task TEXT] [--project NAME] [--workspace PATH] // [--harness NAME] [--repo PATH] [--config PATH] [-- args...] // mosaic seat task [--by NAME] [--layout repo|fleet|unknown] [--config PATH] // // `seat task` records who set the task as taskSetBy: `--by NAME`, else // $MOSAIC_AGENT_NAME, else "unknown". This is what the caller claimed; it is // not verified and it authorizes nothing. // // `launch` writes /seats///registration.json, then replaces // itself with the seat's launch.sh, unchanged, with everything after `--` as // its arguments and MOSAIC_LAUNCH_REGISTERED set to the record path. A launch // script that sees that variable is already registered and must not call // mosaic launch again. The exit code is the launch script's own. // // Exit codes: the launch script's code (launch); 0 ok; 1 operation failed; // 2 invalid data or configuration; 4 usage. import { rmSync } from "node:fs"; import { SeatError, defaultConfigPath, loadDataRoot, seatsDir, resolveSeat, tmuxContext, makeRegistration, writeRegistration, updateTask, resolveSetBy, } from "./seat.mjs"; const USAGE = [ "usage: mosaic launch [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...]", " mosaic seat task [--by NAME] [--layout repo|fleet|unknown] [--config PATH]", ].join("\n"); export const REGISTERED_ENV = "MOSAIC_LAUNCH_REGISTERED"; function parseLaunch(argv) { const opts = { seat: null, task: "", project: undefined, workspace: undefined, harness: null, repo: process.cwd(), config: defaultConfigPath(), args: [] }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; const next = () => { if (i + 1 >= argv.length) throw new SeatError(`missing value for ${a}`, 4); return argv[++i]; }; if (a === "--") { opts.args = argv.slice(i + 1); break; } else if (a === "--task") opts.task = next(); else if (a === "--project") opts.project = next(); else if (a === "--workspace") opts.workspace = next(); else if (a === "--harness") opts.harness = next(); else if (a === "--repo") opts.repo = next(); else if (a === "--config") opts.config = next(); else if (a.startsWith("--")) throw new SeatError(`unknown argument: ${a}\n${USAGE}`, 4); else if (opts.seat === null) opts.seat = a; else throw new SeatError(`unexpected argument: ${a} (put launch script arguments after --)`, 4); } if (opts.seat === null) throw new SeatError(USAGE, 4); return opts; } function parseSeatTask(argv) { const opts = { seat: null, task: null, layout: null, by: null, config: defaultConfigPath() }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--config" || a === "--layout" || a === "--by") { if (i + 1 >= argv.length) throw new SeatError(`missing value for ${a}`, 4); opts[a.slice(2)] = argv[++i]; } else if (a.startsWith("--")) throw new SeatError(`unknown argument: ${a}\n${USAGE}`, 4); else if (opts.seat === null) opts.seat = a; else if (opts.task === null) opts.task = a; else throw new SeatError(`unexpected argument: ${a}`, 4); } if (opts.seat === null || opts.task === null) throw new SeatError(USAGE, 4); return opts; } function launch(argv) { const opts = parseLaunch(argv); const resolved = resolveSeat(opts.seat, { repo: opts.repo }); const seats = seatsDir(loadDataRoot(opts.config)); // Register first, then replace this process with the launch script (same // pid, same terminal, same process group). The launch script's exit code // and signals are then the shell's to see directly, and the tmux pane's // foreground command stays the harness itself, which the control board's // liveness check depends on. const record = makeRegistration({ resolved, task: opts.task, project: opts.project, workspace: opts.workspace, harness: opts.harness, tmux: tmuxContext(), pid: process.pid, }); const path = writeRegistration(seats, record); process.stderr.write(`mosaic launch: registered ${resolved.seat} (${resolved.layout} layout) at ${path}\n`); try { process.execve(resolved.launchScript, [resolved.launchScript, ...opts.args], { ...process.env, [REGISTERED_ENV]: path }); } catch (err) { rmSync(path, { force: true }); throw new SeatError(`could not run ${resolved.launchScript}: ${err.message}`, 1); } return 0; } function seatTask(argv) { const opts = parseSeatTask(argv); // Attribution is resolved (and an invalid value refused) before any read // or write, so a refusal leaves the record exactly as it was. const setBy = resolveSetBy({ by: opts.by, env: process.env }); const seats = seatsDir(loadDataRoot(opts.config)); const record = updateTask(seats, opts.seat, opts.task, { layout: opts.layout, setBy }); process.stdout.write(`mosaic seat task: ${opts.seat} (${record.layout} layout) task set (${opts.task.length} characters, set by ${setBy})\n`); return 0; } function main(argv) { if (process.env[REGISTERED_ENV] && argv[0] === "launch") { throw new SeatError(`${REGISTERED_ENV} is already set; refusing to register a seat from inside a registered launch`, 4); } if (argv[0] === "launch") return launch(argv.slice(1)); if (argv[0] === "seat" && argv[1] === "task") return seatTask(argv.slice(2)); throw new SeatError(USAGE, 4); } try { process.exitCode = main(process.argv.slice(2)); } catch (err) { if (!(err instanceof SeatError)) throw err; process.stderr.write(`refused: ${err.message}\n`); process.exitCode = err.exitCode; }