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.
- package/.env.example +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +123 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/package.json +25 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +227 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +13 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/spawn.ts +101 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/support/version.ts +37 -0
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
- package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
- package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
- package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
- package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
- package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -766
- package/scripts/cli/version.ts +0 -66
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
package/scripts/cli/version.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
4
|
-
|
|
5
|
-
/** Reads the version from the installed package manifest. */
|
|
6
|
-
export async function readInstalledVersion(): Promise<string> {
|
|
7
|
-
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
8
|
-
if (
|
|
9
|
-
metadata === null ||
|
|
10
|
-
typeof metadata !== "object" ||
|
|
11
|
-
!("version" in metadata) ||
|
|
12
|
-
typeof metadata.version !== "string"
|
|
13
|
-
) {
|
|
14
|
-
throw new Error("Cartethyia package version is unavailable");
|
|
15
|
-
}
|
|
16
|
-
return metadata.version;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Asks the npm registry for the newest published version, or null on failure. */
|
|
20
|
-
export async function readLatestPublishedVersion(): Promise<string | null> {
|
|
21
|
-
try {
|
|
22
|
-
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
23
|
-
signal: AbortSignal.timeout(4_000),
|
|
24
|
-
});
|
|
25
|
-
if (!response.ok) return null;
|
|
26
|
-
const metadata: unknown = await response.json();
|
|
27
|
-
if (
|
|
28
|
-
metadata !== null &&
|
|
29
|
-
typeof metadata === "object" &&
|
|
30
|
-
"version" in metadata &&
|
|
31
|
-
typeof metadata.version === "string"
|
|
32
|
-
) {
|
|
33
|
-
return metadata.version;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Runs `npm install --global cartethyia@latest`. On Windows npm is a shell
|
|
43
|
-
* script, so it has to go through cmd.exe rather than being spawned directly.
|
|
44
|
-
*/
|
|
45
|
-
export function npmUpdate(): Promise<number> {
|
|
46
|
-
return new Promise((resolveExit) => {
|
|
47
|
-
const command = process.platform === "win32" ? "cmd.exe" : "npm";
|
|
48
|
-
const args =
|
|
49
|
-
process.platform === "win32"
|
|
50
|
-
? ["/d", "/s", "/c", "npm install --global cartethyia@latest"]
|
|
51
|
-
: ["install", "--global", "cartethyia@latest"];
|
|
52
|
-
let child;
|
|
53
|
-
try {
|
|
54
|
-
child = spawn(command, args, { cwd: tmpdir(), stdio: "inherit" });
|
|
55
|
-
} catch {
|
|
56
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
57
|
-
resolveExit(1);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
child.once("error", () => {
|
|
61
|
-
console.error("Could not start npm. Install Node.js with npm, then retry the update.");
|
|
62
|
-
resolveExit(1);
|
|
63
|
-
});
|
|
64
|
-
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Asks a provider whether it will serve traffic from a given egress.
|
|
3
|
-
*
|
|
4
|
-
* The question this answers is not "is the proxy up". A proxy can carry
|
|
5
|
-
* traffic, pass a neutral canary, and still be refused by one provider: OpenCode
|
|
6
|
-
* answers `GET /inference/v1/models` with 200 from a flagged address and only
|
|
7
|
-
* refuses on the dispatch route. So this sends the provider's own dispatch —
|
|
8
|
-
* the adapter's headers, the agent tool set, `stream: true` — through the same
|
|
9
|
-
* `createValidatedFetch` + pool agent the gateway uses. A hand-built stand-in
|
|
10
|
-
* can pass while dispatch fails, which is the failure the probe exists to
|
|
11
|
-
* avoid.
|
|
12
|
-
*
|
|
13
|
-
* Run it instead of hand-rolling a `fetch` in a scratch file:
|
|
14
|
-
*
|
|
15
|
-
* bun run scripts/operations/probe-egress.ts --provider opencode \
|
|
16
|
-
* --direct \
|
|
17
|
-
* --proxy http://nozomi.proxy.rlwy.net:48011 \
|
|
18
|
-
* --proxy iriguchi.proxy.rlwy.net:23244
|
|
19
|
-
*
|
|
20
|
-
* # Read endpoints from a file, one per line (# comments allowed).
|
|
21
|
-
* bun run scripts/operations/probe-egress.ts -p opencode -f proxies.txt
|
|
22
|
-
*
|
|
23
|
-
* # Pipe them in.
|
|
24
|
-
* cat proxies.txt | bun run scripts/operations/probe-egress.ts -p opencode
|
|
25
|
-
*
|
|
26
|
-
* Every endpoint is probed concurrently (default 4 at a time, `--concurrency`).
|
|
27
|
-
* Exit code is 0 when at least one egress is usable, 1 otherwise, so it can gate
|
|
28
|
-
* a shell pipeline.
|
|
29
|
-
*
|
|
30
|
-
* Verdicts:
|
|
31
|
-
*
|
|
32
|
-
* allowed the upstream served the dispatch (2xx) — or answered with its
|
|
33
|
-
* own validation error, which proves the IP gate let the address
|
|
34
|
-
* through. This is the distinction that matters: a 400
|
|
35
|
-
* `messages must not be empty` is an ALLOWED egress and a broken
|
|
36
|
-
* probe body, not a broken proxy.
|
|
37
|
-
* blocked the upstream refused the address itself (`403 FreeTierError`).
|
|
38
|
-
* Replacing the proxy is the only fix; waiting does nothing.
|
|
39
|
-
* unreachable the probe never got a usable answer: CONNECT failed, DNS
|
|
40
|
-
* failed, or the upstream answered 5xx. Says nothing about
|
|
41
|
-
* whether the address is refused.
|
|
42
|
-
* throttled the upstream answered 429. The address is accepted; it is the
|
|
43
|
-
* rate that is spent.
|
|
44
|
-
*/
|
|
45
|
-
import { createHttpProxyAgent } from "../../src/network/pool/agent";
|
|
46
|
-
import { createValidatedFetch } from "../../src/network/outbound-fetch";
|
|
47
|
-
import { buildProviderProbeRequest, isProbeableProvider } from "../../src/providers/provider-probe";
|
|
48
|
-
import { classifyProviderProbeAnswer, type ProbeVerdict } from "../../src/console/routing/pools/probe-verdict";
|
|
49
|
-
import { BUNDLED_PROVIDER_METADATA } from "../../src/providers/provider-metadata";
|
|
50
|
-
|
|
51
|
-
/** One egress under test, plus how to build its dial. */
|
|
52
|
-
interface EgressTarget {
|
|
53
|
-
/** Human label for the result row. */
|
|
54
|
-
readonly label: string;
|
|
55
|
-
/** `undefined` dials direct. */
|
|
56
|
-
readonly proxy?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface ProbeOutcome {
|
|
60
|
-
readonly label: string;
|
|
61
|
-
readonly verdict: ProbeVerdict;
|
|
62
|
-
readonly httpStatus?: number;
|
|
63
|
-
readonly latencyMs?: number;
|
|
64
|
-
readonly detail?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
68
|
-
const PROBE_TIMEOUT_MS = 30_000;
|
|
69
|
-
|
|
70
|
-
interface Options {
|
|
71
|
-
readonly providerId: string;
|
|
72
|
-
readonly targets: readonly EgressTarget[];
|
|
73
|
-
readonly concurrency: number;
|
|
74
|
-
readonly timeoutMs: number;
|
|
75
|
-
readonly json: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function usage(): never {
|
|
79
|
-
console.error(
|
|
80
|
-
[
|
|
81
|
-
"Usage: bun run scripts/operations/probe-egress.ts --provider <id> [options]",
|
|
82
|
-
"",
|
|
83
|
-
"Options:",
|
|
84
|
-
" -p, --provider <id> Provider to probe (must be IP-scoped; opencode is the only one)",
|
|
85
|
-
" --direct Also probe this machine's own egress",
|
|
86
|
-
" --proxy <endpoint> Add a proxy (repeatable). Bare host:port is read as http://",
|
|
87
|
-
" -f, --file <path> Read endpoints from a file, one per line ('#' comments allowed)",
|
|
88
|
-
" --stdin Read endpoints from stdin",
|
|
89
|
-
" --concurrency <n> Parallel probes (default 4)",
|
|
90
|
-
" --timeout <ms> Per-probe timeout (default 30000)",
|
|
91
|
-
" --json Print one JSON object per line instead of a table",
|
|
92
|
-
" -h, --help This message",
|
|
93
|
-
"",
|
|
94
|
-
"Endpoints may be http://, https://, socks5://, or bare host:port (http).",
|
|
95
|
-
"Exit code: 0 if any egress is usable, 1 otherwise.",
|
|
96
|
-
].join("\n"),
|
|
97
|
-
);
|
|
98
|
-
process.exit(2);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Normalizes an operator-supplied endpoint. Bare `host:port` means HTTP. */
|
|
102
|
-
function normalizeEndpoint(raw: string): string {
|
|
103
|
-
const trimmed = raw.trim();
|
|
104
|
-
if (trimmed.includes("://")) return trimmed;
|
|
105
|
-
return `http://${trimmed}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function collectEndpointsFromText(text: string): string[] {
|
|
109
|
-
return text
|
|
110
|
-
.split(/\r?\n/)
|
|
111
|
-
.map((line) => line.replace(/#.*$/, "").trim())
|
|
112
|
-
.filter((line) => line.length > 0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseArgs(argv: readonly string[]): Options {
|
|
116
|
-
let providerId: string | undefined;
|
|
117
|
-
const targets: EgressTarget[] = [];
|
|
118
|
-
let concurrency = DEFAULT_CONCURRENCY;
|
|
119
|
-
let timeoutMs = PROBE_TIMEOUT_MS;
|
|
120
|
-
let json = false;
|
|
121
|
-
const files: string[] = [];
|
|
122
|
-
let readStdin = false;
|
|
123
|
-
|
|
124
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
125
|
-
const arg = argv[index];
|
|
126
|
-
if (arg === undefined) continue;
|
|
127
|
-
const next = (): string => {
|
|
128
|
-
const value = argv[index + 1];
|
|
129
|
-
if (value === undefined) usage();
|
|
130
|
-
index += 1;
|
|
131
|
-
return value;
|
|
132
|
-
};
|
|
133
|
-
switch (arg) {
|
|
134
|
-
case "-p":
|
|
135
|
-
case "--provider":
|
|
136
|
-
providerId = next();
|
|
137
|
-
break;
|
|
138
|
-
case "--direct":
|
|
139
|
-
targets.push({ label: "direct" });
|
|
140
|
-
break;
|
|
141
|
-
case "--proxy":
|
|
142
|
-
targets.push({ label: "", proxy: normalizeEndpoint(next()) });
|
|
143
|
-
break;
|
|
144
|
-
case "-f":
|
|
145
|
-
case "--file":
|
|
146
|
-
files.push(next());
|
|
147
|
-
break;
|
|
148
|
-
case "--stdin":
|
|
149
|
-
readStdin = true;
|
|
150
|
-
break;
|
|
151
|
-
case "--concurrency":
|
|
152
|
-
concurrency = Math.max(1, Number(next()) || DEFAULT_CONCURRENCY);
|
|
153
|
-
break;
|
|
154
|
-
case "--timeout":
|
|
155
|
-
timeoutMs = Math.max(1_000, Number(next()) || PROBE_TIMEOUT_MS);
|
|
156
|
-
break;
|
|
157
|
-
case "--json":
|
|
158
|
-
json = true;
|
|
159
|
-
break;
|
|
160
|
-
case "-h":
|
|
161
|
-
case "--help":
|
|
162
|
-
usage();
|
|
163
|
-
break;
|
|
164
|
-
default:
|
|
165
|
-
// A bare argument is an endpoint, so `... --provider opencode host:port`
|
|
166
|
-
// works without a flag. Anything starting with `-` is a mistake.
|
|
167
|
-
if (arg.startsWith("-")) {
|
|
168
|
-
console.error(`Unknown option: ${arg}`);
|
|
169
|
-
usage();
|
|
170
|
-
}
|
|
171
|
-
targets.push({ label: "", proxy: normalizeEndpoint(arg) });
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (providerId === undefined) {
|
|
177
|
-
console.error("--provider is required");
|
|
178
|
-
usage();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const fromFiles: EgressTarget[] = [];
|
|
182
|
-
for (const file of files) {
|
|
183
|
-
const text = readFileText(file);
|
|
184
|
-
for (const endpoint of collectEndpointsFromText(text)) {
|
|
185
|
-
fromFiles.push({ label: "", proxy: normalizeEndpoint(endpoint) });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (readStdin) {
|
|
189
|
-
const text = readFileText(0);
|
|
190
|
-
for (const endpoint of collectEndpointsFromText(text)) {
|
|
191
|
-
fromFiles.push({ label: "", proxy: normalizeEndpoint(endpoint) });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const all = [...targets, ...fromFiles];
|
|
196
|
-
if (all.length === 0) {
|
|
197
|
-
console.error("No egress to probe: pass --direct, --proxy, --file, or --stdin");
|
|
198
|
-
usage();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
providerId,
|
|
203
|
-
targets: all.map((target) => ({
|
|
204
|
-
// The label is what the operator typed, minus the scheme they may have
|
|
205
|
-
// omitted — printing `http://` on every row buries the distinguishing part.
|
|
206
|
-
label: target.proxy === undefined ? target.label : target.proxy.replace(/^[a-z0-9+.-]+:\/\//i, ""),
|
|
207
|
-
...(target.proxy === undefined ? {} : { proxy: target.proxy }),
|
|
208
|
-
})),
|
|
209
|
-
concurrency,
|
|
210
|
-
timeoutMs,
|
|
211
|
-
json,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Reads a file path, or fd 0 for stdin. Kept separate so `parseArgs` stays pure-ish. */
|
|
216
|
-
function readFileText(path: string | 0): string {
|
|
217
|
-
try {
|
|
218
|
-
return require("node:fs").readFileSync(path, "utf8") as string;
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.error(`Cannot read ${path === 0 ? "stdin" : path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
221
|
-
process.exit(2);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Maps one response to a verdict through the same classifier the console uses,
|
|
227
|
-
* so a CLI run and the dashboard's "Test OpenCode" button can never disagree
|
|
228
|
-
* about what an answer means.
|
|
229
|
-
*/
|
|
230
|
-
async function probeOne(
|
|
231
|
-
spec: NonNullable<Awaited<ReturnType<typeof buildProviderProbeRequest>>>,
|
|
232
|
-
target: EgressTarget,
|
|
233
|
-
timeoutMs: number,
|
|
234
|
-
): Promise<ProbeOutcome> {
|
|
235
|
-
const started = performance.now();
|
|
236
|
-
try {
|
|
237
|
-
const agent = target.proxy === undefined ? undefined : createHttpProxyAgent(target.proxy);
|
|
238
|
-
const dial = createValidatedFetch(agent === undefined ? {} : { agent });
|
|
239
|
-
const response = await dial(spec.url, {
|
|
240
|
-
method: "POST",
|
|
241
|
-
headers: spec.headers,
|
|
242
|
-
body: JSON.stringify(spec.body),
|
|
243
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
244
|
-
});
|
|
245
|
-
const body = await response.text().catch(() => "");
|
|
246
|
-
const answer = classifyProviderProbeAnswer(response.status, body);
|
|
247
|
-
return {
|
|
248
|
-
label: target.label,
|
|
249
|
-
verdict: answer.status,
|
|
250
|
-
httpStatus: response.status,
|
|
251
|
-
latencyMs: Math.round(performance.now() - started),
|
|
252
|
-
...(answer.reason === undefined ? {} : { detail: answer.reason }),
|
|
253
|
-
};
|
|
254
|
-
} catch (error) {
|
|
255
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
256
|
-
const timedOut =
|
|
257
|
-
(error instanceof Error && error.name === "AbortError") ||
|
|
258
|
-
/timed out|timeout|ETIMEDOUT/i.test(message);
|
|
259
|
-
return {
|
|
260
|
-
label: target.label,
|
|
261
|
-
verdict: "unreachable",
|
|
262
|
-
latencyMs: Math.round(performance.now() - started),
|
|
263
|
-
detail: timedOut ? "probe timed out" : message.split("\n")[0]!.slice(0, 120),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const VERDICT_GLYPH: Record<ProbeVerdict, string> = {
|
|
269
|
-
allowed: "OK ",
|
|
270
|
-
blocked: "BLOCK",
|
|
271
|
-
throttled: "RATE ",
|
|
272
|
-
unreachable: "FAIL ",
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
async function main(): Promise<void> {
|
|
276
|
-
const options = parseArgs(process.argv.slice(2));
|
|
277
|
-
|
|
278
|
-
if (!isProbeableProvider(options.providerId)) {
|
|
279
|
-
console.error(
|
|
280
|
-
`${options.providerId} is not IP-scoped, so a probe cannot tell an egress refusal from a credential problem. ` +
|
|
281
|
-
`Probeable: ${BUNDLED_PROVIDER_METADATA.filter((entry) => isProbeableProvider(entry.id)).map((entry) => entry.id).join(", ") || "(none)"}`,
|
|
282
|
-
);
|
|
283
|
-
process.exit(2);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const spec = await buildProviderProbeRequest(options.providerId);
|
|
287
|
-
if (!spec) {
|
|
288
|
-
console.error(`No probe spec for ${options.providerId}`);
|
|
289
|
-
process.exit(2);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!options.json) {
|
|
293
|
-
console.error(`Probing ${options.providerId} via ${spec.url}`);
|
|
294
|
-
console.error(`${options.targets.length} egress(es), ${options.concurrency} at a time\n`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const results: ProbeOutcome[] = new Array(options.targets.length);
|
|
298
|
-
let cursor = 0;
|
|
299
|
-
const worker = async (): Promise<void> => {
|
|
300
|
-
for (;;) {
|
|
301
|
-
const index = cursor++;
|
|
302
|
-
const target = options.targets[index];
|
|
303
|
-
if (target === undefined) return;
|
|
304
|
-
results[index] = await probeOne(spec, target, options.timeoutMs);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
await Promise.all(
|
|
308
|
-
Array.from({ length: Math.min(options.concurrency, options.targets.length) }, () => worker()),
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
if (options.json) {
|
|
312
|
-
for (const result of results) console.log(JSON.stringify(result));
|
|
313
|
-
} else {
|
|
314
|
-
const labelWidth = Math.max(...results.map((result) => result.label.length), 8);
|
|
315
|
-
for (const result of results) {
|
|
316
|
-
const status = result.httpStatus === undefined ? " -" : String(result.httpStatus).padStart(3);
|
|
317
|
-
const latency = result.latencyMs === undefined ? "" : `${String(result.latencyMs).padStart(6)}ms`;
|
|
318
|
-
console.log(
|
|
319
|
-
`${result.label.padEnd(labelWidth)} ${VERDICT_GLYPH[result.verdict]} ${status} ${latency.padStart(9)} ${result.detail ?? ""}`,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
const tally = (verdict: ProbeVerdict): number => results.filter((r) => r.verdict === verdict).length;
|
|
323
|
-
console.log(
|
|
324
|
-
`\nallowed ${tally("allowed")} · blocked ${tally("blocked")} · ` +
|
|
325
|
-
`throttled ${tally("throttled")} · unreachable ${tally("unreachable")}`,
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const usable = results.some(
|
|
330
|
-
(result) => result.verdict === "allowed" || result.verdict === "throttled",
|
|
331
|
-
);
|
|
332
|
-
process.exit(usable ? 0 : 1);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
await main();
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* restart.ts — rebuild and restart the compiled gateway gracefully.
|
|
4
|
-
*
|
|
5
|
-
* Why this exists: a hard kill (`taskkill /F`, `kill -9`) truncates every
|
|
6
|
-
* in-flight response. On Linux the process handles SIGTERM/SIGINT and drains,
|
|
7
|
-
* but Windows has no deliverable catchable signal — `process.kill(pid,
|
|
8
|
-
* "SIGTERM")` of a console-less Bun process runs no JS handler (measured). So
|
|
9
|
-
* this script stops the running instance through the drain endpoint when
|
|
10
|
-
* `CARTETHYIA_DRAIN_TOKEN` is set (loopback + `x-drain-token`), and only falls
|
|
11
|
-
* back to a hard kill when no token is configured.
|
|
12
|
-
*
|
|
13
|
-
* Env:
|
|
14
|
-
* CARTETHYIA_DRAIN_TOKEN token for POST /admin/drain (required for graceful stop)
|
|
15
|
-
* PORT gateway port (default 12800)
|
|
16
|
-
* DRAIN_WAIT_MS how long to wait for the drain to finish (default 30000)
|
|
17
|
-
*/
|
|
18
|
-
import { compiledBinaryPath } from "../build/binary";
|
|
19
|
-
|
|
20
|
-
function intEnv(name: string, fallback: number): number {
|
|
21
|
-
const raw = process.env[name];
|
|
22
|
-
if (raw === undefined || raw.trim() === "") return fallback;
|
|
23
|
-
const value = Number(raw);
|
|
24
|
-
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const PORT = intEnv("PORT", 12800);
|
|
28
|
-
const DRAIN_WAIT_MS = intEnv("DRAIN_WAIT_MS", 30_000);
|
|
29
|
-
|
|
30
|
-
/** PIDs currently listening on `port` (best-effort, platform-specific). */
|
|
31
|
-
async function listeningPids(port: number): Promise<number[]> {
|
|
32
|
-
try {
|
|
33
|
-
if (process.platform === "win32") {
|
|
34
|
-
const proc = Bun.spawnSync(["netstat", "-ano", "-p", "TCP"]);
|
|
35
|
-
const out = new TextDecoder().decode(proc.stdout);
|
|
36
|
-
const pids = new Set<number>();
|
|
37
|
-
for (const line of out.split(/\r?\n/)) {
|
|
38
|
-
if (!line.includes(`:${port} `) || !line.includes("LISTENING")) continue;
|
|
39
|
-
const pid = Number(line.trim().split(/\s+/).pop());
|
|
40
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
41
|
-
}
|
|
42
|
-
return [...pids];
|
|
43
|
-
}
|
|
44
|
-
const proc = Bun.spawnSync(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
45
|
-
return new TextDecoder()
|
|
46
|
-
.decode(proc.stdout)
|
|
47
|
-
.split(/\s+/)
|
|
48
|
-
.map(Number)
|
|
49
|
-
.filter((n) => Number.isInteger(n) && n > 0);
|
|
50
|
-
} catch {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function waitForPortFree(port: number, timeoutMs: number): Promise<boolean> {
|
|
56
|
-
const deadline = Date.now() + timeoutMs;
|
|
57
|
-
while (Date.now() < deadline) {
|
|
58
|
-
if ((await listeningPids(port)).length === 0) return true;
|
|
59
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
60
|
-
}
|
|
61
|
-
return (await listeningPids(port)).length === 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function stopGracefully(): Promise<boolean> {
|
|
65
|
-
const token = process.env.CARTETHYIA_DRAIN_TOKEN?.trim();
|
|
66
|
-
if (token === undefined || token.length === 0) {
|
|
67
|
-
console.warn("[restart] CARTETHYIA_DRAIN_TOKEN unset — cannot drain gracefully");
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
const response = await fetch(`http://127.0.0.1:${PORT}/admin/drain`, {
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: { "x-drain-token": token },
|
|
74
|
-
});
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
console.warn(`[restart] drain request returned HTTP ${response.status}`);
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
console.log("[restart] drain acknowledged; waiting for the process to exit");
|
|
80
|
-
return await waitForPortFree(PORT, DRAIN_WAIT_MS);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.warn(`[restart] drain request failed: ${(error as Error).message}`);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function hardKill(): Promise<void> {
|
|
88
|
-
const pids = await listeningPids(PORT);
|
|
89
|
-
if (pids.length === 0) return;
|
|
90
|
-
console.warn(`[restart] force-killing pid(s) ${pids.join(", ")} on :${PORT}`);
|
|
91
|
-
for (const pid of pids) {
|
|
92
|
-
try {
|
|
93
|
-
process.kill(pid, "SIGKILL");
|
|
94
|
-
} catch {
|
|
95
|
-
// Already gone.
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
await waitForPortFree(PORT, 5_000);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function main(): Promise<number> {
|
|
102
|
-
const graceful = await stopGracefully();
|
|
103
|
-
if (!graceful) await hardKill();
|
|
104
|
-
|
|
105
|
-
console.log("[restart] building");
|
|
106
|
-
const build = Bun.spawnSync(["bun", "run", "build"], { stdout: "inherit", stderr: "inherit" });
|
|
107
|
-
if (build.exitCode !== 0) {
|
|
108
|
-
console.error("[restart] build failed");
|
|
109
|
-
return build.exitCode ?? 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const binary = compiledBinaryPath();
|
|
113
|
-
console.log(`[restart] starting ${binary}`);
|
|
114
|
-
const child = Bun.spawn([binary], { stdin: "ignore", stdout: "inherit", stderr: "inherit" });
|
|
115
|
-
return await child.exited;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (import.meta.main) {
|
|
119
|
-
process.exit(await main());
|
|
120
|
-
}
|