@celilo/cli 0.1.4 → 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.
Files changed (161) hide show
  1. package/drizzle/0004_caddy_hostname_list.sql +25 -0
  2. package/drizzle/meta/_journal.json +14 -0
  3. package/package.json +9 -2
  4. package/src/ansible/inventory.test.ts +9 -8
  5. package/src/ansible/inventory.ts +9 -7
  6. package/src/capabilities/public-web-helpers.test.ts +2 -2
  7. package/src/capabilities/public-web-publish.test.ts +45 -12
  8. package/src/capabilities/registration.test.ts +6 -6
  9. package/src/capabilities/well-known.test.ts +2 -2
  10. package/src/capabilities/well-known.ts +5 -5
  11. package/src/cli/cli.test.ts +2 -2
  12. package/src/cli/command-registry.ts +146 -3
  13. package/src/cli/command-tree-parser.test.ts +1 -1
  14. package/src/cli/command-tree-parser.ts +9 -8
  15. package/src/cli/commands/hook-run.ts +15 -66
  16. package/src/cli/commands/module-audit.ts +14 -44
  17. package/src/cli/commands/module-deploy.ts +4 -1
  18. package/src/cli/commands/module-import-registry.test.ts +115 -0
  19. package/src/cli/commands/module-import.ts +106 -22
  20. package/src/cli/commands/module-publish.test.ts +235 -0
  21. package/src/cli/commands/module-publish.ts +234 -0
  22. package/src/cli/commands/module-remove.ts +82 -2
  23. package/src/cli/commands/module-search.ts +57 -0
  24. package/src/cli/commands/module-secret-get.ts +59 -0
  25. package/src/cli/commands/module-show.ts +1 -1
  26. package/src/cli/commands/module-terraform-unlock.ts +57 -0
  27. package/src/cli/commands/module-verify.test.ts +59 -0
  28. package/src/cli/commands/module-verify.ts +53 -0
  29. package/src/cli/commands/status.ts +30 -20
  30. package/src/cli/commands/system-audit.test.ts +138 -0
  31. package/src/cli/commands/system-audit.ts +571 -0
  32. package/src/cli/commands/system-update.ts +391 -0
  33. package/src/cli/completion.ts +15 -1
  34. package/src/cli/fuel-gauge.ts +68 -3
  35. package/src/cli/generate-zsh-completion.ts +13 -3
  36. package/src/cli/index.ts +112 -5
  37. package/src/cli/parser.ts +11 -0
  38. package/src/cli/prompts.ts +36 -5
  39. package/src/cli/tui/audit-state.test.ts +246 -0
  40. package/src/cli/tui/audit-state.ts +525 -0
  41. package/src/cli/tui/audit-tui.test.tsx +135 -0
  42. package/src/cli/tui/audit-tui.tsx +624 -0
  43. package/src/cli/tui/celebration.tsx +29 -0
  44. package/src/cli/tui/clipboard.test.ts +94 -0
  45. package/src/cli/tui/clipboard.ts +101 -0
  46. package/src/cli/tui/icons.ts +22 -0
  47. package/src/cli/tui/keybar.tsx +65 -0
  48. package/src/cli/tui/keymap.test.ts +105 -0
  49. package/src/cli/tui/keymap.ts +70 -0
  50. package/src/cli/tui/modals/analyzing.tsx +75 -0
  51. package/src/cli/tui/modals/celebration.tsx +44 -0
  52. package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
  53. package/src/cli/tui/modals/remediate.tsx +44 -0
  54. package/src/cli/tui/modals.test.ts +137 -0
  55. package/src/cli/tui/mouse.test.ts +78 -0
  56. package/src/cli/tui/mouse.ts +114 -0
  57. package/src/cli/tui/panes/categories.tsx +62 -0
  58. package/src/cli/tui/panes/command-log.tsx +87 -0
  59. package/src/cli/tui/panes/detail.tsx +175 -0
  60. package/src/cli/tui/panes/findings.tsx +97 -0
  61. package/src/cli/tui/panes/summary.tsx +64 -0
  62. package/src/cli/tui/spawn.ts +130 -0
  63. package/src/cli/tui/theme.ts +42 -0
  64. package/src/cli/tui/wrap.test.ts +43 -0
  65. package/src/cli/tui/wrap.ts +45 -0
  66. package/src/cli/types.ts +5 -0
  67. package/src/db/client.ts +55 -2
  68. package/src/db/schema.test.ts +3 -3
  69. package/src/db/schema.ts +26 -17
  70. package/src/hooks/capability-loader.ts +135 -72
  71. package/src/hooks/define-hook.test.ts +11 -3
  72. package/src/hooks/executor.ts +22 -1
  73. package/src/hooks/load-hook-config.test.ts +165 -0
  74. package/src/hooks/load-hook-config.ts +60 -0
  75. package/src/hooks/logger.ts +42 -12
  76. package/src/hooks/run-named-hook.ts +128 -0
  77. package/src/hooks/types.ts +19 -0
  78. package/src/manifest/ensure-schema.test.ts +115 -0
  79. package/src/manifest/schema.ts +76 -0
  80. package/src/manifest/template-validator.test.ts +1 -1
  81. package/src/manifest/template-validator.ts +1 -1
  82. package/src/manifest/validate.test.ts +1 -1
  83. package/src/module/import.ts +20 -12
  84. package/src/module/packaging/build.ts +121 -25
  85. package/src/module/packaging/release-metadata.test.ts +103 -0
  86. package/src/module/packaging/release-metadata.ts +145 -0
  87. package/src/registry/client.test.ts +228 -0
  88. package/src/registry/client.ts +157 -0
  89. package/src/services/audit/backups.test.ts +233 -0
  90. package/src/services/audit/backups.ts +128 -0
  91. package/src/services/audit/capability-abi.test.ts +153 -0
  92. package/src/services/audit/capability-abi.ts +204 -0
  93. package/src/services/audit/cli-version.test.ts +60 -0
  94. package/src/services/audit/cli-version.ts +87 -0
  95. package/src/services/audit/health.test.ts +84 -0
  96. package/src/services/audit/health.ts +43 -0
  97. package/src/services/audit/index.test.ts +99 -0
  98. package/src/services/audit/index.ts +118 -0
  99. package/src/services/audit/machines-reachable.test.ts +87 -0
  100. package/src/services/audit/machines-reachable.ts +87 -0
  101. package/src/services/audit/module-configs.test.ts +131 -0
  102. package/src/services/audit/module-configs.ts +80 -0
  103. package/src/services/audit/module-versions.test.ts +99 -0
  104. package/src/services/audit/module-versions.ts +154 -0
  105. package/src/services/audit/schema.test.ts +68 -0
  106. package/src/services/audit/schema.ts +115 -0
  107. package/src/services/audit/secrets-decryptable.test.ts +82 -0
  108. package/src/services/audit/secrets-decryptable.ts +97 -0
  109. package/src/services/audit/services-credentials.test.ts +54 -0
  110. package/src/services/audit/services-credentials.ts +64 -0
  111. package/src/services/audit/services-reachable.test.ts +60 -0
  112. package/src/services/audit/services-reachable.ts +64 -0
  113. package/src/services/audit/terraform-plan.test.ts +127 -0
  114. package/src/services/audit/terraform-plan.ts +153 -0
  115. package/src/services/audit/types.test.ts +36 -0
  116. package/src/services/audit/types.ts +90 -0
  117. package/src/services/audit/unconfigured-modules.test.ts +48 -0
  118. package/src/services/audit/unconfigured-modules.ts +71 -0
  119. package/src/services/audit/undeployed-modules.test.ts +66 -0
  120. package/src/services/audit/undeployed-modules.ts +72 -0
  121. package/src/services/build-stream.ts +122 -122
  122. package/src/services/config-interview.ts +407 -2
  123. package/src/services/deploy-ansible.ts +73 -7
  124. package/src/services/deploy-planner.ts +5 -5
  125. package/src/services/deploy-preflight.ts +45 -4
  126. package/src/services/deploy-terraform.ts +31 -24
  127. package/src/services/deploy-validation.ts +167 -23
  128. package/src/services/dns-auto-register.ts +4 -4
  129. package/src/services/ensure-interview.test.ts +245 -0
  130. package/src/services/health-runner.ts +110 -38
  131. package/src/services/infrastructure-variable-resolver.test.ts +1 -1
  132. package/src/services/infrastructure-variable-resolver.ts +3 -3
  133. package/src/services/module-build.ts +11 -13
  134. package/src/services/module-deploy.ts +372 -61
  135. package/src/services/proxmox-state-recovery.ts +6 -6
  136. package/src/services/ssh-key-manager.test.ts +1 -1
  137. package/src/services/ssh-key-manager.ts +3 -2
  138. package/src/services/terraform-env.ts +62 -0
  139. package/src/services/update/dep-graph.test.ts +214 -0
  140. package/src/services/update/dep-graph.ts +215 -0
  141. package/src/services/update/orchestrator.test.ts +463 -0
  142. package/src/services/update/orchestrator.ts +359 -0
  143. package/src/services/update/progress.ts +49 -0
  144. package/src/services/update/self-update.test.ts +68 -0
  145. package/src/services/update/self-update.ts +57 -0
  146. package/src/services/update/types.ts +94 -0
  147. package/src/templates/generator.test.ts +3 -3
  148. package/src/templates/generator.ts +43 -2
  149. package/src/test-utils/completion-harness.test.ts +1 -1
  150. package/src/test-utils/completion-harness.ts +4 -4
  151. package/src/variables/capability-self-ref.test.ts +203 -0
  152. package/src/variables/context.test.ts +31 -31
  153. package/src/variables/context.ts +65 -17
  154. package/src/variables/declarative-derivation.test.ts +306 -0
  155. package/src/variables/declarative-derivation.ts +4 -2
  156. package/src/variables/parser.test.ts +64 -9
  157. package/src/variables/parser.ts +47 -6
  158. package/src/variables/resolver.test.ts +14 -14
  159. package/src/variables/resolver.ts +27 -9
  160. package/src/variables/types.ts +1 -1
  161. package/tsconfig.json +1 -0
