/** * System Init Command * * Guides users through initial system configuration with interactive prompts * or applies defaults for non-interactive use. */ import { getDb } from '../../db/client'; import { askConfirm, askSelect, askText, withInterviewSession } from '../../services/bus-interview'; import { autoDetectSSHKeys, getDefaultConfiguration, initializeSystem, isSystemInitialized, loadExistingConfiguration, } from '../../services/system-init'; import { celiloIntro, celiloOutro } from '../prompts'; import type { CommandResult } from '../types'; /** * Parse key=value pairs from positional arguments */ function parseOverrides(args: string[]): Record { const overrides: Record = {}; for (const arg of args) { const eqIndex = arg.indexOf('='); if (eqIndex > 0) { overrides[arg.slice(0, eqIndex)] = arg.slice(eqIndex + 1); } } return overrides; } /** * Handle system init command * * @param args - Command arguments (key=value overrides) * @param flags - Command flags (accept-defaults) */ export async function handleSystemInit( args: string[], flags: Record = {}, ): Promise { const acceptDefaults = flags['accept-defaults'] === true; const cliOverrides = parseOverrides(args); const db = getDb(); // Phase 5 of openspec/specs/management-as-module/spec.md — surface that this command // is on its way out so operators have time to migrate to the new // paths. Keeps working unchanged; the banner just points at the // replacements. CELILO_SUPPRESS_DEPRECATION=1 silences for callers // that already know (e.g. the celilo-mgmt module's on_install hook // — wait, that one shells to apply-config now, but other tooling // that legitimately wants the interactive interview can still opt // out of the banner). if (process.env.CELILO_SUPPRESS_DEPRECATION !== '1') { console.warn('⚠ celilo system init is deprecated.'); console.warn(' Recommended paths:'); console.warn( ' • Fresh management server: curl -fsSL https://celilo.computer/bootstrap.sh | bash', ); console.warn( ' • Headless config write (CI / hooks): celilo system apply-config key=value ...', ); console.warn( ' This command still works; suppress this banner with CELILO_SUPPRESS_DEPRECATION=1.\n', ); } // Check if already initialized const alreadyInitialized = isSystemInitialized(db); if (alreadyInitialized && !acceptDefaults && Object.keys(cliOverrides).length === 0) { console.log('✓ System already initialized.'); console.log(' Existing configuration will be used as defaults.'); console.log(' Press Enter to keep current values, or type new values to update.\n'); } try { if (acceptDefaults) { return initWithDefaults(cliOverrides); } return await initInteractive(cliOverrides); } catch (error) { return { success: false, error: `System initialization failed: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Initialize system with default values (non-interactive) * Accepts key=value overrides from command-line arguments. */ function initWithDefaults(overrides: Record = {}): CommandResult { const db = getDb(); const hasOverrides = Object.keys(overrides).length > 0; if (hasOverrides) { console.log('Initializing Celilo with custom configuration...\n'); } else { console.log('Initializing Celilo with default configuration...\n'); } // Apply defaults + any command-line overrides const config = initializeSystem(db, overrides); const sshKeyDetected = config['ssh.public_key'] !== undefined; console.log('✓ Celilo state initialized'); console.log(' Network addressing is NOT defaulted: the `internal` zone is'); console.log(' discovered when you deploy celilo-mgmt, and dmz/app/secure'); console.log(' appear when you deploy a firewall module.'); if (sshKeyDetected) { console.log('✓ SSH key (auto-detected)'); } else { console.log('⚠ SSH key not detected - you will need to set it manually'); } console.log('\n📦 Next: Configure infrastructure'); console.log(' Container services:'); console.log(' - celilo service add proxmox'); console.log(' - celilo service add digitalocean'); console.log(' Existing hardware:'); console.log(' - celilo machine add'); if (!sshKeyDetected) { console.log('\n⚠ SSH key required:'); console.log(' celilo system config set ssh.public_key "$(cat ~/.ssh/id_ed25519.pub)"'); } console.log('\nSystem initialization complete!'); return { success: true, message: 'System initialized with defaults', }; } /** * Initialize system with interactive prompts, skipping any keys already * provided as command-line key=value overrides. */ async function initInteractive(cliOverrides: Record = {}): Promise { const db = getDb(); const schemaDefaults = getDefaultConfiguration(); const existing = loadExistingConfiguration(db); // Use existing config as defaults, fall back to schema defaults const defaults: Record = {}; for (const [key, value] of Object.entries(schemaDefaults)) { defaults[key] = value; } for (const [key, value] of Object.entries(existing)) { defaults[key] = value; } // Start with CLI overrides already applied const overrides: Record = { ...cliOverrides }; // Helper: skip prompt if value already provided via CLI const has = (key: string) => key in cliOverrides; // Every prompt below is a bus interview (ISS-0127), so `system init` is // drivable headlessly via `celilo events respond --values` / `events reply`. // `withInterviewSession` renders bus questions locally when stdin is a TTY. const scope = 'system-init'; return withInterviewSession(async () => { await celiloIntro('🎛️ Welcome to Celilo System Setup'); // Network and DNS addressing are intentionally NOT prompted here. // Network topology is no longer owned by `system init` // (openspec/specs/progressive-zone-disclosure/spec.md): the `internal` zone and DNS are // discovered when celilo-mgmt is deployed, and `dmz`/`app`/`secure` // come from a firewall module. The only thing left to capture // interactively is the SSH key celilo uses to reach managed machines. // SSH Key — skip if provided via CLI if (!has('ssh.public_key')) { const detectedKeys = autoDetectSSHKeys(); const existingSshKey = defaults['ssh.public_key']; // Fresh box, no key in ~/.ssh and no previously-saved key — point the // user at ssh-keygen rather than making them paste at a blank prompt. // They can also skip this branch by passing ssh.public_key=... on the CLI. if (detectedKeys.length === 0 && !existingSshKey) { console.log('No SSH key found in ~/.ssh/'); console.log(); console.log('Celilo uses an SSH key to reach managed machines'); console.log('(Proxmox hosts, VPS, Raspberry Pi, etc.). Generate one:'); console.log(); console.log(' ssh-keygen -t ed25519 -C ""'); console.log(); console.log('Then re-run: celilo system init'); console.log(); console.log('Or, to paste a key from elsewhere without generating one:'); console.log(' celilo system init ssh.public_key="ssh-ed25519 AAAA..."'); return { success: false, error: 'No SSH key available — run ssh-keygen first' }; } let keySelected = false; if (detectedKeys.length === 1) { // Single key: confirm const key = detectedKeys[0]; const keyPreview = `${key.keyType} ...${key.content.slice(-20)}`; const useDetected = await askConfirm({ scope, key: 'ssh_use_detected', message: `Auto-detected SSH key (${key.filename}: ${keyPreview}) - Use it?`, defaultValue: true, }); if (useDetected) { overrides['ssh.public_key'] = key.content; keySelected = true; } } else if (detectedKeys.length > 1) { // Multiple keys: selection menu const PASTE_OPTION = '__paste__'; const selected = await askSelect({ scope, key: 'ssh_key_choice', message: `Found ${detectedKeys.length} SSH keys in ~/.ssh/`, options: [ ...detectedKeys.map((key) => ({ value: key.content, label: key.filename, hint: `${key.keyType} ...${key.content.slice(-20)}`, })), { value: PASTE_OPTION, label: 'Paste a different key' }, ], }); if (selected !== PASTE_OPTION) { overrides['ssh.public_key'] = selected; keySelected = true; } } // Manual prompt if no key selected from detection if (!keySelected) { const entered = await askText({ scope, key: 'ssh_public_key', message: existingSshKey ? 'SSH public key (press Enter to keep existing)' : 'SSH public key', defaultValue: existingSshKey ? String(existingSshKey) : undefined, // String(undefined) === 'undefined' (truthy), so guard explicitly — // otherwise the prompt shows "undefined" as the placeholder text. placeholder: existingSshKey ? String(existingSshKey) : 'ssh-ed25519 AAAA...', required: !existingSshKey, }); overrides['ssh.public_key'] = entered || existingSshKey; } else if (existingSshKey) { const entered = await askText({ scope, key: 'ssh_public_key', message: 'SSH public key (press Enter to keep existing)', defaultValue: String(existingSshKey), placeholder: String(existingSshKey), }); overrides['ssh.public_key'] = entered || existingSshKey; } } // Apply configuration (called for its DB side effects). initializeSystem(db, overrides); await celiloOutro('✅ System initialization complete!'); console.log('\nNext steps:'); console.log(' 1. Configure infrastructure:'); console.log(' Container services:'); console.log(' - celilo service add proxmox'); console.log(' - celilo service add digitalocean'); console.log(' Existing hardware:'); console.log(' - celilo machine add'); console.log(''); console.log(' 2. Import a module: celilo module import '); console.log(' 3. Configure module: celilo module config set '); console.log(' 4. Generate infra: celilo module generate '); return { success: true, message: 'System initialized successfully', }; }); }