/** * Module journal command — read a deployed module's runtime daemon logs * (`journalctl -u `) through celilo, without SSH to the host. * * Sibling to `celilo module logs`, not an extension of it: `logs` reads the * LOCAL deploy log written by Ansible on celilo-mgr, `journal` reads the * REMOTE daemon's journal on the host that runs the module. Different source, * different failure modes (a host can be unreachable; a log file cannot), and * different answers to different questions. Folding them together would have * meant one command whose meaning depended on a flag. * * Read-only by construction — see services/module-journal.ts. */ import { getDb } from '../../db/client'; import { type JournalReport, readModuleJournal } from '../../services/module-journal'; import { getArg, hasFlag, validateRequiredArgs } from '../parser'; import type { CommandResult } from '../types'; const USAGE = 'Usage: celilo module journal [--unit ] [--lines ] [--since ] [--grep ] [--json]'; function numericFlag(value: string | boolean | undefined): number | undefined { return typeof value === 'string' ? Number(value) : undefined; } function stringFlag(value: string | boolean | undefined): string | undefined { return typeof value === 'string' ? value : undefined; } /** * Handle module journal command. */ export async function handleModuleJournal( args: string[], flags: Record = {}, ): Promise { const argError = validateRequiredArgs(args, 1); if (argError) { return { success: false, error: `${argError}\n\n${USAGE}` }; } const moduleId = getArg(args, 0); if (!moduleId) { return { success: false, error: 'Module ID is required' }; } const report = readModuleJournal( { moduleId, unit: stringFlag(flags.unit), lines: numericFlag(flags.lines), since: stringFlag(flags.since), grep: stringFlag(flags.grep), }, getDb(), ); if ('error' in report) { return { success: false, error: report.error }; } if (hasFlag(flags, 'json')) { return { success: true, message: JSON.stringify(report, null, 2), rawOutput: true, data: report, }; } const { message, allOk } = formatJournalReport(report); return allOk ? { success: true, message } : { success: false, error: message }; } /** * Presentation. A host celilo could not read is a FAILURE, not a quiet success * — the defect this whole change exists to fix is unreadable and empty being * byte-identical to the operator. */ export function formatJournalReport(report: JournalReport): { message: string; allOk: boolean } { const out: string[] = []; for (const sys of report.systems) { out.push(`── ${sys.hostname} (${sys.ipv4Address}) · unit ${sys.unit}`); if (!sys.ok) { out.push(` ✗ UNREADABLE: ${sys.error}`); } else if (sys.lines.length === 0) { out.push(' (no matching journal lines)'); } else { out.push(...sys.lines.map((line) => ` ${line}`)); } out.push(''); } return { message: out.join('\n').trimEnd(), allOk: report.systems.every((s) => s.ok), }; }