@celilo/cli 0.3.22 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -218,8 +218,23 @@ function buildOps(registry: RegistryClient): OrchestratorOps {
218
218
  return { status: r.status, detail: r.error };
219
219
  },
220
220
  snapshotCeliloDb: async (_updateId) => {
221
- const result = await createSystemStateBackup();
222
- return result.success ? { ok: true } : { ok: false, error: result.error };
221
+ // resolveStorage throws when no default backup storage is configured
222
+ // (or it isn't verified). Catch here so the orchestrator gets a
223
+ // clean { ok: false, error } shape — the alternative is the throw
224
+ // escapes runSystemUpdate and surfaces as an unhandled stack trace.
225
+ // The handleSystemUpdate pre-flight already catches the common
226
+ // "no storage configured" case with a friendly message; this is
227
+ // a belt-and-suspenders for any other throw path resolveStorage
228
+ // takes (e.g. storage exists but isn't verified).
229
+ try {
230
+ const result = await createSystemStateBackup();
231
+ return result.success ? { ok: true } : { ok: false, error: result.error };
232
+ } catch (err) {
233
+ return {
234
+ ok: false,
235
+ error: err instanceof Error ? err.message : String(err),
236
+ };
237
+ }
223
238
  },
224
239
  };
225
240
  }
@@ -233,15 +248,45 @@ function formatResult(result: SystemUpdateResult): string {
233
248
  ` self-update: ${result.selfUpdate.performed ? `${result.selfUpdate.from} → ${result.selfUpdate.to}` : `(${result.selfUpdate.reason})`}`,
234
249
  );
235
250
  lines.push(` backups: ${result.backupsCreated ? 'created' : 'skipped'}`);
236
- lines.push('');
237
- for (const m of result.modules) {
238
- const tag =
239
- m.step === 'done' ? '✓' : m.step === 'failed' ? '✗' : m.step === 'skipped' ? '↳' : '?';
240
- const change =
241
- m.fromVersion === m.toVersion ? m.fromVersion : `${m.fromVersion} ${m.toVersion}`;
242
- lines.push(` ${tag} ${m.moduleId} (${change}) ${m.step}`);
243
- if (m.error) lines.push(` ${m.error}`);
244
- if (m.skipReason) lines.push(` ${m.skipReason}`);
251
+
252
+ // Surface audit findings inline. DRIFT means "something is drifting
253
+ // but it didn't block the update" — without listing the findings the
254
+ // operator has no idea what; they'd have to run `celilo system audit`
255
+ // as a follow-up. Show them here so it's a single round-trip.
256
+ // BLOCKED also gets the listing (the orchestrator already short-
257
+ // circuited to skip the run, but the operator still needs to know
258
+ // why — formatResult is the friendly path for that too).
259
+ if (result.audit.findings.length > 0) {
260
+ lines.push('');
261
+ const drift = result.audit.findings.filter((f) => f.severity === 'drift');
262
+ const blocked = result.audit.findings.filter((f) => f.severity === 'blocked');
263
+ if (blocked.length > 0) {
264
+ lines.push(` BLOCKED (${blocked.length}):`);
265
+ for (const f of blocked) {
266
+ lines.push(` ✗ [${f.category}] ${f.subject}: ${f.message}`);
267
+ if (f.remediation) lines.push(` → ${f.remediation}`);
268
+ }
269
+ }
270
+ if (drift.length > 0) {
271
+ lines.push(` Drift findings (${drift.length}, informational):`);
272
+ for (const f of drift) {
273
+ lines.push(` ▸ [${f.category}] ${f.subject}: ${f.message}`);
274
+ if (f.remediation) lines.push(` → ${f.remediation}`);
275
+ }
276
+ }
277
+ }
278
+
279
+ if (result.modules.length > 0) {
280
+ lines.push('');
281
+ for (const m of result.modules) {
282
+ const tag =
283
+ m.step === 'done' ? '✓' : m.step === 'failed' ? '✗' : m.step === 'skipped' ? '↳' : '?';
284
+ const change =
285
+ m.fromVersion === m.toVersion ? m.fromVersion : `${m.fromVersion} → ${m.toVersion}`;
286
+ lines.push(` ${tag} ${m.moduleId} (${change}) — ${m.step}`);
287
+ if (m.error) lines.push(` ${m.error}`);
288
+ if (m.skipReason) lines.push(` ${m.skipReason}`);
289
+ }
245
290
  }
246
291
  return lines.join('\n');
247
292
  }
@@ -394,6 +439,49 @@ export async function handleSystemUpdate(
394
439
  };
395
440
  }
396
441
 
442
+ // Decide whether the celilo-db snapshot is even needed for this run.
443
+ // If nothing's changing at the module level, there's nothing to roll
444
+ // back to — taking a snapshot would be pointless work, and on a fresh
445
+ // box (no storage configured yet) it would actively fail. Treat the
446
+ // "nothing-to-update" case as implicit --no-backup.
447
+ const hasModuleUpdates = [...snapshots.values()].some(
448
+ (s) => s.latestVersion && s.latestVersion !== s.installedVersion,
449
+ );
450
+ const effectiveNoBackup = noBackup || !hasModuleUpdates;
451
+
452
+ // Pre-flight the storage check so a missing default doesn't reach the
453
+ // orchestrator's snapshot hook (where the throw would surface as a
454
+ // hostile stack trace). Skip when we're already not going to backup.
455
+ if (!effectiveNoBackup) {
456
+ const { getDefaultBackupStorage } = await import('../../services/backup-storage');
457
+ const storage = getDefaultBackupStorage();
458
+ if (!storage) {
459
+ return {
460
+ success: false,
461
+ error: `No default backup storage configured.
462
+
463
+ celilo system update snapshots the celilo DB before applying module
464
+ updates as a safety net. Configure backup storage first:
465
+
466
+ celilo storage add local
467
+
468
+ Or skip the safety net entirely (the CLI self-update still runs):
469
+
470
+ celilo system update --no-backup`,
471
+ };
472
+ }
473
+ if (!storage.verified) {
474
+ return {
475
+ success: false,
476
+ error: `Default backup storage '${storage.storageId}' is not verified.
477
+
478
+ Run: celilo storage verify ${storage.storageId}
479
+
480
+ Then re-run system update.`,
481
+ };
482
+ }
483
+ }
484
+
397
485
  const ops = buildOps(registry);
398
486
 
399
487
  const result = await runSystemUpdate({
@@ -423,7 +511,7 @@ export async function handleSystemUpdate(
423
511
  },
424
512
  },
425
513
  progress: { emit() {} },
426
- noBackup,
514
+ noBackup: effectiveNoBackup,
427
515
  allowDestructive,
428
516
  onlyModule,
429
517
  });