@build-astron-co/nimbus 0.2.0 → 0.4.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/bin/nimbus +26 -10
- package/bin/nimbus.cmd +41 -0
- package/bin/nimbus.mjs +70 -0
- package/completions/nimbus.bash +38 -0
- package/completions/nimbus.fish +48 -0
- package/completions/nimbus.zsh +81 -0
- package/dist/src/agent/compaction-agent.js +215 -0
- package/dist/src/agent/context-manager.js +385 -0
- package/dist/src/agent/context.js +322 -0
- package/dist/src/agent/deploy-preview.js +395 -0
- package/dist/src/agent/expand-files.js +95 -0
- package/dist/src/agent/index.js +18 -0
- package/dist/src/agent/loop.js +1535 -0
- package/dist/src/agent/modes.js +347 -0
- package/dist/src/agent/permissions.js +396 -0
- package/dist/src/agent/subagents/base.js +67 -0
- package/dist/src/agent/subagents/cost.js +45 -0
- package/dist/src/agent/subagents/explore.js +36 -0
- package/dist/src/agent/subagents/general.js +41 -0
- package/dist/src/agent/subagents/index.js +88 -0
- package/dist/src/agent/subagents/infra.js +52 -0
- package/dist/src/agent/subagents/security.js +60 -0
- package/dist/src/agent/system-prompt.js +860 -0
- package/dist/src/app.js +152 -0
- package/dist/src/audit/activity-log.js +209 -0
- package/dist/src/audit/compliance-checker.js +419 -0
- package/dist/src/audit/cost-tracker.js +231 -0
- package/dist/src/audit/index.js +10 -0
- package/dist/src/audit/security-scanner.js +490 -0
- package/dist/src/auth/guard.js +64 -0
- package/dist/src/auth/index.js +19 -0
- package/dist/src/auth/keychain.js +79 -0
- package/dist/src/auth/oauth.js +389 -0
- package/dist/src/auth/providers.js +415 -0
- package/dist/src/auth/sso.js +87 -0
- package/dist/src/auth/store.js +424 -0
- package/dist/src/auth/types.js +5 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/init.js +1048 -0
- package/dist/src/cli/openapi-spec.js +346 -0
- package/dist/src/cli/run.js +505 -0
- package/dist/src/cli/serve-auth.js +56 -0
- package/dist/src/cli/serve.js +432 -0
- package/dist/src/cli/web.js +50 -0
- package/dist/src/cli.js +1574 -0
- package/dist/src/clients/core-engine-client.js +156 -0
- package/dist/src/clients/enterprise-client.js +246 -0
- package/dist/src/clients/generator-client.js +219 -0
- package/dist/src/clients/git-client.js +367 -0
- package/dist/src/clients/github-client.js +229 -0
- package/dist/src/clients/helm-client.js +299 -0
- package/dist/src/clients/index.js +18 -0
- package/dist/src/clients/k8s-client.js +270 -0
- package/dist/src/clients/llm-client.js +119 -0
- package/dist/src/clients/rest-client.js +104 -0
- package/dist/src/clients/service-discovery.js +35 -0
- package/dist/src/clients/terraform-client.js +302 -0
- package/dist/src/clients/tools-client.js +1227 -0
- package/dist/src/clients/ws-client.js +93 -0
- package/dist/src/commands/alias.js +91 -0
- package/dist/src/commands/analyze/index.js +313 -0
- package/dist/src/commands/apply/helm.js +375 -0
- package/dist/src/commands/apply/index.js +176 -0
- package/dist/src/commands/apply/k8s.js +350 -0
- package/dist/src/commands/apply/terraform.js +465 -0
- package/dist/src/commands/ask.js +137 -0
- package/dist/src/commands/audit/index.js +322 -0
- package/dist/src/commands/auth-cloud.js +345 -0
- package/dist/src/commands/auth-list.js +112 -0
- package/dist/src/commands/auth-profile.js +104 -0
- package/dist/src/commands/auth-refresh.js +161 -0
- package/dist/src/commands/auth-status.js +122 -0
- package/dist/src/commands/aws/ec2.js +402 -0
- package/dist/src/commands/aws/iam.js +304 -0
- package/dist/src/commands/aws/index.js +108 -0
- package/dist/src/commands/aws/lambda.js +317 -0
- package/dist/src/commands/aws/rds.js +345 -0
- package/dist/src/commands/aws/s3.js +346 -0
- package/dist/src/commands/aws/vpc.js +302 -0
- package/dist/src/commands/aws-discover.js +413 -0
- package/dist/src/commands/aws-terraform.js +618 -0
- package/dist/src/commands/azure/aks.js +305 -0
- package/dist/src/commands/azure/functions.js +200 -0
- package/dist/src/commands/azure/index.js +93 -0
- package/dist/src/commands/azure/storage.js +378 -0
- package/dist/src/commands/azure/vm.js +291 -0
- package/dist/src/commands/billing/index.js +224 -0
- package/dist/src/commands/chat.js +259 -0
- package/dist/src/commands/completions.js +255 -0
- package/dist/src/commands/config.js +291 -0
- package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
- package/dist/src/commands/cost/estimator.js +73 -0
- package/dist/src/commands/cost/index.js +625 -0
- package/dist/src/commands/cost/parsers/terraform.js +234 -0
- package/dist/src/commands/cost/parsers/types.js +4 -0
- package/dist/src/commands/cost/pricing/aws.js +501 -0
- package/dist/src/commands/cost/pricing/azure.js +462 -0
- package/dist/src/commands/cost/pricing/gcp.js +359 -0
- package/dist/src/commands/cost/pricing/index.js +24 -0
- package/dist/src/commands/demo.js +196 -0
- package/dist/src/commands/deploy.js +215 -0
- package/dist/src/commands/doctor.js +1291 -0
- package/dist/src/commands/drift/index.js +674 -0
- package/dist/src/commands/explain.js +235 -0
- package/dist/src/commands/export.js +120 -0
- package/dist/src/commands/feedback.js +319 -0
- package/dist/src/commands/fix.js +263 -0
- package/dist/src/commands/fs/index.js +338 -0
- package/dist/src/commands/gcp/compute.js +266 -0
- package/dist/src/commands/gcp/functions.js +221 -0
- package/dist/src/commands/gcp/gke.js +357 -0
- package/dist/src/commands/gcp/iam.js +295 -0
- package/dist/src/commands/gcp/index.js +105 -0
- package/dist/src/commands/gcp/storage.js +232 -0
- package/dist/src/commands/generate-helm.js +1026 -0
- package/dist/src/commands/generate-k8s.js +1263 -0
- package/dist/src/commands/generate-terraform.js +1058 -0
- package/dist/src/commands/gh/index.js +663 -0
- package/dist/src/commands/git/index.js +1208 -0
- package/dist/src/commands/helm/index.js +985 -0
- package/dist/src/commands/help.js +639 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/src/commands/import.js +782 -0
- package/dist/src/commands/incident.js +144 -0
- package/dist/src/commands/index.js +109 -0
- package/dist/src/commands/init.js +955 -0
- package/dist/src/commands/k8s/index.js +979 -0
- package/dist/src/commands/login.js +588 -0
- package/dist/src/commands/logout.js +61 -0
- package/dist/src/commands/logs.js +160 -0
- package/dist/src/commands/onboarding.js +382 -0
- package/dist/src/commands/pipeline.js +153 -0
- package/dist/src/commands/plan/display.js +216 -0
- package/dist/src/commands/plan/index.js +525 -0
- package/dist/src/commands/plugin.js +325 -0
- package/dist/src/commands/preview.js +356 -0
- package/dist/src/commands/profile.js +297 -0
- package/dist/src/commands/questionnaire.js +1021 -0
- package/dist/src/commands/resume.js +35 -0
- package/dist/src/commands/rollback.js +259 -0
- package/dist/src/commands/rollout.js +74 -0
- package/dist/src/commands/runbook.js +307 -0
- package/dist/src/commands/schedule.js +202 -0
- package/dist/src/commands/status.js +213 -0
- package/dist/src/commands/team/index.js +309 -0
- package/dist/src/commands/team-context.js +200 -0
- package/dist/src/commands/template.js +204 -0
- package/dist/src/commands/tf/index.js +989 -0
- package/dist/src/commands/upgrade.js +515 -0
- package/dist/src/commands/usage/index.js +118 -0
- package/dist/src/commands/version.js +145 -0
- package/dist/src/commands/watch.js +127 -0
- package/dist/src/compat/index.js +2 -0
- package/dist/src/compat/runtime.js +10 -0
- package/dist/src/compat/sqlite.js +144 -0
- package/dist/src/config/index.js +6 -0
- package/dist/src/config/manager.js +469 -0
- package/dist/src/config/mode-store.js +57 -0
- package/dist/src/config/profiles.js +66 -0
- package/dist/src/config/safety-policy.js +251 -0
- package/dist/src/config/schema.js +107 -0
- package/dist/src/config/types.js +311 -0
- package/dist/src/config/workspace-state.js +38 -0
- package/dist/src/context/context-db.js +138 -0
- package/dist/src/demo/index.js +295 -0
- package/dist/src/demo/scenarios/full-journey.js +226 -0
- package/dist/src/demo/scenarios/getting-started.js +124 -0
- package/dist/src/demo/scenarios/helm-release.js +334 -0
- package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
- package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
- package/dist/src/demo/types.js +6 -0
- package/dist/src/engine/cost-estimator.js +334 -0
- package/dist/src/engine/diagram-generator.js +192 -0
- package/dist/src/engine/drift-detector.js +688 -0
- package/dist/src/engine/executor.js +832 -0
- package/dist/src/engine/index.js +39 -0
- package/dist/src/engine/orchestrator.js +436 -0
- package/dist/src/engine/planner.js +616 -0
- package/dist/src/engine/safety.js +609 -0
- package/dist/src/engine/verifier.js +664 -0
- package/dist/src/enterprise/audit.js +241 -0
- package/dist/src/enterprise/auth.js +189 -0
- package/dist/src/enterprise/billing.js +512 -0
- package/dist/src/enterprise/index.js +16 -0
- package/dist/src/enterprise/teams.js +315 -0
- package/dist/src/generator/best-practices.js +1375 -0
- package/dist/src/generator/helm.js +495 -0
- package/dist/src/generator/index.js +11 -0
- package/dist/src/generator/intent-parser.js +420 -0
- package/dist/src/generator/kubernetes.js +773 -0
- package/dist/src/generator/terraform.js +1472 -0
- package/dist/src/history/index.js +6 -0
- package/dist/src/history/manager.js +199 -0
- package/dist/src/history/types.js +6 -0
- package/dist/src/hooks/config.js +318 -0
- package/dist/src/hooks/engine.js +317 -0
- package/dist/src/hooks/index.js +2 -0
- package/dist/src/llm/auth-bridge.js +157 -0
- package/dist/src/llm/circuit-breaker.js +116 -0
- package/dist/src/llm/config-loader.js +172 -0
- package/dist/src/llm/cost-calculator.js +137 -0
- package/dist/src/llm/index.js +7 -0
- package/dist/src/llm/model-aliases.js +99 -0
- package/dist/src/llm/provider-registry.js +57 -0
- package/dist/src/llm/providers/anthropic.js +430 -0
- package/dist/src/llm/providers/bedrock.js +409 -0
- package/dist/src/llm/providers/google.js +344 -0
- package/dist/src/llm/providers/ollama.js +661 -0
- package/dist/src/llm/providers/openai-compatible.js +289 -0
- package/dist/src/llm/providers/openai.js +284 -0
- package/dist/src/llm/providers/openrouter.js +293 -0
- package/dist/src/llm/router.js +844 -0
- package/dist/src/llm/types.js +69 -0
- package/dist/src/lsp/client.js +239 -0
- package/dist/src/lsp/languages.js +95 -0
- package/dist/src/lsp/manager.js +243 -0
- package/dist/src/mcp/client.js +289 -0
- package/dist/src/mcp/index.js +5 -0
- package/dist/src/mcp/manager.js +113 -0
- package/dist/src/nimbus.js +212 -0
- package/dist/src/plugins/index.js +13 -0
- package/dist/src/plugins/loader.js +280 -0
- package/dist/src/plugins/manager.js +282 -0
- package/dist/src/plugins/types.js +23 -0
- package/dist/src/scanners/cicd-scanner.js +230 -0
- package/dist/src/scanners/cloud-scanner.js +415 -0
- package/dist/src/scanners/framework-scanner.js +430 -0
- package/dist/src/scanners/iac-scanner.js +350 -0
- package/dist/src/scanners/index.js +454 -0
- package/dist/src/scanners/language-scanner.js +258 -0
- package/dist/src/scanners/package-manager-scanner.js +252 -0
- package/dist/src/scanners/types.js +6 -0
- package/dist/src/sessions/manager.js +395 -0
- package/dist/src/sessions/types.js +4 -0
- package/dist/src/sharing/sync.js +238 -0
- package/dist/src/sharing/viewer.js +131 -0
- package/dist/src/snapshots/index.js +1 -0
- package/dist/src/snapshots/manager.js +432 -0
- package/dist/src/state/artifacts.js +94 -0
- package/dist/src/state/audit.js +73 -0
- package/dist/src/state/billing.js +126 -0
- package/dist/src/state/checkpoints.js +81 -0
- package/dist/src/state/config.js +58 -0
- package/dist/src/state/conversations.js +7 -0
- package/dist/src/state/credentials.js +96 -0
- package/dist/src/state/db.js +53 -0
- package/dist/src/state/index.js +23 -0
- package/dist/src/state/messages.js +76 -0
- package/dist/src/state/projects.js +92 -0
- package/dist/src/state/schema.js +233 -0
- package/dist/src/state/sessions.js +79 -0
- package/dist/src/state/teams.js +131 -0
- package/dist/src/telemetry.js +91 -0
- package/dist/src/tools/aws-ops.js +747 -0
- package/dist/src/tools/azure-ops.js +491 -0
- package/dist/src/tools/file-ops.js +451 -0
- package/dist/src/tools/gcp-ops.js +559 -0
- package/dist/src/tools/git-ops.js +557 -0
- package/dist/src/tools/github-ops.js +460 -0
- package/dist/src/tools/helm-ops.js +634 -0
- package/dist/src/tools/index.js +16 -0
- package/dist/src/tools/k8s-ops.js +579 -0
- package/dist/src/tools/schemas/converter.js +129 -0
- package/dist/src/tools/schemas/devops.js +3319 -0
- package/dist/src/tools/schemas/index.js +19 -0
- package/dist/src/tools/schemas/standard.js +966 -0
- package/dist/src/tools/schemas/types.js +409 -0
- package/dist/src/tools/spawn-exec.js +109 -0
- package/dist/src/tools/terraform-ops.js +627 -0
- package/dist/src/types/config.js +1 -0
- package/dist/src/types/drift.js +4 -0
- package/dist/src/types/enterprise.js +5 -0
- package/dist/src/types/index.js +14 -0
- package/dist/src/types/plan.js +1 -0
- package/dist/src/types/request.js +1 -0
- package/dist/src/types/response.js +1 -0
- package/dist/src/types/service.js +1 -0
- package/dist/src/ui/App.js +1672 -0
- package/dist/src/ui/DeployPreview.js +60 -0
- package/dist/src/ui/FileDiffModal.js +108 -0
- package/dist/src/ui/Header.js +46 -0
- package/dist/src/ui/HelpModal.js +9 -0
- package/dist/src/ui/InputBox.js +408 -0
- package/dist/src/ui/MessageList.js +795 -0
- package/dist/src/ui/PermissionPrompt.js +72 -0
- package/dist/src/ui/StatusBar.js +109 -0
- package/dist/src/ui/TerminalPane.js +31 -0
- package/dist/src/ui/ToolCallDisplay.js +303 -0
- package/dist/src/ui/TreePane.js +83 -0
- package/dist/src/ui/chat-ui.js +721 -0
- package/dist/src/ui/index.js +11 -0
- package/dist/src/ui/ink/index.js +1325 -0
- package/dist/src/ui/streaming.js +137 -0
- package/dist/src/ui/theme.js +78 -0
- package/dist/src/ui/types.js +7 -0
- package/dist/src/utils/analytics.js +61 -0
- package/dist/src/utils/cost-warning.js +25 -0
- package/dist/src/utils/env.js +42 -0
- package/dist/src/utils/errors.js +54 -0
- package/dist/src/utils/event-bus.js +22 -0
- package/dist/src/utils/index.js +16 -0
- package/dist/src/utils/logger.js +150 -0
- package/dist/src/utils/rate-limiter.js +90 -0
- package/dist/src/utils/service-auth.js +36 -0
- package/dist/src/utils/validation.js +39 -0
- package/dist/src/version.js +3 -0
- package/dist/src/watcher/index.js +192 -0
- package/dist/src/wizard/approval.js +275 -0
- package/dist/src/wizard/index.js +13 -0
- package/dist/src/wizard/prompts.js +273 -0
- package/dist/src/wizard/types.js +4 -0
- package/dist/src/wizard/ui.js +453 -0
- package/dist/src/wizard/wizard.js +227 -0
- package/package.json +31 -23
- package/src/__tests__/alias.test.ts +133 -0
- package/src/__tests__/app.test.ts +1 -1
- package/src/__tests__/audit.test.ts +1 -1
- package/src/__tests__/circuit-breaker.test.ts +1 -1
- package/src/__tests__/cli-run.test.ts +237 -1
- package/src/__tests__/compat-sqlite.test.ts +68 -0
- package/src/__tests__/context-manager.test.ts +131 -1
- package/src/__tests__/context.test.ts +1 -1
- package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
- package/src/__tests__/doctor.test.ts +48 -0
- package/src/__tests__/enterprise.test.ts +1 -1
- package/src/__tests__/export.test.ts +236 -0
- package/src/__tests__/gap-11-18-20.test.ts +958 -0
- package/src/__tests__/generator.test.ts +1 -1
- package/src/__tests__/helm-streaming.test.ts +127 -0
- package/src/__tests__/hooks.test.ts +1 -1
- package/src/__tests__/incident.test.ts +179 -0
- package/src/__tests__/init.test.ts +55 -4
- package/src/__tests__/intent-parser.test.ts +1 -1
- package/src/__tests__/llm-router.test.ts +1 -1
- package/src/__tests__/logs.test.ts +107 -0
- package/src/__tests__/loop-errors.test.ts +244 -0
- package/src/__tests__/lsp.test.ts +1 -1
- package/src/__tests__/modes.test.ts +1 -1
- package/src/__tests__/perf-optimizations.test.ts +847 -0
- package/src/__tests__/permissions.test.ts +1 -1
- package/src/__tests__/pipeline.test.ts +50 -0
- package/src/__tests__/polish-phase3.test.ts +340 -0
- package/src/__tests__/profile.test.ts +237 -0
- package/src/__tests__/rollback.test.ts +83 -0
- package/src/__tests__/runbook.test.ts +219 -0
- package/src/__tests__/schedule.test.ts +206 -0
- package/src/__tests__/serve.test.ts +1 -1
- package/src/__tests__/sessions.test.ts +96 -1
- package/src/__tests__/sharing.test.ts +53 -1
- package/src/__tests__/snapshots.test.ts +1 -1
- package/src/__tests__/standalone-migration.test.ts +199 -0
- package/src/__tests__/state-db.test.ts +1 -1
- package/src/__tests__/status.test.ts +158 -0
- package/src/__tests__/stream-with-tools.test.ts +71 -25
- package/src/__tests__/subagents.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +82 -3
- package/src/__tests__/terminal-gap-v2.test.ts +395 -0
- package/src/__tests__/terminal-parity.test.ts +393 -0
- package/src/__tests__/tf-apply.test.ts +187 -0
- package/src/__tests__/tool-converter.test.ts +1 -1
- package/src/__tests__/tool-schemas.test.ts +209 -4
- package/src/__tests__/tools.test.ts +4 -3
- package/src/__tests__/version-json.test.ts +184 -0
- package/src/__tests__/version.test.ts +1 -1
- package/src/__tests__/watch.test.ts +129 -0
- package/src/agent/compaction-agent.ts +40 -1
- package/src/agent/context-manager.ts +67 -3
- package/src/agent/deploy-preview.ts +62 -1
- package/src/agent/expand-files.ts +108 -0
- package/src/agent/loop.ts +1312 -31
- package/src/agent/permissions.ts +51 -4
- package/src/agent/system-prompt.ts +573 -19
- package/src/app.ts +58 -0
- package/src/audit/security-scanner.ts +45 -0
- package/src/auth/keychain.ts +82 -0
- package/src/auth/oauth.ts +15 -5
- package/src/cli/init.ts +378 -5
- package/src/cli/run.ts +407 -16
- package/src/cli/serve.ts +78 -1
- package/src/cli/web.ts +10 -6
- package/src/cli.ts +312 -1
- package/src/clients/service-discovery.ts +30 -25
- package/src/commands/alias.ts +100 -0
- package/src/commands/audit/index.ts +121 -2
- package/src/commands/auth-cloud.ts +113 -0
- package/src/commands/auth-refresh.ts +187 -0
- package/src/commands/aws-discover.ts +144 -251
- package/src/commands/aws-terraform.ts +68 -118
- package/src/commands/chat.ts +9 -3
- package/src/commands/completions.ts +268 -0
- package/src/commands/config.ts +26 -0
- package/src/commands/cost/index.ts +218 -2
- package/src/commands/deploy.ts +260 -0
- package/src/commands/doctor.ts +744 -152
- package/src/commands/drift/index.ts +371 -23
- package/src/commands/export.ts +146 -0
- package/src/commands/generate-k8s.ts +9 -61
- package/src/commands/generate-terraform.ts +191 -449
- package/src/commands/help.ts +212 -36
- package/src/commands/history.ts +8 -1
- package/src/commands/incident.ts +166 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/login.ts +86 -1
- package/src/commands/logs.ts +167 -0
- package/src/commands/onboarding.ts +211 -34
- package/src/commands/pipeline.ts +186 -0
- package/src/commands/plugin.ts +398 -0
- package/src/commands/profile.ts +342 -0
- package/src/commands/questionnaire.ts +0 -98
- package/src/commands/resume.ts +26 -34
- package/src/commands/rollback.ts +315 -0
- package/src/commands/rollout.ts +88 -0
- package/src/commands/runbook.ts +346 -0
- package/src/commands/schedule.ts +236 -0
- package/src/commands/status.ts +252 -0
- package/src/commands/team-context.ts +220 -0
- package/src/commands/template.ts +58 -57
- package/src/commands/tf/index.ts +70 -11
- package/src/commands/upgrade.ts +57 -0
- package/src/commands/version.ts +54 -50
- package/src/commands/watch.ts +153 -0
- package/src/compat/runtime.ts +1 -1
- package/src/compat/sqlite.ts +75 -5
- package/src/config/mode-store.ts +62 -0
- package/src/config/profiles.ts +84 -0
- package/src/config/types.ts +83 -1
- package/src/config/workspace-state.ts +53 -0
- package/src/engine/cost-estimator.ts +52 -10
- package/src/engine/executor.ts +33 -2
- package/src/engine/planner.ts +68 -1
- package/src/generator/terraform.ts +8 -0
- package/src/history/manager.ts +2 -74
- package/src/hooks/engine.ts +5 -4
- package/src/llm/cost-calculator.ts +2 -2
- package/src/llm/providers/anthropic.ts +50 -21
- package/src/llm/router.ts +76 -7
- package/src/lsp/languages.ts +3 -0
- package/src/lsp/manager.ts +21 -5
- package/src/nimbus.ts +37 -18
- package/src/sessions/manager.ts +108 -1
- package/src/sharing/sync.ts +4 -0
- package/src/sharing/viewer.ts +66 -0
- package/src/tools/file-ops.ts +22 -0
- package/src/tools/schemas/devops.ts +3007 -117
- package/src/tools/schemas/standard.ts +5 -1
- package/src/tools/schemas/types.ts +31 -1
- package/src/tools/spawn-exec.ts +148 -0
- package/src/ui/App.tsx +1183 -66
- package/src/ui/DeployPreview.tsx +62 -57
- package/src/ui/FileDiffModal.tsx +162 -0
- package/src/ui/Header.tsx +87 -24
- package/src/ui/HelpModal.tsx +57 -0
- package/src/ui/InputBox.tsx +163 -10
- package/src/ui/MessageList.tsx +487 -40
- package/src/ui/PermissionPrompt.tsx +17 -5
- package/src/ui/StatusBar.tsx +122 -3
- package/src/ui/TerminalPane.tsx +84 -0
- package/src/ui/ToolCallDisplay.tsx +252 -18
- package/src/ui/TreePane.tsx +132 -0
- package/src/ui/chat-ui.ts +41 -44
- package/src/ui/ink/index.ts +771 -38
- package/src/ui/streaming.ts +1 -1
- package/src/ui/theme.ts +104 -0
- package/src/ui/types.ts +18 -0
- package/src/version.ts +1 -1
- package/src/watcher/index.ts +66 -15
- package/src/wizard/types.ts +1 -0
- package/src/wizard/ui.ts +1 -1
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,1058 @@
|
|
|
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
|
+
import { logger } from '../utils';
|
|
9
|
+
import { createWizard, ui, select, multiSelect, confirm, input, pathInput, } from '../wizard';
|
|
10
|
+
import { generateTerraformProject } from '../generator/terraform';
|
|
11
|
+
// ---- Cloud CLI helpers (replace microservice REST calls) ----
|
|
12
|
+
function getAwsProfiles() {
|
|
13
|
+
try {
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
const out = execFileSync('aws', ['configure', 'list-profiles'], {
|
|
16
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
return out.trim().split('\n').map((s) => s.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return ['default'];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function validateAwsProfile(profile) {
|
|
25
|
+
try {
|
|
26
|
+
const { execFileSync } = require('child_process');
|
|
27
|
+
const out = execFileSync('aws', ['sts', 'get-caller-identity', '--profile', profile, '--output', 'json'], {
|
|
28
|
+
encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
29
|
+
});
|
|
30
|
+
const data = JSON.parse(out);
|
|
31
|
+
return { valid: true, accountId: data.Account };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { valid: false, error: e.message?.slice(0, 100) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getGcpProject() {
|
|
38
|
+
try {
|
|
39
|
+
const { execFileSync } = require('child_process');
|
|
40
|
+
return execFileSync('gcloud', ['config', 'get-value', 'project'], {
|
|
41
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
}).trim();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function validateAzureSubscription(subscriptionId) {
|
|
49
|
+
try {
|
|
50
|
+
const { execFileSync } = require('child_process');
|
|
51
|
+
const out = execFileSync('az', ['account', 'show', '--subscription', subscriptionId, '--output', 'json'], {
|
|
52
|
+
encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
});
|
|
54
|
+
const data = JSON.parse(out);
|
|
55
|
+
return { valid: true, name: data.name };
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { valid: false, error: e.message?.slice(0, 100) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run the generate terraform command
|
|
63
|
+
*/
|
|
64
|
+
export async function generateTerraformCommand(options = {}) {
|
|
65
|
+
logger.info('Starting Terraform generation wizard');
|
|
66
|
+
// Non-interactive mode
|
|
67
|
+
if (options.nonInteractive) {
|
|
68
|
+
await runNonInteractive(options);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Questionnaire mode
|
|
72
|
+
if (options.questionnaire) {
|
|
73
|
+
const { questionnaireCommand } = await import('./questionnaire');
|
|
74
|
+
await questionnaireCommand({
|
|
75
|
+
type: 'terraform',
|
|
76
|
+
outputDir: options.output,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Conversational mode (Mode B)
|
|
81
|
+
if (options.conversational) {
|
|
82
|
+
await runConversational(options);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Interactive wizard mode
|
|
86
|
+
const steps = createWizardSteps();
|
|
87
|
+
const wizard = createWizard({
|
|
88
|
+
title: 'nimbus generate terraform',
|
|
89
|
+
description: 'Generate Terraform from your cloud infrastructure',
|
|
90
|
+
initialContext: {
|
|
91
|
+
provider: 'aws',
|
|
92
|
+
awsProfile: options.profile,
|
|
93
|
+
awsRegions: options.regions,
|
|
94
|
+
servicesToScan: options.services,
|
|
95
|
+
outputPath: options.output,
|
|
96
|
+
},
|
|
97
|
+
steps,
|
|
98
|
+
onEvent: event => {
|
|
99
|
+
if (event.type === 'step:start' && process.stdout.isTTY) {
|
|
100
|
+
const idx = steps.findIndex(s => s.id === event.stepId);
|
|
101
|
+
if (idx >= 0) {
|
|
102
|
+
// Visual step progress bar
|
|
103
|
+
const progress = steps.map((s, i) => {
|
|
104
|
+
if (i < idx) {
|
|
105
|
+
return ui.color(`\u2713 ${s.title}`, 'green');
|
|
106
|
+
}
|
|
107
|
+
if (i === idx) {
|
|
108
|
+
return ui.color(`\u25CF ${s.title}`, 'cyan');
|
|
109
|
+
}
|
|
110
|
+
return ui.dim(`\u25CB ${s.title}`);
|
|
111
|
+
});
|
|
112
|
+
ui.newLine();
|
|
113
|
+
ui.print(ui.dim(' Progress: ') + progress.join(ui.dim(' \u2500 ')));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
logger.debug('Wizard event', { type: event.type });
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const result = await wizard.run();
|
|
120
|
+
if (result.success) {
|
|
121
|
+
ui.newLine();
|
|
122
|
+
ui.box({
|
|
123
|
+
title: 'Complete!',
|
|
124
|
+
content: [
|
|
125
|
+
'Your infrastructure has been codified as Terraform.',
|
|
126
|
+
'',
|
|
127
|
+
'Next steps:',
|
|
128
|
+
` 1. Review the generated files in ${result.context.outputPath}`,
|
|
129
|
+
' 2. Run "terraform plan" to see what will be imported',
|
|
130
|
+
' 3. Run "terraform apply" to bring resources under Terraform control',
|
|
131
|
+
'',
|
|
132
|
+
'Scan saved to history. View with: nimbus infra history',
|
|
133
|
+
],
|
|
134
|
+
style: 'rounded',
|
|
135
|
+
borderColor: 'green',
|
|
136
|
+
padding: 1,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
ui.error(`Wizard failed: ${result.error?.message || 'Unknown error'}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Create wizard steps
|
|
146
|
+
*/
|
|
147
|
+
function createWizardSteps() {
|
|
148
|
+
return [
|
|
149
|
+
// Step 1: Provider Selection
|
|
150
|
+
{
|
|
151
|
+
id: 'provider',
|
|
152
|
+
title: 'Cloud Provider Selection',
|
|
153
|
+
description: 'Select the cloud provider to scan for infrastructure',
|
|
154
|
+
execute: providerSelectionStep,
|
|
155
|
+
},
|
|
156
|
+
// Step 2: AWS Configuration
|
|
157
|
+
{
|
|
158
|
+
id: 'aws-config',
|
|
159
|
+
title: 'AWS Configuration',
|
|
160
|
+
description: 'Configure AWS profile and regions to scan',
|
|
161
|
+
condition: ctx => ctx.provider === 'aws',
|
|
162
|
+
execute: awsConfigStep,
|
|
163
|
+
},
|
|
164
|
+
// Step 3: Service Selection
|
|
165
|
+
{
|
|
166
|
+
id: 'services',
|
|
167
|
+
title: 'Service Selection',
|
|
168
|
+
description: 'Select which AWS services to scan',
|
|
169
|
+
condition: ctx => ctx.provider === 'aws',
|
|
170
|
+
execute: serviceSelectionStep,
|
|
171
|
+
},
|
|
172
|
+
// GCP Configuration
|
|
173
|
+
{
|
|
174
|
+
id: 'gcp-config',
|
|
175
|
+
title: 'GCP Configuration',
|
|
176
|
+
description: 'Configure GCP project and regions to scan',
|
|
177
|
+
condition: ctx => ctx.provider === 'gcp',
|
|
178
|
+
execute: gcpConfigStep,
|
|
179
|
+
},
|
|
180
|
+
// GCP Service Selection
|
|
181
|
+
{
|
|
182
|
+
id: 'gcp-services',
|
|
183
|
+
title: 'GCP Service Selection',
|
|
184
|
+
description: 'Select which GCP services to scan',
|
|
185
|
+
condition: ctx => ctx.provider === 'gcp',
|
|
186
|
+
execute: gcpServiceSelectionStep,
|
|
187
|
+
},
|
|
188
|
+
// Azure Configuration
|
|
189
|
+
{
|
|
190
|
+
id: 'azure-config',
|
|
191
|
+
title: 'Azure Configuration',
|
|
192
|
+
description: 'Configure Azure subscription and resource group',
|
|
193
|
+
condition: ctx => ctx.provider === 'azure',
|
|
194
|
+
execute: azureConfigStep,
|
|
195
|
+
},
|
|
196
|
+
// Azure Service Selection
|
|
197
|
+
{
|
|
198
|
+
id: 'azure-services',
|
|
199
|
+
title: 'Azure Service Selection',
|
|
200
|
+
description: 'Select which Azure services to scan',
|
|
201
|
+
condition: ctx => ctx.provider === 'azure',
|
|
202
|
+
execute: azureServiceSelectionStep,
|
|
203
|
+
},
|
|
204
|
+
// Step 4: Discovery
|
|
205
|
+
{
|
|
206
|
+
id: 'discovery',
|
|
207
|
+
title: 'Infrastructure Discovery',
|
|
208
|
+
description: 'Scanning your AWS infrastructure...',
|
|
209
|
+
execute: discoveryStep,
|
|
210
|
+
},
|
|
211
|
+
// Step 5: Generation Options
|
|
212
|
+
{
|
|
213
|
+
id: 'generation-options',
|
|
214
|
+
title: 'Generation Options',
|
|
215
|
+
description: 'Configure Terraform generation options',
|
|
216
|
+
execute: generationOptionsStep,
|
|
217
|
+
},
|
|
218
|
+
// Step 6: Output Location
|
|
219
|
+
{
|
|
220
|
+
id: 'output',
|
|
221
|
+
title: 'Output Location',
|
|
222
|
+
description: 'Where should the Terraform files be saved?',
|
|
223
|
+
execute: outputLocationStep,
|
|
224
|
+
},
|
|
225
|
+
// Future steps (Phase 2+):
|
|
226
|
+
// - Terraform Generation
|
|
227
|
+
// - Best Practices Analysis
|
|
228
|
+
// - Interactive Review
|
|
229
|
+
// - Starter Kit Generation
|
|
230
|
+
// - Terraform Operations
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Step 1: Provider Selection
|
|
235
|
+
*/
|
|
236
|
+
async function providerSelectionStep(ctx) {
|
|
237
|
+
const provider = await select({
|
|
238
|
+
message: 'Select cloud provider:',
|
|
239
|
+
options: [
|
|
240
|
+
{
|
|
241
|
+
value: 'aws',
|
|
242
|
+
label: 'AWS (Amazon Web Services)',
|
|
243
|
+
description: 'Scan EC2, S3, RDS, Lambda, VPC, IAM, and more',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
value: 'gcp',
|
|
247
|
+
label: 'GCP (Google Cloud Platform)',
|
|
248
|
+
description: 'Scan Compute, GCS, GKE, Cloud Functions, VPC, IAM',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
value: 'azure',
|
|
252
|
+
label: 'Azure (Microsoft Azure)',
|
|
253
|
+
description: 'Scan VMs, Storage, AKS, Functions, VNet, IAM',
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
defaultValue: ctx.provider || 'aws',
|
|
257
|
+
});
|
|
258
|
+
if (!provider) {
|
|
259
|
+
return { success: false, error: 'No provider selected' };
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
data: { provider },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Step 2: AWS Configuration
|
|
268
|
+
*/
|
|
269
|
+
async function awsConfigStep(ctx) {
|
|
270
|
+
// Fetch available profiles via CLI
|
|
271
|
+
ui.startSpinner({ message: 'Fetching AWS profiles...' });
|
|
272
|
+
const profileNames = getAwsProfiles();
|
|
273
|
+
ui.stopSpinnerSuccess(`Found ${profileNames.length} AWS profile(s)`);
|
|
274
|
+
// Profile selection
|
|
275
|
+
let selectedProfile = ctx.awsProfile;
|
|
276
|
+
if (!selectedProfile) {
|
|
277
|
+
const profileOptions = profileNames.map(p => ({ value: p, label: p }));
|
|
278
|
+
selectedProfile = await select({
|
|
279
|
+
message: 'Select AWS profile:',
|
|
280
|
+
options: profileOptions,
|
|
281
|
+
defaultValue: 'default',
|
|
282
|
+
});
|
|
283
|
+
if (!selectedProfile) {
|
|
284
|
+
return { success: false, error: 'No profile selected' };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Validate credentials via CLI
|
|
288
|
+
ui.startSpinner({ message: `Validating credentials for profile "${selectedProfile}"...` });
|
|
289
|
+
const validation = validateAwsProfile(selectedProfile);
|
|
290
|
+
if (!validation.valid) {
|
|
291
|
+
ui.stopSpinnerFail(`Invalid credentials: ${validation.error || 'Unknown error'}`);
|
|
292
|
+
return { success: false, error: 'Invalid AWS credentials' };
|
|
293
|
+
}
|
|
294
|
+
ui.stopSpinnerSuccess(`Authenticated to account ${validation.accountId || 'unknown'}`);
|
|
295
|
+
ctx.awsAccountId = validation.accountId;
|
|
296
|
+
// Region selection
|
|
297
|
+
ui.newLine();
|
|
298
|
+
const regionChoice = await select({
|
|
299
|
+
message: 'Select regions to scan:',
|
|
300
|
+
options: [
|
|
301
|
+
{
|
|
302
|
+
value: 'all',
|
|
303
|
+
label: 'All enabled regions',
|
|
304
|
+
description: 'Scan all regions enabled for your account',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
value: 'specific',
|
|
308
|
+
label: 'Specific regions',
|
|
309
|
+
description: 'Select specific regions to scan',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
defaultValue: 'all',
|
|
313
|
+
});
|
|
314
|
+
let selectedRegions = [];
|
|
315
|
+
if (regionChoice === 'specific') {
|
|
316
|
+
// Hardcoded common AWS regions (no service needed)
|
|
317
|
+
const regionOptions = [
|
|
318
|
+
{ value: 'us-east-1', label: 'us-east-1 - N. Virginia' },
|
|
319
|
+
{ value: 'us-east-2', label: 'us-east-2 - Ohio' },
|
|
320
|
+
{ value: 'us-west-1', label: 'us-west-1 - N. California' },
|
|
321
|
+
{ value: 'us-west-2', label: 'us-west-2 - Oregon' },
|
|
322
|
+
{ value: 'eu-west-1', label: 'eu-west-1 - Ireland' },
|
|
323
|
+
{ value: 'eu-central-1', label: 'eu-central-1 - Frankfurt' },
|
|
324
|
+
{ value: 'ap-southeast-1', label: 'ap-southeast-1 - Singapore' },
|
|
325
|
+
{ value: 'ap-northeast-1', label: 'ap-northeast-1 - Tokyo' },
|
|
326
|
+
];
|
|
327
|
+
selectedRegions = (await multiSelect({
|
|
328
|
+
message: 'Select regions to scan:',
|
|
329
|
+
options: regionOptions,
|
|
330
|
+
required: true,
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
data: {
|
|
336
|
+
awsProfile: selectedProfile,
|
|
337
|
+
awsRegions: regionChoice === 'all' ? undefined : selectedRegions,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Step 3: Service Selection
|
|
343
|
+
*/
|
|
344
|
+
async function serviceSelectionStep(_ctx) {
|
|
345
|
+
const serviceChoice = await select({
|
|
346
|
+
message: 'Select services to scan:',
|
|
347
|
+
options: [
|
|
348
|
+
{
|
|
349
|
+
value: 'all',
|
|
350
|
+
label: 'All supported services',
|
|
351
|
+
description: 'EC2, S3, RDS, Lambda, VPC, IAM, ECS, EKS, DynamoDB, CloudFront',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
value: 'specific',
|
|
355
|
+
label: 'Specific services',
|
|
356
|
+
description: 'Select specific services to scan',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
defaultValue: 'all',
|
|
360
|
+
});
|
|
361
|
+
if (serviceChoice === 'all') {
|
|
362
|
+
return { success: true, data: { servicesToScan: undefined } };
|
|
363
|
+
}
|
|
364
|
+
const serviceOptions = [
|
|
365
|
+
{ value: 'EC2', label: 'EC2', description: 'Instances, volumes, security groups, AMIs' },
|
|
366
|
+
{ value: 'S3', label: 'S3', description: 'Buckets and bucket policies' },
|
|
367
|
+
{ value: 'RDS', label: 'RDS', description: 'Database instances and clusters' },
|
|
368
|
+
{ value: 'Lambda', label: 'Lambda', description: 'Functions and layers' },
|
|
369
|
+
{ value: 'VPC', label: 'VPC', description: 'VPCs, subnets, route tables, NAT gateways' },
|
|
370
|
+
{ value: 'IAM', label: 'IAM', description: 'Roles, policies, users, groups' },
|
|
371
|
+
{ value: 'ECS', label: 'ECS', description: 'Clusters, services, task definitions' },
|
|
372
|
+
{ value: 'EKS', label: 'EKS', description: 'Clusters and node groups' },
|
|
373
|
+
{ value: 'DynamoDB', label: 'DynamoDB', description: 'Tables' },
|
|
374
|
+
{ value: 'CloudFront', label: 'CloudFront', description: 'Distributions' },
|
|
375
|
+
];
|
|
376
|
+
const selectedServices = await multiSelect({
|
|
377
|
+
message: 'Select services to scan:',
|
|
378
|
+
options: serviceOptions,
|
|
379
|
+
required: true,
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
success: true,
|
|
383
|
+
data: { servicesToScan: selectedServices },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* GCP Configuration Step
|
|
388
|
+
*/
|
|
389
|
+
async function gcpConfigStep(ctx) {
|
|
390
|
+
// Project ID
|
|
391
|
+
const projectId = await input({
|
|
392
|
+
message: 'Enter your GCP project ID:',
|
|
393
|
+
defaultValue: ctx.gcpProject || '',
|
|
394
|
+
});
|
|
395
|
+
if (!projectId) {
|
|
396
|
+
return { success: false, error: 'GCP project ID is required' };
|
|
397
|
+
}
|
|
398
|
+
// Validate project access via gcloud CLI
|
|
399
|
+
ui.startSpinner({ message: `Validating access to project "${projectId}"...` });
|
|
400
|
+
try {
|
|
401
|
+
const { execFileSync } = await import('child_process');
|
|
402
|
+
execFileSync('gcloud', ['projects', 'describe', projectId, '--format=json'], {
|
|
403
|
+
encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
404
|
+
});
|
|
405
|
+
ui.stopSpinnerSuccess(`Connected to project ${projectId}`);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
ui.stopSpinnerFail(`Could not validate project: ${error.message?.slice(0, 80) || 'unknown'}`);
|
|
409
|
+
// Non-fatal — user may still proceed if gcloud is not configured
|
|
410
|
+
ui.info('Proceeding without validation. Ensure gcloud credentials are configured.');
|
|
411
|
+
}
|
|
412
|
+
// Region selection
|
|
413
|
+
ui.newLine();
|
|
414
|
+
const regionChoice = await select({
|
|
415
|
+
message: 'Select regions to scan:',
|
|
416
|
+
options: [
|
|
417
|
+
{
|
|
418
|
+
value: 'all',
|
|
419
|
+
label: 'All available regions',
|
|
420
|
+
description: 'Scan all GCP regions',
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
value: 'specific',
|
|
424
|
+
label: 'Specific regions',
|
|
425
|
+
description: 'Select specific regions to scan',
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
defaultValue: 'all',
|
|
429
|
+
});
|
|
430
|
+
let selectedRegions = [];
|
|
431
|
+
if (regionChoice === 'specific') {
|
|
432
|
+
const gcpRegionOptions = [
|
|
433
|
+
{ value: 'us-central1', label: 'us-central1 - Iowa' },
|
|
434
|
+
{ value: 'us-east1', label: 'us-east1 - South Carolina' },
|
|
435
|
+
{ value: 'us-east4', label: 'us-east4 - Northern Virginia' },
|
|
436
|
+
{ value: 'us-west1', label: 'us-west1 - Oregon' },
|
|
437
|
+
{ value: 'europe-west1', label: 'europe-west1 - Belgium' },
|
|
438
|
+
{ value: 'europe-west2', label: 'europe-west2 - London' },
|
|
439
|
+
{ value: 'asia-east1', label: 'asia-east1 - Taiwan' },
|
|
440
|
+
{ value: 'asia-southeast1', label: 'asia-southeast1 - Singapore' },
|
|
441
|
+
];
|
|
442
|
+
selectedRegions = (await multiSelect({
|
|
443
|
+
message: 'Select GCP regions to scan:',
|
|
444
|
+
options: gcpRegionOptions,
|
|
445
|
+
required: true,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
success: true,
|
|
450
|
+
data: {
|
|
451
|
+
gcpProject: projectId,
|
|
452
|
+
gcpRegions: regionChoice === 'all' ? undefined : selectedRegions,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* GCP Service Selection Step
|
|
458
|
+
*/
|
|
459
|
+
async function gcpServiceSelectionStep(_ctx) {
|
|
460
|
+
const serviceChoice = await select({
|
|
461
|
+
message: 'Select GCP services to scan:',
|
|
462
|
+
options: [
|
|
463
|
+
{
|
|
464
|
+
value: 'all',
|
|
465
|
+
label: 'All supported services',
|
|
466
|
+
description: 'Compute, GCS, GKE, Cloud Functions, VPC, IAM, Cloud SQL, Pub/Sub',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
value: 'specific',
|
|
470
|
+
label: 'Specific services',
|
|
471
|
+
description: 'Select specific services to scan',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
defaultValue: 'all',
|
|
475
|
+
});
|
|
476
|
+
if (serviceChoice === 'all') {
|
|
477
|
+
return { success: true, data: { servicesToScan: undefined } };
|
|
478
|
+
}
|
|
479
|
+
const serviceOptions = [
|
|
480
|
+
{ value: 'Compute', label: 'Compute Engine', description: 'VMs, disks, images' },
|
|
481
|
+
{ value: 'GCS', label: 'Cloud Storage', description: 'Buckets and objects' },
|
|
482
|
+
{ value: 'GKE', label: 'Google Kubernetes Engine', description: 'Clusters and node pools' },
|
|
483
|
+
{ value: 'CloudFunctions', label: 'Cloud Functions', description: 'Serverless functions' },
|
|
484
|
+
{ value: 'VPC', label: 'VPC Network', description: 'Networks, subnets, firewalls' },
|
|
485
|
+
{ value: 'IAM', label: 'IAM', description: 'Roles, service accounts, policies' },
|
|
486
|
+
{ value: 'CloudSQL', label: 'Cloud SQL', description: 'Database instances' },
|
|
487
|
+
{ value: 'PubSub', label: 'Pub/Sub', description: 'Topics and subscriptions' },
|
|
488
|
+
];
|
|
489
|
+
const selectedServices = await multiSelect({
|
|
490
|
+
message: 'Select GCP services to scan:',
|
|
491
|
+
options: serviceOptions,
|
|
492
|
+
required: true,
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
success: true,
|
|
496
|
+
data: { servicesToScan: selectedServices },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Azure Configuration Step
|
|
501
|
+
*/
|
|
502
|
+
async function azureConfigStep(ctx) {
|
|
503
|
+
// Subscription ID
|
|
504
|
+
const subscriptionId = await input({
|
|
505
|
+
message: 'Enter your Azure subscription ID:',
|
|
506
|
+
defaultValue: ctx.azureSubscription || '',
|
|
507
|
+
});
|
|
508
|
+
if (!subscriptionId) {
|
|
509
|
+
return { success: false, error: 'Azure subscription ID is required' };
|
|
510
|
+
}
|
|
511
|
+
// Validate subscription access via Azure CLI
|
|
512
|
+
ui.startSpinner({ message: `Validating access to subscription "${subscriptionId}"...` });
|
|
513
|
+
const azVal = validateAzureSubscription(subscriptionId);
|
|
514
|
+
if (!azVal.valid) {
|
|
515
|
+
ui.stopSpinnerFail(`Could not validate subscription: ${azVal.error || 'unknown'}`);
|
|
516
|
+
ui.info('Proceeding without validation. Ensure az CLI credentials are configured.');
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
ui.stopSpinnerSuccess(`Connected to subscription${azVal.name ? ` (${azVal.name})` : ''}`);
|
|
520
|
+
}
|
|
521
|
+
// Resource group (optional)
|
|
522
|
+
ui.newLine();
|
|
523
|
+
const resourceGroup = await input({
|
|
524
|
+
message: 'Resource group (leave empty to scan all):',
|
|
525
|
+
defaultValue: ctx.azureResourceGroup || '',
|
|
526
|
+
});
|
|
527
|
+
// Region selection
|
|
528
|
+
ui.newLine();
|
|
529
|
+
const regionChoice = await select({
|
|
530
|
+
message: 'Select regions to scan:',
|
|
531
|
+
options: [
|
|
532
|
+
{
|
|
533
|
+
value: 'all',
|
|
534
|
+
label: 'All available regions',
|
|
535
|
+
description: 'Scan all Azure regions',
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
value: 'specific',
|
|
539
|
+
label: 'Specific regions',
|
|
540
|
+
description: 'Select specific regions to scan',
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
defaultValue: 'all',
|
|
544
|
+
});
|
|
545
|
+
let _selectedRegions = [];
|
|
546
|
+
if (regionChoice === 'specific') {
|
|
547
|
+
const azureRegionOptions = [
|
|
548
|
+
{ value: 'eastus', label: 'East US' },
|
|
549
|
+
{ value: 'eastus2', label: 'East US 2' },
|
|
550
|
+
{ value: 'westus2', label: 'West US 2' },
|
|
551
|
+
{ value: 'centralus', label: 'Central US' },
|
|
552
|
+
{ value: 'westeurope', label: 'West Europe' },
|
|
553
|
+
{ value: 'northeurope', label: 'North Europe' },
|
|
554
|
+
{ value: 'southeastasia', label: 'Southeast Asia' },
|
|
555
|
+
{ value: 'eastasia', label: 'East Asia' },
|
|
556
|
+
];
|
|
557
|
+
_selectedRegions = (await multiSelect({
|
|
558
|
+
message: 'Select Azure regions to scan:',
|
|
559
|
+
options: azureRegionOptions,
|
|
560
|
+
required: true,
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
data: {
|
|
566
|
+
azureSubscription: subscriptionId,
|
|
567
|
+
azureResourceGroup: resourceGroup || undefined,
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Azure Service Selection Step
|
|
573
|
+
*/
|
|
574
|
+
async function azureServiceSelectionStep(_ctx) {
|
|
575
|
+
const serviceChoice = await select({
|
|
576
|
+
message: 'Select Azure services to scan:',
|
|
577
|
+
options: [
|
|
578
|
+
{
|
|
579
|
+
value: 'all',
|
|
580
|
+
label: 'All supported services',
|
|
581
|
+
description: 'VMs, Storage, AKS, Functions, VNet, IAM, SQL, Service Bus',
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
value: 'specific',
|
|
585
|
+
label: 'Specific services',
|
|
586
|
+
description: 'Select specific services to scan',
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
defaultValue: 'all',
|
|
590
|
+
});
|
|
591
|
+
if (serviceChoice === 'all') {
|
|
592
|
+
return { success: true, data: { servicesToScan: undefined } };
|
|
593
|
+
}
|
|
594
|
+
const serviceOptions = [
|
|
595
|
+
{ value: 'VirtualMachines', label: 'Virtual Machines', description: 'VMs, disks, images' },
|
|
596
|
+
{
|
|
597
|
+
value: 'Storage',
|
|
598
|
+
label: 'Storage Accounts',
|
|
599
|
+
description: 'Blob, file, queue, table storage',
|
|
600
|
+
},
|
|
601
|
+
{ value: 'AKS', label: 'Azure Kubernetes Service', description: 'Clusters and node pools' },
|
|
602
|
+
{ value: 'Functions', label: 'Azure Functions', description: 'Serverless functions' },
|
|
603
|
+
{ value: 'VNet', label: 'Virtual Network', description: 'VNets, subnets, NSGs' },
|
|
604
|
+
{ value: 'IAM', label: 'IAM', description: 'Role assignments, managed identities' },
|
|
605
|
+
{ value: 'SQLDatabase', label: 'Azure SQL', description: 'SQL databases and servers' },
|
|
606
|
+
{ value: 'ServiceBus', label: 'Service Bus', description: 'Queues and topics' },
|
|
607
|
+
];
|
|
608
|
+
const selectedServices = await multiSelect({
|
|
609
|
+
message: 'Select Azure services to scan:',
|
|
610
|
+
options: serviceOptions,
|
|
611
|
+
required: true,
|
|
612
|
+
});
|
|
613
|
+
return {
|
|
614
|
+
success: true,
|
|
615
|
+
data: { servicesToScan: selectedServices },
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Run synchronous CLI-based infrastructure discovery.
|
|
620
|
+
* Replaces the old REST polling approach.
|
|
621
|
+
*/
|
|
622
|
+
async function discoverInfra(ctx) {
|
|
623
|
+
const { execFileSync } = await import('child_process');
|
|
624
|
+
const components = [];
|
|
625
|
+
let resourceCount = 0;
|
|
626
|
+
if (ctx.provider === 'aws') {
|
|
627
|
+
const profile = ctx.awsProfile || 'default';
|
|
628
|
+
const env = { ...process.env, AWS_PROFILE: profile };
|
|
629
|
+
// EC2 instances
|
|
630
|
+
try {
|
|
631
|
+
const out = execFileSync('aws', ['ec2', 'describe-instances', '--query', 'Reservations[*].Instances[*].InstanceId', '--output', 'json'], { encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
632
|
+
const ids = JSON.parse(out).flat();
|
|
633
|
+
if (ids.length > 0) {
|
|
634
|
+
components.push('ec2');
|
|
635
|
+
resourceCount += ids.length;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch { /* not available */ }
|
|
639
|
+
// S3 buckets
|
|
640
|
+
try {
|
|
641
|
+
const out = execFileSync('aws', ['s3', 'ls'], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
642
|
+
const buckets = out.trim().split('\n').filter(Boolean).length;
|
|
643
|
+
if (buckets > 0) {
|
|
644
|
+
components.push('s3');
|
|
645
|
+
resourceCount += buckets;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch { /* not available */ }
|
|
649
|
+
// RDS
|
|
650
|
+
try {
|
|
651
|
+
const out = execFileSync('aws', ['rds', 'describe-db-instances', '--query', 'DBInstances[*].DBInstanceIdentifier', '--output', 'json'], { encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
652
|
+
const dbs = JSON.parse(out);
|
|
653
|
+
if (dbs.length > 0) {
|
|
654
|
+
components.push('rds');
|
|
655
|
+
resourceCount += dbs.length;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch { /* not available */ }
|
|
659
|
+
// EKS clusters
|
|
660
|
+
try {
|
|
661
|
+
const out = execFileSync('aws', ['eks', 'list-clusters', '--output', 'json'], { encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
662
|
+
const clusters = JSON.parse(out).clusters;
|
|
663
|
+
if (clusters?.length > 0) {
|
|
664
|
+
components.push('eks');
|
|
665
|
+
resourceCount += clusters.length;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch { /* not available */ }
|
|
669
|
+
// VPC (always include as foundational)
|
|
670
|
+
components.push('vpc');
|
|
671
|
+
}
|
|
672
|
+
else if (ctx.provider === 'gcp') {
|
|
673
|
+
try {
|
|
674
|
+
execFileSync('gcloud', ['compute', 'instances', 'list', '--format=json'], { encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
675
|
+
components.push('compute');
|
|
676
|
+
}
|
|
677
|
+
catch { /* not available */ }
|
|
678
|
+
components.push('vpc');
|
|
679
|
+
}
|
|
680
|
+
else if (ctx.provider === 'azure') {
|
|
681
|
+
try {
|
|
682
|
+
const out = execFileSync('az', ['resource', 'list', '--output', 'json'], { encoding: 'utf-8', timeout: 20000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
683
|
+
const resources = JSON.parse(out);
|
|
684
|
+
resourceCount += resources.length;
|
|
685
|
+
components.push('vnet');
|
|
686
|
+
}
|
|
687
|
+
catch { /* not available */ }
|
|
688
|
+
}
|
|
689
|
+
return { resourceCount, components: [...new Set(components)] };
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Step: Discovery — uses direct CLI calls instead of REST polling
|
|
693
|
+
*/
|
|
694
|
+
async function discoveryStep(ctx) {
|
|
695
|
+
ui.startSpinner({ message: 'Discovering infrastructure via CLI...' });
|
|
696
|
+
try {
|
|
697
|
+
const { resourceCount, components } = await discoverInfra(ctx);
|
|
698
|
+
ui.stopSpinnerSuccess(`Discovery complete — found ${resourceCount} resource(s), components: ${components.join(', ') || 'vpc'}`);
|
|
699
|
+
ctx.discoveredComponents = components;
|
|
700
|
+
return { success: true, data: { discoveredComponents: components } };
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
ui.stopSpinnerFail('Discovery failed');
|
|
704
|
+
ui.warning(`Could not auto-discover: ${e.message}. You can still generate a template.`);
|
|
705
|
+
return { success: true, data: { discoveredComponents: ['vpc'] } };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Step 5: Generation Options
|
|
710
|
+
*/
|
|
711
|
+
async function generationOptionsStep(_ctx) {
|
|
712
|
+
// Import method
|
|
713
|
+
const importMethod = await select({
|
|
714
|
+
message: 'How should imports be generated?',
|
|
715
|
+
options: [
|
|
716
|
+
{
|
|
717
|
+
value: 'both',
|
|
718
|
+
label: 'Both import blocks and shell script (Recommended)',
|
|
719
|
+
description: 'Maximum compatibility with all Terraform versions',
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
value: 'blocks',
|
|
723
|
+
label: 'Import blocks only (Terraform 1.5+)',
|
|
724
|
+
description: 'Modern declarative imports',
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
value: 'script',
|
|
728
|
+
label: 'Shell script only',
|
|
729
|
+
description: 'Traditional terraform import commands',
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
defaultValue: 'both',
|
|
733
|
+
});
|
|
734
|
+
// Starter kit options
|
|
735
|
+
ui.newLine();
|
|
736
|
+
const includeStarterKit = await confirm({
|
|
737
|
+
message: 'Generate starter kit (README, .gitignore, Makefile, CI/CD)?',
|
|
738
|
+
defaultValue: true,
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
success: true,
|
|
742
|
+
data: {
|
|
743
|
+
importMethod,
|
|
744
|
+
includeReadme: includeStarterKit,
|
|
745
|
+
includeGitignore: includeStarterKit,
|
|
746
|
+
includeMakefile: includeStarterKit,
|
|
747
|
+
includeGithubActions: includeStarterKit,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Step 6: Output Location
|
|
753
|
+
*/
|
|
754
|
+
async function outputLocationStep(ctx) {
|
|
755
|
+
const outputPath = await pathInput('Where should the Terraform files be saved?', ctx.outputPath || './terraform-infrastructure');
|
|
756
|
+
if (!outputPath) {
|
|
757
|
+
return { success: false, error: 'Output path is required' };
|
|
758
|
+
}
|
|
759
|
+
// Ask about saving preferences
|
|
760
|
+
ui.newLine();
|
|
761
|
+
const savePreferences = await confirm({
|
|
762
|
+
message: 'Save your preferences as organization policy for future runs?',
|
|
763
|
+
defaultValue: false,
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
success: true,
|
|
767
|
+
data: {
|
|
768
|
+
outputPath,
|
|
769
|
+
savePreferences,
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Run in conversational mode (Mode B)
|
|
775
|
+
* Uses the generator service's conversational endpoints to describe infrastructure
|
|
776
|
+
* in natural language and generate Terraform from the conversation.
|
|
777
|
+
*/
|
|
778
|
+
async function runConversational(options) {
|
|
779
|
+
const crypto = await import('crypto');
|
|
780
|
+
const fs = await import('fs/promises');
|
|
781
|
+
const pathMod = await import('path');
|
|
782
|
+
const sessionId = crypto.randomUUID();
|
|
783
|
+
ui.header('nimbus generate terraform', 'Conversational mode');
|
|
784
|
+
ui.print('Describe your infrastructure in natural language.');
|
|
785
|
+
ui.print('Type "generate" or "done" when ready to generate Terraform.');
|
|
786
|
+
ui.print('Type "exit" to quit.');
|
|
787
|
+
ui.newLine();
|
|
788
|
+
for (;;) {
|
|
789
|
+
const message = await input({
|
|
790
|
+
message: 'You:',
|
|
791
|
+
defaultValue: '',
|
|
792
|
+
});
|
|
793
|
+
if (!message || message.trim() === '') {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const trimmed = message.trim().toLowerCase();
|
|
797
|
+
if (trimmed === 'exit') {
|
|
798
|
+
ui.info('Exiting conversational mode.');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
// User explicitly wants to generate
|
|
802
|
+
if (trimmed === 'generate' || trimmed === 'done') {
|
|
803
|
+
const generated = await generateFromConversation(sessionId, options, fs, pathMod);
|
|
804
|
+
if (generated) {
|
|
805
|
+
ui.newLine();
|
|
806
|
+
ui.print('You can refine the generated Terraform by continuing the conversation.');
|
|
807
|
+
ui.print('Type "generate" to regenerate, or "exit" to finish.');
|
|
808
|
+
ui.newLine();
|
|
809
|
+
continue; // stays in the while(true) loop with same sessionId
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
// Build request from conversational description — use chatCommand for natural language interaction
|
|
814
|
+
ui.newLine();
|
|
815
|
+
ui.info(`You said: "${message}"`);
|
|
816
|
+
ui.info('Type "generate" or "done" to generate Terraform from this description, or describe your infrastructure further.');
|
|
817
|
+
ui.newLine();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Generate Terraform files from a conversational session using the local generator
|
|
822
|
+
*/
|
|
823
|
+
async function generateFromConversation(_sessionId, options, fs, pathMod) {
|
|
824
|
+
ui.newLine();
|
|
825
|
+
ui.startSpinner({ message: 'Generating Terraform from description...' });
|
|
826
|
+
try {
|
|
827
|
+
const provider = options.provider || 'aws';
|
|
828
|
+
const outputDir = options.output || './infrastructure';
|
|
829
|
+
const generatedProject = await generateTerraformProject({
|
|
830
|
+
projectName: 'infrastructure',
|
|
831
|
+
provider: provider,
|
|
832
|
+
region: options.regions?.[0] || (provider === 'aws' ? 'us-east-1' : provider === 'gcp' ? 'us-central1' : 'eastus'),
|
|
833
|
+
components: options.services || ['vpc'],
|
|
834
|
+
});
|
|
835
|
+
ui.stopSpinnerSuccess('Terraform code generated');
|
|
836
|
+
const files = generatedProject.files;
|
|
837
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
838
|
+
for (const file of files) {
|
|
839
|
+
const filePath = pathMod.join(outputDir, file.path);
|
|
840
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
841
|
+
await fs.writeFile(filePath, file.content);
|
|
842
|
+
}
|
|
843
|
+
ui.newLine();
|
|
844
|
+
ui.success(`Generated ${files.length} Terraform file(s) in ${outputDir}`);
|
|
845
|
+
ui.newLine();
|
|
846
|
+
ui.print('Generated files:');
|
|
847
|
+
for (const file of files) {
|
|
848
|
+
ui.print(` ${ui.color('●', 'green')} ${file.path}`);
|
|
849
|
+
}
|
|
850
|
+
ui.newLine();
|
|
851
|
+
ui.print('Next steps:');
|
|
852
|
+
ui.print(` 1. Review the generated files in ${outputDir}`);
|
|
853
|
+
ui.print(' 2. Run "terraform plan" to preview changes');
|
|
854
|
+
ui.print(' 3. Run "terraform apply" to create infrastructure');
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
ui.stopSpinnerFail('Generation failed');
|
|
859
|
+
ui.error(`Failed to generate Terraform: ${error.message}`);
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Run in non-interactive mode
|
|
865
|
+
*/
|
|
866
|
+
async function runNonInteractive(options) {
|
|
867
|
+
ui.header('nimbus generate terraform', 'Non-interactive mode');
|
|
868
|
+
const provider = options.provider || 'aws';
|
|
869
|
+
// Validate required flags per provider
|
|
870
|
+
if (provider === 'aws' && !options.profile) {
|
|
871
|
+
ui.error('AWS profile is required in non-interactive mode (--profile)');
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
if (provider === 'gcp' && !options.gcpProject) {
|
|
875
|
+
ui.error('GCP project is required in non-interactive mode (--gcp-project)');
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
if (provider === 'azure' && !options.azureSubscription) {
|
|
879
|
+
ui.error('Azure subscription is required in non-interactive mode (--azure-subscription)');
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
ui.info(`Provider: ${provider}`);
|
|
883
|
+
if (provider === 'aws') {
|
|
884
|
+
ui.info(`Profile: ${options.profile}`);
|
|
885
|
+
}
|
|
886
|
+
else if (provider === 'gcp') {
|
|
887
|
+
ui.info(`Project: ${options.gcpProject}`);
|
|
888
|
+
}
|
|
889
|
+
else if (provider === 'azure') {
|
|
890
|
+
ui.info(`Subscription: ${options.azureSubscription}`);
|
|
891
|
+
}
|
|
892
|
+
ui.info(`Regions: ${options.regions?.join(', ') || 'all'}`);
|
|
893
|
+
ui.info(`Services: ${options.services?.join(', ') || 'all'}`);
|
|
894
|
+
ui.info(`Output: ${options.output || './terraform-infrastructure'}`);
|
|
895
|
+
ui.newLine();
|
|
896
|
+
// Build discovery context
|
|
897
|
+
const ctx = {
|
|
898
|
+
provider,
|
|
899
|
+
awsProfile: options.profile,
|
|
900
|
+
awsRegions: options.regions,
|
|
901
|
+
gcpProject: options.gcpProject,
|
|
902
|
+
azureSubscription: options.azureSubscription,
|
|
903
|
+
servicesToScan: options.services,
|
|
904
|
+
outputPath: options.output || './terraform-infrastructure',
|
|
905
|
+
};
|
|
906
|
+
// Run direct CLI discovery
|
|
907
|
+
ui.info('Starting infrastructure discovery...');
|
|
908
|
+
ui.newLine();
|
|
909
|
+
const { components: discoveredComponents } = await discoverInfra(ctx).catch(() => ({ components: ['vpc'] }));
|
|
910
|
+
ui.success(`Discovered components: ${discoveredComponents.join(', ')}`);
|
|
911
|
+
ui.newLine();
|
|
912
|
+
// Generate Terraform from discovered inventory using src/generator/terraform.ts
|
|
913
|
+
ui.startSpinner({ message: 'Generating Terraform code...' });
|
|
914
|
+
try {
|
|
915
|
+
const outputDir = options.output || './terraform-infrastructure';
|
|
916
|
+
const components = options.services || discoveredComponents;
|
|
917
|
+
const generatedProject = await generateTerraformProject({
|
|
918
|
+
projectName: 'infrastructure',
|
|
919
|
+
provider: provider,
|
|
920
|
+
region: options.regions?.[0] || (provider === 'aws' ? 'us-east-1' : provider === 'gcp' ? 'us-central1' : 'eastus'),
|
|
921
|
+
components,
|
|
922
|
+
});
|
|
923
|
+
ui.stopSpinnerSuccess('Terraform code generated');
|
|
924
|
+
// Write generated files
|
|
925
|
+
const fs = await import('fs/promises');
|
|
926
|
+
const path = await import('path');
|
|
927
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
928
|
+
const files = generatedProject.files;
|
|
929
|
+
for (const file of files) {
|
|
930
|
+
const filePath = path.join(outputDir, file.path);
|
|
931
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
932
|
+
await fs.writeFile(filePath, file.content);
|
|
933
|
+
}
|
|
934
|
+
if (options.jsonOutput) {
|
|
935
|
+
const summary = {
|
|
936
|
+
success: true,
|
|
937
|
+
provider,
|
|
938
|
+
outputDir,
|
|
939
|
+
filesGenerated: files.map(f => f.path),
|
|
940
|
+
componentsGenerated: components,
|
|
941
|
+
};
|
|
942
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
ui.newLine();
|
|
946
|
+
ui.success(`Generated ${files.length} Terraform file(s) in ${outputDir}`);
|
|
947
|
+
ui.newLine();
|
|
948
|
+
ui.print('Generated files:');
|
|
949
|
+
for (const file of files) {
|
|
950
|
+
ui.print(` ${ui.color('●', 'green')} ${file.path}`);
|
|
951
|
+
}
|
|
952
|
+
ui.newLine();
|
|
953
|
+
ui.print('Next steps:');
|
|
954
|
+
ui.print(` 1. Review the generated files in ${outputDir}`);
|
|
955
|
+
ui.print(' 2. Run "terraform plan" to see what will be imported');
|
|
956
|
+
ui.print(' 3. Run "terraform apply" to bring resources under Terraform control');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
ui.stopSpinnerFail('Generation failed');
|
|
961
|
+
ui.error(`Failed to generate Terraform: ${error.message}`);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Run post-generation validation using terraform fmt/validate if available.
|
|
967
|
+
* Non-blocking: warnings shown but errors don't abort.
|
|
968
|
+
*/
|
|
969
|
+
async function runPostGenerationValidation(files, jsonOutput) {
|
|
970
|
+
if (!jsonOutput) {
|
|
971
|
+
ui.newLine();
|
|
972
|
+
ui.info('Tip: Run "terraform init && terraform validate" in the output directory to validate the generated files.');
|
|
973
|
+
}
|
|
974
|
+
return undefined;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Display a human-readable validation report.
|
|
978
|
+
* Shows results for terraform fmt, terraform validate, tflint, and checkov.
|
|
979
|
+
* Tools that are not installed show as "not installed" gracefully.
|
|
980
|
+
*/
|
|
981
|
+
function displayValidationReport(report) {
|
|
982
|
+
const items = report.items || [];
|
|
983
|
+
const summary = report.summary || { errors: 0, warnings: 0, info: 0 };
|
|
984
|
+
// Overall status
|
|
985
|
+
const isValid = report.valid !== false && summary.errors === 0;
|
|
986
|
+
if (isValid) {
|
|
987
|
+
ui.print(` ${ui.color('\u2713', 'green')} Validation passed`);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
ui.print(` ${ui.color('\u2717', 'red')} Validation found issues`);
|
|
991
|
+
}
|
|
992
|
+
// Summary line
|
|
993
|
+
const parts = [];
|
|
994
|
+
if (summary.errors > 0) {
|
|
995
|
+
parts.push(ui.color(`${summary.errors} error(s)`, 'red'));
|
|
996
|
+
}
|
|
997
|
+
if (summary.warnings > 0) {
|
|
998
|
+
parts.push(ui.color(`${summary.warnings} warning(s)`, 'yellow'));
|
|
999
|
+
}
|
|
1000
|
+
if (summary.info > 0) {
|
|
1001
|
+
parts.push(ui.dim(`${summary.info} info`));
|
|
1002
|
+
}
|
|
1003
|
+
if (parts.length > 0) {
|
|
1004
|
+
ui.print(` Summary: ${parts.join(', ')}`);
|
|
1005
|
+
}
|
|
1006
|
+
// Tool-level results (grouped by rule prefix)
|
|
1007
|
+
const toolStatus = {
|
|
1008
|
+
'terraform-fmt': 'pass',
|
|
1009
|
+
'terraform-validate': 'pass',
|
|
1010
|
+
tflint: 'pass',
|
|
1011
|
+
checkov: 'pass',
|
|
1012
|
+
};
|
|
1013
|
+
for (const item of items) {
|
|
1014
|
+
if (item.severity === 'error' || item.severity === 'warning') {
|
|
1015
|
+
const rule = item.rule || '';
|
|
1016
|
+
if (rule.startsWith('fmt') || rule.includes('format')) {
|
|
1017
|
+
toolStatus['terraform-fmt'] = 'fail';
|
|
1018
|
+
}
|
|
1019
|
+
else if (rule.startsWith('hcl') || rule.includes('syntax')) {
|
|
1020
|
+
toolStatus['terraform-validate'] = 'fail';
|
|
1021
|
+
}
|
|
1022
|
+
else if (rule.startsWith('require-') || rule.includes('anti-pattern')) {
|
|
1023
|
+
toolStatus['tflint'] = 'fail';
|
|
1024
|
+
}
|
|
1025
|
+
else if (rule.startsWith('checkov') || rule.includes('security')) {
|
|
1026
|
+
toolStatus['checkov'] = 'fail';
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
ui.newLine();
|
|
1031
|
+
ui.print(' Tool Results:');
|
|
1032
|
+
for (const [tool, status] of Object.entries(toolStatus)) {
|
|
1033
|
+
const icon = status === 'pass'
|
|
1034
|
+
? ui.color('\u2713', 'green')
|
|
1035
|
+
: status === 'fail'
|
|
1036
|
+
? ui.color('\u2717', 'red')
|
|
1037
|
+
: ui.dim('-');
|
|
1038
|
+
const label = status === 'not-installed' ? ui.dim('not installed') : status;
|
|
1039
|
+
ui.print(` ${icon} ${tool}: ${label}`);
|
|
1040
|
+
}
|
|
1041
|
+
// Show first 5 error/warning details
|
|
1042
|
+
const significant = items.filter(i => i.severity === 'error' || i.severity === 'warning');
|
|
1043
|
+
if (significant.length > 0) {
|
|
1044
|
+
ui.newLine();
|
|
1045
|
+
ui.print(' Details:');
|
|
1046
|
+
const toShow = significant.slice(0, 5);
|
|
1047
|
+
for (const item of toShow) {
|
|
1048
|
+
const sevIcon = item.severity === 'error' ? ui.color('E', 'red') : ui.color('W', 'yellow');
|
|
1049
|
+
const fileInfo = item.file ? ` (${item.file})` : '';
|
|
1050
|
+
ui.print(` [${sevIcon}] ${item.message}${fileInfo}`);
|
|
1051
|
+
}
|
|
1052
|
+
if (significant.length > 5) {
|
|
1053
|
+
ui.print(ui.dim(` ... and ${significant.length - 5} more`));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Export as default command
|
|
1058
|
+
export default generateTerraformCommand;
|