import { spawnSync } from 'node:child_process'; import type { Command } from 'commander'; import { createConfigService } from '../config/config-service.js'; import { DEFAULT_MOSAIC_HOME } from '../constants.js'; /** * Resolve mosaicHome from the MOSAIC_HOME env var or the default constant. */ function getMosaicHome(): string { return process.env['MOSAIC_HOME'] ?? DEFAULT_MOSAIC_HOME; } /** * Guard: print an error and exit(1) if config has not been initialised. */ function assertInitialized(svc: ReturnType): void { if (!svc.isInitialized()) { console.error('No config found — run `mosaic wizard` first.'); process.exit(1); } } /** * Flatten a nested object into dotted-key rows for table display. */ function flattenConfig(obj: Record, prefix = ''): Array<[string, string]> { const rows: Array<[string, string]> = []; for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k; if (v !== null && typeof v === 'object' && !Array.isArray(v)) { rows.push(...flattenConfig(v as Record, key)); } else { rows.push([key, v === undefined || v === null ? '' : String(v)]); } } return rows; } /** * Print rows as a padded ASCII table. */ function printTable(rows: Array<[string, string]>): void { if (rows.length === 0) { console.log('(no config values)'); return; } const maxKey = Math.max(...rows.map(([k]) => k.length)); const header = `${'Key'.padEnd(maxKey)} Value`; const divider = '-'.repeat(header.length); console.log(header); console.log(divider); for (const [k, v] of rows) { console.log(`${k.padEnd(maxKey)} ${v}`); } } export function registerConfigCommand(program: Command): void { const cmd = program .command('config') .description('Manage Mosaic framework configuration') .configureHelp({ sortSubcommands: true }); // ── config show ───────────────────────────────────────────────────────── cmd .command('show') .description('Print the current resolved config') .option('-f, --format ', 'Output format: table or json', 'table') .action(async (opts: { format: string }) => { const mosaicHome = getMosaicHome(); const svc = createConfigService(mosaicHome, mosaicHome); assertInitialized(svc); const config = await svc.readAll(); if (opts.format === 'json') { console.log(JSON.stringify(config, null, 2)); return; } // Default: table const rows = flattenConfig(config as unknown as Record); printTable(rows); }); // ── config get ──────────────────────────────────────────────────── cmd .command('get ') .description('Print a single config value (supports dotted keys, e.g. soul.agentName)') .action(async (key: string) => { const mosaicHome = getMosaicHome(); const svc = createConfigService(mosaicHome, mosaicHome); assertInitialized(svc); const value = await svc.getValue(key); if (value === undefined) { console.error(`Key "${key}" not found.`); process.exit(1); } if (typeof value === 'object') { console.log(JSON.stringify(value, null, 2)); } else { console.log(String(value)); } }); // ── config set ──────────────────────────────────────────── cmd .command('set ') .description( 'Set a config value and persist (supports dotted keys, e.g. soul.agentName "Jarvis")', ) .action(async (key: string, value: string) => { const mosaicHome = getMosaicHome(); const svc = createConfigService(mosaicHome, mosaicHome); assertInitialized(svc); let previous: unknown; try { previous = await svc.setValue(key, value); } catch (err) { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } const prevStr = previous === undefined ? '(unset)' : String(previous); console.log(`${key}`); console.log(` old: ${prevStr}`); console.log(` new: ${value}`); }); // ── config edit ───────────────────────────────────────────────────────── cmd .command('edit') .description('Open the config directory in $EDITOR (or vi)') .option('-s, --section
', 'Open a specific section file: soul | user | tools') .action(async (opts: { section?: string }) => { const mosaicHome = getMosaicHome(); const svc = createConfigService(mosaicHome, mosaicHome); assertInitialized(svc); const editor = process.env['EDITOR'] ?? 'vi'; let targetPath: string; if (opts.section) { const validSections = ['soul', 'user', 'tools'] as const; if (!validSections.includes(opts.section as (typeof validSections)[number])) { console.error(`Invalid section "${opts.section}". Choose: soul, user, tools`); process.exit(1); } targetPath = svc.getConfigPath(opts.section as 'soul' | 'user' | 'tools'); } else { targetPath = svc.getConfigPath(); } const result = spawnSync(editor, [targetPath], { stdio: 'inherit' }); if (result.error) { console.error(`Failed to open editor: ${result.error.message}`); process.exit(1); } if (result.status !== 0) { console.error(`Editor exited with code ${String(result.status ?? 1)}`); process.exit(result.status ?? 1); } // Re-read after edit and report any issues try { await svc.readAll(); console.log('Config looks valid.'); } catch (err) { console.error('Warning: config may have validation issues:'); console.error(err instanceof Error ? err.message : String(err)); process.exit(1); } }); // ── config path ───────────────────────────────────────────────────────── cmd .command('path') .description('Print the active config directory path (for scripting)') .option( '-s, --section
', 'Print path for a specific section file: soul | user | tools', ) .action(async (opts: { section?: string }) => { const mosaicHome = getMosaicHome(); const svc = createConfigService(mosaicHome, mosaicHome); if (opts.section) { const validSections = ['soul', 'user', 'tools'] as const; if (!validSections.includes(opts.section as (typeof validSections)[number])) { console.error(`Invalid section "${opts.section}". Choose: soul, user, tools`); process.exit(1); } console.log(svc.getConfigPath(opts.section as 'soul' | 'user' | 'tools')); } else { console.log(svc.getConfigPath()); } }); }