/** * `celilo console …` — the narrow reads the web console polls. * * A NOTE ON THE VERB NAMES, because they read oddly on purpose. celilo's remote * API authorises at two levels (`command:subcommand`), and a leaf is classified * read-only by its subcommand token against `READ_VERBS` in `@celilo/core`. So * these are named from that vocabulary — `status`, `list`, `get` — rather than * for prose. `console roster` would be a nicer name and would classify as a * WRITE, which would either deny the console its own data or require widening * the read-verb set for every principal in the fleet. * * All three are reads. The console's principal is granted read ops only, so a * mutating verb added to this file would be refused at the API boundary rather * than running. */ import { DEFAULT_CLOSURE_DEPTH, UNBOUNDED_CLOSURE_DEPTH, computeClosure, } from '../../console/closure'; import { consoleStatus, loadBindings, loadClosureInputs, moduleExists, } from '../../console/projection'; import { getDb } from '../../db/client'; import { loadCapabilityProviderRows } from '../../services/consumer-cleanup'; import { hasFlag } from '../parser'; import type { CommandResult } from '../types'; /** `celilo console status [--json]` — the dashboard's single poll. */ export function handleConsoleStatus(flags: Record = {}): CommandResult { const payload = consoleStatus(getDb()); if (hasFlag(flags, 'json')) { return { success: true, message: JSON.stringify(payload), rawOutput: true, data: payload }; } const lines = payload.modules.map( (m) => `${m.id.padEnd(24)} ${m.state.padEnd(12)} ${m.health.cell.padEnd(14)} ${ m.systems.map((s) => `${s.hostname}@${s.zone}`).join(', ') || '(no system)' }`, ); return { success: true, message: [`zones: ${payload.zones.join(' > ')}`, '', ...lines].join('\n'), }; } /** * `celilo console get [--depth N] [--json]` — one module's bounded * capability closure. * * `--depth 0` walks the whole graph. The walk terminates on cycles either way. */ export function handleConsoleGet( args: string[], flags: Record = {}, ): CommandResult { const moduleId = args[0]; if (!moduleId) { return { success: false, error: 'Module ID required\n\nUsage: celilo console get ' }; } const db = getDb(); if (!moduleExists(db, moduleId)) { return { success: false, error: `Module not found: ${moduleId}` }; } const depth = parseDepth(flags.depth); if (depth instanceof Error) return { success: false, error: depth.message }; const { manifests, providerStates, chains } = loadClosureInputs(db); // Real bindings, for every module the walk might reach. The console draws what // the fleet IS doing, not what its manifests permit. const bindings = new Map([...manifests.keys()].map((id) => [id, loadBindings(db, id)] as const)); const result = computeClosure({ rootModuleId: moduleId, manifests, providerRows: loadCapabilityProviderRows(db), providerStates, depth, bindings, chains, }); if (hasFlag(flags, 'json')) { return { success: true, message: JSON.stringify(result), rawOutput: true, data: result }; } if (result.nodes.length === 0) { // An answer, not a failure. The console says the same thing in words rather // than rendering an empty picture that reads as still loading. return { success: true, message: `${moduleId} depends on nothing.` }; } // The chain is printed as a path rather than as more rows, because that is the // whole claim: these providers are ordered, and a list of them is not. const chainLines = result.chains.map( (chain) => `${chain.capability}: ${chain.moduleIds.join(' -> ')}`, ); return { success: true, message: [ ...result.nodes.map( (n) => `hop ${n.hop} ${n.moduleId.padEnd(24)} ${n.optional ? '(optional)' : ' '} via ${n.via.join(', ')}`, ), ...(chainLines.length > 0 ? ['', 'delegates upstream:', ...chainLines] : []), ].join('\n'), }; } /** Depth flag, or an Error explaining what was wrong with it. */ function parseDepth(raw: string | boolean | undefined): number | Error { if (raw === undefined || raw === true) return DEFAULT_CLOSURE_DEPTH; const parsed = Number(raw); if (!Number.isInteger(parsed) || parsed < 0) { return new Error( `Invalid --depth: ${raw}\n\nExpected a non-negative integer (${UNBOUNDED_CLOSURE_DEPTH} walks the whole graph).`, ); } return parsed; }