diffing 0.7.0 → 0.9.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 (241) hide show
  1. package/README.md +52 -18
  2. package/dist/{cli-agent-D71pq2ep.mjs → cli-agent-CLrK54k2.mjs} +2 -2
  3. package/dist/{cli-gh-D5OCnjNe.mjs → cli-gh-8YC1NzyM.mjs} +6 -1
  4. package/dist/cli.mjs +3 -3
  5. package/dist/client/assets/{angular-html-Dh86jUvr.js → angular-html-DdAR2u-k.js} +1 -1
  6. package/dist/client/assets/{angular-ts-Bh4wRJtT.js → angular-ts-Dz_vWKSu.js} +1 -1
  7. package/dist/client/assets/{apl-BSXBHduT.js → apl-BJ5QXrIk.js} +1 -1
  8. package/dist/client/assets/{arc-CRQlhBrV.js → arc-D9erSW6x.js} +1 -1
  9. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BXq3UvEv.js → architectureDiagram-3BPJPVTR-CCQWZLXR.js} +1 -1
  10. package/dist/client/assets/{astro-Bh00klUw.js → astro-BQAej5a5.js} +1 -1
  11. package/dist/client/assets/{blade-CisA3zhm.js → blade-DkAmuwDx.js} +1 -1
  12. package/dist/client/assets/{blockDiagram-GPEHLZMM-Bw-PTAJr.js → blockDiagram-GPEHLZMM-vwjxNybI.js} +1 -1
  13. package/dist/client/assets/{c-D-pHVWrf.js → c-DG5p-ChJ.js} +1 -1
  14. package/dist/client/assets/{c4Diagram-AAUBKEIU-C0XyiqC_.js → c4Diagram-AAUBKEIU-DJ_hVlh4.js} +1 -1
  15. package/dist/client/assets/channel-DmZgR6r0.js +1 -0
  16. package/dist/client/assets/{chunk-2J33WTMH-DekjZXNj.js → chunk-2J33WTMH-CiLowxCo.js} +1 -1
  17. package/dist/client/assets/{chunk-3OPIFGDE-BtROpcL8.js → chunk-3OPIFGDE-H3jjMR5Y.js} +1 -1
  18. package/dist/client/assets/{chunk-4BX2VUAB-C92XLTlx.js → chunk-4BX2VUAB-Dk0dk4n5.js} +1 -1
  19. package/dist/client/assets/{chunk-55IACEB6-CceUgh8N.js → chunk-55IACEB6-nbLiUMJS.js} +1 -1
  20. package/dist/client/assets/{chunk-5ZQYHXKU-DSbYPAe7.js → chunk-5ZQYHXKU-DZ8RgHg8.js} +1 -1
  21. package/dist/client/assets/{chunk-727SXJPM-DVzUJa-9.js → chunk-727SXJPM-DDe5dqJV.js} +1 -1
  22. package/dist/client/assets/{chunk-AQP2D5EJ-6n3Z_IFa.js → chunk-AQP2D5EJ-DmpNclOr.js} +1 -1
  23. package/dist/client/assets/{chunk-BSJP7CBP-BgVLnkxr.js → chunk-BSJP7CBP-BKQKvtIj.js} +1 -1
  24. package/dist/client/assets/{chunk-CSCIHK7Q-CkGMXntp.js → chunk-CSCIHK7Q-D27t4cyv.js} +1 -1
  25. package/dist/client/assets/{chunk-FMBD7UC4-BM8lrLBo.js → chunk-FMBD7UC4-Bu3LA663.js} +1 -1
  26. package/dist/client/assets/{chunk-KSCS5N6A-Bm2pjMQg.js → chunk-KSCS5N6A-Ed5HNXRW.js} +1 -1
  27. package/dist/client/assets/{chunk-L5ZTLDWV-VL17ikXM.js → chunk-L5ZTLDWV-DLjJZgf8.js} +1 -1
  28. package/dist/client/assets/{chunk-LZXEDZCA-B629g67M.js → chunk-LZXEDZCA-B-cs89SM.js} +2 -2
  29. package/dist/client/assets/{chunk-ND2GUHAM-CjZGwhKA.js → chunk-ND2GUHAM-D_kPSuUA.js} +1 -1
  30. package/dist/client/assets/{chunk-NZK2D7GU-DGsLjESY.js → chunk-NZK2D7GU-D1u3pjq-.js} +1 -1
  31. package/dist/client/assets/{chunk-O5CBEL6O-EyUrGY3l.js → chunk-O5CBEL6O-Mt0cnPSf.js} +1 -1
  32. package/dist/client/assets/chunk-QZHKN3VN-BrYnBs7h.js +1 -0
  33. package/dist/client/assets/chunk-WU5MYG2G-CdBcsQs_.js +1 -0
  34. package/dist/client/assets/{chunk-XPW4576I-C2TeAQmQ.js → chunk-XPW4576I-Bt8a8x28.js} +1 -1
  35. package/dist/client/assets/classDiagram-4FO5ZUOK-CCpp3t5M.js +1 -0
  36. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B1aV1sYb.js +1 -0
  37. package/dist/client/assets/{cobol-DsRZMP16.js → cobol-Daidj_5Y.js} +1 -1
  38. package/dist/client/assets/{coffee-DP0-MMsE.js → coffee-DUMdB2Cr.js} +1 -1
  39. package/dist/client/assets/{cose-bilkent-S5V4N54A-DMmK0b0W.js → cose-bilkent-S5V4N54A-DVUDLXie.js} +1 -1
  40. package/dist/client/assets/{cpp-B6XYbbze.js → cpp-C2FeX1Mc.js} +1 -1
  41. package/dist/client/assets/{crystal-dBLPFLMp.js → crystal-D4yb9blJ.js} +1 -1
  42. package/dist/client/assets/{css-tgwuSZL0.js → css-B5cr0U9F.js} +1 -1
  43. package/dist/client/assets/{dagre-BM42HDAG-CgNkf0TR.js → dagre-BM42HDAG-DdZWb-Ee.js} +1 -1
  44. package/dist/client/assets/{diagram-2AECGRRQ-ocAqn0LV.js → diagram-2AECGRRQ-C9DOZt92.js} +1 -1
  45. package/dist/client/assets/{diagram-5GNKFQAL-CB1Yo4Gc.js → diagram-5GNKFQAL-Bbc07UKr.js} +1 -1
  46. package/dist/client/assets/{diagram-KO2AKTUF-B2vpMBRT.js → diagram-KO2AKTUF-C-lUzY3m.js} +1 -1
  47. package/dist/client/assets/{diagram-LMA3HP47-DuxZOaQS.js → diagram-LMA3HP47-D-OjEFhp.js} +1 -1
  48. package/dist/client/assets/{diagram-OG6HWLK6-CkShDmcv.js → diagram-OG6HWLK6-BjtWGvrf.js} +1 -1
  49. package/dist/client/assets/{dist-BgZIvk5w.js → dist-BXZygPdX.js} +1 -1
  50. package/dist/client/assets/{edge-Bokubn8E.js → edge-RCQWcWNp.js} +1 -1
  51. package/dist/client/assets/{elixir-BEzcf3Xg.js → elixir-DwKyVg2X.js} +1 -1
  52. package/dist/client/assets/{elm-vJyLTAER.js → elm-DjR33IVs.js} +1 -1
  53. package/dist/client/assets/{erDiagram-TEJ5UH35-BFV1NQAH.js → erDiagram-TEJ5UH35-C5T4ywLG.js} +1 -1
  54. package/dist/client/assets/{erb-DDkAmjBS.js → erb-BnhUH1Hk.js} +1 -1
  55. package/dist/client/assets/{flowDiagram-I6XJVG4X-DU8xbFdO.js → flowDiagram-I6XJVG4X-BJmjm7Kx.js} +1 -1
  56. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DCb7Qh0c.js → ganttDiagram-6RSMTGT7-DREPnzu5.js} +1 -1
  57. package/dist/client/assets/{git-rebase-DWcifAdK.js → git-rebase-DzXoSDXA.js} +1 -1
  58. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-C_Tw-piE.js → gitGraphDiagram-PVQCEYII-DIR4G_-_.js} +1 -1
  59. package/dist/client/assets/{glimmer-js--HDru5ys.js → glimmer-js-HKnZpCn_.js} +1 -1
  60. package/dist/client/assets/{glimmer-ts-DAdXisHh.js → glimmer-ts-DedV7rWs.js} +1 -1
  61. package/dist/client/assets/{glsl-CmIRlJht.js → glsl-Bz-0Alp2.js} +1 -1
  62. package/dist/client/assets/{graphql-tx4-7I3U.js → graphql-DSKkDYCD.js} +1 -1
  63. package/dist/client/assets/{hack-yA0XjhF7.js → hack-CnaHZm8k.js} +1 -1
  64. package/dist/client/assets/{haml-BR5MUE1b.js → haml-huGFYKlj.js} +1 -1
  65. package/dist/client/assets/{handlebars-BPOF7iOf.js → handlebars-CKmgzfWY.js} +1 -1
  66. package/dist/client/assets/{html-BbteRk96.js → html-B9Ih_dUU.js} +1 -1
  67. package/dist/client/assets/{html-derivative-59NNrcMm.js → html-derivative-YabD-M5d.js} +1 -1
  68. package/dist/client/assets/{http-D3odGQI4.js → http-Bdu12JYX.js} +1 -1
  69. package/dist/client/assets/{hurl-BLTj8fPj.js → hurl-twZzDTNs.js} +1 -1
  70. package/dist/client/assets/{index-DHXpO7eS.js → index-BktdZRWB.js} +122 -118
  71. package/dist/client/assets/{index-BZcKS8Sb.css → index-YlwaPTSG.css} +1 -1
  72. package/dist/client/assets/{infoDiagram-5YYISTIA-CI6hJyRl.js → infoDiagram-5YYISTIA-D2ty2R-i.js} +1 -1
  73. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-gMS1P1LQ.js → ishikawaDiagram-YF4QCWOH-Ccnc9xlE.js} +1 -1
  74. package/dist/client/assets/{java-CsViuzs_.js → java-_5trO-p9.js} +1 -1
  75. package/dist/client/assets/{javascript-DQgsl2Dh.js → javascript-BOQn0ikE.js} +1 -1
  76. package/dist/client/assets/{jinja-DQNp2NUA.js → jinja-BgY-0IBz.js} +1 -1
  77. package/dist/client/assets/{jison-CDDOZQdc.js → jison-BLNTmT1_.js} +1 -1
  78. package/dist/client/assets/{journeyDiagram-JHISSGLW-BTQ_Ws_h.js → journeyDiagram-JHISSGLW-W7eWXlw0.js} +1 -1
  79. package/dist/client/assets/{json-DP4TqCuE.js → json-BLbH8rZf.js} +1 -1
  80. package/dist/client/assets/{jsx-Df5hn0Tf.js → jsx-CxteptsJ.js} +1 -1
  81. package/dist/client/assets/{julia--u33yGLi.js → julia-B__Ncp_z.js} +1 -1
  82. package/dist/client/assets/{just-BCpMmSOD.js → just-DBsHUGW1.js} +1 -1
  83. package/dist/client/assets/{kanban-definition-UN3LZRKU-CvqGbLR_.js → kanban-definition-UN3LZRKU-Z_AFL-rd.js} +1 -1
  84. package/dist/client/assets/{latex-D8D4jZGw.js → latex-DFF_0wxo.js} +1 -1
  85. package/dist/client/assets/{line-DBgaS_RW.js → line-aAv8TyOs.js} +1 -1
  86. package/dist/client/assets/{linear-RQVzKNVU.js → linear-BOvEZKH8.js} +1 -1
  87. package/dist/client/assets/{liquid-CkH85f12.js → liquid-kDWCO6d7.js} +1 -1
  88. package/dist/client/assets/{lua-iKoV7WE3.js → lua-CYTFayq-.js} +1 -1
  89. package/dist/client/assets/{marko-N8ScQSqT.js → marko-Dpmz3Pdx.js} +1 -1
  90. package/dist/client/assets/{mdc-BvHTOxsp.js → mdc-BsxHyM1i.js} +1 -1
  91. package/dist/client/assets/{mermaid-parser.core-CwDRwMR7.js → mermaid-parser.core-D8z_CWL1.js} +1 -1
  92. package/dist/client/assets/{mermaid.core-D2LDJgXO.js → mermaid.core-DNKchnly.js} +3 -3
  93. package/dist/client/assets/{mindmap-definition-RKZ34NQL-FRnPh642.js → mindmap-definition-RKZ34NQL-xQKMV4Kw.js} +1 -1
  94. package/dist/client/assets/{nginx-DGgvASuI.js → nginx-CcdTDme-.js} +1 -1
  95. package/dist/client/assets/{nim-D-v9hzvR.js → nim-B5wWDskL.js} +1 -1
  96. package/dist/client/assets/{perl-CG8leJ8K.js → perl-Ce2_iKLz.js} +1 -1
  97. package/dist/client/assets/{php-DvkeSP10.js → php-DuLCFERd.js} +1 -1
  98. package/dist/client/assets/{pieDiagram-4H26LBE5-CepOg38Y.js → pieDiagram-4H26LBE5-CBSCJnUr.js} +1 -1
  99. package/dist/client/assets/{pug-B4T7-xaL.js → pug-BfQJoQj1.js} +1 -1
  100. package/dist/client/assets/{qml-BE6D5Bxn.js → qml-DYeovlVl.js} +1 -1
  101. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DOFAsP60.js → quadrantDiagram-W4KKPZXB-uEcgb0Rm.js} +1 -1
  102. package/dist/client/assets/{r-BKZg9Mcq.js → r-DgHSP56L.js} +1 -1
  103. package/dist/client/assets/{razor-D-02oaxL.js → razor-B00x7W-i.js} +1 -1
  104. package/dist/client/assets/{regexp-BDFvAR1k.js → regexp-CmWVupmd.js} +1 -1
  105. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BBVNH-2W.js → requirementDiagram-4Y6WPE33-Q4yY_ALe.js} +1 -1
  106. package/dist/client/assets/{review-export-D5iIl46_.js → review-export-pcqDKNqx.js} +1 -1
  107. package/dist/client/assets/{rst-BcT26OSj.js → rst-B3VARbTX.js} +1 -1
  108. package/dist/client/assets/{ruby-Cq_YQvFT.js → ruby-CaxKSSbT.js} +1 -1
  109. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BQfCTbBX.js → sankeyDiagram-5OEKKPKP-B-GLzFGa.js} +1 -1
  110. package/dist/client/assets/{sas-DN9oTxKY.js → sas-Ue0rHUIk.js} +1 -1
  111. package/dist/client/assets/{scss-BK_sp3z2.js → scss-BJJ7_L6c.js} +1 -1
  112. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CKAW2eyx.js → sequenceDiagram-3UESZ5HK-DW0em0D_.js} +1 -1
  113. package/dist/client/assets/{shellscript-CvNpOu0J.js → shellscript-C6LkZs1u.js} +1 -1
  114. package/dist/client/assets/{shellsession-8-yj2cJj.js → shellsession-CkZu1e84.js} +1 -1
  115. package/dist/client/assets/{soy-C6Q9atPF.js → soy-CDaZV57J.js} +1 -1
  116. package/dist/client/assets/{sql-CBL3BuHb.js → sql-CLfMGH4x.js} +1 -1
  117. package/dist/client/assets/{src-D-VUWIK5.js → src-C27zxuNO.js} +1 -1
  118. package/dist/client/assets/{stata-B2Q7Vqzu.js → stata-B0kNMHc9.js} +1 -1
  119. package/dist/client/assets/{stateDiagram-AJRCARHV-BGJiv0qO.js → stateDiagram-AJRCARHV-CrO_D3m4.js} +1 -1
  120. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C9KDGD7R.js +1 -0
  121. package/dist/client/assets/{surrealql-C-A2N8vk.js → surrealql-D9os_yZO.js} +1 -1
  122. package/dist/client/assets/{svelte-VAut_-uy.js → svelte-2SxVToeO.js} +1 -1
  123. package/dist/client/assets/{templ-mUetM--K.js → templ-DzmZB67T.js} +1 -1
  124. package/dist/client/assets/{tex-BT53mWGp.js → tex-BMPk3nHk.js} +1 -1
  125. package/dist/client/assets/{timeline-definition-PNZ67QCA-lVwMt1rk.js → timeline-definition-PNZ67QCA-C9FBevHv.js} +1 -1
  126. package/dist/client/assets/{ts-tags-VkIOkgLN.js → ts-tags-CG14Bplc.js} +1 -1
  127. package/dist/client/assets/{tsx-D_aaGHTf.js → tsx-D6NK461A.js} +1 -1
  128. package/dist/client/assets/{twig-CufwqKkk.js → twig-Agh0j5sD.js} +1 -1
  129. package/dist/client/assets/{typescript-osFD2CEV.js → typescript-_AkeziZ2.js} +1 -1
  130. package/dist/client/assets/{vennDiagram-CIIHVFJN-_K9NtpA5.js → vennDiagram-CIIHVFJN-2H9HfhhR.js} +1 -1
  131. package/dist/client/assets/{vue-Bub1rXmL.js → vue-C1z3QGG9.js} +1 -1
  132. package/dist/client/assets/{vue-html-CN3x2pYg.js → vue-html-C1RMzLZk.js} +1 -1
  133. package/dist/client/assets/{vue-vine-C0csYkGm.js → vue-vine-oOixA6ge.js} +1 -1
  134. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-B2jTMfra.js → wardleyDiagram-YWT4CUSO-BJxrQhTf.js} +1 -1
  135. package/dist/client/assets/{xml-my6OjeGb.js → xml-okuDZ7vq.js} +1 -1
  136. package/dist/client/assets/{xsl-Bx0YVn26.js → xsl-S_n3XrKb.js} +1 -1
  137. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DfZ4YYXU.js → xychartDiagram-2RQKCTM6-BGdee7KG.js} +1 -1
  138. package/dist/client/assets/{yaml-DanwOypk.js → yaml-BoX-kqX1.js} +1 -1
  139. package/dist/client/index.html +2 -2
  140. package/dist/{doctor-q6oA64hm.mjs → doctor-CmP0u7-d.mjs} +1 -1
  141. package/dist/github-BjZZcSKV.mjs +2 -0
  142. package/dist/{github-HfIlIS-J.mjs → github-oCZx5aim.mjs} +270 -29
  143. package/dist/{mcp-eor39qmD.mjs → mcp-CqxXILYL.mjs} +1 -1
  144. package/dist/{server-C105rwQw.mjs → server-CczuXSWo.mjs} +164 -11
  145. package/package.json +3 -3
  146. package/dist/cli-agent-B4hU6Xay.mjs +0 -499
  147. package/dist/cli-agent-B_PHd635.mjs +0 -499
  148. package/dist/cli-agent-BatQgghr.mjs +0 -499
  149. package/dist/cli-agent-BkkRbKqq.mjs +0 -513
  150. package/dist/cli-agent-BlMtx1Tx.mjs +0 -736
  151. package/dist/cli-agent-BwZE05CQ.mjs +0 -499
  152. package/dist/cli-agent-C4Fb90GL.mjs +0 -728
  153. package/dist/cli-agent-CKE1Cvor.mjs +0 -513
  154. package/dist/cli-agent-CLvCYpmH.mjs +0 -499
  155. package/dist/cli-agent-CX8Fa1Ph.mjs +0 -499
  156. package/dist/cli-agent-ChALuNbw.mjs +0 -841
  157. package/dist/cli-agent-KxNAFIPf.mjs +0 -450
  158. package/dist/cli-agent-SILzesQP.mjs +0 -513
  159. package/dist/cli-agent-USzLoAit.mjs +0 -499
  160. package/dist/cli-agent-qBT8clNM.mjs +0 -841
  161. package/dist/cli-gh-6qiblgj7.mjs +0 -150
  162. package/dist/cli-gh-BV5az425.mjs +0 -147
  163. package/dist/cli-gh-Bo6fOHQR.mjs +0 -147
  164. package/dist/cli-gh-CDGSpndZ.mjs +0 -147
  165. package/dist/cli-gh-D1LoWJl-.mjs +0 -147
  166. package/dist/cli-gh-D1sIZ-jX.mjs +0 -147
  167. package/dist/cli-gh-DROAJlHi.mjs +0 -147
  168. package/dist/cli-gh-DgqscIkg.mjs +0 -147
  169. package/dist/cli-gh-DiT8BWwu.mjs +0 -147
  170. package/dist/cli-gh-KjPnyST6.mjs +0 -150
  171. package/dist/cli-gh-abs-IJrz.mjs +0 -147
  172. package/dist/client/assets/channel-CDxgOu-3.js +0 -1
  173. package/dist/client/assets/chunk-QZHKN3VN-DJCeMpP8.js +0 -1
  174. package/dist/client/assets/chunk-WU5MYG2G-BbuOkMaH.js +0 -1
  175. package/dist/client/assets/classDiagram-4FO5ZUOK-D5oJbiED.js +0 -1
  176. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-5MJt5ZxQ.js +0 -1
  177. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdUgMghp.js +0 -1
  178. package/dist/comment-format-CKTt0fzi.mjs +0 -113
  179. package/dist/comment-format-DG_PAehC.mjs +0 -117
  180. package/dist/github-C9GLNak7.mjs +0 -2
  181. package/dist/mcp-7eXATtez.mjs +0 -1334
  182. package/dist/mcp-B6mCC7D3.mjs +0 -1047
  183. package/dist/mcp-BBk8AIO4.mjs +0 -1334
  184. package/dist/mcp-BJxrW9dS.mjs +0 -1334
  185. package/dist/mcp-BMTZLQuB.mjs +0 -758
  186. package/dist/mcp-BUF2nEL7.mjs +0 -1036
  187. package/dist/mcp-BWeWR4yd.mjs +0 -255
  188. package/dist/mcp-BgZO-iXI.mjs +0 -1047
  189. package/dist/mcp-BhT5GgYF.mjs +0 -1334
  190. package/dist/mcp-Bpx76OGU.mjs +0 -945
  191. package/dist/mcp-BsGdNA88.mjs +0 -1334
  192. package/dist/mcp-BxBm3d5l.mjs +0 -1047
  193. package/dist/mcp-C1T7LI0H.mjs +0 -1036
  194. package/dist/mcp-C4Bjmf9m.mjs +0 -1047
  195. package/dist/mcp-CFxcdEcJ.mjs +0 -733
  196. package/dist/mcp-Ca9QAlJ0.mjs +0 -1334
  197. package/dist/mcp-CphIUJUg.mjs +0 -1047
  198. package/dist/mcp-CtKRXQRC.mjs +0 -1047
  199. package/dist/mcp-CwjThGi2.mjs +0 -913
  200. package/dist/mcp-D4OsB9lt.mjs +0 -1047
  201. package/dist/mcp-DJ2batvt.mjs +0 -913
  202. package/dist/mcp-DNZQvKKF.mjs +0 -298
  203. package/dist/mcp-DO4zfNZX.mjs +0 -298
  204. package/dist/mcp-D_LGvBTw.mjs +0 -1047
  205. package/dist/mcp-Ds_lPX_o.mjs +0 -913
  206. package/dist/plan-format-B6nbAw_c.mjs +0 -132
  207. package/dist/plan-format-Bi46QS0m.mjs +0 -132
  208. package/dist/plan-format-DVnF8xrq.mjs +0 -126
  209. package/dist/plan-format-f62JWFI-.mjs +0 -117
  210. package/dist/plan-format-mSDtsPcC.mjs +0 -132
  211. package/dist/review-export-BoV0884M.mjs +0 -66
  212. package/dist/review-export-CKTI5Od_.mjs +0 -66
  213. package/dist/server-95UE5bgh.mjs +0 -3367
  214. package/dist/server-AisDKkpW.mjs +0 -3227
  215. package/dist/server-B0cS95Lf.mjs +0 -3488
  216. package/dist/server-BUsRhL1B.mjs +0 -3283
  217. package/dist/server-BZBFquG8.mjs +0 -3875
  218. package/dist/server-Bc2m9R5-.mjs +0 -3263
  219. package/dist/server-Bj-6KlK5.mjs +0 -3367
  220. package/dist/server-Bmt57wV5.mjs +0 -3875
  221. package/dist/server-Bpf84Il0.mjs +0 -3367
  222. package/dist/server-C15WRRnS.mjs +0 -3740
  223. package/dist/server-CYSpLbdA.mjs +0 -3360
  224. package/dist/server-ChT2DCaR.mjs +0 -3701
  225. package/dist/server-D0Ni8PKP.mjs +0 -3252
  226. package/dist/server-D2xEbakj.mjs +0 -3874
  227. package/dist/server-DJxysGxV.mjs +0 -3701
  228. package/dist/server-Dov7jbV3.mjs +0 -3367
  229. package/dist/server-TOElOuNP.mjs +0 -3258
  230. package/dist/server-eQ2E4Axo.mjs +0 -3367
  231. package/dist/server-lock-C_wPtEsH.mjs +0 -909
  232. package/dist/server-lock-Cbu3qHdu.mjs +0 -924
  233. package/dist/server-lock-D32inEn9.mjs +0 -924
  234. package/dist/server-lock-D73NKKbn.mjs +0 -842
  235. package/dist/server-lock-DaSx7DjW.mjs +0 -1049
  236. package/dist/server-lock-Ddcat0HF.mjs +0 -851
  237. package/dist/server-lock-Dj9fG9md.mjs +0 -838
  238. package/dist/server-lock-Dq_1CJaF.mjs +0 -1047
  239. package/dist/server-lock-Ujbcgq28.mjs +0 -954
  240. package/dist/update-check-CXeSZANo.mjs +0 -129
  241. package/dist/update-check-DAPFCqpF.mjs +0 -129
