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,813 @@
1
+ import { test, before, after, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { DEFAULTS, loadConfig, saveConfig, ensureMetaDir, readMeta, writeMeta, deleteMeta, resolveSessionSettings, deleteRepoSettingKeys, } from '../server/config.js';
7
+ let tmpDir;
8
+ before(() => {
9
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-remote-cli-config-test-'));
10
+ });
11
+ afterEach(() => {
12
+ for (const entry of fs.readdirSync(tmpDir, { withFileTypes: true })) {
13
+ const fullPath = path.join(tmpDir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ fs.rmSync(fullPath, { recursive: true });
16
+ }
17
+ else {
18
+ fs.unlinkSync(fullPath);
19
+ }
20
+ }
21
+ });
22
+ after(() => {
23
+ fs.rmdirSync(tmpDir);
24
+ });
25
+ test('loadConfig loads a JSON config file', () => {
26
+ const configPath = path.join(tmpDir, 'config.json');
27
+ const data = { port: 4000, host: '127.0.0.1' };
28
+ fs.writeFileSync(configPath, JSON.stringify(data), 'utf8');
29
+ const config = loadConfig(configPath);
30
+ assert.equal(config.port, 4000);
31
+ assert.equal(config.host, '127.0.0.1');
32
+ });
33
+ test('loadConfig merges with defaults for missing fields', () => {
34
+ const configPath = path.join(tmpDir, 'config.json');
35
+ fs.writeFileSync(configPath, JSON.stringify({ port: 9000 }), 'utf8');
36
+ const config = loadConfig(configPath);
37
+ assert.equal(config.port, 9000);
38
+ assert.equal(config.host, DEFAULTS.host);
39
+ assert.equal(config.cookieTTL, DEFAULTS.cookieTTL);
40
+ assert.deepEqual(config.repos, DEFAULTS.repos);
41
+ assert.equal(config.claudeCommand, DEFAULTS.claudeCommand);
42
+ assert.deepEqual(config.claudeArgs, DEFAULTS.claudeArgs);
43
+ assert.equal(config.defaultAgent, DEFAULTS.defaultAgent);
44
+ });
45
+ test('loadConfig throws if config file not found', () => {
46
+ const configPath = path.join(tmpDir, 'nonexistent.json');
47
+ assert.throws(() => loadConfig(configPath), /Config file not found/);
48
+ });
49
+ test('saveConfig writes JSON with 2-space indent', () => {
50
+ const configPath = path.join(tmpDir, 'output.json');
51
+ const config = { port: 3456, host: '0.0.0.0' };
52
+ saveConfig(configPath, config);
53
+ const raw = fs.readFileSync(configPath, 'utf8');
54
+ assert.equal(raw, JSON.stringify(config, null, 2));
55
+ });
56
+ test('DEFAULTS has expected keys and values', () => {
57
+ assert.equal(DEFAULTS.host, '0.0.0.0');
58
+ assert.equal(DEFAULTS.port, 3456);
59
+ assert.equal(DEFAULTS.cookieTTL, '24h');
60
+ assert.deepEqual(DEFAULTS.repos, []);
61
+ assert.equal(DEFAULTS.claudeCommand, 'claude');
62
+ assert.deepEqual(DEFAULTS.claudeArgs, []);
63
+ assert.equal(DEFAULTS.defaultAgent, 'claude');
64
+ assert.equal(DEFAULTS.defaultContinue, true);
65
+ assert.equal(DEFAULTS.defaultYolo, false);
66
+ assert.equal(DEFAULTS.launchInTmux, false);
67
+ });
68
+ test('loadConfig returns correct defaults for defaultContinue, defaultYolo, and launchInTmux', () => {
69
+ const configPath = path.join(tmpDir, 'config.json');
70
+ fs.writeFileSync(configPath, JSON.stringify({ port: 3456 }), 'utf8');
71
+ const config = loadConfig(configPath);
72
+ assert.equal(config.defaultContinue, true);
73
+ assert.equal(config.defaultYolo, false);
74
+ assert.equal(config.launchInTmux, false);
75
+ });
76
+ test('ensureMetaDir creates worktree-meta directory', () => {
77
+ const configPath = path.join(tmpDir, 'config.json');
78
+ ensureMetaDir(configPath);
79
+ const metaPath = path.join(tmpDir, 'worktree-meta');
80
+ assert.ok(fs.existsSync(metaPath));
81
+ });
82
+ test('writeMeta creates and readMeta reads metadata file', () => {
83
+ const configPath = path.join(tmpDir, 'config.json');
84
+ const meta = {
85
+ worktreePath: '/tmp/test-worktree',
86
+ displayName: 'My Feature',
87
+ lastActivity: '2026-02-22T00:00:00.000Z',
88
+ };
89
+ writeMeta(configPath, meta);
90
+ const read = readMeta(configPath, '/tmp/test-worktree');
91
+ assert.deepEqual(read, meta);
92
+ });
93
+ test('readMeta returns null for non-existent metadata', () => {
94
+ const configPath = path.join(tmpDir, 'config.json');
95
+ const result = readMeta(configPath, '/no/such/worktree');
96
+ assert.equal(result, null);
97
+ });
98
+ test('writeMeta overwrites existing metadata', () => {
99
+ const configPath = path.join(tmpDir, 'config.json');
100
+ writeMeta(configPath, {
101
+ worktreePath: '/tmp/wt',
102
+ displayName: 'Old Name',
103
+ lastActivity: '2026-01-01T00:00:00.000Z',
104
+ });
105
+ writeMeta(configPath, {
106
+ worktreePath: '/tmp/wt',
107
+ displayName: 'New Name',
108
+ lastActivity: '2026-02-22T00:00:00.000Z',
109
+ });
110
+ const read = readMeta(configPath, '/tmp/wt');
111
+ assert.equal(read.displayName, 'New Name');
112
+ assert.equal(read.lastActivity, '2026-02-22T00:00:00.000Z');
113
+ });
114
+ test('deleteMeta removes metadata file', () => {
115
+ const configPath = path.join(tmpDir, 'config.json');
116
+ writeMeta(configPath, {
117
+ worktreePath: '/tmp/del-test',
118
+ displayName: 'To Delete',
119
+ lastActivity: '2026-02-22T00:00:00.000Z',
120
+ });
121
+ assert.ok(readMeta(configPath, '/tmp/del-test'));
122
+ deleteMeta(configPath, '/tmp/del-test');
123
+ assert.equal(readMeta(configPath, '/tmp/del-test'), null);
124
+ });
125
+ test('deleteMeta is a no-op for non-existent metadata', () => {
126
+ const configPath = path.join(tmpDir, 'config.json');
127
+ assert.doesNotThrow(() => deleteMeta(configPath, '/no/such/path'));
128
+ });
129
+ test('resolveSessionSettings returns global defaults when no workspace or overrides', () => {
130
+ const configPath = path.join(tmpDir, 'config.json');
131
+ fs.writeFileSync(configPath, JSON.stringify({
132
+ defaultAgent: 'claude',
133
+ defaultContinue: true,
134
+ defaultYolo: false,
135
+ launchInTmux: false,
136
+ claudeArgs: [],
137
+ }), 'utf8');
138
+ const config = loadConfig(configPath);
139
+ const result = resolveSessionSettings(config, '/some/repo', {});
140
+ assert.equal(result.agent, 'claude');
141
+ assert.equal(result.yolo, false);
142
+ assert.equal(result.continuePolicy, 'always');
143
+ assert.equal(result.useTmux, false);
144
+ assert.deepEqual(result.claudeArgs, []);
145
+ });
146
+ test('resolveSessionSettings applies workspace overrides over globals', () => {
147
+ const configPath = path.join(tmpDir, 'config.json');
148
+ fs.writeFileSync(configPath, JSON.stringify({
149
+ defaultAgent: 'claude',
150
+ defaultYolo: false,
151
+ defaultContinue: true,
152
+ launchInTmux: false,
153
+ claudeArgs: [],
154
+ repoSettings: {
155
+ '/my/repo': { defaultYolo: true, defaultAgent: 'codex' },
156
+ },
157
+ }), 'utf8');
158
+ const config = loadConfig(configPath);
159
+ const result = resolveSessionSettings(config, '/my/repo', {});
160
+ assert.equal(result.agent, 'codex');
161
+ assert.equal(result.yolo, true);
162
+ assert.equal(result.continuePolicy, 'always');
163
+ });
164
+ test('resolveSessionSettings explicit overrides beat workspace settings', () => {
165
+ const configPath = path.join(tmpDir, 'config.json');
166
+ fs.writeFileSync(configPath, JSON.stringify({
167
+ defaultAgent: 'claude',
168
+ defaultYolo: true,
169
+ defaultContinue: true,
170
+ launchInTmux: false,
171
+ claudeArgs: [],
172
+ repoSettings: {
173
+ '/my/repo': { defaultYolo: true },
174
+ },
175
+ }), 'utf8');
176
+ const config = loadConfig(configPath);
177
+ const result = resolveSessionSettings(config, '/my/repo', { yolo: false });
178
+ assert.equal(result.yolo, false);
179
+ });
180
+ test('resolveSessionSettings uses override claudeArgs, not global', () => {
181
+ const configPath = path.join(tmpDir, 'config.json');
182
+ fs.writeFileSync(configPath, JSON.stringify({
183
+ defaultAgent: 'claude',
184
+ defaultYolo: false,
185
+ defaultContinue: true,
186
+ launchInTmux: false,
187
+ claudeArgs: ['--global-arg'],
188
+ }), 'utf8');
189
+ const config = loadConfig(configPath);
190
+ const result = resolveSessionSettings(config, '/some/repo', {
191
+ claudeArgs: ['--custom'],
192
+ });
193
+ assert.deepEqual(result.claudeArgs, ['--custom']);
194
+ });
195
+ test('resolveSessionSettings falls through to globals when no workspace exists', () => {
196
+ const configPath = path.join(tmpDir, 'config.json');
197
+ fs.writeFileSync(configPath, JSON.stringify({
198
+ defaultAgent: 'codex',
199
+ defaultYolo: true,
200
+ defaultContinue: false,
201
+ launchInTmux: true,
202
+ claudeArgs: ['--verbose'],
203
+ }), 'utf8');
204
+ const config = loadConfig(configPath);
205
+ const result = resolveSessionSettings(config, '/nonexistent/repo', {});
206
+ assert.equal(result.agent, 'codex');
207
+ assert.equal(result.yolo, true);
208
+ assert.equal(result.continuePolicy, 'never');
209
+ assert.equal(result.useTmux, true);
210
+ assert.deepEqual(result.claudeArgs, ['--verbose']);
211
+ });
212
+ test('deleteRepoSettingKeys removes specified keys', () => {
213
+ const configPath = path.join(tmpDir, 'config.json');
214
+ const config = {
215
+ ...DEFAULTS,
216
+ repoSettings: {
217
+ '/my/repo': {
218
+ defaultYolo: true,
219
+ defaultAgent: 'codex',
220
+ branchPrefix: 'dy/',
221
+ },
222
+ },
223
+ };
224
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
225
+ deleteRepoSettingKeys(configPath, config, '/my/repo', [
226
+ 'defaultYolo',
227
+ 'defaultAgent',
228
+ ]);
229
+ assert.equal(config.repoSettings['/my/repo'].defaultYolo, undefined);
230
+ assert.equal(config.repoSettings['/my/repo'].defaultAgent, undefined);
231
+ assert.equal(config.repoSettings['/my/repo'].branchPrefix, 'dy/');
232
+ });
233
+ test('deleteRepoSettingKeys removes entire workspace entry when empty', () => {
234
+ const configPath = path.join(tmpDir, 'config.json');
235
+ const config = {
236
+ ...DEFAULTS,
237
+ repoSettings: {
238
+ '/my/repo': { defaultYolo: true },
239
+ },
240
+ };
241
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
242
+ deleteRepoSettingKeys(configPath, config, '/my/repo', ['defaultYolo']);
243
+ assert.equal(config.repoSettings['/my/repo'], undefined);
244
+ });
245
+ test('deleteRepoSettingKeys is no-op for nonexistent workspace', () => {
246
+ const configPath = path.join(tmpDir, 'config.json');
247
+ const config = { ...DEFAULTS };
248
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
249
+ assert.doesNotThrow(() => deleteRepoSettingKeys(configPath, config, '/no/such/repo', ['defaultYolo']));
250
+ });
251
+ test('workspaceGroups with valid paths loads cleanly', () => {
252
+ const configPath = path.join(tmpDir, 'config.json');
253
+ fs.writeFileSync(configPath, JSON.stringify({
254
+ workspaces: ['/a/repo', '/b/repo'],
255
+ workspaceGroups: {
256
+ 'Group A': ['/a/repo'],
257
+ 'Group B': ['/b/repo'],
258
+ },
259
+ }), 'utf8');
260
+ const config = loadConfig(configPath);
261
+ assert.equal(config.workspaceGroups, undefined);
262
+ const workspaces = config.workspaces;
263
+ const groupA = workspaces?.find((w) => w.name === 'Group A');
264
+ const groupB = workspaces?.find((w) => w.name === 'Group B');
265
+ assert.deepEqual(groupA?.repos, ['/a/repo']);
266
+ assert.deepEqual(groupB?.repos, ['/b/repo']);
267
+ });
268
+ test('workspaceGroups with invalid path filters it out', () => {
269
+ const configPath = path.join(tmpDir, 'config.json');
270
+ fs.writeFileSync(configPath, JSON.stringify({
271
+ workspaces: ['/valid/repo'],
272
+ workspaceGroups: {
273
+ 'My Group': ['/valid/repo', '/not/in/workspaces'],
274
+ },
275
+ }), 'utf8');
276
+ const config = loadConfig(configPath);
277
+ assert.equal(config.workspaceGroups, undefined);
278
+ const myGroup = config.workspaces?.find((w) => w.name === 'My Group');
279
+ assert.deepEqual(myGroup?.repos, ['/valid/repo']);
280
+ });
281
+ test('workspaceGroups with duplicate path allows many-to-many', () => {
282
+ const configPath = path.join(tmpDir, 'config.json');
283
+ fs.writeFileSync(configPath, JSON.stringify({
284
+ workspaces: ['/shared/repo'],
285
+ workspaceGroups: {
286
+ First: ['/shared/repo'],
287
+ Second: ['/shared/repo'],
288
+ },
289
+ }), 'utf8');
290
+ const config = loadConfig(configPath);
291
+ assert.equal(config.workspaceGroups, undefined);
292
+ const workspaces = config.workspaces;
293
+ const first = workspaces?.find((w) => w.name === 'First');
294
+ const second = workspaces?.find((w) => w.name === 'Second');
295
+ // Many-to-many: both groups can contain the same repo
296
+ assert.deepEqual(first?.repos, ['/shared/repo']);
297
+ assert.deepEqual(second?.repos, ['/shared/repo']);
298
+ });
299
+ test('workspaceGroups undefined produces no errors', () => {
300
+ const configPath = path.join(tmpDir, 'config.json');
301
+ fs.writeFileSync(configPath, JSON.stringify({
302
+ workspaces: ['/some/repo'],
303
+ }), 'utf8');
304
+ const config = loadConfig(configPath);
305
+ assert.equal(config.workspaceGroups, undefined);
306
+ assert.deepEqual(config.workspaces, []);
307
+ });
308
+ test('workspaceGroups with all-invalid paths removes empty group', () => {
309
+ const configPath = path.join(tmpDir, 'config.json');
310
+ fs.writeFileSync(configPath, JSON.stringify({
311
+ workspaces: ['/valid/repo'],
312
+ workspaceGroups: {
313
+ 'Ghost Group': ['/not/here', '/also/not/here'],
314
+ },
315
+ }), 'utf8');
316
+ const config = loadConfig(configPath);
317
+ assert.equal(config.workspaceGroups, undefined);
318
+ const workspaces = config.workspaces;
319
+ assert.ok(!workspaces?.find((w) => w.name === 'Ghost Group'));
320
+ });
321
+ // ── Config v4 migration ──
322
+ test('migrateToV4: sets configVersion (migrates to latest)', () => {
323
+ const configPath = path.join(tmpDir, 'config.json');
324
+ fs.writeFileSync(configPath, JSON.stringify({ repos: ['/a'] }), 'utf8');
325
+ const config = loadConfig(configPath);
326
+ // v4 migration runs first, then v5 — final version is 5
327
+ assert.ok(config.configVersion != null && config.configVersion >= 4);
328
+ });
329
+ test('migrateToV4: v4 config upgrades to v5 preserving data', () => {
330
+ const configPath = path.join(tmpDir, 'config.json');
331
+ const v4Config = {
332
+ configVersion: 4,
333
+ repos: ['/a', '/b'],
334
+ repoSettings: { '/a': { defaultYolo: true } },
335
+ workspaces: [{ id: 'ws-1', name: 'My App', repos: ['/a', '/b'], order: 0 }],
336
+ };
337
+ fs.writeFileSync(configPath, JSON.stringify(v4Config), 'utf8');
338
+ const config = loadConfig(configPath);
339
+ // v4 data is preserved after v5 migration
340
+ assert.equal(config.configVersion, 5);
341
+ assert.deepEqual(config.repos, ['/a', '/b']);
342
+ assert.ok(config.repoSettings?.['/a']?.defaultYolo);
343
+ assert.equal(config.workspaces?.length, 1);
344
+ assert.equal(config.workspaces?.[0]?.name, 'My App');
345
+ });
346
+ test('migrateToV4: reconciles legacy workspaces string[] into repos', () => {
347
+ const configPath = path.join(tmpDir, 'config.json');
348
+ fs.writeFileSync(configPath, JSON.stringify({
349
+ workspaces: ['/old/repo1', '/old/repo2'],
350
+ }), 'utf8');
351
+ const config = loadConfig(configPath);
352
+ assert.ok(config.configVersion != null && config.configVersion >= 4);
353
+ assert.ok(config.repos.includes('/old/repo1'));
354
+ assert.ok(config.repos.includes('/old/repo2'));
355
+ });
356
+ test('migrateToV4: merges workspaces and repos arrays with dedup', () => {
357
+ const configPath = path.join(tmpDir, 'config.json');
358
+ fs.writeFileSync(configPath, JSON.stringify({
359
+ repos: ['/a', '/b'],
360
+ workspaces: ['/b', '/c'],
361
+ }), 'utf8');
362
+ const config = loadConfig(configPath);
363
+ assert.deepEqual(config.repos, ['/a', '/b', '/c']);
364
+ });
365
+ test('migrateToV4: renames workspaceSettings to repoSettings', () => {
366
+ const configPath = path.join(tmpDir, 'config.json');
367
+ fs.writeFileSync(configPath, JSON.stringify({
368
+ repos: ['/my/repo'],
369
+ workspaceSettings: {
370
+ '/my/repo': { defaultYolo: true, branchPrefix: 'dy/' },
371
+ },
372
+ }), 'utf8');
373
+ const config = loadConfig(configPath);
374
+ assert.ok(config.repoSettings?.['/my/repo']?.defaultYolo);
375
+ assert.equal(config.repoSettings?.['/my/repo']?.branchPrefix, 'dy/');
376
+ assert.equal(config.workspaceSettings, undefined);
377
+ });
378
+ test('migrateToV4: promotes workspaceGroups to Workspace entities', () => {
379
+ const configPath = path.join(tmpDir, 'config.json');
380
+ fs.writeFileSync(configPath, JSON.stringify({
381
+ repos: ['/frontend', '/backend', '/shared'],
382
+ workspaceGroups: {
383
+ 'My App': ['/frontend', '/backend'],
384
+ Infra: ['/shared'],
385
+ },
386
+ }), 'utf8');
387
+ const config = loadConfig(configPath);
388
+ const workspaces = config.workspaces;
389
+ assert.equal(workspaces?.length, 2);
390
+ const myApp = workspaces?.find((w) => w.name === 'My App');
391
+ assert.ok(myApp);
392
+ assert.ok(myApp.id);
393
+ assert.deepEqual(myApp.repos, ['/frontend', '/backend']);
394
+ assert.equal(myApp.order, 0);
395
+ const infra = workspaces?.find((w) => w.name === 'Infra');
396
+ assert.ok(infra);
397
+ assert.deepEqual(infra.repos, ['/shared']);
398
+ assert.equal(infra.order, 1);
399
+ assert.equal(config.workspaceGroups, undefined);
400
+ });
401
+ test('migrateToV4: workspaceGroups promotion validates against repos[]', () => {
402
+ const configPath = path.join(tmpDir, 'config.json');
403
+ fs.writeFileSync(configPath, JSON.stringify({
404
+ repos: ['/valid'],
405
+ workspaceGroups: {
406
+ Mixed: ['/valid', '/not-in-repos'],
407
+ },
408
+ }), 'utf8');
409
+ const config = loadConfig(configPath);
410
+ const mixed = config.workspaces?.find((w) => w.name === 'Mixed');
411
+ assert.ok(mixed);
412
+ assert.deepEqual(mixed.repos, ['/valid']);
413
+ });
414
+ // ── resolveSessionSettings workspace cascade ──
415
+ test('resolveSessionSettings with workspaceId applies workspace settings between global and repo', () => {
416
+ const configPath = path.join(tmpDir, 'config.json');
417
+ const wsId = 'ws-cascade-1';
418
+ fs.writeFileSync(configPath, JSON.stringify({
419
+ configVersion: 4,
420
+ defaultAgent: 'claude',
421
+ defaultYolo: false,
422
+ defaultContinue: true,
423
+ launchInTmux: false,
424
+ claudeArgs: [],
425
+ repos: ['/my/repo'],
426
+ workspaces: [
427
+ {
428
+ id: wsId,
429
+ name: 'My Workspace',
430
+ repos: ['/my/repo'],
431
+ order: 0,
432
+ settings: {
433
+ defaultYolo: true,
434
+ defaultAgent: 'codex',
435
+ launchInTmux: true,
436
+ },
437
+ },
438
+ ],
439
+ }), 'utf8');
440
+ const config = loadConfig(configPath);
441
+ const result = resolveSessionSettings(config, '/my/repo', {}, wsId);
442
+ // Workspace settings should override global
443
+ assert.equal(result.yolo, true);
444
+ assert.equal(result.agent, 'codex');
445
+ assert.equal(result.useTmux, true);
446
+ });
447
+ test('resolveSessionSettings: repo settings override workspace settings', () => {
448
+ const configPath = path.join(tmpDir, 'config.json');
449
+ const wsId = 'ws-cascade-2';
450
+ fs.writeFileSync(configPath, JSON.stringify({
451
+ configVersion: 4,
452
+ defaultAgent: 'claude',
453
+ defaultYolo: false,
454
+ defaultContinue: true,
455
+ launchInTmux: false,
456
+ claudeArgs: [],
457
+ repos: ['/my/repo'],
458
+ workspaces: [
459
+ {
460
+ id: wsId,
461
+ name: 'My Workspace',
462
+ repos: ['/my/repo'],
463
+ order: 0,
464
+ settings: { defaultYolo: true, defaultAgent: 'codex' },
465
+ },
466
+ ],
467
+ repoSettings: {
468
+ '/my/repo': { defaultYolo: false, defaultAgent: 'claude' },
469
+ },
470
+ }), 'utf8');
471
+ const config = loadConfig(configPath);
472
+ const result = resolveSessionSettings(config, '/my/repo', {}, wsId);
473
+ // Repo settings beat workspace settings
474
+ assert.equal(result.yolo, false);
475
+ assert.equal(result.agent, 'claude');
476
+ });
477
+ test('resolveSessionSettings: overrides beat workspace and repo settings', () => {
478
+ const configPath = path.join(tmpDir, 'config.json');
479
+ const wsId = 'ws-cascade-3';
480
+ fs.writeFileSync(configPath, JSON.stringify({
481
+ configVersion: 4,
482
+ defaultAgent: 'claude',
483
+ defaultYolo: false,
484
+ defaultContinue: true,
485
+ launchInTmux: false,
486
+ claudeArgs: [],
487
+ repos: ['/my/repo'],
488
+ workspaces: [
489
+ {
490
+ id: wsId,
491
+ name: 'My Workspace',
492
+ repos: ['/my/repo'],
493
+ order: 0,
494
+ settings: { defaultYolo: true },
495
+ },
496
+ ],
497
+ repoSettings: {
498
+ '/my/repo': { defaultYolo: true },
499
+ },
500
+ }), 'utf8');
501
+ const config = loadConfig(configPath);
502
+ const result = resolveSessionSettings(config, '/my/repo', { yolo: false }, wsId);
503
+ assert.equal(result.yolo, false);
504
+ });
505
+ test('resolveSessionSettings without workspaceId skips workspace cascade', () => {
506
+ const configPath = path.join(tmpDir, 'config.json');
507
+ fs.writeFileSync(configPath, JSON.stringify({
508
+ configVersion: 4,
509
+ defaultAgent: 'claude',
510
+ defaultYolo: false,
511
+ defaultContinue: true,
512
+ launchInTmux: false,
513
+ claudeArgs: [],
514
+ repos: ['/my/repo'],
515
+ workspaces: [
516
+ {
517
+ id: 'ws-x',
518
+ name: 'My Workspace',
519
+ repos: ['/my/repo'],
520
+ order: 0,
521
+ settings: { defaultYolo: true, defaultAgent: 'codex' },
522
+ },
523
+ ],
524
+ }), 'utf8');
525
+ const config = loadConfig(configPath);
526
+ // No workspaceId passed — workspace settings should NOT apply
527
+ const result = resolveSessionSettings(config, '/my/repo', {});
528
+ assert.equal(result.yolo, false);
529
+ assert.equal(result.agent, 'claude');
530
+ });
531
+ test('resolveSessionSettings with unknown workspaceId falls through to global', () => {
532
+ const configPath = path.join(tmpDir, 'config.json');
533
+ fs.writeFileSync(configPath, JSON.stringify({
534
+ configVersion: 4,
535
+ defaultAgent: 'claude',
536
+ defaultYolo: false,
537
+ defaultContinue: true,
538
+ launchInTmux: false,
539
+ claudeArgs: [],
540
+ repos: ['/my/repo'],
541
+ workspaces: [],
542
+ }), 'utf8');
543
+ const config = loadConfig(configPath);
544
+ const result = resolveSessionSettings(config, '/my/repo', {}, 'no-such-workspace');
545
+ assert.equal(result.yolo, false);
546
+ assert.equal(result.agent, 'claude');
547
+ });
548
+ test('migrateToV4: empty config gets migrated to latest version', () => {
549
+ const configPath = path.join(tmpDir, 'config.json');
550
+ fs.writeFileSync(configPath, JSON.stringify({}), 'utf8');
551
+ const config = loadConfig(configPath);
552
+ assert.ok(config.configVersion != null && config.configVersion >= 4);
553
+ });
554
+ test('migrateToV4: persists migrated config to disk', () => {
555
+ const configPath = path.join(tmpDir, 'config.json');
556
+ fs.writeFileSync(configPath, JSON.stringify({
557
+ repos: ['/a'],
558
+ workspaceSettings: { '/a': { defaultYolo: true } },
559
+ workspaceGroups: { G: ['/a'] },
560
+ }), 'utf8');
561
+ loadConfig(configPath);
562
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
563
+ // After v4+v5 migrations, configVersion is at least 4
564
+ assert.ok(raw.configVersion != null && raw.configVersion >= 4);
565
+ assert.ok(raw.repoSettings);
566
+ assert.equal(raw.workspaceSettings, undefined);
567
+ assert.equal(raw.workspaceGroups, undefined);
568
+ });
569
+ test('resolveSessionSettings maps defaultContinue:true to continuePolicy:always', () => {
570
+ const configPath = path.join(tmpDir, 'config.json');
571
+ fs.writeFileSync(configPath, JSON.stringify({ defaultContinue: true }), 'utf8');
572
+ const config = loadConfig(configPath);
573
+ const resolved = resolveSessionSettings(config, '/some/repo', {});
574
+ assert.equal(resolved.continuePolicy, 'always');
575
+ });
576
+ test('resolveSessionSettings maps defaultContinue:false to continuePolicy:never', () => {
577
+ const configPath = path.join(tmpDir, 'config.json');
578
+ fs.writeFileSync(configPath, JSON.stringify({ defaultContinue: false }), 'utf8');
579
+ const config = loadConfig(configPath);
580
+ const resolved = resolveSessionSettings(config, '/some/repo', {});
581
+ assert.equal(resolved.continuePolicy, 'never');
582
+ });
583
+ test('resolveSessionSettings respects explicit continuePolicy override', () => {
584
+ const configPath = path.join(tmpDir, 'config.json');
585
+ fs.writeFileSync(configPath, JSON.stringify({ defaultContinue: true }), 'utf8');
586
+ const config = loadConfig(configPath);
587
+ const resolved = resolveSessionSettings(config, '/some/repo', {
588
+ continuePolicy: 'never',
589
+ });
590
+ assert.equal(resolved.continuePolicy, 'never');
591
+ });
592
+ test('resolveSessionSettings defaults continuePolicy to always when defaultContinue is missing', () => {
593
+ const configPath = path.join(tmpDir, 'config.json');
594
+ fs.writeFileSync(configPath, JSON.stringify({}), 'utf8');
595
+ const config = loadConfig(configPath);
596
+ const resolved = resolveSessionSettings(config, '/some/repo', {});
597
+ // defaultContinue defaults to true via DEFAULTS, so maps to 'always'
598
+ assert.equal(resolved.continuePolicy, 'always');
599
+ });
600
+ test('cascades workspace settings when workspaceId is provided', () => {
601
+ const config = {
602
+ ...DEFAULTS,
603
+ configVersion: 4,
604
+ repos: ['/tmp/test-repo'],
605
+ workspaces: [
606
+ {
607
+ id: 'ws-1',
608
+ name: 'test workspace',
609
+ repos: ['/tmp/test-repo'],
610
+ order: 0,
611
+ settings: {
612
+ defaultYolo: true,
613
+ defaultAgent: 'claude',
614
+ },
615
+ },
616
+ ],
617
+ repoSettings: {},
618
+ };
619
+ const result = resolveSessionSettings(config, '/tmp/test-repo', {}, 'ws-1');
620
+ assert.strictEqual(result.yolo, true, 'workspace settings should cascade yolo');
621
+ assert.strictEqual(result.agent, 'claude');
622
+ });
623
+ test('repo settings override workspace settings', () => {
624
+ const config = {
625
+ ...DEFAULTS,
626
+ configVersion: 4,
627
+ repos: ['/tmp/test-repo'],
628
+ workspaces: [
629
+ {
630
+ id: 'ws-1',
631
+ name: 'test workspace',
632
+ repos: ['/tmp/test-repo'],
633
+ order: 0,
634
+ settings: {
635
+ defaultYolo: true,
636
+ },
637
+ },
638
+ ],
639
+ repoSettings: {
640
+ '/tmp/test-repo': { defaultYolo: false },
641
+ },
642
+ };
643
+ const result = resolveSessionSettings(config, '/tmp/test-repo', {}, 'ws-1');
644
+ assert.strictEqual(result.yolo, false, 'repo settings should override workspace');
645
+ });
646
+ // ── Config v5 migration ──
647
+ test('migrateToV5: sets configVersion to 5', () => {
648
+ const configPath = path.join(tmpDir, 'config.json');
649
+ fs.writeFileSync(configPath, JSON.stringify({ configVersion: 4, repos: ['/a'] }), 'utf8');
650
+ const config = loadConfig(configPath);
651
+ assert.equal(config.configVersion, 5);
652
+ });
653
+ test('migrateToV5: maps defaultAgent to defaultFramework', () => {
654
+ const configPath = path.join(tmpDir, 'config.json');
655
+ fs.writeFileSync(configPath, JSON.stringify({
656
+ configVersion: 4,
657
+ defaultAgent: 'codex',
658
+ }), 'utf8');
659
+ const config = loadConfig(configPath);
660
+ assert.equal(config.defaultFramework, 'codex');
661
+ });
662
+ test('migrateToV5: maps claudeCommand to frameworks.claude.commandOverride when non-default', () => {
663
+ const configPath = path.join(tmpDir, 'config.json');
664
+ fs.writeFileSync(configPath, JSON.stringify({
665
+ configVersion: 4,
666
+ claudeCommand: '/usr/local/bin/claude',
667
+ }), 'utf8');
668
+ const config = loadConfig(configPath);
669
+ assert.equal(config.frameworks?.['claude']?.commandOverride, '/usr/local/bin/claude');
670
+ });
671
+ test('migrateToV5: does NOT set frameworks.claude.commandOverride when claudeCommand is default', () => {
672
+ const configPath = path.join(tmpDir, 'config.json');
673
+ fs.writeFileSync(configPath, JSON.stringify({
674
+ configVersion: 4,
675
+ claudeCommand: 'claude',
676
+ }), 'utf8');
677
+ const config = loadConfig(configPath);
678
+ assert.equal(config.frameworks?.['claude']?.commandOverride, undefined);
679
+ });
680
+ test('migrateToV5: maps claudeArgs to frameworks.claude.extraArgs when non-empty', () => {
681
+ const configPath = path.join(tmpDir, 'config.json');
682
+ fs.writeFileSync(configPath, JSON.stringify({
683
+ configVersion: 4,
684
+ claudeArgs: ['--verbose', '--no-update-check'],
685
+ }), 'utf8');
686
+ const config = loadConfig(configPath);
687
+ assert.deepEqual(config.frameworks?.['claude']?.extraArgs, [
688
+ '--verbose',
689
+ '--no-update-check',
690
+ ]);
691
+ });
692
+ test('migrateToV5: does NOT set frameworks.claude.extraArgs when claudeArgs is empty', () => {
693
+ const configPath = path.join(tmpDir, 'config.json');
694
+ fs.writeFileSync(configPath, JSON.stringify({
695
+ configVersion: 4,
696
+ claudeArgs: [],
697
+ }), 'utf8');
698
+ const config = loadConfig(configPath);
699
+ assert.equal(config.frameworks?.['claude']?.extraArgs, undefined);
700
+ });
701
+ test('migrateToV5: preserves existing v5 config (no double-migration)', () => {
702
+ const configPath = path.join(tmpDir, 'config.json');
703
+ fs.writeFileSync(configPath, JSON.stringify({
704
+ configVersion: 5,
705
+ defaultFramework: 'opencode',
706
+ defaultAgent: 'claude', // should NOT overwrite defaultFramework
707
+ frameworks: { claude: { commandOverride: '/existing/path' } },
708
+ }), 'utf8');
709
+ const config = loadConfig(configPath);
710
+ assert.equal(config.configVersion, 5);
711
+ assert.equal(config.defaultFramework, 'opencode');
712
+ assert.equal(config.frameworks?.['claude']?.commandOverride, '/existing/path');
713
+ });
714
+ test('migrateToV5: migrates repoSettings defaultAgent to defaultFramework', () => {
715
+ const configPath = path.join(tmpDir, 'config.json');
716
+ fs.writeFileSync(configPath, JSON.stringify({
717
+ configVersion: 4,
718
+ repos: ['/my/repo'],
719
+ repoSettings: {
720
+ '/my/repo': { defaultAgent: 'codex', defaultYolo: true },
721
+ },
722
+ }), 'utf8');
723
+ const config = loadConfig(configPath);
724
+ assert.equal(config.repoSettings?.['/my/repo']?.defaultFramework, 'codex');
725
+ // defaultAgent remains for backward-compat
726
+ assert.equal(config.repoSettings?.['/my/repo']?.defaultYolo, true);
727
+ });
728
+ test('migrateToV5: persists migrated config to disk', () => {
729
+ const configPath = path.join(tmpDir, 'config.json');
730
+ fs.writeFileSync(configPath, JSON.stringify({
731
+ configVersion: 4,
732
+ defaultAgent: 'claude',
733
+ claudeCommand: '/custom/claude',
734
+ claudeArgs: ['--arg1'],
735
+ repos: ['/a'],
736
+ repoSettings: { '/a': { defaultAgent: 'codex' } },
737
+ }), 'utf8');
738
+ loadConfig(configPath);
739
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
740
+ assert.equal(raw.configVersion, 5);
741
+ assert.equal(raw.defaultFramework, 'claude');
742
+ assert.equal(raw.frameworks?.claude?.commandOverride, '/custom/claude');
743
+ assert.deepEqual(raw.frameworks?.claude?.extraArgs, ['--arg1']);
744
+ assert.equal(raw.repoSettings?.['/a']?.defaultFramework, 'codex');
745
+ });
746
+ test('loadConfig applies v5 migration after v4 migration', () => {
747
+ const configPath = path.join(tmpDir, 'config.json');
748
+ // Simulate an old v3-ish config with legacy fields that needs both migrations
749
+ fs.writeFileSync(configPath, JSON.stringify({
750
+ repos: ['/a'],
751
+ workspaces: ['/a'], // triggers v4 migration
752
+ defaultAgent: 'codex', // triggers v5 migration
753
+ claudeCommand: '/alt/claude',
754
+ }), 'utf8');
755
+ const config = loadConfig(configPath);
756
+ assert.equal(config.configVersion, 5);
757
+ assert.equal(config.defaultFramework, 'codex');
758
+ assert.equal(config.frameworks?.['claude']?.commandOverride, '/alt/claude');
759
+ });
760
+ test('migrateToV5: migrates workspace-level settings defaultAgent to defaultFramework', () => {
761
+ const configPath = path.join(tmpDir, 'config.json');
762
+ fs.writeFileSync(configPath, JSON.stringify({
763
+ configVersion: 4,
764
+ repos: ['/my/repo'],
765
+ workspaces: [
766
+ {
767
+ id: 'ws1',
768
+ name: 'Test',
769
+ repos: ['/my/repo'],
770
+ order: 0,
771
+ settings: { defaultAgent: 'codex' },
772
+ },
773
+ ],
774
+ }), 'utf8');
775
+ const config = loadConfig(configPath);
776
+ assert.equal(config.workspaces?.[0]?.settings?.defaultFramework, 'codex');
777
+ });
778
+ test('resolveSessionSettings uses defaultFramework over defaultAgent', () => {
779
+ const configPath = path.join(tmpDir, 'config.json');
780
+ fs.writeFileSync(configPath, JSON.stringify({
781
+ configVersion: 5,
782
+ defaultFramework: 'opencode',
783
+ defaultAgent: 'claude', // should be overridden by defaultFramework
784
+ }), 'utf8');
785
+ const config = loadConfig(configPath);
786
+ const result = resolveSessionSettings(config, '/some/repo', {});
787
+ assert.equal(result.agent, 'opencode');
788
+ });
789
+ test('resolveSessionSettings falls back to defaultAgent when defaultFramework absent', () => {
790
+ const configPath = path.join(tmpDir, 'config.json');
791
+ fs.writeFileSync(configPath, JSON.stringify({
792
+ configVersion: 5,
793
+ defaultAgent: 'codex',
794
+ // no defaultFramework
795
+ }), 'utf8');
796
+ const config = loadConfig(configPath);
797
+ const result = resolveSessionSettings(config, '/some/repo', {});
798
+ assert.equal(result.agent, 'codex');
799
+ });
800
+ test('resolveSessionSettings: repoSettings defaultFramework overrides global', () => {
801
+ const configPath = path.join(tmpDir, 'config.json');
802
+ fs.writeFileSync(configPath, JSON.stringify({
803
+ configVersion: 5,
804
+ defaultFramework: 'claude',
805
+ repos: ['/my/repo'],
806
+ repoSettings: {
807
+ '/my/repo': { defaultFramework: 'opencode' },
808
+ },
809
+ }), 'utf8');
810
+ const config = loadConfig(configPath);
811
+ const result = resolveSessionSettings(config, '/my/repo', {});
812
+ assert.equal(result.agent, 'opencode');
813
+ });