Implements CU-05-03: adds queue list, stats, pause, resume, jobs tail, and drain subcommands. Local adapter stubs unsupported ops with a clear message. Wires into @mosaicstack/mosaic CLI via registerQueueCommand(program). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
430 lines
16 KiB
JavaScript
430 lines
16 KiB
JavaScript
#!/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 <url>', 'Gateway URL', 'http://localhost:14242')
|
|
.option('-e, --email <email>', 'Email address')
|
|
.option('-p, --password <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<string> => 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 <url>', 'Gateway URL', 'http://localhost:14242')
|
|
.option('-c, --conversation <id>', 'Resume a conversation by ID')
|
|
.option('-m, --model <modelId>', 'Model ID to use (e.g. gpt-4o, llama3.2)')
|
|
.option('-p, --provider <provider>', 'Provider to use (e.g. openai, ollama)')
|
|
.option('--agent <idOrName>', 'Connect to a specific agent')
|
|
.option('--project <idOrName>', '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<string> =>
|
|
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 <url>', '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 <id>')
|
|
.description('Resume an existing agent session in the TUI')
|
|
.option('-g, --gateway <url>', '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 <id>')
|
|
.description('Terminate an active agent session')
|
|
.option('-g, --gateway <url>', '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 <path>', 'Source directory for framework files')
|
|
.option('--mosaic-home <path>', 'Target config directory')
|
|
.option('--name <name>', 'Agent name')
|
|
.option('--role <description>', 'Agent role description')
|
|
.option('--style <style>', 'Communication style: direct|friendly|formal')
|
|
.option('--accessibility <prefs>', 'Accessibility preferences')
|
|
.option('--guardrails <rules>', 'Custom guardrails')
|
|
.option('--user-name <name>', 'Your name')
|
|
.option('--pronouns <pronouns>', 'Your pronouns')
|
|
.option('--timezone <tz>', 'Your timezone')
|
|
.action(async (opts: Record<string, string | boolean | undefined>) => {
|
|
// All wizard imports are now static (see top of file)
|
|
|
|
try {
|
|
const mosaicHome = (opts['mosaicHome'] as string | undefined) ?? DEFAULT_MOSAIC_HOME;
|
|
const sourceDir = (opts['sourceDir'] as string | undefined) ?? mosaicHome;
|
|
|
|
const prompter = opts['nonInteractive'] ? new HeadlessPrompter() : new ClackPrompter();
|
|
|
|
const configService = createConfigService(mosaicHome, sourceDir);
|
|
|
|
await runWizard({
|
|
mosaicHome,
|
|
sourceDir,
|
|
prompter,
|
|
configService,
|
|
cliOverrides: {
|
|
soul: {
|
|
agentName: opts['name'] as string | undefined,
|
|
roleDescription: opts['role'] as string | undefined,
|
|
communicationStyle: opts['style'] as 'direct' | 'friendly' | 'formal' | undefined,
|
|
accessibility: opts['accessibility'] as string | undefined,
|
|
customGuardrails: opts['guardrails'] as string | undefined,
|
|
},
|
|
user: {
|
|
userName: opts['userName'] as string | undefined,
|
|
pronouns: opts['pronouns'] as string | undefined,
|
|
timezone: opts['timezone'] as string | undefined,
|
|
},
|
|
},
|
|
});
|
|
} catch (err) {
|
|
if (err instanceof WizardCancelledError) {
|
|
console.log('\nWizard cancelled.');
|
|
process.exit(0);
|
|
}
|
|
console.error('Wizard failed:', err);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
program.parse();
|