relay-ide 0.1.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 (506) hide show
  1. package/README.md +259 -0
  2. package/dist/bin/claude-remote-cli.js +390 -0
  3. package/dist/bin/relay-ide.js +390 -0
  4. package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
  5. package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
  6. package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
  7. package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
  8. package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
  9. package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
  10. package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
  11. package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
  12. package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
  13. package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
  14. package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
  15. package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
  16. package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
  17. package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
  18. package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
  19. package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
  20. package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
  21. package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
  22. package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
  23. package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
  24. package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
  25. package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
  26. package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
  27. package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
  28. package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
  29. package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
  30. package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
  31. package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
  32. package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
  33. package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
  34. package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
  35. package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
  36. package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  37. package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  38. package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  39. package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  40. package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
  41. package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
  42. package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
  43. package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
  44. package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
  45. package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
  46. package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
  47. package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
  48. package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
  49. package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
  50. package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
  51. package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
  52. package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
  53. package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
  54. package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
  55. package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
  56. package/dist/frontend/assets/d-85-TOEBH.js +1 -0
  57. package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
  58. package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
  59. package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
  60. package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
  61. package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
  62. package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
  63. package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
  64. package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
  65. package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
  66. package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
  67. package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
  68. package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
  69. package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
  70. package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
  71. package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
  72. package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
  73. package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
  74. package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
  75. package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
  76. package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
  77. package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
  78. package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  79. package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
  80. package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
  81. package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
  82. package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
  83. package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
  84. package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
  85. package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
  86. package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
  87. package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
  88. package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
  89. package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
  90. package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  91. package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  92. package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
  93. package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
  94. package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  95. package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
  96. package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
  97. package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
  98. package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
  99. package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
  100. package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
  101. package/dist/frontend/assets/go-C27-OAKa.js +1 -0
  102. package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
  103. package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
  104. package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  105. package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  106. package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  107. package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  108. package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  109. package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  110. package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
  111. package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
  112. package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
  113. package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
  114. package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
  115. package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
  116. package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
  117. package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
  118. package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
  119. package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
  120. package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
  121. package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
  122. package/dist/frontend/assets/html-pp8916En.js +1 -0
  123. package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
  124. package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
  125. package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
  126. package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
  127. package/dist/frontend/assets/imba-DGztddWO.js +1 -0
  128. package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
  129. package/dist/frontend/assets/java-CylS5w8V.js +1 -0
  130. package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
  131. package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
  132. package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
  133. package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
  134. package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
  135. package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
  136. package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
  137. package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
  138. package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
  139. package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
  140. package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
  141. package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
  142. package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  143. package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  144. package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
  145. package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
  146. package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
  147. package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
  148. package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
  149. package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
  150. package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
  151. package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
  152. package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
  153. package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
  154. package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
  155. package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
  156. package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
  157. package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
  158. package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
  159. package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
  160. package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
  161. package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
  162. package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
  163. package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
  164. package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
  165. package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
  166. package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  167. package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
  168. package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  169. package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
  170. package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
  171. package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
  172. package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
  173. package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
  174. package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
  175. package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
  176. package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
  177. package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
  178. package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
  179. package/dist/frontend/assets/move-IF9eRakj.js +1 -0
  180. package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
  181. package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
  182. package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  183. package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
  184. package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
  185. package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
  186. package/dist/frontend/assets/nim-BIad80T-.js +1 -0
  187. package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
  188. package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
  189. package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
  190. package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
  191. package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
  192. package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
  193. package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
  194. package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  195. package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
  196. package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
  197. package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
  198. package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
  199. package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
  200. package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
  201. package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
  202. package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
  203. package/dist/frontend/assets/po-BTJTHyun.js +1 -0
  204. package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
  205. package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
  206. package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
  207. package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
  208. package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
  209. package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
  210. package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
  211. package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
  212. package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
  213. package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
  214. package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
  215. package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
  216. package/dist/frontend/assets/qml-3beO22l8.js +1 -0
  217. package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
  218. package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
  219. package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
  220. package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
  221. package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
  222. package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
  223. package/dist/frontend/assets/red-bN70gL4F.js +1 -0
  224. package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
  225. package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
  226. package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
  227. package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
  228. package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
  229. package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  230. package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  231. package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
  232. package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
  233. package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
  234. package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
  235. package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
  236. package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
  237. package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
  238. package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
  239. package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
  240. package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
  241. package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
  242. package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
  243. package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
  244. package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
  245. package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
  246. package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
  247. package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
  248. package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
  249. package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
  250. package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
  251. package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
  252. package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
  253. package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
  254. package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
  255. package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
  256. package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
  257. package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
  258. package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
  259. package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
  260. package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
  261. package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
  262. package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
  263. package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
  264. package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
  265. package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
  266. package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
  267. package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
  268. package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
  269. package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
  270. package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
  271. package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
  272. package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
  273. package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
  274. package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
  275. package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
  276. package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
  277. package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
  278. package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
  279. package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
  280. package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
  281. package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
  282. package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
  283. package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
  284. package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
  285. package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
  286. package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
  287. package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
  288. package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
  289. package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
  290. package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
  291. package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
  292. package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
  293. package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
  294. package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
  295. package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
  296. package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
  297. package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
  298. package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
  299. package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
  300. package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
  301. package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
  302. package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
  303. package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
  304. package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
  305. package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
  306. package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
  307. package/dist/frontend/icon-192.png +0 -0
  308. package/dist/frontend/icon-512.png +0 -0
  309. package/dist/frontend/icon.svg +8 -0
  310. package/dist/frontend/index.html +30 -0
  311. package/dist/frontend/manifest.json +25 -0
  312. package/dist/frontend/sw.js +66 -0
  313. package/dist/server/agent-events.js +39 -0
  314. package/dist/server/analytics.js +885 -0
  315. package/dist/server/auth.js +65 -0
  316. package/dist/server/belayer/executor.js +200 -0
  317. package/dist/server/belayer/intake.js +27 -0
  318. package/dist/server/belayer/pipeline.js +97 -0
  319. package/dist/server/belayer/pr-lifecycle.js +69 -0
  320. package/dist/server/belayer/prompts.js +154 -0
  321. package/dist/server/belayer/types.js +23 -0
  322. package/dist/server/branch-linker.js +137 -0
  323. package/dist/server/browser-content.js +145 -0
  324. package/dist/server/clipboard.js +63 -0
  325. package/dist/server/codex-hooks-adapter.js +93 -0
  326. package/dist/server/config.js +325 -0
  327. package/dist/server/gh-routes.js +163 -0
  328. package/dist/server/gh.js +276 -0
  329. package/dist/server/git-routes.js +154 -0
  330. package/dist/server/git.js +694 -0
  331. package/dist/server/github-app.js +218 -0
  332. package/dist/server/github-graphql.js +178 -0
  333. package/dist/server/hooks.js +373 -0
  334. package/dist/server/index.js +1549 -0
  335. package/dist/server/integration-github.js +137 -0
  336. package/dist/server/integration-jira.js +210 -0
  337. package/dist/server/integration-linear.js +176 -0
  338. package/dist/server/logger.js +18 -0
  339. package/dist/server/mobile-input-pipeline.js +129 -0
  340. package/dist/server/opencode-relay.js +53 -0
  341. package/dist/server/org-dashboard.js +241 -0
  342. package/dist/server/output-parsers/claude-parser.js +56 -0
  343. package/dist/server/output-parsers/codex-parser.js +13 -0
  344. package/dist/server/output-parsers/index.js +14 -0
  345. package/dist/server/output-parsers/null-parser.js +12 -0
  346. package/dist/server/output-parsers/opencode-parser.js +77 -0
  347. package/dist/server/pty-handler.js +586 -0
  348. package/dist/server/push.js +84 -0
  349. package/dist/server/review-poller.js +237 -0
  350. package/dist/server/sdk-handler.js +539 -0
  351. package/dist/server/service.js +189 -0
  352. package/dist/server/sessions.js +638 -0
  353. package/dist/server/telemetry.js +236 -0
  354. package/dist/server/ticket-transitions.js +166 -0
  355. package/dist/server/types.js +146 -0
  356. package/dist/server/utils.js +23 -0
  357. package/dist/server/watcher.js +661 -0
  358. package/dist/server/webhook-manager.js +547 -0
  359. package/dist/server/webhooks.js +73 -0
  360. package/dist/server/workspace-groups.js +363 -0
  361. package/dist/server/workspaces.js +1207 -0
  362. package/dist/server/ws.js +192 -0
  363. package/dist/test/EmptyState.spec.js +51 -0
  364. package/dist/test/action-coverage.test.js +139 -0
  365. package/dist/test/actions/registry.test.js +59 -0
  366. package/dist/test/actions/shortcuts.test.js +79 -0
  367. package/dist/test/agent-events.test.js +151 -0
  368. package/dist/test/analytics.test.js +158 -0
  369. package/dist/test/attention.test.js +91 -0
  370. package/dist/test/auth.test.js +105 -0
  371. package/dist/test/backend-state.test.js +47 -0
  372. package/dist/test/belayer-executor.test.js +33 -0
  373. package/dist/test/belayer-intake.test.js +44 -0
  374. package/dist/test/belayer-pipeline.test.js +113 -0
  375. package/dist/test/belayer-pr-lifecycle.test.js +26 -0
  376. package/dist/test/belayer-prompts.test.js +60 -0
  377. package/dist/test/belayer-types.test.js +69 -0
  378. package/dist/test/bin/claude-remote-cli.js +214 -0
  379. package/dist/test/boot-state.test.js +133 -0
  380. package/dist/test/branch-lifecycle.test.js +75 -0
  381. package/dist/test/branch-linker.test.js +236 -0
  382. package/dist/test/branch-rename.test.js +45 -0
  383. package/dist/test/branch-watcher.test.js +115 -0
  384. package/dist/test/browser-cli.test.js +91 -0
  385. package/dist/test/browser-content.test.js +93 -0
  386. package/dist/test/browser-tabs-ui.test.js +39 -0
  387. package/dist/test/changed-files-api.test.js +140 -0
  388. package/dist/test/clipboard.test.js +12 -0
  389. package/dist/test/codex-hooks-adapter.test.js +237 -0
  390. package/dist/test/components/EmptyState.spec.js +51 -0
  391. package/dist/test/components/ErrorToast.spec.js +65 -0
  392. package/dist/test/components/TuiCheckbox.spec.js +120 -0
  393. package/dist/test/components/TuiInput.spec.js +186 -0
  394. package/dist/test/components/leaf-component-migration.spec.js +104 -0
  395. package/dist/test/config-freshness.test.js +63 -0
  396. package/dist/test/config.test.js +813 -0
  397. package/dist/test/diff-summary.test.js +98 -0
  398. package/dist/test/display-state.test.js +179 -0
  399. package/dist/test/event-message-types.test.js +32 -0
  400. package/dist/test/file-tree-utils.test.js +167 -0
  401. package/dist/test/framework-types.test.js +183 -0
  402. package/dist/test/frameworks-api.test.js +93 -0
  403. package/dist/test/frontend/src/lib/pr-state.js +114 -0
  404. package/dist/test/fs-browse.test.js +246 -0
  405. package/dist/test/fuzzy-scorer.test.js +145 -0
  406. package/dist/test/gh-routes.test.js +156 -0
  407. package/dist/test/git-changed-files.test.js +152 -0
  408. package/dist/test/git-routes.test.js +146 -0
  409. package/dist/test/git-utils.test.js +68 -0
  410. package/dist/test/git-watcher.test.js +110 -0
  411. package/dist/test/git.test.js +140 -0
  412. package/dist/test/github-app.test.js +455 -0
  413. package/dist/test/github-graphql.test.js +301 -0
  414. package/dist/test/greetings.test.js +83 -0
  415. package/dist/test/hooks-agent-event.test.js +412 -0
  416. package/dist/test/hooks.test.js +149 -0
  417. package/dist/test/integration-github.test.js +220 -0
  418. package/dist/test/integration-jira.test.js +238 -0
  419. package/dist/test/integration-linear.test.js +293 -0
  420. package/dist/test/mobile-input.test.js +235 -0
  421. package/dist/test/opencode-relay.test.js +107 -0
  422. package/dist/test/org-dashboard.test.js +349 -0
  423. package/dist/test/output-parser.test.js +217 -0
  424. package/dist/test/paths.test.js +32 -0
  425. package/dist/test/pr-state.test.js +407 -0
  426. package/dist/test/pr-status.test.js +82 -0
  427. package/dist/test/presets.test.js +242 -0
  428. package/dist/test/pty-handler-multi-agent.test.js +149 -0
  429. package/dist/test/pty-handler.test.js +146 -0
  430. package/dist/test/pull-requests.test.js +78 -0
  431. package/dist/test/review-poller.test.js +349 -0
  432. package/dist/test/server/analytics.js +121 -0
  433. package/dist/test/server/auth.js +63 -0
  434. package/dist/test/server/branch-linker.js +124 -0
  435. package/dist/test/server/clipboard.js +56 -0
  436. package/dist/test/server/config.js +137 -0
  437. package/dist/test/server/git.js +308 -0
  438. package/dist/test/server/hooks.js +196 -0
  439. package/dist/test/server/index.js +1124 -0
  440. package/dist/test/server/integration-github.js +117 -0
  441. package/dist/test/server/integration-jira.js +164 -0
  442. package/dist/test/server/integration-linear.js +176 -0
  443. package/dist/test/server/mobile-input-pipeline.js +123 -0
  444. package/dist/test/server/org-dashboard.js +184 -0
  445. package/dist/test/server/output-parsers/claude-parser.js +54 -0
  446. package/dist/test/server/output-parsers/codex-parser.js +13 -0
  447. package/dist/test/server/output-parsers/index.js +7 -0
  448. package/dist/test/server/pty-handler.js +310 -0
  449. package/dist/test/server/push.js +80 -0
  450. package/dist/test/server/review-poller.js +218 -0
  451. package/dist/test/server/service.js +169 -0
  452. package/dist/test/server/sessions.js +434 -0
  453. package/dist/test/server/ticket-transitions.js +216 -0
  454. package/dist/test/server/types.js +20 -0
  455. package/dist/test/server/utils.js +22 -0
  456. package/dist/test/server/watcher.js +139 -0
  457. package/dist/test/server/workspaces.js +657 -0
  458. package/dist/test/server/ws.js +152 -0
  459. package/dist/test/server-startup.test.js +62 -0
  460. package/dist/test/service.test.js +43 -0
  461. package/dist/test/session-analytics-api.test.js +123 -0
  462. package/dist/test/session-analytics.test.js +425 -0
  463. package/dist/test/session-intent.test.js +249 -0
  464. package/dist/test/sessions.test.js +1152 -0
  465. package/dist/test/sidebar-items.test.js +164 -0
  466. package/dist/test/stores/boot-state-store.test.js +165 -0
  467. package/dist/test/stores/sessions-logic.test.js +191 -0
  468. package/dist/test/stores/toasts-store.test.js +66 -0
  469. package/dist/test/stores/ui-store.test.js +203 -0
  470. package/dist/test/stores/unread-store.test.js +97 -0
  471. package/dist/test/telemetry-api.test.js +54 -0
  472. package/dist/test/telemetry-sync.test.js +68 -0
  473. package/dist/test/telemetry.test.js +295 -0
  474. package/dist/test/terminal-zoom.test.js +102 -0
  475. package/dist/test/test/analytics.test.js +152 -0
  476. package/dist/test/test/auth.test.js +95 -0
  477. package/dist/test/test/branch-linker.test.js +231 -0
  478. package/dist/test/test/branch-rename.test.js +45 -0
  479. package/dist/test/test/clipboard.test.js +12 -0
  480. package/dist/test/test/config.test.js +281 -0
  481. package/dist/test/test/fs-browse.test.js +202 -0
  482. package/dist/test/test/git.test.js +67 -0
  483. package/dist/test/test/hooks.test.js +139 -0
  484. package/dist/test/test/integration-github.test.js +203 -0
  485. package/dist/test/test/integration-jira.test.js +294 -0
  486. package/dist/test/test/integration-linear.test.js +293 -0
  487. package/dist/test/test/mobile-input.test.js +193 -0
  488. package/dist/test/test/org-dashboard.test.js +240 -0
  489. package/dist/test/test/output-parser.test.js +95 -0
  490. package/dist/test/test/paths.test.js +32 -0
  491. package/dist/test/test/pr-state.test.js +220 -0
  492. package/dist/test/test/pull-requests.test.js +67 -0
  493. package/dist/test/test/review-poller.test.js +235 -0
  494. package/dist/test/test/service.test.js +43 -0
  495. package/dist/test/test/sessions.test.js +750 -0
  496. package/dist/test/test/ticket-transitions.test.js +130 -0
  497. package/dist/test/test/version.test.js +34 -0
  498. package/dist/test/test/worktrees.test.js +256 -0
  499. package/dist/test/ticket-transitions.test.js +312 -0
  500. package/dist/test/unread.test.js +23 -0
  501. package/dist/test/version.test.js +34 -0
  502. package/dist/test/webhook-manager.test.js +484 -0
  503. package/dist/test/webhooks.test.js +208 -0
  504. package/dist/test/workspace-groups.test.js +377 -0
  505. package/dist/test/worktrees.test.js +531 -0
  506. package/package.json +88 -0
