import { constants } from 'node:fs'; import { access, chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'; import { homedir, hostname } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawn } from 'node:child_process'; import type { Command } from 'commander'; import YAML from 'yaml'; export interface CommandResult { stdout: string; stderr: string; exitCode: number; } export type CommandRunner = (command: string, args: string[]) => Promise; export interface FleetCommandDeps { runner?: CommandRunner; mosaicHome?: string; frameworkRoot?: string; } interface RawFleetRoster { version?: unknown; transport?: unknown; tmux?: { socket_name?: unknown; socketName?: unknown; holder_session?: unknown; holderSession?: unknown; }; defaults?: { working_directory?: unknown; workingDirectory?: unknown; }; runtimes?: Record; agents?: Array<{ name?: unknown; runtime?: unknown; class?: unknown; working_directory?: unknown; workingDirectory?: unknown; model_hint?: unknown; modelHint?: unknown; persistent_persona?: unknown; persistentPersona?: unknown; reset_between_tasks?: unknown; resetBetweenTasks?: unknown; kickstart_template?: unknown; kickstartTemplate?: unknown; }>; } export interface FleetAgent { name: string; runtime: string; className: string; workingDirectory?: string; modelHint?: string; persistentPersona?: boolean | string; resetBetweenTasks?: boolean; kickstartTemplate?: string; } export interface FleetRoster { version: 1; transport: 'tmux'; tmux: { socketName: string; holderSession: string; }; defaults: { workingDirectory: string; }; runtimes: Record; agents: FleetAgent[]; } export interface FleetPaths { mosaicHome: string; rosterPath: string; toolsDir: string; fleetToolsDir: string; tmuxToolsDir: string; systemdUserDir: string; agentEnvDir: string; } type FleetServiceAction = 'start' | 'stop' | 'restart' | 'status'; const DEFAULT_SOCKET_NAME = 'mosaic-factory'; const DEFAULT_HOLDER_SESSION = '_holder'; const DEFAULT_WORKING_DIRECTORY = '~/src'; const DEFAULT_RUNTIME_RESETS: Record = { claude: { resetCommand: '/clear' }, codex: { resetCommand: '/clear' }, opencode: { resetCommand: '/clear' }, pi: { resetCommand: '/new' }, }; export function resolveFleetPaths(mosaicHome = defaultMosaicHome()): FleetPaths { return { mosaicHome, rosterPath: join(mosaicHome, 'fleet', 'roster.yaml'), toolsDir: join(mosaicHome, 'tools'), fleetToolsDir: join(mosaicHome, 'tools', 'fleet'), tmuxToolsDir: join(mosaicHome, 'tools', 'tmux'), systemdUserDir: join(homedir(), '.config', 'systemd', 'user'), agentEnvDir: join(mosaicHome, 'fleet', 'agents'), }; } function defaultMosaicHome(): string { return join(homedir(), '.config', 'mosaic'); } function assertDefaultMosaicHomeForSystemd(mosaicHome: string): void { if (resolve(mosaicHome) !== resolve(defaultMosaicHome())) { throw new Error( `install-systemd only supports the default Mosaic home (${defaultMosaicHome()}) because the user systemd units use %h/.config/mosaic paths.`, ); } } export async function loadFleetRoster(path: string): Promise { const rawText = await readFile(path, 'utf8'); const parsed = parseRosterText(rawText, path); return normalizeRoster(parsed); } export function getRosterAgent(roster: FleetRoster, name: string): FleetAgent { const agent = roster.agents.find((candidate) => candidate.name === name); if (!agent) { throw new Error(`Agent "${name}" is not in the fleet roster.`); } return agent; } export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string { const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory; return [ `MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`, `MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`, `MOSAIC_AGENT_WORKDIR=${shellEnvValue(expandHome(workingDirectory))}`, `MOSAIC_TMUX_SOCKET=${shellEnvValue(roster.tmux.socketName)}`, '', ].join('\n'); } export function mergeAgentEnv(generatedEnv: string, existingEnv?: string): string { if (!existingEnv?.trim()) { return generatedEnv; } const generatedKeys = new Set( generatedEnv .split('\n') .map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]) .filter((key): key is string => key !== undefined), ); const preservedLines = existingEnv.split('\n').filter((line) => { if (!line.trim()) { return false; } const key = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]; return key === undefined || !generatedKeys.has(key); }); if (preservedLines.length === 0) { return generatedEnv; } return [generatedEnv.trimEnd(), ...preservedLines, ''].join('\n'); } export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] { const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service'; return ['systemctl', '--user', action, service]; } export function buildAgentSendCommand( paths: FleetPaths, agentName: string, message: string, socketName = DEFAULT_SOCKET_NAME, sourceLabel = getDefaultOperatorSourceLabel(), ): string[] { return [ join(paths.tmuxToolsDir, 'agent-send.sh'), '-L', socketName, '-S', sourceLabel, '-s', agentName, '-m', message, ]; } export function getDefaultOperatorSourceLabel(): string { const shortHostname = hostname().split('.')[0] || 'localhost'; return `${shortHostname}:operator`; } export function buildAgentResetCommand( paths: FleetPaths, agentName: string, resetCommand: string, socketName = DEFAULT_SOCKET_NAME, ): string[] { return [ join(paths.tmuxToolsDir, 'send-message.sh'), '-L', socketName, '-t', `=${agentName}`, '-m', resetCommand, ]; } export function buildAgentTailCommand( agentName: string, lines: number, socketName = DEFAULT_SOCKET_NAME, ): string[] { return [ 'tmux', '-L', socketName, 'capture-pane', '-t', `=${agentName}:0.0`, '-p', '-S', `-${lines}`, ]; } export function registerFleetCommand(program: Command, deps: FleetCommandDeps = {}): Command { const runner = deps.runner ?? runCommand; const paths = resolveFleetPaths(deps.mosaicHome); const frameworkRoot = deps.frameworkRoot ?? resolveFrameworkRoot(); const cmd = program .command('fleet') .description('Manage the local Mosaic tmux fleet canary') .option('--mosaic-home ', 'Mosaic home directory', paths.mosaicHome) .option('--roster ', 'Fleet roster path'); cmd .command('init') .description('Initialize a local fleet roster') .option('--profile ', 'Roster profile: minimal or local-canary', 'minimal') .option('--write', 'Write the roster to Mosaic home') .option('--force', 'Overwrite an existing roster when used with --write') .action(async (opts: { profile: string; write?: boolean; force?: boolean }) => { const commandOpts = cmd.opts<{ mosaicHome: string; roster?: string }>(); const activePaths = resolveFleetPaths(commandOpts.mosaicHome); const profile = parseInitProfile(opts.profile); const source = join(frameworkRoot, 'fleet', 'examples', `${profile}.yaml`); const content = await readFile(source, 'utf8'); if (!opts.write) { console.log(content.trimEnd()); return; } const destination = commandOpts.roster ?? activePaths.rosterPath; if (!opts.force && (await canRead(destination))) { throw new Error( `Fleet roster already exists: ${destination}. Re-run with --force to overwrite.`, ); } await mkdir(dirname(destination), { recursive: true }); await writeFile(destination, content); console.log(`Wrote fleet roster: ${destination}`); }); cmd .command('install') .description('Install local fleet tools and user systemd units') .action(async () => installFleet(cmd, frameworkRoot)); cmd .command('install-systemd') .description('Install local fleet tools and user systemd units') .action(async () => installFleet(cmd, frameworkRoot)); for (const action of ['start', 'stop', 'restart'] as const) { cmd .command(`${action} [agent]`) .description(`${action} the fleet holder or one agent`) .action(async (agent?: string) => { const roster = await loadRosterForCommand(cmd); if (agent) { getRosterAgent(roster, agent); await runChecked(runner, buildFleetServiceCommand(action, agent)); return; } if (action === 'stop') { await stopFleetBestEffort( runner, roster.agents.map((rosterAgent) => rosterAgent.name), ); return; } await runChecked(runner, buildFleetServiceCommand(action)); for (const rosterAgent of roster.agents) { await runChecked(runner, buildFleetServiceCommand(action, rosterAgent.name)); } }); } cmd .command('status [agent]') .description('Show fleet holder or agent systemd status') .option('--json', 'Print JSON status') .action(async (agent: string | undefined, opts: { json?: boolean }) => { if (agent) { const roster = await loadRosterForCommand(cmd); getRosterAgent(roster, agent); } const result = await runner(...splitCommand(buildFleetServiceCommand('status', agent))); if (opts.json) { console.log( JSON.stringify({ exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, }), ); setExitCodeFromResult(result); return; } writeCommandOutput(result); }); cmd .command('verify') .description('Verify the local canary holder and roster sessions on the isolated socket') .action(async () => { const roster = await loadRosterForCommand(cmd); const socketName = roster.tmux.socketName; await runChecked(runner, [ 'tmux', '-L', socketName, 'has-session', '-t', `=${roster.tmux.holderSession}:0.0`, ]); for (const agent of roster.agents) { await runChecked(runner, [ 'tmux', '-L', socketName, 'has-session', '-t', `=${agent.name}:0.0`, ]); } console.log(`Verified fleet on tmux socket ${socketName}.`); }); return cmd; } export function registerFleetAgentCommands( agentCommand: Command, deps: FleetCommandDeps = {}, ): void { const runner = deps.runner ?? runCommand; agentCommand .command('roster') .description('List agents from the local fleet roster') .option('--json', 'Print JSON') .action(async (opts: { json?: boolean }) => { const roster = await loadRosterFromAgentCommand(agentCommand, deps.mosaicHome); if (opts.json) { console.log(JSON.stringify(roster, null, 2)); return; } for (const agent of roster.agents) { console.log(`${agent.name}\t${agent.runtime}\t${agent.className}`); } }); agentCommand .command('status [agent]') .description('Show tmux status for the local fleet or one agent') .option('--json', 'Print JSON') .action(async (agent: string | undefined, opts: { json?: boolean }) => { const roster = await loadRosterFromAgentCommand(agentCommand, deps.mosaicHome); if (agent) { getRosterAgent(roster, agent); } const command = agent ? ['tmux', '-L', roster.tmux.socketName, 'has-session', '-t', `=${agent}:0.0`] : ['tmux', '-L', roster.tmux.socketName, 'ls']; const result = await runner(...splitCommand(command)); if (opts.json) { console.log( JSON.stringify({ exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, }), ); setExitCodeFromResult(result); return; } writeCommandOutput(result); }); agentCommand .command('send ') .description('Send a message to a local fleet agent') .requiredOption('--message ', 'Message text') .option('--source-label