#!/usr/bin/env node import { Command } from 'commander'; import { buildPrdyCli } from '@mosaic/prdy'; import { createQualityRailsCli } from '@mosaic/quality-rails'; const program = new Command(); program.name('mosaic').description('Mosaic Stack CLI').version('0.0.0'); // ─── login ────────────────────────────────────────────────────────────── program .command('login') .description('Sign in to a Mosaic gateway') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:4000') .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:4000') .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)') .action( async (opts: { gateway: string; conversation?: string; model?: string; provider?: 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); } } // 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: opts.conversation, sessionCookie: session.cookie, initialModel: opts.model, initialProvider: opts.provider, }), ); }, ); // ─── 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:4000') .action(async (opts: { gateway: string }) => { const { loadSession, validateSession } = await import('./auth.js'); const { fetchSessions } = await import('./tui/gateway-api.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); } try { const result = await fetchSessions(opts.gateway, session.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:4000') .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, }), ); }); sessionsCmd .command('destroy ') .description('Terminate an active agent session') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:4000') .action(async (id: string, opts: { gateway: string }) => { const { loadSession, validateSession } = await import('./auth.js'); const { deleteSession } = await import('./tui/gateway-api.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); } try { await deleteSession(opts.gateway, session.cookie, id); console.log(`Session ${id} destroyed.`); } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } }); // ─── prdy ─────────────────────────────────────────────────────────────── const prdyWrapper = buildPrdyCli(); const prdyCmd = prdyWrapper.commands.find((c) => c.name() === 'prdy'); if (prdyCmd !== undefined) { program.addCommand(prdyCmd as unknown as Command); } // ─── quality-rails ────────────────────────────────────────────────────── const qrWrapper = createQualityRailsCli(); const qrCmd = qrWrapper.commands.find((c) => c.name() === 'quality-rails'); if (qrCmd !== undefined) { program.addCommand(qrCmd as unknown as Command); } // ─── 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