@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,445 @@
1
+ import { join } from 'node:path';
2
+ import { log } from '../cli/prompts';
3
+ import { executeBuildWithProgress } from './build-stream';
4
+ import { validateTerraformPlanSafety } from './terraform-safety';
5
+
6
+ export interface TerraformResult {
7
+ success: boolean;
8
+ output: string;
9
+ error?: string;
10
+ exitCode?: number;
11
+ }
12
+
13
+ /**
14
+ * Execute Terraform workflow with safety validation and streaming progress
15
+ * Execution function - performs infrastructure provisioning
16
+ *
17
+ * @param generatedPath - Path to generated module artifacts
18
+ * @param phases - Phase tracking object to update during execution
19
+ * @param extraEnvVars - Optional environment variables (e.g., TF_VAR_* for credentials)
20
+ * @returns Terraform execution result
21
+ */
22
+ export async function executeTerraform(
23
+ generatedPath: string,
24
+ phases?: {
25
+ terraformInit?: boolean;
26
+ terraformPlan?: boolean;
27
+ terraformApply?: boolean;
28
+ },
29
+ extraEnvVars?: Record<string, string>,
30
+ options?: { noInteractive?: boolean },
31
+ ): Promise<TerraformResult> {
32
+ const terraformDir = join(generatedPath, 'terraform');
33
+
34
+ // Build environment variables for Terraform
35
+ const terraformEnv: Record<string, string> = {
36
+ TF_IN_AUTOMATION: '1', // Disable interactive prompts
37
+ ...extraEnvVars, // Merge in any additional env vars (e.g., TF_VAR_* credentials)
38
+ };
39
+
40
+ const noInteractive = options?.noInteractive;
41
+
42
+ // 1. Always run terraform init -upgrade
43
+ // This ensures provider versions are updated when templates change
44
+ // It's idempotent and safe to run on every deployment
45
+ const initResult = await executeTerraformCommand(
46
+ 'init',
47
+ ['-upgrade'],
48
+ terraformDir,
49
+ 'Initializing Terraform',
50
+ terraformEnv,
51
+ noInteractive,
52
+ );
53
+ if (!initResult.success) {
54
+ if (phases) phases.terraformInit = false;
55
+ const errorMessage = parseTerraformError(initResult.output);
56
+ return { ...initResult, error: errorMessage };
57
+ }
58
+
59
+ if (phases) phases.terraformInit = true;
60
+
61
+ // 2. Run terraform plan with streaming progress
62
+ let planResult = await executeBuildWithProgress({
63
+ command: 'terraform',
64
+ args: ['plan', '-detailed-exitcode', '-no-color'],
65
+ cwd: terraformDir,
66
+ title: 'Planning infrastructure',
67
+ env: terraformEnv,
68
+ noInteractive,
69
+ });
70
+
71
+ // Auto-recover from stale state lock (e.g., interrupted previous deploy)
72
+ if (!planResult.success && planResult.output.includes('Error acquiring the state lock')) {
73
+ const unlocked = await autoForceUnlock(
74
+ terraformDir,
75
+ planResult.output,
76
+ terraformEnv,
77
+ noInteractive,
78
+ );
79
+ if (unlocked) {
80
+ log.info('Retrying plan after lock recovery...');
81
+ planResult = await executeBuildWithProgress({
82
+ command: 'terraform',
83
+ args: ['plan', '-detailed-exitcode', '-no-color'],
84
+ cwd: terraformDir,
85
+ title: 'Planning infrastructure',
86
+ env: terraformEnv,
87
+ noInteractive,
88
+ });
89
+ }
90
+ }
91
+
92
+ // Terraform plan exit codes:
93
+ // 0 = no changes
94
+ // 1 = error
95
+ // 2 = changes present (success!)
96
+ const planExitCode = planResult.exitCode;
97
+
98
+ if (planExitCode !== 0 && planExitCode !== 2) {
99
+ if (phases) phases.terraformPlan = false;
100
+
101
+ // Parse common errors into clear messages
102
+ const errorMessage = parseTerraformError(planResult.output);
103
+
104
+ return {
105
+ success: false,
106
+ output: planResult.output,
107
+ error: errorMessage,
108
+ exitCode: planExitCode,
109
+ };
110
+ }
111
+
112
+ if (phases) phases.terraformPlan = true;
113
+
114
+ // Check exit code: 0 = no changes, 2 = changes present
115
+ if (planExitCode === 0) {
116
+ log.message(' No changes needed');
117
+ if (phases) phases.terraformApply = true;
118
+ return { success: true, output: planResult.output, exitCode: 0 };
119
+ }
120
+
121
+ // Exit code 2 = changes present, validate safety
122
+ const safetyCheck = validateTerraformPlanSafety(planResult.output);
123
+ if (!safetyCheck.safe) {
124
+ if (phases) phases.terraformApply = false;
125
+ return {
126
+ success: false,
127
+ output: planResult.output,
128
+ error: safetyCheck.error,
129
+ exitCode: 2,
130
+ };
131
+ }
132
+
133
+ // 3. Run terraform apply with streaming progress
134
+ let applyResult = await executeTerraformCommand(
135
+ 'apply',
136
+ ['-auto-approve'],
137
+ terraformDir,
138
+ 'Applying infrastructure',
139
+ terraformEnv,
140
+ noInteractive,
141
+ );
142
+
143
+ // Auto-recover from stale state lock during apply
144
+ if (!applyResult.success && applyResult.output.includes('Error acquiring the state lock')) {
145
+ const unlocked = await autoForceUnlock(
146
+ terraformDir,
147
+ applyResult.output,
148
+ terraformEnv,
149
+ noInteractive,
150
+ );
151
+ if (unlocked) {
152
+ log.info('Retrying apply after lock recovery...');
153
+ applyResult = await executeTerraformCommand(
154
+ 'apply',
155
+ ['-auto-approve'],
156
+ terraformDir,
157
+ 'Applying infrastructure',
158
+ terraformEnv,
159
+ noInteractive,
160
+ );
161
+ }
162
+ }
163
+
164
+ if (!applyResult.success) {
165
+ // Check if failure is due to "already exists" - this happens when Terraform
166
+ // loses connection during creation but the resource was actually created
167
+ if (applyResult.output.includes('already exists')) {
168
+ log.warn('Resource already exists - attempting automatic recovery...');
169
+
170
+ // Try to import existing resource into state
171
+ const importResult = await attemptAutoImport(terraformDir, applyResult.output, terraformEnv);
172
+
173
+ if (importResult.success) {
174
+ log.success('Resource imported into Terraform state');
175
+ log.message(' Retrying deployment...');
176
+
177
+ // Retry apply after successful import
178
+ const retryResult = await executeTerraformCommand(
179
+ 'apply',
180
+ ['-auto-approve'],
181
+ terraformDir,
182
+ 'Applying infrastructure',
183
+ terraformEnv,
184
+ noInteractive,
185
+ );
186
+
187
+ if (!retryResult.success) {
188
+ if (phases) phases.terraformApply = false;
189
+ return retryResult;
190
+ }
191
+
192
+ if (phases) phases.terraformApply = true;
193
+ log.success('Infrastructure deployed (recovered from state drift)');
194
+ return retryResult;
195
+ }
196
+ log.error('Automatic recovery failed - manual import required');
197
+ if (phases) phases.terraformApply = false;
198
+ return {
199
+ ...applyResult,
200
+ error: `${applyResult.error}\n\nAutomatic recovery failed. Manual fix:\ncd "${terraformDir}"\nterraform import ${importResult.suggestion || '<resource>'}`,
201
+ };
202
+ }
203
+
204
+ if (phases) phases.terraformApply = false;
205
+ return applyResult;
206
+ }
207
+
208
+ if (phases) phases.terraformApply = true;
209
+ log.success('Infrastructure deployed');
210
+ return applyResult;
211
+ }
212
+
213
+ /**
214
+ * Attempt to automatically import a resource that already exists
215
+ * Parses Terraform error output to determine resource type and ID, then imports it
216
+ *
217
+ * @param terraformDir - Terraform working directory
218
+ * @param errorOutput - Terraform error output containing "already exists"
219
+ * @param terraformEnv - Environment variables including provider credentials
220
+ * @returns Import result with success status and suggestion
221
+ */
222
+ async function attemptAutoImport(
223
+ terraformDir: string,
224
+ errorOutput: string,
225
+ terraformEnv: Record<string, string>,
226
+ ): Promise<{ success: boolean; suggestion?: string }> {
227
+ // Parse error message to extract resource info
228
+ // Example: "CT 200 already exists on node 'node2'"
229
+ // Example: "with proxmox_lxc.homebridge"
230
+
231
+ const resourceMatch = errorOutput.match(/with\s+([\w_]+\.\w+)/);
232
+ if (!resourceMatch) {
233
+ return { success: false, suggestion: '<resource> <id>' };
234
+ }
235
+
236
+ const resourceName = resourceMatch[1]; // e.g., "proxmox_lxc.homebridge"
237
+
238
+ // Try to extract resource ID from error message
239
+ let resourceId: string | null = null;
240
+
241
+ // LXC container: "CT 200 already exists on node 'node2'" -> "node2/lxc/200"
242
+ const lxcMatch = errorOutput.match(/CT (\d+) already exists on node '(\w+)'/);
243
+ if (lxcMatch) {
244
+ const vmid = lxcMatch[1];
245
+ const node = lxcMatch[2];
246
+ resourceId = `${node}/lxc/${vmid}`;
247
+ }
248
+
249
+ // VM: "VM 100 already exists" -> need to determine format
250
+ // Add more patterns as needed for other resource types
251
+
252
+ if (!resourceId) {
253
+ return {
254
+ success: false,
255
+ suggestion: `${resourceName} <id>`,
256
+ };
257
+ }
258
+
259
+ // Attempt the import
260
+ log.info(` Importing ${resourceName} as ${resourceId}...`);
261
+
262
+ const result = await executeBuildWithProgress({
263
+ command: 'terraform',
264
+ args: ['import', resourceName, resourceId],
265
+ cwd: terraformDir,
266
+ title: 'Importing existing resource',
267
+ env: {
268
+ ...terraformEnv,
269
+ TF_IN_AUTOMATION: '1',
270
+ },
271
+ });
272
+
273
+ return {
274
+ success: result.success,
275
+ suggestion: `${resourceName} ${resourceId}`,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Execute a single Terraform command with streaming output
281
+ * Execution function - runs terraform command with fuel-gauge progress
282
+ *
283
+ * @param command - Terraform subcommand (init, apply, etc.)
284
+ * @param args - Additional arguments
285
+ * @param cwd - Working directory
286
+ * @param title - Progress indicator title
287
+ * @param env - Environment variables
288
+ * @returns Execution result
289
+ */
290
+ /**
291
+ * Auto-recover from a stale Terraform state lock.
292
+ * Parses the lock ID from the error output and runs force-unlock.
293
+ */
294
+ async function autoForceUnlock(
295
+ terraformDir: string,
296
+ errorOutput: string,
297
+ terraformEnv: Record<string, string>,
298
+ noInteractive?: boolean,
299
+ ): Promise<boolean> {
300
+ const lockIdMatch = errorOutput.match(/ID:\s+([0-9a-f-]+)/);
301
+ if (!lockIdMatch) {
302
+ log.warn('State lock detected but could not parse lock ID');
303
+ return false;
304
+ }
305
+
306
+ const lockId = lockIdMatch[1];
307
+ log.warn(`Stale state lock detected (${lockId}) -- auto-recovering...`);
308
+
309
+ const result = await executeBuildWithProgress({
310
+ command: 'terraform',
311
+ args: ['force-unlock', '-force', lockId],
312
+ cwd: terraformDir,
313
+ title: 'Removing stale state lock',
314
+ env: { ...terraformEnv, TF_IN_AUTOMATION: '1' },
315
+ noInteractive,
316
+ });
317
+
318
+ if (result.success) {
319
+ log.success('State lock removed');
320
+ return true;
321
+ }
322
+
323
+ log.error(`Failed to remove state lock: ${result.error || result.output}`);
324
+ return false;
325
+ }
326
+
327
+ /**
328
+ * Parse Terraform error output into a clear, actionable message
329
+ */
330
+ function parseTerraformError(output: string): string {
331
+ // Proxmox unreachable
332
+ if (output.includes('dial tcp') && output.includes('connect: operation timed out')) {
333
+ const ipMatch = output.match(/dial tcp ([^:]+:\d+)/);
334
+ const target = ipMatch ? ipMatch[1] : 'unknown';
335
+ return `Proxmox server unreachable at ${target}\n\nCheck:\n - Is the Proxmox server running?\n - Can this machine reach ${target}?\n - Is a firewall blocking the connection?`;
336
+ }
337
+
338
+ // Proxmox auth error
339
+ if (output.includes('401') && output.includes('proxmox')) {
340
+ return 'Proxmox authentication failed\n\nCheck:\n - API token ID and secret in service config\n - Token permissions in Proxmox';
341
+ }
342
+
343
+ // Digital Ocean auth error
344
+ if (output.includes('401') && output.includes('digitalocean')) {
345
+ return 'Digital Ocean authentication failed\n\nCheck:\n - API token in service config\n - Token has write permissions';
346
+ }
347
+
348
+ // Connection refused
349
+ if (output.includes('connection refused')) {
350
+ const ipMatch = output.match(/dial tcp ([^:]+:\d+)/);
351
+ const target = ipMatch ? ipMatch[1] : 'unknown';
352
+ return `Connection refused to ${target}\n\nThe server is not accepting connections on that port.`;
353
+ }
354
+
355
+ // DNS resolution failure
356
+ if (output.includes('no such host') || output.includes('could not resolve')) {
357
+ return 'DNS resolution failed for the infrastructure provider\n\nCheck your network connection and DNS settings.';
358
+ }
359
+
360
+ // Proxmox permission denied (e.g., API token can't set certain LXC feature flags)
361
+ if (output.includes('Permission check failed') && output.includes('proxmox')) {
362
+ const reasonMatch = output.match(/Permission check failed \(([^)]+)\)/);
363
+ const reason = reasonMatch ? reasonMatch[1] : 'insufficient permissions';
364
+ return `Proxmox permission denied: ${reason}\n\nYour API token lacks the required privileges.\n\nCheck:\n - Token permissions in Proxmox (Datacenter → Permissions → API Tokens)\n - Some operations (e.g., LXC feature flags like keyctl) require root@pam\n - Consider removing unsupported features from the module's Terraform template`;
365
+ }
366
+
367
+ // Terraform registry unreachable (provider download failure during init)
368
+ if (
369
+ output.includes('registry.terraform.io') &&
370
+ (output.includes('request canceled') ||
371
+ output.includes('Timeout exceeded') ||
372
+ output.includes('could not connect'))
373
+ ) {
374
+ return 'Cannot reach Terraform registry (registry.terraform.io)\n\nTerraform needs internet access to download provider plugins.\n\nCheck:\n - Network connectivity from the machine running Celilo\n - DNS resolution: nslookup registry.terraform.io\n - Firewall rules allowing outbound HTTPS (port 443)';
375
+ }
376
+
377
+ // Generic: extract the Error: line
378
+ const errorLineMatch = output.match(/Error: (.+?)(?:\n|$)/);
379
+ if (errorLineMatch) {
380
+ return errorLineMatch[1].trim();
381
+ }
382
+
383
+ return `Terraform failed:\n${output.substring(0, 300)}`;
384
+ }
385
+
386
+ async function executeTerraformCommand(
387
+ command: string,
388
+ args: string[],
389
+ cwd: string,
390
+ title: string,
391
+ env: Record<string, string>,
392
+ noInteractive?: boolean,
393
+ ): Promise<TerraformResult> {
394
+ // Execute with streaming progress
395
+ const result = await executeBuildWithProgress({
396
+ command: 'terraform',
397
+ args: [command, ...args],
398
+ cwd,
399
+ title,
400
+ env,
401
+ noInteractive,
402
+ });
403
+
404
+ return {
405
+ success: result.success,
406
+ output: result.output,
407
+ error: result.error,
408
+ exitCode: result.exitCode,
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Parse Terraform outputs after successful apply
414
+ * Execution function - queries Terraform state for outputs
415
+ *
416
+ * @param terraformDir - Terraform working directory
417
+ * @returns Parsed Terraform outputs or null if none exist
418
+ */
419
+ export async function parseTerraformOutputs(
420
+ terraformDir: string,
421
+ ): Promise<Record<string, unknown> | null> {
422
+ const result = await executeBuildWithProgress({
423
+ command: 'terraform',
424
+ args: ['output', '-json'],
425
+ cwd: terraformDir,
426
+ title: 'Reading Terraform outputs',
427
+ env: { TF_IN_AUTOMATION: '1' },
428
+ });
429
+
430
+ if (!result.success) {
431
+ // No outputs defined is not an error - return null
432
+ if (result.output.includes('no outputs')) {
433
+ return null;
434
+ }
435
+ throw new Error(`Failed to read Terraform outputs: ${result.error}`);
436
+ }
437
+
438
+ try {
439
+ return JSON.parse(result.output) as Record<string, unknown>;
440
+ } catch (error) {
441
+ throw new Error(
442
+ `Failed to parse Terraform outputs JSON: ${error instanceof Error ? error.message : 'Unknown error'}`,
443
+ );
444
+ }
445
+ }