#!/usr/bin/env node import { createRequire } from 'module'; import { Command } from 'commander'; import { registerQualityRails } from '@mosaicstack/quality-rails'; import { registerQueueCommand } from '@mosaicstack/queue'; import { registerAgentCommand } from './commands/agent.js'; import { registerMissionCommand } from './commands/mission.js'; // prdy is registered via launch.ts import { registerLaunchCommands } from './commands/launch.js'; import { registerGatewayCommand } from './commands/gateway.js'; import { backgroundUpdateCheck, checkForAllUpdates, formatAllPackagesTable, getInstallAllCommand, } from './runtime/update-checker.js'; import { runWizard } from './wizard.js'; import { ClackPrompter } from './prompter/clack-prompter.js'; import { HeadlessPrompter } from './prompter/headless-prompter.js'; import { createConfigService } from './config/config-service.js'; import { WizardCancelledError } from './errors.js'; import { DEFAULT_MOSAIC_HOME } from './constants.js'; const _require = createRequire(import.meta.url); const CLI_VERSION: string = (_require('../package.json') as { version: string }).version; // Fire-and-forget update check at startup (non-blocking, cached 1h) try { backgroundUpdateCheck(); } catch { // Silently ignore — update check is best-effort } const program = new Command(); program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION); // ─── runtime launchers + framework commands ──────────────────────────── registerLaunchCommands(program); // ─── login ────────────────────────────────────────────────────────────── program .command('login') .description('Sign in to a Mosaic gateway') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('-e, --email ', 'Email address') .option('-p, --password ', 'Password') .action(async (opts: { gateway: string; email?: string; password?: string }) => { const { signIn, saveSession } = await import('./auth.js'); let email = opts.email; let password = opts.password; if (!email || !password) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); if (!email) email = await ask('Email: '); if (!password) password = await ask('Password: '); rl.close(); } try { const auth = await signIn(opts.gateway, email, password); saveSession(opts.gateway, auth); console.log(`Signed in as ${auth.email} (${opts.gateway})`); } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } }); // ─── tui ──────────────────────────────────────────────────────────────── program .command('tui') .description('Launch interactive TUI connected to the gateway') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('-c, --conversation ', 'Resume a conversation by ID') .option('-m, --model ', 'Model ID to use (e.g. gpt-4o, llama3.2)') .option('-p, --provider ', 'Provider to use (e.g. openai, ollama)') .option('--agent ', 'Connect to a specific agent') .option('--project ', 'Scope session to project') .action( async (opts: { gateway: string; conversation?: string; model?: string; provider?: string; agent?: string; project?: string; }) => { const { loadSession, validateSession, signIn, saveSession } = await import('./auth.js'); // Try loading saved session let session = loadSession(opts.gateway); if (session) { const valid = await validateSession(opts.gateway, session.cookie); if (!valid) { console.log('Session expired. Please sign in again.'); session = null; } } // No valid session — prompt for credentials if (!session) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); console.log(`Sign in to ${opts.gateway}`); const email = await ask('Email: '); const password = await ask('Password: '); rl.close(); try { const auth = await signIn(opts.gateway, email, password); saveSession(opts.gateway, auth); session = auth; console.log(`Signed in as ${auth.email}\n`); } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } } // Resolve agent ID if --agent was passed by name let agentId: string | undefined; let agentName: string | undefined; if (opts.agent) { try { const { fetchAgentConfigs } = await import('./tui/gateway-api.js'); const agents = await fetchAgentConfigs(opts.gateway, session.cookie); const match = agents.find((a) => a.id === opts.agent || a.name === opts.agent); if (match) { agentId = match.id; agentName = match.name; } else { console.error(`Agent "${opts.agent}" not found.`); process.exit(1); } } catch (err) { console.error( `Failed to resolve agent: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } // Resolve project ID if --project was passed by name let projectId: string | undefined; if (opts.project) { try { const { fetchProjects } = await import('./tui/gateway-api.js'); const projects = await fetchProjects(opts.gateway, session.cookie); const match = projects.find((p) => p.id === opts.project || p.name === opts.project); if (match) { projectId = match.id; } else { console.error(`Project "${opts.project}" not found.`); process.exit(1); } } catch (err) { console.error( `Failed to resolve project: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } // Auto-create a conversation if none was specified let conversationId = opts.conversation; if (!conversationId) { try { const { createConversation } = await import('./tui/gateway-api.js'); const conv = await createConversation(opts.gateway, session.cookie, { ...(projectId ? { projectId } : {}), }); conversationId = conv.id; } catch (err) { console.error( `Failed to create conversation: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } // Dynamic import to avoid loading React/Ink for other commands const { render } = await import('ink'); const React = await import('react'); const { TuiApp } = await import('./tui/app.js'); render( React.createElement(TuiApp, { gatewayUrl: opts.gateway, conversationId, sessionCookie: session.cookie, initialModel: opts.model, initialProvider: opts.provider, agentId, agentName: agentName ?? undefined, projectId, version: CLI_VERSION, }), { exitOnCtrlC: false }, ); }, ); // ─── sessions ─────────────────────────────────────────────────────────── const sessionsCmd = program.command('sessions').description('Manage active agent sessions'); sessionsCmd .command('list') .description('List active agent sessions') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .action(async (opts: { gateway: string }) => { const { withAuth } = await import('./commands/with-auth.js'); const auth = await withAuth(opts.gateway); const { fetchSessions } = await import('./tui/gateway-api.js'); try { const result = await fetchSessions(auth.gateway, auth.cookie); if (result.total === 0) { console.log('No active sessions.'); return; } console.log(`Active sessions (${result.total}):\n`); for (const s of result.sessions) { const created = new Date(s.createdAt).toLocaleString(); const durationSec = Math.round(s.durationMs / 1000); console.log(` ID: ${s.id}`); console.log(` Model: ${s.provider}/${s.modelId}`); console.log(` Created: ${created}`); console.log(` Prompts: ${s.promptCount}`); console.log(` Duration: ${durationSec}s`); if (s.channels.length > 0) { console.log(` Channels: ${s.channels.join(', ')}`); } console.log(''); } } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } }); sessionsCmd .command('resume ') .description('Resume an existing agent session in the TUI') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .action(async (id: string, opts: { gateway: string }) => { const { loadSession, validateSession } = await import('./auth.js'); const session = loadSession(opts.gateway); if (!session) { console.error('Not signed in. Run `mosaic login` first.'); process.exit(1); } const valid = await validateSession(opts.gateway, session.cookie); if (!valid) { console.error('Session expired. Run `mosaic login` again.'); process.exit(1); } const { render } = await import('ink'); const React = await import('react'); const { TuiApp } = await import('./tui/app.js'); render( React.createElement(TuiApp, { gatewayUrl: opts.gateway, conversationId: id, sessionCookie: session.cookie, version: CLI_VERSION, }), ); }); sessionsCmd .command('destroy ') .description('Terminate an active agent session') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .action(async (id: string, opts: { gateway: string }) => { const { withAuth } = await import('./commands/with-auth.js'); const auth = await withAuth(opts.gateway); const { deleteSession } = await import('./tui/gateway-api.js'); try { await deleteSession(auth.gateway, auth.cookie, id); console.log(`Session ${id} destroyed.`); } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } }); // ─── gateway ────────────────────────────────────────────────────────── registerGatewayCommand(program); // ─── agent ───────────────────────────────────────────────────────────── registerAgentCommand(program); // ─── mission ─────────────────────────────────────────────────────────── registerMissionCommand(program); // ─── quality-rails ────────────────────────────────────────────────────── registerQualityRails(program); // ─── queue ─────────────────────────────────────────────────────────────── registerQueueCommand(program); // ─── update ───────────────────────────────────────────────────────────── program .command('update') .description('Check for and install Mosaic CLI updates') .option('--check', 'Check only, do not install') .action(async (opts: { check?: boolean }) => { // checkForAllUpdates imported statically above const { execSync } = await import('node:child_process'); console.log('Checking for updates…'); const results = checkForAllUpdates({ skipCache: true }); console.log(''); console.log(formatAllPackagesTable(results)); const outdated = results.filter((r: { updateAvailable: boolean }) => r.updateAvailable); if (outdated.length === 0) { const anyInstalled = results.some((r: { current: string }) => r.current); if (!anyInstalled) { console.error('No @mosaicstack/* packages are installed.'); process.exit(1); } console.log('\n✔ All packages up to date.'); return; } if (opts.check) { process.exit(2); // Signal to callers that an update exists } console.log(`\nInstalling ${outdated.length} update(s)…`); try { // Relies on @mosaicstack:registry in ~/.npmrc const cmd = getInstallAllCommand(outdated); execSync(cmd, { stdio: 'inherit', timeout: 60_000, }); console.log('\n✔ Updated successfully.'); } catch { console.error('\nUpdate failed. Try manually: bash tools/install.sh'); process.exit(1); } }); // ─── wizard ───────────────────────────────────────────────────────────── program .command('wizard') .description('Run the Mosaic installation wizard') .option('--non-interactive', 'Run without prompts (uses defaults + flags)') .option('--source-dir ', 'Source directory for framework files') .option('--mosaic-home ', 'Target config directory') .option('--name ', 'Agent name') .option('--role ', 'Agent role description') .option('--style