@celilo/cli 0.1.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 (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Cross-Module Data Manager
3
+ *
4
+ * Manages data exchange between modules, including:
5
+ * - Storing/retrieving configuration data from function calls
6
+ * - Encrypting sensitive data (secrets)
7
+ * - Resolving variables in parameters
8
+ * - Looking up capability providers
9
+ *
10
+ * Future: Full function call orchestration, lifecycle management
11
+ */
12
+
13
+ import { and, eq } from 'drizzle-orm';
14
+ import { type DbClient, createDbClient } from '../db/client';
15
+ import { capabilities, moduleConfigs, secrets } from '../db/schema';
16
+ import { decryptSecret, encryptSecret } from '../secrets/encryption';
17
+ import { getOrCreateMasterKey } from '../secrets/master-key';
18
+ import { buildResolutionContext } from '../variables/context';
19
+ import { resolveTemplate } from '../variables/resolver';
20
+
21
+ /**
22
+ * Configuration data type
23
+ */
24
+ export interface ConfigData {
25
+ key: string;
26
+ value: string | number | boolean | unknown[] | Record<string, unknown>;
27
+ isSecret: boolean;
28
+ }
29
+
30
+ /**
31
+ * Capability lookup result
32
+ */
33
+ export interface CapabilityInfo {
34
+ moduleId: string;
35
+ capabilityName: string;
36
+ version: string;
37
+ data: Record<string, unknown>;
38
+ }
39
+
40
+ /**
41
+ * Cross-module data manager
42
+ * Orchestrates data exchange between modules
43
+ */
44
+ export class CrossModuleDataManager {
45
+ private db: DbClient;
46
+ private masterKey: Buffer | null = null;
47
+
48
+ constructor(db?: DbClient) {
49
+ this.db = db || createDbClient();
50
+ }
51
+
52
+ /**
53
+ * Initialize manager (load master key)
54
+ * Execution function - performs I/O
55
+ */
56
+ async initialize(): Promise<void> {
57
+ this.masterKey = await getOrCreateMasterKey();
58
+ }
59
+
60
+ /**
61
+ * Ensure master key is loaded
62
+ * Policy function - validates state
63
+ */
64
+ private ensureMasterKey(): Buffer {
65
+ if (!this.masterKey) {
66
+ throw new Error('CrossModuleDataManager not initialized. Call initialize() first.');
67
+ }
68
+ return this.masterKey;
69
+ }
70
+
71
+ /**
72
+ * Store configuration data for a module
73
+ * Handles both regular config and secrets
74
+ *
75
+ * Execution function - performs database writes and encryption
76
+ *
77
+ * @param moduleId - Module ID
78
+ * @param key - Configuration key
79
+ * @param value - Configuration value
80
+ * @param isSecret - Whether to encrypt the value
81
+ */
82
+ async storeConfigData(
83
+ moduleId: string,
84
+ key: string,
85
+ value: string | number | boolean | unknown[] | Record<string, unknown>,
86
+ isSecret = false,
87
+ ): Promise<void> {
88
+ if (isSecret) {
89
+ await this.storeSecret(moduleId, key, String(value));
90
+ } else {
91
+ await this.storeConfig(moduleId, key, value);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Store regular (non-secret) configuration
97
+ * Execution function - performs database write
98
+ */
99
+ private async storeConfig(
100
+ moduleId: string,
101
+ key: string,
102
+ value: string | number | boolean | unknown[] | Record<string, unknown>,
103
+ ): Promise<void> {
104
+ const isComplex = typeof value === 'object' && value !== null;
105
+
106
+ const existing = this.db
107
+ .select()
108
+ .from(moduleConfigs)
109
+ .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
110
+ .get();
111
+
112
+ if (existing) {
113
+ // Update existing
114
+ this.db
115
+ .update(moduleConfigs)
116
+ .set({
117
+ value: isComplex ? '' : String(value),
118
+ valueJson: isComplex ? JSON.stringify(value) : null,
119
+ updatedAt: new Date(Date.now()),
120
+ })
121
+ .where(eq(moduleConfigs.id, existing.id))
122
+ .run();
123
+ } else {
124
+ // Insert new
125
+ this.db
126
+ .insert(moduleConfigs)
127
+ .values({
128
+ moduleId,
129
+ key,
130
+ value: isComplex ? '' : String(value),
131
+ valueJson: isComplex ? JSON.stringify(value) : null,
132
+ })
133
+ .run();
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Store secret (encrypted)
139
+ * Execution function - performs encryption and database write
140
+ *
141
+ * @param moduleId - Module ID
142
+ * @param name - Secret name
143
+ * @param value - Secret value (plaintext)
144
+ */
145
+ async storeSecret(moduleId: string, name: string, value: string): Promise<void> {
146
+ const masterKey = this.ensureMasterKey();
147
+
148
+ // Encrypt the secret
149
+ const encrypted = encryptSecret(value, masterKey);
150
+
151
+ // Check if secret already exists
152
+ const existing = this.db
153
+ .select()
154
+ .from(secrets)
155
+ .where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name)))
156
+ .get();
157
+
158
+ if (existing) {
159
+ // Update existing
160
+ this.db
161
+ .update(secrets)
162
+ .set({
163
+ encryptedValue: encrypted.encryptedValue,
164
+ iv: encrypted.iv,
165
+ authTag: encrypted.authTag,
166
+ updatedAt: new Date(Date.now()),
167
+ })
168
+ .where(eq(secrets.id, existing.id))
169
+ .run();
170
+ } else {
171
+ // Insert new
172
+ this.db
173
+ .insert(secrets)
174
+ .values({
175
+ moduleId,
176
+ name,
177
+ encryptedValue: encrypted.encryptedValue,
178
+ iv: encrypted.iv,
179
+ authTag: encrypted.authTag,
180
+ })
181
+ .run();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Get configuration data (non-secret)
187
+ * Execution function - performs database read
188
+ *
189
+ * @param moduleId - Module ID
190
+ * @param key - Configuration key
191
+ * @returns Configuration value or null if not found
192
+ */
193
+ getConfigData(
194
+ moduleId: string,
195
+ key: string,
196
+ ): string | number | boolean | unknown[] | Record<string, unknown> | null {
197
+ const config = this.db
198
+ .select()
199
+ .from(moduleConfigs)
200
+ .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
201
+ .get();
202
+
203
+ if (!config) {
204
+ return null;
205
+ }
206
+
207
+ // Return complex type from valueJson
208
+ if (config.valueJson) {
209
+ try {
210
+ return JSON.parse(config.valueJson);
211
+ } catch (error) {
212
+ throw new Error(
213
+ `Failed to parse config value for ${moduleId}.${key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ // Return primitive type from value
219
+ // Parse to correct type
220
+ if (config.value === 'true') return true;
221
+ if (config.value === 'false') return false;
222
+
223
+ const num = Number(config.value);
224
+ if (!Number.isNaN(num)) {
225
+ return num;
226
+ }
227
+
228
+ return config.value;
229
+ }
230
+
231
+ /**
232
+ * Get secret (decrypted)
233
+ * Execution function - performs database read and decryption
234
+ *
235
+ * @param moduleId - Module ID
236
+ * @param name - Secret name
237
+ * @returns Decrypted secret value or null if not found
238
+ */
239
+ getSecret(moduleId: string, name: string): string | null {
240
+ const masterKey = this.ensureMasterKey();
241
+
242
+ const secret = this.db
243
+ .select()
244
+ .from(secrets)
245
+ .where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name)))
246
+ .get();
247
+
248
+ if (!secret) {
249
+ return null;
250
+ }
251
+
252
+ // Decrypt and return
253
+ return decryptSecret(
254
+ {
255
+ encryptedValue: secret.encryptedValue,
256
+ iv: secret.iv,
257
+ authTag: secret.authTag,
258
+ },
259
+ masterKey,
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Get all secrets for a module (decrypted)
265
+ * Execution function - performs database read and decryption
266
+ *
267
+ * @param moduleId - Module ID
268
+ * @returns Record of secret name -> decrypted value
269
+ */
270
+ getAllSecrets(moduleId: string): Record<string, string> {
271
+ const masterKey = this.ensureMasterKey();
272
+
273
+ const secretRecords = this.db
274
+ .select()
275
+ .from(secrets)
276
+ .where(eq(secrets.moduleId, moduleId))
277
+ .all();
278
+
279
+ const result: Record<string, string> = {};
280
+
281
+ for (const secret of secretRecords) {
282
+ result[secret.name] = decryptSecret(
283
+ {
284
+ encryptedValue: secret.encryptedValue,
285
+ iv: secret.iv,
286
+ authTag: secret.authTag,
287
+ },
288
+ masterKey,
289
+ );
290
+ }
291
+
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * Look up which module provides a capability
297
+ * Execution function - performs database query
298
+ *
299
+ * @param capabilityName - Capability name (e.g., 'dns_registrar', 'idp')
300
+ * @returns Capability info or null if not found
301
+ */
302
+ findCapabilityProvider(capabilityName: string): CapabilityInfo | null {
303
+ const capability = this.db
304
+ .select()
305
+ .from(capabilities)
306
+ .where(eq(capabilities.capabilityName, capabilityName))
307
+ .get();
308
+
309
+ if (!capability) {
310
+ return null;
311
+ }
312
+
313
+ return {
314
+ moduleId: capability.moduleId,
315
+ capabilityName: capability.capabilityName,
316
+ version: capability.version,
317
+ data: capability.data as Record<string, unknown>,
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Resolve variables in parameters
323
+ * Uses the variable resolver to handle $self:, $system:, $capability:, etc.
324
+ *
325
+ * Execution function - performs database reads for variable resolution
326
+ *
327
+ * @param moduleId - Module ID for $self: resolution
328
+ * @param params - Parameters with variables to resolve
329
+ * @returns Resolved parameters
330
+ */
331
+ async resolveParameters(
332
+ moduleId: string,
333
+ params: Record<string, unknown>,
334
+ ): Promise<Record<string, unknown>> {
335
+ // Build resolution context
336
+ const context = await buildResolutionContext(moduleId, this.db);
337
+
338
+ // Resolve each parameter value
339
+ const resolved: Record<string, unknown> = {};
340
+
341
+ for (const [key, value] of Object.entries(params)) {
342
+ if (typeof value === 'string') {
343
+ const result = await resolveTemplate(value, context, this.db);
344
+ if (!result.success) {
345
+ throw new Error(
346
+ `Failed to resolve parameter '${key}': ${result.errors.map((e) => e.error).join(', ')}`,
347
+ );
348
+ }
349
+ resolved[key] = result.content;
350
+ } else if (Array.isArray(value)) {
351
+ // Resolve each array element
352
+ resolved[key] = await Promise.all(
353
+ value.map(async (item) => {
354
+ if (typeof item === 'string') {
355
+ const result = await resolveTemplate(item, context, this.db);
356
+ if (!result.success) {
357
+ throw new Error(`Failed to resolve array element in '${key}'`);
358
+ }
359
+ return result.content;
360
+ }
361
+ return item;
362
+ }),
363
+ );
364
+ } else if (typeof value === 'object' && value !== null) {
365
+ // Recursively resolve object properties
366
+ resolved[key] = await this.resolveParameters(moduleId, value as Record<string, unknown>);
367
+ } else {
368
+ // Primitive value - keep as-is
369
+ resolved[key] = value;
370
+ }
371
+ }
372
+
373
+ return resolved;
374
+ }
375
+
376
+ /**
377
+ * Generate TSIG key for DNS dynamic updates
378
+ * Planning function (Rule 10.1) - decides what type of key to generate
379
+ *
380
+ * @param algorithm - TSIG algorithm (default: hmac-sha256)
381
+ * @returns Base64-encoded TSIG key
382
+ */
383
+ generateTSIGKey(algorithm = 'hmac-sha256'): string {
384
+ // TSIG keys are typically 256-bit (32 bytes) for hmac-sha256
385
+ const keyLength = algorithm === 'hmac-sha256' ? 32 : 32;
386
+ const key = new Uint8Array(keyLength);
387
+ crypto.getRandomValues(key);
388
+ return btoa(String.fromCharCode(...key));
389
+ }
390
+
391
+ /**
392
+ * Generate secure random secret
393
+ * Planning function - decides length and encoding
394
+ *
395
+ * @param length - Length in bytes (default: 32)
396
+ * @param encoding - Encoding format (default: hex)
397
+ * @returns Random secret
398
+ */
399
+ generateSecret(length = 32, encoding: 'hex' | 'base64' = 'hex'): string {
400
+ const bytes = new Uint8Array(length);
401
+ crypto.getRandomValues(bytes);
402
+
403
+ if (encoding === 'base64') {
404
+ return btoa(String.fromCharCode(...bytes));
405
+ }
406
+
407
+ // Hex encoding
408
+ return Array.from(bytes)
409
+ .map((b) => b.toString(16).padStart(2, '0'))
410
+ .join('');
411
+ }
412
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Ansible Execution Service
3
+ *
4
+ * Executes Ansible playbooks with vault password and streaming progress
5
+ */
6
+
7
+ import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { log } from '../cli/prompts';
11
+ import { getVaultPassword } from '../secrets/vault';
12
+ import { shellEscape } from '../utils/shell';
13
+ import { executeBuildWithProgress } from './build-stream';
14
+
15
+ export interface AnsibleResult {
16
+ success: boolean;
17
+ output: string;
18
+ error?: string;
19
+ }
20
+
21
+ /**
22
+ * Execute Ansible playbook with streaming progress
23
+ * Execution function - performs software deployment
24
+ *
25
+ * @param generatedPath - Path to generated module artifacts
26
+ * @returns Ansible execution result
27
+ */
28
+ export async function executeAnsible(
29
+ generatedPath: string,
30
+ options?: { noInteractive?: boolean },
31
+ ): Promise<AnsibleResult> {
32
+ const ansibleDir = join(generatedPath, 'ansible');
33
+ const inventoryPath = join(ansibleDir, 'inventory', 'hosts.ini');
34
+ const playbookPath = join(ansibleDir, 'playbook.yml');
35
+
36
+ // Get Ansible Vault password
37
+ const vaultPassword = await getVaultPassword();
38
+
39
+ // Write vault password to temp file (same approach as ansible/secrets.ts)
40
+ // Using /dev/stdin doesn't work reliably — Ansible tries to execute it as a
41
+ // script and hits permission errors on some platforms.
42
+ const tempDir = await mkdtemp(join(tmpdir(), 'celilo-vault-'));
43
+ const passwordPath = join(tempDir, 'vault-pass');
44
+ await writeFile(passwordPath, vaultPassword, { mode: 0o600 });
45
+
46
+ const logPath = join(generatedPath, 'deploy.log');
47
+ log.info('Deploying software...');
48
+
49
+ try {
50
+ // Execute ansible-playbook with streaming progress
51
+ // Note: Paths escaped for shell execution (macOS paths contain spaces)
52
+ const result = await executeBuildWithProgress({
53
+ command: 'ansible-playbook',
54
+ args: [
55
+ '-i',
56
+ shellEscape(inventoryPath),
57
+ '--vault-password-file',
58
+ shellEscape(passwordPath),
59
+ shellEscape(playbookPath),
60
+ ],
61
+ cwd: ansibleDir,
62
+ title: 'Deploying software',
63
+ noInteractive: options?.noInteractive,
64
+ });
65
+
66
+ // Persist full output to log file for debugging
67
+ const timestamp = new Date().toISOString();
68
+ const logHeader = `\n--- Ansible deploy ${timestamp} ---\n`;
69
+ await appendFile(logPath, logHeader + result.output, 'utf-8');
70
+
71
+ if (!result.success) {
72
+ log.warn(`Full deploy log: ${logPath}`);
73
+ return {
74
+ success: false,
75
+ output: result.output,
76
+ error: result.error || 'Ansible deployment failed',
77
+ };
78
+ }
79
+
80
+ log.success('Software deployed');
81
+ return {
82
+ success: true,
83
+ output: result.output,
84
+ };
85
+ } finally {
86
+ await rm(tempDir, { recursive: true, force: true });
87
+ }
88
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Deployment Planning Service
3
+ *
4
+ * Determines what deployment steps are needed based on current state
5
+ */
6
+
7
+ import { join } from 'node:path';
8
+ import { eq } from 'drizzle-orm';
9
+ import type { DbClient } from '../db/client';
10
+ import { moduleConfigs, moduleInfrastructure } from '../db/schema';
11
+ import type { ModuleManifest } from '../manifest/schema';
12
+
13
+ export interface DeploymentPlan {
14
+ needsTerraform: boolean;
15
+ needsSSHWait: boolean;
16
+ ansiblePlaybook: string;
17
+ targetHost: {
18
+ hostname: string;
19
+ ip: string;
20
+ user: string;
21
+ };
22
+ infrastructure?: {
23
+ type: 'machine' | 'container_service';
24
+ machineId?: string;
25
+ serviceId?: string;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Determine what deployment steps are needed
31
+ * Planning function - analyzes state and returns plan
32
+ *
33
+ * - Machine infrastructure: skip Terraform (machine already exists)
34
+ * - Container service: run Terraform (need to provision container)
35
+ *
36
+ * @param moduleId - Module identifier
37
+ * @param generatedPath - Path to generated artifacts
38
+ * @param manifest - Module manifest
39
+ * @param db - Database connection
40
+ * @returns Deployment plan
41
+ */
42
+ export async function planDeployment(
43
+ moduleId: string,
44
+ generatedPath: string,
45
+ _manifest: ModuleManifest,
46
+ db: DbClient,
47
+ ): Promise<DeploymentPlan> {
48
+ const infrastructure = await db
49
+ .select()
50
+ .from(moduleInfrastructure)
51
+ .where(eq(moduleInfrastructure.moduleId, moduleId))
52
+ .get();
53
+
54
+ if (!infrastructure) {
55
+ throw new Error(
56
+ `No infrastructure selected for module ${moduleId}. Run 'celilo module generate ${moduleId}' first to select infrastructure (container service or machine) and generate templates.`,
57
+ );
58
+ }
59
+
60
+ let needsTerraform: boolean;
61
+ let needsSSHWait: boolean;
62
+
63
+ if (infrastructure.infrastructureType === 'machine') {
64
+ // Machine infrastructure: skip Terraform (machine already exists)
65
+ // No SSH wait needed (machine already running)
66
+ needsTerraform = false;
67
+ needsSSHWait = false;
68
+ } else if (infrastructure.infrastructureType === 'container_service') {
69
+ // Container service: run Terraform to provision container
70
+ needsTerraform = true;
71
+ needsSSHWait = true;
72
+ } else {
73
+ throw new Error(`Unknown infrastructure type: ${infrastructure.infrastructureType}`);
74
+ }
75
+
76
+ // Extract target host information from module config
77
+ const targetHost = await extractTargetHost(moduleId, db);
78
+
79
+ return {
80
+ needsTerraform,
81
+ needsSSHWait,
82
+ ansiblePlaybook: join(generatedPath, 'ansible', 'playbook.yml'),
83
+ targetHost,
84
+ infrastructure: infrastructure
85
+ ? {
86
+ type: infrastructure.infrastructureType as 'machine' | 'container_service',
87
+ machineId: infrastructure.machineId || undefined,
88
+ serviceId: infrastructure.serviceId || undefined,
89
+ }
90
+ : undefined,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Extract target host information from module configuration
96
+ * Execution function - queries database
97
+ *
98
+ * Must be called AFTER infrastructure variable resolution
99
+ * to pick up infrastructure-derived variables like ip.primary
100
+ *
101
+ * @param moduleId - Module identifier
102
+ * @param db - Database connection
103
+ * @returns Target host information
104
+ */
105
+ export async function extractTargetHost(
106
+ moduleId: string,
107
+ db: DbClient,
108
+ ): Promise<{ hostname: string; ip: string; user: string }> {
109
+ // Get module configuration
110
+ const configs = await db
111
+ .select()
112
+ .from(moduleConfigs)
113
+ .where(eq(moduleConfigs.moduleId, moduleId))
114
+ .all();
115
+
116
+ // Build config map
117
+ const configMap = new Map<string, string>();
118
+ for (const config of configs) {
119
+ const value = config.valueJson || config.value;
120
+ if (value) {
121
+ configMap.set(config.key, value);
122
+ }
123
+ }
124
+
125
+ // Extract hostname
126
+ const hostname = configMap.get('hostname') || moduleId;
127
+
128
+ // Extract IP - support infrastructure-derived variables
129
+ // Priority: container_ip > ip.primary > vps_ip (backward compatibility)
130
+ let ip = '';
131
+ const containerIp = configMap.get('container_ip');
132
+ const ipPrimary = configMap.get('ip.primary');
133
+ const vpsIp = configMap.get('vps_ip');
134
+
135
+ if (containerIp) {
136
+ // Container IP format: "10.0.10.10/24" - extract just IP
137
+ ip = containerIp.split('/')[0];
138
+ } else if (ipPrimary) {
139
+ // Infrastructure-derived IP (already plain format)
140
+ ip = ipPrimary;
141
+ } else if (vpsIp) {
142
+ ip = vpsIp;
143
+ }
144
+
145
+ // Extract ansible user (defaults to root)
146
+ const user = configMap.get('ansible_user') || 'root';
147
+
148
+ return {
149
+ hostname,
150
+ ip,
151
+ user,
152
+ };
153
+ }