@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.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -33,6 +33,7 @@ import {
33
33
  getTrackingBranch,
34
34
  getWorktrees,
35
35
  isGitRepository,
36
+ isPatchEquivalentOfHead,
36
37
  observeWorktreeTopology,
37
38
  populateWorktreeWithLockRecovery,
38
39
  previewWorktreeCreate,
@@ -49,6 +50,7 @@ import {
49
50
  getGlobalIdentity,
50
51
  stageFiles,
51
52
  subscribeWorktreeTopologyChanges,
53
+ renameBranch,
52
54
  unstageFiles,
53
55
  applyHunk,
54
56
  getDiff,
@@ -56,7 +58,6 @@ import {
56
58
  revertFile,
57
59
  getUntrackedDiffs,
58
60
  getFileDiff,
59
- commit,
60
61
  hasLocalIdentity,
61
62
  validateWorktreeCreate,
62
63
  parseBranchCreationSource,
@@ -905,6 +906,28 @@ describe('getStatus', () => {
905
906
  await expect(getStatus(repo)).resolves.toMatchObject({ current: 'main' });
906
907
  });
907
908
 
909
+ it('never rewrites the index, so a terminal commit does not meet its lock (#2229)', async () => {
910
+ if (!canRunGit()) return;
911
+
912
+ const repo = createTempDir();
913
+ runGit(repo, ['init', '-b', 'main']);
914
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
915
+ runGit(repo, ['config', 'user.name', 'Test User']);
916
+ fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
917
+ runGit(repo, ['add', 'README.md']);
918
+ runGit(repo, ['commit', '-m', 'Initial commit']);
919
+ // Same content under an older timestamp: the index entry is stale, and a
920
+ // status that takes the optional lock rewrites the index to refresh it.
921
+ const older = new Date(Date.now() - 60_000);
922
+ fs.utimesSync(path.join(repo, 'README.md'), older, older);
923
+ const indexPath = path.join(repo, '.git', 'index');
924
+ const indexBefore = fs.readFileSync(indexPath);
925
+
926
+ await expect(getStatus(repo)).resolves.toMatchObject({ isClean: true });
927
+
928
+ expect(fs.readFileSync(indexPath).equals(indexBefore)).toBe(true);
929
+ });
930
+
908
931
  it('names the base an upstream-less branch was counted against, and only then', async () => {
909
932
  if (!canRunGit()) return;
910
933
 
@@ -1149,6 +1172,48 @@ describe('worktree root resolution', () => {
1149
1172
  }
1150
1173
  });
1151
1174
 
1175
+ it('creates and removes a managed worktree from a bare repository project directory', async () => {
1176
+ if (!canRunGit()) return;
1177
+ const previousDataHome = process.env.XDG_DATA_HOME;
1178
+ const parent = createTempDir();
1179
+ process.env.XDG_DATA_HOME = path.join(parent, 'data');
1180
+ try {
1181
+ const source = path.join(parent, 'source');
1182
+ fs.mkdirSync(source);
1183
+ runGit(source, ['init', '-b', 'main']);
1184
+ runGit(source, ['config', 'user.email', 'test@example.com']);
1185
+ runGit(source, ['config', 'user.name', 'Test User']);
1186
+ fs.writeFileSync(path.join(source, 'README.md'), 'initial\n');
1187
+ runGit(source, ['add', 'README.md']);
1188
+ runGit(source, ['commit', '-m', 'Initial commit']);
1189
+
1190
+ // The layout this covers: a bare git dir at <project>/.git with linked
1191
+ // worktrees as siblings, the project directory itself being bare.
1192
+ const bareRoot = path.join(parent, 'project');
1193
+ fs.mkdirSync(bareRoot);
1194
+ runGit(source, ['clone', '--bare', source, path.join(bareRoot, '.git')]);
1195
+
1196
+ const created = await createWorktree(bareRoot, {
1197
+ mode: 'new', branchName: 'feature/from-bare', worktreeName: 'from-bare',
1198
+ });
1199
+ await expect.poll(
1200
+ async () => (await getWorktreeBootstrapStatus(created.path)).status,
1201
+ { timeout: 20_000 },
1202
+ ).not.toBe('pending');
1203
+ expect(await getWorktreeBootstrapStatus(created.path)).toMatchObject({ status: 'ready', error: null });
1204
+ expect(fs.readFileSync(path.join(created.path, 'README.md'), 'utf8')).toBe('initial\n');
1205
+ // The bare root lists only real checkouts: the created worktree, not itself.
1206
+ const entries = await getWorktrees(bareRoot);
1207
+ expect(entries.map((entry) => fs.realpathSync(entry.path))).toEqual([fs.realpathSync(created.path)]);
1208
+ await removeWorktree(bareRoot, { directory: created.path });
1209
+ expect(fs.existsSync(created.path)).toBe(false);
1210
+ expect(await getWorktrees(bareRoot)).toHaveLength(0);
1211
+ } finally {
1212
+ if (previousDataHome === undefined) delete process.env.XDG_DATA_HOME;
1213
+ else process.env.XDG_DATA_HOME = previousDataHome;
1214
+ }
1215
+ });
1216
+
1152
1217
  it('resolves the git toplevel for a repository subdirectory', async () => {
1153
1218
  if (!canRunGit()) return;
1154
1219
 
@@ -1178,6 +1243,56 @@ describe('worktree root resolution', () => {
1178
1243
  });
1179
1244
  });
1180
1245
 
1246
+ describe.runIf(canRunGit())('isPatchEquivalentOfHead', () => {
1247
+ it('finds a rebased commit patch in HEAD history', async () => {
1248
+ const repo = createTempDir();
1249
+ runGit(repo, ['init', '-b', 'main']);
1250
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
1251
+ runGit(repo, ['config', 'user.name', 'Test User']);
1252
+ fs.writeFileSync(path.join(repo, 'file.txt'), 'base\n');
1253
+ runGit(repo, ['add', 'file.txt']);
1254
+ runGit(repo, ['commit', '-m', 'base']);
1255
+ const root = runGit(repo, ['rev-parse', 'HEAD']).trim();
1256
+ runGit(repo, ['checkout', '-b', 'feature']);
1257
+ fs.appendFileSync(path.join(repo, 'file.txt'), 'feature\n');
1258
+ runGit(repo, ['commit', '-am', 'feature change']);
1259
+ const original = runGit(repo, ['rev-parse', 'HEAD']).trim();
1260
+ runGit(repo, ['tag', 'before-rebase', original]);
1261
+ runGit(repo, ['checkout', 'main']);
1262
+ fs.writeFileSync(path.join(repo, 'main.txt'), 'main\n');
1263
+ runGit(repo, ['add', 'main.txt']);
1264
+ runGit(repo, ['commit', '-m', 'main change']);
1265
+ runGit(repo, ['checkout', 'feature']);
1266
+ runGit(repo, ['rebase', 'main']);
1267
+
1268
+ await expect(isPatchEquivalentOfHead(repo, original)).resolves.toBe(true);
1269
+ await expect(isPatchEquivalentOfHead(repo, root)).resolves.toBe(false);
1270
+ });
1271
+
1272
+ it('rejects an unrelated commit with a different patch', async () => {
1273
+ const repo = createTempDir();
1274
+ runGit(repo, ['init', '-b', 'main']);
1275
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
1276
+ runGit(repo, ['config', 'user.name', 'Test User']);
1277
+ runGit(repo, ['commit', '--allow-empty', '-m', 'base']);
1278
+ runGit(repo, ['checkout', '-b', 'old-feature']);
1279
+ fs.writeFileSync(path.join(repo, 'old.txt'), 'old\n');
1280
+ runGit(repo, ['add', 'old.txt']);
1281
+ runGit(repo, ['commit', '-m', 'old change']);
1282
+ const oldCommit = runGit(repo, ['rev-parse', 'HEAD']).trim();
1283
+ runGit(repo, ['commit', '--allow-empty', '-m', 'empty change']);
1284
+ const emptyCommit = runGit(repo, ['rev-parse', 'HEAD']).trim();
1285
+ runGit(repo, ['checkout', 'main']);
1286
+ fs.writeFileSync(path.join(repo, 'new.txt'), 'new\n');
1287
+ runGit(repo, ['add', 'new.txt']);
1288
+ runGit(repo, ['commit', '-m', 'new change']);
1289
+
1290
+ await expect(isPatchEquivalentOfHead(repo, oldCommit)).resolves.toBe(false);
1291
+ await expect(isPatchEquivalentOfHead(repo, emptyCommit)).resolves.toBe(false);
1292
+ await expect(isPatchEquivalentOfHead(repo, 'missing')).resolves.toBe(false);
1293
+ });
1294
+ });
1295
+
1181
1296
  // ---------------------------------------------------------------------------
1182
1297
  // getWorktrees
1183
1298
  // ---------------------------------------------------------------------------
@@ -1221,6 +1336,36 @@ describe('getWorktrees', () => {
1221
1336
  expect(Array.isArray(result)).toBe(true);
1222
1337
  expect(warnSpy).not.toHaveBeenCalled();
1223
1338
  });
1339
+
1340
+ it('lists the linked worktrees of a bare repository used as the project directory', async () => {
1341
+ if (!canRunGit()) return;
1342
+
1343
+ const source = createTempDir();
1344
+ runGit(source, ['init', '-b', 'main']);
1345
+ runGit(source, ['config', 'user.email', 'test@example.com']);
1346
+ runGit(source, ['config', 'user.name', 'Test User']);
1347
+ runGit(source, ['commit', '--allow-empty', '-m', 'init']);
1348
+
1349
+ // Two bare layouts in the wild: a `clone --bare` directory whose git dir
1350
+ // is the directory itself, and one whose git dir is a `.git` child.
1351
+ for (const bareRoot of [path.join(createTempDir(), 'repo.git'), path.join(createTempDir(), 'repo')]) {
1352
+ const gitDir = bareRoot.endsWith('.git') ? bareRoot : path.join(bareRoot, '.git');
1353
+ fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1354
+ runGit(source, ['clone', '--bare', source, gitDir]);
1355
+ const linked = path.join(createTempDir(), 'linked');
1356
+ runGit(gitDir, ['worktree', 'add', linked, 'main']);
1357
+
1358
+ for (const directory of [bareRoot, gitDir, linked]) {
1359
+ expect(await isGitRepository(directory)).toBe(true);
1360
+ const entries = await getWorktrees(directory);
1361
+ // The bare repository lists itself as a worktree; it has no working
1362
+ // tree, so only the linked checkout is a worktree anyone can open.
1363
+ expect(entries.map((entry) => fs.realpathSync(entry.path))).toEqual([fs.realpathSync(linked)]);
1364
+ expect(entries[0].branch).toBe('main');
1365
+ }
1366
+ }
1367
+ expect(warnSpy).not.toHaveBeenCalled();
1368
+ });
1224
1369
  it('notifies subscribers only when another git process changes the worktree set', async () => {
1225
1370
  if (!canRunGit()) return;
1226
1371
 
@@ -1254,6 +1399,37 @@ describe('getWorktrees', () => {
1254
1399
  }
1255
1400
  });
1256
1401
 
1402
+ it('renames a worktree branch, tells subscribers, and names the worktree that holds a taken name', async () => {
1403
+ if (!canRunGit()) return;
1404
+
1405
+ const repo = createTempDir();
1406
+ runGit(repo, ['init', '-b', 'main']);
1407
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
1408
+ runGit(repo, ['config', 'user.name', 'Test User']);
1409
+ runGit(repo, ['commit', '--allow-empty', '-m', 'init']);
1410
+ const first = path.join(createTempDir(), 'first');
1411
+ const second = path.join(createTempDir(), 'second');
1412
+ runGit(repo, ['worktree', 'add', first, '-b', 'feature-one']);
1413
+ runGit(repo, ['worktree', 'add', second, '-b', 'feature-two']);
1414
+ runGit(repo, ['branch', 'parked']);
1415
+
1416
+ const events = [];
1417
+ const unsubscribe = subscribeWorktreeTopologyChanges((event) => events.push(event));
1418
+ try {
1419
+ await expect(renameBranch(first, 'feature-one', 'feature-two'))
1420
+ .rejects.toMatchObject({ statusCode: 409, message: expect.stringContaining(second) });
1421
+ await expect(renameBranch(first, 'feature-one', 'parked'))
1422
+ .rejects.toMatchObject({ statusCode: 409, message: 'A branch named parked already exists' });
1423
+ expect(events).toHaveLength(0);
1424
+
1425
+ await renameBranch(first, 'feature-one', 'login-form');
1426
+ expect(runGit(first, ['branch', '--show-current']).trim()).toBe('login-form');
1427
+ expect(events).toHaveLength(1);
1428
+ } finally {
1429
+ unsubscribe();
1430
+ }
1431
+ });
1432
+
1257
1433
  it('publishes worktrees this server creates and removes', async () => {
1258
1434
  if (!canRunGit()) return;
1259
1435
 
@@ -4613,6 +4789,28 @@ describe('parseRemoteListing', () => {
4613
4789
  { name: 'origin', fetchUrl: 'git@github.com:owner/repo.git', pushUrl: 'git@github.com:owner/push.git' },
4614
4790
  ]);
4615
4791
  });
4792
+
4793
+ it('ignores decoration after the listing kind, recognized or not (#4479)', () => {
4794
+ // Git only annotates the fetch line in practice; the parser ignores
4795
+ // whatever follows the marker so it does not depend on the annotation's
4796
+ // exact shape.
4797
+ const listing = [
4798
+ 'origin\tgit@github.com:owner/repo.git (fetch) [blob:none]',
4799
+ 'origin\tgit@github.com:owner/repo.git (push)',
4800
+ 'mirror\thttps://example.com/repo.git (fetch) [blob:limit=1m] [tree:1]',
4801
+ 'mirror\thttps://example.com/repo.git (push) [blob:none]',
4802
+ 'future\thttps://example.com/f.git (fetch) [blob:none] (extra)',
4803
+ 'future\thttps://example.com/f.git (push)',
4804
+ 'bare\t',
4805
+ '',
4806
+ ].join('\n');
4807
+ expect(parseRemoteListing(['bare', 'future', 'mirror', 'origin'], listing)).toEqual([
4808
+ { name: 'bare', fetchUrl: 'bare', pushUrl: 'bare' },
4809
+ { name: 'future', fetchUrl: 'https://example.com/f.git', pushUrl: 'https://example.com/f.git' },
4810
+ { name: 'mirror', fetchUrl: 'https://example.com/repo.git', pushUrl: 'https://example.com/repo.git' },
4811
+ { name: 'origin', fetchUrl: 'git@github.com:owner/repo.git', pushUrl: 'git@github.com:owner/repo.git' },
4812
+ ]);
4813
+ });
4616
4814
  });
4617
4815
 
4618
4816
  describe.runIf(canRunGit())('getRepositoryRemoteUrls', () => {
@@ -4636,6 +4834,22 @@ describe.runIf(canRunGit())('getRepositoryRemoteUrls', () => {
4636
4834
  expect(remotes.find((remote) => remote.name === 'rewritten')?.fetchUrl).toBe('https://github.com/other/repo.git');
4637
4835
  expect(remotes.find((remote) => remote.name === 'origin')?.pushUrl).toBe('git@github.com:owner/push.git');
4638
4836
  });
4837
+
4838
+ it('reads a partial-clone remote as `git remote get-url` reports it, without the 2.54 listing annotation (#4479)', async () => {
4839
+ const repository = createTempDir();
4840
+ runGit(repository, ['init', '-b', 'main']);
4841
+ runGit(repository, ['remote', 'add', 'origin', 'git@github.com:owner/repo.git']);
4842
+ // Git 2.54+ annotates the fetch line of `git remote -v` with the filter:
4843
+ // `... (fetch) [blob:none]`. Setting just the config key is enough.
4844
+ runGit(repository, ['config', 'remote.origin.partialclonefilter', 'blob:none']);
4845
+
4846
+ const [origin] = await getRepositoryRemoteUrls(repository);
4847
+ expect(origin).toEqual({
4848
+ name: 'origin',
4849
+ fetchUrl: 'git@github.com:owner/repo.git',
4850
+ pushUrl: 'git@github.com:owner/repo.git',
4851
+ });
4852
+ });
4639
4853
  });
4640
4854
 
4641
4855
  describe.runIf(canRunGit())('getUnpushedBranchCounts', () => {
@@ -1,13 +1,10 @@
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 { constants as fsConstants } from 'node:fs';
4
5
  import { randomUUID } from 'node:crypto';
5
6
  import { withSourceControlFileLock } from '../source-control/file-lock.js';
6
7
 
7
- const isPlainObject = (value) => value === Object(value)
8
- && !Array.isArray(value)
9
- && Object.getPrototypeOf(value) === Object.prototype;
10
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
11
8
  const hasExactKeys = (value, keys) => {
12
9
  const actual = Object.keys(value);
13
10
  return actual.length === keys.length && actual.every((key) => keys.includes(key));
@@ -1,3 +1,4 @@
1
+ import { isString } from '../shared/guards.js';
1
2
  import { normalizeDiscoveryEndpoint } from './discovery-endpoint.js';
2
3
 
3
4
  export const SUBMODULE_DISCOVERY_LIMITS = Object.freeze({
@@ -13,7 +14,6 @@ const CONFIG_KEY_PATTERN = /^submodule\.(.+)\.(path|url|update)$/i;
13
14
  const GITLINK_PATTERN = /^160000 (?:(?:commit )?([0-9a-f]{40}|[0-9a-f]{64})|([0-9a-f]{40}|[0-9a-f]{64}) 0)\t(.+)$/i;
14
15
  const CONTROL_PATTERN = /[\0-\x1f\x7f]/;
15
16
  const UPDATE_MODES = new Set(['checkout', 'merge', 'rebase', 'none']);
16
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
17
17
 
18
18
  const manifestError = (message, code = 'INVALID_SUBMODULE_MANIFEST') => Object.assign(new Error(message), { code });
19
19
 
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from '../shared/guards.js';
1
2
  import crypto, { randomUUID } from 'node:crypto';
2
3
  import fs from 'node:fs/promises';
3
4
  import path from 'node:path';
@@ -13,7 +14,7 @@ const ERROR_CODES = new Set([
13
14
  'INVALID_REQUEST', 'NOT_FOUND', 'STALE_REPOSITORY', 'STALE_BINDING', 'STALE_CONFIG',
14
15
  'REMOTE_CHANGED', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
15
16
  'CONFLICT', 'CANCELLED', 'TIMEOUT', 'OUTCOME_UNKNOWN', 'RUNTIME_UNSUPPORTED', 'UNKNOWN',
16
- 'GIT_LFS_CLIENT_MISSING', 'PATH_LENGTH_LIMIT',
17
+ 'GIT_LFS_CLIENT_MISSING', 'PATH_LENGTH_LIMIT', 'CHECKOUT_TOO_LARGE',
17
18
  ]);
18
19
  const HYDRATION_STATUSES = new Set([
19
20
  'succeeded', 'authorization-required', 'invalid', 'client-missing', 'failed', 'cancelled', 'not-needed',
@@ -35,12 +36,10 @@ const SAFE_ERROR_MESSAGES = Object.freeze({
35
36
  RUNTIME_UNSUPPORTED: 'Worktree checkout hydration is unsupported by this runtime',
36
37
  UNKNOWN: 'Worktree bootstrap completion is unknown. Inspect the checkout and repair setup before use.',
37
38
  GIT_LFS_CLIENT_MISSING: 'Git LFS is required but unavailable',
39
+ CHECKOUT_TOO_LARGE: 'Worktree checkout has too many files to inspect',
38
40
  PATH_LENGTH_LIMIT: 'Git reported "File name too long". The worktree checkout path exceeds this system\'s path-length limit. Enable OS long paths or use a shorter repository path.',
39
41
  });
40
42
 
41
- const isPlainObject = (value) => value === Object(value)
42
- && !Array.isArray(value)
43
- && Object.getPrototypeOf(value) === Object.prototype;
44
43
  const exactKeys = (value, required, optional = []) => {
45
44
  const keys = Object.keys(value);
46
45
  return required.every((key) => keys.includes(key))
@@ -88,6 +88,21 @@ describe('worktree bootstrap storage', () => {
88
88
  expect(encoded).not.toContain('process output');
89
89
  });
90
90
 
91
+ it('round-trips a checkout with too many files to inspect with its own public message', async () => {
92
+ const dataDirectory = await temporaryDirectory('worktree-bootstrap-too-large-');
93
+ const filePath = path.join(dataDirectory, 'bootstrap.json');
94
+ const failure = { code: 'CHECKOUT_TOO_LARGE', message: 'Checkout has too many files to inspect' };
95
+ await createWorktreeBootstrapStore({ filePath }).write('/worktrees/large', {
96
+ status: 'failed', phase: 'directory-created', error: failure.message, errorCode: failure.code, updatedAt: 12,
97
+ hydration: { status: 'invalid', submodules: [], lfs: [{ path: '.', status: 'invalid', error: failure }] },
98
+ });
99
+
100
+ await expect(createWorktreeBootstrapStore({ filePath }).read('/worktrees/large')).resolves.toMatchObject({
101
+ status: 'failed', errorCode: 'CHECKOUT_TOO_LARGE', error: 'Worktree checkout has too many files to inspect',
102
+ hydration: { lfs: [{ status: 'invalid', error: { code: 'CHECKOUT_TOO_LARGE' } }] },
103
+ });
104
+ });
105
+
91
106
  it('turns a restarted pending bootstrap into a git-ready repair blocker without resuming it', async () => {
92
107
  if (!gitAvailable()) return;
93
108
  const dataDirectory = await temporaryDirectory('worktree-bootstrap-restart-');
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Worktree creation with the server's authority checks, shared by
3
+ * `POST /api/git/worktrees` (the New worktree dialog) and the session service
4
+ * (`session.create` with a pull or merge request). One implementation, so a
5
+ * worktree made from a change request is the same kind whichever way it was
6
+ * asked for: the head is resolved and transferred here, never taken from the
7
+ * caller, and a contributor fork gets its named remote, provenance and no
8
+ * upstream.
9
+ */
10
+
11
+ /** A refusal with the HTTP status and body fields the route answers with. */
12
+ export class WorktreeCreationError extends Error {
13
+ constructor(message, status, code, details = {}) {
14
+ super(message);
15
+ this.name = 'WorktreeCreationError';
16
+ this.status = status;
17
+ this.code = code;
18
+ Object.assign(this, details);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Contributor authority is the server's to derive. A request carrying any of
24
+ * these fields itself is refused before anything else happens.
25
+ */
26
+ export const hasClientContributorAuthority = (body) => body?.contributorFork !== undefined
27
+ || body?.changeRequestTransfer !== undefined || body?.contributorTransferComplete !== undefined
28
+ || (body?.changeRequestSource && body?.ensureRemoteUrl !== undefined);
29
+
30
+ // A change request from this repository itself is fetched onto the primary
31
+ // remote it already has, under that remote's own URL, so the checkout adds no
32
+ // second remote for the same project. Only a fork gets a remote of its own.
33
+ // A fork's remote is named after its owner, which the provider just said; the
34
+ // picker cannot always know it (GitLab lists merge requests without it).
35
+ const forkRemoteName = (owner) => `pr-${String(owner || '').trim().toLowerCase()
36
+ .replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'head'}`;
37
+
38
+ const worktreeInputForSource = (input, source) => ({
39
+ ...input,
40
+ changeRequestSource: undefined,
41
+ contributorFork: source.classification === 'contributor-fork',
42
+ changeRequestTransfer: true,
43
+ ensureRemoteName: source.requestedRemoteName,
44
+ ensureRemoteUrl: source.remoteUrl ?? source.endpoint,
45
+ expectedRevision: source.headSha,
46
+ });
47
+
48
+ export const createWorktreeCreation = ({
49
+ getGitLibraries,
50
+ networkOperations,
51
+ getSourceControlBinding,
52
+ contributorProvenance,
53
+ resolveChangeRequestSource,
54
+ createHttpsCredentialReference,
55
+ resolveSourceControlAccount,
56
+ worktreeBootstrapStore,
57
+ }) => {
58
+ const changeRequestSourceOnRepository = async (directory, source) => {
59
+ if (source.classification !== 'same-repository') {
60
+ return source.sourceProject?.owner
61
+ ? Object.freeze({ ...source, requestedRemoteName: forkRemoteName(source.sourceProject.owner) })
62
+ : source;
63
+ }
64
+ const { getRepositoryRemoteUrls } = await getGitLibraries();
65
+ const primary = (await getRepositoryRemoteUrls(directory)).find((remote) => remote.name === source.context.primaryRemote);
66
+ if (!primary?.fetchUrl) return source;
67
+ return Object.freeze({ ...source, requestedRemoteName: primary.name, remoteUrl: primary.fetchUrl });
68
+ };
69
+
70
+ /** The input `validateWorktreeCreate` checks: a change request becomes the server-resolved remote and head. */
71
+ const validationInput = async (directory, input) => {
72
+ if (!input.changeRequestSource) return input;
73
+ if (!(resolveChangeRequestSource instanceof Function)) {
74
+ throw new WorktreeCreationError('Contributor worktrees are unavailable', 501, 'RUNTIME_UNSUPPORTED');
75
+ }
76
+ const source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
77
+ return worktreeInputForSource(input, source);
78
+ };
79
+
80
+ /**
81
+ * Resolves the change request, checks the account that will fetch it, and
82
+ * transfers its head into the repository. Nothing is created on disk before
83
+ * the transfer succeeds.
84
+ */
85
+ const transferChangeRequestHead = async (directory, input) => {
86
+ const { validateWorktreeCreate } = await getGitLibraries();
87
+ if (!(resolveChangeRequestSource instanceof Function)
88
+ || !(createHttpsCredentialReference instanceof Function)
89
+ || !(resolveSourceControlAccount instanceof Function)
90
+ || !(networkOperations?.transferContributorHead instanceof Function)) {
91
+ throw new WorktreeCreationError('Contributor worktrees are unavailable', 501, 'RUNTIME_UNSUPPORTED');
92
+ }
93
+ const source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
94
+ const headBranch = source.headRef.slice('refs/heads/'.length);
95
+ const destinationRef = `refs/remotes/${source.requestedRemoteName}/${headBranch}`;
96
+ const transferInput = {
97
+ ...worktreeInputForSource(input, source),
98
+ existingBranch: destinationRef.slice('refs/'.length),
99
+ setUpstream: false,
100
+ };
101
+ const validation = await validateWorktreeCreate(directory, transferInput);
102
+ if (!validation.ok && !validation.errors.every((entry) => entry.code === 'contributor_transfer_unavailable')) {
103
+ const collision = validation.errors.find((entry) => entry.code === 'remote_name_collision');
104
+ if (collision) {
105
+ throw new WorktreeCreationError(collision.message, 409, 'CONTRIBUTOR_REMOTE_COLLISION', { remoteName: transferInput.ensureRemoteName });
106
+ }
107
+ throw new WorktreeCreationError(
108
+ validation.errors.map((entry) => entry.message).filter(Boolean).join('\n') || 'Failed to validate worktree creation',
109
+ 409,
110
+ 'INVALID_REQUEST',
111
+ );
112
+ }
113
+ const account = await resolveSourceControlAccount(source.context);
114
+ if (account?.credentialId !== source.context.accountId || account?.status !== 'valid'
115
+ || !Number.isSafeInteger(account?.credentialRevision) || account.credentialRevision < 1
116
+ || Object.prototype.toString.call(account?.providerUserId) !== '[object String]' || !account.providerUserId) {
117
+ throw new WorktreeCreationError('Contributor credential is unavailable', 409, 'AUTHENTICATION_REQUIRED');
118
+ }
119
+ const credentialId = createHttpsCredentialReference({
120
+ provider: source.context.provider,
121
+ instance: source.context.instance,
122
+ credentialId: account.credentialId,
123
+ credentialRevision: account.credentialRevision,
124
+ providerUserId: account.providerUserId,
125
+ });
126
+ const transfer = await networkOperations.transferContributorHead({
127
+ directory,
128
+ sourceRequest: input.changeRequestSource,
129
+ source,
130
+ destinationRef,
131
+ credentialId,
132
+ });
133
+ if (transfer.state !== 'succeeded') {
134
+ throw new WorktreeCreationError(
135
+ transfer.error?.message || 'Contributor head transfer failed',
136
+ transfer.error?.code === 'AUTHENTICATION_REQUIRED' ? 401 : 409,
137
+ transfer.error?.code || 'UNKNOWN',
138
+ );
139
+ }
140
+ return { source, input: { ...transferInput, contributorTransferComplete: true } };
141
+ };
142
+
143
+ /** Creates a worktree; with `input.changeRequestSource`, from that change request's head. */
144
+ const create = async (directory, requestInput) => {
145
+ const { createWorktree, validateWorktreeCreate } = await getGitLibraries();
146
+ if (!(createWorktree instanceof Function) || !(validateWorktreeCreate instanceof Function)) {
147
+ throw new WorktreeCreationError('Worktree creation is not available', 501);
148
+ }
149
+ if (hasClientContributorAuthority(requestInput)) {
150
+ throw new WorktreeCreationError('Invalid contributor worktree request', 400, 'INVALID_CONTRIBUTOR_WORKTREE');
151
+ }
152
+ if (!(networkOperations?.hydrateBoundCheckout instanceof Function)
153
+ || !(worktreeBootstrapStore?.read instanceof Function)
154
+ || !(worktreeBootstrapStore?.write instanceof Function)) {
155
+ throw new WorktreeCreationError('Worktree checkout bootstrap is not available', 501);
156
+ }
157
+ let input = requestInput || {};
158
+ let source = null;
159
+ if (input.changeRequestSource) {
160
+ ({ source, input } = await transferChangeRequestHead(directory, input));
161
+ }
162
+ const bindingRead = getSourceControlBinding instanceof Function
163
+ ? await getSourceControlBinding(directory)
164
+ : null;
165
+ // An unbound repository hydrates through the machine's own Git for every
166
+ // remote, the same way it pushes; its authority is the unbound revision.
167
+ const repositoryAuthority = bindingRead ? {
168
+ repositoryId: bindingRead.repository.repositoryId,
169
+ bindingRevision: bindingRead.revision,
170
+ configRevision: bindingRead.repository.configRevision,
171
+ } : null;
172
+ const hydrateCheckout = ({ directory: checkoutDirectory, parentRemoteName }) => networkOperations.hydrateBoundCheckout({
173
+ directory: checkoutDirectory,
174
+ parentRemoteName,
175
+ parentEndpoint: source?.endpoint,
176
+ repositoryAuthority,
177
+ });
178
+ try {
179
+ return await createWorktree(directory, input, {
180
+ contributorProvenance,
181
+ contributorSource: source,
182
+ hydrateCheckout,
183
+ bootstrapStore: worktreeBootstrapStore,
184
+ });
185
+ } catch (error) {
186
+ if (error?.code === 'CONTRIBUTOR_REMOTE_COLLISION') {
187
+ throw new WorktreeCreationError('Contributor remote name is already used by a different endpoint', 409, 'CONTRIBUTOR_REMOTE_COLLISION', { remoteName: error.remoteName });
188
+ }
189
+ throw error;
190
+ }
191
+ };
192
+
193
+ return { validationInput, create };
194
+ };