cartethyia 2.4.4 → 2.4.6

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 (304) hide show
  1. package/.env.example +4 -1
  2. package/README.md +18 -0
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
  30. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  31. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
  32. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  37. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
  38. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  39. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
  40. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  42. package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
  43. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  44. package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
  45. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  46. package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
  47. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  48. package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
  49. package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
  50. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  52. package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
  53. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  54. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  55. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
  56. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  57. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
  58. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  59. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  60. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
  61. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  62. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  64. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  66. package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
  67. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  68. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
  69. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  70. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  71. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  72. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
  73. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  74. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  75. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  76. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  77. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
  78. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
  80. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  81. package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
  82. package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
  83. package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
  84. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  85. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  87. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  88. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  89. package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
  90. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  91. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  92. package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
  93. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  94. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
  95. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  96. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
  97. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  98. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  99. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  100. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  101. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
  102. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  103. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
  104. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  105. package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
  106. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  107. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  108. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  109. package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
  110. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  111. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  113. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  114. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  115. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  116. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  118. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  120. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  121. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  130. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  137. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  138. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  139. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  140. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  141. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  142. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  143. package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
  144. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  145. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  146. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  147. package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
  148. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  149. package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
  150. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  151. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  152. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  153. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  154. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
  155. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  156. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
  157. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  158. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  159. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
  160. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  161. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  162. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  163. package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
  164. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  165. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  166. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  167. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  168. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  169. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  170. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  171. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
  172. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  173. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  174. package/dist/dashboard/index.html +1 -1
  175. package/dist/dashboard/index.html.gz +0 -0
  176. package/dist/dashboard/precompressed-assets.json +123 -0
  177. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  178. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  179. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  180. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  181. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  182. package/package.json +25 -30
  183. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  184. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  185. package/scripts/cli/command/admin.ts +65 -0
  186. package/scripts/cli/command/console-menu.ts +227 -0
  187. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  188. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  189. package/scripts/cli/command/start.ts +13 -0
  190. package/scripts/cli/command/tray.ts +165 -0
  191. package/scripts/cli/command/update.ts +23 -0
  192. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  193. package/scripts/cli/gateway/spawn.ts +101 -0
  194. package/scripts/cli/gateway/stop.ts +51 -0
  195. package/scripts/cli/main.ts +94 -0
  196. package/scripts/cli/support/bun.ts +27 -0
  197. package/scripts/cli/support/env.ts +99 -0
  198. package/scripts/cli/support/platform.ts +64 -0
  199. package/scripts/cli/support/services.ts +88 -0
  200. package/scripts/cli/support/startup-signal.ts +23 -0
  201. package/scripts/cli/support/version.ts +37 -0
  202. package/scripts/cli/tray/hidden-window.vbs +28 -0
  203. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  204. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  205. package/scripts/cli/ui/background.ts +49 -0
  206. package/src/boot/app.ts +3 -1
  207. package/src/boot/dependencies.ts +36 -13
  208. package/src/config/providers.ts +27 -1
  209. package/src/config/spec.ts +11 -0
  210. package/src/console/backup/routes.ts +8 -0
  211. package/src/console/cli-tools/contracts.ts +6 -6
  212. package/src/console/console-router.ts +5 -3
  213. package/src/console/dashboard-assets.ts +81 -11
  214. package/src/console/observability/contracts.ts +54 -22
  215. package/src/console/observability/routes.ts +31 -6
  216. package/src/console/observability/store.ts +68 -28
  217. package/src/console/providers/catalog/public-model-store.ts +91 -60
  218. package/src/console/providers/catalog/store.ts +5 -1
  219. package/src/console/quota/account-quota-tenant.ts +9 -6
  220. package/src/console/response-compression.ts +69 -2
  221. package/src/console/routing/pools/probe-verdict.ts +72 -0
  222. package/src/console/share/share-router.ts +7 -4
  223. package/src/console/share/share-stats.ts +12 -5
  224. package/src/console/shared/read-through-cache.ts +133 -7
  225. package/src/console/updates/repository-updates.ts +39 -34
  226. package/src/network/outbound-fetch.ts +113 -25
  227. package/src/network/tls/bun-client-hello.ts +112 -0
  228. package/src/observability/event-log.ts +29 -8
  229. package/src/observability/metrics.ts +40 -13
  230. package/src/observability/runtime-metrics.ts +7 -0
  231. package/src/persistence/schema.ts +40 -2
  232. package/src/protocol/response/codex.ts +9 -75
  233. package/src/protocol/tool-identity.ts +43 -9
  234. package/src/providers/authentication/jwt-validator.ts +130 -187
  235. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  236. package/src/providers/default-registry.ts +4 -0
  237. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  238. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  239. package/src/providers/discovery/probe-wire.ts +29 -5
  240. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
  241. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  242. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
  243. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  244. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  245. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  246. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  247. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  248. package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
  249. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  250. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  251. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  252. package/src/providers/integrations/codex/codex.ts +5 -8
  253. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  254. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  255. package/src/providers/integrations/opencode/provider.ts +42 -3
  256. package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
  257. package/src/providers/integrations/shared/install-id.ts +29 -2
  258. package/src/providers/operations/client-versions.ts +11 -1
  259. package/src/providers/operations/model-catalog-cache.ts +18 -3
  260. package/src/providers/operations/provider-catalog-service.ts +76 -0
  261. package/src/transport/dispatch/attempt-finalize.ts +7 -1
  262. package/src/transport/dispatch/attempt-loop.ts +9 -7
  263. package/src/transport/dispatch/empty-stream.ts +17 -26
  264. package/src/transport/failure-policy.ts +20 -0
  265. package/src/transport/routing/route-catalog.ts +42 -3
  266. package/src/transport/surface/chat/encode.ts +18 -31
  267. package/src/transport/surface/messages/stream.ts +3 -2
  268. package/src/transport/translation/tool-repair.ts +37 -16
  269. package/src/workers/account-identity-sweep.ts +14 -12
  270. package/src/workers/daily-checkin.ts +62 -0
  271. package/src/workers/quota-refresh-sweep.ts +27 -2
  272. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  273. package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
  275. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  276. package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
  277. package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
  278. package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
  279. package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
  280. package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
  281. package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
  282. package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
  283. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  284. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  285. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  286. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  287. package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
  288. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  289. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  290. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  291. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  292. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  293. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  294. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  295. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  296. package/scripts/cli/cli.ts +0 -766
  297. package/scripts/cli/version.ts +0 -66
  298. package/scripts/operations/probe-egress.ts +0 -335
  299. package/scripts/operations/restart.ts +0 -120
  300. package/scripts/shared/env.ts +0 -269
  301. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  302. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  303. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  304. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -0,0 +1,165 @@