@@ -8,15 +8,15 @@
8
8
  import { eq } from 'drizzle-orm';
9
9
  import { FuelGauge } from '../cli/fuel-gauge';
10
10
  import type { DbClient } from '../db/client';
11
- import { moduleInfrastructure, modules } from '../db/schema';
11
+ import { modules } from '../db/schema';
12
12
  import { loadCapabilityFunctions } from '../hooks/capability-loader';
13
13
  import { invokeHook } from '../hooks/executor';
14
- import { createConsoleLogger, createGaugeLogger } from '../hooks/logger';
14
+ import { loadHookConfigMap } from '../hooks/load-hook-config';
15
+ import { createCapturingLogger, createConsoleLogger, createGaugeLogger } from '../hooks/logger';
15
16
  import type { HookResult } from '../hooks/types';
16
17
  import type { ModuleManifest } from '../manifest/schema';
17
18
  import { decryptSecret } from '../secrets/encryption';
18
19
  import { getOrCreateMasterKey } from '../secrets/master-key';
19
- import { getMachine } from './machine-pool';
20
20
 
21
21
  export interface HealthCheckItem {
22
22
  name: string;
@@ -34,11 +34,19 @@ export interface HealthCheckResult {
34
34
 
35
35
  /**
36
36
  * Run health checks for a single module
37
+ *
38
+ * `onProgress` (when supplied) replaces the FuelGauge animation. Used
39
+ * by the audit TUI to stream progress messages into its own UI rather
40
+ * than letting them leak to stdout before the alt-screen takes over.
37
41
  */
38
42
  export async function runModuleHealthCheck(
39
43
  moduleId: string,
40
44
  db: DbClient,
41
- options: { debug?: boolean; noInteractive?: boolean } = {},
45
+ options: {
46
+ debug?: boolean;
47
+ noInteractive?: boolean;
48
+ onProgress?: (msg: string) => void;
49
+ } = {},
42
50
  ): Promise<HealthCheckResult> {
43
51
  const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
44
52
  if (!module) {
@@ -52,14 +60,13 @@ export async function runModuleHealthCheck(
52
60
  return { moduleId, status: 'no-checks', checks: [] };
53
61
  }
54
62
 
55
- // Build config and secrets for hook context
56
- const { moduleConfigs, secrets } = await import('../db/schema');
57
- const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
58
- const configMap: Record<string, unknown> = {};
59
- for (const c of configs) {
60
- configMap[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
61
- }
63
+ // Build config + secrets for hook context. The config map shape
64
+ // (including the machine-IP fallback for machine deploys) lives in
65
+ // a single helper so health_check, on_install/on_uninstall via
66
+ // run-named-hook, and capability-loader all see the same thing.
67
+ const configMap = await loadHookConfigMap(moduleId, db);
62
68
 
69
+ const { secrets } = await import('../db/schema');
63
70
  const secretRecords = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
64
71
  const masterKey = await getOrCreateMasterKey();
65
72
  const secretMap: Record<string, string> = {};
@@ -70,20 +77,6 @@ export async function runModuleHealthCheck(
70
77
  );
71
78
  }
72
79
 
73
- // Inject machine IP for machine-based deployments
74
- const infraRecord = db
75
- .select()
76
- .from(moduleInfrastructure)
77
- .where(eq(moduleInfrastructure.moduleId, moduleId))
78
- .get();
79
- if (infraRecord?.machineId) {
80
- const machine = await getMachine(infraRecord.machineId);
81
- if (machine) {
82
- configMap['ip.primary'] = machine.ipAddress;
83
- configMap.container_ip = machine.ipAddress;
84
- }
85
- }
86
-
87
80
  const requiredCapabilities = manifest.requires.capabilities.map((c) => c.name);
88
81
 
89
82
  // Run the hook. Logger is constructed BEFORE loadCapabilityFunctions
@@ -104,6 +97,25 @@ export async function runModuleHealthCheck(
104
97
  logger,
105
98
  { debug: true, capabilities: capabilityFunctions, requiredCapabilities },
106
99
  );
100
+ } else if (options.onProgress) {
101
+ // TUI mode: emit a progress message instead of drawing FuelGauge
102
+ // (which writes to stdout and would corrupt the alt-screen render).
103
+ // Use a capturing logger to absorb hook-level log lines that would
104
+ // otherwise leak to stdout via createConsoleLogger.
105
+ options.onProgress(`Checking ${moduleId}`);
106
+ const { logger } = createCapturingLogger();
107
+ const capabilityFunctions = await loadCapabilityFunctions(moduleId, db, logger);
108
+ hookResult = await invokeHook(
109
+ module.sourcePath,
110
+ 'health_check',
111
+ manifest.celilo_contract,
112
+ hookDef,
113
+ {},
114
+ configMap,
115
+ secretMap,
116
+ logger,
117
+ { debug: false, capabilities: capabilityFunctions, requiredCapabilities },
118
+ );
107
119
  } else {
108
120
  const gauge = new FuelGauge(`Checking ${moduleId}`, {
109
121
  skipAnimation: options.noInteractive,
@@ -162,23 +174,83 @@ export async function runModuleHealthCheck(
162
174
  }
163
175
 
164
176
  /**
165
- * Run health checks for all deployed modules
177
+ * Cap on concurrent health checks. Picked small on purpose — the
178
+ * waits are SSH-bound, but each invocation also spins up a hook
179
+ * subprocess locally, and an unbounded fan-out can swamp a low-spec
180
+ * machine or a slow link. Four lets us overlap most network waits
181
+ * without thrashing.
182
+ */
183
+ const HEALTH_CHECK_CONCURRENCY = 4;
184
+
185
+ /**
186
+ * Run `worker` over `items`, with at most `cap` in flight at a time.
187
+ * Results land in input order. `worker` errors are wrapped by the
188
+ * caller — this helper does not catch them itself.
189
+ */
190
+ async function mapConcurrent<T, R>(
191
+ items: T[],
192
+ cap: number,
193
+ worker: (item: T) => Promise<R>,
194
+ ): Promise<R[]> {
195
+ const results = new Array<R>(items.length);
196
+ let cursor = 0;
197
+ async function pump(): Promise<void> {
198
+ while (cursor < items.length) {
199
+ const idx = cursor++;
200
+ results[idx] = await worker(items[idx]);
201
+ }
202
+ }
203
+ const workers = Array.from({ length: Math.min(cap, items.length) }, pump);
204
+ await Promise.all(workers);
205
+ return results;
206
+ }
207
+
208
+ /**
209
+ * Run health checks for all deployed modules.
210
+ *
211
+ * Parallelizes per-module checks with a small concurrency cap so SSH
212
+ * waits overlap. Per-module failures are converted into `error`
213
+ * results so one bad module doesn't prevent siblings from being
214
+ * audited.
166
215
  */
167
216
  export async function runAllHealthChecks(
168
217
  db: DbClient,
169
- options: { debug?: boolean } = {},
218
+ options: { debug?: boolean; onProgress?: (msg: string) => void } = {},
170
219
  ): Promise<HealthCheckResult[]> {
171
- const allModules = db.select().from(modules).all();
172
- const results: HealthCheckResult[] = [];
220
+ const eligible = db
221
+ .select()
222
+ .from(modules)
223
+ .all()
224
+ .filter((m) => ['INSTALLED', 'VERIFIED'].includes(m.state));
173
225
 
174
- for (const module of allModules) {
175
- // Only check modules that have been deployed
176
- if (!['INSTALLED', 'VERIFIED'].includes(module.state)) {
177
- continue;
178
- }
179
- const result = await runModuleHealthCheck(module.id, db, options);
180
- results.push(result);
181
- }
226
+ // Wrap onProgress with a "done N/M" counter so the TUI surfaces
227
+ // overall progress instead of flickering between the latest of
228
+ // four concurrent in-flight checks.
229
+ const total = eligible.length;
230
+ let done = 0;
231
+ const wrappedOnProgress = options.onProgress
232
+ ? (msg: string) => {
233
+ options.onProgress?.(`${msg} (${done}/${total} done)`);
234
+ }
235
+ : undefined;
236
+ const childOptions = { ...options, onProgress: wrappedOnProgress };
182
237
 
183
- return results;
238
+ return mapConcurrent(eligible, HEALTH_CHECK_CONCURRENCY, async (module) => {
239
+ try {
240
+ const result = await runModuleHealthCheck(module.id, db, childOptions);
241
+ done++;
242
+ // Emit one final "N/M done" tick after each module completes,
243
+ // so progress moves even when no new check is starting.
244
+ wrappedOnProgress?.(`Checked ${module.id}`);
245
+ return result;
246
+ } catch (err) {
247
+ done++;
248
+ return {
249
+ moduleId: module.id,
250
+ status: 'error',
251
+ checks: [],
252
+ error: err instanceof Error ? err.message : String(err),
253
+ };
254
+ }
255
+ });
184
256
  }
@@ -277,7 +277,7 @@ describe('resolveInfrastructureVariables - Proxmox Container Service', () => {
277
277
  },
278
278
  {
279
279
  moduleId,
280
- key: 'container_ip',
280
+ key: 'target_ip',
281
281
  value: '10.0.10.5',
282
282
  valueJson: null,
283
283
  },
@@ -128,10 +128,10 @@ export async function resolveInfrastructureVariables(
128
128
  } else if (service.providerName === 'proxmox') {
129
129
  // Proxmox - use IPAM allocation + service provider config
130
130
  const vmid = await getModuleConfig(moduleId, 'vmid', db);
131
- const containerIp = await getModuleConfig(moduleId, 'container_ip', db);
131
+ const targetIp = await getModuleConfig(moduleId, 'target_ip', db);
132
132
  const hostname = (await getModuleConfig(moduleId, 'hostname', db)) || moduleId;
133
133
 
134
- if (!vmid || !containerIp) {
134
+ if (!vmid || !targetIp) {
135
135
  throw new Error(
136
136
  `IPAM allocation not found for module ${moduleId}. ` +
137
137
  `Run 'celilo module generate ${moduleId}' first.`,
@@ -143,7 +143,7 @@ export async function resolveInfrastructureVariables(
143
143
 
144
144
  properties = extractProxmoxProperties(
145
145
  Number.parseInt(vmid, 10),
146
- containerIp,
146
+ targetIp,
147
147
  hostname,
148
148
  providerConfig,
149
149
  );
@@ -10,6 +10,7 @@
10
10
  import { existsSync, statSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { eq } from 'drizzle-orm';
13
+ import { log } from '../cli/prompts';
13
14
  import type { DbClient } from '../db/client';
14
15
  import { type BuildStatus, moduleBuilds, modules } from '../db/schema';
15
16
  import type { ModuleManifest } from '../manifest/schema';
@@ -90,21 +91,18 @@ async function executeBuildCommand(
90
91
  useNix: boolean,
91
92
  ): Promise<{ success: boolean; output: string; error?: string }> {
92
93
  const { executeBuildWithProgress } = await import('./build-stream');
94
+ const { shellEscape } = await import('../utils/shell');
93
95
 
94
- let command: string;
95
- let args: string[];
96
-
97
- if (useNix) {
98
- command = 'nix';
99
- args = ['develop', '--command', 'bash', '-c', commandStr];
100
- } else {
101
- command = 'bash';
102
- args = ['-c', commandStr];
103
- }
96
+ // build-stream passes through `shell: true`, so spawn re-wraps everything in
97
+ // `/bin/sh -c`. If we sent ['bash', '-c', commandStr] as command+args, sh
98
+ // would re-parse the join — `&&` outside the inner quotes splits the chain
99
+ // and `bun install` runs in modulePath instead of the cd'd subdir. Pass the
100
+ // full command as one string so sh -c gets a single, unsplit shell command.
101
+ const command = useNix ? `nix develop --command bash -c ${shellEscape(commandStr)}` : commandStr;
104
102
 
105
103
  const result = await executeBuildWithProgress({
106
104
  command,
107
- args,
105
+ args: [],
108
106
  cwd: modulePath,
109
107
  });
110
108
 
@@ -308,7 +306,7 @@ export async function buildModuleFromSource(moduleId: string, db: DbClient): Pro
308
306
 
309
307
  // Verify artifacts
310
308
  if (artifacts && artifacts.length > 0) {
311
- console.log('Verifying build artifacts...');
309
+ log.message('Verifying build artifacts...');
312
310
  const verification = verifyBuildArtifacts(artifacts, modulePath);
313
311
  const allExist = verification.every((v) => v.exists);
314
312
 
@@ -337,7 +335,7 @@ export async function buildModuleFromSource(moduleId: string, db: DbClient): Pro
337
335
  // Display artifact info
338
336
  for (const result of verification) {
339
337
  const sizeStr = result.size ? ` (${formatBytes(result.size)})` : '';
340
- console.log(`✓ Artifact verified: ${result.path}${sizeStr}`);
338
+ log.success(`Artifact verified: ${result.path}${sizeStr}`);
341
339
  }
342
340
 
343
341
  // Record successful build