207 lines
7.2 KiB
TypeScript
207 lines
7.2 KiB
TypeScript
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<typeof createConfigService>): 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<string, unknown>, 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<string, unknown>, 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 <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<string, unknown>);
|
|
printTable(rows);
|
|
});
|
|
|
|
// ── config get <key> ────────────────────────────────────────────────────
|
|
|
|
cmd
|
|
.command('get <key>')
|
|
.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 <key> <value> ────────────────────────────────────────────
|
|
|
|
cmd
|
|
.command('set <key> <value>')
|
|
.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 <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 <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());
|
|
}
|
|
});
|
|
}
|