import { getDb } from '../../db/client'; import { modules } from '../../db/schema'; import { loadObservedHealth } from '../../services/alerting/observed-health'; import { type JailExemption, collectJailExemptions } from '../../services/jail-exemptions'; import { formatPausedDuration } from '../../services/module-pause'; import { hasFlag } from '../parser'; import type { CommandResult } from '../types'; /** * Handle module list command * * Usage: celilo module list [--json] * * @returns Command result */ export async function handleModuleList( flags: Record = {}, ): Promise { const db = getDb(); // Query all modules const moduleRows = db.select().from(modules).all(); // Per-module jail exemptions (per-module-jail-policy task 3.1), from the // shared collector so list, doctor and audit report the same set. A bad // stored row throws here rather than silently listing unmarked — the same // row will fail that module's hooks at execution time. let jailExemptions: JailExemption[]; try { jailExemptions = collectJailExemptions(db); } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } const exemptedById = new Map(jailExemptions.map((e) => [e.moduleId, e])); // Stable machine-readable roster — the backbone the MCP composite // troubleshooting tools correlate audit findings against (ce-77i.5). if (hasFlag(flags, 'json')) { return { success: true, message: JSON.stringify({ modules: moduleRows, jailExemptions }, null, 2), rawOutput: true, data: { modules: moduleRows, jailExemptions }, }; } if (moduleRows.length === 0) { return { success: true, message: 'No modules installed', }; } // Observed health, derived from live alerts rather than from module state. // The two answer different questions: `state` says whether someone // deliberately verified this module, `health` says whether it is working // right now. "not observed" is a finding, not a blank — see design D15. const health = loadObservedHealth(db); // Format module list const lines = ['Installed modules:', '']; for (const module of moduleRows) { const observed = health.get(module.id); const healthNote = observed ? ` [${observed}]` : ''; // A pause suppresses the alerting that would otherwise report this module // as down, so the state alone is not enough — the AGE is what separates a // maintenance window from an outage nobody remembers taking (design D7). const stateCell = module.state === 'PAUSED' ? `PAUSED (${formatPausedDuration(module.pausedAt)})` : module.state; lines.push( `${module.id} (v${module.version}) - ${stateCell}${healthNote}${jailNote(exemptedById.get(module.id))}`, ); if (module.state === 'PAUSED' && module.pauseReason) { lines.push(` Paused: ${module.pauseReason}`); } if (module.description) { lines.push(` ${module.description}`); } if (module.errorMessage) { lines.push(` Error: ${module.errorMessage}`); } lines.push(''); } return { success: true, message: lines.join('\n'), data: moduleRows, }; } /** * The list marker for an exempted module, or '' when the module has no row * (per-module-jail-policy task 3.4: no row, no output). */ function jailNote(exemption: JailExemption | undefined): string { if (!exemption) return ''; return ` [jail: ${exemption.policy} — weaker than the system's ${exemption.systemPolicy}]`; }