@celilo/cli 1.3.0 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,7 +58,7 @@
58
58
  "dependencies": {
59
59
  "@aws-sdk/client-s3": "^3.1109.0",
60
60
  "@aws-sdk/lib-storage": "^3.1101.0",
61
- "@celilo/capabilities": "^2.1.0",
61
+ "@celilo/capabilities": "^2.2.0",
62
62
  "@celilo/cli-display": "^0.2.0",
63
63
  "@celilo/core": "^0.9.0",
64
64
  "@celilo/event-bus": "^0.6.0",
@@ -6,70 +6,22 @@
6
6
  * (e.g., container_created triggers web automation for API key creation).
7
7
  */
8
8
 
9
- import type { DeployedSystem } from '@celilo/capabilities';
9
+ import type { HookDefinition, HookResult as ModuleHookResult } from '@celilo/capabilities';
10
10
 
11
11
  /**
12
- * Hook definition from module manifest.
12
+ * `HookDefinition`, `HookLogger` and `HookContext` are re-exported from
13
+ * `@celilo/capabilities`, which owns them. All three were declared a second
14
+ * time here and the `HookLogger` copy said so in its own comment ("kept in
15
+ * sync") — a duplicate that announces it needs syncing is one that will
16
+ * eventually not be.
13
17
  *
14
- * Inputs and outputs are no longer carried by the manifest they live in the
15
- * contract registry (see `apps/celilo/src/manifest/contracts/v1.ts`),
16
- * keyed by the manifest's `celilo_contract` version. The executor resolves
17
- * the signature at runtime.
18
+ * A second declaration of a shared shape does not fail loudly when it drifts:
19
+ * both sides keep compiling and the two meanings diverge silently. `HookName`
20
+ * was the same duplication and HAD already drifted — it was missing
21
+ * `reconcile_routes`, so caddy's reconcile hook was a hook the celilo side of
22
+ * the codebase did not believe in (celilo#821).
18
23
  */
19
- export interface HookDefinition {
20
- /** Path to hook script relative to module directory */
21
- script: string;
22
- /** Total timeout in milliseconds (default: 60000) */
23
- timeout?: number;
24
- /** If true, hook needs interactive terminal (no FuelGauge wrapping) */
25
- interactive?: boolean;
26
- }
27
-
28
- /**
29
- * Logger interface provided to hook scripts.
30
- *
31
- * Mirrors `HookLogger` in `@celilo/capabilities/types` — kept in sync so
32
- * concrete loggers we build here (createGaugeLogger, createConsoleLogger,
33
- * createCapturingLogger) satisfy both the in-process consumers and the
34
- * cross-package contract used by `wrapWithLogging`.
35
- */
36
- export interface HookLogger {
37
- info(message: string): void;
38
- warn(message: string): void;
39
- error(message: string): void;
40
- success(message: string): void;
41
- /** Begin a logical span. Subsequent log calls nest under it visually. */
42
- beginStep?(name: string): void;
43
- /** End the current span (success). */
44
- endStep?(name: string): void;
45
- /** End the current span (failure) with an error message. */
46
- failStep?(name: string, error: string): void;
47
- }
48
-
49
- /**
50
- * Context passed to hook scripts
51
- */
52
- export interface HookContext {
53
- /** Module configuration values */
54
- config: Record<string, unknown>;
55
- /** Module secret values (decrypted) */
56
- secrets: Record<string, string>;
57
- /**
58
- * The 0..N systems this module has deployed onto (openspec/specs/module-systems-addressing/spec.md).
59
- * Always an array — no singular convenience, so the 0/1/N reality stays visible.
60
- */
61
- systems: DeployedSystem[];
62
- /** Logger for reporting progress */
63
- logger: HookLogger;
64
- /** Run in debug mode (e.g., Playwright headless: false) */
65
- debug: boolean;
66
- /** Directory for saving screenshots on failure */
67
- screenshotDir: string;
68
- /** Capability function interfaces from provider modules (e.g., context.capabilities.dns_registrar) */
69
- capabilities: Record<string, unknown>;
70
- /** Dynamic inputs specified in manifest (e.g., vps_ip) */
71
- [key: string]: unknown;
72
- }
24
+ export type { HookDefinition, HookLogger, HookContext } from '@celilo/capabilities';
73
25
 
74
26
  /**
75
27
  * Surfaced by `invokeHook` when a capability call inside the hook threw
@@ -85,21 +37,15 @@ export interface MissingProviderInputDetails {
85
37
  }
86
38
 
87
39
  /**
88
- * Result returned from hook execution
40
+ * Result returned from hook execution.
41
+ *
42
+ * The shape a module's hook produces is `@celilo/capabilities`' `HookResult`;
43
+ * this adds the one field only the framework sets. A module never writes
44
+ * `missingProviderInput` — the executor sets it when a capability call throws
45
+ * `MissingProviderInputError` — so it belongs on the framework's view of the
46
+ * result rather than in the module-facing contract.
89
47
  */
90
- export interface HookResult {
91
- success: boolean;
92
- outputs: Record<string, unknown>;
93
- error?: string;
94
- /**
95
- * Every file the hook wrote to its per-run artifact directory.
96
- * All of them, not just the newest image: a screenshot without the
97
- * DOM and the observed requests is the least useful third of a
98
- * post-mortem.
99
- */
100
- artifactPaths?: string[];
101
- /** Duration in milliseconds */
102
- duration: number;
48
+ export interface HookResult extends ModuleHookResult {
103
49
  /**
104
50
  * Set when the hook failed because a capability call needs the
105
51
  * framework to extend another module's config. Mutually exclusive
@@ -261,6 +261,41 @@ export const V1_HOOKS: ContractHooks = {
261
261
  inputs: {},
262
262
  outputs: {},
263
263
  },
264
+ /**
265
+ * Report what a tunnel/interface is ACTUALLY carrying, diffed against what
266
+ * celilo's config says it should carry.
267
+ *
268
+ * Deliberately a read of the live thing rather than of celilo's own record.
269
+ * celilo#928 was exactly a divergence between the two — a device present in
270
+ * config, rendered as active by a UI, and absent from the interface — and
271
+ * diagnosing it needed a screenshot and a root SSH because nothing celilo
272
+ * offered could tell the two apart. A hook that reported the record would
273
+ * have reproduced the misleading signal rather than exposing it.
274
+ *
275
+ * No structured outputs: it reports through the logger, so an operator and an
276
+ * agent read the same thing.
277
+ */
278
+ list_peers: {
279
+ inputs: {},
280
+ outputs: {},
281
+ },
282
+ /**
283
+ * Re-assert a module's desired peer set onto the thing carrying it, on a
284
+ * timer, without an operator.
285
+ *
286
+ * The counterpart to applying on change: apply-on-change bounds latency, this
287
+ * bounds how long ANY divergence can persist — including drift nobody
288
+ * predicted (a hand-edit on the box, an apply that failed, a record written
289
+ * before the applying code existed). celilo#934, where the module could
290
+ * already SEE the drift in `health_check` and had nowhere to act on it.
291
+ *
292
+ * A converge that changes nothing must be silent, so the log stays a record
293
+ * of things happening.
294
+ */
295
+ reconcile_peers: {
296
+ inputs: {},
297
+ outputs: {},
298
+ },
264
299
  /**
265
300
  * Build-bus upstream publish hook. The executor passes the
266
301
  * PublishEvent fields as env vars (CELILO_EVENT_PAYLOAD,
@@ -364,6 +364,8 @@ const LIFECYCLE_HOOK_SCHEMAS = {
364
364
  refresh_registrations: LifecycleHookSchema.optional(),
365
365
  reassert_dhcp_dns: LifecycleHookSchema.optional(),
366
366
  reconcile_clients: LifecycleHookSchema.optional(),
367
+ list_peers: LifecycleHookSchema.optional(),
368
+ reconcile_peers: LifecycleHookSchema.optional(),
367
369
  } satisfies Record<HookName, z.ZodTypeAny>;
368
370
 
369
371
  /**
@@ -11,6 +11,19 @@
11
11
  * refer to that document's sections 1-4 — read the row before changing a line
12
12
  * here.
13
13
  *
14
+ * **Every debt entry names the issue tracking its removal.** The audit is a
15
+ * ruling; the issues are the work. If you are here because the gate went red,
16
+ * the issue in the entry's `why` is where the fix belongs:
17
+ *
18
+ * #937 the hardcoded well-known registry + its duplicate zone table
19
+ * #938 the loader's capName branches, CAPABILITY_MODULE_MAP, isFirewallModule
20
+ * #939 the four provider-domain tables
21
+ * #940 the Caddyfile generator + interface classification in the shared package
22
+ * #941 core shelling iptables-save
23
+ * #943 the three capability-named CLI verbs
24
+ * #944 core shelling wg pubkey
25
+ * #945 three core primitives gated on one capability (generalise, don't move)
26
+ *
14
27
  * Two kinds of entry live here and they must not be confused:
15
28
  *
16
29
  * - **Debt** — the default. It is here because it is wrong and not yet fixed.
@@ -25,6 +38,9 @@
25
38
  * each belongs to. Scan A asserts the tagged set EQUALS this map — not that it
26
39
  * is empty. These four exist today (audit T1, T2, T3, T6) and Phase 1 migrates
27
40
  * nothing; an assertion that core holds none would be red the day it landed.
41
+ *
42
+ * Tracked by #939. Migrating them keeps the ownership CLAIM in core — dropping
43
+ * it is the mistake that made `dns_registration_consumers` necessary (#626).
28
44
  */
29
45
  export const CAPABILITY_OWNED_TABLES: Readonly<Record<string, string>> = {
30
46
  web_routes: 'public_web',
@@ -68,25 +84,25 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
68
84
  file: 'apps/celilo/src/capabilities/well-known.ts',
69
85
  capability: 'dhcp_server',
70
86
  count: 1,
71
- why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2',
87
+ why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2 (#937)',
72
88
  },
73
89
  {
74
90
  file: 'apps/celilo/src/capabilities/well-known.ts',
75
91
  capability: 'dns_internal',
76
92
  count: 1,
77
- why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2',
93
+ why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2 (#937)',
78
94
  },
79
95
  {
80
96
  file: 'apps/celilo/src/capabilities/well-known.ts',
81
97
  capability: 'dns_registrar',
82
98
  count: 1,
83
- why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2',
99
+ why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2 (#937)',
84
100
  },
85
101
  {
86
102
  file: 'apps/celilo/src/capabilities/well-known.ts',
87
103
  capability: 'public_web',
88
104
  count: 1,
89
- why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2',
105
+ why: 'X1 — hardcoded canonical hostnames, required zones and literal ports; Phase 2 (#937)',
90
106
  },
91
107
  {
92
108
  file: 'apps/celilo/src/cli/commands/notify-config.ts',
@@ -98,91 +114,91 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
98
114
  file: 'apps/celilo/src/cli/commands/token.ts',
99
115
  capability: 'idp',
100
116
  count: 1,
101
- why: 'X7 — a top-level CLI verb backed by the idp capability; Phase 5',
117
+ why: 'X7 — a top-level CLI verb backed by the idp capability; Phase 5 (#943)',
102
118
  },
103
119
  {
104
120
  file: 'apps/celilo/src/cli/completion.ts',
105
121
  capability: 'firewall',
106
122
  count: 3,
107
- why: 'X6 — tab completion for the capability-named CLI verbs; falls out with X6',
123
+ why: 'X6 — tab completion for the capability-named CLI verbs; falls out with X6 (#943)',
108
124
  },
109
125
  {
110
126
  file: 'apps/celilo/src/cli/index.ts',
111
127
  capability: 'firewall',
112
128
  count: 2,
113
- why: 'X5/X6/X7 — the command tree wiring the capability-named CLI verbs; Phase 5',
129
+ why: 'X5/X6/X7 — the command tree wiring the capability-named CLI verbs; Phase 5 (#943)',
114
130
  },
115
131
  {
116
132
  file: 'apps/celilo/src/hooks/capability-loader.ts',
117
133
  capability: 'control_plane_vpn',
118
134
  count: 1,
119
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
135
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
120
136
  },
121
137
  {
122
138
  file: 'apps/celilo/src/hooks/capability-loader.ts',
123
139
  capability: 'dhcp_server',
124
140
  count: 1,
125
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
141
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
126
142
  },
127
143
  {
128
144
  file: 'apps/celilo/src/hooks/capability-loader.ts',
129
145
  capability: 'dns_internal',
130
146
  count: 3,
131
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
147
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
132
148
  },
133
149
  {
134
150
  file: 'apps/celilo/src/hooks/capability-loader.ts',
135
151
  capability: 'dns_registrar',
136
152
  count: 4,
137
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
153
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
138
154
  },
139
155
  {
140
156
  file: 'apps/celilo/src/hooks/capability-loader.ts',
141
157
  capability: 'external_web',
142
158
  count: 1,
143
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
159
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
144
160
  },
145
161
  {
146
162
  file: 'apps/celilo/src/hooks/capability-loader.ts',
147
163
  capability: 'firewall',
148
164
  count: 12,
149
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
165
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
150
166
  },
151
167
  {
152
168
  file: 'apps/celilo/src/hooks/capability-loader.ts',
153
169
  capability: 'idp',
154
170
  count: 1,
155
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
171
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
156
172
  },
157
173
  {
158
174
  file: 'apps/celilo/src/hooks/capability-loader.ts',
159
175
  capability: 'notification',
160
176
  count: 1,
161
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
177
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
162
178
  },
163
179
  {
164
180
  file: 'apps/celilo/src/hooks/capability-loader.ts',
165
181
  capability: 'private_web',
166
182
  count: 1,
167
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
183
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
168
184
  },
169
185
  {
170
186
  file: 'apps/celilo/src/hooks/capability-loader.ts',
171
187
  capability: 'public_web',
172
188
  count: 8,
173
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
189
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
174
190
  },
175
191
  {
176
192
  file: 'apps/celilo/src/hooks/capability-loader.ts',
177
193
  capability: 'registry_publish',
178
194
  count: 1,
179
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
195
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
180
196
  },
181
197
  {
182
198
  file: 'apps/celilo/src/hooks/capability-loader.ts',
183
199
  capability: 'source_forge',
184
200
  count: 1,
185
- why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3',
201
+ why: 'L1-L12 — core branches on the capability NAME; becomes a provider declaration in Phase 3 (#938)',
186
202
  },
187
203
  {
188
204
  file: 'apps/celilo/src/manifest/validate.ts',
@@ -212,49 +228,49 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
212
228
  file: 'apps/celilo/src/services/dns-provider-backfill.ts',
213
229
  capability: 'dns_internal',
214
230
  count: 2,
215
- why: 'S3 — the generic event-replay half is core; the dns_internal gate is not',
231
+ why: 'S3 — the generic event-replay half is core; the dns_internal gate is not (#945)',
216
232
  },
217
233
  {
218
234
  file: 'apps/celilo/src/services/firewall-reach.ts',
219
235
  capability: 'firewall',
220
236
  count: 1,
221
- why: 'S8 — core shells iptables-save at a remote box and parses the output',
237
+ why: 'S8 — core shells iptables-save at a remote box and parses the output (#941)',
222
238
  },
223
239
  {
224
240
  file: 'apps/celilo/src/services/infrastructure-selector.ts',
225
241
  capability: 'firewall',
226
242
  count: 1,
227
- why: 'S15 — isFirewallModule hardcodes a placement rule the manifest should declare',
243
+ why: 'S15 — isFirewallModule hardcodes a placement rule the manifest should declare (#938)',
228
244
  },
229
245
  {
230
246
  file: 'apps/celilo/src/services/module-deploy.ts',
231
247
  capability: 'firewall',
232
248
  count: 2,
233
- why: "S16 — two more copies of S15's decision; fixing S15 removes all three",
249
+ why: "S16 — two more copies of S15's decision; fixing S15 removes all three (#938)",
234
250
  },
235
251
  {
236
252
  file: 'apps/celilo/src/services/public-web-republish.ts',
237
253
  capability: 'public_web',
238
254
  count: 1,
239
- why: "S2 — encodes caddy's redeploy behaviour; generalise to a provider-declared re-assert signal",
255
+ why: "S2 — encodes caddy's redeploy behaviour; generalise to a provider-declared re-assert signal (#945)",
240
256
  },
241
257
  {
242
258
  file: 'apps/celilo/src/services/zone-policy.ts',
243
259
  capability: 'public_web',
244
260
  count: 1,
245
- why: 'S13 — ZONE_REQUIREMENTS, a second hand-maintained copy of well-known.ts; Phase 2',
261
+ why: 'S13 — ZONE_REQUIREMENTS, a second hand-maintained copy of well-known.ts; Phase 2 (#937)',
246
262
  },
247
263
  {
248
264
  file: 'apps/celilo/src/templates/generator.ts',
249
265
  capability: 'dns_internal',
250
266
  count: 1,
251
- why: 'X4 — declaration-driven and the model for the rest; only the error string names the capability',
267
+ why: 'X4 — declaration-driven and the model for the rest; only the error string names the capability (#945)',
252
268
  },
253
269
  {
254
270
  file: 'apps/celilo/src/variables/context.ts',
255
271
  capability: 'dns_internal',
256
272
  count: 1,
257
- why: 'X3 — reaches capabilitiesMap.dns_internal by name; should read a declared field (X4 is the model)',
273
+ why: 'X3 — reaches capabilitiesMap.dns_internal by name; should read a declared field (X4 is the model) (#945)',
258
274
  },
259
275
  {
260
276
  file: 'packages/capabilities/src/capability-contract.ts',
@@ -344,13 +360,13 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
344
360
  file: 'packages/capabilities/src/public-web.ts',
345
361
  capability: 'public_web',
346
362
  count: 1,
347
- why: "X8 — generateCaddyfile renders caddy's complete config from the shared package; Phase 5",
363
+ why: "X8 — generateCaddyfile renders caddy's complete config from the shared package; Phase 5 (#940)",
348
364
  },
349
365
  {
350
366
  file: 'packages/capabilities/src/utils.ts',
351
367
  capability: 'public_web',
352
368
  count: 3,
353
- why: 'X8 — shared helpers that name the capability they serve; moves with X8 in Phase 5',
369
+ why: 'X8 — shared helpers that name the capability they serve; moves with X8 in Phase 5 (#940)',
354
370
  },
355
371
  ];
356
372
 
@@ -368,17 +384,17 @@ export const SERVICE_FILENAME_BASELINE: readonly ServiceFilenameRow[] = [
368
384
  {
369
385
  file: 'apps/celilo/src/services/dns-internal-records.ts',
370
386
  capability: 'dns_internal',
371
- why: 'S5 — the store for T6; moves with the table in Phase 4',
387
+ why: 'S5 — the store for T6; moves with the table in Phase 4 (#939)',
372
388
  },
373
389
  {
374
390
  file: 'apps/celilo/src/services/firewall-reach.ts',
375
391
  capability: 'firewall',
376
- why: 'S8 — core reaching into one provider implementation',
392
+ why: 'S8 — core reaching into one provider implementation (#941)',
377
393
  },
378
394
  {
379
395
  file: 'apps/celilo/src/services/public-web-republish.ts',
380
396
  capability: 'public_web',
381
- why: "S2 — named for one provider's redeploy behaviour",
397
+ why: "S2 — named for one provider's redeploy behaviour (#945)",
382
398
  },
383
399
  ];
384
400
 
@@ -387,18 +403,18 @@ export const PROVIDER_LITERAL_BASELINE: readonly ProviderLiteralRow[] = [
387
403
  file: 'apps/celilo/src/secrets/generators.ts',
388
404
  literal: 'wg pubkey',
389
405
  count: 1,
390
- why: 'X11 — core shells `wg pubkey` to derive a key, and tells the operator to brew install wireguard-tools',
406
+ why: 'X11 — core shells `wg pubkey` to derive a key, and tells the operator to brew install wireguard-tools (#944)',
391
407
  },
392
408
  {
393
409
  file: 'apps/celilo/src/services/firewall-reach.ts',
394
410
  literal: 'iptables-save',
395
411
  count: 1,
396
- why: "S8 — core parses a firewall's live ruleset; implementation-specific, not merely capability-specific",
412
+ why: "S8 — core parses a firewall's live ruleset; implementation-specific, not merely capability-specific (#941)",
397
413
  },
398
414
  {
399
415
  file: 'packages/capabilities/src/public-web.ts',
400
416
  literal: '/srv/www',
401
417
  count: 4,
402
- why: "X8 — the Caddyfile generator knows caddy's on-disk asset layout",
418
+ why: "X8 — the Caddyfile generator knows caddy's on-disk asset layout (#940)",
403
419
  },
404
420
  ];