@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.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- 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;
|