@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
@@ -6,7 +6,6 @@
6
6
 
7
7
  import { ui } from '../../wizard/ui';
8
8
  import { select, confirm } from '../../wizard/prompts';
9
- import { CoreEngineClient } from '../../clients/core-engine-client';
10
9
  import type { DriftReport, DriftRemediationResult, DriftProvider } from '../../types';
11
10
 
12
11
  // ==========================================
@@ -236,6 +235,318 @@ function displayRemediationResult(result: DriftRemediationResult): void {
236
235
  // Commands
237
236
  // ==========================================
238
237
 
238
+ /**
239
+ * Detect drift directly using CLI tools (no CoreEngineClient).
240
+ * For terraform: uses terraform plan -detailed-exitcode.
241
+ * For kubernetes/helm: returns a minimal "no API" report.
242
+ */
243
+ async function detectDriftDirect(provider: DriftProvider, directory: string): Promise<DriftReport> {
244
+ const { execFileSync } = await import('child_process');
245
+
246
+ if (provider === 'terraform') {
247
+ try {
248
+ execFileSync('terraform', ['plan', '-no-color', '-detailed-exitcode'], {
249
+ cwd: directory,
250
+ encoding: 'utf-8',
251
+ timeout: 120_000,
252
+ stdio: ['pipe', 'pipe', 'pipe'],
253
+ });
254
+ // exit 0 = no drift
255
+ return { hasDrift: false, provider, directory, detectedAt: new Date().toISOString(), resources: [], summary: { total: 0, added: 0, removed: 0, modified: 0, bySeverity: {} } };
256
+ } catch (e: any) {
257
+ if (e.status === 2) {
258
+ // exit 2 = changes present
259
+ const planOutput: string = e.stdout ?? '';
260
+ const planLine = planOutput.split('\n').find((l: string) => l.startsWith('Plan:')) ?? 'Changes detected';
261
+ return {
262
+ hasDrift: true,
263
+ provider,
264
+ directory,
265
+ detectedAt: new Date().toISOString(),
266
+ resources: [{ resourceId: planLine.trim(), resourceType: 'terraform', driftType: 'modified', severity: 'medium', changes: [] }],
267
+ summary: { total: 1, added: 0, removed: 0, modified: 1, bySeverity: { medium: 1 } },
268
+ };
269
+ }
270
+ throw new Error(`terraform plan failed: ${String(e.message ?? e).slice(0, 200)}`);
271
+ }
272
+ }
273
+
274
+ if (provider === 'kubernetes') {
275
+ try {
276
+ const out = execFileSync('kubectl', ['diff', '-R', '-f', directory], {
277
+ encoding: 'utf-8', timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
278
+ });
279
+ const hasDiff = out.trim().length > 0;
280
+ return { hasDrift: hasDiff, provider, directory, detectedAt: new Date().toISOString(), resources: [], summary: { total: hasDiff ? 1 : 0, added: 0, removed: 0, modified: hasDiff ? 1 : 0, bySeverity: {} } };
281
+ } catch (e: any) {
282
+ // kubectl diff exits 1 when there are differences
283
+ if (e.status === 1) {
284
+ return { hasDrift: true, provider, directory, detectedAt: new Date().toISOString(), resources: [], summary: { total: 1, added: 0, removed: 0, modified: 1, bySeverity: { medium: 1 } } };
285
+ }
286
+ throw new Error(`kubectl diff failed: ${String(e.message ?? e).slice(0, 200)}`);
287
+ }
288
+ }
289
+
290
+ if (provider === 'helm') {
291
+ try {
292
+ const out = execFileSync('helm', ['list', '--all-namespaces', '--output', 'json'], {
293
+ encoding: 'utf-8', timeout: 15_000, stdio: ['pipe', 'pipe', 'pipe'],
294
+ });
295
+ const releases: Array<{ status: string; name: string }> = JSON.parse(out || '[]');
296
+ const drifted = releases.filter(r => r.status !== 'deployed');
297
+ return {
298
+ hasDrift: drifted.length > 0,
299
+ provider,
300
+ directory,
301
+ detectedAt: new Date().toISOString(),
302
+ resources: drifted.map(r => ({ resourceId: r.name, resourceType: 'helm', driftType: 'modified' as const, severity: 'medium' as const, changes: [{ attribute: 'status', expected: 'deployed', actual: r.status }] })),
303
+ summary: { total: drifted.length, added: 0, removed: 0, modified: drifted.length, bySeverity: { medium: drifted.length } },
304
+ };
305
+ } catch (e: any) {
306
+ throw new Error(`helm list failed: ${String(e.message ?? e).slice(0, 200)}`);
307
+ }
308
+ }
309
+
310
+ throw new Error(`Unknown provider: ${provider}`);
311
+ }
312
+
313
+ /**
314
+ * Fix drift directly using CLI tools (no CoreEngineClient).
315
+ * For terraform: runs terraform apply -auto-approve.
316
+ * For kubernetes: runs kubectl apply -f <dir>.
317
+ * For helm: no automated fix; returns guidance.
318
+ */
319
+ async function fixDriftDirect(provider: DriftProvider, directory: string): Promise<DriftRemediationResult> {
320
+ const { execFileSync } = await import('child_process');
321
+
322
+ if (provider === 'terraform') {
323
+ try {
324
+ const output = execFileSync('terraform', ['apply', '-auto-approve', '-no-color'], {
325
+ cwd: directory,
326
+ encoding: 'utf-8',
327
+ timeout: 300_000,
328
+ stdio: ['pipe', 'pipe', 'pipe'],
329
+ });
330
+ return { success: true, appliedCount: 1, failedCount: 0, skippedCount: 0, actions: [{ id: '1', type: 'apply' as const, resourceId: directory, description: 'terraform apply', status: 'applied' as const }], report: output.slice(0, 500) };
331
+ } catch (e: any) {
332
+ return { success: false, appliedCount: 0, failedCount: 1, skippedCount: 0, actions: [{ id: '1', type: 'apply' as const, resourceId: directory, description: 'terraform apply', status: 'failed' as const, error: String(e.message ?? e).slice(0, 200) }] };
333
+ }
334
+ }
335
+
336
+ if (provider === 'kubernetes') {
337
+ try {
338
+ const output = execFileSync('kubectl', ['apply', '-R', '-f', directory], {
339
+ encoding: 'utf-8', timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'],
340
+ });
341
+ return { success: true, appliedCount: 1, failedCount: 0, skippedCount: 0, actions: [{ id: '1', type: 'apply' as const, resourceId: directory, description: 'kubectl apply', status: 'applied' as const }], report: output.slice(0, 500) };
342
+ } catch (e: any) {
343
+ return { success: false, appliedCount: 0, failedCount: 1, skippedCount: 0, actions: [{ id: '1', type: 'apply' as const, resourceId: directory, description: 'kubectl apply', status: 'failed' as const, error: String(e.message ?? e).slice(0, 200) }] };
344
+ }
345
+ }
346
+
347
+ // Helm: no automated fix
348
+ return { success: false, appliedCount: 0, failedCount: 0, skippedCount: 1, actions: [{ id: '1', type: 'manual' as const, resourceId: directory, description: 'helm fix', status: 'skipped' as const, error: 'Helm drift fix requires manual intervention. Run "helm upgrade <release> <chart>" to remediate.' }] };
349
+ }
350
+
351
+ /**
352
+ * H3: Direct drift scan — runs terraform plan -detailed-exitcode in all
353
+ * subdirectories that contain Terraform configs, without needing CoreEngineClient.
354
+ */
355
+ export async function driftScanCommand(opts: { workdir?: string; format?: 'table' | 'json' } = {}): Promise<void> {
356
+ const { execFileSync } = await import('child_process');
357
+ const fsSync = await import('fs');
358
+ const pathMod = await import('path');
359
+
360
+ const rootDir = pathMod.resolve(opts.workdir ?? process.cwd());
361
+
362
+ interface ScanResult {
363
+ directory: string;
364
+ status: 'clean' | 'drift' | 'error';
365
+ summary: string;
366
+ }
367
+
368
+ // Find terraform directories up to depth 3
369
+ const tfDirs: string[] = [];
370
+
371
+ function walk(dir: string, depth: number): void {
372
+ if (depth > 3) return;
373
+ try {
374
+ if (fsSync.existsSync(pathMod.join(dir, '.terraform')) || fsSync.readdirSync(dir).some(f => f.endsWith('.tf'))) {
375
+ tfDirs.push(dir);
376
+ }
377
+ for (const entry of fsSync.readdirSync(dir, { withFileTypes: true })) {
378
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
379
+ walk(pathMod.join(dir, entry.name), depth + 1);
380
+ }
381
+ }
382
+ } catch { /* skip unreadable dirs */ }
383
+ }
384
+
385
+ walk(rootDir, 0);
386
+
387
+ if (tfDirs.length === 0) {
388
+ ui.info('No Terraform directories found.');
389
+ return;
390
+ }
391
+
392
+ ui.header('Terraform Drift Scan');
393
+ const results: ScanResult[] = [];
394
+
395
+ for (const dir of tfDirs) {
396
+ const relDir = pathMod.relative(rootDir, dir) || '.';
397
+ try {
398
+ execFileSync('terraform', ['plan', '-no-color', '-detailed-exitcode'], {
399
+ cwd: dir,
400
+ encoding: 'utf-8',
401
+ timeout: 120_000,
402
+ stdio: ['pipe', 'pipe', 'pipe'],
403
+ });
404
+ // exit 0 = no changes
405
+ results.push({ directory: relDir, status: 'clean', summary: 'No changes' });
406
+ } catch (e: any) {
407
+ if (e.status === 2) {
408
+ // exit 2 = changes present
409
+ const planOutput: string = e.stdout ?? '';
410
+ const planLine = planOutput.split('\n').find((l: string) => l.startsWith('Plan:')) ?? 'Changes detected';
411
+ results.push({ directory: relDir, status: 'drift', summary: planLine.trim() });
412
+ } else {
413
+ results.push({ directory: relDir, status: 'error', summary: String(e.message ?? 'error').slice(0, 80) });
414
+ }
415
+ }
416
+ }
417
+
418
+ if (opts.format === 'json') {
419
+ console.log(JSON.stringify(results, null, 2));
420
+ return;
421
+ }
422
+
423
+ // Print table
424
+ const COL = { dir: 40, status: 8, summary: 60 };
425
+ const pad = (s: string, n: number) => s.slice(0, n).padEnd(n);
426
+ const divider = `${'-'.repeat(COL.dir + 2)}+${'-'.repeat(COL.status + 2)}+${'-'.repeat(COL.summary + 2)}`;
427
+ console.log(divider);
428
+ console.log(`| ${pad('Directory', COL.dir)} | ${pad('Status', COL.status)} | ${pad('Summary', COL.summary)} |`);
429
+ console.log(divider);
430
+ for (const r of results) {
431
+ console.log(`| ${pad(r.directory, COL.dir)} | ${pad(r.status, COL.status)} | ${pad(r.summary, COL.summary)} |`);
432
+ }
433
+ console.log(divider);
434
+ }
435
+
436
+ // ==========================================
437
+ // K8s Drift Detection (H3)
438
+ // ==========================================
439
+
440
+ export interface K8sDriftOptions {
441
+ /** Update baseline with current resource counts. */
442
+ updateBaseline?: boolean;
443
+ /** Output format. */
444
+ format?: 'table' | 'json';
445
+ }
446
+
447
+ /** Stored baseline: map of namespace → resource count. */
448
+ interface K8sBaseline {
449
+ capturedAt: string;
450
+ namespaceCounts: Record<string, number>;
451
+ }
452
+
453
+ /**
454
+ * H3: Check ConfigMap/Secret drift versus a stored baseline.
455
+ * Runs `kubectl get configmap,secret -A` to get current counts per namespace,
456
+ * compares against ~/.nimbus/drift-baseline.json, and reports new/missing resources.
457
+ */
458
+ export async function checkK8sDrift(opts: K8sDriftOptions = {}): Promise<void> {
459
+ const { execFile } = await import('child_process');
460
+ const { promisify } = await import('util');
461
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('fs');
462
+ const { join } = await import('path');
463
+ const { homedir } = await import('os');
464
+ const execFileAsync = promisify(execFile);
465
+
466
+ const baselinePath = join(homedir(), '.nimbus', 'drift-baseline.json');
467
+
468
+ // Get current resource counts per namespace
469
+ let rawOutput = '';
470
+ try {
471
+ const { stdout } = await execFileAsync('kubectl', [
472
+ 'get', 'configmap,secret', '-A', '--no-headers', '-o',
473
+ 'custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name',
474
+ ], { timeout: 15000 });
475
+ rawOutput = stdout;
476
+ } catch {
477
+ ui.error('kubectl not available or cluster unreachable.');
478
+ return;
479
+ }
480
+
481
+ const lines = rawOutput.trim().split('\n').filter(Boolean);
482
+ const namespaceCounts: Record<string, number> = {};
483
+ for (const line of lines) {
484
+ const parts = line.trim().split(/\s+/);
485
+ const ns = parts[0] ?? 'default';
486
+ namespaceCounts[ns] = (namespaceCounts[ns] ?? 0) + 1;
487
+ }
488
+
489
+ if (opts.updateBaseline) {
490
+ const baseline: K8sBaseline = { capturedAt: new Date().toISOString(), namespaceCounts };
491
+ mkdirSync(join(homedir(), '.nimbus'), { recursive: true });
492
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), 'utf-8');
493
+ ui.success(`Baseline saved: ${Object.keys(namespaceCounts).length} namespaces, ${lines.length} resources total.`);
494
+ return;
495
+ }
496
+
497
+ // Load baseline
498
+ if (!existsSync(baselinePath)) {
499
+ ui.warning('No K8s drift baseline found. Run with --update-baseline to capture current state.');
500
+ ui.newLine();
501
+ ui.print(`Current state: ${lines.length} ConfigMaps/Secrets across ${Object.keys(namespaceCounts).length} namespaces.`);
502
+ return;
503
+ }
504
+
505
+ let baseline: K8sBaseline;
506
+ try {
507
+ baseline = JSON.parse(readFileSync(baselinePath, 'utf-8')) as K8sBaseline;
508
+ } catch {
509
+ ui.error('Failed to parse drift baseline file. Re-capture with --update-baseline.');
510
+ return;
511
+ }
512
+
513
+ // Compare counts
514
+ const driftEntries: Array<{ namespace: string; baseline: number; current: number; delta: number }> = [];
515
+ const allNamespaces = new Set([...Object.keys(baseline.namespaceCounts), ...Object.keys(namespaceCounts)]);
516
+
517
+ for (const ns of allNamespaces) {
518
+ const baseCount = baseline.namespaceCounts[ns] ?? 0;
519
+ const currCount = namespaceCounts[ns] ?? 0;
520
+ if (baseCount !== currCount) {
521
+ driftEntries.push({ namespace: ns, baseline: baseCount, current: currCount, delta: currCount - baseCount });
522
+ }
523
+ }
524
+
525
+ if (opts.format === 'json') {
526
+ console.log(JSON.stringify({ capturedAt: baseline.capturedAt, drift: driftEntries }, null, 2));
527
+ return;
528
+ }
529
+
530
+ ui.header('K8s ConfigMap/Secret Drift');
531
+ ui.print(` Baseline captured: ${new Date(baseline.capturedAt).toLocaleString()}`);
532
+ ui.newLine();
533
+
534
+ if (driftEntries.length === 0) {
535
+ ui.success('No drift detected — ConfigMap/Secret counts match baseline.');
536
+ return;
537
+ }
538
+
539
+ ui.warning(`${driftEntries.length} namespace(s) have drifted from baseline:`);
540
+ ui.newLine();
541
+ for (const entry of driftEntries) {
542
+ const sign = entry.delta > 0 ? '+' : '';
543
+ const color = entry.delta > 0 ? 'yellow' : 'red';
544
+ ui.print(` ${ui.bold(entry.namespace.padEnd(30))} baseline: ${String(entry.baseline).padStart(4)} current: ${String(entry.current).padStart(4)} delta: ${ui.color(sign + entry.delta, color)}`);
545
+ }
546
+ ui.newLine();
547
+ ui.print(ui.dim('Run "nimbus drift k8s --update-baseline" to update the baseline.'));
548
+ }
549
+
239
550
  /**
240
551
  * Drift parent command
241
552
  */
