@celilo/cli 0.3.30 → 0.4.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/drizzle/0005_module_operations.sql +12 -0
  2. package/drizzle/0006_base_module_aspects.sql +15 -0
  3. package/drizzle/0007_module_systems.sql +17 -0
  4. package/drizzle/meta/_journal.json +21 -0
  5. package/package.json +5 -4
  6. package/schemas/system_config.json +14 -28
  7. package/src/ansible/inventory.test.ts +46 -62
  8. package/src/ansible/inventory.ts +48 -25
  9. package/src/capabilities/registration.ts +25 -7
  10. package/src/capabilities/validation.test.ts +30 -0
  11. package/src/capabilities/validation.ts +8 -0
  12. package/src/cli/backup-rename.test.ts +95 -0
  13. package/src/cli/cli.test.ts +17 -23
  14. package/src/cli/command-registry.ts +199 -0
  15. package/src/cli/commands/backup-list.ts +1 -1
  16. package/src/cli/commands/events.ts +96 -0
  17. package/src/cli/commands/machine-add.ts +103 -59
  18. package/src/cli/commands/module-import.ts +153 -4
  19. package/src/cli/commands/module-remove.ts +86 -17
  20. package/src/cli/commands/module-status.ts +6 -2
  21. package/src/cli/commands/publish/alpha.test.ts +185 -0
  22. package/src/cli/commands/publish/alpha.ts +226 -0
  23. package/src/cli/commands/publish/changesets.test.ts +89 -0
  24. package/src/cli/commands/publish/changesets.ts +144 -0
  25. package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
  26. package/src/cli/commands/publish/consumer-pins.ts +149 -0
  27. package/src/cli/commands/publish/execute.ts +131 -0
  28. package/src/cli/commands/publish/global-install.test.ts +154 -0
  29. package/src/cli/commands/publish/global-install.ts +171 -0
  30. package/src/cli/commands/publish/helpers.ts +227 -0
  31. package/src/cli/commands/publish/index.ts +365 -0
  32. package/src/cli/commands/publish/module-registry.test.ts +40 -0
  33. package/src/cli/commands/publish/module-registry.ts +64 -0
  34. package/src/cli/commands/publish/plan.ts +107 -0
  35. package/src/cli/commands/publish/preflight.ts +238 -0
  36. package/src/cli/commands/publish/types.ts +264 -0
  37. package/src/cli/commands/publish/workspace.test.ts +323 -0
  38. package/src/cli/commands/publish/workspace.ts +596 -0
  39. package/src/cli/commands/restore.ts +126 -0
  40. package/src/cli/commands/storage-add-local.ts +1 -1
  41. package/src/cli/commands/storage-add-s3.ts +1 -1
  42. package/src/cli/commands/subscribers-add.ts +68 -0
  43. package/src/cli/commands/subscribers-list.ts +48 -0
  44. package/src/cli/commands/subscribers-remove.ts +38 -0
  45. package/src/cli/commands/subscribers-serve.ts +77 -0
  46. package/src/cli/commands/subscribers-status.ts +33 -0
  47. package/src/cli/commands/subscribers-test.ts +71 -0
  48. package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
  49. package/src/cli/commands/system-apply-config.test.ts +70 -0
  50. package/src/cli/commands/system-apply-config.ts +130 -0
  51. package/src/cli/commands/system-audit.ts +2 -1
  52. package/src/cli/commands/system-init-deprecation.test.ts +90 -0
  53. package/src/cli/commands/system-init.ts +36 -70
  54. package/src/cli/commands/system-update.ts +3 -2
  55. package/src/cli/completion.ts +22 -1
  56. package/src/cli/index.ts +214 -6
  57. package/src/cli/interactive-config.test.ts +19 -0
  58. package/src/cli/restore-command.test.ts +131 -0
  59. package/src/db/client.ts +42 -0
  60. package/src/db/schema.test.ts +13 -16
  61. package/src/db/schema.ts +161 -9
  62. package/src/hooks/capability-loader-firewall.test.ts +6 -15
  63. package/src/hooks/capability-loader.test.ts +2 -3
  64. package/src/hooks/capability-loader.ts +36 -2
  65. package/src/hooks/define-hook.test.ts +4 -0
  66. package/src/hooks/executor.test.ts +18 -0
  67. package/src/hooks/executor.ts +21 -2
  68. package/src/hooks/load-hook-config.test.ts +26 -24
  69. package/src/hooks/load-hook-config.ts +11 -2
  70. package/src/hooks/run-named-hook.ts +16 -0
  71. package/src/hooks/types.ts +9 -1
  72. package/src/manifest/contracts/v1.ts +70 -0
  73. package/src/manifest/schema.ts +262 -16
  74. package/src/manifest/validate-privileged.test.ts +84 -0
  75. package/src/manifest/validate.test.ts +156 -0
  76. package/src/manifest/validate.ts +69 -0
  77. package/src/module/import.ts +12 -0
  78. package/src/services/aspect-approvals.test.ts +231 -0
  79. package/src/services/aspect-approvals.ts +120 -0
  80. package/src/services/aspect-runner.test.ts +493 -0
  81. package/src/services/aspect-runner.ts +438 -0
  82. package/src/services/aspect-template-resolver.test.ts +101 -0
  83. package/src/services/aspect-template-resolver.ts +122 -0
  84. package/src/services/backup-create.ts +104 -25
  85. package/src/services/backup-envelope-roundtrip.test.ts +199 -0
  86. package/src/services/backup-in-flight-refusal.test.ts +163 -0
  87. package/src/services/backup-manifest.test.ts +115 -0
  88. package/src/services/backup-manifest.ts +163 -0
  89. package/src/services/backup-restore.ts +154 -19
  90. package/src/services/build-bus/delivery-events.ts +92 -0
  91. package/src/services/build-bus/event-factory.ts +54 -0
  92. package/src/services/build-bus/fan-out.test.ts +279 -0
  93. package/src/services/build-bus/fan-out.ts +161 -0
  94. package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
  95. package/src/services/build-bus/hook-dispatch.test.ts +207 -0
  96. package/src/services/build-bus/hook-dispatch.ts +198 -0
  97. package/src/services/build-bus/hook-dispatcher.ts +115 -0
  98. package/src/services/build-bus/index.ts +41 -0
  99. package/src/services/build-bus/receiver-server.test.ts +179 -0
  100. package/src/services/build-bus/receiver-server.ts +159 -0
  101. package/src/services/build-bus/status.test.ts +212 -0
  102. package/src/services/build-bus/status.ts +213 -0
  103. package/src/services/build-bus/subscriber-store.ts +113 -0
  104. package/src/services/celilo-events.test.ts +70 -0
  105. package/src/services/celilo-events.ts +92 -0
  106. package/src/services/celilo-mgmt-hooks.test.ts +296 -0
  107. package/src/services/config-interview.ts +13 -95
  108. package/src/services/cross-module-data-manager.ts +2 -31
  109. package/src/services/cross-module-read.test.ts +250 -0
  110. package/src/services/cross-module-read.ts +232 -0
  111. package/src/services/deploy-validation.ts +7 -0
  112. package/src/services/deployed-systems.test.ts +235 -0
  113. package/src/services/deployed-systems.ts +308 -0
  114. package/src/services/dns-provider-backfill.ts +75 -0
  115. package/src/services/health-runner.ts +19 -3
  116. package/src/services/infrastructure-variable-resolver.test.ts +6 -32
  117. package/src/services/infrastructure-variable-resolver.ts +3 -13
  118. package/src/services/machine-detector.ts +104 -48
  119. package/src/services/machine-pool.ts +145 -2
  120. package/src/services/module-config.ts +78 -120
  121. package/src/services/module-deploy.ts +113 -40
  122. package/src/services/module-operations.test.ts +154 -0
  123. package/src/services/module-operations.ts +154 -0
  124. package/src/services/module-subscriptions.test.ts +58 -0
  125. package/src/services/module-subscriptions.ts +24 -1
  126. package/src/services/module-types-generator.test.ts +3 -3
  127. package/src/services/module-types-generator.ts +7 -2
  128. package/src/services/proxmox-reconcile.test.ts +333 -0
  129. package/src/services/proxmox-reconcile.ts +156 -0
  130. package/src/services/proxmox-state-recovery.ts +3 -24
  131. package/src/services/restore-from-file.test.ts +177 -0
  132. package/src/services/restore-from-file.ts +355 -0
  133. package/src/services/restore-preflight.test.ts +127 -0
  134. package/src/services/restore-preflight.ts +118 -0
  135. package/src/services/storage-providers/s3.ts +10 -2
  136. package/src/services/system-identity.ts +30 -0
  137. package/src/services/system-init.test.ts +64 -21
  138. package/src/services/system-init.ts +28 -26
  139. package/src/templates/generator.test.ts +7 -16
  140. package/src/templates/generator.ts +28 -115
  141. package/src/test-utils/integration.ts +5 -2
  142. package/src/types/infrastructure.ts +8 -0
  143. package/src/variables/computed/computed-integration.test.ts +191 -0
  144. package/src/variables/computed/computed.test.ts +177 -0
  145. package/src/variables/computed/evaluate.ts +271 -0
  146. package/src/variables/computed/marker.ts +53 -0
  147. package/src/variables/computed/parse.ts +262 -0
  148. package/src/variables/computed/provider-lookup.ts +130 -0
  149. package/src/variables/context.test.ts +89 -28
  150. package/src/variables/context.ts +196 -191
  151. package/src/variables/parser.ts +3 -3
  152. package/src/variables/resolver.test.ts +61 -0
  153. package/src/variables/resolver.ts +81 -0
  154. package/src/variables/types.ts +23 -1
  155. package/src/services/dns-auto-register.ts +0 -211