@@ -1,3283 +0,0 @@
1
- import { C as isSafePath, S as revertHunk, _ as getUntrackedFilePathsAsync, a as getCustomGitDiffAsync, b as listRepoFiles, c as getGitDiffAsync, d as getProjectStorageDir, f as getRepoName, g as getTabSizeForFiles, h as getShowDiff, i as getCommitSeriesSummary, l as getHunkHistory, o as getFileContent, p as getRepoRoot, r as getBranchName, u as getMergeStatus, v as gitAddFile, w as toSafeRelativePath, y as isGitRepo } from "./git-Czo96ymM.mjs";
2
- import { a as searchContent, c as trackSelection, i as searchAll, n as saveSettings, o as searchFiles, r as getSearchStatus, s as searchSymbols, t as loadSettings } from "./settings-BVnzGFL7.mjs";
3
- import { t as formatComments } from "./comment-format-DG_PAehC.mjs";
4
- import { n as formatPlanReview, r as sectionTitleForLine, t as extractPlanLines } from "./plan-format-BQyx--mp.mjs";
5
- import { f as refreshPrSession, i as detectCwdRepo, m as submitReview, t as buildPrSession, u as parsePrRef } from "./github-HfIlIS-J.mjs";
6
- import { execFileSync } from "node:child_process";
7
- import { fileURLToPath } from "node:url";
8
- import { dirname, extname, join, resolve } from "node:path";
9
- import { existsSync, mkdirSync, readFileSync, watch } from "node:fs";
10
- import { parseArgs } from "node:util";
11
- import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
12
- import { homedir } from "node:os";
13
- import { Hono } from "hono";
14
- import { streamSSE } from "hono/streaming";
15
- import { serve } from "@hono/node-server";
16
- //#region src/lib/diff-options.ts
17
- const DEFAULTS = {
18
- revisions: [],
19
- staged: false,
20
- merge: false,
21
- pathspecs: [],
22
- indentHeuristic: true,
23
- noIndentHeuristic: false,
24
- ignoreSpaceChange: false,
25
- ignoreAllSpace: false,
26
- ignoreBlankLines: false,
27
- ignoreCrAtEol: false,
28
- functionContext: false,
29
- patchWithRaw: false,
30
- patchWithStat: false,
31
- compactSummary: false,
32
- cumulative: false,
33
- pickaxeAll: false,
34
- exitCode: false,
35
- quiet: false,
36
- noPrefix: false,
37
- binary: false,
38
- fullIndex: false,
39
- text: false,
40
- textconv: false,
41
- noExtDiff: false,
42
- itaVisible: false,
43
- noRelative: false,
44
- findCopiesHarder: false,
45
- noRenames: false,
46
- base: false,
47
- ours: false,
48
- theirs: false,
49
- check: false,
50
- outputMode: "auto",
51
- host: "127.0.0.1",
52
- noOpen: false,
53
- help: false,
54
- version: false,
55
- includeUnstaged: true,
56
- includeUntracked: true,
57
- tui: false,
58
- gpu: false,
59
- showMode: false,
60
- showRevspecs: []
61
- };
62
- /**
63
- * All git-diff option definitions for `node:util.parseArgs`.
64
- * Categorised to match `git diff --help`.
65
- */
66
- const GIT_DIFF_OPTIONS = {
67
- staged: { type: "boolean" },
68
- cached: { type: "boolean" },
69
- merge: { type: "boolean" },
70
- "diff-algorithm": { type: "string" },
71
- "indent-heuristic": {
72
- type: "boolean",
73
- default: true
74
- },
75
- "no-indent-heuristic": { type: "boolean" },
76
- anchored: { type: "string" },
77
- "ignore-space-change": {
78
- type: "boolean",
79
- short: "b"
80
- },
81
- "ignore-all-space": {
82
- type: "boolean",
83
- short: "w"
84
- },
85
- "ignore-blank-lines": { type: "boolean" },
86
- "ignore-cr-at-eol": { type: "boolean" },
87
- "ws-error-highlight": { type: "string" },
88
- "unified": {
89
- type: "string",
90
- short: "U"
91
- },
92
- "inter-hunk-context": { type: "string" },
93
- "function-context": {
94
- type: "boolean",
95
- short: "W"
96
- },
97
- "word-diff": { type: "string" },
98
- "word-diff-regex": { type: "string" },
99
- "color-words": { type: "string" },
100
- "color-moved": { type: "string" },
101
- "color-moved-ws": { type: "string" },
102
- "find-copies": {
103
- type: "string",
104
- short: "C"
105
- },
106
- "find-copies-harder": { type: "boolean" },
107
- "find-renames": {
108
- type: "string",
109
- short: "M"
110
- },
111
- "break-rewrites": {
112
- type: "string",
113
- short: "B"
114
- },
115
- "no-renames": { type: "boolean" },
116
- patch: {
117
- type: "boolean",
118
- short: "p"
119
- },
120
- "no-patch": {
121
- type: "boolean",
122
- short: "s"
123
- },
124
- raw: { type: "boolean" },
125
- "patch-with-raw": { type: "boolean" },
126
- "patch-with-stat": { type: "boolean" },
127
- numstat: { type: "boolean" },
128
- shortstat: { type: "boolean" },
129
- stat: { type: "string" },
130
- summary: { type: "boolean" },
131
- "name-only": { type: "boolean" },
132
- "name-status": { type: "boolean" },
133
- check: { type: "boolean" },
134
- "compact-summary": { type: "boolean" },
135
- dirstat: { type: "string" },
136
- "dirstat-by-file": { type: "string" },
137
- cumulative: { type: "boolean" },
138
- "diff-filter": { type: "string" },
139
- S: { type: "string" },
140
- G: { type: "string" },
141
- "pickaxe-all": { type: "boolean" },
142
- output: {
143
- type: "string",
144
- short: "o"
145
- },
146
- "exit-code": { type: "boolean" },
147
- quiet: { type: "boolean" },
148
- "src-prefix": { type: "string" },
149
- "dst-prefix": { type: "string" },
150
- "no-prefix": { type: "boolean" },
151
- "line-prefix": { type: "string" },
152
- submodule: { type: "string" },
153
- binary: { type: "boolean" },
154
- "no-binary": { type: "boolean" },
155
- "full-index": { type: "boolean" },
156
- text: {
157
- type: "boolean",
158
- short: "a"
159
- },
160
- textconv: { type: "boolean" },
161
- "no-textconv": { type: "boolean" },
162
- "no-ext-diff": { type: "boolean" },
163
- "ext-diff": { type: "string" },
164
- "ita-visible": { type: "boolean" },
165
- "ita-invisible-in-index": { type: "boolean" },
166
- relative: { type: "string" },
167
- "no-relative": { type: "boolean" },
168
- "ignore-submodules": { type: "string" },
169
- base: { type: "boolean" },
170
- ours: { type: "boolean" },
171
- theirs: { type: "boolean" }
172
- };
173
- /** Options that are diffing-specific, not git-diff options. */
174
- const DIFFING_OPTIONS = {
175
- port: { type: "string" },
176
- host: { type: "string" },
177
- "no-open": {
178
- type: "boolean",
179
- default: false
180
- },
181
- web: {
182
- type: "boolean",
183
- default: false
184
- },
185
- terminal: {
186
- type: "boolean",
187
- default: false
188
- },
189
- tui: {
190
- type: "boolean",
191
- default: false
192
- },
193
- gpu: {
194
- type: "boolean",
195
- default: false
196
- },
197
- "gh-pr": { type: "string" },
198
- help: {
199
- type: "boolean",
200
- short: "h"
201
- },
202
- version: { type: "boolean" }
203
- };
204
- function getVersion() {
205
- try {
206
- const __dirname = dirname(fileURLToPath(import.meta.url));
207
- return JSON.parse(readFileSync(resolve(__dirname, "..", "..", "package.json"), "utf-8")).version;
208
- } catch {
209
- return "unknown";
210
- }
211
- }
212
- function printHelp() {
213
- const version = getVersion();
214
- console.log(`diffing v${version} – Local code review tool for git diffs
215
-
216
- Usage: diffing [<git diff options>] [<revision>...] [-- <path>...]
217
- diffing --web [<git diff options>] [<revision>...] [-- <path>...]
218
- diffing [server options]
219
-
220
- Git Diff Options (drop-in replacement for git diff):`);
221
- printGitDiffHelp();
222
- console.log(`
223
- Diffing Server Options:
224
- --port <port> Port to run the server on (default: random available port)
225
- --host <host> Host address to bind to (default: 127.0.0.1). Pass
226
- 0.0.0.0 to expose the server to the local network.
227
- --no-open Don't open the browser automatically
228
- --gh-pr <ref> Open a GitHub PR review session (number, owner/repo#N, or URL)
229
- Same as: diffing "gh pr <ref>"
230
-
231
- Output Modes:
232
- By default diffing auto-selects the best output mode:
233
- - Terminal mode: when output is a pipe, redirect, or non-TTY
234
- - Web mode: when output is a TTY (interactive terminal)
235
-
236
- Force a mode with --web, --terminal, or --tui flags.
237
- --tui is experimental and opt-in. It opens a native-Rust terminal
238
- interface that mirrors the web dashboard. Falls back to git diff
239
- output if the environment cannot support a TUI (piped stdin, CI,
240
- missing binary).
241
-
242
- Subcommands:
243
- show <revspec>... Drop-in for 'git show'. Renders commit metadata
244
- above the diff. Accepts commits, ranges, tags.
245
- await-review Block until human clicks Send to agent; print XML
246
- comments [--open] [--format xml|json|md]
247
- reply <id> --body … Reply on a comment thread
248
- resolve|unresolve <id>
249
- comment edit|delete <id>
250
- progress --message … Live agent progress toast
251
- url Print the active review base URL
252
- plan <submit|await|list|show|versions|reply|resolve>
253
- gh <status|pr-fetch|pr-list-comments|pr-review>
254
- mcp [--repo PATH] Stdio MCP server for agents
255
- inspect <summary|files|hunks|slice|search>
256
- Bounded reads from a running TUI session
257
- doctor Environment self-check
258
- completion <bash|zsh|fish>
259
- update Check for a newer npm release
260
-
261
- Custom ranges (e.g. main..feature) and 'gh pr' sessions also display a
262
- "what is this diff" banner in the web UI summarising the diff source.
263
-
264
- Examples:
265
- diffing Review uncommitted changes (web UI)
266
- diffing --staged Review staged changes
267
- diffing HEAD~3 Review last 3 commits
268
- diffing main..feature Compare branches
269
- diffing show HEAD Review a single commit's metadata + diff
270
- diffing show HEAD~3..HEAD Review the last 3 commits as a series
271
- diffing show HEAD -- src/ Limit a commit review to a directory
272
- diffing "gh pr 1234" Review a GitHub pull request
273
- diffing --diff-algorithm=patience src/ Diff with patience algorithm
274
- diffing -U10 Diff with 10 context lines
275
- diffing --word-diff=color Word-level diff in color
276
- diffing -b -w Ignore whitespace changes
277
- diffing --host 0.0.0.0 Allow other machines on the LAN to review
278
- diffing --tui Open the experimental TUI (native Rust)
279
- diffing await-review Agent: wait for human handoff
280
- diffing plan submit PLAN.md Agent: submit a plan for approval
281
- diffing mcp --repo /abs/path MCP server bound to one repository
282
-
283
- Note: 'diffing <sha>' runs 'git diff <sha>' (working tree vs <sha>); it
284
- is not the same as 'diffing show <sha>'. Use 'show' when you want to
285
- review a commit's changes.
286
-
287
- Full reference: docs/cli.md (bundled in the repository).`);
288
- }
289
- function printGitDiffHelp() {
290
- console.log(`
291
- Revision / Range:
292
- --staged, --cached Show staged changes
293
- --merge Show merge conflicts
294
-
295
- Diff Algorithm:
296
- --diff-algorithm=<A> Choose diff algorithm (minimal, patience, histogram, myers)
297
- --indent-heuristic Enable indent heuristic (default: on)
298
- --no-indent-heuristic Disable indent heuristic
299
- --anchored=<TEXT> Anchor diff algorithm to <TEXT>
300
-
301
- Whitespace:
302
- -b, --ignore-space-change Ignore changes in amount of whitespace
303
- -w, --ignore-all-space Ignore whitespace when comparing lines
304
- --ignore-blank-lines Ignore blank lines
305
- --ignore-cr-at-eol Ignore carriage-return at EOL
306
- --ws-error-highlight=<KIND> Highlight whitespace errors (none, default, all)
307
-
308
- Context:
309
- -U<n>, --unified=<n> Generate diffs with <n> lines of context
310
- --inter-hunk-context=<n> Show <n> lines between diff hunks
311
- -W, --function-context Show whole function as context
312
-
313
- Word Diff:
314
- --word-diff=<MODE> Word diff (color, plain, porcelain, none)
315
- --word-diff-regex=<REGEX> Regex to determine a word
316
- --color-words[=<REGEX>] Word diff with color
317
-
318
- Moved / Copied Detection:
319
- --color-moved=<MODE> Color moved lines (no, default, plain, blocks, zebra, dimmed-zebra)
320
- --color-moved-ws=<MODE> How whitespace is treated in --color-moved
321
- -C[<n>], --find-copies[=<n>] Detect copies
322
- --find-copies-harder Spend extra cycles to find copies
323
- -M[<n>], --find-renames[=<n>] Detect renames
324
- -B[<n>], --break-rewrites[=<n>] Break complete rewrite changes into delete/add pairs
325
- --no-renames Turn off rename detection
326
-
327
- Merge / Conflict:
328
- --base Compare with merge base (stage 1)
329
- --ours Compare with our branch (stage 2)
330
- --theirs Compare with their branch (stage 3)
331
-
332
- Output Format:
333
- -p, --patch Generate patch (default)
334
- -s, --no-patch Suppress diff output
335
- --raw Generate raw diff
336
- --patch-with-raw Both patch and raw output
337
- --patch-with-stat Both patch and stat
338
- --numstat Machine-friendly stat: additions/deletions per file
339
- --shortstat Summary of additions/deletions
340
- --stat[=<width>] Diffstat with graph
341
- --summary Extended header summary
342
- --name-only Show only names of changed files
343
- --name-status Show only names and status of changed files
344
- --check Check if diff introduces whitespace errors
345
- --compact-summary Compact summary for submodule changes
346
- --dirstat[=<param>] Directory-level change stats
347
- --dirstat-by-file[=<param>] Per-file directory stats
348
- --cumulative Cumulative directory stats
349
-
350
- Filtering:
351
- --diff-filter=[(A|C|D|M|R|T|U|X|B)...[*]]
352
- -S<string> Look for differences that change number of occurrences of <string>
353
- -G<regex> Look for differences whose patch text contains <regex>
354
- --pickaxe-all Show all changes with -S or -G
355
-
356
- Output Control:
357
- -o, --output=<file> Write diff to <file>
358
- --exit-code Exit with 1 if differences, 0 if not
359
- --quiet Disable all output, imply --exit-code
360
-
361
- Prefixes:
362
- --src-prefix=<PREFIX> Source prefix (default: "a/")
363
- --dst-prefix=<PREFIX> Destination prefix (default: "b/")
364
- --no-prefix Do not show source/destination prefixes
365
- --line-prefix=<PREFIX> Prepend <PREFIX> to every line
366
-
367
- Submodule:
368
- --submodule=<FORMAT> Submodule diff format (diff, log, short)
369
-
370
- Miscellaneous:
371
- --binary Output binary diff (apply patch to binary files)
372
- --no-binary Don't output binary diff
373
- --full-index Show full blob object names
374
- -a, --text Treat all files as text
375
- --textconv Allow external text conversion filters
376
- --no-textconv Disallow external text conversion filters
377
- --no-ext-diff Disallow external diff drivers
378
- --ext-diff=<CMD> Allow external diff driver
379
- --ita-visible Include intent-to-add files
380
- --ignore-submodules=<WHEN> Ignore submodule changes
381
- --relative[=<path>] Show changes relative to <path>
382
- --no-relative Don't show relative paths`);
383
- }
384
- /**
385
- * Pre-process argv to handle git's optional-value flags.
386
- *
387
- * `-C`, `-M`, `-B` and their long forms (`--find-copies`, `--find-renames`,
388
- * `--break-rewrites`) take an **optional** numeric argument in git — when the
389
- * value is omitted, git uses the default similarity threshold (40%, 50%, 50%
390
- * respectively for `-B`'s percentage component, 60% for the `x` part of
391
- * `Bx/y`). `node:util.parseArgs` always consumes the next token as the value
392
- * for `type: 'string'`, which means `diffing -C main..feature` would
393
- * swallow `main..feature` as the `-C` value instead of treating it as a
394
- * revision.
395
- *
396
- * This function rewrites the input argv so that:
397
- * - `-C` (no attached value) followed by a non-digit → `--find-copies=40`
398
- * - `-C` (no attached value) followed by a digit → left as-is so
399
- * parseArgs consumes
400
- * the numeric value
401
- * - `-C40` (attached value) → left as-is
402
- * - `--find-copies` (no value) followed by a digit → `--find-copies=<n>`
403
- * - `--find-copies=40` (attached value) → left as-is
404
- *
405
- * The defaults mirror git's own behaviour: 40, 50, 50/60.
406
- */
407
- function preprocessOptionalValueArgs(rawArgs) {
408
- const SHORTS = {
409
- C: {
410
- long: "find-copies",
411
- def: "40"
412
- },
413
- M: {
414
- long: "find-renames",
415
- def: "50"
416
- },
417
- B: {
418
- long: "break-rewrites",
419
- def: "50/60"
420
- }
421
- };
422
- const LONGS = new Set([
423
- "find-copies",
424
- "find-renames",
425
- "break-rewrites"
426
- ]);
427
- const startsWithDigit = (s) => typeof s === "string" && /^[0-9]/.test(s);
428
- const out = [];
429
- for (let i = 0; i < rawArgs.length; i++) {
430
- const arg = rawArgs[i];
431
- if (arg.length === 2 && arg[0] === "-" && SHORTS[arg[1]]) {
432
- const cfg = SHORTS[arg[1]];
433
- const next = rawArgs[i + 1];
434
- if (startsWithDigit(next)) out.push(arg);
435
- else out.push(`--${cfg.long}=${cfg.def}`);
436
- continue;
437
- }
438
- const longMatch = /^--(find-copies|find-renames|break-rewrites)$/.exec(arg);
439
- if (longMatch) {
440
- const name = longMatch[1];
441
- if (!LONGS.has(name)) {
442
- out.push(arg);
443
- continue;
444
- }
445
- const next = rawArgs[i + 1];
446
- if (startsWithDigit(next)) {
447
- out.push(`--${name}=${next}`);
448
- i++;
449
- } else {
450
- const def = name === "find-copies" ? "40" : name === "find-renames" ? "50" : "50/60";
451
- out.push(`--${name}=${def}`);
452
- }
453
- continue;
454
- }
455
- out.push(arg);
456
- }
457
- return out;
458
- }
459
- /**
460
- * Parse raw `process.argv` slice into typed DiffOptions.
461
- *
462
- * Strategy:
463
- * 1. Pre-process optional-value args (`-C`, `-M`, `-B`, long forms) so they
464
- * don't swallow following revisions like `main..feature`.
465
- * 2. Split argv into: [diffing options] [git revisions] [-- pathspecs]
466
- * 3. Parse known flags via node:util.parseArgs
467
- * 4. Anything unknown / positional becomes a revision or pathspec
468
- */
469
- function parseDiffOptions(rawArgs) {
470
- const { values, positionals } = parseArgs({
471
- options: {
472
- ...GIT_DIFF_OPTIONS,
473
- ...DIFFING_OPTIONS
474
- },
475
- allowPositionals: true,
476
- strict: false,
477
- args: preprocessOptionalValueArgs(rawArgs)
478
- });
479
- const opts = {
480
- ...DEFAULTS,
481
- revisions: [],
482
- pathspecs: [],
483
- showRevspecs: []
484
- };
485
- if (values.help) opts.help = true;
486
- if (values.version) opts.version = true;
487
- if (values.port) opts.port = parseInt(values.port, 10);
488
- if (values.host) opts.host = values.host;
489
- if (values["no-open"]) opts.noOpen = true;
490
- if (values["gh-pr"]) opts.ghPr = String(values["gh-pr"]);
491
- if (values.staged || values.cached) opts.staged = true;
492
- if (values.merge) opts.merge = true;
493
- const algo = values["diff-algorithm"];
494
- if (algo && [
495
- "minimal",
496
- "patience",
497
- "histogram",
498
- "myers"
499
- ].includes(algo)) opts.algorithm = algo;
500
- if (values["no-indent-heuristic"]) {
501
- opts.noIndentHeuristic = true;
502
- opts.indentHeuristic = false;
503
- }
504
- if (values.anchored) opts.anchored = values.anchored;
505
- if (values["ignore-space-change"]) opts.ignoreSpaceChange = true;
506
- if (values["ignore-all-space"]) opts.ignoreAllSpace = true;
507
- if (values["ignore-blank-lines"]) opts.ignoreBlankLines = true;
508
- if (values["ignore-cr-at-eol"]) opts.ignoreCrAtEol = true;
509
- const wsh = values["ws-error-highlight"];
510
- if (wsh && [
511
- "none",
512
- "default",
513
- "all"
514
- ].includes(wsh)) opts.wsErrorHighlight = wsh;
515
- if (values.unified) opts.unifiedContext = parseInt(values.unified, 10);
516
- if (values["inter-hunk-context"]) opts.interHunkContext = parseInt(values["inter-hunk-context"], 10);
517
- if (values["function-context"]) opts.functionContext = true;
518
- const wd = values["word-diff"];
519
- if (wd && [
520
- "color",
521
- "plain",
522
- "porcelain",
523
- "none"
524
- ].includes(wd)) opts.wordDiff = wd;
525
- if (values["word-diff-regex"]) opts.wordDiffRegex = values["word-diff-regex"];
526
- if (values["color-words"]) {
527
- opts.wordDiff = "color";
528
- opts.wordDiffRegex = values["color-words"] || void 0;
529
- }
530
- const cm = values["color-moved"];
531
- if (cm && [
532
- "default",
533
- "plain",
534
- "blocks",
535
- "zebra",
536
- "dimmed-zebra",
537
- "no"
538
- ].includes(cm)) opts.colorMoved = cm;
539
- const cmw = values["color-moved-ws"];
540
- if (cmw && [
541
- "no",
542
- "ignore-space-at-eol",
543
- "ignore-space-change",
544
- "ignore-all-space",
545
- "allow-indentation-change"
546
- ].includes(cmw)) opts.colorMovedWs = cmw;
547
- if (values["find-copies"] !== void 0) {
548
- const v = values["find-copies"];
549
- opts.findCopies = v ? parseInt(v, 10) : 40;
550
- }
551
- if (values["find-copies-harder"]) opts.findCopiesHarder = true;
552
- if (values["find-renames"] !== void 0) {
553
- const v = values["find-renames"];
554
- opts.findRenames = v ? parseInt(v, 10) : 50;
555
- }
556
- if (values["break-rewrites"] !== void 0) opts.breakRewrites = values["break-rewrites"];
557
- if (values["no-renames"]) opts.noRenames = true;
558
- if (values.base) opts.base = true;
559
- if (values.ours) opts.ours = true;
560
- if (values.theirs) opts.theirs = true;
561
- if (values.raw) opts.outputFormat = "raw";
562
- if (values.numstat) opts.outputFormat = "numstat";
563
- if (values.shortstat) opts.outputFormat = "shortstat";
564
- if (values.stat !== void 0) opts.outputFormat = "stat";
565
- if (values.summary) opts.outputFormat = "summary";
566
- if (values["name-only"]) opts.outputFormat = "name-only";
567
- if (values["name-status"]) opts.outputFormat = "name-status";
568
- if (values["patch-with-raw"]) opts.patchWithRaw = true;
569
- if (values["patch-with-stat"]) opts.patchWithStat = true;
570
- if (values["compact-summary"]) opts.compactSummary = true;
571
- if (values.dirstat) opts.dirstat = values.dirstat;
572
- if (values["dirstat-by-file"]) opts.dirstatByFile = values["dirstat-by-file"];
573
- if (values.cumulative) opts.cumulative = true;
574
- if (values["no-patch"]) opts.outputFormat = void 0;
575
- if (values.check) opts.outputFormat = void 0;
576
- if (values["diff-filter"]) opts.diffFilter = values["diff-filter"];
577
- if (values.S) opts.pickaxeString = values.S;
578
- if (values.G) opts.pickaxeRegex = values.G;
579
- if (values["pickaxe-all"]) opts.pickaxeAll = true;
580
- if (values.output) opts.outputFile = values.output;
581
- if (values["exit-code"]) opts.exitCode = true;
582
- if (values.quiet) {
583
- opts.quiet = true;
584
- opts.exitCode = true;
585
- }
586
- if (values["src-prefix"]) opts.srcPrefix = values["src-prefix"];
587
- if (values["dst-prefix"]) opts.dstPrefix = values["dst-prefix"];
588
- if (values["no-prefix"]) opts.noPrefix = true;
589
- if (values["line-prefix"]) opts.linePrefix = values["line-prefix"];
590
- const sm = values.submodule;
591
- if (sm && [
592
- "diff",
593
- "log",
594
- "short"
595
- ].includes(sm)) opts.submodule = sm;
596
- if (values.binary) opts.binary = true;
597
- if (values["no-binary"]) opts.binary = false;
598
- if (values["full-index"]) opts.fullIndex = true;
599
- if (values.text) opts.text = true;
600
- if (values.textconv) opts.textconv = true;
601
- if (values["no-textconv"]) opts.textconv = false;
602
- if (values["no-ext-diff"]) opts.noExtDiff = true;
603
- if (values["ext-diff"]) opts.extDiff = values["ext-diff"];
604
- if (values["ita-visible"] || values["ita-invisible-in-index"]) opts.itaVisible = true;
605
- if (values.relative !== void 0) opts.relative = values.relative || void 0;
606
- if (values["no-relative"]) opts.noRelative = true;
607
- if (values["ignore-submodules"]) {
608
- const ig = values["ignore-submodules"];
609
- if ([
610
- "none",
611
- "untracked",
612
- "dirty",
613
- "all"
614
- ].includes(ig)) opts.ignoreSubmodules = ig;
615
- }
616
- const dashDashIdx = rawArgs.indexOf("--");
617
- if (dashDashIdx >= 0) {
618
- for (let i = 0; i < dashDashIdx; i++) {
619
- const arg = rawArgs[i];
620
- if (!arg.startsWith("-")) opts.revisions.push(arg);
621
- }
622
- for (let i = dashDashIdx + 1; i < rawArgs.length; i++) opts.pathspecs.push(rawArgs[i]);
623
- } else for (const p of positionals) opts.revisions.push(p);
624
- if (values.tui) opts.outputMode = "tui";
625
- else if (values.web) opts.outputMode = "web";
626
- else if (values.terminal) opts.outputMode = "terminal";
627
- if (process.stdout.isTTY && opts.outputMode === "auto") opts.outputMode = "web";
628
- else if (!process.stdout.isTTY && opts.outputMode === "auto") opts.outputMode = "terminal";
629
- if (opts.outputFormat || opts.patchWithRaw || opts.patchWithStat || opts.exitCode || opts.quiet || opts.outputFile) opts.outputMode = "terminal";
630
- return opts;
631
- }
632
- /**
633
- * Build the `git diff` argument array from parsed options.
634
- * Returns args that can be spread into `git diff <args>`.
635
- */
636
- function buildGitDiffArgs(opts) {
637
- const args = [];
638
- if (opts.algorithm) args.push(`--diff-algorithm=${opts.algorithm}`);
639
- if (opts.noIndentHeuristic) args.push("--no-indent-heuristic");
640
- if (opts.anchored) args.push(`--anchored=${opts.anchored}`);
641
- if (opts.staged) args.push("--staged");
642
- if (opts.merge) args.push("--merge");
643
- if (opts.base) args.push("--base");
644
- if (opts.ours) args.push("--ours");
645
- if (opts.theirs) args.push("--theirs");
646
- if (opts.ignoreSpaceChange) args.push("--ignore-space-change");
647
- if (opts.ignoreAllSpace) args.push("--ignore-all-space");
648
- if (opts.ignoreBlankLines) args.push("--ignore-blank-lines");
649
- if (opts.ignoreCrAtEol) args.push("--ignore-cr-at-eol");
650
- if (opts.wsErrorHighlight) args.push(`--ws-error-highlight=${opts.wsErrorHighlight}`);
651
- if (opts.unifiedContext !== void 0) args.push(`--unified=${opts.unifiedContext}`);
652
- if (opts.interHunkContext !== void 0) args.push(`--inter-hunk-context=${opts.interHunkContext}`);
653
- if (opts.functionContext) args.push("--function-context");
654
- if (opts.wordDiff) if (opts.wordDiff === "color" && opts.wordDiffRegex && !opts.colorWords) {
655
- args.push(`--word-diff=color`);
656
- args.push(`--word-diff-regex=${opts.wordDiffRegex}`);
657
- } else if (opts.wordDiff === "color" && opts.colorWords) args.push(`--color-words=${opts.wordDiffRegex || "."}`);
658
- else args.push(`--word-diff=${opts.wordDiff}`);
659
- if (opts.wordDiffRegex && opts.wordDiff !== "color") args.push(`--word-diff-regex=${opts.wordDiffRegex}`);
660
- if (opts.colorMoved) args.push(`--color-moved=${opts.colorMoved}`);
661
- if (opts.colorMovedWs) args.push(`--color-moved-ws=${opts.colorMovedWs}`);
662
- if (opts.findCopies !== void 0) args.push(opts.findCopies === 40 ? "-C" : `-C${opts.findCopies}`);
663
- if (opts.findCopiesHarder) args.push("--find-copies-harder");
664
- if (opts.findRenames !== void 0) args.push(opts.findRenames === 50 ? "-M" : `-M${opts.findRenames}`);
665
- if (opts.breakRewrites !== void 0) args.push(`-B${opts.breakRewrites}`);
666
- if (opts.noRenames) args.push("--no-renames");
667
- if (opts.outputFormat === "raw") args.push("--raw");
668
- if (opts.outputFormat === "numstat") args.push("--numstat");
669
- if (opts.outputFormat === "shortstat") args.push("--shortstat");
670
- if (opts.outputFormat === "stat") args.push("--stat");
671
- if (opts.outputFormat === "summary") args.push("--summary");
672
- if (opts.outputFormat === "name-only") args.push("--name-only");
673
- if (opts.outputFormat === "name-status") args.push("--name-status");
674
- if (opts.patchWithRaw) args.push("--patch-with-raw");
675
- if (opts.patchWithStat) args.push("--patch-with-stat");
676
- if (opts.compactSummary) args.push("--compact-summary");
677
- if (opts.dirstat) args.push(`--dirstat=${opts.dirstat}`);
678
- if (opts.dirstatByFile) args.push(`--dirstat-by-file=${opts.dirstatByFile}`);
679
- if (opts.cumulative) args.push("--cumulative");
680
- if (opts.check) args.push("--check");
681
- if (opts.diffFilter) args.push(`--diff-filter=${opts.diffFilter}`);
682
- if (opts.pickaxeString) args.push(`-S${opts.pickaxeString}`);
683
- if (opts.pickaxeRegex) args.push(`-G${opts.pickaxeRegex}`);
684
- if (opts.pickaxeAll) args.push("--pickaxe-all");
685
- if (opts.outputFile) args.push(`--output=${opts.outputFile}`);
686
- if (opts.exitCode) args.push("--exit-code");
687
- if (opts.quiet) args.push("--quiet");
688
- if (opts.srcPrefix) args.push(`--src-prefix=${opts.srcPrefix}`);
689
- if (opts.dstPrefix) args.push(`--dst-prefix=${opts.dstPrefix}`);
690
- if (opts.noPrefix) args.push("--no-prefix");
691
- if (opts.linePrefix) args.push(`--line-prefix=${opts.linePrefix}`);
692
- if (opts.submodule) args.push(`--submodule=${opts.submodule}`);
693
- if (opts.binary) args.push("--binary");
694
- if (opts.fullIndex) args.push("--full-index");
695
- if (opts.text) args.push("--text");
696
- if (opts.textconv) args.push("--textconv");
697
- if (opts.noExtDiff) args.push("--no-ext-diff");
698
- if (opts.extDiff) args.push(`--ext-diff=${opts.extDiff}`);
699
- if (opts.itaVisible) args.push("--ita-visible");
700
- if (opts.relative !== void 0) args.push(opts.relative ? `--relative=${opts.relative}` : "--relative");
701
- if (opts.noRelative) args.push("--no-relative");
702
- if (opts.ignoreSubmodules) args.push(`--ignore-submodules=${opts.ignoreSubmodules}`);
703
- for (const rev of opts.revisions) args.push(rev);
704
- if (opts.pathspecs.length > 0) {
705
- args.push("--");
706
- args.push(...opts.pathspecs);
707
- }
708
- return args;
709
- }
710
- /**
711
- * Transform a `parseDiffOptions(...)` result into a "show mode" variant:
712
- * moves any parsed revisions into `showRevspecs`, clears `revisions` (so the
713
- * existing diff-engine path does not try to `git diff` them), and sets the
714
- * `showMode` flag. Pathspecs and every other flag survive unchanged.
715
- *
716
- * Returns the same object for callsite ergonomics — the mutation is in-place
717
- * because `parseDiffOptions` always returns a fresh object.
718
- */
719
- function intoShowMode(opts) {
720
- opts.showRevspecs = opts.revisions;
721
- opts.revisions = [];
722
- opts.showMode = true;
723
- return opts;
724
- }
725
- //#endregion
726
- //#region src/lib/diff-overview.ts
727
- /**
728
- * Compose the working-tree banner. Used by the default `git diff` (unstaged +
729
- * optionally staged + optionally untracked) flow.
730
- */
731
- function buildWorkingTreeOverview(input) {
732
- const { branch, staged, untracked, untrackedCount, fileCount } = input;
733
- const fileLabel = fileCount === 1 ? "file" : "files";
734
- const headline = `Working-tree changes${branch ? ` on ${branch}` : ""}`;
735
- const parts = [`${fileCount} ${fileLabel} changed`];
736
- if (staged) parts.push("staged included");
737
- if (untracked) {
738
- const u = untrackedCount === 1 ? "1 untracked file" : `${untrackedCount} untracked files`;
739
- parts.push(u);
740
- }
741
- return {
742
- kind: "working-tree",
743
- headline,
744
- subtitle: parts.join(" · "),
745
- commitSubjects: [],
746
- commitCount: 0,
747
- truncated: 0,
748
- authors: []
749
- };
750
- }
751
- /**
752
- * Staged-only banner: only `--staged` is on, no untracked, no revisions.
753
- * Shown when the user reviews a clean staging area.
754
- */
755
- function buildStagedOnlyOverview(input) {
756
- const { branch, fileCount } = input;
757
- const fileLabel = fileCount === 1 ? "file" : "files";
758
- return {
759
- kind: "staged-only",
760
- headline: `Staged changes${branch ? ` on ${branch}` : ""}`,
761
- subtitle: `${fileCount} ${fileLabel} staged for commit`,
762
- commitSubjects: [],
763
- commitCount: 0,
764
- truncated: 0,
765
- authors: []
766
- };
767
- }
768
- /**
769
- * Range banner: user-supplied revisions like `main..feature`, `HEAD~3..HEAD`,
770
- * or multiple SHAs/tags. `revspecs` are joined into the `rangeLabel` verbatim
771
- * (preserving `..` / `...` form the user typed).
772
- */
773
- function buildRangeOverview(input) {
774
- const { revspecs, branch, commitSeries } = input;
775
- const rangeLabel = revspecs.join(" ");
776
- const { commitCount, truncated, subjects, authors, fromDate, toDate } = summariseCommitSeries(commitSeries);
777
- const plural = commitCount === 1 ? "commit" : "commits";
778
- return {
779
- kind: "range",
780
- headline: `Comparing ${rangeLabel}${branch ? ` (current: ${branch})` : ""}`,
781
- subtitle: truncated > 0 ? `${commitCount} of ${commitCount + truncated} ${plural} shown` : `${commitCount} ${plural}`,
782
- commitSubjects: subjects,
783
- commitCount,
784
- truncated,
785
- fromDate,
786
- toDate,
787
- authors,
788
- rangeLabel
789
- };
790
- }
791
- /**
792
- * Commit banner: invoked by `diffing show <rev>` — single SHA or series. The
793
- * kind branches on `commits.length` so a single-commit review gets a tighter
794
- * headline than a series.
795
- */
796
- function buildCommitOverview(input) {
797
- const { commits, truncated, rangeLabel } = input;
798
- const { commitCount, subjects, authors, fromDate, toDate } = summariseCommitRows(commits);
799
- const isSingle = commitCount === 1;
800
- const kind = isSingle ? "commit-single" : "commit-series";
801
- let headline;
802
- let subtitle;
803
- if (isSingle) {
804
- headline = subjects[0] ? `Commit: ${subjects[0]}` : "Single commit";
805
- const subParts = [];
806
- if (rangeLabel) subParts.push(rangeLabel);
807
- if (authors[0]) subParts.push(authors[0]);
808
- if (fromDate) subParts.push(formatDateShort(fromDate));
809
- if (truncated > 0) subParts.push(`${commitCount} of ${commitCount + truncated} commits shown`);
810
- subtitle = subParts.length > 0 ? subParts.join(" · ") : void 0;
811
- } else {
812
- const plural = "commits";
813
- headline = `Reviewing ${commitCount} ${plural}`;
814
- const subParts = [];
815
- if (rangeLabel) subParts.push(rangeLabel);
816
- if (authors.length > 0) subParts.push(authors.join(", "));
817
- if (fromDate && toDate) subParts.push(`${formatDateShort(fromDate)} – ${formatDateShort(toDate)}`);
818
- if (truncated > 0) subParts.push(`${commitCount} of ${commitCount + truncated} ${plural} shown`);
819
- subtitle = subParts.length > 0 ? subParts.join(" · ") : void 0;
820
- }
821
- return {
822
- kind,
823
- headline,
824
- subtitle,
825
- commitSubjects: subjects,
826
- commitCount,
827
- truncated,
828
- fromDate,
829
- toDate,
830
- authors,
831
- rangeLabel
832
- };
833
- }
834
- function formatDateShort(iso) {
835
- try {
836
- const d = new Date(iso);
837
- if (Number.isNaN(d.getTime())) return iso;
838
- return d.toLocaleDateString(void 0, {
839
- year: "numeric",
840
- month: "short",
841
- day: "numeric"
842
- });
843
- } catch {
844
- return iso;
845
- }
846
- }
847
- /**
848
- * PR banner: built from fields already in `pr-session.json`. No extra git or
849
- * GitHub calls — the server's PR-mode branch hands us the headline shape and
850
- * we keep the field set minimal so the banner never lies about counts.
851
- */
852
- function buildPrOverview(input) {
853
- const { prNumber, prTitle, prAuthor, additions, deletions } = input;
854
- const subParts = [];
855
- if (prAuthor) subParts.push(`by ${prAuthor}`);
856
- if (additions || deletions) subParts.push(`+${additions} / -${deletions}`);
857
- return {
858
- kind: "pr",
859
- headline: `PR #${prNumber}: ${prTitle}`,
860
- subtitle: subParts.length > 0 ? subParts.join(" · ") : void 0,
861
- commitSubjects: [],
862
- commitCount: 0,
863
- truncated: 0,
864
- authors: prAuthor ? [prAuthor] : [],
865
- prNumber,
866
- prTitle
867
- };
868
- }
869
- /**
870
- * Normalise a list of commit rows for the banner. Aggregates author + date
871
- * ranges and trims subjects past the cap (so the banner never tries to render
872
- * thousands of rows). The caller is responsible for passing the *visible*
873
- * slice; `truncated` is preserved as-is.
874
- */
875
- function summariseCommitRows(rows) {
876
- return summariseCommitSeries({
877
- subjects: rows.map((r) => r.subject),
878
- authors: [],
879
- commitCount: rows.length,
880
- truncated: 0
881
- }, rows);
882
- }
883
- /**
884
- * Internal helper used by both the row-list and the pre-aggregated series
885
- * shape. When the caller already has aggregated metadata, it passes that
886
- * directly; the rows are only used to recover authors + dates if the caller
887
- * didn't supply them.
888
- */
889
- function summariseCommitSeries(series, rows) {
890
- const subjects = series.subjects.filter((s) => s.length > 0);
891
- let authors = series.authors;
892
- let fromDate = series.fromDate;
893
- let toDate = series.toDate;
894
- if (rows && (authors.length === 0 || !fromDate || !toDate)) {
895
- const seen = new Set(authors);
896
- for (const r of rows) {
897
- if (r.author && !seen.has(r.author)) {
898
- seen.add(r.author);
899
- authors = [...authors, r.author];
900
- }
901
- if (r.date) {
902
- if (!fromDate || r.date < fromDate) fromDate = r.date;
903
- if (!toDate || r.date > toDate) toDate = r.date;
904
- }
905
- }
906
- }
907
- return {
908
- commitCount: series.commitCount,
909
- subjects,
910
- authors,
911
- fromDate,
912
- toDate,
913
- truncated: series.truncated
914
- };
915
- }
916
- //#endregion
917
- //#region src/lib/diff-engine.ts
918
- const MAX_BUFFER = 50 * 1024 * 1024;
919
- /**
920
- * Determine whether to use the "standard" web mode (unstaged+staged+untracked)
921
- * or a "custom" mode (specific revisions / pathspecs).
922
- *
923
- * Custom mode is used when the user provides explicit revisions or pathspecs
924
- * beyond just toggling staged/untracked. `showMode` is *also* a custom mode
925
- * — the "Show staged / Show untracked" toggles don't apply to a commit view.
926
- */
927
- function isCustomMode$1(opts) {
928
- return opts.revisions.length > 0 || opts.pathspecs.length > 0 || opts.showMode;
929
- }
930
- function parseFilePaths(patch) {
931
- const paths = /* @__PURE__ */ new Set();
932
- for (const line of patch.split("\n")) {
933
- const match = line.match(/^diff --git a\/.+ b\/(.+)$/);
934
- if (match) paths.add(match[1]);
935
- }
936
- return [...paths];
937
- }
938
- function parseBinaryFiles(patch, untrackedFiles) {
939
- const binaryFiles = [];
940
- const lines = patch.split("\n");
941
- for (let i = 0; i < lines.length; i++) {
942
- const line = lines[i];
943
- if (!line.startsWith("Binary files ") || !line.includes(" differ")) continue;
944
- let filePath = "";
945
- for (let j = i - 1; j >= 0; j--) {
946
- const match = lines[j].match(/^diff --git a\/.+ b\/(.+)$/);
947
- if (match) {
948
- filePath = match[1];
949
- break;
950
- }
951
- }
952
- if (!filePath) continue;
953
- let changeType = "changed";
954
- for (let j = i - 1; j >= 0; j--) {
955
- if (lines[j].startsWith("diff --git")) break;
956
- if (lines[j].startsWith("new file mode")) {
957
- changeType = "added";
958
- break;
959
- }
960
- if (lines[j].startsWith("deleted file mode")) {
961
- changeType = "deleted";
962
- break;
963
- }
964
- }
965
- if (changeType === "added" && untrackedFiles?.has(filePath)) changeType = "untracked";
966
- binaryFiles.push({
967
- path: filePath,
968
- type: changeType
969
- });
970
- }
971
- return binaryFiles;
972
- }
973
- /**
974
- * Execute a diff asynchronously — used by the server.
975
- *
976
- * Show mode bypasses the normal `git diff` path entirely: it resolves
977
- * revspecs to a commit list and fetches per-commit metadata + patches in
978
- * one `git log` call. The concatenated patch is returned in the same shape
979
- * the existing UI pipeline expects (so file tree, binary detection, and
980
- * tab-size detection all keep working unchanged); the per-commit data
981
- * rides along on `commits` for the metadata banners.
982
- */
983
- async function executeDiffAsync(opts) {
984
- if (opts.showMode) {
985
- const { commits, patch, truncated } = await getShowDiff(opts.showRevspecs, opts.pathspecs);
986
- return {
987
- patch,
988
- args: [],
989
- commits,
990
- truncated
991
- };
992
- }
993
- if (isCustomMode$1(opts)) {
994
- const args = buildGitDiffArgs(opts);
995
- return {
996
- patch: await getCustomGitDiffAsync(args),
997
- args
998
- };
999
- }
1000
- return {
1001
- patch: await getGitDiffAsync({
1002
- staged: opts.staged,
1003
- untracked: opts.includeUntracked
1004
- }),
1005
- args: []
1006
- };
1007
- }
1008
- /**
1009
- * Execute a diff and produce the full enriched result used by the web API.
1010
- */
1011
- async function executeDiffWithMeta(opts) {
1012
- const { patch, commits, truncated } = await executeDiffAsync(opts);
1013
- const repoName = getRepoName();
1014
- const branch = getBranchName();
1015
- const untrackedFiles = opts.includeUntracked && !opts.showMode ? await getUntrackedFilePathsAsync() : [];
1016
- const binaryFiles = parseBinaryFiles(patch, new Set(untrackedFiles));
1017
- const filePaths = parseFilePaths(patch);
1018
- const tabSizeMap = getTabSizeForFiles(filePaths);
1019
- let overview;
1020
- if (opts.showMode && commits && commits.length > 0) overview = buildCommitOverview({
1021
- commits: commits.map((c) => ({
1022
- subject: c.subject,
1023
- author: c.authorName,
1024
- date: c.authorDate
1025
- })),
1026
- truncated: truncated ?? 0,
1027
- rangeLabel: deriveShowRangeLabel(opts.showRevspecs)
1028
- });
1029
- else if (!opts.showMode && opts.revisions.length > 0) {
1030
- const series = await getCommitSeriesSummary(opts.revisions, opts.pathspecs);
1031
- overview = buildRangeOverview({
1032
- revspecs: opts.revisions,
1033
- branch,
1034
- commitSeries: {
1035
- subjects: series.subjects,
1036
- authors: series.authors,
1037
- fromDate: series.fromDate,
1038
- toDate: series.toDate,
1039
- commitCount: series.commitCount,
1040
- truncated: series.truncated
1041
- }
1042
- });
1043
- } else if (!opts.showMode && opts.staged && !opts.includeUntracked && opts.revisions.length === 0) overview = buildStagedOnlyOverview({
1044
- branch,
1045
- fileCount: filePaths.length
1046
- });
1047
- else if (!opts.showMode) overview = buildWorkingTreeOverview({
1048
- branch,
1049
- staged: opts.staged,
1050
- untracked: opts.includeUntracked,
1051
- untrackedCount: untrackedFiles.length,
1052
- fileCount: filePaths.length
1053
- });
1054
- return {
1055
- patch,
1056
- binaryFiles,
1057
- filePaths,
1058
- tabSizeMap,
1059
- untrackedFiles,
1060
- repoName,
1061
- branch,
1062
- ...commits ? { commits } : {},
1063
- ...truncated ? { truncated } : {},
1064
- ...overview ? { overview } : {}
1065
- };
1066
- }
1067
- /**
1068
- * Derive a display label for `diffing show` revspecs. Multi-revspecs and
1069
- * range-like expressions (`A..B`, `A...B`) keep their raw form so the banner
1070
- * subtitle tells the reviewer exactly what range is being shown. A plain
1071
- * single SHA/tag is omitted because the headline already names the commit.
1072
- */
1073
- function deriveShowRangeLabel(revspecs) {
1074
- if (revspecs.length === 0) return void 0;
1075
- if (revspecs.length > 1) return revspecs.join(" ");
1076
- const single = revspecs[0];
1077
- if (single && (single.includes("..") || single.includes("..."))) return single;
1078
- }
1079
- /**
1080
- * Run the diff in terminal mode: forward to git diff with full flags,
1081
- * output to stdout. Behaves identically to `git diff`. In show mode this
1082
- * delegates to `git show` instead so `diffing show <rev>... --terminal`
1083
- * is a drop-in for `git show <rev>...`.
1084
- */
1085
- function runTerminalDiff(opts) {
1086
- if (opts.showMode) return runTerminalShow(opts);
1087
- const args = buildGitDiffArgs(opts);
1088
- const enforceDefaults = !opts.outputFormat && !opts.quiet;
1089
- const finalArgs = [];
1090
- if (enforceDefaults) {
1091
- if (!args.includes("--no-ext-diff") && !opts.extDiff) finalArgs.push("--no-ext-diff");
1092
- }
1093
- finalArgs.push(...args);
1094
- try {
1095
- execFileSync("git", ["diff", ...finalArgs], {
1096
- encoding: "utf-8",
1097
- maxBuffer: MAX_BUFFER,
1098
- stdio: "inherit"
1099
- });
1100
- return 0;
1101
- } catch (err) {
1102
- if (err.status === 1 && opts.exitCode) return 1;
1103
- return err.status ?? 1;
1104
- }
1105
- }
1106
- /**
1107
- * Terminal mode for `diffing show`: spawn `git show` with the user's revspecs
1108
- * and pathspecs, inherit stdio, propagate exit code. We pass `--no-ext-diff`
1109
- * so the output is a clean unified diff regardless of the user's git config
1110
- * (matches the web-mode pipeline), but otherwise hand everything to git.
1111
- */
1112
- function runTerminalShow(opts) {
1113
- const args = [
1114
- "show",
1115
- "--no-ext-diff",
1116
- ...opts.showRevspecs
1117
- ];
1118
- if (opts.pathspecs.length > 0) args.push("--", ...opts.pathspecs);
1119
- try {
1120
- execFileSync("git", args, {
1121
- encoding: "utf-8",
1122
- maxBuffer: MAX_BUFFER,
1123
- stdio: "inherit"
1124
- });
1125
- return 0;
1126
- } catch (err) {
1127
- return err.status ?? 1;
1128
- }
1129
- }
1130
- /**
1131
- * Validate the environment: check that we're in a git repo.
1132
- * Returns an error message or null.
1133
- */
1134
- function validateEnvironment() {
1135
- if (!isGitRepo()) return "Error: not inside a git repository";
1136
- return null;
1137
- }
1138
- //#endregion
1139
- //#region src/lib/comments.ts
1140
- var FileCommentStore = class {
1141
- dirPath;
1142
- filePath;
1143
- /**
1144
- * @param storageDir Absolute directory to persist `comments.json` in.
1145
- * Defaults to the per-repo storage dir under `~/.diffing` — comments are
1146
- * NEVER written inside the reviewed (consumer) repo, so a consumer project
1147
- * stays free of any diffing-specific artifacts. The override exists only so
1148
- * tests can point at a throwaway temp dir.
1149
- */
1150
- constructor(storageDir) {
1151
- this.dirPath = storageDir ?? getProjectStorageDir();
1152
- this.filePath = join(this.dirPath, "comments.json");
1153
- }
1154
- async getAll() {
1155
- try {
1156
- const data = await readFile(this.filePath, "utf-8");
1157
- return JSON.parse(data);
1158
- } catch {
1159
- return [];
1160
- }
1161
- }
1162
- async save(comments) {
1163
- try {
1164
- await mkdir(this.dirPath, { recursive: true });
1165
- try {
1166
- const repoRoot = getRepoRoot();
1167
- await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1168
- } catch {}
1169
- await writeFile(this.filePath, JSON.stringify(comments, null, 2), "utf-8");
1170
- } catch (err) {
1171
- console.error("Failed to save comments to file:", err);
1172
- }
1173
- }
1174
- async add(comment) {
1175
- const comments = await this.getAll();
1176
- comments.push(comment);
1177
- await this.save(comments);
1178
- return comment;
1179
- }
1180
- async update(id, fields) {
1181
- const comments = await this.getAll();
1182
- const index = comments.findIndex((c) => c.id === id);
1183
- if (index === -1) return null;
1184
- const comment = comments[index];
1185
- if (fields.body !== void 0) comment.body = fields.body;
1186
- if (fields.status !== void 0) comment.status = fields.status;
1187
- await this.save(comments);
1188
- return comment;
1189
- }
1190
- async resolveAllOpen() {
1191
- const comments = await this.getAll();
1192
- let n = 0;
1193
- for (const c of comments) if (c.status === "open") {
1194
- c.status = "resolved";
1195
- n++;
1196
- }
1197
- if (n > 0) await this.save(comments);
1198
- return n;
1199
- }
1200
- async remove(id) {
1201
- const comments = await this.getAll();
1202
- const index = comments.findIndex((c) => c.id === id);
1203
- if (index === -1) return false;
1204
- comments.splice(index, 1);
1205
- await this.save(comments);
1206
- return true;
1207
- }
1208
- async addReply(commentId, reply) {
1209
- const comments = await this.getAll();
1210
- const index = comments.findIndex((c) => c.id === commentId);
1211
- if (index === -1) return null;
1212
- const comment = comments[index];
1213
- if (!comment.replies) comment.replies = [];
1214
- comment.replies.push(reply);
1215
- await this.save(comments);
1216
- return comment;
1217
- }
1218
- async removeReply(commentId, replyId) {
1219
- const comments = await this.getAll();
1220
- const index = comments.findIndex((c) => c.id === commentId);
1221
- if (index === -1) return null;
1222
- const comment = comments[index];
1223
- const replyIndex = comment.replies.findIndex((r) => r.id === replyId);
1224
- if (replyIndex === -1) return null;
1225
- comment.replies.splice(replyIndex, 1);
1226
- await this.save(comments);
1227
- return comment;
1228
- }
1229
- async updateReply(commentId, replyId, body) {
1230
- const comments = await this.getAll();
1231
- const index = comments.findIndex((c) => c.id === commentId);
1232
- if (index === -1) return null;
1233
- const comment = comments[index];
1234
- const reply = comment.replies.find((r) => r.id === replyId);
1235
- if (!reply) return null;
1236
- reply.body = body;
1237
- await this.save(comments);
1238
- return comment;
1239
- }
1240
- };
1241
- //#endregion
1242
- //#region src/lib/plans.ts
1243
- /** Absolute path of the persisted markdown mirror for a plan id. */
1244
- function planSourceFilePath(storageDir, planId) {
1245
- return join(storageDir, "plan-sources", `${planId}.md`);
1246
- }
1247
- function newId() {
1248
- return crypto.randomUUID();
1249
- }
1250
- /**
1251
- * Backfill / repair a plan loaded from disk so it matches the current schema.
1252
- * Older persisted plans may be missing `versions` and `createdAtPlanVersion`;
1253
- * we synthesize sensible values so callers can always rely on the new fields.
1254
- */
1255
- function backfillPlan(plan) {
1256
- const currentVersion = plan.version ?? 1;
1257
- const have = Array.isArray(plan.versions) ? plan.versions.length : 0;
1258
- if (!plan.versions || have === 0 || have < currentVersion) {
1259
- const createdAt = plan.updatedAt ?? plan.createdAt ?? Date.now();
1260
- const haveEntries = Array.isArray(plan.versions) ? plan.versions : [];
1261
- const present = new Set(haveEntries.map((v) => v.version));
1262
- const next = [];
1263
- for (let i = 1; i <= currentVersion; i++) {
1264
- const existing = haveEntries.find((v) => v.version === i);
1265
- if (existing) next.push(existing);
1266
- else next.push({
1267
- version: i,
1268
- body: plan.body,
1269
- title: plan.title,
1270
- source: plan.source,
1271
- model: plan.model,
1272
- createdAt
1273
- });
1274
- }
1275
- plan.versions = next.filter((v) => v.version >= 1 && v.version <= currentVersion);
1276
- if (have < currentVersion || !present.has(currentVersion)) markBackfilled(plan);
1277
- }
1278
- if (plan.comments) {
1279
- for (const c of plan.comments) if (typeof c.createdAtPlanVersion !== "number") c.createdAtPlanVersion = plan.version ?? 1;
1280
- }
1281
- if (!plan.comments) plan.comments = [];
1282
- }
1283
- function markBackfilled(plan) {
1284
- plan.__backfilledVersions = true;
1285
- }
1286
- /** Shared mutation helpers so both stores behave identically. */
1287
- function applyUpsert(plans, input, now) {
1288
- if (input.id) {
1289
- const existing = plans.find((p) => p.id === input.id);
1290
- if (existing) {
1291
- existing.title = input.title;
1292
- existing.body = input.body;
1293
- if (input.source !== void 0) existing.source = input.source;
1294
- if (input.model !== void 0) existing.model = input.model;
1295
- existing.version += 1;
1296
- existing.updatedAt = now;
1297
- if (!existing.versions) existing.versions = [];
1298
- existing.versions.push({
1299
- version: existing.version,
1300
- body: existing.body,
1301
- title: existing.title,
1302
- source: existing.source,
1303
- model: existing.model,
1304
- createdAt: now
1305
- });
1306
- existing.decision = "pending";
1307
- existing.decisionComment = void 0;
1308
- existing.decidedAt = void 0;
1309
- return existing;
1310
- }
1311
- }
1312
- const plan = {
1313
- id: input.id || newId(),
1314
- title: input.title,
1315
- body: input.body,
1316
- source: input.source,
1317
- model: input.model,
1318
- createdAt: now,
1319
- updatedAt: now,
1320
- version: 1,
1321
- decision: "pending",
1322
- comments: [],
1323
- versions: [{
1324
- version: 1,
1325
- body: input.body,
1326
- title: input.title,
1327
- source: input.source,
1328
- model: input.model,
1329
- createdAt: now
1330
- }]
1331
- };
1332
- plans.push(plan);
1333
- return plan;
1334
- }
1335
- /**
1336
- * Keep the most-recent `PlanVersion` snapshot in sync with the current `Plan`
1337
- * fields after a non-resubmit edit (PUT). Doesn't add a new entry — that only
1338
- * happens via `upsert` (the resubmit flow).
1339
- */
1340
- function syncCurrentVersion(plan) {
1341
- if (!plan.versions || plan.versions.length === 0) plan.versions = [];
1342
- if (plan.versions.length === 0) {
1343
- plan.versions.push({
1344
- version: plan.version,
1345
- body: plan.body,
1346
- title: plan.title,
1347
- source: plan.source,
1348
- model: plan.model,
1349
- createdAt: plan.updatedAt
1350
- });
1351
- return;
1352
- }
1353
- const last = plan.versions[plan.versions.length - 1];
1354
- last.body = plan.body;
1355
- last.title = plan.title;
1356
- last.source = plan.source;
1357
- last.model = plan.model;
1358
- }
1359
- var FilePlanStore = class {
1360
- dirPath;
1361
- filePath;
1362
- /**
1363
- * @param storageDir Absolute directory to persist `plans.json` in. Defaults
1364
- * to the per-repo storage dir under `~/.diffing`, the same place comments
1365
- * and media live — plans are NEVER written inside the reviewed (consumer)
1366
- * repo, so a consumer project stays free of any diffing-specific artifacts.
1367
- * The override exists only so tests can point at a throwaway temp dir.
1368
- */
1369
- constructor(storageDir) {
1370
- this.dirPath = storageDir ?? getProjectStorageDir();
1371
- this.filePath = join(this.dirPath, "plans.json");
1372
- }
1373
- async getAll() {
1374
- try {
1375
- const data = await readFile(this.filePath, "utf-8");
1376
- const plans = JSON.parse(data);
1377
- let anyBackfilled = false;
1378
- for (const p of plans) {
1379
- const before = Array.isArray(p.versions) ? p.versions.length : 0;
1380
- const beforeValid = Array.isArray(p.versions) && p.versions.length > 0;
1381
- backfillPlan(p);
1382
- if (!p.sourcePath) {
1383
- p.sourcePath = planSourceFilePath(this.dirPath, p.id);
1384
- anyBackfilled = true;
1385
- }
1386
- const after = Array.isArray(p.versions) ? p.versions.length : 0;
1387
- if ((!beforeValid || before < (p.version ?? 1)) && after > before) anyBackfilled = true;
1388
- }
1389
- if (anyBackfilled) try {
1390
- await this.save(plans);
1391
- } catch {}
1392
- return plans;
1393
- } catch {
1394
- return [];
1395
- }
1396
- }
1397
- async get(id) {
1398
- return (await this.getAll()).find((p) => p.id === id) ?? null;
1399
- }
1400
- async getVersion(id, version) {
1401
- const plan = await this.get(id);
1402
- if (!plan) return null;
1403
- return plan.versions.find((v) => v.version === version) ?? null;
1404
- }
1405
- async save(plans) {
1406
- try {
1407
- await mkdir(this.dirPath, { recursive: true });
1408
- try {
1409
- const repoRoot = getRepoRoot();
1410
- await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1411
- } catch {}
1412
- const clean = plans.map((p) => {
1413
- const copy = { ...p };
1414
- delete copy.__backfilledVersions;
1415
- return copy;
1416
- });
1417
- await writeFile(this.filePath, JSON.stringify(clean, null, 2), "utf-8");
1418
- } catch (err) {
1419
- console.error("Failed to save plans to file:", err);
1420
- }
1421
- }
1422
- /**
1423
- * Mirror the plan body to `plan-sources/<id>.md` and stamp `sourcePath` so
1424
- * the UI / agents can always copy a stable absolute path (even when the
1425
- * submitter only piped stdin and never set `--source`).
1426
- */
1427
- async writeSourceMirror(plan) {
1428
- try {
1429
- await mkdir(join(this.dirPath, "plan-sources"), { recursive: true });
1430
- const path = planSourceFilePath(this.dirPath, plan.id);
1431
- await writeFile(path, plan.body, "utf-8");
1432
- plan.sourcePath = path;
1433
- } catch (err) {
1434
- console.error("Failed to write plan source mirror:", err);
1435
- }
1436
- }
1437
- async upsert(input) {
1438
- const plans = await this.getAll();
1439
- const plan = applyUpsert(plans, input, Date.now());
1440
- backfillPlan(plan);
1441
- await this.writeSourceMirror(plan);
1442
- await this.save(plans);
1443
- return plan;
1444
- }
1445
- async update(id, fields) {
1446
- const plans = await this.getAll();
1447
- const plan = plans.find((p) => p.id === id);
1448
- if (!plan) return null;
1449
- if (fields.title !== void 0) plan.title = fields.title;
1450
- if (fields.body !== void 0) plan.body = fields.body;
1451
- if (fields.source !== void 0) plan.source = fields.source;
1452
- if (fields.model !== void 0) plan.model = fields.model;
1453
- plan.updatedAt = Date.now();
1454
- syncCurrentVersion(plan);
1455
- backfillPlan(plan);
1456
- if (fields.body !== void 0) await this.writeSourceMirror(plan);
1457
- await this.save(plans);
1458
- return plan;
1459
- }
1460
- async remove(id) {
1461
- const plans = await this.getAll();
1462
- const idx = plans.findIndex((p) => p.id === id);
1463
- if (idx === -1) return false;
1464
- plans.splice(idx, 1);
1465
- await this.save(plans);
1466
- return true;
1467
- }
1468
- async setDecision(id, decision, decisionComment) {
1469
- const plans = await this.getAll();
1470
- const plan = plans.find((p) => p.id === id);
1471
- if (!plan) return null;
1472
- plan.decision = decision;
1473
- plan.decisionComment = decisionComment?.trim() || void 0;
1474
- plan.decidedAt = Date.now();
1475
- plan.updatedAt = plan.decidedAt;
1476
- await this.save(plans);
1477
- return plan;
1478
- }
1479
- async mutate(planId, fn) {
1480
- const plans = await this.getAll();
1481
- const plan = plans.find((p) => p.id === planId);
1482
- if (!plan) return null;
1483
- if (!plan.comments) plan.comments = [];
1484
- if (!fn(plan)) return null;
1485
- backfillPlan(plan);
1486
- await this.save(plans);
1487
- return plan;
1488
- }
1489
- async addComment(planId, comment) {
1490
- return this.mutate(planId, (plan) => {
1491
- if (typeof comment.createdAtPlanVersion !== "number") comment.createdAtPlanVersion = plan.version ?? 1;
1492
- plan.comments.push(comment);
1493
- return true;
1494
- });
1495
- }
1496
- async updateComment(planId, commentId, fields) {
1497
- return this.mutate(planId, (plan) => {
1498
- const c = plan.comments.find((x) => x.id === commentId);
1499
- if (!c) return false;
1500
- if (fields.body !== void 0) c.body = fields.body;
1501
- if (fields.status !== void 0) c.status = fields.status;
1502
- return true;
1503
- });
1504
- }
1505
- async removeComment(planId, commentId) {
1506
- return this.mutate(planId, (plan) => {
1507
- const idx = plan.comments.findIndex((x) => x.id === commentId);
1508
- if (idx === -1) return false;
1509
- plan.comments.splice(idx, 1);
1510
- return true;
1511
- });
1512
- }
1513
- async addReply(planId, commentId, reply) {
1514
- return this.mutate(planId, (plan) => {
1515
- const c = plan.comments.find((x) => x.id === commentId);
1516
- if (!c) return false;
1517
- if (typeof c.createdAtPlanVersion === "number" && typeof reply.createdAtPlanVersion !== "number") reply.createdAtPlanVersion = c.createdAtPlanVersion;
1518
- if (!c.replies) c.replies = [];
1519
- c.replies.push(reply);
1520
- return true;
1521
- });
1522
- }
1523
- async removeReply(planId, commentId, replyId) {
1524
- return this.mutate(planId, (plan) => {
1525
- const c = plan.comments.find((x) => x.id === commentId);
1526
- if (!c) return false;
1527
- const idx = c.replies.findIndex((r) => r.id === replyId);
1528
- if (idx === -1) return false;
1529
- c.replies.splice(idx, 1);
1530
- return true;
1531
- });
1532
- }
1533
- async updateReply(planId, commentId, replyId, body) {
1534
- return this.mutate(planId, (plan) => {
1535
- const c = plan.comments.find((x) => x.id === commentId);
1536
- if (!c) return false;
1537
- const reply = c.replies.find((r) => r.id === replyId);
1538
- if (!reply) return false;
1539
- reply.body = body;
1540
- return true;
1541
- });
1542
- }
1543
- };
1544
- //#endregion
1545
- //#region src/lib/state.ts
1546
- var FileUiStateStore = class {
1547
- dirPath;
1548
- filePath;
1549
- constructor(storageDir) {
1550
- this.dirPath = storageDir ?? getProjectStorageDir();
1551
- this.filePath = join(this.dirPath, "ui-state.json");
1552
- }
1553
- async getAll() {
1554
- try {
1555
- const data = await readFile(this.filePath, "utf-8");
1556
- return JSON.parse(data);
1557
- } catch {
1558
- return {};
1559
- }
1560
- }
1561
- async setAll(state) {
1562
- await this.save(state);
1563
- }
1564
- async save(state) {
1565
- try {
1566
- await mkdir(this.dirPath, { recursive: true });
1567
- try {
1568
- const repoRoot = getRepoRoot();
1569
- await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1570
- } catch {}
1571
- await writeFile(this.filePath, JSON.stringify(state, null, 2), "utf-8");
1572
- } catch (err) {
1573
- console.error("Failed to save UI state to file:", err);
1574
- }
1575
- }
1576
- };
1577
- //#endregion
1578
- //#region src/lib/editor-launcher.ts
1579
- function resolveEditorCommand(editor, absolutePath, platform = process.platform, env = process.env) {
1580
- switch (editor) {
1581
- case "vscode": return guiEditorCommand("code", absolutePath, platform);
1582
- case "zed": return guiEditorCommand("zed", absolutePath, platform);
1583
- case "vim": return terminalEditorCommand("vim", absolutePath, platform, env);
1584
- case "neovim": return terminalEditorCommand("nvim", absolutePath, platform, env);
1585
- default: return null;
1586
- }
1587
- }
1588
- function guiEditorCommand(bin, absolutePath, platform) {
1589
- if (platform === "win32") return {
1590
- cmd: `${bin}.cmd`,
1591
- args: [absolutePath]
1592
- };
1593
- return {
1594
- cmd: bin,
1595
- args: [absolutePath]
1596
- };
1597
- }
1598
- function terminalEditorCommand(bin, absolutePath, platform, env) {
1599
- if (platform === "darwin") return {
1600
- cmd: "osascript",
1601
- args: ["-e", `tell application "Terminal" to do script "${bin} \\"${absolutePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}\\""`]
1602
- };
1603
- if (platform === "win32") return {
1604
- cmd: "cmd.exe",
1605
- args: [
1606
- "/c",
1607
- "start",
1608
- "\"\"",
1609
- "cmd.exe",
1610
- "/k",
1611
- bin,
1612
- absolutePath
1613
- ]
1614
- };
1615
- const term = env.TERMINAL?.trim();
1616
- if (term) return {
1617
- cmd: term,
1618
- args: [
1619
- "-e",
1620
- bin,
1621
- absolutePath
1622
- ]
1623
- };
1624
- return {
1625
- cmd: "x-terminal-emulator",
1626
- args: [
1627
- "-e",
1628
- bin,
1629
- absolutePath
1630
- ]
1631
- };
1632
- }
1633
- //#endregion
1634
- //#region src/lib/review-session.ts
1635
- const MAX_ROUND_HISTORY = 20;
1636
- var ReviewSession = class {
1637
- round = 0;
1638
- lastSentAt = null;
1639
- lastPayload = null;
1640
- history = [];
1641
- waiters = /* @__PURE__ */ new Set();
1642
- onStatusChange;
1643
- constructor(onStatusChange) {
1644
- this.onStatusChange = onStatusChange;
1645
- }
1646
- /**
1647
- * Block until the next `send()` or until `timeoutMs` elapses.
1648
- *
1649
- * If `sinceRound` is provided and is behind the current round, a send already
1650
- * happened since the client last checked → resolve immediately with the
1651
- * cached payload (the race guard). Otherwise register a waiter.
1652
- */
1653
- await(opts) {
1654
- const { sinceRound, timeoutMs, signal } = opts;
1655
- if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
1656
- status: "released",
1657
- payload: this.lastPayload
1658
- });
1659
- if (signal?.aborted) return Promise.resolve({
1660
- status: "keep-waiting",
1661
- round: this.round
1662
- });
1663
- return new Promise((resolve) => {
1664
- const waiter = {
1665
- resolve,
1666
- timer: setTimeout(() => {
1667
- this.removeWaiter(waiter);
1668
- resolve({
1669
- status: "keep-waiting",
1670
- round: this.round
1671
- });
1672
- }, timeoutMs),
1673
- cleanup: () => {}
1674
- };
1675
- if (signal) {
1676
- const onAbort = () => {
1677
- this.removeWaiter(waiter);
1678
- resolve({
1679
- status: "keep-waiting",
1680
- round: this.round
1681
- });
1682
- };
1683
- signal.addEventListener("abort", onAbort, { once: true });
1684
- waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
1685
- }
1686
- this.waiters.add(waiter);
1687
- this.emitStatus();
1688
- });
1689
- }
1690
- /** Capture a review batch and release every blocked waiter. */
1691
- send(input) {
1692
- this.round += 1;
1693
- this.lastSentAt = input.sentAt;
1694
- const mode = input.mode ?? "standard";
1695
- const payload = {
1696
- round: this.round,
1697
- sentAt: input.sentAt,
1698
- commentXml: input.commentXml,
1699
- openCount: input.openCount,
1700
- comments: input.comments,
1701
- decision: input.decision,
1702
- mode
1703
- };
1704
- this.lastPayload = payload;
1705
- const filePaths = [...new Set(input.comments.map((c) => c.filePath))].sort();
1706
- this.history.push({
1707
- round: this.round,
1708
- sentAt: input.sentAt,
1709
- openCount: input.openCount,
1710
- decision: input.decision,
1711
- mode,
1712
- filePaths,
1713
- diffFingerprints: input.diffFingerprints ? { ...input.diffFingerprints } : void 0
1714
- });
1715
- if (this.history.length > MAX_ROUND_HISTORY) this.history.splice(0, this.history.length - MAX_ROUND_HISTORY);
1716
- const waiters = [...this.waiters];
1717
- this.waiters.clear();
1718
- for (const waiter of waiters) {
1719
- clearTimeout(waiter.timer);
1720
- waiter.cleanup();
1721
- waiter.resolve({
1722
- status: "released",
1723
- payload
1724
- });
1725
- }
1726
- this.emitStatus();
1727
- return payload;
1728
- }
1729
- snapshot() {
1730
- const last = this.history[this.history.length - 1];
1731
- return {
1732
- round: this.round,
1733
- waiters: this.waiters.size,
1734
- lastSentAt: this.lastSentAt,
1735
- lastDecision: last?.decision,
1736
- lastOpenCount: last?.openCount,
1737
- hasSinceLastBaseline: Boolean(last?.diffFingerprints && Object.keys(last.diffFingerprints).length > 0)
1738
- };
1739
- }
1740
- /** Newest-first round summaries for the UI timeline. */
1741
- getHistory() {
1742
- return [...this.history].reverse();
1743
- }
1744
- /** Fingerprints captured at the most recent send, if any. */
1745
- getLastDiffFingerprints() {
1746
- const last = this.history[this.history.length - 1];
1747
- if (!last?.diffFingerprints) return null;
1748
- return { ...last.diffFingerprints };
1749
- }
1750
- removeWaiter(waiter) {
1751
- if (!this.waiters.delete(waiter)) return;
1752
- clearTimeout(waiter.timer);
1753
- waiter.cleanup();
1754
- this.emitStatus();
1755
- }
1756
- emitStatus() {
1757
- this.onStatusChange?.(this.snapshot());
1758
- }
1759
- };
1760
- //#endregion
1761
- //#region src/lib/plan-review-session.ts
1762
- var PlanReviewSession = class {
1763
- round = 0;
1764
- lastDecidedAt = null;
1765
- lastPayload = null;
1766
- waiters = /* @__PURE__ */ new Set();
1767
- onStatusChange;
1768
- constructor(onStatusChange) {
1769
- this.onStatusChange = onStatusChange;
1770
- }
1771
- /** Block until the next `decide()` or until `timeoutMs` elapses. */
1772
- await(opts) {
1773
- const { sinceRound, timeoutMs, signal } = opts;
1774
- if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
1775
- status: "released",
1776
- payload: this.lastPayload
1777
- });
1778
- if (signal?.aborted) return Promise.resolve({
1779
- status: "keep-waiting",
1780
- round: this.round
1781
- });
1782
- return new Promise((resolve) => {
1783
- const waiter = {
1784
- resolve,
1785
- timer: setTimeout(() => {
1786
- this.removeWaiter(waiter);
1787
- resolve({
1788
- status: "keep-waiting",
1789
- round: this.round
1790
- });
1791
- }, timeoutMs),
1792
- cleanup: () => {}
1793
- };
1794
- if (signal) {
1795
- const onAbort = () => {
1796
- this.removeWaiter(waiter);
1797
- resolve({
1798
- status: "keep-waiting",
1799
- round: this.round
1800
- });
1801
- };
1802
- signal.addEventListener("abort", onAbort, { once: true });
1803
- waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
1804
- }
1805
- this.waiters.add(waiter);
1806
- this.emitStatus();
1807
- });
1808
- }
1809
- /** Capture a decision and release every blocked waiter. */
1810
- decide(input) {
1811
- this.round += 1;
1812
- this.lastDecidedAt = input.sentAt;
1813
- const payload = {
1814
- round: this.round,
1815
- sentAt: input.sentAt,
1816
- planId: input.planId,
1817
- decision: input.decision,
1818
- decisionComment: input.decisionComment,
1819
- reviewXml: input.reviewXml,
1820
- openCommentCount: input.openCommentCount,
1821
- plan: input.plan,
1822
- mode: input.mode ?? "standard"
1823
- };
1824
- this.lastPayload = payload;
1825
- const waiters = [...this.waiters];
1826
- this.waiters.clear();
1827
- for (const waiter of waiters) {
1828
- clearTimeout(waiter.timer);
1829
- waiter.cleanup();
1830
- waiter.resolve({
1831
- status: "released",
1832
- payload
1833
- });
1834
- }
1835
- this.emitStatus();
1836
- return payload;
1837
- }
1838
- snapshot() {
1839
- return {
1840
- round: this.round,
1841
- waiters: this.waiters.size,
1842
- lastDecidedAt: this.lastDecidedAt
1843
- };
1844
- }
1845
- removeWaiter(waiter) {
1846
- if (!this.waiters.delete(waiter)) return;
1847
- clearTimeout(waiter.timer);
1848
- waiter.cleanup();
1849
- this.emitStatus();
1850
- }
1851
- emitStatus() {
1852
- this.onStatusChange?.(this.snapshot());
1853
- }
1854
- };
1855
- //#endregion
1856
- //#region src/lib/secrets-scan.ts
1857
- const RULES = [
1858
- {
1859
- id: "aws-access-key",
1860
- re: /\b(AKIA[0-9A-Z]{16})\b/g,
1861
- redact: (m) => `${m[0].slice(0, 4)}…${m[0].slice(-4)}`
1862
- },
1863
- {
1864
- id: "github-token",
1865
- re: /\b(gh[pousr]_[A-Za-z0-9_]{20,})\b/g,
1866
- redact: (m) => `${m[0].slice(0, 7)}…`
1867
- },
1868
- {
1869
- id: "slack-token",
1870
- re: /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g,
1871
- redact: (m) => `${m[0].slice(0, 8)}…`
1872
- },
1873
- {
1874
- id: "private-key",
1875
- re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
1876
- redact: () => "-----BEGIN PRIVATE KEY-----"
1877
- },
1878
- {
1879
- id: "generic-api-key",
1880
- re: /\b(?:api[_-]?key|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"]?([A-Za-z0-9_\-]{16,})['"]?/gi,
1881
- redact: (m) => `…${(m[1] ?? "").slice(-4)}`
1882
- },
1883
- {
1884
- id: "jwt",
1885
- re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
1886
- redact: (m) => `${m[0].slice(0, 12)}…`
1887
- },
1888
- {
1889
- id: "connection-string",
1890
- re: /\b(?:postgres|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s'"]+/gi,
1891
- redact: (m) => {
1892
- try {
1893
- const u = new URL(m[0]);
1894
- if (u.password) u.password = "***";
1895
- return u.toString();
1896
- } catch {
1897
- return m[0].replace(/:([^:@/]+)@/, ":***@");
1898
- }
1899
- }
1900
- }
1901
- ];
1902
- function scanTextForSecrets(text, source) {
1903
- if (!text) return [];
1904
- const findings = [];
1905
- for (const rule of RULES) {
1906
- rule.re.lastIndex = 0;
1907
- let match;
1908
- while ((match = rule.re.exec(text)) !== null) {
1909
- findings.push({
1910
- rule: rule.id,
1911
- snippet: rule.redact(match),
1912
- source
1913
- });
1914
- if (match.index === rule.re.lastIndex) rule.re.lastIndex++;
1915
- }
1916
- }
1917
- return findings;
1918
- }
1919
- function scanReviewForSecrets(input) {
1920
- const out = [];
1921
- if (input.generalComment) out.push(...scanTextForSecrets(input.generalComment, "overall comment"));
1922
- for (const c of input.comments) {
1923
- out.push(...scanTextForSecrets(c.body, `comment ${c.id} (${c.filePath})`));
1924
- if (c.lineContent) out.push(...scanTextForSecrets(c.lineContent, `code at ${c.filePath}`));
1925
- }
1926
- const seen = /* @__PURE__ */ new Set();
1927
- return out.filter((f) => {
1928
- const k = `${f.rule}|${f.snippet}|${f.source}`;
1929
- if (seen.has(k)) return false;
1930
- seen.add(k);
1931
- return true;
1932
- });
1933
- }
1934
- //#endregion
1935
- //#region src/lib/pr-session.ts
1936
- var FilePrSessionStore = class {
1937
- dirPath;
1938
- filePath;
1939
- constructor(storageDir) {
1940
- this.dirPath = storageDir ?? getProjectStorageDir();
1941
- this.filePath = join(this.dirPath, "pr-session.json");
1942
- }
1943
- async get() {
1944
- try {
1945
- const data = await readFile(this.filePath, "utf-8");
1946
- return JSON.parse(data);
1947
- } catch {
1948
- return null;
1949
- }
1950
- }
1951
- async save(session) {
1952
- try {
1953
- await mkdir(this.dirPath, { recursive: true });
1954
- await writeFile(this.filePath, JSON.stringify(session, null, 2), "utf-8");
1955
- } catch (err) {
1956
- console.error("Failed to save pr-session to file:", err);
1957
- }
1958
- }
1959
- async set(session) {
1960
- await this.save(session);
1961
- }
1962
- async update(fields) {
1963
- const current = await this.get();
1964
- if (!current) return null;
1965
- const next = {
1966
- ...current,
1967
- ...fields
1968
- };
1969
- await this.save(next);
1970
- return next;
1971
- }
1972
- async clear() {
1973
- try {
1974
- const { unlink } = await import("node:fs/promises");
1975
- await unlink(this.filePath);
1976
- } catch {}
1977
- }
1978
- };
1979
- //#endregion
1980
- //#region src/server.ts
1981
- const MIME_TYPES = {
1982
- ".html": "text/html",
1983
- ".js": "application/javascript",
1984
- ".css": "text/css",
1985
- ".json": "application/json",
1986
- ".svg": "image/svg+xml",
1987
- ".png": "image/png",
1988
- ".ico": "image/x-icon",
1989
- ".jpg": "image/jpeg",
1990
- ".jpeg": "image/jpeg",
1991
- ".gif": "image/gif",
1992
- ".webp": "image/webp",
1993
- ".bmp": "image/bmp",
1994
- ".avif": "image/avif"
1995
- };
1996
- function isCustomMode(opts) {
1997
- return opts.revisions.length > 0 || opts.pathspecs.length > 0 || opts.showMode;
1998
- }
1999
- function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore, prSessionStore, prMode = false) {
2000
- const app = new Hono();
2001
- let diffOpts = { ...diffOptsInput };
2002
- const customMode = isCustomMode(diffOpts);
2003
- const store = commentStore ?? new FileCommentStore();
2004
- const plans = planStore ?? new FilePlanStore();
2005
- const prStore = prSessionStore ?? new FilePrSessionStore();
2006
- const uiStateStore = new FileUiStateStore();
2007
- const viewedFiles = /* @__PURE__ */ new Set();
2008
- /** Agent-reported progress events for the human UI (SSE `agent-progress`). */
2009
- let lastAgentProgress = null;
2010
- /** Waiters registered with optional identity for multi-agent display. */
2011
- const agentWaiters = /* @__PURE__ */ new Map();
2012
- const activeClients = /* @__PURE__ */ new Set();
2013
- const broadcast = (event, data) => {
2014
- for (const send of activeClients) try {
2015
- send(event, data);
2016
- } catch {}
2017
- };
2018
- const agentsSnapshot = () => [...agentWaiters.entries()].map(([id, a]) => ({
2019
- id,
2020
- ...a
2021
- }));
2022
- const reviewSession = new ReviewSession((snapshot) => broadcast("agent-status", JSON.stringify({
2023
- ...snapshot,
2024
- agents: agentsSnapshot()
2025
- })));
2026
- const planReviewSession = new PlanReviewSession((snapshot) => broadcast("plan-review-status", JSON.stringify(snapshot)));
2027
- let repoRoot;
2028
- try {
2029
- repoRoot = getRepoRoot();
2030
- } catch {
2031
- repoRoot = process.cwd();
2032
- }
2033
- if (!commentStore && !planStore) try {
2034
- const storageDir = getProjectStorageDir();
2035
- mkdirSync(storageDir, { recursive: true });
2036
- let commentsDebounce = null;
2037
- let plansDebounce = null;
2038
- let prSessionDebounce = null;
2039
- watch(storageDir, (_eventType, filename) => {
2040
- if (!filename) return;
2041
- if (filename.startsWith("comments.json")) {
2042
- if (commentsDebounce) clearTimeout(commentsDebounce);
2043
- commentsDebounce = setTimeout(() => broadcast("comments", Date.now().toString()), 120);
2044
- } else if (filename.startsWith("plans.json")) {
2045
- if (plansDebounce) clearTimeout(plansDebounce);
2046
- plansDebounce = setTimeout(() => broadcast("plans", Date.now().toString()), 120);
2047
- } else if (filename.startsWith("pr-session.json")) {
2048
- if (prSessionDebounce) clearTimeout(prSessionDebounce);
2049
- prSessionDebounce = setTimeout(() => broadcast("pr-session", Date.now().toString()), 120);
2050
- }
2051
- }).unref();
2052
- } catch (err) {
2053
- console.warn("Failed to initialize storage watcher:", err);
2054
- }
2055
- let debounceTimeout = null;
2056
- try {
2057
- watch(repoRoot, { recursive: true }, (eventType, filename) => {
2058
- if (!filename) return;
2059
- const parts = filename.split(/[/\\]/);
2060
- const isGit = parts.includes(".git");
2061
- const isNodeModules = parts.includes("node_modules");
2062
- const isDist = parts.includes("dist");
2063
- const isChangeset = parts.includes(".changeset");
2064
- let shouldTrigger = false;
2065
- if (isGit) {
2066
- const isIndex = filename.endsWith("index");
2067
- const isHead = filename.endsWith("HEAD");
2068
- const isRefs = filename.includes("refs/") || filename.includes("refs\\");
2069
- if (isIndex || isHead || isRefs) shouldTrigger = true;
2070
- } else if (!isNodeModules && !isDist && !isChangeset) shouldTrigger = true;
2071
- if (shouldTrigger) {
2072
- if (debounceTimeout) clearTimeout(debounceTimeout);
2073
- debounceTimeout = setTimeout(() => broadcast("change", Date.now().toString()), 200);
2074
- }
2075
- }).unref();
2076
- } catch (err) {
2077
- console.warn("Failed to initialize repository watcher:", err);
2078
- }
2079
- app.get("/api/live", async (c) => {
2080
- return streamSSE(c, async (stream) => {
2081
- const sendUpdate = (event, data) => {
2082
- stream.writeSSE({
2083
- event,
2084
- data
2085
- });
2086
- };
2087
- activeClients.add(sendUpdate);
2088
- await stream.writeSSE({
2089
- event: "heartbeat",
2090
- data: Date.now().toString()
2091
- });
2092
- const heartbeatInterval = setInterval(() => {
2093
- stream.writeSSE({
2094
- event: "heartbeat",
2095
- data: Date.now().toString()
2096
- }).catch(() => {});
2097
- }, 15e3);
2098
- await new Promise((resolve) => {
2099
- stream.onAbort(() => {
2100
- clearInterval(heartbeatInterval);
2101
- activeClients.delete(sendUpdate);
2102
- resolve();
2103
- });
2104
- });
2105
- });
2106
- });
2107
- app.get("/api/diff", async (c) => {
2108
- const stagedQuery = c.req.query("staged");
2109
- const untrackedQuery = c.req.query("untracked");
2110
- const staged = stagedQuery === void 0 ? diffOpts.staged : stagedQuery === "true";
2111
- const untracked = untrackedQuery === void 0 ? diffOpts.includeUntracked : untrackedQuery === "true";
2112
- if (prMode) {
2113
- const prSession = await prStore.get();
2114
- if (prSession) {
2115
- const binaryFiles = [];
2116
- const filePaths = [];
2117
- for (const line of prSession.diff.split("\n")) {
2118
- const m = /^diff --git a\/.+ b\/(.+)$/.exec(line);
2119
- if (m) filePaths.push(m[1]);
2120
- }
2121
- const prOverview = buildPrOverview({
2122
- prNumber: prSession.pullNumber,
2123
- prTitle: prSession.title,
2124
- prAuthor: prSession.author?.login ?? null,
2125
- additions: prSession.additions,
2126
- deletions: prSession.deletions
2127
- });
2128
- return c.json({
2129
- patch: prSession.diff,
2130
- repoName: prSession.repo,
2131
- branch: `#${prSession.pullNumber}`,
2132
- customMode: true,
2133
- binaryFiles,
2134
- tabSizeMap: {},
2135
- untrackedFiles: [],
2136
- prMode: true,
2137
- prRef: prSession.ref,
2138
- prOwner: prSession.owner,
2139
- prRepo: prSession.repo,
2140
- prPullNumber: prSession.pullNumber,
2141
- prUrl: prSession.url,
2142
- prTitle: prSession.title,
2143
- prAuthor: prSession.author,
2144
- prHeadSha: prSession.headSha,
2145
- prBaseSha: prSession.baseSha,
2146
- overview: prOverview
2147
- });
2148
- }
2149
- }
2150
- const optsForDiff = customMode ? diffOpts : {
2151
- ...diffOpts,
2152
- staged,
2153
- includeUntracked: untracked
2154
- };
2155
- const result = await executeDiffWithMeta(optsForDiff);
2156
- return c.json({
2157
- patch: result.patch,
2158
- repoName: result.repoName,
2159
- branch: result.branch,
2160
- customMode,
2161
- binaryFiles: result.binaryFiles,
2162
- tabSizeMap: result.tabSizeMap,
2163
- untrackedFiles: result.untrackedFiles,
2164
- showMode: optsForDiff.showMode || void 0,
2165
- commits: result.commits,
2166
- truncated: result.truncated,
2167
- overview: result.overview
2168
- });
2169
- });
2170
- app.get("/api/file-content", (c) => {
2171
- const path = c.req.query("path");
2172
- const version = c.req.query("version");
2173
- if (!path || !version) return c.json({ error: "Missing path or version" }, 400);
2174
- const content = getFileContent(path, version);
2175
- if (!content) return c.json({ error: "File not found" }, 404);
2176
- const contentType = MIME_TYPES[extname(path)] || "application/octet-stream";
2177
- return new Response(new Uint8Array(content), { headers: { "Content-Type": contentType } });
2178
- });
2179
- app.get("/api/file-text", (c) => {
2180
- const path = c.req.query("path");
2181
- const version = c.req.query("version");
2182
- if (!path || !version) return c.json({ error: "Missing path or version" }, 400);
2183
- const buffer = getFileContent(path, version);
2184
- if (!buffer) return c.json({
2185
- content: "",
2186
- missing: true
2187
- });
2188
- const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
2189
- for (let i = 0; i < sample.length; i++) if (sample[i] === 0) return c.json({ error: "Binary file" }, 415);
2190
- return c.json({
2191
- content: buffer.toString("utf-8"),
2192
- missing: false
2193
- });
2194
- });
2195
- app.post("/api/open-file", async (c) => {
2196
- const { filePath, editor } = await c.req.json();
2197
- if (!filePath) return c.json({ error: "filePath is required" }, 400);
2198
- try {
2199
- const root = getRepoRoot();
2200
- let absolutePath = null;
2201
- const relPath = toSafeRelativePath(filePath, root);
2202
- if (relPath) absolutePath = resolve(root, relPath);
2203
- else {
2204
- const resolved = resolve(filePath);
2205
- const diffingHome = resolve(homedir(), ".diffing");
2206
- if ((resolved === diffingHome || resolved.startsWith(diffingHome + "/") || resolved.startsWith(diffingHome + "\\")) && existsSync(resolved)) absolutePath = resolved;
2207
- }
2208
- if (!absolutePath) return c.json({ error: "Forbidden file path" }, 403);
2209
- if (editor && editor !== "default") {
2210
- const command = resolveEditorCommand(editor, absolutePath);
2211
- if (command) {
2212
- const { execFile } = await import("node:child_process");
2213
- execFile(command.cmd, command.args, (err) => {
2214
- if (err) console.error(`Failed to launch ${editor} for ${absolutePath}: ${err.message}`);
2215
- });
2216
- return c.json({ ok: true });
2217
- }
2218
- }
2219
- await (await import("open")).default(absolutePath);
2220
- return c.json({ ok: true });
2221
- } catch (err) {
2222
- return c.json({ error: `Failed to open file: ${err.message}` }, 500);
2223
- }
2224
- });
2225
- app.get("/api/repo-files", (c) => {
2226
- try {
2227
- return c.json({ files: listRepoFiles() });
2228
- } catch (err) {
2229
- return c.json({ error: err.message }, 500);
2230
- }
2231
- });
2232
- app.post("/api/search", async (c) => {
2233
- const body = await c.req.json().catch(() => ({}));
2234
- const scope = body.scope ?? "files";
2235
- const query = typeof body.query === "string" ? body.query : "";
2236
- const limit = typeof body.limit === "number" ? body.limit : void 0;
2237
- const paths = Array.isArray(body.changedPaths) ? body.changedPaths : void 0;
2238
- try {
2239
- if (scope === "all") return c.json(await searchAll(query, {
2240
- limit,
2241
- regex: !!body.regex,
2242
- paths
2243
- }));
2244
- if (scope === "text") return c.json(await searchContent(query, {
2245
- limit,
2246
- regex: !!body.regex,
2247
- paths
2248
- }));
2249
- if (scope === "symbols") return c.json(await searchSymbols(query, {
2250
- limit,
2251
- paths
2252
- }));
2253
- return c.json(await searchFiles(query, {
2254
- limit,
2255
- paths
2256
- }));
2257
- } catch (err) {
2258
- return c.json({
2259
- scope,
2260
- items: [],
2261
- total: 0,
2262
- indexing: false,
2263
- error: err?.message ?? "Search failed"
2264
- }, 500);
2265
- }
2266
- });
2267
- app.get("/api/search/status", async (c) => {
2268
- return c.json(await getSearchStatus());
2269
- });
2270
- app.post("/api/search/track", async (c) => {
2271
- const { query, path } = await c.req.json().catch(() => ({}));
2272
- if (path) await trackSelection(query ?? "", path);
2273
- return c.json({ ok: true });
2274
- });
2275
- app.get("/api/merge-status", (c) => {
2276
- try {
2277
- const status = getMergeStatus();
2278
- return c.json(status);
2279
- } catch (err) {
2280
- return c.json({ error: err.message }, 500);
2281
- }
2282
- });
2283
- app.post("/api/revert-hunk", async (c) => {
2284
- const { filePath, hunkIndex } = await c.req.json();
2285
- if (!filePath || typeof hunkIndex !== "number") return c.json({ error: "Missing filePath or hunkIndex" }, 400);
2286
- try {
2287
- const relPath = toSafeRelativePath(filePath, getRepoRoot());
2288
- if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2289
- revertHunk(relPath, hunkIndex);
2290
- return c.json({ ok: true });
2291
- } catch (err) {
2292
- const stderr = typeof err?.stderr === "string" ? err.stderr : err?.stderr instanceof Buffer ? err.stderr.toString("utf-8") : "";
2293
- return c.json({ error: stderr || err?.message || "Failed to revert hunk" }, 500);
2294
- }
2295
- });
2296
- /**
2297
- * Pick a single tree-ish suitable for `git blame` from a list of revisions
2298
- * or show-revspecs. `git blame` does not accept range notation, so for
2299
- * `A..B` or `A...B` we use the right endpoint (the side being compared
2300
- * against), which matches the diff's "destination" side.
2301
- */
2302
- function extractBlameRevision(revisions, showRevspecs, showMode) {
2303
- const list = showMode ? showRevspecs : revisions;
2304
- if (list.length === 0) return "HEAD";
2305
- const last = list[list.length - 1];
2306
- const threeDot = last.lastIndexOf("...");
2307
- if (threeDot >= 0) {
2308
- const target = last.slice(threeDot + 3);
2309
- if (target) return target;
2310
- }
2311
- const twoDot = last.lastIndexOf("..");
2312
- if (twoDot >= 0) {
2313
- const target = last.slice(twoDot + 2);
2314
- if (target) return target;
2315
- }
2316
- return last;
2317
- }
2318
- app.get("/api/hunk-history", async (c) => {
2319
- const filePath = c.req.query("filePath");
2320
- const deletionStart = Number(c.req.query("deletionStart"));
2321
- const deletionCount = Number(c.req.query("deletionCount"));
2322
- if (!filePath || Number.isNaN(deletionStart) || Number.isNaN(deletionCount)) return c.json({ error: "Missing or invalid parameters" }, 400);
2323
- try {
2324
- const relPath = toSafeRelativePath(filePath, getRepoRoot());
2325
- if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2326
- const history = getHunkHistory(relPath, deletionStart, deletionCount, extractBlameRevision(diffOpts.revisions, diffOpts.showRevspecs, diffOpts.showMode));
2327
- return c.json(history);
2328
- } catch (err) {
2329
- return c.json({ error: err.message || "Failed to fetch hunk history" }, 500);
2330
- }
2331
- });
2332
- app.post("/api/save-file", async (c) => {
2333
- const { filePath, content, gitAdd } = await c.req.json();
2334
- if (!filePath || typeof content !== "string") return c.json({ error: "Missing filePath or content" }, 400);
2335
- try {
2336
- const root = getRepoRoot();
2337
- const relPath = toSafeRelativePath(filePath, root);
2338
- if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2339
- await writeFile(resolve(root, relPath), content, "utf-8");
2340
- if (gitAdd) try {
2341
- gitAddFile(relPath);
2342
- } catch (err) {
2343
- return c.json({
2344
- ok: true,
2345
- gitAddError: err.message
2346
- });
2347
- }
2348
- return c.json({ ok: true });
2349
- } catch (err) {
2350
- return c.json({ error: `Failed to save file: ${err.message}` }, 500);
2351
- }
2352
- });
2353
- app.get("/api/settings", (c) => {
2354
- return c.json(loadSettings());
2355
- });
2356
- app.put("/api/settings", async (c) => {
2357
- const body = await c.req.json();
2358
- const settings = saveSettings(body);
2359
- let whitespaceChanged = false;
2360
- if (typeof body.ignoreSpaceChange === "boolean" && body.ignoreSpaceChange !== diffOpts.ignoreSpaceChange) {
2361
- diffOpts = {
2362
- ...diffOpts,
2363
- ignoreSpaceChange: body.ignoreSpaceChange
2364
- };
2365
- whitespaceChanged = true;
2366
- }
2367
- if (typeof body.ignoreAllSpace === "boolean" && body.ignoreAllSpace !== diffOpts.ignoreAllSpace) {
2368
- diffOpts = {
2369
- ...diffOpts,
2370
- ignoreAllSpace: body.ignoreAllSpace
2371
- };
2372
- whitespaceChanged = true;
2373
- }
2374
- if (whitespaceChanged) broadcast("change", Date.now().toString());
2375
- return c.json(settings);
2376
- });
2377
- /** Patch live diff options (subset). Broadcasts `change` so clients refetch. */
2378
- app.put("/api/diff-options", async (c) => {
2379
- const body = await c.req.json().catch(() => ({}));
2380
- const next = { ...diffOpts };
2381
- if (typeof body.ignoreSpaceChange === "boolean") next.ignoreSpaceChange = body.ignoreSpaceChange;
2382
- if (typeof body.ignoreAllSpace === "boolean") next.ignoreAllSpace = body.ignoreAllSpace;
2383
- if (typeof body.ignoreBlankLines === "boolean") next.ignoreBlankLines = body.ignoreBlankLines;
2384
- diffOpts = next;
2385
- const settingsPatch = {};
2386
- if (typeof body.ignoreSpaceChange === "boolean") settingsPatch.ignoreSpaceChange = body.ignoreSpaceChange;
2387
- if (typeof body.ignoreAllSpace === "boolean") settingsPatch.ignoreAllSpace = body.ignoreAllSpace;
2388
- if (Object.keys(settingsPatch).length) saveSettings(settingsPatch);
2389
- broadcast("change", Date.now().toString());
2390
- return c.json({
2391
- ignoreSpaceChange: diffOpts.ignoreSpaceChange,
2392
- ignoreAllSpace: diffOpts.ignoreAllSpace,
2393
- ignoreBlankLines: diffOpts.ignoreBlankLines
2394
- });
2395
- });
2396
- app.get("/api/diff-options", (c) => {
2397
- return c.json({
2398
- ignoreSpaceChange: diffOpts.ignoreSpaceChange,
2399
- ignoreAllSpace: diffOpts.ignoreAllSpace,
2400
- ignoreBlankLines: diffOpts.ignoreBlankLines
2401
- });
2402
- });
2403
- app.post("/api/agent/progress", async (c) => {
2404
- const body = await c.req.json().catch(() => ({}));
2405
- const message = typeof body.message === "string" ? body.message.trim() : "";
2406
- if (!message) return c.json({ error: "message is required" }, 400);
2407
- const payload = {
2408
- at: Date.now(),
2409
- message,
2410
- model: typeof body.model === "string" ? body.model : void 0,
2411
- agentId: typeof body.agentId === "string" ? body.agentId : void 0,
2412
- commentId: typeof body.commentId === "string" ? body.commentId : void 0,
2413
- pct: typeof body.pct === "number" && body.pct >= 0 && body.pct <= 100 ? body.pct : void 0
2414
- };
2415
- lastAgentProgress = payload;
2416
- broadcast("agent-progress", JSON.stringify(payload));
2417
- return c.json({
2418
- ok: true,
2419
- ...payload
2420
- });
2421
- });
2422
- app.get("/api/agent/progress", (c) => {
2423
- return c.json({ progress: lastAgentProgress });
2424
- });
2425
- app.post("/api/agent/register", async (c) => {
2426
- const body = await c.req.json().catch(() => ({}));
2427
- const agentId = typeof body.agentId === "string" && body.agentId || crypto.randomUUID();
2428
- const model = typeof body.model === "string" ? body.model : void 0;
2429
- const label = typeof body.label === "string" ? body.label : void 0;
2430
- agentWaiters.set(agentId, {
2431
- model,
2432
- label,
2433
- connectedAt: Date.now()
2434
- });
2435
- broadcast("agent-status", JSON.stringify({
2436
- ...reviewSession.snapshot(),
2437
- agents: [...agentWaiters.entries()].map(([id, a]) => ({
2438
- id,
2439
- ...a
2440
- }))
2441
- }));
2442
- return c.json({
2443
- ok: true,
2444
- agentId
2445
- });
2446
- });
2447
- app.delete("/api/agent/register/:id", async (c) => {
2448
- const id = c.req.param("id");
2449
- agentWaiters.delete(id);
2450
- broadcast("agent-status", JSON.stringify({
2451
- ...reviewSession.snapshot(),
2452
- agents: [...agentWaiters.entries()].map(([aid, a]) => ({
2453
- id: aid,
2454
- ...a
2455
- }))
2456
- }));
2457
- return c.json({ ok: true });
2458
- });
2459
- app.get("/api/ui-state", async (c) => {
2460
- return c.json(await uiStateStore.getAll());
2461
- });
2462
- app.put("/api/ui-state", async (c) => {
2463
- const body = await c.req.json();
2464
- const merged = {
2465
- ...await uiStateStore.getAll(),
2466
- ...body
2467
- };
2468
- for (const key of Object.keys(body)) if (body[key] === null || body[key] === void 0) delete merged[key];
2469
- await uiStateStore.setAll(merged);
2470
- return c.json(merged);
2471
- });
2472
- app.get("/api/viewed", (c) => {
2473
- return c.json([...viewedFiles]);
2474
- });
2475
- app.put("/api/viewed", async (c) => {
2476
- const { filePath, viewed } = await c.req.json();
2477
- if (viewed) viewedFiles.add(filePath);
2478
- else viewedFiles.delete(filePath);
2479
- broadcast("viewed", JSON.stringify([...viewedFiles]));
2480
- return c.json({ ok: true });
2481
- });
2482
- app.get("/api/comments", async (c) => {
2483
- const comments = await store.getAll();
2484
- return c.json(comments);
2485
- });
2486
- app.post("/api/comments/resolve-all", async (c) => {
2487
- const resolved = await store.resolveAllOpen();
2488
- return c.json({
2489
- ok: true,
2490
- resolved
2491
- });
2492
- });
2493
- app.post("/api/comments", async (c) => {
2494
- const body = await c.req.json();
2495
- const severityRaw = body.severity;
2496
- const severity = severityRaw === "blocking" || severityRaw === "nit" || severityRaw === "question" || severityRaw === "praise" || severityRaw === "none" ? severityRaw : void 0;
2497
- const comment = {
2498
- id: crypto.randomUUID(),
2499
- filePath: body.filePath,
2500
- side: body.side,
2501
- lineNumber: body.lineNumber,
2502
- startLineNumber: body.startLineNumber,
2503
- lineContent: body.lineContent,
2504
- body: body.body,
2505
- status: "open",
2506
- createdAt: Date.now(),
2507
- replies: [],
2508
- ...severity ? { severity } : {}
2509
- };
2510
- const created = await store.add(comment);
2511
- return c.json(created, 201);
2512
- });
2513
- app.put("/api/comments/:id", async (c) => {
2514
- const id = c.req.param("id");
2515
- const { body, status } = await c.req.json();
2516
- const updated = await store.update(id, {
2517
- body,
2518
- status
2519
- });
2520
- if (!updated) return c.json({ error: "Comment not found" }, 404);
2521
- return c.json(updated);
2522
- });
2523
- app.post("/api/comments/:id/replies", async (c) => {
2524
- const commentId = c.req.param("id");
2525
- const { body, role, model } = await c.req.json();
2526
- const reply = {
2527
- id: crypto.randomUUID(),
2528
- body,
2529
- createdAt: Date.now(),
2530
- role: role || (model ? "agent" : "user"),
2531
- model: model || void 0
2532
- };
2533
- const updated = await store.addReply(commentId, reply);
2534
- if (!updated) return c.json({ error: "Comment not found" }, 404);
2535
- return c.json(updated);
2536
- });
2537
- app.delete("/api/comments/:id/replies/:replyId", async (c) => {
2538
- const commentId = c.req.param("id");
2539
- const replyId = c.req.param("replyId");
2540
- const updated = await store.removeReply(commentId, replyId);
2541
- if (!updated) return c.json({ error: "Comment or reply not found" }, 404);
2542
- return c.json(updated);
2543
- });
2544
- app.put("/api/comments/:id/replies/:replyId", async (c) => {
2545
- const commentId = c.req.param("id");
2546
- const replyId = c.req.param("replyId");
2547
- const { body } = await c.req.json();
2548
- if (!body) return c.json({ error: "Body is required" }, 400);
2549
- const updated = await store.updateReply(commentId, replyId, body);
2550
- if (!updated) return c.json({ error: "Comment or reply not found" }, 404);
2551
- return c.json(updated);
2552
- });
2553
- app.post("/api/comments/:id/apply-suggestion", async (c) => {
2554
- const id = c.req.param("id");
2555
- const comment = (await store.getAll()).find((c) => c.id === id);
2556
- if (!comment) return c.json({ error: "Comment not found" }, 404);
2557
- try {
2558
- const root = getRepoRoot();
2559
- const relPath = toSafeRelativePath(comment.filePath, root);
2560
- if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2561
- const absolutePath = resolve(root, relPath);
2562
- const content = await readFile(absolutePath, "utf-8");
2563
- const { applySuggestionToContent } = await import("./apply-suggestion-BAuyCYk6.mjs");
2564
- const result = applySuggestionToContent({
2565
- content,
2566
- lineNumber: comment.lineNumber,
2567
- startLineNumber: comment.startLineNumber,
2568
- body: comment.body,
2569
- side: comment.side
2570
- });
2571
- if (!result.ok) return c.json({ error: result.error }, 400);
2572
- await writeFile(absolutePath, result.content, "utf-8");
2573
- await store.update(id, { status: "resolved" });
2574
- return c.json({
2575
- ok: true,
2576
- replacedLines: result.replacedLines
2577
- });
2578
- } catch (err) {
2579
- return c.json({ error: `Failed to apply suggestion: ${err.message}` }, 500);
2580
- }
2581
- });
2582
- app.delete("/api/comments/:id", async (c) => {
2583
- const id = c.req.param("id");
2584
- if (!await store.remove(id)) return c.json({ error: "Comment not found" }, 404);
2585
- return c.json({ ok: true });
2586
- });
2587
- /** Shared helper: 404 with a stable shape so the client knows "not in PR mode". */
2588
- const notInPrMode = (c) => c.json({
2589
- error: "Not in PR review mode",
2590
- prMode: false
2591
- }, 404);
2592
- app.get("/api/gh/session", async (c) => {
2593
- if (!prMode) return c.json({ prMode: false });
2594
- const session = await prStore.get();
2595
- if (!session) return c.json({ prMode: false });
2596
- return c.json({
2597
- prMode: true,
2598
- ref: session.ref,
2599
- owner: session.owner,
2600
- repo: session.repo,
2601
- pullNumber: session.pullNumber,
2602
- baseSha: session.baseSha,
2603
- headSha: session.headSha,
2604
- title: session.title,
2605
- url: session.url,
2606
- author: session.author,
2607
- additions: session.additions,
2608
- deletions: session.deletions,
2609
- changedFiles: session.changedFiles,
2610
- existingComments: session.existingComments,
2611
- submittedAt: session.submittedAt,
2612
- submittedReviewId: session.submittedReviewId,
2613
- submittedReviewUrl: session.submittedReviewUrl,
2614
- authSource: session.authSource
2615
- });
2616
- });
2617
- /** Re-fetch PR metadata (head SHA, diff, existing comments) and persist. */
2618
- app.post("/api/gh/pr/refresh", async (c) => {
2619
- if (!prMode) return notInPrMode(c);
2620
- const session = await prStore.get();
2621
- if (!session) return notInPrMode(c);
2622
- try {
2623
- const refreshed = await refreshPrSession(session);
2624
- await prStore.set(refreshed);
2625
- return c.json({
2626
- ok: true,
2627
- headSha: refreshed.headSha
2628
- });
2629
- } catch (err) {
2630
- return c.json({ error: err?.message ?? "Refresh failed" }, 500);
2631
- }
2632
- });
2633
- /** CI / check-runs status for the PR head. */
2634
- app.get("/api/gh/checks", async (c) => {
2635
- if (!prMode) return notInPrMode(c);
2636
- const session = await prStore.get();
2637
- if (!session) return notInPrMode(c);
2638
- try {
2639
- const { fetchPrChecks } = await import("./github-C9GLNak7.mjs");
2640
- const checks = await fetchPrChecks({
2641
- owner: session.owner,
2642
- repo: session.repo,
2643
- pullNumber: session.pullNumber,
2644
- ref: session.ref
2645
- }, session.headSha);
2646
- const summary = {
2647
- total: checks.length,
2648
- success: checks.filter((x) => x.state === "success").length,
2649
- failure: checks.filter((x) => x.state === "failure" || x.state === "error").length,
2650
- pending: checks.filter((x) => x.state === "pending").length
2651
- };
2652
- return c.json({
2653
- checks,
2654
- summary,
2655
- headSha: session.headSha
2656
- });
2657
- } catch (err) {
2658
- return c.json({
2659
- error: err?.message ?? "Failed to fetch checks",
2660
- checks: [],
2661
- summary: null
2662
- }, 500);
2663
- }
2664
- });
2665
- /** Reply to an existing GitHub review comment thread. */
2666
- app.post("/api/gh/existing-comments/:id/replies", async (c) => {
2667
- if (!prMode) return notInPrMode(c);
2668
- const session = await prStore.get();
2669
- if (!session) return notInPrMode(c);
2670
- const id = Number(c.req.param("id"));
2671
- if (!Number.isFinite(id)) return c.json({ error: "Invalid comment id" }, 400);
2672
- const body = await c.req.json().catch(() => ({}));
2673
- const text = typeof body.body === "string" ? body.body.trim() : "";
2674
- if (!text) return c.json({ error: "body is required" }, 400);
2675
- try {
2676
- const { replyToPrComment } = await import("./github-C9GLNak7.mjs");
2677
- const result = await replyToPrComment({
2678
- resolved: {
2679
- owner: session.owner,
2680
- repo: session.repo,
2681
- pullNumber: session.pullNumber,
2682
- ref: session.ref
2683
- },
2684
- inReplyTo: id,
2685
- body: text
2686
- });
2687
- if (!result.ok) return c.json({ error: result.error ?? "Reply failed" }, 500);
2688
- const refreshed = await refreshPrSession(session);
2689
- await prStore.set(refreshed);
2690
- return c.json({
2691
- ok: true,
2692
- id: result.id
2693
- });
2694
- } catch (err) {
2695
- return c.json({ error: err?.message ?? "Reply failed" }, 500);
2696
- }
2697
- });
2698
- /** Initialize a PR session from a ref like `1234`, `o/r#42`, or a GitHub URL. */
2699
- app.post("/api/gh/pr/init", async (c) => {
2700
- if (!prMode) return notInPrMode(c);
2701
- const body = await c.req.json().catch(() => ({}));
2702
- const ref = typeof body?.ref === "string" ? body.ref : "";
2703
- if (!ref.trim()) return c.json({ error: "ref is required" }, 400);
2704
- try {
2705
- parsePrRef(ref, await detectCwdRepo() ?? void 0);
2706
- const session = await buildPrSession(ref);
2707
- await prStore.set(session);
2708
- return c.json({
2709
- ok: true,
2710
- ref: session.ref,
2711
- owner: session.owner,
2712
- repo: session.repo,
2713
- pullNumber: session.pullNumber,
2714
- url: session.url
2715
- });
2716
- } catch (err) {
2717
- return c.json({ error: err?.message ?? "Failed to initialise PR session" }, 500);
2718
- }
2719
- });
2720
- app.get("/api/gh/pr-session/comments", async (c) => {
2721
- if (!prMode) return notInPrMode(c);
2722
- const session = await prStore.get();
2723
- if (!session) return notInPrMode(c);
2724
- return c.json(session.comments ?? []);
2725
- });
2726
- app.post("/api/gh/pr-session/comments", async (c) => {
2727
- if (!prMode) return notInPrMode(c);
2728
- const body = await c.req.json();
2729
- const session = await prStore.get();
2730
- if (!session) return notInPrMode(c);
2731
- const comment = {
2732
- id: crypto.randomUUID(),
2733
- filePath: body.filePath,
2734
- side: body.side,
2735
- lineNumber: body.lineNumber,
2736
- startLineNumber: body.startLineNumber,
2737
- lineContent: body.lineContent,
2738
- body: body.body,
2739
- status: "open",
2740
- createdAt: Date.now(),
2741
- replies: []
2742
- };
2743
- const next = {
2744
- ...session,
2745
- comments: [...session.comments ?? [], comment]
2746
- };
2747
- await prStore.set(next);
2748
- return c.json(comment, 201);
2749
- });
2750
- app.put("/api/gh/pr-session/comments/:id", async (c) => {
2751
- if (!prMode) return notInPrMode(c);
2752
- const id = c.req.param("id");
2753
- const { body, status } = await c.req.json();
2754
- const session = await prStore.get();
2755
- if (!session) return notInPrMode(c);
2756
- const comments = (session.comments ?? []).map((cm) => cm.id === id ? {
2757
- ...cm,
2758
- body: body ?? cm.body,
2759
- status: status ?? cm.status
2760
- } : cm);
2761
- await prStore.set({
2762
- ...session,
2763
- comments
2764
- });
2765
- const updated = comments.find((cm) => cm.id === id) ?? null;
2766
- if (!updated) return c.json({ error: "Comment not found" }, 404);
2767
- return c.json(updated);
2768
- });
2769
- app.delete("/api/gh/pr-session/comments/:id", async (c) => {
2770
- if (!prMode) return notInPrMode(c);
2771
- const id = c.req.param("id");
2772
- const session = await prStore.get();
2773
- if (!session) return notInPrMode(c);
2774
- const comments = (session.comments ?? []).filter((cm) => cm.id !== id);
2775
- await prStore.set({
2776
- ...session,
2777
- comments
2778
- });
2779
- return c.json({ ok: true });
2780
- });
2781
- app.post("/api/gh/pr-session/comments/:id/replies", async (c) => {
2782
- if (!prMode) return notInPrMode(c);
2783
- const id = c.req.param("id");
2784
- const { body, role, model } = await c.req.json();
2785
- const session = await prStore.get();
2786
- if (!session) return notInPrMode(c);
2787
- const comments = (session.comments ?? []).map((cm) => {
2788
- if (cm.id !== id) return cm;
2789
- const reply = {
2790
- id: crypto.randomUUID(),
2791
- body,
2792
- createdAt: Date.now(),
2793
- role: role || (model ? "agent" : "user"),
2794
- model: model || void 0
2795
- };
2796
- return {
2797
- ...cm,
2798
- replies: [...cm.replies ?? [], reply]
2799
- };
2800
- });
2801
- await prStore.set({
2802
- ...session,
2803
- comments
2804
- });
2805
- return c.json(comments.find((cm) => cm.id === id));
2806
- });
2807
- /** Submit the current PR session's review (new comments + verdict + body) to GitHub. */
2808
- app.post("/api/gh/submit", async (c) => {
2809
- if (!prMode) return notInPrMode(c);
2810
- const session = await prStore.get();
2811
- if (!session) return notInPrMode(c);
2812
- const body = await c.req.json().catch(() => ({}));
2813
- const decision = body.decision;
2814
- if (decision !== "approve" && decision !== "comment" && decision !== "request-changes" && decision !== "draft") return c.json({ error: "decision must be one of: approve, comment, request-changes, draft" }, 400);
2815
- const generalBody = typeof body.body === "string" ? body.body : "";
2816
- if (body.dryRun === true) {
2817
- const { buildReviewPayload } = await import("./github-C9GLNak7.mjs");
2818
- const payload = buildReviewPayload({
2819
- decision,
2820
- body: generalBody,
2821
- comments: session.comments ?? []
2822
- });
2823
- return c.json({
2824
- ok: true,
2825
- dryRun: true,
2826
- authSource: session.authSource ?? "none",
2827
- failedComments: 0,
2828
- payload
2829
- });
2830
- }
2831
- const result = await submitReview({
2832
- resolved: {
2833
- owner: session.owner,
2834
- repo: session.repo,
2835
- pullNumber: session.pullNumber,
2836
- ref: session.ref
2837
- },
2838
- decision,
2839
- body: generalBody,
2840
- comments: session.comments ?? []
2841
- });
2842
- if (result.ok) {
2843
- const next = {
2844
- ...session,
2845
- submittedAt: Date.now(),
2846
- submittedReviewId: result.reviewId,
2847
- submittedReviewUrl: result.reviewUrl,
2848
- authSource: result.authSource
2849
- };
2850
- await prStore.set(next);
2851
- }
2852
- return c.json({
2853
- ok: result.ok,
2854
- reviewId: result.reviewId,
2855
- reviewUrl: result.reviewUrl,
2856
- failedComments: result.failedComments ?? 0,
2857
- authSource: result.authSource,
2858
- error: result.error,
2859
- dryRun: false
2860
- });
2861
- });
2862
- app.post("/api/review/send", async (c) => {
2863
- const body = await c.req.json().catch(() => ({}));
2864
- const generalComment = typeof body?.generalComment === "string" ? body.generalComment : void 0;
2865
- const decision = body?.decision === "approved" || body?.decision === "changes-requested" || body?.decision === "rejected" || body?.decision === "comment-only" ? body.decision : void 0;
2866
- const mode = body?.mode === "comment-only" || body?.mode === "standard" ? body.mode : "standard";
2867
- const force = body?.force === true;
2868
- const all = await store.getAll();
2869
- const findings = scanReviewForSecrets({
2870
- generalComment,
2871
- comments: all
2872
- });
2873
- if (findings.length > 0 && !force) return c.json({
2874
- ok: false,
2875
- error: "secrets-detected",
2876
- findings
2877
- }, 400);
2878
- const openCount = all.filter((x) => x.status === "open").length;
2879
- let diffFingerprints;
2880
- try {
2881
- const { fingerprintDiffFiles } = await import("./diff-fingerprint-CWa-soYD.mjs");
2882
- if (prMode) {
2883
- const prSession = await prStore.get();
2884
- if (prSession?.diff) diffFingerprints = fingerprintDiffFiles(prSession.diff);
2885
- } else {
2886
- const live = await executeDiffWithMeta(diffOpts);
2887
- if (live.patch) diffFingerprints = fingerprintDiffFiles(live.patch);
2888
- }
2889
- } catch {}
2890
- const payload = reviewSession.send({
2891
- sentAt: Date.now(),
2892
- commentXml: formatComments(all, generalComment, decision, mode),
2893
- openCount,
2894
- comments: all,
2895
- decision,
2896
- mode,
2897
- diffFingerprints
2898
- });
2899
- return c.json({
2900
- ok: true,
2901
- round: payload.round,
2902
- openCount: payload.openCount,
2903
- decision: payload.decision,
2904
- mode: payload.mode,
2905
- waiters: reviewSession.snapshot().waiters,
2906
- secretsBypassed: findings.length > 0 && force,
2907
- hasSinceLastBaseline: Boolean(diffFingerprints && Object.keys(diffFingerprints).length > 0)
2908
- });
2909
- });
2910
- app.get("/api/review/await", async (c) => {
2911
- const sinceRaw = c.req.query("sinceRound");
2912
- const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
2913
- const requested = Number(c.req.query("timeoutMs")) || 25e3;
2914
- const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
2915
- const result = await reviewSession.await({
2916
- sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
2917
- timeoutMs,
2918
- signal: c.req.raw.signal
2919
- });
2920
- return c.json(result);
2921
- });
2922
- app.get("/api/review/status", (c) => {
2923
- return c.json(reviewSession.snapshot());
2924
- });
2925
- app.get("/api/review/history", (c) => {
2926
- return c.json({ rounds: reviewSession.getHistory() });
2927
- });
2928
- /**
2929
- * Files that differ from the last handoff baseline (content changed or new
2930
- * in the live diff). Used by the "Since last" file-tree chip.
2931
- */
2932
- app.get("/api/review/since-last", async (c) => {
2933
- const previous = reviewSession.getLastDiffFingerprints();
2934
- if (!previous) return c.json({
2935
- hasBaseline: false,
2936
- round: reviewSession.snapshot().round,
2937
- changed: [],
2938
- added: [],
2939
- removed: [],
2940
- reviewFiles: []
2941
- });
2942
- let currentPatch = "";
2943
- try {
2944
- if (prMode) currentPatch = (await prStore.get())?.diff ?? "";
2945
- else currentPatch = (await executeDiffWithMeta(diffOpts)).patch ?? "";
2946
- } catch (err) {
2947
- return c.json({ error: err?.message ?? "Failed to compute current diff" }, 500);
2948
- }
2949
- const { fingerprintDiffFiles, diffSinceLast, filesToReviewSinceLast } = await import("./diff-fingerprint-CWa-soYD.mjs");
2950
- const delta = diffSinceLast(previous, fingerprintDiffFiles(currentPatch));
2951
- return c.json({
2952
- hasBaseline: true,
2953
- round: reviewSession.snapshot().round,
2954
- ...delta,
2955
- reviewFiles: filesToReviewSinceLast(delta)
2956
- });
2957
- });
2958
- app.get("/api/plans", async (c) => {
2959
- return c.json(await plans.getAll());
2960
- });
2961
- app.get("/api/plans/:id", async (c) => {
2962
- const plan = await plans.get(c.req.param("id"));
2963
- if (!plan) return c.json({ error: "Plan not found" }, 404);
2964
- return c.json(plan);
2965
- });
2966
- app.get("/api/plans/:id/versions", async (c) => {
2967
- const plan = await plans.get(c.req.param("id"));
2968
- if (!plan) return c.json({ error: "Plan not found" }, 404);
2969
- return c.json(plan.versions ?? []);
2970
- });
2971
- app.get("/api/plans/:id/versions/:n", async (c) => {
2972
- const id = c.req.param("id");
2973
- const n = Number(c.req.param("n"));
2974
- if (!Number.isFinite(n) || n < 1) return c.json({ error: "version must be a positive integer" }, 400);
2975
- const plan = await plans.get(id);
2976
- if (!plan) return c.json({ error: "Plan not found" }, 404);
2977
- const version = (plan.versions ?? []).find((v) => v.version === n);
2978
- if (!version) return c.json({ error: `Version ${n} not found` }, 404);
2979
- return c.json({
2980
- version,
2981
- plan: {
2982
- id: plan.id,
2983
- title: plan.title,
2984
- decision: plan.decision,
2985
- currentVersion: plan.version
2986
- }
2987
- });
2988
- });
2989
- app.post("/api/plans", async (c) => {
2990
- const body = await c.req.json().catch(() => ({}));
2991
- if (typeof body.body !== "string" || !body.body.trim()) return c.json({ error: "A plan body (markdown) is required" }, 400);
2992
- const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "Untitled plan";
2993
- const plan = await plans.upsert({
2994
- id: typeof body.id === "string" && body.id ? body.id : void 0,
2995
- title,
2996
- body: body.body,
2997
- source: typeof body.source === "string" ? body.source : void 0,
2998
- model: typeof body.model === "string" ? body.model : void 0
2999
- });
3000
- return c.json(plan, 201);
3001
- });
3002
- app.put("/api/plans/:id", async (c) => {
3003
- const id = c.req.param("id");
3004
- const { title, body, source, model } = await c.req.json().catch(() => ({}));
3005
- const updated = await plans.update(id, {
3006
- title: typeof title === "string" ? title : void 0,
3007
- body: typeof body === "string" ? body : void 0,
3008
- source: typeof source === "string" ? source : void 0,
3009
- model: typeof model === "string" ? model : void 0
3010
- });
3011
- if (!updated) return c.json({ error: "Plan not found" }, 404);
3012
- return c.json(updated);
3013
- });
3014
- app.delete("/api/plans/:id", async (c) => {
3015
- if (!await plans.remove(c.req.param("id"))) return c.json({ error: "Plan not found" }, 404);
3016
- return c.json({ ok: true });
3017
- });
3018
- app.post("/api/plans/:id/comments", async (c) => {
3019
- const planId = c.req.param("id");
3020
- const plan = await plans.get(planId);
3021
- if (!plan) return c.json({ error: "Plan not found" }, 404);
3022
- const body = await c.req.json();
3023
- const lineNumber = Number.isFinite(body.lineNumber) ? Number(body.lineNumber) : 0;
3024
- const startLineNumber = Number.isFinite(body.startLineNumber) ? Number(body.startLineNumber) : void 0;
3025
- const anchorStart = startLineNumber ?? lineNumber;
3026
- const lineContent = typeof body.lineContent === "string" && body.lineContent ? body.lineContent : lineNumber > 0 ? extractPlanLines(plan.body, anchorStart, lineNumber) : "";
3027
- const sectionTitle = typeof body.sectionTitle === "string" && body.sectionTitle ? body.sectionTitle : lineNumber > 0 ? sectionTitleForLine(plan.body, anchorStart) : void 0;
3028
- const createdAtPlanVersion = Number.isFinite(body.createdAtPlanVersion) ? Number(body.createdAtPlanVersion) : plan.version;
3029
- const selectedQuote = typeof body.selectedQuote === "string" && body.selectedQuote.trim() ? body.selectedQuote.trim() : void 0;
3030
- const severityRaw = body.severity;
3031
- const severity = severityRaw === "blocking" || severityRaw === "nit" || severityRaw === "question" || severityRaw === "praise" || severityRaw === "none" ? severityRaw : void 0;
3032
- const comment = {
3033
- id: crypto.randomUUID(),
3034
- lineNumber,
3035
- startLineNumber,
3036
- lineContent,
3037
- selectedQuote,
3038
- sectionTitle,
3039
- body: body.body,
3040
- status: "open",
3041
- createdAt: Date.now(),
3042
- createdAtPlanVersion,
3043
- replies: [],
3044
- ...severity && severity !== "none" ? { severity } : {}
3045
- };
3046
- const updated = await plans.addComment(planId, comment);
3047
- if (!updated) return c.json({ error: "Plan not found" }, 404);
3048
- return c.json(updated, 201);
3049
- });
3050
- app.put("/api/plans/:id/comments/:commentId", async (c) => {
3051
- const { body, status } = await c.req.json();
3052
- const updated = await plans.updateComment(c.req.param("id"), c.req.param("commentId"), {
3053
- body,
3054
- status
3055
- });
3056
- if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
3057
- return c.json(updated);
3058
- });
3059
- app.delete("/api/plans/:id/comments/:commentId", async (c) => {
3060
- const updated = await plans.removeComment(c.req.param("id"), c.req.param("commentId"));
3061
- if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
3062
- return c.json(updated);
3063
- });
3064
- app.post("/api/plans/:id/comments/:commentId/replies", async (c) => {
3065
- const { body, role, model } = await c.req.json();
3066
- const reply = {
3067
- id: crypto.randomUUID(),
3068
- body,
3069
- createdAt: Date.now(),
3070
- role: role || (model ? "agent" : "user"),
3071
- model: model || void 0
3072
- };
3073
- const updated = await plans.addReply(c.req.param("id"), c.req.param("commentId"), reply);
3074
- if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
3075
- return c.json(updated);
3076
- });
3077
- app.put("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
3078
- const { body } = await c.req.json();
3079
- if (!body) return c.json({ error: "Body is required" }, 400);
3080
- const updated = await plans.updateReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"), body);
3081
- if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
3082
- return c.json(updated);
3083
- });
3084
- app.delete("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
3085
- const updated = await plans.removeReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"));
3086
- if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
3087
- return c.json(updated);
3088
- });
3089
- app.post("/api/plans/:id/decision", async (c) => {
3090
- const planId = c.req.param("id");
3091
- const body = await c.req.json().catch(() => ({}));
3092
- const decision = body.decision;
3093
- if (decision !== "approved" && decision !== "rejected" && decision !== "changes-requested" && decision !== "comment-only") return c.json({ error: "decision must be one of: approved, rejected, changes-requested, comment-only" }, 400);
3094
- const decisionComment = typeof body.decisionComment === "string" ? body.decisionComment : void 0;
3095
- const mode = body?.mode === "comment-only" || body?.mode === "standard" ? body.mode : "standard";
3096
- const plan = await plans.setDecision(planId, decision, decisionComment);
3097
- if (!plan) return c.json({ error: "Plan not found" }, 404);
3098
- const openCommentCount = (plan.comments ?? []).filter((x) => x.status === "open").length;
3099
- const payload = planReviewSession.decide({
3100
- sentAt: Date.now(),
3101
- planId,
3102
- decision,
3103
- decisionComment: plan.decisionComment,
3104
- reviewXml: formatPlanReview(plan, { mode }),
3105
- openCommentCount,
3106
- plan,
3107
- mode
3108
- });
3109
- return c.json({
3110
- ok: true,
3111
- round: payload.round,
3112
- decision,
3113
- mode: payload.mode,
3114
- openCommentCount,
3115
- waiters: planReviewSession.snapshot().waiters
3116
- });
3117
- });
3118
- app.get("/api/plan-review/await", async (c) => {
3119
- const sinceRaw = c.req.query("sinceRound");
3120
- const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
3121
- const requested = Number(c.req.query("timeoutMs")) || 25e3;
3122
- const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
3123
- const result = await planReviewSession.await({
3124
- sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
3125
- timeoutMs,
3126
- signal: c.req.raw.signal
3127
- });
3128
- return c.json(result);
3129
- });
3130
- app.get("/api/plan-review/status", (c) => {
3131
- return c.json(planReviewSession.snapshot());
3132
- });
3133
- app.post("/api/attachments", async (c) => {
3134
- const file = (await c.req.parseBody())["file"];
3135
- if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
3136
- try {
3137
- const storageDir = getProjectStorageDir();
3138
- const attachmentsDir = join(storageDir, "attachments");
3139
- await mkdir(attachmentsDir, { recursive: true });
3140
- const repoRoot = getRepoRoot();
3141
- await writeFile(join(storageDir, "repo_path.txt"), repoRoot, "utf-8");
3142
- const ext = extname(file.name) || ".png";
3143
- const filename = `pasted_image_${crypto.randomUUID()}${ext}`;
3144
- const absolutePath = join(attachmentsDir, filename);
3145
- const arrayBuffer = await file.arrayBuffer();
3146
- await writeFile(absolutePath, new Uint8Array(arrayBuffer));
3147
- return c.json({ url: `/api/attachments/${filename}` });
3148
- } catch (err) {
3149
- return c.json({ error: `Failed to save attachment: ${err.message}` }, 500);
3150
- }
3151
- });
3152
- app.get("/api/attachments/:filename", async (c) => {
3153
- const filename = c.req.param("filename");
3154
- const attachmentsDir = join(getProjectStorageDir(), "attachments");
3155
- const absolutePath = resolve(attachmentsDir, filename);
3156
- if (!absolutePath.startsWith(attachmentsDir)) return c.text("Forbidden", 403);
3157
- try {
3158
- const content = await readFile(absolutePath);
3159
- const contentType = MIME_TYPES[extname(absolutePath)] || "application/octet-stream";
3160
- return new Response(content, { headers: { "Content-Type": contentType } });
3161
- } catch {
3162
- return c.text("Attachment not found", 404);
3163
- }
3164
- });
3165
- app.get("/*", async (c) => {
3166
- let filePath = c.req.path;
3167
- if (filePath === "/") filePath = "/index.html";
3168
- const relativePath = filePath.slice(1);
3169
- if (!isSafePath(relativePath, clientDir)) return c.text("Forbidden", 403);
3170
- const fullPath = resolve(clientDir, relativePath);
3171
- try {
3172
- const content = await readFile(fullPath);
3173
- const contentType = MIME_TYPES[extname(fullPath)] || "application/octet-stream";
3174
- return new Response(content, { headers: { "Content-Type": contentType } });
3175
- } catch {
3176
- const indexContent = await readFile(join(clientDir, "index.html"));
3177
- return new Response(indexContent, { headers: { "Content-Type": "text/html" } });
3178
- }
3179
- });
3180
- return app;
3181
- }
3182
- /**
3183
- * Newest mtime (epoch ms) across everything that counts as project activity:
3184
- * comments.json, plans.json, and any attachment (media) file. Plans and media
3185
- * extend a project's life exactly like comments do, so retention is uniform
3186
- * across all three. Returns null when none of them exist yet.
3187
- */
3188
- async function newestActivityMs(projectDir) {
3189
- const candidates = [];
3190
- const safeStat = async (p) => {
3191
- try {
3192
- return (await stat(p)).mtimeMs;
3193
- } catch {
3194
- return null;
3195
- }
3196
- };
3197
- for (const name of [
3198
- "comments.json",
3199
- "plans.json",
3200
- "pr-session.json"
3201
- ]) {
3202
- const file = join(projectDir, name);
3203
- if (!existsSync(file)) continue;
3204
- const m = await safeStat(file);
3205
- if (m !== null) candidates.push(m);
3206
- }
3207
- const attachmentsDir = join(projectDir, "attachments");
3208
- if (existsSync(attachmentsDir)) {
3209
- const dirM = await safeStat(attachmentsDir);
3210
- if (dirM !== null) candidates.push(dirM);
3211
- try {
3212
- for (const file of await readdir(attachmentsDir)) {
3213
- const m = await safeStat(join(attachmentsDir, file));
3214
- if (m !== null) candidates.push(m);
3215
- }
3216
- } catch {}
3217
- }
3218
- return candidates.length ? Math.max(...candidates) : null;
3219
- }
3220
- async function cleanupStaleProjects() {
3221
- const baseDir = join(homedir(), ".diffing");
3222
- if (!existsSync(baseDir)) return;
3223
- try {
3224
- const entries = await readdir(baseDir, { withFileTypes: true });
3225
- const now = Date.now();
3226
- const STALE_TIME = 336 * 60 * 60 * 1e3;
3227
- for (const entry of entries) if (entry.isDirectory()) {
3228
- const projectDir = join(baseDir, entry.name);
3229
- const repoPathFile = join(projectDir, "repo_path.txt");
3230
- let shouldDelete = false;
3231
- if (existsSync(repoPathFile)) try {
3232
- const repoPath = (await readFile(repoPathFile, "utf-8")).trim();
3233
- if (!repoPath || !existsSync(repoPath)) shouldDelete = true;
3234
- } catch {}
3235
- if (!shouldDelete) {
3236
- const newest = await newestActivityMs(projectDir);
3237
- if (newest !== null && now - newest > STALE_TIME) shouldDelete = true;
3238
- }
3239
- if (shouldDelete) await rm(projectDir, {
3240
- recursive: true,
3241
- force: true
3242
- });
3243
- }
3244
- } catch (err) {
3245
- console.error("Failed to cleanup stale projects:", err);
3246
- }
3247
- }
3248
- async function startServer(options) {
3249
- cleanupStaleProjects().catch((err) => {
3250
- console.error("Failed to clean up stale projects:", err);
3251
- });
3252
- let prMode = false;
3253
- if (options.prRef) {
3254
- console.error(`Building PR session for ${options.prRef}...`);
3255
- try {
3256
- const store = new FilePrSessionStore();
3257
- const existing = await store.get();
3258
- if (!existing || existing.ref !== options.prRef) {
3259
- const session = await buildPrSession(options.prRef);
3260
- await store.set(session);
3261
- }
3262
- prMode = true;
3263
- } catch (err) {
3264
- const detail = err?.message ?? String(err);
3265
- throw new Error(`Failed to build PR session for ${options.prRef}: ${detail}`);
3266
- }
3267
- }
3268
- const app = createApp(options.clientDir, options.diffOpts, void 0, void 0, void 0, prMode);
3269
- return new Promise((resolve) => {
3270
- serve({
3271
- fetch: app.fetch,
3272
- port: options.port,
3273
- hostname: options.host
3274
- }, (info) => {
3275
- resolve({
3276
- port: info.port,
3277
- prMode
3278
- });
3279
- });
3280
- });
3281
- }
3282
- //#endregion
3283
- export { intoShowMode as a, buildGitDiffArgs as i, runTerminalDiff as n, parseDiffOptions as o, validateEnvironment as r, printHelp as s, startServer as t };