@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
@@ -9,13 +9,19 @@ import type { VariableReference } from './types';
9
9
  * Examples:
10
10
  * - $self:container_ip
11
11
  * - ${self:disk}G
12
+ * - $self:domains[0] ← array indexing on $self: refs
12
13
  * - $capability:dns_registrar.primary_domain
13
14
  * - ${system:base_url}/api
14
15
  *
15
- * Path must start with letter or underscore, not digit
16
+ * Path must start with letter or underscore, not digit. An optional
17
+ * [N] suffix indexes into an array variable (currently only
18
+ * meaningful on $self: refs whose target is `type: array` — e.g.
19
+ * namecheap's `domains` declared as an array; the manifest's
20
+ * capability data block can then read `$self:domains[0]` as a
21
+ * computed alias for the canonical default).
16
22
  */
17
23
  const VARIABLE_PATTERN =
18
- /\$\{(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)\}|\$(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)/g;
24
+ /\$\{(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)\}|\$(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)/g;
19
25
 
20
26
  /**
21
27
  * Parse template content to extract variable references
@@ -56,8 +62,9 @@ export function parseVariables(content: string): VariableReference[] {
56
62
  */
57
63
  export function hasVariables(content: string): boolean {
58
64
  // Create new regex without state to avoid issues with global flag
59
- // Matches both ${type:path} and $type:path
60
- const pattern = /\$\{?(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)/;
65
+ // Matches both ${type:path} and $type:path (with optional [N] index)
66
+ const pattern =
67
+ /\$\{?(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)/;
61
68
  return pattern.test(content);
62
69
  }
63
70
 
@@ -69,8 +76,42 @@ export function hasVariables(content: string): boolean {
69
76
  */
70
77
  export function isValidVariableFormat(variable: string): boolean {
71
78
  // Path must start with letter or underscore, not digit
72
- // Accepts both ${type:path} and $type:path
79
+ // Accepts both ${type:path} and $type:path with optional [N] indexing
73
80
  const pattern =
74
- /^(?:\$\{(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*\}|\$(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*)$/;
81
+ /^(?:\$\{(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?\}|\$(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*(?:\[\d+\])?)$/;
75
82
  return pattern.test(variable);
76
83
  }
84
+
85
+ /**
86
+ * Split a variable path into its base name and optional array index.
87
+ *
88
+ * Examples:
89
+ * parsePath("domains") → { name: "domains" }
90
+ * parsePath("domains[0]") → { name: "domains", index: 0 }
91
+ * parsePath("a.b.c[2]") → { name: "a.b.c", index: 2 }
92
+ *
93
+ * Pure helper used wherever `$self:NAME[N]` semantics need to be
94
+ * applied to a resolved value (currently the capability-data
95
+ * resolver in `context.ts` and the lazy-resolution path in
96
+ * `resolver.ts`).
97
+ */
98
+ export function parsePath(path: string): { name: string; index?: number } {
99
+ const match = /^(.+)\[(\d+)\]$/.exec(path);
100
+ if (!match) return { name: path };
101
+ return { name: match[1], index: Number.parseInt(match[2], 10) };
102
+ }
103
+
104
+ /**
105
+ * Apply an optional `[N]` index to a value. Used by the variable
106
+ * resolver after looking up a name like `domains` in a config map —
107
+ * if the original path had `[N]`, we drill into the array.
108
+ *
109
+ * Returns `undefined` for out-of-bounds indexes or when an index is
110
+ * applied to a non-array. Callers turn that into a resolver error.
111
+ */
112
+ export function applyIndex(value: unknown, index: number | undefined): unknown {
113
+ if (index === undefined) return value;
114
+ if (!Array.isArray(value)) return undefined;
115
+ if (index < 0 || index >= value.length) return undefined;
116
+ return value[index];
117
+ }
@@ -29,7 +29,7 @@ const mockDb = {
29
29
  const createContext = (overrides?: Partial<ResolutionContext>): ResolutionContext => ({
30
30
  moduleId: 'test-module',
31
31
  selfConfig: {
32
- container_ip: '192.168.0.50',
32
+ target_ip: '192.168.0.50',
33
33
  hostname: 'homebridge',
34
34
  cores: '2',
35
35
  'resources.machine.cpu': '2',
@@ -69,8 +69,8 @@ describe('resolveVariable', () => {
69
69
  test('should resolve self variable', async () => {
70
70
  const variable: VariableReference = {
71
71
  type: 'self',
72
- path: 'container_ip',
73
- raw: '$self:container_ip',
72
+ path: 'target_ip',
73
+ raw: '$self:target_ip',
74
74
  };
75
75
  const context = createContext();
76
76
 
@@ -245,7 +245,7 @@ describe('resolveVariable', () => {
245
245
 
246
246
  describe('resolveTemplate', () => {
247
247
  test('should resolve template with single variable', async () => {
248
- const template = 'ip: $self:container_ip';
248
+ const template = 'ip: $self:target_ip';
249
249
  const context = createContext();
250
250
 
251
251
  const result = await resolveTemplate(template, context, mockDb);
@@ -259,7 +259,7 @@ describe('resolveTemplate', () => {
259
259
  test('should resolve template with multiple variables', async () => {
260
260
  const template = `
261
261
  hostname: $self:hostname
262
- ip: $self:container_ip
262
+ ip: $self:target_ip
263
263
  domain: $system:network.domain
264
264
  `;
265
265
  const context = createContext();
@@ -276,7 +276,7 @@ domain: $system:network.domain
276
276
 
277
277
  test('should resolve template with mixed variable types', async () => {
278
278
  const template = `
279
- container_ip: $self:container_ip
279
+ target_ip: $self:target_ip
280
280
  management_ip: $system:management.ip
281
281
  dns_server: $capability:dns_external.nameserver
282
282
  api_key: $secret:api_key
@@ -287,7 +287,7 @@ api_key: $secret:api_key
287
287
 
288
288
  expect(result.success).toBe(true);
289
289
  if (result.success) {
290
- expect(result.content).toContain('container_ip: 192.168.0.50');
290
+ expect(result.content).toContain('target_ip: 192.168.0.50');
291
291
  expect(result.content).toContain('management_ip: 192.168.0.10');
292
292
  expect(result.content).toContain('dns_server: ns1.example.com');
293
293
  expect(result.content).toContain('api_key: secret123');
@@ -324,7 +324,7 @@ resource "proxmox_lxc" "container" {
324
324
  hostname = "$self:hostname"
325
325
  cores = $self:cores
326
326
  network {
327
- ip = "$self:container_ip/24"
327
+ ip = "$self:target_ip/24"
328
328
  gateway = "$system:management.ip"
329
329
  }
330
330
  environment = {
@@ -362,7 +362,7 @@ resource "proxmox_lxc" "container" {
362
362
 
363
363
  test('should return errors for missing variables', async () => {
364
364
  const template = `
365
- ip: $self:container_ip
365
+ ip: $self:target_ip
366
366
  missing: $self:missing_var
367
367
  another_missing: $secret:missing_secret
368
368
  `;
@@ -380,9 +380,9 @@ another_missing: $secret:missing_secret
380
380
 
381
381
  test('should handle repeated variables', async () => {
382
382
  const template = `
383
- ip1: $self:container_ip
384
- ip2: $self:container_ip
385
- ip3: $self:container_ip
383
+ ip1: $self:target_ip
384
+ ip2: $self:target_ip
385
+ ip3: $self:target_ip
386
386
  `;
387
387
  const context = createContext();
388
388
 
@@ -393,7 +393,7 @@ ip3: $self:container_ip
393
393
  expect(result.content).toContain('ip1: 192.168.0.50');
394
394
  expect(result.content).toContain('ip2: 192.168.0.50');
395
395
  expect(result.content).toContain('ip3: 192.168.0.50');
396
- expect(result.content).not.toContain('$self:container_ip');
396
+ expect(result.content).not.toContain('$self:target_ip');
397
397
  }
398
398
  });
399
399
 
@@ -420,7 +420,7 @@ $self:hostname
420
420
  const template = 'ansible_host: $self:inventory.ansible_host';
421
421
  const context = createContext({
422
422
  selfConfig: {
423
- container_ip: '10.0.10.10/24',
423
+ target_ip: '10.0.10.10/24',
424
424
  'inventory.ansible_host': '10.0.10.10', // Auto-derived (CIDR stripped)
425
425
  'inventory.ansible_user': 'root',
426
426
  'inventory.groups': 'test',
@@ -1,4 +1,4 @@
1
- import { parseVariables } from './parser';
1
+ import { applyIndex, parsePath, parseVariables } from './parser';
2
2
  import type {
3
3
  ResolutionContext,
4
4
  ResolveResult,
@@ -173,8 +173,11 @@ export async function resolveVariable(
173
173
 
174
174
  // Check if value contains unresolved $self: variable (lazy resolution)
175
175
  if (typeof value === 'string' && value.startsWith('$self:')) {
176
- // Get provider module's config to resolve the variable
177
- const selfVarPath = value.substring(6); // Remove "$self:" prefix
176
+ // Get provider module's config to resolve the variable.
177
+ // The path can be a plain key ("primary_domain") or include
178
+ // an array index ("domains[0]") — see parsePath/applyIndex
179
+ // and the syntax note in the multi-domain DDNS design doc.
180
+ const { name, index } = parsePath(value.substring(6));
178
181
 
179
182
  // Get provider module ID
180
183
  const providerQuery = db.$client.prepare(
@@ -193,23 +196,38 @@ export async function resolveVariable(
193
196
  };
194
197
  }
195
198
 
196
- // Get provider module's config value
199
+ // Fetch both `value` and `value_json` so we can index into
200
+ // arrays when the path contained `[N]`.
197
201
  const configQuery = db.$client.prepare(
198
- 'SELECT value FROM module_configs WHERE module_id = ? AND key = ?',
202
+ 'SELECT value, value_json FROM module_configs WHERE module_id = ? AND key = ?',
199
203
  );
200
- const configResult = configQuery.get(providerResult.id, selfVarPath) as
201
- | { value: string }
204
+ const configResult = configQuery.get(providerResult.id, name) as
205
+ | { value: string; value_json: string | null }
202
206
  | undefined;
203
207
 
204
208
  if (!configResult) {
205
209
  return {
206
210
  success: false,
207
211
  variable: variable.raw,
208
- error: `Provider module '${providerResult.id}' has not configured '${selfVarPath}' (required by capability '${capabilityName}')`,
212
+ error: `Provider module '${providerResult.id}' has not configured '${name}' (required by capability '${capabilityName}')`,
209
213
  };
210
214
  }
211
215
 
212
- return { success: true, value: configResult.value };
216
+ const rawValue: unknown = configResult.value_json
217
+ ? JSON.parse(configResult.value_json)
218
+ : configResult.value;
219
+ const indexed = applyIndex(rawValue, index);
220
+ if (indexed === undefined) {
221
+ return {
222
+ success: false,
223
+ variable: variable.raw,
224
+ error:
225
+ index === undefined
226
+ ? `Provider module '${providerResult.id}' has not configured '${name}'`
227
+ : `'$self:${name}[${index}]' is out of bounds or '${name}' is not an array on '${providerResult.id}'`,
228
+ };
229
+ }
230
+ return { success: true, value: typeof indexed === 'string' ? indexed : String(indexed) };
213
231
  }
214
232
 
215
233
  // Convert value to string
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * Variable reference parsed from template
7
- * Example: $self:container_ip -> { type: 'self', path: 'container_ip', raw: '$self:container_ip' }
7
+ * Example: $self:target_ip -> { type: 'self', path: 'target_ip', raw: '$self:target_ip' }
8
8
  */
9
9
  export interface VariableReference {
10
10
  type: 'self' | 'system' | 'system_secret' | 'secret' | 'capability';
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "lib": ["ESNext"],
6
6
  "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
7
8
  "strict": true,
8
9
  "esModuleInterop": true,
9
10
  "skipLibCheck": true,