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