diffing 0.10.5 → 0.12.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 (182) hide show
  1. package/README.md +76 -41
  2. package/dist/{cli-agent-BJiATn11.mjs → cli-agent-CWsOHN-j.mjs} +198 -57
  3. package/dist/{cli-gh-_bkDCxhh.mjs → cli-gh-DI69gJGo.mjs} +27 -17
  4. package/dist/cli.mjs +189 -229
  5. package/dist/client/assets/{angular-html-CBcwKYDC.js → angular-html-PLPuQo0k.js} +1 -1
  6. package/dist/client/assets/{angular-ts-DRBYUaJA.js → angular-ts-CE02Ij4b.js} +1 -1
  7. package/dist/client/assets/{apl-BM0X8jeL.js → apl-CsuUKZoi.js} +1 -1
  8. package/dist/client/assets/{arc-DTkmmKl3.js → arc-CgL5kFe-.js} +1 -1
  9. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Cw75ay7q.js → architectureDiagram-3BPJPVTR-DxZQWGnx.js} +1 -1
  10. package/dist/client/assets/{astro-DKTNkoRZ.js → astro-DkFc7sbz.js} +1 -1
  11. package/dist/client/assets/{blade-DaM7a9Zp.js → blade-BkY07kcC.js} +1 -1
  12. package/dist/client/assets/{blockDiagram-GPEHLZMM-DAzINMr_.js → blockDiagram-GPEHLZMM-D5a2KZV2.js} +1 -1
  13. package/dist/client/assets/{c-B3C0Or4o.js → c-T1BrPfJ6.js} +1 -1
  14. package/dist/client/assets/{c4Diagram-AAUBKEIU-C7SsS28g.js → c4Diagram-AAUBKEIU-mpMH4kFC.js} +1 -1
  15. package/dist/client/assets/channel-D3niKhl0.js +1 -0
  16. package/dist/client/assets/{chunk-2J33WTMH-DHOUGWx8.js → chunk-2J33WTMH-vMtgRLFH.js} +1 -1
  17. package/dist/client/assets/{chunk-3OPIFGDE-D8d2scLX.js → chunk-3OPIFGDE-Dx-FqyO2.js} +1 -1
  18. package/dist/client/assets/{chunk-4BX2VUAB-BSC-E5yg.js → chunk-4BX2VUAB-CUrPd8Kv.js} +1 -1
  19. package/dist/client/assets/{chunk-55IACEB6-27pC5pKn.js → chunk-55IACEB6-DHg3Nr8A.js} +1 -1
  20. package/dist/client/assets/{chunk-5ZQYHXKU-BM60x82y.js → chunk-5ZQYHXKU-CpDPvbGs.js} +1 -1
  21. package/dist/client/assets/{chunk-727SXJPM-CHCF96vE.js → chunk-727SXJPM-3VOGMxwk.js} +1 -1
  22. package/dist/client/assets/{chunk-AQP2D5EJ-BoVsvmTa.js → chunk-AQP2D5EJ-4dbfbIS6.js} +1 -1
  23. package/dist/client/assets/{chunk-BSJP7CBP-Iip4uSio.js → chunk-BSJP7CBP-Dfyn0Uwp.js} +1 -1
  24. package/dist/client/assets/{chunk-CSCIHK7Q-DuIWPyay.js → chunk-CSCIHK7Q-VTpSKo0F.js} +1 -1
  25. package/dist/client/assets/{chunk-FMBD7UC4-Du6rtd2k.js → chunk-FMBD7UC4-kXD1MyOL.js} +1 -1
  26. package/dist/client/assets/{chunk-KSCS5N6A-BBq105vI.js → chunk-KSCS5N6A-BrVoWOAb.js} +1 -1
  27. package/dist/client/assets/{chunk-L5ZTLDWV-0DmB5Qp8.js → chunk-L5ZTLDWV-wZEQDPsa.js} +1 -1
  28. package/dist/client/assets/{chunk-LZXEDZCA-CoRsjUfr.js → chunk-LZXEDZCA-D3i0B0eg.js} +2 -2
  29. package/dist/client/assets/{chunk-ND2GUHAM-tX8cKEls.js → chunk-ND2GUHAM-hDVHT0bN.js} +1 -1
  30. package/dist/client/assets/{chunk-NZK2D7GU-BoJGgPic.js → chunk-NZK2D7GU-jQUfhp9h.js} +1 -1
  31. package/dist/client/assets/{chunk-O5CBEL6O-mlwCWs97.js → chunk-O5CBEL6O-CX_vPWhk.js} +1 -1
  32. package/dist/client/assets/chunk-QZHKN3VN-CZLFuGpt.js +1 -0
  33. package/dist/client/assets/chunk-WU5MYG2G-BhcpjkaB.js +1 -0
  34. package/dist/client/assets/{chunk-XPW4576I-B5efww8M.js → chunk-XPW4576I-Co_-vDB4.js} +1 -1
  35. package/dist/client/assets/classDiagram-4FO5ZUOK-DK5KJNIt.js +1 -0
  36. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-y5K9t6Se.js +1 -0
  37. package/dist/client/assets/{cobol-De9QR_Td.js → cobol-8-qBLs-a.js} +1 -1
  38. package/dist/client/assets/{coffee-Ct_GvuHP.js → coffee-Bjz5lkyW.js} +1 -1
  39. package/dist/client/assets/{cose-bilkent-S5V4N54A-ATaJZGF7.js → cose-bilkent-S5V4N54A-BljJ0FSA.js} +1 -1
  40. package/dist/client/assets/{cpp-Dw52w9cj.js → cpp-5BVQCBXT.js} +1 -1
  41. package/dist/client/assets/{crystal-Dx8ADdsd.js → crystal-CCkY93GS.js} +1 -1
  42. package/dist/client/assets/{css-CZ13XvDU.js → css-BTM7Tp2a.js} +1 -1
  43. package/dist/client/assets/{dagre-BM42HDAG-aDltuYut.js → dagre-BM42HDAG-BSynd_2D.js} +1 -1
  44. package/dist/client/assets/{diagram-2AECGRRQ-DGWnHVMJ.js → diagram-2AECGRRQ-K3E2CVdy.js} +1 -1
  45. package/dist/client/assets/{diagram-5GNKFQAL-Dd7eZBKe.js → diagram-5GNKFQAL-BQHIrfKc.js} +1 -1
  46. package/dist/client/assets/{diagram-KO2AKTUF-DWThXREf.js → diagram-KO2AKTUF-Bqs8sNN4.js} +1 -1
  47. package/dist/client/assets/{diagram-LMA3HP47-CXR4Bjtc.js → diagram-LMA3HP47-C9K4bpQe.js} +1 -1
  48. package/dist/client/assets/{diagram-OG6HWLK6-D4Cbf0nW.js → diagram-OG6HWLK6-DK6wIXY2.js} +1 -1
  49. package/dist/client/assets/{dist-CUDhrHNw.js → dist-1nkZYMRy.js} +1 -1
  50. package/dist/client/assets/{edge-CQCjdJKj.js → edge-DqN2FJce.js} +1 -1
  51. package/dist/client/assets/{elixir-bkjwPazA.js → elixir-BiAgRUFF.js} +1 -1
  52. package/dist/client/assets/{elm-CCweX5Cq.js → elm-N1IbY8sz.js} +1 -1
  53. package/dist/client/assets/{erDiagram-TEJ5UH35-BmAyt7SU.js → erDiagram-TEJ5UH35-jsHCiP02.js} +1 -1
  54. package/dist/client/assets/{erb-D-Z-KBLp.js → erb-B-yYlWAZ.js} +1 -1
  55. package/dist/client/assets/{flowDiagram-I6XJVG4X-C6nIFUVc.js → flowDiagram-I6XJVG4X-DOB1dAWG.js} +1 -1
  56. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DMlxThBf.js → ganttDiagram-6RSMTGT7-BYIewcY5.js} +1 -1
  57. package/dist/client/assets/{git-rebase-BdoFdenB.js → git-rebase-CVkPxOAF.js} +1 -1
  58. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-JaIFkAIT.js → gitGraphDiagram-PVQCEYII-VNQzbpv2.js} +1 -1
  59. package/dist/client/assets/{glimmer-js-CZQrBJKq.js → glimmer-js-BmG1xm72.js} +1 -1
  60. package/dist/client/assets/{glimmer-ts-CBYW46D4.js → glimmer-ts-CvTgwZsg.js} +1 -1
  61. package/dist/client/assets/{glsl-wxixKkY_.js → glsl-DwjNNCbW.js} +1 -1
  62. package/dist/client/assets/{graphql-CqZ0Lo5a.js → graphql-BvgkyF8E.js} +1 -1
  63. package/dist/client/assets/{hack-rqWymQRV.js → hack-DH3188x2.js} +1 -1
  64. package/dist/client/assets/{haml-DFw_IoGM.js → haml-B0GTfMhX.js} +1 -1
  65. package/dist/client/assets/{handlebars-3VmTVMYt.js → handlebars-he7hLDd9.js} +1 -1
  66. package/dist/client/assets/{html-C0Li76jB.js → html-DIYgdTBq.js} +1 -1
  67. package/dist/client/assets/{html-derivative-CO-RQW8y.js → html-derivative-2s6MXlci.js} +1 -1
  68. package/dist/client/assets/{http-sPrZOeBY.js → http-COS23XJP.js} +1 -1
  69. package/dist/client/assets/{hurl-DUoPe4Lc.js → hurl-BlZPjeJt.js} +1 -1
  70. package/dist/client/assets/index-BrEVVQOB.js +4021 -0
  71. package/dist/client/assets/index-BsuX6HVI.css +1 -0
  72. package/dist/client/assets/{infoDiagram-5YYISTIA-B6AZ5SCV.js → infoDiagram-5YYISTIA-DQRqk24I.js} +1 -1
  73. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CytMrpo8.js → ishikawaDiagram-YF4QCWOH-DU1rqrLx.js} +1 -1
  74. package/dist/client/assets/{java-Cj0G6QRg.js → java-BAa5w-Qd.js} +1 -1
  75. package/dist/client/assets/{javascript-d5SUuyWD.js → javascript-C9e02ssI.js} +1 -1
  76. package/dist/client/assets/{jinja-BTw9k0vO.js → jinja-Bx37xXwq.js} +1 -1
  77. package/dist/client/assets/{jison-9xSVvR1q.js → jison-m_DEf8Jo.js} +1 -1
  78. package/dist/client/assets/{journeyDiagram-JHISSGLW-Bd4jZUYI.js → journeyDiagram-JHISSGLW-CoHlfevf.js} +1 -1
  79. package/dist/client/assets/{json-DjAt53ka.js → json-DtN5AWRW.js} +1 -1
  80. package/dist/client/assets/{jsx-BX5KmQ7d.js → jsx-BlPpu8bR.js} +1 -1
  81. package/dist/client/assets/{julia-CLLx1dHC.js → julia-BAOmCbHl.js} +1 -1
  82. package/dist/client/assets/{just-C0v80bKe.js → just-C_bvVjwE.js} +1 -1
  83. package/dist/client/assets/{kanban-definition-UN3LZRKU-Ds4pXHJa.js → kanban-definition-UN3LZRKU-BiqYI7_m.js} +1 -1
  84. package/dist/client/assets/{latex-DY-oQfwO.js → latex-81pMfNMt.js} +1 -1
  85. package/dist/client/assets/{line-BbSLfOl6.js → line-DCovs9My.js} +1 -1
  86. package/dist/client/assets/{linear-BMIZJAAR.js → linear-cJF5XHE7.js} +1 -1
  87. package/dist/client/assets/{liquid-DDHDgbZx.js → liquid-sKRTk6dT.js} +1 -1
  88. package/dist/client/assets/{lua-CXymerKS.js → lua-C1uZL4IF.js} +1 -1
  89. package/dist/client/assets/{marko-DoPFsptl.js → marko-BKEzP1CL.js} +1 -1
  90. package/dist/client/assets/{mdc-BxSmTphd.js → mdc-CLkfm94H.js} +1 -1
  91. package/dist/client/assets/{mermaid-parser.core-CcYSvN4q.js → mermaid-parser.core-CTsTbvb2.js} +1 -1
  92. package/dist/client/assets/{mermaid.core-COTNtqgQ.js → mermaid.core-D86HJEwL.js} +3 -3
  93. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BTxraRzn.js → mindmap-definition-RKZ34NQL-CL3heUmL.js} +1 -1
  94. package/dist/client/assets/{nginx-Bma0jNOT.js → nginx-eDGm66pc.js} +1 -1
  95. package/dist/client/assets/{nim-DMBpjygU.js → nim-CoWZPv8-.js} +1 -1
  96. package/dist/client/assets/{perl-CrnrV-AJ.js → perl-MoMqtMVr.js} +1 -1
  97. package/dist/client/assets/{php-CDYxzX6t.js → php-DXXVMAtN.js} +1 -1
  98. package/dist/client/assets/{pieDiagram-4H26LBE5-DglRmzer.js → pieDiagram-4H26LBE5-CWV3TMu4.js} +1 -1
  99. package/dist/client/assets/{pug-DNewy3GU.js → pug-CPWaB94J.js} +1 -1
  100. package/dist/client/assets/{qml-COjcnZ-W.js → qml-lhvpx8vk.js} +1 -1
  101. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BjSmyES7.js → quadrantDiagram-W4KKPZXB-QV25BYos.js} +1 -1
  102. package/dist/client/assets/{r-CrCaMxLS.js → r-DYbUnmmK.js} +1 -1
  103. package/dist/client/assets/{razor-jeuOZIar.js → razor-DcyOLv96.js} +1 -1
  104. package/dist/client/assets/{regexp-DSHVUX08.js → regexp-BdY10jm0.js} +1 -1
  105. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B4f430jS.js → requirementDiagram-4Y6WPE33-BPMsmU6J.js} +1 -1
  106. package/dist/client/assets/{review-export-Cen1WEcX.js → review-export-CMwjJURs.js} +1 -1
  107. package/dist/client/assets/{rst-CIDgytzc.js → rst-6ShFOzgH.js} +1 -1
  108. package/dist/client/assets/{ruby-DtuseoNq.js → ruby-B04DeVbi.js} +1 -1
  109. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-TQPPlxc6.js → sankeyDiagram-5OEKKPKP-DQJeroPp.js} +1 -1
  110. package/dist/client/assets/{sas-C72Theia.js → sas-Dj26ftNa.js} +1 -1
  111. package/dist/client/assets/{scss-ClECtwRk.js → scss-BOHpBkgl.js} +1 -1
  112. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-D1tx07Wg.js → sequenceDiagram-3UESZ5HK-DdV0Y6Sz.js} +1 -1
  113. package/dist/client/assets/{shellscript-BCwBsi2k.js → shellscript-CpHwjRcU.js} +1 -1
  114. package/dist/client/assets/{shellsession-BrOddEl5.js → shellsession-BJAvf4GN.js} +1 -1
  115. package/dist/client/assets/{soy-Dcs0GWFW.js → soy-B67bZJnL.js} +1 -1
  116. package/dist/client/assets/{sql-BAjA7Gv7.js → sql-8MzOqSsK.js} +1 -1
  117. package/dist/client/assets/{src-CYYhkqTK.js → src-AaoRSmFl.js} +1 -1
  118. package/dist/client/assets/{stata-19ngQtzR.js → stata-aCME945h.js} +1 -1
  119. package/dist/client/assets/{stateDiagram-AJRCARHV-BZRCEzb5.js → stateDiagram-AJRCARHV-CMD_Xj2M.js} +1 -1
  120. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-vLl7D4ue.js +1 -0
  121. package/dist/client/assets/{surrealql-Ce2TBRjE.js → surrealql-B_V_HxAi.js} +1 -1
  122. package/dist/client/assets/{svelte-DQ_1eGZz.js → svelte-BcQvTlqz.js} +1 -1
  123. package/dist/client/assets/{templ-Cw2DDxWF.js → templ-DZCQ2blz.js} +1 -1
  124. package/dist/client/assets/{tex-myDSO2eP.js → tex-BfeQ5Wuu.js} +1 -1
  125. package/dist/client/assets/{timeline-definition-PNZ67QCA-CP0OZhBT.js → timeline-definition-PNZ67QCA-CyIS7Zoe.js} +1 -1
  126. package/dist/client/assets/{ts-tags-rh39ZwVs.js → ts-tags-DVWE0E87.js} +1 -1
  127. package/dist/client/assets/{tsx-B0-26jrU.js → tsx-DdJ7Sfqf.js} +1 -1
  128. package/dist/client/assets/{twig-CgasHzPb.js → twig-_gVgaha5.js} +1 -1
  129. package/dist/client/assets/{typescript-Dq3T0c-m.js → typescript-Bj0_LzZV.js} +1 -1
  130. package/dist/client/assets/{vennDiagram-CIIHVFJN-DHaKHj0j.js → vennDiagram-CIIHVFJN-C-J2kWyk.js} +1 -1
  131. package/dist/client/assets/{vue-CzFVh92-.js → vue-Dnt_GBRb.js} +1 -1
  132. package/dist/client/assets/{vue-html-Bv2uFkTH.js → vue-html-ClVA5__5.js} +1 -1
  133. package/dist/client/assets/{vue-vine-DzieBk9Y.js → vue-vine-BCcz4SVP.js} +1 -1
  134. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Dky7jxlZ.js → wardleyDiagram-YWT4CUSO-Dzt4EPh8.js} +1 -1
  135. package/dist/client/assets/{xml-wcdHRHN8.js → xml-BYeUHEti.js} +1 -1
  136. package/dist/client/assets/{xsl-CXrwsLG9.js → xsl-Vsx0SZEV.js} +1 -1
  137. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BQ2hG60h.js → xychartDiagram-2RQKCTM6-BAWZKvwP.js} +1 -1
  138. package/dist/client/assets/{yaml-lO6lK6Xe.js → yaml-C6L1khMK.js} +1 -1
  139. package/dist/client/index.html +2 -2
  140. package/dist/{completions-3UF8uzhx.mjs → completions-DvyV49HY.mjs} +22 -1
  141. package/dist/{doctor-BGYZNIYP.mjs → doctor-Bz9VHuBI.mjs} +7 -6
  142. package/dist/find-tui-binary-DVQADOSF.mjs +101 -0
  143. package/dist/{git-Czo96ymM.mjs → git-BGj31GX-.mjs} +71 -7
  144. package/dist/git-CfDSkUcE.mjs +2 -0
  145. package/dist/{github-DKT0lJXv.mjs → github-DXiEeHzk.mjs} +1 -1
  146. package/dist/{github-CEjtGd41.mjs → github-DpzqNrUh.mjs} +3 -3
  147. package/dist/{github-attachments-Cf17YWp4.mjs → github-attachments-DgOf7BhP.mjs} +2 -2
  148. package/dist/{mcp-Dq7J5QOP.mjs → mcp-zDVzU1vl.mjs} +42 -33
  149. package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
  150. package/dist/native/tui-darwin-x64/diffing-tui +0 -0
  151. package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
  152. package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
  153. package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
  154. package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
  155. package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
  156. package/dist/{review-export-BBDWvftT.mjs → review-export-B63eRAVa.mjs} +1 -1
  157. package/dist/{settings-bwe7yQ8U.mjs → search-3R2E5xxr.mjs} +133 -98
  158. package/dist/search-CQ4nsjBX.mjs +2 -0
  159. package/dist/{server-C9i4V36o.mjs → server-Dw3lkQ5q.mjs} +296 -110
  160. package/dist/server-lock-BNI8FKV1.mjs +427 -0
  161. package/dist/session-conflict-Bqt1M4Mp.mjs +96 -0
  162. package/dist/session-manager-BKSZQqDJ.mjs +159 -0
  163. package/dist/session-url-sr7lXXVc.mjs +100 -0
  164. package/dist/tui-search-bridge-BGgLCwMF.mjs +133 -0
  165. package/dist/{update-check-DUuHmPeZ.mjs → update-check-BUfh4Y6c.mjs} +16 -15
  166. package/package.json +9 -4
  167. package/dist/client/assets/channel-lsVHTuYR.js +0 -1
  168. package/dist/client/assets/chunk-QZHKN3VN-AHUiTM0z.js +0 -1
  169. package/dist/client/assets/chunk-WU5MYG2G-D41j4yxb.js +0 -1
  170. package/dist/client/assets/classDiagram-4FO5ZUOK-D1Eo3kly.js +0 -1
  171. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-bAMsbsfB.js +0 -1
  172. package/dist/client/assets/index-B42c0TyV.css +0 -1
  173. package/dist/client/assets/index-BN_-di_r.js +0 -3959
  174. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Da-mGMA2.js +0 -1
  175. package/dist/find-tui-binary-CRpbYXLC.mjs +0 -52
  176. package/dist/git-DaP_1rVy.mjs +0 -2
  177. package/dist/server-lock-Dd_ss_Il.mjs +0 -153
  178. /package/dist/{apply-suggestion-DNNsC5gx.mjs → apply-suggestion-CHYnw68I.mjs} +0 -0
  179. /package/dist/{comment-format-B5AcGcR-.mjs → comment-format-B85NHZnG.mjs} +0 -0
  180. /package/dist/{diff-fingerprint-C8YO3TTE.mjs → diff-fingerprint-BgR0tu1o.mjs} +0 -0
  181. /package/dist/{handoff-DPoIyeWn.mjs → handoff-LAFtmnHf.mjs} +0 -0
  182. /package/dist/{plan-format-BQyx--mp.mjs → plan-format-BwJh5JSE.mjs} +0 -0
