@vantran-se/openchamber-web 1.23.0 → 1.24.3
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/bin/cli.test.js +29 -6
- package/bin/lib/cli-http.js +5 -1
- package/bin/lib/commands-status.js +5 -0
- package/dist/assets/{CodeMirrorCommentWidgets-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
- package/dist/assets/DiffView-COVQhWGd.js +4 -0
- package/dist/assets/FilesView-B1V5JxFX.js +13 -0
- package/dist/assets/GitView-CDOJy5Ay.js +6 -0
- package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
- package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.js} +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
- package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
- package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
- package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
- package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
- package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
- package/dist/assets/PluginPane-B4tKno2h.js +3 -0
- package/dist/assets/SettingsView-DEaMulN9.js +29 -0
- package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
- package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
- package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
- package/dist/assets/de-DgxgFyGG.js +10 -0
- package/dist/assets/es-BFsZj5F_.js +16 -0
- package/dist/assets/fr-jdZ5KKQi.js +14 -0
- package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
- package/dist/assets/index-Cbci4i1W.js +40 -0
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
- package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
- package/dist/assets/ja-C5kKrhO4.js +16 -0
- package/dist/assets/ko-DXDTT_0O.js +16 -0
- package/dist/assets/main-CZALtVek.js +896 -0
- package/dist/assets/main-CgXMBNvG.js +2 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
- package/dist/assets/miniChat-CwTmDixi.js +2 -0
- package/dist/assets/mobile-CLf2vCtO.js +2 -0
- package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
- package/dist/assets/pl-D-7x4Y8Q.js +16 -0
- package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
- package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
- package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
- package/dist/assets/surface-B6n7wJtt.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
- package/dist/assets/tr-Dr-oJ22L.js +7 -0
- package/dist/assets/uk-9Nx83UcI.js +16 -0
- package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
- package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
- package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
- package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
- package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
- package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
- package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
- package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
- package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
- package/dist/assets/vendor-zod-CgAdXWZO.js +39 -0
- package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
- package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
- package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
- package/dist/index.html +6 -4
- package/dist/mini-chat.html +6 -4
- package/dist/mobile.html +6 -4
- package/package.json +12 -9
- package/server/built-in-extensions/registry.json +4 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +47 -23
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
- package/server/lib/agent-tool/runtime.js +62 -15
- package/server/lib/agent-tool/runtime.test.js +88 -2
- package/server/lib/changelog/update-notes.js +2 -1
- package/server/lib/changelog/update-notes.test.js +19 -0
- package/server/lib/event-stream/DOCUMENTATION.md +26 -0
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +292 -0
- package/server/lib/event-stream/global-hub.js +68 -13
- package/server/lib/event-stream/global-hub.test.js +182 -0
- package/server/lib/event-stream/global-ws-bridge.js +16 -18
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +60 -0
- package/server/lib/fs/DOCUMENTATION.md +10 -1
- package/server/lib/fs/git-process.test.js +77 -0
- package/server/lib/fs/routes.js +58 -5
- package/server/lib/fs/routes.test.js +116 -0
- package/server/lib/fs/search.js +2 -1
- package/server/lib/git/DOCUMENTATION.md +30 -6
- package/server/lib/git/routes.js +37 -11
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/serial-refresh.js +93 -0
- package/server/lib/git/serial-refresh.test.js +112 -0
- package/server/lib/git/service.js +588 -74
- package/server/lib/git/service.test.js +492 -0
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/pr-status.js +5 -5
- package/server/lib/github/pr-status.test.js +1 -1
- package/server/lib/github/routes.js +6 -6
- package/server/lib/guests/DOCUMENTATION.md +68 -0
- package/server/lib/guests/auth-store.js +184 -0
- package/server/lib/guests/auth-store.test.js +112 -0
- package/server/lib/guests/background.test.js +83 -0
- package/server/lib/guests/builtins.js +34 -0
- package/server/lib/guests/builtins.test.js +174 -0
- package/server/lib/guests/catalog.js +477 -0
- package/server/lib/guests/catalog.test.js +390 -0
- package/server/lib/guests/clone.js +297 -0
- package/server/lib/guests/extract-zip.js +136 -0
- package/server/lib/guests/extract-zip.test.js +109 -0
- package/server/lib/guests/files.js +308 -0
- package/server/lib/guests/files.test.js +262 -0
- package/server/lib/guests/grant-scope.js +81 -0
- package/server/lib/guests/grant-scope.test.js +75 -0
- package/server/lib/guests/host-session.js +73 -0
- package/server/lib/guests/host-session.test.js +30 -0
- package/server/lib/guests/html-styles.js +6 -0
- package/server/lib/guests/html-styles.test.js +45 -0
- package/server/lib/guests/html-tokens.js +73 -0
- package/server/lib/guests/html-tokens.test.js +53 -0
- package/server/lib/guests/install.js +406 -0
- package/server/lib/guests/install.test.js +338 -0
- package/server/lib/guests/oauth.js +501 -0
- package/server/lib/guests/oauth.test.js +481 -0
- package/server/lib/guests/persist.js +336 -0
- package/server/lib/guests/persist.test.js +288 -0
- package/server/lib/guests/request.js +112 -0
- package/server/lib/guests/request.test.js +206 -0
- package/server/lib/guests/routes.js +838 -0
- package/server/lib/guests/service.js +538 -0
- package/server/lib/guests/service.test.js +277 -0
- package/server/lib/guests/sockets.js +129 -0
- package/server/lib/guests/sockets.test.js +110 -0
- package/server/lib/guests/ssh-install.test.js +188 -0
- package/server/lib/guests/storage.js +68 -0
- package/server/lib/guests/storage.test.js +59 -0
- package/server/lib/guests/updates.js +294 -0
- package/server/lib/guests/updates.test.js +334 -0
- package/server/lib/guests/upload.js +50 -0
- package/server/lib/guests/upload.test.js +131 -0
- package/server/lib/linear/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/DOCUMENTATION.md +17 -1
- package/server/lib/message-queue/runtime.js +34 -1
- package/server/lib/message-queue/runtime.test.js +83 -6
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/notifications/routes.js +5 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
- package/server/lib/openchamber-sessions/routes.js +62 -29
- package/server/lib/openchamber-sessions/routes.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +73 -5
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +30 -1
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +58 -70
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/session-runtime.js +68 -0
- package/server/lib/opencode/session-runtime.test.js +43 -0
- package/server/lib/opencode/settings-helpers.js +11 -0
- package/server/lib/opencode/settings-helpers.test.js +24 -2
- package/server/lib/opencode/settings-normalization-runtime.js +2 -0
- package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
- package/server/lib/opencode/settings-registry.json +18 -0
- package/server/lib/opencode/settings-runtime.js +36 -9
- package/server/lib/opencode/settings-runtime.test.js +158 -2
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/shutdown-runtime.test.js +24 -0
- package/server/lib/opencode/theme-archive.js +53 -0
- package/server/lib/opencode/theme-catalog.js +171 -0
- package/server/lib/opencode/theme-catalog.test.js +125 -0
- package/server/lib/opencode/theme-import.test.js +144 -0
- package/server/lib/opencode/theme-runtime.js +100 -28
- package/server/lib/opencode/theme-runtime.test.js +15 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
- package/server/lib/permission-auto-accept/runtime.js +13 -0
- package/server/lib/permission-auto-accept/runtime.test.js +37 -1
- package/server/lib/project-context/DOCUMENTATION.md +13 -3
- package/server/lib/project-context/runtime.js +8 -2
- package/server/lib/project-context/runtime.test.js +54 -0
- package/server/lib/projects/DOCUMENTATION.md +2 -1
- package/server/lib/projects/project-config.js +40 -11
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +26 -0
- package/server/lib/quota/DOCUMENTATION.md +0 -1
- package/server/lib/quota/providers/index.js +0 -7
- package/server/lib/realtime-proxy-terminal.test.js +150 -0
- package/server/lib/realtime-proxy.js +2 -0
- package/server/lib/relay/DOCUMENTATION.md +20 -0
- package/server/lib/relay/host-client.js +7 -0
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/routing/DOCUMENTATION.md +83 -0
- package/server/lib/routing/defaults.js +98 -0
- package/server/lib/routing/feature-flag.js +11 -0
- package/server/lib/routing/history.js +34 -0
- package/server/lib/routing/jev.js +98 -0
- package/server/lib/routing/routes.http.test.js +100 -0
- package/server/lib/routing/routes.js +81 -0
- package/server/lib/routing/runtime.js +217 -0
- package/server/lib/routing/runtime.test.js +188 -0
- package/server/lib/routing/store.js +249 -0
- package/server/lib/routing/store.test.js +96 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +14 -4
- package/server/lib/scheduled-tasks/runtime.test.js +67 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +111 -257
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/small-model/DOCUMENTATION.md +14 -2
- package/server/lib/small-model/call.js +37 -6
- package/server/lib/small-model/call.test.js +182 -0
- package/server/lib/terminal/DOCUMENTATION.md +23 -2
- package/server/lib/terminal/runtime.js +27 -5
- package/server/lib/terminal/runtime.test.js +67 -0
- package/server/lib/terminal/shutdown.js +106 -0
- package/server/lib/terminal/shutdown.test.js +84 -0
- package/server/lib/tts/service.test.js +78 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
- package/server/lib/ui-auth/session-cookie.js +51 -0
- package/server/lib/ui-auth/session-cookie.test.js +46 -0
- package/server/lib/ui-auth/ui-auth.js +56 -17
- package/server/lib/ui-auth/ui-auth.test.js +96 -0
- package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +13 -2
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
- package/dist/assets/FilesView-BdTOpew9.js +0 -17
- package/dist/assets/GitView-Dqf9vHJh.js +0 -6
- package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
- package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
- package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
- package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
- package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
- package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-C8jV7J9y.js +0 -10
- package/dist/assets/es-BI6J1M5R.js +0 -16
- package/dist/assets/fr-D6oDFt7E.js +0 -14
- package/dist/assets/index-BqhiCmYS.js +0 -39
- package/dist/assets/index-FqSOBz0J.css +0 -1
- package/dist/assets/ja-C_4i5EQu.js +0 -16
- package/dist/assets/ko-BFbOTpbq.js +0 -16
- package/dist/assets/main-C9ETLKFc.js +0 -2
- package/dist/assets/main-CJmb2dF8.js +0 -930
- package/dist/assets/markdownCore-DVuZpnli.js +0 -5
- package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
- package/dist/assets/mobile-BUT6-lo1.js +0 -2
- package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
- package/dist/assets/pl-COFvT56l.js +0 -16
- package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
- package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
- package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
- package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
- package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
- package/dist/assets/surface-BvRzS58U.js +0 -1
- package/dist/assets/tr-B8Q0Go2l.js +0 -7
- package/dist/assets/uk-Dk4a-Fn-.js +0 -16
- package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
- package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
- package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
- package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
- package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
- package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
- package/server/lib/quota/providers/crof.js +0 -96
- package/server/lib/quota/providers/crof.test.js +0 -98
- /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
1
|
+
import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
|
|
2
2
|
import {
|
|
3
3
|
buildPreferencesFields,
|
|
4
4
|
flattenPreferences,
|
|
@@ -288,10 +288,10 @@ export const createSettingsRuntime = (deps) => {
|
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${oldId}.json`);
|
|
292
|
-
const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${newId}.json`);
|
|
293
|
-
const oldStorageDir = path.join(PROJECTS_ROOT_DIR, oldId);
|
|
294
|
-
const newStorageDir = path.join(PROJECTS_ROOT_DIR, newId);
|
|
291
|
+
const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(oldId)}.json`);
|
|
292
|
+
const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(newId)}.json`);
|
|
293
|
+
const oldStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(oldId));
|
|
294
|
+
const newStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(newId));
|
|
295
295
|
|
|
296
296
|
const [oldConfig, newConfig] = await Promise.all([
|
|
297
297
|
readJsonFile(oldConfigPath),
|
|
@@ -308,6 +308,28 @@ export const createSettingsRuntime = (deps) => {
|
|
|
308
308
|
await fsPromises.rm(oldConfigPath, { force: true });
|
|
309
309
|
};
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* A build before the bounded folder name created `<projectId>/` for an id
|
|
313
|
+
* the filesystem still accepted (201 to 255 characters; longer ids never
|
|
314
|
+
* got a folder). Only the bounded folder is read now, so that folder's
|
|
315
|
+
* notes, plans, and memory are moved over once. A raw name the filesystem
|
|
316
|
+
* cannot hold (ENAMETOOLONG) means no such folder ever existed.
|
|
317
|
+
*/
|
|
318
|
+
const migrateRawIdStorageFolder = async (projectId) => {
|
|
319
|
+
const stem = projectConfigFileStemOf(projectId);
|
|
320
|
+
if (stem === projectId) return;
|
|
321
|
+
const rawStorageDir = path.join(PROJECTS_ROOT_DIR, projectId);
|
|
322
|
+
try {
|
|
323
|
+
await fsPromises.stat(rawStorageDir);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENAMETOOLONG') return;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
const boundedStorageDir = path.join(PROJECTS_ROOT_DIR, stem);
|
|
329
|
+
await mergeProjectContextFiles(rawStorageDir, boundedStorageDir);
|
|
330
|
+
await moveDirectoryContents(rawStorageDir, boundedStorageDir);
|
|
331
|
+
};
|
|
332
|
+
|
|
311
333
|
const migrateSettingsToDeterministicProjectIds = async (current) => {
|
|
312
334
|
const settings = current && typeof current === 'object' ? current : {};
|
|
313
335
|
const projects = sanitizeProjects(settings.projects) || [];
|
|
@@ -328,6 +350,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
328
350
|
await migrateProjectScopedStorage({ oldId: project.id, newId: nextId, projectPath: project.path });
|
|
329
351
|
await migrateProjectIconFiles({ oldId: project.id, newId: nextId });
|
|
330
352
|
}
|
|
353
|
+
await migrateRawIdStorageFolder(nextId);
|
|
331
354
|
nextProjects.push({ ...project, id: nextId });
|
|
332
355
|
}
|
|
333
356
|
|
|
@@ -468,19 +491,23 @@ export const createSettingsRuntime = (deps) => {
|
|
|
468
491
|
for (const [projectId, orphansForProject] of matches.entries()) {
|
|
469
492
|
const project = canonicalProjects.find((p) => p.id === projectId);
|
|
470
493
|
if (!project) continue;
|
|
471
|
-
const
|
|
494
|
+
const targetStem = projectConfigFileStemOf(project.id);
|
|
495
|
+
const targetPath = path.join(PROJECTS_ROOT_DIR, `${targetStem}.json`);
|
|
496
|
+
const targetStorageDir = path.join(PROJECTS_ROOT_DIR, targetStem);
|
|
472
497
|
|
|
473
498
|
for (const orphan of orphansForProject) {
|
|
474
499
|
const targetExisting = (await readJsonFile(targetPath)) || {};
|
|
500
|
+
// An orphan is named by the file found on disk, so its folder is the raw name.
|
|
501
|
+
const orphanStorageDir = path.join(PROJECTS_ROOT_DIR, orphan.orphanId);
|
|
475
502
|
const merged = mergeProjectConfigData({
|
|
476
503
|
oldConfig: orphan.content,
|
|
477
504
|
newConfig: targetExisting,
|
|
478
|
-
oldStorageDir:
|
|
479
|
-
newStorageDir:
|
|
505
|
+
oldStorageDir: orphanStorageDir,
|
|
506
|
+
newStorageDir: targetStorageDir,
|
|
480
507
|
projectPath: project.path,
|
|
481
508
|
});
|
|
482
509
|
await writeJsonFile(targetPath, merged);
|
|
483
|
-
await moveDirectoryContents(
|
|
510
|
+
await moveDirectoryContents(orphanStorageDir, targetStorageDir);
|
|
484
511
|
await fsPromises.rm(orphan.filePath, { force: true });
|
|
485
512
|
orphansConsumed.add(orphan.orphanId);
|
|
486
513
|
console.log(`[projects] Recovered orphan ${orphan.orphanId} -> ${project.id} (${project.path})`);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import fsPromises from 'fs/promises';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
6
|
+
import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
|
|
7
7
|
import { createSettingsRuntime } from './settings-runtime.js';
|
|
8
8
|
|
|
9
9
|
const createRuntime = async ({ mergePersistedSettings = (_current, changes) => changes } = {}) => {
|
|
@@ -39,6 +39,20 @@ const createRuntime = async ({ mergePersistedSettings = (_current, changes) => c
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
describe('settings runtime', () => {
|
|
42
|
+
it('round-trips both archived-only retention states through instance settings', async () => {
|
|
43
|
+
const { runtime, settingsFilePath, cleanup } = await createRuntime();
|
|
44
|
+
try {
|
|
45
|
+
for (const sessionRetentionOnlyArchived of [true, false]) {
|
|
46
|
+
const settings = { sessionRetentionOnlyArchived, sessionRetentionAction: 'delete', autoDeleteAfterDays: 30 };
|
|
47
|
+
await runtime.persistSettings(settings);
|
|
48
|
+
expect(await runtime.readSettingsFromDisk()).toEqual(settings);
|
|
49
|
+
expect(JSON.parse(await fsPromises.readFile(settingsFilePath, 'utf8'))).toEqual(settings);
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
await cleanup();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
42
56
|
it('uses OpenChamber themes when a new install has no theme preferences', async () => {
|
|
43
57
|
const { runtime, cleanup } = await createRuntime();
|
|
44
58
|
try {
|
|
@@ -145,6 +159,52 @@ describe('settings runtime', () => {
|
|
|
145
159
|
}
|
|
146
160
|
});
|
|
147
161
|
|
|
162
|
+
it('migrates a legacy id into the bounded file of a project whose path is too long for a file name', async () => {
|
|
163
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
164
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
try {
|
|
166
|
+
const projectPath = path.join(tempRoot, 'a'.repeat(160));
|
|
167
|
+
const oldProjectId = 'legacy-project-id';
|
|
168
|
+
const newProjectId = createProjectIdFromPath(projectPath);
|
|
169
|
+
expect(newProjectId.length).toBeGreaterThan(200);
|
|
170
|
+
const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
|
|
171
|
+
const boundedPath = path.join(projectsRoot, `${projectConfigFileStemOf(newProjectId)}.json`);
|
|
172
|
+
expect(path.basename(boundedPath).startsWith('path_sha256_')).toBe(true);
|
|
173
|
+
|
|
174
|
+
await fsPromises.mkdir(projectPath, { recursive: true });
|
|
175
|
+
await fsPromises.mkdir(projectsRoot, { recursive: true });
|
|
176
|
+
await fsPromises.writeFile(
|
|
177
|
+
settingsFilePath,
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
projects: [{ id: oldProjectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }],
|
|
180
|
+
activeProjectId: oldProjectId,
|
|
181
|
+
}, null, 2),
|
|
182
|
+
'utf8',
|
|
183
|
+
);
|
|
184
|
+
await fsPromises.writeFile(
|
|
185
|
+
path.join(projectsRoot, `${oldProjectId}.json`),
|
|
186
|
+
JSON.stringify({ 'setup-worktree': ['bun install'] }, null, 2),
|
|
187
|
+
'utf8',
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const settings = await runtime.readSettingsFromDiskMigrated();
|
|
191
|
+
|
|
192
|
+
expect(settings.projects[0].id).toBe(newProjectId);
|
|
193
|
+
const migratedConfig = JSON.parse(await fsPromises.readFile(boundedPath, 'utf8'));
|
|
194
|
+
expect(migratedConfig['setup-worktree']).toEqual(['bun install']);
|
|
195
|
+
await expect(fsPromises.readFile(path.join(projectsRoot, `${oldProjectId}.json`), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
|
|
196
|
+
|
|
197
|
+
// A second pass sees the bounded file beside the settings; it is the
|
|
198
|
+
// project's own file, not a stray from the random-id era.
|
|
199
|
+
await runtime.readSettingsFromDiskMigrated();
|
|
200
|
+
expect(JSON.parse(await fsPromises.readFile(boundedPath, 'utf8'))['setup-worktree']).toEqual(['bun install']);
|
|
201
|
+
expect(warn.mock.calls.some((call) => String(call[0]).includes('orphan'))).toBe(false);
|
|
202
|
+
} finally {
|
|
203
|
+
warn.mockRestore();
|
|
204
|
+
await cleanup();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
148
208
|
it.skipIf(process.platform !== 'win32')('falls back when Windows blocks atomic settings replacement', async () => {
|
|
149
209
|
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
|
|
150
210
|
const settingsFilePath = path.join(tempRoot, 'settings.json');
|
|
@@ -184,6 +244,102 @@ describe('settings runtime', () => {
|
|
|
184
244
|
}
|
|
185
245
|
});
|
|
186
246
|
|
|
247
|
+
it('moves project storage into the bounded folder when the canonical id is too long for a folder name', async () => {
|
|
248
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
249
|
+
try {
|
|
250
|
+
const projectPath = path.join(tempRoot, `${'segment-'.repeat(18)}`, 'demo-repo');
|
|
251
|
+
const oldProjectId = 'legacy-project-id';
|
|
252
|
+
const newProjectId = createProjectIdFromPath(projectPath);
|
|
253
|
+
const stem = projectConfigFileStemOf(newProjectId);
|
|
254
|
+
expect(newProjectId.length).toBeGreaterThan(240);
|
|
255
|
+
expect(stem).not.toBe(newProjectId);
|
|
256
|
+
const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
|
|
257
|
+
const oldStorageDir = path.join(projectsRoot, oldProjectId);
|
|
258
|
+
const newStorageDir = path.join(projectsRoot, stem);
|
|
259
|
+
|
|
260
|
+
await fsPromises.mkdir(projectPath, { recursive: true });
|
|
261
|
+
await fsPromises.mkdir(path.join(oldStorageDir, 'plans'), { recursive: true });
|
|
262
|
+
await fsPromises.writeFile(path.join(oldStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [], todos: [{ id: 't1', text: 'keep', completed: false, createdAt: 1 }], plans: [] }), 'utf8');
|
|
263
|
+
await fsPromises.writeFile(path.join(oldStorageDir, 'plans', 'inside.md'), '# Inside\n', 'utf8');
|
|
264
|
+
await fsPromises.writeFile(path.join(oldStorageDir, 'memory.json'), JSON.stringify({ version: 1, entries: [] }), 'utf8');
|
|
265
|
+
await fsPromises.writeFile(
|
|
266
|
+
settingsFilePath,
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
projects: [{ id: oldProjectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }],
|
|
269
|
+
activeProjectId: oldProjectId,
|
|
270
|
+
}, null, 2),
|
|
271
|
+
'utf8',
|
|
272
|
+
);
|
|
273
|
+
await fsPromises.writeFile(
|
|
274
|
+
path.join(projectsRoot, `${oldProjectId}.json`),
|
|
275
|
+
JSON.stringify({ projectPlanFiles: [{ id: 'inside', path: path.join(oldStorageDir, 'plans', 'inside.md') }] }, null, 2),
|
|
276
|
+
'utf8',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const settings = await runtime.readSettingsFromDiskMigrated();
|
|
280
|
+
|
|
281
|
+
expect(settings.projects.map((project) => project.id)).toEqual([newProjectId]);
|
|
282
|
+
const migratedConfig = JSON.parse(await fsPromises.readFile(path.join(projectsRoot, `${stem}.json`), 'utf8'));
|
|
283
|
+
expect(migratedConfig.projectPlanFiles).toEqual([{ id: 'inside', path: path.join(newStorageDir, 'plans', 'inside.md') }]);
|
|
284
|
+
expect(JSON.parse(await fsPromises.readFile(path.join(newStorageDir, 'context.json'), 'utf8')).todos).toHaveLength(1);
|
|
285
|
+
await fsPromises.access(path.join(newStorageDir, 'plans', 'inside.md'));
|
|
286
|
+
await fsPromises.access(path.join(newStorageDir, 'memory.json'));
|
|
287
|
+
expect((await fsPromises.readdir(projectsRoot)).sort()).toEqual([stem, `${stem}.json`]);
|
|
288
|
+
} finally {
|
|
289
|
+
await cleanup();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('moves a folder an older build created under a raw id that still fit a folder name into the bounded folder', async () => {
|
|
294
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
295
|
+
try {
|
|
296
|
+
// Long enough for the bounded name, short enough that an older build
|
|
297
|
+
// could still have created `<id>/` on this filesystem. The temp root
|
|
298
|
+
// differs per OS (a few characters on Linux runners, dozens on macOS),
|
|
299
|
+
// so pad to a fixed path length instead of a fixed segment.
|
|
300
|
+
const projectPathLength = 170;
|
|
301
|
+
const segment = 'x'.repeat(Math.max(1, projectPathLength - path.join(tempRoot, 'demo-repo').length - 1));
|
|
302
|
+
const projectPath = path.join(tempRoot, segment, 'demo-repo');
|
|
303
|
+
const projectId = createProjectIdFromPath(projectPath);
|
|
304
|
+
const stem = projectConfigFileStemOf(projectId);
|
|
305
|
+
expect(projectId.length).toBeGreaterThan(200);
|
|
306
|
+
expect(projectId.length).toBeLessThanOrEqual(255);
|
|
307
|
+
expect(stem).not.toBe(projectId);
|
|
308
|
+
const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects');
|
|
309
|
+
const rawStorageDir = path.join(projectsRoot, projectId);
|
|
310
|
+
const boundedStorageDir = path.join(projectsRoot, stem);
|
|
311
|
+
|
|
312
|
+
await fsPromises.mkdir(projectPath, { recursive: true });
|
|
313
|
+
await fsPromises.mkdir(path.join(rawStorageDir, 'plans'), { recursive: true });
|
|
314
|
+
await fsPromises.writeFile(path.join(rawStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [{ id: 'n1', body: 'old note', createdAt: 1, updatedAt: 1 }], todos: [], plans: [] }), 'utf8');
|
|
315
|
+
await fsPromises.writeFile(path.join(rawStorageDir, 'plans', 'old.md'), '# Old\n', 'utf8');
|
|
316
|
+
await fsPromises.writeFile(path.join(rawStorageDir, 'memory.json'), JSON.stringify({ version: 1, entries: [{ id: 'm1', title: 'Old memory', body: 'x', type: 'fact', createdAt: 1, updatedAt: 1 }] }), 'utf8');
|
|
317
|
+
// The bounded folder already holds newer context: both sides survive.
|
|
318
|
+
await fsPromises.mkdir(boundedStorageDir, { recursive: true });
|
|
319
|
+
await fsPromises.writeFile(path.join(boundedStorageDir, 'context.json'), JSON.stringify({ version: 2, notes: [{ id: 'n2', body: 'new note', createdAt: 2, updatedAt: 2 }], todos: [], plans: [] }), 'utf8');
|
|
320
|
+
await fsPromises.writeFile(
|
|
321
|
+
settingsFilePath,
|
|
322
|
+
JSON.stringify({ projects: [{ id: projectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }], activeProjectId: projectId }, null, 2),
|
|
323
|
+
'utf8',
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const settings = await runtime.readSettingsFromDiskMigrated();
|
|
327
|
+
|
|
328
|
+
expect(settings.projects.map((project) => project.id)).toEqual([projectId]);
|
|
329
|
+
const context = JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'context.json'), 'utf8'));
|
|
330
|
+
expect(context.notes.map((note) => note.id).sort()).toEqual(['n1', 'n2']);
|
|
331
|
+
await fsPromises.access(path.join(boundedStorageDir, 'plans', 'old.md'));
|
|
332
|
+
expect(JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'memory.json'), 'utf8')).entries).toHaveLength(1);
|
|
333
|
+
await expect(fsPromises.access(rawStorageDir)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
334
|
+
|
|
335
|
+
// A second startup finds nothing left to move.
|
|
336
|
+
await runtime.readSettingsFromDiskMigrated();
|
|
337
|
+
expect(JSON.parse(await fsPromises.readFile(path.join(boundedStorageDir, 'context.json'), 'utf8')).notes).toHaveLength(2);
|
|
338
|
+
} finally {
|
|
339
|
+
await cleanup();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
187
343
|
it('cleans up orphaned settings.json.tmp files during startup migration', async () => {
|
|
188
344
|
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
|
189
345
|
try {
|
|
@@ -108,6 +108,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
108
108
|
console.log('HTTP server closed');
|
|
109
109
|
resolve();
|
|
110
110
|
});
|
|
111
|
+
// The backend has stopped. Active SSE/HTTP clients must not keep
|
|
112
|
+
// Desktop waiting for the outer shutdown deadline.
|
|
113
|
+
server.closeAllConnections?.();
|
|
111
114
|
}),
|
|
112
115
|
new Promise((resolve) => {
|
|
113
116
|
closeTimeout = setTimeout(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import http from 'node:http';
|
|
2
3
|
|
|
3
4
|
import { createGracefulShutdownRuntime } from './shutdown-runtime.js';
|
|
4
5
|
|
|
@@ -55,4 +56,27 @@ describe('graceful shutdown runtime', () => {
|
|
|
55
56
|
expect(warnSpy).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
|
|
56
57
|
expect(vi.getTimerCount()).toBe(0);
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
it('closes an active HTTP stream instead of waiting for the shutdown deadline', async () => {
|
|
61
|
+
const server = http.createServer((_request, response) => {
|
|
62
|
+
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
63
|
+
response.write('data: fixture\n\n');
|
|
64
|
+
});
|
|
65
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
66
|
+
const request = http.get(`http://127.0.0.1:${server.address().port}`);
|
|
67
|
+
request.on('error', () => {});
|
|
68
|
+
const response = await new Promise((resolve) => request.once('response', resolve));
|
|
69
|
+
response.resume();
|
|
70
|
+
const closed = new Promise((resolve) => response.once('close', resolve));
|
|
71
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
72
|
+
try {
|
|
73
|
+
await createRuntime(server).gracefulShutdown({ exitProcess: false });
|
|
74
|
+
expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
|
|
75
|
+
await closed;
|
|
76
|
+
} finally {
|
|
77
|
+
request.destroy();
|
|
78
|
+
server.closeAllConnections();
|
|
79
|
+
await new Promise((resolve) => server.close(resolve));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
58
82
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { inflateRaw } from 'node:zlib';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const inflate = promisify(inflateRaw);
|
|
5
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
6
|
+
|
|
7
|
+
// Read only selected JSON entries in memory. Nothing from a VSIX is executed or
|
|
8
|
+
// extracted onto the host filesystem.
|
|
9
|
+
export function openThemeArchive(bytes) {
|
|
10
|
+
let end = bytes.length - 22;
|
|
11
|
+
const lower = Math.max(0, end - 65535);
|
|
12
|
+
while (end >= lower && (bytes.readUInt32LE(end) !== 0x06054b50 || end + 22 + bytes.readUInt16LE(end + 20) !== bytes.length)) end--;
|
|
13
|
+
if (end < lower) throw new Error('Invalid VSIX directory');
|
|
14
|
+
const count = bytes.readUInt16LE(end + 10);
|
|
15
|
+
const directorySize = bytes.readUInt32LE(end + 12);
|
|
16
|
+
let cursor = bytes.readUInt32LE(end + 16);
|
|
17
|
+
if (count > 5000 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6)
|
|
18
|
+
|| bytes.readUInt16LE(end + 8) !== count || cursor + directorySize !== end) throw new Error('Unsupported VSIX directory');
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
let totalSize = 0;
|
|
21
|
+
for (let index = 0; index < count; index++) {
|
|
22
|
+
if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) throw new Error('Invalid VSIX entry');
|
|
23
|
+
const flags = bytes.readUInt16LE(cursor + 8);
|
|
24
|
+
const method = bytes.readUInt16LE(cursor + 10);
|
|
25
|
+
const compressed = bytes.readUInt32LE(cursor + 20);
|
|
26
|
+
const size = bytes.readUInt32LE(cursor + 24);
|
|
27
|
+
const nameLength = bytes.readUInt16LE(cursor + 28);
|
|
28
|
+
const next = cursor + 46 + nameLength + bytes.readUInt16LE(cursor + 30) + bytes.readUInt16LE(cursor + 32);
|
|
29
|
+
const offset = bytes.readUInt32LE(cursor + 42);
|
|
30
|
+
if (next > end || nameLength > 1024 || flags & 1 || ![0, 8].includes(method)) throw new Error('Unsupported VSIX entry');
|
|
31
|
+
const name = bytes.toString('utf8', cursor + 46, cursor + 46 + nameLength);
|
|
32
|
+
const parts = name.replace(/\/$/, '').split('/');
|
|
33
|
+
if (parts.some((part) => !part || part === '.' || part === '..') || /[\\\0:]/.test(name) || entries.has(name)) throw new Error('Unsafe VSIX path');
|
|
34
|
+
totalSize += size;
|
|
35
|
+
if (totalSize > 100 * 1024 * 1024) throw new Error('VSIX contents too large');
|
|
36
|
+
entries.set(name, { offset, compressed, size, method });
|
|
37
|
+
cursor = next;
|
|
38
|
+
}
|
|
39
|
+
if (cursor !== end) throw new Error('Invalid VSIX directory size');
|
|
40
|
+
return async (name) => {
|
|
41
|
+
const entry = entries.get(name);
|
|
42
|
+
if (!entry || entry.size > MAX_FILE_BYTES) throw new Error('Theme file missing or too large');
|
|
43
|
+
const { offset, compressed, size, method } = entry;
|
|
44
|
+
if (offset + 30 > bytes.length || bytes.readUInt32LE(offset) !== 0x04034b50 || bytes.readUInt16LE(offset + 8) !== method) throw new Error('Invalid VSIX file');
|
|
45
|
+
const nameLength = bytes.readUInt16LE(offset + 26);
|
|
46
|
+
const start = offset + 30 + nameLength + bytes.readUInt16LE(offset + 28);
|
|
47
|
+
if (start + compressed > bytes.length || bytes.toString('utf8', offset + 30, offset + 30 + nameLength) !== name) throw new Error('Invalid VSIX file bounds');
|
|
48
|
+
const data = bytes.subarray(start, start + compressed);
|
|
49
|
+
const result = method === 0 ? data : await inflate(data, { maxOutputLength: MAX_FILE_BYTES });
|
|
50
|
+
if (result.length !== size) throw new Error('Invalid VSIX file size');
|
|
51
|
+
return result.toString('utf8');
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { parse } from 'jsonc-parser';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { openThemeArchive } from './theme-archive.js';
|
|
6
|
+
|
|
7
|
+
const identifier = z.string().min(1).max(160).regex(/^[\w-]+$/);
|
|
8
|
+
const version = z.string().min(1).max(80).regex(/^[\w.+-]+$/);
|
|
9
|
+
const settings = z.object({ foreground: z.string().optional() });
|
|
10
|
+
const tokenRules = z.array(z.object({ scope: z.union([z.string(), z.array(z.string())]).optional(), settings }));
|
|
11
|
+
const sourceSchema = z.object({
|
|
12
|
+
name: z.string().optional(), type: z.string().optional(), include: z.string().optional(),
|
|
13
|
+
colors: z.record(z.string(), z.string().nullable()).default({}),
|
|
14
|
+
tokenColors: z.union([z.string(), tokenRules]).optional(),
|
|
15
|
+
semanticHighlighting: z.boolean().optional(),
|
|
16
|
+
semanticTokenColors: z.record(z.string(), z.union([z.string(), settings])).optional(),
|
|
17
|
+
});
|
|
18
|
+
const extensionSchema = z.object({
|
|
19
|
+
namespace: identifier, name: identifier, version,
|
|
20
|
+
displayName: z.string().optional(), files: z.object({ icon: z.string().optional() }),
|
|
21
|
+
});
|
|
22
|
+
const detailSchema = extensionSchema.extend({ files: z.object({ download: z.string(), sha256: z.string() }) });
|
|
23
|
+
const manifestSchema = z.object({
|
|
24
|
+
publisher: identifier, name: identifier, version,
|
|
25
|
+
contributes: z.object({ themes: z.array(z.object({
|
|
26
|
+
label: z.string().min(1).max(160), path: z.string().min(1).max(1024),
|
|
27
|
+
uiTheme: z.enum(['vs', 'vs-dark', 'hc-black', 'hc-light']).optional(),
|
|
28
|
+
})).min(1).max(40) }),
|
|
29
|
+
});
|
|
30
|
+
const querySchema = z.object({ query: z.string().trim().min(1).max(160) });
|
|
31
|
+
const packageSchema = z.object({ namespace: identifier, name: identifier, version });
|
|
32
|
+
const hosts = new Set(['open-vsx.org', 'openvsx.eclipsecontent.org']);
|
|
33
|
+
|
|
34
|
+
function trustedUrl(value) {
|
|
35
|
+
const url = new URL(value);
|
|
36
|
+
if (url.protocol !== 'https:' || !hosts.has(url.hostname) || url.port || url.username || url.password) throw new Error('Unexpected catalog host');
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function download(url, maxBytes, signal, fetchImpl) {
|
|
41
|
+
for (let redirects = 0; redirects < 4; redirects++) {
|
|
42
|
+
const response = await fetchImpl(trustedUrl(url), { signal, redirect: 'manual' });
|
|
43
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
44
|
+
await response.body?.cancel();
|
|
45
|
+
url = new URL(response.headers.get('location') ?? '', url).href;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok || !response.body || Number(response.headers.get('content-length')) > maxBytes) {
|
|
49
|
+
await response.body?.cancel();
|
|
50
|
+
throw new Error('Catalog request failed');
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let length = 0;
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) break;
|
|
59
|
+
length += value.byteLength;
|
|
60
|
+
if (length > maxBytes) throw new Error('Catalog response too large');
|
|
61
|
+
chunks.push(value);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
await reader.cancel();
|
|
65
|
+
reader.releaseLock();
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks, length);
|
|
68
|
+
}
|
|
69
|
+
throw new Error('Too many catalog redirects');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseJsonc(text, schema) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
75
|
+
if (errors.length) throw new Error('Invalid theme JSON');
|
|
76
|
+
return schema.parse(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function themePath(from, reference) {
|
|
80
|
+
if (/[\\\0:]/.test(reference) || reference.startsWith('/')) throw new Error('Invalid theme reference');
|
|
81
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(from), reference));
|
|
82
|
+
if (!resolved.startsWith('extension/')) throw new Error('Theme reference escapes package');
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createThemeCatalog({ fetchImpl = fetch } = {}) {
|
|
87
|
+
const search = async (input, signal = AbortSignal.timeout(15000)) => {
|
|
88
|
+
const { query } = querySchema.parse(input);
|
|
89
|
+
const url = new URL('https://open-vsx.org/api/-/search');
|
|
90
|
+
url.search = new URLSearchParams({ query, category: 'Themes', size: '24', sortBy: 'relevance' }).toString();
|
|
91
|
+
const bytes = await download(url.href, 1024 * 1024, signal, fetchImpl);
|
|
92
|
+
const result = z.object({ extensions: z.array(extensionSchema).max(24) }).parse(JSON.parse(bytes.toString('utf8')));
|
|
93
|
+
return result.extensions.map((extension) => ({
|
|
94
|
+
namespace: extension.namespace, name: extension.name, version: extension.version,
|
|
95
|
+
label: extension.displayName || extension.name,
|
|
96
|
+
icon: extension.files.icon && URL.canParse(extension.files.icon) && hosts.has(new URL(extension.files.icon).hostname)
|
|
97
|
+
? trustedUrl(extension.files.icon).href : null,
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const readPackage = async (input, signal = AbortSignal.timeout(45000)) => {
|
|
102
|
+
const requested = packageSchema.parse(input);
|
|
103
|
+
const url = `https://open-vsx.org/api/${requested.namespace}/${requested.name}/${requested.version}`;
|
|
104
|
+
const detail = detailSchema.parse(JSON.parse((await download(url, 512 * 1024, signal, fetchImpl)).toString('utf8')));
|
|
105
|
+
const bytes = await download(detail.files.download, 20 * 1024 * 1024, signal, fetchImpl);
|
|
106
|
+
const checksum = (await download(detail.files.sha256, 256, signal, fetchImpl)).toString('utf8').trim().split(/\s+/)[0];
|
|
107
|
+
if (createHash('sha256').update(bytes).digest('hex') !== checksum?.toLowerCase()) throw new Error('VSIX checksum mismatch');
|
|
108
|
+
const read = openThemeArchive(bytes);
|
|
109
|
+
const manifest = parseJsonc(await read('extension/package.json'), manifestSchema);
|
|
110
|
+
if (manifest.publisher.toLowerCase() !== requested.namespace.toLowerCase() || manifest.name !== requested.name || manifest.version !== requested.version) throw new Error('VSIX identity mismatch');
|
|
111
|
+
let reads = 0;
|
|
112
|
+
const load = async (file, ancestors = []) => {
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
if (ancestors.includes(file) || ancestors.length >= 8 || ++reads > 320) throw new Error('Too many theme references');
|
|
115
|
+
const source = parseJsonc(await read(file), sourceSchema);
|
|
116
|
+
const parent = source.include ? await load(themePath(file, source.include), [...ancestors, file]) : null;
|
|
117
|
+
const reference = z.string().safeParse(source.tokenColors);
|
|
118
|
+
const rules = reference.success
|
|
119
|
+
? parseJsonc(await read(themePath(file, reference.data)), tokenRules) : tokenRules.parse(source.tokenColors ?? []);
|
|
120
|
+
const { include: _include, ...own } = source;
|
|
121
|
+
return {
|
|
122
|
+
...parent, ...own,
|
|
123
|
+
colors: { ...parent?.colors, ...source.colors },
|
|
124
|
+
tokenColors: [...(parent?.tokenColors ?? []), ...(rules ?? [])],
|
|
125
|
+
semanticTokenColors: { ...parent?.semanticTokenColors, ...source.semanticTokenColors },
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const themes = [];
|
|
129
|
+
let totalBytes = 0;
|
|
130
|
+
for (const contribution of manifest.contributes.themes) {
|
|
131
|
+
try {
|
|
132
|
+
const file = themePath('extension/package.json', contribution.path);
|
|
133
|
+
const source = await load(file);
|
|
134
|
+
source.name = contribution.label;
|
|
135
|
+
if (contribution.uiTheme) source.type = contribution.uiTheme === 'vs' ? 'light' : contribution.uiTheme === 'vs-dark' ? 'dark' : contribution.uiTheme;
|
|
136
|
+
const text = JSON.stringify(source);
|
|
137
|
+
totalBytes += Buffer.byteLength(text);
|
|
138
|
+
if (Buffer.byteLength(text) > 512 * 1024 || totalBytes > 10 * 1024 * 1024) throw new Error('Resolved themes too large');
|
|
139
|
+
themes.push({ path: contribution.path, name: contribution.label, text, error: false });
|
|
140
|
+
} catch {
|
|
141
|
+
signal.throwIfAborted();
|
|
142
|
+
themes.push({ path: contribution.path, name: contribution.label, text: '', error: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return themes;
|
|
146
|
+
};
|
|
147
|
+
return { search, readPackage };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function registerThemeCatalogRoutes(app, catalog = createThemeCatalog()) {
|
|
151
|
+
for (const [route, operation, schema] of [
|
|
152
|
+
['/api/config/themes/catalog/search', catalog.search, querySchema],
|
|
153
|
+
['/api/config/themes/catalog/package', catalog.readPackage, packageSchema],
|
|
154
|
+
]) {
|
|
155
|
+
app.post(route, async (req, res) => {
|
|
156
|
+
const input = schema.safeParse(req.body);
|
|
157
|
+
if (!input.success) return res.status(400).json({ error: 'invalid' });
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const cancel = () => controller.abort();
|
|
160
|
+
res.on('close', cancel);
|
|
161
|
+
try {
|
|
162
|
+
const items = await operation(input.data, AbortSignal.any([controller.signal, AbortSignal.timeout(45000)]));
|
|
163
|
+
res.json({ items });
|
|
164
|
+
} catch {
|
|
165
|
+
if (!res.destroyed) res.status(502).json({ error: 'catalog' });
|
|
166
|
+
} finally {
|
|
167
|
+
res.off('close', cancel);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|