@celilo/cli 0.3.30 → 0.4.0-alpha.1
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/drizzle/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +6 -5
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
package/src/cli/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
handleEventsRepair,
|
|
22
22
|
handleEventsRespond,
|
|
23
23
|
handleEventsRun,
|
|
24
|
+
handleEventsRunHook,
|
|
24
25
|
handleEventsShowDaemon,
|
|
25
26
|
handleEventsStatus,
|
|
26
27
|
handleEventsTail,
|
|
@@ -62,6 +63,7 @@ import { handleModuleTypesCheck, handleModuleTypesGenerate } from './commands/mo
|
|
|
62
63
|
import { handleModuleUpgrade } from './commands/module-upgrade';
|
|
63
64
|
import { moduleVerify } from './commands/module-verify';
|
|
64
65
|
import { handlePackage } from './commands/package';
|
|
66
|
+
import { main as runPublish } from './commands/publish';
|
|
65
67
|
import { handleSecretList } from './commands/secret-list';
|
|
66
68
|
import { handleSecretSet } from './commands/secret-set';
|
|
67
69
|
import { handleServiceAddDigitalOcean } from './commands/service-add-digitalocean';
|
|
@@ -73,6 +75,13 @@ import { handleServiceReconfigure } from './commands/service-reconfigure';
|
|
|
73
75
|
import { handleServiceRemove } from './commands/service-remove';
|
|
74
76
|
import { handleServiceVerify } from './commands/service-verify';
|
|
75
77
|
import { handleStatus } from './commands/status';
|
|
78
|
+
import { handleSubscribersAdd } from './commands/subscribers-add';
|
|
79
|
+
import { handleSubscribersList } from './commands/subscribers-list';
|
|
80
|
+
import { handleSubscribersRemove } from './commands/subscribers-remove';
|
|
81
|
+
import { handleSubscribersServe } from './commands/subscribers-serve';
|
|
82
|
+
import { handleSubscribersStatus } from './commands/subscribers-status';
|
|
83
|
+
import { handleSubscribersTest } from './commands/subscribers-test';
|
|
84
|
+
import { handleSystemApplyConfig } from './commands/system-apply-config';
|
|
76
85
|
import { handleSystemAudit } from './commands/system-audit';
|
|
77
86
|
import { handleSystemConfigGet, handleSystemConfigSet } from './commands/system-config';
|
|
78
87
|
import { handleSystemDoctor } from './commands/system-doctor';
|
|
@@ -157,9 +166,12 @@ Commands:
|
|
|
157
166
|
service Manage container services (Proxmox, Digital Ocean)
|
|
158
167
|
storage Manage backup storage destinations
|
|
159
168
|
backup Create and manage backups
|
|
169
|
+
restore Restore a celilo-mgmt backup from a local file (fresh-bootstrap path)
|
|
160
170
|
machine Manage machine pool (bring-your-own-hardware)
|
|
161
171
|
system Manage system configuration
|
|
162
172
|
ipam Manage IP address and VMID allocations and reservations
|
|
173
|
+
publish Publish workspace packages to npm and modules to celilo.computer
|
|
174
|
+
subscribers Manage build-bus subscribers (cross-machine publish-event delivery)
|
|
163
175
|
completion Generate shell completion scripts (bash/zsh)
|
|
164
176
|
|
|
165
177
|
help, --help, -h Show this help message
|
|
@@ -271,6 +283,99 @@ Examples:
|
|
|
271
283
|
return { success: true, message: helpText.trim() };
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
function displaySubscribersHelp(): CommandResult {
|
|
287
|
+
const helpText = `
|
|
288
|
+
Celilo - Build-Bus Subscribers
|
|
289
|
+
|
|
290
|
+
Usage:
|
|
291
|
+
celilo subscribers <subcommand> [args...] [options]
|
|
292
|
+
|
|
293
|
+
Subcommands:
|
|
294
|
+
list Show registered subscribers (truncated secret fingerprints)
|
|
295
|
+
add <url> --secret <hmac-secret> Add a subscriber (replaces any with the same URL)
|
|
296
|
+
[--name <label>] [--registry <r>]
|
|
297
|
+
[--tag <t>] [--package-pattern <glob>]
|
|
298
|
+
remove <url> Remove a subscriber
|
|
299
|
+
test <url> Fire a synthetic event at a subscriber; report delivery
|
|
300
|
+
serve --secret <hmac-secret> Run the receiver daemon (verifies + emits to local bus)
|
|
301
|
+
[--port <p>] [--no-dispatch]
|
|
302
|
+
status Per-subscriber delivery history (success rate, recent failures)
|
|
303
|
+
--help, -h Show this help
|
|
304
|
+
|
|
305
|
+
Description:
|
|
306
|
+
Subscribers receive signed webhooks when 'celilo publish' ships a
|
|
307
|
+
new package version. Each subscriber has a per-target HMAC secret,
|
|
308
|
+
a target URL, and optional match filters (registry, dist-tag,
|
|
309
|
+
package name glob). The publisher signs every event with the
|
|
310
|
+
subscriber's secret; the receiver verifies before reacting.
|
|
311
|
+
|
|
312
|
+
This is the static-config first cut of v2/BUILD_BUS.md — operators
|
|
313
|
+
manage subscribers explicitly. Auto-discovery via the celilo
|
|
314
|
+
registry switchboard is a follow-on.
|
|
315
|
+
|
|
316
|
+
Examples:
|
|
317
|
+
celilo subscribers add https://lunacycle.lab/build-bus --secret <hex> \\
|
|
318
|
+
--name "lunacycle build box" --registry npm --package-pattern '@celilo/*'
|
|
319
|
+
celilo subscribers list
|
|
320
|
+
celilo subscribers test https://lunacycle.lab/build-bus
|
|
321
|
+
celilo subscribers remove https://lunacycle.lab/build-bus
|
|
322
|
+
`;
|
|
323
|
+
return { success: true, message: helpText.trim() };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function displayPublishHelp(): CommandResult {
|
|
327
|
+
const helpText = `
|
|
328
|
+
Celilo - Publish Workspace Packages and Modules
|
|
329
|
+
|
|
330
|
+
Usage:
|
|
331
|
+
celilo publish [options]
|
|
332
|
+
|
|
333
|
+
Options:
|
|
334
|
+
--dry-run Preflight report only — no publishing, no file changes
|
|
335
|
+
--release-touch Auto-touch drifted module manifests with a fresh release marker
|
|
336
|
+
--allow-stale Bypass workspace and module stale-version safeguards
|
|
337
|
+
-y, --yes Auto-confirm every prompt (doesn't bypass safety gates)
|
|
338
|
+
--alpha Publish X.Y.Z-alpha.N to npm @alpha dist-tag
|
|
339
|
+
--track-alpha Force-pin just-published alphas into bun global (with --alpha)
|
|
340
|
+
--alpha-modules Also publish module +N revisions in alpha mode (with --alpha)
|
|
341
|
+
--promote <spec> Graduate a named alpha to its base X.Y.Z (e.g. @celilo/e2e@0.7.14-alpha.3)
|
|
342
|
+
--skip-changesets Skip the pending-changesets prompt at the top of normal-mode publishes
|
|
343
|
+
--help, -h Show this help message
|
|
344
|
+
|
|
345
|
+
Description:
|
|
346
|
+
Publishes @celilo/* workspace packages to npm, bumps consumer dependency pins,
|
|
347
|
+
updates the bun global install, and ships modules to celilo.computer. Runs four
|
|
348
|
+
phases: preflight + workspace publish, consumer pin bump, global install update,
|
|
349
|
+
module registry publish.
|
|
350
|
+
|
|
351
|
+
In normal mode, if pending changesets exist under .changeset/ (recorded by
|
|
352
|
+
'bun changeset' during dev), 'celilo publish' applies them via
|
|
353
|
+
'bunx changeset version' before planning. Skipped in alpha + promote modes.
|
|
354
|
+
|
|
355
|
+
--alpha mode publishes pre-release versions under the @alpha dist-tag so consumers
|
|
356
|
+
can opt into iteration via 'bun add @celilo/<pkg>@alpha' without disturbing @latest.
|
|
357
|
+
Auto-skips packages whose source hasn't changed since the prior alpha.
|
|
358
|
+
|
|
359
|
+
--promote graduates a vetted alpha to its real X.Y.Z and runs all four phases.
|
|
360
|
+
|
|
361
|
+
Examples:
|
|
362
|
+
celilo publish # Full publish flow
|
|
363
|
+
celilo publish --dry-run # Show plan, no side effects
|
|
364
|
+
celilo publish --alpha # Ship pre-release alphas
|
|
365
|
+
celilo publish --alpha --track-alpha # Alpha + pin into global install
|
|
366
|
+
celilo publish --promote @celilo/e2e@0.7.14-alpha.3 # Graduate alpha to real release
|
|
367
|
+
celilo publish --skip-changesets # Publish current versions; ignore .changeset/*.md
|
|
368
|
+
|
|
369
|
+
Related Commands:
|
|
370
|
+
bun changeset Record a pending version bump (read by 'celilo publish')
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
message: helpText.trim(),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
274
379
|
function displayCapabilityHelp(): CommandResult {
|
|
275
380
|
const helpText = `
|
|
276
381
|
Celilo - Capability Management
|
|
@@ -499,7 +604,7 @@ Usage:
|
|
|
499
604
|
celilo backup <subcommand> [args...] [options]
|
|
500
605
|
|
|
501
606
|
Subcommands:
|
|
502
|
-
create [module-id] Create a backup
|
|
607
|
+
create [module-id] (DEPRECATED) Create a backup. Use 'celilo module backup' instead.
|
|
503
608
|
Options:
|
|
504
609
|
--force Ignore schedule, back up now
|
|
505
610
|
--storage <id> Use specific storage destination
|
|
@@ -541,11 +646,11 @@ Description:
|
|
|
541
646
|
Use "celilo backup name" to assign human-readable names.
|
|
542
647
|
|
|
543
648
|
Examples:
|
|
544
|
-
# Create a full backup (system + all due modules)
|
|
545
|
-
celilo backup
|
|
649
|
+
# Create a full backup (system + all due modules) — canonical path:
|
|
650
|
+
celilo module backup
|
|
546
651
|
|
|
547
|
-
# Back up a specific module
|
|
548
|
-
celilo backup
|
|
652
|
+
# Back up a specific module — canonical path:
|
|
653
|
+
celilo module backup authentik --force
|
|
549
654
|
|
|
550
655
|
# List recent backups
|
|
551
656
|
celilo backup list
|
|
@@ -617,7 +722,7 @@ Examples:
|
|
|
617
722
|
celilo storage remove local-backups --force
|
|
618
723
|
|
|
619
724
|
Related Commands:
|
|
620
|
-
celilo backup
|
|
725
|
+
celilo module backup <module-id> Create a backup
|
|
621
726
|
celilo backup list List available backups
|
|
622
727
|
celilo backup restore <id> Restore from a backup
|
|
623
728
|
`;
|
|
@@ -780,6 +885,10 @@ Usage:
|
|
|
780
885
|
Subcommands:
|
|
781
886
|
init [--accept-defaults] [key=val...] Initialize system with guided setup or defaults
|
|
782
887
|
|
|
888
|
+
apply-config <key=val>... | --from-stdin
|
|
889
|
+
Headless config write — designed for module
|
|
890
|
+
hooks (no prompts, no guidance, just writes).
|
|
891
|
+
|
|
783
892
|
config set <key> <value> Set system-wide configuration value
|
|
784
893
|
config get [key] Get system configuration value(s)
|
|
785
894
|
|
|
@@ -991,6 +1100,26 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
991
1100
|
};
|
|
992
1101
|
}
|
|
993
1102
|
|
|
1103
|
+
if (parsed.command === 'publish') {
|
|
1104
|
+
if (parsed.flags.help || parsed.flags.h) {
|
|
1105
|
+
return displayPublishHelp();
|
|
1106
|
+
}
|
|
1107
|
+
const pubFlagError = checkFlags('publish', undefined, parsed.flags, parsed.args);
|
|
1108
|
+
if (pubFlagError) return pubFlagError;
|
|
1109
|
+
// Slice from `publish` onwards in the raw argv so the publish entry
|
|
1110
|
+
// sees the operator's exact flag spelling (`-y` vs `--yes`, etc.).
|
|
1111
|
+
// Falling back to args+flags reconstruction would lose short-form
|
|
1112
|
+
// flag identity since the parser canonicalises those.
|
|
1113
|
+
const publishIdx = process.argv.indexOf('publish');
|
|
1114
|
+
const publishArgv = publishIdx >= 0 ? process.argv.slice(publishIdx + 1) : [...parsed.args];
|
|
1115
|
+
await runPublish(publishArgv);
|
|
1116
|
+
// runPublish handles its own console output (multi-phase, multi-line);
|
|
1117
|
+
// returning an empty success message tells the outer CLI loop to skip
|
|
1118
|
+
// the clack outro and exit 0 cleanly. Any failure inside runPublish
|
|
1119
|
+
// calls process.exit() directly and never returns here.
|
|
1120
|
+
return { success: true, message: '' };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
994
1123
|
if (parsed.command === 'events') {
|
|
995
1124
|
if (parsed.flags.help || parsed.flags.h) {
|
|
996
1125
|
return displayEventsHelp();
|
|
@@ -1017,6 +1146,8 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1017
1146
|
return handleEventsDrain(parsed.args, parsed.flags);
|
|
1018
1147
|
case 'run':
|
|
1019
1148
|
return handleEventsRun(parsed.args, parsed.flags);
|
|
1149
|
+
case 'run-hook':
|
|
1150
|
+
return handleEventsRunHook(parsed.args);
|
|
1020
1151
|
case 'emit':
|
|
1021
1152
|
return handleEventsEmit(parsed.args, parsed.flags);
|
|
1022
1153
|
case 'ack':
|
|
@@ -1155,6 +1286,14 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1155
1286
|
case 'validate':
|
|
1156
1287
|
// Alias for `module deploy --preflight`
|
|
1157
1288
|
return handleModuleDeploy(parsed.args, { ...parsed.flags, preflight: true });
|
|
1289
|
+
case 'backup': {
|
|
1290
|
+
// Canonical create-a-module-backup surface, matching the rest of
|
|
1291
|
+
// the module CLI verbs (deploy, health, remove, ...). The legacy
|
|
1292
|
+
// `celilo backup create <module>` still works but prints a
|
|
1293
|
+
// deprecation banner.
|
|
1294
|
+
const { handleBackupCreate } = await import('./commands/backup-create');
|
|
1295
|
+
return handleBackupCreate(parsed.args, parsed.flags);
|
|
1296
|
+
}
|
|
1158
1297
|
case 'run-hook':
|
|
1159
1298
|
return handleHookRun(parsed.args, parsed.flags);
|
|
1160
1299
|
case 'terraform-unlock':
|
|
@@ -1267,6 +1406,41 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1267
1406
|
};
|
|
1268
1407
|
}
|
|
1269
1408
|
|
|
1409
|
+
if (parsed.command === 'subscribers') {
|
|
1410
|
+
if (parsed.flags.help || parsed.flags.h) {
|
|
1411
|
+
return displaySubscribersHelp();
|
|
1412
|
+
}
|
|
1413
|
+
if (!parsed.subcommand) {
|
|
1414
|
+
return {
|
|
1415
|
+
success: false,
|
|
1416
|
+
error:
|
|
1417
|
+
'Subscribers subcommand required (list, add, remove, test).\n\nRun "celilo subscribers --help" for usage.',
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
const subFlagError = checkFlags('subscribers', parsed.subcommand, parsed.flags, parsed.args);
|
|
1421
|
+
if (subFlagError) return subFlagError;
|
|
1422
|
+
|
|
1423
|
+
switch (parsed.subcommand) {
|
|
1424
|
+
case 'list':
|
|
1425
|
+
return handleSubscribersList();
|
|
1426
|
+
case 'add':
|
|
1427
|
+
return handleSubscribersAdd(parsed.args, parsed.flags);
|
|
1428
|
+
case 'remove':
|
|
1429
|
+
return handleSubscribersRemove(parsed.args);
|
|
1430
|
+
case 'test':
|
|
1431
|
+
return handleSubscribersTest(parsed.args);
|
|
1432
|
+
case 'serve':
|
|
1433
|
+
return handleSubscribersServe(parsed.args, parsed.flags);
|
|
1434
|
+
case 'status':
|
|
1435
|
+
return handleSubscribersStatus();
|
|
1436
|
+
default:
|
|
1437
|
+
return {
|
|
1438
|
+
success: false,
|
|
1439
|
+
error: `Unknown subscribers subcommand: ${parsed.subcommand}\n\nRun "celilo subscribers --help" for usage.`,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1270
1444
|
if (parsed.command === 'storage') {
|
|
1271
1445
|
if (parsed.flags.help || parsed.flags.h) {
|
|
1272
1446
|
return displayStorageHelp();
|
|
@@ -1350,6 +1524,17 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1350
1524
|
if (backupFlagError) return backupFlagError;
|
|
1351
1525
|
|
|
1352
1526
|
if (parsed.subcommand === 'create') {
|
|
1527
|
+
// Legacy surface — `celilo module backup <id>` is the canonical
|
|
1528
|
+
// create command now (matches the module-namespaced verbs:
|
|
1529
|
+
// deploy, health, remove). Print a one-line deprecation banner
|
|
1530
|
+
// and forward to the same handler. Suppress via
|
|
1531
|
+
// `CELILO_SUPPRESS_DEPRECATION=1` so tests / cron stay quiet.
|
|
1532
|
+
if (process.env.CELILO_SUPPRESS_DEPRECATION !== '1') {
|
|
1533
|
+
const target = parsed.args[0]
|
|
1534
|
+
? `celilo module backup ${parsed.args[0]}`
|
|
1535
|
+
: 'celilo module backup';
|
|
1536
|
+
console.warn(`⚠ "celilo backup create" is deprecated. Use: ${target}`);
|
|
1537
|
+
}
|
|
1353
1538
|
const { handleBackupCreate } = await import('./commands/backup-create');
|
|
1354
1539
|
return handleBackupCreate(parsed.args, parsed.flags);
|
|
1355
1540
|
}
|
|
@@ -1390,6 +1575,25 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1390
1575
|
};
|
|
1391
1576
|
}
|
|
1392
1577
|
|
|
1578
|
+
if (parsed.command === 'restore') {
|
|
1579
|
+
// Phase 4 of v2/SYSTEM_BACKUP_TERRAFORM_STATE.md — top-level
|
|
1580
|
+
// fresh-bootstrap restore. Distinct from `celilo backup restore
|
|
1581
|
+
// <id>` (which restores from a configured storage destination
|
|
1582
|
+
// using an existing backup record); this one takes a local file
|
|
1583
|
+
// directly so it works before any storage is configured.
|
|
1584
|
+
//
|
|
1585
|
+
// `restore` has no subcommands but the parser eagerly captures
|
|
1586
|
+
// the second positional as `subcommand`. Re-thread it as the
|
|
1587
|
+
// first arg so `celilo restore <file>` works alongside
|
|
1588
|
+
// `celilo restore --from <file>`.
|
|
1589
|
+
const restoreArgs = parsed.subcommand ? [parsed.subcommand, ...parsed.args] : parsed.args;
|
|
1590
|
+
const restoreFlagError = checkFlags('restore', undefined, parsed.flags, restoreArgs);
|
|
1591
|
+
if (restoreFlagError) return restoreFlagError;
|
|
1592
|
+
|
|
1593
|
+
const { handleRestore } = await import('./commands/restore');
|
|
1594
|
+
return handleRestore(restoreArgs, parsed.flags);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1393
1597
|
if (parsed.command === 'machine') {
|
|
1394
1598
|
// Handle machine --help
|
|
1395
1599
|
if (parsed.flags.help || parsed.flags.h) {
|
|
@@ -1452,6 +1656,10 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
|
|
|
1452
1656
|
return handleSystemInit(parsed.args, parsed.flags);
|
|
1453
1657
|
}
|
|
1454
1658
|
|
|
1659
|
+
if (parsed.subcommand === 'apply-config') {
|
|
1660
|
+
return handleSystemApplyConfig(parsed.args, parsed.flags);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1455
1663
|
if (parsed.subcommand === 'config') {
|
|
1456
1664
|
const configSubcommand = parsed.args[0];
|
|
1457
1665
|
if (!configSubcommand) {
|
|
@@ -23,11 +23,30 @@ const celiloIntroMock = mock(() => undefined);
|
|
|
23
23
|
// Install module mock BEFORE dynamically importing the SUT.
|
|
24
24
|
// bun:test's mock.module is a runtime call (no hoisting), so the order
|
|
25
25
|
// matters: anything that imports './prompts' AFTER this call gets the mock.
|
|
26
|
+
//
|
|
27
|
+
// IMPORTANT: mock.module replaces the module PROCESS-GLOBALLY for the
|
|
28
|
+
// remainder of the test run. Every export from the real module needs a
|
|
29
|
+
// stub here, or test files loaded after this one will fail with
|
|
30
|
+
// `SyntaxError: Export named 'X' not found in module 'prompts.ts'` when
|
|
31
|
+
// they try to import a missing one. The mocks below replace what this
|
|
32
|
+
// test actively asserts on; the no-op stubs satisfy other modules that
|
|
33
|
+
// import from prompts.ts just to use them as side-effects.
|
|
26
34
|
mock.module('./prompts', () => ({
|
|
27
35
|
celiloIntro: celiloIntroMock,
|
|
36
|
+
celiloOutro: async () => undefined,
|
|
28
37
|
promptText: promptTextMock,
|
|
29
38
|
promptPassword: promptPasswordMock,
|
|
30
39
|
promptConfirm: promptConfirmMock,
|
|
40
|
+
showNote: () => undefined,
|
|
41
|
+
getActiveDisplay: () => undefined,
|
|
42
|
+
setActiveDisplay: () => undefined,
|
|
43
|
+
log: {
|
|
44
|
+
success: () => undefined,
|
|
45
|
+
error: () => undefined,
|
|
46
|
+
warn: () => undefined,
|
|
47
|
+
info: () => undefined,
|
|
48
|
+
message: () => undefined,
|
|
49
|
+
},
|
|
31
50
|
}));
|
|
32
51
|
|
|
33
52
|
// Dynamic import after the mock is installed.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `celilo restore` top-level command (Phase 4 of
|
|
3
|
+
* v2/SYSTEM_BACKUP_TERRAFORM_STATE.md).
|
|
4
|
+
*
|
|
5
|
+
* Covers the CLI surface: routing, argument parsing, pre-flight
|
|
6
|
+
* gating, error propagation. The actual file-direct restore +
|
|
7
|
+
* system-file swap logic is tested in restore-from-file.test.ts;
|
|
8
|
+
* here we focus on what the user-facing command does (and refuses
|
|
9
|
+
* to do).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
13
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { closeDb, getDb } from '../db/client';
|
|
17
|
+
import { runMigrations } from '../db/migrate';
|
|
18
|
+
import { modules } from '../db/schema';
|
|
19
|
+
import { runCli } from './index';
|
|
20
|
+
|
|
21
|
+
describe('celilo restore', () => {
|
|
22
|
+
let dir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
dir = mkdtempSync(join(tmpdir(), 'celilo-restore-cmd-test-'));
|
|
26
|
+
process.env.CELILO_DB_PATH = join(dir, 'celilo.db');
|
|
27
|
+
process.env.CELILO_DATA_DIR = dir;
|
|
28
|
+
process.env.CELILO_MASTER_KEY_PATH = join(dir, 'master.key');
|
|
29
|
+
process.env.CELILO_SUPPRESS_DEPRECATION = '1';
|
|
30
|
+
await runMigrations(process.env.CELILO_DB_PATH);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
closeDb();
|
|
35
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
36
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
37
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
38
|
+
process.env.CELILO_SUPPRESS_DEPRECATION = undefined;
|
|
39
|
+
try {
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('errors when --from is missing', async () => {
|
|
47
|
+
const result = await runCli(['node', 'celilo', 'restore']);
|
|
48
|
+
expect(result.success).toBe(false);
|
|
49
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
50
|
+
expect(err).toContain('--from');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('errors when --from points at a non-existent file', async () => {
|
|
54
|
+
const result = await runCli([
|
|
55
|
+
'node',
|
|
56
|
+
'celilo',
|
|
57
|
+
'restore',
|
|
58
|
+
'--from',
|
|
59
|
+
'/tmp/does-not-exist-celilo-test.backup',
|
|
60
|
+
]);
|
|
61
|
+
expect(result.success).toBe(false);
|
|
62
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
63
|
+
expect(err).toContain('not found');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('refuses when target DB has modules (non-empty)', async () => {
|
|
67
|
+
// Plant a fake module to simulate a populated target.
|
|
68
|
+
const db = getDb();
|
|
69
|
+
db.insert(modules)
|
|
70
|
+
.values({
|
|
71
|
+
id: 'fake-existing-mod',
|
|
72
|
+
name: 'fake',
|
|
73
|
+
version: '0.0.1',
|
|
74
|
+
sourcePath: '/tmp/fake',
|
|
75
|
+
manifestData: {} as Record<string, unknown>,
|
|
76
|
+
})
|
|
77
|
+
.run();
|
|
78
|
+
|
|
79
|
+
// Plant a phony artifact file so the path check passes.
|
|
80
|
+
const phonyArtifact = join(dir, 'phony.backup');
|
|
81
|
+
writeFileSync(phonyArtifact, '{}');
|
|
82
|
+
|
|
83
|
+
const result = await runCli(['node', 'celilo', 'restore', '--from', phonyArtifact]);
|
|
84
|
+
expect(result.success).toBe(false);
|
|
85
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
86
|
+
expect(err).toContain('not empty');
|
|
87
|
+
expect(err).toContain('fake-existing-mod');
|
|
88
|
+
expect(err).toContain('--force');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('refuses when target has terraform state on disk (non-empty)', async () => {
|
|
92
|
+
const tfDir = join(dir, 'modules', 'half-deployed', 'generated', 'terraform');
|
|
93
|
+
mkdirSync(tfDir, { recursive: true });
|
|
94
|
+
writeFileSync(join(tfDir, 'terraform.tfstate'), '{}');
|
|
95
|
+
|
|
96
|
+
const phonyArtifact = join(dir, 'phony.backup');
|
|
97
|
+
writeFileSync(phonyArtifact, '{}');
|
|
98
|
+
|
|
99
|
+
const result = await runCli(['node', 'celilo', 'restore', '--from', phonyArtifact]);
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
102
|
+
expect(err).toContain('not empty');
|
|
103
|
+
expect(err).toContain('half-deployed');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('routes to handleRestore when --from is provided (errors at decrypt of non-artifact)', async () => {
|
|
107
|
+
// No state in DB or disk, but the file isn't a real artifact —
|
|
108
|
+
// we expect to get PAST pre-flight and fail at the
|
|
109
|
+
// decrypt/manifest stage. That proves routing is wired up.
|
|
110
|
+
const notAnArtifact = join(dir, 'random.backup');
|
|
111
|
+
writeFileSync(notAnArtifact, 'this is not encrypted JSON');
|
|
112
|
+
|
|
113
|
+
const result = await runCli(['node', 'celilo', 'restore', '--from', notAnArtifact]);
|
|
114
|
+
expect(result.success).toBe(false);
|
|
115
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
116
|
+
// Pre-flight passed (no mention of "not empty"); decryption failed.
|
|
117
|
+
expect(err).not.toContain('not empty');
|
|
118
|
+
expect(err.toLowerCase()).toMatch(/json|decrypt|artifact/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('accepts artifact path as positional too (not just --from)', async () => {
|
|
122
|
+
const notAnArtifact = join(dir, 'random2.backup');
|
|
123
|
+
writeFileSync(notAnArtifact, 'still not a real artifact');
|
|
124
|
+
|
|
125
|
+
const result = await runCli(['node', 'celilo', 'restore', notAnArtifact]);
|
|
126
|
+
expect(result.success).toBe(false);
|
|
127
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
128
|
+
// Reached the decrypt stage = positional arg was honored.
|
|
129
|
+
expect(err).not.toContain('missing --from');
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/db/client.ts
CHANGED
|
@@ -92,6 +92,23 @@ function needsMigration(sqlite: Database): boolean {
|
|
|
92
92
|
)`,
|
|
93
93
|
// Backup naming support
|
|
94
94
|
'ALTER TABLE backups ADD name text',
|
|
95
|
+
// Module systems (v2/MODULE_SYSTEMS_ADDRESSING.md) — a module's 0..N
|
|
96
|
+
// deployed hosts. Fresh DBs get this via migration 0007; existing installs
|
|
97
|
+
// get it here. Replaces the scalar target_ip/vmid rows in module_configs.
|
|
98
|
+
`CREATE TABLE IF NOT EXISTS module_systems (
|
|
99
|
+
module_id text NOT NULL REFERENCES modules(id) ON DELETE cascade,
|
|
100
|
+
name text NOT NULL,
|
|
101
|
+
hostname text NOT NULL,
|
|
102
|
+
ipv4_address text NOT NULL,
|
|
103
|
+
zone text NOT NULL,
|
|
104
|
+
infra_type text NOT NULL,
|
|
105
|
+
machine_id text REFERENCES machines(id),
|
|
106
|
+
service_id text REFERENCES container_services(id),
|
|
107
|
+
vmid integer,
|
|
108
|
+
created_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
109
|
+
updated_at integer DEFAULT (unixepoch()) NOT NULL,
|
|
110
|
+
PRIMARY KEY (module_id, name)
|
|
111
|
+
)`,
|
|
95
112
|
];
|
|
96
113
|
|
|
97
114
|
for (const stmt of alterStatements) {
|
|
@@ -200,6 +217,31 @@ export function createDbClient(config?: Partial<DatabaseConfig>) {
|
|
|
200
217
|
}
|
|
201
218
|
}
|
|
202
219
|
|
|
220
|
+
// One-time upgrade backfill for the target_ip → module_systems refactor
|
|
221
|
+
// (v2/MODULE_SYSTEMS_ADDRESSING.md). Runs for both paths above: needsMigration
|
|
222
|
+
// has already ensured the module_systems table exists (via migration 0007 for
|
|
223
|
+
// a fresh DB, or the imperative CREATE for an existing install). A deployment
|
|
224
|
+
// created before the refactor has its host data only in module_configs /
|
|
225
|
+
// ip_allocations / module_infrastructure and an EMPTY module_systems, so its
|
|
226
|
+
// modules resolve to no system and the migrated hooks throw "No deployed
|
|
227
|
+
// system found". This lifts that state across. Idempotent (skips modules
|
|
228
|
+
// already recorded), so a no-op on a fresh DB and on every steady-state open.
|
|
229
|
+
// Logged so the upgrade is operator-visible, not silent magic.
|
|
230
|
+
if (!readonly) {
|
|
231
|
+
try {
|
|
232
|
+
const { backfillModuleSystems } = require('../services/deployed-systems');
|
|
233
|
+
const filled = backfillModuleSystems(db) as string[];
|
|
234
|
+
if (filled.length > 0) {
|
|
235
|
+
console.log(
|
|
236
|
+
`Backfilled module_systems for ${filled.length} module(s): ${filled.join(', ')}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Failed to backfill module_systems:', error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
203
245
|
return db;
|
|
204
246
|
}
|
|
205
247
|
|
package/src/db/schema.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { unlink } from 'node:fs/promises';
|
|
4
|
-
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { and, eq } from 'drizzle-orm';
|
|
5
|
+
import { upsertModuleConfig } from '../services/module-config';
|
|
5
6
|
import { type DbClient, createDbClient } from './client';
|
|
6
7
|
import { capabilities, moduleConfigs, modules, secrets } from './schema';
|
|
7
|
-
import type { NewCapability, NewModule,
|
|
8
|
+
import type { NewCapability, NewModule, NewSecret } from './schema';
|
|
8
9
|
|
|
9
10
|
describe('Database Schema', () => {
|
|
10
11
|
let db: DbClient;
|
|
@@ -118,16 +119,17 @@ describe('Database Schema', () => {
|
|
|
118
119
|
};
|
|
119
120
|
db.insert(modules).values(newModule).run();
|
|
120
121
|
|
|
121
|
-
// Insert config
|
|
122
|
-
|
|
123
|
-
moduleId: 'homebridge',
|
|
124
|
-
key: 'target_ip',
|
|
125
|
-
value: '192.168.0.50',
|
|
126
|
-
};
|
|
122
|
+
// Insert config via the shared upsert helper
|
|
123
|
+
upsertModuleConfig(db, 'homebridge', 'target_ip', '192.168.0.50');
|
|
127
124
|
|
|
128
|
-
const result = db
|
|
125
|
+
const result = db
|
|
126
|
+
.select()
|
|
127
|
+
.from(moduleConfigs)
|
|
128
|
+
.where(and(eq(moduleConfigs.moduleId, 'homebridge'), eq(moduleConfigs.key, 'target_ip')))
|
|
129
|
+
.get();
|
|
129
130
|
|
|
130
131
|
expect(result).toBeDefined();
|
|
132
|
+
if (!result) throw new Error('expected row');
|
|
131
133
|
expect(result.moduleId).toBe('homebridge');
|
|
132
134
|
expect(result.key).toBe('target_ip');
|
|
133
135
|
expect(result.value).toBe('192.168.0.50');
|
|
@@ -203,13 +205,8 @@ describe('Database Schema', () => {
|
|
|
203
205
|
};
|
|
204
206
|
db.insert(modules).values(newModule).run();
|
|
205
207
|
|
|
206
|
-
// Insert config
|
|
207
|
-
|
|
208
|
-
moduleId: 'homebridge',
|
|
209
|
-
key: 'target_ip',
|
|
210
|
-
value: '192.168.0.50',
|
|
211
|
-
};
|
|
212
|
-
db.insert(moduleConfigs).values(newConfig).run();
|
|
208
|
+
// Insert config via the shared upsert helper
|
|
209
|
+
upsertModuleConfig(db, 'homebridge', 'target_ip', '192.168.0.50');
|
|
213
210
|
|
|
214
211
|
// Delete module
|
|
215
212
|
db.delete(modules).where(eq(modules.id, 'homebridge')).run();
|