@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
|
@@ -313,6 +313,29 @@ describe('Git network operation planner', () => {
|
|
|
313
313
|
expect(JSON.stringify(plans.publicPlan)).not.toContain('credential_one');
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
it('plans clone and checkout hydration transfers to ssh:// endpoints that name their user', async () => {
|
|
317
|
+
const endpoint = 'ssh://git@modules.example/library.git';
|
|
318
|
+
const publicEndpoint = { displayUrl: 'ssh://modules.example/library.git', fingerprint: fingerprintRemoteUrl(endpoint) };
|
|
319
|
+
const clone = await makePlanner().planNetworkOperation({
|
|
320
|
+
operation: 'clone', remoteUrl: endpoint, destinationPath: '/new/repo', transportMode: 'system', unverifiedConfirmed: true,
|
|
321
|
+
});
|
|
322
|
+
expect(clone.internalPlan.rawEndpoint).toBe(endpoint);
|
|
323
|
+
expect(clone.publicPlan.target.remote).toEqual(publicEndpoint);
|
|
324
|
+
|
|
325
|
+
const plans = await makePlanner({ inspectCheckoutHydration: async () => ({
|
|
326
|
+
headSha: SHA,
|
|
327
|
+
requirements: [{ kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint }],
|
|
328
|
+
transfers: [{ kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint, rawEndpoint: endpoint }],
|
|
329
|
+
}) }).planNetworkOperation({
|
|
330
|
+
operation: 'checkout-hydration', directory: '/repository', repositoryId: 'repo_one',
|
|
331
|
+
bindingRevision: 4, configRevision: 'config_one', remote: existingInput('fetch').remote,
|
|
332
|
+
});
|
|
333
|
+
expect(plans.internalPlan.plannedTransfers).toEqual([{
|
|
334
|
+
kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint, rawEndpoint: endpoint,
|
|
335
|
+
}]);
|
|
336
|
+
expect(JSON.stringify(plans.publicPlan)).not.toContain('git@');
|
|
337
|
+
});
|
|
338
|
+
|
|
316
339
|
it('rejects stale checkout hydration source before inspection and rejects absolute discovered paths', async () => {
|
|
317
340
|
const inspectCheckoutHydration = vi.fn(async () => ({ headSha: SHA, requirements: [], transfers: [] }));
|
|
318
341
|
const input = {
|
|
@@ -479,7 +502,9 @@ describe('Git network operation planner', () => {
|
|
|
479
502
|
'https://user@example.com/repo.git',
|
|
480
503
|
'https://example.com/repo.git?access_token=secret',
|
|
481
504
|
'https://example.com/repo.git#private',
|
|
482
|
-
'ssh://git@example.com/owner/repo.git',
|
|
505
|
+
'ssh://git:secret@example.com/owner/repo.git',
|
|
506
|
+
'ssh://:secret@example.com/owner/repo.git',
|
|
507
|
+
'ssh://-oProxyCommand=touch@example.com/owner/repo.git',
|
|
483
508
|
'ssh://example.com/owner/repo.git?key=secret',
|
|
484
509
|
'git@example.com:owner/repo.git?key=secret',
|
|
485
510
|
'git@example.com:owner/repo.git#private',
|
|
@@ -506,6 +531,46 @@ describe('Git network operation planner', () => {
|
|
|
506
531
|
await expect(planner.planNetworkOperation(input)).resolves.toHaveProperty('publicPlan.target.remote.endpoint.displayUrl', endpoint);
|
|
507
532
|
});
|
|
508
533
|
|
|
534
|
+
it('plans every existing-remote operation for an ssh:// endpoint that names its user, keeping the user out of public data', async () => {
|
|
535
|
+
const endpoint = 'ssh://git@example.com/owner/repo.git';
|
|
536
|
+
const displayUrl = 'ssh://example.com/owner/repo.git';
|
|
537
|
+
const endpointAuthority = { ...authority, endpoint, endpointFingerprint: fingerprintRemoteUrl(endpoint) };
|
|
538
|
+
expect(endpointAuthority.endpointFingerprint).toBe(fingerprintRemoteUrl(displayUrl));
|
|
539
|
+
const remote = (name) => ({ name, endpoint: { displayUrl, fingerprint: endpointAuthority.endpointFingerprint } });
|
|
540
|
+
const deletion = existingInput('delete-remote-branch', { remote: remote('upstream') });
|
|
541
|
+
delete deletion.sourceRef;
|
|
542
|
+
const inputs = [
|
|
543
|
+
existingInput('fetch', { remote: remote('upstream') }),
|
|
544
|
+
existingInput('pull', { remote: remote('upstream') }),
|
|
545
|
+
existingInput('push', { remote: remote('upstream') }),
|
|
546
|
+
deletion,
|
|
547
|
+
syncInput({
|
|
548
|
+
fetch: { ...syncInput().fetch, remote: remote('upstream') },
|
|
549
|
+
push: { ...syncInput().push, remote: remote('origin') },
|
|
550
|
+
}),
|
|
551
|
+
];
|
|
552
|
+
for (const input of inputs) {
|
|
553
|
+
const plans = await makePlanner({ validateGitTransportContext: async () => endpointAuthority }).planNetworkOperation(input);
|
|
554
|
+
expect(JSON.stringify(plans.publicPlan)).not.toContain('git@');
|
|
555
|
+
const rawEndpoints = input.operation === 'sync'
|
|
556
|
+
? [plans.internalPlan.fetch.rawEndpoint, plans.internalPlan.push.rawEndpoint]
|
|
557
|
+
: [plans.internalPlan.rawEndpoint];
|
|
558
|
+
expect(rawEndpoints).toEqual(rawEndpoints.map(() => endpoint));
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('rejects a URL display endpoint that carries userinfo', async () => {
|
|
563
|
+
const endpoint = 'ssh://git@modules.example/child.git';
|
|
564
|
+
await expect(makePlanner().planNetworkOperation({
|
|
565
|
+
operation: 'clone', remoteUrl: authority.endpoint, destinationPath: '/new/repo',
|
|
566
|
+
transportMode: 'system', unverifiedConfirmed: true,
|
|
567
|
+
auxiliaryGrants: [{
|
|
568
|
+
kind: 'submodule', endpoint: { displayUrl: endpoint, fingerprint: fingerprintRemoteUrl(endpoint) },
|
|
569
|
+
transportMode: 'system', unverifiedConfirmed: true,
|
|
570
|
+
}],
|
|
571
|
+
})).rejects.toThrow('Git endpoint is invalid');
|
|
572
|
+
});
|
|
573
|
+
|
|
509
574
|
it.each([
|
|
510
575
|
'http://example.com/repo.git',
|
|
511
576
|
'file:///tmp/repo',
|
|
@@ -514,6 +579,8 @@ describe('Git network operation planner', () => {
|
|
|
514
579
|
'ext::command repo',
|
|
515
580
|
'custom://example.com/repo',
|
|
516
581
|
'https://user:secret@example.com/repo.git',
|
|
582
|
+
'https://user@example.com/repo.git',
|
|
583
|
+
'ssh://git:secret@example.com/owner/repo.git',
|
|
517
584
|
'https://example.com/repo.git?token=secret',
|
|
518
585
|
'git@example.com:../repo',
|
|
519
586
|
])('rejects unsafe clone endpoint %s', async (remoteUrl) => {
|
|
@@ -590,6 +657,41 @@ describe('Git network operation planner', () => {
|
|
|
590
657
|
.rejects.toMatchObject({ code: 'GIT_NETWORK_OPERATION_AUTHORITY_CHANGED', status: 409 });
|
|
591
658
|
});
|
|
592
659
|
|
|
660
|
+
// A sha256/base64url digest can legitimately start with '-' (~1 in 64). These
|
|
661
|
+
// are opaque values the server generated and the client echoes back, never CLI
|
|
662
|
+
// arguments, so the leading-dash injection guard must not reject them.
|
|
663
|
+
const DASH_DIGEST = '-GtzS0gxC9C70rmAQhz4wNDSF-h_ePs0Ztxk1GUtHns';
|
|
664
|
+
|
|
665
|
+
it('accepts a configRevision that begins with a dash', async () => {
|
|
666
|
+
const plans = await makePlanner().planNetworkOperation(existingInput('fetch', {
|
|
667
|
+
configRevision: DASH_DIGEST,
|
|
668
|
+
}));
|
|
669
|
+
expect(plans.publicPlan.target.configRevision).toBe(DASH_DIGEST);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('accepts endpoint and transport digests that begin with a dash', async () => {
|
|
673
|
+
const endpoint = 'https://example.com/owner/repository.git';
|
|
674
|
+
const planner = makePlanner({
|
|
675
|
+
validateGitTransportContext: vi.fn(async () => ({
|
|
676
|
+
...authority,
|
|
677
|
+
endpoint,
|
|
678
|
+
endpointFingerprint: DASH_DIGEST,
|
|
679
|
+
transportRevision: DASH_DIGEST,
|
|
680
|
+
})),
|
|
681
|
+
});
|
|
682
|
+
const plans = await planner.planNetworkOperation(existingInput('fetch', {
|
|
683
|
+
remote: { name: 'upstream', endpoint: { displayUrl: endpoint, fingerprint: DASH_DIGEST } },
|
|
684
|
+
}));
|
|
685
|
+
expect(plans.publicPlan.target.remote.endpoint.fingerprint).toBe(DASH_DIGEST);
|
|
686
|
+
expect(plans.internalPlan.transportRevision).toBe(DASH_DIGEST);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('still rejects a digest carrying control characters', async () => {
|
|
690
|
+
await expect(makePlanner().planNetworkOperation(existingInput('fetch', {
|
|
691
|
+
configRevision: 'cfg\nbad',
|
|
692
|
+
}))).rejects.toMatchObject({ code: 'INVALID_GIT_NETWORK_OPERATION' });
|
|
693
|
+
});
|
|
694
|
+
|
|
593
695
|
it('plans clone into a deterministic operation-owned sibling', async () => {
|
|
594
696
|
const stat = vi.fn(async () => { const error = new Error('missing'); error.code = 'ENOENT'; throw error; });
|
|
595
697
|
const plans = await makePlanner({ fsImpl: { stat } }).planNetworkOperation({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_MAX_ENTRIES = 256;
|
|
2
4
|
const DEFAULT_TERMINAL_RETENTION_MS = 60 * 60 * 1000;
|
|
3
5
|
const DEFAULT_PLANNED_RETENTION_MS = 15 * 60 * 1000;
|
|
@@ -11,11 +13,11 @@ const ERROR_CODES = new Set([
|
|
|
11
13
|
'INVALID_REQUEST', 'NOT_FOUND', 'STALE_REPOSITORY', 'STALE_BINDING', 'STALE_CONFIG',
|
|
12
14
|
'REMOTE_CHANGED', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
|
|
13
15
|
'CONFLICT', 'CANCELLED', 'TIMEOUT', 'OUTCOME_UNKNOWN', 'RUNTIME_UNSUPPORTED', 'UNKNOWN',
|
|
14
|
-
'GIT_LFS_CLIENT_MISSING',
|
|
16
|
+
'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE',
|
|
15
17
|
]);
|
|
16
18
|
const FAILURE_ERROR_CODES = new Set([
|
|
17
19
|
'INVALID_REQUEST', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
|
|
18
|
-
'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'UNKNOWN',
|
|
20
|
+
'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE', 'UNKNOWN',
|
|
19
21
|
]);
|
|
20
22
|
const STATE_ERROR_CODES = Object.freeze({
|
|
21
23
|
partial: FAILURE_ERROR_CODES,
|
|
@@ -74,10 +76,6 @@ const isValidHydration = (value) => isPlainObject(value)
|
|
|
74
76
|
&& value.lfs.length <= 256
|
|
75
77
|
&& value.lfs.every((entry) => isHydrationPart(entry, true))
|
|
76
78
|
&& expectedHydrationStatus([...value.submodules, ...value.lfs]) === value.status;
|
|
77
|
-
const isPlainObject = (value) => value === Object(value)
|
|
78
|
-
&& !Array.isArray(value)
|
|
79
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
80
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
81
79
|
const isBoundedString = (value, max) => isString(value) && value.trim() === value
|
|
82
80
|
&& value.length > 0 && value.length <= max && !/[\0\r\n]/.test(value);
|
|
83
81
|
const isSafeDisplayEndpoint = (value) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -20,11 +21,11 @@ const ERROR_CODES = new Set([
|
|
|
20
21
|
'INVALID_REQUEST', 'NOT_FOUND', 'STALE_REPOSITORY', 'STALE_BINDING', 'STALE_CONFIG',
|
|
21
22
|
'REMOTE_CHANGED', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
|
|
22
23
|
'CONFLICT', 'CANCELLED', 'TIMEOUT', 'OUTCOME_UNKNOWN', 'RUNTIME_UNSUPPORTED', 'UNKNOWN',
|
|
23
|
-
'GIT_LFS_CLIENT_MISSING',
|
|
24
|
+
'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE',
|
|
24
25
|
]);
|
|
25
26
|
const FAILURE_ERROR_CODES = new Set([
|
|
26
27
|
'INVALID_REQUEST', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
|
|
27
|
-
'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'UNKNOWN',
|
|
28
|
+
'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE', 'UNKNOWN',
|
|
28
29
|
]);
|
|
29
30
|
const STATE_ERROR_CODES = Object.freeze({
|
|
30
31
|
partial: FAILURE_ERROR_CODES,
|
|
@@ -54,12 +55,9 @@ const SAFE_ERROR_MESSAGES = Object.freeze({
|
|
|
54
55
|
RUNTIME_UNSUPPORTED: 'Git operation is unsupported by this runtime',
|
|
55
56
|
UNKNOWN: 'Git operation failed; inspect repository state before retrying',
|
|
56
57
|
GIT_LFS_CLIENT_MISSING: 'Git LFS is required but unavailable',
|
|
58
|
+
CHECKOUT_TOO_LARGE: 'Checkout has too many files to inspect',
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
const isPlainObject = (value) => value === Object(value)
|
|
60
|
-
&& !Array.isArray(value)
|
|
61
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
62
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
63
61
|
const exactKeys = (value, required, optional = []) => {
|
|
64
62
|
const keys = Object.keys(value);
|
|
65
63
|
return required.every((key) => keys.includes(key))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
@@ -41,7 +42,6 @@ const MANAGED_ENV_NAMES = new Set([
|
|
|
41
42
|
'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy', 'NETRC',
|
|
42
43
|
]);
|
|
43
44
|
const SHA_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i;
|
|
44
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
45
45
|
|
|
46
46
|
const operationError = (code, message, status = 500, details = {}) => Object.assign(new Error(message), { code, status, ...details });
|
|
47
47
|
const publicError = (code, message) => ({ code, message });
|
|
@@ -796,6 +796,12 @@ export function createNetworkOperations({
|
|
|
796
796
|
if (error?.code === 'GIT_AUXILIARY_AUTHORIZATION_REQUIRED') {
|
|
797
797
|
return { status: 'authorization-required', error: publicError('AUTHENTICATION_REQUIRED', 'This checkout endpoint requires an explicit credential grant') };
|
|
798
798
|
}
|
|
799
|
+
// An LFS discovery bound trips on the number of tracked files. It gets its
|
|
800
|
+
// own code so the message names that; the configuration message sent
|
|
801
|
+
// people to fix settings that were fine.
|
|
802
|
+
if (error?.code === 'LFS_DISCOVERY_LIMIT_EXCEEDED') {
|
|
803
|
+
return { status: 'invalid', error: publicError('CHECKOUT_TOO_LARGE', 'Checkout has too many files to inspect') };
|
|
804
|
+
}
|
|
799
805
|
if (String(error?.code || '').includes('INVALID') || String(error?.code || '').includes('LIMIT_EXCEEDED')
|
|
800
806
|
|| error?.code === 'UNSAFE_LFS_EXECUTABLE_CONFIG') {
|
|
801
807
|
return { status: 'invalid', error: publicError('INVALID_REQUEST', 'Checkout hydration configuration is invalid') };
|
|
@@ -1979,8 +1985,9 @@ export function createNetworkOperations({
|
|
|
1979
1985
|
await controls.markStepCompleted('updated-local-repository');
|
|
1980
1986
|
if (['succeeded', 'not-needed'].includes(hydration.status)) result = { state: 'succeeded', hydration };
|
|
1981
1987
|
else {
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1988
|
+
const specific = [...hydration.submodules, ...hydration.lfs]
|
|
1989
|
+
.find((entry) => ['RUNTIME_UNSUPPORTED', 'CHECKOUT_TOO_LARGE'].includes(entry.error?.code));
|
|
1990
|
+
const failure = specific?.error ?? (hydration.status === 'client-missing'
|
|
1984
1991
|
? publicError('GIT_LFS_CLIENT_MISSING', 'Git LFS is required for this checkout; install git-lfs and retry')
|
|
1985
1992
|
: hydration.status === 'authorization-required'
|
|
1986
1993
|
? publicError('AUTHENTICATION_REQUIRED', 'Checkout hydration requires an explicit endpoint grant')
|
|
@@ -2099,7 +2106,8 @@ export function createNetworkOperations({
|
|
|
2099
2106
|
return { state: 'failed', error: publicError('AUTHENTICATION_REQUIRED', 'Checkout hydration requires an explicit endpoint grant'), hydration };
|
|
2100
2107
|
}
|
|
2101
2108
|
if (hydration.status === 'invalid') {
|
|
2102
|
-
|
|
2109
|
+
const tooLarge = [...hydration.submodules, ...hydration.lfs].find((entry) => entry.error?.code === 'CHECKOUT_TOO_LARGE');
|
|
2110
|
+
return { state: 'failed', error: tooLarge?.error ?? publicError('INVALID_REQUEST', 'Checkout hydration configuration is invalid'), hydration };
|
|
2103
2111
|
}
|
|
2104
2112
|
const unsupported = [...hydration.submodules, ...hydration.lfs].find((entry) => entry.error?.code === 'RUNTIME_UNSUPPORTED');
|
|
2105
2113
|
return { state: 'failed', error: unsupported?.error ?? publicError('TRANSPORT_FAILED', 'Checkout hydration failed'), hydration };
|
|
@@ -2336,17 +2336,27 @@ process.exit(safe ? 0 : 1);
|
|
|
2336
2336
|
});
|
|
2337
2337
|
});
|
|
2338
2338
|
|
|
2339
|
-
|
|
2339
|
+
// Past the old 256-record ceiling too: a repository of thousands of LFS
|
|
2340
|
+
// files reaches the same client check instead of a discovery failure.
|
|
2341
|
+
it.each([1, 3_000])('returns the stable missing git-lfs result for %i LFS files without starting an LFS transfer', async (count) => {
|
|
2340
2342
|
const pointer = `version https://git-lfs.github.com/spec/v1\noid sha256:${'d'.repeat(64)}\nsize 12\n`;
|
|
2343
|
+
const files = Array.from({ length: count }, (_, index) => `assets/asset-${index}.bin`);
|
|
2344
|
+
const batch = (child, separator) => child.stdin.read().toString().slice(0, -1).split(separator);
|
|
2341
2345
|
const setupValue = setup({
|
|
2342
2346
|
spawnResponder: ({ args }) => {
|
|
2343
2347
|
const command = args.join(' ');
|
|
2344
2348
|
if (command.includes('config --blob HEAD:.gitmodules')) return { code: 1 };
|
|
2345
2349
|
if (command.includes('ls-tree -rz')) return { code: 0 };
|
|
2346
|
-
if (command.includes('ls-files -z')) return { code: 0, stdout: '
|
|
2347
|
-
if (command.includes('check-attr')) return {
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
+
if (command.includes('ls-files -z')) return { code: 0, stdout: `${files.join('\0')}\0` };
|
|
2351
|
+
if (command.includes('check-attr')) return { onSpawn: ({ child }) => {
|
|
2352
|
+
child.stdout.write(batch(child, '\0').map((file) => `${file}\0filter\0lfs\0`).join(''));
|
|
2353
|
+
} };
|
|
2354
|
+
if (command.includes('cat-file --batch-check')) return { onSpawn: ({ child }) => {
|
|
2355
|
+
child.stdout.write(batch(child, '\n').map(() => `${SHA} blob ${Buffer.byteLength(pointer)}\n`).join(''));
|
|
2356
|
+
} };
|
|
2357
|
+
if (command.includes('cat-file --batch')) return { onSpawn: ({ child }) => {
|
|
2358
|
+
child.stdout.write(batch(child, '\n').map(() => `${SHA} blob ${Buffer.byteLength(pointer)}\n${pointer}\n`).join(''));
|
|
2359
|
+
} };
|
|
2350
2360
|
if (command.includes('HEAD:.lfsconfig') || command.includes('config --includes')) return { code: 1 };
|
|
2351
2361
|
if (command.includes('lfs version')) return { code: 1, stderr: 'git-lfs is not installed at /private/bin' };
|
|
2352
2362
|
return { code: 0 };
|
|
@@ -2502,7 +2512,10 @@ process.exit(safe ? 0 : 1);
|
|
|
2502
2512
|
plan = await operation.service.plan({ operation: 'clone', remoteUrl: ENDPOINT, destinationPath: destination, transportMode: 'system', unverifiedConfirmed: true });
|
|
2503
2513
|
const result = await operation.service.execute(plan.operationId);
|
|
2504
2514
|
expect(result).toMatchObject(interruption === 'overflow'
|
|
2505
|
-
? {
|
|
2515
|
+
? {
|
|
2516
|
+
state: 'partial', error: { code: 'CHECKOUT_TOO_LARGE' },
|
|
2517
|
+
hydration: { status: 'invalid', lfs: [{ status: 'invalid', error: { code: 'CHECKOUT_TOO_LARGE' } }] },
|
|
2518
|
+
}
|
|
2506
2519
|
: { state: 'cancelled', error: { code: interruption.startsWith('CANCELLED') ? 'CANCELLED' : interruption }, hydration: { status: 'cancelled' } });
|
|
2507
2520
|
expect(metadataBatches).toBe(2);
|
|
2508
2521
|
expect(operation.calls.filter((call) => call.args.includes('cat-file'))).toHaveLength(4);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_MAX_CHARS = 8_192;
|
|
2
4
|
const REDACTED = '[redacted]';
|
|
3
5
|
const SENSITIVE_QUERY_KEYS = /^(?:access_token|auth|authorization|credential|key|oauth_token|password|private_token|secret|token)$/i;
|
|
4
6
|
const URL_PATTERN = /[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi;
|
|
5
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
6
7
|
|
|
7
8
|
const stringValue = (value) => {
|
|
8
9
|
if (value instanceof Error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isString } from '../shared/guards.js';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -11,8 +12,6 @@ const MAX_QUERY_BYTES = 64 * 1024;
|
|
|
11
12
|
const NONE = Object.freeze({ mode: 'none' });
|
|
12
13
|
const SYSTEM = Object.freeze({ mode: 'system' });
|
|
13
14
|
|
|
14
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
15
|
-
|
|
16
15
|
/** The origin a redacted remote URL points at, or null when it is not HTTPS. */
|
|
17
16
|
const httpsOrigin = (displayUrl) => {
|
|
18
17
|
try {
|
|
@@ -122,6 +121,7 @@ export function createGitRepositoryCredentialRuntime({
|
|
|
122
121
|
getActivePort,
|
|
123
122
|
getActiveHost = () => null,
|
|
124
123
|
helperPath = HELPER_PATH,
|
|
124
|
+
helperLaunch,
|
|
125
125
|
randomBytes = crypto.randomBytes,
|
|
126
126
|
}) {
|
|
127
127
|
if (!(readBinding instanceof Function) || !credentialResolver || !isString(dataDir)
|
|
@@ -205,7 +205,9 @@ export function createGitRepositoryCredentialRuntime({
|
|
|
205
205
|
activeSecret = randomBytes(32).toString('base64url');
|
|
206
206
|
const url = `http://${callbackHost()}:${port}/api/git/repository-credential`;
|
|
207
207
|
await writePrivate(endpointFilePath, `${JSON.stringify({ version: 1, url, secret: activeSecret })}\n`, 0o600);
|
|
208
|
-
|
|
208
|
+
// No `exec`: under Electron the command starts with a variable
|
|
209
|
+
// assignment, which `exec` would take for the program to run.
|
|
210
|
+
const script = `#!/bin/sh\n${helperShellCommand(path.resolve(helperPath), [endpointFilePath], helperLaunch)} "$@"\n`;
|
|
209
211
|
await writePrivate(launcherPath, script, 0o700);
|
|
210
212
|
},
|
|
211
213
|
registerRoutes: (app) => {
|
|
@@ -64,6 +64,28 @@ describe('createGitRepositoryCredentialRuntime', () => {
|
|
|
64
64
|
expect(runtime.helperCommand()).toBe(`!'${path.join(dataDir, 'bin', 'git-credential-openchamber')}'`);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it.skipIf(process.platform === 'win32')('publishes a launcher that runs the helper as Node under Electron', async () => {
|
|
68
|
+
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openchamber-repo-credential-'));
|
|
69
|
+
roots.push(dataDir);
|
|
70
|
+
const executable = path.join(dataDir, 'electron');
|
|
71
|
+
await fs.writeFile(executable, '#!/bin/sh\nprintf "%s|%s" "$ELECTRON_RUN_AS_NODE" "$*"\n', { mode: 0o700 });
|
|
72
|
+
const runtime = createGitRepositoryCredentialRuntime({
|
|
73
|
+
readBinding: async () => read(null),
|
|
74
|
+
credentialResolver: { resolve: async () => null },
|
|
75
|
+
dataDir, fsPromises: fs,
|
|
76
|
+
getActivePort: () => 4399,
|
|
77
|
+
helperPath: '/opt/openchamber/repository-credential-helper.js',
|
|
78
|
+
helperLaunch: { execPath: executable, versions: { electron: '1.0.0' } },
|
|
79
|
+
});
|
|
80
|
+
await runtime.publish();
|
|
81
|
+
|
|
82
|
+
const launcher = path.join(dataDir, 'bin', 'git-credential-openchamber');
|
|
83
|
+
const output = await new Promise((resolve, reject) => {
|
|
84
|
+
execFile(launcher, ['get'], (error, stdout) => (error ? reject(error) : resolve(stdout)));
|
|
85
|
+
});
|
|
86
|
+
expect(output).toBe(`1|/opt/openchamber/repository-credential-helper.js ${path.join(dataDir, 'git-credential-endpoint.json')} get`);
|
|
87
|
+
});
|
|
88
|
+
|
|
67
89
|
it('answers the bound account credential for the repository the helper runs in', async () => {
|
|
68
90
|
const { runtime, dataDir, resolved } = await setup();
|
|
69
91
|
await runtime.publish();
|
package/server/lib/git/routes.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { isPlainObject, isString } from '../shared/guards.js';
|
|
1
2
|
import { OpenCode } from '@opencode/client';
|
|
2
3
|
import { createGitRedactor, redactGitText } from './redaction.js';
|
|
3
4
|
import { runAsUserAction } from '../environment/refresh-scope.js';
|
|
4
5
|
import { redactRemoteUrl } from '../source-control/url-redaction.js';
|
|
5
6
|
import { parsePublicGitIdentityProfile, toPublicGitIdentityProfile } from './identity-storage.js';
|
|
7
|
+
import { WorktreeCreationError, createWorktreeCreation, hasClientContributorAuthority } from './worktree-creation.js';
|
|
6
8
|
|
|
7
9
|
const NETWORK_OPERATION_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
8
|
-
const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
9
|
-
const isPlainObject = (value) => value === Object(value)
|
|
10
|
-
&& !Array.isArray(value)
|
|
11
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
12
10
|
const toGitIdentitySummary = (identity) => identity ? {
|
|
13
11
|
userName: identity.userName ?? null,
|
|
14
12
|
userEmail: identity.userEmail ?? null,
|
|
@@ -180,35 +178,15 @@ export function registerGitRoutes(app, {
|
|
|
180
178
|
return res.status(500).json({ code: 'UNKNOWN', error: errors[input.operation] });
|
|
181
179
|
}
|
|
182
180
|
});
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'head'}`;
|
|
193
|
-
const changeRequestSourceOnRepository = async (directory, source) => {
|
|
194
|
-
if (source.classification !== 'same-repository') {
|
|
195
|
-
return source.sourceProject?.owner
|
|
196
|
-
? Object.freeze({ ...source, requestedRemoteName: forkRemoteName(source.sourceProject.owner) })
|
|
197
|
-
: source;
|
|
198
|
-
}
|
|
199
|
-
const { getRepositoryRemoteUrls } = await getGitLibraries();
|
|
200
|
-
const primary = (await getRepositoryRemoteUrls(directory)).find((remote) => remote.name === source.context.primaryRemote);
|
|
201
|
-
if (!primary?.fetchUrl) return source;
|
|
202
|
-
return Object.freeze({ ...source, requestedRemoteName: primary.name, remoteUrl: primary.fetchUrl });
|
|
203
|
-
};
|
|
204
|
-
const worktreeInputForSource = (input, source) => ({
|
|
205
|
-
...input,
|
|
206
|
-
changeRequestSource: undefined,
|
|
207
|
-
contributorFork: source.classification === 'contributor-fork',
|
|
208
|
-
changeRequestTransfer: true,
|
|
209
|
-
ensureRemoteName: source.requestedRemoteName,
|
|
210
|
-
ensureRemoteUrl: source.remoteUrl ?? source.endpoint,
|
|
211
|
-
expectedRevision: source.headSha,
|
|
181
|
+
const worktreeCreation = createWorktreeCreation({
|
|
182
|
+
getGitLibraries,
|
|
183
|
+
networkOperations,
|
|
184
|
+
getSourceControlBinding,
|
|
185
|
+
contributorProvenance,
|
|
186
|
+
resolveChangeRequestSource,
|
|
187
|
+
createHttpsCredentialReference,
|
|
188
|
+
resolveSourceControlAccount,
|
|
189
|
+
worktreeBootstrapStore,
|
|
212
190
|
});
|
|
213
191
|
const rejectLegacyNetworkOperation = async (directory, res) => {
|
|
214
192
|
if (contributorProvenance?.read instanceof Function) {
|
|
@@ -1290,7 +1268,6 @@ export function registerGitRoutes(app, {
|
|
|
1290
1268
|
}
|
|
1291
1269
|
});
|
|
1292
1270
|
|
|
1293
|
-
|
|
1294
1271
|
app.put('/api/git/branches/rename', async (req, res) => {
|
|
1295
1272
|
const { renameBranch } = await getGitLibraries();
|
|
1296
1273
|
try {
|
|
@@ -1310,6 +1287,9 @@ export function registerGitRoutes(app, {
|
|
|
1310
1287
|
const result = await renameBranch(directory, oldName, newName);
|
|
1311
1288
|
res.json(result);
|
|
1312
1289
|
} catch (error) {
|
|
1290
|
+
if (error.statusCode === 409) {
|
|
1291
|
+
return res.status(409).json({ error: error.message });
|
|
1292
|
+
}
|
|
1313
1293
|
console.error('Failed to rename branch:', error);
|
|
1314
1294
|
res.status(500).json({ error: error.message || 'Failed to rename branch' });
|
|
1315
1295
|
}
|
|
@@ -1442,8 +1422,9 @@ export function registerGitRoutes(app, {
|
|
|
1442
1422
|
}
|
|
1443
1423
|
}
|
|
1444
1424
|
}
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1425
|
+
// An empty list means "not a repository", or a bare repository with no
|
|
1426
|
+
// linked worktree yet; neither has topology to track. A worktree created
|
|
1427
|
+
// from the terminal in such a repository shows up on the next list call.
|
|
1447
1428
|
if (worktrees.length > 0) {
|
|
1448
1429
|
void observeWorktreeTopology(directory);
|
|
1449
1430
|
}
|
|
@@ -1475,123 +1456,33 @@ export function registerGitRoutes(app, {
|
|
|
1475
1456
|
if (hasClientContributorAuthority(req.body)) {
|
|
1476
1457
|
return res.status(400).json({ error: 'Invalid contributor worktree request', code: 'INVALID_CONTRIBUTOR_WORKTREE' });
|
|
1477
1458
|
}
|
|
1478
|
-
|
|
1479
|
-
if (input.changeRequestSource) {
|
|
1480
|
-
if (!(resolveChangeRequestSource instanceof Function)) return res.status(501).json({ error: 'Contributor worktrees are unavailable', code: 'RUNTIME_UNSUPPORTED' });
|
|
1481
|
-
const source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
|
|
1482
|
-
input = worktreeInputForSource(input, source);
|
|
1483
|
-
}
|
|
1459
|
+
const input = await worktreeCreation.validationInput(directory, req.body || {});
|
|
1484
1460
|
const result = await validateWorktreeCreate(directory, input);
|
|
1485
1461
|
res.json(result);
|
|
1486
1462
|
} catch (error) {
|
|
1463
|
+
if (error instanceof WorktreeCreationError) {
|
|
1464
|
+
return res.status(error.status).json({ error: error.message, code: error.code });
|
|
1465
|
+
}
|
|
1487
1466
|
console.error('Failed to validate worktree creation:', error);
|
|
1488
1467
|
res.status(500).json({ error: error.message || 'Failed to validate worktree creation' });
|
|
1489
1468
|
}
|
|
1490
1469
|
});
|
|
1491
1470
|
|
|
1492
1471
|
app.post('/api/git/worktrees', userAction(async (req, res) => {
|
|
1493
|
-
const { createWorktree, validateWorktreeCreate } = await getGitLibraries();
|
|
1494
|
-
if (typeof createWorktree !== 'function' || typeof validateWorktreeCreate !== 'function') {
|
|
1495
|
-
return res.status(501).json({ error: 'Worktree creation is not available' });
|
|
1496
|
-
}
|
|
1497
1472
|
try {
|
|
1498
1473
|
const directory = req.query.directory;
|
|
1499
1474
|
if (!directory || typeof directory !== 'string') {
|
|
1500
1475
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
1501
1476
|
}
|
|
1502
|
-
|
|
1503
|
-
if (hasClientContributorAuthority(req.body)) {
|
|
1504
|
-
return res.status(400).json({ error: 'Invalid contributor worktree request', code: 'INVALID_CONTRIBUTOR_WORKTREE' });
|
|
1505
|
-
}
|
|
1506
|
-
if (!(networkOperations?.hydrateBoundCheckout instanceof Function)
|
|
1507
|
-
|| !(worktreeBootstrapStore?.read instanceof Function)
|
|
1508
|
-
|| !(worktreeBootstrapStore?.write instanceof Function)) {
|
|
1509
|
-
return res.status(501).json({ error: 'Worktree checkout bootstrap is not available' });
|
|
1510
|
-
}
|
|
1511
|
-
let input = req.body || {};
|
|
1512
|
-
let source = null;
|
|
1513
|
-
if (input.changeRequestSource) {
|
|
1514
|
-
if (!(resolveChangeRequestSource instanceof Function)
|
|
1515
|
-
|| !(createHttpsCredentialReference instanceof Function)
|
|
1516
|
-
|| !(resolveSourceControlAccount instanceof Function)
|
|
1517
|
-
|| !(networkOperations?.transferContributorHead instanceof Function)) {
|
|
1518
|
-
return res.status(501).json({ error: 'Contributor worktrees are unavailable', code: 'RUNTIME_UNSUPPORTED' });
|
|
1519
|
-
}
|
|
1520
|
-
source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
|
|
1521
|
-
const headBranch = source.headRef.slice('refs/heads/'.length);
|
|
1522
|
-
const destinationRef = `refs/remotes/${source.requestedRemoteName}/${headBranch}`;
|
|
1523
|
-
input = {
|
|
1524
|
-
...worktreeInputForSource(input, source),
|
|
1525
|
-
existingBranch: destinationRef.slice('refs/'.length),
|
|
1526
|
-
setUpstream: false,
|
|
1527
|
-
};
|
|
1528
|
-
const validation = await validateWorktreeCreate(directory, input);
|
|
1529
|
-
if (!validation.ok && !validation.errors.every((entry) => entry.code === 'contributor_transfer_unavailable')) {
|
|
1530
|
-
// The same error shape as every other refusal here, so the reason reaches the user.
|
|
1531
|
-
const collision = validation.errors.find((entry) => entry.code === 'remote_name_collision');
|
|
1532
|
-
return res.status(409).json(collision
|
|
1533
|
-
? { error: collision.message, code: 'CONTRIBUTOR_REMOTE_COLLISION', remoteName: input.ensureRemoteName }
|
|
1534
|
-
: {
|
|
1535
|
-
error: validation.errors.map((entry) => entry.message).filter(Boolean).join('\n') || 'Failed to validate worktree creation',
|
|
1536
|
-
code: 'INVALID_REQUEST',
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
const account = await resolveSourceControlAccount(source.context);
|
|
1540
|
-
if (account?.credentialId !== source.context.accountId || account?.status !== 'valid'
|
|
1541
|
-
|| !Number.isSafeInteger(account?.credentialRevision) || account.credentialRevision < 1
|
|
1542
|
-
|| Object.prototype.toString.call(account?.providerUserId) !== '[object String]' || !account.providerUserId) {
|
|
1543
|
-
throw Object.assign(new Error('Contributor credential is unavailable'), { code: 'AUTHENTICATION_REQUIRED', status: 409 });
|
|
1544
|
-
}
|
|
1545
|
-
const credentialId = createHttpsCredentialReference({
|
|
1546
|
-
provider: source.context.provider,
|
|
1547
|
-
instance: source.context.instance,
|
|
1548
|
-
credentialId: account.credentialId,
|
|
1549
|
-
credentialRevision: account.credentialRevision,
|
|
1550
|
-
providerUserId: account.providerUserId,
|
|
1551
|
-
});
|
|
1552
|
-
const transfer = await networkOperations.transferContributorHead({
|
|
1553
|
-
directory,
|
|
1554
|
-
sourceRequest: req.body.changeRequestSource,
|
|
1555
|
-
source,
|
|
1556
|
-
destinationRef,
|
|
1557
|
-
credentialId,
|
|
1558
|
-
});
|
|
1559
|
-
if (transfer.state !== 'succeeded') {
|
|
1560
|
-
const status = transfer.error?.code === 'AUTHENTICATION_REQUIRED' ? 401 : 409;
|
|
1561
|
-
return res.status(status).json({ error: transfer.error?.message || 'Contributor head transfer failed', code: transfer.error?.code || 'UNKNOWN' });
|
|
1562
|
-
}
|
|
1563
|
-
input.contributorTransferComplete = true;
|
|
1564
|
-
}
|
|
1565
|
-
const bindingRead = getSourceControlBinding instanceof Function
|
|
1566
|
-
? await getSourceControlBinding(directory)
|
|
1567
|
-
: null;
|
|
1568
|
-
const repositoryAuthority = bindingRead?.binding ? {
|
|
1569
|
-
repositoryId: bindingRead.repository.repositoryId,
|
|
1570
|
-
bindingRevision: bindingRead.revision,
|
|
1571
|
-
configRevision: bindingRead.repository.configRevision,
|
|
1572
|
-
} : null;
|
|
1573
|
-
const hydrateCheckout = ({ directory: checkoutDirectory, parentRemoteName }) => networkOperations.hydrateBoundCheckout({
|
|
1574
|
-
directory: checkoutDirectory,
|
|
1575
|
-
parentRemoteName,
|
|
1576
|
-
parentEndpoint: source?.endpoint,
|
|
1577
|
-
repositoryAuthority,
|
|
1578
|
-
});
|
|
1579
|
-
const created = await createWorktree(directory, input, {
|
|
1580
|
-
contributorProvenance,
|
|
1581
|
-
contributorSource: source,
|
|
1582
|
-
hydrateCheckout,
|
|
1583
|
-
bootstrapStore: worktreeBootstrapStore,
|
|
1584
|
-
});
|
|
1585
|
-
res.json(created);
|
|
1477
|
+
res.json(await worktreeCreation.create(directory, req.body || {}));
|
|
1586
1478
|
} catch (error) {
|
|
1587
|
-
|
|
1588
|
-
if (error
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
remoteName: error.remoteName,
|
|
1593
|
-
});
|
|
1479
|
+
// A refusal carries the same error shape as every other one here, so the reason reaches the user.
|
|
1480
|
+
if (error instanceof WorktreeCreationError) {
|
|
1481
|
+
const body = { error: error.message, code: error.code };
|
|
1482
|
+
if (error.remoteName !== undefined) body.remoteName = error.remoteName;
|
|
1483
|
+
return res.status(error.status).json(body);
|
|
1594
1484
|
}
|
|
1485
|
+
console.error('Failed to create worktree:', error);
|
|
1595
1486
|
res.status(Number.isInteger(error?.status) ? error.status : 500).json({
|
|
1596
1487
|
error: error.message || 'Failed to create worktree',
|
|
1597
1488
|
code: error?.code,
|
|
@@ -1835,4 +1726,7 @@ export function registerGitRoutes(app, {
|
|
|
1835
1726
|
}
|
|
1836
1727
|
});
|
|
1837
1728
|
|
|
1729
|
+
// The session service creates worktrees from change requests through the
|
|
1730
|
+
// same checks the route uses.
|
|
1731
|
+
return { createWorktree: worktreeCreation.create };
|
|
1838
1732
|
}
|
|
@@ -527,7 +527,7 @@ describe('contributor worktree authority', () => {
|
|
|
527
527
|
expect(hydrateBoundCheckout).toHaveBeenCalledWith(expect.objectContaining({ parentRemoteName: '' }));
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
-
it('
|
|
530
|
+
it('hydrates an unbound checkout under the unbound revision and passes the durable store', async () => {
|
|
531
531
|
const hydrateBoundCheckout = vi.fn(async () => ({ status: 'not-needed', submodules: [], lfs: [] }));
|
|
532
532
|
const worktreeBootstrapStore = { read: vi.fn(), write: vi.fn(), remove: vi.fn() };
|
|
533
533
|
gitLibraries.createWorktree.mockImplementationOnce(async (_directory, _input, options) => {
|
|
@@ -556,7 +556,7 @@ describe('contributor worktree authority', () => {
|
|
|
556
556
|
directory: '/repo-local',
|
|
557
557
|
parentRemoteName: '',
|
|
558
558
|
parentEndpoint: undefined,
|
|
559
|
-
repositoryAuthority:
|
|
559
|
+
repositoryAuthority: { repositoryId: 'repo_one', bindingRevision: 0, configRevision: 'config_one' },
|
|
560
560
|
});
|
|
561
561
|
});
|
|
562
562
|
|