@cloudcli-ai/cloudcli 1.37.2 → 1.37.4

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 (453) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
  34. package/dist/assets/index-CdjIFxM2.js +5004 -0
  35. package/dist/assets/index-DgafhGTF.css +32 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +11 -4
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.module.js +9 -2
  65. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  66. package/dist-server/server/modules/agent/agent.routes.js +177 -130
  67. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  68. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  69. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  70. package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
  71. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  73. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  74. package/dist-server/server/modules/database/connection.js +1 -1
  75. package/dist-server/server/modules/database/connection.js.map +1 -1
  76. package/dist-server/server/modules/database/index.js +5 -0
  77. package/dist-server/server/modules/database/index.js.map +1 -1
  78. package/dist-server/server/modules/database/migrations.js +45 -1
  79. package/dist-server/server/modules/database/migrations.js.map +1 -1
  80. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  81. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  83. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  84. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  85. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  86. package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
  87. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  88. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  89. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  90. package/dist-server/server/modules/database/schema.js +99 -0
  91. package/dist-server/server/modules/database/schema.js.map +1 -1
  92. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  93. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  94. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  96. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  97. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  98. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  99. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  100. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  101. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  102. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  103. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  104. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  105. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  106. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  107. package/dist-server/server/modules/git/git.routes.js +92 -5
  108. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  109. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  110. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  111. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  112. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  113. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  114. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  115. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  116. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  117. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  118. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  119. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  120. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  121. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  122. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  123. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  124. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  125. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  126. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  127. package/dist-server/server/modules/providers/index.js +3 -0
  128. package/dist-server/server/modules/providers/index.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
  130. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  132. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  133. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
  134. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  135. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
  136. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  137. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  138. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
  140. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  142. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  143. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  144. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  146. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  148. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
  152. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  155. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
  156. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  157. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  158. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  160. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
  162. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  164. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
  165. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  166. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  167. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  168. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  169. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/provider.routes.js +134 -2
  171. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  172. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  173. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  174. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  175. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  176. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
  177. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  179. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  181. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  182. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  183. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  185. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
  187. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  188. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  189. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  190. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  191. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  192. package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
  193. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
  195. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  196. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  197. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  199. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  201. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  203. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  205. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  206. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
  207. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  209. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  210. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  211. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  212. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  213. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  215. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  217. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
  219. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  221. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  222. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
  223. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  224. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  225. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  226. package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
  227. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  228. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  229. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  230. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  231. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  232. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  233. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  234. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  235. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  236. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  237. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  239. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  240. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
  241. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  242. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  243. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  244. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  245. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  246. package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
  247. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  248. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  249. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  250. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  251. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  252. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  253. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  254. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
  255. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  256. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  257. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  258. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
  259. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  260. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  261. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  262. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  263. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  264. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  265. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  266. package/dist-server/server/modules/user/user.module.js +10 -1
  267. package/dist-server/server/modules/user/user.module.js.map +1 -1
  268. package/dist-server/server/modules/user/user.routes.js +44 -0
  269. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  270. package/dist-server/server/modules/user/user.service.js +75 -0
  271. package/dist-server/server/modules/user/user.service.js.map +1 -1
  272. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  273. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  274. package/dist-server/server/modules/websocket/index.js +7 -0
  275. package/dist-server/server/modules/websocket/index.js.map +1 -1
  276. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
  277. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  278. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  279. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  280. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
  281. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  282. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  283. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  284. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
  285. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  286. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  287. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  288. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  289. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  290. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  291. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  292. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  293. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  294. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  295. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  296. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
  297. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  298. package/dist-server/server/shared/claude-cli-path.js +22 -5
  299. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  300. package/dist-server/server/shared/index.js +3 -0
  301. package/dist-server/server/shared/index.js.map +1 -0
  302. package/dist-server/server/shared/message-unification.js +376 -0
  303. package/dist-server/server/shared/message-unification.js.map +1 -0
  304. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  305. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  306. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  307. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  308. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  309. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  310. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  311. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  312. package/dist-server/server/shared/utils.js +160 -0
  313. package/dist-server/server/shared/utils.js.map +1 -1
  314. package/electron/desktopWindow.js +6 -0
  315. package/electron/viewHost.js +25 -2
  316. package/package.json +20 -17
  317. package/server/index.ts +16 -5
  318. package/server/modules/agent/agent.module.ts +11 -1
  319. package/server/modules/agent/agent.routes.ts +200 -142
  320. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  321. package/server/modules/agent/tests/agent.routes.test.ts +51 -2
  322. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  323. package/server/modules/database/connection.ts +2 -1
  324. package/server/modules/database/index.ts +10 -0
  325. package/server/modules/database/migrations.ts +51 -0
  326. package/server/modules/database/repositories/provider-models.ts +79 -6
  327. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  328. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  329. package/server/modules/database/repositories/sessions.db.ts +204 -3
  330. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  331. package/server/modules/database/schema.ts +103 -0
  332. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  333. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  334. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  335. package/server/modules/file-tree/file-tree.module.ts +2 -1
  336. package/server/modules/file-tree/file-tree.service.ts +49 -7
  337. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  338. package/server/modules/git/git-branch-diff.service.ts +204 -0
  339. package/server/modules/git/git-branch.service.ts +2 -12
  340. package/server/modules/git/git.routes.ts +100 -6
  341. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  342. package/server/modules/projects/project-clone.routes.ts +142 -0
  343. package/server/modules/projects/projects.routes.ts +16 -81
  344. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  345. package/server/modules/projects/services/project-clone.service.ts +102 -34
  346. package/server/modules/projects/services/project-delete.service.ts +13 -1
  347. package/server/modules/projects/services/project-star.service.ts +2 -2
  348. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  349. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  350. package/server/modules/providers/index.ts +4 -0
  351. package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
  352. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  353. package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
  354. package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
  355. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  356. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
  357. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  358. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  359. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  360. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  361. package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
  362. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
  363. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  364. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
  365. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  366. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  367. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
  368. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  369. package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
  370. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  371. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  372. package/server/modules/providers/provider.routes.ts +162 -2
  373. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  374. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  375. package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
  376. package/server/modules/providers/services/provider-models.service.ts +71 -4
  377. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  378. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  379. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  380. package/server/modules/providers/services/session-history-cache.service.ts +150 -0
  381. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  382. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  383. package/server/modules/providers/services/sessions.service.ts +309 -17
  384. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  385. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  386. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  387. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  388. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  389. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  390. package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
  391. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  392. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  393. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  394. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  395. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  396. package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
  397. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  398. package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
  399. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  400. package/server/modules/providers/tests/opencode-models.test.ts +266 -8
  401. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  402. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  403. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  404. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  405. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  406. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  407. package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
  408. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  409. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  410. package/server/modules/providers/tests/sessions.service.test.ts +303 -1
  411. package/server/modules/providers/tests/skills.test.ts +21 -5
  412. package/server/modules/scheduled-messages/index.ts +8 -0
  413. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  414. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
  415. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  416. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
  417. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  418. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  419. package/server/modules/user/tests/user.service.test.ts +90 -0
  420. package/server/modules/user/user.module.ts +10 -1
  421. package/server/modules/user/user.routes.ts +44 -0
  422. package/server/modules/user/user.service.ts +105 -0
  423. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  424. package/server/modules/websocket/README.md +4 -2
  425. package/server/modules/websocket/index.ts +8 -0
  426. package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
  427. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  428. package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
  429. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  430. package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
  431. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  432. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  433. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  434. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  435. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  436. package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
  437. package/server/shared/claude-cli-path.ts +24 -6
  438. package/server/shared/index.ts +3 -0
  439. package/server/shared/interfaces.ts +91 -0
  440. package/server/shared/message-unification.ts +449 -0
  441. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  442. package/server/shared/tests/message-unification.test.ts +218 -0
  443. package/server/shared/tests/read-only-roots.test.ts +139 -0
  444. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  445. package/server/shared/types.ts +382 -7
  446. package/server/shared/utils.ts +176 -0
  447. package/dist/assets/channel-_falYbU1.js +0 -1
  448. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  449. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  450. package/dist/assets/index-CzPLBvXT.css +0 -32
  451. package/dist/assets/index-DFkBBuSA.js +0 -1549
  452. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  453. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -0,0 +1,135 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import {
8
+ closeConnection,
9
+ getConnection,
10
+ initializeDatabase,
11
+ sessionDraftsDb,
12
+ userPreferencesDb,
13
+ } from '@/modules/database/index.js';
14
+
15
+ const USER_ID = 1;
16
+
17
+ async function withDatabase(runTest: () => void | Promise<void>): Promise<void> {
18
+ const previousDatabasePath = process.env.DATABASE_PATH;
19
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'user-prefs-db-'));
20
+ const databasePath = path.join(tempDirectory, 'auth.db');
21
+
22
+ closeConnection();
23
+ process.env.DATABASE_PATH = databasePath;
24
+ await writeFile(databasePath, '');
25
+ await initializeDatabase();
26
+
27
+ // Both tables cascade from users(id), so a row has to exist to write against.
28
+ getConnection()
29
+ .prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)')
30
+ .run(USER_ID, 'tester', 'hash');
31
+
32
+ try {
33
+ await runTest();
34
+ } finally {
35
+ closeConnection();
36
+ if (previousDatabasePath === undefined) {
37
+ delete process.env.DATABASE_PATH;
38
+ } else {
39
+ process.env.DATABASE_PATH = previousDatabasePath;
40
+ }
41
+ await rm(tempDirectory, { recursive: true, force: true });
42
+ }
43
+ }
44
+
45
+ test('preferences round-trip every JSON shape a setting can take', async () => {
46
+ await withDatabase(() => {
47
+ userPreferencesDb.savePreferences(USER_ID, {
48
+ theme: 'dark',
49
+ tasksEnabled: false,
50
+ claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
51
+ });
52
+
53
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
54
+ theme: 'dark',
55
+ tasksEnabled: false,
56
+ claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
57
+ });
58
+ });
59
+ });
60
+
61
+ test('saving preferences merge-patches instead of replacing the whole set', async () => {
62
+ await withDatabase(() => {
63
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
64
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'light' });
65
+
66
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
67
+ theme: 'light',
68
+ userLanguage: 'de',
69
+ });
70
+ });
71
+ });
72
+
73
+ test('a preference set to undefined is removed', async () => {
74
+ await withDatabase(() => {
75
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
76
+ userPreferencesDb.savePreferences(USER_ID, { userLanguage: undefined });
77
+
78
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
79
+ });
80
+ });
81
+
82
+ test('one unreadable preference value does not cost the user the others', async () => {
83
+ await withDatabase(() => {
84
+ userPreferencesDb.savePreferences(USER_ID, { theme: 'dark' });
85
+ getConnection()
86
+ .prepare(
87
+ `INSERT INTO user_preferences (user_id, preference_key, preference_value)
88
+ VALUES (?, ?, ?)`
89
+ )
90
+ .run(USER_ID, 'broken', 'not json');
91
+
92
+ assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
93
+ });
94
+ });
95
+
96
+ test('drafts round-trip text and queued message per scope', async () => {
97
+ await withDatabase(() => {
98
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'half typed', queuedMessage: null });
99
+ sessionDraftsDb.saveDraft(USER_ID, 'project:p1', {
100
+ text: '',
101
+ queuedMessage: { content: 'run it' },
102
+ });
103
+
104
+ const drafts = sessionDraftsDb.getDrafts(USER_ID);
105
+ const byScope = new Map(drafts.map((draft) => [draft.scope, draft]));
106
+
107
+ assert.equal(byScope.get('session-a')?.text, 'half typed');
108
+ assert.equal(byScope.get('session-a')?.queuedMessage, null);
109
+ assert.equal(byScope.get('project:p1')?.text, '');
110
+ assert.deepEqual(byScope.get('project:p1')?.queuedMessage, { content: 'run it' });
111
+ });
112
+ });
113
+
114
+ test('saving an empty draft deletes the row rather than keeping a blank one', async () => {
115
+ await withDatabase(() => {
116
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'typed', queuedMessage: null });
117
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: '', queuedMessage: null });
118
+
119
+ assert.deepEqual(sessionDraftsDb.getDrafts(USER_ID), []);
120
+ });
121
+ });
122
+
123
+ test('deleteDraft removes only the named scope', async () => {
124
+ await withDatabase(() => {
125
+ sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'a', queuedMessage: null });
126
+ sessionDraftsDb.saveDraft(USER_ID, 'session-b', { text: 'b', queuedMessage: null });
127
+
128
+ sessionDraftsDb.deleteDraft(USER_ID, 'session-a');
129
+
130
+ assert.deepEqual(
131
+ sessionDraftsDb.getDrafts(USER_ID).map((draft) => draft.scope),
132
+ ['session-b'],
133
+ );
134
+ });
135
+ });
@@ -14,7 +14,7 @@ import type {
14
14
  FileTreeProjectGateway,
15
15
  FileTreeWorkspaceGateway,
16
16
  } from '@/shared/types.js';
