@build-astron-co/nimbus 0.4.2 → 0.4.3

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 (430) hide show
  1. package/dist/src/agent/compaction-agent.js +24 -12
  2. package/dist/src/agent/context-manager.js +2 -1
  3. package/dist/src/agent/expand-files.js +2 -1
  4. package/dist/src/agent/loop.js +71 -33
  5. package/dist/src/agent/permissions.js +4 -2
  6. package/dist/src/agent/system-prompt.js +34 -17
  7. package/dist/src/app.js +1 -1
  8. package/dist/src/auth/keychain.js +8 -4
  9. package/dist/src/auth/store.js +70 -107
  10. package/dist/src/cli/init.js +35 -19
  11. package/dist/src/cli/run.js +18 -10
  12. package/dist/src/cli/serve.js +4 -2
  13. package/dist/src/cli.js +52 -11
  14. package/dist/src/commands/alias.js +5 -3
  15. package/dist/src/commands/audit/index.js +2 -1
  16. package/dist/src/commands/aws-terraform.js +36 -18
  17. package/dist/src/commands/completions.js +1 -1
  18. package/dist/src/commands/config.js +3 -2
  19. package/dist/src/commands/connect-github.js +92 -0
  20. package/dist/src/commands/cost/index.js +3 -2
  21. package/dist/src/commands/deploy.js +15 -10
  22. package/dist/src/commands/doctor.js +6 -3
  23. package/dist/src/commands/drift/index.js +2 -1
  24. package/dist/src/commands/export.js +5 -3
  25. package/dist/src/commands/generate-terraform.js +110 -2
  26. package/dist/src/commands/import.js +3 -3
  27. package/dist/src/commands/incident.js +10 -5
  28. package/dist/src/commands/login.js +8 -93
  29. package/dist/src/commands/logs.js +16 -8
  30. package/dist/src/commands/onboarding.js +6 -4
  31. package/dist/src/commands/pipeline.js +6 -3
  32. package/dist/src/commands/plugin.js +3 -2
  33. package/dist/src/commands/profile.js +27 -14
  34. package/dist/src/commands/questionnaire.js +1 -1
  35. package/dist/src/commands/rollback.js +3 -2
  36. package/dist/src/commands/rollout.js +5 -3
  37. package/dist/src/commands/runbook.js +17 -10
  38. package/dist/src/commands/schedule.js +10 -5
  39. package/dist/src/commands/status.js +2 -1
  40. package/dist/src/commands/team-context.js +12 -7
  41. package/dist/src/commands/template.js +1 -1
  42. package/dist/src/commands/tf/index.js +6 -3
  43. package/dist/src/commands/version.js +6 -3
  44. package/dist/src/commands/watch.js +6 -3
  45. package/dist/src/compat/sqlite.js +5 -3
  46. package/dist/src/config/mode-store.js +2 -1
  47. package/dist/src/config/profiles.js +4 -2
  48. package/dist/src/config/types.js +2 -1
  49. package/dist/src/engine/executor.js +8 -4
  50. package/dist/src/engine/planner.js +9 -5
  51. package/dist/src/llm/providers/anthropic.js +6 -3
  52. package/dist/src/llm/providers/ollama.js +1 -1
  53. package/dist/src/llm/router.js +22 -7
  54. package/dist/src/sessions/manager.js +6 -3
  55. package/dist/src/sharing/viewer.js +2 -1
  56. package/dist/src/tools/file-ops.js +1 -2
  57. package/dist/src/tools/schemas/devops.js +197 -108
  58. package/dist/src/tools/schemas/standard.js +1 -1
  59. package/dist/src/ui/App.js +25 -13
  60. package/dist/src/ui/FileDiffModal.js +22 -11
  61. package/dist/src/ui/HelpModal.js +2 -1
  62. package/dist/src/ui/InputBox.js +6 -3
  63. package/dist/src/ui/MessageList.js +40 -20
  64. package/dist/src/ui/TerminalPane.js +2 -1
  65. package/dist/src/ui/ToolCallDisplay.js +12 -6
  66. package/dist/src/ui/TreePane.js +2 -1
  67. package/dist/src/ui/ink/index.js +37 -21
  68. package/dist/src/watcher/index.js +8 -4
  69. package/package.json +3 -5
  70. package/src/__tests__/alias.test.ts +0 -133
  71. package/src/__tests__/app.test.ts +0 -76
  72. package/src/__tests__/audit.test.ts +0 -877
  73. package/src/__tests__/circuit-breaker.test.ts +0 -116
  74. package/src/__tests__/cli-run.test.ts +0 -351
  75. package/src/__tests__/compat-sqlite.test.ts +0 -68
  76. package/src/__tests__/context-manager.test.ts +0 -632
  77. package/src/__tests__/context.test.ts +0 -242
  78. package/src/__tests__/devops-terminal-gaps.test.ts +0 -718
  79. package/src/__tests__/doctor.test.ts +0 -48
  80. package/src/__tests__/enterprise.test.ts +0 -401
  81. package/src/__tests__/export.test.ts +0 -236
  82. package/src/__tests__/gap-11-18-20.test.ts +0 -958
  83. package/src/__tests__/generator.test.ts +0 -433
  84. package/src/__tests__/helm-streaming.test.ts +0 -127
  85. package/src/__tests__/hooks.test.ts +0 -582
  86. package/src/__tests__/incident.test.ts +0 -179
  87. package/src/__tests__/init.test.ts +0 -487
  88. package/src/__tests__/intent-parser.test.ts +0 -229
  89. package/src/__tests__/llm-router.test.ts +0 -209
  90. package/src/__tests__/logs.test.ts +0 -107
  91. package/src/__tests__/loop-errors.test.ts +0 -244
  92. package/src/__tests__/lsp.test.ts +0 -293
  93. package/src/__tests__/modes.test.ts +0 -336
  94. package/src/__tests__/perf-optimizations.test.ts +0 -847
  95. package/src/__tests__/permissions.test.ts +0 -338
  96. package/src/__tests__/pipeline.test.ts +0 -50
  97. package/src/__tests__/polish-phase3.test.ts +0 -340
  98. package/src/__tests__/profile.test.ts +0 -237
  99. package/src/__tests__/rollback.test.ts +0 -83
  100. package/src/__tests__/runbook.test.ts +0 -219
  101. package/src/__tests__/schedule.test.ts +0 -206
  102. package/src/__tests__/serve.test.ts +0 -275
  103. package/src/__tests__/sessions.test.ts +0 -322
  104. package/src/__tests__/sharing.test.ts +0 -340
  105. package/src/__tests__/snapshots.test.ts +0 -581
  106. package/src/__tests__/standalone-migration.test.ts +0 -199
  107. package/src/__tests__/state-db.test.ts +0 -334
  108. package/src/__tests__/status.test.ts +0 -158
  109. package/src/__tests__/stream-with-tools.test.ts +0 -778
  110. package/src/__tests__/subagents.test.ts +0 -176
  111. package/src/__tests__/system-prompt.test.ts +0 -248
  112. package/src/__tests__/terminal-gap-v2.test.ts +0 -395
  113. package/src/__tests__/terminal-parity.test.ts +0 -393
  114. package/src/__tests__/tf-apply.test.ts +0 -187
  115. package/src/__tests__/tool-converter.test.ts +0 -256
  116. package/src/__tests__/tool-schemas.test.ts +0 -602
  117. package/src/__tests__/tools.test.ts +0 -144
  118. package/src/__tests__/version-json.test.ts +0 -184
  119. package/src/__tests__/version.test.ts +0 -49
  120. package/src/__tests__/watch.test.ts +0 -129
  121. package/src/agent/compaction-agent.ts +0 -266
  122. package/src/agent/context-manager.ts +0 -499
  123. package/src/agent/context.ts +0 -427
  124. package/src/agent/deploy-preview.ts +0 -487
  125. package/src/agent/expand-files.ts +0 -108
  126. package/src/agent/index.ts +0 -68
  127. package/src/agent/loop.ts +0 -1998
  128. package/src/agent/modes.ts +0 -429
  129. package/src/agent/permissions.ts +0 -513
  130. package/src/agent/subagents/base.ts +0 -116
  131. package/src/agent/subagents/cost.ts +0 -51
  132. package/src/agent/subagents/explore.ts +0 -42
  133. package/src/agent/subagents/general.ts +0 -54
  134. package/src/agent/subagents/index.ts +0 -102
  135. package/src/agent/subagents/infra.ts +0 -59
  136. package/src/agent/subagents/security.ts +0 -69
  137. package/src/agent/system-prompt.ts +0 -990
  138. package/src/app.ts +0 -180
  139. package/src/audit/activity-log.ts +0 -290
  140. package/src/audit/compliance-checker.ts +0 -540
  141. package/src/audit/cost-tracker.ts +0 -318
  142. package/src/audit/index.ts +0 -23
  143. package/src/audit/security-scanner.ts +0 -641
  144. package/src/auth/guard.ts +0 -75
  145. package/src/auth/index.ts +0 -56
  146. package/src/auth/keychain.ts +0 -82
  147. package/src/auth/oauth.ts +0 -465
  148. package/src/auth/providers.ts +0 -470
  149. package/src/auth/sso.ts +0 -113
  150. package/src/auth/store.ts +0 -505
  151. package/src/auth/types.ts +0 -187
  152. package/src/build.ts +0 -141
  153. package/src/cli/index.ts +0 -16
  154. package/src/cli/init.ts +0 -1227
  155. package/src/cli/openapi-spec.ts +0 -356
  156. package/src/cli/run.ts +0 -628
  157. package/src/cli/serve-auth.ts +0 -80
  158. package/src/cli/serve.ts +0 -539
  159. package/src/cli/web.ts +0 -71
  160. package/src/cli.ts +0 -1728
  161. package/src/clients/core-engine-client.ts +0 -227
  162. package/src/clients/enterprise-client.ts +0 -334
  163. package/src/clients/generator-client.ts +0 -351
  164. package/src/clients/git-client.ts +0 -627
  165. package/src/clients/github-client.ts +0 -410
  166. package/src/clients/helm-client.ts +0 -504
  167. package/src/clients/index.ts +0 -80
  168. package/src/clients/k8s-client.ts +0 -497
  169. package/src/clients/llm-client.ts +0 -161
  170. package/src/clients/rest-client.ts +0 -130
  171. package/src/clients/service-discovery.ts +0 -38
  172. package/src/clients/terraform-client.ts +0 -482
  173. package/src/clients/tools-client.ts +0 -1843
  174. package/src/clients/ws-client.ts +0 -115
  175. package/src/commands/alias.ts +0 -100
  176. package/src/commands/analyze/index.ts +0 -352
  177. package/src/commands/apply/helm.ts +0 -473
  178. package/src/commands/apply/index.ts +0 -213
  179. package/src/commands/apply/k8s.ts +0 -454
  180. package/src/commands/apply/terraform.ts +0 -582
  181. package/src/commands/ask.ts +0 -167
  182. package/src/commands/audit/index.ts +0 -357
  183. package/src/commands/auth-cloud.ts +0 -407
  184. package/src/commands/auth-list.ts +0 -134
  185. package/src/commands/auth-profile.ts +0 -121
  186. package/src/commands/auth-refresh.ts +0 -187
  187. package/src/commands/auth-status.ts +0 -141
  188. package/src/commands/aws/ec2.ts +0 -501
  189. package/src/commands/aws/iam.ts +0 -397
  190. package/src/commands/aws/index.ts +0 -133
  191. package/src/commands/aws/lambda.ts +0 -396
  192. package/src/commands/aws/rds.ts +0 -439
  193. package/src/commands/aws/s3.ts +0 -439
  194. package/src/commands/aws/vpc.ts +0 -393
  195. package/src/commands/aws-discover.ts +0 -542
  196. package/src/commands/aws-terraform.ts +0 -755
  197. package/src/commands/azure/aks.ts +0 -376
  198. package/src/commands/azure/functions.ts +0 -253
  199. package/src/commands/azure/index.ts +0 -116
  200. package/src/commands/azure/storage.ts +0 -478
  201. package/src/commands/azure/vm.ts +0 -355
  202. package/src/commands/billing/index.ts +0 -256
  203. package/src/commands/chat.ts +0 -320
  204. package/src/commands/completions.ts +0 -268
  205. package/src/commands/config.ts +0 -372
  206. package/src/commands/cost/cloud-cost-estimator.ts +0 -266
  207. package/src/commands/cost/estimator.ts +0 -79
  208. package/src/commands/cost/index.ts +0 -810
  209. package/src/commands/cost/parsers/terraform.ts +0 -273
  210. package/src/commands/cost/parsers/types.ts +0 -25
  211. package/src/commands/cost/pricing/aws.ts +0 -544
  212. package/src/commands/cost/pricing/azure.ts +0 -499
  213. package/src/commands/cost/pricing/gcp.ts +0 -396
  214. package/src/commands/cost/pricing/index.ts +0 -40
  215. package/src/commands/demo.ts +0 -250
  216. package/src/commands/deploy.ts +0 -260
  217. package/src/commands/doctor.ts +0 -1386
  218. package/src/commands/drift/index.ts +0 -787
  219. package/src/commands/explain.ts +0 -277
  220. package/src/commands/export.ts +0 -146
  221. package/src/commands/feedback.ts +0 -389
  222. package/src/commands/fix.ts +0 -324
  223. package/src/commands/fs/index.ts +0 -402
  224. package/src/commands/gcp/compute.ts +0 -325
  225. package/src/commands/gcp/functions.ts +0 -271
  226. package/src/commands/gcp/gke.ts +0 -438
  227. package/src/commands/gcp/iam.ts +0 -344
  228. package/src/commands/gcp/index.ts +0 -129
  229. package/src/commands/gcp/storage.ts +0 -284
  230. package/src/commands/generate-helm.ts +0 -1249
  231. package/src/commands/generate-k8s.ts +0 -1508
  232. package/src/commands/generate-terraform.ts +0 -1202
  233. package/src/commands/gh/index.ts +0 -863
  234. package/src/commands/git/index.ts +0 -1343
  235. package/src/commands/helm/index.ts +0 -1126
  236. package/src/commands/help.ts +0 -715
  237. package/src/commands/history.ts +0 -149
  238. package/src/commands/import.ts +0 -868
  239. package/src/commands/incident.ts +0 -166
  240. package/src/commands/index.ts +0 -367
  241. package/src/commands/init.ts +0 -1051
  242. package/src/commands/k8s/index.ts +0 -1137
  243. package/src/commands/login.ts +0 -716
  244. package/src/commands/logout.ts +0 -83
  245. package/src/commands/logs.ts +0 -167
  246. package/src/commands/onboarding.ts +0 -405
  247. package/src/commands/pipeline.ts +0 -186
  248. package/src/commands/plan/display.ts +0 -279
  249. package/src/commands/plan/index.ts +0 -599
  250. package/src/commands/plugin.ts +0 -398
  251. package/src/commands/preview.ts +0 -452
  252. package/src/commands/profile.ts +0 -342
  253. package/src/commands/questionnaire.ts +0 -1172
  254. package/src/commands/resume.ts +0 -47
  255. package/src/commands/rollback.ts +0 -315
  256. package/src/commands/rollout.ts +0 -88
  257. package/src/commands/runbook.ts +0 -346
  258. package/src/commands/schedule.ts +0 -236
  259. package/src/commands/status.ts +0 -252
  260. package/src/commands/team/index.ts +0 -346
  261. package/src/commands/team-context.ts +0 -220
  262. package/src/commands/template.ts +0 -233
  263. package/src/commands/tf/index.ts +0 -1093
  264. package/src/commands/upgrade.ts +0 -609
  265. package/src/commands/usage/index.ts +0 -134
  266. package/src/commands/version.ts +0 -174
  267. package/src/commands/watch.ts +0 -153
  268. package/src/compat/index.ts +0 -2
  269. package/src/compat/runtime.ts +0 -12
  270. package/src/compat/sqlite.ts +0 -177
  271. package/src/config/index.ts +0 -17
  272. package/src/config/manager.ts +0 -530
  273. package/src/config/mode-store.ts +0 -62
  274. package/src/config/profiles.ts +0 -84
  275. package/src/config/safety-policy.ts +0 -358
  276. package/src/config/schema.ts +0 -125
  277. package/src/config/types.ts +0 -609
  278. package/src/config/workspace-state.ts +0 -53
  279. package/src/context/context-db.ts +0 -199
  280. package/src/demo/index.ts +0 -349
  281. package/src/demo/scenarios/full-journey.ts +0 -229
  282. package/src/demo/scenarios/getting-started.ts +0 -127
  283. package/src/demo/scenarios/helm-release.ts +0 -341
  284. package/src/demo/scenarios/k8s-deployment.ts +0 -194
  285. package/src/demo/scenarios/terraform-vpc.ts +0 -170
  286. package/src/demo/types.ts +0 -92
  287. package/src/engine/cost-estimator.ts +0 -480
  288. package/src/engine/diagram-generator.ts +0 -256
  289. package/src/engine/drift-detector.ts +0 -902
  290. package/src/engine/executor.ts +0 -1066
  291. package/src/engine/index.ts +0 -76
  292. package/src/engine/orchestrator.ts +0 -636
  293. package/src/engine/planner.ts +0 -787
  294. package/src/engine/safety.ts +0 -743
  295. package/src/engine/verifier.ts +0 -770
  296. package/src/enterprise/audit.ts +0 -348
  297. package/src/enterprise/auth.ts +0 -270
  298. package/src/enterprise/billing.ts +0 -822
  299. package/src/enterprise/index.ts +0 -17
  300. package/src/enterprise/teams.ts +0 -443
  301. package/src/generator/best-practices.ts +0 -1608
  302. package/src/generator/helm.ts +0 -630
  303. package/src/generator/index.ts +0 -37
  304. package/src/generator/intent-parser.ts +0 -514
  305. package/src/generator/kubernetes.ts +0 -976
  306. package/src/generator/terraform.ts +0 -1875
  307. package/src/history/index.ts +0 -8
  308. package/src/history/manager.ts +0 -250
  309. package/src/history/types.ts +0 -34
  310. package/src/hooks/config.ts +0 -432
  311. package/src/hooks/engine.ts +0 -392
  312. package/src/hooks/index.ts +0 -4
  313. package/src/llm/auth-bridge.ts +0 -198
  314. package/src/llm/circuit-breaker.ts +0 -140
  315. package/src/llm/config-loader.ts +0 -201
  316. package/src/llm/cost-calculator.ts +0 -171
  317. package/src/llm/index.ts +0 -8
  318. package/src/llm/model-aliases.ts +0 -115
  319. package/src/llm/provider-registry.ts +0 -63
  320. package/src/llm/providers/anthropic.ts +0 -462
  321. package/src/llm/providers/bedrock.ts +0 -477
  322. package/src/llm/providers/google.ts +0 -405
  323. package/src/llm/providers/ollama.ts +0 -767
  324. package/src/llm/providers/openai-compatible.ts +0 -340
  325. package/src/llm/providers/openai.ts +0 -328
  326. package/src/llm/providers/openrouter.ts +0 -338
  327. package/src/llm/router.ts +0 -1104
  328. package/src/llm/types.ts +0 -232
  329. package/src/lsp/client.ts +0 -298
  330. package/src/lsp/languages.ts +0 -119
  331. package/src/lsp/manager.ts +0 -294
  332. package/src/mcp/client.ts +0 -402
  333. package/src/mcp/index.ts +0 -5
  334. package/src/mcp/manager.ts +0 -133
  335. package/src/nimbus.ts +0 -234
  336. package/src/plugins/index.ts +0 -27
  337. package/src/plugins/loader.ts +0 -334
  338. package/src/plugins/manager.ts +0 -376
  339. package/src/plugins/types.ts +0 -284
  340. package/src/scanners/cicd-scanner.ts +0 -258
  341. package/src/scanners/cloud-scanner.ts +0 -466
  342. package/src/scanners/framework-scanner.ts +0 -469
  343. package/src/scanners/iac-scanner.ts +0 -388
  344. package/src/scanners/index.ts +0 -539
  345. package/src/scanners/language-scanner.ts +0 -276
  346. package/src/scanners/package-manager-scanner.ts +0 -277
  347. package/src/scanners/types.ts +0 -172
  348. package/src/sessions/manager.ts +0 -472
  349. package/src/sessions/types.ts +0 -44
  350. package/src/sharing/sync.ts +0 -300
  351. package/src/sharing/viewer.ts +0 -163
  352. package/src/snapshots/index.ts +0 -2
  353. package/src/snapshots/manager.ts +0 -530
  354. package/src/state/artifacts.ts +0 -147
  355. package/src/state/audit.ts +0 -137
  356. package/src/state/billing.ts +0 -240
  357. package/src/state/checkpoints.ts +0 -117
  358. package/src/state/config.ts +0 -67
  359. package/src/state/conversations.ts +0 -14
  360. package/src/state/credentials.ts +0 -154
  361. package/src/state/db.ts +0 -58
  362. package/src/state/index.ts +0 -26
  363. package/src/state/messages.ts +0 -115
  364. package/src/state/projects.ts +0 -123
  365. package/src/state/schema.ts +0 -236
  366. package/src/state/sessions.ts +0 -147
  367. package/src/state/teams.ts +0 -200
  368. package/src/telemetry.ts +0 -108
  369. package/src/tools/aws-ops.ts +0 -952
  370. package/src/tools/azure-ops.ts +0 -579
  371. package/src/tools/file-ops.ts +0 -615
  372. package/src/tools/gcp-ops.ts +0 -625
  373. package/src/tools/git-ops.ts +0 -773
  374. package/src/tools/github-ops.ts +0 -799
  375. package/src/tools/helm-ops.ts +0 -943
  376. package/src/tools/index.ts +0 -17
  377. package/src/tools/k8s-ops.ts +0 -819
  378. package/src/tools/schemas/converter.ts +0 -184
  379. package/src/tools/schemas/devops.ts +0 -3502
  380. package/src/tools/schemas/index.ts +0 -73
  381. package/src/tools/schemas/standard.ts +0 -1148
  382. package/src/tools/schemas/types.ts +0 -735
  383. package/src/tools/spawn-exec.ts +0 -148
  384. package/src/tools/terraform-ops.ts +0 -862
  385. package/src/types/ambient.d.ts +0 -193
  386. package/src/types/config.ts +0 -83
  387. package/src/types/drift.ts +0 -116
  388. package/src/types/enterprise.ts +0 -335
  389. package/src/types/index.ts +0 -20
  390. package/src/types/plan.ts +0 -44
  391. package/src/types/request.ts +0 -65
  392. package/src/types/response.ts +0 -54
  393. package/src/types/service.ts +0 -51
  394. package/src/ui/App.tsx +0 -2114
  395. package/src/ui/DeployPreview.tsx +0 -174
  396. package/src/ui/FileDiffModal.tsx +0 -162
  397. package/src/ui/Header.tsx +0 -131
  398. package/src/ui/HelpModal.tsx +0 -57
  399. package/src/ui/InputBox.tsx +0 -503
  400. package/src/ui/MessageList.tsx +0 -1032
  401. package/src/ui/PermissionPrompt.tsx +0 -163
  402. package/src/ui/StatusBar.tsx +0 -277
  403. package/src/ui/TerminalPane.tsx +0 -84
  404. package/src/ui/ToolCallDisplay.tsx +0 -643
  405. package/src/ui/TreePane.tsx +0 -132
  406. package/src/ui/chat-ui.ts +0 -850
  407. package/src/ui/index.ts +0 -33
  408. package/src/ui/ink/index.ts +0 -1444
  409. package/src/ui/streaming.ts +0 -176
  410. package/src/ui/theme.ts +0 -104
  411. package/src/ui/types.ts +0 -75
  412. package/src/utils/analytics.ts +0 -72
  413. package/src/utils/cost-warning.ts +0 -27
  414. package/src/utils/env.ts +0 -46
  415. package/src/utils/errors.ts +0 -69
  416. package/src/utils/event-bus.ts +0 -38
  417. package/src/utils/index.ts +0 -24
  418. package/src/utils/logger.ts +0 -171
  419. package/src/utils/rate-limiter.ts +0 -121
  420. package/src/utils/service-auth.ts +0 -49
  421. package/src/utils/validation.ts +0 -53
  422. package/src/version.ts +0 -4
  423. package/src/watcher/index.ts +0 -214
  424. package/src/wizard/approval.ts +0 -383
  425. package/src/wizard/index.ts +0 -25
  426. package/src/wizard/prompts.ts +0 -338
  427. package/src/wizard/types.ts +0 -172
  428. package/src/wizard/ui.ts +0 -556
  429. package/src/wizard/wizard.ts +0 -304
  430. package/tsconfig.json +0 -24
