@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.
Files changed (469) hide show
  1. package/bin/nimbus +26 -10
  2. package/bin/nimbus.cmd +41 -0
  3. package/bin/nimbus.mjs +70 -0
  4. package/completions/nimbus.bash +38 -0
  5. package/completions/nimbus.fish +48 -0
  6. package/completions/nimbus.zsh +81 -0
  7. package/dist/src/agent/compaction-agent.js +215 -0
  8. package/dist/src/agent/context-manager.js +385 -0
  9. package/dist/src/agent/context.js +322 -0
  10. package/dist/src/agent/deploy-preview.js +395 -0
  11. package/dist/src/agent/expand-files.js +95 -0
  12. package/dist/src/agent/index.js +18 -0
  13. package/dist/src/agent/loop.js +1535 -0
  14. package/dist/src/agent/modes.js +347 -0
  15. package/dist/src/agent/permissions.js +396 -0
  16. package/dist/src/agent/subagents/base.js +67 -0
  17. package/dist/src/agent/subagents/cost.js +45 -0
  18. package/dist/src/agent/subagents/explore.js +36 -0
  19. package/dist/src/agent/subagents/general.js +41 -0
  20. package/dist/src/agent/subagents/index.js +88 -0
  21. package/dist/src/agent/subagents/infra.js +52 -0
  22. package/dist/src/agent/subagents/security.js +60 -0
  23. package/dist/src/agent/system-prompt.js +860 -0
  24. package/dist/src/app.js +152 -0
  25. package/dist/src/audit/activity-log.js +209 -0
  26. package/dist/src/audit/compliance-checker.js +419 -0
  27. package/dist/src/audit/cost-tracker.js +231 -0
  28. package/dist/src/audit/index.js +10 -0
  29. package/dist/src/audit/security-scanner.js +490 -0
  30. package/dist/src/auth/guard.js +64 -0
  31. package/dist/src/auth/index.js +19 -0
  32. package/dist/src/auth/keychain.js +79 -0
  33. package/dist/src/auth/oauth.js +389 -0
  34. package/dist/src/auth/providers.js +415 -0
  35. package/dist/src/auth/sso.js +87 -0
  36. package/dist/src/auth/store.js +424 -0
  37. package/dist/src/auth/types.js +5 -0
  38. package/dist/src/cli/index.js +8 -0
  39. package/dist/src/cli/init.js +1048 -0
  40. package/dist/src/cli/openapi-spec.js +346 -0
  41. package/dist/src/cli/run.js +505 -0
  42. package/dist/src/cli/serve-auth.js +56 -0
  43. package/dist/src/cli/serve.js +432 -0
  44. package/dist/src/cli/web.js +50 -0
  45. package/dist/src/cli.js +1574 -0
  46. package/dist/src/clients/core-engine-client.js +156 -0
  47. package/dist/src/clients/enterprise-client.js +246 -0
  48. package/dist/src/clients/generator-client.js +219 -0
  49. package/dist/src/clients/git-client.js +367 -0
  50. package/dist/src/clients/github-client.js +229 -0
  51. package/dist/src/clients/helm-client.js +299 -0
  52. package/dist/src/clients/index.js +18 -0
  53. package/dist/src/clients/k8s-client.js +270 -0
  54. package/dist/src/clients/llm-client.js +119 -0
  55. package/dist/src/clients/rest-client.js +104 -0
  56. package/dist/src/clients/service-discovery.js +35 -0
  57. package/dist/src/clients/terraform-client.js +302 -0
  58. package/dist/src/clients/tools-client.js +1227 -0
  59. package/dist/src/clients/ws-client.js +93 -0
  60. package/dist/src/commands/alias.js +91 -0
  61. package/dist/src/commands/analyze/index.js +313 -0
  62. package/dist/src/commands/apply/helm.js +375 -0
  63. package/dist/src/commands/apply/index.js +176 -0
  64. package/dist/src/commands/apply/k8s.js +350 -0
  65. package/dist/src/commands/apply/terraform.js +465 -0
  66. package/dist/src/commands/ask.js +137 -0
  67. package/dist/src/commands/audit/index.js +322 -0
  68. package/dist/src/commands/auth-cloud.js +345 -0
  69. package/dist/src/commands/auth-list.js +112 -0
  70. package/dist/src/commands/auth-profile.js +104 -0
  71. package/dist/src/commands/auth-refresh.js +161 -0
  72. package/dist/src/commands/auth-status.js +122 -0
  73. package/dist/src/commands/aws/ec2.js +402 -0
  74. package/dist/src/commands/aws/iam.js +304 -0
  75. package/dist/src/commands/aws/index.js +108 -0
  76. package/dist/src/commands/aws/lambda.js +317 -0
  77. package/dist/src/commands/aws/rds.js +345 -0
  78. package/dist/src/commands/aws/s3.js +346 -0
  79. package/dist/src/commands/aws/vpc.js +302 -0
  80. package/dist/src/commands/aws-discover.js +413 -0
  81. package/dist/src/commands/aws-terraform.js +618 -0
  82. package/dist/src/commands/azure/aks.js +305 -0
  83. package/dist/src/commands/azure/functions.js +200 -0
  84. package/dist/src/commands/azure/index.js +93 -0
  85. package/dist/src/commands/azure/storage.js +378 -0
  86. package/dist/src/commands/azure/vm.js +291 -0
  87. package/dist/src/commands/billing/index.js +224 -0
  88. package/dist/src/commands/chat.js +259 -0
  89. package/dist/src/commands/completions.js +255 -0
  90. package/dist/src/commands/config.js +291 -0
  91. package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
  92. package/dist/src/commands/cost/estimator.js +73 -0
  93. package/dist/src/commands/cost/index.js +625 -0
  94. package/dist/src/commands/cost/parsers/terraform.js +234 -0
  95. package/dist/src/commands/cost/parsers/types.js +4 -0
  96. package/dist/src/commands/cost/pricing/aws.js +501 -0
  97. package/dist/src/commands/cost/pricing/azure.js +462 -0
  98. package/dist/src/commands/cost/pricing/gcp.js +359 -0
  99. package/dist/src/commands/cost/pricing/index.js +24 -0
  100. package/dist/src/commands/demo.js +196 -0
  101. package/dist/src/commands/deploy.js +215 -0
  102. package/dist/src/commands/doctor.js +1291 -0
  103. package/dist/src/commands/drift/index.js +674 -0
  104. package/dist/src/commands/explain.js +235 -0
  105. package/dist/src/commands/export.js +120 -0
  106. package/dist/src/commands/feedback.js +319 -0
  107. package/dist/src/commands/fix.js +263 -0
  108. package/dist/src/commands/fs/index.js +338 -0
  109. package/dist/src/commands/gcp/compute.js +266 -0
  110. package/dist/src/commands/gcp/functions.js +221 -0
  111. package/dist/src/commands/gcp/gke.js +357 -0
  112. package/dist/src/commands/gcp/iam.js +295 -0
  113. package/dist/src/commands/gcp/index.js +105 -0
  114. package/dist/src/commands/gcp/storage.js +232 -0
  115. package/dist/src/commands/generate-helm.js +1026 -0
  116. package/dist/src/commands/generate-k8s.js +1263 -0
  117. package/dist/src/commands/generate-terraform.js +1058 -0
  118. package/dist/src/commands/gh/index.js +663 -0
  119. package/dist/src/commands/git/index.js +1208 -0
  120. package/dist/src/commands/helm/index.js +985 -0
  121. package/dist/src/commands/help.js +639 -0
  122. package/dist/src/commands/history.js +120 -0
  123. package/dist/src/commands/import.js +782 -0
  124. package/dist/src/commands/incident.js +144 -0
  125. package/dist/src/commands/index.js +109 -0
  126. package/dist/src/commands/init.js +955 -0
  127. package/dist/src/commands/k8s/index.js +979 -0
  128. package/dist/src/commands/login.js +588 -0
  129. package/dist/src/commands/logout.js +61 -0
  130. package/dist/src/commands/logs.js +160 -0
  131. package/dist/src/commands/onboarding.js +382 -0
  132. package/dist/src/commands/pipeline.js +153 -0
  133. package/dist/src/commands/plan/display.js +216 -0
  134. package/dist/src/commands/plan/index.js +525 -0
  135. package/dist/src/commands/plugin.js +325 -0
  136. package/dist/src/commands/preview.js +356 -0
  137. package/dist/src/commands/profile.js +297 -0
  138. package/dist/src/commands/questionnaire.js +1021 -0
  139. package/dist/src/commands/resume.js +35 -0
  140. package/dist/src/commands/rollback.js +259 -0
  141. package/dist/src/commands/rollout.js +74 -0
  142. package/dist/src/commands/runbook.js +307 -0
  143. package/dist/src/commands/schedule.js +202 -0
  144. package/dist/src/commands/status.js +213 -0
  145. package/dist/src/commands/team/index.js +309 -0
  146. package/dist/src/commands/team-context.js +200 -0
  147. package/dist/src/commands/template.js +204 -0
  148. package/dist/src/commands/tf/index.js +989 -0
  149. package/dist/src/commands/upgrade.js +515 -0
  150. package/dist/src/commands/usage/index.js +118 -0
  151. package/dist/src/commands/version.js +145 -0
  152. package/dist/src/commands/watch.js +127 -0
  153. package/dist/src/compat/index.js +2 -0
  154. package/dist/src/compat/runtime.js +10 -0
  155. package/dist/src/compat/sqlite.js +144 -0
  156. package/dist/src/config/index.js +6 -0
  157. package/dist/src/config/manager.js +469 -0
  158. package/dist/src/config/mode-store.js +57 -0
  159. package/dist/src/config/profiles.js +66 -0
  160. package/dist/src/config/safety-policy.js +251 -0
  161. package/dist/src/config/schema.js +107 -0
  162. package/dist/src/config/types.js +311 -0
  163. package/dist/src/config/workspace-state.js +38 -0
  164. package/dist/src/context/context-db.js +138 -0
  165. package/dist/src/demo/index.js +295 -0
  166. package/dist/src/demo/scenarios/full-journey.js +226 -0
  167. package/dist/src/demo/scenarios/getting-started.js +124 -0
  168. package/dist/src/demo/scenarios/helm-release.js +334 -0
  169. package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
  170. package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
  171. package/dist/src/demo/types.js +6 -0
  172. package/dist/src/engine/cost-estimator.js +334 -0
  173. package/dist/src/engine/diagram-generator.js +192 -0
  174. package/dist/src/engine/drift-detector.js +688 -0
  175. package/dist/src/engine/executor.js +832 -0
  176. package/dist/src/engine/index.js +39 -0
  177. package/dist/src/engine/orchestrator.js +436 -0
  178. package/dist/src/engine/planner.js +616 -0
  179. package/dist/src/engine/safety.js +609 -0
  180. package/dist/src/engine/verifier.js +664 -0
  181. package/dist/src/enterprise/audit.js +241 -0
  182. package/dist/src/enterprise/auth.js +189 -0
  183. package/dist/src/enterprise/billing.js +512 -0
  184. package/dist/src/enterprise/index.js +16 -0
  185. package/dist/src/enterprise/teams.js +315 -0
  186. package/dist/src/generator/best-practices.js +1375 -0
  187. package/dist/src/generator/helm.js +495 -0
  188. package/dist/src/generator/index.js +11 -0
  189. package/dist/src/generator/intent-parser.js +420 -0
  190. package/dist/src/generator/kubernetes.js +773 -0
  191. package/dist/src/generator/terraform.js +1472 -0
  192. package/dist/src/history/index.js +6 -0
  193. package/dist/src/history/manager.js +199 -0
  194. package/dist/src/history/types.js +6 -0
  195. package/dist/src/hooks/config.js +318 -0
  196. package/dist/src/hooks/engine.js +317 -0
  197. package/dist/src/hooks/index.js +2 -0
  198. package/dist/src/llm/auth-bridge.js +157 -0
  199. package/dist/src/llm/circuit-breaker.js +116 -0
  200. package/dist/src/llm/config-loader.js +172 -0
  201. package/dist/src/llm/cost-calculator.js +137 -0
  202. package/dist/src/llm/index.js +7 -0
  203. package/dist/src/llm/model-aliases.js +99 -0
  204. package/dist/src/llm/provider-registry.js +57 -0
  205. package/dist/src/llm/providers/anthropic.js +430 -0
  206. package/dist/src/llm/providers/bedrock.js +409 -0
  207. package/dist/src/llm/providers/google.js +344 -0
  208. package/dist/src/llm/providers/ollama.js +661 -0
  209. package/dist/src/llm/providers/openai-compatible.js +289 -0
  210. package/dist/src/llm/providers/openai.js +284 -0
  211. package/dist/src/llm/providers/openrouter.js +293 -0
  212. package/dist/src/llm/router.js +844 -0
  213. package/dist/src/llm/types.js +69 -0
  214. package/dist/src/lsp/client.js +239 -0
  215. package/dist/src/lsp/languages.js +95 -0
  216. package/dist/src/lsp/manager.js +243 -0
  217. package/dist/src/mcp/client.js +289 -0
  218. package/dist/src/mcp/index.js +5 -0
  219. package/dist/src/mcp/manager.js +113 -0
  220. package/dist/src/nimbus.js +212 -0
  221. package/dist/src/plugins/index.js +13 -0
  222. package/dist/src/plugins/loader.js +280 -0
  223. package/dist/src/plugins/manager.js +282 -0
  224. package/dist/src/plugins/types.js +23 -0
  225. package/dist/src/scanners/cicd-scanner.js +230 -0
  226. package/dist/src/scanners/cloud-scanner.js +415 -0
  227. package/dist/src/scanners/framework-scanner.js +430 -0
  228. package/dist/src/scanners/iac-scanner.js +350 -0
  229. package/dist/src/scanners/index.js +454 -0
  230. package/dist/src/scanners/language-scanner.js +258 -0
  231. package/dist/src/scanners/package-manager-scanner.js +252 -0
  232. package/dist/src/scanners/types.js +6 -0
  233. package/dist/src/sessions/manager.js +395 -0
  234. package/dist/src/sessions/types.js +4 -0
  235. package/dist/src/sharing/sync.js +238 -0
  236. package/dist/src/sharing/viewer.js +131 -0
  237. package/dist/src/snapshots/index.js +1 -0
  238. package/dist/src/snapshots/manager.js +432 -0
  239. package/dist/src/state/artifacts.js +94 -0
  240. package/dist/src/state/audit.js +73 -0
  241. package/dist/src/state/billing.js +126 -0
  242. package/dist/src/state/checkpoints.js +81 -0
  243. package/dist/src/state/config.js +58 -0
  244. package/dist/src/state/conversations.js +7 -0
  245. package/dist/src/state/credentials.js +96 -0
  246. package/dist/src/state/db.js +53 -0
  247. package/dist/src/state/index.js +23 -0
  248. package/dist/src/state/messages.js +76 -0
  249. package/dist/src/state/projects.js +92 -0
  250. package/dist/src/state/schema.js +233 -0
  251. package/dist/src/state/sessions.js +79 -0
  252. package/dist/src/state/teams.js +131 -0
  253. package/dist/src/telemetry.js +91 -0
  254. package/dist/src/tools/aws-ops.js +747 -0
  255. package/dist/src/tools/azure-ops.js +491 -0
  256. package/dist/src/tools/file-ops.js +451 -0
  257. package/dist/src/tools/gcp-ops.js +559 -0
  258. package/dist/src/tools/git-ops.js +557 -0
  259. package/dist/src/tools/github-ops.js +460 -0
  260. package/dist/src/tools/helm-ops.js +634 -0
  261. package/dist/src/tools/index.js +16 -0
  262. package/dist/src/tools/k8s-ops.js +579 -0
  263. package/dist/src/tools/schemas/converter.js +129 -0
  264. package/dist/src/tools/schemas/devops.js +3319 -0
  265. package/dist/src/tools/schemas/index.js +19 -0
  266. package/dist/src/tools/schemas/standard.js +966 -0
  267. package/dist/src/tools/schemas/types.js +409 -0
  268. package/dist/src/tools/spawn-exec.js +109 -0
  269. package/dist/src/tools/terraform-ops.js +627 -0
  270. package/dist/src/types/config.js +1 -0
  271. package/dist/src/types/drift.js +4 -0
  272. package/dist/src/types/enterprise.js +5 -0
  273. package/dist/src/types/index.js +14 -0
  274. package/dist/src/types/plan.js +1 -0
  275. package/dist/src/types/request.js +1 -0
  276. package/dist/src/types/response.js +1 -0
  277. package/dist/src/types/service.js +1 -0
  278. package/dist/src/ui/App.js +1672 -0
  279. package/dist/src/ui/DeployPreview.js +60 -0
  280. package/dist/src/ui/FileDiffModal.js +108 -0
  281. package/dist/src/ui/Header.js +46 -0
  282. package/dist/src/ui/HelpModal.js +9 -0
  283. package/dist/src/ui/InputBox.js +408 -0
  284. package/dist/src/ui/MessageList.js +795 -0
  285. package/dist/src/ui/PermissionPrompt.js +72 -0
  286. package/dist/src/ui/StatusBar.js +109 -0
  287. package/dist/src/ui/TerminalPane.js +31 -0
  288. package/dist/src/ui/ToolCallDisplay.js +303 -0
  289. package/dist/src/ui/TreePane.js +83 -0
  290. package/dist/src/ui/chat-ui.js +721 -0
  291. package/dist/src/ui/index.js +11 -0
  292. package/dist/src/ui/ink/index.js +1325 -0
  293. package/dist/src/ui/streaming.js +137 -0
  294. package/dist/src/ui/theme.js +78 -0
  295. package/dist/src/ui/types.js +7 -0
  296. package/dist/src/utils/analytics.js +61 -0
  297. package/dist/src/utils/cost-warning.js +25 -0
  298. package/dist/src/utils/env.js +42 -0
  299. package/dist/src/utils/errors.js +54 -0
  300. package/dist/src/utils/event-bus.js +22 -0
  301. package/dist/src/utils/index.js +16 -0
  302. package/dist/src/utils/logger.js +150 -0
  303. package/dist/src/utils/rate-limiter.js +90 -0
  304. package/dist/src/utils/service-auth.js +36 -0
  305. package/dist/src/utils/validation.js +39 -0
  306. package/dist/src/version.js +3 -0
  307. package/dist/src/watcher/index.js +192 -0
  308. package/dist/src/wizard/approval.js +275 -0
  309. package/dist/src/wizard/index.js +13 -0
  310. package/dist/src/wizard/prompts.js +273 -0
  311. package/dist/src/wizard/types.js +4 -0
  312. package/dist/src/wizard/ui.js +453 -0
  313. package/dist/src/wizard/wizard.js +227 -0
  314. package/package.json +31 -23
  315. package/src/__tests__/alias.test.ts +133 -0
  316. package/src/__tests__/app.test.ts +1 -1
  317. package/src/__tests__/audit.test.ts +1 -1
  318. package/src/__tests__/circuit-breaker.test.ts +1 -1
  319. package/src/__tests__/cli-run.test.ts +237 -1
  320. package/src/__tests__/compat-sqlite.test.ts +68 -0
  321. package/src/__tests__/context-manager.test.ts +131 -1
  322. package/src/__tests__/context.test.ts +1 -1
  323. package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
  324. package/src/__tests__/doctor.test.ts +48 -0
  325. package/src/__tests__/enterprise.test.ts +1 -1
  326. package/src/__tests__/export.test.ts +236 -0
  327. package/src/__tests__/gap-11-18-20.test.ts +958 -0
  328. package/src/__tests__/generator.test.ts +1 -1
  329. package/src/__tests__/helm-streaming.test.ts +127 -0
  330. package/src/__tests__/hooks.test.ts +1 -1
  331. package/src/__tests__/incident.test.ts +179 -0
  332. package/src/__tests__/init.test.ts +55 -4
  333. package/src/__tests__/intent-parser.test.ts +1 -1
  334. package/src/__tests__/llm-router.test.ts +1 -1
  335. package/src/__tests__/logs.test.ts +107 -0
  336. package/src/__tests__/loop-errors.test.ts +244 -0
  337. package/src/__tests__/lsp.test.ts +1 -1
  338. package/src/__tests__/modes.test.ts +1 -1
  339. package/src/__tests__/perf-optimizations.test.ts +847 -0
  340. package/src/__tests__/permissions.test.ts +1 -1
  341. package/src/__tests__/pipeline.test.ts +50 -0
  342. package/src/__tests__/polish-phase3.test.ts +340 -0
  343. package/src/__tests__/profile.test.ts +237 -0
  344. package/src/__tests__/rollback.test.ts +83 -0
  345. package/src/__tests__/runbook.test.ts +219 -0
  346. package/src/__tests__/schedule.test.ts +206 -0
  347. package/src/__tests__/serve.test.ts +1 -1
  348. package/src/__tests__/sessions.test.ts +96 -1
  349. package/src/__tests__/sharing.test.ts +53 -1
  350. package/src/__tests__/snapshots.test.ts +1 -1
  351. package/src/__tests__/standalone-migration.test.ts +199 -0
  352. package/src/__tests__/state-db.test.ts +1 -1
  353. package/src/__tests__/status.test.ts +158 -0
  354. package/src/__tests__/stream-with-tools.test.ts +71 -25
  355. package/src/__tests__/subagents.test.ts +1 -1
  356. package/src/__tests__/system-prompt.test.ts +82 -3
  357. package/src/__tests__/terminal-gap-v2.test.ts +395 -0
  358. package/src/__tests__/terminal-parity.test.ts +393 -0
  359. package/src/__tests__/tf-apply.test.ts +187 -0
  360. package/src/__tests__/tool-converter.test.ts +1 -1
  361. package/src/__tests__/tool-schemas.test.ts +209 -4
  362. package/src/__tests__/tools.test.ts +4 -3
  363. package/src/__tests__/version-json.test.ts +184 -0
  364. package/src/__tests__/version.test.ts +1 -1
  365. package/src/__tests__/watch.test.ts +129 -0
  366. package/src/agent/compaction-agent.ts +40 -1
  367. package/src/agent/context-manager.ts +67 -3
  368. package/src/agent/deploy-preview.ts +62 -1
  369. package/src/agent/expand-files.ts +108 -0
  370. package/src/agent/loop.ts +1312 -31
  371. package/src/agent/permissions.ts +51 -4
  372. package/src/agent/system-prompt.ts +573 -19
  373. package/src/app.ts +58 -0
  374. package/src/audit/security-scanner.ts +45 -0
  375. package/src/auth/keychain.ts +82 -0
  376. package/src/auth/oauth.ts +15 -5
  377. package/src/cli/init.ts +378 -5
  378. package/src/cli/run.ts +407 -16
  379. package/src/cli/serve.ts +78 -1
  380. package/src/cli/web.ts +10 -6
  381. package/src/cli.ts +312 -1
  382. package/src/clients/service-discovery.ts +30 -25
  383. package/src/commands/alias.ts +100 -0
  384. package/src/commands/audit/index.ts +121 -2
  385. package/src/commands/auth-cloud.ts +113 -0
  386. package/src/commands/auth-refresh.ts +187 -0
  387. package/src/commands/aws-discover.ts +144 -251
  388. package/src/commands/aws-terraform.ts +68 -118
  389. package/src/commands/chat.ts +9 -3
  390. package/src/commands/completions.ts +268 -0
  391. package/src/commands/config.ts +26 -0
  392. package/src/commands/cost/index.ts +218 -2
  393. package/src/commands/deploy.ts +260 -0
  394. package/src/commands/doctor.ts +744 -152
  395. package/src/commands/drift/index.ts +371 -23
  396. package/src/commands/export.ts +146 -0
  397. package/src/commands/generate-k8s.ts +9 -61
  398. package/src/commands/generate-terraform.ts +191 -449
  399. package/src/commands/help.ts +212 -36
  400. package/src/commands/history.ts +8 -1
  401. package/src/commands/incident.ts +166 -0
  402. package/src/commands/init.ts +5 -0
  403. package/src/commands/login.ts +86 -1
  404. package/src/commands/logs.ts +167 -0
  405. package/src/commands/onboarding.ts +211 -34
  406. package/src/commands/pipeline.ts +186 -0
  407. package/src/commands/plugin.ts +398 -0
  408. package/src/commands/profile.ts +342 -0
  409. package/src/commands/questionnaire.ts +0 -98
  410. package/src/commands/resume.ts +26 -34
  411. package/src/commands/rollback.ts +315 -0
  412. package/src/commands/rollout.ts +88 -0
  413. package/src/commands/runbook.ts +346 -0
  414. package/src/commands/schedule.ts +236 -0
  415. package/src/commands/status.ts +252 -0
  416. package/src/commands/team-context.ts +220 -0
  417. package/src/commands/template.ts +58 -57
  418. package/src/commands/tf/index.ts +70 -11
  419. package/src/commands/upgrade.ts +57 -0
  420. package/src/commands/version.ts +54 -50
  421. package/src/commands/watch.ts +153 -0
  422. package/src/compat/runtime.ts +1 -1
  423. package/src/compat/sqlite.ts +75 -5
  424. package/src/config/mode-store.ts +62 -0
  425. package/src/config/profiles.ts +84 -0
  426. package/src/config/types.ts +83 -1
  427. package/src/config/workspace-state.ts +53 -0
  428. package/src/engine/cost-estimator.ts +52 -10
  429. package/src/engine/executor.ts +33 -2
  430. package/src/engine/planner.ts +68 -1
  431. package/src/generator/terraform.ts +8 -0
  432. package/src/history/manager.ts +2 -74
  433. package/src/hooks/engine.ts +5 -4
  434. package/src/llm/cost-calculator.ts +2 -2
  435. package/src/llm/providers/anthropic.ts +50 -21
  436. package/src/llm/router.ts +76 -7
  437. package/src/lsp/languages.ts +3 -0
  438. package/src/lsp/manager.ts +21 -5
  439. package/src/nimbus.ts +37 -18
  440. package/src/sessions/manager.ts +108 -1
  441. package/src/sharing/sync.ts +4 -0
  442. package/src/sharing/viewer.ts +66 -0
  443. package/src/tools/file-ops.ts +22 -0
  444. package/src/tools/schemas/devops.ts +3007 -117
  445. package/src/tools/schemas/standard.ts +5 -1
  446. package/src/tools/schemas/types.ts +31 -1
  447. package/src/tools/spawn-exec.ts +148 -0
  448. package/src/ui/App.tsx +1183 -66
  449. package/src/ui/DeployPreview.tsx +62 -57
  450. package/src/ui/FileDiffModal.tsx +162 -0
  451. package/src/ui/Header.tsx +87 -24
  452. package/src/ui/HelpModal.tsx +57 -0
  453. package/src/ui/InputBox.tsx +163 -10
  454. package/src/ui/MessageList.tsx +487 -40
  455. package/src/ui/PermissionPrompt.tsx +17 -5
  456. package/src/ui/StatusBar.tsx +122 -3
  457. package/src/ui/TerminalPane.tsx +84 -0
  458. package/src/ui/ToolCallDisplay.tsx +252 -18
  459. package/src/ui/TreePane.tsx +132 -0
  460. package/src/ui/chat-ui.ts +41 -44
  461. package/src/ui/ink/index.ts +771 -38
  462. package/src/ui/streaming.ts +1 -1
  463. package/src/ui/theme.ts +104 -0
  464. package/src/ui/types.ts +18 -0
  465. package/src/version.ts +1 -1
  466. package/src/watcher/index.ts +66 -15
  467. package/src/wizard/types.ts +1 -0
  468. package/src/wizard/ui.ts +1 -1
  469. 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;