@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { deflateRawSync } from 'node:zlib';
|
|
4
|
+
import { createThemeCatalog, registerThemeCatalogRoutes } from './theme-catalog.js';
|
|
5
|
+
import { registerCommonRequestMiddleware } from './core-routes.js';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import request from 'supertest';
|
|
8
|
+
import { openThemeArchive } from './theme-archive.js';
|
|
9
|
+
|
|
10
|
+
function archive(files) {
|
|
11
|
+
const locals = [];
|
|
12
|
+
const directory = [];
|
|
13
|
+
let offset = 0;
|
|
14
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
15
|
+
const name = Buffer.from(filename);
|
|
16
|
+
const data = Buffer.from(content);
|
|
17
|
+
const compressed = deflateRawSync(data);
|
|
18
|
+
const header = Buffer.alloc(30);
|
|
19
|
+
header.writeUInt32LE(0x04034b50);
|
|
20
|
+
header.writeUInt16LE(8, 8);
|
|
21
|
+
header.writeUInt32LE(compressed.length, 18);
|
|
22
|
+
header.writeUInt32LE(data.length, 22);
|
|
23
|
+
header.writeUInt16LE(name.length, 26);
|
|
24
|
+
locals.push(header, name, compressed);
|
|
25
|
+
const central = Buffer.alloc(46);
|
|
26
|
+
central.writeUInt32LE(0x02014b50);
|
|
27
|
+
central.writeUInt16LE(8, 10);
|
|
28
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
29
|
+
central.writeUInt32LE(data.length, 24);
|
|
30
|
+
central.writeUInt16LE(name.length, 28);
|
|
31
|
+
central.writeUInt32LE(offset, 42);
|
|
32
|
+
directory.push(central, name);
|
|
33
|
+
offset += header.length + name.length + compressed.length;
|
|
34
|
+
}
|
|
35
|
+
const end = Buffer.alloc(22);
|
|
36
|
+
end.writeUInt32LE(0x06054b50);
|
|
37
|
+
end.writeUInt16LE(directory.length / 2, 8);
|
|
38
|
+
end.writeUInt16LE(directory.length / 2, 10);
|
|
39
|
+
end.writeUInt32LE(Buffer.concat(directory).length, 12);
|
|
40
|
+
end.writeUInt32LE(offset, 16);
|
|
41
|
+
return Buffer.concat([...locals, ...directory, end]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const identity = { namespace: 'Author', name: 'fixture', version: '1.0.0' };
|
|
45
|
+
const manifest = { publisher: 'Author', name: 'fixture', version: '1.0.0', contributes: { themes: [
|
|
46
|
+
{ label: 'Readable Night', path: './themes/night.json', uiTheme: 'vs-dark' },
|
|
47
|
+
{ label: 'Broken sibling', path: '../../outside.json', uiTheme: 'vs' },
|
|
48
|
+
] } };
|
|
49
|
+
const packageFiles = {
|
|
50
|
+
'extension/package.json': JSON.stringify(manifest),
|
|
51
|
+
'extension/base.json': '{"colors":{"editor.background":"#111111","editor.foreground":"#eeeeee"},"tokenColors":[{"scope":"string","settings":{"foreground":"#aaaaaa"}}]}',
|
|
52
|
+
'extension/themes/night.json': '{ // JSONC\n"include":"../base.json", "colors":{"editor.foreground":"#ffffff"},"tokenColors":"./tokens.json",}',
|
|
53
|
+
'extension/themes/tokens.json': '[{"scope":"keyword","settings":{"foreground":"#aabbcc"}}]',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function transport(bytes, overrides = {}) {
|
|
57
|
+
const responses = new Map([
|
|
58
|
+
['https://open-vsx.org/api/Author/fixture/1.0.0', JSON.stringify({ ...identity, files: { download: 'https://open-vsx.org/package.vsix', sha256: 'https://open-vsx.org/package.sha256' } })],
|
|
59
|
+
['https://open-vsx.org/package.vsix', bytes],
|
|
60
|
+
['https://open-vsx.org/package.sha256', createHash('sha256').update(bytes).digest('hex')],
|
|
61
|
+
...Object.entries(overrides),
|
|
62
|
+
]);
|
|
63
|
+
return async (url) => {
|
|
64
|
+
const body = responses.get(url.href);
|
|
65
|
+
if (body === undefined) throw new Error(`Unexpected request ${url}`);
|
|
66
|
+
return new Response(body);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('Open VSX theme catalog', () => {
|
|
71
|
+
test('catalog requests retain their bodies through the production middleware', async () => {
|
|
72
|
+
const app = express();
|
|
73
|
+
registerCommonRequestMiddleware(app, { express });
|
|
74
|
+
const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
|
|
75
|
+
registerThemeCatalogRoutes(app, {
|
|
76
|
+
search: async (input) => { expect(input).toEqual({ query: 'nord' }); return []; },
|
|
77
|
+
readPackage: catalog.readPackage,
|
|
78
|
+
});
|
|
79
|
+
await request(app).post('/api/config/themes/catalog/search').send({ query: 'nord' }).expect(200);
|
|
80
|
+
const response = await request(app).post('/api/config/themes/catalog/package').send(identity).expect(200);
|
|
81
|
+
expect(response.body.items[0].name).toBe('Readable Night');
|
|
82
|
+
});
|
|
83
|
+
test('resolves JSONC includes and external token files, preserving healthy siblings and manifest labels', async () => {
|
|
84
|
+
const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
|
|
85
|
+
const result = await catalog.readPackage(identity);
|
|
86
|
+
expect(result[0].error).toBe(false);
|
|
87
|
+
const source = JSON.parse(result[0].text);
|
|
88
|
+
expect(source.name).toBe('Readable Night');
|
|
89
|
+
expect(source.type).toBe('dark');
|
|
90
|
+
expect(source.colors).toEqual({ 'editor.background': '#111111', 'editor.foreground': '#ffffff' });
|
|
91
|
+
expect(source.tokenColors.map((rule) => rule.scope)).toEqual(['string', 'keyword']);
|
|
92
|
+
expect(source.include).toBeUndefined();
|
|
93
|
+
expect(result[1].error).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('rejects checksum and package identity mismatches', async () => {
|
|
97
|
+
const bytes = archive(packageFiles);
|
|
98
|
+
await expect(createThemeCatalog({ fetchImpl: transport(bytes, { 'https://open-vsx.org/package.sha256': '0'.repeat(64) }) }).readPackage(identity)).rejects.toThrow('checksum');
|
|
99
|
+
const wrong = archive({ ...packageFiles, 'extension/package.json': JSON.stringify({ ...manifest, publisher: 'Impostor' }) });
|
|
100
|
+
await expect(createThemeCatalog({ fetchImpl: transport(wrong) }).readPackage(identity)).rejects.toThrow('identity');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('limits decompression and rejects traversal and cyclic includes', async () => {
|
|
104
|
+
expect(() => openThemeArchive(archive({ '../outside.json': '{}' }))).toThrow('Unsafe');
|
|
105
|
+
const read = openThemeArchive(archive({ 'extension/huge.json': ' '.repeat(512 * 1024 + 1) }));
|
|
106
|
+
await expect(read('extension/huge.json')).rejects.toThrow('large');
|
|
107
|
+
const cyclic = archive({ ...packageFiles, 'extension/base.json': '{"include":"./themes/night.json"}' });
|
|
108
|
+
expect((await createThemeCatalog({ fetchImpl: transport(cyclic) }).readPackage(identity))[0].error).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('searches only the theme category and rejects redirects outside the catalog', async () => {
|
|
112
|
+
const catalog = createThemeCatalog({ fetchImpl: async (url, options) => {
|
|
113
|
+
expect(url.searchParams.get('category')).toBe('Themes');
|
|
114
|
+
expect(url.searchParams.get('query')).toBe('Dune');
|
|
115
|
+
expect(options.redirect).toBe('manual');
|
|
116
|
+
return Response.json({ extensions: [{ ...identity, displayName: 'Dune', files: {} }] });
|
|
117
|
+
} });
|
|
118
|
+
expect((await catalog.search({ query: 'Dune' }))[0].label).toBe('Dune');
|
|
119
|
+
let calls = 0;
|
|
120
|
+
const hostile = createThemeCatalog({ fetchImpl: async () => { calls++; return new Response(null, { status: 302, headers: { location: 'http://127.0.0.1/private' } }); } });
|
|
121
|
+
await expect(hostile.search({ query: 'Dune' })).rejects.toThrow('host');
|
|
122
|
+
expect(calls).toBe(1);
|
|
123
|
+
await expect(catalog.readPackage({ ...identity, name: '../secret' })).rejects.toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import request from 'supertest';
|
|
7
|
+
import { createThemeRuntime } from './theme-runtime.js';
|
|
8
|
+
import { registerSettingsUtilityRoutes, registerCommonRequestMiddleware } from './core-routes.js';
|
|
9
|
+
|
|
10
|
+
const directories = [];
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await Promise.all(directories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const definition = {
|
|
16
|
+
metadata: { id: '../../escape', name: 'Imported fixture', variant: 'dark' },
|
|
17
|
+
colors: {
|
|
18
|
+
primary: { base: '#ffcc66' },
|
|
19
|
+
surface: { background: '#111111', foreground: '#eeeeee', muted: '#181818', mutedForeground: '#999999', elevated: '#222222' },
|
|
20
|
+
interactive: { border: '#333333' },
|
|
21
|
+
status: { error: '#ff6666', warning: '#ffcc66', success: '#66cc66', info: '#66aaff' },
|
|
22
|
+
syntax: { base: { comment: '#888888', keyword: '#aa88ff', string: '#88cc66', number: '#ffcc66', function: '#66aaff', variable: '#eeeeee', type: '#66dddd', operator: '#ff6677' } },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function setup(overrides = {}) {
|
|
27
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-theme-import-'));
|
|
28
|
+
directories.push(root);
|
|
29
|
+
const themesDir = path.join(root, 'themes');
|
|
30
|
+
const runtime = createThemeRuntime({ fsPromises: { ...fs, ...overrides }, path, themesDir, maxThemeJsonBytes: 512 * 1024, logger: console });
|
|
31
|
+
const app = express();
|
|
32
|
+
registerCommonRequestMiddleware(app, { express });
|
|
33
|
+
registerSettingsUtilityRoutes(app, runtime);
|
|
34
|
+
return { root, themesDir, runtime, app };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('theme import persistence', () => {
|
|
38
|
+
it('deletes only imported files, leaves siblings intact and allows an identical reimport', async () => {
|
|
39
|
+
const { app, runtime, themesDir } = await setup();
|
|
40
|
+
const theme = await runtime.saveImportedTheme(definition);
|
|
41
|
+
await fs.writeFile(path.join(themesDir, 'custom.json'), JSON.stringify(definition));
|
|
42
|
+
await request(app).delete('/api/config/themes/openchamber-dark').expect(200);
|
|
43
|
+
await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
|
|
44
|
+
expect(await fs.readdir(themesDir)).toEqual(['custom.json']);
|
|
45
|
+
await request(app).delete(`/api/config/themes/${theme.metadata.id}`).expect(200);
|
|
46
|
+
expect(await runtime.saveImportedTheme(definition)).toEqual(theme);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not follow a symlink during deletion or report deletion failure as success', async () => {
|
|
50
|
+
const { app, root, themesDir } = await setup();
|
|
51
|
+
await fs.mkdir(themesDir);
|
|
52
|
+
const outside = path.join(root, 'outside.json');
|
|
53
|
+
await fs.writeFile(outside, 'keep');
|
|
54
|
+
const id = `imported-vscode-${'a'.repeat(24)}`;
|
|
55
|
+
await fs.symlink(outside, path.join(themesDir, `${id}.json`));
|
|
56
|
+
await request(app).delete(`/api/config/themes/${id}`).expect(200);
|
|
57
|
+
expect(await fs.readFile(outside, 'utf8')).toBe('keep');
|
|
58
|
+
const denied = await setup({ unlink: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
|
|
59
|
+
await fs.mkdir(denied.themesDir);
|
|
60
|
+
await fs.writeFile(path.join(denied.themesDir, `${id}.json`), JSON.stringify({ ...definition, metadata: { ...definition.metadata, id } }));
|
|
61
|
+
await request(denied.app).delete(`/api/config/themes/${id}`).expect(500);
|
|
62
|
+
expect(await fs.readdir(denied.themesDir)).toEqual([`${id}.json`]);
|
|
63
|
+
});
|
|
64
|
+
it('deletes a hand-added theme by its metadata ID, not by an assumed filename', async () => {
|
|
65
|
+
const { app, themesDir } = await setup();
|
|
66
|
+
await fs.mkdir(themesDir);
|
|
67
|
+
const custom = { ...definition, metadata: { ...definition.metadata, id: 't3-code-dark', name: 'T3 Code' } };
|
|
68
|
+
await fs.writeFile(path.join(themesDir, 'my-custom-palette.json'), JSON.stringify(custom));
|
|
69
|
+
const loaded = await request(app).get('/api/config/themes').expect(200);
|
|
70
|
+
expect(loaded.body.themes[0].metadata.id).toBe('t3-code-dark');
|
|
71
|
+
await request(app).delete('/api/config/themes/t3-code-dark').expect(200);
|
|
72
|
+
expect(await fs.readdir(themesDir)).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('refuses ambiguous duplicate IDs and does not turn an ID into a filesystem path', async () => {
|
|
76
|
+
const { app, themesDir, root } = await setup();
|
|
77
|
+
await fs.mkdir(themesDir);
|
|
78
|
+
const custom = { ...definition, metadata: { ...definition.metadata, id: 'duplicate' } };
|
|
79
|
+
for (const name of ['one.json', 'two.json']) await fs.writeFile(path.join(themesDir, name), JSON.stringify(custom));
|
|
80
|
+
await request(app).delete('/api/config/themes/duplicate').expect(409);
|
|
81
|
+
expect((await fs.readdir(themesDir)).sort()).toEqual(['one.json', 'two.json']);
|
|
82
|
+
await fs.writeFile(path.join(root, 'outside.json'), JSON.stringify(custom));
|
|
83
|
+
await request(app).delete('/api/config/themes/..%2Foutside').expect(200);
|
|
84
|
+
expect(await fs.readFile(path.join(root, 'outside.json'), 'utf8')).toBe(JSON.stringify(custom));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('allows deleting a readable theme beside an unreadable sibling, but does not treat failed reads as absence', async () => {
|
|
88
|
+
const { runtime, themesDir } = await setup({ readFile: async (file, ...args) => {
|
|
89
|
+
if (path.basename(file) === 'unreadable.json') throw Object.assign(new Error('denied'), { code: 'EACCES' });
|
|
90
|
+
return fs.readFile(file, ...args);
|
|
91
|
+
} });
|
|
92
|
+
const theme = await runtime.saveImportedTheme(definition);
|
|
93
|
+
await fs.writeFile(path.join(themesDir, 'unreadable.json'), JSON.stringify(definition));
|
|
94
|
+
await runtime.deleteImportedTheme(theme.metadata.id);
|
|
95
|
+
expect(await fs.readdir(themesDir)).toEqual(['unreadable.json']);
|
|
96
|
+
await expect(runtime.deleteImportedTheme('possibly-unreadable')).rejects.toMatchObject({ code: 'EACCES' });
|
|
97
|
+
});
|
|
98
|
+
it('saves through the API under a server-owned ID and returns it through reload', async () => {
|
|
99
|
+
const { app, themesDir, root } = await setup();
|
|
100
|
+
const response = await request(app).post('/api/config/themes').send({ theme: definition }).expect(201);
|
|
101
|
+
expect(response.body.theme.metadata.id).toMatch(/^imported-vscode-[a-f0-9]{24}$/);
|
|
102
|
+
expect(await fs.readdir(root)).toEqual(['themes']);
|
|
103
|
+
expect(await fs.readdir(themesDir)).toEqual([`${response.body.theme.metadata.id}.json`]);
|
|
104
|
+
const loaded = await request(app).get('/api/config/themes').expect(200);
|
|
105
|
+
expect(loaded.body.themes).toEqual([response.body.theme]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('deduplicates simultaneous imports and never overwrites a subsequently edited theme', async () => {
|
|
109
|
+
const { runtime, themesDir } = await setup();
|
|
110
|
+
const [first, second] = await Promise.all([runtime.saveImportedTheme(definition), runtime.saveImportedTheme(definition)]);
|
|
111
|
+
expect(first).toEqual(second);
|
|
112
|
+
expect((await fs.readdir(themesDir)).length).toBe(1);
|
|
113
|
+
const filename = path.join(themesDir, `${first.metadata.id}.json`);
|
|
114
|
+
await fs.writeFile(filename, 'manually edited');
|
|
115
|
+
await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'conflict', status: 409 });
|
|
116
|
+
expect(await fs.readFile(filename, 'utf8')).toBe('manually edited');
|
|
117
|
+
expect(await fs.readdir(themesDir)).toEqual([`${first.metadata.id}.json`]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('rejects malformed definitions and unsafe token names without creating files', async () => {
|
|
121
|
+
const { app, root } = await setup();
|
|
122
|
+
await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: {} } }).expect(400);
|
|
123
|
+
await request(app).post('/api/config/themes').send({ theme: { ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens: { 'x;body': '#000000' } } } } }).expect(400);
|
|
124
|
+
expect(await fs.readdir(root)).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('cleans temporary files after a publication failure', async () => {
|
|
128
|
+
const { runtime, themesDir } = await setup({ link: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
|
|
129
|
+
await expect(runtime.saveImportedTheme(definition)).rejects.toMatchObject({ code: 'EACCES' });
|
|
130
|
+
expect(await fs.readdir(themesDir)).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not report a directory read failure as an empty theme library', async () => {
|
|
134
|
+
const { runtime } = await setup({ readdir: async () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }); } });
|
|
135
|
+
await expect(runtime.readCustomThemesFromDisk()).rejects.toMatchObject({ code: 'EACCES' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('rejects oversized imports without touching the themes directory', async () => {
|
|
139
|
+
const { runtime, root } = await setup();
|
|
140
|
+
const tokens = Object.fromEntries(Array.from({ length: 30000 }, (_, index) => [`token${index}`, '#123456']));
|
|
141
|
+
await expect(runtime.saveImportedTheme({ ...definition, colors: { ...definition.colors, syntax: { ...definition.colors.syntax, tokens } } })).rejects.toMatchObject({ code: 'size', status: 413 });
|
|
142
|
+
expect(await fs.readdir(root)).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const color = z.string().regex(/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i);
|
|
5
|
+
const role = z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/).max(64);
|
|
6
|
+
const importedThemeSchema = z.object({
|
|
7
|
+
metadata: z.object({
|
|
8
|
+
name: z.string().trim().min(1).max(160),
|
|
9
|
+
author: z.string().max(160).optional(),
|
|
10
|
+
variant: z.enum(['light', 'dark']),
|
|
11
|
+
description: z.string().max(1024).default(''),
|
|
12
|
+
version: z.string().max(40).default('1.0.0'),
|
|
13
|
+
tags: z.array(z.string().max(40)).max(16).default([]),
|
|
14
|
+
}),
|
|
15
|
+
colors: z.record(role, z.record(role, z.union([color, z.record(role, color)]))),
|
|
16
|
+
});
|
|
17
|
+
const fileErrorSchema = z.object({ code: z.string() });
|
|
18
|
+
|
|
19
|
+
export class ThemeImportStorageError extends Error {
|
|
20
|
+
constructor(code, status) {
|
|
21
|
+
super(code);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.status = status;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
1
27
|
export const createThemeRuntime = (dependencies) => {
|
|
2
28
|
const {
|
|
3
29
|
fsPromises,
|
|
@@ -34,46 +60,24 @@ export const createThemeRuntime = (dependencies) => {
|
|
|
34
60
|
const status = colors.status;
|
|
35
61
|
const syntax = colors.syntax;
|
|
36
62
|
const syntaxBase = syntax && typeof syntax === 'object' ? syntax.base : null;
|
|
37
|
-
const syntaxHighlights = syntax && typeof syntax === 'object' ? syntax.highlights : null;
|
|
38
63
|
|
|
39
|
-
if (!primary || !surface || !interactive || !status || !syntaxBase
|
|
64
|
+
if (!primary || !surface || !interactive || !status || !syntaxBase) {
|
|
40
65
|
return null;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
//
|
|
68
|
+
// Authored inputs only. The UI resolves optional roles before rendering.
|
|
44
69
|
const required = [
|
|
45
70
|
primary.base,
|
|
46
|
-
primary.foreground,
|
|
47
71
|
surface.background,
|
|
48
72
|
surface.foreground,
|
|
49
73
|
surface.muted,
|
|
50
74
|
surface.mutedForeground,
|
|
51
75
|
surface.elevated,
|
|
52
|
-
surface.elevatedForeground,
|
|
53
|
-
surface.subtle,
|
|
54
76
|
interactive.border,
|
|
55
|
-
interactive.selection,
|
|
56
|
-
interactive.selectionForeground,
|
|
57
|
-
interactive.focusRing,
|
|
58
|
-
interactive.hover,
|
|
59
77
|
status.error,
|
|
60
|
-
status.errorForeground,
|
|
61
|
-
status.errorBackground,
|
|
62
|
-
status.errorBorder,
|
|
63
78
|
status.warning,
|
|
64
|
-
status.warningForeground,
|
|
65
|
-
status.warningBackground,
|
|
66
|
-
status.warningBorder,
|
|
67
79
|
status.success,
|
|
68
|
-
status.successForeground,
|
|
69
|
-
status.successBackground,
|
|
70
|
-
status.successBorder,
|
|
71
80
|
status.info,
|
|
72
|
-
status.infoForeground,
|
|
73
|
-
status.infoBackground,
|
|
74
|
-
status.infoBorder,
|
|
75
|
-
syntaxBase.background,
|
|
76
|
-
syntaxBase.foreground,
|
|
77
81
|
syntaxBase.keyword,
|
|
78
82
|
syntaxBase.string,
|
|
79
83
|
syntaxBase.number,
|
|
@@ -82,9 +86,6 @@ export const createThemeRuntime = (dependencies) => {
|
|
|
82
86
|
syntaxBase.type,
|
|
83
87
|
syntaxBase.comment,
|
|
84
88
|
syntaxBase.operator,
|
|
85
|
-
syntaxHighlights.diffAdded,
|
|
86
|
-
syntaxHighlights.diffRemoved,
|
|
87
|
-
syntaxHighlights.lineNumber,
|
|
88
89
|
];
|
|
89
90
|
|
|
90
91
|
if (!required.every(isValidThemeColor)) {
|
|
@@ -156,12 +157,83 @@ export const createThemeRuntime = (dependencies) => {
|
|
|
156
157
|
return [];
|
|
157
158
|
}
|
|
158
159
|
logger.warn('[themes] Failed to list custom themes dir:', error);
|
|
159
|
-
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const saveImportedTheme = async (raw) => {
|
|
165
|
+
const parsed = importedThemeSchema.safeParse(raw);
|
|
166
|
+
if (!parsed.success) throw new ThemeImportStorageError('invalid', 400);
|
|
167
|
+
const normalized = normalizeThemeJson({ ...parsed.data, metadata: { ...parsed.data.metadata, id: 'import' } });
|
|
168
|
+
if (!normalized) throw new ThemeImportStorageError('invalid', 400);
|
|
169
|
+
// The client supplies colors, never a filename or an overwrite target.
|
|
170
|
+
// Identical imports have the same ID, including retries after a lost reply.
|
|
171
|
+
const digest = createHash('sha256').update(JSON.stringify(parsed.data)).digest('hex').slice(0, 24);
|
|
172
|
+
const theme = { ...normalized, metadata: { ...normalized.metadata, id: `imported-vscode-${digest}` } };
|
|
173
|
+
const contents = `${JSON.stringify(theme, null, 2)}\n`;
|
|
174
|
+
if (Buffer.byteLength(contents, 'utf8') > maxThemeJsonBytes) throw new ThemeImportStorageError('size', 413);
|
|
175
|
+
await fsPromises.mkdir(themesDir, { recursive: true });
|
|
176
|
+
const target = path.join(themesDir, `${theme.metadata.id}.json`);
|
|
177
|
+
const temporary = path.join(themesDir, `${randomUUID()}.tmp`);
|
|
178
|
+
try {
|
|
179
|
+
await fsPromises.writeFile(temporary, contents, { flag: 'wx', mode: 0o600 });
|
|
180
|
+
try {
|
|
181
|
+
// Link publishes the complete file atomically and cannot overwrite an
|
|
182
|
+
// existing theme, even if another import finishes at the same time.
|
|
183
|
+
await fsPromises.link(temporary, target);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const parsedError = fileErrorSchema.safeParse(error);
|
|
186
|
+
if (!parsedError.success || parsedError.data.code !== 'EEXIST') throw error;
|
|
187
|
+
if (await fsPromises.readFile(target, 'utf8') !== contents) throw new ThemeImportStorageError('conflict', 409);
|
|
188
|
+
}
|
|
189
|
+
return theme;
|
|
190
|
+
} finally {
|
|
191
|
+
await fsPromises.unlink(temporary).catch((error) => {
|
|
192
|
+
const parsedError = fileErrorSchema.safeParse(error);
|
|
193
|
+
if (!parsedError.success || parsedError.data.code !== 'ENOENT') logger.warn('[themes] Failed to clean import temporary file');
|
|
194
|
+
});
|
|
160
195
|
}
|
|
161
196
|
};
|
|
162
197
|
|
|
163
198
|
return {
|
|
164
199
|
normalizeThemeJson,
|
|
165
200
|
readCustomThemesFromDisk,
|
|
201
|
+
saveImportedTheme,
|
|
202
|
+
async deleteImportedTheme(id) {
|
|
203
|
+
if (!z.string().trim().min(1).max(256).safeParse(id).success) throw new ThemeImportStorageError('invalid', 400);
|
|
204
|
+
const matches = [];
|
|
205
|
+
let readFailure;
|
|
206
|
+
let entries;
|
|
207
|
+
try { entries = await fsPromises.readdir(themesDir, { withFileTypes: true }); }
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (fileErrorSchema.safeParse(error).data?.code === 'ENOENT') return;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
214
|
+
const target = path.join(themesDir, entry.name);
|
|
215
|
+
let text;
|
|
216
|
+
try {
|
|
217
|
+
const stat = await fsPromises.lstat(target);
|
|
218
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxThemeJsonBytes) continue;
|
|
219
|
+
text = await fsPromises.readFile(target, 'utf8');
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (fileErrorSchema.safeParse(error).data?.code !== 'ENOENT') readFailure = error;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
let theme;
|
|
225
|
+
try { theme = normalizeThemeJson(JSON.parse(text)); }
|
|
226
|
+
catch { continue; }
|
|
227
|
+
if (theme?.metadata.id === id) matches.push(target);
|
|
228
|
+
}
|
|
229
|
+
// IDs identify themes, not filenames. Refuse ambiguous matches rather
|
|
230
|
+
// than deleting an arbitrary sibling or constructing a path from the ID.
|
|
231
|
+
if (matches.length > 1) throw new ThemeImportStorageError('conflict', 409);
|
|
232
|
+
if (!matches.length) {
|
|
233
|
+
if (readFailure) throw readFailure;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await fsPromises.unlink(matches[0]);
|
|
237
|
+
},
|
|
166
238
|
};
|
|
167
239
|
};
|
|
@@ -90,6 +90,21 @@ const createTestRuntime = ({ entries, files, stats }) => createThemeRuntime({
|
|
|
90
90
|
|
|
91
91
|
describe('theme runtime', () => {
|
|
92
92
|
describe('readCustomThemesFromDisk', () => {
|
|
93
|
+
it('loads compact authored roles without requiring derived colors', async () => {
|
|
94
|
+
const theme = validTheme('compact');
|
|
95
|
+
theme.colors.primary = { base: '#ffffff' };
|
|
96
|
+
theme.colors.interactive = { border: '#444444' };
|
|
97
|
+
theme.colors.status = { error: '#ff0000', warning: '#ffaa00', success: '#00ff00', info: '#0000ff' };
|
|
98
|
+
delete theme.colors.syntax.highlights;
|
|
99
|
+
delete theme.colors.syntax.base.background;
|
|
100
|
+
delete theme.colors.syntax.base.foreground;
|
|
101
|
+
const runtime = createTestRuntime({
|
|
102
|
+
entries: [fileEntry('compact.json'), fileEntry('bad.json')],
|
|
103
|
+
files: { '/themes/compact.json': JSON.stringify(theme), '/themes/bad.json': '{broken' },
|
|
104
|
+
stats: { '/themes/compact.json': { isFile: () => true, size: 1024 }, '/themes/bad.json': { isFile: () => true, size: 10 } },
|
|
105
|
+
});
|
|
106
|
+
expect((await runtime.readCustomThemesFromDisk()).map((item) => item.metadata.id)).toEqual(['compact']);
|
|
107
|
+
});
|
|
93
108
|
it('loads valid theme files', async () => {
|
|
94
109
|
const runtime = createTestRuntime({
|
|
95
110
|
entries: [fileEntry('direct.json')],
|
|
@@ -16,6 +16,14 @@ Policy inheritance uses the nearest explicit session value. A child `false` ther
|
|
|
16
16
|
|
|
17
17
|
Unknown lineage and failed policy loads fail closed. A failed pending-permission fetch is distinct from an empty successful response and never clears policy state.
|
|
18
18
|
|
|
19
|
+
## Safety net
|
|
20
|
+
|
|
21
|
+
`evaluatePermission` (the routing runtime, `../routing/DOCUMENTATION.md`) is
|
|
22
|
+
consulted after the policy check and before the reply. `hold` counts the
|
|
23
|
+
request as handled without replying, so it stays on screen; `accept` replies as
|
|
24
|
+
before. A `permission.replied` event is passed to `onPermissionReplied` so the
|
|
25
|
+
routing runtime forgets its cached decision.
|
|
26
|
+
|
|
19
27
|
## Routes
|
|
20
28
|
|
|
21
29
|
- `GET /api/permission-auto-accept`
|
|
@@ -25,6 +25,10 @@ export function createPermissionAutoAcceptRuntime({
|
|
|
25
25
|
readSettingsFromDiskMigrated,
|
|
26
26
|
persistSettings,
|
|
27
27
|
broadcastGlobalUiEvent,
|
|
28
|
+
// The routing safety net: asked once per request before the automatic reply.
|
|
29
|
+
// `hold` leaves the request for the user; absent means every request is replied to.
|
|
30
|
+
evaluatePermission = null,
|
|
31
|
+
onPermissionReplied = null,
|
|
28
32
|
fetchImpl = fetch,
|
|
29
33
|
retryDelaysMs = RETRY_DELAYS_MS,
|
|
30
34
|
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
@@ -148,6 +152,10 @@ export function createPermissionAutoAcceptRuntime({
|
|
|
148
152
|
if (!permission?.id || !permission?.sessionID) return false;
|
|
149
153
|
await load();
|
|
150
154
|
if (!(await isSessionAutoAccepting(permission.sessionID, directory))) return false;
|
|
155
|
+
if (evaluatePermission) {
|
|
156
|
+
const verdict = await evaluatePermission(permission, directory);
|
|
157
|
+
if (verdict?.action === 'hold') return true;
|
|
158
|
+
}
|
|
151
159
|
await request(`/permission/${encodeURIComponent(permission.id)}/reply`, {
|
|
152
160
|
directory,
|
|
153
161
|
method: 'POST',
|
|
@@ -218,6 +226,11 @@ export function createPermissionAutoAcceptRuntime({
|
|
|
218
226
|
}
|
|
219
227
|
if (payload?.type === 'permission.asked') {
|
|
220
228
|
void processPermission(payload.properties, directory);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (payload?.type === 'permission.replied') {
|
|
232
|
+
const permissionId = payload.properties?.requestID;
|
|
233
|
+
if (typeof permissionId === 'string') onPermissionReplied?.(permissionId);
|
|
221
234
|
}
|
|
222
235
|
};
|
|
223
236
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { createPermissionAutoAcceptRuntime } from './runtime.js';
|
|
3
3
|
|
|
4
|
-
const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
|
|
4
|
+
const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0], evaluatePermission, onPermissionReplied } = {}) => {
|
|
5
5
|
let settings = stored ?? { permissionAutoAccept: { sessions: {} } };
|
|
6
6
|
let eventHandler;
|
|
7
7
|
let statusHandler;
|
|
@@ -16,6 +16,8 @@ const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
|
|
|
16
16
|
persistSettings: async (changes) => { settings = { ...settings, ...changes }; },
|
|
17
17
|
fetchImpl: fetchImpl ?? vi.fn(async () => new Response('[]')),
|
|
18
18
|
retryDelaysMs,
|
|
19
|
+
evaluatePermission,
|
|
20
|
+
onPermissionReplied,
|
|
19
21
|
});
|
|
20
22
|
runtime.start();
|
|
21
23
|
return {
|
|
@@ -143,4 +145,38 @@ describe('permission auto-accept runtime', () => {
|
|
|
143
145
|
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
|
|
144
146
|
expect(await runtime.load()).toEqual({ sessions: { root: true }, revision: 1 });
|
|
145
147
|
});
|
|
148
|
+
|
|
149
|
+
it('leaves a request held by the safety net unanswered and forgets it once replied', async () => {
|
|
150
|
+
const fetchImpl = vi.fn(async () => new Response('[]'));
|
|
151
|
+
const verdicts = { held: { action: 'hold', score: 0.9, kind: 'git_history' }, safe: { action: 'accept', score: 0.1 } };
|
|
152
|
+
const evaluatePermission = vi.fn(async (permission) => verdicts[permission.id]);
|
|
153
|
+
const onPermissionReplied = vi.fn();
|
|
154
|
+
const { runtime, emit } = createRuntime({
|
|
155
|
+
stored: { permissionAutoAccept: { sessions: { root: true } } },
|
|
156
|
+
fetchImpl,
|
|
157
|
+
evaluatePermission,
|
|
158
|
+
onPermissionReplied,
|
|
159
|
+
});
|
|
160
|
+
await runtime.load();
|
|
161
|
+
|
|
162
|
+
emit({ type: 'permission.asked', properties: { id: 'held', sessionID: 'root', permission: 'bash', metadata: {} } });
|
|
163
|
+
emit({ type: 'permission.asked', properties: { id: 'safe', sessionID: 'root', permission: 'bash', metadata: {} } });
|
|
164
|
+
await flush();
|
|
165
|
+
|
|
166
|
+
const replies = fetchImpl.mock.calls.map(([url]) => String(url)).filter((url) => url.includes('/reply'));
|
|
167
|
+
expect(replies).toEqual(['http://opencode.test/permission/safe/reply?directory=%2Fproject']);
|
|
168
|
+
expect(evaluatePermission).toHaveBeenCalledTimes(2);
|
|
169
|
+
|
|
170
|
+
emit({ type: 'permission.replied', properties: { sessionID: 'root', requestID: 'held', reply: 'once' } });
|
|
171
|
+
expect(onPermissionReplied).toHaveBeenCalledWith('held');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('does not consult the safety net for sessions that are not auto-accepting', async () => {
|
|
175
|
+
const evaluatePermission = vi.fn(async () => ({ action: 'hold' }));
|
|
176
|
+
const { runtime, emit } = createRuntime({ evaluatePermission });
|
|
177
|
+
await runtime.load();
|
|
178
|
+
emit({ type: 'permission.asked', properties: { id: 'p', sessionID: 'manual', permission: 'bash', metadata: {} } });
|
|
179
|
+
await flush();
|
|
180
|
+
expect(evaluatePermission).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
146
182
|
});
|
|
@@ -12,8 +12,18 @@ The managed Chats root (`~/.config/openchamber/chats`) is also one context owner
|
|
|
12
12
|
| `<projectsDir>/<projectId>.json` | `packages/web/server/lib/projects` (`project-setup.js` for the client-owned keys behind `/api/projects/:projectId/config`; `project-config.js` for `version` / `scheduledTasks`), one write lock for both | worktree setup, draft starters, project actions, scheduled tasks |
|
|
13
13
|
| `<projectsDir>/<projectId>/context.json` | **this module, exclusively** | notes, todos, plan manifest |
|
|
14
14
|
| `<projectsDir>/<projectId>/plans/*.md` | **this module, exclusively** | plan bodies |
|
|
15
|
+
| `<projectsDir>/<projectId>/memory.json` | `packages/web/server/lib/agent-memory` | what the agent chose to remember about the project |
|
|
15
16
|
| `<repo>/<plansDir>/*.md` | this module (read, edit, delete, move) when the team config names a `plansDir`; the folder is the team's, any tool may write there | shared plan bodies |
|
|
16
17
|
|
|
18
|
+
`<projectId>` in these paths is the bounded stem `projectConfigFileStemOf`
|
|
19
|
+
(`packages/web/server/lib/projects/project-id.js`) gives the id: the id itself
|
|
20
|
+
up to 200 characters, `path_sha256_<digest>` beyond that, so a deeply nested
|
|
21
|
+
checkout gets a folder the filesystem accepts. The folder, the config file,
|
|
22
|
+
and the memory file all share that one stem; nothing here composes a path from
|
|
23
|
+
the raw id. The legacy-migration read below looks at the bounded config file
|
|
24
|
+
for the same reason: a read of `<raw id>.json` would fail with ENAMETOOLONG
|
|
25
|
+
and turn an empty project into an error.
|
|
26
|
+
|
|
17
27
|
The split is the point. Both files were previously one, written by the client
|
|
18
28
|
with a whole-file read-modify-write. Adding a server writer to that file would
|
|
19
29
|
have made unrelated features (project actions, draft starters) clobber notes
|
|
@@ -147,9 +157,9 @@ and I/O failures are `500`.
|
|
|
147
157
|
## Legacy migration
|
|
148
158
|
|
|
149
159
|
`projectNotes`, `projectTodos`, and `projectPlanFiles` originally lived in
|
|
150
|
-
`<projectId>.json
|
|
151
|
-
are moved out and deleted from the
|
|
152
|
-
preserved untouched.
|
|
160
|
+
`<projectId>.json` (the bounded name, see Ownership). On the first read with
|
|
161
|
+
no `context.json`, those three keys are moved out and deleted from the
|
|
162
|
+
client-owned file; every other key is preserved untouched.
|
|
153
163
|
|
|
154
164
|
Plan links carried absolute paths. Migration converts each to a base name. A
|
|
155
165
|
file already in the plans directory is used in place; one referenced from
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* directory never invalidates a reference.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { projectConfigFileStemOf } from '../projects/project-id.js';
|
|
16
|
+
|
|
15
17
|
const PROJECT_CONTEXT_VERSION = 2;
|
|
16
18
|
const PROJECT_NOTE_BODY_MAX_LENGTH = 3000;
|
|
17
19
|
const PROJECT_NOTE_MAX_ITEMS = 200;
|
|
@@ -221,10 +223,14 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
221
223
|
return value;
|
|
222
224
|
};
|
|
223
225
|
|
|
224
|
-
|
|
226
|
+
// The folder and the client-owned config file share one bounded stem
|
|
227
|
+
// (`projectConfigFileStemOf`): an id too long for a file name would
|
|
228
|
+
// otherwise fail every read with ENAMETOOLONG instead of reading as empty.
|
|
229
|
+
const storageStemFor = (projectId) => projectConfigFileStemOf(sanitizeProjectId(projectId));
|
|
230
|
+
const storageDirFor = (projectId) => path.join(projectsDirPath, storageStemFor(projectId));
|
|
225
231
|
const contextPathFor = (projectId) => path.join(storageDirFor(projectId), 'context.json');
|
|
226
232
|
const plansDirFor = (projectId) => path.join(storageDirFor(projectId), 'plans');
|
|
227
|
-
const legacyConfigPathFor = (projectId) => path.join(projectsDirPath, `${
|
|
233
|
+
const legacyConfigPathFor = (projectId) => path.join(projectsDirPath, `${storageStemFor(projectId)}.json`);
|
|
228
234
|
|
|
229
235
|
const readJson = async (filePath) => {
|
|
230
236
|
let raw;
|