@@ -246,10 +557,16 @@ export async function driftCommand(args: string[]): Promise<void> {
246
557
  ui.print('Usage: nimbus drift <command> [options]');
247
558
  ui.newLine();
248
559
  ui.print('Commands:');
560
+ ui.print(` ${ui.bold('scan')} Direct terraform drift scan (no service dependency)`);
561
+ ui.print(` ${ui.bold('k8s')} K8s ConfigMap/Secret drift vs baseline`);
249
562
  ui.print(` ${ui.bold('detect')} Detect infrastructure drift`);
250
563
  ui.print(` ${ui.bold('fix')} Fix detected drift`);
251
564
  ui.newLine();
252
565
  ui.print('Examples:');
566
+ ui.print(' nimbus drift scan');
567
+ ui.print(' nimbus drift scan --format json');
568
+ ui.print(' nimbus drift k8s');
569
+ ui.print(' nimbus drift k8s --update-baseline');
253
570
  ui.print(' nimbus drift detect --provider terraform');
254
571
  ui.print(' nimbus drift detect kubernetes -d ./manifests');
255
572
  ui.print(' nimbus drift fix terraform --auto-approve');
@@ -261,11 +578,24 @@ export async function driftCommand(args: string[]): Promise<void> {
261
578
  const subArgs = args.slice(1);
262
579
 
263
580
  switch (subcommand) {
581
+ case 'scan': {
582
+ const format = subArgs.includes('--json') ? 'json' : 'table';
583
+ const workdirIdx = subArgs.indexOf('--workdir');
584
+ const workdir = workdirIdx !== -1 ? subArgs[workdirIdx + 1] : undefined;
585
+ await driftScanCommand({ workdir, format });
586
+ break;
587
+ }
588
+ case 'k8s': {
589
+ const updateBaseline = subArgs.includes('--update-baseline');
590
+ const format = subArgs.includes('--json') ? 'json' : 'table';
591
+ await checkK8sDrift({ updateBaseline, format });
592
+ break;
593
+ }
264
594
  case 'detect':
265
595
  await driftDetectCommand(parseDriftDetectOptions(subArgs));
266
596
  break;
267
597
  case 'fix':
268
- await driftFixCommand(parseDriftFixOptions(subArgs));
598
+ await driftFixCommand(parseDriftFixOptions(subArgs), subArgs);
269
599
  break;
270
600
  default:
271
601
  ui.error(`Unknown drift command: ${subcommand}`);
@@ -302,12 +632,7 @@ export async function driftDetectCommand(options: DriftDetectOptions): Promise<v
302
632
  ui.startSpinner({ message: `Detecting ${provider} drift...` });
303
633
 
304
634
  try {
305
- const client = new CoreEngineClient();
306
- const report = await client.detectDrift({
307
- provider,
308
- directory,
309
- });
310
-
635
+ const report = await detectDriftDirect(provider, directory);
311
636
  ui.stopSpinnerSuccess('Drift detection complete');
312
637
 
313
638
  if (options.json) {
@@ -330,7 +655,7 @@ export async function driftDetectCommand(options: DriftDetectOptions): Promise<v
330
655
  /**
331
656
  * Fix drift command
332
657
  */
333
- export async function driftFixCommand(options: DriftFixOptions): Promise<void> {
658
+ export async function driftFixCommand(options: DriftFixOptions, args: string[] = []): Promise<void> {
334
659
  const directory = options.directory || process.cwd();
335
660
  let provider = options.provider;
336
661
 
@@ -354,11 +679,7 @@ export async function driftFixCommand(options: DriftFixOptions): Promise<void> {
354
679
 
355
680
  let report: DriftReport;
356
681
  try {
357
- const client = new CoreEngineClient();
358
- report = await client.detectDrift({
359
- provider,
360
- directory,
361
- });
682
+ report = await detectDriftDirect(provider, directory);
362
683
  ui.stopSpinnerSuccess('Drift detection complete');
363
684
  } catch (error) {
364
685
  ui.stopSpinnerFail('Drift detection failed');
@@ -412,13 +733,11 @@ export async function driftFixCommand(options: DriftFixOptions): Promise<void> {
412
733
  // Apply fixes
413
734
  ui.startSpinner({ message: 'Applying remediation...' });
414
735
 
736
+ let driftReport: DriftReport | undefined;
415
737
  try {
416
- const client = new CoreEngineClient();
417
- const result = await client.fixDrift({
418
- provider,
419
- directory,
420
- dryRun: false,
421
- });
738
+ const result = await fixDriftDirect(provider, directory);
739
+
740
+ driftReport = report;
422
741
 
423
742
  if (result.success) {
424
743
  ui.stopSpinnerSuccess('Remediation complete');
@@ -428,12 +747,41 @@ export async function driftFixCommand(options: DriftFixOptions): Promise<void> {
428
747
 
429
748
  if (options.json) {
430
749
  console.log(JSON.stringify(result, null, 2));
431
- return;
750
+ } else {
751
+ displayRemediationResult(result);
432
752
  }
433
-
434
- displayRemediationResult(result);
435
753
  } catch (error) {
436
754
  ui.stopSpinnerFail('Remediation failed');
437
755
  ui.error((error as Error).message);
438
756
  }
757
+
758
+ // GAP-23: --notify support for Slack/email
759
+ const notifyFlag = args.find((a: string) => a.startsWith('--notify='))?.split('=')[1]
760
+ ?? (args.includes('--notify') ? args[args.indexOf('--notify') + 1] : undefined);
761
+
762
+ if (notifyFlag === 'slack') {
763
+ const webhookUrl = process.env.NIMBUS_SLACK_WEBHOOK;
764
+ if (!webhookUrl) {
765
+ ui.warning('Set NIMBUS_SLACK_WEBHOOK env var to enable Slack notifications');
766
+ } else {
767
+ try {
768
+ const summary = driftReport ? JSON.stringify(driftReport).slice(0, 2000) : 'Drift check completed';
769
+ await fetch(webhookUrl, {
770
+ method: 'POST',
771
+ headers: { 'Content-Type': 'application/json' },
772
+ body: JSON.stringify({ text: `*Nimbus Drift Report*\n${summary}` }),
773
+ });
774
+ ui.success('Slack notification sent');
775
+ } catch (e) {
776
+ ui.warning(`Slack notification failed: ${e instanceof Error ? e.message : String(e)}`);
777
+ }
778
+ }
779
+ } else if (notifyFlag === 'email') {
780
+ // Generate curl command for email via SMTP relay / webhook
781
+ ui.print('To send drift report via email, run:');
782
+ ui.print(` curl -X POST https://api.mailersend.com/v1/email \\
783
+ -H "Authorization: Bearer $MAILERSEND_API_KEY" \\
784
+ -H "Content-Type: application/json" \\
785
+ -d '{"from":{"email":"nimbus@yourdomain.com"},"to":[{"email":"team@yourdomain.com"}],"subject":"Nimbus Drift Report","text":"${JSON.stringify(driftReport ?? 'completed').slice(0, 500)}"}'`);
786
+ }
439
787
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Export Command (G19)
3
+ *
4
+ * Export session conversations to markdown, HTML, or JSON.
5
+ *
6
+ * Usage:
7
+ * nimbus export # most recent session → stdout (md)
8
+ * nimbus export <session-id> # specific session
9
+ * nimbus export --format html # HTML output
10
+ * nimbus export --format json # JSON output
11
+ * nimbus export --output report.md # save to file
12
+ */
13
+
14
+ import { writeFileSync } from 'node:fs';
15
+
16
+ export interface ExportOptions {
17
+ format?: 'md' | 'html' | 'json';
18
+ output?: string;
19
+ sessionId?: string;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function messageToMarkdown(role: string, content: string): string {
27
+ const label = role === 'user' ? '**User**' : role === 'assistant' ? '**Agent**' : `*[${role}]*`;
28
+ return `${label}:\n${content}\n`;
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Main export
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export async function exportCommand(options: ExportOptions = {}): Promise<void> {
36
+ const format = options.format ?? 'md';
37
+
38
+ // Load session manager + conversation
39
+ let conversation: Array<{ role: string; content: string; timestamp?: Date }> = [];
40
+ let sessionMeta: { id: string; name?: string; model?: string; mode?: string; createdAt?: string } = { id: 'unknown' };
41
+
42
+ try {
43
+ const { SessionManager } = await import('../sessions/manager');
44
+ const sm = SessionManager.getInstance();
45
+ const sessions = sm.list();
46
+
47
+ // Find the target session
48
+ let targetSession = sessions.find(s =>
49
+ options.sessionId && (s.id === options.sessionId || s.id.startsWith(options.sessionId))
50
+ ) ?? sessions[0];
51
+
52
+ if (!targetSession) {
53
+ console.error('No sessions found. Run "nimbus chat" first to create a session.');
54
+ process.exit(1);
55
+ }
56
+
57
+ sessionMeta = {
58
+ id: targetSession.id,
59
+ name: targetSession.name,
60
+ model: targetSession.model,
61
+ mode: targetSession.mode,
62
+ createdAt: targetSession.createdAt,
63
+ };
64
+
65
+ const messages = sm.loadConversation(targetSession.id);
66
+ conversation = messages
67
+ .filter(m => m.role === 'user' || m.role === 'assistant')
68
+ .map(m => ({
69
+ role: m.role,
70
+ content: Array.isArray(m.content)
71
+ ? m.content.map((b: unknown) => {
72
+ if (typeof b === 'string') return b;
73
+ if (b && typeof b === 'object' && 'text' in b) return (b as { text: string }).text;
74
+ return '';
75
+ }).join('')
76
+ : String(m.content ?? ''),
77
+ }));
78
+ } catch (err) {
79
+ console.error(`Failed to load session: ${err instanceof Error ? err.message : String(err)}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ // Generate output
84
+ let output = '';
85
+
86
+ if (format === 'json') {
87
+ output = JSON.stringify({ session: sessionMeta, messages: conversation }, null, 2);
88
+ } else if (format === 'html') {
89
+ output = buildHtml(sessionMeta, conversation);
90
+ } else {
91
+ // Default: markdown
92
+ output = buildMarkdown(sessionMeta, conversation);
93
+ }
94
+
95
+ // Output to file or stdout
96
+ if (options.output) {
97
+ writeFileSync(options.output, output, 'utf-8');
98
+ console.log(`Exported to: ${options.output}`);
99
+ } else {
100
+ process.stdout.write(output);
101
+ }
102
+ }
103
+
104
+ function buildMarkdown(
105
+ meta: { id: string; name?: string; model?: string; mode?: string; createdAt?: string },
106
+ conversation: Array<{ role: string; content: string }>
107
+ ): string {
108
+ const lines: string[] = [
109
+ `# Nimbus Session Export`,
110
+ ``,
111
+ `| Field | Value |`,
112
+ `|-------|-------|`,
113
+ `| Session ID | ${meta.id} |`,
114
+ `| Name | ${meta.name ?? 'N/A'} |`,
115
+ `| Model | ${meta.model ?? 'N/A'} |`,
116
+ `| Mode | ${meta.mode ?? 'N/A'} |`,
117
+ `| Created | ${meta.createdAt ?? new Date().toISOString()} |`,
118
+ ``,
119
+ `---`,
120
+ ``,
121
+ `## Conversation`,
122
+ ``,
123
+ ];
124
+
125
+ for (const msg of conversation) {
126
+ lines.push(messageToMarkdown(msg.role, msg.content));
127
+ lines.push('---');
128
+ lines.push('');
129
+ }
130
+
131
+ return lines.join('\n');
132
+ }
133
+
134
+ function buildHtml(
135
+ meta: { id: string; name?: string; model?: string; mode?: string; createdAt?: string },
136
+ conversation: Array<{ role: string; content: string }>
137
+ ): string {
138
+ const escape = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
139
+ const msgs = conversation.map(m => {
140
+ const isUser = m.role === 'user';
141
+ const bg = isUser ? '#3b82f6' : '#1e293b';
142
+ const align = isUser ? 'flex-end' : 'flex-start';
143
+ return `<div style="display:flex;justify-content:${align};margin:8px 0"><div style="max-width:75%;padding:12px 16px;border-radius:16px;background:${bg};color:#e2e8f0;font-size:14px;line-height:1.6;white-space:pre-wrap">${escape(m.content)}</div></div>`;
144
+ }).join('\n');
145
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Nimbus Session Export — ${escape(meta.id)}</title></head><body style="background:#0f172a;color:#e2e8f0;font-family:monospace;padding:24px"><h2>Nimbus Session: ${escape(meta.name ?? meta.id)}</h2><p>Model: ${escape(meta.model ?? 'N/A')} | Mode: ${escape(meta.mode ?? 'N/A')} | Created: ${escape(meta.createdAt ?? '')}</p><div>${msgs}</div></body></html>`;
146
+ }