@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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Command Tests — H2
|
|
3
|
+
*
|
|
4
|
+
* Validates provider auto-detection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import { detectProvider } from '../commands/pipeline';
|
|
12
|
+
|
|
13
|
+
describe('detectProvider (H2)', () => {
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-pipeline-test-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('detects GitHub Actions from .github/workflows directory', () => {
|
|
25
|
+
fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true });
|
|
26
|
+
expect(detectProvider(tmpDir)).toBe('github');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('detects GitLab CI from .gitlab-ci.yml', () => {
|
|
30
|
+
fs.writeFileSync(path.join(tmpDir, '.gitlab-ci.yml'), 'stages:\n - test\n', 'utf-8');
|
|
31
|
+
expect(detectProvider(tmpDir)).toBe('gitlab');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('detects CircleCI from .circleci/config.yml', () => {
|
|
35
|
+
fs.mkdirSync(path.join(tmpDir, '.circleci'), { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(tmpDir, '.circleci', 'config.yml'), 'version: 2\n', 'utf-8');
|
|
37
|
+
expect(detectProvider(tmpDir)).toBe('circleci');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('returns null when no CI config is found', () => {
|
|
41
|
+
expect(detectProvider(tmpDir)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('prefers GitHub over GitLab when both exist', () => {
|
|
45
|
+
fs.mkdirSync(path.join(tmpDir, '.github', 'workflows'), { recursive: true });
|
|
46
|
+
fs.writeFileSync(path.join(tmpDir, '.gitlab-ci.yml'), '{}', 'utf-8');
|
|
47
|
+
// GitHub is checked first
|
|
48
|
+
expect(detectProvider(tmpDir)).toBe('github');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polish Phase 3 Tests — M1, M3, M4, M5
|
|
3
|
+
*
|
|
4
|
+
* Tests for:
|
|
5
|
+
* M1 — dry-run mode (parseRunArgs + buildSystemPrompt)
|
|
6
|
+
* M3 — cost estimate/compare/report subcommands
|
|
7
|
+
* M4 — config primaryClouds key
|
|
8
|
+
* M5 — MCP add/list/remove in plugin.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import { parseRunArgs } from '../cli/run';
|
|
16
|
+
import { buildSystemPrompt } from '../agent/system-prompt';
|
|
17
|
+
import { CONFIG_KEYS } from '../config/types';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
|
|
25
|
+
// Patch os.homedir so MCP/plugin files go into a temp dir
|
|
26
|
+
vi.mock('node:os', async () => {
|
|
27
|
+
const actual = await vi.importActual<typeof os>('node:os');
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
homedir: () => tmpDir ?? actual.homedir(),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
async function getMcpModule() {
|
|
35
|
+
vi.resetModules();
|
|
36
|
+
return await import('../commands/plugin');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// M1 — Dry-run mode
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
describe('M1 — dry-run mode (parseRunArgs)', () => {
|
|
44
|
+
test('--dry-run sets dryRun=true', () => {
|
|
45
|
+
const result = parseRunArgs(['--dry-run', 'check infrastructure']);
|
|
46
|
+
expect(result.dryRun).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('--dry-run forces mode to "plan"', () => {
|
|
50
|
+
const result = parseRunArgs(['--dry-run', 'check everything']);
|
|
51
|
+
expect(result.mode).toBe('plan');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('--dry-run overrides --mode build to plan', () => {
|
|
55
|
+
const result = parseRunArgs(['--mode', 'build', '--dry-run', 'prompt']);
|
|
56
|
+
expect(result.mode).toBe('plan');
|
|
57
|
+
expect(result.dryRun).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('--dry-run overrides --mode deploy to plan', () => {
|
|
61
|
+
const result = parseRunArgs(['--mode', 'deploy', '--dry-run', 'prompt']);
|
|
62
|
+
expect(result.mode).toBe('plan');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('dryRun defaults to undefined when not specified', () => {
|
|
66
|
+
const result = parseRunArgs(['my prompt']);
|
|
67
|
+
expect(result.dryRun).toBeFalsy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('--dry-run preserves other flags', () => {
|
|
71
|
+
const result = parseRunArgs(['--dry-run', '--auto-approve', '--format', 'json', 'the prompt']);
|
|
72
|
+
expect(result.dryRun).toBe(true);
|
|
73
|
+
expect(result.autoApprove).toBe(true);
|
|
74
|
+
expect(result.format).toBe('json');
|
|
75
|
+
expect(result.prompt).toBe('the prompt');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('M1 — dry-run mode (buildSystemPrompt)', () => {
|
|
80
|
+
test('includes DRY-RUN MODE block when dryRun=true', () => {
|
|
81
|
+
const prompt = buildSystemPrompt({ mode: 'plan', tools: [], dryRun: true });
|
|
82
|
+
expect(prompt).toContain('DRY-RUN MODE');
|
|
83
|
+
expect(prompt).toContain('Do not execute any mutating operations');
|
|
84
|
+
expect(prompt).toContain('List exactly what you would do step by step');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('does not include DRY-RUN block when dryRun is false', () => {
|
|
88
|
+
const prompt = buildSystemPrompt({ mode: 'plan', tools: [], dryRun: false });
|
|
89
|
+
expect(prompt).not.toContain('DRY-RUN MODE');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('does not include DRY-RUN block when dryRun is omitted', () => {
|
|
93
|
+
const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
|
|
94
|
+
expect(prompt).not.toContain('DRY-RUN MODE');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('DRY-RUN block appears after environment context section', () => {
|
|
98
|
+
const prompt = buildSystemPrompt({ mode: 'plan', tools: [], dryRun: true });
|
|
99
|
+
const envIdx = prompt.indexOf('# Environment');
|
|
100
|
+
const dryRunIdx = prompt.indexOf('DRY-RUN MODE');
|
|
101
|
+
expect(envIdx).toBeGreaterThan(-1);
|
|
102
|
+
expect(dryRunIdx).toBeGreaterThan(envIdx);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// M3 — Cost estimate subcommand recognition
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('M3 — cost command subcommands', () => {
|
|
111
|
+
test('costCommand exports costCompareCommand function', async () => {
|
|
112
|
+
const mod = await import('../commands/cost/index');
|
|
113
|
+
expect(typeof mod.costCompareCommand).toBe('function');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('costCommand exports costReportCommand function', async () => {
|
|
117
|
+
const mod = await import('../commands/cost/index');
|
|
118
|
+
expect(typeof mod.costReportCommand).toBe('function');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('costReportCommand outputs JSON report when format is json', async () => {
|
|
122
|
+
const { costReportCommand } = await import('../commands/cost/index');
|
|
123
|
+
const logs: string[] = [];
|
|
124
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => logs.push(String(args[0])));
|
|
125
|
+
|
|
126
|
+
await costReportCommand('json');
|
|
127
|
+
|
|
128
|
+
vi.restoreAllMocks();
|
|
129
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
130
|
+
const parsed = JSON.parse(logs[0]) as Record<string, unknown>;
|
|
131
|
+
expect(parsed).toHaveProperty('generatedAt');
|
|
132
|
+
expect(parsed).toHaveProperty('format', 'json');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('costReportCommand outputs CSV header when format is csv', async () => {
|
|
136
|
+
const { costReportCommand } = await import('../commands/cost/index');
|
|
137
|
+
const logs: string[] = [];
|
|
138
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => logs.push(String(args[0])));
|
|
139
|
+
|
|
140
|
+
await costReportCommand('csv');
|
|
141
|
+
|
|
142
|
+
vi.restoreAllMocks();
|
|
143
|
+
expect(logs.some(l => l.includes('timestamp,description,cost_usd'))).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('costReportCommand outputs text summary by default', async () => {
|
|
147
|
+
const { costReportCommand } = await import('../commands/cost/index');
|
|
148
|
+
const outputs: string[] = [];
|
|
149
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
150
|
+
outputs.push(String(chunk));
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await costReportCommand('text');
|
|
155
|
+
|
|
156
|
+
vi.restoreAllMocks();
|
|
157
|
+
// Should not throw — text mode uses ui.print which writes to stdout
|
|
158
|
+
expect(true).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// M4 — Config primaryClouds key
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe('M4 — config primaryClouds key', () => {
|
|
167
|
+
test('CONFIG_KEYS contains primaryClouds entry', () => {
|
|
168
|
+
const entry = CONFIG_KEYS.find(k => k.key === 'primaryClouds');
|
|
169
|
+
expect(entry).toBeDefined();
|
|
170
|
+
expect(entry?.type).toBe('string');
|
|
171
|
+
expect(entry?.description).toContain('cloud');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('CONFIG_KEYS primaryClouds description mentions comma-separated', () => {
|
|
175
|
+
const entry = CONFIG_KEYS.find(k => k.key === 'primaryClouds');
|
|
176
|
+
expect(entry?.description).toMatch(/comma.?separated|aws.*gcp|gcp.*azure/i);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('CONFIG_KEYS contains model entry', () => {
|
|
180
|
+
const entry = CONFIG_KEYS.find(k => k.key === 'model');
|
|
181
|
+
expect(entry).toBeDefined();
|
|
182
|
+
expect(entry?.type).toBe('string');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('CONFIG_KEYS model description mentions known model IDs', () => {
|
|
186
|
+
const entry = CONFIG_KEYS.find(k => k.key === 'model');
|
|
187
|
+
expect(entry?.description).toContain('claude');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// M5 — MCP add/list/remove
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
describe('M5 — MCP server management', () => {
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-mcp-test-'));
|
|
198
|
+
vi.resetModules();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
203
|
+
vi.resetModules();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('mcpCommand is exported from plugin.ts', async () => {
|
|
207
|
+
const mod = await getMcpModule();
|
|
208
|
+
expect(typeof mod.mcpCommand).toBe('function');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('list shows no servers when config is absent', async () => {
|
|
212
|
+
const { mcpCommand } = await getMcpModule();
|
|
213
|
+
const outputs: string[] = [];
|
|
214
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
215
|
+
outputs.push(String(chunk));
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await mcpCommand('list', []);
|
|
220
|
+
vi.restoreAllMocks();
|
|
221
|
+
// Should not throw
|
|
222
|
+
expect(true).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('add writes server entry to mcp.json', async () => {
|
|
226
|
+
const { mcpCommand } = await getMcpModule();
|
|
227
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
228
|
+
|
|
229
|
+
await mcpCommand('add', ['npx -y @my/mcp-server', '--name', 'my-server']);
|
|
230
|
+
|
|
231
|
+
vi.restoreAllMocks();
|
|
232
|
+
|
|
233
|
+
const mcpFile = path.join(tmpDir, '.nimbus', 'mcp.json');
|
|
234
|
+
expect(fs.existsSync(mcpFile)).toBe(true);
|
|
235
|
+
const config = JSON.parse(fs.readFileSync(mcpFile, 'utf-8')) as { servers: Array<{ name: string }> };
|
|
236
|
+
expect(config.servers.some(s => s.name === 'my-server')).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('add stores correct command and args for npm package', async () => {
|
|
240
|
+
const { mcpCommand } = await getMcpModule();
|
|
241
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
242
|
+
|
|
243
|
+
await mcpCommand('add', ['npx -y @my/mcp-server', '--name', 'test-server']);
|
|
244
|
+
|
|
245
|
+
vi.restoreAllMocks();
|
|
246
|
+
|
|
247
|
+
const mcpFile = path.join(tmpDir, '.nimbus', 'mcp.json');
|
|
248
|
+
const config = JSON.parse(fs.readFileSync(mcpFile, 'utf-8')) as {
|
|
249
|
+
servers: Array<{ name: string; command: string; args: string[]; type: string }>;
|
|
250
|
+
};
|
|
251
|
+
const server = config.servers.find(s => s.name === 'test-server');
|
|
252
|
+
expect(server).toBeDefined();
|
|
253
|
+
expect(server?.command).toBe('npx');
|
|
254
|
+
expect(server?.args).toContain('-y');
|
|
255
|
+
expect(server?.type).toBe('stdio');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('add stores HTTP server with type=http', async () => {
|
|
259
|
+
const { mcpCommand } = await getMcpModule();
|
|
260
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
261
|
+
|
|
262
|
+
await mcpCommand('add', ['https://my.mcp.server.example.com', '--name', 'http-server']);
|
|
263
|
+
|
|
264
|
+
vi.restoreAllMocks();
|
|
265
|
+
|
|
266
|
+
const mcpFile = path.join(tmpDir, '.nimbus', 'mcp.json');
|
|
267
|
+
const config = JSON.parse(fs.readFileSync(mcpFile, 'utf-8')) as {
|
|
268
|
+
servers: Array<{ name: string; type: string; url: string }>;
|
|
269
|
+
};
|
|
270
|
+
const server = config.servers.find(s => s.name === 'http-server');
|
|
271
|
+
expect(server?.type).toBe('http');
|
|
272
|
+
expect(server?.url).toBe('https://my.mcp.server.example.com');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('add warns on duplicate name', async () => {
|
|
276
|
+
// Pre-create config with an existing entry
|
|
277
|
+
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
278
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
279
|
+
fs.writeFileSync(
|
|
280
|
+
path.join(nimbusDir, 'mcp.json'),
|
|
281
|
+
JSON.stringify({ servers: [{ name: 'existing', command: 'npx', args: ['existing'], type: 'stdio' }] }),
|
|
282
|
+
'utf-8'
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const { mcpCommand } = await getMcpModule();
|
|
286
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
287
|
+
|
|
288
|
+
// Should not throw or add duplicate
|
|
289
|
+
await mcpCommand('add', ['npx existing', '--name', 'existing']);
|
|
290
|
+
vi.restoreAllMocks();
|
|
291
|
+
|
|
292
|
+
const config = JSON.parse(
|
|
293
|
+
fs.readFileSync(path.join(nimbusDir, 'mcp.json'), 'utf-8')
|
|
294
|
+
) as { servers: unknown[] };
|
|
295
|
+
expect(config.servers).toHaveLength(1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('remove deletes server from mcp.json', async () => {
|
|
299
|
+
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
300
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
301
|
+
fs.writeFileSync(
|
|
302
|
+
path.join(nimbusDir, 'mcp.json'),
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
servers: [
|
|
305
|
+
{ name: 'server-a', command: 'npx', args: ['a'], type: 'stdio' },
|
|
306
|
+
{ name: 'server-b', command: 'npx', args: ['b'], type: 'stdio' },
|
|
307
|
+
],
|
|
308
|
+
}),
|
|
309
|
+
'utf-8'
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const { mcpCommand } = await getMcpModule();
|
|
313
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
314
|
+
|
|
315
|
+
await mcpCommand('remove', ['server-a']);
|
|
316
|
+
vi.restoreAllMocks();
|
|
317
|
+
|
|
318
|
+
const config = JSON.parse(
|
|
319
|
+
fs.readFileSync(path.join(nimbusDir, 'mcp.json'), 'utf-8')
|
|
320
|
+
) as { servers: Array<{ name: string }> };
|
|
321
|
+
expect(config.servers.some(s => s.name === 'server-a')).toBe(false);
|
|
322
|
+
expect(config.servers.some(s => s.name === 'server-b')).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('remove warns when server not found', async () => {
|
|
326
|
+
const { mcpCommand } = await getMcpModule();
|
|
327
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
328
|
+
|
|
329
|
+
// Should not throw
|
|
330
|
+
await expect(mcpCommand('remove', ['nonexistent-server'])).resolves.not.toThrow();
|
|
331
|
+
vi.restoreAllMocks();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('MCPServerEntry type is exported', async () => {
|
|
335
|
+
// Type test — verifies the export exists at runtime via typeof usage
|
|
336
|
+
const mod = await getMcpModule();
|
|
337
|
+
// mcpCommand should be a function (type shape check)
|
|
338
|
+
expect(typeof mod.mcpCommand).toBe('function');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Command Tests — H1
|
|
3
|
+
*
|
|
4
|
+
* Tests for the per-project credential profile management system.
|
|
5
|
+
* Profiles are stored in ~/.nimbus/profiles.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
|
|
13
|
+
// Mock homedir to use a temp directory so tests don't touch ~/.nimbus
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
vi.mock('node:os', async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof os>('node:os');
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
homedir: () => tmpDir ?? actual.homedir(),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function getProfileModule() {
|
|
25
|
+
// Force re-import so homedir mock is active
|
|
26
|
+
return await import('../commands/profile');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('profile store helpers (H1)', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-profile-test-'));
|
|
32
|
+
vi.resetModules();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
37
|
+
vi.resetModules();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('loadProfiles returns empty object when no file exists', async () => {
|
|
41
|
+
const { loadProfiles } = await getProfileModule();
|
|
42
|
+
const profiles = loadProfiles();
|
|
43
|
+
expect(profiles).toEqual({});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('saveProfiles then loadProfiles round-trips correctly', async () => {
|
|
47
|
+
const { loadProfiles, saveProfiles } = await getProfileModule();
|
|
48
|
+
const data = {
|
|
49
|
+
prod: { awsProfile: 'production', kubectlContext: 'prod-cluster', tfWorkspace: 'prod' },
|
|
50
|
+
staging: { awsProfile: 'staging', gcpProject: 'my-project-staging' },
|
|
51
|
+
};
|
|
52
|
+
saveProfiles(data);
|
|
53
|
+
|
|
54
|
+
const loaded = loadProfiles();
|
|
55
|
+
expect(loaded.prod?.awsProfile).toBe('production');
|
|
56
|
+
expect(loaded.prod?.kubectlContext).toBe('prod-cluster');
|
|
57
|
+
expect(loaded.prod?.tfWorkspace).toBe('prod');
|
|
58
|
+
expect(loaded.staging?.awsProfile).toBe('staging');
|
|
59
|
+
expect(loaded.staging?.gcpProject).toBe('my-project-staging');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('saveProfiles creates ~/.nimbus directory if missing', async () => {
|
|
63
|
+
const { saveProfiles } = await getProfileModule();
|
|
64
|
+
saveProfiles({ test: { awsProfile: 'test' } });
|
|
65
|
+
|
|
66
|
+
const profilesPath = path.join(tmpDir, '.nimbus', 'profiles.json');
|
|
67
|
+
expect(fs.existsSync(profilesPath)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('profiles.json format is valid JSON with proper structure', async () => {
|
|
71
|
+
const { saveProfiles } = await getProfileModule();
|
|
72
|
+
const data = {
|
|
73
|
+
myprofile: {
|
|
74
|
+
awsProfile: 'my-aws',
|
|
75
|
+
tfWorkspace: 'dev',
|
|
76
|
+
kubectlContext: 'dev-cluster',
|
|
77
|
+
gcpProject: 'my-gcp-project',
|
|
78
|
+
azureSubscription: 'my-sub-id',
|
|
79
|
+
k8sNamespace: 'default',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
saveProfiles(data);
|
|
83
|
+
|
|
84
|
+
const profilesPath = path.join(tmpDir, '.nimbus', 'profiles.json');
|
|
85
|
+
const raw = fs.readFileSync(profilesPath, 'utf-8');
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
expect(parsed.myprofile.awsProfile).toBe('my-aws');
|
|
88
|
+
expect(parsed.myprofile.tfWorkspace).toBe('dev');
|
|
89
|
+
expect(parsed.myprofile.kubectlContext).toBe('dev-cluster');
|
|
90
|
+
expect(parsed.myprofile.gcpProject).toBe('my-gcp-project');
|
|
91
|
+
expect(parsed.myprofile.azureSubscription).toBe('my-sub-id');
|
|
92
|
+
expect(parsed.myprofile.k8sNamespace).toBe('default');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('getCurrentProfileName returns null when no current profile set', async () => {
|
|
96
|
+
const { getCurrentProfileName } = await getProfileModule();
|
|
97
|
+
// Clear env var if set
|
|
98
|
+
delete process.env.NIMBUS_PROFILE;
|
|
99
|
+
const current = getCurrentProfileName();
|
|
100
|
+
expect(current).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('NIMBUS_PROFILE env var overrides current profile file', async () => {
|
|
104
|
+
process.env.NIMBUS_PROFILE = 'env-profile';
|
|
105
|
+
const { getCurrentProfileName } = await getProfileModule();
|
|
106
|
+
expect(getCurrentProfileName()).toBe('env-profile');
|
|
107
|
+
delete process.env.NIMBUS_PROFILE;
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('profileCommand subcommands (H1)', () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-profile-cmd-test-'));
|
|
114
|
+
vi.resetModules();
|
|
115
|
+
delete process.env.NIMBUS_PROFILE;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
120
|
+
vi.resetModules();
|
|
121
|
+
delete process.env.NIMBUS_PROFILE;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('list shows "No profiles" message when empty', async () => {
|
|
125
|
+
const { profileCommand } = await getProfileModule();
|
|
126
|
+
const logs: string[] = [];
|
|
127
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
128
|
+
vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
|
|
129
|
+
await expect(profileCommand('list', [])).resolves.not.toThrow();
|
|
130
|
+
vi.restoreAllMocks();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('profileCommand exports function', async () => {
|
|
134
|
+
const { profileCommand } = await getProfileModule();
|
|
135
|
+
expect(typeof profileCommand).toBe('function');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('CredentialProfile type accepts all fields', async () => {
|
|
139
|
+
const { loadProfiles, saveProfiles } = await getProfileModule();
|
|
140
|
+
const profile = {
|
|
141
|
+
awsProfile: 'aws-prod',
|
|
142
|
+
tfWorkspace: 'production',
|
|
143
|
+
kubectlContext: 'prod-k8s',
|
|
144
|
+
gcpProject: 'gcp-prod',
|
|
145
|
+
azureSubscription: 'az-sub',
|
|
146
|
+
k8sNamespace: 'production',
|
|
147
|
+
};
|
|
148
|
+
saveProfiles({ prod: profile });
|
|
149
|
+
const loaded = loadProfiles();
|
|
150
|
+
expect(loaded.prod).toEqual(profile);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('profile list subcommand does not throw', async () => {
|
|
154
|
+
const { profileCommand, saveProfiles } = await getProfileModule();
|
|
155
|
+
saveProfiles({ dev: { awsProfile: 'dev' }, prod: { awsProfile: 'prod' } });
|
|
156
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
157
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
158
|
+
await expect(profileCommand('list', [])).resolves.not.toThrow();
|
|
159
|
+
vi.restoreAllMocks();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('profile show subcommand displays profile JSON', async () => {
|
|
163
|
+
const { profileCommand, saveProfiles } = await getProfileModule();
|
|
164
|
+
saveProfiles({ myenv: { awsProfile: 'myenv-aws', tfWorkspace: 'myenv' } });
|
|
165
|
+
const logged: string[] = [];
|
|
166
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
167
|
+
vi.spyOn(console, 'log').mockImplementation((...args) => logged.push(args.join(' ')));
|
|
168
|
+
await expect(profileCommand('show', ['myenv'])).resolves.not.toThrow();
|
|
169
|
+
// console.log output should include the awsProfile value
|
|
170
|
+
const combined = logged.join('\n');
|
|
171
|
+
expect(combined).toContain('myenv-aws');
|
|
172
|
+
vi.restoreAllMocks();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('profile delete subcommand with missing profile does not crash', async () => {
|
|
176
|
+
const { profileCommand, saveProfiles } = await getProfileModule();
|
|
177
|
+
saveProfiles({});
|
|
178
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
179
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
180
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
181
|
+
// delete a non-existent profile should call process.exit(1)
|
|
182
|
+
const mockExit = vi.spyOn(process, 'exit').mockImplementation((_code) => { throw new Error('exit'); });
|
|
183
|
+
await expect(profileCommand('delete', ['nonexistent'])).rejects.toThrow('exit');
|
|
184
|
+
mockExit.mockRestore();
|
|
185
|
+
vi.restoreAllMocks();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('unknown subcommand shows usage without throwing', async () => {
|
|
189
|
+
const { profileCommand } = await getProfileModule();
|
|
190
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
191
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
192
|
+
await expect(profileCommand('unknown-sub', [])).resolves.not.toThrow();
|
|
193
|
+
vi.restoreAllMocks();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('profile lifecycle (H1)', () => {
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-profile-lifecycle-'));
|
|
200
|
+
vi.resetModules();
|
|
201
|
+
delete process.env.NIMBUS_PROFILE;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
206
|
+
vi.resetModules();
|
|
207
|
+
delete process.env.NIMBUS_PROFILE;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('save → load → overwrite → load cycle', async () => {
|
|
211
|
+
const { loadProfiles, saveProfiles } = await getProfileModule();
|
|
212
|
+
|
|
213
|
+
// Create
|
|
214
|
+
saveProfiles({ alpha: { awsProfile: 'alpha' } });
|
|
215
|
+
expect(loadProfiles().alpha?.awsProfile).toBe('alpha');
|
|
216
|
+
|
|
217
|
+
// Overwrite
|
|
218
|
+
saveProfiles({ alpha: { awsProfile: 'alpha-v2', tfWorkspace: 'ws' } });
|
|
219
|
+
const updated = loadProfiles();
|
|
220
|
+
expect(updated.alpha?.awsProfile).toBe('alpha-v2');
|
|
221
|
+
expect(updated.alpha?.tfWorkspace).toBe('ws');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('multiple profiles coexist in same file', async () => {
|
|
225
|
+
const { loadProfiles, saveProfiles } = await getProfileModule();
|
|
226
|
+
saveProfiles({
|
|
227
|
+
dev: { awsProfile: 'dev', tfWorkspace: 'dev' },
|
|
228
|
+
staging: { awsProfile: 'staging', kubectlContext: 'staging-k8s' },
|
|
229
|
+
prod: { awsProfile: 'prod', gcpProject: 'prod-gcp' },
|
|
230
|
+
});
|
|
231
|
+
const profiles = loadProfiles();
|
|
232
|
+
expect(Object.keys(profiles)).toHaveLength(3);
|
|
233
|
+
expect(profiles.dev?.awsProfile).toBe('dev');
|
|
234
|
+
expect(profiles.staging?.kubectlContext).toBe('staging-k8s');
|
|
235
|
+
expect(profiles.prod?.gcpProject).toBe('prod-gcp');
|
|
236
|
+
});
|
|
237
|
+
});
|