@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,274 @@
1
+ /**
2
+ * Deployment Pre-flight Check
3
+ *
4
+ * Fast validation that catches deployment-blocking issues BEFORE
5
+ * spinning up infrastructure. Designed to run in ~100ms, not ~120s.
6
+ *
7
+ * Checks:
8
+ * 1. Module exists and has a valid manifest
9
+ * 2. All required capabilities have installed providers
10
+ * 3. All required variables have values (from config, derivation,
11
+ * or capability chain)
12
+ * 4. Capability derivation chains resolve to actual values (not
13
+ * unresolved templates like $self:primary_domain)
14
+ * 5. Infrastructure is available for the module's zone (machine or
15
+ * container service exists)
16
+ *
17
+ * Does NOT:
18
+ * - Run template generation
19
+ * - Run Ansible or Terraform
20
+ * - Start any containers
21
+ * - Modify any state
22
+ */
23
+
24
+ import { eq } from 'drizzle-orm';
25
+ import type { DbClient } from '../db/client';
26
+ import { getDb } from '../db/client';
27
+ import { capabilities, moduleConfigs, modules, secrets } from '../db/schema';
28
+ import type { ModuleManifest } from '../manifest/schema';
29
+ import { buildResolutionContext } from '../variables/context';
30
+
31
+ export interface PreflightResult {
32
+ success: boolean;
33
+ moduleId: string;
34
+ errors: PreflightError[];
35
+ warnings: PreflightWarning[];
36
+ }
37
+
38
+ export interface PreflightError {
39
+ category:
40
+ | 'missing-config'
41
+ | 'missing-capability'
42
+ | 'unresolved-template'
43
+ | 'no-infrastructure'
44
+ | 'missing-secret';
45
+ message: string;
46
+ variable?: string;
47
+ suggestion?: string;
48
+ }
49
+
50
+ export interface PreflightWarning {
51
+ category: string;
52
+ message: string;
53
+ }
54
+
55
+ /**
56
+ * Run a fast pre-flight check for a module deployment.
57
+ *
58
+ * Returns structured errors that describe exactly what's wrong and
59
+ * how to fix it, without actually attempting the deployment.
60
+ */
61
+ export async function runPreflight(
62
+ moduleId: string,
63
+ db: DbClient = getDb(),
64
+ ): Promise<PreflightResult> {
65
+ const errors: PreflightError[] = [];
66
+ const warnings: PreflightWarning[] = [];
67
+
68
+ // 1. Module exists?
69
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
70
+ if (!module) {
71
+ return {
72
+ success: false,
73
+ moduleId,
74
+ errors: [
75
+ {
76
+ category: 'missing-config',
77
+ message: `Module '${moduleId}' not found`,
78
+ suggestion: 'Run: celilo module import <path>',
79
+ },
80
+ ],
81
+ warnings: [],
82
+ };
83
+ }
84
+
85
+ const manifest = module.manifestData as ModuleManifest;
86
+
87
+ // 2. Required capabilities have providers?
88
+ if (manifest.requires?.capabilities) {
89
+ for (const cap of manifest.requires.capabilities) {
90
+ const provider = db
91
+ .select()
92
+ .from(capabilities)
93
+ .where(eq(capabilities.capabilityName, cap.name))
94
+ .get();
95
+ if (!provider) {
96
+ errors.push({
97
+ category: 'missing-capability',
98
+ message: `Required capability '${cap.name}' has no provider installed`,
99
+ suggestion: `Deploy a module that provides '${cap.name}' first`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+
105
+ // 3. Required variables have values?
106
+ const configRows = db
107
+ .select()
108
+ .from(moduleConfigs)
109
+ .where(eq(moduleConfigs.moduleId, moduleId))
110
+ .all();
111
+ const configMap = new Map(
112
+ configRows.map((c) => [c.key, c.valueJson ? JSON.parse(c.valueJson) : c.value]),
113
+ );
114
+
115
+ const secretRows = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
116
+ const secretNames = new Set(secretRows.map((s) => s.name));
117
+
118
+ if (manifest.variables?.owns) {
119
+ for (const variable of manifest.variables.owns) {
120
+ if (!variable.required) continue;
121
+
122
+ // Infrastructure/terraform variables are auto-derived during deploy
123
+ if (variable.source === 'infrastructure' || variable.source === 'terraform') {
124
+ continue;
125
+ }
126
+
127
+ const hasValue = configMap.has(variable.name);
128
+ const hasDeriveFrom = !!variable.derive_from;
129
+ const hasDefault = variable.default !== undefined;
130
+
131
+ if (!hasValue && !hasDeriveFrom && !hasDefault) {
132
+ errors.push({
133
+ category: 'missing-config',
134
+ message: `Required variable '${variable.name}' is not configured`,
135
+ variable: variable.name,
136
+ suggestion: `celilo module config set ${moduleId} ${variable.name} <value>`,
137
+ });
138
+ }
139
+ }
140
+ }
141
+
142
+ // Check secrets
143
+ if (manifest.secrets?.declares) {
144
+ for (const secret of manifest.secrets.declares) {
145
+ if (!secret.required) continue;
146
+ const hasSecret = secretNames.has(secret.name);
147
+ const hasGenerate = !!secret.generate;
148
+ if (!hasSecret && !hasGenerate) {
149
+ errors.push({
150
+ category: 'missing-secret',
151
+ message: `Required secret '${secret.name}' is not set`,
152
+ variable: secret.name,
153
+ suggestion: `celilo module secret set ${moduleId} ${secret.name} <value>`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ // 4. Check for unresolved template strings in configured values
160
+ // This catches the bug where capability derivation chains produce
161
+ // raw template strings like "$self:primary_domain" instead of
162
+ // actual values.
163
+ try {
164
+ const context = await buildResolutionContext(moduleId, db);
165
+ for (const [key, value] of Object.entries(context.selfConfig)) {
166
+ if (
167
+ typeof value === 'string' &&
168
+ (value.includes('$self:') ||
169
+ value.includes('$system:') ||
170
+ value.includes('$capability:') ||
171
+ value.includes('$secret:'))
172
+ ) {
173
+ errors.push({
174
+ category: 'unresolved-template',
175
+ message: `Variable '${key}' has unresolved template: ${value}`,
176
+ variable: key,
177
+ suggestion: `Set it explicitly: celilo module config set ${moduleId} ${key} <value>`,
178
+ });
179
+ }
180
+ }
181
+ } catch (error) {
182
+ // Resolution context may fail — that's informative too
183
+ warnings.push({
184
+ category: 'resolution',
185
+ message: `Variable resolution warning: ${error instanceof Error ? error.message : String(error)}`,
186
+ });
187
+ }
188
+
189
+ // 5. Infrastructure availability (for modules that need it)
190
+ if (manifest.requires?.machine) {
191
+ const zone = manifest.requires.machine.zone;
192
+ if (zone) {
193
+ // Simplified check: is there a machine or service for the module's zone?
194
+ // The full infrastructure selector (selectInfrastructure) needs a Module
195
+ // object, which is more than we want for a fast pre-flight. Instead,
196
+ // check if the deploy-validation flow would fail at the infrastructure
197
+ // selection step by looking for machines/services in the zone directly.
198
+ try {
199
+ const { listMachines } = await import('./machine-pool');
200
+ const machines = await listMachines();
201
+ const { listContainerServices } = await import('./container-service');
202
+ const services = await listContainerServices();
203
+
204
+ const hasMachine = machines.some((m) => m.zone === zone || m.earmarkedModule === moduleId);
205
+ const hasService = services.some((s) => {
206
+ const zones = s.zones || [];
207
+ return zones.includes(zone);
208
+ });
209
+
210
+ if (!hasMachine && !hasService) {
211
+ errors.push({
212
+ category: 'no-infrastructure',
213
+ message: `No infrastructure available for zone '${zone}'`,
214
+ suggestion: `Add a machine: celilo machine add <ip> --zone ${zone}\nOr add a container service: celilo service add proxmox`,
215
+ });
216
+ }
217
+ } catch {
218
+ // Machine/service queries may fail — non-fatal for preflight
219
+ }
220
+ }
221
+ }
222
+
223
+ return {
224
+ success: errors.length === 0,
225
+ moduleId,
226
+ errors,
227
+ warnings,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Format a preflight result as a human-readable string.
233
+ */
234
+ export function formatPreflightResult(result: PreflightResult): string {
235
+ if (result.success) {
236
+ const warnCount = result.warnings.length;
237
+ return warnCount > 0
238
+ ? `Pre-flight check passed with ${warnCount} warning(s)`
239
+ : 'Pre-flight check passed';
240
+ }
241
+
242
+ const lines: string[] = [`Pre-flight check failed for '${result.moduleId}':`, ''];
243
+
244
+ for (const error of result.errors) {
245
+ lines.push(` ${errorIcon(error.category)} ${error.message}`);
246
+ if (error.suggestion) {
247
+ lines.push(` Fix: ${error.suggestion}`);
248
+ }
249
+ }
250
+
251
+ if (result.warnings.length > 0) {
252
+ lines.push('');
253
+ for (const warning of result.warnings) {
254
+ lines.push(` ⚠ ${warning.message}`);
255
+ }
256
+ }
257
+
258
+ return lines.join('\n');
259
+ }
260
+
261
+ function errorIcon(category: PreflightError['category']): string {
262
+ switch (category) {
263
+ case 'missing-config':
264
+ return '✗';
265
+ case 'missing-capability':
266
+ return '◯';
267
+ case 'unresolved-template':
268
+ return '⟲';
269
+ case 'no-infrastructure':
270
+ return '▢';
271
+ case 'missing-secret':
272
+ return '🔑';
273
+ }
274
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * SSH Readiness Service
3
+ *
4
+ * Waits for SSH to become available on target host with exponential backoff
5
+ */
6
+
7
+ import { spawn } from 'node:child_process';
8
+ import { FuelGauge } from '../cli/fuel-gauge';
9
+ import { log } from '../cli/prompts';
10
+
11
+ export interface SSHResult {
12
+ success: boolean;
13
+ error?: string;
14
+ attempts?: number;
15
+ }
16
+
17
+ /**
18
+ * Wait for SSH to become available on target host
19
+ * Execution function - polls SSH connectivity with backoff
20
+ *
21
+ * @param host - Target host IP address
22
+ * @param user - SSH user (default: root)
23
+ * @param timeoutSeconds - Timeout in seconds (default: 120)
24
+ * @returns SSH readiness result
25
+ */
26
+ export async function waitForSSH(
27
+ host: string,
28
+ user = 'root',
29
+ timeoutSeconds = 120,
30
+ ): Promise<SSHResult> {
31
+ const startTime = Date.now();
32
+ const timeoutMs = timeoutSeconds * 1000;
33
+ let attempt = 0;
34
+
35
+ const gauge = new FuelGauge(`Waiting for SSH access to ${user}@${host}`);
36
+ gauge.start();
37
+
38
+ while (Date.now() - startTime < timeoutMs) {
39
+ attempt++;
40
+
41
+ gauge.addOutput(`Attempt ${attempt} - connecting to ${user}@${host}...`);
42
+
43
+ const connected = await trySSHConnection(host, user);
44
+
45
+ if (connected) {
46
+ gauge.stop(true);
47
+ log.success(`SSH ready after ${attempt} attempt${attempt === 1 ? '' : 's'}`);
48
+ return { success: true, attempts: attempt };
49
+ }
50
+
51
+ // Calculate backoff delay: 1s, 1.5s, 2.25s, ..., max 10s
52
+ const backoffDelay = Math.min(1000 * 1.5 ** (attempt - 1), 10000);
53
+ await sleep(backoffDelay);
54
+ }
55
+
56
+ gauge.stop(false);
57
+
58
+ return {
59
+ success: false,
60
+ error: `SSH connection timeout after ${timeoutSeconds} seconds (${attempt} attempts)\n\nTarget: ${user}@${host}\n\nPossible causes:\n - Container failed to start\n - SSH service not configured\n - Firewall blocking connection\n - Wrong IP address`,
61
+ attempts: attempt,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Try SSH connection to host
67
+ * Execution function - attempts single SSH connection
68
+ *
69
+ * @param host - Target host IP
70
+ * @param user - SSH user
71
+ * @returns True if connection successful
72
+ */
73
+ async function trySSHConnection(host: string, user: string): Promise<boolean> {
74
+ return new Promise((resolve) => {
75
+ // Try SSH connection with short timeout
76
+ const child = spawn(
77
+ 'ssh',
78
+ [
79
+ '-o',
80
+ 'ConnectTimeout=5',
81
+ '-o',
82
+ 'StrictHostKeyChecking=no',
83
+ '-o',
84
+ 'UserKnownHostsFile=/dev/null',
85
+ '-o',
86
+ 'LogLevel=ERROR',
87
+ `${user}@${host}`,
88
+ 'echo',
89
+ 'ready',
90
+ ],
91
+ {
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ },
94
+ );
95
+
96
+ let output = '';
97
+
98
+ child.stdout.on('data', (data) => {
99
+ output += data.toString();
100
+ });
101
+
102
+ child.on('close', (exitCode) => {
103
+ // Success if command executed and returned "ready"
104
+ if (exitCode === 0 && output.trim() === 'ready') {
105
+ resolve(true);
106
+ } else {
107
+ resolve(false);
108
+ }
109
+ });
110
+
111
+ child.on('error', () => {
112
+ resolve(false);
113
+ });
114
+
115
+ // Timeout after 6 seconds
116
+ setTimeout(() => {
117
+ child.kill();
118
+ resolve(false);
119
+ }, 6000);
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Sleep for specified milliseconds
125
+ * Utility function
126
+ *
127
+ * @param ms - Milliseconds to sleep
128
+ */
129
+ function sleep(ms: number): Promise<void> {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
@@ -0,0 +1,55 @@
1
+ import { afterEach, beforeEach, describe, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+
6
+ const TEST_TERRAFORM_DIR = '/tmp/test-terraform-outputs';
7
+
8
+ beforeEach(async () => {
9
+ // Create test directory
10
+ await mkdir(TEST_TERRAFORM_DIR, { recursive: true });
11
+ });
12
+
13
+ afterEach(async () => {
14
+ // Clean up test directory
15
+ if (existsSync(TEST_TERRAFORM_DIR)) {
16
+ await rm(TEST_TERRAFORM_DIR, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ describe('parseTerraformOutputs', () => {
21
+ test('parses wrapped Terraform output format', async () => {
22
+ // Create a mock terraform state directory
23
+ const tfDir = join(TEST_TERRAFORM_DIR, 'wrapped');
24
+ await mkdir(tfDir, { recursive: true });
25
+
26
+ // Mock terraform output -json by creating a state file
27
+ // (In reality, terraform reads from .tfstate, but we'll mock the command)
28
+ // We can't actually test this without mocking executeBuildWithProgress
29
+
30
+ // For now, skip this test - it requires mocking the terraform command
31
+ // The function is tested indirectly through integration tests
32
+ });
33
+
34
+ test('returns null when no outputs defined', async () => {
35
+ // This would require mocking terraform command that returns "no outputs"
36
+ // Skip for now - tested through integration tests
37
+ });
38
+
39
+ test('throws on invalid JSON output', async () => {
40
+ // This would require mocking terraform command that returns invalid JSON
41
+ // Skip for now - tested through integration tests
42
+ });
43
+ });
44
+
45
+ // Note: These tests are placeholders. The parseTerraformOutputs function
46
+ // calls executeBuildWithProgress which executes actual terraform commands.
47
+ // Proper testing requires either:
48
+ // 1. Mocking executeBuildWithProgress (refactor needed)
49
+ // 2. Integration tests with real terraform (slow)
50
+ // 3. Dependency injection of the executor (architectural change)
51
+ //
52
+ // We'll rely on:
53
+ // - Manual testing with real modules
54
+ // - Integration tests in test-integration/
55
+ // - Unit tests of property-extractor (already passing)