@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,1291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor Command
|
|
3
|
+
*
|
|
4
|
+
* Run diagnostic checks on Nimbus installation and configuration
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus doctor [options]
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from '../utils';
|
|
9
|
+
import { ui } from '../wizard';
|
|
10
|
+
/**
|
|
11
|
+
* Check configuration files
|
|
12
|
+
*/
|
|
13
|
+
async function checkConfiguration(options) {
|
|
14
|
+
const fs = await import('fs/promises');
|
|
15
|
+
const path = await import('path');
|
|
16
|
+
const os = await import('os');
|
|
17
|
+
const configDir = path.join(os.homedir(), '.nimbus');
|
|
18
|
+
const configFile = path.join(configDir, 'config.json');
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(configDir);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {
|
|
24
|
+
name: 'Configuration',
|
|
25
|
+
passed: false,
|
|
26
|
+
error: 'Configuration directory not found',
|
|
27
|
+
fix: 'Run "nimbus init" to create configuration',
|
|
28
|
+
runFix: async () => {
|
|
29
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(configFile);
|
|
35
|
+
const content = await fs.readFile(configFile, 'utf-8');
|
|
36
|
+
JSON.parse(content); // Validate JSON
|
|
37
|
+
return {
|
|
38
|
+
name: 'Configuration',
|
|
39
|
+
passed: true,
|
|
40
|
+
message: 'Configuration file valid',
|
|
41
|
+
details: options.verbose ? { path: configFile } : undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === 'ENOENT') {
|
|
46
|
+
return {
|
|
47
|
+
name: 'Configuration',
|
|
48
|
+
passed: false,
|
|
49
|
+
error: 'Configuration file not found',
|
|
50
|
+
fix: 'Run "nimbus config init" to create configuration',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name: 'Configuration',
|
|
55
|
+
passed: false,
|
|
56
|
+
error: `Invalid configuration: ${error.message}`,
|
|
57
|
+
fix: 'Run "nimbus config reset" to reset configuration',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check LLM provider configuration
|
|
63
|
+
*/
|
|
64
|
+
async function checkLLMProvider(options) {
|
|
65
|
+
const fs = await import('fs/promises');
|
|
66
|
+
const path = await import('path');
|
|
67
|
+
const os = await import('os');
|
|
68
|
+
// Check for API keys
|
|
69
|
+
const envKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'AWS_ACCESS_KEY_ID'];
|
|
70
|
+
const foundKeys = [];
|
|
71
|
+
for (const key of envKeys) {
|
|
72
|
+
if (process.env[key]) {
|
|
73
|
+
foundKeys.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check credentials file
|
|
77
|
+
const credentialsFile = path.join(os.homedir(), '.nimbus', 'credentials.json');
|
|
78
|
+
let hasStoredCredentials = false;
|
|
79
|
+
try {
|
|
80
|
+
await fs.access(credentialsFile);
|
|
81
|
+
const content = await fs.readFile(credentialsFile, 'utf-8');
|
|
82
|
+
const creds = JSON.parse(content);
|
|
83
|
+
hasStoredCredentials = Object.keys(creds.providers || {}).length > 0;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// No stored credentials
|
|
87
|
+
}
|
|
88
|
+
if (foundKeys.length === 0 && !hasStoredCredentials) {
|
|
89
|
+
return {
|
|
90
|
+
name: 'LLM Provider',
|
|
91
|
+
passed: false,
|
|
92
|
+
error: 'No LLM provider configured',
|
|
93
|
+
fix: 'Run "nimbus login" to configure an LLM provider',
|
|
94
|
+
runFix: async () => {
|
|
95
|
+
const { loginCommand } = await import('./login');
|
|
96
|
+
await loginCommand();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
name: 'LLM Provider',
|
|
102
|
+
passed: true,
|
|
103
|
+
message: hasStoredCredentials ? 'Credentials configured' : `Using ${foundKeys.join(', ')}`,
|
|
104
|
+
details: options.verbose
|
|
105
|
+
? {
|
|
106
|
+
envKeys: foundKeys,
|
|
107
|
+
hasStoredCredentials,
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check cloud credentials (AWS, etc.)
|
|
114
|
+
*/
|
|
115
|
+
async function checkCloudCredentials(options) {
|
|
116
|
+
const fs = await import('fs/promises');
|
|
117
|
+
const path = await import('path');
|
|
118
|
+
const os = await import('os');
|
|
119
|
+
const checks = [];
|
|
120
|
+
// Check AWS credentials
|
|
121
|
+
const awsConfigDir = path.join(os.homedir(), '.aws');
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(path.join(awsConfigDir, 'credentials'));
|
|
124
|
+
checks.push('AWS credentials');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Check environment variables
|
|
128
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
129
|
+
checks.push('AWS (env vars)');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Check GCP credentials
|
|
133
|
+
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
134
|
+
try {
|
|
135
|
+
await fs.access(process.env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
136
|
+
checks.push('GCP credentials');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Invalid path
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check Azure credentials
|
|
143
|
+
if (process.env.AZURE_CLIENT_ID || process.env.AZURE_SUBSCRIPTION_ID) {
|
|
144
|
+
checks.push('Azure (env vars)');
|
|
145
|
+
}
|
|
146
|
+
// Check kubeconfig
|
|
147
|
+
const kubeconfigPath = process.env.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
|
|
148
|
+
try {
|
|
149
|
+
await fs.access(kubeconfigPath);
|
|
150
|
+
checks.push('Kubernetes');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// No kubeconfig
|
|
154
|
+
}
|
|
155
|
+
if (checks.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
name: 'Cloud Credentials',
|
|
158
|
+
passed: false,
|
|
159
|
+
error: 'No cloud credentials found',
|
|
160
|
+
fix: 'Configure AWS credentials (~/.aws/credentials) or set environment variables',
|
|
161
|
+
runFix: async () => {
|
|
162
|
+
ui.info('To configure cloud credentials, run one of:');
|
|
163
|
+
ui.print(' AWS: nimbus login --cloud aws (runs aws configure)');
|
|
164
|
+
ui.print(' GCP: nimbus login --cloud gcp (runs gcloud auth login)');
|
|
165
|
+
ui.print(' Azure: nimbus login --cloud azure (runs az login)');
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
name: 'Cloud Credentials',
|
|
171
|
+
passed: true,
|
|
172
|
+
message: `Found: ${checks.join(', ')}`,
|
|
173
|
+
details: options.verbose ? { providers: checks } : undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check cloud connectivity (real API calls)
|
|
178
|
+
*/
|
|
179
|
+
async function checkCloudConnectivity(options) {
|
|
180
|
+
const { execFileSync } = await import('child_process');
|
|
181
|
+
const results = [];
|
|
182
|
+
// AWS: try sts get-caller-identity
|
|
183
|
+
try {
|
|
184
|
+
const output = execFileSync('aws', ['sts', 'get-caller-identity', '--output', 'json'], {
|
|
185
|
+
encoding: 'utf-8',
|
|
186
|
+
timeout: 10000,
|
|
187
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
188
|
+
});
|
|
189
|
+
const identity = JSON.parse(output);
|
|
190
|
+
results.push({
|
|
191
|
+
provider: 'AWS',
|
|
192
|
+
status: 'connected',
|
|
193
|
+
details: `Account: ${identity.Account}, User: ${identity.UserId}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT') {
|
|
198
|
+
results.push({
|
|
199
|
+
provider: 'AWS',
|
|
200
|
+
status: 'not installed',
|
|
201
|
+
details: 'Install AWS CLI: https://aws.amazon.com/cli/',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
results.push({
|
|
206
|
+
provider: 'AWS',
|
|
207
|
+
status: 'failed',
|
|
208
|
+
details: 'Run "aws configure" or check credentials',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// GCP: try gcloud auth print-access-token
|
|
213
|
+
try {
|
|
214
|
+
const output = execFileSync('gcloud', ['auth', 'print-access-token'], {
|
|
215
|
+
encoding: 'utf-8',
|
|
216
|
+
timeout: 10000,
|
|
217
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
218
|
+
});
|
|
219
|
+
if (output.trim().length > 0) {
|
|
220
|
+
results.push({ provider: 'GCP', status: 'connected', details: 'Access token valid' });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
results.push({ provider: 'GCP', status: 'failed', details: 'Run "gcloud auth login"' });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (error.code === 'ENOENT') {
|
|
228
|
+
results.push({
|
|
229
|
+
provider: 'GCP',
|
|
230
|
+
status: 'not installed',
|
|
231
|
+
details: 'Install gcloud: https://cloud.google.com/sdk/docs/install',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
results.push({ provider: 'GCP', status: 'failed', details: 'Run "gcloud auth login"' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Azure: try az account show
|
|
239
|
+
try {
|
|
240
|
+
const output = execFileSync('az', ['account', 'show', '--output', 'json'], {
|
|
241
|
+
encoding: 'utf-8',
|
|
242
|
+
timeout: 10000,
|
|
243
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
244
|
+
});
|
|
245
|
+
const account = JSON.parse(output);
|
|
246
|
+
results.push({
|
|
247
|
+
provider: 'Azure',
|
|
248
|
+
status: 'connected',
|
|
249
|
+
details: `Subscription: ${account.name || account.id}`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error.code === 'ENOENT') {
|
|
254
|
+
results.push({
|
|
255
|
+
provider: 'Azure',
|
|
256
|
+
status: 'not installed',
|
|
257
|
+
details: 'Install Azure CLI: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
results.push({ provider: 'Azure', status: 'failed', details: 'Run "az login"' });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const connected = results.filter(r => r.status === 'connected');
|
|
265
|
+
if (connected.length === 0) {
|
|
266
|
+
const installed = results.filter(r => r.status !== 'not installed');
|
|
267
|
+
if (installed.length === 0) {
|
|
268
|
+
return {
|
|
269
|
+
name: 'Cloud Connectivity',
|
|
270
|
+
passed: true,
|
|
271
|
+
message: 'No cloud CLIs installed (optional)',
|
|
272
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
name: 'Cloud Connectivity',
|
|
277
|
+
passed: false,
|
|
278
|
+
error: 'No cloud provider connected',
|
|
279
|
+
fix: results
|
|
280
|
+
.map(r => r.details)
|
|
281
|
+
.filter(Boolean)
|
|
282
|
+
.join('; '),
|
|
283
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
284
|
+
runFix: async () => {
|
|
285
|
+
const { execFileSync } = await import('child_process');
|
|
286
|
+
// Try AWS SSO refresh
|
|
287
|
+
const awsFailed = results.find(r => r.provider === 'AWS' && r.status === 'failed');
|
|
288
|
+
if (awsFailed) {
|
|
289
|
+
ui.info('Attempting AWS SSO login...');
|
|
290
|
+
try {
|
|
291
|
+
execFileSync('aws', ['sso', 'login'], { stdio: 'inherit', timeout: 120000 });
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
ui.warning('AWS SSO login failed. Run `aws configure` manually.');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Try GCP refresh
|
|
298
|
+
const gcpFailed = results.find(r => r.provider === 'GCP' && r.status === 'failed');
|
|
299
|
+
if (gcpFailed) {
|
|
300
|
+
ui.info('Attempting GCP application-default login...');
|
|
301
|
+
try {
|
|
302
|
+
execFileSync('gcloud', ['auth', 'application-default', 'login'], { stdio: 'inherit', timeout: 120000 });
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
ui.warning('GCP login failed. Run `gcloud auth login` manually.');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
name: 'Cloud Connectivity',
|
|
313
|
+
passed: true,
|
|
314
|
+
message: connected.map(r => `${r.provider}: ${r.details}`).join(', '),
|
|
315
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Check embedded core systems (SQLite database + LLM auth + tool registry)
|
|
320
|
+
*/
|
|
321
|
+
async function checkCoreServices(options) {
|
|
322
|
+
const fs = await import('fs/promises');
|
|
323
|
+
const path = await import('path');
|
|
324
|
+
const os = await import('os');
|
|
325
|
+
const results = [];
|
|
326
|
+
// Check SQLite database
|
|
327
|
+
const dbPath = path.join(os.homedir(), '.nimbus', 'nimbus.db');
|
|
328
|
+
try {
|
|
329
|
+
await fs.access(dbPath);
|
|
330
|
+
const stat = await fs.stat(dbPath);
|
|
331
|
+
results.push({
|
|
332
|
+
name: 'SQLite DB',
|
|
333
|
+
status: 'ok',
|
|
334
|
+
details: options.verbose ? `${dbPath} (${(stat.size / 1024).toFixed(1)} KB)` : undefined,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
results.push({
|
|
339
|
+
name: 'SQLite DB',
|
|
340
|
+
status: 'not initialized',
|
|
341
|
+
details: 'Will be created on first use',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Check LLM credentials
|
|
345
|
+
const credFile = path.join(os.homedir(), '.nimbus', 'credentials.json');
|
|
346
|
+
let llmStatus = 'not configured';
|
|
347
|
+
let llmDetails;
|
|
348
|
+
try {
|
|
349
|
+
const content = await fs.readFile(credFile, 'utf-8');
|
|
350
|
+
const creds = JSON.parse(content);
|
|
351
|
+
const providers = Object.keys(creds.providers || {});
|
|
352
|
+
if (providers.length > 0) {
|
|
353
|
+
llmStatus = 'configured';
|
|
354
|
+
llmDetails = options.verbose ? `Providers: ${providers.join(', ')}` : undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Check env vars as fallback
|
|
359
|
+
const envKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'AWS_ACCESS_KEY_ID'];
|
|
360
|
+
const found = envKeys.filter(k => process.env[k]);
|
|
361
|
+
if (found.length > 0) {
|
|
362
|
+
llmStatus = 'via env vars';
|
|
363
|
+
llmDetails = options.verbose ? found.join(', ') : undefined;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
results.push({ name: 'LLM Auth', status: llmStatus, details: llmDetails });
|
|
367
|
+
// Check tool registry (Nimbus built-in tools)
|
|
368
|
+
try {
|
|
369
|
+
const { standardTools } = await import('../tools/schemas/standard');
|
|
370
|
+
const { devopsTools } = await import('../tools/schemas/devops');
|
|
371
|
+
// Count expected tools
|
|
372
|
+
const expectedCount = standardTools.length + devopsTools.length;
|
|
373
|
+
results.push({
|
|
374
|
+
name: 'Tool Registry',
|
|
375
|
+
status: 'ok',
|
|
376
|
+
details: options.verbose ? `${expectedCount} tools available` : undefined,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
results.push({ name: 'Tool Registry', status: 'error', details: e.message });
|
|
381
|
+
}
|
|
382
|
+
const failed = results.filter(r => r.status === 'error' || r.status === 'not configured');
|
|
383
|
+
const passed = failed.length === 0;
|
|
384
|
+
const summary = results.map(r => `${r.name}: ${r.status}`).join(', ');
|
|
385
|
+
return {
|
|
386
|
+
name: 'Core Systems',
|
|
387
|
+
passed,
|
|
388
|
+
message: passed ? summary : `Issues: ${failed.map(r => r.name).join(', ')}`,
|
|
389
|
+
details: options.verbose ? { systems: results } : undefined,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Check DevOps CLI tools availability (terraform, kubectl, helm, aws)
|
|
394
|
+
*/
|
|
395
|
+
async function checkToolServices(options) {
|
|
396
|
+
const { execFileSync } = await import('child_process');
|
|
397
|
+
const devopsTools = [
|
|
398
|
+
{ name: 'terraform', cmd: 'terraform', args: ['version', '-json'] },
|
|
399
|
+
{ name: 'kubectl', cmd: 'kubectl', args: ['version', '--client', '--output=json'] },
|
|
400
|
+
{ name: 'helm', cmd: 'helm', args: ['version', '--short'] },
|
|
401
|
+
{ name: 'aws', cmd: 'aws', args: ['--version'] },
|
|
402
|
+
{ name: 'gcloud', cmd: 'gcloud', args: ['version', '--format=json'] },
|
|
403
|
+
{ name: 'az', cmd: 'az', args: ['version', '--output=json'] },
|
|
404
|
+
];
|
|
405
|
+
const results = [];
|
|
406
|
+
for (const tool of devopsTools) {
|
|
407
|
+
try {
|
|
408
|
+
const output = execFileSync(tool.cmd, tool.args, {
|
|
409
|
+
encoding: 'utf-8',
|
|
410
|
+
timeout: 5000,
|
|
411
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
412
|
+
});
|
|
413
|
+
// Extract version number
|
|
414
|
+
let version = 'installed';
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(output);
|
|
417
|
+
// terraform: { terraform_version: "1.7.0" }, kubectl: { clientVersion: { gitVersion: "v1.28.0" } }
|
|
418
|
+
version = parsed.terraform_version || parsed.clientVersion?.gitVersion || 'installed';
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
const match = output.match(/[\d]+\.[\d]+\.[\d]+/);
|
|
422
|
+
if (match)
|
|
423
|
+
version = match[0];
|
|
424
|
+
}
|
|
425
|
+
results.push({ name: tool.name, version, available: true });
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
results.push({ name: tool.name, version: 'not found', available: false });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const available = results.filter(r => r.available);
|
|
432
|
+
const missing = results.filter(r => !r.available);
|
|
433
|
+
// GAP-12: OS-aware runFix — actually installs missing tools via Homebrew on macOS
|
|
434
|
+
const BREW_INSTALL = {
|
|
435
|
+
terraform: 'terraform',
|
|
436
|
+
kubectl: 'kubernetes-cli',
|
|
437
|
+
helm: 'helm',
|
|
438
|
+
aws: 'awscli',
|
|
439
|
+
gcloud: '--cask google-cloud-sdk',
|
|
440
|
+
az: 'azure-cli',
|
|
441
|
+
};
|
|
442
|
+
const INSTALL_URLS = {
|
|
443
|
+
terraform: 'https://developer.hashicorp.com/terraform/install',
|
|
444
|
+
kubectl: 'https://kubernetes.io/docs/tasks/tools/',
|
|
445
|
+
helm: 'https://helm.sh/docs/intro/install/',
|
|
446
|
+
aws: 'https://aws.amazon.com/cli/',
|
|
447
|
+
gcloud: 'https://cloud.google.com/sdk/docs/install',
|
|
448
|
+
az: 'https://learn.microsoft.com/en-us/cli/azure/install-azure-cli',
|
|
449
|
+
};
|
|
450
|
+
const osAwareRunFix = async () => {
|
|
451
|
+
const { execFileSync: brew } = await import('child_process');
|
|
452
|
+
const isMac = process.platform === 'darwin';
|
|
453
|
+
const isLinux = process.platform === 'linux';
|
|
454
|
+
for (const tool of missing) {
|
|
455
|
+
const toolName = tool.name;
|
|
456
|
+
if (isMac && BREW_INSTALL[toolName]) {
|
|
457
|
+
ui.print(`Installing ${toolName} via Homebrew...`);
|
|
458
|
+
try {
|
|
459
|
+
const brewArgs = ['install', ...BREW_INSTALL[toolName].split(' ')];
|
|
460
|
+
brew('brew', brewArgs, { stdio: 'inherit', timeout: 120_000 });
|
|
461
|
+
ui.success(`${toolName} installed successfully`);
|
|
462
|
+
}
|
|
463
|
+
catch (brewErr) {
|
|
464
|
+
ui.warning(`brew install failed for ${toolName}: ${brewErr instanceof Error ? brewErr.message : String(brewErr)}`);
|
|
465
|
+
ui.print(` Manual install: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (isLinux) {
|
|
469
|
+
ui.print(` ${toolName}: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
ui.print(` ${toolName}: ${INSTALL_URLS[toolName] ?? 'check official docs'}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
if (available.length === 0) {
|
|
477
|
+
return {
|
|
478
|
+
name: 'DevOps Tools',
|
|
479
|
+
passed: false,
|
|
480
|
+
error: 'No DevOps CLI tools found (terraform, kubectl, helm, aws, gcloud, az)',
|
|
481
|
+
fix: 'Install at least one: terraform, kubectl, or helm',
|
|
482
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
483
|
+
runFix: osAwareRunFix,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
name: 'DevOps Tools',
|
|
488
|
+
passed: true,
|
|
489
|
+
message: `${available.length}/${devopsTools.length} available: ${available.map(t => `${t.name} ${t.version}`).join(', ')}${missing.length > 0 ? ` | missing: ${missing.map(t => t.name).join(', ')}` : ''}`,
|
|
490
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Check dependencies (CLI tools)
|
|
495
|
+
*/
|
|
496
|
+
async function checkDependencies(options) {
|
|
497
|
+
const { execFileSync } = await import('child_process');
|
|
498
|
+
// Use execFileSync with args arrays to prevent shell injection
|
|
499
|
+
const tools = [
|
|
500
|
+
{ name: 'git', cmd: 'git', args: ['--version'], required: true },
|
|
501
|
+
{ name: 'terraform', cmd: 'terraform', args: ['version'], required: false },
|
|
502
|
+
{ name: 'kubectl', cmd: 'kubectl', args: ['version', '--client'], required: false },
|
|
503
|
+
{ name: 'helm', cmd: 'helm', args: ['version', '--short'], required: false },
|
|
504
|
+
{ name: 'aws', cmd: 'aws', args: ['--version'], required: false },
|
|
505
|
+
{ name: 'gcloud', cmd: 'gcloud', args: ['version'], required: false },
|
|
506
|
+
{ name: 'az', cmd: 'az', args: ['version'], required: false },
|
|
507
|
+
];
|
|
508
|
+
const results = [];
|
|
509
|
+
const requiredMissing = [];
|
|
510
|
+
for (const tool of tools) {
|
|
511
|
+
try {
|
|
512
|
+
const output = execFileSync(tool.cmd, tool.args, {
|
|
513
|
+
encoding: 'utf-8',
|
|
514
|
+
timeout: 5000,
|
|
515
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
516
|
+
});
|
|
517
|
+
// Extract version from output
|
|
518
|
+
const versionMatch = output.match(/\d+\.\d+(\.\d+)?/);
|
|
519
|
+
results.push({
|
|
520
|
+
name: tool.name,
|
|
521
|
+
version: versionMatch ? versionMatch[0] : 'installed',
|
|
522
|
+
available: true,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
results.push({ name: tool.name, available: false });
|
|
527
|
+
if (tool.required) {
|
|
528
|
+
requiredMissing.push(tool.name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (requiredMissing.length > 0) {
|
|
533
|
+
return {
|
|
534
|
+
name: 'Dependencies',
|
|
535
|
+
passed: false,
|
|
536
|
+
error: `Required tools not found: ${requiredMissing.join(', ')}`,
|
|
537
|
+
fix: `Install missing tools: ${requiredMissing.join(', ')}`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const availableCount = results.filter(r => r.available).length;
|
|
541
|
+
return {
|
|
542
|
+
name: 'Dependencies',
|
|
543
|
+
passed: true,
|
|
544
|
+
message: `${availableCount}/${tools.length} tools available`,
|
|
545
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
546
|
+
// G21: runFix checks for .tf files without .terraform/ and suggests terraform init
|
|
547
|
+
runFix: async () => {
|
|
548
|
+
const fs = await import('fs/promises');
|
|
549
|
+
const path = await import('path');
|
|
550
|
+
const cwd = process.cwd();
|
|
551
|
+
// Check for .tf files without .terraform dir
|
|
552
|
+
try {
|
|
553
|
+
const entries = await fs.readdir(cwd);
|
|
554
|
+
const hasTfFiles = entries.some(e => e.endsWith('.tf'));
|
|
555
|
+
const hasTerraformDir = entries.includes('.terraform');
|
|
556
|
+
if (hasTfFiles && !hasTerraformDir) {
|
|
557
|
+
ui.info('Found .tf files without .terraform/ directory. Run:');
|
|
558
|
+
ui.print(' terraform init');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch { /* ignore */ }
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Check disk space
|
|
567
|
+
*/
|
|
568
|
+
async function checkDiskSpace(_options) {
|
|
569
|
+
const os = await import('os');
|
|
570
|
+
const { execFileSync } = await import('child_process');
|
|
571
|
+
try {
|
|
572
|
+
// Get disk space for home directory
|
|
573
|
+
const homeDir = os.homedir();
|
|
574
|
+
let available;
|
|
575
|
+
if (process.platform === 'win32') {
|
|
576
|
+
// Windows - use execFileSync with args array to prevent shell injection
|
|
577
|
+
const output = execFileSync('wmic', ['logicaldisk', 'get', 'size,freespace,caption'], {
|
|
578
|
+
encoding: 'utf-8',
|
|
579
|
+
});
|
|
580
|
+
const lines = output.trim().split('\n');
|
|
581
|
+
const drive = homeDir.charAt(0).toUpperCase();
|
|
582
|
+
for (const line of lines) {
|
|
583
|
+
if (line.startsWith(drive)) {
|
|
584
|
+
const parts = line.trim().split(/\s+/);
|
|
585
|
+
available = parseInt(parts[1], 10);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Unix-like - use execFileSync with args array to prevent shell injection
|
|
592
|
+
const output = execFileSync('df', ['-k', homeDir], { encoding: 'utf-8' });
|
|
593
|
+
// Skip header line and parse the data line
|
|
594
|
+
const lines = output.trim().split('\n');
|
|
595
|
+
const dataLine = lines[lines.length - 1];
|
|
596
|
+
const parts = dataLine.trim().split(/\s+/);
|
|
597
|
+
available = parseInt(parts[3], 10) * 1024; // Convert KB to bytes
|
|
598
|
+
}
|
|
599
|
+
// Handle case where disk space could not be determined
|
|
600
|
+
if (available === undefined || isNaN(available)) {
|
|
601
|
+
return {
|
|
602
|
+
name: 'Disk Space',
|
|
603
|
+
passed: true,
|
|
604
|
+
message: 'Unable to determine disk space (assuming OK)',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const availableGB = available / (1024 * 1024 * 1024);
|
|
608
|
+
const minRequired = 1; // 1 GB minimum
|
|
609
|
+
if (availableGB < minRequired) {
|
|
610
|
+
return {
|
|
611
|
+
name: 'Disk Space',
|
|
612
|
+
passed: false,
|
|
613
|
+
error: `Low disk space: ${availableGB.toFixed(1)} GB available`,
|
|
614
|
+
fix: 'Free up disk space (at least 1 GB recommended)',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
name: 'Disk Space',
|
|
619
|
+
passed: true,
|
|
620
|
+
message: `${availableGB.toFixed(1)} GB available`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return {
|
|
625
|
+
name: 'Disk Space',
|
|
626
|
+
passed: true,
|
|
627
|
+
message: 'Unable to check (assuming OK)',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Check network connectivity
|
|
633
|
+
*/
|
|
634
|
+
async function checkNetwork(options) {
|
|
635
|
+
const endpoints = [
|
|
636
|
+
{ name: 'api.anthropic.com', url: 'https://api.anthropic.com' },
|
|
637
|
+
{ name: 'api.openai.com', url: 'https://api.openai.com' },
|
|
638
|
+
];
|
|
639
|
+
const results = [];
|
|
640
|
+
for (const endpoint of endpoints) {
|
|
641
|
+
try {
|
|
642
|
+
await fetch(endpoint.url, {
|
|
643
|
+
method: 'HEAD',
|
|
644
|
+
signal: AbortSignal.timeout(5000),
|
|
645
|
+
});
|
|
646
|
+
results.push({ name: endpoint.name, reachable: true });
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
results.push({ name: endpoint.name, reachable: false });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const reachableCount = results.filter(r => r.reachable).length;
|
|
653
|
+
if (reachableCount === 0) {
|
|
654
|
+
return {
|
|
655
|
+
name: 'Network',
|
|
656
|
+
passed: false,
|
|
657
|
+
error: 'Cannot reach LLM APIs',
|
|
658
|
+
fix: 'Check network connection and firewall settings',
|
|
659
|
+
details: options.verbose ? { endpoints: results } : undefined,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
name: 'Network',
|
|
664
|
+
passed: true,
|
|
665
|
+
message: `${reachableCount}/${endpoints.length} API endpoints reachable`,
|
|
666
|
+
details: options.verbose ? { endpoints: results } : undefined,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Check Docker daemon availability (C1/L10)
|
|
671
|
+
*/
|
|
672
|
+
async function checkDockerDaemon(_options) {
|
|
673
|
+
const { execFileSync } = await import('child_process');
|
|
674
|
+
try {
|
|
675
|
+
execFileSync('docker', ['info'], { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
676
|
+
return { name: 'Docker Daemon', passed: true, message: 'Docker daemon running' };
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
try {
|
|
680
|
+
// Just check if docker binary exists
|
|
681
|
+
execFileSync('docker', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
682
|
+
return {
|
|
683
|
+
name: 'Docker Daemon',
|
|
684
|
+
passed: false,
|
|
685
|
+
error: 'Docker installed but daemon not running',
|
|
686
|
+
fix: 'Start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux)',
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return { name: 'Docker Daemon', passed: false, error: 'Docker not installed', fix: 'Install Docker Desktop from https://www.docker.com' };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Check Vault CLI and status (C2/L10)
|
|
696
|
+
*/
|
|
697
|
+
async function checkVault(_options) {
|
|
698
|
+
const { execFileSync } = await import('child_process');
|
|
699
|
+
try {
|
|
700
|
+
execFileSync('vault', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
701
|
+
if (process.env.VAULT_ADDR) {
|
|
702
|
+
try {
|
|
703
|
+
const out = execFileSync('vault', ['status', '-format=json'], {
|
|
704
|
+
encoding: 'utf-8',
|
|
705
|
+
timeout: 5000,
|
|
706
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
707
|
+
env: process.env,
|
|
708
|
+
});
|
|
709
|
+
const status = JSON.parse(out);
|
|
710
|
+
if (status.sealed) {
|
|
711
|
+
return { name: 'Vault', passed: false, error: 'Vault is sealed', fix: 'Run `vault operator unseal`' };
|
|
712
|
+
}
|
|
713
|
+
return { name: 'Vault', passed: true, message: `Vault available at ${process.env.VAULT_ADDR} (unsealed)` };
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return { name: 'Vault', passed: false, error: `Cannot reach Vault at ${process.env.VAULT_ADDR}`, fix: 'Check VAULT_ADDR and network connectivity' };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return { name: 'Vault', passed: true, message: 'vault CLI installed (VAULT_ADDR not set)' };
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return { name: 'Vault', passed: true, message: 'vault CLI not installed (optional)' };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Check CI/CD CLIs: gh, glab, circleci (C3/L10)
|
|
727
|
+
*/
|
|
728
|
+
async function checkCICDCLIs(_options) {
|
|
729
|
+
const { execFileSync } = await import('child_process');
|
|
730
|
+
const clis = [
|
|
731
|
+
{ name: 'gh (GitHub CLI)', cmd: 'gh', args: ['--version'] },
|
|
732
|
+
{ name: 'glab (GitLab CLI)', cmd: 'glab', args: ['--version'] },
|
|
733
|
+
{ name: 'circleci CLI', cmd: 'circleci', args: ['--version'] },
|
|
734
|
+
];
|
|
735
|
+
const found = [];
|
|
736
|
+
for (const cli of clis) {
|
|
737
|
+
try {
|
|
738
|
+
execFileSync(cli.cmd, cli.args, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
739
|
+
found.push(cli.name);
|
|
740
|
+
}
|
|
741
|
+
catch { /* not installed */ }
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
name: 'CI/CD CLIs',
|
|
745
|
+
passed: true,
|
|
746
|
+
message: found.length > 0 ? `Found: ${found.join(', ')}` : 'No CI/CD CLIs installed (gh, glab, circleci are optional)',
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Check GitOps CLIs: argocd, flux (H2/L10)
|
|
751
|
+
*/
|
|
752
|
+
async function checkGitOpsCLIs(_options) {
|
|
753
|
+
const { execFileSync } = await import('child_process');
|
|
754
|
+
const clis = [
|
|
755
|
+
{ name: 'argocd', cmd: 'argocd', args: ['version', '--client'] },
|
|
756
|
+
{ name: 'flux', cmd: 'flux', args: ['--version'] },
|
|
757
|
+
];
|
|
758
|
+
const found = [];
|
|
759
|
+
for (const cli of clis) {
|
|
760
|
+
try {
|
|
761
|
+
execFileSync(cli.cmd, cli.args, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
762
|
+
found.push(cli.name);
|
|
763
|
+
}
|
|
764
|
+
catch { /* not installed */ }
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
name: 'GitOps CLIs',
|
|
768
|
+
passed: true,
|
|
769
|
+
message: found.length > 0 ? `Found: ${found.join(', ')}` : 'No GitOps CLIs installed (argocd, flux are optional)',
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Pre-flight checks for common DevOps issues (L10)
|
|
774
|
+
*/
|
|
775
|
+
async function checkDevOpsPreFlight(options) {
|
|
776
|
+
const { execFileSync } = await import('child_process');
|
|
777
|
+
const issues = [];
|
|
778
|
+
const hints = [];
|
|
779
|
+
// kubectl cluster reachability
|
|
780
|
+
try {
|
|
781
|
+
execFileSync('kubectl', ['cluster-info', '--request-timeout=5s'], {
|
|
782
|
+
encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
787
|
+
if (!msg.includes('not found') && !msg.includes('ENOENT')) {
|
|
788
|
+
issues.push('kubectl: cannot reach cluster');
|
|
789
|
+
hints.push('Check kubectl context: `kubectl config current-context`');
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// helm repos
|
|
793
|
+
try {
|
|
794
|
+
const out = execFileSync('helm', ['repo', 'list', '-o', 'json'], {
|
|
795
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
796
|
+
});
|
|
797
|
+
const repos = JSON.parse(out || '[]');
|
|
798
|
+
if (!Array.isArray(repos) || repos.length === 0) {
|
|
799
|
+
hints.push('No Helm repos configured. Add one: `helm repo add stable https://charts.helm.sh/stable`');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
catch { /* helm not installed or no repos */ }
|
|
803
|
+
// GCP project
|
|
804
|
+
if (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.CLOUDSDK_CORE_PROJECT) {
|
|
805
|
+
try {
|
|
806
|
+
const proj = execFileSync('gcloud', ['config', 'get-value', 'project'], {
|
|
807
|
+
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
808
|
+
}).trim();
|
|
809
|
+
if (!proj || proj === '(unset)') {
|
|
810
|
+
hints.push('GCP project not set. Run: `gcloud config set project <PROJECT_ID>`');
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch { /* gcloud not installed */ }
|
|
814
|
+
}
|
|
815
|
+
if (options.fix) {
|
|
816
|
+
// Auto-fix: helm repo update
|
|
817
|
+
try {
|
|
818
|
+
execFileSync('helm', ['repo', 'update'], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
819
|
+
}
|
|
820
|
+
catch { /* ignore */ }
|
|
821
|
+
}
|
|
822
|
+
if (issues.length > 0) {
|
|
823
|
+
return {
|
|
824
|
+
name: 'DevOps Pre-flight',
|
|
825
|
+
passed: false,
|
|
826
|
+
error: issues.join('; '),
|
|
827
|
+
fix: hints.join(' | '),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
name: 'DevOps Pre-flight',
|
|
832
|
+
passed: true,
|
|
833
|
+
message: hints.length > 0 ? `OK (warnings: ${hints.join('; ')})` : 'All pre-flight checks passed',
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/** M5: Check helm-secrets plugin and sops availability */
|
|
837
|
+
async function checkHelmSecrets(_options) {
|
|
838
|
+
const { execFileSync } = await import('child_process');
|
|
839
|
+
const warnings = [];
|
|
840
|
+
try {
|
|
841
|
+
const out = execFileSync('helm', ['plugin', 'list'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
842
|
+
if (!out.includes('secrets')) {
|
|
843
|
+
warnings.push('helm-secrets plugin not installed (run: helm plugin install https://github.com/jkroepke/helm-secrets)');
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
warnings.push('helm not available — cannot check helm-secrets plugin');
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
execFileSync('sops', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
warnings.push('sops not installed (run: brew install sops)');
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
name: 'Helm Secrets (M5)',
|
|
857
|
+
passed: true,
|
|
858
|
+
message: warnings.length > 0
|
|
859
|
+
? `Optional: ${warnings.join('; ')}`
|
|
860
|
+
: 'helm-secrets plugin and sops are available',
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* H6: Check Terraform infrastructure context
|
|
865
|
+
*/
|
|
866
|
+
async function checkInfraContext() {
|
|
867
|
+
const { existsSync } = await import('node:fs');
|
|
868
|
+
const { join } = await import('node:path');
|
|
869
|
+
const { exec } = await import('node:child_process');
|
|
870
|
+
const { promisify } = await import('node:util');
|
|
871
|
+
const execAsync2 = promisify(exec);
|
|
872
|
+
const cwd = process.cwd();
|
|
873
|
+
const hasTerraformDir = existsSync(join(cwd, '.terraform'));
|
|
874
|
+
const hasTfFiles = existsSync(join(cwd, 'main.tf')) || existsSync(join(cwd, 'variables.tf'));
|
|
875
|
+
if (!hasTfFiles && !hasTerraformDir) {
|
|
876
|
+
return { name: 'Terraform Context', passed: true, message: 'No Terraform configuration in current directory' };
|
|
877
|
+
}
|
|
878
|
+
if (hasTfFiles && !hasTerraformDir) {
|
|
879
|
+
return {
|
|
880
|
+
name: 'Terraform Context',
|
|
881
|
+
passed: false,
|
|
882
|
+
error: 'Terraform files found but not initialized.',
|
|
883
|
+
fix: 'Run: terraform init',
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (hasTerraformDir) {
|
|
887
|
+
try {
|
|
888
|
+
const { stdout } = await execAsync2('terraform workspace list', { cwd, timeout: 10_000 });
|
|
889
|
+
const workspaces = stdout.trim().split('\n').map((w) => w.trim());
|
|
890
|
+
const active = workspaces.find((w) => w.startsWith('*')) ?? 'default';
|
|
891
|
+
return { name: 'Terraform Context', passed: true, message: `Terraform initialized. Active workspace: ${active.replace('* ', '')}` };
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return { name: 'Terraform Context', passed: true, message: 'Terraform initialized but workspace check failed (connectivity issue)' };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return { name: 'Terraform Context', passed: true, message: 'No Terraform context found' };
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* H6: Check Kubernetes cluster reachability
|
|
901
|
+
*/
|
|
902
|
+
async function checkKubeConfig() {
|
|
903
|
+
const { exec } = await import('node:child_process');
|
|
904
|
+
const { promisify } = await import('node:util');
|
|
905
|
+
const execAsync2 = promisify(exec);
|
|
906
|
+
try {
|
|
907
|
+
const { stdout: ctx } = await execAsync2('kubectl config current-context', { timeout: 5_000 });
|
|
908
|
+
const context = ctx.trim();
|
|
909
|
+
if (!context)
|
|
910
|
+
return { name: 'Kubernetes Reachability', passed: true, message: 'kubectl: no active context' };
|
|
911
|
+
try {
|
|
912
|
+
await execAsync2('kubectl cluster-info --request-timeout=3s', { timeout: 8_000 });
|
|
913
|
+
try {
|
|
914
|
+
const { stdout: ns } = await execAsync2('kubectl config view --minify -o jsonpath={..namespace}', { timeout: 3_000 });
|
|
915
|
+
const namespace = ns.trim() || 'default';
|
|
916
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}", namespace "${namespace}" — cluster reachable` };
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}" — cluster reachable` };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
return { name: 'Kubernetes Reachability', passed: true, message: `kubectl: context "${context}" — cluster not reachable (check VPN/credentials)` };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
return { name: 'Kubernetes Reachability', passed: true, message: 'kubectl: no context configured (not required)' };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* H6: Check Helm releases
|
|
932
|
+
*/
|
|
933
|
+
async function checkHelmReleases() {
|
|
934
|
+
const { exec } = await import('node:child_process');
|
|
935
|
+
const { promisify } = await import('node:util');
|
|
936
|
+
const execAsync2 = promisify(exec);
|
|
937
|
+
try {
|
|
938
|
+
await execAsync2('which helm', { timeout: 3_000 });
|
|
939
|
+
const { stdout } = await execAsync2('helm list -A --output json', { timeout: 15_000 });
|
|
940
|
+
const releases = JSON.parse(stdout || '[]');
|
|
941
|
+
return { name: 'Helm Releases', passed: true, message: `Helm: ${releases.length} release(s) across all namespaces` };
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
return { name: 'Helm Releases', passed: true, message: 'Helm not installed or no releases found' };
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* M2: Check LLM connectivity by sending a minimal ping request.
|
|
949
|
+
*/
|
|
950
|
+
async function checkLLMConnectivity(_options) {
|
|
951
|
+
try {
|
|
952
|
+
const { initApp } = await import('../app');
|
|
953
|
+
const { router } = await initApp();
|
|
954
|
+
let provider = 'unknown';
|
|
955
|
+
try {
|
|
956
|
+
const { loadLLMConfig } = await import('../llm/config-loader');
|
|
957
|
+
const cfg = loadLLMConfig();
|
|
958
|
+
provider = cfg.defaultProvider ?? 'anthropic';
|
|
959
|
+
}
|
|
960
|
+
catch { /* ignore */ }
|
|
961
|
+
await Promise.race([
|
|
962
|
+
router.route({ messages: [{ role: 'user', content: 'ping' }], maxTokens: 1 }),
|
|
963
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000)),
|
|
964
|
+
]);
|
|
965
|
+
return { name: 'LLM Connectivity', passed: true, message: `Connected to ${provider}` };
|
|
966
|
+
}
|
|
967
|
+
catch (e) {
|
|
968
|
+
return {
|
|
969
|
+
name: 'LLM Connectivity',
|
|
970
|
+
passed: false,
|
|
971
|
+
error: e.message,
|
|
972
|
+
fix: 'Run nimbus login to reconfigure',
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* H4: Check DevOps CLI versions with structured version parsing
|
|
978
|
+
*/
|
|
979
|
+
async function checkDevOpsCLIs(_options) {
|
|
980
|
+
const { execFileSync } = await import('child_process');
|
|
981
|
+
const tools = [
|
|
982
|
+
{ name: 'terraform', args: ['version', '-json'], parse: (o) => { try {
|
|
983
|
+
return JSON.parse(o).terraform_version;
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return undefined;
|
|
987
|
+
} } },
|
|
988
|
+
{ name: 'kubectl', args: ['version', '--client', '--output=json'], parse: (o) => { try {
|
|
989
|
+
return JSON.parse(o).clientVersion?.gitVersion;
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return undefined;
|
|
993
|
+
} } },
|
|
994
|
+
{ name: 'helm', args: ['version', '--short'], parse: (o) => o.trim() },
|
|
995
|
+
{ name: 'aws', args: ['--version'], parse: (o) => o.split('/')[1]?.split(' ')[0] ?? o.trim() },
|
|
996
|
+
{ name: 'docker', args: ['--version'], parse: (o) => o.replace('Docker version ', '').split(',')[0] },
|
|
997
|
+
];
|
|
998
|
+
const results = [];
|
|
999
|
+
const missing = [];
|
|
1000
|
+
for (const t of tools) {
|
|
1001
|
+
try {
|
|
1002
|
+
const out = execFileSync(t.name, t.args, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1003
|
+
const ver = t.parse(out);
|
|
1004
|
+
results.push(` ${t.name.padEnd(12)} ${ver ?? 'installed'}`);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
missing.push(t.name);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const passed = missing.length === 0;
|
|
1011
|
+
return {
|
|
1012
|
+
name: 'DevOps CLIs',
|
|
1013
|
+
passed,
|
|
1014
|
+
message: passed ? `All CLIs found:\n${results.join('\n')}` : `Installed:\n${results.join('\n')}`,
|
|
1015
|
+
error: missing.length > 0 ? `Not found in PATH: ${missing.join(', ')}` : undefined,
|
|
1016
|
+
fix: missing.length > 0 ? `Install missing tools: ${missing.join(', ')}` : undefined,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* H7: Check Node.js version (>= 18) and tsx availability
|
|
1021
|
+
*/
|
|
1022
|
+
async function checkNodeRuntime(_options) {
|
|
1023
|
+
const nodeVersion = process.versions.node;
|
|
1024
|
+
const majorStr = nodeVersion.split('.')[0];
|
|
1025
|
+
const major = parseInt(majorStr ?? '0', 10);
|
|
1026
|
+
if (major < 18) {
|
|
1027
|
+
return {
|
|
1028
|
+
name: 'Node.js Runtime',
|
|
1029
|
+
passed: false,
|
|
1030
|
+
error: `Node.js ${nodeVersion} is too old (requires >= 18)`,
|
|
1031
|
+
fix: 'Upgrade Node.js: https://nodejs.org/',
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
// Check tsx availability
|
|
1035
|
+
const { execFileSync } = await import('child_process');
|
|
1036
|
+
let tsxVersion;
|
|
1037
|
+
try {
|
|
1038
|
+
tsxVersion = execFileSync('npx', ['tsx', '--version'], {
|
|
1039
|
+
encoding: 'utf-8',
|
|
1040
|
+
timeout: 5000,
|
|
1041
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1042
|
+
}).trim();
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
// tsx may be installed locally without npx
|
|
1046
|
+
try {
|
|
1047
|
+
const path = await import('path');
|
|
1048
|
+
const { existsSync } = await import('fs');
|
|
1049
|
+
const localTsx = path.join(process.cwd(), 'node_modules', '.bin', 'tsx');
|
|
1050
|
+
if (existsSync(localTsx)) {
|
|
1051
|
+
tsxVersion = 'installed (local)';
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
catch { /* ignore */ }
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
name: 'Node.js Runtime',
|
|
1058
|
+
passed: true,
|
|
1059
|
+
message: `Node.js ${nodeVersion}${tsxVersion ? ` tsx: ${tsxVersion}` : ' tsx: not found (install tsx for dev mode)'}`,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* All diagnostic checks
|
|
1064
|
+
*/
|
|
1065
|
+
const DIAGNOSTIC_CHECKS = [
|
|
1066
|
+
{ name: 'Node.js Runtime', check: checkNodeRuntime },
|
|
1067
|
+
{ name: 'Configuration', check: checkConfiguration },
|
|
1068
|
+
{ name: 'LLM Provider', check: checkLLMProvider },
|
|
1069
|
+
{ name: 'LLM Connectivity', check: checkLLMConnectivity },
|
|
1070
|
+
{ name: 'Core Systems', check: checkCoreServices },
|
|
1071
|
+
{ name: 'DevOps Tools', check: checkToolServices },
|
|
1072
|
+
{ name: 'Cloud Credentials', check: checkCloudCredentials },
|
|
1073
|
+
{ name: 'Cloud Connectivity', check: checkCloudConnectivity },
|
|
1074
|
+
{ name: 'Dependencies', check: checkDependencies },
|
|
1075
|
+
{ name: 'Disk Space', check: checkDiskSpace },
|
|
1076
|
+
{ name: 'Network', check: checkNetwork },
|
|
1077
|
+
{ name: 'Docker Daemon', check: checkDockerDaemon },
|
|
1078
|
+
{ name: 'Vault', check: checkVault },
|
|
1079
|
+
{ name: 'CI/CD CLIs', check: checkCICDCLIs },
|
|
1080
|
+
{ name: 'GitOps CLIs', check: checkGitOpsCLIs },
|
|
1081
|
+
{ name: 'Helm Secrets', check: checkHelmSecrets },
|
|
1082
|
+
{ name: 'DevOps Pre-flight', check: checkDevOpsPreFlight },
|
|
1083
|
+
{ name: 'Terraform Context', check: checkInfraContext },
|
|
1084
|
+
{ name: 'Kubernetes Reachability', check: checkKubeConfig },
|
|
1085
|
+
{ name: 'Helm Releases', check: checkHelmReleases },
|
|
1086
|
+
{ name: 'DevOps CLIs', check: checkDevOpsCLIs },
|
|
1087
|
+
];
|
|
1088
|
+
/**
|
|
1089
|
+
* Run a fast pre-flight check before starting the TUI (<500ms per check).
|
|
1090
|
+
* Only checks that do NOT require network access are included here.
|
|
1091
|
+
*
|
|
1092
|
+
* Critical failures prevent TUI startup; warnings are surfaced as system messages.
|
|
1093
|
+
*/
|
|
1094
|
+
export async function runStartupChecks() {
|
|
1095
|
+
const critical = [];
|
|
1096
|
+
const warnings = [];
|
|
1097
|
+
// Critical: LLM credentials must be present
|
|
1098
|
+
const llmKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY'];
|
|
1099
|
+
const hasLLMKey = llmKeys.some(k => process.env[k]);
|
|
1100
|
+
if (!hasLLMKey) {
|
|
1101
|
+
// Also check stored credentials file
|
|
1102
|
+
try {
|
|
1103
|
+
const { join } = await import('node:path');
|
|
1104
|
+
const { homedir } = await import('node:os');
|
|
1105
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
1106
|
+
const credsFile = join(homedir(), '.nimbus', 'credentials.json');
|
|
1107
|
+
if (existsSync(credsFile)) {
|
|
1108
|
+
const creds = JSON.parse(readFileSync(credsFile, 'utf-8'));
|
|
1109
|
+
if (Object.keys(creds.providers ?? {}).length === 0) {
|
|
1110
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
critical.push('No LLM credentials found. Set ANTHROPIC_API_KEY or run `nimbus login`.');
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// Warning: no NIMBUS.md in CWD
|
|
1122
|
+
try {
|
|
1123
|
+
const { existsSync } = await import('node:fs');
|
|
1124
|
+
const { join } = await import('node:path');
|
|
1125
|
+
const hasNimbusMd = existsSync(join(process.cwd(), 'NIMBUS.md')) ||
|
|
1126
|
+
existsSync(join(process.cwd(), '.nimbus', 'NIMBUS.md'));
|
|
1127
|
+
if (!hasNimbusMd) {
|
|
1128
|
+
warnings.push('No NIMBUS.md found. Run `nimbus init` to generate project context.');
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch { /* ignore */ }
|
|
1132
|
+
// Warning: kubectl context not set
|
|
1133
|
+
try {
|
|
1134
|
+
const { execSync } = await import('node:child_process');
|
|
1135
|
+
execSync('kubectl config current-context', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
warnings.push('kubectl not configured or not in PATH. K8s operations will be unavailable.');
|
|
1139
|
+
}
|
|
1140
|
+
// Warning: terraform not in PATH
|
|
1141
|
+
try {
|
|
1142
|
+
const { execSync } = await import('node:child_process');
|
|
1143
|
+
execSync('terraform version', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
warnings.push('terraform not in PATH. Install terraform to use Terraform operations.');
|
|
1147
|
+
}
|
|
1148
|
+
return { critical, warnings };
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Run the doctor command
|
|
1152
|
+
*/
|
|
1153
|
+
export async function doctorCommand(options = {}) {
|
|
1154
|
+
logger.debug('Running doctor command', { options });
|
|
1155
|
+
// In quiet mode, suppress banner/header — only show findings
|
|
1156
|
+
if (!options.quiet) {
|
|
1157
|
+
ui.header('Nimbus Doctor');
|
|
1158
|
+
ui.info('Running diagnostic checks...');
|
|
1159
|
+
ui.newLine();
|
|
1160
|
+
}
|
|
1161
|
+
const results = [];
|
|
1162
|
+
let allPassed = true;
|
|
1163
|
+
for (const { name, check } of DIAGNOSTIC_CHECKS) {
|
|
1164
|
+
if (!options.quiet) {
|
|
1165
|
+
ui.write(` ${name.padEnd(20)}`);
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
const result = await check(options);
|
|
1169
|
+
results.push(result);
|
|
1170
|
+
if (result.passed) {
|
|
1171
|
+
if (!options.quiet) {
|
|
1172
|
+
ui.print(`${ui.color('✓', 'green')} ${result.message || 'OK'}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
allPassed = false;
|
|
1177
|
+
if (options.quiet) {
|
|
1178
|
+
// In quiet mode, only print failures
|
|
1179
|
+
ui.print(`FAIL ${name}: ${result.error || 'Failed'}${result.fix ? ` — ${result.fix}` : ''}`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
ui.print(`${ui.color('✗', 'red')} ${result.error || 'Failed'}`);
|
|
1183
|
+
if (options.fix && result.runFix) {
|
|
1184
|
+
ui.print(` → Attempting fix...`);
|
|
1185
|
+
try {
|
|
1186
|
+
await result.runFix();
|
|
1187
|
+
ui.print(` → ${ui.color('Fixed', 'green')}`);
|
|
1188
|
+
}
|
|
1189
|
+
catch (fixError) {
|
|
1190
|
+
ui.print(` → ${ui.color(`Fix failed: ${fixError.message}`, 'red')}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
else if (result.fix) {
|
|
1194
|
+
ui.print(` → ${ui.dim(result.fix)}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// Show details in verbose mode (not quiet)
|
|
1199
|
+
if (!options.quiet && options.verbose && result.details) {
|
|
1200
|
+
for (const [key, value] of Object.entries(result.details)) {
|
|
1201
|
+
if (Array.isArray(value)) {
|
|
1202
|
+
ui.print(` ${key}:`);
|
|
1203
|
+
for (const item of value) {
|
|
1204
|
+
if (typeof item === 'object') {
|
|
1205
|
+
ui.print(` - ${JSON.stringify(item)}`);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
ui.print(` - ${item}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
ui.print(` ${key}: ${value}`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
if (!options.quiet) {
|
|
1220
|
+
ui.print(`${ui.color('✗', 'red')} Error: ${error.message}`);
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
ui.print(`FAIL ${name}: Error: ${error.message}`);
|
|
1224
|
+
}
|
|
1225
|
+
results.push({
|
|
1226
|
+
name,
|
|
1227
|
+
passed: false,
|
|
1228
|
+
error: error.message,
|
|
1229
|
+
});
|
|
1230
|
+
allPassed = false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (!options.quiet) {
|
|
1234
|
+
ui.newLine();
|
|
1235
|
+
}
|
|
1236
|
+
// JSON output
|
|
1237
|
+
if (options.json) {
|
|
1238
|
+
console.log(JSON.stringify({
|
|
1239
|
+
passed: allPassed,
|
|
1240
|
+
results: results.map(r => ({
|
|
1241
|
+
name: r.name,
|
|
1242
|
+
passed: r.passed,
|
|
1243
|
+
message: r.message,
|
|
1244
|
+
error: r.error,
|
|
1245
|
+
details: r.details,
|
|
1246
|
+
})),
|
|
1247
|
+
}, null, 2));
|
|
1248
|
+
if (!allPassed)
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
// Summary
|
|
1253
|
+
const passedCount = results.filter(r => r.passed).length;
|
|
1254
|
+
const totalCount = results.length;
|
|
1255
|
+
if (allPassed) {
|
|
1256
|
+
if (!options.quiet) {
|
|
1257
|
+
ui.success(`All checks passed! (${passedCount}/${totalCount})`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
const failedCount = totalCount - passedCount;
|
|
1262
|
+
if (!options.quiet) {
|
|
1263
|
+
ui.warning(`${failedCount} check(s) failed. ${passedCount}/${totalCount} passed.`);
|
|
1264
|
+
ui.newLine();
|
|
1265
|
+
ui.info('Run with --fix to attempt automatic fixes');
|
|
1266
|
+
ui.info('Run with --verbose for more details');
|
|
1267
|
+
}
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
// Quality Metrics (suppressed in quiet mode)
|
|
1271
|
+
if (options.metrics && !options.quiet) {
|
|
1272
|
+
ui.newLine();
|
|
1273
|
+
ui.header('Quality Metrics');
|
|
1274
|
+
try {
|
|
1275
|
+
const { getDb } = await import('../state/db');
|
|
1276
|
+
const db = getDb();
|
|
1277
|
+
// Get basic usage stats from the local SQLite database
|
|
1278
|
+
const sessionsRow = db.prepare('SELECT COUNT(*) as count FROM sessions').get();
|
|
1279
|
+
const sessionCount = sessionsRow?.count ?? 0;
|
|
1280
|
+
ui.newLine();
|
|
1281
|
+
ui.print(` Total sessions ${sessionCount}`);
|
|
1282
|
+
ui.print(` Database ~/.nimbus/nimbus.db`);
|
|
1283
|
+
ui.print(` Detailed metrics nimbus serve (HTTP API)`);
|
|
1284
|
+
}
|
|
1285
|
+
catch {
|
|
1286
|
+
ui.warning('Could not fetch metrics. Run "nimbus serve" for the full metrics API.');
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
// Export as default command
|
|
1291
|
+
export default doctorCommand;
|