@celilo/cli 0.3.21 → 0.3.23
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
|
@@ -108,7 +108,7 @@ export function classifyVersionChange(installed: string, latest: string): Versio
|
|
|
108
108
|
* the standard upgradeOne path against it. Cleans the temp file in a
|
|
109
109
|
* finally block so a mid-flight failure doesn't leak a tar.zst on disk.
|
|
110
110
|
*/
|
|
111
|
-
async function fetchAndUpgrade(
|
|
111
|
+
export async function fetchAndUpgrade(
|
|
112
112
|
client: RegistryClient,
|
|
113
113
|
moduleId: string,
|
|
114
114
|
version: string,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* lives in `services/update/orchestrator.ts`.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
18
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
19
20
|
import { dirname, join } from 'node:path';
|
|
20
21
|
import { fileURLToPath } from 'node:url';
|
|
@@ -38,6 +39,16 @@ import {
|
|
|
38
39
|
import type { SystemUpdateResult } from '../../services/update/types';
|
|
39
40
|
import { getFlag, hasFlag } from '../parser';
|
|
40
41
|
import type { CommandResult } from '../types';
|
|
42
|
+
import { fetchAndUpgrade } from './module-upgrade';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Packages we manage via the global bun install. The self-update step
|
|
46
|
+
* runs `bun update -g` against all of these in one shot, so operators
|
|
47
|
+
* never have to remember the three-package incantation.
|
|
48
|
+
*
|
|
49
|
+
* Ordering matters cosmetically only — bun resolves them all together.
|
|
50
|
+
*/
|
|
51
|
+
const MANAGED_PACKAGES = ['@celilo/cli', '@celilo/event-bus', '@celilo/e2e'] as const;
|
|
41
52
|
|
|
42
53
|
function readInstalledCliVersion(): string {
|
|
43
54
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -141,16 +152,14 @@ async function buildSnapshots(
|
|
|
141
152
|
*
|
|
142
153
|
* - `backup` calls `createModuleBackup`. Modules without an
|
|
143
154
|
* `on_backup` hook return ok (nothing to back up; not an error).
|
|
144
|
-
* - `upgrade`
|
|
145
|
-
* in `
|
|
146
|
-
*
|
|
147
|
-
* against whatever's installed, which is the right behavior for
|
|
148
|
-
* "redeploy what's there."
|
|
155
|
+
* - `upgrade` fetches the latest version from the registry and runs
|
|
156
|
+
* the in-place upgrade (`fetchAndUpgrade` from module-upgrade.ts).
|
|
157
|
+
* Same code path that `module update` uses for its sweep mode.
|
|
149
158
|
* - `deploy` calls `deployModule`.
|
|
150
159
|
* - `health` calls `runModuleHealthCheck`.
|
|
151
160
|
* - `snapshotCeliloDb` calls `createSystemStateBackup`.
|
|
152
161
|
*/
|
|
153
|
-
function buildOps(): OrchestratorOps {
|
|
162
|
+
function buildOps(registry: RegistryClient): OrchestratorOps {
|
|
154
163
|
return {
|
|
155
164
|
backup: async (moduleId, _updateId) => {
|
|
156
165
|
const db = getDb();
|
|
@@ -163,7 +172,35 @@ function buildOps(): OrchestratorOps {
|
|
|
163
172
|
const result = await createModuleBackup(moduleId);
|
|
164
173
|
return result.success ? { ok: true } : { ok: false, error: result.error };
|
|
165
174
|
},
|
|
166
|
-
|
|
175
|
+
// Fetch the latest version from the registry and run the in-place
|
|
176
|
+
// upgrade (file-copy + DB update + capability re-register, preserving
|
|
177
|
+
// configs/secrets/infra). Reuses the same path `module update` runs
|
|
178
|
+
// for its sweep mode, so behavior is consistent regardless of which
|
|
179
|
+
// entry point the operator uses.
|
|
180
|
+
upgrade: async (moduleId) => {
|
|
181
|
+
const db = getDb();
|
|
182
|
+
try {
|
|
183
|
+
const entries = await registry.getIndex(moduleId);
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
return { ok: false, error: `${moduleId}: not in registry` };
|
|
186
|
+
}
|
|
187
|
+
const latest = registry.latestVersion(entries);
|
|
188
|
+
if (!latest) {
|
|
189
|
+
return { ok: false, error: `${moduleId}: no non-yanked version` };
|
|
190
|
+
}
|
|
191
|
+
const result = await fetchAndUpgrade(registry, moduleId, latest.vers, db, {});
|
|
192
|
+
if (result.status === 'success') return { ok: true };
|
|
193
|
+
if (result.status === 'failed') return { ok: false, error: result.error };
|
|
194
|
+
// 'skipped' isn't expected here (the module IS installed), but
|
|
195
|
+
// surface it as a non-fatal so the orchestrator can decide.
|
|
196
|
+
return { ok: false, error: `unexpected skip: ${result.reason}` };
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
error: err instanceof Error ? err.message : String(err),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
},
|
|
167
204
|
deploy: async (id) => {
|
|
168
205
|
const db = getDb();
|
|
169
206
|
// The deploy interview runs through the bus; if config is
|
|
@@ -181,8 +218,23 @@ function buildOps(): OrchestratorOps {
|
|
|
181
218
|
return { status: r.status, detail: r.error };
|
|
182
219
|
},
|
|
183
220
|
snapshotCeliloDb: async (_updateId) => {
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
238
|
},
|
|
187
239
|
};
|
|
188
240
|
}
|
|
@@ -357,7 +409,50 @@ export async function handleSystemUpdate(
|
|
|
357
409
|
};
|
|
358
410
|
}
|
|
359
411
|
|
|
360
|
-
|
|
412
|
+
// Decide whether the celilo-db snapshot is even needed for this run.
|
|
413
|
+
// If nothing's changing at the module level, there's nothing to roll
|
|
414
|
+
// back to — taking a snapshot would be pointless work, and on a fresh
|
|
415
|
+
// box (no storage configured yet) it would actively fail. Treat the
|
|
416
|
+
// "nothing-to-update" case as implicit --no-backup.
|
|
417
|
+
const hasModuleUpdates = [...snapshots.values()].some(
|
|
418
|
+
(s) => s.latestVersion && s.latestVersion !== s.installedVersion,
|
|
419
|
+
);
|
|
420
|
+
const effectiveNoBackup = noBackup || !hasModuleUpdates;
|
|
421
|
+
|
|
422
|
+
// Pre-flight the storage check so a missing default doesn't reach the
|
|
423
|
+
// orchestrator's snapshot hook (where the throw would surface as a
|
|
424
|
+
// hostile stack trace). Skip when we're already not going to backup.
|
|
425
|
+
if (!effectiveNoBackup) {
|
|
426
|
+
const { getDefaultBackupStorage } = await import('../../services/backup-storage');
|
|
427
|
+
const storage = getDefaultBackupStorage();
|
|
428
|
+
if (!storage) {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: `No default backup storage configured.
|
|
432
|
+
|
|
433
|
+
celilo system update snapshots the celilo DB before applying module
|
|
434
|
+
updates as a safety net. Configure backup storage first:
|
|
435
|
+
|
|
436
|
+
celilo storage add local
|
|
437
|
+
|
|
438
|
+
Or skip the safety net entirely (the CLI self-update still runs):
|
|
439
|
+
|
|
440
|
+
celilo system update --no-backup`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (!storage.verified) {
|
|
444
|
+
return {
|
|
445
|
+
success: false,
|
|
446
|
+
error: `Default backup storage '${storage.storageId}' is not verified.
|
|
447
|
+
|
|
448
|
+
Run: celilo storage verify ${storage.storageId}
|
|
449
|
+
|
|
450
|
+
Then re-run system update.`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const ops = buildOps(registry);
|
|
361
456
|
|
|
362
457
|
const result = await runSystemUpdate({
|
|
363
458
|
audit: auditDeps,
|
|
@@ -367,13 +462,26 @@ export async function handleSystemUpdate(
|
|
|
367
462
|
selfUpdate: {
|
|
368
463
|
installedVersion: readInstalledCliVersion(),
|
|
369
464
|
fetcher: fetchLatestCliVersion,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
465
|
+
// Refresh ALL managed @celilo/* packages in one bun-update call.
|
|
466
|
+
// The orchestrator only tracks @celilo/cli's from/to versions,
|
|
467
|
+
// but we sweep event-bus and e2e along with it so operators
|
|
468
|
+
// don't have to type the three-package incantation themselves.
|
|
469
|
+
// Failures from `bun update` propagate as stderr; the orchestrator
|
|
470
|
+
// surfaces them in the run summary.
|
|
471
|
+
updater: async () => {
|
|
472
|
+
const r = spawnSync('bun', ['update', '-g', ...MANAGED_PACKAGES], {
|
|
473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
+
encoding: 'utf-8',
|
|
475
|
+
});
|
|
476
|
+
if (r.status === 0) return { ok: true, stderr: '' };
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
stderr: (r.stderr ?? '').trim() || `bun update -g exited ${r.status}`,
|
|
480
|
+
};
|
|
481
|
+
},
|
|
374
482
|
},
|
|
375
483
|
progress: { emit() {} },
|
|
376
|
-
noBackup,
|
|
484
|
+
noBackup: effectiveNoBackup,
|
|
377
485
|
allowDestructive,
|
|
378
486
|
onlyModule,
|
|
379
487
|
});
|