cartethyia 2.5.10 → 2.6.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.
- package/.env.example +8 -0
- package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.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 +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -95
- package/scripts/cli/admin.ts +126 -128
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +39 -30
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -260
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -167
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +217 -79
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -85
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +29 -4
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +10 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
- package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
- package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
- package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
- package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
- package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
- package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
- package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
- package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
- package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
- package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
- package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
- package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -439
|
@@ -16,6 +16,9 @@ import { chooseFromMenu, confirmByTyping, confirmStep, type MenuRow } from "./pr
|
|
|
16
16
|
import { renderWordmark } from "./wordmark";
|
|
17
17
|
import { exportBackup, resetConsoleAccount } from "./admin";
|
|
18
18
|
import { runDetachedUpdate } from "./update";
|
|
19
|
+
import { acquireController } from "./controller";
|
|
20
|
+
import { restoreControllerAfterFailedHandoff } from "./controller";
|
|
21
|
+
import { attachGateway as attachRunningGateway } from "./instance";
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -25,6 +28,18 @@ import { runDetachedUpdate } from "./update";
|
|
|
25
28
|
* and is never seen. `carte --tray` opts into the icon instead.
|
|
26
29
|
*/
|
|
27
30
|
export async function startMenu(): Promise<void> {
|
|
31
|
+
const controller = acquireController("menu");
|
|
32
|
+
const releaseController = (): void => controller.release();
|
|
33
|
+
process.once("exit", releaseController);
|
|
34
|
+
try {
|
|
35
|
+
await runMenu(controller.handoff);
|
|
36
|
+
} finally {
|
|
37
|
+
controller.release();
|
|
38
|
+
process.removeListener("exit", releaseController);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runMenu(handoff: { readonly token: string; readonly parentPid: number }): Promise<void> {
|
|
28
43
|
// A fresh install has no config file yet, and the gateway refuses to boot
|
|
29
44
|
// without CARTETHYIA_DB_MODE — which surfaced as a stack trace about a
|
|
30
45
|
// missing database mode rather than "you have not set this up". Run setup
|
|
@@ -136,6 +151,7 @@ export async function startMenu(): Promise<void> {
|
|
|
136
151
|
|
|
137
152
|
let latestVersion = await readLatestPublishedVersion();
|
|
138
153
|
let running = true;
|
|
154
|
+
let backupStatus: { readonly tone: "success" | "error"; readonly text: string } | undefined;
|
|
139
155
|
|
|
140
156
|
while (running) {
|
|
141
157
|
const upToDate = latestVersion === null || !isNewerRelease(currentVersion, latestVersion);
|
|
@@ -146,16 +162,15 @@ export async function startMenu(): Promise<void> {
|
|
|
146
162
|
// action that matters.
|
|
147
163
|
const versionCheckerLine =
|
|
148
164
|
latestVersion === null
|
|
149
|
-
?
|
|
165
|
+
? `${currentVersion} \u00b7 registry unavailable`
|
|
150
166
|
: upToDate
|
|
151
|
-
?
|
|
152
|
-
:
|
|
167
|
+
? `${currentVersion} \u00b7 up to date`
|
|
168
|
+
: `${currentVersion} \u2192 ${latestVersion} available`;
|
|
153
169
|
const header = [
|
|
154
170
|
...renderWordmark(),
|
|
155
|
-
`
|
|
156
|
-
versionCheckerLine
|
|
171
|
+
`Console ${dashboardUrl}`,
|
|
172
|
+
`Version ${versionCheckerLine}`,
|
|
157
173
|
"",
|
|
158
|
-
"Section menu",
|
|
159
174
|
];
|
|
160
175
|
|
|
161
176
|
// Anything that talks to the gateway waits for it to be listening; the menu
|
|
@@ -167,30 +182,23 @@ export async function startMenu(): Promise<void> {
|
|
|
167
182
|
const actions: readonly (() => Promise<void>)[] = [
|
|
168
183
|
async () => openUrl(dashboardUrl),
|
|
169
184
|
async () => {
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// so starting it before this process has let go made it fail with
|
|
174
|
-
// "already running on port" every time — while the menu still printed
|
|
175
|
-
// that everything was running, because a detached child with no stdio
|
|
176
|
-
// cannot report anything back. Releasing the port first is the fix, and
|
|
177
|
-
// waiting on the child's signal is what stops the menu from claiming
|
|
178
|
-
// success for a child that died.
|
|
179
|
-
console.log("Stopping the gateway to free the port…");
|
|
185
|
+
// Transfer the live gateway rather than draining it first. The tray
|
|
186
|
+
// adopts this instance through the ownership marker and reports ready
|
|
187
|
+
// before the menu returns to the shell.
|
|
180
188
|
running = false;
|
|
181
189
|
stopping = true;
|
|
182
190
|
handedOff = true;
|
|
183
|
-
await shutDown();
|
|
184
191
|
console.log("Starting the tray in the background…");
|
|
185
192
|
const signalPath = join(tmpdir(), `cartethyia-tray-menu-${process.pid}-${randomUUID()}.json`);
|
|
186
193
|
try {
|
|
187
194
|
const url = await launchBackgroundWithStatus(
|
|
188
195
|
["run", resolvePackagePath("scripts", "cli", "main.ts"), "tray"],
|
|
189
|
-
{ signalPath },
|
|
196
|
+
{ signalPath, controllerHandoff: handoff },
|
|
190
197
|
);
|
|
191
198
|
console.log(`Cartethyia is running in the background at ${url || dashboardUrl}.`);
|
|
192
199
|
console.log("The tray icon owns it; use Exit there to stop it.");
|
|
193
200
|
} catch (error) {
|
|
201
|
+
restoreControllerAfterFailedHandoff(handoff);
|
|
194
202
|
handedOff = false;
|
|
195
203
|
console.error(
|
|
196
204
|
`Could not hand off to the tray: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
@@ -205,11 +213,12 @@ export async function startMenu(): Promise<void> {
|
|
|
205
213
|
// the database, so the loopback token is the whole gate.
|
|
206
214
|
await whenReady();
|
|
207
215
|
const result = await exportBackup(port, drainToken);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
backupStatus = "error" in result
|
|
217
|
+
? { tone: "error", text: `Backup failed: ${result.error}` }
|
|
218
|
+
: {
|
|
219
|
+
tone: "success",
|
|
220
|
+
text: `Backup saved at: ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB)`,
|
|
221
|
+
};
|
|
213
222
|
},
|
|
214
223
|
async () => {
|
|
215
224
|
// Deleting the console account is the only real password recovery:
|
|
@@ -266,23 +275,23 @@ export async function startMenu(): Promise<void> {
|
|
|
266
275
|
: [...actions.slice(0, actions.length - 1), updateAction, actions[actions.length - 1] ?? (async () => undefined)];
|
|
267
276
|
|
|
268
277
|
const rows: MenuRow[] = [
|
|
269
|
-
{ kind: "action", label: "Open Console", hint:
|
|
270
|
-
{ kind: "action", label: "Hide to Tray", hint: "
|
|
271
|
-
{ kind: "action", label: "Backup", hint: "
|
|
272
|
-
{ kind: "action", label: "Reset", hint: "
|
|
278
|
+
{ kind: "action", label: "Open Console", hint: "Open dashboard in browser" },
|
|
279
|
+
{ kind: "action", label: "Hide to Tray", hint: "Keep the gateway running" },
|
|
280
|
+
{ kind: "action", label: "Backup", hint: "Export config to Downloads" },
|
|
281
|
+
{ kind: "action", label: "Reset", hint: "Delete console account" },
|
|
273
282
|
];
|
|
274
283
|
if (!upToDate && latestVersion !== null) {
|
|
275
284
|
rows.push({ kind: "action", label: `Update to ${latestVersion}`, hint: "installs now and exits" });
|
|
276
285
|
}
|
|
277
286
|
rows.push({ kind: "rule" });
|
|
278
|
-
rows.push({ kind: "action", label: "Exit", hint: "
|
|
287
|
+
rows.push({ kind: "action", label: "Exit", hint: "Stop the gateway" });
|
|
279
288
|
|
|
280
289
|
// Focus the Update row by default when it exists, so the operator who
|
|
281
290
|
// launched `carte` *because* they saw the version checker gets a one-key
|
|
282
291
|
// path to the fix.
|
|
283
292
|
const updateIndex = upToDate || latestVersion === null ? -1 : 4;
|
|
284
293
|
const initialActionIndex = updateIndex >= 0 ? updateIndex : 0;
|
|
285
|
-
const choice = await chooseFromMenu(header, rows, { initialIndex: initialActionIndex });
|
|
294
|
+
const choice = await chooseFromMenu(header, rows, { initialIndex: initialActionIndex, ...(backupStatus ? { status: backupStatus } : {}) });
|
|
286
295
|
if (choice === -1) break;
|
|
287
296
|
const action = extendedActions[choice];
|
|
288
297
|
if (action) await action();
|
|
@@ -291,7 +300,7 @@ export async function startMenu(): Promise<void> {
|
|
|
291
300
|
// would stop the very process the tray just adopted, which is why the menu
|
|
292
301
|
// appeared to hand over a gateway that was not listening.
|
|
293
302
|
if (handedOff) return;
|
|
294
|
-
if (runningAttached !== null) {
|
|
303
|
+
if (runningAttached !== null || (await attachRunningGateway(port, drainToken)) !== null) {
|
|
295
304
|
detachAttached();
|
|
296
305
|
return;
|
|
297
306
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pidAlive } from "./instance";
|
|
4
|
+
|
|
5
|
+
/** A menu or tray that exclusively owns this installation's interactive UI. */
|
|
6
|
+
export type ControllerKind = "menu" | "tray";
|
|
7
|
+
|
|
8
|
+
/** Proof passed to the tray during Hide to Tray. Never printed or persisted in logs. */
|
|
9
|
+
export interface ControllerHandoff {
|
|
10
|
+
readonly token: string;
|
|
11
|
+
readonly parentPid: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ControllerLease {
|
|
15
|
+
readonly handoff: ControllerHandoff;
|
|
16
|
+
readonly release: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ControllerRecord extends ControllerHandoff {
|
|
20
|
+
readonly pid: number;
|
|
21
|
+
readonly kind: ControllerKind;
|
|
22
|
+
readonly startedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CONTROLLER_FILE = "controller.json";
|
|
26
|
+
|
|
27
|
+
function controllerPath(): string {
|
|
28
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
29
|
+
if (!configDir) throw new Error("Cartethyia config directory is unavailable");
|
|
30
|
+
mkdirSync(configDir, { recursive: true });
|
|
31
|
+
return join(configDir, CONTROLLER_FILE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readController(path: string): ControllerRecord | null {
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as Partial<ControllerRecord>;
|
|
37
|
+
if (typeof value.pid !== "number" || typeof value.parentPid !== "number" ||
|
|
38
|
+
typeof value.token !== "string" || value.token.length === 0 ||
|
|
39
|
+
(value.kind !== "menu" && value.kind !== "tray") || typeof value.startedAt !== "string") return null;
|
|
40
|
+
return value as ControllerRecord;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
43
|
+
// A partially rewritten or corrupt file must not be treated as permission
|
|
44
|
+
// to start a second gateway. It is only safe to reclaim a record whose old
|
|
45
|
+
// owner can be identified and is definitely dead.
|
|
46
|
+
throw new Error("The Cartethyia controller lock is unreadable; close carte/tray before retrying");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeController(path: string, record: ControllerRecord, exclusive: boolean): void {
|
|
51
|
+
writeFileSync(path, JSON.stringify(record), {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
mode: 0o600,
|
|
54
|
+
...(exclusive ? { flag: "wx" } : {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Acquire the one interactive controller for an installation.
|
|
60
|
+
*
|
|
61
|
+
* Hide to Tray passes the existing token to its child. The child replaces the
|
|
62
|
+
* pid in the same file without unlinking it, so a second menu cannot sneak into
|
|
63
|
+
* the handoff gap and drain the gateway while the first tray is taking over.
|
|
64
|
+
*/
|
|
65
|
+
export function acquireController(kind: ControllerKind, handoff?: ControllerHandoff): ControllerLease {
|
|
66
|
+
const path = controllerPath();
|
|
67
|
+
if (handoff !== undefined) {
|
|
68
|
+
const current = readController(path);
|
|
69
|
+
if (current === null || current.token !== handoff.token || current.parentPid !== handoff.parentPid ||
|
|
70
|
+
current.kind !== "menu") {
|
|
71
|
+
throw new Error("The menu-to-tray handoff is no longer owned by its original menu");
|
|
72
|
+
}
|
|
73
|
+
writeController(path, { ...current, pid: process.pid, kind: "tray" }, false);
|
|
74
|
+
} else {
|
|
75
|
+
const current = readController(path);
|
|
76
|
+
if (current !== null && pidAlive(current.pid)) {
|
|
77
|
+
throw new Error(`Cartethyia is already controlled by a ${current.kind} (PID ${current.pid}); not starting another instance`);
|
|
78
|
+
}
|
|
79
|
+
const token = crypto.randomUUID();
|
|
80
|
+
// Writing the replacement atomically avoids a corruption window in which
|
|
81
|
+
// another carte cannot tell whether the old owner was alive. A concurrent
|
|
82
|
+
// claimant reads either the complete old record or this complete new one.
|
|
83
|
+
const next: ControllerRecord = {
|
|
84
|
+
pid: process.pid,
|
|
85
|
+
parentPid: process.pid,
|
|
86
|
+
token,
|
|
87
|
+
kind,
|
|
88
|
+
startedAt: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
if (current !== null) {
|
|
92
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
93
|
+
writeFileSync(temporary, JSON.stringify(next), { encoding: "utf8", mode: 0o600 });
|
|
94
|
+
try { renameSync(temporary, path); } catch (error) {
|
|
95
|
+
try { unlinkSync(temporary); } catch { /* Preserve the rename error. */ }
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
} else writeController(path, next, true);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
101
|
+
throw new Error("Another Cartethyia menu or tray acquired the controller; not starting another instance");
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const token = handoff?.token ?? readController(path)?.token;
|
|
108
|
+
if (!token) throw new Error("Could not establish the Cartethyia controller lock");
|
|
109
|
+
const pid = process.pid;
|
|
110
|
+
let released = false;
|
|
111
|
+
return {
|
|
112
|
+
handoff: { token, parentPid: pid },
|
|
113
|
+
release: () => {
|
|
114
|
+
if (released) return;
|
|
115
|
+
released = true;
|
|
116
|
+
const current = readController(path);
|
|
117
|
+
// During a successful handoff the file already belongs to the tray. The
|
|
118
|
+
// parent must not delete its child's lease while returning to the shell.
|
|
119
|
+
if (current?.token === token && current.pid === pid) {
|
|
120
|
+
try { unlinkSync(path); } catch { /* A dead owner is reclaimed next start. */ }
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Hand a failed tray startup back to the still-running menu. */
|
|
127
|
+
export function restoreControllerAfterFailedHandoff(handoff: ControllerHandoff): void {
|
|
128
|
+
const path = controllerPath();
|
|
129
|
+
const current = readController(path);
|
|
130
|
+
if (current?.token === handoff.token && current.parentPid === handoff.parentPid && current.kind === "tray") {
|
|
131
|
+
writeController(path, { ...current, pid: handoff.parentPid, kind: "menu" }, false);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Read the one-shot handoff proof without exposing it to normal process logs. */
|
|
136
|
+
export function inheritedControllerHandoff(env: NodeJS.ProcessEnv = process.env): ControllerHandoff | undefined {
|
|
137
|
+
const token = env.CARTETHYIA_CONTROLLER_TOKEN;
|
|
138
|
+
const parentPid = Number(env.CARTETHYIA_CONTROLLER_PARENT_PID);
|
|
139
|
+
if (!token || !Number.isInteger(parentPid) || parentPid <= 0) {
|
|
140
|
+
delete env.CARTETHYIA_CONTROLLER_TOKEN;
|
|
141
|
+
delete env.CARTETHYIA_CONTROLLER_PARENT_PID;
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return { token, parentPid };
|
|
145
|
+
}
|
package/scripts/cli/env.ts
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { PACKAGE_ROOT } from "../../src/config/package-paths";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Absolute path of the repository root.
|
|
8
|
-
*
|
|
9
|
-
* Operational scripts and the npm runtime share one package-root resolver so
|
|
10
|
-
* assets and repository config do not depend on the caller's working directory.
|
|
11
|
-
*/
|
|
12
|
-
export const PROJECT_ROOT = PACKAGE_ROOT;
|
|
13
|
-
|
|
14
|
-
/** Resolve the active `.env`; npm CLI sets a user-owned config directory. */
|
|
15
|
-
export function resolveEnvPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
16
|
-
const configDir = env.CARTETHYIA_CONFIG_DIR?.trim();
|
|
17
|
-
return configDir ? resolve(configDir, ".env") : resolve(PROJECT_ROOT, ".env");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Absolute path of the active `.env`. */
|
|
21
|
-
export const ENV_PATH = resolveEnvPath();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Reads a dotenv-style file without overriding already-exported variables.
|
|
25
|
-
*/
|
|
26
|
-
export async function readEnvFile(path: string): Promise<Record<string, string>> {
|
|
27
|
-
if (!existsSync(path)) return {};
|
|
28
|
-
const content = await readFile(path, "utf8");
|
|
29
|
-
const values: Record<string, string> = {};
|
|
30
|
-
for (const line of content.split(/\r?\n/)) {
|
|
31
|
-
if (line.startsWith("#")) continue;
|
|
32
|
-
const separator = line.indexOf("=");
|
|
33
|
-
if (separator <= 0) continue;
|
|
34
|
-
const key = line.slice(0, separator).trim();
|
|
35
|
-
const value = line.slice(separator + 1).trim();
|
|
36
|
-
if (key) values[key] = value;
|
|
37
|
-
}
|
|
38
|
-
return values;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Checks if a string contains a configured placeholder value.
|
|
43
|
-
*/
|
|
44
|
-
export function hasPlaceholderSecrets(value: string): boolean {
|
|
45
|
-
const normalized = value.toLowerCase();
|
|
46
|
-
return (
|
|
47
|
-
normalized.includes("replace-with") ||
|
|
48
|
-
normalized.includes("replace_me") ||
|
|
49
|
-
normalized.includes("your_")
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generates a 256-bit hex cartethyia encryption key.
|
|
55
|
-
*
|
|
56
|
-
* Hex is preferred over base64 because it never carries padding or `+`/`/`
|
|
57
|
-
* characters that confuse some dotenv shells, and it round-trips through
|
|
58
|
-
* every adapter that accepts either encoding. The install/setup helpers call
|
|
59
|
-
* this when the key is absent or still a placeholder.
|
|
60
|
-
*/
|
|
61
|
-
export function generateCartethyiaEncryptionKey(): string {
|
|
62
|
-
const bytes = new Uint8Array(32);
|
|
63
|
-
crypto.getRandomValues(bytes);
|
|
64
|
-
let hex = "";
|
|
65
|
-
for (let i = 0; i < bytes.length; i += 1) hex += bytes[i]!.toString(16).padStart(2, "0");
|
|
66
|
-
return hex;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Returns the mandatory-only `.env` body derived from `.env.example`.
|
|
71
|
-
*
|
|
72
|
-
* Mandatory means the key is assigned without a leading `#`. Optional entries
|
|
73
|
-
* (leading `#`) carry built-in defaults and must not be copied into a fresh
|
|
74
|
-
* `.env` — they would override the default with a literal `example` value.
|
|
75
|
-
* The generated body therefore contains only mandatory rows, plus a single
|
|
76
|
-
* `CARTETHYIA_ENCRYPTION_KEY` replacement when the caller supplies one.
|
|
77
|
-
*/
|
|
78
|
-
export async function mandatoryEnvBody(
|
|
79
|
-
examplePath: string,
|
|
80
|
-
overrides: Readonly<Record<string, string>> = {},
|
|
81
|
-
): Promise<string> {
|
|
82
|
-
const raw = await readFile(examplePath, "utf8");
|
|
83
|
-
const lines: string[] = [];
|
|
84
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
85
|
-
const trimmed = line.trim();
|
|
86
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
87
|
-
const sep = line.indexOf("=");
|
|
88
|
-
if (sep <= 0) continue;
|
|
89
|
-
const key = line.slice(0, sep).trim();
|
|
90
|
-
if (!key) continue;
|
|
91
|
-
const value = (overrides as Record<string, string>)[key] ?? line.slice(sep + 1).trim();
|
|
92
|
-
lines.push(`${key}=${value}`);
|
|
93
|
-
}
|
|
94
|
-
// Preserve overrides even when the example row was commented out (legacy case).
|
|
95
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
96
|
-
if (!lines.some((l) => l.startsWith(`${key}=`))) lines.push(`${key}=${value}`);
|
|
97
|
-
}
|
|
98
|
-
return `${lines.join("\n")}\n`;
|
|
99
|
-
}
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { PACKAGE_ROOT } from "../../src/config/package-paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path of the repository root.
|
|
8
|
+
*
|
|
9
|
+
* Operational scripts and the npm runtime share one package-root resolver so
|
|
10
|
+
* assets and repository config do not depend on the caller's working directory.
|
|
11
|
+
*/
|
|
12
|
+
export const PROJECT_ROOT = PACKAGE_ROOT;
|
|
13
|
+
|
|
14
|
+
/** Resolve the active `.env`; npm CLI sets a user-owned config directory. */
|
|
15
|
+
export function resolveEnvPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
16
|
+
const configDir = env.CARTETHYIA_CONFIG_DIR?.trim();
|
|
17
|
+
return configDir ? resolve(configDir, ".env") : resolve(PROJECT_ROOT, ".env");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Absolute path of the active `.env`. */
|
|
21
|
+
export const ENV_PATH = resolveEnvPath();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reads a dotenv-style file without overriding already-exported variables.
|
|
25
|
+
*/
|
|
26
|
+
export async function readEnvFile(path: string): Promise<Record<string, string>> {
|
|
27
|
+
if (!existsSync(path)) return {};
|
|
28
|
+
const content = await readFile(path, "utf8");
|
|
29
|
+
const values: Record<string, string> = {};
|
|
30
|
+
for (const line of content.split(/\r?\n/)) {
|
|
31
|
+
if (line.startsWith("#")) continue;
|
|
32
|
+
const separator = line.indexOf("=");
|
|
33
|
+
if (separator <= 0) continue;
|
|
34
|
+
const key = line.slice(0, separator).trim();
|
|
35
|
+
const value = line.slice(separator + 1).trim();
|
|
36
|
+
if (key) values[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if a string contains a configured placeholder value.
|
|
43
|
+
*/
|
|
44
|
+
export function hasPlaceholderSecrets(value: string): boolean {
|
|
45
|
+
const normalized = value.toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
normalized.includes("replace-with") ||
|
|
48
|
+
normalized.includes("replace_me") ||
|
|
49
|
+
normalized.includes("your_")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a 256-bit hex cartethyia encryption key.
|
|
55
|
+
*
|
|
56
|
+
* Hex is preferred over base64 because it never carries padding or `+`/`/`
|
|
57
|
+
* characters that confuse some dotenv shells, and it round-trips through
|
|
58
|
+
* every adapter that accepts either encoding. The install/setup helpers call
|
|
59
|
+
* this when the key is absent or still a placeholder.
|
|
60
|
+
*/
|
|
61
|
+
export function generateCartethyiaEncryptionKey(): string {
|
|
62
|
+
const bytes = new Uint8Array(32);
|
|
63
|
+
crypto.getRandomValues(bytes);
|
|
64
|
+
let hex = "";
|
|
65
|
+
for (let i = 0; i < bytes.length; i += 1) hex += bytes[i]!.toString(16).padStart(2, "0");
|
|
66
|
+
return hex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the mandatory-only `.env` body derived from `.env.example`.
|
|
71
|
+
*
|
|
72
|
+
* Mandatory means the key is assigned without a leading `#`. Optional entries
|
|
73
|
+
* (leading `#`) carry built-in defaults and must not be copied into a fresh
|
|
74
|
+
* `.env` — they would override the default with a literal `example` value.
|
|
75
|
+
* The generated body therefore contains only mandatory rows, plus a single
|
|
76
|
+
* `CARTETHYIA_ENCRYPTION_KEY` replacement when the caller supplies one.
|
|
77
|
+
*/
|
|
78
|
+
export async function mandatoryEnvBody(
|
|
79
|
+
examplePath: string,
|
|
80
|
+
overrides: Readonly<Record<string, string>> = {},
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
const raw = await readFile(examplePath, "utf8");
|
|
83
|
+
const lines: string[] = [];
|
|
84
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
87
|
+
const sep = line.indexOf("=");
|
|
88
|
+
if (sep <= 0) continue;
|
|
89
|
+
const key = line.slice(0, sep).trim();
|
|
90
|
+
if (!key) continue;
|
|
91
|
+
const value = (overrides as Record<string, string>)[key] ?? line.slice(sep + 1).trim();
|
|
92
|
+
lines.push(`${key}=${value}`);
|
|
93
|
+
}
|
|
94
|
+
// Preserve overrides even when the example row was commented out (legacy case).
|
|
95
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
96
|
+
if (!lines.some((l) => l.startsWith(`${key}=`))) lines.push(`${key}=${value}`);
|
|
97
|
+
}
|
|
98
|
+
return `${lines.join("\n")}\n`;
|
|
99
|
+
}
|