@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,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift Detection System
|
|
3
|
+
*
|
|
4
|
+
* Detects infrastructure drift between desired state (IaC) and actual state (cloud provider).
|
|
5
|
+
* Supports Terraform, Kubernetes, and Helm.
|
|
6
|
+
*
|
|
7
|
+
* Embedded version: replaces HTTP client calls with direct tool imports.
|
|
8
|
+
*/
|
|
9
|
+
import { logger } from '../utils';
|
|
10
|
+
import { TerraformOperations } from '../tools/terraform-ops';
|
|
11
|
+
import { KubernetesOperations } from '../tools/k8s-ops';
|
|
12
|
+
import { HelmOperations } from '../tools/helm-ops';
|
|
13
|
+
// ==========================================
|
|
14
|
+
// DriftDetector
|
|
15
|
+
// ==========================================
|
|
16
|
+
export class DriftDetector {
|
|
17
|
+
terraformOps;
|
|
18
|
+
constructor() {
|
|
19
|
+
// TerraformOperations is stateless — workDir is passed per-call
|
|
20
|
+
this.terraformOps = new TerraformOperations();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect drift based on provider type
|
|
24
|
+
*/
|
|
25
|
+
async detectDrift(options) {
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
logger.info(`Starting drift detection for ${options.provider} in ${options.workDir}`);
|
|
28
|
+
try {
|
|
29
|
+
switch (options.provider) {
|
|
30
|
+
case 'terraform':
|
|
31
|
+
return await this.detectTerraformDrift(options, startTime);
|
|
32
|
+
case 'kubernetes':
|
|
33
|
+
return await this.detectKubernetesDrift(options, startTime);
|
|
34
|
+
case 'helm':
|
|
35
|
+
return await this.detectHelmDrift(options, startTime);
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unsupported provider: ${options.provider}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error('Drift detection failed', error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect Terraform drift using terraform plan.
|
|
47
|
+
* Uses the embedded TerraformOperations directly — no HTTP round-trip.
|
|
48
|
+
*/
|
|
49
|
+
async detectTerraformDrift(options, startTime) {
|
|
50
|
+
const reportId = this.generateReportId();
|
|
51
|
+
const resources = [];
|
|
52
|
+
const errors = [];
|
|
53
|
+
// Build a TerraformOperations scoped to the working directory
|
|
54
|
+
const tfOps = new TerraformOperations(options.workDir);
|
|
55
|
+
try {
|
|
56
|
+
// Refresh state to get latest actual values
|
|
57
|
+
if (options.refresh !== false) {
|
|
58
|
+
logger.info('Refreshing Terraform state...');
|
|
59
|
+
try {
|
|
60
|
+
// terraform refresh is equivalent to plan -refresh-only; use plan with refresh flag
|
|
61
|
+
await tfOps.plan({ refresh: true, varFile: options.varFile });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
errors.push(`State refresh warning: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Run terraform plan to detect drift
|
|
68
|
+
logger.info('Running Terraform plan to detect drift...');
|
|
69
|
+
const planFile = `${options.workDir}/.drift-plan.tfplan`;
|
|
70
|
+
const planResult = await tfOps.plan({
|
|
71
|
+
varFile: options.varFile,
|
|
72
|
+
out: planFile,
|
|
73
|
+
target: options.targets,
|
|
74
|
+
});
|
|
75
|
+
if (planResult.hasChanges) {
|
|
76
|
+
// Parse the plan output text to extract basic drift information.
|
|
77
|
+
// The embedded TerraformOperations returns text output, not structured JSON,
|
|
78
|
+
// so we extract resource addresses using regex rather than JSON parsing.
|
|
79
|
+
const changeLines = planResult.output
|
|
80
|
+
.split('\n')
|
|
81
|
+
.filter(line => line.includes('will be') || line.includes('must be') || line.includes('resource "'));
|
|
82
|
+
for (const line of changeLines) {
|
|
83
|
+
// Extract resource addresses like: aws_vpc.main will be updated in-place
|
|
84
|
+
const match = line.match(/^\s*([\w.[\]"]+)\s+(?:will|must)\s+be\s+(\w+)/);
|
|
85
|
+
if (match) {
|
|
86
|
+
const address = match[1];
|
|
87
|
+
const action = match[2]; // created, updated, destroyed, replaced
|
|
88
|
+
const parts = address.split('.');
|
|
89
|
+
const resourceType = parts[0] || 'unknown';
|
|
90
|
+
const resourceName = parts[1] || address;
|
|
91
|
+
let driftType = 'unchanged';
|
|
92
|
+
if (action.startsWith('destroy') || action.startsWith('delet')) {
|
|
93
|
+
driftType = 'removed';
|
|
94
|
+
}
|
|
95
|
+
else if (action.startsWith('creat')) {
|
|
96
|
+
driftType = 'added';
|
|
97
|
+
}
|
|
98
|
+
else if (action.startsWith('updat') || action.startsWith('replac')) {
|
|
99
|
+
driftType = 'modified';
|
|
100
|
+
}
|
|
101
|
+
if (driftType !== 'unchanged') {
|
|
102
|
+
resources.push({
|
|
103
|
+
address,
|
|
104
|
+
provider: 'terraform',
|
|
105
|
+
resourceType,
|
|
106
|
+
drifts: [
|
|
107
|
+
{
|
|
108
|
+
resourceId: address,
|
|
109
|
+
resourceType,
|
|
110
|
+
resourceName,
|
|
111
|
+
driftType,
|
|
112
|
+
severity: this.determineSeverity(resourceType, ''),
|
|
113
|
+
expected: `Resource should be ${driftType === 'removed' ? 'present' : 'absent'}`,
|
|
114
|
+
actual: `Resource is ${driftType === 'removed' ? 'absent' : 'present'}`,
|
|
115
|
+
description: `Resource '${address}' ${action}`,
|
|
116
|
+
remediation: `Run 'terraform apply' to reconcile the drift`,
|
|
117
|
+
autoFixable: true,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
detectedAt: new Date(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const summary = this.calculateSummary(resources);
|
|
127
|
+
return {
|
|
128
|
+
id: reportId,
|
|
129
|
+
provider: 'terraform',
|
|
130
|
+
workDir: options.workDir,
|
|
131
|
+
environment: options.environment,
|
|
132
|
+
summary,
|
|
133
|
+
resources,
|
|
134
|
+
generatedAt: new Date(),
|
|
135
|
+
duration: Date.now() - startTime,
|
|
136
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error('Terraform drift detection failed', error);
|
|
141
|
+
throw new Error(`Terraform drift detection failed: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Detect Kubernetes drift by comparing manifests to actual state.
|
|
146
|
+
* Uses the embedded KubernetesOperations directly — no HTTP round-trip.
|
|
147
|
+
*/
|
|
148
|
+
async detectKubernetesDrift(options, startTime) {
|
|
149
|
+
const reportId = this.generateReportId();
|
|
150
|
+
const resources = [];
|
|
151
|
+
const errors = [];
|
|
152
|
+
try {
|
|
153
|
+
logger.info('Detecting Kubernetes drift...');
|
|
154
|
+
const diffs = await this.compareKubernetesManifests(options);
|
|
155
|
+
for (const diff of diffs) {
|
|
156
|
+
const resourceDrift = this.parseKubernetesDiff(diff);
|
|
157
|
+
if (resourceDrift.drifts.length > 0) {
|
|
158
|
+
resources.push(resourceDrift);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const summary = this.calculateSummary(resources);
|
|
162
|
+
return {
|
|
163
|
+
id: reportId,
|
|
164
|
+
provider: 'kubernetes',
|
|
165
|
+
workDir: options.workDir,
|
|
166
|
+
environment: options.environment,
|
|
167
|
+
summary,
|
|
168
|
+
resources,
|
|
169
|
+
generatedAt: new Date(),
|
|
170
|
+
duration: Date.now() - startTime,
|
|
171
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
logger.error('Kubernetes drift detection failed', error);
|
|
176
|
+
throw new Error(`Kubernetes drift detection failed: ${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Detect Helm drift by comparing deployed values to chart values.
|
|
181
|
+
* Uses the embedded HelmOperations directly — no HTTP round-trip.
|
|
182
|
+
*/
|
|
183
|
+
async detectHelmDrift(options, startTime) {
|
|
184
|
+
const reportId = this.generateReportId();
|
|
185
|
+
const resources = [];
|
|
186
|
+
const errors = [];
|
|
187
|
+
try {
|
|
188
|
+
logger.info('Detecting Helm drift...');
|
|
189
|
+
const diffs = await this.compareHelmReleases(options);
|
|
190
|
+
for (const diff of diffs) {
|
|
191
|
+
const resourceDrift = this.parseHelmDiff(diff);
|
|
192
|
+
if (resourceDrift.drifts.length > 0) {
|
|
193
|
+
resources.push(resourceDrift);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const summary = this.calculateSummary(resources);
|
|
197
|
+
return {
|
|
198
|
+
id: reportId,
|
|
199
|
+
provider: 'helm',
|
|
200
|
+
workDir: options.workDir,
|
|
201
|
+
environment: options.environment,
|
|
202
|
+
summary,
|
|
203
|
+
resources,
|
|
204
|
+
generatedAt: new Date(),
|
|
205
|
+
duration: Date.now() - startTime,
|
|
206
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
logger.error('Helm drift detection failed', error);
|
|
211
|
+
throw new Error(`Helm drift detection failed: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if a change is not related to drift (e.g., planned new resources).
|
|
216
|
+
* Only used when terraform JSON output is available.
|
|
217
|
+
*/
|
|
218
|
+
isNonDriftChange(change) {
|
|
219
|
+
// If the only action is "no-op", it's not drift
|
|
220
|
+
if (change.change.actions.length === 1 && change.change.actions[0] === 'no-op') {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
// If it's a create without prior state, it's not drift
|
|
224
|
+
if (change.change.actions.includes('create') && !change.change.before) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parse a structured Terraform change (JSON plan output) into ResourceDrift.
|
|
231
|
+
* Used when terraform show -json is available.
|
|
232
|
+
*/
|
|
233
|
+
parseTerraformChange(change) {
|
|
234
|
+
const drifts = [];
|
|
235
|
+
const actions = change.change.actions;
|
|
236
|
+
const before = change.change.before || {};
|
|
237
|
+
const after = change.change.after || {};
|
|
238
|
+
// Determine drift type based on actions
|
|
239
|
+
let driftType = 'unchanged';
|
|
240
|
+
if (actions.includes('delete')) {
|
|
241
|
+
driftType = 'removed';
|
|
242
|
+
}
|
|
243
|
+
else if (actions.includes('create')) {
|
|
244
|
+
driftType = 'added';
|
|
245
|
+
}
|
|
246
|
+
else if (actions.includes('update')) {
|
|
247
|
+
driftType = 'modified';
|
|
248
|
+
}
|
|
249
|
+
// Find specific attribute changes for modifications
|
|
250
|
+
if (driftType === 'modified') {
|
|
251
|
+
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
252
|
+
for (const key of allKeys) {
|
|
253
|
+
const beforeVal = before[key];
|
|
254
|
+
const afterVal = after[key];
|
|
255
|
+
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
|
|
256
|
+
drifts.push({
|
|
257
|
+
resourceId: change.address,
|
|
258
|
+
resourceType: change.type,
|
|
259
|
+
resourceName: change.name,
|
|
260
|
+
driftType: 'modified',
|
|
261
|
+
severity: this.determineSeverity(change.type, key),
|
|
262
|
+
expected: afterVal,
|
|
263
|
+
actual: beforeVal,
|
|
264
|
+
attribute: key,
|
|
265
|
+
description: `Attribute '${key}' has drifted from expected value`,
|
|
266
|
+
remediation: `Run 'terraform apply' to restore the expected value`,
|
|
267
|
+
autoFixable: true,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (driftType !== 'unchanged') {
|
|
273
|
+
// For added/removed resources, create a single drift item
|
|
274
|
+
drifts.push({
|
|
275
|
+
resourceId: change.address,
|
|
276
|
+
resourceType: change.type,
|
|
277
|
+
resourceName: change.name,
|
|
278
|
+
driftType,
|
|
279
|
+
severity: 'high',
|
|
280
|
+
expected: driftType === 'removed' ? before : after,
|
|
281
|
+
actual: driftType === 'removed' ? null : before,
|
|
282
|
+
description: `Resource ${driftType === 'removed' ? 'exists in state but not in config' : 'exists in config but not in state'}`,
|
|
283
|
+
remediation: `Run 'terraform apply' to ${driftType === 'removed' ? 'remove' : 'create'} the resource`,
|
|
284
|
+
autoFixable: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
address: change.address,
|
|
289
|
+
provider: 'terraform',
|
|
290
|
+
resourceType: change.type,
|
|
291
|
+
drifts,
|
|
292
|
+
detectedAt: new Date(),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Compare Kubernetes manifests to actual cluster state.
|
|
297
|
+
* Uses the embedded KubernetesOperations to query each resource.
|
|
298
|
+
*/
|
|
299
|
+
async compareKubernetesManifests(options) {
|
|
300
|
+
const diffs = [];
|
|
301
|
+
try {
|
|
302
|
+
const { readdir, readFile } = await import('fs/promises');
|
|
303
|
+
const { join } = await import('path');
|
|
304
|
+
const jsYaml = await import('js-yaml');
|
|
305
|
+
let files;
|
|
306
|
+
try {
|
|
307
|
+
files = await readdir(options.workDir);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
313
|
+
// Build a KubernetesOperations instance scoped to the provided kubeconfig/context
|
|
314
|
+
const k8sOps = new KubernetesOperations({
|
|
315
|
+
kubeconfig: options.kubeconfig,
|
|
316
|
+
context: options.context,
|
|
317
|
+
namespace: options.namespace,
|
|
318
|
+
});
|
|
319
|
+
for (const file of yamlFiles) {
|
|
320
|
+
try {
|
|
321
|
+
const content = await readFile(join(options.workDir, file), 'utf-8');
|
|
322
|
+
const docs = jsYaml.loadAll(content);
|
|
323
|
+
for (const doc of docs) {
|
|
324
|
+
if (!doc || !doc.kind || !doc.metadata?.name) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const namespace = doc.metadata.namespace || options.namespace || 'default';
|
|
328
|
+
try {
|
|
329
|
+
// Use the embedded KubernetesOperations.get() instead of HTTP fetch
|
|
330
|
+
const result = await k8sOps.get({
|
|
331
|
+
resource: `${doc.kind.toLowerCase()}s`,
|
|
332
|
+
name: doc.metadata.name,
|
|
333
|
+
namespace,
|
|
334
|
+
output: 'json',
|
|
335
|
+
});
|
|
336
|
+
if (result.success && result.output) {
|
|
337
|
+
const actual = JSON.parse(result.output);
|
|
338
|
+
const differences = this.deepCompare(doc.spec || {}, actual.spec || {}, 'spec');
|
|
339
|
+
if (differences.length > 0) {
|
|
340
|
+
diffs.push({
|
|
341
|
+
kind: doc.kind,
|
|
342
|
+
name: doc.metadata.name,
|
|
343
|
+
namespace,
|
|
344
|
+
differences,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Individual resource fetch failed — skip gracefully
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// File parse failed — skip
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Graceful degradation if filesystem or kubectl are unavailable
|
|
361
|
+
logger.warn('Kubernetes drift detection: unable to compare manifests, returning empty diff');
|
|
362
|
+
}
|
|
363
|
+
return diffs;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Deep compare two objects and return a flat list of differences.
|
|
367
|
+
*/
|
|
368
|
+
deepCompare(expected, actual, prefix) {
|
|
369
|
+
const differences = [];
|
|
370
|
+
const allKeys = new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
371
|
+
for (const key of allKeys) {
|
|
372
|
+
const path = `${prefix}.${key}`;
|
|
373
|
+
const exp = expected[key];
|
|
374
|
+
const act = actual[key];
|
|
375
|
+
if (exp !== null &&
|
|
376
|
+
act !== null &&
|
|
377
|
+
typeof exp === 'object' &&
|
|
378
|
+
typeof act === 'object' &&
|
|
379
|
+
!Array.isArray(exp) &&
|
|
380
|
+
!Array.isArray(act)) {
|
|
381
|
+
differences.push(...this.deepCompare(exp, act, path));
|
|
382
|
+
}
|
|
383
|
+
else if (JSON.stringify(exp) !== JSON.stringify(act)) {
|
|
384
|
+
differences.push({ path, expected: exp, actual: act });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return differences;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Parse a K8sResourceDiff into a ResourceDrift.
|
|
391
|
+
*/
|
|
392
|
+
parseKubernetesDiff(diff) {
|
|
393
|
+
const drifts = [];
|
|
394
|
+
for (const d of diff.differences) {
|
|
395
|
+
drifts.push({
|
|
396
|
+
resourceId: `${diff.kind}/${diff.namespace || 'default'}/${diff.name}`,
|
|
397
|
+
resourceType: diff.kind,
|
|
398
|
+
resourceName: diff.name,
|
|
399
|
+
driftType: 'modified',
|
|
400
|
+
severity: this.determineSeverity(diff.kind, d.path),
|
|
401
|
+
expected: d.expected,
|
|
402
|
+
actual: d.actual,
|
|
403
|
+
attribute: d.path,
|
|
404
|
+
description: `Attribute '${d.path}' has drifted`,
|
|
405
|
+
remediation: `Run 'kubectl apply' to restore the expected value`,
|
|
406
|
+
autoFixable: true,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
address: `${diff.kind}/${diff.namespace || 'default'}/${diff.name}`,
|
|
411
|
+
provider: 'kubernetes',
|
|
412
|
+
resourceType: diff.kind,
|
|
413
|
+
drifts,
|
|
414
|
+
detectedAt: new Date(),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compare Helm releases to local expected values.
|
|
419
|
+
* Uses the embedded HelmOperations directly — no HTTP round-trip.
|
|
420
|
+
*/
|
|
421
|
+
async compareHelmReleases(options) {
|
|
422
|
+
const diffs = [];
|
|
423
|
+
try {
|
|
424
|
+
// Build a HelmOperations instance scoped to the provided kubeconfig/context/namespace
|
|
425
|
+
const helmOps = new HelmOperations({
|
|
426
|
+
kubeconfig: options.kubeconfig,
|
|
427
|
+
kubeContext: options.context,
|
|
428
|
+
namespace: options.namespace,
|
|
429
|
+
});
|
|
430
|
+
// List all deployed releases in the target namespace
|
|
431
|
+
const listResult = await helmOps.list({
|
|
432
|
+
namespace: options.namespace || 'default',
|
|
433
|
+
});
|
|
434
|
+
if (!listResult.success || !listResult.output) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
let releases;
|
|
438
|
+
try {
|
|
439
|
+
releases = JSON.parse(listResult.output);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
// Read local expected values from workDir
|
|
445
|
+
const { readdir, readFile } = await import('fs/promises');
|
|
446
|
+
const { join } = await import('path');
|
|
447
|
+
const jsYaml = await import('js-yaml');
|
|
448
|
+
let localFiles;
|
|
449
|
+
try {
|
|
450
|
+
localFiles = await readdir(options.workDir);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
for (const release of releases) {
|
|
456
|
+
try {
|
|
457
|
+
// Get actual deployed values using embedded HelmOperations
|
|
458
|
+
const valuesResult = await helmOps.getValues({
|
|
459
|
+
name: release.name,
|
|
460
|
+
namespace: release.namespace,
|
|
461
|
+
});
|
|
462
|
+
if (!valuesResult.success || !valuesResult.output) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
let actualValues;
|
|
466
|
+
try {
|
|
467
|
+
actualValues = jsYaml.load(valuesResult.output) || {};
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Find matching local values file
|
|
473
|
+
const valuesFile = localFiles.find(f => f === `${release.name}-values.yaml` ||
|
|
474
|
+
f === `${release.name}.values.yaml` ||
|
|
475
|
+
f === 'values.yaml');
|
|
476
|
+
if (valuesFile) {
|
|
477
|
+
const localContent = await readFile(join(options.workDir, valuesFile), 'utf-8');
|
|
478
|
+
const expectedValues = jsYaml.load(localContent) || {};
|
|
479
|
+
const valuesDiff = [];
|
|
480
|
+
const allKeys = new Set([...Object.keys(expectedValues), ...Object.keys(actualValues)]);
|
|
481
|
+
for (const key of allKeys) {
|
|
482
|
+
const exp = expectedValues[key];
|
|
483
|
+
const act = actualValues[key];
|
|
484
|
+
if (JSON.stringify(exp) !== JSON.stringify(act)) {
|
|
485
|
+
valuesDiff.push({ path: key, expected: exp, actual: act });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (valuesDiff.length > 0) {
|
|
489
|
+
diffs.push({
|
|
490
|
+
name: release.name,
|
|
491
|
+
namespace: release.namespace,
|
|
492
|
+
valuesDiff,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// Individual release comparison failed — skip gracefully
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Graceful degradation if helm CLI is unavailable
|
|
504
|
+
logger.warn('Helm drift detection: unable to compare releases, returning empty diff');
|
|
505
|
+
}
|
|
506
|
+
return diffs;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Parse a HelmReleaseDiff into a ResourceDrift.
|
|
510
|
+
*/
|
|
511
|
+
parseHelmDiff(diff) {
|
|
512
|
+
const drifts = [];
|
|
513
|
+
// Check chart version drift
|
|
514
|
+
if (diff.chartVersion && diff.chartVersion.expected !== diff.chartVersion.actual) {
|
|
515
|
+
drifts.push({
|
|
516
|
+
resourceId: `${diff.namespace}/${diff.name}`,
|
|
517
|
+
resourceType: 'helm-release',
|
|
518
|
+
resourceName: diff.name,
|
|
519
|
+
driftType: 'modified',
|
|
520
|
+
severity: 'medium',
|
|
521
|
+
expected: diff.chartVersion.expected,
|
|
522
|
+
actual: diff.chartVersion.actual,
|
|
523
|
+
attribute: 'chartVersion',
|
|
524
|
+
description: `Chart version has drifted`,
|
|
525
|
+
remediation: `Run 'helm upgrade' to restore the expected version`,
|
|
526
|
+
autoFixable: true,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Check values drift
|
|
530
|
+
for (const v of diff.valuesDiff) {
|
|
531
|
+
drifts.push({
|
|
532
|
+
resourceId: `${diff.namespace}/${diff.name}`,
|
|
533
|
+
resourceType: 'helm-release',
|
|
534
|
+
resourceName: diff.name,
|
|
535
|
+
driftType: 'modified',
|
|
536
|
+
severity: 'medium',
|
|
537
|
+
expected: v.expected,
|
|
538
|
+
actual: v.actual,
|
|
539
|
+
attribute: v.path,
|
|
540
|
+
description: `Value '${v.path}' has drifted`,
|
|
541
|
+
remediation: `Run 'helm upgrade' with correct values`,
|
|
542
|
+
autoFixable: true,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
address: `${diff.namespace}/${diff.name}`,
|
|
547
|
+
provider: 'helm',
|
|
548
|
+
resourceType: 'helm-release',
|
|
549
|
+
drifts,
|
|
550
|
+
detectedAt: new Date(),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Determine drift severity based on resource type and attribute name.
|
|
555
|
+
*/
|
|
556
|
+
determineSeverity(resourceType, attribute) {
|
|
557
|
+
const criticalPatterns = [
|
|
558
|
+
'security_group',
|
|
559
|
+
'iam',
|
|
560
|
+
'policy',
|
|
561
|
+
'password',
|
|
562
|
+
'secret',
|
|
563
|
+
'key',
|
|
564
|
+
'encryption',
|
|
565
|
+
'kms',
|
|
566
|
+
];
|
|
567
|
+
const lowerType = resourceType.toLowerCase();
|
|
568
|
+
const lowerAttr = attribute.toLowerCase();
|
|
569
|
+
for (const pattern of criticalPatterns) {
|
|
570
|
+
if (lowerType.includes(pattern) || lowerAttr.includes(pattern)) {
|
|
571
|
+
return 'critical';
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const highPatterns = ['vpc', 'subnet', 'instance', 'cluster', 'node', 'ingress'];
|
|
575
|
+
for (const pattern of highPatterns) {
|
|
576
|
+
if (lowerType.includes(pattern)) {
|
|
577
|
+
return 'high';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const mediumPatterns = ['bucket', 'storage', 'config', 'database', 'rds'];
|
|
581
|
+
for (const pattern of mediumPatterns) {
|
|
582
|
+
if (lowerType.includes(pattern)) {
|
|
583
|
+
return 'medium';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Tag changes are usually low severity
|
|
587
|
+
if (lowerAttr === 'tags' || lowerAttr.includes('tag')) {
|
|
588
|
+
return 'low';
|
|
589
|
+
}
|
|
590
|
+
return 'medium';
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Calculate the summary metrics from a list of ResourceDrift objects.
|
|
594
|
+
*/
|
|
595
|
+
calculateSummary(resources) {
|
|
596
|
+
const byDriftType = {
|
|
597
|
+
added: 0,
|
|
598
|
+
removed: 0,
|
|
599
|
+
modified: 0,
|
|
600
|
+
unchanged: 0,
|
|
601
|
+
};
|
|
602
|
+
const bySeverity = {
|
|
603
|
+
critical: 0,
|
|
604
|
+
high: 0,
|
|
605
|
+
medium: 0,
|
|
606
|
+
low: 0,
|
|
607
|
+
info: 0,
|
|
608
|
+
};
|
|
609
|
+
let autoFixable = 0;
|
|
610
|
+
let driftedResources = 0;
|
|
611
|
+
for (const resource of resources) {
|
|
612
|
+
if (resource.drifts.length > 0) {
|
|
613
|
+
driftedResources++;
|
|
614
|
+
}
|
|
615
|
+
for (const drift of resource.drifts) {
|
|
616
|
+
byDriftType[drift.driftType]++;
|
|
617
|
+
bySeverity[drift.severity]++;
|
|
618
|
+
if (drift.autoFixable) {
|
|
619
|
+
autoFixable++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
totalResources: resources.length,
|
|
625
|
+
driftedResources,
|
|
626
|
+
unchangedResources: resources.length - driftedResources,
|
|
627
|
+
byDriftType,
|
|
628
|
+
bySeverity,
|
|
629
|
+
autoFixable,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Generate a unique report ID.
|
|
634
|
+
*/
|
|
635
|
+
generateReportId() {
|
|
636
|
+
return `drift_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Format a drift report as a Markdown string.
|
|
640
|
+
*/
|
|
641
|
+
formatReportAsMarkdown(report) {
|
|
642
|
+
const lines = [
|
|
643
|
+
`# Drift Detection Report`,
|
|
644
|
+
``,
|
|
645
|
+
`**Provider:** ${report.provider}`,
|
|
646
|
+
`**Working Directory:** ${report.workDir}`,
|
|
647
|
+
`**Environment:** ${report.environment || 'N/A'}`,
|
|
648
|
+
`**Generated:** ${report.generatedAt.toISOString()}`,
|
|
649
|
+
`**Duration:** ${report.duration}ms`,
|
|
650
|
+
``,
|
|
651
|
+
`## Summary`,
|
|
652
|
+
``,
|
|
653
|
+
`| Metric | Value |`,
|
|
654
|
+
`|--------|-------|`,
|
|
655
|
+
`| Total Resources | ${report.summary.totalResources} |`,
|
|
656
|
+
`| Drifted Resources | ${report.summary.driftedResources} |`,
|
|
657
|
+
`| Unchanged Resources | ${report.summary.unchangedResources} |`,
|
|
658
|
+
`| Auto-Fixable | ${report.summary.autoFixable} |`,
|
|
659
|
+
``,
|
|
660
|
+
`### By Severity`,
|
|
661
|
+
``,
|
|
662
|
+
`| Severity | Count |`,
|
|
663
|
+
`|----------|-------|`,
|
|
664
|
+
`| Critical | ${report.summary.bySeverity.critical} |`,
|
|
665
|
+
`| High | ${report.summary.bySeverity.high} |`,
|
|
666
|
+
`| Medium | ${report.summary.bySeverity.medium} |`,
|
|
667
|
+
`| Low | ${report.summary.bySeverity.low} |`,
|
|
668
|
+
`| Info | ${report.summary.bySeverity.info} |`,
|
|
669
|
+
``,
|
|
670
|
+
];
|
|
671
|
+
if (report.resources.length > 0) {
|
|
672
|
+
lines.push(`## Drifted Resources`, ``);
|
|
673
|
+
for (const resource of report.resources) {
|
|
674
|
+
lines.push(`### ${resource.address}`, ``);
|
|
675
|
+
for (const drift of resource.drifts) {
|
|
676
|
+
lines.push(`- **${drift.attribute || 'Resource'}** (${drift.severity})`, ` - Type: ${drift.driftType}`, ` - ${drift.description}`, ` - Remediation: ${drift.remediation}`, ` - Auto-fixable: ${drift.autoFixable ? 'Yes' : 'No'}`, ``);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (report.errors && report.errors.length > 0) {
|
|
681
|
+
lines.push(`## Errors`, ``);
|
|
682
|
+
for (const error of report.errors) {
|
|
683
|
+
lines.push(`- ${error}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return lines.join('\n');
|
|
687
|
+
}
|
|
688
|
+
}
|