@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
package/src/commands/doctor.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface DoctorOptions {
|
|
|
17
17
|
verbose?: boolean;
|
|
18
18
|
json?: boolean;
|
|
19
19
|
metrics?: boolean;
|
|
20
|
+
quiet?: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -127,35 +128,13 @@ async function checkLLMProvider(options: DoctorOptions): Promise<CheckResult> {
|
|
|
127
128
|
passed: false,
|
|
128
129
|
error: 'No LLM provider configured',
|
|
129
130
|
fix: 'Run "nimbus login" to configure an LLM provider',
|
|
131
|
+
runFix: async () => {
|
|
132
|
+
const { loginCommand } = await import('./login');
|
|
133
|
+
await loginCommand();
|
|
134
|
+
},
|
|
130
135
|
};
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
// Try to verify LLM service is reachable
|
|
134
|
-
const llmUrl = process.env.LLM_SERVICE_URL || 'http://localhost:3002';
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const response = await fetch(`${llmUrl}/health`, {
|
|
138
|
-
signal: AbortSignal.timeout(3000),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (response.ok) {
|
|
142
|
-
return {
|
|
143
|
-
name: 'LLM Provider',
|
|
144
|
-
passed: true,
|
|
145
|
-
message: 'LLM service connected',
|
|
146
|
-
details: options.verbose
|
|
147
|
-
? {
|
|
148
|
-
envKeys: foundKeys,
|
|
149
|
-
hasStoredCredentials,
|
|
150
|
-
serviceUrl: llmUrl,
|
|
151
|
-
}
|
|
152
|
-
: undefined,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// Service not available, but that's okay if we have credentials
|
|
157
|
-
}
|
|
158
|
-
|
|
159
138
|
return {
|
|
160
139
|
name: 'LLM Provider',
|
|
161
140
|
passed: true,
|
|
@@ -222,6 +201,12 @@ async function checkCloudCredentials(options: DoctorOptions): Promise<CheckResul
|
|
|
222
201
|
passed: false,
|
|
223
202
|
error: 'No cloud credentials found',
|
|
224
203
|
fix: 'Configure AWS credentials (~/.aws/credentials) or set environment variables',
|
|
204
|
+
runFix: async () => {
|
|
205
|
+
ui.info('To configure cloud credentials, run one of:');
|
|
206
|
+
ui.print(' AWS: nimbus login --cloud aws (runs aws configure)');
|
|
207
|
+
ui.print(' GCP: nimbus login --cloud gcp (runs gcloud auth login)');
|
|
208
|
+
ui.print(' Azure: nimbus login --cloud azure (runs az login)');
|
|
209
|
+
},
|
|
225
210
|
};
|
|
226
211
|
}
|
|
227
212
|
|
|
@@ -340,6 +325,25 @@ async function checkCloudConnectivity(options: DoctorOptions): Promise<CheckResu
|
|
|
340
325
|
.filter(Boolean)
|
|
341
326
|
.join('; '),
|
|
342
327
|
details: options.verbose ? { providers: results } : undefined,
|
|
328
|
+
runFix: async () => {
|
|
329
|
+
const { execFileSync } = await import('child_process');
|
|
330
|
+
// Try AWS SSO refresh
|
|
331
|
+
const awsFailed = results.find(r => r.provider === 'AWS' && r.status === 'failed');
|
|
332
|
+
if (awsFailed) {
|
|
333
|
+
ui.info('Attempting AWS SSO login...');
|
|
334
|
+
try {
|
|
335
|
+
execFileSync('aws', ['sso', 'login'], { stdio: 'inherit', timeout: 120000 });
|
|
336
|
+
} catch { ui.warning('AWS SSO login failed. Run `aws configure` manually.'); }
|
|
337
|
+
}
|
|
338
|
+
// Try GCP refresh
|
|
339
|
+
const gcpFailed = results.find(r => r.provider === 'GCP' && r.status === 'failed');
|
|
340
|
+
if (gcpFailed) {
|
|
341
|
+
ui.info('Attempting GCP application-default login...');
|
|
342
|
+
try {
|
|
343
|
+
execFileSync('gcloud', ['auth', 'application-default', 'login'], { stdio: 'inherit', timeout: 120000 });
|
|
344
|
+
} catch { ui.warning('GCP login failed. Run `gcloud auth login` manually.'); }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
343
347
|
};
|
|
344
348
|
}
|
|
345
349
|
|
|
@@ -352,105 +356,184 @@ async function checkCloudConnectivity(options: DoctorOptions): Promise<CheckResu
|
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
/**
|
|
355
|
-
* Check core
|
|
359
|
+
* Check embedded core systems (SQLite database + LLM auth + tool registry)
|
|
356
360
|
*/
|
|
357
361
|
async function checkCoreServices(options: DoctorOptions): Promise<CheckResult> {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
{ name: 'Generator', url: process.env.GENERATOR_SERVICE_URL || 'http://localhost:3003' },
|
|
362
|
-
];
|
|
362
|
+
const fs = await import('fs/promises');
|
|
363
|
+
const path = await import('path');
|
|
364
|
+
const os = await import('os');
|
|
363
365
|
|
|
364
|
-
const results: Array<{ name: string; status: string;
|
|
365
|
-
let anyAvailable = false;
|
|
366
|
+
const results: Array<{ name: string; status: string; details?: string }> = [];
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
// Check SQLite database
|
|
369
|
+
const dbPath = path.join(os.homedir(), '.nimbus', 'nimbus.db');
|
|
370
|
+
try {
|
|
371
|
+
await fs.access(dbPath);
|
|
372
|
+
const stat = await fs.stat(dbPath);
|
|
373
|
+
results.push({
|
|
374
|
+
name: 'SQLite DB',
|
|
375
|
+
status: 'ok',
|
|
376
|
+
details: options.verbose ? `${dbPath} (${(stat.size / 1024).toFixed(1)} KB)` : undefined,
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
results.push({
|
|
380
|
+
name: 'SQLite DB',
|
|
381
|
+
status: 'not initialized',
|
|
382
|
+
details: 'Will be created on first use',
|
|
383
|
+
});
|
|
384
|
+
}
|
|
372
385
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
// Check LLM credentials
|
|
387
|
+
const credFile = path.join(os.homedir(), '.nimbus', 'credentials.json');
|
|
388
|
+
let llmStatus = 'not configured';
|
|
389
|
+
let llmDetails: string | undefined;
|
|
390
|
+
try {
|
|
391
|
+
const content = await fs.readFile(credFile, 'utf-8');
|
|
392
|
+
const creds = JSON.parse(content);
|
|
393
|
+
const providers = Object.keys(creds.providers || {});
|
|
394
|
+
if (providers.length > 0) {
|
|
395
|
+
llmStatus = 'configured';
|
|
396
|
+
llmDetails = options.verbose ? `Providers: ${providers.join(', ')}` : undefined;
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// Check env vars as fallback
|
|
400
|
+
const envKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'AWS_ACCESS_KEY_ID'];
|
|
401
|
+
const found = envKeys.filter(k => process.env[k]);
|
|
402
|
+
if (found.length > 0) {
|
|
403
|
+
llmStatus = 'via env vars';
|
|
404
|
+
llmDetails = options.verbose ? found.join(', ') : undefined;
|
|
385
405
|
}
|
|
386
406
|
}
|
|
407
|
+
results.push({ name: 'LLM Auth', status: llmStatus, details: llmDetails });
|
|
387
408
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
409
|
+
// Check tool registry (Nimbus built-in tools)
|
|
410
|
+
try {
|
|
411
|
+
const { standardTools } = await import('../tools/schemas/standard');
|
|
412
|
+
const { devopsTools } = await import('../tools/schemas/devops');
|
|
413
|
+
// Count expected tools
|
|
414
|
+
const expectedCount = standardTools.length + devopsTools.length;
|
|
415
|
+
results.push({
|
|
416
|
+
name: 'Tool Registry',
|
|
417
|
+
status: 'ok',
|
|
418
|
+
details: options.verbose ? `${expectedCount} tools available` : undefined,
|
|
419
|
+
});
|
|
420
|
+
} catch (e: any) {
|
|
421
|
+
results.push({ name: 'Tool Registry', status: 'error', details: e.message });
|
|
398
422
|
}
|
|
399
423
|
|
|
400
|
-
const
|
|
424
|
+
const failed = results.filter(r => r.status === 'error' || r.status === 'not configured');
|
|
425
|
+
const passed = failed.length === 0;
|
|
426
|
+
|
|
427
|
+
const summary = results.map(r => `${r.name}: ${r.status}`).join(', ');
|
|
401
428
|
|
|
402
429
|
return {
|
|
403
|
-
name: 'Core
|
|
404
|
-
passed
|
|
405
|
-
message:
|
|
406
|
-
details: options.verbose ? {
|
|
430
|
+
name: 'Core Systems',
|
|
431
|
+
passed,
|
|
432
|
+
message: passed ? summary : `Issues: ${failed.map(r => r.name).join(', ')}`,
|
|
433
|
+
details: options.verbose ? { systems: results } : undefined,
|
|
407
434
|
};
|
|
408
435
|
}
|
|
409
436
|
|
|
410
437
|
/**
|
|
411
|
-
* Check
|
|
438
|
+
* Check DevOps CLI tools availability (terraform, kubectl, helm, aws)
|
|
412
439
|
*/
|
|
413
440
|
async function checkToolServices(options: DoctorOptions): Promise<CheckResult> {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
{ name: '
|
|
418
|
-
{ name: '
|
|
419
|
-
{ name: '
|
|
420
|
-
{ name: '
|
|
421
|
-
{ name: '
|
|
422
|
-
{ name: '
|
|
441
|
+
const { execFileSync } = await import('child_process');
|
|
442
|
+
|
|
443
|
+
const devopsTools = [
|
|
444
|
+
{ name: 'terraform', cmd: 'terraform', args: ['version', '-json'] },
|
|
445
|
+
{ name: 'kubectl', cmd: 'kubectl', args: ['version', '--client', '--output=json'] },
|
|
446
|
+
{ name: 'helm', cmd: 'helm', args: ['version', '--short'] },
|
|
447
|
+
{ name: 'aws', cmd: 'aws', args: ['--version'] },
|
|
448
|
+
{ name: 'gcloud', cmd: 'gcloud', args: ['version', '--format=json'] },
|
|
449
|
+
{ name: 'az', cmd: 'az', args: ['version', '--output=json'] },
|
|
423
450
|
];
|
|
424
451
|
|
|
425
|
-
const results: Array<{ name: string;
|
|
452
|
+
const results: Array<{ name: string; version: string; available: boolean }> = [];
|
|
426
453
|
|
|
427
|
-
for (const
|
|
454
|
+
for (const tool of devopsTools) {
|
|
428
455
|
try {
|
|
429
|
-
const
|
|
430
|
-
|
|
456
|
+
const output = execFileSync(tool.cmd, tool.args, {
|
|
457
|
+
encoding: 'utf-8',
|
|
458
|
+
timeout: 5000,
|
|
459
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
431
460
|
});
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
461
|
+
// Extract version number
|
|
462
|
+
let version = 'installed';
|
|
463
|
+
try {
|
|
464
|
+
const parsed = JSON.parse(output);
|
|
465
|
+
// terraform: { terraform_version: "1.7.0" }, kubectl: { clientVersion: { gitVersion: "v1.28.0" } }
|
|
466
|
+
version = parsed.terraform_version || parsed.clientVersion?.gitVersion || 'installed';
|
|
467
|
+
} catch {
|
|
468
|
+
const match = output.match(/[\d]+\.[\d]+\.[\d]+/);
|
|
469
|
+
if (match) version = match[0];
|
|
437
470
|
}
|
|
471
|
+
results.push({ name: tool.name, version, available: true });
|
|
438
472
|
} catch {
|
|
439
|
-
results.push({ name:
|
|
473
|
+
results.push({ name: tool.name, version: 'not found', available: false });
|
|
440
474
|
}
|
|
441
475
|
}
|
|
442
476
|
|
|
443
|
-
const
|
|
477
|
+
const available = results.filter(r => r.available);
|
|
478
|
+
const missing = results.filter(r => !r.available);
|
|
479
|
+
|
|
480
|
+
// GAP-12: OS-aware runFix — actually installs missing tools via Homebrew on macOS
|
|
481
|
+
const BREW_INSTALL: Record<string, string> = {
|
|
482
|
+
terraform: 'terraform',
|
|
483
|
+
kubectl: 'kubernetes-cli',
|
|
484
|
+
helm: 'helm',
|
|
485
|
+
aws: 'awscli',
|
|
486
|
+
gcloud: '--cask google-cloud-sdk',
|
|
487
|
+
az: 'azure-cli',
|
|
488
|
+
};
|
|
489
|
+
const INSTALL_URLS: Record<string, string> = {
|
|
490
|
+
terraform: 'https://developer.hashicorp.com/terraform/install',
|
|
491
|
+
kubectl: 'https://kubernetes.io/docs/tasks/tools/',
|
|
492
|
+
helm: 'https://helm.sh/docs/intro/install/',
|
|
493
|
+
aws: 'https://aws.amazon.com/cli/',
|
|
494
|
+
gcloud: 'https://cloud.google.com/sdk/docs/install',
|
|
495
|
+
az: 'https://learn.microsoft.com/en-us/cli/azure/install-azure-cli',
|
|
496
|
+
};
|
|
497
|
+
const osAwareRunFix = async () => {
|
|
498
|
+
const { execFileSync: brew } = await import('child_process');
|
|
499
|
+
const isMac = process.platform === 'darwin';
|
|
500
|
+
const isLinux = process.platform === 'linux';
|
|
501
|
+
for (const tool of missing) {
|
|
502
|
+
const toolName = tool.name;
|
|
503
|
+
if (isMac && BREW_INSTALL[toolName]) {
|
|
504
|
+
ui.print(`Installing ${toolName} via Homebrew...`);
|
|
505
|
+
try {
|
|
506
|
+
const brewArgs = ['install', ...BREW_INSTALL[toolName].split(' ')];
|
|
507
|
+
brew('brew', brewArgs, { stdio: 'inherit', timeout: 120_000 });
|
|
508
|
+
ui.success(`${toolName} installed successfully`);
|
|
509
|
+
} catch (brewErr) {
|
|
510
|
+
ui.warning(`brew install failed for ${toolName}: ${brewErr instanceof Error ? brewErr.message : String(brewErr)}`);
|
|
511
|
+
ui.print(` Manual install: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
512
|
+
}
|
|
513
|
+
} else if (isLinux) {
|
|
514
|
+
ui.print(` ${toolName}: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
515
|
+
} else {
|
|
516
|
+
ui.print(` ${toolName}: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (available.length === 0) {
|
|
522
|
+
return {
|
|
523
|
+
name: 'DevOps Tools',
|
|
524
|
+
passed: false,
|
|
525
|
+
error: 'No DevOps CLI tools found (terraform, kubectl, helm, aws, gcloud, az)',
|
|
526
|
+
fix: 'Install at least one: terraform, kubectl, or helm',
|
|
527
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
528
|
+
runFix: osAwareRunFix,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
444
531
|
|
|
445
|
-
// Tool services are optional - the CLI has local fallbacks
|
|
446
532
|
return {
|
|
447
|
-
name: '
|
|
533
|
+
name: 'DevOps Tools',
|
|
448
534
|
passed: true,
|
|
449
|
-
message:
|
|
450
|
-
|
|
451
|
-
? `${runningCount}/${services.length} services running`
|
|
452
|
-
: 'Using local tools (services unavailable)',
|
|
453
|
-
details: options.verbose ? { services: results } : undefined,
|
|
535
|
+
message: `${available.length}/${devopsTools.length} available: ${available.map(t => `${t.name} ${t.version}`).join(', ')}${missing.length > 0 ? ` | missing: ${missing.map(t => t.name).join(', ')}` : ''}`,
|
|
536
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
454
537
|
};
|
|
455
538
|
}
|
|
456
539
|
|
|
@@ -513,6 +596,24 @@ async function checkDependencies(options: DoctorOptions): Promise<CheckResult> {
|
|
|
513
596
|
passed: true,
|
|
514
597
|
message: `${availableCount}/${tools.length} tools available`,
|
|
515
598
|
details: options.verbose ? { tools: results } : undefined,
|
|
599
|
+
// G21: runFix checks for .tf files without .terraform/ and suggests terraform init
|
|
600
|
+
runFix: async () => {
|
|
601
|
+
const fs = await import('fs/promises');
|
|
602
|
+
const path = await import('path');
|
|
603
|
+
const cwd = process.cwd();
|
|
604
|
+
|
|
605
|
+
// Check for .tf files without .terraform dir
|
|
606
|
+
try {
|
|
607
|
+
const entries = await fs.readdir(cwd);
|
|
608
|
+
const hasTfFiles = entries.some(e => e.endsWith('.tf'));
|
|
609
|
+
const hasTerraformDir = entries.includes('.terraform');
|
|
610
|
+
|
|
611
|
+
if (hasTfFiles && !hasTerraformDir) {
|
|
612
|
+
ui.info('Found .tf files without .terraform/ directory. Run:');
|
|
613
|
+
ui.print(' terraform init');
|
|
614
|
+
}
|
|
615
|
+
} catch { /* ignore */ }
|
|
616
|
+
},
|
|
516
617
|
};
|
|
517
618
|
}
|
|
518
619
|
|
|
@@ -630,64 +731,561 @@ async function checkNetwork(options: DoctorOptions): Promise<CheckResult> {
|
|
|
630
731
|
};
|
|
631
732
|
}
|
|
632
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Check Docker daemon availability (C1/L10)
|
|
736
|
+
*/
|
|
737
|
+
async function checkDockerDaemon(_options: DoctorOptions): Promise<CheckResult> {
|
|
738
|
+
const { execFileSync } = await import('child_process');
|
|
739
|
+
try {
|
|
740
|
+
execFileSync('docker', ['info'], { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
741
|
+
return { name: 'Docker Daemon', passed: true, message: 'Docker daemon running' };
|
|
742
|
+
} catch {
|
|
743
|
+
try {
|
|
744
|
+
// Just check if docker binary exists
|
|
745
|
+
execFileSync('docker', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
746
|
+
return {
|
|
747
|
+
name: 'Docker Daemon',
|
|
748
|
+
passed: false,
|
|
749
|
+
error: 'Docker installed but daemon not running',
|
|
750
|
+
fix: 'Start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux)',
|
|
751
|
+
};
|
|
752
|
+
} catch {
|
|
753
|
+
return { name: 'Docker Daemon', passed: false, error: 'Docker not installed', fix: 'Install Docker Desktop from https://www.docker.com' };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Check Vault CLI and status (C2/L10)
|
|
760
|
+
*/
|
|
761
|
+
async function checkVault(_options: DoctorOptions): Promise<CheckResult> {
|
|
762
|
+
const { execFileSync } = await import('child_process');
|
|
763
|
+
try {
|
|
764
|
+
execFileSync('vault', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
765
|
+
if (process.env.VAULT_ADDR) {
|
|
766
|
+
try {
|
|
767
|
+
const out = execFileSync('vault', ['status', '-format=json'], {
|
|
768
|
+
encoding: 'utf-8',
|
|
769
|
+
timeout: 5000,
|
|
770
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
771
|
+
env: process.env,
|
|
772
|
+
});
|
|
773
|
+
const status = JSON.parse(out);
|
|
774
|
+
if (status.sealed) {
|
|
775
|
+
return { name: 'Vault', passed: false, error: 'Vault is sealed', fix: 'Run `vault operator unseal`' };
|
|
776
|
+
}
|
|
777
|
+
return { name: 'Vault', passed: true, message: `Vault available at ${process.env.VAULT_ADDR} (unsealed)` };
|
|
778
|
+
} catch {
|
|
779
|
+
return { name: 'Vault', passed: false, error: `Cannot reach Vault at ${process.env.VAULT_ADDR}`, fix: 'Check VAULT_ADDR and network connectivity' };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { name: 'Vault', passed: true, message: 'vault CLI installed (VAULT_ADDR not set)' };
|
|
783
|
+
} catch {
|
|
784
|
+
return { name: 'Vault', passed: true, message: 'vault CLI not installed (optional)' };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Check CI/CD CLIs: gh, glab, circleci (C3/L10)
|
|
790
|
+
*/
|
|
791
|
+
async function checkCICDCLIs(_options: DoctorOptions): Promise<CheckResult> {
|
|
792
|
+
const { execFileSync } = await import('child_process');
|
|
793
|
+
const clis = [
|
|
794
|
+
{ name: 'gh (GitHub CLI)', cmd: 'gh', args: ['--version'] },
|
|
795
|
+
{ name: 'glab (GitLab CLI)', cmd: 'glab', args: ['--version'] },
|
|
796
|
+
{ name: 'circleci CLI', cmd: 'circleci', args: ['--version'] },
|
|
797
|
+
];
|
|
798
|
+
const found: string[] = [];
|
|
799
|
+
for (const cli of clis) {
|
|
800
|
+
try {
|
|
801
|
+
execFileSync(cli.cmd, cli.args, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
802
|
+
found.push(cli.name);
|
|
803
|
+
} catch { /* not installed */ }
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
name: 'CI/CD CLIs',
|
|
807
|
+
passed: true,
|
|
808
|
+
message: found.length > 0 ? `Found: ${found.join(', ')}` : 'No CI/CD CLIs installed (gh, glab, circleci are optional)',
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Check GitOps CLIs: argocd, flux (H2/L10)
|
|
814
|
+
*/
|
|
815
|
+
async function checkGitOpsCLIs(_options: DoctorOptions): Promise<CheckResult> {
|
|
816
|
+
const { execFileSync } = await import('child_process');
|
|
817
|
+
const clis = [
|
|
818
|
+
{ name: 'argocd', cmd: 'argocd', args: ['version', '--client'] },
|
|
819
|
+
{ name: 'flux', cmd: 'flux', args: ['--version'] },
|
|
820
|
+
];
|
|
821
|
+
const found: string[] = [];
|
|
822
|
+
for (const cli of clis) {
|
|
823
|
+
try {
|
|
824
|
+
execFileSync(cli.cmd, cli.args, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
825
|
+
found.push(cli.name);
|
|
826
|
+
} catch { /* not installed */ }
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
name: 'GitOps CLIs',
|
|
830
|
+
passed: true,
|
|
831
|
+
message: found.length > 0 ? `Found: ${found.join(', ')}` : 'No GitOps CLIs installed (argocd, flux are optional)',
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Pre-flight checks for common DevOps issues (L10)
|
|
837
|
+
*/
|
|
838
|
+
async function checkDevOpsPreFlight(options: DoctorOptions): Promise<CheckResult> {
|
|
839
|
+
const { execFileSync } = await import('child_process');
|
|
840
|
+
const issues: string[] = [];
|
|
841
|
+
const hints: string[] = [];
|
|
842
|
+
|
|
843
|
+
// kubectl cluster reachability
|
|
844
|
+
try {
|
|
845
|
+
execFileSync('kubectl', ['cluster-info', '--request-timeout=5s'], {
|
|
846
|
+
encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
847
|
+
});
|
|
848
|
+
} catch (e) {
|
|
849
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
850
|
+
if (!msg.includes('not found') && !msg.includes('ENOENT')) {
|
|
851
|
+
issues.push('kubectl: cannot reach cluster');
|
|
852
|
+
hints.push('Check kubectl context: `kubectl config current-context`');
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// helm repos
|
|
857
|
+
try {
|
|
858
|
+
const out = execFileSync('helm', ['repo', 'list', '-o', 'json'], {
|
|
859
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
860
|
+
});
|
|
861
|
+
const repos = JSON.parse(out || '[]');
|
|
862
|
+
if (!Array.isArray(repos) || repos.length === 0) {
|
|
863
|
+
hints.push('No Helm repos configured. Add one: `helm repo add stable https://charts.helm.sh/stable`');
|
|
864
|
+
}
|
|
865
|
+
} catch { /* helm not installed or no repos */ }
|
|
866
|
+
|
|
867
|
+
// GCP project
|
|
868
|
+
if (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.CLOUDSDK_CORE_PROJECT) {
|
|
869
|
+
try {
|
|
870
|
+
const proj = execFileSync('gcloud', ['config', 'get-value', 'project'], {
|
|
871
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
872
|
+
}).trim();
|
|
873
|
+
if (!proj || proj === '(unset)') {
|
|
874
|
+
hints.push('GCP project not set. Run: `gcloud config set project <PROJECT_ID>`');
|
|
875
|
+
}
|
|
876
|
+
} catch { /* gcloud not installed */ }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (options.fix) {
|
|
880
|
+
// Auto-fix: helm repo update
|
|
881
|
+
try {
|
|
882
|
+
execFileSync('helm', ['repo', 'update'], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
883
|
+
} catch { /* ignore */ }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (issues.length > 0) {
|
|
887
|
+
return {
|
|
888
|
+
name: 'DevOps Pre-flight',
|
|
889
|
+
passed: false,
|
|
890
|
+
error: issues.join('; '),
|
|
891
|
+
fix: hints.join(' | '),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
name: 'DevOps Pre-flight',
|
|
897
|
+
passed: true,
|
|
898
|
+
message: hints.length > 0 ? `OK (warnings: ${hints.join('; ')})` : 'All pre-flight checks passed',
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** M5: Check helm-secrets plugin and sops availability */
|
|
903
|
+
async function checkHelmSecrets(_options: DoctorOptions): Promise<CheckResult> {
|
|
904
|
+
const { execFileSync } = await import('child_process');
|
|
905
|
+
const warnings: string[] = [];
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
const out = execFileSync('helm', ['plugin', 'list'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
909
|
+
if (!out.includes('secrets')) {
|
|
910
|
+
warnings.push('helm-secrets plugin not installed (run: helm plugin install https://github.com/jkroepke/helm-secrets)');
|
|
911
|
+
}
|
|
912
|
+
} catch {
|
|
913
|
+
warnings.push('helm not available — cannot check helm-secrets plugin');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
execFileSync('sops', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
918
|
+
} catch {
|
|
919
|
+
warnings.push('sops not installed (run: brew install sops)');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
name: 'Helm Secrets (M5)',
|
|
924
|
+
passed: true,
|
|
925
|
+
message: warnings.length > 0
|
|
926
|
+
? `Optional: ${warnings.join('; ')}`
|
|
927
|
+
: 'helm-secrets plugin and sops are available',
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* H6: Check Terraform infrastructure context
|
|
934
|
+
*/
|
|
935
|
+
async function checkInfraContext(): Promise<CheckResult> {
|
|
936
|
+
const { existsSync } = await import('node:fs');
|
|
937
|
+
const { join } = await import('node:path');
|
|
938
|
+
const { exec } = await import('node:child_process');
|
|
939
|
+
const { promisify } = await import('node:util');
|
|
940
|
+
const execAsync2 = promisify(exec);
|
|
941
|
+
|
|
942
|
+
const cwd = process.cwd();
|
|
943
|
+
const hasTerraformDir = existsSync(join(cwd, '.terraform'));
|
|
944
|
+
const hasTfFiles = existsSync(join(cwd, 'main.tf')) || existsSync(join(cwd, 'variables.tf'));
|
|
945
|
+
|
|
946
|
+
if (!hasTfFiles && !hasTerraformDir) {
|
|
947
|
+
return { name: 'Terraform Context', passed: true, message: 'No Terraform configuration in current directory' };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (hasTfFiles && !hasTerraformDir) {
|
|
951
|
+
return {
|
|
952
|
+
name: 'Terraform Context',
|
|
953
|
+
passed: false,
|
|
954
|
+
error: 'Terraform files found but not initialized.',
|
|
955
|
+
fix: 'Run: terraform init',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (hasTerraformDir) {
|
|
960
|
+
try {
|
|
961
|
+
const { stdout } = await execAsync2('terraform workspace list', { cwd, timeout: 10_000 });
|
|
962
|
+
const workspaces = stdout.trim().split('\n').map((w: string) => w.trim());
|
|
963
|
+
const active = workspaces.find((w: string) => w.startsWith('*')) ?? 'default';
|
|
964
|
+
return { name: 'Terraform Context', passed: true, message: `Terraform initialized. Active workspace: ${active.replace('* ', '')}` };
|
|
965
|
+
} catch {
|
|
966
|
+
return { name: 'Terraform Context', passed: true, message: 'Terraform initialized but workspace check failed (connectivity issue)' };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return { name: 'Terraform Context', passed: true, message: 'No Terraform context found' };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* H6: Check Kubernetes cluster reachability
|
|
975
|
+
*/
|
|
976
|
+
async function checkKubeConfig(): Promise<CheckResult> {
|
|
977
|
+
const { exec } = await import('node:child_process');
|
|
978
|
+
const { promisify } = await import('node:util');
|
|
979
|
+
const execAsync2 = promisify(exec);
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
const { stdout: ctx } = await execAsync2('kubectl config current-context', { timeout: 5_000 });
|
|
983
|
+
const context = ctx.trim();
|
|
984
|
+
if (!context) return { name: 'Kubernetes Reachability', passed: true, message: 'kubectl: no active context' };
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
await execAsync2('kubectl cluster-info --request-timeout=3s', { timeout: 8_000 });
|
|
988
|
+
try {
|
|
989
|
+
const { stdout: ns } = await execAsync2('kubectl config view --minify -o jsonpath={..namespace}', { timeout: 3_000 });
|
|
990
|
+
const namespace = ns.trim() || 'default';
|
|
991
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}", namespace "${namespace}" — cluster reachable` };
|
|
992
|
+
} catch {
|
|
993
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}" — cluster reachable` };
|
|
994
|
+
}
|
|
995
|
+
} catch {
|
|
996
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}" — cluster not reachable (check VPN/credentials)` };
|
|
997
|
+
}
|
|
998
|
+
} catch {
|
|
999
|
+
return { name: 'Kubernetes Reachability', passed: true, message: 'kubectl: no context configured (not required)' };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* H6: Check Helm releases
|
|
1005
|
+
*/
|
|
1006
|
+
async function checkHelmReleases(): Promise<CheckResult> {
|
|
1007
|
+
const { exec } = await import('node:child_process');
|
|
1008
|
+
const { promisify } = await import('node:util');
|
|
1009
|
+
const execAsync2 = promisify(exec);
|
|
1010
|
+
|
|
1011
|
+
try {
|
|
1012
|
+
await execAsync2('which helm', { timeout: 3_000 });
|
|
1013
|
+
const { stdout } = await execAsync2('helm list -A --output json', { timeout: 15_000 });
|
|
1014
|
+
const releases: unknown[] = JSON.parse(stdout || '[]');
|
|
1015
|
+
return { name: 'Helm Releases', passed: true, message: `Helm: ${releases.length} release(s) across all namespaces` };
|
|
1016
|
+
} catch {
|
|
1017
|
+
return { name: 'Helm Releases', passed: true, message: 'Helm not installed or no releases found' };
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* M2: Check LLM connectivity by sending a minimal ping request.
|
|
1023
|
+
*/
|
|
1024
|
+
async function checkLLMConnectivity(_options: DoctorOptions): Promise<CheckResult> {
|
|
1025
|
+
try {
|
|
1026
|
+
const { initApp } = await import('../app');
|
|
1027
|
+
const { router } = await initApp();
|
|
1028
|
+
let provider = 'unknown';
|
|
1029
|
+
try {
|
|
1030
|
+
const { loadLLMConfig } = await import('../llm/config-loader');
|
|
1031
|
+
const cfg = loadLLMConfig();
|
|
1032
|
+
provider = (cfg as unknown as Record<string, unknown>).defaultProvider as string ?? 'anthropic';
|
|
1033
|
+
} catch { /* ignore */ }
|
|
1034
|
+
|
|
1035
|
+
await Promise.race([
|
|
1036
|
+
router.route({ messages: [{ role: 'user', content: 'ping' }], maxTokens: 1 }),
|
|
1037
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
|
|
1038
|
+
]);
|
|
1039
|
+
return { name: 'LLM Connectivity', passed: true, message: `Connected to ${provider}` };
|
|
1040
|
+
} catch (e: any) {
|
|
1041
|
+
return {
|
|
1042
|
+
name: 'LLM Connectivity',
|
|
1043
|
+
passed: false,
|
|
1044
|
+
error: e.message,
|
|
1045
|
+
fix: 'Run nimbus login to reconfigure',
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* H4: Check DevOps CLI versions with structured version parsing
|
|
1052
|
+
*/
|
|
1053
|
+
async function checkDevOpsCLIs(_options: DoctorOptions): Promise<CheckResult> {
|
|
1054
|
+
const { execFileSync } = await import('child_process');
|
|
1055
|
+
|
|
1056
|
+
const tools = [
|
|
1057
|
+
{ name: 'terraform', args: ['version', '-json'], parse: (o: string) => { try { return JSON.parse(o).terraform_version; } catch { return undefined; } } },
|
|
1058
|
+
{ name: 'kubectl', args: ['version', '--client', '--output=json'], parse: (o: string) => { try { return JSON.parse(o).clientVersion?.gitVersion; } catch { return undefined; } } },
|
|
1059
|
+
{ name: 'helm', args: ['version', '--short'], parse: (o: string) => o.trim() },
|
|
1060
|
+
{ name: 'aws', args: ['--version'], parse: (o: string) => o.split('/')[1]?.split(' ')[0] ?? o.trim() },
|
|
1061
|
+
{ name: 'docker', args: ['--version'], parse: (o: string) => o.replace('Docker version ', '').split(',')[0] },
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
const results: string[] = [];
|
|
1065
|
+
const missing: string[] = [];
|
|
1066
|
+
|
|
1067
|
+
for (const t of tools) {
|
|
1068
|
+
try {
|
|
1069
|
+
const out = execFileSync(t.name, t.args, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1070
|
+
const ver = t.parse(out);
|
|
1071
|
+
results.push(` ${t.name.padEnd(12)} ${ver ?? 'installed'}`);
|
|
1072
|
+
} catch {
|
|
1073
|
+
missing.push(t.name);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const passed = missing.length === 0;
|
|
1078
|
+
return {
|
|
1079
|
+
name: 'DevOps CLIs',
|
|
1080
|
+
passed,
|
|
1081
|
+
message: passed ? `All CLIs found:\n${results.join('\n')}` : `Installed:\n${results.join('\n')}`,
|
|
1082
|
+
error: missing.length > 0 ? `Not found in PATH: ${missing.join(', ')}` : undefined,
|
|
1083
|
+
fix: missing.length > 0 ? `Install missing tools: ${missing.join(', ')}` : undefined,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* H7: Check Node.js version (>= 18) and tsx availability
|
|
1089
|
+
*/
|
|
1090
|
+
async function checkNodeRuntime(_options: DoctorOptions): Promise<CheckResult> {
|
|
1091
|
+
const nodeVersion = process.versions.node;
|
|
1092
|
+
const majorStr = nodeVersion.split('.')[0];
|
|
1093
|
+
const major = parseInt(majorStr ?? '0', 10);
|
|
1094
|
+
|
|
1095
|
+
if (major < 18) {
|
|
1096
|
+
return {
|
|
1097
|
+
name: 'Node.js Runtime',
|
|
1098
|
+
passed: false,
|
|
1099
|
+
error: `Node.js ${nodeVersion} is too old (requires >= 18)`,
|
|
1100
|
+
fix: 'Upgrade Node.js: https://nodejs.org/',
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Check tsx availability
|
|
1105
|
+
const { execFileSync } = await import('child_process');
|
|
1106
|
+
let tsxVersion: string | undefined;
|
|
1107
|
+
try {
|
|
1108
|
+
tsxVersion = execFileSync('npx', ['tsx', '--version'], {
|
|
1109
|
+
encoding: 'utf-8',
|
|
1110
|
+
timeout: 5000,
|
|
1111
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1112
|
+
}).trim();
|
|
1113
|
+
} catch {
|
|
1114
|
+
// tsx may be installed locally without npx
|
|
1115
|
+
try {
|
|
1116
|
+
const path = await import('path');
|
|
1117
|
+
const { existsSync } = await import('fs');
|
|
1118
|
+
const localTsx = path.join(process.cwd(), 'node_modules', '.bin', 'tsx');
|
|
1119
|
+
if (existsSync(localTsx)) {
|
|
1120
|
+
tsxVersion = 'installed (local)';
|
|
1121
|
+
}
|
|
1122
|
+
} catch { /* ignore */ }
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
name: 'Node.js Runtime',
|
|
1127
|
+
passed: true,
|
|
1128
|
+
message: `Node.js ${nodeVersion}${tsxVersion ? ` tsx: ${tsxVersion}` : ' tsx: not found (install tsx for dev mode)'}`,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
633
1132
|
/**
|
|
634
1133
|
* All diagnostic checks
|
|
635
1134
|
*/
|
|
636
1135
|
const DIAGNOSTIC_CHECKS: Array<{ name: string; check: DiagnosticCheck }> = [
|
|
1136
|
+
{ name: 'Node.js Runtime', check: checkNodeRuntime },
|
|
637
1137
|
{ name: 'Configuration', check: checkConfiguration },
|
|
638
1138
|
{ name: 'LLM Provider', check: checkLLMProvider },
|
|
1139
|
+
{ name: 'LLM Connectivity', check: checkLLMConnectivity },
|
|
1140
|
+
{ name: 'Core Systems', check: checkCoreServices },
|
|
1141
|
+
{ name: 'DevOps Tools', check: checkToolServices },
|
|
639
1142
|
{ name: 'Cloud Credentials', check: checkCloudCredentials },
|
|
640
1143
|
{ name: 'Cloud Connectivity', check: checkCloudConnectivity },
|
|
641
|
-
{ name: 'Core Services', check: checkCoreServices },
|
|
642
|
-
{ name: 'Tool Services', check: checkToolServices },
|
|
643
1144
|
{ name: 'Dependencies', check: checkDependencies },
|
|
644
1145
|
{ name: 'Disk Space', check: checkDiskSpace },
|
|
645
1146
|
{ name: 'Network', check: checkNetwork },
|
|
1147
|
+
{ name: 'Docker Daemon', check: checkDockerDaemon },
|
|
1148
|
+
{ name: 'Vault', check: checkVault },
|
|
1149
|
+
{ name: 'CI/CD CLIs', check: checkCICDCLIs },
|
|
1150
|
+
{ name: 'GitOps CLIs', check: checkGitOpsCLIs },
|
|
1151
|
+
{ name: 'Helm Secrets', check: checkHelmSecrets },
|
|
1152
|
+
{ name: 'DevOps Pre-flight', check: checkDevOpsPreFlight },
|
|
1153
|
+
{ name: 'Terraform Context', check: checkInfraContext },
|
|
1154
|
+
{ name: 'Kubernetes Reachability', check: checkKubeConfig },
|
|
1155
|
+
{ name: 'Helm Releases', check: checkHelmReleases },
|
|
1156
|
+
{ name: 'DevOps CLIs', check: checkDevOpsCLIs },
|
|
646
1157
|
];
|
|
647
1158
|
|
|
1159
|
+
// ---------------------------------------------------------------------------
|
|
1160
|
+
// Gap 19: Fast startup health checks (subset of doctor, no network calls)
|
|
1161
|
+
// ---------------------------------------------------------------------------
|
|
1162
|
+
|
|
1163
|
+
export interface StartupCheckResult {
|
|
1164
|
+
/** Issues that prevent Nimbus from starting (shown as blocking errors). */
|
|
1165
|
+
critical: string[];
|
|
1166
|
+
/** Non-blocking warnings shown as first system message in TUI. */
|
|
1167
|
+
warnings: string[];
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Run a fast pre-flight check before starting the TUI (<500ms per check).
|
|
1172
|
+
* Only checks that do NOT require network access are included here.
|
|
1173
|
+
*
|
|
1174
|
+
* Critical failures prevent TUI startup; warnings are surfaced as system messages.
|
|
1175
|
+
*/
|
|
1176
|
+
export async function runStartupChecks(): Promise<StartupCheckResult> {
|
|
1177
|
+
const critical: string[] = [];
|
|
1178
|
+
const warnings: string[] = [];
|
|
1179
|
+
|
|
1180
|
+
// Critical: LLM credentials must be present
|
|
1181
|
+
const llmKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY'];
|
|
1182
|
+
const hasLLMKey = llmKeys.some(k => process.env[k]);
|
|
1183
|
+
if (!hasLLMKey) {
|
|
1184
|
+
// Also check stored credentials file
|
|
1185
|
+
try {
|
|
1186
|
+
const { join } = await import('node:path');
|
|
1187
|
+
const { homedir } = await import('node:os');
|
|
1188
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
1189
|
+
const credsFile = join(homedir(), '.nimbus', 'credentials.json');
|
|
1190
|
+
if (existsSync(credsFile)) {
|
|
1191
|
+
const creds = JSON.parse(readFileSync(credsFile, 'utf-8'));
|
|
1192
|
+
if (Object.keys(creds.providers ?? {}).length === 0) {
|
|
1193
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1197
|
+
}
|
|
1198
|
+
} catch {
|
|
1199
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Warning: no NIMBUS.md in CWD
|
|
1204
|
+
try {
|
|
1205
|
+
const { existsSync } = await import('node:fs');
|
|
1206
|
+
const { join } = await import('node:path');
|
|
1207
|
+
const hasNimbusMd = existsSync(join(process.cwd(), 'NIMBUS.md')) ||
|
|
1208
|
+
existsSync(join(process.cwd(), '.nimbus', 'NIMBUS.md'));
|
|
1209
|
+
if (!hasNimbusMd) {
|
|
1210
|
+
warnings.push('No NIMBUS.md found. Run `nimbus init` to generate project context.');
|
|
1211
|
+
}
|
|
1212
|
+
} catch { /* ignore */ }
|
|
1213
|
+
|
|
1214
|
+
// Warning: kubectl context not set
|
|
1215
|
+
try {
|
|
1216
|
+
const { execSync } = await import('node:child_process');
|
|
1217
|
+
execSync('kubectl config current-context', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1218
|
+
} catch {
|
|
1219
|
+
warnings.push('kubectl not configured or not in PATH. K8s operations will be unavailable.');
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Warning: terraform not in PATH
|
|
1223
|
+
try {
|
|
1224
|
+
const { execSync } = await import('node:child_process');
|
|
1225
|
+
execSync('terraform version', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1226
|
+
} catch {
|
|
1227
|
+
warnings.push('terraform not in PATH. Install terraform to use Terraform operations.');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return { critical, warnings };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
648
1233
|
/**
|
|
649
1234
|
* Run the doctor command
|
|
650
1235
|
*/
|
|
651
1236
|
export async function doctorCommand(options: DoctorOptions = {}): Promise<void> {
|
|
652
1237
|
logger.debug('Running doctor command', { options });
|
|
653
1238
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1239
|
+
// In quiet mode, suppress banner/header — only show findings
|
|
1240
|
+
if (!options.quiet) {
|
|
1241
|
+
ui.header('Nimbus Doctor');
|
|
1242
|
+
ui.info('Running diagnostic checks...');
|
|
1243
|
+
ui.newLine();
|
|
1244
|
+
}
|
|
657
1245
|
|
|
658
1246
|
const results: CheckResult[] = [];
|
|
659
1247
|
let allPassed = true;
|
|
660
1248
|
|
|
661
1249
|
for (const { name, check } of DIAGNOSTIC_CHECKS) {
|
|
662
|
-
|
|
1250
|
+
if (!options.quiet) {
|
|
1251
|
+
ui.write(` ${name.padEnd(20)}`);
|
|
1252
|
+
}
|
|
663
1253
|
|
|
664
1254
|
try {
|
|
665
1255
|
const result = await check(options);
|
|
666
1256
|
results.push(result);
|
|
667
1257
|
|
|
668
1258
|
if (result.passed) {
|
|
669
|
-
|
|
1259
|
+
if (!options.quiet) {
|
|
1260
|
+
ui.print(`${ui.color('✓', 'green')} ${result.message || 'OK'}`);
|
|
1261
|
+
}
|
|
670
1262
|
} else {
|
|
671
|
-
ui.print(`${ui.color('✗', 'red')} ${result.error || 'Failed'}`);
|
|
672
1263
|
allPassed = false;
|
|
673
1264
|
|
|
674
|
-
if (options.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1265
|
+
if (options.quiet) {
|
|
1266
|
+
// In quiet mode, only print failures
|
|
1267
|
+
ui.print(`FAIL ${name}: ${result.error || 'Failed'}${result.fix ? ` — ${result.fix}` : ''}`);
|
|
1268
|
+
} else {
|
|
1269
|
+
ui.print(`${ui.color('✗', 'red')} ${result.error || 'Failed'}`);
|
|
1270
|
+
|
|
1271
|
+
if (options.fix && result.runFix) {
|
|
1272
|
+
ui.print(` → Attempting fix...`);
|
|
1273
|
+
try {
|
|
1274
|
+
await result.runFix();
|
|
1275
|
+
ui.print(` → ${ui.color('Fixed', 'green')}`);
|
|
1276
|
+
} catch (fixError: any) {
|
|
1277
|
+
ui.print(
|
|
1278
|
+
` → ${ui.color(`Fix failed: ${fixError.message}`, 'red')}`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
} else if (result.fix) {
|
|
1282
|
+
ui.print(` → ${ui.dim(result.fix)}`);
|
|
683
1283
|
}
|
|
684
|
-
} else if (result.fix) {
|
|
685
|
-
ui.print(` → ${ui.dim(result.fix)}`);
|
|
686
1284
|
}
|
|
687
1285
|
}
|
|
688
1286
|
|
|
689
|
-
// Show details in verbose mode
|
|
690
|
-
if (options.verbose && result.details) {
|
|
1287
|
+
// Show details in verbose mode (not quiet)
|
|
1288
|
+
if (!options.quiet && options.verbose && result.details) {
|
|
691
1289
|
for (const [key, value] of Object.entries(result.details)) {
|
|
692
1290
|
if (Array.isArray(value)) {
|
|
693
1291
|
ui.print(` ${key}:`);
|
|
@@ -704,7 +1302,11 @@ export async function doctorCommand(options: DoctorOptions = {}): Promise<void>
|
|
|
704
1302
|
}
|
|
705
1303
|
}
|
|
706
1304
|
} catch (error: any) {
|
|
707
|
-
|
|
1305
|
+
if (!options.quiet) {
|
|
1306
|
+
ui.print(`${ui.color('✗', 'red')} Error: ${error.message}`);
|
|
1307
|
+
} else {
|
|
1308
|
+
ui.print(`FAIL ${name}: Error: ${error.message}`);
|
|
1309
|
+
}
|
|
708
1310
|
results.push({
|
|
709
1311
|
name,
|
|
710
1312
|
passed: false,
|
|
@@ -714,7 +1316,9 @@ export async function doctorCommand(options: DoctorOptions = {}): Promise<void>
|
|
|
714
1316
|
}
|
|
715
1317
|
}
|
|
716
1318
|
|
|
717
|
-
|
|
1319
|
+
if (!options.quiet) {
|
|
1320
|
+
ui.newLine();
|
|
1321
|
+
}
|
|
718
1322
|
|
|
719
1323
|
// JSON output
|
|
720
1324
|
if (options.json) {
|
|
@@ -734,6 +1338,7 @@ export async function doctorCommand(options: DoctorOptions = {}): Promise<void>
|
|
|
734
1338
|
2
|
|
735
1339
|
)
|
|
736
1340
|
);
|
|
1341
|
+
if (!allPassed) process.exit(1);
|
|
737
1342
|
return;
|
|
738
1343
|
}
|
|
739
1344
|
|
|
@@ -742,50 +1347,37 @@ export async function doctorCommand(options: DoctorOptions = {}): Promise<void>
|
|
|
742
1347
|
const totalCount = results.length;
|
|
743
1348
|
|
|
744
1349
|
if (allPassed) {
|
|
745
|
-
|
|
1350
|
+
if (!options.quiet) {
|
|
1351
|
+
ui.success(`All checks passed! (${passedCount}/${totalCount})`);
|
|
1352
|
+
}
|
|
746
1353
|
} else {
|
|
747
1354
|
const failedCount = totalCount - passedCount;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1355
|
+
if (!options.quiet) {
|
|
1356
|
+
ui.warning(`${failedCount} check(s) failed. ${passedCount}/${totalCount} passed.`);
|
|
1357
|
+
ui.newLine();
|
|
1358
|
+
ui.info('Run with --fix to attempt automatic fixes');
|
|
1359
|
+
ui.info('Run with --verbose for more details');
|
|
1360
|
+
}
|
|
1361
|
+
process.exit(1);
|
|
752
1362
|
}
|
|
753
1363
|
|
|
754
|
-
// Quality Metrics
|
|
755
|
-
if (options.metrics) {
|
|
1364
|
+
// Quality Metrics (suppressed in quiet mode)
|
|
1365
|
+
if (options.metrics && !options.quiet) {
|
|
756
1366
|
ui.newLine();
|
|
757
1367
|
ui.header('Quality Metrics');
|
|
758
1368
|
|
|
759
|
-
const stateUrl = process.env.STATE_SERVICE_URL || 'http://localhost:3011';
|
|
760
1369
|
try {
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
ui.print(` Response Time (P50) ${data.responseTime.p50}ms`);
|
|
771
|
-
ui.print(` Response Time (Avg) ${data.responseTime.avg}ms`);
|
|
772
|
-
ui.print(` Error Rate ${data.errorRate}%`);
|
|
773
|
-
ui.print(` Total Operations ${data.totalOperations}`);
|
|
774
|
-
ui.print(` Total Tokens Used ${data.totalTokensUsed.toLocaleString()}`);
|
|
775
|
-
ui.print(` Total Cost $${data.totalCostUsd.toFixed(4)}`);
|
|
776
|
-
|
|
777
|
-
if (Object.keys(data.operationsByType).length > 0) {
|
|
778
|
-
ui.newLine();
|
|
779
|
-
ui.print(' Operations by type:');
|
|
780
|
-
for (const [type, count] of Object.entries(data.operationsByType)) {
|
|
781
|
-
ui.print(` ${type.padEnd(20)} ${count}`);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
} else {
|
|
785
|
-
ui.warning('Could not fetch metrics (State service unavailable)');
|
|
786
|
-
}
|
|
1370
|
+
const { getDb } = await import('../state/db');
|
|
1371
|
+
const db = getDb();
|
|
1372
|
+
// Get basic usage stats from the local SQLite database
|
|
1373
|
+
const sessionsRow = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number } | undefined;
|
|
1374
|
+
const sessionCount = sessionsRow?.count ?? 0;
|
|
1375
|
+
ui.newLine();
|
|
1376
|
+
ui.print(` Total sessions ${sessionCount}`);
|
|
1377
|
+
ui.print(` Database ~/.nimbus/nimbus.db`);
|
|
1378
|
+
ui.print(` Detailed metrics nimbus serve (HTTP API)`);
|
|
787
1379
|
} catch {
|
|
788
|
-
ui.warning('Could not fetch metrics
|
|
1380
|
+
ui.warning('Could not fetch metrics. Run "nimbus serve" for the full metrics API.');
|
|
789
1381
|
}
|
|
790
1382
|
}
|
|
791
1383
|
}
|