1
+ import { isNewerRelease } from "../../../src/console/updates/version-compare";
2
+ import { resolvePackagePath } from "../../../src/config/package-paths";
3
+ import { resolvePort } from "../../../src/config/runtime";
4
+ import { ensureDrainToken } from "../gateway/drain-token";
5
+ import { startGateway } from "../gateway/spawn";
6
+ import { stopGateway } from "../gateway/stop";
7
+ import { readInstalledVersion, readLatestPublishedVersion } from "../support/version";
8
+ import { reportTrayStartup } from "../support/startup-signal";
9
+ import { createTray } from "../tray";
10
+ import { openUrl } from "../ui/background";
11
+ import { exportBackup, resetConsoleAccount } from "./admin";
12
+ import { printUpdateInstructions } from "./update";
13
+
14
+ /**
15
+ * Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
16
+ * on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
17
+ * an empty console tab open for the tray's whole lifetime, and that window is
18
+ * owned by Windows Terminal — not by this process, so it cannot be found and
19
+ * hidden from inside.
20
+ */
21
+ export async function startTray(): Promise<void> {
22
+ const port = resolvePort();
23
+ const drainToken = ensureDrainToken();
24
+ const dashboardUrl = `http://127.0.0.1:${port}/console`;
25
+ const repositoryUrl = "https://github.com/risunCode/cartethyia";
26
+ const currentVersion = await readInstalledVersion();
27
+
28
+ let gateway: Bun.Subprocess | null = null;
29
+ let closing = false;
30
+ let busy = false;
31
+ let resetArmed = false;
32
+ let latestVersion: string | null = null;
33
+ let closePromise: Promise<void> | undefined;
34
+
35
+ const tray = createTray({
36
+ iconPath: resolvePackagePath(
37
+ "scripts",
38
+ "cli",
39
+ "tray",
40
+ "assets",
41
+ process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
42
+ ),
43
+ tooltip: `Cartethyia ${currentVersion}`,
44
+ items: [
45
+ { kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: true },
46
+ { kind: "separator" },
47
+ { kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
48
+ { kind: "action", id: "reset", title: "Reset console password" },
49
+ { kind: "action", id: "backup", title: "Backup" },
50
+ { kind: "separator" },
51
+ { kind: "action", id: "exit", title: "Exit" },
52
+ ],
53
+ onSelect: (id: string) => void handleSelect(id),
54
+ onExit: () => void close(1),
55
+ });
56
+
57
+ const close = (exitCode = 0): Promise<void> => {
58
+ if (closePromise) return closePromise;
59
+ closing = true;
60
+ closePromise = (async () => {
61
+ const current: Bun.Subprocess | null = gateway;
62
+ gateway = null;
63
+ if (current) await stopGateway(current, port, drainToken);
64
+ await tray.close();
65
+ process.exitCode = exitCode;
66
+ })();
67
+ return closePromise;
68
+ };
69
+
70
+ const refreshVersion = async (): Promise<void> => {
71
+ const fetched = await readLatestPublishedVersion();
72
+ if (closing) return;
73
+ latestVersion = fetched;
74
+ if (fetched === null) {
75
+ tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
76
+ return;
77
+ }
78
+ const newer = isNewerRelease(currentVersion, fetched);
79
+ tray.update(
80
+ "update",
81
+ newer
82
+ ? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
83
+ : `Cartethyia ${currentVersion} \u00b7 up to date`,
84
+ // Always clickable: the row opens the repository, and only offers the
85
+ // update command when there is something newer.
86
+ true,
87
+ );
88
+ };
89
+
90
+ const runUpdate = async (): Promise<void> => {
91
+ if (busy) return;
92
+ busy = true;
93
+ const latest = await readLatestPublishedVersion();
94
+ await close();
95
+ printUpdateInstructions(currentVersion, latest);
96
+ process.exit(0);
97
+ };
98
+
99
+ async function handleSelect(id: string): Promise<void> {
100
+ if (closing) return;
101
+ if (id === "exit") return void (await close());
102
+ if (id === "console") return void (await openUrl(dashboardUrl));
103
+ if (id === "backup") {
104
+ const result = await exportBackup(port, drainToken);
105
+ if (result !== null) {
106
+ console.log(
107
+ `Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
108
+ );
109
+ }
110
+ return;
111
+ }
112
+ if (id === "reset") {
113
+ // A tray click cannot ask for the typed confirmation the menu uses, so
114
+ // the first click arms the action and the second one runs it.
115
+ if (!resetArmed) {
116
+ resetArmed = true;
117
+ tray.update("reset", "Reset console password \u00b7 click again to delete", true);
118
+ return;
119
+ }
120
+ resetArmed = false;
121
+ tray.update("reset", "Reset console password", true);
122
+ await resetConsoleAccount(port, drainToken);
123
+ return;
124
+ }
125
+ if (id === "update") {
126
+ // Nothing to install: the row is the way to the repository instead.
127
+ if (latestVersion === null || !isNewerRelease(currentVersion, latestVersion)) {
128
+ await openUrl(repositoryUrl);
129
+ return;
130
+ }
131
+ await runUpdate();
132
+ }
133
+ }
134
+
135
+ process.once("SIGINT", () => void close());
136
+ process.once("SIGTERM", () => void close());
137
+
138
+ try {
139
+ await tray.ready();
140
+ gateway = await startGateway(port, drainToken);
141
+ // The launcher that spawned this tray is blocked on a signal file: it
142
+ // cannot read our stdio, because the tray is detached. Without this write
143
+ // it waits out its 60 s deadline and reports a failure for a tray that is
144
+ // running fine.
145
+ reportTrayStartup({ status: "ready", url: dashboardUrl });
146
+ console.log(`Cartethyia is running at ${dashboardUrl}`);
147
+ void refreshVersion();
148
+
149
+ while (!closing) {
150
+ const current: Bun.Subprocess | null = gateway;
151
+ if (!current) {
152
+ await Bun.sleep(100);
153
+ continue;
154
+ }
155
+ const code = await current.exited;
156
+ if (gateway !== current || closing) continue;
157
+ gateway = null;
158
+ console.error(`Cartethyia exited with code ${code}.`);
159
+ await close(code ?? 1);
160
+ }
161
+ } catch (error) {
162
+ await close(1);
163
+ throw error;
164
+ }
165
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The update path, which prints a command rather than running one.
3
+ *
4
+ * npm cannot replace this package's directory while a process is executing out
5
+ * of it: on Windows the rename fails with EBUSY and npm exits 14. Stopping the
6
+ * gateway is not enough either, because the CLI or tray itself is the process
7
+ * holding the directory. So every caller stops, prints the command, and exits —
8
+ * the same shape 9router uses.
9
+ */
10
+ export function updateCommand(): string {
11
+ return "npm install --global cartethyia@latest --prefer-online";
12
+ }
13
+
14
+ /** Prints the "run this now" block after Cartethyia has stopped. */
15
+ export function printUpdateInstructions(from: string, to: string | null): void {
16
+ console.log("");
17
+ console.log(`Update available: ${from} \u2192 ${to ?? "latest"}`);
18
+ console.log("");
19
+ console.log("Run this now that Cartethyia has stopped:");
20
+ console.log("");
21
+ console.log(` ${updateCommand()}`);
22
+ console.log("");
23
+ }
@@ -1,78 +1,78 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- /**
5
- * The install's drain token, persisted in the config directory.
6
- *
7
- * Why it persists instead of being generated per run: `cartethyia stop` runs in
8
- * a different process, possibly minutes later, and the token is the only way it
9
- * can authenticate to a gateway it did not start. A token regenerated on each
10
- * start could never match the gateway that is actually running, which is why
11
- * `stop` reported "no gateway found" or a 401 on a live gateway.
12
- *
13
- * It is kept out of `.env` on purpose: it authorises a shutdown, so it belongs
14
- * in a machine-written file the operator never hand-edits and never commits.
15
- * Mode 0600 where the platform honours it.
16
- */
17
- const RUNTIME_STATE_FILE = "runtime.json";
18
-
19
- interface RuntimeState {
20
- readonly drainToken: string;
21
- }
22
-
23
- function statePath(): string | null {
24
- const configDir = process.env.CARTETHYIA_CONFIG_DIR;
25
- return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
26
- }
27
-
28
- function readState(): RuntimeState | null {
29
- const path = statePath();
30
- if (path === null || !existsSync(path)) return null;
31
- try {
32
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
33
- if (
34
- parsed !== null &&
35
- typeof parsed === "object" &&
36
- "drainToken" in parsed &&
37
- typeof parsed.drainToken === "string" &&
38
- parsed.drainToken.length > 0
39
- ) {
40
- return { drainToken: parsed.drainToken };
41
- }
42
- } catch {
43
- // A corrupt or stale state file reads as "no token".
44
- }
45
- return null;
46
- }
47
-
48
- /**
49
- * Returns the install's drain token, creating and persisting one on first use.
50
- *
51
- * Every start calls this, so a gateway started from the menu, from `tray`, or
52
- * from `cartethyia start` all authenticate with the same token and `stop`
53
- * always has the one that works.
54
- */
55
- export function ensureDrainToken(): string {
56
- const existing = readState();
57
- if (existing !== null) return existing.drainToken;
58
-
59
- const token = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "");
60
- const path = statePath();
61
- if (path !== null) {
62
- try {
63
- writeFileSync(path, JSON.stringify({ drainToken: token }), {
64
- encoding: "utf8",
65
- mode: 0o600,
66
- });
67
- } catch {
68
- // A read-only config directory still yields a usable in-process token;
69
- // only `stop` from another process is lost.
70
- }
71
- }
72
- return token;
73
- }
74
-
75
- /** The persisted token, or null when this install has never started. */
76
- export function readPersistedDrainToken(): string | null {
77
- return readState()?.drainToken ?? null;
78
- }
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * The install's drain token, persisted in the config directory.
6
+ *
7
+ * Why it persists instead of being generated per run: the tray that "Hide to
8
+ * tray" starts runs in a different process and drains the gateway through the
9
+ * loopback endpoint, so the token has to match across processes. A token
10
+ * regenerated on each start could never authenticate against a gateway another
11
+ * process had started.
12
+ *
13
+ * It is kept out of `.env` on purpose: it authorises a shutdown, so it belongs
14
+ * in a machine-written file the operator never hand-edits and never commits.
15
+ * Mode 0600 where the platform honours it.
16
+ */
17
+ const RUNTIME_STATE_FILE = "runtime.json";
18
+
19
+ interface RuntimeState {
20
+ readonly drainToken: string;
21
+ }
22
+
23
+ function statePath(): string | null {
24
+ const configDir = process.env.CARTETHYIA_CONFIG_DIR;
25
+ return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
26
+ }
27
+
28
+ function readState(): RuntimeState | null {
29
+ const path = statePath();
30
+ if (path === null || !existsSync(path)) return null;
31
+ try {
32
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
33
+ if (
34
+ parsed !== null &&
35
+ typeof parsed === "object" &&
36
+ "drainToken" in parsed &&
37
+ typeof parsed.drainToken === "string" &&
38
+ parsed.drainToken.length > 0
39
+ ) {
40
+ return { drainToken: parsed.drainToken };
41
+ }
42
+ } catch {
43
+ // A corrupt or stale state file reads as "no token".
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Returns the install's drain token, creating and persisting one on first use.
50
+ *
51
+ * Every start calls this, so a gateway started from the menu, from `tray`, or
52
+ * from `cartethyia start` all authenticate with the same token and `stop`
53
+ * always has the one that works.
54
+ */
55
+ export function ensureDrainToken(): string {
56
+ const existing = readState();
57
+ if (existing !== null) return existing.drainToken;
58
+
59
+ const token = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "");
60
+ const path = statePath();
61
+ if (path !== null) {
62
+ try {
63
+ writeFileSync(path, JSON.stringify({ drainToken: token }), {
64
+ encoding: "utf8",
65
+ mode: 0o600,
66
+ });
67
+ } catch {
68
+ // A read-only config directory still yields a usable in-process token;
69
+ // only `stop` from another process is lost.
70
+ }
71
+ }
72
+ return token;
73
+ }
74
+
75
+ /** The persisted token, or null when this install has never started. */
76
+ export function readPersistedDrainToken(): string | null {
77
+ return readState()?.drainToken ?? null;
78
+ }
@@ -0,0 +1,101 @@
1
+ import { openSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { PACKAGE_ROOT, resolvePackagePath } from "../../../src/config/package-paths";
4
+
5
+ /**
6
+ * Starts the gateway and resolves once it answers its own health probe.
7
+ *
8
+ * The child is attached by default. A detached child cannot be killed from an
9
+ * `exit` handler on Windows (measured: `kill()` runs, the gateway keeps
10
+ * serving), and a terminal's Ctrl-C only reaches children in the same process
11
+ * group. Staying attached means Ctrl-C and the `exit` backstop both stop the
12
+ * gateway. "Hide to tray" passes `detached: true` because that gateway must
13
+ * outlive this process.
14
+ */
15
+ export async function startGateway(
16
+ port: number,
17
+ drainToken: string,
18
+ options: { readonly detached?: boolean } = {},
19
+ ): Promise<Bun.Subprocess> {
20
+ // Starting a second gateway on the same port used to fail with "stopped
21
+ // before becoming ready (exit code 1)", which says nothing about the real
22
+ // cause. Probe the port first and say what is actually there.
23
+ const existing = await fetch(`http://127.0.0.1:${port}/health`, {
24
+ signal: AbortSignal.timeout(1_000),
25
+ }).catch(() => null);
26
+ if (existing !== null) {
27
+ throw new Error(
28
+ `Cartethyia is already running on port ${port}. Open http://127.0.0.1:${port}/console, or stop the terminal that started it.`,
29
+ );
30
+ }
31
+ // The gateway logs a startup phase line per stage. In menu mode those lines
32
+ // would scroll through the same terminal the menu is drawing on, so they go
33
+ // to a file instead and the operator sees only the menu.
34
+ const configDir = process.env.CARTETHYIA_CONFIG_DIR;
35
+ let logFd: number | null = null;
36
+ if (configDir) {
37
+ try {
38
+ logFd = openSync(join(configDir, "gateway.log"), "a");
39
+ } catch {
40
+ // A read-only config directory costs only the log, not startup.
41
+ }
42
+ }
43
+ const child = Bun.spawn(
44
+ [process.execPath, "run", resolvePackagePath("src", "main.ts")],
45
+ {
46
+ cwd: PACKAGE_ROOT,
47
+ env: { ...process.env, CARTETHYIA_DRAIN_TOKEN: drainToken },
48
+ // "ignore", not "inherit": the gateway does not read stdin, and in menu
49
+ // mode the menu is the only stdin consumer. With "inherit" the gateway
50
+ // held the fd and swallowed every keypress after the first, so the menu
51
+ // rendered but never received Enter.
52
+ stdin: "ignore",
53
+ stdout: logFd ?? "ignore",
54
+ // stderr goes to the same file: "inherit" ties the gateway to the
55
+ // terminal, so closing that terminal took the gateway down with it and
56
+ // "Hide to tray" left nothing running.
57
+ stderr: logFd ?? "ignore",
58
+ detached: options.detached === true,
59
+ },
60
+ );
61
+ const startupController = new AbortController();
62
+ const ready = (async (): Promise<void> => {
63
+ const deadline = Date.now() + 30_000;
64
+ while (!startupController.signal.aborted && Date.now() < deadline) {
65
+ try {
66
+ const response = await fetch(`http://127.0.0.1:${port}/health`, {
67
+ signal: AbortSignal.timeout(500),
68
+ });
69
+ if (response.ok) {
70
+ const body: unknown = await response.json();
71
+ if (body !== null && typeof body === "object" && "status" in body && body.status === "ok") return;
72
+ }
73
+ } catch {
74
+ // The gateway is not accepting health requests yet.
75
+ }
76
+ await Bun.sleep(250);
77
+ }
78
+ if (startupController.signal.aborted) throw new Error("Cartethyia exited before becoming ready");
79
+ throw new Error("Cartethyia did not open its configured port within 30 seconds");
80
+ })();
81
+ const exitedEarly = child.exited.then((code) => {
82
+ startupController.abort();
83
+ return { status: "exit" as const, code };
84
+ });
85
+ const readyResult = ready.then(
86
+ () => ({ status: "ready" as const }),
87
+ (error: unknown) => ({ status: "failure" as const, error }),
88
+ );
89
+ try {
90
+ const result = await Promise.race([readyResult, exitedEarly]);
91
+ if (result.status === "exit") {
92
+ throw new Error(`Cartethyia stopped before becoming ready (exit code ${result.code})`);
93
+ }
94
+ if (result.status === "failure") throw result.error;
95
+ return child;
96
+ } catch (error) {
97
+ child.kill();
98
+ await child.exited;
99
+ throw error;
100
+ }
101
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Stops a gateway this process started, gracefully where possible.
3
+ *
4
+ * The drain endpoint is the only graceful path: it lets in-flight responses
5
+ * finish. The OS-level kill is the fallback, and it is reached whenever the
6
+ * endpoint is unreachable — which is also what happens when the gateway is
7
+ * already on its way out, so a failure here is not necessarily an error the
8
+ * operator needs to act on.
9
+ */
10
+ export async function stopGateway(
11
+ current: Bun.Subprocess,
12
+ port: number,
13
+ drainToken: string,
14
+ ): Promise<void> {
15
+ let drainAccepted = false;
16
+ try {
17
+ const response = await fetch(`http://127.0.0.1:${port}/admin/drain`, {
18
+ method: "POST",
19
+ headers: { "x-drain-token": drainToken },
20
+ signal: AbortSignal.timeout(2_000),
21
+ });
22
+ drainAccepted = response.status === 202;
23
+ } catch {
24
+ // The process may already be exiting; fall back to its OS-level termination.
25
+ }
26
+ if (!drainAccepted) {
27
+ console.error("Could not request a graceful gateway drain; terminating the gateway process.");
28
+ current.kill();
29
+ await current.exited;
30
+ return;
31
+ }
32
+
33
+ // The drain is bounded: a gateway that accepted the request but never exits
34
+ // would otherwise hang the CLI. 30 s is the drain budget `src/main.ts` uses.
35
+ // The timer is cleared unconditionally afterwards — leaving it pending keeps
36
+ // the event loop alive for its full 30 s after the gateway is already gone,
37
+ // which made every `carte` exit take half a minute.
38
+ let timer: Timer | undefined;
39
+ const drained = await Promise.race([
40
+ current.exited,
41
+ new Promise<null>((resolveExit) => {
42
+ timer = setTimeout(() => resolveExit(null), 30_000);
43
+ }),
44
+ ]);
45
+ clearTimeout(timer);
46
+ if (drained === null) {
47
+ console.error("Graceful drain timed out; terminating the gateway process.");
48
+ current.kill();
49
+ await current.exited;
50
+ }
51
+ }
@@ -0,0 +1,94 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { resolveUserConfigDir } from "../../src/config/package-paths";
3
+ import { ENV_PATH, readEnvFile } from "./support/env";
4
+ import { MINIMUM_BUN_VERSION, assertBunVersion } from "./support/bun";
5
+ import { printUpdateInstructions } from "./command/update";
6
+ import { reportTrayStartup } from "./support/startup-signal";
7
+
8
+ process.env.CARTETHYIA_CONFIG_DIR ??= resolveUserConfigDir();
9
+
10
+ const HELP = `Cartethyia ${MINIMUM_BUN_VERSION}+ (Bun is required)
11
+
12
+ Usage:
13
+ carte start, then show an arrow-key menu (no GUI needed)
14
+ carte --tray start with a system-tray icon instead
15
+ cartethyia setup [--non-interactive]
16
+ cartethyia start
17
+ cartethyia menu
18
+ cartethyia doctor
19
+ cartethyia update
20
+
21
+ carte starts the gateway in the background and leaves only the console menu on
22
+ your terminal — Open Console, Reset password, Backup, Update, Hide to tray,
23
+ Exit. Arrow keys and Enter; no desktop environment needed, so it works over SSH
24
+ and headless. Exiting the menu drains the gateway.
25
+ carte --tray uses a system-tray icon instead, which needs a graphical session.
26
+ Stop a gateway whose terminal is gone with Ctrl-C in that terminal.
27
+ Setup explains Lite (embedded SQLite) and Full (external PostgreSQL), then saves
28
+ configuration under the current user's Cartethyia config directory.
29
+ `;
30
+
31
+ /** Loads the user's `.env` into the environment without overriding it. */
32
+ async function loadUserConfig(): Promise<void> {
33
+ const configDir = process.env.CARTETHYIA_CONFIG_DIR;
34
+ if (!configDir) throw new Error("Cartethyia config directory could not be resolved");
35
+ mkdirSync(configDir, { recursive: true });
36
+ const values = await readEnvFile(ENV_PATH);
37
+ for (const [key, value] of Object.entries(values)) {
38
+ if (!process.env[key]) process.env[key] = value;
39
+ }
40
+ }
41
+
42
+ async function main(): Promise<void> {
43
+ assertBunVersion();
44
+ const [command] = process.argv.slice(2);
45
+ if (command === "setup" || command === "start" || command === "tray" || command === "menu" || command === "doctor") {
46
+ await loadUserConfig();
47
+ }
48
+
49
+ switch (command) {
50
+ case "setup": {
51
+ const { setup } = await import("./command/setup");
52
+ await setup();
53
+ return;
54
+ }
55
+ case "start": {
56
+ const { startForeground } = await import("./command/start");
57
+ await startForeground();
58
+ return;
59
+ }
60
+ case "menu": {
61
+ const { startMenu } = await import("./command/console-menu");
62
+ await startMenu();
63
+ return;
64
+ }
65
+ case "tray": {
66
+ const { startTray } = await import("./command/tray");
67
+ await startTray();
68
+ return;
69
+ }
70
+ case "doctor": {
71
+ const { doctor } = await import("./command/doctor");
72
+ await doctor();
73
+ return;
74
+ }
75
+ case "update":
76
+ printUpdateInstructions("this install", null);
77
+ return;
78
+ case "--help":
79
+ case "-h":
80
+ case undefined:
81
+ console.log(HELP);
82
+ return;
83
+ default:
84
+ console.error(`Unknown command: ${command}\n\n${HELP}`);
85
+ process.exitCode = 2;
86
+ }
87
+ }
88
+
89
+ main().catch((error: unknown) => {
90
+ const message = error instanceof Error ? error.message : "unknown error";
91
+ if (process.argv[2] === "tray") reportTrayStartup({ status: "failed", message });
92
+ console.error(`Cartethyia failed: ${message}`);
93
+ process.exitCode = 1;
94
+ });
@@ -0,0 +1,27 @@
1
+ /** Minimum Bun the repository declares in `package.json` `engines`. */
2
+ export const MINIMUM_BUN_VERSION = "1.4.0";
3
+
4
+ /**
5
+ * True when `actual` is at least `minimum`, compared as dotted integers.
6
+ * A non-numeric segment reads as 0, so a malformed version fails closed
7
+ * against a real minimum instead of passing.
8
+ */
9
+ export function bunVersionAtLeast(actual: string, minimum: string): boolean {
10
+ const left = actual.split(".").map((part) => Number.parseInt(part, 10) || 0);
11
+ const right = minimum.split(".").map((part) => Number.parseInt(part, 10) || 0);
12
+ const length = Math.max(left.length, right.length);
13
+ for (let i = 0; i < length; i += 1) {
14
+ const a = left[i] ?? 0;
15
+ const b = right[i] ?? 0;
16
+ if (a !== b) return a > b;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ /** Throws unless the running Bun satisfies the declared minimum. */
22
+ export function assertBunVersion(): void {
23
+ if (bunVersionAtLeast(Bun.version, MINIMUM_BUN_VERSION)) return;
24
+ throw new Error(
25
+ `Bun ${MINIMUM_BUN_VERSION} or newer is required (found ${Bun.version}). Install it from https://bun.sh and run this command again.`,
26
+ );
27
+ }