/** * Machine List Command * List all machines in the machine pool */ import type { NetworkZone } from '../../db/schema'; import { type MachineFilters, getModulesOnMachine, listMachines, } from '../../services/machine-pool'; import type { Machine } from '../../types/infrastructure'; import { celiloIntro } from '../prompts'; import type { CommandResult } from '../types'; /** * Machine-readable rows for `machine list --json`. One row per machine, * with the same fields the human table renders. The encrypted SSH key * envelope is deliberately absent: machine-readable output travels into * scripts, logs and artifacts, where a secret is one more copy to leak. */ export function machineListJson(machines: Machine[]): CommandResult { const payload = machines.map((machine) => ({ id: machine.id, hostname: machine.hostname, zone: machine.zone, ipAddress: machine.ipAddress, sshUser: machine.sshUser, role: machine.role, hardware: machine.hardware, interfaces: machine.interfaces, earmarkedModule: machine.earmarkedModule, apiOnly: machine.apiOnly, createdAt: machine.createdAt.getTime(), updatedAt: machine.updatedAt.getTime(), })); // `rawOutput` keeps the payload out of the decorating renderer, which // would wrap it and stop it parsing (celilo#698). return { success: true, message: JSON.stringify(payload, null, 2), rawOutput: true }; } /** * Handle machine list command * * @param args - Command arguments (unused) * @param flags - Command flags (--zone filter, --json for machine-readable output) */ export async function handleMachineList( _args: string[], flags: Record = {}, ): Promise { try { const filters: MachineFilters = {}; if (flags.zone && typeof flags.zone === 'string') { filters.zone = flags.zone as NetworkZone; } if (flags.json) { return machineListJson(await listMachines(filters)); } celiloIntro('Machine Pool'); const machines = await listMachines(filters); if (machines.length === 0) { console.log('No machines in pool.\n'); console.log('Add a machine:'); console.log(' celilo machine add'); return { success: true, message: 'No machines found' }; } console.log(''); for (const machine of machines) { // Derived, not a stored snapshot (celilo#773): this line reported // "None (available)" for a machine that was in fact hosting a VERIFIED // module, which is the one place an operator would look to check. const occupants = getModulesOnMachine(machine.id); const assignedText = occupants.length === 0 ? 'None (available)' : occupants.join(', '); const roleLabel = machine.role === 'router' ? ' [router]' : ''; console.log(`${machine.hostname} (${machine.zone})${roleLabel}`); console.log(` IP: ${machine.ipAddress}`); console.log(` SSH: ${machine.sshUser}@${machine.ipAddress}`); if (machine.role === 'router' && machine.interfaces.length > 1) { console.log(' Interfaces:'); for (const iface of machine.interfaces) { console.log(` ${iface.name}: ${iface.ipAddress} (${iface.zone})`); } } console.log( ` Hardware: ${machine.hardware.cpu_cores} cores, ${machine.hardware.memory_mb} MB RAM, ${machine.hardware.disk_gb} GB disk`, ); console.log(` Assigned: ${assignedText}`); if (machine.earmarkedModule) { console.log(` Earmarked: ${machine.earmarkedModule}`); } console.log(''); } console.log(`Total: ${machines.length} machine${machines.length === 1 ? '' : 's'}\n`); return { success: true, message: `Found ${machines.length} machine(s)`, }; } catch (error) { return { success: false, error: `Failed to list machines: ${error instanceof Error ? error.message : String(error)}`, }; } }