@build-astron-co/nimbus 0.2.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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,582 @@
1
+ /**
2
+ * Apply Terraform Command
3
+ *
4
+ * Apply Terraform configuration to create/update infrastructure
5
+ *
6
+ * Usage: nimbus apply terraform [directory] [options]
7
+ */
8
+
9
+ import { logger } from '../../utils';
10
+ import { ui, confirm } from '../../wizard';
11
+ import { terraformClient } from '../../clients';
12
+ import { CostEstimator } from '../cost/estimator';
13
+ import {
14
+ loadSafetyPolicy,
15
+ evaluateSafety,
16
+ type SafetyContext,
17
+ type SafetyCheckResult,
18
+ } from '../../config/safety-policy';
19
+ import {
20
+ promptForApproval,
21
+ displaySafetySummary,
22
+ confirmWithResourceName,
23
+ } from '../../wizard/approval';
24
+
25
+ /**
26
+ * Command options
27
+ */
28
+ export interface ApplyTerraformOptions {
29
+ directory?: string;
30
+ dryRun?: boolean;
31
+ autoApprove?: boolean;
32
+ target?: string;
33
+ var?: Record<string, string>;
34
+ varFile?: string;
35
+ parallelism?: number;
36
+ refresh?: boolean;
37
+ lock?: boolean;
38
+ /** Skip safety checks */
39
+ skipSafety?: boolean;
40
+ /** Environment name (for safety policy) */
41
+ environment?: string;
42
+ }
43
+
44
+ /**
45
+ * Display inline cost estimate for a terraform directory
46
+ */
47
+ async function displayCostEstimate(directory: string): Promise<void> {
48
+ try {
49
+ const estimate = await CostEstimator.estimateDirectory(directory);
50
+ if (estimate.totalMonthlyCost > 0) {
51
+ ui.newLine();
52
+ ui.print(
53
+ ` ${ui.color('$', 'yellow')} Estimated monthly cost: ${ui.bold(`$${estimate.totalMonthlyCost.toFixed(2)}/mo`)}`
54
+ );
55
+ const projects = estimate.projects || [];
56
+ const costResources = projects.length > 0 ? projects[0].resources || [] : [];
57
+ if (costResources.length > 0) {
58
+ for (const resource of costResources.slice(0, 5)) {
59
+ ui.print(` ${resource.name}: $${resource.monthlyCost.toFixed(2)}/mo`);
60
+ }
61
+ if (costResources.length > 5) {
62
+ ui.print(ui.dim(` ... and ${costResources.length - 5} more resources`));
63
+ }
64
+ }
65
+ }
66
+ } catch {
67
+ // Silently skip if cost estimation fails — don't block the apply
68
+ ui.print(ui.dim(' Cost estimation available: run "nimbus cost estimate"'));
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Run terraform apply command
74
+ */
75
+ export async function applyTerraformCommand(options: ApplyTerraformOptions = {}): Promise<void> {
76
+ logger.info('Running terraform apply', { options });
77
+
78
+ const directory = options.directory || '.';
79
+
80
+ ui.header('Terraform Apply');
81
+ ui.info(`Directory: ${directory}`);
82
+ ui.newLine();
83
+
84
+ // Check if terraform client is available
85
+ const clientAvailable = await terraformClient.isAvailable();
86
+
87
+ if (clientAvailable) {
88
+ // Use terraform tools service
89
+ await applyWithService(options);
90
+ } else {
91
+ // Fall back to local terraform CLI
92
+ await applyWithLocalCLI(options);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Apply using Terraform Tools Service
98
+ */
99
+ async function applyWithService(options: ApplyTerraformOptions): Promise<void> {
100
+ const directory = options.directory || '.';
101
+
102
+ // First, run plan if not auto-approved
103
+ if (!options.autoApprove) {
104
+ ui.startSpinner({ message: 'Creating execution plan...' });
105
+
106
+ const planResult = await terraformClient.plan(directory, {
107
+ vars: options.var,
108
+ varFile: options.varFile,
109
+ });
110
+
111
+ ui.stopSpinnerSuccess('Plan created');
112
+ ui.newLine();
113
+
114
+ if (!planResult.success) {
115
+ ui.error(`Plan failed: ${planResult.error}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ // Display plan summary
120
+ displayPlanSummary(planResult);
121
+
122
+ // Check if there are changes
123
+ if (!planResult.hasChanges) {
124
+ ui.success('No changes. Infrastructure is up to date.');
125
+ return;
126
+ }
127
+
128
+ // Show inline cost estimate if resources are being created
129
+ if (planResult.hasChanges) {
130
+ await displayCostEstimate(directory);
131
+ }
132
+
133
+ // Dry run - don't apply
134
+ if (options.dryRun) {
135
+ ui.newLine();
136
+ ui.info('Dry run mode - no changes applied');
137
+ return;
138
+ }
139
+
140
+ // Parse destroy count to determine confirmation type
141
+ const destroyCountMatch = planResult.output.match(/(\d+) to destroy/);
142
+ const destroyCount = parseInt(destroyCountMatch?.[1] || '0', 10);
143
+
144
+ // Run safety checks if not skipped
145
+ if (!options.skipSafety) {
146
+ const safetyResult = await runSafetyChecks('apply', planResult.output, options);
147
+
148
+ if (!safetyResult.passed) {
149
+ ui.newLine();
150
+ ui.error('Safety checks failed - operation blocked');
151
+ for (const blocker of safetyResult.blockers) {
152
+ ui.print(` ${ui.color('✗', 'red')} ${blocker.message}`);
153
+ }
154
+ process.exit(1);
155
+ }
156
+
157
+ // If safety requires approval, prompt for it
158
+ if (safetyResult.requiresApproval) {
159
+ // Destructive plans require type-name confirmation first
160
+ if (destroyCount > 0) {
161
+ const confirmed = await confirmWithResourceName(directory, 'terraform directory');
162
+ if (!confirmed) {
163
+ ui.newLine();
164
+ ui.info('Apply cancelled');
165
+ return;
166
+ }
167
+ }
168
+
169
+ const approvalResult = await promptForApproval({
170
+ title: 'Terraform Apply',
171
+ operation: 'terraform apply',
172
+ risks: safetyResult.risks,
173
+ environment: options.environment,
174
+ affectedResources: safetyResult.affectedResources,
175
+ estimatedCost: safetyResult.estimatedCost,
176
+ });
177
+
178
+ if (!approvalResult.approved) {
179
+ ui.newLine();
180
+ ui.info(`Apply cancelled: ${approvalResult.reason || 'User declined'}`);
181
+ return;
182
+ }
183
+ } else {
184
+ // Show safety summary and simple confirm (or type-name confirm for destroys)
185
+ displaySafetySummary({
186
+ operation: 'terraform apply',
187
+ risks: safetyResult.risks,
188
+ passed: safetyResult.passed,
189
+ });
190
+
191
+ ui.newLine();
192
+
193
+ if (destroyCount > 0) {
194
+ const confirmed = await confirmWithResourceName(directory, 'terraform directory');
195
+ if (!confirmed) {
196
+ ui.newLine();
197
+ ui.info('Apply cancelled');
198
+ return;
199
+ }
200
+ } else {
201
+ const proceed = await confirm({
202
+ message: 'Do you want to apply these changes?',
203
+ defaultValue: false,
204
+ });
205
+
206
+ if (!proceed) {
207
+ ui.info('Apply cancelled');
208
+ return;
209
+ }
210
+ }
211
+ }
212
+ } else {
213
+ // Simple confirmation when safety is skipped, but still enforce type-name for destroys
214
+ ui.newLine();
215
+
216
+ if (destroyCount > 0) {
217
+ const confirmed = await confirmWithResourceName(directory, 'terraform directory');
218
+ if (!confirmed) {
219
+ ui.newLine();
220
+ ui.info('Apply cancelled');
221
+ return;
222
+ }
223
+ } else {
224
+ const proceed = await confirm({
225
+ message: 'Do you want to apply these changes?',
226
+ defaultValue: false,
227
+ });
228
+
229
+ if (!proceed) {
230
+ ui.info('Apply cancelled');
231
+ return;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // Run apply
238
+ ui.newLine();
239
+ ui.startSpinner({ message: 'Applying changes...' });
240
+
241
+ const applyResult = await terraformClient.apply(directory, {
242
+ autoApprove: true, // Already confirmed above
243
+ vars: options.var,
244
+ varFile: options.varFile,
245
+ });
246
+
247
+ if (!applyResult.success) {
248
+ ui.stopSpinnerFail('Apply failed');
249
+ ui.error(applyResult.error || 'Unknown error');
250
+
251
+ if (applyResult.output) {
252
+ ui.newLine();
253
+ ui.print(applyResult.output);
254
+ }
255
+
256
+ process.exit(1);
257
+ }
258
+
259
+ ui.stopSpinnerSuccess('Apply complete!');
260
+
261
+ // Track successful terraform apply
262
+ try {
263
+ const { trackGeneration } = await import('../../telemetry');
264
+ trackGeneration('terraform-apply', ['terraform']);
265
+ } catch {
266
+ /* telemetry failure is non-critical */
267
+ }
268
+
269
+ // Display output
270
+ if (applyResult.output) {
271
+ ui.newLine();
272
+ ui.print(applyResult.output);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Apply using local Terraform CLI
278
+ */
279
+ async function applyWithLocalCLI(options: ApplyTerraformOptions): Promise<void> {
280
+ const { spawn } = await import('child_process');
281
+
282
+ const directory = options.directory || '.';
283
+
284
+ // First, run plan to get the output for safety checks (unless auto-approved)
285
+ if (!options.autoApprove && !options.skipSafety) {
286
+ ui.startSpinner({ message: 'Creating execution plan...' });
287
+
288
+ const planOutput = await runLocalTerraformPlan(directory, options);
289
+
290
+ ui.stopSpinnerSuccess('Plan created');
291
+ ui.newLine();
292
+
293
+ // Display plan summary
294
+ const hasChanges =
295
+ planOutput.includes('to add') ||
296
+ planOutput.includes('to change') ||
297
+ planOutput.includes('to destroy');
298
+
299
+ displayPlanSummary({
300
+ success: true,
301
+ hasChanges,
302
+ output: planOutput,
303
+ });
304
+
305
+ if (!hasChanges) {
306
+ ui.success('No changes. Infrastructure is up to date.');
307
+ return;
308
+ }
309
+
310
+ // Dry run - don't apply
311
+ if (options.dryRun) {
312
+ ui.newLine();
313
+ ui.info('Dry run mode - no changes applied');
314
+ return;
315
+ }
316
+
317
+ // Parse destroy count to determine confirmation type
318
+ const destroyCountMatch = planOutput.match(/(\d+) to destroy/);
319
+ const destroyCount = parseInt(destroyCountMatch?.[1] || '0', 10);
320
+
321
+ // Run safety checks
322
+ const safetyResult = await runSafetyChecks('apply', planOutput, options);
323
+
324
+ if (!safetyResult.passed) {
325
+ ui.newLine();
326
+ ui.error('Safety checks failed - operation blocked');
327
+ for (const blocker of safetyResult.blockers) {
328
+ ui.print(` ${ui.color('✗', 'red')} ${blocker.message}`);
329
+ }
330
+ process.exit(1);
331
+ }
332
+
333
+ // If safety requires approval, prompt for it
334
+ if (safetyResult.requiresApproval) {
335
+ // Destructive plans require type-name confirmation first
336
+ if (destroyCount > 0) {
337
+ const confirmed = await confirmWithResourceName(directory, 'terraform directory');
338
+ if (!confirmed) {
339
+ ui.newLine();
340
+ ui.info('Apply cancelled');
341
+ return;
342
+ }
343
+ }
344
+
345
+ const approvalResult = await promptForApproval({
346
+ title: 'Terraform Apply',
347
+ operation: 'terraform apply',
348
+ risks: safetyResult.risks,
349
+ environment: options.environment,
350
+ affectedResources: safetyResult.affectedResources,
351
+ estimatedCost: safetyResult.estimatedCost,
352
+ });
353
+
354
+ if (!approvalResult.approved) {
355
+ ui.newLine();
356
+ ui.info(`Apply cancelled: ${approvalResult.reason || 'User declined'}`);
357
+ return;
358
+ }
359
+ } else {
360
+ // Show safety summary and simple confirm (or type-name confirm for destroys)
361
+ displaySafetySummary({
362
+ operation: 'terraform apply',
363
+ risks: safetyResult.risks,
364
+ passed: safetyResult.passed,
365
+ });
366
+
367
+ ui.newLine();
368
+
369
+ if (destroyCount > 0) {
370
+ const confirmed = await confirmWithResourceName(directory, 'terraform directory');
371
+ if (!confirmed) {
372
+ ui.newLine();
373
+ ui.info('Apply cancelled');
374
+ return;
375
+ }
376
+ } else {
377
+ const proceed = await confirm({
378
+ message: 'Do you want to apply these changes?',
379
+ defaultValue: false,
380
+ });
381
+
382
+ if (!proceed) {
383
+ ui.info('Apply cancelled');
384
+ return;
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ // Build terraform apply command
391
+ const args = ['apply', '-auto-approve']; // Auto-approve since we already confirmed
392
+
393
+ if (options.var) {
394
+ for (const [key, value] of Object.entries(options.var)) {
395
+ args.push('-var', `${key}=${value}`);
396
+ }
397
+ }
398
+
399
+ if (options.varFile) {
400
+ args.push('-var-file', options.varFile);
401
+ }
402
+
403
+ if (options.target) {
404
+ args.push('-target', options.target);
405
+ }
406
+
407
+ if (options.parallelism !== undefined) {
408
+ args.push('-parallelism', String(options.parallelism));
409
+ }
410
+
411
+ if (options.refresh === false) {
412
+ args.push('-refresh=false');
413
+ }
414
+
415
+ if (options.lock === false) {
416
+ args.push('-lock=false');
417
+ }
418
+
419
+ ui.newLine();
420
+ ui.info(`Running: terraform ${args.join(' ')}`);
421
+ ui.newLine();
422
+
423
+ // Run terraform
424
+ return new Promise(resolve => {
425
+ const proc = spawn('terraform', args, {
426
+ cwd: directory,
427
+ stdio: 'inherit',
428
+ });
429
+
430
+ proc.on('error', error => {
431
+ ui.error(`Failed to run terraform: ${error.message}`);
432
+ ui.info('Make sure terraform is installed and in your PATH');
433
+ process.exit(1);
434
+ });
435
+
436
+ proc.on('close', code => {
437
+ if (code === 0) {
438
+ ui.newLine();
439
+ ui.success('Terraform apply completed successfully');
440
+
441
+ // Track successful terraform apply
442
+ try {
443
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
444
+ const { trackGeneration } = require('../../telemetry');
445
+ trackGeneration('terraform-apply', ['terraform']);
446
+ } catch {
447
+ /* telemetry failure is non-critical */
448
+ }
449
+
450
+ resolve();
451
+ } else {
452
+ ui.newLine();
453
+ ui.error(`Terraform apply failed with exit code ${code}`);
454
+ process.exit(code || 1);
455
+ }
456
+ });
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Run local terraform plan and capture output
462
+ */
463
+ async function runLocalTerraformPlan(
464
+ directory: string,
465
+ options: ApplyTerraformOptions
466
+ ): Promise<string> {
467
+ const { spawn } = await import('child_process');
468
+
469
+ const args = ['plan', '-no-color'];
470
+
471
+ if (options.var) {
472
+ for (const [key, value] of Object.entries(options.var)) {
473
+ args.push('-var', `${key}=${value}`);
474
+ }
475
+ }
476
+
477
+ if (options.varFile) {
478
+ args.push('-var-file', options.varFile);
479
+ }
480
+
481
+ if (options.target) {
482
+ args.push('-target', options.target);
483
+ }
484
+
485
+ return new Promise((resolve, reject) => {
486
+ let output = '';
487
+
488
+ const proc = spawn('terraform', args, {
489
+ cwd: directory,
490
+ stdio: ['inherit', 'pipe', 'pipe'],
491
+ });
492
+
493
+ proc.stdout?.on('data', data => {
494
+ output += data.toString();
495
+ });
496
+
497
+ proc.stderr?.on('data', data => {
498
+ output += data.toString();
499
+ });
500
+
501
+ proc.on('error', error => {
502
+ reject(new Error(`Failed to run terraform plan: ${error.message}`));
503
+ });
504
+
505
+ proc.on('close', code => {
506
+ if (code === 0) {
507
+ resolve(output);
508
+ } else {
509
+ reject(new Error(`Terraform plan failed with exit code ${code}`));
510
+ }
511
+ });
512
+ });
513
+ }
514
+
515
+ /**
516
+ * Run safety checks for the operation
517
+ */
518
+ async function runSafetyChecks(
519
+ operation: string,
520
+ planOutput: string,
521
+ options: ApplyTerraformOptions
522
+ ): Promise<SafetyCheckResult> {
523
+ const policy = loadSafetyPolicy();
524
+
525
+ const context: SafetyContext = {
526
+ operation,
527
+ type: 'terraform',
528
+ environment: options.environment,
529
+ planOutput,
530
+ metadata: {
531
+ directory: options.directory,
532
+ target: options.target,
533
+ },
534
+ };
535
+
536
+ return evaluateSafety(context, policy);
537
+ }
538
+
539
+ /**
540
+ * Display plan summary
541
+ */
542
+ function displayPlanSummary(planResult: {
543
+ success: boolean;
544
+ hasChanges: boolean;
545
+ output: string;
546
+ }): void {
547
+ if (!planResult.hasChanges) {
548
+ ui.print('Plan Summary:');
549
+ ui.newLine();
550
+ ui.print(' No changes');
551
+ return;
552
+ }
553
+
554
+ // Parse changes from output
555
+ const addMatch = planResult.output.match(/(\d+) to add/);
556
+ const changeMatch = planResult.output.match(/(\d+) to change/);
557
+ const destroyMatch = planResult.output.match(/(\d+) to destroy/);
558
+
559
+ const add = parseInt(addMatch?.[1] || '0', 10);
560
+ const change = parseInt(changeMatch?.[1] || '0', 10);
561
+ const destroy = parseInt(destroyMatch?.[1] || '0', 10);
562
+
563
+ ui.print('Plan Summary:');
564
+ ui.newLine();
565
+
566
+ if (add > 0) {
567
+ ui.print(` ${ui.color(`+ ${add} to add`, 'green')}`);
568
+ }
569
+ if (change > 0) {
570
+ ui.print(` ${ui.color(`~ ${change} to change`, 'yellow')}`);
571
+ }
572
+ if (destroy > 0) {
573
+ ui.print(` ${ui.color(`- ${destroy} to destroy`, 'red')}`);
574
+ }
575
+
576
+ if (add === 0 && change === 0 && destroy === 0) {
577
+ ui.print(' Changes detected (see output)');
578
+ }
579
+ }
580
+
581
+ // Export as default
582
+ export default applyTerraformCommand;