@@ -0,0 +1,1207 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { Router } from 'express';
8
+ import { loadConfig, saveConfig, getRepoSettings, setRepoSettings, deleteRepoSettingKeys, writeMeta, readMeta, } from './config.js';
9
+ import { findOrCreateWorktreeForBranch } from './watcher.js';
10
+ import { trackEvent } from './analytics.js';
11
+ import { listBranches, getActivityFeed, switchBranch, getCurrentBranch, extractOwnerRepo, renameBranch, createBranch, pushBranch, getChangedFiles, getFileDiff, getDefaultBranch, ensureBranchLocal, } from './git.js';
12
+ import { clearPrCache as clearPrCacheImpl } from './gh.js';
13
+ import { MOUNTAIN_NAMES } from './types.js';
14
+ import { createLogger } from './logger.js';
15
+ const execFileAsync = promisify(execFile);
16
+ const logger = createLogger('workspaces');
17
+ /** Extract repo name from a git remote URL (SSH or HTTPS). */
18
+ export function repoNameFromRemoteUrl(url) {
19
+ // Strip trailing slash before splitting so pop() gets the last real segment
20
+ const name = url
21
+ .replace(/\/+$/, '')
22
+ .split('/')
23
+ .pop()
24
+ ?.replace(/\.git$/, '');
25
+ return name || undefined;
26
+ }
27
+ const BROWSE_DENYLIST = new Set([
28
+ 'node_modules',
29
+ '.git',
30
+ '.Trash',
31
+ '__pycache__',
32
+ '.cache',
33
+ '.npm',
34
+ '.yarn',
35
+ '.nvm',
36
+ ]);
37
+ // ── Files-list cache (used by GET /workspaces/files-list) ──
38
+ const filesListCache = new Map();
39
+ const FILES_LIST_TTL = 30_000;
40
+ const FILES_LIST_MAX = 50_000;
41
+ export function clearFilesListCache(workspacePath) {
42
+ if (!workspacePath) {
43
+ filesListCache.clear();
44
+ return;
45
+ }
46
+ // Direct match (most common: watcher path = repo root)
47
+ if (filesListCache.delete(workspacePath))
48
+ return;
49
+ // Subdirectory match: watcher path may be a subdirectory of the cached repo root
50
+ for (const key of filesListCache.keys()) {
51
+ if (workspacePath.startsWith(key + path.sep)) {
52
+ filesListCache.delete(key);
53
+ return;
54
+ }
55
+ }
56
+ }
57
+ const BROWSE_MAX_ENTRIES = 100;
58
+ const BULK_MAX_PATHS = 50;
59
+ export { clearPrCacheImpl as clearPrCache };
60
+ // Exported helpers
61
+ /**
62
+ * Resolves and validates a raw workspace path string.
63
+ * Throws with a human-readable message if the path is invalid.
64
+ */
65
+ export async function validateWorkspacePath(rawPath) {
66
+ if (!rawPath || typeof rawPath !== 'string') {
67
+ throw new Error('Path must be a non-empty string');
68
+ }
69
+ const resolved = path.resolve(rawPath);
70
+ let stat;
71
+ try {
72
+ stat = await fs.promises.stat(resolved);
73
+ }
74
+ catch {
75
+ throw new Error(`Path does not exist: ${resolved}`);
76
+ }
77
+ if (!stat.isDirectory()) {
78
+ throw new Error(`Path is not a directory: ${resolved}`);
79
+ }
80
+ return resolved;
81
+ }
82
+ /**
83
+ * Detects whether a directory is the root of a git repository and, if so,
84
+ * what the default branch name is.
85
+ */
86
+ export async function detectGitRepo(dirPath, execAsync = execFileAsync) {
87
+ try {
88
+ await execAsync('git', ['rev-parse', '--git-dir'], { cwd: dirPath });
89
+ }
90
+ catch {
91
+ return { isGitRepo: false, defaultBranch: null };
92
+ }
93
+ // Attempt to determine the default branch from remote HEAD
94
+ let defaultBranch = null;
95
+ try {
96
+ const { stdout } = await execAsync('git', ['symbolic-ref', 'refs/remotes/origin/HEAD', '--short'], { cwd: dirPath });
97
+ const trimmed = stdout.trim();
98
+ // "origin/main" → "main"
99
+ defaultBranch = trimmed.replace(/^origin\//, '') || null;
100
+ }
101
+ catch {
102
+ // Fall back to checking local HEAD
103
+ try {
104
+ const { stdout } = await execAsync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: dirPath });
105
+ defaultBranch = stdout.trim() || null;
106
+ }
107
+ catch {
108
+ // Cannot determine default branch
109
+ }
110
+ }
111
+ return { isGitRepo: true, defaultBranch };
112
+ }
113
+ function expandTilde(p) {
114
+ if (p === '~' || p.startsWith('~/')) {
115
+ const homeDir = os.homedir();
116
+ const resolved = path.resolve(path.join(homeDir, p.slice(1)));
117
+ const relative = path.relative(homeDir, resolved);
118
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
119
+ return p; // traversal attempt — return unexpanded
120
+ }
121
+ return resolved;
122
+ }
123
+ return p;
124
+ }
125
+ // Router factory
126
+ /**
127
+ * Creates and returns an Express Router that handles all /workspaces routes.
128
+ *
129
+ * Caller is responsible for mounting and applying auth middleware:
130
+ * app.use('/workspaces', requireAuth, createWorkspaceRouter({ configPath }));
131
+ */
132
+ export function createWorkspaceRouter(deps) {
133
+ const { configPath } = deps;
134
+ const exec = deps.execAsync ?? execFileAsync;
135
+ const router = Router();
136
+ // Helper: reload config on every request so concurrent changes are reflected
137
+ function getConfig() {
138
+ return loadConfig(configPath);
139
+ }
140
+ // GET /workspaces — list all workspaces with git info
141
+ router.get('/', async (_req, res) => {
142
+ const config = getConfig();
143
+ const workspacePaths = config.repos ?? [];
144
+ const results = await Promise.all(workspacePaths.map(async (p) => {
145
+ const { isGitRepo, defaultBranch } = await detectGitRepo(p, exec);
146
+ let name = path.basename(p);
147
+ let currentBranch = null;
148
+ if (isGitRepo) {
149
+ try {
150
+ const { stdout } = await exec('git', ['remote', 'get-url', 'origin'], { cwd: p });
151
+ const url = stdout.trim();
152
+ if (url) {
153
+ const remoteName = repoNameFromRemoteUrl(url);
154
+ if (remoteName)
155
+ name = remoteName;
156
+ }
157
+ }
158
+ catch {
159
+ // No remote configured — fall back to directory name
160
+ }
161
+ try {
162
+ const { stdout } = await exec('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: p });
163
+ currentBranch = stdout.trim() || null;
164
+ }
165
+ catch {
166
+ /* detached HEAD or other error */
167
+ }
168
+ }
169
+ return { path: p, name, isGitRepo, defaultBranch, currentBranch };
170
+ }));
171
+ res.json({ workspaces: results });
172
+ });
173
+ // POST /workspaces — add a workspace
174
+ router.post('/', async (req, res) => {
175
+ const body = req.body;
176
+ const rawPath = body.path;
177
+ if (typeof rawPath !== 'string' || !rawPath) {
178
+ res.status(400).json({ error: 'path is required' });
179
+ return;
180
+ }
181
+ let resolved;
182
+ try {
183
+ resolved = await validateWorkspacePath(rawPath);
184
+ }
185
+ catch (err) {
186
+ res
187
+ .status(400)
188
+ .json({ error: err instanceof Error ? err.message : String(err) });
189
+ return;
190
+ }
191
+ const config = getConfig();
192
+ const workspaces = config.repos ?? [];
193
+ if (workspaces.includes(resolved)) {
194
+ res.status(409).json({ error: 'Workspace already exists' });
195
+ return;
196
+ }
197
+ const { isGitRepo, defaultBranch } = await detectGitRepo(resolved, exec);
198
+ config.repos = [...workspaces, resolved];
199
+ // Store detected default branch in per-repo settings
200
+ if (isGitRepo && defaultBranch) {
201
+ if (!config.repoSettings)
202
+ config.repoSettings = {};
203
+ config.repoSettings[resolved] = {
204
+ ...config.repoSettings[resolved],
205
+ defaultBranch,
206
+ };
207
+ }
208
+ saveConfig(configPath, config);
209
+ try {
210
+ deps.onWorkspacesChanged?.();
211
+ }
212
+ catch (err) {
213
+ logger.error('onWorkspacesChanged failed:', err);
214
+ }
215
+ trackEvent({
216
+ category: 'workspace',
217
+ action: 'added',
218
+ target: resolved,
219
+ properties: { name: path.basename(resolved) },
220
+ });
221
+ let currentBranch = null;
222
+ if (isGitRepo) {
223
+ try {
224
+ const { stdout } = await exec('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: resolved });
225
+ currentBranch = stdout.trim() || null;
226
+ }
227
+ catch {
228
+ /* detached HEAD */
229
+ }
230
+ }
231
+ const workspace = {
232
+ path: resolved,
233
+ name: path.basename(resolved),
234
+ isGitRepo,
235
+ defaultBranch,
236
+ currentBranch,
237
+ };
238
+ res.status(201).json(workspace);
239
+ });
240
+ // DELETE /workspaces — remove a workspace
241
+ router.delete('/', async (req, res) => {
242
+ const body = req.body;
243
+ const rawPath = body.path;
244
+ if (typeof rawPath !== 'string' || !rawPath) {
245
+ res.status(400).json({ error: 'path is required' });
246
+ return;
247
+ }
248
+ const resolved = path.resolve(rawPath);
249
+ const config = getConfig();
250
+ const workspaces = config.repos ?? [];
251
+ const idx = workspaces.indexOf(resolved);
252
+ if (idx === -1) {
253
+ res.status(404).json({ error: 'Workspace not found' });
254
+ return;
255
+ }
256
+ // Clean up GitHub webhook if one exists for this workspace
257
+ const wsSettings = config.repoSettings?.[resolved];
258
+ if (wsSettings?.webhookId && config.github?.accessToken) {
259
+ try {
260
+ const { stdout } = await exec('git', ['remote', 'get-url', 'origin'], {
261
+ cwd: resolved,
262
+ timeout: 5000,
263
+ });
264
+ const ownerRepo = extractOwnerRepo(stdout.trim());
265
+ if (ownerRepo) {
266
+ await globalThis.fetch(`https://api.github.com/repos/${ownerRepo}/hooks/${wsSettings.webhookId}`, {
267
+ method: 'DELETE',
268
+ headers: {
269
+ Authorization: `Bearer ${config.github.accessToken}`,
270
+ Accept: 'application/vnd.github+json',
271
+ 'X-GitHub-Api-Version': '2022-11-28',
272
+ },
273
+ });
274
+ }
275
+ }
276
+ catch (err) {
277
+ // Best-effort — log but don't block workspace removal
278
+ logger.warn('Failed to delete webhook for', resolved, err instanceof Error ? err.message : String(err));
279
+ }
280
+ }
281
+ // Also clean up webhook-related repoSettings
282
+ if (config.repoSettings?.[resolved]) {
283
+ delete config.repoSettings[resolved].webhookId;
284
+ delete config.repoSettings[resolved].webhookEnabled;
285
+ delete config.repoSettings[resolved].webhookError;
286
+ }
287
+ config.repos = workspaces.filter((p) => p !== resolved);
288
+ saveConfig(configPath, config);
289
+ try {
290
+ deps.onWorkspacesChanged?.();
291
+ }
292
+ catch (err) {
293
+ logger.error('onWorkspacesChanged failed:', err);
294
+ }
295
+ trackEvent({ category: 'workspace', action: 'removed', target: resolved });
296
+ res.json({ removed: resolved });
297
+ });
298
+ // PUT /workspaces/reorder — reorder workspaces
299
+ router.put('/reorder', async (req, res) => {
300
+ const body = req.body;
301
+ const rawPaths = body.paths;
302
+ if (!Array.isArray(rawPaths)) {
303
+ res.status(400).json({ error: 'paths array is required' });
304
+ return;
305
+ }
306
+ const config = getConfig();
307
+ const current = config.repos ?? [];
308
+ // Validate that the submitted paths are the same set as the current workspaces
309
+ if (rawPaths.length !== current.length) {
310
+ res.status(400).json({
311
+ error: 'paths must contain the same set of workspaces as the current configuration',
312
+ });
313
+ return;
314
+ }
315
+ const currentSet = new Set(current);
316
+ for (const p of rawPaths) {
317
+ if (typeof p !== 'string' || !currentSet.has(p)) {
318
+ res.status(400).json({
319
+ error: 'paths must contain the same set of workspaces as the current configuration',
320
+ });
321
+ return;
322
+ }
323
+ }
324
+ config.repos = rawPaths;
325
+ saveConfig(configPath, config);
326
+ try {
327
+ deps.onWorkspacesChanged?.();
328
+ }
329
+ catch (err) {
330
+ logger.error('onWorkspacesChanged failed:', err);
331
+ }
332
+ const results = await Promise.all(rawPaths.map(async (p) => {
333
+ const name = path.basename(p);
334
+ const { isGitRepo, defaultBranch } = await detectGitRepo(p, exec);
335
+ let currentBranch = null;
336
+ if (isGitRepo) {
337
+ try {
338
+ const { stdout } = await exec('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: p });
339
+ currentBranch = stdout.trim() || null;
340
+ }
341
+ catch {
342
+ /* detached HEAD */
343
+ }
344
+ }
345
+ return { path: p, name, isGitRepo, defaultBranch, currentBranch };
346
+ }));
347
+ res.json({ workspaces: results });
348
+ });
349
+ // POST /workspaces/bulk — add multiple workspaces at once
350
+ router.post('/bulk', async (req, res) => {
351
+ const body = req.body;
352
+ const rawPaths = body.paths;
353
+ if (!Array.isArray(rawPaths) || rawPaths.length === 0) {
354
+ res.status(400).json({ error: 'paths array is required' });
355
+ return;
356
+ }
357
+ if (rawPaths.length > BULK_MAX_PATHS) {
358
+ res.status(400).json({ error: `Too many paths (max ${BULK_MAX_PATHS})` });
359
+ return;
360
+ }
361
+ const config = getConfig();
362
+ const existing = new Set(config.repos ?? []);
363
+ const added = [];
364
+ const errors = [];
365
+ for (const rawPath of rawPaths) {
366
+ if (typeof rawPath !== 'string' || !rawPath) {
367
+ errors.push({ path: String(rawPath), error: 'Invalid path' });
368
+ continue;
369
+ }
370
+ let resolved;
371
+ try {
372
+ resolved = await validateWorkspacePath(rawPath);
373
+ }
374
+ catch (err) {
375
+ errors.push({
376
+ path: rawPath,
377
+ error: err instanceof Error ? err.message : String(err),
378
+ });
379
+ continue;
380
+ }
381
+ if (existing.has(resolved)) {
382
+ errors.push({ path: rawPath, error: 'Already exists' });
383
+ continue;
384
+ }
385
+ const { isGitRepo, defaultBranch } = await detectGitRepo(resolved, exec);
386
+ existing.add(resolved);
387
+ let currentBranch = null;
388
+ if (isGitRepo) {
389
+ try {
390
+ const { stdout } = await exec('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: resolved });
391
+ currentBranch = stdout.trim() || null;
392
+ }
393
+ catch {
394
+ /* detached HEAD */
395
+ }
396
+ }
397
+ added.push({
398
+ path: resolved,
399
+ name: path.basename(resolved),
400
+ isGitRepo,
401
+ defaultBranch,
402
+ currentBranch,
403
+ });
404
+ // Store detected default branch in per-repo settings
405
+ if (isGitRepo && defaultBranch) {
406
+ if (!config.repoSettings)
407
+ config.repoSettings = {};
408
+ config.repoSettings[resolved] = {
409
+ ...config.repoSettings[resolved],
410
+ defaultBranch,
411
+ };
412
+ }
413
+ }
414
+ if (added.length > 0) {
415
+ config.repos = [...(config.repos ?? []), ...added.map((a) => a.path)];
416
+ saveConfig(configPath, config);
417
+ try {
418
+ deps.onWorkspacesChanged?.();
419
+ }
420
+ catch (err) {
421
+ logger.error('onWorkspacesChanged failed:', err);
422
+ }
423
+ }
424
+ res.status(201).json({ added, errors });
425
+ });
426
+ // GET /workspaces/dashboard — aggregated PR + activity data for a workspace
427
+ router.get('/dashboard', async (req, res) => {
428
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
429
+ if (!repoPath) {
430
+ res.status(400).json({ error: 'path query parameter is required' });
431
+ return;
432
+ }
433
+ const fields = 'number,title,url,headRefName,baseRefName,state,author,updatedAt,additions,deletions,reviewDecision,mergeable,mergeStateStatus,isDraft';
434
+ // Get current GitHub user
435
+ let currentUser = '';
436
+ try {
437
+ const { stdout: whoami } = await exec('gh', ['api', 'user', '--jq', '.login'], { cwd: repoPath });
438
+ currentUser = whoami.trim();
439
+ }
440
+ catch {
441
+ const response = {
442
+ prs: [],
443
+ error: 'gh_not_authenticated',
444
+ };
445
+ res.json({ pullRequests: response, branches: [] });
446
+ return;
447
+ }
448
+ // Helper to map raw gh JSON to PullRequest
449
+ function mapRawPr(raw, role, fallbackAuthor) {
450
+ return {
451
+ number: raw.number,
452
+ title: raw.title,
453
+ url: raw.url,
454
+ headRefName: raw.headRefName,
455
+ baseRefName: raw.baseRefName ?? '',
456
+ state: raw.state,
457
+ author: raw.author?.login ?? fallbackAuthor,
458
+ role,
459
+ updatedAt: raw.updatedAt,
460
+ additions: raw.additions ?? 0,
461
+ deletions: raw.deletions ?? 0,
462
+ reviewDecision: raw.reviewDecision ?? null,
463
+ mergeable: raw.mergeable ??
464
+ null,
465
+ isDraft: raw.isDraft ?? false,
466
+ ciStatus: null,
467
+ };
468
+ }
469
+ // Fetch authored + review-requested PRs in parallel
470
+ const [authored, reviewing] = await Promise.all([
471
+ (async () => {
472
+ try {
473
+ const { stdout } = await exec('gh', [
474
+ 'pr',
475
+ 'list',
476
+ '--author',
477
+ currentUser,
478
+ '--state',
479
+ 'open',
480
+ '--limit',
481
+ '30',
482
+ '--json',
483
+ fields,
484
+ ], { cwd: repoPath });
485
+ return JSON.parse(stdout).map((pr) => mapRawPr(pr, 'author', currentUser));
486
+ }
487
+ catch {
488
+ return [];
489
+ }
490
+ })(),
491
+ (async () => {
492
+ try {
493
+ const { stdout } = await exec('gh', [
494
+ 'pr',
495
+ 'list',
496
+ '--search',
497
+ `review-requested:${currentUser}`,
498
+ '--state',
499
+ 'open',
500
+ '--limit',
501
+ '30',
502
+ '--json',
503
+ fields,
504
+ ], { cwd: repoPath });
505
+ return JSON.parse(stdout).map((pr) => mapRawPr(pr, 'reviewer', ''));
506
+ }
507
+ catch {
508
+ return [];
509
+ }
510
+ })(),
511
+ ]);
512
+ // Deduplicate: if a PR appears in both, keep as 'author'
513
+ const seen = new Set(authored.map((pr) => pr.number));
514
+ const combined = [
515
+ ...authored,
516
+ ...reviewing.filter((pr) => !seen.has(pr.number)),
517
+ ];
518
+ // Sort by updatedAt descending
519
+ combined.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
520
+ const pullRequests = { prs: combined };
521
+ // Fetch branches for the repo
522
+ let branches = [];
523
+ try {
524
+ branches = await listBranches(repoPath);
525
+ }
526
+ catch {
527
+ /* not a git repo or git unavailable */
528
+ }
529
+ // Fetch recent activity
530
+ let activity = [];
531
+ try {
532
+ activity = await getActivityFeed(repoPath);
533
+ }
534
+ catch {
535
+ /* git log unavailable */
536
+ }
537
+ res.json({
538
+ pullRequests,
539
+ branches,
540
+ activity,
541
+ });
542
+ });
543
+ function buildMergedSettings(config, repoPath) {
544
+ const resolved = path.resolve(repoPath);
545
+ const wsOverrides = config.repoSettings?.[resolved] ?? {};
546
+ const effective = getRepoSettings(config, resolved);
547
+ const overridden = [];
548
+ for (const key of [
549
+ 'defaultAgent',
550
+ 'defaultContinue',
551
+ 'defaultYolo',
552
+ 'launchInTmux',
553
+ ]) {
554
+ if (wsOverrides[key] !== undefined)
555
+ overridden.push(key);
556
+ }
557
+ return { settings: effective, overridden };
558
+ }
559
+ // GET /workspaces/settings — per-repo overrides only
560
+ router.get('/settings', async (req, res) => {
561
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
562
+ if (!repoPath) {
563
+ res.status(400).json({ error: 'path query parameter is required' });
564
+ return;
565
+ }
566
+ // Backward compat: handle merged=true inline (same logic as /settings/merged)
567
+ if (req.query.merged === 'true') {
568
+ res.json(buildMergedSettings(getConfig(), repoPath));
569
+ return;
570
+ }
571
+ const config = getConfig();
572
+ const resolved = path.resolve(repoPath);
573
+ const settings = config.repoSettings?.[resolved] ?? {};
574
+ res.json(settings);
575
+ });
576
+ // GET /workspaces/settings/merged — effective settings with override tracking
577
+ router.get('/settings/merged', async (req, res) => {
578
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
579
+ if (!repoPath) {
580
+ res.status(400).json({ error: 'path query parameter is required' });
581
+ return;
582
+ }
583
+ res.json(buildMergedSettings(getConfig(), repoPath));
584
+ });
585
+ // PATCH /workspaces/settings — update per-repo settings
586
+ router.patch('/settings', async (req, res) => {
587
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
588
+ if (!repoPath) {
589
+ res.status(400).json({ error: 'path query parameter is required' });
590
+ return;
591
+ }
592
+ const resolved = path.resolve(repoPath);
593
+ const updates = req.body;
594
+ const config = getConfig();
595
+ // Separate null values (deletions) from actual updates
596
+ const keysToDelete = [];
597
+ const keysToUpdate = {};
598
+ for (const [key, value] of Object.entries(updates)) {
599
+ if (value === null) {
600
+ keysToDelete.push(key);
601
+ }
602
+ else {
603
+ keysToUpdate[key] = value;
604
+ }
605
+ }
606
+ // Apply deletions first
607
+ if (keysToDelete.length > 0) {
608
+ deleteRepoSettingKeys(configPath, config, resolved, keysToDelete);
609
+ }
610
+ // Apply updates
611
+ if (Object.keys(keysToUpdate).length > 0) {
612
+ setRepoSettings(configPath, config, resolved, keysToUpdate);
613
+ }
614
+ // Return the current raw repo settings
615
+ const final = config.repoSettings?.[resolved] ?? {};
616
+ res.json(final);
617
+ });
618
+ // POST /workspaces/branch — switch branch for a workspace
619
+ router.post('/branch', async (req, res) => {
620
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
621
+ if (!repoPath) {
622
+ res.status(400).json({ error: 'path query parameter is required' });
623
+ return;
624
+ }
625
+ const body = req.body;
626
+ const branch = body.branch;
627
+ if (typeof branch !== 'string' || !branch) {
628
+ res.status(400).json({ error: 'branch is required in request body' });
629
+ return;
630
+ }
631
+ const result = await switchBranch(repoPath, branch);
632
+ if (result.success) {
633
+ res.json({ path: repoPath, branch });
634
+ }
635
+ else {
636
+ res.status(400).json({
637
+ error: result.error ?? `Failed to switch to branch: ${branch}`,
638
+ });
639
+ }
640
+ });
641
+ // POST /workspaces/worktree — create a new worktree with the next mountain name
642
+ router.post('/worktree', async (req, res) => {
643
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
644
+ if (!repoPath) {
645
+ res.status(400).json({ error: 'path query parameter is required' });
646
+ return;
647
+ }
648
+ const existingBranch = typeof req.body?.branch === 'string' ? req.body.branch : undefined;
649
+ const resolved = path.resolve(repoPath);
650
+ const config = getConfig();
651
+ const settings = getRepoSettings(config, resolved);
652
+ let branchName = '';
653
+ let mountainName = '';
654
+ let gitArgs;
655
+ let nextMountainIndex;
656
+ if (existingBranch) {
657
+ // Ensure branch exists locally (fetch from remote if needed)
658
+ let branchResult;
659
+ try {
660
+ branchResult = await ensureBranchLocal(resolved, existingBranch, {
661
+ exec,
662
+ });
663
+ }
664
+ catch (err) {
665
+ logger.error('ensureBranchLocal failed unexpectedly:', err instanceof Error ? err.message : err);
666
+ res.status(500).json({ error: 'Git operation failed' });
667
+ return;
668
+ }
669
+ if (!branchResult.found) {
670
+ if (branchResult.reason === 'fetch_failed') {
671
+ res.status(502).json({
672
+ error: 'fetch_failed',
673
+ branch: existingBranch,
674
+ remote: 'origin',
675
+ });
676
+ return;
677
+ }
678
+ res.status(404).json({
679
+ error: 'branch_not_found',
680
+ branch: existingBranch,
681
+ remote: 'origin',
682
+ });
683
+ return;
684
+ }
685
+ // Find existing checkout or create new worktree
686
+ try {
687
+ const result = await findOrCreateWorktreeForBranch(resolved, existingBranch, exec);
688
+ // For main worktree matches, return worktreePath: null to signal "use the main repo"
689
+ // and skip writeMeta (main repo is not a disposable worktree)
690
+ if (!result.isMain) {
691
+ const meta = readMeta(configPath, result.worktreePath);
692
+ writeMeta(configPath, {
693
+ worktreePath: result.worktreePath,
694
+ displayName: meta?.displayName || result.dirName,
695
+ lastActivity: new Date().toISOString(),
696
+ branchName: result.branchName,
697
+ });
698
+ res.json({
699
+ branchName: result.branchName,
700
+ mountainName: meta?.displayName || result.dirName,
701
+ worktreePath: result.worktreePath,
702
+ existing: result.existing,
703
+ });
704
+ }
705
+ else {
706
+ res.json({
707
+ branchName: result.branchName,
708
+ mountainName: result.dirName,
709
+ worktreePath: null,
710
+ existing: true,
711
+ });
712
+ }
713
+ }
714
+ catch (err) {
715
+ const msg = err instanceof Error ? err.message : String(err);
716
+ res.status(500).json({ error: `Failed to create worktree: ${msg}` });
717
+ }
718
+ return;
719
+ }
720
+ else {
721
+ // Create a new branch: <mountain>-<hex-suffix> — with retry if directory is taken
722
+ const baseIndex = settings.nextMountainIndex ?? 0;
723
+ let found = false;
724
+ for (let attempt = 0; attempt < MOUNTAIN_NAMES.length; attempt++) {
725
+ const candidateIndex = (baseIndex + attempt) % MOUNTAIN_NAMES.length;
726
+ const candidateName = MOUNTAIN_NAMES[candidateIndex] ?? 'everest';
727
+ const suffix = crypto.randomBytes(2).toString('hex');
728
+ const candidateBranch = (settings.branchPrefix ?? '') + candidateName + '-' + suffix;
729
+ const candidatePath = path.join(resolved, '.worktrees', candidateName);
730
+ // Check if branch or directory already exists
731
+ const branchExists = await exec('git', ['rev-parse', '--verify', candidateBranch], { cwd: resolved }).then(() => true, () => false);
732
+ const dirExists = fs.existsSync(candidatePath);
733
+ if (!branchExists && !dirExists) {
734
+ mountainName = candidateName;
735
+ branchName = candidateBranch;
736
+ nextMountainIndex = candidateIndex + 1;
737
+ found = true;
738
+ break;
739
+ }
740
+ }
741
+ if (!found) {
742
+ res.status(409).json({
743
+ error: 'All mountain names are taken for this workspace. Delete some worktrees first.',
744
+ });
745
+ return;
746
+ }
747
+ // Detect base branch (keep existing logic)
748
+ let baseBranch = settings.defaultBranch;
749
+ if (!baseBranch) {
750
+ const detected = await detectGitRepo(resolved);
751
+ baseBranch = detected.defaultBranch ?? 'main';
752
+ }
753
+ gitArgs = [
754
+ 'worktree',
755
+ 'add',
756
+ '-b',
757
+ branchName,
758
+ path.join(resolved, '.worktrees', mountainName),
759
+ baseBranch,
760
+ ];
761
+ }
762
+ const worktreePath = path.join(resolved, '.worktrees', mountainName);
763
+ try {
764
+ // Ensure .worktrees/ is in .gitignore
765
+ const gitignorePath = path.join(resolved, '.gitignore');
766
+ try {
767
+ const existing = await fs.promises.readFile(gitignorePath, 'utf8');
768
+ if (!existing.includes('.worktrees/')) {
769
+ await fs.promises.appendFile(gitignorePath, '\n.worktrees/\n');
770
+ }
771
+ }
772
+ catch {
773
+ await fs.promises.writeFile(gitignorePath, '.worktrees/\n');
774
+ }
775
+ await exec('git', gitArgs, { cwd: resolved });
776
+ }
777
+ catch (err) {
778
+ const msg = err instanceof Error ? err.message : String(err);
779
+ res.status(500).json({ error: `Failed to create worktree: ${msg}` });
780
+ return;
781
+ }
782
+ // Increment mountain counter AFTER successful creation (don't skip names on failure)
783
+ if (nextMountainIndex !== undefined) {
784
+ setRepoSettings(configPath, config, resolved, { nextMountainIndex });
785
+ }
786
+ // Write metadata so DELETE /worktrees can find the suffixed branch name
787
+ writeMeta(configPath, {
788
+ worktreePath,
789
+ displayName: mountainName,
790
+ lastActivity: new Date().toISOString(),
791
+ branchName,
792
+ });
793
+ res.json({ branchName, mountainName, worktreePath });
794
+ });
795
+ // GET /workspaces/current-branch — current checked-out branch for a path
796
+ router.get('/current-branch', async (req, res) => {
797
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
798
+ if (!repoPath) {
799
+ res.status(400).json({ error: 'path query parameter is required' });
800
+ return;
801
+ }
802
+ const branch = await getCurrentBranch(path.resolve(repoPath));
803
+ res.json({ branch });
804
+ });
805
+ // GET /workspaces/browse — browse filesystem directories for tree UI
806
+ router.get('/browse', async (req, res) => {
807
+ const rawPath = typeof req.query.path === 'string' ? req.query.path : '~';
808
+ const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : '';
809
+ const showHidden = req.query.showHidden === 'true';
810
+ const resolved = path.resolve(expandTilde(rawPath));
811
+ let stat;
812
+ try {
813
+ stat = await fs.promises.stat(resolved);
814
+ }
815
+ catch (err) {
816
+ const code = err.code;
817
+ if (code === 'EACCES') {
818
+ res.status(403).json({ error: 'Permission denied' });
819
+ }
820
+ else {
821
+ res.status(400).json({ error: `Path does not exist: ${resolved}` });
822
+ }
823
+ return;
824
+ }
825
+ if (!stat.isDirectory()) {
826
+ res.status(400).json({ error: `Not a directory: ${resolved}` });
827
+ return;
828
+ }
829
+ let dirents;
830
+ try {
831
+ dirents = await fs.promises.readdir(resolved, { withFileTypes: true });
832
+ }
833
+ catch {
834
+ res.status(403).json({ error: 'Cannot read directory' });
835
+ return;
836
+ }
837
+ const includeFiles = req.query.includeFiles === 'true';
838
+ // Filter entries: directories always, files only when includeFiles is set
839
+ let filtered = dirents.filter((d) => {
840
+ const isDir = d.isDirectory();
841
+ if (!isDir && !includeFiles)
842
+ return false;
843
+ if (BROWSE_DENYLIST.has(d.name))
844
+ return false;
845
+ if (!showHidden && d.name.startsWith('.'))
846
+ return false;
847
+ if (prefix && !d.name.toLowerCase().startsWith(prefix.toLowerCase()))
848
+ return false;
849
+ return true;
850
+ });
851
+ filtered.sort((a, b) => {
852
+ // Directories first, then files
853
+ const aDir = a.isDirectory() ? 0 : 1;
854
+ const bDir = b.isDirectory() ? 0 : 1;
855
+ if (aDir !== bDir)
856
+ return aDir - bDir;
857
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
858
+ });
859
+ const total = filtered.length;
860
+ const truncated = filtered.length > BROWSE_MAX_ENTRIES;
861
+ if (truncated)
862
+ filtered = filtered.slice(0, BROWSE_MAX_ENTRIES);
863
+ // Enrich each entry (parallelized)
864
+ const entries = await Promise.all(filtered.map(async (d) => {
865
+ const entryPath = path.join(resolved, d.name);
866
+ const isDir = d.isDirectory();
867
+ let isGitRepo = false;
868
+ let hasChildren = false;
869
+ let size;
870
+ if (isDir) {
871
+ try {
872
+ const gitStat = await fs.promises.stat(path.join(entryPath, '.git'));
873
+ isGitRepo = gitStat.isDirectory();
874
+ }
875
+ catch {
876
+ // not a git repo
877
+ }
878
+ try {
879
+ const children = await fs.promises.readdir(entryPath, {
880
+ withFileTypes: true,
881
+ });
882
+ hasChildren = children.some((c) => (c.isDirectory() || includeFiles) &&
883
+ !BROWSE_DENYLIST.has(c.name) &&
884
+ (showHidden || !c.name.startsWith('.')));
885
+ }
886
+ catch {
887
+ // can't read — treat as no children
888
+ }
889
+ }
890
+ else {
891
+ try {
892
+ const fileStat = await fs.promises.stat(entryPath);
893
+ size = fileStat.size;
894
+ }
895
+ catch {
896
+ // best effort
897
+ }
898
+ }
899
+ return {
900
+ name: d.name,
901
+ path: entryPath,
902
+ isGitRepo,
903
+ hasChildren,
904
+ isDirectory: isDir,
905
+ ...(size !== undefined ? { size } : {}),
906
+ };
907
+ }));
908
+ res.json({ resolved, entries, truncated, total });
909
+ });
910
+ // GET /workspaces/autocomplete — path prefix autocomplete
911
+ router.get('/autocomplete', async (req, res) => {
912
+ const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : '';
913
+ if (!prefix) {
914
+ res.json({ suggestions: [] });
915
+ return;
916
+ }
917
+ const expanded = prefix.startsWith('~')
918
+ ? path.join(process.env.HOME ?? '~', prefix.slice(1))
919
+ : prefix;
920
+ let dirToRead;
921
+ let partialName;
922
+ if (expanded.endsWith('/') || expanded.endsWith(path.sep)) {
923
+ // User typed a trailing slash — list immediate children of that dir
924
+ dirToRead = expanded;
925
+ partialName = '';
926
+ }
927
+ else {
928
+ dirToRead = path.dirname(expanded);
929
+ partialName = path.basename(expanded).toLowerCase();
930
+ }
931
+ let suggestions = [];
932
+ try {
933
+ const entries = await fs.promises.readdir(dirToRead, {
934
+ withFileTypes: true,
935
+ });
936
+ suggestions = entries
937
+ .filter((e) => {
938
+ if (!e.isDirectory())
939
+ return false;
940
+ if (e.name.startsWith('.'))
941
+ return false;
942
+ if (!partialName)
943
+ return true;
944
+ return e.name.toLowerCase().startsWith(partialName);
945
+ })
946
+ .map((e) => path.join(dirToRead, e.name))
947
+ .slice(0, 20); // cap results
948
+ }
949
+ catch {
950
+ // Directory doesn't exist or permission denied — return empty
951
+ }
952
+ res.json({ suggestions });
953
+ });
954
+ // POST /workspaces/rename-branch — rename the current branch for a workspace
955
+ router.post('/rename-branch', async (req, res) => {
956
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
957
+ const { newName } = req.body;
958
+ if (!repoPath) {
959
+ res.status(400).json({ error: 'path query parameter required' });
960
+ return;
961
+ }
962
+ if (!newName || typeof newName !== 'string') {
963
+ res.status(400).json({ error: 'newName is required' });
964
+ return;
965
+ }
966
+ const result = await renameBranch(repoPath, newName);
967
+ if (result.success) {
968
+ res.json(result);
969
+ }
970
+ else {
971
+ res.status(400).json({ error: result.error });
972
+ }
973
+ });
974
+ // POST /workspaces/create-branch — create and checkout a new branch for a workspace
975
+ router.post('/create-branch', async (req, res) => {
976
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
977
+ const { branchName } = req.body;
978
+ if (!repoPath) {
979
+ res.status(400).json({ error: 'path query parameter required' });
980
+ return;
981
+ }
982
+ if (!branchName || typeof branchName !== 'string') {
983
+ res.status(400).json({ error: 'branchName is required' });
984
+ return;
985
+ }
986
+ const result = await createBranch(repoPath, branchName);
987
+ if (result.success) {
988
+ res.json(result);
989
+ }
990
+ else {
991
+ res.status(400).json({ error: result.error });
992
+ }
993
+ });
994
+ // POST /workspaces/push-branch — push a branch to origin for a workspace
995
+ router.post('/push-branch', async (req, res) => {
996
+ const repoPath = typeof req.query.path === 'string' ? req.query.path : undefined;
997
+ const { branch, deleteOldBranch } = req.body;
998
+ if (!repoPath) {
999
+ res.status(400).json({ error: 'path query parameter required' });
1000
+ return;
1001
+ }
1002
+ if (!branch || typeof branch !== 'string') {
1003
+ res.status(400).json({ error: 'branch is required' });
1004
+ return;
1005
+ }
1006
+ const result = await pushBranch(repoPath, branch, deleteOldBranch);
1007
+ if (result.success) {
1008
+ res.json(result);
1009
+ }
1010
+ else {
1011
+ res.status(400).json({ error: result.error });
1012
+ }
1013
+ });
1014
+ function validateWorkspaceAccess(repoPath) {
1015
+ const resolved = path.resolve(repoPath);
1016
+ const allowed = getConfig().repos ?? [];
1017
+ return allowed.some((p) => resolved === p || resolved.startsWith(p + path.sep))
1018
+ ? resolved
1019
+ : null;
1020
+ }
1021
+ // GET /workspaces/changed-files — list changed files in a repo
1022
+ router.get('/changed-files', async (req, res) => {
1023
+ if (typeof req.query.path !== 'string') {
1024
+ res.status(400).json({
1025
+ files: [],
1026
+ aggregate: { additions: 0, deletions: 0, fileCount: 0 },
1027
+ error: 'path parameter required',
1028
+ });
1029
+ return;
1030
+ }
1031
+ const base = typeof req.query.base === 'string' ? req.query.base : undefined;
1032
+ const resolvedRepo = validateWorkspaceAccess(req.query.path);
1033
+ if (!resolvedRepo) {
1034
+ res.status(403).json({
1035
+ files: [],
1036
+ aggregate: { additions: 0, deletions: 0, fileCount: 0 },
1037
+ error: 'path not in configured workspaces',
1038
+ });
1039
+ return;
1040
+ }
1041
+ if (base && base.startsWith('-')) {
1042
+ res.status(400).json({
1043
+ files: [],
1044
+ aggregate: { additions: 0, deletions: 0, fileCount: 0 },
1045
+ error: 'invalid base ref',
1046
+ });
1047
+ return;
1048
+ }
1049
+ try {
1050
+ const files = await getChangedFiles(resolvedRepo, base, exec);
1051
+ const aggregate = {
1052
+ additions: files.reduce((sum, f) => sum + f.additions, 0),
1053
+ deletions: files.reduce((sum, f) => sum + f.deletions, 0),
1054
+ fileCount: files.length,
1055
+ };
1056
+ res.json({ files, aggregate });
1057
+ }
1058
+ catch (err) {
1059
+ logger.warn('/changed-files failed for', resolvedRepo, err instanceof Error ? err.message : String(err));
1060
+ res.status(500).json({
1061
+ files: [],
1062
+ aggregate: { additions: 0, deletions: 0, fileCount: 0 },
1063
+ error: 'Failed to get changed files',
1064
+ });
1065
+ }
1066
+ });
1067
+ // GET /workspaces/files-list — list all files in a repo for quick-open picker
1068
+ router.get('/files-list', async (req, res) => {
1069
+ if (typeof req.query.path !== 'string') {
1070
+ res.status(400).json({
1071
+ files: [],
1072
+ truncated: false,
1073
+ total: 0,
1074
+ error: 'path parameter required',
1075
+ });
1076
+ return;
1077
+ }
1078
+ const resolved = validateWorkspaceAccess(req.query.path);
1079
+ if (!resolved) {
1080
+ res.status(403).json({
1081
+ files: [],
1082
+ truncated: false,
1083
+ total: 0,
1084
+ error: 'path not in configured workspaces',
1085
+ });
1086
+ return;
1087
+ }
1088
+ const cached = filesListCache.get(resolved);
1089
+ if (cached && Date.now() - cached.ts < FILES_LIST_TTL) {
1090
+ res.json({
1091
+ files: cached.files,
1092
+ truncated: cached.truncated,
1093
+ total: cached.total,
1094
+ });
1095
+ return;
1096
+ }
1097
+ try {
1098
+ const { stdout } = await exec('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: resolved, maxBuffer: 10 * 1024 * 1024, timeout: 15_000 });
1099
+ const allFiles = stdout.split('\0').filter(Boolean);
1100
+ const truncated = allFiles.length > FILES_LIST_MAX;
1101
+ const files = truncated ? allFiles.slice(0, FILES_LIST_MAX) : allFiles;
1102
+ filesListCache.set(resolved, {
1103
+ files,
1104
+ truncated,
1105
+ total: allFiles.length,
1106
+ ts: Date.now(),
1107
+ });
1108
+ res.json({ files, truncated, total: allFiles.length });
1109
+ }
1110
+ catch (err) {
1111
+ logger.warn('/files-list failed for', resolved, err instanceof Error ? err.message : String(err));
1112
+ res.json({
1113
+ files: [],
1114
+ truncated: false,
1115
+ total: 0,
1116
+ error: 'not a git repository or git not available',
1117
+ });
1118
+ }
1119
+ });
1120
+ // GET /workspaces/file-diff — get diff for a specific file
1121
+ router.get('/file-diff', async (req, res) => {
1122
+ if (typeof req.query.path !== 'string' ||
1123
+ typeof req.query.file !== 'string') {
1124
+ res
1125
+ .status(400)
1126
+ .json({ diff: '', error: 'path and file parameters required' });
1127
+ return;
1128
+ }
1129
+ const filePath = req.query.file;
1130
+ const base = typeof req.query.base === 'string' ? req.query.base : undefined;
1131
+ const resolvedRepo = validateWorkspaceAccess(req.query.path);
1132
+ if (!resolvedRepo) {
1133
+ res
1134
+ .status(403)
1135
+ .json({ diff: '', error: 'path not in configured workspaces' });
1136
+ return;
1137
+ }
1138
+ const expandedFile = expandTilde(filePath);
1139
+ if (expandedFile.includes('..') ||
1140
+ (path.isAbsolute(filePath) && !filePath.startsWith('~'))) {
1141
+ res.status(400).json({ diff: '', error: 'invalid file path' });
1142
+ return;
1143
+ }
1144
+ if (path.isAbsolute(expandedFile)) {
1145
+ try {
1146
+ const stat = await fs.promises.stat(expandedFile);
1147
+ if (!stat.isFile()) {
1148
+ res.status(400).json({ diff: '', error: 'not a regular file' });
1149
+ return;
1150
+ }
1151
+ if (stat.size > 2 * 1024 * 1024) {
1152
+ res.status(413).json({ diff: '', error: 'file too large' });
1153
+ return;
1154
+ }
1155
+ const content = await fs.promises.readFile(expandedFile, 'utf-8');
1156
+ res.json({ diff: content });
1157
+ }
1158
+ catch (err) {
1159
+ const code = err.code;
1160
+ if (code === 'EACCES' || code === 'EPERM') {
1161
+ res.status(403).json({ diff: '', error: 'permission denied' });
1162
+ }
1163
+ else {
1164
+ res.status(404).json({ diff: '', error: 'file not found' });
1165
+ }
1166
+ }
1167
+ return;
1168
+ }
1169
+ if (base && base.startsWith('-')) {
1170
+ res.status(400).json({ diff: '', error: 'invalid base ref' });
1171
+ return;
1172
+ }
1173
+ try {
1174
+ const diff = await getFileDiff(resolvedRepo, expandedFile, base, exec);
1175
+ res.json({ diff });
1176
+ }
1177
+ catch (err) {
1178
+ logger.warn('/file-diff failed for', resolvedRepo, filePath, err instanceof Error ? err.message : String(err));
1179
+ res.status(500).json({ diff: '', error: 'Failed to get file diff' });
1180
+ }
1181
+ });
1182
+ // GET /workspaces/default-branch — detect the default branch for a repo
1183
+ router.get('/default-branch', async (req, res) => {
1184
+ if (typeof req.query.path !== 'string') {
1185
+ res.status(400).json({ branch: '', error: 'path parameter required' });
1186
+ return;
1187
+ }
1188
+ const resolvedRepo = validateWorkspaceAccess(req.query.path);
1189
+ if (!resolvedRepo) {
1190
+ res
1191
+ .status(403)
1192
+ .json({ branch: '', error: 'path not in configured workspaces' });
1193
+ return;
1194
+ }
1195
+ try {
1196
+ const branch = await getDefaultBranch(resolvedRepo, exec);
1197
+ res.json({ branch });
1198
+ }
1199
+ catch (err) {
1200
+ logger.warn('/default-branch failed for', resolvedRepo, err instanceof Error ? err.message : String(err));
1201
+ res
1202
+ .status(500)
1203
+ .json({ branch: 'main', error: 'Failed to detect default branch' });
1204
+ }
1205
+ });
1206
+ return router;
1207
+ }