@vantran-se/openchamber-web 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -84,6 +84,16 @@ function sendAuthStorageError(res, error) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** A list's state and whose items, from the query; unknown values read as open and anyone. */
|
|
88
|
+
function readListFilter(query) {
|
|
89
|
+
const state = requestText(query?.state);
|
|
90
|
+
const people = requestText(query?.people);
|
|
91
|
+
return {
|
|
92
|
+
state: ['open', 'closed', 'merged', 'all'].includes(state) ? state : 'open',
|
|
93
|
+
people: ['any', 'assigned', 'created', 'reviewRequested'].includes(people) ? people : 'any',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
function requestText(value) {
|
|
88
98
|
return isString(value) ? value.trim() : '';
|
|
89
99
|
}
|
|
@@ -420,20 +430,21 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
420
430
|
record: mutationRecord(context, kind, actor, credential, existing.target, normalized.digest),
|
|
421
431
|
providerAccountId: resource.providerUserId,
|
|
422
432
|
perform: () => { throw mutationConflict(); },
|
|
423
|
-
reconcile: async () => reconcile(service, await resolveReplay()),
|
|
433
|
+
reconcile: async () => reconcile(service, await resolveReplay(), normalized),
|
|
424
434
|
classifyError: classifyMutationError,
|
|
425
435
|
});
|
|
426
436
|
statusCache.invalidate({ instance: context.instance, accountId: context.accountId, repositoryId: context.repositoryId });
|
|
427
437
|
return mutationReceipt(execution.record, execution.replayed, resource.providerUserId);
|
|
428
438
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
439
|
+
let resolved;
|
|
440
|
+
if (kind === 'change-request-create') resolved = await service.resolveCreateMutation(context, context.target, normalized);
|
|
441
|
+
else if (kind.startsWith('issue-')) resolved = await service.resolveIssueMutation(context, context.target);
|
|
442
|
+
else resolved = await service.resolveChangeRequestMutation(context, context.target);
|
|
432
443
|
const execution = await options.mutationExecutor.execute({
|
|
433
444
|
record: mutationRecord(context, kind, actor, credential, resolved.target, normalized.digest),
|
|
434
445
|
providerAccountId: resource.providerUserId,
|
|
435
446
|
perform: () => perform(service, normalized, resolved),
|
|
436
|
-
reconcile: () => reconcile(service, resolved),
|
|
447
|
+
reconcile: () => reconcile(service, resolved, normalized),
|
|
437
448
|
classifyError: classifyMutationError,
|
|
438
449
|
});
|
|
439
450
|
statusCache.invalidate({ instance: context.instance, accountId: context.accountId, repositoryId: context.repositoryId });
|
|
@@ -785,6 +796,156 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
785
796
|
}
|
|
786
797
|
});
|
|
787
798
|
|
|
799
|
+
// A comment or a review leaves nothing that tells a lost write from one
|
|
800
|
+
// never sent short of guessing from its text, so the outcome stays unknown.
|
|
801
|
+
const unknownOutcome = () => ({ state: 'outcome-unknown' });
|
|
802
|
+
// GitLab's own limit for a note.
|
|
803
|
+
const NOTE_MAX_LENGTH = 1_000_000;
|
|
804
|
+
const noteText = (value, required) => {
|
|
805
|
+
if (value === undefined && !required) return undefined;
|
|
806
|
+
if (!isString(value) || !value.trim() || value.length > NOTE_MAX_LENGTH) {
|
|
807
|
+
throw invalidMutationContext('body is required and must fit a GitLab note');
|
|
808
|
+
}
|
|
809
|
+
return value;
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
app.post('/api/source-control/gitlab/pr/comment', async (req, res) => {
|
|
813
|
+
try {
|
|
814
|
+
const body = req.body ?? {};
|
|
815
|
+
return res.json(await executeMutation(body, 'change-request-comment', (input, context) => {
|
|
816
|
+
if (!context.target.number) throw invalidMutationContext('target number is required');
|
|
817
|
+
const text = noteText(input.body, true);
|
|
818
|
+
return { body: text, digest: { body: text } };
|
|
819
|
+
}, async (service, normalized, resolved) => {
|
|
820
|
+
await service.commentChangeRequest({ providerTarget: resolved.providerTarget, body: normalized.body });
|
|
821
|
+
return {};
|
|
822
|
+
}, unknownOutcome));
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return sendMutationError(res, error);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
app.post('/api/source-control/gitlab/issues/comment', async (req, res) => {
|
|
829
|
+
try {
|
|
830
|
+
const body = req.body ?? {};
|
|
831
|
+
return res.json(await executeMutation(body, 'issue-comment', (input, context) => {
|
|
832
|
+
if (!context.target.number) throw invalidMutationContext('target number is required');
|
|
833
|
+
const text = noteText(input.body, true);
|
|
834
|
+
return { body: text, digest: { body: text } };
|
|
835
|
+
}, async (service, normalized, resolved) => {
|
|
836
|
+
await service.commentIssue({ providerTarget: resolved.providerTarget, body: normalized.body });
|
|
837
|
+
return {};
|
|
838
|
+
}, unknownOutcome));
|
|
839
|
+
} catch (error) {
|
|
840
|
+
return sendMutationError(res, error);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// Close or reopen: `state` is what the item should become.
|
|
845
|
+
for (const [path, kind, method] of [
|
|
846
|
+
['/api/source-control/gitlab/pr/state', 'change-request-state', 'setChangeRequestState'],
|
|
847
|
+
['/api/source-control/gitlab/issues/state', 'issue-state', 'setIssueState'],
|
|
848
|
+
]) {
|
|
849
|
+
app.post(path, async (req, res) => {
|
|
850
|
+
try {
|
|
851
|
+
const body = req.body ?? {};
|
|
852
|
+
return res.json(await executeMutation(body, kind, (input, context) => {
|
|
853
|
+
if (!context.target.number || !['open', 'closed'].includes(input.state)) {
|
|
854
|
+
throw invalidMutationContext('target number and state are required');
|
|
855
|
+
}
|
|
856
|
+
return { state: input.state, digest: { state: input.state } };
|
|
857
|
+
}, (service, normalized, resolved) => service[method]({
|
|
858
|
+
state: normalized.state,
|
|
859
|
+
providerTarget: resolved.providerTarget,
|
|
860
|
+
targetProject: resolved.target.project,
|
|
861
|
+
expectedTarget: resolved.target,
|
|
862
|
+
}), (service, resolved, normalized) => service.reconcileStateMutation(resolved.providerTarget, kind, normalized.state)));
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return sendMutationError(res, error);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Labels and reviewers as one whole set, so a repeat asks for the same result.
|
|
870
|
+
const nameList = (value, limit) => {
|
|
871
|
+
if (!Array.isArray(value) || value.length > limit
|
|
872
|
+
|| value.some((entry) => !isString(entry) || !entry.trim() || entry.length > 256)) {
|
|
873
|
+
throw invalidMutationContext('labels or reviewers are invalid');
|
|
874
|
+
}
|
|
875
|
+
return [...new Set(value)].sort();
|
|
876
|
+
};
|
|
877
|
+
for (const [path, kind, field, method] of [
|
|
878
|
+
['/api/source-control/gitlab/pr/labels', 'change-request-labels', 'labels', 'setLabels'],
|
|
879
|
+
['/api/source-control/gitlab/issues/labels', 'issue-labels', 'labels', 'setLabels'],
|
|
880
|
+
['/api/source-control/gitlab/pr/reviewers', 'change-request-reviewers', 'reviewers', 'setReviewers'],
|
|
881
|
+
]) {
|
|
882
|
+
app.post(path, async (req, res) => {
|
|
883
|
+
try {
|
|
884
|
+
const body = req.body ?? {};
|
|
885
|
+
return res.json(await executeMutation(body, kind, (input, context) => {
|
|
886
|
+
if (!context.target.number) throw invalidMutationContext('target number is required');
|
|
887
|
+
const names = nameList(input[field], field === 'labels' ? 100 : 50);
|
|
888
|
+
// GitLab names a reviewer by user id.
|
|
889
|
+
if (field === 'reviewers' && names.some((id) => !/^\d+$/.test(id))) throw invalidMutationContext('reviewers are invalid');
|
|
890
|
+
return { names, digest: { [field]: names } };
|
|
891
|
+
}, (service, normalized, resolved) => service[method]({
|
|
892
|
+
kind,
|
|
893
|
+
[field]: normalized.names,
|
|
894
|
+
providerTarget: resolved.providerTarget,
|
|
895
|
+
targetProject: resolved.target.project,
|
|
896
|
+
expectedTarget: resolved.target,
|
|
897
|
+
}), (service, resolved, normalized) => service.reconcileSetMutation(resolved.providerTarget, kind, normalized.names)));
|
|
898
|
+
} catch (error) {
|
|
899
|
+
return sendMutationError(res, error);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// The project's labels and who can review in it, for the board's pickers.
|
|
905
|
+
for (const [path, method] of [
|
|
906
|
+
['/api/source-control/gitlab/references/labels', 'listLabels'],
|
|
907
|
+
['/api/source-control/gitlab/references/reviewers', 'listReviewerCandidates'],
|
|
908
|
+
]) {
|
|
909
|
+
app.get(path, async (req, res) => {
|
|
910
|
+
try {
|
|
911
|
+
const owner = requestText(req.query?.owner);
|
|
912
|
+
const repo = requestText(req.query?.repo);
|
|
913
|
+
if (!owner || !repo) return res.status(400).json({ error: 'a complete project selector is required' });
|
|
914
|
+
const origin = requestOrigin(req);
|
|
915
|
+
const directory = requestText(req.query?.directory);
|
|
916
|
+
const trustedContext = await validateReadContext(req, origin, directory);
|
|
917
|
+
if (!trustedContext) return res.status(501).json({ error: 'Bound source control status is unavailable' });
|
|
918
|
+
const service = await getResourceService(origin, trustedContext.accountId, true);
|
|
919
|
+
return res.json({ connected: true, items: await service[method](directory, { owner, name: repo }, trustedContext.primaryRemote) });
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (isReadContextError(error)) return res.status(error.status ?? 400).json(sourceControlErrorBody(error));
|
|
922
|
+
return sendResourceError(res, error);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
app.post('/api/source-control/gitlab/pr/review', async (req, res) => {
|
|
928
|
+
try {
|
|
929
|
+
const body = req.body ?? {};
|
|
930
|
+
return res.json(await executeMutation(body, 'change-request-review', (input, context) => {
|
|
931
|
+
// A verdict names the commit it is for; without one it would land on whatever was pushed last.
|
|
932
|
+
if (!context.target.number || !context.target.headSha || !['approve', 'request-changes'].includes(input.verdict)) {
|
|
933
|
+
throw invalidMutationContext('target number, head SHA and review verdict are required');
|
|
934
|
+
}
|
|
935
|
+
const text = noteText(input.body, false);
|
|
936
|
+
return { verdict: input.verdict, body: text, digest: { verdict: input.verdict, body: text ?? null } };
|
|
937
|
+
}, (service, normalized, resolved) => service.reviewChangeRequest({
|
|
938
|
+
verdict: normalized.verdict,
|
|
939
|
+
body: normalized.body,
|
|
940
|
+
providerTarget: resolved.providerTarget,
|
|
941
|
+
targetProject: resolved.target.project,
|
|
942
|
+
expectedTarget: resolved.target,
|
|
943
|
+
}), unknownOutcome));
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return sendMutationError(res, error);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
|
|
788
949
|
app.get('/api/source-control/gitlab/pulls/list', async (req, res) => {
|
|
789
950
|
try {
|
|
790
951
|
const origin = requestOrigin(req);
|
|
@@ -795,6 +956,7 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
795
956
|
return res.json(await service.listChangeRequests(directory, {
|
|
796
957
|
page: requestNumber(req.query?.page) ?? 1,
|
|
797
958
|
query: requestText(req.query?.query) || undefined,
|
|
959
|
+
...readListFilter(req.query),
|
|
798
960
|
remote: trustedContext.primaryRemote,
|
|
799
961
|
}));
|
|
800
962
|
} catch (error) {
|
|
@@ -818,7 +980,7 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
818
980
|
if (!number) return res.status(400).json({ error: 'number is required' });
|
|
819
981
|
const service = await getResourceService(origin, trustedContext.accountId, true);
|
|
820
982
|
return res.json(await service.changeRequestContext(directory, number, {
|
|
821
|
-
includeDiff: req.query?.diff === '1', includeCIDetails: req.query?.checkDetails === '1',
|
|
983
|
+
includeDiff: req.query?.diff === '1', includeCIDetails: req.query?.checkDetails === '1', includeTimeline: req.query?.timeline === '1',
|
|
822
984
|
project: owner ? { owner, name: repo } : undefined,
|
|
823
985
|
remote: trustedContext.primaryRemote,
|
|
824
986
|
constrainToPrimary: true,
|
|
@@ -840,6 +1002,7 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
840
1002
|
const service = await getResourceService(origin, trustedContext.accountId, true);
|
|
841
1003
|
return res.json(await service.listIssues(directory, {
|
|
842
1004
|
page: requestNumber(req.query?.page) ?? 1, query: requestText(req.query?.query) || undefined,
|
|
1005
|
+
...readListFilter(req.query),
|
|
843
1006
|
remote: trustedContext.primaryRemote,
|
|
844
1007
|
}));
|
|
845
1008
|
} catch (error) {
|
|
@@ -967,6 +1130,37 @@ export function registerGitLabRoutes(app, options = {}) {
|
|
|
967
1130
|
.changeRequestFileContents(context.directory, number, {
|
|
968
1131
|
project: projectSelector(sourceRepo), remote: context.primaryRemote, path: filePath, previousPath, status,
|
|
969
1132
|
})),
|
|
1133
|
+
/**
|
|
1134
|
+
* The account GitLab reads on this instance use when none is named: the
|
|
1135
|
+
* glab login while the user has switched to it, else the active account.
|
|
1136
|
+
* Null when neither is there.
|
|
1137
|
+
*/
|
|
1138
|
+
readCurrentAccountId: async (origin) => {
|
|
1139
|
+
const instance = await store.readInstance(origin);
|
|
1140
|
+
// A glab login that stopped working leaves the active account, as the status read does.
|
|
1141
|
+
const cli = instance.cliActive ? await getUsableGlab(origin) : null;
|
|
1142
|
+
if (cli) return `${origin}#cli:${cli.user.id}`;
|
|
1143
|
+
return instance.accounts.find((account) => account.id === instance.activeAccountId && account.status !== 'invalid')?.id ?? null;
|
|
1144
|
+
},
|
|
1145
|
+
/** A merge request's head on the context's primary project network, by number, for a worktree made from it. */
|
|
1146
|
+
readChangeRequestHead: async ({ context, number }) => {
|
|
1147
|
+
const trusted = await options.validateReadContext(context);
|
|
1148
|
+
const service = await getResourceService(trusted.instance, trusted.accountId, true);
|
|
1149
|
+
const { changeRequest: request } = await service.changeRequestContext(trusted.directory, number, {
|
|
1150
|
+
remote: trusted.primaryRemote,
|
|
1151
|
+
constrainToPrimary: true,
|
|
1152
|
+
});
|
|
1153
|
+
const headSha = String(request?.headSha || '').toLowerCase();
|
|
1154
|
+
if (!request?.project || !request.head || !/^[0-9a-f]{40}$/.test(headSha)) {
|
|
1155
|
+
throw Object.assign(new Error('GitLab returned an invalid merge request head'), { code: 'MALFORMED_PROVIDER_RESPONSE' });
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
project: { id: request.project.id, owner: request.project.owner, name: request.project.name },
|
|
1159
|
+
headSha,
|
|
1160
|
+
headBranch: request.head,
|
|
1161
|
+
headOwner: (request.headProject ?? request.project).owner,
|
|
1162
|
+
};
|
|
1163
|
+
},
|
|
970
1164
|
resolveChangeRequestSource: async ({ context, project, number, expectedHeadSha, requestedRemoteName }) => {
|
|
971
1165
|
const trusted = await options.validateReadContext(context);
|
|
972
1166
|
const service = await getResourceService(trusted.instance, trusted.accountId, true);
|
|
@@ -103,6 +103,20 @@ function canonicalMutationOptions(overrides = {}) {
|
|
|
103
103
|
ready: vi.fn(async () => ({ ready: true })),
|
|
104
104
|
reconcileCreate: vi.fn(async () => ({ state: 'succeeded', result: {} })),
|
|
105
105
|
reconcileExisting: vi.fn(async () => ({ state: 'outcome-unknown' })),
|
|
106
|
+
comment: vi.fn(async () => undefined),
|
|
107
|
+
commentIssue: vi.fn(async () => undefined),
|
|
108
|
+
review: vi.fn(async () => ({ commented: true })),
|
|
109
|
+
setPullState: vi.fn(async (payload) => ({ state: payload.state })),
|
|
110
|
+
setIssueState: vi.fn(async (payload) => ({ state: payload.state })),
|
|
111
|
+
reconcileState: vi.fn(async (_target, _kind, state) => ({ state: 'succeeded', result: { state } })),
|
|
112
|
+
setLabels: vi.fn(async () => ({})),
|
|
113
|
+
setReviewers: vi.fn(async () => ({})),
|
|
114
|
+
listLabels: vi.fn(async () => [{ name: 'bug', color: 'd73a4a' }]),
|
|
115
|
+
listReviewers: vi.fn(async () => [{ id: '7', login: 'ann' }]),
|
|
116
|
+
resolveIssue: vi.fn(async (context) => ({
|
|
117
|
+
providerTarget: { projectId: '2', number: context.target.number },
|
|
118
|
+
target: { repositoryId: target.repositoryId, bindingRevision: target.bindingRevision, primaryRemote: target.primaryRemote, project: target.project, number: context.target.number },
|
|
119
|
+
})),
|
|
106
120
|
};
|
|
107
121
|
const service = {
|
|
108
122
|
resolveCreateMutation: calls.resolveCreate,
|
|
@@ -117,6 +131,18 @@ function canonicalMutationOptions(overrides = {}) {
|
|
|
117
131
|
readyChangeRequest: calls.ready,
|
|
118
132
|
reconcileCreateMutation: calls.reconcileCreate,
|
|
119
133
|
reconcileChangeRequestMutation: calls.reconcileExisting,
|
|
134
|
+
commentChangeRequest: calls.comment,
|
|
135
|
+
commentIssue: calls.commentIssue,
|
|
136
|
+
reviewChangeRequest: calls.review,
|
|
137
|
+
resolveIssueMutation: calls.resolveIssue,
|
|
138
|
+
setChangeRequestState: calls.setPullState,
|
|
139
|
+
setIssueState: calls.setIssueState,
|
|
140
|
+
reconcileStateMutation: calls.reconcileState,
|
|
141
|
+
setLabels: calls.setLabels,
|
|
142
|
+
setReviewers: calls.setReviewers,
|
|
143
|
+
reconcileSetMutation: vi.fn(async () => ({ state: 'outcome-unknown' })),
|
|
144
|
+
listLabels: calls.listLabels,
|
|
145
|
+
listReviewerCandidates: calls.listReviewers,
|
|
120
146
|
};
|
|
121
147
|
return {
|
|
122
148
|
account,
|
|
@@ -403,6 +429,104 @@ describe('GitLab routes', () => {
|
|
|
403
429
|
expect(setup.calls.create).toHaveBeenCalledOnce();
|
|
404
430
|
});
|
|
405
431
|
|
|
432
|
+
it('comments, reviews and comments on issues through the canonical executor', async () => {
|
|
433
|
+
const setup = canonicalMutationOptions();
|
|
434
|
+
const app = appWith(setup.options);
|
|
435
|
+
const existing = (key, number, extra = {}) => ({
|
|
436
|
+
...mutationBody({ idempotencyKey: key, target: { project: { owner: 'team', name: 'repo' }, number, ...extra } }),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const commented = await request(app).post('/api/source-control/gitlab/pr/comment')
|
|
440
|
+
.send({ ...existing('comment-one', 5), body: 'Looks good' }).expect(200);
|
|
441
|
+
const reviewed = await request(app).post('/api/source-control/gitlab/pr/review')
|
|
442
|
+
.send({ ...existing('review-one', 5, { headSha: 'abc' }), verdict: 'request-changes', body: 'Rename it' }).expect(200);
|
|
443
|
+
const issue = await request(app).post('/api/source-control/gitlab/issues/comment')
|
|
444
|
+
.send({ ...existing('issue-one', 3), body: 'On it' }).expect(200);
|
|
445
|
+
|
|
446
|
+
expect(commented.body.result).toEqual({});
|
|
447
|
+
expect(reviewed.body.result).toEqual({ commented: true });
|
|
448
|
+
expect(issue.body.target).toMatchObject({ number: 3 });
|
|
449
|
+
expect(setup.calls.comment).toHaveBeenCalledWith({ providerTarget: { projectId: '2', number: 5 }, body: 'Looks good' });
|
|
450
|
+
expect(setup.calls.review).toHaveBeenCalledWith(expect.objectContaining({
|
|
451
|
+
verdict: 'request-changes', body: 'Rename it', expectedTarget: expect.objectContaining({ number: 5, headSha: 'abc' }),
|
|
452
|
+
}));
|
|
453
|
+
expect(setup.calls.commentIssue).toHaveBeenCalledWith({ providerTarget: { projectId: '2', number: 3 }, body: 'On it' });
|
|
454
|
+
expect(setup.calls.resolveIssue).toHaveBeenCalledOnce();
|
|
455
|
+
|
|
456
|
+
await request(app).post('/api/source-control/gitlab/pr/comment').send({ ...existing('comment-two', 5), body: ' ' }).expect(400);
|
|
457
|
+
await request(app).post('/api/source-control/gitlab/pr/review').send({ ...existing('review-two', 5, { headSha: 'abc' }), verdict: 'merge' }).expect(400);
|
|
458
|
+
await request(app).post('/api/source-control/gitlab/pr/review').send({ ...existing('review-three', 5), verdict: 'approve' }).expect(400);
|
|
459
|
+
expect(setup.calls.review).toHaveBeenCalledOnce();
|
|
460
|
+
expect(setup.calls.comment).toHaveBeenCalledOnce();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('closes and reopens through the canonical executor and reconciles by state after a restart', async () => {
|
|
464
|
+
let failCompletion = false;
|
|
465
|
+
const mutationStore = makeMutationStore([], async () => {
|
|
466
|
+
if (failCompletion) {
|
|
467
|
+
failCompletion = false;
|
|
468
|
+
throw new Error('disk unavailable');
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
const setup = canonicalMutationOptions({ mutationExecutor: createMutationExecutor({ store: mutationStore }) });
|
|
472
|
+
const app = appWith(setup.options);
|
|
473
|
+
const pull = { ...mutationBody({ idempotencyKey: 'state-one', target: { project: { owner: 'team', name: 'repo' }, number: 5 } }), state: 'closed' };
|
|
474
|
+
const issue = { ...mutationBody({ idempotencyKey: 'state-two', target: { project: { owner: 'team', name: 'repo' }, number: 3 } }), state: 'open' };
|
|
475
|
+
|
|
476
|
+
const closed = await request(app).post('/api/source-control/gitlab/pr/state').send(pull).expect(200);
|
|
477
|
+
failCompletion = true;
|
|
478
|
+
await request(app).post('/api/source-control/gitlab/issues/state').send(issue).expect(409);
|
|
479
|
+
setup.options.mutationExecutor = createMutationExecutor({ store: mutationStore });
|
|
480
|
+
const recovered = await request(appWith(setup.options)).post('/api/source-control/gitlab/issues/state').send(issue).expect(200);
|
|
481
|
+
await request(app).post('/api/source-control/gitlab/pr/state').send({ ...pull, idempotencyKey: 'state-three', state: 'merged' }).expect(400);
|
|
482
|
+
|
|
483
|
+
expect(closed.body.result).toEqual({ state: 'closed' });
|
|
484
|
+
expect(recovered.body).toMatchObject({ replayed: true, result: { state: 'open' } });
|
|
485
|
+
expect(setup.calls.setPullState).toHaveBeenCalledWith(expect.objectContaining({ state: 'closed', providerTarget: { projectId: '2', number: 5 } }));
|
|
486
|
+
expect(setup.calls.setIssueState).toHaveBeenCalledOnce();
|
|
487
|
+
expect(setup.calls.reconcileState).toHaveBeenCalledWith({ projectId: '2', number: 3 }, 'issue-state', 'open');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('sets labels and reviewers as sorted sets and reads the choices for the pickers', async () => {
|
|
491
|
+
const setup = canonicalMutationOptions();
|
|
492
|
+
const app = appWith(setup.options);
|
|
493
|
+
const target = (number) => ({ project: { owner: 'team', name: 'repo' }, number });
|
|
494
|
+
|
|
495
|
+
await request(app).post('/api/source-control/gitlab/pr/labels')
|
|
496
|
+
.send({ ...mutationBody({ idempotencyKey: 'labels-one', target: target(5) }), labels: ['docs', 'bug', 'bug'] }).expect(200);
|
|
497
|
+
await request(app).post('/api/source-control/gitlab/issues/labels')
|
|
498
|
+
.send({ ...mutationBody({ idempotencyKey: 'labels-two', target: target(3) }), labels: [] }).expect(200);
|
|
499
|
+
await request(app).post('/api/source-control/gitlab/pr/reviewers')
|
|
500
|
+
.send({ ...mutationBody({ idempotencyKey: 'reviewers-one', target: target(5) }), reviewers: ['9', '7'] }).expect(200);
|
|
501
|
+
await request(app).post('/api/source-control/gitlab/pr/reviewers')
|
|
502
|
+
.send({ ...mutationBody({ idempotencyKey: 'reviewers-two', target: target(5) }), reviewers: ['ann'] }).expect(400);
|
|
503
|
+
|
|
504
|
+
expect(setup.calls.setLabels).toHaveBeenNthCalledWith(1, expect.objectContaining({ kind: 'change-request-labels', labels: ['bug', 'docs'] }));
|
|
505
|
+
expect(setup.calls.setLabels).toHaveBeenNthCalledWith(2, expect.objectContaining({ kind: 'issue-labels', labels: [], providerTarget: { projectId: '2', number: 3 } }));
|
|
506
|
+
expect(setup.calls.setReviewers).toHaveBeenCalledOnce();
|
|
507
|
+
expect(setup.calls.setReviewers).toHaveBeenCalledWith(expect.objectContaining({ reviewers: ['7', '9'] }));
|
|
508
|
+
|
|
509
|
+
const read = { instance: origin, directory: '/repo', repositoryId: 'repo_one', accountId: `${origin}#9`, bindingRevision: 3, primaryRemote: 'origin', owner: 'team', repo: 'repo' };
|
|
510
|
+
const labels = await request(app).get('/api/source-control/gitlab/references/labels').query(read).expect(200);
|
|
511
|
+
const reviewers = await request(app).get('/api/source-control/gitlab/references/reviewers').query(read).expect(200);
|
|
512
|
+
expect(labels.body).toEqual({ connected: true, items: [{ name: 'bug', color: 'd73a4a' }] });
|
|
513
|
+
expect(reviewers.body).toEqual({ connected: true, items: [{ id: '7', login: 'ann' }] });
|
|
514
|
+
expect(setup.calls.listLabels).toHaveBeenCalledWith('/repo', { owner: 'team', name: 'repo' }, 'origin');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('never posts a comment twice when its outcome was lost', async () => {
|
|
518
|
+
const setup = canonicalMutationOptions();
|
|
519
|
+
setup.calls.comment.mockRejectedValueOnce(Object.assign(new Error('socket closed'), { code: 'ECONNRESET' }));
|
|
520
|
+
const app = appWith(setup.options);
|
|
521
|
+
const body = { ...mutationBody({ target: { project: { owner: 'team', name: 'repo' }, number: 5 } }), body: 'Looks good' };
|
|
522
|
+
|
|
523
|
+
await request(app).post('/api/source-control/gitlab/pr/comment').send(body).expect(409);
|
|
524
|
+
const replay = await request(app).post('/api/source-control/gitlab/pr/comment').send(body).expect(409);
|
|
525
|
+
|
|
526
|
+
expect(replay.body.code).toBe('SOURCE_CONTROL_MUTATION_OUTCOME_UNKNOWN');
|
|
527
|
+
expect(setup.calls.comment).toHaveBeenCalledOnce();
|
|
528
|
+
});
|
|
529
|
+
|
|
406
530
|
it('reconciles a durable create after restart without repeating the write', async () => {
|
|
407
531
|
let failCompletion = true;
|
|
408
532
|
const mutationStore = makeMutationStore([], async () => {
|
|
@@ -638,10 +762,11 @@ describe('GitLab routes', () => {
|
|
|
638
762
|
|
|
639
763
|
expect(createClient).toHaveBeenCalledTimes(2);
|
|
640
764
|
expect(createClient).toHaveBeenCalledWith({ origin, token: 'bound-token', tokenType: 'oauth' });
|
|
641
|
-
expect(listChangeRequests).toHaveBeenCalledWith('/repo', { page: 1, query: undefined, remote: 'upstream' });
|
|
765
|
+
expect(listChangeRequests).toHaveBeenCalledWith('/repo', { page: 1, query: undefined, state: 'open', people: 'any', remote: 'upstream' });
|
|
642
766
|
expect(changeRequestContext).toHaveBeenCalledWith('/repo', 5, {
|
|
643
767
|
includeDiff: true,
|
|
644
768
|
includeCIDetails: true,
|
|
769
|
+
includeTimeline: false,
|
|
645
770
|
project: { owner: 'team', name: 'repo' },
|
|
646
771
|
remote: 'upstream',
|
|
647
772
|
constrainToPrimary: true,
|
|
@@ -715,7 +840,7 @@ describe('GitLab routes', () => {
|
|
|
715
840
|
expect(createClient).toHaveBeenCalledTimes(5);
|
|
716
841
|
expect(createClient).toHaveBeenCalledWith({ origin, token: 'bound-token', tokenType: 'oauth' });
|
|
717
842
|
expect(createResourceService).toHaveBeenCalledWith(expect.objectContaining({ canonicalReads: true }));
|
|
718
|
-
expect(listIssues).toHaveBeenCalledWith('/repo', { page: 2, query: 'bug', remote: 'upstream' });
|
|
843
|
+
expect(listIssues).toHaveBeenCalledWith('/repo', { page: 2, query: 'bug', state: 'open', people: 'any', remote: 'upstream' });
|
|
719
844
|
expect(getIssue).toHaveBeenCalledWith('/repo', 3, { owner: 'team', name: 'repo' }, 'upstream');
|
|
720
845
|
expect(issueComments).toHaveBeenCalledWith('/repo', 3, { owner: 'team', name: 'repo' }, 'upstream');
|
|
721
846
|
expect(projectUpstream).toHaveBeenCalledWith('/repo', 'upstream');
|
|
@@ -1087,3 +1212,62 @@ describe('GitLab routes', () => {
|
|
|
1087
1212
|
expect(active.body).toMatchObject({ connected: true, cli: { available: true, active: true } });
|
|
1088
1213
|
});
|
|
1089
1214
|
});
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
describe('merge request head for a worktree made by number', () => {
|
|
1218
|
+
const account = { id: `${origin}#2`, token: 'bound-token', user: { id: 2 }, source: 'oauth', scope: 'api', status: 'valid' };
|
|
1219
|
+
const register = (changeRequest, store = makeStore(account)) => {
|
|
1220
|
+
const changeRequestContext = vi.fn(async () => ({ changeRequest }));
|
|
1221
|
+
const gitlab = registerGitLabRoutes(express(), {
|
|
1222
|
+
authFile: '/unused/source-control-auth.json',
|
|
1223
|
+
execFile: async () => ({ stdout: '' }),
|
|
1224
|
+
store,
|
|
1225
|
+
createClient: vi.fn(() => ({})),
|
|
1226
|
+
createResourceService: vi.fn(() => ({ changeRequestContext })),
|
|
1227
|
+
validateReadContext: async (context) => ({ ...context, primaryRemote: 'upstream' }),
|
|
1228
|
+
});
|
|
1229
|
+
return { gitlab, changeRequestContext };
|
|
1230
|
+
};
|
|
1231
|
+
const context = {
|
|
1232
|
+
provider: 'gitlab', instance: origin, directory: '/repo', repositoryId: 'repo_one',
|
|
1233
|
+
accountId: account.id, bindingRevision: 3, primaryRemote: 'upstream',
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
it('answers the active account, and none once it is invalid', async () => {
|
|
1237
|
+
const store = makeStore(account);
|
|
1238
|
+
const { gitlab } = register(null, store);
|
|
1239
|
+
await expect(gitlab.readCurrentAccountId(origin)).resolves.toBe(account.id);
|
|
1240
|
+
await store.markAccountInvalid(origin, account.id);
|
|
1241
|
+
await expect(gitlab.readCurrentAccountId(origin)).resolves.toBeNull();
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('falls back to the active account when the glab login it was switched to stopped working', async () => {
|
|
1245
|
+
const store = makeStore(account);
|
|
1246
|
+
await store.setCliActive(origin, true);
|
|
1247
|
+
const { gitlab } = register(null, store);
|
|
1248
|
+
await expect(gitlab.readCurrentAccountId(origin)).resolves.toBe(account.id);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it('reads the target project and the fork head on the primary project network', async () => {
|
|
1252
|
+
const { gitlab, changeRequestContext } = register({
|
|
1253
|
+
number: 7, head: 'feature/fix', headSha: 'C'.repeat(40),
|
|
1254
|
+
project: { id: '11', owner: 'team', name: 'repo' },
|
|
1255
|
+
headProject: { id: '22', owner: 'alice', name: 'repo' },
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
await expect(gitlab.readChangeRequestHead({ context, number: 7 })).resolves.toEqual({
|
|
1259
|
+
project: { id: '11', owner: 'team', name: 'repo' },
|
|
1260
|
+
headSha: 'c'.repeat(40),
|
|
1261
|
+
headBranch: 'feature/fix',
|
|
1262
|
+
headOwner: 'alice',
|
|
1263
|
+
});
|
|
1264
|
+
expect(changeRequestContext).toHaveBeenCalledExactlyOnceWith('/repo', 7, { remote: 'upstream', constrainToPrimary: true });
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it('refuses a merge request answer without a usable head', async () => {
|
|
1268
|
+
const { gitlab } = register({ number: 7, head: 'feature/fix', headSha: '', project: { id: '11', owner: 'team', name: 'repo' } });
|
|
1269
|
+
|
|
1270
|
+
await expect(gitlab.readChangeRequestHead({ context, number: 7 }))
|
|
1271
|
+
.rejects.toMatchObject({ code: 'MALFORMED_PROVIDER_RESPONSE' });
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
@@ -1,5 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export const isPlainObject = (value) => value === Object(value)
|
|
4
|
-
&& !Array.isArray(value)
|
|
5
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
1
|
+
export { isString, isPlainObject } from '../shared/guards.js';
|
|
@@ -46,7 +46,7 @@ Folder installs store their realpath with `source: "path"`. ZIP and Git copies l
|
|
|
46
46
|
|
|
47
47
|
OAuth and pasted tokens live in `{openchamberDataDir}/guest-auth.json`, each entry with the `target` it was minted for (`apiOrigin`, and for OAuth `authorizeUrl` and `tokenUrl`). `takeUsableGuestAuth` runs before every use (proxy, refresh, the status card): tokens whose target differs from what the package declares now, or that carry no target, are dropped on the spot, whatever the grants say. A pending authorization carries the same target; the callback refuses the exchange (`TARGET_CHANGED`) when the package changed its endpoints in between, and the exchange itself goes to the pending target's `tokenUrl`, never to the current manifest's. The client id and secret carry their own `clientTarget`, written by `PUT /api/guests/:id/oauth/client` (`saveGuestOAuthClient`: an empty secret field keeps the stored secret only for the same client id and the same endpoints, decided under the store lock; a new id or moved endpoints drop it); `usableClientCredentials` treats a client entered for other endpoints as absent (`CLIENT_MISSING` on Connect, `hasClient: false` on the card), so a moved package gets no old secret and the user enters new credentials. Every token write that follows a network round trip (the code exchange, a refresh) goes through `patchGuestAuth` with an `expect` predicate checked under the store lock: the entry must still hold the tokens and target the round trip started from, else the answer is dropped and the entry left as it is. The proxy's cleanup after a 401 is scoped the same way: `dropGuestTokens` with an `expect` on the access token the request was refused with, so a refresh that came back empty because the user reconnected meanwhile never erases the new connection. Host Linear tokens stay in `linear-auth.json`. Writes are atomic and `0o600`. The file is per OpenChamber instance, same as `extensions.json`. Web and desktop share it. VS Code and mobile set the guest catalog to `unsupported`. That is not an empty ready list. Those cards stay hidden.
|
|
48
48
|
|
|
49
|
-
A missing folder is omitted from the list. Other guests stay. A corrupt `extensions.json` is 500, not an empty catalog. A missing or blank `guest-auth.json` is an empty store. Corrupt JSON there is 500, not a disconnected card. This is not Settings → Plugins.
|
|
49
|
+
A missing folder is omitted from the list. Other guests stay. The renderer reads catalog rows one by one (`packages/ui/src/lib/guests/parse.ts`): a row it cannot read, such as one from a newer server with a value this build does not know, shows in Settings → Extensions with its name and Remove, and the other rows still load. An install answered with such a row is reported as added, and the list reloads to show it. A corrupt `extensions.json` is 500, not an empty catalog. A missing or blank `guest-auth.json` is an empty store. Corrupt JSON there is 500, not a disconnected card. This is not Settings → Plugins.
|
|
50
50
|
|
|
51
51
|
## Invariants
|
|
52
52
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import { dropGuestTokens, getGuestAuth, patchGuestAuth } from './auth-store.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
|
|
14
14
|
|
|
15
15
|
const pendingByState = new Map();
|
|
16
16
|
const pendingByGuestId = new Map();
|
|
@@ -262,7 +262,7 @@ export const storedTokensUsable = (stored, integration) => (
|
|
|
262
262
|
* the package names now. Anything else is treated as missing.
|
|
263
263
|
* @returns {{ clientId: string, clientSecret: string }}
|
|
264
264
|
*/
|
|
265
|
-
|
|
265
|
+
const usableClientCredentials = (stored, integration) => {
|
|
266
266
|
if (!sameTarget(stored?.clientTarget, credentialTarget(integration))) {
|
|
267
267
|
return { clientId: '', clientSecret: '' };
|
|
268
268
|
}
|
|
@@ -504,7 +504,7 @@ const INHERITED_SERVICE_ENV_NAMES = new Set([
|
|
|
504
504
|
]);
|
|
505
505
|
|
|
506
506
|
/** @param {NodeJS.ProcessEnv} source */
|
|
507
|
-
|
|
507
|
+
const inheritedServiceEnv = (source) => {
|
|
508
508
|
/** @type {Record<string, string>} */
|
|
509
509
|
const env = {};
|
|
510
510
|
for (const [name, value] of Object.entries(source)) {
|
|
@@ -23,7 +23,7 @@ export const expandHomePath = (value, home = process.env.HOME || os.homedir()) =
|
|
|
23
23
|
* @param {NodeJS.Platform} [platform]
|
|
24
24
|
* @param {string} [home]
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
const platformSocketCandidates = (
|
|
27
27
|
binding,
|
|
28
28
|
platform = process.platform,
|
|
29
29
|
home = process.env.HOME || os.homedir(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const INJECTED_ENV_KEY: 'OPENCHAMBER_INJECTED_ENV';
|
|
2
|
+
|
|
3
|
+
export function injectedEnvKeys(env: Record<string, string | undefined>): Set<string>;
|
|
4
|
+
|
|
5
|
+
export function assignInjectedEnv<T extends Record<string, string | undefined>>(
|
|
6
|
+
env: T,
|
|
7
|
+
values: Record<string, string | undefined>,
|
|
8
|
+
): T;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variables OpenChamber puts into an environment other processes inherit: its
|
|
3
|
+
* own host process (the desktop app, `serve --foreground`), the managed
|
|
4
|
+
* OpenCode server, and through them the integrated terminal and an agent's
|
|
5
|
+
* shell. Every such write goes through `assignInjectedEnv`, which also records
|
|
6
|
+
* the variable's name in `OPENCHAMBER_INJECTED_ENV`. A command run from one of
|
|
7
|
+
* those shells (`openchamber startup enable`) reads the record to tell
|
|
8
|
+
* OpenChamber's configuration from what the user exported, with no list of
|
|
9
|
+
* names that could drift from the code that sets them (#4604).
|
|
10
|
+
*
|
|
11
|
+
* A write that leaves the value as it was is not recorded: the shell already
|
|
12
|
+
* had that value, so it is the user's, and a user's `OPENCODE_PASSWORD` that
|
|
13
|
+
* OpenChamber passes through to the managed OpenCode stays theirs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const INJECTED_ENV_KEY = 'OPENCHAMBER_INJECTED_ENV';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Names recorded in `env`, plus the record itself. Empty when nothing was.
|
|
20
|
+
* @param {Record<string, string | undefined>} env
|
|
21
|
+
* @returns {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
export function injectedEnvKeys(env) {
|
|
24
|
+
const record = env[INJECTED_ENV_KEY] ?? '';
|
|
25
|
+
if (record.length === 0) return new Set();
|
|
26
|
+
const keys = new Set(record.split(',').filter((key) => key.length > 0));
|
|
27
|
+
keys.add(INJECTED_ENV_KEY);
|
|
28
|
+
return keys;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set each defined value in `env` and record the names whose value changed.
|
|
33
|
+
* @template {Record<string, string | undefined>} T
|
|
34
|
+
* @param {T} env
|
|
35
|
+
* @param {Record<string, string | undefined>} values
|
|
36
|
+
* @returns {T}
|
|
37
|
+
*/
|
|
38
|
+
export function assignInjectedEnv(env, values) {
|
|
39
|
+
const recorded = injectedEnvKeys(env);
|
|
40
|
+
recorded.delete(INJECTED_ENV_KEY);
|
|
41
|
+
for (const [key, value] of Object.entries(values)) {
|
|
42
|
+
if (value === undefined || env[key] === value) continue;
|
|
43
|
+
env[key] = value;
|
|
44
|
+
recorded.add(key);
|
|
45
|
+
}
|
|
46
|
+
if (recorded.size > 0) {
|
|
47
|
+
env[INJECTED_ENV_KEY] = [...recorded].join(',');
|
|
48
|
+
}
|
|
49
|
+
return env;
|
|
50
|
+
}
|