@@ -1,3 +1,6 @@
1
+ import { evaluateComputed } from './computed/evaluate';
2
+ import { asComputedExpression } from './computed/marker';
3
+ import { buildProviderLookup } from './computed/provider-lookup';
1
4
  import { applyIndex, parsePath, parseVariables } from './parser';
2
5
  import type {
3
6
  ResolutionContext,
@@ -70,6 +73,40 @@ export async function resolveVariable(
70
73
  return { success: true, value };
71
74
  }
72
75
 
76
+ case 'infra': {
77
+ // Format: $infra:<system-name>.<field> — references a deployed system by
78
+ // its stable handle (requires.systems[].name). v2/MODULE_SYSTEMS_ADDRESSING.md.
79
+ const dot = variable.path.indexOf('.');
80
+ if (dot === -1) {
81
+ return {
82
+ success: false,
83
+ variable: variable.raw,
84
+ error: `Infra variable must be '$infra:<system-name>.<field>' (e.g. $infra:main.ipv4_address); got '${variable.path}'`,
85
+ };
86
+ }
87
+ const systemName = variable.path.slice(0, dot);
88
+ const field = variable.path.slice(dot + 1);
89
+ const systems = context.systems ?? {};
90
+ const system = systems[systemName];
91
+ if (!system) {
92
+ const known = Object.keys(systems);
93
+ return {
94
+ success: false,
95
+ variable: variable.raw,
96
+ error: `No deployed system named '${systemName}' for module ${context.moduleId}${known.length ? ` (known: ${known.join(', ')})` : ' (none recorded yet)'}`,
97
+ };
98
+ }
99
+ const value = (system as unknown as Record<string, unknown>)[field];
100
+ if (value === undefined || value === '') {
101
+ return {
102
+ success: false,
103
+ variable: variable.raw,
104
+ error: `Infra field '${field}' not available on system '${systemName}' (fields: name, hostname, ipv4_address, zone, cidr, vmid)`,
105
+ };
106
+ }
107
+ return { success: true, value: value as string };
108
+ }
109
+
73
110
  case 'system_secret': {
74
111
  const value = context.systemSecrets[variable.path];
75
112
  if (value === undefined) {
@@ -171,6 +208,50 @@ export async function resolveVariable(
171
208
  };
172
209
  }
173
210
 
211
+ // Computed field (v2/INTERNAL_DNS_DHCP_AND_SPLIT_HORIZON.md D1):
212
+ // evaluate the DSL expression in the PROVIDER's context. Like the
213
+ // lazy $self: block below, we look up the provider module, but here
214
+ // we build a typed lookup over its config/secrets/system data.
215
+ const computedExpr = asComputedExpression(value);
216
+ if (computedExpr !== null) {
217
+ // Find the provider module (raw SQL, matching the lazy $self: block).
218
+ const providerResult = db.$client
219
+ .prepare(
220
+ `SELECT p.id FROM modules p
221
+ JOIN capabilities c ON p.id = c.module_id
222
+ WHERE c.capability_name = ?
223
+ LIMIT 1`,
224
+ )
225
+ .get(capabilityName) as { id: string } | undefined;
226
+
227
+ if (!providerResult) {
228
+ return {
229
+ success: false,
230
+ variable: variable.raw,
231
+ error: `Capability '${capabilityName}' has a computed field '${fieldPath}' but no provider module was found`,
232
+ };
233
+ }
234
+
235
+ try {
236
+ const lookup = await buildProviderLookup(providerResult.id, db);
237
+ const result = evaluateComputed(computedExpr, lookup);
238
+ // Computed results are often arrays/objects (e.g. domain_list).
239
+ // In a string-template context we serialize non-scalars as JSON;
240
+ // structured consumers read the evaluated value directly.
241
+ const serialized =
242
+ typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean'
243
+ ? String(result)
244
+ : JSON.stringify(result);
245
+ return { success: true, value: serialized };
246
+ } catch (error) {
247
+ return {
248
+ success: false,
249
+ variable: variable.raw,
250
+ error: `Failed to evaluate computed field '${fieldPath}' of '${capabilityName}': ${error instanceof Error ? error.message : 'Unknown error'}`,
251
+ };
252
+ }
253
+ }
254
+
174
255
  // Check if value contains unresolved $self: variable (lazy resolution)
175
256
  if (typeof value === 'string' && value.startsWith('$self:')) {
176
257
  // Get provider module's config to resolve the variable.
@@ -7,7 +7,7 @@
7
7
  * Example: $self:target_ip -> { type: 'self', path: 'target_ip', raw: '$self:target_ip' }
8
8
  */
9
9
  export interface VariableReference {
10
- type: 'self' | 'system' | 'system_secret' | 'secret' | 'capability';
10
+ type: 'self' | 'system' | 'system_secret' | 'secret' | 'capability' | 'infra';
11
11
  path: string;
12
12
  raw: string;
13
13
  }
@@ -22,6 +22,28 @@ export interface ResolutionContext {
22
22
  systemSecrets: Record<string, string>;
23
23
  secrets: Record<string, string>;
24
24
  capabilities: Record<string, Record<string, unknown>>;
25
+ /**
26
+ * The module's deployed systems, keyed by name, for `$infra:<name>.<field>`
27
+ * resolution at generate time (v2/MODULE_SYSTEMS_ADDRESSING.md). Each value
28
+ * exposes the snake_cased fields a template can reference: `ipv4_address`,
29
+ * `hostname`, `zone`, `vmid`, `cidr`. Optional only so the many test-stub
30
+ * `ResolutionContext` literals need no churn — production (buildResolutionContext)
31
+ * always populates it; the resolver treats undefined as the empty map.
32
+ */
33
+ systems?: Record<string, InfraSystemFields>;
34
+ }
35
+
36
+ /**
37
+ * The `$infra:<name>.<field>` fields a template can read for one deployed
38
+ * system. `cidr` is derived (ipv4_address + the zone's prefix length).
39
+ */
40
+ export interface InfraSystemFields {
41
+ name: string;
42
+ hostname: string;
43
+ ipv4_address: string;
44
+ zone: string;
45
+ cidr: string;
46
+ vmid: string;
25
47
  }
26
48
 
27
49
  /**
@@ -1,211 +0,0 @@
1
- /**
2
- * Automatic DNS registration for deployed modules.
3
- *
4
- * After any module deploys successfully, registers its hostname as an
5
- * A record in the internal DNS server (if dns_internal capability is
6
- * available). When the dns_internal provider itself deploys, backfills
7
- * records for all previously-deployed modules.
8
- */
9
-
10
- import type { HookLogger } from '@celilo/capabilities';
11
- import type { DnsInternalCapability } from '@celilo/capabilities';
12
- import { eq } from 'drizzle-orm';
13
- import type { DbClient } from '../db/client';
14
- import {
15
- capabilities as capabilitiesTable,
16
- moduleConfigs,
17
- moduleInfrastructure,
18
- modules,
19
- systemConfig,
20
- } from '../db/schema';
21
- import { loadCapabilityFunctions } from '../hooks/capability-loader';
22
-
23
- /**
24
- * Get a module's hostname and IP for DNS registration.
25
- * Returns null if the module doesn't have both.
26
- */
27
- async function getModuleHostAndIp(
28
- moduleId: string,
29
- db: DbClient,
30
- ): Promise<{ hostname: string; ip: string } | null> {
31
- // Get hostname from module config
32
- const hostnameConfig = db
33
- .select()
34
- .from(moduleConfigs)
35
- .where(eq(moduleConfigs.moduleId, moduleId))
36
- .all()
37
- .find((c) => c.key === 'hostname');
38
-
39
- if (!hostnameConfig?.value) return null;
40
-
41
- // Get IP: try target_ip first, then machine IP
42
- const targetIpConfig = db
43
- .select()
44
- .from(moduleConfigs)
45
- .where(eq(moduleConfigs.moduleId, moduleId))
46
- .all()
47
- .find((c) => c.key === 'target_ip');
48
-
49
- let ip = targetIpConfig?.value;
50
-
51
- if (!ip) {
52
- // Try machine IP from infrastructure assignment
53
- const infra = db
54
- .select()
55
- .from(moduleInfrastructure)
56
- .where(eq(moduleInfrastructure.moduleId, moduleId))
57
- .all();
58
-
59
- if (infra.length > 0 && infra[0].machineId) {
60
- const { machines } = await import('../db/schema');
61
- const machine = db.select().from(machines).where(eq(machines.id, infra[0].machineId)).get();
62
- if (machine) {
63
- ip = machine.ipAddress;
64
- }
65
- }
66
- }
67
-
68
- if (!ip) return null;
69
-
70
- // Strip CIDR notation
71
- const bareIp = ip.split('/')[0];
72
-
73
- return { hostname: hostnameConfig.value, ip: bareIp };
74
- }
75
-
76
- /**
77
- * Get the system's primary domain.
78
- */
79
- function getPrimaryDomain(db: DbClient): string | null {
80
- const record = db.select().from(systemConfig).where(eq(systemConfig.key, 'primary_domain')).get();
81
-
82
- return record?.value || null;
83
- }
84
-
85
- /**
86
- * Load the dns_internal capability if it's available.
87
- * Returns null if no dns_internal provider is registered.
88
- */
89
- async function loadDnsInternal(
90
- db: DbClient,
91
- logger: HookLogger,
92
- ): Promise<DnsInternalCapability | null> {
93
- const providers = db
94
- .select()
95
- .from(capabilitiesTable)
96
- .where(eq(capabilitiesTable.capabilityName, 'dns_internal'))
97
- .all();
98
-
99
- if (providers.length === 0) return null;
100
-
101
- // Load capability functions using the provider's own module ID as context
102
- const caps = await loadCapabilityFunctions(providers[0].moduleId, db, logger);
103
- return (caps.dns_internal as DnsInternalCapability) || null;
104
- }
105
-
106
- /**
107
- * Auto-register a module's hostname in internal DNS after successful deploy.
108
- *
109
- * If the deployed module IS the dns_internal provider, also backfills
110
- * records for all previously-deployed modules.
111
- */
112
- export async function autoRegisterDns(
113
- moduleId: string,
114
- db: DbClient,
115
- logger: HookLogger,
116
- ): Promise<void> {
117
- const primaryDomain = getPrimaryDomain(db);
118
- if (!primaryDomain) {
119
- return; // No domain configured, skip silently
120
- }
121
-
122
- const dnsInternal = await loadDnsInternal(db, logger);
123
- if (!dnsInternal) {
124
- return; // No dns_internal provider, skip silently
125
- }
126
-
127
- // Check if the module being deployed IS the dns_internal provider
128
- const providers = db
129
- .select()
130
- .from(capabilitiesTable)
131
- .where(eq(capabilitiesTable.capabilityName, 'dns_internal'))
132
- .all();
133
- const isProvider = providers.some((p) => p.moduleId === moduleId);
134
-
135
- if (isProvider) {
136
- // Backfill: register all deployed modules
137
- logger.info('dns_internal provider deployed -- backfilling DNS records for all modules');
138
- const allModules = db
139
- .select()
140
- .from(modules)
141
- .all()
142
- .filter((m) => m.id !== moduleId); // Skip self
143
-
144
- for (const mod of allModules) {
145
- const hostInfo = await getModuleHostAndIp(mod.id, db);
146
- if (!hostInfo) continue;
147
-
148
- const fqdn = `${hostInfo.hostname}.${primaryDomain}`;
149
- try {
150
- await dnsInternal.registerRecord({
151
- host: fqdn,
152
- type: 'A',
153
- value: hostInfo.ip,
154
- });
155
- } catch (error) {
156
- const msg = error instanceof Error ? error.message : String(error);
157
- logger.warn(`Failed to register ${fqdn}: ${msg}`);
158
- }
159
- }
160
- }
161
-
162
- // Register the deployed module itself
163
- const hostInfo = await getModuleHostAndIp(moduleId, db);
164
- if (!hostInfo) return;
165
-
166
- const fqdn = `${hostInfo.hostname}.${primaryDomain}`;
167
- try {
168
- await dnsInternal.registerRecord({
169
- host: fqdn,
170
- type: 'A',
171
- value: hostInfo.ip,
172
- });
173
- logger.success(`Registered DNS: ${fqdn} → ${hostInfo.ip}`);
174
- } catch (error) {
175
- const msg = error instanceof Error ? error.message : String(error);
176
- logger.warn(`DNS registration failed for ${fqdn}: ${msg}`);
177
- // Non-fatal -- deployment still succeeded
178
- }
179
- }
180
-
181
- /**
182
- * Remove a module's DNS record when the module is removed.
183
- */
184
- export async function autoDeregisterDns(
185
- moduleId: string,
186
- db: DbClient,
187
- logger: HookLogger,
188
- ): Promise<void> {
189
- const primaryDomain = getPrimaryDomain(db);
190
- if (!primaryDomain) return;
191
-
192
- const dnsInternal = await loadDnsInternal(db, logger);
193
- if (!dnsInternal) return;
194
-
195
- const hostInfo = await getModuleHostAndIp(moduleId, db);
196
- if (!hostInfo) return;
197
-
198
- const fqdn = `${hostInfo.hostname}.${primaryDomain}`;
199
- try {
200
- await dnsInternal.deleteRecord({
201
- host: fqdn,
202
- type: 'A',
203
- value: hostInfo.ip,
204
- });
205
- logger.success(`Removed DNS: ${fqdn}`);
206
- } catch (error) {
207
- const msg = error instanceof Error ? error.message : String(error);
208
- logger.warn(`DNS cleanup failed for ${fqdn}: ${msg}`);
209
- // Non-fatal
210
- }
211
- }