@celilo/cli 0.1.5 → 0.1.6
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/0004_caddy_hostname_list.sql +25 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +9 -2
- package/src/ansible/inventory.test.ts +3 -2
- package/src/ansible/inventory.ts +5 -1
- package/src/capabilities/public-web-helpers.test.ts +2 -2
- package/src/capabilities/public-web-publish.test.ts +34 -1
- package/src/cli/cli.test.ts +2 -2
- package/src/cli/command-registry.ts +146 -3
- package/src/cli/command-tree-parser.test.ts +1 -1
- package/src/cli/command-tree-parser.ts +9 -8
- package/src/cli/commands/hook-run.ts +15 -66
- package/src/cli/commands/module-audit.ts +14 -44
- package/src/cli/commands/module-deploy.ts +4 -1
- package/src/cli/commands/module-import-registry.test.ts +115 -0
- package/src/cli/commands/module-import.ts +106 -22
- package/src/cli/commands/module-publish.test.ts +235 -0
- package/src/cli/commands/module-publish.ts +234 -0
- package/src/cli/commands/module-remove.ts +82 -2
- package/src/cli/commands/module-search.ts +57 -0
- package/src/cli/commands/module-secret-get.ts +59 -0
- package/src/cli/commands/module-terraform-unlock.ts +57 -0
- package/src/cli/commands/module-verify.test.ts +59 -0
- package/src/cli/commands/module-verify.ts +53 -0
- package/src/cli/commands/status.ts +30 -20
- package/src/cli/commands/system-audit.test.ts +138 -0
- package/src/cli/commands/system-audit.ts +571 -0
- package/src/cli/commands/system-update.ts +391 -0
- package/src/cli/completion.ts +15 -1
- package/src/cli/fuel-gauge.ts +68 -3
- package/src/cli/generate-zsh-completion.ts +13 -3
- package/src/cli/index.ts +112 -5
- package/src/cli/parser.ts +11 -0
- package/src/cli/prompts.ts +36 -5
- package/src/cli/tui/audit-state.test.ts +246 -0
- package/src/cli/tui/audit-state.ts +525 -0
- package/src/cli/tui/audit-tui.test.tsx +135 -0
- package/src/cli/tui/audit-tui.tsx +624 -0
- package/src/cli/tui/celebration.tsx +29 -0
- package/src/cli/tui/clipboard.test.ts +94 -0
- package/src/cli/tui/clipboard.ts +101 -0
- package/src/cli/tui/icons.ts +22 -0
- package/src/cli/tui/keybar.tsx +65 -0
- package/src/cli/tui/keymap.test.ts +105 -0
- package/src/cli/tui/keymap.ts +70 -0
- package/src/cli/tui/modals/analyzing.tsx +75 -0
- package/src/cli/tui/modals/celebration.tsx +44 -0
- package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
- package/src/cli/tui/modals/remediate.tsx +44 -0
- package/src/cli/tui/modals.test.ts +137 -0
- package/src/cli/tui/mouse.test.ts +78 -0
- package/src/cli/tui/mouse.ts +114 -0
- package/src/cli/tui/panes/categories.tsx +62 -0
- package/src/cli/tui/panes/command-log.tsx +87 -0
- package/src/cli/tui/panes/detail.tsx +175 -0
- package/src/cli/tui/panes/findings.tsx +97 -0
- package/src/cli/tui/panes/summary.tsx +64 -0
- package/src/cli/tui/spawn.ts +130 -0
- package/src/cli/tui/theme.ts +42 -0
- package/src/cli/tui/wrap.test.ts +43 -0
- package/src/cli/tui/wrap.ts +45 -0
- package/src/cli/types.ts +5 -0
- package/src/db/client.ts +55 -2
- package/src/db/schema.ts +26 -17
- package/src/hooks/capability-loader.ts +133 -73
- package/src/hooks/define-hook.test.ts +9 -1
- package/src/hooks/executor.ts +22 -1
- package/src/hooks/load-hook-config.test.ts +165 -0
- package/src/hooks/load-hook-config.ts +60 -0
- package/src/hooks/logger.ts +42 -12
- package/src/hooks/run-named-hook.ts +128 -0
- package/src/hooks/types.ts +19 -0
- package/src/manifest/ensure-schema.test.ts +115 -0
- package/src/manifest/schema.ts +76 -0
- package/src/module/import.ts +20 -12
- package/src/module/packaging/build.ts +85 -16
- package/src/module/packaging/release-metadata.test.ts +103 -0
- package/src/module/packaging/release-metadata.ts +145 -0
- package/src/registry/client.test.ts +228 -0
- package/src/registry/client.ts +157 -0
- package/src/services/audit/backups.test.ts +233 -0
- package/src/services/audit/backups.ts +128 -0
- package/src/services/audit/capability-abi.test.ts +153 -0
- package/src/services/audit/capability-abi.ts +204 -0
- package/src/services/audit/cli-version.test.ts +60 -0
- package/src/services/audit/cli-version.ts +87 -0
- package/src/services/audit/health.test.ts +84 -0
- package/src/services/audit/health.ts +43 -0
- package/src/services/audit/index.test.ts +99 -0
- package/src/services/audit/index.ts +118 -0
- package/src/services/audit/machines-reachable.test.ts +87 -0
- package/src/services/audit/machines-reachable.ts +87 -0
- package/src/services/audit/module-configs.test.ts +131 -0
- package/src/services/audit/module-configs.ts +80 -0
- package/src/services/audit/module-versions.test.ts +99 -0
- package/src/services/audit/module-versions.ts +154 -0
- package/src/services/audit/schema.test.ts +68 -0
- package/src/services/audit/schema.ts +115 -0
- package/src/services/audit/secrets-decryptable.test.ts +82 -0
- package/src/services/audit/secrets-decryptable.ts +97 -0
- package/src/services/audit/services-credentials.test.ts +54 -0
- package/src/services/audit/services-credentials.ts +64 -0
- package/src/services/audit/services-reachable.test.ts +60 -0
- package/src/services/audit/services-reachable.ts +64 -0
- package/src/services/audit/terraform-plan.test.ts +127 -0
- package/src/services/audit/terraform-plan.ts +153 -0
- package/src/services/audit/types.test.ts +36 -0
- package/src/services/audit/types.ts +90 -0
- package/src/services/audit/unconfigured-modules.test.ts +48 -0
- package/src/services/audit/unconfigured-modules.ts +71 -0
- package/src/services/audit/undeployed-modules.test.ts +66 -0
- package/src/services/audit/undeployed-modules.ts +72 -0
- package/src/services/build-stream.ts +122 -122
- package/src/services/config-interview.ts +407 -2
- package/src/services/deploy-ansible.ts +73 -7
- package/src/services/deploy-preflight.ts +45 -4
- package/src/services/deploy-terraform.ts +31 -24
- package/src/services/deploy-validation.ts +167 -23
- package/src/services/ensure-interview.test.ts +245 -0
- package/src/services/health-runner.ts +110 -38
- package/src/services/module-build.ts +11 -13
- package/src/services/module-deploy.ts +370 -59
- package/src/services/ssh-key-manager.test.ts +1 -1
- package/src/services/ssh-key-manager.ts +3 -2
- package/src/services/terraform-env.ts +62 -0
- package/src/services/update/dep-graph.test.ts +214 -0
- package/src/services/update/dep-graph.ts +215 -0
- package/src/services/update/orchestrator.test.ts +463 -0
- package/src/services/update/orchestrator.ts +359 -0
- package/src/services/update/progress.ts +49 -0
- package/src/services/update/self-update.test.ts +68 -0
- package/src/services/update/self-update.ts +57 -0
- package/src/services/update/types.ts +94 -0
- package/src/templates/generator.test.ts +1 -1
- package/src/templates/generator.ts +42 -1
- package/src/test-utils/completion-harness.test.ts +1 -1
- package/src/test-utils/completion-harness.ts +4 -4
- package/src/variables/capability-self-ref.test.ts +203 -0
- package/src/variables/context.ts +49 -1
- package/src/variables/declarative-derivation.test.ts +306 -0
- package/src/variables/declarative-derivation.ts +4 -2
- package/src/variables/parser.test.ts +56 -1
- package/src/variables/parser.ts +47 -6
- package/src/variables/resolver.ts +27 -9
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Caddy hostname list refactor (CADDY_HOSTNAME_LIST design).
|
|
2
|
+
-- web_routes used to carry `subdomain` + `custom_domain` and a flat
|
|
3
|
+
-- unique index on `path`. The new model has a single `hostname` FQDN
|
|
4
|
+
-- and a (hostname, path) unique index. Phase 0 / no production users
|
|
5
|
+
-- means this is a destructive migration — modules repopulate routes
|
|
6
|
+
-- on their next deploy after the migration runs.
|
|
7
|
+
DROP TABLE IF EXISTS `web_routes`;
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE `web_routes` (
|
|
10
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
11
|
+
`slug` text NOT NULL,
|
|
12
|
+
`module_id` text NOT NULL,
|
|
13
|
+
`type` text NOT NULL,
|
|
14
|
+
`path` text NOT NULL,
|
|
15
|
+
`hostname` text NOT NULL,
|
|
16
|
+
`target_host` text,
|
|
17
|
+
`target_port` integer,
|
|
18
|
+
`websocket` integer DEFAULT false NOT NULL,
|
|
19
|
+
`content_hash` text,
|
|
20
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
21
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
22
|
+
FOREIGN KEY (`module_id`) REFERENCES `modules`(`id`) ON UPDATE no action ON DELETE cascade
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE UNIQUE INDEX `web_routes_hostname_path_idx` ON `web_routes` (`hostname`,`path`);
|
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
"when": 1774881303520,
|
|
23
23
|
"tag": "0002_web_routes",
|
|
24
24
|
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "6",
|
|
29
|
+
"when": 1774924800000,
|
|
30
|
+
"tag": "0003_backup_storage",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "6",
|
|
36
|
+
"when": 1777680000000,
|
|
37
|
+
"tag": "0004_caddy_hostname_list",
|
|
38
|
+
"breakpoints": true
|
|
25
39
|
}
|
|
26
40
|
]
|
|
27
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Celilo — home lab orchestration CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,10 +46,15 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@aws-sdk/client-s3": "^3.1024.0",
|
|
49
|
-
"@clack/prompts": "^1.1.0",
|
|
50
49
|
"@celilo/capabilities": "^0.1.2",
|
|
50
|
+
"@celilo/cli-display": "0.1.0",
|
|
51
|
+
"@clack/prompts": "^1.1.0",
|
|
51
52
|
"ajv": "^8.18.0",
|
|
52
53
|
"drizzle-orm": "^0.36.4",
|
|
54
|
+
"ink": "^7.0.1",
|
|
55
|
+
"ink-spinner": "^5.0.0",
|
|
56
|
+
"ink-text-input": "^6.0.0",
|
|
57
|
+
"react": "^19.2.5",
|
|
53
58
|
"tar": "^7.5.10",
|
|
54
59
|
"xxhash-wasm": "^1.1.0",
|
|
55
60
|
"yaml": "^2.8.2",
|
|
@@ -58,7 +63,9 @@
|
|
|
58
63
|
"devDependencies": {
|
|
59
64
|
"@biomejs/biome": "^1.9.4",
|
|
60
65
|
"@types/bun": "^1.1.14",
|
|
66
|
+
"@types/react": "^19.2.14",
|
|
61
67
|
"drizzle-kit": "^0.30.0",
|
|
68
|
+
"ink-testing-library": "^4.0.0",
|
|
62
69
|
"typescript": "^5.9.3",
|
|
63
70
|
"zod-to-json-schema": "^3.25.2"
|
|
64
71
|
}
|
|
@@ -75,13 +75,14 @@ describe('generateHostsIni', () => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test('includes SSH private key file path when provided', () => {
|
|
78
|
+
const keyPath = '/tmp/celilo-ansible-keys/machine-machine-1.key';
|
|
78
79
|
const hosts: InventoryHost[] = [
|
|
79
80
|
{
|
|
80
81
|
hostname: 'machine-host',
|
|
81
82
|
ansibleHost: '192.168.1.100',
|
|
82
83
|
ansibleUser: 'ubuntu',
|
|
83
84
|
groups: ['machines'],
|
|
84
|
-
ansibleSshPrivateKeyFile:
|
|
85
|
+
ansibleSshPrivateKeyFile: keyPath,
|
|
85
86
|
},
|
|
86
87
|
];
|
|
87
88
|
|
|
@@ -89,7 +90,7 @@ describe('generateHostsIni', () => {
|
|
|
89
90
|
|
|
90
91
|
expect(result).toContain('[machines]');
|
|
91
92
|
expect(result).toContain(
|
|
92
|
-
|
|
93
|
+
`machine-host ansible_host=192.168.1.100 ansible_user=ubuntu ansible_ssh_private_key_file=${keyPath}`,
|
|
93
94
|
);
|
|
94
95
|
});
|
|
95
96
|
});
|
package/src/ansible/inventory.ts
CHANGED
|
@@ -66,7 +66,11 @@ export function generateHostsIni(hosts: InventoryHost[]): string {
|
|
|
66
66
|
// Add [all:vars] section with common variables
|
|
67
67
|
lines.push('[all:vars]');
|
|
68
68
|
lines.push('ansible_python_interpreter=/usr/bin/python3');
|
|
69
|
-
|
|
69
|
+
// Disable ControlMaster to avoid Python format string issues with ControlPath
|
|
70
|
+
// ControlPath uses %h, %p, %r placeholders which trigger Python's % formatter
|
|
71
|
+
lines.push(
|
|
72
|
+
'ansible_ssh_common_args=-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=no',
|
|
73
|
+
);
|
|
70
74
|
lines.push('');
|
|
71
75
|
|
|
72
76
|
return lines.join('\n');
|
|
@@ -101,11 +101,11 @@ describe('validatePublishStaticSiteRequest', () => {
|
|
|
101
101
|
expect(result.errors).toEqual([]);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
test('accepts a request with optional clientConfig and
|
|
104
|
+
test('accepts a request with optional clientConfig and hostname', () => {
|
|
105
105
|
const result = validatePublishStaticSiteRequest(
|
|
106
106
|
basePublishRequest({
|
|
107
107
|
clientConfig: { FOO: 'bar' },
|
|
108
|
-
|
|
108
|
+
hostname: 'app.iamtheinternet.org',
|
|
109
109
|
}),
|
|
110
110
|
);
|
|
111
111
|
expect(result.valid).toBe(true);
|
|
@@ -53,9 +53,9 @@ function makeRouteOps(): { ops: RouteOps; routes: WebRoute[] } {
|
|
|
53
53
|
moduleId: route.moduleId,
|
|
54
54
|
type: route.type,
|
|
55
55
|
path: route.path,
|
|
56
|
+
hostname: route.hostname,
|
|
56
57
|
targetHost: route.targetHost ?? null,
|
|
57
58
|
targetPort: route.targetPort ?? null,
|
|
58
|
-
subdomain: route.subdomain ?? null,
|
|
59
59
|
websocket: route.websocket ?? false,
|
|
60
60
|
contentHash: route.contentHash ?? null,
|
|
61
61
|
createdAt: existing >= 0 ? routes[existing].createdAt : now,
|
|
@@ -116,6 +116,9 @@ describe('publishStaticSite — clientConfig injection', () => {
|
|
|
116
116
|
},
|
|
117
117
|
secrets: {},
|
|
118
118
|
routeOps: ops,
|
|
119
|
+
hostnames: ['www.example.com'],
|
|
120
|
+
caddyModuleId: 'caddy',
|
|
121
|
+
dnsManagedDomains: ['www.example.com'],
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
const result = await cap.publishStaticSite({
|
|
@@ -157,6 +160,9 @@ describe('publishStaticSite — clientConfig injection', () => {
|
|
|
157
160
|
},
|
|
158
161
|
secrets: {},
|
|
159
162
|
routeOps: ops,
|
|
163
|
+
hostnames: ['www.example.com'],
|
|
164
|
+
caddyModuleId: 'caddy',
|
|
165
|
+
dnsManagedDomains: ['www.example.com'],
|
|
160
166
|
});
|
|
161
167
|
|
|
162
168
|
await cap.publishStaticSite({
|
|
@@ -181,6 +187,9 @@ describe('publishStaticSite — clientConfig injection', () => {
|
|
|
181
187
|
},
|
|
182
188
|
secrets: {},
|
|
183
189
|
routeOps: ops,
|
|
190
|
+
hostnames: ['www.example.com'],
|
|
191
|
+
caddyModuleId: 'caddy',
|
|
192
|
+
dnsManagedDomains: ['www.example.com'],
|
|
184
193
|
});
|
|
185
194
|
|
|
186
195
|
await cap.publishStaticSite({
|
|
@@ -208,6 +217,9 @@ describe('publishStaticSite — clientConfig injection', () => {
|
|
|
208
217
|
},
|
|
209
218
|
secrets: {},
|
|
210
219
|
routeOps: ops,
|
|
220
|
+
hostnames: ['www.example.com'],
|
|
221
|
+
caddyModuleId: 'caddy',
|
|
222
|
+
dnsManagedDomains: ['www.example.com'],
|
|
211
223
|
});
|
|
212
224
|
|
|
213
225
|
await expect(
|
|
@@ -232,6 +244,9 @@ describe('publishStaticSite — clientConfig injection', () => {
|
|
|
232
244
|
},
|
|
233
245
|
secrets: {},
|
|
234
246
|
routeOps: ops,
|
|
247
|
+
hostnames: ['www.example.com'],
|
|
248
|
+
caddyModuleId: 'caddy',
|
|
249
|
+
dnsManagedDomains: ['www.example.com'],
|
|
235
250
|
});
|
|
236
251
|
|
|
237
252
|
await expect(
|
|
@@ -268,6 +283,9 @@ describe('registerReverseProxy', () => {
|
|
|
268
283
|
},
|
|
269
284
|
secrets: {},
|
|
270
285
|
routeOps: ops,
|
|
286
|
+
hostnames: ['www.example.com'],
|
|
287
|
+
caddyModuleId: 'caddy',
|
|
288
|
+
dnsManagedDomains: ['www.example.com'],
|
|
271
289
|
});
|
|
272
290
|
|
|
273
291
|
const result = await cap.registerReverseProxy({
|
|
@@ -301,6 +319,9 @@ describe('registerReverseProxy', () => {
|
|
|
301
319
|
},
|
|
302
320
|
secrets: {},
|
|
303
321
|
routeOps: ops,
|
|
322
|
+
hostnames: ['www.example.com'],
|
|
323
|
+
caddyModuleId: 'caddy',
|
|
324
|
+
dnsManagedDomains: ['www.example.com'],
|
|
304
325
|
});
|
|
305
326
|
|
|
306
327
|
await expect(
|
|
@@ -344,6 +365,9 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
|
|
|
344
365
|
},
|
|
345
366
|
secrets: {},
|
|
346
367
|
routeOps: ops,
|
|
368
|
+
hostnames: ['www.example.com'],
|
|
369
|
+
caddyModuleId: 'caddy',
|
|
370
|
+
dnsManagedDomains: ['www.example.com'],
|
|
347
371
|
});
|
|
348
372
|
|
|
349
373
|
await cap.register_route({
|
|
@@ -375,6 +399,9 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
|
|
|
375
399
|
},
|
|
376
400
|
secrets: {},
|
|
377
401
|
routeOps: ops,
|
|
402
|
+
hostnames: ['www.example.com'],
|
|
403
|
+
caddyModuleId: 'caddy',
|
|
404
|
+
dnsManagedDomains: ['www.example.com'],
|
|
378
405
|
});
|
|
379
406
|
|
|
380
407
|
// Bad path triggers the validator inside register_route, which
|
|
@@ -407,6 +434,9 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
|
|
|
407
434
|
},
|
|
408
435
|
secrets: {},
|
|
409
436
|
routeOps: ops,
|
|
437
|
+
hostnames: ['www.example.com'],
|
|
438
|
+
caddyModuleId: 'caddy',
|
|
439
|
+
dnsManagedDomains: ['www.example.com'],
|
|
410
440
|
});
|
|
411
441
|
|
|
412
442
|
await cap.publishStaticSite({ path: '/lunacycle', sourceDir });
|
|
@@ -439,6 +469,9 @@ describe('auto-logging — end-to-end through createPublicWeb', () => {
|
|
|
439
469
|
},
|
|
440
470
|
secrets: {},
|
|
441
471
|
routeOps: ops,
|
|
472
|
+
hostnames: ['www.example.com'],
|
|
473
|
+
caddyModuleId: 'caddy',
|
|
474
|
+
dnsManagedDomains: ['www.example.com'],
|
|
442
475
|
});
|
|
443
476
|
|
|
444
477
|
await cap.register_route({
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -574,12 +574,12 @@ secrets:
|
|
|
574
574
|
});
|
|
575
575
|
|
|
576
576
|
describe('module import', () => {
|
|
577
|
-
test('should error on missing
|
|
577
|
+
test('should error on missing subcommand', async () => {
|
|
578
578
|
const result = await runCli(['node', 'celilo', 'module', 'import']);
|
|
579
579
|
|
|
580
580
|
expect(result.success).toBe(false);
|
|
581
581
|
if (result.success) return;
|
|
582
|
-
expect(result.error).toContain('
|
|
582
|
+
expect(result.error).toContain('Subcommand required');
|
|
583
583
|
});
|
|
584
584
|
|
|
585
585
|
test('should error on non-existent path', async () => {
|
|
@@ -66,6 +66,32 @@ export const COMMANDS: CommandDef[] = [
|
|
|
66
66
|
name: 'status',
|
|
67
67
|
description: 'Show system and module status',
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: 'audit',
|
|
71
|
+
description: 'Top-level alias for `system audit`',
|
|
72
|
+
flags: [
|
|
73
|
+
{
|
|
74
|
+
name: 'json',
|
|
75
|
+
description: 'Output a stable JSON schema instead of the human-readable table',
|
|
76
|
+
takesValue: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'tui',
|
|
80
|
+
description: 'Force the interactive TUI (default when stdout is a terminal)',
|
|
81
|
+
takesValue: false,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'no-tui',
|
|
85
|
+
description: 'Force the static text report even when stdout is a terminal',
|
|
86
|
+
takesValue: false,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'theme',
|
|
90
|
+
description: 'TUI color theme (dark|light); defaults to dark',
|
|
91
|
+
takesValue: true,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
69
95
|
{
|
|
70
96
|
name: 'capability',
|
|
71
97
|
description: 'View registered module capabilities',
|
|
@@ -87,8 +113,26 @@ export const COMMANDS: CommandDef[] = [
|
|
|
87
113
|
subcommands: [
|
|
88
114
|
{
|
|
89
115
|
name: 'import',
|
|
90
|
-
description: 'Import module
|
|
91
|
-
|
|
116
|
+
description: 'Import a module (file <path> | public-registry <name>)',
|
|
117
|
+
subcommands: [
|
|
118
|
+
{
|
|
119
|
+
name: 'file',
|
|
120
|
+
description: 'Import from local filesystem',
|
|
121
|
+
args: [{ name: 'path', description: 'Module path', completion: 'directories' }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'public-registry',
|
|
125
|
+
description: 'Import from celilo.computer registry',
|
|
126
|
+
args: [{ name: 'name', description: 'Module name' }],
|
|
127
|
+
flags: [
|
|
128
|
+
{
|
|
129
|
+
name: 'registry',
|
|
130
|
+
description: 'Registry URL (overrides default celilo.computer)',
|
|
131
|
+
takesValue: true,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
92
136
|
flags: [
|
|
93
137
|
{
|
|
94
138
|
name: 'target',
|
|
@@ -141,8 +185,15 @@ export const COMMANDS: CommandDef[] = [
|
|
|
141
185
|
],
|
|
142
186
|
},
|
|
143
187
|
{
|
|
188
|
+
name: 'verify',
|
|
189
|
+
description: 'Verify module integrity (signature + checksums)',
|
|
190
|
+
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
// Deprecation alias for `module verify`. Removed after one
|
|
194
|
+
// release cycle. See CELILO_UPDATE D11.
|
|
144
195
|
name: 'audit',
|
|
145
|
-
description: '
|
|
196
|
+
description: 'DEPRECATED — use `module verify` instead',
|
|
146
197
|
args: [{ name: 'id', description: 'Module ID', completion: 'module_ids' }],
|
|
147
198
|
},
|
|
148
199
|
{
|
|
@@ -277,6 +328,41 @@ export const COMMANDS: CommandDef[] = [
|
|
|
277
328
|
},
|
|
278
329
|
],
|
|
279
330
|
},
|
|
331
|
+
{
|
|
332
|
+
name: 'install',
|
|
333
|
+
description: 'Download and import a module from the registry',
|
|
334
|
+
args: [{ name: 'name', description: 'Module name' }],
|
|
335
|
+
flags: [{ name: 'registry', description: 'Registry URL', takesValue: true }],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'search',
|
|
339
|
+
description: 'Search the module registry',
|
|
340
|
+
args: [{ name: 'query', description: 'Search query (optional)' }],
|
|
341
|
+
flags: [
|
|
342
|
+
{ name: 'registry', description: 'Registry URL', takesValue: true },
|
|
343
|
+
{ name: 'limit', description: 'Max results', takesValue: true },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'publish',
|
|
348
|
+
description: 'Build and publish a module to the registry',
|
|
349
|
+
args: [{ name: 'module-dir', description: 'Module directory', completion: 'directories' }],
|
|
350
|
+
flags: [
|
|
351
|
+
{ name: 'token', description: 'Publish token', takesValue: true },
|
|
352
|
+
{ name: 'registry', description: 'Registry URL', takesValue: true },
|
|
353
|
+
{ name: 'revision', description: 'Package revision number', takesValue: true },
|
|
354
|
+
{
|
|
355
|
+
name: 'message',
|
|
356
|
+
description: 'One-line release note stamped into release.json',
|
|
357
|
+
takesValue: true,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'allow-dirty',
|
|
361
|
+
description: 'Bypass the clean-working-tree check (emergency publish)',
|
|
362
|
+
takesValue: false,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
280
366
|
],
|
|
281
367
|
},
|
|
282
368
|
{
|
|
@@ -500,6 +586,63 @@ export const COMMANDS: CommandDef[] = [
|
|
|
500
586
|
],
|
|
501
587
|
},
|
|
502
588
|
{ name: 'vault-password', description: 'Display Ansible vault password' },
|
|
589
|
+
{
|
|
590
|
+
name: 'audit',
|
|
591
|
+
description: 'Report system-wide drift (no mutations)',
|
|
592
|
+
flags: [
|
|
593
|
+
{
|
|
594
|
+
name: 'json',
|
|
595
|
+
description: 'Output a stable JSON schema instead of the human-readable table',
|
|
596
|
+
takesValue: false,
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: 'tui',
|
|
600
|
+
description: 'Force the interactive TUI (default when stdout is a terminal)',
|
|
601
|
+
takesValue: false,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: 'no-tui',
|
|
605
|
+
description: 'Force the static text report even when stdout is a terminal',
|
|
606
|
+
takesValue: false,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'theme',
|
|
610
|
+
description: 'TUI color theme (dark|light); defaults to dark',
|
|
611
|
+
takesValue: true,
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'update',
|
|
617
|
+
description: 'Bring the system to the audit-determined READY state',
|
|
618
|
+
flags: [
|
|
619
|
+
{
|
|
620
|
+
name: 'module',
|
|
621
|
+
description: 'Restrict the run to a single module',
|
|
622
|
+
takesValue: true,
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: 'dry-run',
|
|
626
|
+
description: 'Print the plan without executing',
|
|
627
|
+
takesValue: false,
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: 'no-backup',
|
|
631
|
+
description: 'Skip pre-update backups (requires explicit acknowledgement)',
|
|
632
|
+
takesValue: false,
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'allow-destructive',
|
|
636
|
+
description: 'Allow destructive terraform plans through',
|
|
637
|
+
takesValue: false,
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: 'json',
|
|
641
|
+
description: 'Output a stable JSON schema instead of the human-readable summary',
|
|
642
|
+
takesValue: false,
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
},
|
|
503
646
|
],
|
|
504
647
|
},
|
|
505
648
|
{
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Used for automated completion testing to ensure 100% coverage.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { CLIContext } from '../test-utils/cli-context';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* A node in the command tree
|
|
@@ -21,7 +21,7 @@ export interface CommandNode {
|
|
|
21
21
|
* Parser that discovers command structure from help text
|
|
22
22
|
*/
|
|
23
23
|
export class CommandTreeParser {
|
|
24
|
-
constructor(private cli:
|
|
24
|
+
constructor(private cli: CLIContext) {}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Discover all commands by parsing help output
|
|
@@ -30,15 +30,15 @@ export class CommandTreeParser {
|
|
|
30
30
|
*/
|
|
31
31
|
async discover(): Promise<Map<string, CommandNode>> {
|
|
32
32
|
// Get top-level commands
|
|
33
|
-
const
|
|
34
|
-
const topLevelCommands = this.parseHelpText(
|
|
33
|
+
const helpResult = await this.cli.run('--help');
|
|
34
|
+
const topLevelCommands = this.parseHelpText(helpResult.stdout);
|
|
35
35
|
const tree = new Map<string, CommandNode>();
|
|
36
36
|
|
|
37
37
|
// For each top-level command, discover its subcommands
|
|
38
38
|
for (const cmdName of topLevelCommands) {
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
41
|
-
const subcommandNames = this.parseHelpText(
|
|
40
|
+
const subHelpResult = await this.cli.run(`${cmdName} --help`);
|
|
41
|
+
const subcommandNames = this.parseHelpText(subHelpResult.stdout);
|
|
42
42
|
|
|
43
43
|
// Check if subcommands are actually the same as top-level commands
|
|
44
44
|
// This happens when a command like 'help' just shows the main help again
|
|
@@ -111,10 +111,11 @@ export class CommandTreeParser {
|
|
|
111
111
|
|
|
112
112
|
// Check if we're leaving the Commands section
|
|
113
113
|
// Look for major section headers that appear with little/no indentation
|
|
114
|
-
// Examples: "
|
|
114
|
+
// Examples: "Usage:", "Options:", "Description:", " For command-specific help:"
|
|
115
|
+
// NOTE: Must NOT match nested " Options:" (4-space indent) inside subcommand listings
|
|
115
116
|
if (
|
|
116
117
|
inCommandSection &&
|
|
117
|
-
/^[│\s]{0,
|
|
118
|
+
/^[│\s]{0,3}(Usage|Options|Description|Examples|For|Enable|Related):/i.test(line)
|
|
118
119
|
) {
|
|
119
120
|
inCommandSection = false;
|
|
120
121
|
continue;
|
|
@@ -10,14 +10,11 @@
|
|
|
10
10
|
import { eq } from 'drizzle-orm';
|
|
11
11
|
import { FuelGauge } from '../../cli/fuel-gauge';
|
|
12
12
|
import { getDb } from '../../db/client';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { createGaugeLogger } from '../../hooks/logger';
|
|
13
|
+
import { modules } from '../../db/schema';
|
|
14
|
+
import { createConsoleLogger, createGaugeLogger } from '../../hooks/logger';
|
|
15
|
+
import { runNamedHook } from '../../hooks/run-named-hook';
|
|
16
|
+
import type { HookName } from '../../hooks/types';
|
|
18
17
|
import type { ModuleManifest } from '../../manifest/schema';
|
|
19
|
-
import { decryptSecret } from '../../secrets/encryption';
|
|
20
|
-
import { getOrCreateMasterKey } from '../../secrets/master-key';
|
|
21
18
|
import { getArg, hasFlag, validateRequiredArgs } from '../parser';
|
|
22
19
|
import type { CommandResult } from '../types';
|
|
23
20
|
|
|
@@ -52,19 +49,15 @@ export async function handleHookRun(
|
|
|
52
49
|
const debug = hasFlag(flags, 'debug');
|
|
53
50
|
const db = getDb();
|
|
54
51
|
|
|
55
|
-
//
|
|
52
|
+
// Surface "module not found" / "hook not in manifest" with the same
|
|
53
|
+
// error messages we used to produce inline. The runNamedHook helper
|
|
54
|
+
// returns a structured result we can format here.
|
|
56
55
|
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
57
56
|
if (!module) {
|
|
58
|
-
return {
|
|
59
|
-
success: false,
|
|
60
|
-
error: `Module not found: ${moduleId}`,
|
|
61
|
-
};
|
|
57
|
+
return { success: false, error: `Module not found: ${moduleId}` };
|
|
62
58
|
}
|
|
63
|
-
|
|
64
|
-
// Find hook definition in manifest
|
|
65
59
|
const manifest = module.manifestData as ModuleManifest;
|
|
66
|
-
|
|
67
|
-
if (!hookDef) {
|
|
60
|
+
if (!manifest.hooks?.[hookName as keyof typeof manifest.hooks]) {
|
|
68
61
|
const available = Object.keys(manifest.hooks || {});
|
|
69
62
|
return {
|
|
70
63
|
success: false,
|
|
@@ -82,48 +75,12 @@ export async function handleHookRun(
|
|
|
82
75
|
}
|
|
83
76
|
}
|
|
84
77
|
|
|
85
|
-
// Build config map from DB
|
|
86
|
-
const configRecords = db
|
|
87
|
-
.select()
|
|
88
|
-
.from(moduleConfigs)
|
|
89
|
-
.where(eq(moduleConfigs.moduleId, moduleId))
|
|
90
|
-
.all();
|
|
91
|
-
const configMap: Record<string, unknown> = {};
|
|
92
|
-
for (const c of configRecords) {
|
|
93
|
-
configMap[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Build secrets map from DB
|
|
97
|
-
const secretRecords = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
|
|
98
|
-
const masterKey = await getOrCreateMasterKey();
|
|
99
|
-
const secretMap: Record<string, string> = {};
|
|
100
|
-
for (const s of secretRecords) {
|
|
101
|
-
secretMap[s.name] = decryptSecret(
|
|
102
|
-
{ encryptedValue: s.encryptedValue, iv: s.iv, authTag: s.authTag },
|
|
103
|
-
masterKey,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const requiredCapabilities = manifest.requires.capabilities.map((c) => c.name);
|
|
108
|
-
|
|
109
|
-
// Run the hook — use console logger in debug mode, FuelGauge otherwise.
|
|
110
|
-
// The logger is constructed BEFORE loadCapabilityFunctions so the
|
|
111
|
-
// auto-logging wrapper (HOOK_API_V2 D6) can capture it for every
|
|
112
|
-
// capability call.
|
|
113
78
|
if (debug) {
|
|
114
79
|
const logger = createConsoleLogger(moduleId, hookName);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
module.sourcePath,
|
|
118
|
-
hookName,
|
|
119
|
-
manifest.celilo_contract,
|
|
120
|
-
hookDef,
|
|
80
|
+
const result = await runNamedHook(moduleId, hookName as HookName, db, logger, {
|
|
81
|
+
debug: true,
|
|
121
82
|
inputs,
|
|
122
|
-
|
|
123
|
-
secretMap,
|
|
124
|
-
logger,
|
|
125
|
-
{ debug, capabilities: capabilityFunctions, requiredCapabilities },
|
|
126
|
-
);
|
|
83
|
+
});
|
|
127
84
|
|
|
128
85
|
if (!result.success) {
|
|
129
86
|
let errorMsg = result.error || 'Hook execution failed';
|
|
@@ -144,19 +101,11 @@ export async function handleHookRun(
|
|
|
144
101
|
const gauge = new FuelGauge(`Running hook: ${hookName}`);
|
|
145
102
|
gauge.start();
|
|
146
103
|
const logger = createGaugeLogger(gauge, moduleId, hookName);
|
|
147
|
-
const capabilityFunctions = await loadCapabilityFunctions(moduleId, db, logger);
|
|
148
104
|
|
|
149
|
-
const result = await
|
|
150
|
-
|
|
151
|
-
hookName,
|
|
152
|
-
manifest.celilo_contract,
|
|
153
|
-
hookDef,
|
|
105
|
+
const result = await runNamedHook(moduleId, hookName as HookName, db, logger, {
|
|
106
|
+
debug: false,
|
|
154
107
|
inputs,
|
|
155
|
-
|
|
156
|
-
secretMap,
|
|
157
|
-
logger,
|
|
158
|
-
{ debug: false, capabilities: capabilityFunctions, requiredCapabilities },
|
|
159
|
-
);
|
|
108
|
+
});
|
|
160
109
|
|
|
161
110
|
if (!result.success) {
|
|
162
111
|
gauge.stop(false);
|