@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,1460 @@
1
+ /**
2
+ * Generate Terraform Command
3
+ *
4
+ * Interactive wizard for AWS infrastructure discovery and Terraform generation
5
+ *
6
+ * Usage: nimbus generate terraform [options]
7
+ */
8
+
9
+ import { logger } from '../utils';
10
+ import { RestClient } from '../clients';
11
+ import {
12
+ createWizard,
13
+ ui,
14
+ select,
15
+ multiSelect,
16
+ confirm,
17
+ input,
18
+ pathInput,
19
+ type TerraformWizardContext,
20
+ type WizardStep,
21
+ type StepResult,
22
+ } from '../wizard';
23
+
24
+ // AWS Tools Service client
25
+ const awsToolsUrl = process.env.AWS_TOOLS_SERVICE_URL || 'http://localhost:3009';
26
+ const awsClient = new RestClient(awsToolsUrl);
27
+
28
+ // Generator Service client
29
+ const generatorUrl = process.env.GENERATOR_SERVICE_URL || 'http://localhost:3003';
30
+ const generatorClient = new RestClient(generatorUrl);
31
+
32
+ // GCP Tools Service client
33
+ const gcpToolsUrl = process.env.GCP_TOOLS_SERVICE_URL || 'http://localhost:3016';
34
+ const gcpClient = new RestClient(gcpToolsUrl);
35
+
36
+ // Azure Tools Service client
37
+ const azureToolsUrl = process.env.AZURE_TOOLS_SERVICE_URL || 'http://localhost:3017';
38
+ const azureClient = new RestClient(azureToolsUrl);
39
+
40
+ /**
41
+ * Command options from CLI arguments
42
+ */
43
+ export interface GenerateTerraformOptions {
44
+ profile?: string;
45
+ regions?: string[];
46
+ services?: string[];
47
+ output?: string;
48
+ nonInteractive?: boolean;
49
+ acceptAllImprovements?: boolean;
50
+ rejectAllImprovements?: boolean;
51
+ acceptCategories?: string[];
52
+ mock?: boolean;
53
+ provider?: 'aws' | 'gcp' | 'azure';
54
+ gcpProject?: string;
55
+ azureSubscription?: string;
56
+ jsonOutput?: boolean;
57
+ questionnaire?: boolean;
58
+ conversational?: boolean;
59
+ skipValidation?: boolean;
60
+ validationMode?: 'required' | 'optional';
61
+ }
62
+
63
+ /**
64
+ * Run the generate terraform command
65
+ */
66
+ export async function generateTerraformCommand(
67
+ options: GenerateTerraformOptions = {}
68
+ ): Promise<void> {
69
+ logger.info('Starting Terraform generation wizard');
70
+
71
+ // Non-interactive mode
72
+ if (options.nonInteractive) {
73
+ await runNonInteractive(options);
74
+ return;
75
+ }
76
+
77
+ // Questionnaire mode
78
+ if (options.questionnaire) {
79
+ const { questionnaireCommand } = await import('./questionnaire');
80
+ await questionnaireCommand({
81
+ type: 'terraform',
82
+ outputDir: options.output,
83
+ });
84
+ return;
85
+ }
86
+
87
+ // Conversational mode (Mode B)
88
+ if (options.conversational) {
89
+ await runConversational(options);
90
+ return;
91
+ }
92
+
93
+ // Interactive wizard mode
94
+ const steps = createWizardSteps();
95
+
96
+ const wizard = createWizard<TerraformWizardContext>({
97
+ title: 'nimbus generate terraform',
98
+ description: 'Generate Terraform from your cloud infrastructure',
99
+ initialContext: {
100
+ provider: 'aws',
101
+ awsProfile: options.profile,
102
+ awsRegions: options.regions,
103
+ servicesToScan: options.services,
104
+ outputPath: options.output,
105
+ },
106
+ steps,
107
+ onEvent: event => {
108
+ if (event.type === 'step:start' && process.stdout.isTTY) {
109
+ const idx = steps.findIndex(s => s.id === event.stepId);
110
+ if (idx >= 0) {
111
+ // Visual step progress bar
112
+ const progress = steps.map((s, i) => {
113
+ if (i < idx) {
114
+ return ui.color(`\u2713 ${s.title}`, 'green');
115
+ }
116
+ if (i === idx) {
117
+ return ui.color(`\u25CF ${s.title}`, 'cyan');
118
+ }
119
+ return ui.dim(`\u25CB ${s.title}`);
120
+ });
121
+ ui.newLine();
122
+ ui.print(ui.dim(' Progress: ') + progress.join(ui.dim(' \u2500 ')));
123
+ }
124
+ }
125
+ logger.debug('Wizard event', { type: event.type });
126
+ },
127
+ });
128
+
129
+ const result = await wizard.run();
130
+
131
+ if (result.success) {
132
+ ui.newLine();
133
+ ui.box({
134
+ title: 'Complete!',
135
+ content: [
136
+ 'Your infrastructure has been codified as Terraform.',
137
+ '',
138
+ 'Next steps:',
139
+ ` 1. Review the generated files in ${result.context.outputPath}`,
140
+ ' 2. Run "terraform plan" to see what will be imported',
141
+ ' 3. Run "terraform apply" to bring resources under Terraform control',
142
+ '',
143
+ 'Scan saved to history. View with: nimbus infra history',
144
+ ],
145
+ style: 'rounded',
146
+ borderColor: 'green',
147
+ padding: 1,
148
+ });
149
+ } else {
150
+ ui.error(`Wizard failed: ${result.error?.message || 'Unknown error'}`);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Create wizard steps
157
+ */
158
+ function createWizardSteps(): WizardStep<TerraformWizardContext>[] {
159
+ return [
160
+ // Step 1: Provider Selection
161
+ {
162
+ id: 'provider',
163
+ title: 'Cloud Provider Selection',
164
+ description: 'Select the cloud provider to scan for infrastructure',
165
+ execute: providerSelectionStep,
166
+ },
167
+
168
+ // Step 2: AWS Configuration
169
+ {
170
+ id: 'aws-config',
171
+ title: 'AWS Configuration',
172
+ description: 'Configure AWS profile and regions to scan',
173
+ condition: ctx => ctx.provider === 'aws',
174
+ execute: awsConfigStep,
175
+ },
176
+
177
+ // Step 3: Service Selection
178
+ {
179
+ id: 'services',
180
+ title: 'Service Selection',
181
+ description: 'Select which AWS services to scan',
182
+ condition: ctx => ctx.provider === 'aws',
183
+ execute: serviceSelectionStep,
184
+ },
185
+
186
+ // GCP Configuration
187
+ {
188
+ id: 'gcp-config',
189
+ title: 'GCP Configuration',
190
+ description: 'Configure GCP project and regions to scan',
191
+ condition: ctx => ctx.provider === 'gcp',
192
+ execute: gcpConfigStep,
193
+ },
194
+
195
+ // GCP Service Selection
196
+ {
197
+ id: 'gcp-services',
198
+ title: 'GCP Service Selection',
199
+ description: 'Select which GCP services to scan',
200
+ condition: ctx => ctx.provider === 'gcp',
201
+ execute: gcpServiceSelectionStep,
202
+ },
203
+
204
+ // Azure Configuration
205
+ {
206
+ id: 'azure-config',
207
+ title: 'Azure Configuration',
208
+ description: 'Configure Azure subscription and resource group',
209
+ condition: ctx => ctx.provider === 'azure',
210
+ execute: azureConfigStep,
211
+ },
212
+
213
+ // Azure Service Selection
214
+ {
215
+ id: 'azure-services',
216
+ title: 'Azure Service Selection',
217
+ description: 'Select which Azure services to scan',
218
+ condition: ctx => ctx.provider === 'azure',
219
+ execute: azureServiceSelectionStep,
220
+ },
221
+
222
+ // Step 4: Discovery
223
+ {
224
+ id: 'discovery',
225
+ title: 'Infrastructure Discovery',
226
+ description: 'Scanning your AWS infrastructure...',
227
+ execute: discoveryStep,
228
+ },
229
+
230
+ // Step 5: Generation Options
231
+ {
232
+ id: 'generation-options',
233
+ title: 'Generation Options',
234
+ description: 'Configure Terraform generation options',
235
+ execute: generationOptionsStep,
236
+ },
237
+
238
+ // Step 6: Output Location
239
+ {
240
+ id: 'output',
241
+ title: 'Output Location',
242
+ description: 'Where should the Terraform files be saved?',
243
+ execute: outputLocationStep,
244
+ },
245
+
246
+ // Future steps (Phase 2+):
247
+ // - Terraform Generation
248
+ // - Best Practices Analysis
249
+ // - Interactive Review
250
+ // - Starter Kit Generation
251
+ // - Terraform Operations
252
+ ];
253
+ }
254
+
255
+ /**
256
+ * Step 1: Provider Selection
257
+ */
258
+ async function providerSelectionStep(ctx: TerraformWizardContext): Promise<StepResult> {
259
+ const provider = await select<'aws' | 'gcp' | 'azure'>({
260
+ message: 'Select cloud provider:',
261
+ options: [
262
+ {
263
+ value: 'aws',
264
+ label: 'AWS (Amazon Web Services)',
265
+ description: 'Scan EC2, S3, RDS, Lambda, VPC, IAM, and more',
266
+ },
267
+ {
268
+ value: 'gcp',
269
+ label: 'GCP (Google Cloud Platform)',
270
+ description: 'Scan Compute, GCS, GKE, Cloud Functions, VPC, IAM',
271
+ },
272
+ {
273
+ value: 'azure',
274
+ label: 'Azure (Microsoft Azure)',
275
+ description: 'Scan VMs, Storage, AKS, Functions, VNet, IAM',
276
+ },
277
+ ],
278
+ defaultValue: ctx.provider || 'aws',
279
+ });
280
+
281
+ if (!provider) {
282
+ return { success: false, error: 'No provider selected' };
283
+ }
284
+
285
+ return {
286
+ success: true,
287
+ data: { provider },
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Step 2: AWS Configuration
293
+ */
294
+ async function awsConfigStep(ctx: TerraformWizardContext): Promise<StepResult> {
295
+ // Fetch available profiles
296
+ ui.startSpinner({ message: 'Fetching AWS profiles...' });
297
+
298
+ let profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }> = [];
299
+
300
+ try {
301
+ const profilesResponse = await awsClient.get<{
302
+ profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }>;
303
+ }>('/api/aws/profiles');
304
+
305
+ if (profilesResponse.success && profilesResponse.data?.profiles) {
306
+ profiles = profilesResponse.data.profiles;
307
+ }
308
+
309
+ ui.stopSpinnerSuccess(`Found ${profiles.length} AWS profiles`);
310
+ } catch (error) {
311
+ ui.stopSpinnerFail('Could not fetch AWS profiles');
312
+ // Continue with manual input
313
+ profiles = [{ name: 'default', source: 'credentials', isSSO: false }];
314
+ }
315
+
316
+ // Profile selection
317
+ let selectedProfile = ctx.awsProfile;
318
+
319
+ if (!selectedProfile) {
320
+ const profileOptions = profiles.map(p => ({
321
+ value: p.name,
322
+ label: p.name + (p.isSSO ? ' (SSO)' : ''),
323
+ description: `Source: ${p.source}${p.region ? `, Region: ${p.region}` : ''}`,
324
+ }));
325
+
326
+ selectedProfile = await select({
327
+ message: 'Select AWS profile:',
328
+ options: profileOptions,
329
+ defaultValue: 'default',
330
+ });
331
+
332
+ if (!selectedProfile) {
333
+ return { success: false, error: 'No profile selected' };
334
+ }
335
+ }
336
+
337
+ // Validate credentials
338
+ ui.startSpinner({ message: `Validating credentials for profile "${selectedProfile}"...` });
339
+
340
+ try {
341
+ const validateResponse = await awsClient.post<{
342
+ valid: boolean;
343
+ accountId?: string;
344
+ accountAlias?: string;
345
+ error?: string;
346
+ }>('/api/aws/profiles/validate', { profile: selectedProfile });
347
+
348
+ if (!validateResponse.success || !validateResponse.data?.valid) {
349
+ ui.stopSpinnerFail(`Invalid credentials: ${validateResponse.data?.error || 'Unknown error'}`);
350
+ return { success: false, error: 'Invalid AWS credentials' };
351
+ }
352
+
353
+ ui.stopSpinnerSuccess(
354
+ `Authenticated to account ${validateResponse.data.accountId}${
355
+ validateResponse.data.accountAlias ? ` (${validateResponse.data.accountAlias})` : ''
356
+ }`
357
+ );
358
+
359
+ ctx.awsAccountId = validateResponse.data.accountId;
360
+ ctx.awsAccountAlias = validateResponse.data.accountAlias;
361
+ } catch (error: any) {
362
+ ui.stopSpinnerFail(`Failed to validate credentials: ${error.message}`);
363
+ return { success: false, error: 'Credential validation failed' };
364
+ }
365
+
366
+ // Region selection
367
+ ui.newLine();
368
+
369
+ const regionChoice = await select<'all' | 'specific'>({
370
+ message: 'Select regions to scan:',
371
+ options: [
372
+ {
373
+ value: 'all',
374
+ label: 'All enabled regions',
375
+ description: 'Scan all regions enabled for your account',
376
+ },
377
+ {
378
+ value: 'specific',
379
+ label: 'Specific regions',
380
+ description: 'Select specific regions to scan',
381
+ },
382
+ ],
383
+ defaultValue: 'all',
384
+ });
385
+
386
+ let selectedRegions: string[] = [];
387
+
388
+ if (regionChoice === 'specific') {
389
+ // Fetch available regions
390
+ ui.startSpinner({ message: 'Fetching available regions...' });
391
+
392
+ try {
393
+ const regionsResponse = await awsClient.get<{
394
+ regions: Array<{ name: string; displayName: string }>;
395
+ }>(`/api/aws/regions?profile=${selectedProfile}`);
396
+
397
+ ui.stopSpinnerSuccess(`Found ${regionsResponse.data?.regions?.length || 0} regions`);
398
+
399
+ if (regionsResponse.success && regionsResponse.data?.regions) {
400
+ const regionOptions = regionsResponse.data.regions.map(r => ({
401
+ value: r.name,
402
+ label: `${r.name} - ${r.displayName}`,
403
+ }));
404
+
405
+ selectedRegions = (await multiSelect({
406
+ message: 'Select regions to scan:',
407
+ options: regionOptions,
408
+ required: true,
409
+ })) as string[];
410
+ }
411
+ } catch (error) {
412
+ ui.stopSpinnerFail('Could not fetch regions');
413
+ // Use common regions as fallback
414
+ selectedRegions = ['us-east-1'];
415
+ }
416
+ }
417
+
418
+ return {
419
+ success: true,
420
+ data: {
421
+ awsProfile: selectedProfile,
422
+ awsRegions: regionChoice === 'all' ? undefined : selectedRegions,
423
+ },
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Step 3: Service Selection
429
+ */
430
+ async function serviceSelectionStep(_ctx: TerraformWizardContext): Promise<StepResult> {
431
+ const serviceChoice = await select<'all' | 'specific'>({
432
+ message: 'Select services to scan:',
433
+ options: [
434
+ {
435
+ value: 'all',
436
+ label: 'All supported services',
437
+ description: 'EC2, S3, RDS, Lambda, VPC, IAM, ECS, EKS, DynamoDB, CloudFront',
438
+ },
439
+ {
440
+ value: 'specific',
441
+ label: 'Specific services',
442
+ description: 'Select specific services to scan',
443
+ },
444
+ ],
445
+ defaultValue: 'all',
446
+ });
447
+
448
+ if (serviceChoice === 'all') {
449
+ return { success: true, data: { servicesToScan: undefined } };
450
+ }
451
+
452
+ const serviceOptions = [
453
+ { value: 'EC2', label: 'EC2', description: 'Instances, volumes, security groups, AMIs' },
454
+ { value: 'S3', label: 'S3', description: 'Buckets and bucket policies' },
455
+ { value: 'RDS', label: 'RDS', description: 'Database instances and clusters' },
456
+ { value: 'Lambda', label: 'Lambda', description: 'Functions and layers' },
457
+ { value: 'VPC', label: 'VPC', description: 'VPCs, subnets, route tables, NAT gateways' },
458
+ { value: 'IAM', label: 'IAM', description: 'Roles, policies, users, groups' },
459
+ { value: 'ECS', label: 'ECS', description: 'Clusters, services, task definitions' },
460
+ { value: 'EKS', label: 'EKS', description: 'Clusters and node groups' },
461
+ { value: 'DynamoDB', label: 'DynamoDB', description: 'Tables' },
462
+ { value: 'CloudFront', label: 'CloudFront', description: 'Distributions' },
463
+ ];
464
+
465
+ const selectedServices = await multiSelect({
466
+ message: 'Select services to scan:',
467
+ options: serviceOptions,
468
+ required: true,
469
+ });
470
+
471
+ return {
472
+ success: true,
473
+ data: { servicesToScan: selectedServices as string[] },
474
+ };
475
+ }
476
+
477
+ /**
478
+ * GCP Configuration Step
479
+ */
480
+ async function gcpConfigStep(ctx: TerraformWizardContext): Promise<StepResult> {
481
+ // Project ID
482
+ const projectId = await input({
483
+ message: 'Enter your GCP project ID:',
484
+ defaultValue: ctx.gcpProject || '',
485
+ });
486
+
487
+ if (!projectId) {
488
+ return { success: false, error: 'GCP project ID is required' };
489
+ }
490
+
491
+ // Validate project access
492
+ ui.startSpinner({ message: `Validating access to project "${projectId}"...` });
493
+
494
+ try {
495
+ const validateResponse = await gcpClient.post<{
496
+ valid: boolean;
497
+ projectName?: string;
498
+ error?: string;
499
+ }>('/api/gcp/projects/validate', { projectId });
500
+
501
+ if (!validateResponse.success || !validateResponse.data?.valid) {
502
+ ui.stopSpinnerFail(`Invalid project: ${validateResponse.data?.error || 'Unknown error'}`);
503
+ return { success: false, error: 'Invalid GCP project' };
504
+ }
505
+
506
+ ui.stopSpinnerSuccess(
507
+ `Connected to project ${projectId}${
508
+ validateResponse.data.projectName ? ` (${validateResponse.data.projectName})` : ''
509
+ }`
510
+ );
511
+ } catch (error: any) {
512
+ ui.stopSpinnerFail(`Failed to validate project: ${error.message}`);
513
+ return { success: false, error: 'Project validation failed' };
514
+ }
515
+
516
+ // Region selection
517
+ ui.newLine();
518
+
519
+ const regionChoice = await select<'all' | 'specific'>({
520
+ message: 'Select regions to scan:',
521
+ options: [
522
+ {
523
+ value: 'all',
524
+ label: 'All available regions',
525
+ description: 'Scan all GCP regions',
526
+ },
527
+ {
528
+ value: 'specific',
529
+ label: 'Specific regions',
530
+ description: 'Select specific regions to scan',
531
+ },
532
+ ],
533
+ defaultValue: 'all',
534
+ });
535
+
536
+ let selectedRegions: string[] = [];
537
+
538
+ if (regionChoice === 'specific') {
539
+ const gcpRegionOptions = [
540
+ { value: 'us-central1', label: 'us-central1 - Iowa' },
541
+ { value: 'us-east1', label: 'us-east1 - South Carolina' },
542
+ { value: 'us-east4', label: 'us-east4 - Northern Virginia' },
543
+ { value: 'us-west1', label: 'us-west1 - Oregon' },
544
+ { value: 'europe-west1', label: 'europe-west1 - Belgium' },
545
+ { value: 'europe-west2', label: 'europe-west2 - London' },
546
+ { value: 'asia-east1', label: 'asia-east1 - Taiwan' },
547
+ { value: 'asia-southeast1', label: 'asia-southeast1 - Singapore' },
548
+ ];
549
+
550
+ selectedRegions = (await multiSelect({
551
+ message: 'Select GCP regions to scan:',
552
+ options: gcpRegionOptions,
553
+ required: true,
554
+ })) as string[];
555
+ }
556
+
557
+ return {
558
+ success: true,
559
+ data: {
560
+ gcpProject: projectId,
561
+ gcpRegions: regionChoice === 'all' ? undefined : selectedRegions,
562
+ },
563
+ };
564
+ }
565
+
566
+ /**
567
+ * GCP Service Selection Step
568
+ */
569
+ async function gcpServiceSelectionStep(_ctx: TerraformWizardContext): Promise<StepResult> {
570
+ const serviceChoice = await select<'all' | 'specific'>({
571
+ message: 'Select GCP services to scan:',
572
+ options: [
573
+ {
574
+ value: 'all',
575
+ label: 'All supported services',
576
+ description: 'Compute, GCS, GKE, Cloud Functions, VPC, IAM, Cloud SQL, Pub/Sub',
577
+ },
578
+ {
579
+ value: 'specific',
580
+ label: 'Specific services',
581
+ description: 'Select specific services to scan',
582
+ },
583
+ ],
584
+ defaultValue: 'all',
585
+ });
586
+
587
+ if (serviceChoice === 'all') {
588
+ return { success: true, data: { servicesToScan: undefined } };
589
+ }
590
+
591
+ const serviceOptions = [
592
+ { value: 'Compute', label: 'Compute Engine', description: 'VMs, disks, images' },
593
+ { value: 'GCS', label: 'Cloud Storage', description: 'Buckets and objects' },
594
+ { value: 'GKE', label: 'Google Kubernetes Engine', description: 'Clusters and node pools' },
595
+ { value: 'CloudFunctions', label: 'Cloud Functions', description: 'Serverless functions' },
596
+ { value: 'VPC', label: 'VPC Network', description: 'Networks, subnets, firewalls' },
597
+ { value: 'IAM', label: 'IAM', description: 'Roles, service accounts, policies' },
598
+ { value: 'CloudSQL', label: 'Cloud SQL', description: 'Database instances' },
599
+ { value: 'PubSub', label: 'Pub/Sub', description: 'Topics and subscriptions' },
600
+ ];
601
+
602
+ const selectedServices = await multiSelect({
603
+ message: 'Select GCP services to scan:',
604
+ options: serviceOptions,
605
+ required: true,
606
+ });
607
+
608
+ return {
609
+ success: true,
610
+ data: { servicesToScan: selectedServices as string[] },
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Azure Configuration Step
616
+ */
617
+ async function azureConfigStep(ctx: TerraformWizardContext): Promise<StepResult> {
618
+ // Subscription ID
619
+ const subscriptionId = await input({
620
+ message: 'Enter your Azure subscription ID:',
621
+ defaultValue: ctx.azureSubscription || '',
622
+ });
623
+
624
+ if (!subscriptionId) {
625
+ return { success: false, error: 'Azure subscription ID is required' };
626
+ }
627
+
628
+ // Validate subscription access
629
+ ui.startSpinner({ message: `Validating access to subscription "${subscriptionId}"...` });
630
+
631
+ try {
632
+ const validateResponse = await azureClient.post<{
633
+ valid: boolean;
634
+ subscriptionName?: string;
635
+ error?: string;
636
+ }>('/api/azure/subscriptions/validate', { subscriptionId });
637
+
638
+ if (!validateResponse.success || !validateResponse.data?.valid) {
639
+ ui.stopSpinnerFail(
640
+ `Invalid subscription: ${validateResponse.data?.error || 'Unknown error'}`
641
+ );
642
+ return { success: false, error: 'Invalid Azure subscription' };
643
+ }
644
+
645
+ ui.stopSpinnerSuccess(
646
+ `Connected to subscription ${subscriptionId}${
647
+ validateResponse.data.subscriptionName ? ` (${validateResponse.data.subscriptionName})` : ''
648
+ }`
649
+ );
650
+ } catch (error: any) {
651
+ ui.stopSpinnerFail(`Failed to validate subscription: ${error.message}`);
652
+ return { success: false, error: 'Subscription validation failed' };
653
+ }
654
+
655
+ // Resource group (optional)
656
+ ui.newLine();
657
+ const resourceGroup = await input({
658
+ message: 'Resource group (leave empty to scan all):',
659
+ defaultValue: ctx.azureResourceGroup || '',
660
+ });
661
+
662
+ // Region selection
663
+ ui.newLine();
664
+
665
+ const regionChoice = await select<'all' | 'specific'>({
666
+ message: 'Select regions to scan:',
667
+ options: [
668
+ {
669
+ value: 'all',
670
+ label: 'All available regions',
671
+ description: 'Scan all Azure regions',
672
+ },
673
+ {
674
+ value: 'specific',
675
+ label: 'Specific regions',
676
+ description: 'Select specific regions to scan',
677
+ },
678
+ ],
679
+ defaultValue: 'all',
680
+ });
681
+
682
+ let _selectedRegions: string[] = [];
683
+
684
+ if (regionChoice === 'specific') {
685
+ const azureRegionOptions = [
686
+ { value: 'eastus', label: 'East US' },
687
+ { value: 'eastus2', label: 'East US 2' },
688
+ { value: 'westus2', label: 'West US 2' },
689
+ { value: 'centralus', label: 'Central US' },
690
+ { value: 'westeurope', label: 'West Europe' },
691
+ { value: 'northeurope', label: 'North Europe' },
692
+ { value: 'southeastasia', label: 'Southeast Asia' },
693
+ { value: 'eastasia', label: 'East Asia' },
694
+ ];
695
+
696
+ _selectedRegions = (await multiSelect({
697
+ message: 'Select Azure regions to scan:',
698
+ options: azureRegionOptions,
699
+ required: true,
700
+ })) as string[];
701
+ }
702
+
703
+ return {
704
+ success: true,
705
+ data: {
706
+ azureSubscription: subscriptionId,
707
+ azureResourceGroup: resourceGroup || undefined,
708
+ },
709
+ };
710
+ }
711
+
712
+ /**
713
+ * Azure Service Selection Step
714
+ */
715
+ async function azureServiceSelectionStep(_ctx: TerraformWizardContext): Promise<StepResult> {
716
+ const serviceChoice = await select<'all' | 'specific'>({
717
+ message: 'Select Azure services to scan:',
718
+ options: [
719
+ {
720
+ value: 'all',
721
+ label: 'All supported services',
722
+ description: 'VMs, Storage, AKS, Functions, VNet, IAM, SQL, Service Bus',
723
+ },
724
+ {
725
+ value: 'specific',
726
+ label: 'Specific services',
727
+ description: 'Select specific services to scan',
728
+ },
729
+ ],
730
+ defaultValue: 'all',
731
+ });
732
+
733
+ if (serviceChoice === 'all') {
734
+ return { success: true, data: { servicesToScan: undefined } };
735
+ }
736
+
737
+ const serviceOptions = [
738
+ { value: 'VirtualMachines', label: 'Virtual Machines', description: 'VMs, disks, images' },
739
+ {
740
+ value: 'Storage',
741
+ label: 'Storage Accounts',
742
+ description: 'Blob, file, queue, table storage',
743
+ },
744
+ { value: 'AKS', label: 'Azure Kubernetes Service', description: 'Clusters and node pools' },
745
+ { value: 'Functions', label: 'Azure Functions', description: 'Serverless functions' },
746
+ { value: 'VNet', label: 'Virtual Network', description: 'VNets, subnets, NSGs' },
747
+ { value: 'IAM', label: 'IAM', description: 'Role assignments, managed identities' },
748
+ { value: 'SQLDatabase', label: 'Azure SQL', description: 'SQL databases and servers' },
749
+ { value: 'ServiceBus', label: 'Service Bus', description: 'Queues and topics' },
750
+ ];
751
+
752
+ const selectedServices = await multiSelect({
753
+ message: 'Select Azure services to scan:',
754
+ options: serviceOptions,
755
+ required: true,
756
+ });
757
+
758
+ return {
759
+ success: true,
760
+ data: { servicesToScan: selectedServices as string[] },
761
+ };
762
+ }
763
+
764
+ /**
765
+ * Poll a discovery session until completion
766
+ */
767
+ async function pollDiscovery(
768
+ client: RestClient,
769
+ startPath: string,
770
+ statusPath: (sessionId: string) => string,
771
+ startPayload: Record<string, unknown>,
772
+ ctx: TerraformWizardContext
773
+ ): Promise<StepResult> {
774
+ try {
775
+ const startResponse = await client.post<{
776
+ sessionId: string;
777
+ status: string;
778
+ }>(startPath, startPayload);
779
+
780
+ if (!startResponse.success || !startResponse.data?.sessionId) {
781
+ return { success: false, error: 'Failed to start discovery' };
782
+ }
783
+
784
+ const sessionId = startResponse.data.sessionId;
785
+ ctx.discoverySessionId = sessionId;
786
+
787
+ // Poll for progress
788
+ let completed = false;
789
+ let lastResourceCount = 0;
790
+
791
+ while (!completed) {
792
+ await new Promise(resolve => setTimeout(resolve, 1000));
793
+
794
+ const statusResponse = await client.get<{
795
+ status: string;
796
+ progress: {
797
+ regionsScanned: number;
798
+ totalRegions: number;
799
+ resourcesFound: number;
800
+ currentRegion?: string;
801
+ currentService?: string;
802
+ };
803
+ inventory?: any;
804
+ }>(statusPath(sessionId));
805
+
806
+ if (!statusResponse.success) {
807
+ continue;
808
+ }
809
+
810
+ const { status, progress, inventory } = statusResponse.data!;
811
+
812
+ // Update progress display
813
+ if (progress.resourcesFound !== lastResourceCount) {
814
+ ui.clearLine();
815
+ ui.write(
816
+ ` Scanning: ${progress.regionsScanned}/${progress.totalRegions} regions | ` +
817
+ `${progress.resourcesFound} resources found`
818
+ );
819
+ if (progress.currentRegion) {
820
+ ui.write(` | Current: ${progress.currentRegion}`);
821
+ }
822
+ lastResourceCount = progress.resourcesFound;
823
+ }
824
+
825
+ if (status === 'completed') {
826
+ completed = true;
827
+ ctx.inventory = inventory;
828
+
829
+ ui.newLine();
830
+ ui.newLine();
831
+ ui.success(`Discovery complete! Found ${progress.resourcesFound} resources`);
832
+
833
+ // Show summary
834
+ if (inventory?.summary) {
835
+ ui.newLine();
836
+ ui.print(' Resources by service:');
837
+ for (const [service, count] of Object.entries(
838
+ inventory.summary.resourcesByService || {}
839
+ )) {
840
+ ui.print(` ${service}: ${count}`);
841
+ }
842
+ }
843
+ } else if (status === 'failed') {
844
+ ui.newLine();
845
+ return { success: false, error: 'Discovery failed' };
846
+ }
847
+ }
848
+
849
+ return {
850
+ success: true,
851
+ data: {
852
+ discoverySessionId: sessionId,
853
+ inventory: ctx.inventory,
854
+ },
855
+ };
856
+ } catch (error: any) {
857
+ return { success: false, error: error.message };
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Step: Discovery
863
+ */
864
+ async function discoveryStep(ctx: TerraformWizardContext): Promise<StepResult> {
865
+ ui.print(' Starting infrastructure discovery...');
866
+ ui.newLine();
867
+
868
+ switch (ctx.provider) {
869
+ case 'gcp':
870
+ return pollDiscovery(
871
+ gcpClient,
872
+ '/api/gcp/discover/start',
873
+ id => `/api/gcp/discover/session/${id}`,
874
+ {
875
+ projectId: ctx.gcpProject,
876
+ regions: ctx.gcpRegions || 'all',
877
+ services: ctx.servicesToScan,
878
+ },
879
+ ctx
880
+ );
881
+
882
+ case 'azure':
883
+ return pollDiscovery(
884
+ azureClient,
885
+ '/api/azure/discover/start',
886
+ id => `/api/azure/discover/session/${id}`,
887
+ {
888
+ subscriptionId: ctx.azureSubscription,
889
+ resourceGroup: ctx.azureResourceGroup,
890
+ services: ctx.servicesToScan,
891
+ },
892
+ ctx
893
+ );
894
+
895
+ case 'aws':
896
+ default:
897
+ return pollDiscovery(
898
+ awsClient,
899
+ '/api/aws/discover',
900
+ id => `/api/aws/discover/${id}`,
901
+ {
902
+ profile: ctx.awsProfile,
903
+ regions: ctx.awsRegions || 'all',
904
+ services: ctx.servicesToScan,
905
+ },
906
+ ctx
907
+ );
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Step 5: Generation Options
913
+ */
914
+ async function generationOptionsStep(_ctx: TerraformWizardContext): Promise<StepResult> {
915
+ // Import method
916
+ const importMethod = await select<'both' | 'blocks' | 'script'>({
917
+ message: 'How should imports be generated?',
918
+ options: [
919
+ {
920
+ value: 'both',
921
+ label: 'Both import blocks and shell script (Recommended)',
922
+ description: 'Maximum compatibility with all Terraform versions',
923
+ },
924
+ {
925
+ value: 'blocks',
926
+ label: 'Import blocks only (Terraform 1.5+)',
927
+ description: 'Modern declarative imports',
928
+ },
929
+ {
930
+ value: 'script',
931
+ label: 'Shell script only',
932
+ description: 'Traditional terraform import commands',
933
+ },
934
+ ],
935
+ defaultValue: 'both',
936
+ });
937
+
938
+ // Starter kit options
939
+ ui.newLine();
940
+ const includeStarterKit = await confirm({
941
+ message: 'Generate starter kit (README, .gitignore, Makefile, CI/CD)?',
942
+ defaultValue: true,
943
+ });
944
+
945
+ return {
946
+ success: true,
947
+ data: {
948
+ importMethod,
949
+ includeReadme: includeStarterKit,
950
+ includeGitignore: includeStarterKit,
951
+ includeMakefile: includeStarterKit,
952
+ includeGithubActions: includeStarterKit,
953
+ },
954
+ };
955
+ }
956
+
957
+ /**
958
+ * Step 6: Output Location
959
+ */
960
+ async function outputLocationStep(ctx: TerraformWizardContext): Promise<StepResult> {
961
+ const outputPath = await pathInput(
962
+ 'Where should the Terraform files be saved?',
963
+ ctx.outputPath || './terraform-infrastructure'
964
+ );
965
+
966
+ if (!outputPath) {
967
+ return { success: false, error: 'Output path is required' };
968
+ }
969
+
970
+ // Ask about saving preferences
971
+ ui.newLine();
972
+ const savePreferences = await confirm({
973
+ message: 'Save your preferences as organization policy for future runs?',
974
+ defaultValue: false,
975
+ });
976
+
977
+ return {
978
+ success: true,
979
+ data: {
980
+ outputPath,
981
+ savePreferences,
982
+ },
983
+ };
984
+ }
985
+
986
+ /**
987
+ * Run in conversational mode (Mode B)
988
+ * Uses the generator service's conversational endpoints to describe infrastructure
989
+ * in natural language and generate Terraform from the conversation.
990
+ */
991
+ async function runConversational(options: GenerateTerraformOptions): Promise<void> {
992
+ const crypto = await import('crypto');
993
+ const fs = await import('fs/promises');
994
+ const pathMod = await import('path');
995
+
996
+ const sessionId = crypto.randomUUID();
997
+
998
+ ui.header('nimbus generate terraform', 'Conversational mode');
999
+ ui.print('Describe your infrastructure in natural language.');
1000
+ ui.print('Type "generate" or "done" when ready to generate Terraform.');
1001
+ ui.print('Type "exit" to quit.');
1002
+ ui.newLine();
1003
+
1004
+ for (;;) {
1005
+ const message = await input({
1006
+ message: 'You:',
1007
+ defaultValue: '',
1008
+ });
1009
+
1010
+ if (!message || message.trim() === '') {
1011
+ continue;
1012
+ }
1013
+
1014
+ const trimmed = message.trim().toLowerCase();
1015
+
1016
+ if (trimmed === 'exit') {
1017
+ ui.info('Exiting conversational mode.');
1018
+ return;
1019
+ }
1020
+
1021
+ // User explicitly wants to generate
1022
+ if (trimmed === 'generate' || trimmed === 'done') {
1023
+ const generated = await generateFromConversation(sessionId, options, fs, pathMod);
1024
+ if (generated) {
1025
+ ui.newLine();
1026
+ ui.print('You can refine the generated Terraform by continuing the conversation.');
1027
+ ui.print('Type "generate" to regenerate, or "exit" to finish.');
1028
+ ui.newLine();
1029
+ continue; // stays in the while(true) loop with same sessionId
1030
+ }
1031
+ return;
1032
+ }
1033
+
1034
+ // Send message to conversational endpoint
1035
+ try {
1036
+ const response = await generatorClient.post<{
1037
+ message: string;
1038
+ suggested_actions?: Array<{ type: string; label?: string }>;
1039
+ }>('/api/conversational/message', { sessionId, message });
1040
+
1041
+ if (response.success && response.data) {
1042
+ const data = response.data as any;
1043
+ // Handle double-unwrap pattern: response.data may contain { data: { message } }
1044
+ const replyMessage = data.data?.message || data.message || 'No response';
1045
+ ui.newLine();
1046
+ ui.print(replyMessage);
1047
+ ui.newLine();
1048
+
1049
+ // Check for generate suggestion
1050
+ const actions = data.data?.suggested_actions || data.suggested_actions || [];
1051
+ const generateAction = actions.find((a: any) => a.type === 'generate');
1052
+ if (generateAction) {
1053
+ const shouldGenerate = await confirm({
1054
+ message: 'Ready to generate Terraform from this conversation?',
1055
+ defaultValue: true,
1056
+ });
1057
+ if (shouldGenerate) {
1058
+ const generated = await generateFromConversation(sessionId, options, fs, pathMod);
1059
+ if (generated) {
1060
+ ui.newLine();
1061
+ ui.print('You can refine the generated Terraform by continuing the conversation.');
1062
+ ui.print('Type "generate" to regenerate, or "exit" to finish.');
1063
+ ui.newLine();
1064
+ continue; // stays in the while(true) loop with same sessionId
1065
+ }
1066
+ return;
1067
+ }
1068
+ }
1069
+ } else {
1070
+ ui.error('Failed to get response from generator service.');
1071
+ }
1072
+ } catch (error: any) {
1073
+ ui.error(`Error: ${error.message}`);
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Generate Terraform files from a conversational session
1080
+ */
1081
+ async function generateFromConversation(
1082
+ sessionId: string,
1083
+ options: GenerateTerraformOptions,
1084
+ fs: typeof import('fs/promises'),
1085
+ pathMod: typeof import('path')
1086
+ ): Promise<boolean> {
1087
+ ui.newLine();
1088
+ ui.startSpinner({ message: 'Generating Terraform from conversation...' });
1089
+
1090
+ try {
1091
+ const genResponse = await generatorClient.post<{
1092
+ files: Array<{ path: string; content: string }>;
1093
+ }>('/api/generate/from-conversation', {
1094
+ sessionId,
1095
+ applyBestPractices: true,
1096
+ });
1097
+
1098
+ if (!genResponse.success || !genResponse.data) {
1099
+ ui.stopSpinnerFail('Generation failed');
1100
+ return false;
1101
+ }
1102
+
1103
+ ui.stopSpinnerSuccess('Terraform code generated');
1104
+
1105
+ // Write files — same pattern as runNonInteractive
1106
+ const data = genResponse.data as any;
1107
+ const files: Array<{ path: string; content: string }> = data.data?.files || data.files || [];
1108
+ const outputDir = options.output || './infrastructure';
1109
+
1110
+ await fs.mkdir(outputDir, { recursive: true });
1111
+
1112
+ for (const file of files) {
1113
+ const filePath = pathMod.join(outputDir, file.path);
1114
+ await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
1115
+ await fs.writeFile(filePath, file.content);
1116
+ }
1117
+
1118
+ // Post-generation validation (Gaps C+D)
1119
+ if (!options.skipValidation && files.length > 0) {
1120
+ await runPostGenerationValidation(files, false);
1121
+ }
1122
+
1123
+ ui.newLine();
1124
+ ui.success(`Generated ${files.length} Terraform file(s) in ${outputDir}`);
1125
+ ui.newLine();
1126
+ ui.print('Generated files:');
1127
+ for (const file of files) {
1128
+ ui.print(` ${ui.color('●', 'green')} ${file.path}`);
1129
+ }
1130
+ ui.newLine();
1131
+ ui.print('Next steps:');
1132
+ ui.print(` 1. Review the generated files in ${outputDir}`);
1133
+ ui.print(' 2. Run "terraform plan" to preview changes');
1134
+ ui.print(' 3. Run "terraform apply" to create infrastructure');
1135
+ return true;
1136
+ } catch (error: any) {
1137
+ ui.stopSpinnerFail('Generation failed');
1138
+ ui.error(`Failed to generate Terraform: ${error.message}`);
1139
+ return false;
1140
+ }
1141
+ }
1142
+
1143
+ /**
1144
+ * Run in non-interactive mode
1145
+ */
1146
+ async function runNonInteractive(options: GenerateTerraformOptions): Promise<void> {
1147
+ ui.header('nimbus generate terraform', 'Non-interactive mode');
1148
+
1149
+ const provider = options.provider || 'aws';
1150
+
1151
+ // Validate required flags per provider
1152
+ if (provider === 'aws' && !options.profile) {
1153
+ ui.error('AWS profile is required in non-interactive mode (--profile)');
1154
+ process.exit(1);
1155
+ }
1156
+ if (provider === 'gcp' && !options.gcpProject) {
1157
+ ui.error('GCP project is required in non-interactive mode (--gcp-project)');
1158
+ process.exit(1);
1159
+ }
1160
+ if (provider === 'azure' && !options.azureSubscription) {
1161
+ ui.error('Azure subscription is required in non-interactive mode (--azure-subscription)');
1162
+ process.exit(1);
1163
+ }
1164
+
1165
+ ui.info(`Provider: ${provider}`);
1166
+ if (provider === 'aws') {
1167
+ ui.info(`Profile: ${options.profile}`);
1168
+ } else if (provider === 'gcp') {
1169
+ ui.info(`Project: ${options.gcpProject}`);
1170
+ } else if (provider === 'azure') {
1171
+ ui.info(`Subscription: ${options.azureSubscription}`);
1172
+ }
1173
+ ui.info(`Regions: ${options.regions?.join(', ') || 'all'}`);
1174
+ ui.info(`Services: ${options.services?.join(', ') || 'all'}`);
1175
+ ui.info(`Output: ${options.output || './terraform-infrastructure'}`);
1176
+ ui.newLine();
1177
+
1178
+ // Build discovery context
1179
+ const ctx: TerraformWizardContext = {
1180
+ provider,
1181
+ awsProfile: options.profile,
1182
+ awsRegions: options.regions,
1183
+ gcpProject: options.gcpProject,
1184
+ azureSubscription: options.azureSubscription,
1185
+ servicesToScan: options.services,
1186
+ outputPath: options.output || './terraform-infrastructure',
1187
+ };
1188
+
1189
+ // Run discovery using the pollDiscovery helper (already implemented for Gap 1)
1190
+ ui.info('Starting infrastructure discovery...');
1191
+ ui.newLine();
1192
+
1193
+ let discoveryResult: StepResult;
1194
+
1195
+ switch (provider) {
1196
+ case 'gcp':
1197
+ discoveryResult = await pollDiscovery(
1198
+ gcpClient,
1199
+ '/api/gcp/discover/start',
1200
+ id => `/api/gcp/discover/session/${id}`,
1201
+ {
1202
+ projectId: ctx.gcpProject,
1203
+ regions: ctx.awsRegions || 'all',
1204
+ services: ctx.servicesToScan,
1205
+ },
1206
+ ctx
1207
+ );
1208
+ break;
1209
+
1210
+ case 'azure':
1211
+ discoveryResult = await pollDiscovery(
1212
+ azureClient,
1213
+ '/api/azure/discover/start',
1214
+ id => `/api/azure/discover/session/${id}`,
1215
+ {
1216
+ subscriptionId: ctx.azureSubscription,
1217
+ services: ctx.servicesToScan,
1218
+ },
1219
+ ctx
1220
+ );
1221
+ break;
1222
+
1223
+ case 'aws':
1224
+ default:
1225
+ discoveryResult = await pollDiscovery(
1226
+ awsClient,
1227
+ '/api/aws/discover',
1228
+ id => `/api/aws/discover/${id}`,
1229
+ {
1230
+ profile: ctx.awsProfile,
1231
+ regions: ctx.awsRegions || 'all',
1232
+ services: ctx.servicesToScan,
1233
+ },
1234
+ ctx
1235
+ );
1236
+ break;
1237
+ }
1238
+
1239
+ if (!discoveryResult.success) {
1240
+ ui.error(`Discovery failed: ${discoveryResult.error || 'Unknown error'}`);
1241
+ process.exit(1);
1242
+ }
1243
+
1244
+ // Generate Terraform from discovered inventory
1245
+ ui.newLine();
1246
+ ui.startSpinner({ message: 'Generating Terraform code...' });
1247
+
1248
+ try {
1249
+ const genResponse = await generatorClient.post<{
1250
+ files: Array<{ path: string; content: string }>;
1251
+ validation?: any;
1252
+ }>('/api/generators/terraform/project', {
1253
+ projectName: 'infrastructure',
1254
+ provider,
1255
+ region: options.regions?.[0],
1256
+ components: options.services,
1257
+ inventory: ctx.inventory,
1258
+ });
1259
+
1260
+ if (!genResponse.success || !genResponse.data) {
1261
+ ui.stopSpinnerFail('Generation failed');
1262
+ ui.error(genResponse.error?.message || 'Failed to generate Terraform code');
1263
+ process.exit(1);
1264
+ }
1265
+
1266
+ ui.stopSpinnerSuccess('Terraform code generated');
1267
+
1268
+ // Write generated files
1269
+ const outputDir = options.output || './terraform-infrastructure';
1270
+ const fs = await import('fs/promises');
1271
+ const path = await import('path');
1272
+
1273
+ await fs.mkdir(outputDir, { recursive: true });
1274
+
1275
+ const files = genResponse.data.files || [];
1276
+ for (const file of files) {
1277
+ const filePath = path.join(outputDir, file.path);
1278
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1279
+ await fs.writeFile(filePath, file.content);
1280
+ }
1281
+
1282
+ // --- Post-generation validation (Gaps C+D) ---
1283
+ let validationResults: Record<string, unknown> | undefined;
1284
+ if (!options.skipValidation && files.length > 0) {
1285
+ validationResults = await runPostGenerationValidation(files, options.jsonOutput);
1286
+ }
1287
+
1288
+ if (options.jsonOutput) {
1289
+ // JSON output mode
1290
+ const summary = {
1291
+ success: true,
1292
+ provider,
1293
+ outputDir,
1294
+ filesGenerated: files.map(f => f.path),
1295
+ resourcesDiscovered: ctx.inventory?.summary?.totalResources || 0,
1296
+ validation: genResponse.data.validation,
1297
+ postGenerationValidation: validationResults,
1298
+ };
1299
+ console.log(JSON.stringify(summary, null, 2));
1300
+ } else {
1301
+ // Human-readable output
1302
+ ui.newLine();
1303
+ ui.success(`Generated ${files.length} Terraform file(s) in ${outputDir}`);
1304
+ ui.newLine();
1305
+ ui.print('Generated files:');
1306
+ for (const file of files) {
1307
+ ui.print(` ${ui.color('●', 'green')} ${file.path}`);
1308
+ }
1309
+ ui.newLine();
1310
+ ui.print('Next steps:');
1311
+ ui.print(` 1. Review the generated files in ${outputDir}`);
1312
+ ui.print(' 2. Run "terraform plan" to see what will be imported');
1313
+ ui.print(' 3. Run "terraform apply" to bring resources under Terraform control');
1314
+ }
1315
+ } catch (error: any) {
1316
+ ui.stopSpinnerFail('Generation failed');
1317
+ ui.error(`Failed to generate Terraform: ${error.message}`);
1318
+ process.exit(1);
1319
+ }
1320
+ }
1321
+
1322
+ /**
1323
+ * Run post-generation validation by calling the generator service's
1324
+ * existing validation endpoint with the generated files.
1325
+ *
1326
+ * Non-blocking: if the validation service call fails, a warning is shown
1327
+ * and the function returns undefined so the caller can continue normally.
1328
+ */
1329
+ async function runPostGenerationValidation(
1330
+ files: Array<{ path: string; content: string }>,
1331
+ jsonOutput?: boolean
1332
+ ): Promise<Record<string, unknown> | undefined> {
1333
+ try {
1334
+ if (!jsonOutput) {
1335
+ ui.newLine();
1336
+ ui.startSpinner({ message: 'Running post-generation validation...' });
1337
+ }
1338
+
1339
+ const validateResponse = await generatorClient.post<{
1340
+ valid: boolean;
1341
+ items: Array<{ severity: string; message: string; file?: string; rule?: string }>;
1342
+ summary: { errors: number; warnings: number; info: number };
1343
+ }>('/api/generators/terraform/validate', { files });
1344
+
1345
+ if (!validateResponse.success || !validateResponse.data) {
1346
+ if (!jsonOutput) {
1347
+ ui.stopSpinnerFail('Validation service unavailable');
1348
+ ui.warning('Skipping validation — generator service did not respond.');
1349
+ }
1350
+ return undefined;
1351
+ }
1352
+
1353
+ const report = validateResponse.data as any;
1354
+ const data = report.data || report;
1355
+
1356
+ if (!jsonOutput) {
1357
+ ui.stopSpinnerSuccess('Validation complete');
1358
+ ui.newLine();
1359
+ displayValidationReport(data);
1360
+ }
1361
+
1362
+ return data;
1363
+ } catch (error: any) {
1364
+ if (!jsonOutput) {
1365
+ ui.stopSpinnerFail('Validation failed');
1366
+ ui.warning(`Post-generation validation could not run: ${error.message}`);
1367
+ ui.warning('You can run validation manually with: nimbus validate terraform');
1368
+ }
1369
+ return undefined;
1370
+ }
1371
+ }
1372
+
1373
+ /**
1374
+ * Display a human-readable validation report.
1375
+ * Shows results for terraform fmt, terraform validate, tflint, and checkov.
1376
+ * Tools that are not installed show as "not installed" gracefully.
1377
+ */
1378
+ function displayValidationReport(report: any): void {
1379
+ const items: Array<{ severity: string; message: string; file?: string; rule?: string }> =
1380
+ report.items || [];
1381
+ const summary = report.summary || { errors: 0, warnings: 0, info: 0 };
1382
+
1383
+ // Overall status
1384
+ const isValid = report.valid !== false && summary.errors === 0;
1385
+ if (isValid) {
1386
+ ui.print(` ${ui.color('\u2713', 'green')} Validation passed`);
1387
+ } else {
1388
+ ui.print(` ${ui.color('\u2717', 'red')} Validation found issues`);
1389
+ }
1390
+
1391
+ // Summary line
1392
+ const parts: string[] = [];
1393
+ if (summary.errors > 0) {
1394
+ parts.push(ui.color(`${summary.errors} error(s)`, 'red'));
1395
+ }
1396
+ if (summary.warnings > 0) {
1397
+ parts.push(ui.color(`${summary.warnings} warning(s)`, 'yellow'));
1398
+ }
1399
+ if (summary.info > 0) {
1400
+ parts.push(ui.dim(`${summary.info} info`));
1401
+ }
1402
+ if (parts.length > 0) {
1403
+ ui.print(` Summary: ${parts.join(', ')}`);
1404
+ }
1405
+
1406
+ // Tool-level results (grouped by rule prefix)
1407
+ const toolStatus: Record<string, 'pass' | 'fail' | 'not-installed'> = {
1408
+ 'terraform-fmt': 'pass',
1409
+ 'terraform-validate': 'pass',
1410
+ tflint: 'pass',
1411
+ checkov: 'pass',
1412
+ };
1413
+
1414
+ for (const item of items) {
1415
+ if (item.severity === 'error' || item.severity === 'warning') {
1416
+ const rule = item.rule || '';
1417
+ if (rule.startsWith('fmt') || rule.includes('format')) {
1418
+ toolStatus['terraform-fmt'] = 'fail';
1419
+ } else if (rule.startsWith('hcl') || rule.includes('syntax')) {
1420
+ toolStatus['terraform-validate'] = 'fail';
1421
+ } else if (rule.startsWith('require-') || rule.includes('anti-pattern')) {
1422
+ toolStatus['tflint'] = 'fail';
1423
+ } else if (rule.startsWith('checkov') || rule.includes('security')) {
1424
+ toolStatus['checkov'] = 'fail';
1425
+ }
1426
+ }
1427
+ }
1428
+
1429
+ ui.newLine();
1430
+ ui.print(' Tool Results:');
1431
+ for (const [tool, status] of Object.entries(toolStatus)) {
1432
+ const icon =
1433
+ status === 'pass'
1434
+ ? ui.color('\u2713', 'green')
1435
+ : status === 'fail'
1436
+ ? ui.color('\u2717', 'red')
1437
+ : ui.dim('-');
1438
+ const label = status === 'not-installed' ? ui.dim('not installed') : status;
1439
+ ui.print(` ${icon} ${tool}: ${label}`);
1440
+ }
1441
+
1442
+ // Show first 5 error/warning details
1443
+ const significant = items.filter(i => i.severity === 'error' || i.severity === 'warning');
1444
+ if (significant.length > 0) {
1445
+ ui.newLine();
1446
+ ui.print(' Details:');
1447
+ const toShow = significant.slice(0, 5);
1448
+ for (const item of toShow) {
1449
+ const sevIcon = item.severity === 'error' ? ui.color('E', 'red') : ui.color('W', 'yellow');
1450
+ const fileInfo = item.file ? ` (${item.file})` : '';
1451
+ ui.print(` [${sevIcon}] ${item.message}${fileInfo}`);
1452
+ }
1453
+ if (significant.length > 5) {
1454
+ ui.print(ui.dim(` ... and ${significant.length - 5} more`));
1455
+ }
1456
+ }
1457
+ }
1458
+
1459
+ // Export as default command
1460
+ export default generateTerraformCommand;