/** * Backup Create Command * Creates backups of system state and/or module data. */ import { createModuleBackup, createSystemStateBackup, findBackupEligibleModules, isBackupDue, } from '../../services/backup-create'; import { formatSize } from '../../services/backup-metadata'; import { effectiveBackupRetention, pruneBackupsForModule, prunesNothing, } from '../../services/backup-retention'; import { BACKUP_SCHEDULE_CONFIG_KEY, effectiveBackupSchedule, } from '../../services/backup-schedule'; import { formatCadence } from '../../services/cadence'; import { configOverride } from '../../services/module-config'; import { celiloIntro, celiloOutro } from '../prompts'; import type { CommandResult } from '../types'; export async function handleBackupCreate( args: string[], flags: Record = {}, ): Promise { try { celiloIntro('Create Backup'); const specificModule = args[0]; const force = Boolean(flags.force || flags.now); const storageId = typeof flags.storage === 'string' ? flags.storage : undefined; let moduleBackupCount = 0; let moduleSkipCount = 0; let moduleFailCount = 0; if (specificModule) { // Back up a specific module console.log(`\n▸ Backing up ${specificModule}...`); const result = await createModuleBackup(specificModule, { storageId, force }); if (!result.success) { console.log(`✗ ${specificModule} backup failed: ${result.error}`); return { success: false, error: `Backup failed: ${result.error}` }; } console.log(`✓ ${specificModule} backed up (${formatSize(result.sizeBytes ?? 0)})`); console.log(` → ${result.storagePath}`); moduleBackupCount = 1; } else { // Back up system state + all due modules console.log('\n▸ Backing up system state...'); const sysResult = await createSystemStateBackup({ storageId }); if (!sysResult.success) { console.log(`✗ System state backup failed: ${sysResult.error}`); return { success: false, error: `Backup failed: ${sysResult.error}` }; } console.log(`✓ System state backed up (${formatSize(sysResult.sizeBytes ?? 0)})`); // Find and back up eligible modules const eligible = findBackupEligibleModules(); if (eligible.length > 0) { console.log('\nChecking module backup schedules...'); for (const { module: mod, manifest, configs } of eligible) { // Through the shared accessor, like every other reader. This site // used to read the manifest directly AND default an absent cadence // to `manual` — the opposite of what the accessor has always said — // so `celilo backup create` quietly skipped modules the hourly sweep // was backing up daily. const schedule = effectiveBackupSchedule( manifest, configOverride(configs, BACKUP_SCHEDULE_CONFIG_KEY), ); const due = force || isBackupDue(mod.id, schedule); if (!due) { console.log(` ${mod.id}: skipping (not due, schedule: ${formatCadence(schedule)})`); moduleSkipCount++; continue; } console.log(`\n▸ Backing up ${mod.id}...`); const result = await createModuleBackup(mod.id, { storageId }); if (!result.success) { console.log(`✗ ${mod.id} backup failed: ${result.error}`); moduleFailCount++; continue; } console.log(`✓ ${mod.id} backed up (${formatSize(result.sizeBytes ?? 0)})`); console.log(` → ${result.storagePath}`); moduleBackupCount++; // Auto-prune per the effective retention policy const policy = effectiveBackupRetention(manifest, configs); if (!prunesNothing(policy)) { const pruneResult = await pruneBackupsForModule(mod.id, policy); if (pruneResult.deleted > 0) { console.log(` ✓ Pruned ${pruneResult.deleted} old backup(s)`); } } } } // List modules without backup hooks const db = (await import('../../db/client')).getDb(); const { modules: modulesTable } = await import('../../db/schema'); const allInstalled = db .select() .from(modulesTable) .all() .filter((m) => m.state === 'INSTALLED' || m.state === 'VERIFIED'); const noHookModules = allInstalled.filter((m) => !eligible.some((e) => e.module.id === m.id)); if (noHookModules.length > 0) { console.log('\nModules without backup facilities:'); for (const mod of noHookModules) { console.log(` - ${mod.id}: no on_backup hook defined`); } } } // Summary const parts: string[] = []; if (moduleBackupCount > 0) { parts.push(`${moduleBackupCount} module${moduleBackupCount === 1 ? '' : 's'} backed up`); } if (moduleSkipCount > 0) { parts.push(`${moduleSkipCount} skipped (not due)`); } if (moduleFailCount > 0) { parts.push(`${moduleFailCount} failed`); } celiloOutro(parts.length > 0 ? `Backup complete: ${parts.join(', ')}.` : 'Backup complete.'); if (moduleFailCount > 0) { return { success: false, error: `${moduleFailCount} module backup(s) failed` }; } return { success: true, message: 'Backup complete' }; } catch (error) { return { success: false, error: `Backup failed: ${error instanceof Error ? error.message : String(error)}`, }; } }