@@ -1,1093 +0,0 @@
1
- /**
2
- * Terraform Commands
3
- *
4
- * CLI commands for Terraform operations
5
- */
6
-
7
- import { terraformClient } from '../../clients';
8
- import { ui } from '../../wizard/ui';
9
- import { confirmWithResourceName } from '../../wizard/approval';
10
- import { showDestructionCostWarning } from '../../utils/cost-warning';
11
- import { historyManager } from '../../history';
12
-
13
- export interface TfCommandOptions {
14
- directory?: string;
15
- varFile?: string;
16
- vars?: Record<string, string>;
17
- autoApprove?: boolean;
18
- dryRun?: boolean;
19
- out?: string;
20
- planFile?: string;
21
- check?: boolean;
22
- recursive?: boolean;
23
- diff?: boolean;
24
- type?: 'plan' | 'apply';
25
- workspace?: string;
26
- }
27
-
28
- /**
29
- * Initialize Terraform working directory
30
- */
31
- export async function tfInitCommand(options: TfCommandOptions = {}): Promise<void> {
32
- const directory = options.directory || process.cwd();
33
-
34
- ui.header('Terraform Init');
35
- ui.info(`Directory: ${directory}`);
36
-
37
- ui.startSpinner({ message: 'Initializing Terraform...' });
38
-
39
- try {
40
- const available = await terraformClient.isAvailable();
41
- if (!available) {
42
- ui.stopSpinnerFail('Terraform Tools Service not available');
43
- ui.error('Please ensure the Terraform Tools Service is running.');
44
- return;
45
- }
46
-
47
- const result = await terraformClient.init(directory);
48
-
49
- if (result.success) {
50
- ui.stopSpinnerSuccess('Terraform initialized successfully');
51
- if (result.output) {
52
- ui.box({ title: 'Output', content: result.output });
53
- }
54
- } else {
55
- ui.stopSpinnerFail('Terraform init failed');
56
- if (result.error) {
57
- ui.error(result.error);
58
- }
59
- }
60
- } catch (error: any) {
61
- ui.stopSpinnerFail('Error initializing Terraform');
62
- ui.error(error.message);
63
- }
64
- }
65
-
66
- /**
67
- * Generate Terraform execution plan
68
- */
69
- export async function tfPlanCommand(options: TfCommandOptions = {}): Promise<void> {
70
- const directory = options.directory || process.cwd();
71
-
72
- ui.header('Terraform Plan');
73
- ui.info(`Directory: ${directory}`);
74
-
75
- ui.startSpinner({ message: 'Generating Terraform plan...' });
76
-
77
- try {
78
- const available = await terraformClient.isAvailable();
79
- if (!available) {
80
- ui.stopSpinnerFail('Terraform Tools Service not available');
81
- ui.error('Please ensure the Terraform Tools Service is running.');
82
- return;
83
- }
84
-
85
- const result = await terraformClient.plan(directory, {
86
- varFile: options.varFile,
87
- vars: options.vars,
88
- out: options.out,
89
- });
90
-
91
- if (result.success) {
92
- if (result.hasChanges) {
93
- ui.stopSpinnerSuccess('Plan generated with changes');
94
- } else {
95
- ui.stopSpinnerSuccess('Plan generated - no changes');
96
- }
97
-
98
- if (result.output) {
99
- ui.box({ title: 'Plan Output', content: result.output });
100
- }
101
-
102
- if (result.planFile) {
103
- ui.info(`Plan saved to: ${result.planFile}`);
104
- }
105
- } else {
106
- ui.stopSpinnerFail('Terraform plan failed');
107
- if (result.error) {
108
- ui.error(result.error);
109
- }
110
- }
111
- } catch (error: any) {
112
- ui.stopSpinnerFail('Error generating Terraform plan');
113
- ui.error(error.message);
114
- }
115
- }
116
-
117
- /**
118
- * Apply Terraform changes — shows plan first and prompts for confirmation
119
- * unless --auto-approve or a planFile is provided.
120
- */
121
- export async function tfApplyCommand(options: TfCommandOptions = {}): Promise<void> {
122
- const directory = options.directory || process.cwd();
123
- const { confirm } = await import('../../wizard/prompts');
124
- const os = await import('os');
125
- const path = await import('path');
126
-
127
- ui.header('Terraform Apply');
128
- ui.info(`Directory: ${directory}`);
129
-
130
- try {
131
- const available = await terraformClient.isAvailable();
132
- if (!available) {
133
- ui.error('Terraform Tools Service not available');
134
- ui.error('Please ensure the Terraform Tools Service is running.');
135
- return;
136
- }
137
-
138
- // Select workspace if specified
139
- if (options.workspace) {
140
- ui.startSpinner({ message: `Selecting workspace "${options.workspace}"...` });
141
- await terraformClient.workspace.select(options.workspace, directory);
142
- ui.stopSpinnerSuccess(`Workspace: ${options.workspace}`);
143
- }
144
-
145
- // If no planFile and not auto-approving, run plan first and prompt
146
- if (!options.planFile && !options.autoApprove) {
147
- const tmpPlanPath = path.join(os.tmpdir(), `nimbus-tfplan-${Date.now()}.bin`);
148
-
149
- ui.startSpinner({ message: 'Running terraform plan...' });
150
- const planResult = await terraformClient.plan(directory, {
151
- varFile: options.varFile,
152
- vars: options.vars,
153
- out: tmpPlanPath,
154
- });
155
-
156
- if (!planResult.success) {
157
- ui.stopSpinnerFail('Terraform plan failed');
158
- if (planResult.error) ui.error(planResult.error);
159
- return;
160
- }
161
-
162
- if (!planResult.hasChanges) {
163
- ui.stopSpinnerSuccess('No changes — infrastructure is up-to-date');
164
- return;
165
- }
166
-
167
- ui.stopSpinnerSuccess('Plan complete');
168
- if (planResult.output) {
169
- ui.box({ title: 'Terraform Plan', content: planResult.output });
170
- }
171
-
172
- const proceed = await confirm({ message: 'Apply these changes?', defaultValue: false });
173
- if (!proceed) {
174
- ui.info('Apply cancelled.');
175
- return;
176
- }
177
-
178
- // Apply using the saved plan file
179
- ui.startSpinner({ message: 'Applying Terraform changes...' });
180
- const applyResult = await terraformClient.apply(directory, {
181
- planFile: tmpPlanPath,
182
- varFile: options.varFile,
183
- vars: options.vars,
184
- });
185
-
186
- if (applyResult.success) {
187
- ui.stopSpinnerSuccess('Terraform apply completed successfully');
188
- if (applyResult.output) {
189
- ui.box({ title: 'Apply Output', content: applyResult.output });
190
- }
191
- } else {
192
- ui.stopSpinnerFail('Terraform apply failed');
193
- if (applyResult.error) ui.error(applyResult.error);
194
- }
195
- return;
196
- }
197
-
198
- // auto-approve or explicit planFile path: apply directly
199
- ui.startSpinner({ message: 'Applying Terraform changes...' });
200
- const result = await terraformClient.apply(directory, {
201
- planFile: options.planFile,
202
- autoApprove: options.autoApprove,
203
- varFile: options.varFile,
204
- vars: options.vars,
205
- });
206
-
207
- if (result.success) {
208
- ui.stopSpinnerSuccess('Terraform apply completed successfully');
209
- if (result.output) {
210
- ui.box({ title: 'Apply Output', content: result.output });
211
- }
212
- } else {
213
- ui.stopSpinnerFail('Terraform apply failed');
214
- if (result.error) ui.error(result.error);
215
- }
216
- } catch (error: any) {
217
- ui.stopSpinnerFail('Error applying Terraform changes');
218
- ui.error(error.message);
219
- }
220
- }
221
-
222
- /**
223
- * Validate Terraform configuration
224
- */
225
- export async function tfValidateCommand(options: TfCommandOptions = {}): Promise<void> {
226
- const directory = options.directory || process.cwd();
227
-
228
- ui.header('Terraform Validate');
229
- ui.info(`Directory: ${directory}`);
230
-
231
- ui.startSpinner({ message: 'Validating Terraform configuration...' });
232
-
233
- try {
234
- const available = await terraformClient.isAvailable();
235
- if (!available) {
236
- ui.stopSpinnerFail('Terraform Tools Service not available');
237
- ui.error('Please ensure the Terraform Tools Service is running.');
238
- return;
239
- }
240
-
241
- const result = await terraformClient.validate(directory);
242
-
243
- if (result.valid) {
244
- ui.stopSpinnerSuccess('Configuration is valid');
245
- if (result.output) {
246
- ui.box({ title: 'Validation Output', content: result.output });
247
- }
248
- } else {
249
- ui.stopSpinnerFail('Configuration is invalid');
250
- if (result.error) {
251
- ui.error(result.error);
252
- }
253
- }
254
- } catch (error: any) {
255
- ui.stopSpinnerFail('Error validating Terraform configuration');
256
- ui.error(error.message);
257
- }
258
- }
259
-
260
- /**
261
- * Destroy Terraform-managed infrastructure
262
- */
263
- export async function tfDestroyCommand(options: TfCommandOptions = {}): Promise<void> {
264
- const directory = options.directory || process.cwd();
265
- const path = await import('path');
266
- const workspaceName = path.basename(path.resolve(directory));
267
-
268
- ui.header('Terraform Destroy');
269
- ui.info(`Directory: ${directory}`);
270
- ui.warning('This will destroy all managed infrastructure!');
271
-
272
- // Show cost warning before destructive operation
273
- await showDestructionCostWarning(directory);
274
-
275
- if (!options.autoApprove) {
276
- // Require type-name-to-delete confirmation
277
- const confirmed = await confirmWithResourceName(workspaceName, 'terraform workspace');
278
- if (!confirmed) {
279
- return;
280
- }
281
- }
282
-
283
- ui.startSpinner({ message: 'Destroying Terraform resources...' });
284
-
285
- try {
286
- const available = await terraformClient.isAvailable();
287
- if (!available) {
288
- ui.stopSpinnerFail('Terraform Tools Service not available');
289
- ui.error('Please ensure the Terraform Tools Service is running.');
290
- return;
291
- }
292
-
293
- const result = await terraformClient.destroy(directory, {
294
- autoApprove: options.autoApprove,
295
- varFile: options.varFile,
296
- });
297
-
298
- if (result.success) {
299
- ui.stopSpinnerSuccess('Terraform destroy completed');
300
- if (result.output) {
301
- ui.box({ title: 'Destroy Output', content: result.output });
302
- }
303
- } else {
304
- ui.stopSpinnerFail('Terraform destroy failed');
305
- if (result.error) {
306
- ui.error(result.error);
307
- }
308
- }
309
- } catch (error: any) {
310
- ui.stopSpinnerFail('Error destroying Terraform resources');
311
- ui.error(error.message);
312
- }
313
- }
314
-
315
- /**
316
- * Show Terraform state
317
- */
318
- export async function tfShowCommand(options: TfCommandOptions = {}): Promise<void> {
319
- const directory = options.directory || process.cwd();
320
-
321
- ui.header('Terraform Show');
322
- ui.info(`Directory: ${directory}`);
323
-
324
- ui.startSpinner({ message: 'Retrieving Terraform state...' });
325
-
326
- try {
327
- const available = await terraformClient.isAvailable();
328
- if (!available) {
329
- ui.stopSpinnerFail('Terraform Tools Service not available');
330
- ui.error('Please ensure the Terraform Tools Service is running.');
331
- return;
332
- }
333
-
334
- const result = await terraformClient.show(directory);
335
-
336
- if (result.success) {
337
- ui.stopSpinnerSuccess('State retrieved');
338
- if (result.output) {
339
- ui.box({ title: 'State', content: result.output });
340
- }
341
- } else {
342
- ui.stopSpinnerFail('Failed to retrieve state');
343
- }
344
- } catch (error: any) {
345
- ui.stopSpinnerFail('Error retrieving Terraform state');
346
- ui.error(error.message);
347
- }
348
- }
349
-
350
- /**
351
- * Format Terraform configuration files
352
- */
353
- export async function tfFmtCommand(options: TfCommandOptions = {}): Promise<void> {
354
- const directory = options.directory || process.cwd();
355
-
356
- ui.header('Terraform Fmt');
357
- ui.info(`Directory: ${directory}`);
358
-
359
- if (options.check) {
360
- ui.info('Mode: check only (no changes will be made)');
361
- }
362
-
363
- ui.startSpinner({ message: 'Formatting Terraform files...' });
364
-
365
- try {
366
- const available = await terraformClient.isAvailable();
367
- if (!available) {
368
- ui.stopSpinnerFail('Terraform Tools Service not available');
369
- ui.error('Please ensure the Terraform Tools Service is running.');
370
- return;
371
- }
372
-
373
- const result = await terraformClient.fmt(directory, {
374
- check: options.check,
375
- recursive: options.recursive,
376
- diff: options.diff,
377
- });
378
-
379
- if (result.success) {
380
- if (result.files && result.files.length > 0) {
381
- ui.stopSpinnerSuccess(`Formatted ${result.files.length} file(s)`);
382
- for (const file of result.files) {
383
- ui.print(` ${ui.color('*', 'green')} ${file}`);
384
- }
385
- } else {
386
- ui.stopSpinnerSuccess('All files already formatted');
387
- }
388
- if (result.output) {
389
- ui.box({ title: 'Fmt Output', content: result.output });
390
- }
391
- } else {
392
- ui.stopSpinnerFail('Terraform fmt failed');
393
- if (result.error) {
394
- ui.error(result.error);
395
- }
396
- }
397
- } catch (error: any) {
398
- ui.stopSpinnerFail('Error formatting Terraform files');
399
- ui.error(error.message);
400
- }
401
- }
402
-
403
- /**
404
- * Manage Terraform workspaces
405
- */
406
- export async function tfWorkspaceCommand(
407
- subcommand: string,
408
- name: string | undefined,
409
- options: TfCommandOptions = {}
410
- ): Promise<void> {
411
- const directory = options.directory || process.cwd();
412
-
413
- ui.header('Terraform Workspace');
414
- ui.info(`Directory: ${directory}`);
415
-
416
- try {
417
- const available = await terraformClient.isAvailable();
418
- if (!available) {
419
- ui.error('Terraform Tools Service not available');
420
- ui.error('Please ensure the Terraform Tools Service is running.');
421
- return;
422
- }
423
-
424
- switch (subcommand) {
425
- case 'list': {
426
- ui.startSpinner({ message: 'Listing workspaces...' });
427
- const result = await terraformClient.workspace.list(directory);
428
- if (result.success) {
429
- ui.stopSpinnerSuccess('Workspaces retrieved');
430
- if (result.workspaces && result.workspaces.length > 0) {
431
- for (const ws of result.workspaces) {
432
- const marker = ws === result.current ? '* ' : ' ';
433
- ui.print(`${marker}${ws}`);
434
- }
435
- }
436
- if (result.output) {
437
- ui.box({ title: 'Workspace List', content: result.output });
438
- }
439
- } else {
440
- ui.stopSpinnerFail('Failed to list workspaces');
441
- if (result.error) {
442
- ui.error(result.error);
443
- }
444
- }
445
- break;
446
- }
447
-
448
- case 'select': {
449
- if (!name) {
450
- ui.error('Usage: nimbus tf workspace select <name>');
451
- return;
452
- }
453
- ui.startSpinner({ message: `Selecting workspace "${name}"...` });
454
- const result = await terraformClient.workspace.select(name, directory);
455
- if (result.success) {
456
- ui.stopSpinnerSuccess(`Switched to workspace "${name}"`);
457
- if (result.output) {
458
- ui.box({ title: 'Output', content: result.output });
459
- }
460
- } else {
461
- ui.stopSpinnerFail(`Failed to select workspace "${name}"`);
462
- if (result.error) {
463
- ui.error(result.error);
464
- }
465
- }
466
- break;
467
- }
468
-
469
- case 'new': {
470
- if (!name) {
471
- ui.error('Usage: nimbus tf workspace new <name>');
472
- return;
473
- }
474
- ui.startSpinner({ message: `Creating workspace "${name}"...` });
475
- const result = await terraformClient.workspace.new(name, directory);
476
- if (result.success) {
477
- ui.stopSpinnerSuccess(`Created and switched to workspace "${name}"`);
478
- if (result.output) {
479
- ui.box({ title: 'Output', content: result.output });
480
- }
481
- } else {
482
- ui.stopSpinnerFail(`Failed to create workspace "${name}"`);
483
- if (result.error) {
484
- ui.error(result.error);
485
- }
486
- }
487
- break;
488
- }
489
-
490
- case 'delete': {
491
- if (!name) {
492
- ui.error('Usage: nimbus tf workspace delete <name>');
493
- return;
494
- }
495
- ui.startSpinner({ message: `Deleting workspace "${name}"...` });
496
- const result = await terraformClient.workspace.delete(name, directory);
497
- if (result.success) {
498
- ui.stopSpinnerSuccess(`Deleted workspace "${name}"`);
499
- if (result.output) {
500
- ui.box({ title: 'Output', content: result.output });
501
- }
502
- } else {
503
- ui.stopSpinnerFail(`Failed to delete workspace "${name}"`);
504
- if (result.error) {
505
- ui.error(result.error);
506
- }
507
- }
508
- break;
509
- }
510
-
511
- default:
512
- ui.error(`Unknown workspace subcommand: ${subcommand}`);
513
- ui.info('Available subcommands: list, select, new, delete');
514
- }
515
- } catch (error: any) {
516
- ui.stopSpinnerFail('Error managing Terraform workspace');
517
- ui.error(error.message);
518
- }
519
- }
520
-
521
- /**
522
- * Import existing infrastructure into Terraform state
523
- */
524
- export async function tfImportCommand(
525
- address: string,
526
- id: string,
527
- options: TfCommandOptions = {}
528
- ): Promise<void> {
529
- const directory = options.directory || process.cwd();
530
-
531
- ui.header('Terraform Import');
532
- ui.info(`Directory: ${directory}`);
533
- ui.info(`Address: ${address}`);
534
- ui.info(`ID: ${id}`);
535
-
536
- ui.startSpinner({ message: `Importing ${address}...` });
537
-
538
- try {
539
- const available = await terraformClient.isAvailable();
540
- if (!available) {
541
- ui.stopSpinnerFail('Terraform Tools Service not available');
542
- ui.error('Please ensure the Terraform Tools Service is running.');
543
- return;
544
- }
545
-
546
- const result = await terraformClient.import(directory, address, id);
547
-
548
- if (result.success) {
549
- ui.stopSpinnerSuccess(`Successfully imported ${address}`);
550
- if (result.output) {
551
- ui.box({ title: 'Import Output', content: result.output });
552
- }
553
- } else {
554
- ui.stopSpinnerFail('Terraform import failed');
555
- if (result.error) {
556
- ui.error(result.error);
557
- }
558
- }
559
- } catch (error: any) {
560
- ui.stopSpinnerFail('Error importing resource');
561
- ui.error(error.message);
562
- }
563
- }
564
-
565
- /**
566
- * Show Terraform output values
567
- */
568
- export async function tfOutputCommand(
569
- options: TfCommandOptions = {},
570
- name?: string
571
- ): Promise<void> {
572
- const directory = options.directory || process.cwd();
573
-
574
- ui.header('Terraform Output');
575
- ui.info(`Directory: ${directory}`);
576
- if (name) {
577
- ui.info(`Output: ${name}`);
578
- }
579
-
580
- ui.startSpinner({ message: 'Retrieving Terraform outputs...' });
581
-
582
- try {
583
- const available = await terraformClient.isAvailable();
584
- if (!available) {
585
- ui.stopSpinnerFail('Terraform Tools Service not available');
586
- ui.error('Please ensure the Terraform Tools Service is running.');
587
- return;
588
- }
589
-
590
- const result = await terraformClient.output(directory, name);
591
-
592
- if (result.success) {
593
- ui.stopSpinnerSuccess('Outputs retrieved');
594
- if (result.outputs) {
595
- for (const [key, val] of Object.entries(result.outputs)) {
596
- const value = val.sensitive ? '<sensitive>' : JSON.stringify(val.value);
597
- ui.print(` ${ui.color(key, 'cyan')} = ${value}`);
598
- }
599
- }
600
- if (result.output) {
601
- ui.box({ title: 'Output', content: result.output });
602
- }
603
- } else {
604
- ui.stopSpinnerFail('Failed to retrieve outputs');
605
- if (result.error) {
606
- ui.error(result.error);
607
- }
608
- }
609
- } catch (error: any) {
610
- ui.stopSpinnerFail('Error retrieving Terraform outputs');
611
- ui.error(error.message);
612
- }
613
- }
614
-
615
- /**
616
- * Manage Terraform state
617
- */
618
- export async function tfStateCommand(
619
- args: string[],
620
- options: TfCommandOptions = {}
621
- ): Promise<void> {
622
- const directory = options.directory || process.cwd();
623
- const subcommand = args[0];
624
-
625
- ui.header('Terraform State');
626
- ui.info(`Directory: ${directory}`);
627
-
628
- try {
629
- const available = await terraformClient.isAvailable();
630
- if (!available) {
631
- ui.error('Terraform Tools Service not available');
632
- ui.error('Please ensure the Terraform Tools Service is running.');
633
- return;
634
- }
635
-
636
- switch (subcommand) {
637
- case 'list': {
638
- ui.startSpinner({ message: 'Listing state resources...' });
639
- const result = await terraformClient.state.list(directory);
640
- if (result.success) {
641
- ui.stopSpinnerSuccess('State resources retrieved');
642
- if (result.resources && result.resources.length > 0) {
643
- for (const resource of result.resources) {
644
- ui.print(` ${resource}`);
645
- }
646
- } else if (result.output) {
647
- ui.box({ title: 'State List', content: result.output });
648
- } else {
649
- ui.info('No resources found in state.');
650
- }
651
- } else {
652
- ui.stopSpinnerFail('Failed to list state resources');
653
- if (result.error) {
654
- ui.error(result.error);
655
- }
656
- }
657
- break;
658
- }
659
-
660
- case 'show': {
661
- const address = args[1];
662
- if (!address) {
663
- ui.error('Usage: nimbus tf state show <address>');
664
- ui.info('Example: nimbus tf state show aws_instance.web');
665
- return;
666
- }
667
- ui.startSpinner({ message: `Showing state for ${address}...` });
668
- const result = await terraformClient.state.show(address, directory);
669
- if (result.success) {
670
- ui.stopSpinnerSuccess(`State retrieved for ${address}`);
671
- if (result.output) {
672
- ui.box({ title: `State: ${address}`, content: result.output });
673
- }
674
- } else {
675
- ui.stopSpinnerFail(`Failed to show state for ${address}`);
676
- if (result.error) {
677
- ui.error(result.error);
678
- }
679
- }
680
- break;
681
- }
682
-
683
- case 'mv': {
684
- const source = args[1];
685
- const destination = args[2];
686
- if (!source || !destination) {
687
- ui.error('Usage: nimbus tf state mv <source> <destination>');
688
- ui.info('Example: nimbus tf state mv aws_instance.old aws_instance.new');
689
- return;
690
- }
691
- ui.startSpinner({ message: `Moving state from ${source} to ${destination}...` });
692
- const result = await terraformClient.state.mv(directory, source, destination);
693
- if (result.success) {
694
- ui.stopSpinnerSuccess(`Moved state: ${source} -> ${destination}`);
695
- if (result.output) {
696
- ui.box({ title: 'State Move Output', content: result.output });
697
- }
698
- } else {
699
- ui.stopSpinnerFail(`Failed to move state from ${source} to ${destination}`);
700
- if (result.error) {
701
- ui.error(result.error);
702
- }
703
- }
704
- break;
705
- }
706
-
707
- case 'pull': {
708
- ui.startSpinner({ message: 'Pulling remote state...' });
709
- const result = await terraformClient.state.pull(directory);
710
- if (result.success) {
711
- ui.stopSpinnerSuccess('Remote state pulled successfully');
712
- if (result.output) {
713
- ui.box({ title: 'State Pull Output', content: result.output });
714
- }
715
- } else {
716
- ui.stopSpinnerFail('Failed to pull remote state');
717
- if (result.error) {
718
- ui.error(result.error);
719
- }
720
- }
721
- break;
722
- }
723
-
724
- case 'push': {
725
- ui.startSpinner({ message: 'Pushing local state...' });
726
- const result = await terraformClient.state.push(directory);
727
- if (result.success) {
728
- ui.stopSpinnerSuccess('Local state pushed successfully');
729
- if (result.output) {
730
- ui.box({ title: 'State Push Output', content: result.output });
731
- }
732
- } else {
733
- ui.stopSpinnerFail('Failed to push local state');
734
- if (result.error) {
735
- ui.error(result.error);
736
- }
737
- }
738
- break;
739
- }
740
-
741
- default:
742
- ui.error(`Unknown state subcommand: ${subcommand || '(none)'}`);
743
- ui.info('Available subcommands: list, show <address>, mv <src> <dst>, pull, push');
744
- }
745
- } catch (error: any) {
746
- ui.stopSpinnerFail('Error managing Terraform state');
747
- ui.error(error.message);
748
- }
749
- }
750
-
751
- /**
752
- * Taint a resource, marking it for recreation on next apply
753
- */
754
- export async function tfTaintCommand(
755
- address: string,
756
- options: TfCommandOptions = {}
757
- ): Promise<void> {
758
- const directory = options.directory || process.cwd();
759
-
760
- ui.header('Terraform Taint');
761
- ui.info(`Directory: ${directory}`);
762
- ui.info(`Address: ${address}`);
763
- ui.warning('This resource will be destroyed and recreated on the next apply.');
764
-
765
- ui.startSpinner({ message: `Tainting ${address}...` });
766
-
767
- try {
768
- const available = await terraformClient.isAvailable();
769
- if (!available) {
770
- ui.stopSpinnerFail('Terraform Tools Service not available');
771
- ui.error('Please ensure the Terraform Tools Service is running.');
772
- return;
773
- }
774
-
775
- const result = await terraformClient.taint(directory, address);
776
-
777
- if (result.success) {
778
- ui.stopSpinnerSuccess(`Resource ${address} tainted successfully`);
779
- if (result.output) {
780
- ui.box({ title: 'Taint Output', content: result.output });
781
- }
782
- } else {
783
- ui.stopSpinnerFail(`Failed to taint resource ${address}`);
784
- if (result.error) {
785
- ui.error(result.error);
786
- }
787
- }
788
- } catch (error: any) {
789
- ui.stopSpinnerFail('Error tainting resource');
790
- ui.error(error.message);
791
- }
792
- }
793
-
794
- /**
795
- * Untaint a resource, removing the taint mark
796
- */
797
- export async function tfUntaintCommand(
798
- address: string,
799
- options: TfCommandOptions = {}
800
- ): Promise<void> {
801
- const directory = options.directory || process.cwd();
802
-
803
- ui.header('Terraform Untaint');
804
- ui.info(`Directory: ${directory}`);
805
- ui.info(`Address: ${address}`);
806
-
807
- ui.startSpinner({ message: `Untainting ${address}...` });
808
-
809
- try {
810
- const available = await terraformClient.isAvailable();
811
- if (!available) {
812
- ui.stopSpinnerFail('Terraform Tools Service not available');
813
- ui.error('Please ensure the Terraform Tools Service is running.');
814
- return;
815
- }
816
-
817
- const result = await terraformClient.untaint(directory, address);
818
-
819
- if (result.success) {
820
- ui.stopSpinnerSuccess(`Resource ${address} untainted successfully`);
821
- if (result.output) {
822
- ui.box({ title: 'Untaint Output', content: result.output });
823
- }
824
- } else {
825
- ui.stopSpinnerFail(`Failed to untaint resource ${address}`);
826
- if (result.error) {
827
- ui.error(result.error);
828
- }
829
- }
830
- } catch (error: any) {
831
- ui.stopSpinnerFail('Error untainting resource');
832
- ui.error(error.message);
833
- }
834
- }
835
-
836
- /**
837
- * Generate a resource dependency graph in DOT format
838
- */
839
- export async function tfGraphCommand(
840
- options: TfCommandOptions & { type?: 'plan' | 'apply' } = {}
841
- ): Promise<void> {
842
- const directory = options.directory || process.cwd();
843
-
844
- ui.header('Terraform Graph');
845
- ui.info(`Directory: ${directory}`);
846
- if (options.type) {
847
- ui.info(`Graph type: ${options.type}`);
848
- }
849
-
850
- ui.startSpinner({ message: 'Generating resource dependency graph...' });
851
-
852
- try {
853
- const available = await terraformClient.isAvailable();
854
- if (!available) {
855
- ui.stopSpinnerFail('Terraform Tools Service not available');
856
- ui.error('Please ensure the Terraform Tools Service is running.');
857
- return;
858
- }
859
-
860
- const result = await terraformClient.graph(directory, { type: options.type });
861
-
862
- if (result.success) {
863
- ui.stopSpinnerSuccess('Dependency graph generated');
864
- if (result.output) {
865
- ui.box({ title: 'Dependency Graph (DOT format)', content: result.output });
866
- }
867
- } else {
868
- ui.stopSpinnerFail('Failed to generate dependency graph');
869
- if (result.error) {
870
- ui.error(result.error);
871
- }
872
- }
873
- } catch (error: any) {
874
- ui.stopSpinnerFail('Error generating Terraform graph');
875
- ui.error(error.message);
876
- }
877
- }
878
-
879
- /**
880
- * Force unlock a locked Terraform state
881
- */
882
- export async function tfForceUnlockCommand(
883
- lockId: string,
884
- options: TfCommandOptions = {}
885
- ): Promise<void> {
886
- const directory = options.directory || process.cwd();
887
-
888
- ui.header('Terraform Force-Unlock');
889
- ui.info(`Directory: ${directory}`);
890
- ui.info(`Lock ID: ${lockId}`);
891
- ui.warning('Force-unlocking state should only be done when a legitimate lock is stuck.');
892
-
893
- ui.startSpinner({ message: `Force-unlocking state with lock ID ${lockId}...` });
894
-
895
- try {
896
- const available = await terraformClient.isAvailable();
897
- if (!available) {
898
- ui.stopSpinnerFail('Terraform Tools Service not available');
899
- ui.error('Please ensure the Terraform Tools Service is running.');
900
- return;
901
- }
902
-
903
- const result = await terraformClient.forceUnlock(directory, lockId);
904
-
905
- if (result.success) {
906
- ui.stopSpinnerSuccess('State lock released successfully');
907
- if (result.output) {
908
- ui.box({ title: 'Force-Unlock Output', content: result.output });
909
- }
910
- } else {
911
- ui.stopSpinnerFail('Failed to force-unlock state');
912
- if (result.error) {
913
- ui.error(result.error);
914
- }
915
- }
916
- } catch (error: any) {
917
- ui.stopSpinnerFail('Error force-unlocking Terraform state');
918
- ui.error(error.message);
919
- }
920
- }
921
-
922
- /**
923
- * Refresh Terraform state against real infrastructure
924
- */
925
- export async function tfRefreshCommand(options: TfCommandOptions = {}): Promise<void> {
926
- const directory = options.directory || process.cwd();
927
-
928
- ui.header('Terraform Refresh');
929
- ui.info(`Directory: ${directory}`);
930
-
931
- ui.startSpinner({ message: 'Refreshing Terraform state...' });
932
-
933
- try {
934
- const available = await terraformClient.isAvailable();
935
- if (!available) {
936
- ui.stopSpinnerFail('Terraform Tools Service not available');
937
- ui.error('Please ensure the Terraform Tools Service is running.');
938
- return;
939
- }
940
-
941
- const result = await terraformClient.refresh(directory, { varFile: options.varFile });
942
-
943
- if (result.success) {
944
- ui.stopSpinnerSuccess('Terraform state refreshed successfully');
945
- if (result.output) {
946
- ui.box({ title: 'Refresh Output', content: result.output });
947
- }
948
- } else {
949
- ui.stopSpinnerFail('Terraform refresh failed');
950
- if (result.error) {
951
- ui.error(result.error);
952
- }
953
- }
954
- } catch (error: any) {
955
- ui.stopSpinnerFail('Error refreshing Terraform state');
956
- ui.error(error.message);
957
- }
958
- }
959
-
960
- /**
961
- * Main terraform command router
962
- */
963
- export async function tfCommand(subcommand: string, args: string[]): Promise<void> {
964
- const options: TfCommandOptions = {
965
- directory: process.cwd(),
966
- };
967
-
968
- // Collect positional args (non-flag args)
969
- const positionalArgs: string[] = [];
970
-
971
- // Parse args
972
- for (let i = 0; i < args.length; i++) {
973
- const arg = args[i];
974
- if (arg === '-d' || arg === '--directory') {
975
- options.directory = args[++i];
976
- } else if (arg === '--var-file') {
977
- options.varFile = args[++i];
978
- } else if (arg === '--auto-approve' || arg === '--yes' || arg === '-y') {
979
- options.autoApprove = true;
980
- } else if (arg === '--dry-run') {
981
- options.dryRun = true;
982
- } else if (arg === '-o' || arg === '--out') {
983
- options.out = args[++i];
984
- } else if (arg === '-p' || arg === '--plan') {
985
- options.planFile = args[++i];
986
- } else if (arg.startsWith('--var=')) {
987
- const [key, value] = arg.slice(6).split('=');
988
- options.vars = options.vars || {};
989
- options.vars[key] = value;
990
- } else if (arg === '--check') {
991
- options.check = true;
992
- } else if (arg === '-r' || arg === '--recursive') {
993
- options.recursive = true;
994
- } else if (arg === '--diff') {
995
- options.diff = true;
996
- } else if (arg === '--type') {
997
- const typeVal = args[++i];
998
- if (typeVal === 'plan' || typeVal === 'apply') {
999
- options.type = typeVal;
1000
- }
1001
- } else if (!arg.startsWith('-')) {
1002
- positionalArgs.push(arg);
1003
- }
1004
- }
1005
-
1006
- const startTime = Date.now();
1007
- const entry = historyManager.addEntry('tf', [subcommand, ...args]);
1008
-
1009
- try {
1010
- switch (subcommand) {
1011
- case 'init':
1012
- await tfInitCommand(options);
1013
- break;
1014
- case 'plan':
1015
- await tfPlanCommand(options);
1016
- break;
1017
- case 'apply':
1018
- await tfApplyCommand(options);
1019
- break;
1020
- case 'validate':
1021
- await tfValidateCommand(options);
1022
- break;
1023
- case 'destroy':
1024
- await tfDestroyCommand(options);
1025
- break;
1026
- case 'show':
1027
- await tfShowCommand(options);
1028
- break;
1029
- case 'fmt':
1030
- await tfFmtCommand(options);
1031
- break;
1032
- case 'workspace':
1033
- await tfWorkspaceCommand(positionalArgs[0] || 'list', positionalArgs[1], options);
1034
- break;
1035
- case 'import':
1036
- if (positionalArgs.length < 2) {
1037
- ui.error('Usage: nimbus tf import <address> <id>');
1038
- ui.info('Example: nimbus tf import aws_instance.web i-1234567890abcdef0');
1039
- return;
1040
- }
1041
- await tfImportCommand(positionalArgs[0], positionalArgs[1], options);
1042
- break;
1043
- case 'output':
1044
- await tfOutputCommand(options, positionalArgs[0]);
1045
- break;
1046
- case 'state':
1047
- await tfStateCommand(positionalArgs, options);
1048
- break;
1049
- case 'taint':
1050
- if (positionalArgs.length < 1) {
1051
- ui.error('Usage: nimbus tf taint <address>');
1052
- ui.info('Example: nimbus tf taint aws_instance.web');
1053
- return;
1054
- }
1055
- await tfTaintCommand(positionalArgs[0], options);
1056
- break;
1057
- case 'untaint':
1058
- if (positionalArgs.length < 1) {
1059
- ui.error('Usage: nimbus tf untaint <address>');
1060
- ui.info('Example: nimbus tf untaint aws_instance.web');
1061
- return;
1062
- }
1063
- await tfUntaintCommand(positionalArgs[0], options);
1064
- break;
1065
- case 'graph':
1066
- await tfGraphCommand(options);
1067
- break;
1068
- case 'force-unlock':
1069
- if (positionalArgs.length < 1) {
1070
- ui.error('Usage: nimbus tf force-unlock <lock-id>');
1071
- ui.info('Example: nimbus tf force-unlock 5b3ab8f0-e74b-5d85-4b2f-b6a9d4b3f3e2');
1072
- return;
1073
- }
1074
- await tfForceUnlockCommand(positionalArgs[0], options);
1075
- break;
1076
- case 'refresh':
1077
- await tfRefreshCommand(options);
1078
- break;
1079
- default:
1080
- ui.error(`Unknown terraform subcommand: ${subcommand}`);
1081
- ui.info(
1082
- 'Available commands: init, plan, apply, validate, destroy, show, fmt, workspace, import, output, state, taint, untaint, graph, force-unlock, refresh'
1083
- );
1084
- }
1085
-
1086
- historyManager.completeEntry(entry.id, 'success', Date.now() - startTime);
1087
- } catch (error: any) {
1088
- historyManager.completeEntry(entry.id, 'failure', Date.now() - startTime, {
1089
- error: error.message,
1090
- });
1091
- throw error;
1092
- }
1093
- }