17
- import { WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
17
+ import { WORKSPACES_ROOT, resolveReadOnlyRootPath, validateWorkspacePath } from '@/shared/utils.js';
18
18
 
19
19
  const MAXIMUM_UPLOAD_SIZE_MEGABYTES = 200;
20
20
  const MAXIMUM_UPLOAD_SIZE_BYTES = MAXIMUM_UPLOAD_SIZE_MEGABYTES * 1024 * 1024;
@@ -73,6 +73,7 @@ const fileTreeProjects: FileTreeProjectGateway = {
73
73
  const fileTreeWorkspace: FileTreeWorkspaceGateway = {
74
74
  rootPath: WORKSPACES_ROOT,
75
75
  validatePath: (candidatePath) => validateWorkspacePath(candidatePath),
76
+ resolveReadOnlyRootPath: (candidatePath) => resolveReadOnlyRootPath(candidatePath),
76
77
  };
77
78
 
78
79
  const fileTreeLogger: FileTreeLogger = {
@@ -312,6 +312,51 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
312
312
  });
313
313
  }
314
314
 
315
+ /**
316
+ * Resolves a directory the browser may list.
317
+ *
318
+ * The read-only roots (the system temp directory and the Claude projects
319
+ * directory, see `READ_ONLY_ROOTS` in shared/utils) are browsable but never
320
+ * writable, so they are checked before the workspace policy rejects them.
321
+ * `createWorkspaceFolder` deliberately does not do this, which is what keeps
322
+ * them read-only.
323
+ */
324
+ async function resolveBrowsablePath(targetPath: string): Promise<string> {
325
+ const readOnlyPath = await dependencies.workspace.resolveReadOnlyRootPath(targetPath);
326
+ if (readOnlyPath) {
327
+ return readOnlyPath;
328
+ }
329
+
330
+ const validation = await dependencies.workspace.validatePath(targetPath);
331
+ if (!validation.valid) {
332
+ throw createFileTreeError(validation.error ?? 'Path is outside the workspace root', 403, 'INVALID_WORKSPACE_PATH');
333
+ }
334
+
335
+ return validation.resolvedPath || targetPath;
336
+ }
337
+
338
+ /**
339
+ * Resolves a path the viewer is allowed to read.
340
+ *
341
+ * A file reference in a transcript is normally inside the project, but a
342
+ * background command's log lives in the system temp directory and a
343
+ * background agent's output file is a link from there into the Claude
344
+ * projects directory, and the transcript quotes those paths verbatim. Those
345
+ * resolve through the read-only root policy instead of the project root; the
346
+ * writing endpoints keep using `resolvePathInsideProject` alone, so nothing
347
+ * outside the project can be changed.
348
+ */
349
+ async function resolveReadablePath(projectRoot: string, targetPath: string): Promise<string> {
350
+ if (path.isAbsolute(targetPath)) {
351
+ const readOnlyPath = await dependencies.workspace.resolveReadOnlyRootPath(targetPath);
352
+ if (readOnlyPath) {
353
+ return readOnlyPath;
354
+ }
355
+ }
356
+
357
+ return resolvePathInsideProject(projectRoot, targetPath);
358
+ }
359
+
315
360
  async function cleanupTemporaryFiles(files: FileTreeUploadedFile[]): Promise<void> {
316
361
  await Promise.all(files.map(async (file) => {
317
362
  try {
@@ -328,12 +373,8 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
328
373
  ? expandWorkspacePath(dependencies.workspace.rootPath, inputPath)
329
374
  : dependencies.workspace.rootPath;
330
375
  const targetPath = path.resolve(requestedPath);
331
- const validation = await dependencies.workspace.validatePath(targetPath);
332
- if (!validation.valid) {
333
- throw createFileTreeError(validation.error ?? 'Path is outside the workspace root', 403, 'INVALID_WORKSPACE_PATH');
334
- }
376
+ const resolvedPath = await resolveBrowsablePath(targetPath);
335
377
 
336
- const resolvedPath = validation.resolvedPath || targetPath;
337
378
  try {
338
379
  await fileSystem.access(resolvedPath);
339
380
  const stats = await fileSystem.stat(resolvedPath);
@@ -409,7 +450,7 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
409
450
 
410
451
  async readTextFile(projectId, filePath) {
411
452
  const projectRoot = await resolveProjectRoot(projectId);
412
- const resolvedPath = resolvePathInsideProject(projectRoot, filePath);
453
+ const resolvedPath = await resolveReadablePath(projectRoot, filePath);
413
454
  try {
414
455
  const content = await fileSystem.readTextFile(resolvedPath);
415
456
  return { content, path: resolvedPath };
@@ -417,13 +458,14 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
417
458
  mapFileSystemError(error, {
418
459
  ENOENT: { message: 'File not found', statusCode: 404 },
419
460
  EACCES: { message: 'Permission denied', statusCode: 403 },
461
+ EISDIR: { message: 'Path is a directory, not a file', statusCode: 400 },
420
462
  });
421
463
  }
422
464
  },
423
465
 
424
466
  async openFile(projectId, filePath) {
425
467
  const projectRoot = await resolveProjectRoot(projectId);
426
- const resolvedPath = resolvePathInsideProject(projectRoot, filePath);
468
+ const resolvedPath = await resolveReadablePath(projectRoot, filePath);
427
469
  try {
428
470
  await fileSystem.access(resolvedPath);
429
471
  } catch {
@@ -1,16 +1,23 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { createReadStream } from 'node:fs';
3
+ import fsPromises from 'node:fs/promises';
4
+ import os from 'node:os';
2
5
  import path from 'node:path';
3
6
  import { Readable } from 'node:stream';
4
7
  import test from 'node:test';
8
+ import { fileURLToPath } from 'node:url';
5
9
 
6
10
  import { createFileTreeService } from '@/modules/file-tree/file-tree.service.js';
7
11
  import type {
8
12
  FileTreeDirectoryEntry,
9
13
  FileTreeFileSystem,
10
14
  FileTreeServiceDependencies,
15
+ FileTreeServices,
11
16
  FileTreeStats,
12
17
  } from '@/shared/types.js';
13
- import { AppError } from '@/shared/utils.js';
18
+ import { AppError, resolveReadOnlyRootPath, validateWorkspacePath } from '@/shared/utils.js';
19
+
20
+ const testDirectory = path.dirname(fileURLToPath(import.meta.url));
14
21
 
15
22
  function createDirectoryEntry(name: string, directory: boolean): FileTreeDirectoryEntry {
16
23
  return {
@@ -80,6 +87,7 @@ function createDependencies(
80
87
  workspace: {
81
88
  rootPath: projectRoot,
82
89
  validatePath: async (candidatePath) => ({ valid: true, resolvedPath: candidatePath }),
90
+ resolveReadOnlyRootPath: async () => null,
83
91
  },
84
92
  resolveMimeType: () => 'text/plain',
85
93
  fileSystemConcurrency: 4,
@@ -343,6 +351,26 @@ test('readTextFile rejects traversal before invoking the filesystem adapter', as
343
351
  assert.deepEqual(readPaths, []);
344
352
  });
345
353
 
354
+ test('readTextFile reports a directory as a client error, not a 500', async () => {
355
+ const projectRoot = path.resolve('file-tree-test-project');
356
+ const fileSystem = createFakeFileSystem({
357
+ readTextFile: async () => {
358
+ const error = new Error('EISDIR: illegal operation on a directory, read');
359
+ (error as NodeJS.ErrnoException).code = 'EISDIR';
360
+ throw error;
361
+ },
362
+ });
363
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
364
+
365
+ await assert.rejects(
366
+ service.readTextFile('project-1', 'decisions'),
367
+ (error: unknown) => error instanceof AppError
368
+ && error.code === 'EISDIR'
369
+ && error.statusCode === 400
370
+ && error.message === 'Path is a directory, not a file',
371
+ );
372
+ });
373
+
346
374
  test('createEntry performs filesystem mutation only through the injected adapter', async () => {
347
375
  const projectRoot = path.resolve('file-tree-test-project');
348
376
  const targetPath = path.join(projectRoot, 'notes.txt');
@@ -369,3 +397,137 @@ test('createEntry performs filesystem mutation only through the injected adapter
369
397
  assert.equal(result.path, targetPath);
370
398
  assert.deepEqual(writtenFiles, [{ filePath: targetPath, content: '' }]);
371
399
  });
400
+
401
+ /**
402
+ * Builds the service against the real filesystem and the real workspace policy,
403
+ * which is the only way to exercise the read-only roots: the whole guarantee
404
+ * rests on `realpath` resolving symlinks before the comparison.
405
+ */
406
+ function createRealFileSystemService(projectRoot: string): FileTreeServices {
407
+ return createFileTreeService({
408
+ fileSystem: {
409
+ access: (candidatePath) => fsPromises.access(candidatePath),
410
+ stat: (candidatePath) => fsPromises.stat(candidatePath),
411
+ lstat: (candidatePath) => fsPromises.lstat(candidatePath),
412
+ openDirectory: async function* (directoryPath) {
413
+ yield* await fsPromises.opendir(directoryPath);
414
+ },
415
+ realpath: (candidatePath) => fsPromises.realpath(candidatePath),
416
+ readTextFile: (filePath) => fsPromises.readFile(filePath, 'utf8'),
417
+ writeTextFile: (filePath, content) => fsPromises.writeFile(filePath, content, 'utf8'),
418
+ async makeDirectory(directoryPath, recursive) {
419
+ await fsPromises.mkdir(directoryPath, { recursive });
420
+ },
421
+ rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
422
+ async removeDirectory(directoryPath) {
423
+ await fsPromises.rm(directoryPath, { recursive: true, force: true });
424
+ },
425
+ unlink: (filePath) => fsPromises.unlink(filePath),
426
+ copyFile: (source, destination) => fsPromises.copyFile(source, destination),
427
+ createReadStream: (filePath) => createReadStream(filePath),
428
+ },
429
+ projects: { getProjectPathById: async () => projectRoot },
430
+ workspace: {
431
+ rootPath: projectRoot,
432
+ validatePath: (candidatePath) => validateWorkspacePath(candidatePath),
433
+ resolveReadOnlyRootPath: (candidatePath) => resolveReadOnlyRootPath(candidatePath),
434
+ },
435
+ resolveMimeType: () => 'text/plain',
436
+ fileSystemConcurrency: 4,
437
+ logger: { error: () => undefined },
438
+ });
439
+ }
440
+
441
+ test('the temp directory can be browsed and read, but never written to', async () => {
442
+ const temporaryDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-tmp-'));
443
+ const projectRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-project-'));
444
+
445
+ try {
446
+ // The shape a task notification quotes: an agent's output file written to
447
+ // the system temp directory, outside every project.
448
+ await fsPromises.mkdir(path.join(temporaryDirectory, 'tasks'));
449
+ const outputPath = path.join(temporaryDirectory, 'tasks', 'agent.output');
450
+ await fsPromises.writeFile(outputPath, 'what the agent found', 'utf8');
451
+
452
+ const service = createRealFileSystemService(projectRoot);
453
+
454
+ const browsed = await service.browseWorkspace(temporaryDirectory);
455
+ assert.deepEqual(browsed.suggestions.map((entry) => entry.name), ['tasks']);
456
+
457
+ const opened = await service.readTextFile('project-1', outputPath);
458
+ assert.equal(opened.content, 'what the agent found');
459
+
460
+ const streamed = await service.openFile('project-1', outputPath);
461
+ streamed.stream.destroy();
462
+
463
+ // Read-only means read-only: nothing may be created or changed there.
464
+ await assert.rejects(
465
+ service.saveTextFile('project-1', outputPath, 'overwritten'),
466
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
467
+ );
468
+ await assert.rejects(
469
+ service.createWorkspaceFolder(path.join(temporaryDirectory, 'new-folder')),
470
+ (error: unknown) => (error as AppError).code === 'INVALID_WORKSPACE_PATH',
471
+ );
472
+ await assert.rejects(
473
+ service.createEntry({
474
+ projectId: 'project-1',
475
+ parentPath: path.join(temporaryDirectory, 'tasks'),
476
+ type: 'file',
477
+ name: 'planted.txt',
478
+ }),
479
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
480
+ );
481
+ await assert.rejects(
482
+ service.renameEntry({ projectId: 'project-1', oldPath: outputPath, newName: 'renamed.output' }),
483
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
484
+ );
485
+ const uploadedTemporaryPath = path.join(projectRoot, 'upload.tmp');
486
+ await fsPromises.writeFile(uploadedTemporaryPath, 'upload', 'utf8');
487
+ await assert.rejects(
488
+ service.storeUploadedFiles({
489
+ projectId: 'project-1',
490
+ targetPath: path.join(temporaryDirectory, 'tasks'),
491
+ relativePaths: [],
492
+ requestedFileCount: 1,
493
+ files: [{ originalName: 'upload.txt', temporaryPath: uploadedTemporaryPath, size: 6, mimeType: 'text/plain' }],
494
+ }),
495
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
496
+ );
497
+ await assert.rejects(
498
+ service.deleteEntry({ projectId: 'project-1', targetPath: outputPath }),
499
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
500
+ );
501
+ assert.deepEqual(await fsPromises.readdir(path.join(temporaryDirectory, 'tasks')), ['agent.output']);
502
+ assert.equal(await fsPromises.readFile(outputPath, 'utf8'), 'what the agent found');
503
+ } finally {
504
+ await fsPromises.rm(temporaryDirectory, { recursive: true, force: true });
505
+ await fsPromises.rm(projectRoot, { recursive: true, force: true });
506
+ }
507
+ });
508
+
509
+ test('reading through a symlink out of the temp directory is still refused', async () => {
510
+ const temporaryDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-tmp-'));
511
+ const projectRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-project-'));
512
+ // The link target has to sit under no read-only root. Beside this file is
513
+ // deterministic; a directory under `$HOME` lands under `/tmp` whenever a
514
+ // test run isolates its home there, and the Claude projects directory is a
515
+ // read-only root too.
516
+ const outsideDirectory = await fsPromises.mkdtemp(path.join(testDirectory, 'file-tree-outside-'));
517
+
518
+ try {
519
+ assert.equal(await resolveReadOnlyRootPath(outsideDirectory), null);
520
+ await fsPromises.writeFile(path.join(outsideDirectory, 'secret.txt'), 'secret', 'utf8');
521
+ await fsPromises.symlink(outsideDirectory, path.join(temporaryDirectory, 'escape'));
522
+
523
+ const service = createRealFileSystemService(projectRoot);
524
+ await assert.rejects(
525
+ service.readTextFile('project-1', path.join(temporaryDirectory, 'escape', 'secret.txt')),
526
+ (error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
527
+ );
528
+ } finally {
529
+ await fsPromises.rm(temporaryDirectory, { recursive: true, force: true });
530
+ await fsPromises.rm(projectRoot, { recursive: true, force: true });
531
+ await fsPromises.rm(outsideDirectory, { recursive: true, force: true });
532
+ }
533
+ });
@@ -0,0 +1,204 @@
1
+ import path from 'node:path';
2
+
3
+ import type { GitProcessRunner } from '@/shared/types.js';
4
+ import { AppError, getGitErrorDetails } from '@/shared/utils.js';
5
+
6
+ // Single-letter status the Compare tab renders as a badge. Git's own
7
+ // `--name-status` letters are folded into this set (see `toBranchDiffStatus`).
8
+ type BranchDiffFileStatus = 'M' | 'A' | 'D' | 'R' | 'U';
9
+
10
+ type BranchDiffFile = {
11
+ path: string;
12
+ /** Pre-rename path, only present for `R` entries. */
13
+ oldPath?: string;
14
+ status: BranchDiffFileStatus;
15
+ };
16
+
17
+ type BranchDiffInput = {
18
+ /** Repository root — every reported path is relative to it. */
19
+ projectPath: string;
20
+ /** Branch or remote-tracking ref the working copy is compared against. */
21
+ base: string;
22
+ runCommand: GitProcessRunner;
23
+ };
24
+
25
+ type BranchDiffFileInput = BranchDiffInput & {
26
+ /** Repository-relative path of the file to diff. */
27
+ file: string;
28
+ /**
29
+ * Pre-rename path for `R` entries. Diffing only the new path would make git
30
+ * report the whole file as added, so both paths are handed to git together
31
+ * and it pairs them into a rename with just the real content changes.
32
+ */
33
+ oldPath?: string;
34
+ /** Reads an untracked file so its content can be rendered as an all-additions diff. */
35
+ readFile: (absolutePath: string) => Promise<string>;
36
+ };
37
+
38
+ function toBranchDiffStatus(rawStatus: string): BranchDiffFileStatus {
39
+ // `--name-status` emits R<score>/C<score> for renames and copies; a copy is
40
+ // a brand-new file from the reader's point of view, and type changes (T) are
41
+ // modifications.
42
+ switch (rawStatus.charAt(0)) {
43
+ case 'A':
44
+ case 'C':
45
+ return 'A';
46
+ case 'D':
47
+ return 'D';
48
+ case 'R':
49
+ return 'R';
50
+ default:
51
+ return 'M';
52
+ }
53
+ }
54
+
55
+ function parseNameStatus(output: string): BranchDiffFile[] {
56
+ // `-z` output is `<status>\0<path>\0`, with renames/copies contributing a
57
+ // second path: `R100\0<old>\0<new>\0`. A trailing NUL leaves an empty token.
58
+ const tokens = output.split('\0');
59
+ const files: BranchDiffFile[] = [];
60
+
61
+ for (let index = 0; index < tokens.length; index += 1) {
62
+ const rawStatus = tokens[index];
63
+ if (!rawStatus) {
64
+ continue;
65
+ }
66
+
67
+ const status = toBranchDiffStatus(rawStatus);
68
+ const isTwoPathEntry = rawStatus.startsWith('R') || rawStatus.startsWith('C');
69
+ const firstPath = tokens[index + 1];
70
+ const secondPath = isTwoPathEntry ? tokens[index + 2] : undefined;
71
+ index += isTwoPathEntry ? 2 : 1;
72
+
73
+ if (!firstPath) {
74
+ continue;
75
+ }
76
+
77
+ if (isTwoPathEntry) {
78
+ if (!secondPath) {
79
+ continue;
80
+ }
81
+ files.push(status === 'R' ? { path: secondPath, oldPath: firstPath, status } : { path: secondPath, status });
82
+ continue;
83
+ }
84
+
85
+ files.push({ path: firstPath, status });
86
+ }
87
+
88
+ return files;
89
+ }
90
+
91
+ function parseUntrackedPaths(output: string): string[] {
92
+ return output.split('\0').filter(Boolean);
93
+ }
94
+
95
+ /**
96
+ * Turns a user-chosen base branch into the merge-base commit shared with HEAD.
97
+ * Rejects refs that would parse as git options and refs git cannot resolve
98
+ * before running `merge-base`, so the routes answer with a 400 and a stable
99
+ * code instead of a raw git failure.
100
+ */
101
+ async function resolveMergeBase(input: BranchDiffInput): Promise<string> {
102
+ const { projectPath, base, runCommand } = input;
103
+ const cwd = { cwd: projectPath };
104
+
105
+ // validateBranchName in the routes accepts a leading '-', which git would
106
+ // otherwise read as an option (e.g. `--output=file`).
107
+ if (!base || base.startsWith('-')) {
108
+ throw new AppError(`Invalid base branch "${base}"`, {
109
+ code: 'GIT_INVALID_BASE_REF',
110
+ statusCode: 400,
111
+ });
112
+ }
113
+
114
+ try {
115
+ await runCommand('git', ['rev-parse', '--verify', '--quiet', '--end-of-options', `${base}^{commit}`], cwd);
116
+ } catch {
117
+ throw new AppError(`Unknown branch or ref "${base}"`, {
118
+ code: 'GIT_UNKNOWN_BASE_REF',
119
+ statusCode: 400,
120
+ });
121
+ }
122
+
123
+ let mergeBaseOutput: string;
124
+ try {
125
+ ({ stdout: mergeBaseOutput } = await runCommand('git', ['merge-base', '--', base, 'HEAD'], cwd));
126
+ } catch (error) {
127
+ throw new AppError(`"${base}" and the current branch share no common history`, {
128
+ code: 'GIT_NO_MERGE_BASE',
129
+ statusCode: 400,
130
+ details: getGitErrorDetails(error),
131
+ });
132
+ }
133
+
134
+ const mergeBase = mergeBaseOutput.trim();
135
+ if (!mergeBase) {
136
+ throw new AppError(`"${base}" and the current branch share no common history`, {
137
+ code: 'GIT_NO_MERGE_BASE',
138
+ statusCode: 400,
139
+ });
140
+ }
141
+
142
+ return mergeBase;
143
+ }
144
+
145
+ /**
146
+ * Used by the Git routes module for `GET /branch-diff`: lists every file the
147
+ * working copy (committed, staged, unstaged and untracked) differs in from the
148
+ * merge base of `base` and HEAD — the PR-style "what this branch changed".
149
+ * Results are sorted by path.
150
+ */
151
+ export async function listBranchDiffFiles(input: BranchDiffInput): Promise<{
152
+ base: string;
153
+ mergeBase: string;
154
+ files: BranchDiffFile[];
155
+ }> {
156
+ const { projectPath, base, runCommand } = input;
157
+ const cwd = { cwd: projectPath };
158
+ const mergeBase = await resolveMergeBase(input);
159
+
160
+ // `git diff <commit>` compares the working tree (index + unstaged edits)
161
+ // against the commit, which is exactly the "everything so far" view.
162
+ const [{ stdout: nameStatusOutput }, { stdout: untrackedOutput }] = await Promise.all([
163
+ runCommand('git', ['diff', '--name-status', '-M', '-z', mergeBase, '--'], cwd),
164
+ runCommand('git', ['ls-files', '--others', '--exclude-standard', '-z'], cwd),
165
+ ]);
166
+
167
+ const files = [
168
+ ...parseNameStatus(nameStatusOutput),
169
+ ...parseUntrackedPaths(untrackedOutput).map((path): BranchDiffFile => ({ path, status: 'U' })),
170
+ ].sort((left, right) => left.path.localeCompare(right.path));
171
+
172
+ return { base, mergeBase, files };
173
+ }
174
+
175
+ /**
176
+ * Used by the Git routes module for `GET /branch-diff/file`: the raw unified
177
+ * diff of one file between the merge base and the working copy. Untracked
178
+ * files are not visible to `git diff`, so their content is synthesized into
179
+ * the same all-additions shape the `/diff` route produces.
180
+ */
181
+ export async function getBranchDiffForFile(input: BranchDiffFileInput): Promise<string> {
182
+ const { projectPath, file, oldPath, runCommand, readFile } = input;
183
+ const cwd = { cwd: projectPath };
184
+ const mergeBase = await resolveMergeBase(input);
185
+
186
+ // Ask git (not the filesystem) whether the path is untracked so a path that
187
+ // escapes the repository is rejected by git before anything is read.
188
+ const { stdout: untrackedOutput } = await runCommand(
189
+ 'git',
190
+ ['ls-files', '--others', '--exclude-standard', '-z', '--', file],
191
+ cwd,
192
+ );
193
+ const isUntracked = parseUntrackedPaths(untrackedOutput).includes(file);
194
+
195
+ if (isUntracked) {
196
+ const content = await readFile(path.join(projectPath, file));
197
+ const lines = content.split('\n');
198
+ return `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
199
+ }
200
+
201
+ const pathspecs = oldPath && oldPath !== file ? [oldPath, file] : [file];
202
+ const { stdout: diff } = await runCommand('git', ['diff', '-M', mergeBase, '--', ...pathspecs], cwd);
203
+ return diff;
204
+ }
@@ -1,21 +1,11 @@
1
+ import type { GitProcessRunner } from '@/shared/types.js';
1
2
  import { AppError } from '@/shared/utils.js';
2
3
 
3
- type GitCommandResult = {
4
- stdout: string;
5
- stderr: string;
6
- };
7
-
8
- type GitCommandRunner = (
9
- command: string,
10
- args: string[],
11
- options: { cwd: string },
12
- ) => Promise<GitCommandResult>;
13
-
14
4
  type DeleteLocalBranchInput = {
15
5
  projectPath: string;
16
6
  branch: string;
17
7
  force: boolean;
18
- runCommand: GitCommandRunner;
8
+ runCommand: GitProcessRunner;
19
9
  };
20
10
 
21
11
  /** Used by the Git routes module to safely delete a non-current local branch. */