@@ -0,0 +1,427 @@
1
+ import { f as getProjectStorageDir, h as getRepoRoot } from "./git-BGj31GX-.mjs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { getCookie } from "hono/cookie";
7
+ //#region src/lib/server-auth.ts
8
+ /** Header, HttpOnly cookie, or legacy query param carrying the per-session review API token. */
9
+ const SESSION_TOKEN_HEADER = "x-diffing-token";
10
+ const SESSION_TOKEN_QUERY = "token";
11
+ const SESSION_TOKEN_COOKIE = "diffing-token";
12
+ function generateSessionToken() {
13
+ return randomBytes(32).toString("hex");
14
+ }
15
+ function isLoopbackHost(host) {
16
+ const normalized = host.toLowerCase();
17
+ if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") return true;
18
+ const octets = normalized.split(".");
19
+ return octets.length === 4 && octets[0] === "127" && octets.every((part) => {
20
+ if (!/^\d{1,3}$/.test(part)) return false;
21
+ const value = Number(part);
22
+ return value >= 0 && value <= 255;
23
+ });
24
+ }
25
+ function isWildcardBindHost(host) {
26
+ const normalized = host.toLowerCase();
27
+ return normalized === "0.0.0.0" || normalized === "::";
28
+ }
29
+ function requestHostHeader(hostHeader) {
30
+ if (!hostHeader) return null;
31
+ const trimmed = hostHeader.trim();
32
+ if (!trimmed) return null;
33
+ if (trimmed.startsWith("[")) {
34
+ const end = trimmed.indexOf("]");
35
+ return end === -1 ? trimmed.toLowerCase() : trimmed.slice(1, end).toLowerCase();
36
+ }
37
+ return trimmed.split(":")[0].toLowerCase();
38
+ }
39
+ /** Reject non-loopback Host headers when the server binds loopback (DNS rebinding guard). */
40
+ function isAllowedRequestHost(hostHeader, bindHost) {
41
+ if (!isLoopbackHost(bindHost)) return true;
42
+ const host = requestHostHeader(hostHeader);
43
+ if (!host) return true;
44
+ return isLoopbackHost(host);
45
+ }
46
+ function readSessionToken(c) {
47
+ const header = c.req.header(SESSION_TOKEN_HEADER);
48
+ if (header) return header;
49
+ const cookie = getCookie(c, SESSION_TOKEN_COOKIE);
50
+ if (cookie) return cookie;
51
+ return c.req.query("token") || null;
52
+ }
53
+ /** `Set-Cookie` value for the review session token (loopback http — no Secure). */
54
+ function buildSessionTokenSetCookieValue(token) {
55
+ return `${SESSION_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict; HttpOnly`;
56
+ }
57
+ /** Escape a session token for embedding in a double-quoted JS string literal. */
58
+ function escapeTokenForJsString(token) {
59
+ return token.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
60
+ }
61
+ /** Inject `window.__DIFFING_SESSION_TOKEN__` into served HTML so deep links work without `?token=`. */
62
+ function injectSessionTokenIntoHtml(html, authToken) {
63
+ if (!authToken) return html;
64
+ const script = `<script>window.__DIFFING_SESSION_TOKEN__="${escapeTokenForJsString(authToken)}";<\/script>`;
65
+ if (html.includes("</head>")) return html.replace("</head>", `${script}</head>`);
66
+ return `${script}${html}`;
67
+ }
68
+ function createServerAuthMiddleware(config) {
69
+ return async (c, next) => {
70
+ if (!c.req.path.startsWith("/api/")) {
71
+ await next();
72
+ return;
73
+ }
74
+ if (!isAllowedRequestHost(c.req.header("host"), config.bindHost)) return c.json({ error: "request Host header is not allowed for this bind address" }, 403);
75
+ if (!config.insecureNoAuth && config.authToken) {
76
+ if (readSessionToken(c) !== config.authToken) return c.json({ error: "invalid or missing review session token" }, 401);
77
+ }
78
+ await next();
79
+ };
80
+ }
81
+ //#endregion
82
+ //#region src/lib/lock-probe.ts
83
+ /** Map wildcard bind hosts to loopback for local probes. */
84
+ function loopbackProbeHost(host) {
85
+ if (host === "0.0.0.0") return "127.0.0.1";
86
+ return host;
87
+ }
88
+ /**
89
+ * Synchronous loopback probe: GET `/api/review/status` and expect a JSON body
90
+ * with a numeric `round` field. Used to detect stale locks after PID reuse.
91
+ */
92
+ function probeLockServerSync(lock) {
93
+ if (lock.port <= 0) return true;
94
+ const host = loopbackProbeHost(lock.host);
95
+ const headers = {};
96
+ if (lock.capability) headers["X-Diffing-Capability"] = lock.capability;
97
+ if (lock.authToken) headers[SESSION_TOKEN_HEADER] = lock.authToken;
98
+ const script = `
99
+ import http from 'node:http';
100
+ const host = ${JSON.stringify(host)};
101
+ const port = ${lock.port};
102
+ const headers = ${JSON.stringify(headers)};
103
+ const req = http.request({
104
+ hostname: host,
105
+ port,
106
+ path: '/api/review/status',
107
+ method: 'GET',
108
+ headers,
109
+ }, (res) => {
110
+ let data = '';
111
+ res.on('data', (chunk) => { data += chunk; });
112
+ res.on('end', () => { process.stdout.write(data); });
113
+ });
114
+ req.on('error', () => process.exit(2));
115
+ req.setTimeout(400, () => { req.destroy(); process.exit(2); });
116
+ req.end();
117
+ `;
118
+ const result = spawnSync(process.execPath, [
119
+ "--input-type=module",
120
+ "-e",
121
+ script
122
+ ], {
123
+ timeout: 900,
124
+ encoding: "utf8"
125
+ });
126
+ if (result.status !== 0) return false;
127
+ try {
128
+ return typeof JSON.parse(result.stdout.trim()).round === "number";
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ //#endregion
134
+ //#region src/lib/server-lock.ts
135
+ const STARTUP_LEASE_STALE_MS = 3e4;
136
+ /**
137
+ * Produce a stable comparison key for the diff a server displays. Runtime-only
138
+ * web options are excluded so `--port`/`--no-open` do not create false scope
139
+ * mismatches between a user-started session and an MCP request.
140
+ */
141
+ function diffScopeKey(options) {
142
+ const { port: _port, host: _host, noOpen: _noOpen, insecureNoAuth: _insecureNoAuth, reuseSession: _reuseSession, replaceSession: _replaceSession, help: _help, version: _version, outputMode: _outputMode, tui: _tui, gpu: _gpu, noExtDiff: _noExtDiff, ...scope } = options;
143
+ return JSON.stringify(scope);
144
+ }
145
+ function lockPath(repoRoot) {
146
+ return join(getProjectStorageDir(repoRoot), "server.json");
147
+ }
148
+ function sessionsPath(repoRoot) {
149
+ return join(getProjectStorageDir(repoRoot), "sessions");
150
+ }
151
+ /** Stable identity for both current and pre-registry lock records. */
152
+ function serverSessionId(lock) {
153
+ return lock.sessionId ?? lock.ownerId ?? `${lock.mode ?? "web"}-${lock.pid}-${lock.startedAt}`;
154
+ }
155
+ function sameServerSession(left, right) {
156
+ if (left.sessionId && right.sessionId) return left.sessionId === right.sessionId;
157
+ return left.pid === right.pid && left.startedAt === right.startedAt && left.port === right.port;
158
+ }
159
+ function sessionPath(lock) {
160
+ return join(sessionsPath(lock.repoRoot), `${encodeURIComponent(serverSessionId(lock))}.json`);
161
+ }
162
+ function writeJsonAtomically(path, value) {
163
+ const parent = join(path, "..");
164
+ mkdirSync(parent, {
165
+ recursive: true,
166
+ mode: 448
167
+ });
168
+ try {
169
+ chmodSync(parent, 448);
170
+ } catch {}
171
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
172
+ writeFileSync(temporary, JSON.stringify(value, null, 2), {
173
+ encoding: "utf-8",
174
+ mode: 384
175
+ });
176
+ renameSync(temporary, path);
177
+ try {
178
+ chmodSync(path, 384);
179
+ } catch {}
180
+ }
181
+ function normalizedLock(lock) {
182
+ return lock.sessionId ? lock : {
183
+ ...lock,
184
+ sessionId: serverSessionId(lock)
185
+ };
186
+ }
187
+ function writeSessionRecord(lock) {
188
+ const normalized = normalizedLock(lock);
189
+ writeJsonAtomically(sessionPath(normalized), normalized);
190
+ return normalized;
191
+ }
192
+ /**
193
+ * Register a session and make it the active target for legacy clients. Before
194
+ * replacing server.json, adopt a live pre-registry lock so an upgrade never
195
+ * makes an already-running review disappear from the session manager.
196
+ */
197
+ function writeServerLock(lock) {
198
+ const current = readServerLock(lock.repoRoot);
199
+ if (current && isLockAlive(current, lock.repoRoot) && !sameServerSession(current, lock)) writeSessionRecord(current);
200
+ const normalized = writeSessionRecord(lock);
201
+ try {
202
+ writeJsonAtomically(lockPath(lock.repoRoot), normalized);
203
+ } catch (error) {
204
+ rmSync(sessionPath(normalized), { force: true });
205
+ throw error;
206
+ }
207
+ }
208
+ function readServerLock(repoRoot) {
209
+ try {
210
+ const raw = readFileSync(lockPath(repoRoot), "utf-8");
211
+ const lock = JSON.parse(raw);
212
+ if (typeof lock.port !== "number" || typeof lock.pid !== "number") return null;
213
+ return lock;
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ /** Return every live registered session, newest first, and prune stale entries. */
219
+ function listServerLocks(repoRoot) {
220
+ let expectedRepoRoot;
221
+ try {
222
+ expectedRepoRoot = repoRoot ?? getRepoRoot();
223
+ } catch {
224
+ return [];
225
+ }
226
+ const sessions = /* @__PURE__ */ new Map();
227
+ const addIfLive = (candidate, stalePath) => {
228
+ if (candidate.repoRoot !== expectedRepoRoot || !isLockAlive(candidate, expectedRepoRoot)) {
229
+ if (stalePath) rmSync(stalePath, { force: true });
230
+ return;
231
+ }
232
+ const normalized = normalizedLock(candidate);
233
+ sessions.set(serverSessionId(normalized), normalized);
234
+ };
235
+ const active = readServerLock(expectedRepoRoot);
236
+ if (active) addIfLive(active);
237
+ const directory = sessionsPath(expectedRepoRoot);
238
+ try {
239
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
240
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
241
+ const path = join(directory, entry.name);
242
+ try {
243
+ const candidate = JSON.parse(readFileSync(path, "utf-8"));
244
+ if (typeof candidate.port !== "number" || typeof candidate.pid !== "number") {
245
+ rmSync(path, { force: true });
246
+ continue;
247
+ }
248
+ addIfLive(candidate, path);
249
+ } catch {
250
+ rmSync(path, { force: true });
251
+ }
252
+ }
253
+ } catch {}
254
+ return [...sessions.values()].sort((left, right) => right.startedAt - left.startedAt);
255
+ }
256
+ /** Resolve the active session, electing the newest live fallback when needed. */
257
+ function resolveActiveServerLock(repoRoot) {
258
+ let expectedRepoRoot;
259
+ try {
260
+ expectedRepoRoot = repoRoot ?? getRepoRoot();
261
+ } catch {
262
+ return null;
263
+ }
264
+ const active = readServerLock(expectedRepoRoot);
265
+ if (active && isLockAlive(active, expectedRepoRoot)) {
266
+ const normalized = normalizedLock(active);
267
+ if (!existsSync(sessionPath(normalized))) writeSessionRecord(normalized);
268
+ if (!active.sessionId) writeJsonAtomically(lockPath(expectedRepoRoot), normalized);
269
+ return normalized;
270
+ }
271
+ const fallback = listServerLocks(expectedRepoRoot)[0];
272
+ if (!fallback) {
273
+ removeServerLock(expectedRepoRoot);
274
+ return null;
275
+ }
276
+ return activateServerLock(fallback);
277
+ }
278
+ /** Make an existing registered session the default target for CLI/MCP clients. */
279
+ function activateServerLock(lock) {
280
+ const normalized = writeSessionRecord(lock);
281
+ writeJsonAtomically(lockPath(lock.repoRoot), normalized);
282
+ return normalized;
283
+ }
284
+ function isPidAlive(pid) {
285
+ try {
286
+ process.kill(pid, 0);
287
+ return true;
288
+ } catch (error) {
289
+ return (error && typeof error === "object" && "code" in error ? error.code : void 0) === "EPERM";
290
+ }
291
+ }
292
+ /**
293
+ * True when the lock pid is alive, belongs to this repo, and the loopback port
294
+ * still serves a diffing review status endpoint (detects PID reuse).
295
+ */
296
+ function isLockAlive(lock, expectedRepoRoot) {
297
+ if (!isPidAlive(lock.pid)) return false;
298
+ try {
299
+ if (lock.repoRoot !== (expectedRepoRoot ?? getRepoRoot())) return false;
300
+ } catch {}
301
+ return probeLockServerSync(lock);
302
+ }
303
+ function removeServerLock(repoRoot) {
304
+ try {
305
+ rmSync(lockPath(repoRoot), { force: true });
306
+ } catch {}
307
+ }
308
+ /** Remove one registry entry and elect another live session if it was active. */
309
+ function removeServerSession(lock) {
310
+ const normalized = normalizedLock(lock);
311
+ const path = sessionPath(normalized);
312
+ try {
313
+ if (sameServerSession(JSON.parse(readFileSync(path, "utf-8")), normalized)) rmSync(path, { force: true });
314
+ } catch {}
315
+ const active = readServerLock(lock.repoRoot);
316
+ if (!active || !sameServerSession(active, normalized)) return;
317
+ const fallback = listServerLocks(lock.repoRoot).find((candidate) => !sameServerSession(candidate, normalized));
318
+ if (fallback) activateServerLock(fallback);
319
+ else removeServerLock(lock.repoRoot);
320
+ }
321
+ /** Unregister live sessions owned by one exact CLI/MCP process identity. */
322
+ function removeServerLockIfOwned(repoRoot, pid, ownerId) {
323
+ const cleanupLease = acquireServerStartupLease(repoRoot, `cleanup-${ownerId}`);
324
+ if (!cleanupLease) return false;
325
+ try {
326
+ const owned = listServerLocks(repoRoot).filter((lock) => lock.pid === pid && lock.ownerId === ownerId);
327
+ if (owned.length === 0) return false;
328
+ for (const lock of owned) removeServerSession(lock);
329
+ return true;
330
+ } finally {
331
+ cleanupLease.release();
332
+ }
333
+ }
334
+ /**
335
+ * Atomically reserve server startup for one repository. The exclusive-create
336
+ * directory is the cross-process serialization point; callers must re-read
337
+ * server.json after acquiring it because another process may have completed
338
+ * startup between their first lock check and this lease acquisition.
339
+ */
340
+ function acquireServerStartupLease(repoRoot, ownerId, now = Date.now()) {
341
+ const path = join(getProjectStorageDir(repoRoot), "server-startup.lock");
342
+ const recordPath = join(path, "lease.json");
343
+ const ownerMarkerPath = join(path, `owner-${encodeURIComponent(ownerId)}`);
344
+ mkdirSync(join(path, ".."), { recursive: true });
345
+ const tryCreate = () => {
346
+ try {
347
+ mkdirSync(path, {
348
+ recursive: false,
349
+ mode: 448
350
+ });
351
+ } catch (error) {
352
+ if (error?.code === "EEXIST") return false;
353
+ throw error;
354
+ }
355
+ try {
356
+ writeFileSync(recordPath, JSON.stringify({
357
+ ownerId,
358
+ createdAt: now,
359
+ pid: process.pid
360
+ }), "utf-8");
361
+ writeFileSync(ownerMarkerPath, "", "utf-8");
362
+ } catch (error) {
363
+ rmSync(path, {
364
+ recursive: true,
365
+ force: true
366
+ });
367
+ throw error;
368
+ }
369
+ return true;
370
+ };
371
+ if (!tryCreate()) {
372
+ let reclaim = false;
373
+ let acquiredAfterRace = false;
374
+ try {
375
+ const record = JSON.parse(readFileSync(recordPath, "utf-8"));
376
+ if (typeof record.createdAt !== "number" || typeof record.pid !== "number") throw new Error("Malformed startup lease");
377
+ let ownerIsAlive = true;
378
+ try {
379
+ process.kill(record.pid, 0);
380
+ } catch (error) {
381
+ ownerIsAlive = error?.code !== "ESRCH";
382
+ }
383
+ reclaim = now - record.createdAt > STARTUP_LEASE_STALE_MS && !ownerIsAlive;
384
+ if (!reclaim) return null;
385
+ } catch {
386
+ try {
387
+ reclaim = now - statSync(path).mtimeMs > STARTUP_LEASE_STALE_MS;
388
+ } catch (statError) {
389
+ if (statError?.code !== "ENOENT") return null;
390
+ if (!tryCreate()) return null;
391
+ acquiredAfterRace = true;
392
+ }
393
+ if (!reclaim && !acquiredAfterRace) return null;
394
+ }
395
+ if (reclaim && !acquiredAfterRace) {
396
+ const stalePath = `${path}.stale-${process.pid}-${now}`;
397
+ try {
398
+ renameSync(path, stalePath);
399
+ rmSync(stalePath, {
400
+ recursive: true,
401
+ force: true
402
+ });
403
+ } catch (error) {
404
+ if (error?.code === "ENOENT") {
405
+ if (!tryCreate()) return null;
406
+ acquiredAfterRace = true;
407
+ } else return null;
408
+ }
409
+ }
410
+ if (!acquiredAfterRace && !tryCreate()) return null;
411
+ }
412
+ let released = false;
413
+ return {
414
+ ownerId,
415
+ release() {
416
+ if (released) return;
417
+ released = true;
418
+ try {
419
+ unlinkSync(ownerMarkerPath);
420
+ unlinkSync(recordPath);
421
+ rmdirSync(path);
422
+ } catch {}
423
+ }
424
+ };
425
+ }
426
+ //#endregion
427
+ export { isWildcardBindHost as S, buildSessionTokenSetCookieValue as _, listServerLocks as a, injectSessionTokenIntoHtml as b, removeServerLockIfOwned as c, sameServerSession as d, serverSessionId as f, SESSION_TOKEN_QUERY as g, SESSION_TOKEN_HEADER as h, isLockAlive as i, removeServerSession as l, probeLockServerSync as m, activateServerLock as n, readServerLock as o, writeServerLock as p, diffScopeKey as r, removeServerLock as s, acquireServerStartupLease as t, resolveActiveServerLock as u, createServerAuthMiddleware as v, isLoopbackHost as x, generateSessionToken as y };
@@ -0,0 +1,96 @@
1
+ import { a as loadSettings, i as reviewSessionUrl } from "./session-url-sr7lXXVc.mjs";
2
+ import { l as removeServerSession, m as probeLockServerSync } from "./server-lock-BNI8FKV1.mjs";
3
+ //#region src/lib/session-conflict.ts
4
+ function sleepMs(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+ /** Loopback-safe review URL for web / gh-pr locks; null for TUI or invalid ports. */
8
+ function existingSessionUrl(lock) {
9
+ return reviewSessionUrl(lock);
10
+ }
11
+ function defaultClearLock(lock) {
12
+ removeServerSession(lock);
13
+ }
14
+ function pidAlive(pid, kill) {
15
+ try {
16
+ kill(pid, 0);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Gracefully stop the process that owns `lock`, then clear a matching lockfile.
24
+ * SIGTERM → wait → SIGKILL → wait → throw.
25
+ */
26
+ async function stopLockOwner(lock, options = {}) {
27
+ const timeoutMs = options.timeoutMs ?? 5e3;
28
+ const killDeadlineMs = options.killDeadlineMs ?? 2e3;
29
+ const pollMs = options.pollMs ?? 100;
30
+ const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
31
+ const sleep = options.sleep ?? sleepMs;
32
+ const now = options.now ?? Date.now;
33
+ const isAlive = options.isAlive ?? ((candidate) => pidAlive(candidate.pid, kill));
34
+ const clearLock = options.clearLock ?? defaultClearLock;
35
+ if (!await (options.lockMatches ?? ((candidate) => probeLockServerSync(candidate)))(lock)) {
36
+ clearLock(lock);
37
+ return;
38
+ }
39
+ if (!isAlive(lock)) {
40
+ clearLock(lock);
41
+ return;
42
+ }
43
+ try {
44
+ kill(lock.pid, "SIGTERM");
45
+ } catch {}
46
+ const termDeadline = now() + timeoutMs;
47
+ while (now() < termDeadline) {
48
+ if (!isAlive(lock)) {
49
+ clearLock(lock);
50
+ return;
51
+ }
52
+ await sleep(pollMs);
53
+ }
54
+ try {
55
+ kill(lock.pid, "SIGKILL");
56
+ } catch {}
57
+ const killDeadline = now() + killDeadlineMs;
58
+ while (now() < killDeadline) {
59
+ if (!isAlive(lock)) {
60
+ clearLock(lock);
61
+ return;
62
+ }
63
+ await sleep(pollMs);
64
+ }
65
+ throw new Error(`Timed out waiting for diffing pid ${lock.pid} to exit after SIGTERM/SIGKILL. End that process manually and try again.`);
66
+ }
67
+ /** Print and optionally open the existing web/gh-pr session; message-only for TUI. */
68
+ async function openExistingSession(lock, options) {
69
+ if ((lock.mode ?? "web") === "tui") {
70
+ console.log(`A diffing TUI session is already open for this repository (pid ${lock.pid}). Use that terminal, or replace it with --replace-session.`);
71
+ return;
72
+ }
73
+ const url = existingSessionUrl(lock);
74
+ if (!url) {
75
+ console.log(`A diffing review is already running for this repository (pid ${lock.pid}).`);
76
+ return;
77
+ }
78
+ console.log(`Opening existing diffing session at ${url}`);
79
+ if (options.noOpen) return;
80
+ if (options.openUrl) {
81
+ await options.openUrl(url);
82
+ return;
83
+ }
84
+ try {
85
+ const settings = loadSettings();
86
+ const openModule = await import("open");
87
+ let appName;
88
+ if (settings.browser) appName = openModule.apps[settings.browser] || settings.browser;
89
+ const openOptions = appName ? { app: { name: appName } } : {};
90
+ await openModule.default(url, openOptions);
91
+ } catch (err) {
92
+ console.error("Failed to open browser:", err instanceof Error ? err.message : err);
93
+ }
94
+ }
95
+ //#endregion
96
+ export { openExistingSession as n, stopLockOwner as r, existingSessionUrl as t };
@@ -0,0 +1,159 @@
1
+ import { a as listServerLocks, d as sameServerSession, f as serverSessionId, n as activateServerLock, u as resolveActiveServerLock } from "./server-lock-BNI8FKV1.mjs";
2
+ import { n as openExistingSession, r as stopLockOwner, t as existingSessionUrl } from "./session-conflict-Bqt1M4Mp.mjs";
3
+ //#region src/lib/session-manager.ts
4
+ const EXIT_OK = 0;
5
+ const EXIT_NOT_FOUND = 4;
6
+ const EXIT_USAGE = 5;
7
+ function scopeLabel(lock) {
8
+ if (lock.prRef) return `PR ${lock.prRef}`;
9
+ if (lock.diffArgs?.length) return lock.diffArgs.join(" ");
10
+ if (!lock.scope) return "working tree";
11
+ try {
12
+ const scope = JSON.parse(lock.scope);
13
+ const parts = [];
14
+ if (scope.showMode) parts.push("show");
15
+ if (scope.staged) parts.push("staged");
16
+ if (scope.revisions?.length) parts.push(scope.revisions.join(" "));
17
+ if (scope.pathspecs?.length) parts.push(`-- ${scope.pathspecs.join(" ")}`);
18
+ return parts.join(" ") || "working tree";
19
+ } catch {
20
+ return "custom diff";
21
+ }
22
+ }
23
+ function summarize(lock, active) {
24
+ return {
25
+ id: serverSessionId(lock),
26
+ active: active ? sameServerSession(lock, active) : false,
27
+ mode: lock.mode ?? "web",
28
+ pid: lock.pid,
29
+ startedAt: lock.startedAt,
30
+ url: existingSessionUrl(lock),
31
+ scope: scopeLabel(lock)
32
+ };
33
+ }
34
+ function formatAge(startedAt) {
35
+ const seconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
36
+ if (seconds < 60) return `${seconds}s`;
37
+ const minutes = Math.floor(seconds / 60);
38
+ if (minutes < 60) return `${minutes}m`;
39
+ const hours = Math.floor(minutes / 60);
40
+ if (hours < 24) return `${hours}h`;
41
+ return `${Math.floor(hours / 24)}d`;
42
+ }
43
+ function printSessions(sessions, active) {
44
+ if (sessions.length === 0) {
45
+ console.log("No diffing sessions are running for this repository.");
46
+ return;
47
+ }
48
+ console.log("ACTIVE ID MODE PID AGE SCOPE / URL");
49
+ for (const lock of sessions) {
50
+ const summary = summarize(lock, active);
51
+ const activeMark = summary.active ? "*" : "";
52
+ const target = summary.url ? `${summary.scope} ${summary.url}` : summary.scope;
53
+ console.log(`${activeMark.padEnd(7)} ${summary.id.slice(0, 8).padEnd(9)} ${summary.mode.padEnd(6)} ${String(summary.pid).padEnd(8)} ${formatAge(summary.startedAt).padEnd(5)} ${target}`);
54
+ }
55
+ console.log("\nUse `diffing sessions use <id>` to retarget agent commands.");
56
+ }
57
+ function findSession(selector, sessions, active) {
58
+ if (selector === "active") {
59
+ if (active) return active;
60
+ throw new Error("No active diffing session is running.");
61
+ }
62
+ const matches = sessions.filter((lock) => serverSessionId(lock).startsWith(selector));
63
+ if (matches.length === 0) throw new Error(`No live diffing session matches ${JSON.stringify(selector)}.`);
64
+ if (matches.length > 1) throw new Error(`Session prefix ${JSON.stringify(selector)} is ambiguous.`);
65
+ return matches[0];
66
+ }
67
+ function usage() {
68
+ console.error(`Usage:
69
+ diffing sessions [list] [--json]
70
+ diffing sessions use <id>
71
+ diffing sessions open [<id>|active] [--no-open]
72
+ diffing sessions stop|kill <id>|active|all`);
73
+ }
74
+ /** Repository-local task manager for every live web, TUI, and PR session. */
75
+ async function runSessionsCommand(args) {
76
+ if (args.includes("--help") || args.includes("-h")) {
77
+ usage();
78
+ return EXIT_OK;
79
+ }
80
+ const action = args[0] && !args[0].startsWith("-") ? args[0] : "list";
81
+ const rest = action === "list" && args[0] !== "list" ? args : args.slice(1);
82
+ const sessions = listServerLocks();
83
+ const active = resolveActiveServerLock();
84
+ if (action === "list") {
85
+ if (rest.filter((arg) => arg !== "--json").length > 0) {
86
+ usage();
87
+ return EXIT_USAGE;
88
+ }
89
+ if (rest.includes("--json")) process.stdout.write(JSON.stringify(sessions.map((lock) => summarize(lock, active)), null, 2) + "\n");
90
+ else printSessions(sessions, active);
91
+ return EXIT_OK;
92
+ }
93
+ if (action === "use") {
94
+ if (rest.length !== 1) {
95
+ usage();
96
+ return EXIT_USAGE;
97
+ }
98
+ try {
99
+ const selected = findSession(rest[0], sessions, active);
100
+ activateServerLock(selected);
101
+ console.log(`Active diffing session set to ${serverSessionId(selected).slice(0, 8)} (${selected.mode ?? "web"}).`);
102
+ return EXIT_OK;
103
+ } catch (error) {
104
+ console.error(error instanceof Error ? error.message : String(error));
105
+ return EXIT_NOT_FOUND;
106
+ }
107
+ }
108
+ if (action === "open") {
109
+ const noOpen = rest.includes("--no-open");
110
+ const positionals = rest.filter((arg) => arg !== "--no-open");
111
+ if (positionals.length > 1) {
112
+ usage();
113
+ return EXIT_USAGE;
114
+ }
115
+ try {
116
+ const selected = findSession(positionals[0] ?? "active", sessions, active);
117
+ activateServerLock(selected);
118
+ await openExistingSession(selected, { noOpen });
119
+ return EXIT_OK;
120
+ } catch (error) {
121
+ console.error(error instanceof Error ? error.message : String(error));
122
+ return EXIT_NOT_FOUND;
123
+ }
124
+ }
125
+ if (action === "stop" || action === "kill") {
126
+ if (rest.length !== 1) {
127
+ usage();
128
+ return EXIT_USAGE;
129
+ }
130
+ let targets;
131
+ try {
132
+ targets = rest[0] === "all" ? sessions : [findSession(rest[0], sessions, active)];
133
+ } catch (error) {
134
+ console.error(error instanceof Error ? error.message : String(error));
135
+ return EXIT_NOT_FOUND;
136
+ }
137
+ if (targets.length === 0) {
138
+ console.log("No diffing sessions are running for this repository.");
139
+ return EXIT_OK;
140
+ }
141
+ let failed = false;
142
+ for (const target of targets) {
143
+ const id = serverSessionId(target).slice(0, 8);
144
+ try {
145
+ console.error(`Stopping ${id} (${target.mode ?? "web"}, pid ${target.pid})…`);
146
+ await stopLockOwner(target);
147
+ } catch (error) {
148
+ failed = true;
149
+ console.error(error instanceof Error ? error.message : String(error));
150
+ }
151
+ }
152
+ if (!failed) console.log(`Stopped ${targets.length} diffing session${targets.length === 1 ? "" : "s"}.`);
153
+ return failed ? 1 : EXIT_OK;
154
+ }
155
+ usage();
156
+ return EXIT_USAGE;
157
+ }
158
+ //#endregion
159
+ export { runSessionsCommand };