@celilo/cli 0.3.30 → 0.4.0-alpha.0
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 +5 -4
- 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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the CLI rename from `celilo backup create <module>` to
|
|
3
|
+
* `celilo module backup <module>`:
|
|
4
|
+
* - The new shape routes to the same handler.
|
|
5
|
+
* - The legacy shape still works but prints a deprecation banner.
|
|
6
|
+
* - The banner is silenced by CELILO_SUPPRESS_DEPRECATION=1.
|
|
7
|
+
*
|
|
8
|
+
* Doesn't invoke a real backup — both routes resolve to handleBackupCreate
|
|
9
|
+
* which fails when there's no module record / no storage. That failure path
|
|
10
|
+
* proves routing landed at the right handler.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
14
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { closeDb } from '../db/client';
|
|
18
|
+
import { runMigrations } from '../db/migrate';
|
|
19
|
+
import { runCli } from './index';
|
|
20
|
+
|
|
21
|
+
describe('celilo module backup (renamed surface)', () => {
|
|
22
|
+
let dir: string;
|
|
23
|
+
let warnSpy: ((...args: unknown[]) => void) | null = null;
|
|
24
|
+
let warnings: string[] = [];
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
dir = mkdtempSync(join(tmpdir(), 'celilo-rename-test-'));
|
|
28
|
+
process.env.CELILO_DB_PATH = join(dir, 'celilo.db');
|
|
29
|
+
process.env.CELILO_MASTER_KEY_PATH = join(dir, 'master.key');
|
|
30
|
+
process.env.CELILO_SUPPRESS_DEPRECATION = undefined;
|
|
31
|
+
await runMigrations(process.env.CELILO_DB_PATH);
|
|
32
|
+
|
|
33
|
+
warnings = [];
|
|
34
|
+
warnSpy = (...args: unknown[]) => {
|
|
35
|
+
warnings.push(args.map(String).join(' '));
|
|
36
|
+
};
|
|
37
|
+
const realWarn = console.warn;
|
|
38
|
+
console.warn = warnSpy as typeof console.warn;
|
|
39
|
+
// Restore via afterEach by holding the original in a closure.
|
|
40
|
+
(warnSpy as unknown as { __restore: () => void }).__restore = () => {
|
|
41
|
+
console.warn = realWarn;
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
if (warnSpy) {
|
|
47
|
+
(warnSpy as unknown as { __restore: () => void }).__restore();
|
|
48
|
+
warnSpy = null;
|
|
49
|
+
}
|
|
50
|
+
closeDb();
|
|
51
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
52
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
53
|
+
process.env.CELILO_SUPPRESS_DEPRECATION = undefined;
|
|
54
|
+
try {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
/* ignore */
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('celilo module backup <id> routes to the backup-create handler', async () => {
|
|
62
|
+
const result = await runCli(['node', 'celilo', 'module', 'backup', 'no-such-module']);
|
|
63
|
+
// Module doesn't exist → handler returns an error. Routing reached the
|
|
64
|
+
// handler if we get THAT error (rather than "Unknown module subcommand").
|
|
65
|
+
expect(result.success).toBe(false);
|
|
66
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
67
|
+
expect(err).not.toContain('Unknown module subcommand');
|
|
68
|
+
// No deprecation banner on the canonical surface.
|
|
69
|
+
expect(warnings.join('\n')).not.toContain('deprecated');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('celilo backup create <id> still works AND prints a deprecation banner', async () => {
|
|
73
|
+
const result = await runCli(['node', 'celilo', 'backup', 'create', 'no-such-module']);
|
|
74
|
+
// Reaches the same handler (same failure mode as the canonical route).
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
const err = result.success ? '' : (result.error ?? '');
|
|
77
|
+
expect(err).not.toContain('Unknown');
|
|
78
|
+
// Banner present, points at the canonical command.
|
|
79
|
+
const banner = warnings.find((w) => w.includes('deprecated'));
|
|
80
|
+
expect(banner).toBeDefined();
|
|
81
|
+
expect(banner ?? '').toContain('celilo module backup no-such-module');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('CELILO_SUPPRESS_DEPRECATION=1 silences the deprecation banner', async () => {
|
|
85
|
+
process.env.CELILO_SUPPRESS_DEPRECATION = '1';
|
|
86
|
+
await runCli(['node', 'celilo', 'backup', 'create', 'no-such-module']);
|
|
87
|
+
expect(warnings.join('\n')).not.toContain('deprecated');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('celilo backup list still routes (only create was renamed)', async () => {
|
|
91
|
+
const result = await runCli(['node', 'celilo', 'backup', 'list']);
|
|
92
|
+
// Empty DB → empty list, success.
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { closeDb, getDb } from '../db/client';
|
|
|
7
7
|
import { moduleConfigs, modules, secrets } from '../db/schema';
|
|
8
8
|
import { encryptSecret } from '../secrets/encryption';
|
|
9
9
|
import { generateMasterKey, writeMasterKey } from '../secrets/master-key';
|
|
10
|
+
import { upsertModuleConfig } from '../services/module-config';
|
|
10
11
|
import { runCli } from './index';
|
|
11
12
|
|
|
12
13
|
const TEST_DB_PATH = `./test-cli-${Date.now()}-${Math.random()}.db`;
|
|
@@ -103,15 +104,16 @@ id: test-module
|
|
|
103
104
|
name: Test Module
|
|
104
105
|
version: 1.0.0
|
|
105
106
|
description: Test module for CLI
|
|
106
|
-
|
|
107
|
-
secrets:
|
|
108
|
-
declares:
|
|
109
|
-
- name: api_key
|
|
110
|
-
type: string
|
|
111
|
-
required: true
|
|
112
|
-
description: Test API key
|
|
113
107
|
`,
|
|
114
108
|
);
|
|
109
|
+
// NOTE: the manifest file deliberately does NOT declare any required
|
|
110
|
+
// secrets. The secret-set tests below construct their own manifest_data
|
|
111
|
+
// JSON in their inner beforeEach (read by handleSecretSet from
|
|
112
|
+
// module.manifestData), so they don't depend on this file. The
|
|
113
|
+
// module-generate test, however, reads the FILE manifest via
|
|
114
|
+
// loadInstalledManifest in services/config-interview.ts — if any
|
|
115
|
+
// required secret is declared here, generate fails on the
|
|
116
|
+
// non-interactive-and-no-responder probe.
|
|
115
117
|
|
|
116
118
|
// Create master key for secret tests
|
|
117
119
|
const masterKey = generateMasterKey();
|
|
@@ -298,9 +300,7 @@ secrets:
|
|
|
298
300
|
test('should update existing config value', async () => {
|
|
299
301
|
const db = getDb();
|
|
300
302
|
// Set initial value
|
|
301
|
-
db
|
|
302
|
-
.values({ moduleId: 'test-module', key: 'hostname', value: 'oldhost' })
|
|
303
|
-
.run();
|
|
303
|
+
upsertModuleConfig(db, 'test-module', 'hostname', 'oldhost');
|
|
304
304
|
|
|
305
305
|
const result = await runCli([
|
|
306
306
|
'node',
|
|
@@ -363,9 +363,7 @@ secrets:
|
|
|
363
363
|
|
|
364
364
|
test('should get specific config value', async () => {
|
|
365
365
|
const db = getDb();
|
|
366
|
-
db
|
|
367
|
-
.values({ moduleId: 'test-module', key: 'hostname', value: 'myhost' })
|
|
368
|
-
.run();
|
|
366
|
+
upsertModuleConfig(db, 'test-module', 'hostname', 'myhost');
|
|
369
367
|
|
|
370
368
|
const result = await runCli([
|
|
371
369
|
'node',
|
|
@@ -384,12 +382,8 @@ secrets:
|
|
|
384
382
|
|
|
385
383
|
test('should get all config values', async () => {
|
|
386
384
|
const db = getDb();
|
|
387
|
-
db
|
|
388
|
-
|
|
389
|
-
{ moduleId: 'test-module', key: 'hostname', value: 'myhost' },
|
|
390
|
-
{ moduleId: 'test-module', key: 'ip', value: '192.168.0.50' },
|
|
391
|
-
])
|
|
392
|
-
.run();
|
|
385
|
+
upsertModuleConfig(db, 'test-module', 'hostname', 'myhost');
|
|
386
|
+
upsertModuleConfig(db, 'test-module', 'ip', '192.168.0.50');
|
|
393
387
|
|
|
394
388
|
const result = await runCli(['node', 'celilo', 'module', 'config', 'get', 'test-module']);
|
|
395
389
|
|
|
@@ -579,7 +573,9 @@ secrets:
|
|
|
579
573
|
|
|
580
574
|
expect(result.success).toBe(false);
|
|
581
575
|
if (result.success) return;
|
|
582
|
-
|
|
576
|
+
// `module import` now takes a positional (name or path), so the error
|
|
577
|
+
// names what's missing rather than "subcommand required".
|
|
578
|
+
expect(result.error).toContain('Module name or path required');
|
|
583
579
|
});
|
|
584
580
|
|
|
585
581
|
test('should error on non-existent path', async () => {
|
|
@@ -614,9 +610,7 @@ secrets:
|
|
|
614
610
|
);
|
|
615
611
|
|
|
616
612
|
// Add config
|
|
617
|
-
db
|
|
618
|
-
.values({ moduleId: 'test-module', key: 'hostname', value: 'testhost' })
|
|
619
|
-
.run();
|
|
613
|
+
upsertModuleConfig(db, 'test-module', 'hostname', 'testhost');
|
|
620
614
|
|
|
621
615
|
// Create template directory
|
|
622
616
|
await mkdir(join(TEST_MODULE_DIR, 'terraform'), { recursive: true });
|
|
@@ -249,6 +249,62 @@ export const COMMANDS: CommandDef[] = [
|
|
|
249
249
|
},
|
|
250
250
|
],
|
|
251
251
|
},
|
|
252
|
+
{
|
|
253
|
+
name: 'publish',
|
|
254
|
+
description:
|
|
255
|
+
'Publish workspace packages to npm, bump consumer pins, refresh global install, and ship modules to celilo.computer',
|
|
256
|
+
flags: [
|
|
257
|
+
{
|
|
258
|
+
name: 'dry-run',
|
|
259
|
+
description: 'Preflight report only — no publishing, no file changes',
|
|
260
|
+
takesValue: false,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'release-touch',
|
|
264
|
+
description: 'Auto-touch drifted module manifests with a fresh release marker, then exit',
|
|
265
|
+
takesValue: false,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'allow-stale',
|
|
269
|
+
description: 'Bypass workspace and module stale-version safeguards',
|
|
270
|
+
takesValue: false,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'yes',
|
|
274
|
+
description: 'Auto-confirm every prompt (also -y). Does NOT bypass safety gates.',
|
|
275
|
+
takesValue: false,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'alpha',
|
|
279
|
+
description: 'Publish X.Y.Z-alpha.N to npm @alpha dist-tag (consumer-first iteration mode)',
|
|
280
|
+
takesValue: false,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'track-alpha',
|
|
284
|
+
description:
|
|
285
|
+
'Force-pin just-published alphas into the bun global install (requires --alpha)',
|
|
286
|
+
takesValue: false,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'alpha-modules',
|
|
290
|
+
description:
|
|
291
|
+
'Also publish module +N revisions in alpha mode (requires --alpha; default skips Phase 4)',
|
|
292
|
+
takesValue: false,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'promote',
|
|
296
|
+
description:
|
|
297
|
+
'Graduate a named alpha to its base X.Y.Z (e.g. --promote @celilo/e2e@0.7.14-alpha.3)',
|
|
298
|
+
takesValue: true,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'skip-changesets',
|
|
302
|
+
description:
|
|
303
|
+
'Skip the pending-changesets prompt; publish current package.json versions as-is',
|
|
304
|
+
takesValue: false,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
252
308
|
{
|
|
253
309
|
name: 'module',
|
|
254
310
|
description: 'Manage modules (import, generate, configure)',
|
|
@@ -286,6 +342,12 @@ export const COMMANDS: CommandDef[] = [
|
|
|
286
342
|
description: 'Skip package signature verification',
|
|
287
343
|
takesValue: false,
|
|
288
344
|
},
|
|
345
|
+
{
|
|
346
|
+
name: 'accept-aspects',
|
|
347
|
+
description:
|
|
348
|
+
'Auto-approve any base-module aspect declared by the imported module (non-interactive contexts; CI; e2e)',
|
|
349
|
+
takesValue: false,
|
|
350
|
+
},
|
|
289
351
|
],
|
|
290
352
|
},
|
|
291
353
|
{ name: 'list', description: 'List all installed modules' },
|
|
@@ -512,6 +574,31 @@ export const COMMANDS: CommandDef[] = [
|
|
|
512
574
|
},
|
|
513
575
|
],
|
|
514
576
|
},
|
|
577
|
+
{
|
|
578
|
+
name: 'backup',
|
|
579
|
+
description: "Create a backup of a module's data (invokes its on_backup hook)",
|
|
580
|
+
args: [
|
|
581
|
+
{
|
|
582
|
+
name: 'module-id',
|
|
583
|
+
description: 'Module ID (optional, backs up all if omitted)',
|
|
584
|
+
completion: 'module_ids',
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
flags: [
|
|
588
|
+
{ name: 'force', description: 'Ignore schedule, back up now', takesValue: false },
|
|
589
|
+
{
|
|
590
|
+
name: 'now',
|
|
591
|
+
description: 'Ignore schedule, back up now (alias for --force)',
|
|
592
|
+
takesValue: false,
|
|
593
|
+
},
|
|
594
|
+
{ name: 'storage', description: 'Storage destination ID', takesValue: true },
|
|
595
|
+
{
|
|
596
|
+
name: 'no-interactive',
|
|
597
|
+
description: 'Non-interactive mode (for cron)',
|
|
598
|
+
takesValue: false,
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
},
|
|
515
602
|
],
|
|
516
603
|
},
|
|
517
604
|
{
|
|
@@ -637,6 +724,12 @@ export const COMMANDS: CommandDef[] = [
|
|
|
637
724
|
description: 'Earmark machine for a specific module',
|
|
638
725
|
takesValue: true,
|
|
639
726
|
},
|
|
727
|
+
{
|
|
728
|
+
name: 'local',
|
|
729
|
+
description:
|
|
730
|
+
'This box itself — deploy over Ansible local connection (no SSH/key). Implied by ip 127.0.0.1.',
|
|
731
|
+
takesValue: false,
|
|
732
|
+
},
|
|
640
733
|
],
|
|
641
734
|
},
|
|
642
735
|
{
|
|
@@ -696,6 +789,25 @@ export const COMMANDS: CommandDef[] = [
|
|
|
696
789
|
},
|
|
697
790
|
],
|
|
698
791
|
},
|
|
792
|
+
{
|
|
793
|
+
name: 'apply-config',
|
|
794
|
+
description:
|
|
795
|
+
'Headless write of <key=value> overrides to systemConfig (for hooks / automation; no prompts, no guidance)',
|
|
796
|
+
args: [
|
|
797
|
+
{
|
|
798
|
+
name: 'overrides',
|
|
799
|
+
description: 'Config overrides as <key=value> positionals',
|
|
800
|
+
variadic: true,
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
flags: [
|
|
804
|
+
{
|
|
805
|
+
name: 'from-stdin',
|
|
806
|
+
description: 'Read a JSON map of overrides from stdin instead of positional args',
|
|
807
|
+
takesValue: false,
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
},
|
|
699
811
|
{
|
|
700
812
|
name: 'config',
|
|
701
813
|
description: 'Get or set system configuration',
|
|
@@ -882,6 +994,75 @@ export const COMMANDS: CommandDef[] = [
|
|
|
882
994
|
},
|
|
883
995
|
],
|
|
884
996
|
},
|
|
997
|
+
{
|
|
998
|
+
name: 'subscribers',
|
|
999
|
+
description: 'Manage build-bus subscribers (cross-machine publish-event delivery)',
|
|
1000
|
+
subcommands: [
|
|
1001
|
+
{
|
|
1002
|
+
name: 'list',
|
|
1003
|
+
description: 'Show registered build-bus subscribers',
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'add',
|
|
1007
|
+
description: 'Add a build-bus subscriber',
|
|
1008
|
+
args: [{ name: 'url', description: 'Subscriber webhook URL (https://...)' }],
|
|
1009
|
+
flags: [
|
|
1010
|
+
{ name: 'secret', description: 'Per-subscriber HMAC secret', takesValue: true },
|
|
1011
|
+
{ name: 'name', description: 'Human-readable label', takesValue: true },
|
|
1012
|
+
{
|
|
1013
|
+
name: 'registry',
|
|
1014
|
+
description:
|
|
1015
|
+
'Filter: deliver only events from this registry (e.g. npm, celilo-registry)',
|
|
1016
|
+
takesValue: true,
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: 'tag',
|
|
1020
|
+
description: 'Filter: deliver only events with this dist-tag (e.g. latest, alpha)',
|
|
1021
|
+
takesValue: true,
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: 'package-pattern',
|
|
1025
|
+
description:
|
|
1026
|
+
'Filter: deliver only events whose package name matches this glob (e.g. @celilo/*)',
|
|
1027
|
+
takesValue: true,
|
|
1028
|
+
},
|
|
1029
|
+
],
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: 'remove',
|
|
1033
|
+
description: 'Remove a subscriber by URL',
|
|
1034
|
+
args: [{ name: 'url', description: 'Subscriber webhook URL to remove' }],
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: 'test',
|
|
1038
|
+
description: 'Fire a synthetic event at a subscriber and report the delivery outcome',
|
|
1039
|
+
args: [{ name: 'url', description: 'Subscriber webhook URL to test' }],
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
name: 'serve',
|
|
1043
|
+
description:
|
|
1044
|
+
'Run the build-bus HTTP receiver daemon (verifies signed webhooks, emits to local bus, dispatches module hooks)',
|
|
1045
|
+
flags: [
|
|
1046
|
+
{ name: 'port', description: 'TCP port to listen on (default 8123)', takesValue: true },
|
|
1047
|
+
{
|
|
1048
|
+
name: 'secret',
|
|
1049
|
+
description: 'Shared HMAC secret incoming webhooks must sign with',
|
|
1050
|
+
takesValue: true,
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: 'no-dispatch',
|
|
1054
|
+
description: 'Skip the in-process hook dispatcher (receiver-only mode)',
|
|
1055
|
+
takesValue: false,
|
|
1056
|
+
},
|
|
1057
|
+
],
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: 'status',
|
|
1061
|
+
description:
|
|
1062
|
+
'Per-subscriber delivery history (success rate, last delivery, recent failures)',
|
|
1063
|
+
},
|
|
1064
|
+
],
|
|
1065
|
+
},
|
|
885
1066
|
{
|
|
886
1067
|
name: 'storage',
|
|
887
1068
|
description: 'Manage backup storage destinations',
|
|
@@ -1031,6 +1212,24 @@ export const COMMANDS: CommandDef[] = [
|
|
|
1031
1212
|
},
|
|
1032
1213
|
],
|
|
1033
1214
|
},
|
|
1215
|
+
{
|
|
1216
|
+
name: 'restore',
|
|
1217
|
+
description: 'Restore a celilo-mgmt backup from a local artifact file (fresh-bootstrap path)',
|
|
1218
|
+
args: [
|
|
1219
|
+
{
|
|
1220
|
+
name: 'artifact-path',
|
|
1221
|
+
description: 'Path to the .backup file (also accepted via --from)',
|
|
1222
|
+
},
|
|
1223
|
+
],
|
|
1224
|
+
flags: [
|
|
1225
|
+
{ name: 'from', description: 'Path to the .backup file', takesValue: true },
|
|
1226
|
+
{
|
|
1227
|
+
name: 'force',
|
|
1228
|
+
description: 'Override non-destructive pre-flight (clobbers existing state)',
|
|
1229
|
+
takesValue: false,
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
},
|
|
1034
1233
|
{
|
|
1035
1234
|
name: 'completion',
|
|
1036
1235
|
description: 'Generate shell completion scripts',
|
|
@@ -99,7 +99,7 @@ export async function handleBackupList(
|
|
|
99
99
|
if (backupList.length === 0) {
|
|
100
100
|
console.log('No backups found.\n');
|
|
101
101
|
console.log('Create a backup:');
|
|
102
|
-
console.log(' celilo backup
|
|
102
|
+
console.log(' celilo module backup <module-id>');
|
|
103
103
|
return { success: true, message: 'No backups found' };
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -26,7 +26,14 @@ import {
|
|
|
26
26
|
recoverFromCrash,
|
|
27
27
|
runDispatcher,
|
|
28
28
|
} from '@celilo/event-bus';
|
|
29
|
+
import { eq } from 'drizzle-orm';
|
|
29
30
|
import { getEventBusPath, shortenPath } from '../../config/paths';
|
|
31
|
+
import { getDb } from '../../db/client';
|
|
32
|
+
import { modules } from '../../db/schema';
|
|
33
|
+
import { createConsoleLogger } from '../../hooks/logger';
|
|
34
|
+
import { runNamedHook } from '../../hooks/run-named-hook';
|
|
35
|
+
import type { HookName } from '../../hooks/types';
|
|
36
|
+
import type { ModuleManifest } from '../../manifest/schema';
|
|
30
37
|
import { installDaemon, readInstalledUnit, uninstallDaemon } from '../../services/events-daemon';
|
|
31
38
|
import { getArg, hasFlag } from '../parser';
|
|
32
39
|
import type { CommandResult } from '../types';
|
|
@@ -54,6 +61,95 @@ export async function handleEventsStatus(): Promise<CommandResult> {
|
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
/** camelCase → snake_case for every key (payloads are camelCase; hook inputs snake_case). */
|
|
65
|
+
function snakeCaseKeys(obj: Record<string, unknown>): Record<string, unknown> {
|
|
66
|
+
const out: Record<string, unknown> = {};
|
|
67
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
68
|
+
out[k.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)] = v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `celilo events run-hook <module> <sub-name> [<event_id>]` — the generic
|
|
75
|
+
* runner a `hook:` subscription resolves to (v2/EVENT_DRIVEN_HOOK_SUBSCRIPTIONS.md).
|
|
76
|
+
*
|
|
77
|
+
* The dispatcher spawns this as a fault-isolated subprocess. It re-reads the
|
|
78
|
+
* module's manifest to find the named subscription's `hook` + `hook_inputs`,
|
|
79
|
+
* assembles the hook inputs as `{ ...hook_inputs, ...snakeCase(eventPayload) }`,
|
|
80
|
+
* and runs the hook through the normal backend executor (config + decrypted
|
|
81
|
+
* secrets + the module's own capabilities injected). It is dumb pass-through —
|
|
82
|
+
* it knows nothing about DNS or any specific event type.
|
|
83
|
+
*/
|
|
84
|
+
export async function handleEventsRunHook(args: string[]): Promise<CommandResult> {
|
|
85
|
+
const moduleId = args[0];
|
|
86
|
+
const subName = args[1];
|
|
87
|
+
// Event id: an explicit 3rd arg wins, else the dispatcher's $EVENT_ID.
|
|
88
|
+
const idStr = args[2] ?? process.env.EVENT_ID;
|
|
89
|
+
if (!moduleId || !subName) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: 'usage: celilo events run-hook <module> <sub-name> [<event_id>]',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const eventId = Number(idStr);
|
|
96
|
+
if (!idStr || !Number.isInteger(eventId)) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: 'events run-hook requires an event id (3rd arg or $EVENT_ID)',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const db = getDb();
|
|
104
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
105
|
+
if (!module) return { success: false, error: `Module not found: ${moduleId}` };
|
|
106
|
+
|
|
107
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
108
|
+
const sub = (manifest.subscriptions ?? []).find((s) => s.name === subName);
|
|
109
|
+
if (!sub) {
|
|
110
|
+
return { success: false, error: `Module '${moduleId}' has no subscription named '${subName}'` };
|
|
111
|
+
}
|
|
112
|
+
if (!sub.hook) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: `Subscription '${subName}' on '${moduleId}' is not a hook subscription`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const bus = openCliBus();
|
|
120
|
+
let payload: Record<string, unknown> = {};
|
|
121
|
+
let eventType: string;
|
|
122
|
+
try {
|
|
123
|
+
const event = bus.getEvent(eventId);
|
|
124
|
+
if (!event) return { success: false, error: `Event ${eventId} not found on the bus` };
|
|
125
|
+
eventType = event.type;
|
|
126
|
+
if (event.payload && typeof event.payload === 'object') {
|
|
127
|
+
payload = event.payload as Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
bus.close();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const inputs: Record<string, unknown> = {
|
|
134
|
+
...(sub.hook_inputs ?? {}),
|
|
135
|
+
...snakeCaseKeys(payload),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const logger = createConsoleLogger(moduleId, sub.hook);
|
|
139
|
+
const result = await runNamedHook(moduleId, sub.hook as HookName, db, logger, { inputs });
|
|
140
|
+
|
|
141
|
+
if (result.notDefined) {
|
|
142
|
+
return { success: false, error: `Module '${moduleId}' declares no '${sub.hook}' hook to run` };
|
|
143
|
+
}
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
return { success: false, error: result.error ?? `hook '${sub.hook}' failed` };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
message: `Ran ${moduleId}.${sub.hook} for event ${eventId} (${eventType})`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
57
153
|
export async function handleEventsTail(
|
|
58
154
|
_args: string[],
|
|
59
155
|
flags: Record<string, string | boolean>,
|