diffing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +164 -0
  2. package/dist/cli-agent-Cfuy9fLC.mjs +179 -0
  3. package/dist/cli-agent-CtNivjQW.mjs +179 -0
  4. package/dist/cli-agent-WzJPMH52.mjs +174 -0
  5. package/dist/cli-agent-uYUoCA4X.mjs +179 -0
  6. package/dist/cli.mjs +1903 -0
  7. package/dist/client/assets/abap-CkQ0tsz-.js +1 -0
  8. package/dist/client/assets/actionscript-3-DHm0ZfF4.js +1 -0
  9. package/dist/client/assets/ada-DK_ua92Z.js +1 -0
  10. package/dist/client/assets/andromeeda-D2ccw8v4.js +1 -0
  11. package/dist/client/assets/angular-html-BmBRnRY-.js +1 -0
  12. package/dist/client/assets/angular-ts-BTywfy8Q.js +1 -0
  13. package/dist/client/assets/apache-DXtpFu7o.js +1 -0
  14. package/dist/client/assets/apex-DlnRRWpl.js +1 -0
  15. package/dist/client/assets/apl-C-Nh4njw.js +1 -0
  16. package/dist/client/assets/applescript-CShk62ad.js +1 -0
  17. package/dist/client/assets/ara-CWN-FDkm.js +1 -0
  18. package/dist/client/assets/asciidoc-Bg-PM9Vn.js +1 -0
  19. package/dist/client/assets/asm-DKOhR24S.js +1 -0
  20. package/dist/client/assets/astro-DGFZsGD0.js +1 -0
  21. package/dist/client/assets/aurora-x-DpaEirL8.js +1 -0
  22. package/dist/client/assets/awk-3ibKHffv.js +1 -0
  23. package/dist/client/assets/ayu-dark-ursiSH5k.js +1 -0
  24. package/dist/client/assets/ayu-light-DMjlrKmx.js +1 -0
  25. package/dist/client/assets/ayu-mirage-DZEMb75h.js +1 -0
  26. package/dist/client/assets/ballerina-xx7VNGxs.js +1 -0
  27. package/dist/client/assets/bat-DGYsZ5wR.js +1 -0
  28. package/dist/client/assets/beancount-D_ATR1UV.js +1 -0
  29. package/dist/client/assets/berry-BqYMORfH.js +1 -0
  30. package/dist/client/assets/bibtex-CQTBDajW.js +1 -0
  31. package/dist/client/assets/bicep-CfNtxPrK.js +1 -0
  32. package/dist/client/assets/bird2-C0dUPe3I.js +1 -0
  33. package/dist/client/assets/blade-CK2WgF42.js +1 -0
  34. package/dist/client/assets/bsl-Dx5tFn3i.js +1 -0
  35. package/dist/client/assets/c-MXSr0H4Y.js +1 -0
  36. package/dist/client/assets/c3-BUwZ5e3k.js +1 -0
  37. package/dist/client/assets/cadence-C3OKDYrr.js +1 -0
  38. package/dist/client/assets/cairo-wCCQTdA9.js +1 -0
  39. package/dist/client/assets/catppuccin-frappe-CMnRXVZP.js +1 -0
  40. package/dist/client/assets/catppuccin-latte-CqhXSjCC.js +1 -0
  41. package/dist/client/assets/catppuccin-macchiato-B6kneE0C.js +1 -0
  42. package/dist/client/assets/catppuccin-mocha-CP8YZpvj.js +1 -0
  43. package/dist/client/assets/clarity-Crxe8gSr.js +1 -0
  44. package/dist/client/assets/clojure-BuzOUPMr.js +1 -0
  45. package/dist/client/assets/cmake-CCpZLdLI.js +1 -0
  46. package/dist/client/assets/cobol-BHSU4T5M.js +1 -0
  47. package/dist/client/assets/codeowners-Bo1VYpHz.js +1 -0
  48. package/dist/client/assets/codeql-Db01hzHE.js +1 -0
  49. package/dist/client/assets/coffee-DiFXs9rA.js +1 -0
  50. package/dist/client/assets/common-lisp-CMlJIc6Y.js +1 -0
  51. package/dist/client/assets/coq-BlzBYp3f.js +1 -0
  52. package/dist/client/assets/cpp-CJWLNzrI.js +1 -0
  53. package/dist/client/assets/crystal-u5M0TrPu.js +1 -0
  54. package/dist/client/assets/csharp-kfNshAa7.js +1 -0
  55. package/dist/client/assets/css-BMVhDkjX.js +1 -0
  56. package/dist/client/assets/csv-Dr8ri5Nd.js +1 -0
  57. package/dist/client/assets/cue-jXfhyOwY.js +1 -0
  58. package/dist/client/assets/cypher-CayLeiaR.js +1 -0
  59. package/dist/client/assets/d-BMZQ0mAy.js +1 -0
  60. package/dist/client/assets/dark-plus-B__q47Nr.js +1 -0
  61. package/dist/client/assets/dart-gMOEMyuz.js +1 -0
  62. package/dist/client/assets/dax-C3t5LadU.js +1 -0
  63. package/dist/client/assets/desktop-DmhXLwLR.js +1 -0
  64. package/dist/client/assets/diff-togOXQX6.js +1 -0
  65. package/dist/client/assets/docker--6N01olR.js +1 -0
  66. package/dist/client/assets/dotenv-Bq930Yx8.js +1 -0
  67. package/dist/client/assets/dracula-CBkSghcw.js +1 -0
  68. package/dist/client/assets/dracula-soft-CCSYeoxL.js +1 -0
  69. package/dist/client/assets/dream-maker-BiMf3DHl.js +1 -0
  70. package/dist/client/assets/edge-Di-AS7wq.js +1 -0
  71. package/dist/client/assets/elixir-CSMS6Yja.js +1 -0
  72. package/dist/client/assets/elm-CgUch6k4.js +1 -0
  73. package/dist/client/assets/emacs-lisp-Dhht7he2.js +1 -0
  74. package/dist/client/assets/erb-BY53XvrH.js +1 -0
  75. package/dist/client/assets/erlang-CtK0kQ6z.js +1 -0
  76. package/dist/client/assets/everforest-dark-NqSz4oHS.js +1 -0
  77. package/dist/client/assets/everforest-light-a3Yzs_lT.js +1 -0
  78. package/dist/client/assets/fennel-BExc9TZf.js +1 -0
  79. package/dist/client/assets/fish-C2p-AJmZ.js +1 -0
  80. package/dist/client/assets/fluent-CeYzs8Qy.js +1 -0
  81. package/dist/client/assets/fortran-fixed-form-DBM4e0_k.js +1 -0
  82. package/dist/client/assets/fortran-free-form-BIQEK2Jw.js +1 -0
  83. package/dist/client/assets/fsharp-CmzzGQLe.js +1 -0
  84. package/dist/client/assets/gdresource-B_wbPqHd.js +1 -0
  85. package/dist/client/assets/gdscript-B5JQPh_-.js +1 -0
  86. package/dist/client/assets/gdshader-vVjJ_Qr8.js +1 -0
  87. package/dist/client/assets/genie-DGl5rqC8.js +1 -0
  88. package/dist/client/assets/gherkin-CY_VLUnD.js +1 -0
  89. package/dist/client/assets/git-commit-1z4D6NzL.js +1 -0
  90. package/dist/client/assets/git-rebase-Cu4zaEs3.js +1 -0
  91. package/dist/client/assets/github-dark-ClO4AStc.js +1 -0
  92. package/dist/client/assets/github-dark-default-CM0dPco8.js +1 -0
  93. package/dist/client/assets/github-dark-dimmed-Cb6xxpXY.js +1 -0
  94. package/dist/client/assets/github-dark-high-contrast-BpYtx2-M.js +1 -0
  95. package/dist/client/assets/github-light-CeOVFgf_.js +1 -0
  96. package/dist/client/assets/github-light-default-BmnCWL6x.js +1 -0
  97. package/dist/client/assets/github-light-high-contrast-rpagNF5O.js +1 -0
  98. package/dist/client/assets/gleam-BarIG9kQ.js +1 -0
  99. package/dist/client/assets/glimmer-js-DgxwVbEv.js +1 -0
  100. package/dist/client/assets/glimmer-ts-CQgpMe8l.js +1 -0
  101. package/dist/client/assets/glsl-C6rPllEZ.js +1 -0
  102. package/dist/client/assets/gn-DhYsTPYc.js +1 -0
  103. package/dist/client/assets/gnuplot-CucLlTBc.js +1 -0
  104. package/dist/client/assets/go-toi-uWvg.js +1 -0
  105. package/dist/client/assets/graphql-D0IXKtPr.js +1 -0
  106. package/dist/client/assets/groovy-Ds4dis_k.js +1 -0
  107. package/dist/client/assets/gruvbox-dark-hard-aPiW8pQh.js +1 -0
  108. package/dist/client/assets/gruvbox-dark-medium-2HJCvzsS.js +1 -0
  109. package/dist/client/assets/gruvbox-dark-soft-Da97ODX9.js +1 -0
  110. package/dist/client/assets/gruvbox-light-hard-BXlEipFe.js +1 -0
  111. package/dist/client/assets/gruvbox-light-medium-Cun-GjGk.js +1 -0
  112. package/dist/client/assets/gruvbox-light-soft-Bd3qiiSw.js +1 -0
  113. package/dist/client/assets/hack-Bbv4uGLh.js +1 -0
  114. package/dist/client/assets/haml-BEq4fVEe.js +1 -0
  115. package/dist/client/assets/handlebars-CGriKhdR.js +1 -0
  116. package/dist/client/assets/haskell-B_jvz5Po.js +1 -0
  117. package/dist/client/assets/haxe-BRbuo5UO.js +1 -0
  118. package/dist/client/assets/hcl-BzNUCtsK.js +1 -0
  119. package/dist/client/assets/hjson-BxVo3sS2.js +1 -0
  120. package/dist/client/assets/hlsl-C8Wes1nf.js +1 -0
  121. package/dist/client/assets/horizon-D54PjLAY.js +1 -0
  122. package/dist/client/assets/horizon-bright-D9RccdIr.js +1 -0
  123. package/dist/client/assets/houston-CMu1N_4K.js +1 -0
  124. package/dist/client/assets/html-DhsWBiF-.js +1 -0
  125. package/dist/client/assets/html-derivative-CU7SM-vj.js +1 -0
  126. package/dist/client/assets/http-DAZNqWUu.js +1 -0
  127. package/dist/client/assets/hurl-DGP3-RE2.js +1 -0
  128. package/dist/client/assets/hxml-DC4KwKt2.js +1 -0
  129. package/dist/client/assets/hy-dMrJKKU_.js +1 -0
  130. package/dist/client/assets/imba-DG_oO8tg.js +1 -0
  131. package/dist/client/assets/index-BqD_EVaR.css +1 -0
  132. package/dist/client/assets/index-Bsby5xp2.js +3856 -0
  133. package/dist/client/assets/ini-geHw79Aj.js +1 -0
  134. package/dist/client/assets/java-CoIvfF-o.js +1 -0
  135. package/dist/client/assets/javascript-D2G0Bybq.js +1 -0
  136. package/dist/client/assets/jinja-BRdOYMmE.js +1 -0
  137. package/dist/client/assets/jison-CVQZsA0e.js +1 -0
  138. package/dist/client/assets/json-BTFmVGAb.js +1 -0
  139. package/dist/client/assets/json5-CDUsNqz6.js +1 -0
  140. package/dist/client/assets/jsonc-D48xcwyD.js +1 -0
  141. package/dist/client/assets/jsonl-cpIYDXEe.js +1 -0
  142. package/dist/client/assets/jsonnet-Da1lhszN.js +1 -0
  143. package/dist/client/assets/jssm-B8BLrEGL.js +1 -0
  144. package/dist/client/assets/jsx-DYLgPaVb.js +1 -0
  145. package/dist/client/assets/julia-CJWtNtRM.js +1 -0
  146. package/dist/client/assets/just-CZyINJ-2.js +1 -0
  147. package/dist/client/assets/kanagawa-dragon-2Sk8Rx83.js +1 -0
  148. package/dist/client/assets/kanagawa-lotus-CQVSWfJp.js +1 -0
  149. package/dist/client/assets/kanagawa-wave-BKWNioLX.js +1 -0
  150. package/dist/client/assets/kdl-B-2tSRmp.js +1 -0
  151. package/dist/client/assets/kotlin-DepsEzh3.js +1 -0
  152. package/dist/client/assets/kusto-BcCtXIb8.js +1 -0
  153. package/dist/client/assets/laserwave-BUqkO46F.js +1 -0
  154. package/dist/client/assets/latex-mNMnTIv1.js +1 -0
  155. package/dist/client/assets/lean-CNO9ffpX.js +1 -0
  156. package/dist/client/assets/less-Dj0T9pWC.js +1 -0
  157. package/dist/client/assets/light-plus-C9Hnuc1T.js +1 -0
  158. package/dist/client/assets/liquid-BgSPl0Kv.js +1 -0
  159. package/dist/client/assets/llvm-DbV9b66l.js +1 -0
  160. package/dist/client/assets/log-BMblmPyH.js +1 -0
  161. package/dist/client/assets/logo-B78qMxws.js +1 -0
  162. package/dist/client/assets/lua-rmIsezWJ.js +1 -0
  163. package/dist/client/assets/luau-BUd8xsdE.js +1 -0
  164. package/dist/client/assets/make-Ev-6cJrd.js +1 -0
  165. package/dist/client/assets/markdown-CS4LI6sN.js +1 -0
  166. package/dist/client/assets/marko-Dfkwq7Gv.js +1 -0
  167. package/dist/client/assets/material-theme-5rA1zn0D.js +1 -0
  168. package/dist/client/assets/material-theme-darker-DGOEiL65.js +1 -0
  169. package/dist/client/assets/material-theme-lighter-CULfFRX0.js +1 -0
  170. package/dist/client/assets/material-theme-ocean-LQZ9CleP.js +1 -0
  171. package/dist/client/assets/material-theme-palenight-Di_f5oks.js +1 -0
  172. package/dist/client/assets/matlab-C0qEtAx9.js +1 -0
  173. package/dist/client/assets/mdc-f53p1REg.js +1 -0
  174. package/dist/client/assets/mdx-CzvnbLXb.js +1 -0
  175. package/dist/client/assets/mermaid-BPquZaKt.js +1 -0
  176. package/dist/client/assets/min-dark-rkuIbtJV.js +1 -0
  177. package/dist/client/assets/min-light-Dj_HOGsr.js +1 -0
  178. package/dist/client/assets/mipsasm-k5DQoS6j.js +1 -0
  179. package/dist/client/assets/mojo-sTTDuNnR.js +1 -0
  180. package/dist/client/assets/monokai-D5p-u2ij.js +1 -0
  181. package/dist/client/assets/moonbit-j5ChFtBC.js +1 -0
  182. package/dist/client/assets/move-Dj0Gmn6R.js +1 -0
  183. package/dist/client/assets/narrat-DJgMz2oW.js +1 -0
  184. package/dist/client/assets/nextflow-CApkvBGW.js +1 -0
  185. package/dist/client/assets/nextflow-groovy-eDLuZ_oi.js +1 -0
  186. package/dist/client/assets/nginx-Cn5ljkEv.js +1 -0
  187. package/dist/client/assets/night-owl-CEdtPl3A.js +1 -0
  188. package/dist/client/assets/night-owl-light-GdSldewm.js +1 -0
  189. package/dist/client/assets/nim-DrAyHk_n.js +1 -0
  190. package/dist/client/assets/nix-CxIPS3pb.js +1 -0
  191. package/dist/client/assets/nord-3WFAz5H9.js +1 -0
  192. package/dist/client/assets/nushell-GDfBmoBU.js +1 -0
  193. package/dist/client/assets/objective-c-MFQ_MGUa.js +1 -0
  194. package/dist/client/assets/objective-cpp-Y83LcBE7.js +1 -0
  195. package/dist/client/assets/ocaml-FxYfaJxu.js +1 -0
  196. package/dist/client/assets/odin-CprihY3z.js +1 -0
  197. package/dist/client/assets/one-dark-pro-BB6mpmmA.js +1 -0
  198. package/dist/client/assets/one-light-BYLoxNtg.js +1 -0
  199. package/dist/client/assets/openscad-Bc1FbpOi.js +1 -0
  200. package/dist/client/assets/pascal-Bb4d8cUL.js +1 -0
  201. package/dist/client/assets/perl-BLNIVaG-.js +1 -0
  202. package/dist/client/assets/php-CxDOF44K.js +1 -0
  203. package/dist/client/assets/pierre-dark-cF19s-RM.js +1 -0
  204. package/dist/client/assets/pierre-light-BdpX8OpD.js +1 -0
  205. package/dist/client/assets/pkl-tM25Sq0w.js +1 -0
  206. package/dist/client/assets/plastic-D3k2Xp_5.js +1 -0
  207. package/dist/client/assets/plsql-CLEfFhSj.js +1 -0
  208. package/dist/client/assets/po-CaAzdrrh.js +1 -0
  209. package/dist/client/assets/poimandres-bPTvT4Da.js +1 -0
  210. package/dist/client/assets/polar-TG7aTGdK.js +1 -0
  211. package/dist/client/assets/postcss-8kqfa9f0.js +1 -0
  212. package/dist/client/assets/powerquery-CtSu1Nfd.js +1 -0
  213. package/dist/client/assets/powershell-87lLBHMy.js +1 -0
  214. package/dist/client/assets/prisma-Dnf694DZ.js +1 -0
  215. package/dist/client/assets/prolog-C0XbHTWa.js +1 -0
  216. package/dist/client/assets/proto-BZP-HLIb.js +1 -0
  217. package/dist/client/assets/pug-DGZxLxM3.js +1 -0
  218. package/dist/client/assets/puppet-CJYWgYC5.js +1 -0
  219. package/dist/client/assets/purescript-B95Q6BAd.js +1 -0
  220. package/dist/client/assets/python-CqTCsi7P.js +1 -0
  221. package/dist/client/assets/qml-Dmqfyov_.js +1 -0
  222. package/dist/client/assets/qmldir-DlIL4hls.js +1 -0
  223. package/dist/client/assets/qss-BW72E0q_.js +1 -0
  224. package/dist/client/assets/r-F2CLYuZ7.js +1 -0
  225. package/dist/client/assets/racket-CCAimEsC.js +1 -0
  226. package/dist/client/assets/raku-mSuqElkM.js +1 -0
  227. package/dist/client/assets/razor-DYbbBV0d.js +1 -0
  228. package/dist/client/assets/red-C-DA4yBI.js +1 -0
  229. package/dist/client/assets/reg-CGwMBJ2c.js +1 -0
  230. package/dist/client/assets/regexp-TyKA4-BB.js +1 -0
  231. package/dist/client/assets/rel-JwrbYgX4.js +1 -0
  232. package/dist/client/assets/riscv-CoYyE75b.js +1 -0
  233. package/dist/client/assets/ron-C5wrhnmJ.js +1 -0
  234. package/dist/client/assets/rose-pine-DlrMPYYd.js +1 -0
  235. package/dist/client/assets/rose-pine-dawn-DBHFtZD2.js +1 -0
  236. package/dist/client/assets/rose-pine-moon-BBfrpe5q.js +1 -0
  237. package/dist/client/assets/rosmsg-DlimTR9t.js +1 -0
  238. package/dist/client/assets/rst-CMLF7yP9.js +1 -0
  239. package/dist/client/assets/ruby-BE4S9bsQ.js +1 -0
  240. package/dist/client/assets/rust-Cxnkxx3h.js +1 -0
  241. package/dist/client/assets/sas-DOhcrhSM.js +1 -0
  242. package/dist/client/assets/sass-DEyqUIpU.js +1 -0
  243. package/dist/client/assets/scala-i3RN3yAN.js +1 -0
  244. package/dist/client/assets/scheme-BcKdjYkO.js +1 -0
  245. package/dist/client/assets/scss-DZgJ0K1I.js +1 -0
  246. package/dist/client/assets/sdbl-Be_XWc3n.js +1 -0
  247. package/dist/client/assets/shaderlab-DXAhMdPo.js +1 -0
  248. package/dist/client/assets/shellscript-CWMAvx8a.js +1 -0
  249. package/dist/client/assets/shellsession-OJxRnKbO.js +1 -0
  250. package/dist/client/assets/slack-dark-3PDtm74m.js +1 -0
  251. package/dist/client/assets/slack-ochin-CwiuFRJD.js +1 -0
  252. package/dist/client/assets/smalltalk-BdBlL6ks.js +1 -0
  253. package/dist/client/assets/snazzy-light-Dyqc8Is2.js +1 -0
  254. package/dist/client/assets/solarized-dark-DZiTscIX.js +1 -0
  255. package/dist/client/assets/solarized-light-DBcfxrfG.js +1 -0
  256. package/dist/client/assets/solidity-DH-yUSWc.js +1 -0
  257. package/dist/client/assets/soy-C_oQW_S0.js +1 -0
  258. package/dist/client/assets/sparql-BdcL8cbr.js +1 -0
  259. package/dist/client/assets/splunk-CIjc4kxs.js +1 -0
  260. package/dist/client/assets/sql-DEXpTiLI.js +1 -0
  261. package/dist/client/assets/ssh-config-BKVd4drG.js +1 -0
  262. package/dist/client/assets/stata-BcrmvsDF.js +1 -0
  263. package/dist/client/assets/stylus-tqAsmugv.js +1 -0
  264. package/dist/client/assets/surrealql-Dwy3iKjf.js +1 -0
  265. package/dist/client/assets/svelte-BbWN9l2M.js +1 -0
  266. package/dist/client/assets/swift-CdDBQ3_Z.js +1 -0
  267. package/dist/client/assets/synthwave-84-CbbRs73K.js +1 -0
  268. package/dist/client/assets/system-verilog-CBqEY-9v.js +1 -0
  269. package/dist/client/assets/systemd-khRP5zwK.js +1 -0
  270. package/dist/client/assets/talonscript-D_lneH-6.js +1 -0
  271. package/dist/client/assets/tasl-CLymawGe.js +1 -0
  272. package/dist/client/assets/tcl-BRY1S87U.js +1 -0
  273. package/dist/client/assets/templ-BGIeAdtg.js +1 -0
  274. package/dist/client/assets/terraform-BqohMo2f.js +1 -0
  275. package/dist/client/assets/tex-9fSuF3Cs.js +1 -0
  276. package/dist/client/assets/tokyo-night-BWUONxad.js +1 -0
  277. package/dist/client/assets/toml-C4c85Lwg.js +1 -0
  278. package/dist/client/assets/ts-tags-BcxYxF8C.js +1 -0
  279. package/dist/client/assets/tsv-DsZq9HBQ.js +1 -0
  280. package/dist/client/assets/tsx-B_z3ONaQ.js +1 -0
  281. package/dist/client/assets/turtle-Dpr8zLlw.js +1 -0
  282. package/dist/client/assets/twig-DQ7osXl9.js +1 -0
  283. package/dist/client/assets/typescript-oc7bycaY.js +1 -0
  284. package/dist/client/assets/typespec-DcqgATB-.js +1 -0
  285. package/dist/client/assets/typst-CeVxmuPg.js +1 -0
  286. package/dist/client/assets/v-C2Qsh2-1.js +1 -0
  287. package/dist/client/assets/vala-CvZ3yL7c.js +1 -0
  288. package/dist/client/assets/vb-B_p4qDw4.js +1 -0
  289. package/dist/client/assets/verilog-D8MGjxD7.js +1 -0
  290. package/dist/client/assets/vesper-DossXiE1.js +1 -0
  291. package/dist/client/assets/vhdl-MLyYh_Gz.js +1 -0
  292. package/dist/client/assets/viml-BuYIArcz.js +1 -0
  293. package/dist/client/assets/vitesse-black-C3kc1227.js +1 -0
  294. package/dist/client/assets/vitesse-dark-kWOu5V36.js +1 -0
  295. package/dist/client/assets/vitesse-light-DMtiL8bK.js +1 -0
  296. package/dist/client/assets/vue-B3o6Rfti.js +1 -0
  297. package/dist/client/assets/vue-html-BB_D6k4v.js +1 -0
  298. package/dist/client/assets/vue-vine-CROD_1yE.js +1 -0
  299. package/dist/client/assets/vyper-CNqG09-_.js +1 -0
  300. package/dist/client/assets/wasm-BsQDV7Y0.js +1 -0
  301. package/dist/client/assets/wasm-DFVlQlgd.js +1 -0
  302. package/dist/client/assets/wenyan-BmOQge13.js +1 -0
  303. package/dist/client/assets/wgsl-CgWrmvIg.js +1 -0
  304. package/dist/client/assets/wikitext-DkwckXCk.js +1 -0
  305. package/dist/client/assets/wit-CtyVFGe7.js +1 -0
  306. package/dist/client/assets/wolfram-CXE4FrKz.js +1 -0
  307. package/dist/client/assets/worker-ZCX3SXK5.js +160 -0
  308. package/dist/client/assets/xml-BeVKeTE5.js +1 -0
  309. package/dist/client/assets/xsl-DaxeysAZ.js +1 -0
  310. package/dist/client/assets/yaml-DJD0ioac.js +1 -0
  311. package/dist/client/assets/zenscript-DH15wJt0.js +1 -0
  312. package/dist/client/assets/zig-CqXN9brD.js +1 -0
  313. package/dist/client/index.html +16 -0
  314. package/dist/mcp-CLgzC8_a.mjs +119 -0
  315. package/dist/mcp-LVbLV17O.mjs +119 -0
  316. package/dist/mcp-nMwTAut3.mjs +119 -0
  317. package/dist/server-lock-BbiF7u3F.mjs +493 -0
  318. package/dist/server-lock-Bx9ixiF6.mjs +575 -0
  319. package/dist/server-lock-DIkImokU.mjs +487 -0
  320. package/package.json +76 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,1903 @@
1
+ #!/usr/bin/env node
2
+ import { _ as gitAddFile, a as formatComments, b as revertHunk, c as getFileContent, d as getMergeStatus, f as getProjectStorageDir, g as getUntrackedFilePathsAsync, h as getTabSizeForFiles, i as writeServerLock, l as getGitDiffAsync, m as getRepoRoot, o as getBranchName, p as getRepoName, r as removeServerLock, s as getCustomGitDiffAsync, u as getHunkHistory, v as isGitRepo, x as isSafePath, y as listRepoFiles } from "./server-lock-Bx9ixiF6.mjs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, extname, join, resolve } from "node:path";
5
+ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
6
+ import getPort from "get-port";
7
+ import { parseArgs } from "node:util";
8
+ import { execFileSync } from "node:child_process";
9
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { Hono } from "hono";
12
+ import { streamSSE } from "hono/streaming";
13
+ import { serve } from "@hono/node-server";
14
+ //#region src/lib/diff-options.ts
15
+ const DEFAULTS$1 = {
16
+ revisions: [],
17
+ staged: false,
18
+ merge: false,
19
+ pathspecs: [],
20
+ indentHeuristic: true,
21
+ noIndentHeuristic: false,
22
+ ignoreSpaceChange: false,
23
+ ignoreAllSpace: false,
24
+ ignoreBlankLines: false,
25
+ ignoreCrAtEol: false,
26
+ functionContext: false,
27
+ patchWithRaw: false,
28
+ patchWithStat: false,
29
+ compactSummary: false,
30
+ cumulative: false,
31
+ pickaxeAll: false,
32
+ exitCode: false,
33
+ quiet: false,
34
+ noPrefix: false,
35
+ binary: false,
36
+ fullIndex: false,
37
+ text: false,
38
+ textconv: false,
39
+ noExtDiff: false,
40
+ itaVisible: false,
41
+ noRelative: false,
42
+ findCopiesHarder: false,
43
+ check: false,
44
+ outputMode: "auto",
45
+ host: "127.0.0.1",
46
+ noOpen: false,
47
+ help: false,
48
+ version: false,
49
+ includeUnstaged: true,
50
+ includeUntracked: true
51
+ };
52
+ /**
53
+ * All git-diff option definitions for `node:util.parseArgs`.
54
+ * Categorised to match `git diff --help`.
55
+ */
56
+ const GIT_DIFF_OPTIONS = {
57
+ staged: { type: "boolean" },
58
+ cached: { type: "boolean" },
59
+ merge: { type: "boolean" },
60
+ "diff-algorithm": { type: "string" },
61
+ "indent-heuristic": {
62
+ type: "boolean",
63
+ default: true
64
+ },
65
+ "no-indent-heuristic": { type: "boolean" },
66
+ anchored: { type: "string" },
67
+ "ignore-space-change": {
68
+ type: "boolean",
69
+ short: "b"
70
+ },
71
+ "ignore-all-space": {
72
+ type: "boolean",
73
+ short: "w"
74
+ },
75
+ "ignore-blank-lines": { type: "boolean" },
76
+ "ignore-cr-at-eol": { type: "boolean" },
77
+ "ws-error-highlight": { type: "string" },
78
+ "unified": {
79
+ type: "string",
80
+ short: "U"
81
+ },
82
+ "inter-hunk-context": { type: "string" },
83
+ "function-context": {
84
+ type: "boolean",
85
+ short: "W"
86
+ },
87
+ "word-diff": { type: "string" },
88
+ "word-diff-regex": { type: "string" },
89
+ "color-words": { type: "string" },
90
+ "color-moved": { type: "string" },
91
+ "color-moved-ws": { type: "string" },
92
+ "find-copies": {
93
+ type: "string",
94
+ short: "C"
95
+ },
96
+ "find-copies-harder": { type: "boolean" },
97
+ "find-renames": {
98
+ type: "string",
99
+ short: "M"
100
+ },
101
+ "break-rewrites": {
102
+ type: "string",
103
+ short: "B"
104
+ },
105
+ patch: {
106
+ type: "boolean",
107
+ short: "p"
108
+ },
109
+ "no-patch": {
110
+ type: "boolean",
111
+ short: "s"
112
+ },
113
+ raw: { type: "boolean" },
114
+ "patch-with-raw": { type: "boolean" },
115
+ "patch-with-stat": { type: "boolean" },
116
+ numstat: { type: "boolean" },
117
+ shortstat: { type: "boolean" },
118
+ stat: { type: "string" },
119
+ summary: { type: "boolean" },
120
+ "name-only": { type: "boolean" },
121
+ "name-status": { type: "boolean" },
122
+ check: { type: "boolean" },
123
+ "compact-summary": { type: "boolean" },
124
+ dirstat: { type: "string" },
125
+ "dirstat-by-file": { type: "string" },
126
+ cumulative: { type: "boolean" },
127
+ "diff-filter": { type: "string" },
128
+ S: { type: "string" },
129
+ G: { type: "string" },
130
+ "pickaxe-all": { type: "boolean" },
131
+ output: {
132
+ type: "string",
133
+ short: "o"
134
+ },
135
+ "exit-code": { type: "boolean" },
136
+ quiet: { type: "boolean" },
137
+ "src-prefix": { type: "string" },
138
+ "dst-prefix": { type: "string" },
139
+ "no-prefix": { type: "boolean" },
140
+ "line-prefix": { type: "string" },
141
+ submodule: { type: "string" },
142
+ binary: { type: "boolean" },
143
+ "no-binary": { type: "boolean" },
144
+ "full-index": { type: "boolean" },
145
+ text: {
146
+ type: "boolean",
147
+ short: "a"
148
+ },
149
+ textconv: { type: "boolean" },
150
+ "no-textconv": { type: "boolean" },
151
+ "no-ext-diff": { type: "boolean" },
152
+ "ext-diff": { type: "string" },
153
+ "ita-visible": { type: "boolean" },
154
+ "ita-invisible-in-index": { type: "boolean" },
155
+ relative: { type: "string" },
156
+ "no-relative": { type: "boolean" },
157
+ "ignore-submodules": { type: "string" }
158
+ };
159
+ /** Options that are diffit-specific, not git-diff options. */
160
+ const DIFFIT_OPTIONS = {
161
+ port: { type: "string" },
162
+ host: { type: "string" },
163
+ "no-open": {
164
+ type: "boolean",
165
+ default: false
166
+ },
167
+ web: {
168
+ type: "boolean",
169
+ default: false
170
+ },
171
+ terminal: {
172
+ type: "boolean",
173
+ default: false
174
+ },
175
+ help: {
176
+ type: "boolean",
177
+ short: "h"
178
+ },
179
+ version: { type: "boolean" }
180
+ };
181
+ function getVersion() {
182
+ try {
183
+ const __dirname = dirname(fileURLToPath(import.meta.url));
184
+ return JSON.parse(readFileSync(resolve(__dirname, "..", "..", "package.json"), "utf-8")).version;
185
+ } catch {
186
+ return "unknown";
187
+ }
188
+ }
189
+ function printHelp() {
190
+ const version = getVersion();
191
+ console.log(`diffit v${version} – Local code review tool for git diffs
192
+
193
+ Usage: diffit [<git diff options>] [<revision>...] [-- <path>...]
194
+ diffit --web [<git diff options>] [<revision>...] [-- <path>...]
195
+ diffit [server options]
196
+
197
+ Git Diff Options (drop-in replacement for git diff):`);
198
+ printGitDiffHelp();
199
+ console.log(`
200
+ Diffit Server Options:
201
+ --port <port> Port to run the server on (default: random available port)
202
+ --host <host> Host address to bind to (default: 127.0.0.1). Pass
203
+ 0.0.0.0 to expose the server to the local network.
204
+ --no-open Don't open the browser automatically
205
+
206
+ Output Modes:
207
+ By default diffit auto-selects the best output mode:
208
+ - Terminal mode: when output is a pipe, redirect, or non-TTY
209
+ - Web mode: when output is a TTY (interactive terminal)
210
+
211
+ Force a mode with --web or --terminal flags.
212
+
213
+ Examples:
214
+ diffit Review uncommitted changes (web UI)
215
+ diffit --staged Review staged changes
216
+ diffit HEAD~3 Review last 3 commits
217
+ diffit main..feature Compare branches
218
+ diffit --diff-algorithm=patience src/ Diff with patience algorithm
219
+ diffit -U10 Diff with 10 context lines
220
+ diffit --word-diff=color Word-level diff in color
221
+ diffit -b -w Ignore whitespace changes
222
+ diffit --host 0.0.0.0 Allow other machines on the LAN to review`);
223
+ }
224
+ function printGitDiffHelp() {
225
+ console.log(`
226
+ Revision / Range:
227
+ --staged, --cached Show staged changes
228
+ --merge Show merge conflicts
229
+
230
+ Diff Algorithm:
231
+ --diff-algorithm=<A> Choose diff algorithm (minimal, patience, histogram, myers)
232
+ --indent-heuristic Enable indent heuristic (default: on)
233
+ --no-indent-heuristic Disable indent heuristic
234
+ --anchored=<TEXT> Anchor diff algorithm to <TEXT>
235
+
236
+ Whitespace:
237
+ -b, --ignore-space-change Ignore changes in amount of whitespace
238
+ -w, --ignore-all-space Ignore whitespace when comparing lines
239
+ --ignore-blank-lines Ignore blank lines
240
+ --ignore-cr-at-eol Ignore carriage-return at EOL
241
+ --ws-error-highlight=<KIND> Highlight whitespace errors (none, default, all)
242
+
243
+ Context:
244
+ -U<n>, --unified=<n> Generate diffs with <n> lines of context
245
+ --inter-hunk-context=<n> Show <n> lines between diff hunks
246
+ -W, --function-context Show whole function as context
247
+
248
+ Word Diff:
249
+ --word-diff=<MODE> Word diff (color, plain, porcelain, none)
250
+ --word-diff-regex=<REGEX> Regex to determine a word
251
+ --color-words[=<REGEX>] Word diff with color
252
+
253
+ Moved / Copied Detection:
254
+ --color-moved=<MODE> Color moved lines (no, default, plain, blocks, zebra, dimmed-zebra)
255
+ --color-moved-ws=<MODE> How whitespace is treated in --color-moved
256
+ -C[<n>], --find-copies[=<n>] Detect copies
257
+ --find-copies-harder Spend extra cycles to find copies
258
+ -M[<n>], --find-renames[=<n>] Detect renames
259
+ -B[<n>], --break-rewrites[=<n>] Break complete rewrite changes into delete/add pairs
260
+
261
+ Output Format:
262
+ -p, --patch Generate patch (default)
263
+ -s, --no-patch Suppress diff output
264
+ --raw Generate raw diff
265
+ --patch-with-raw Both patch and raw output
266
+ --patch-with-stat Both patch and stat
267
+ --numstat Machine-friendly stat: additions/deletions per file
268
+ --shortstat Summary of additions/deletions
269
+ --stat[=<width>] Diffstat with graph
270
+ --summary Extended header summary
271
+ --name-only Show only names of changed files
272
+ --name-status Show only names and status of changed files
273
+ --check Check if diff introduces whitespace errors
274
+ --compact-summary Compact summary for submodule changes
275
+ --dirstat[=<param>] Directory-level change stats
276
+ --dirstat-by-file[=<param>] Per-file directory stats
277
+ --cumulative Cumulative directory stats
278
+
279
+ Filtering:
280
+ --diff-filter=[(A|C|D|M|R|T|U|X|B)...[*]]
281
+ -S<string> Look for differences that change number of occurrences of <string>
282
+ -G<regex> Look for differences whose patch text contains <regex>
283
+ --pickaxe-all Show all changes with -S or -G
284
+
285
+ Output Control:
286
+ -o, --output=<file> Write diff to <file>
287
+ --exit-code Exit with 1 if differences, 0 if not
288
+ --quiet Disable all output, imply --exit-code
289
+
290
+ Prefixes:
291
+ --src-prefix=<PREFIX> Source prefix (default: "a/")
292
+ --dst-prefix=<PREFIX> Destination prefix (default: "b/")
293
+ --no-prefix Do not show source/destination prefixes
294
+ --line-prefix=<PREFIX> Prepend <PREFIX> to every line
295
+
296
+ Submodule:
297
+ --submodule=<FORMAT> Submodule diff format (diff, log, short)
298
+
299
+ Miscellaneous:
300
+ --binary Output binary diff (apply patch to binary files)
301
+ --no-binary Don't output binary diff
302
+ --full-index Show full blob object names
303
+ -a, --text Treat all files as text
304
+ --textconv Allow external text conversion filters
305
+ --no-textconv Disallow external text conversion filters
306
+ --no-ext-diff Disallow external diff drivers
307
+ --ext-diff=<CMD> Allow external diff driver
308
+ --ita-visible Include intent-to-add files
309
+ --ignore-submodules=<WHEN> Ignore submodule changes
310
+ --relative[=<path>] Show changes relative to <path>
311
+ --no-relative Don't show relative paths`);
312
+ }
313
+ /**
314
+ * Parse raw `process.argv` slice into typed DiffOptions.
315
+ *
316
+ * Strategy:
317
+ * 1. Split argv into: [diffit options] [git revisions] [-- pathspecs]
318
+ * 2. Parse known flags via node:util.parseArgs
319
+ * 3. Anything unknown / positional becomes a revision or pathspec
320
+ */
321
+ function parseDiffOptions(rawArgs) {
322
+ const { values, positionals } = parseArgs({
323
+ options: {
324
+ ...GIT_DIFF_OPTIONS,
325
+ ...DIFFIT_OPTIONS
326
+ },
327
+ allowPositionals: true,
328
+ strict: false,
329
+ args: rawArgs
330
+ });
331
+ const opts = { ...DEFAULTS$1 };
332
+ if (values.help) opts.help = true;
333
+ if (values.version) opts.version = true;
334
+ if (values.port) opts.port = parseInt(values.port, 10);
335
+ if (values.host) opts.host = values.host;
336
+ if (values["no-open"]) opts.noOpen = true;
337
+ if (values.staged || values.cached) opts.staged = true;
338
+ if (values.merge) opts.merge = true;
339
+ const algo = values["diff-algorithm"];
340
+ if (algo && [
341
+ "minimal",
342
+ "patience",
343
+ "histogram",
344
+ "myers"
345
+ ].includes(algo)) opts.algorithm = algo;
346
+ if (values["no-indent-heuristic"]) {
347
+ opts.noIndentHeuristic = true;
348
+ opts.indentHeuristic = false;
349
+ }
350
+ if (values.anchored) opts.anchored = values.anchored;
351
+ if (values["ignore-space-change"]) opts.ignoreSpaceChange = true;
352
+ if (values["ignore-all-space"]) opts.ignoreAllSpace = true;
353
+ if (values["ignore-blank-lines"]) opts.ignoreBlankLines = true;
354
+ if (values["ignore-cr-at-eol"]) opts.ignoreCrAtEol = true;
355
+ const wsh = values["ws-error-highlight"];
356
+ if (wsh && [
357
+ "none",
358
+ "default",
359
+ "all"
360
+ ].includes(wsh)) opts.wsErrorHighlight = wsh;
361
+ if (values.unified) opts.unifiedContext = parseInt(values.unified, 10);
362
+ if (values["inter-hunk-context"]) opts.interHunkContext = parseInt(values["inter-hunk-context"], 10);
363
+ if (values["function-context"]) opts.functionContext = true;
364
+ const wd = values["word-diff"];
365
+ if (wd && [
366
+ "color",
367
+ "plain",
368
+ "porcelain",
369
+ "none"
370
+ ].includes(wd)) opts.wordDiff = wd;
371
+ if (values["word-diff-regex"]) opts.wordDiffRegex = values["word-diff-regex"];
372
+ if (values["color-words"]) {
373
+ opts.wordDiff = "color";
374
+ opts.wordDiffRegex = values["color-words"] || void 0;
375
+ }
376
+ const cm = values["color-moved"];
377
+ if (cm && [
378
+ "default",
379
+ "plain",
380
+ "blocks",
381
+ "zebra",
382
+ "dimmed-zebra",
383
+ "no"
384
+ ].includes(cm)) opts.colorMoved = cm;
385
+ const cmw = values["color-moved-ws"];
386
+ if (cmw && [
387
+ "no",
388
+ "ignore-space-at-eol",
389
+ "ignore-space-change",
390
+ "ignore-all-space",
391
+ "allow-indentation-change"
392
+ ].includes(cmw)) opts.colorMovedWs = cmw;
393
+ if (values["find-copies"] !== void 0) {
394
+ const v = values["find-copies"];
395
+ opts.findCopies = v ? parseInt(v, 10) : 40;
396
+ }
397
+ if (values["find-copies-harder"]) opts.findCopiesHarder = true;
398
+ if (values["find-renames"] !== void 0) {
399
+ const v = values["find-renames"];
400
+ opts.findRenames = v ? parseInt(v, 10) : 50;
401
+ }
402
+ if (values["break-rewrites"] !== void 0) opts.breakRewrites = values["break-rewrites"];
403
+ if (values.raw) opts.outputFormat = "raw";
404
+ if (values.numstat) opts.outputFormat = "numstat";
405
+ if (values.shortstat) opts.outputFormat = "shortstat";
406
+ if (values.stat !== void 0) opts.outputFormat = "stat";
407
+ if (values.summary) opts.outputFormat = "summary";
408
+ if (values["name-only"]) opts.outputFormat = "name-only";
409
+ if (values["name-status"]) opts.outputFormat = "name-status";
410
+ if (values["patch-with-raw"]) opts.patchWithRaw = true;
411
+ if (values["patch-with-stat"]) opts.patchWithStat = true;
412
+ if (values["compact-summary"]) opts.compactSummary = true;
413
+ if (values.dirstat) opts.dirstat = values.dirstat;
414
+ if (values["dirstat-by-file"]) opts.dirstatByFile = values["dirstat-by-file"];
415
+ if (values.cumulative) opts.cumulative = true;
416
+ if (values["no-patch"]) opts.outputFormat = void 0;
417
+ if (values.check) opts.outputFormat = void 0;
418
+ if (values["diff-filter"]) opts.diffFilter = values["diff-filter"];
419
+ if (values.S) opts.pickaxeString = values.S;
420
+ if (values.G) opts.pickaxeRegex = values.G;
421
+ if (values["pickaxe-all"]) opts.pickaxeAll = true;
422
+ if (values.output) opts.outputFile = values.output;
423
+ if (values["exit-code"]) opts.exitCode = true;
424
+ if (values.quiet) {
425
+ opts.quiet = true;
426
+ opts.exitCode = true;
427
+ }
428
+ if (values["src-prefix"]) opts.srcPrefix = values["src-prefix"];
429
+ if (values["dst-prefix"]) opts.dstPrefix = values["dst-prefix"];
430
+ if (values["no-prefix"]) opts.noPrefix = true;
431
+ if (values["line-prefix"]) opts.linePrefix = values["line-prefix"];
432
+ const sm = values.submodule;
433
+ if (sm && [
434
+ "diff",
435
+ "log",
436
+ "short"
437
+ ].includes(sm)) opts.submodule = sm;
438
+ if (values.binary) opts.binary = true;
439
+ if (values["no-binary"]) opts.binary = false;
440
+ if (values["full-index"]) opts.fullIndex = true;
441
+ if (values.text) opts.text = true;
442
+ if (values.textconv) opts.textconv = true;
443
+ if (values["no-textconv"]) opts.textconv = false;
444
+ if (values["no-ext-diff"]) opts.noExtDiff = true;
445
+ if (values["ext-diff"]) opts.extDiff = values["ext-diff"];
446
+ if (values["ita-visible"] || values["ita-invisible-in-index"]) opts.itaVisible = true;
447
+ if (values.relative !== void 0) opts.relative = values.relative || void 0;
448
+ if (values["no-relative"]) opts.noRelative = true;
449
+ if (values["ignore-submodules"]) {
450
+ const ig = values["ignore-submodules"];
451
+ if ([
452
+ "none",
453
+ "untracked",
454
+ "dirty",
455
+ "all"
456
+ ].includes(ig)) opts.ignoreSubmodules = ig;
457
+ }
458
+ const dashDashIdx = rawArgs.indexOf("--");
459
+ if (dashDashIdx >= 0) {
460
+ for (let i = 0; i < dashDashIdx; i++) {
461
+ const arg = rawArgs[i];
462
+ if (!arg.startsWith("-")) opts.revisions.push(arg);
463
+ }
464
+ for (let i = dashDashIdx + 1; i < rawArgs.length; i++) opts.pathspecs.push(rawArgs[i]);
465
+ } else for (const p of positionals) opts.revisions.push(p);
466
+ if (values.web) opts.outputMode = "web";
467
+ else if (values.terminal) opts.outputMode = "terminal";
468
+ if (process.stdout.isTTY && opts.outputMode === "auto") opts.outputMode = "web";
469
+ else if (!process.stdout.isTTY && opts.outputMode === "auto") opts.outputMode = "terminal";
470
+ if (opts.outputFormat || opts.patchWithRaw || opts.patchWithStat || opts.exitCode || opts.quiet || opts.outputFile) opts.outputMode = "terminal";
471
+ return opts;
472
+ }
473
+ /**
474
+ * Build the `git diff` argument array from parsed options.
475
+ * Returns args that can be spread into `git diff <args>`.
476
+ */
477
+ function buildGitDiffArgs(opts) {
478
+ const args = [];
479
+ if (opts.algorithm) args.push(`--diff-algorithm=${opts.algorithm}`);
480
+ if (opts.noIndentHeuristic) args.push("--no-indent-heuristic");
481
+ if (opts.anchored) args.push(`--anchored=${opts.anchored}`);
482
+ if (opts.staged) args.push("--staged");
483
+ if (opts.merge) args.push("--merge");
484
+ if (opts.ignoreSpaceChange) args.push("--ignore-space-change");
485
+ if (opts.ignoreAllSpace) args.push("--ignore-all-space");
486
+ if (opts.ignoreBlankLines) args.push("--ignore-blank-lines");
487
+ if (opts.ignoreCrAtEol) args.push("--ignore-cr-at-eol");
488
+ if (opts.wsErrorHighlight) args.push(`--ws-error-highlight=${opts.wsErrorHighlight}`);
489
+ if (opts.unifiedContext !== void 0) args.push(`--unified=${opts.unifiedContext}`);
490
+ if (opts.interHunkContext !== void 0) args.push(`--inter-hunk-context=${opts.interHunkContext}`);
491
+ if (opts.functionContext) args.push("--function-context");
492
+ if (opts.wordDiff) if (opts.wordDiff === "color" && opts.wordDiffRegex && !opts.colorWords) {
493
+ args.push(`--word-diff=color`);
494
+ args.push(`--word-diff-regex=${opts.wordDiffRegex}`);
495
+ } else if (opts.wordDiff === "color" && opts.colorWords) args.push(`--color-words=${opts.wordDiffRegex || "."}`);
496
+ else args.push(`--word-diff=${opts.wordDiff}`);
497
+ if (opts.wordDiffRegex && opts.wordDiff !== "color") args.push(`--word-diff-regex=${opts.wordDiffRegex}`);
498
+ if (opts.colorMoved) args.push(`--color-moved=${opts.colorMoved}`);
499
+ if (opts.colorMovedWs) args.push(`--color-moved-ws=${opts.colorMovedWs}`);
500
+ if (opts.findCopies !== void 0) args.push(opts.findCopies === 40 ? "-C" : `-C${opts.findCopies}`);
501
+ if (opts.findCopiesHarder) args.push("--find-copies-harder");
502
+ if (opts.findRenames !== void 0) args.push(opts.findRenames === 50 ? "-M" : `-M${opts.findRenames}`);
503
+ if (opts.breakRewrites !== void 0) args.push(`-B${opts.breakRewrites}`);
504
+ if (opts.outputFormat === "raw") args.push("--raw");
505
+ if (opts.outputFormat === "numstat") args.push("--numstat");
506
+ if (opts.outputFormat === "shortstat") args.push("--shortstat");
507
+ if (opts.outputFormat === "stat") args.push("--stat");
508
+ if (opts.outputFormat === "summary") args.push("--summary");
509
+ if (opts.outputFormat === "name-only") args.push("--name-only");
510
+ if (opts.outputFormat === "name-status") args.push("--name-status");
511
+ if (opts.patchWithRaw) args.push("--patch-with-raw");
512
+ if (opts.patchWithStat) args.push("--patch-with-stat");
513
+ if (opts.compactSummary) args.push("--compact-summary");
514
+ if (opts.dirstat) args.push(`--dirstat=${opts.dirstat}`);
515
+ if (opts.dirstatByFile) args.push(`--dirstat-by-file=${opts.dirstatByFile}`);
516
+ if (opts.cumulative) args.push("--cumulative");
517
+ if (opts.check) args.push("--check");
518
+ if (opts.diffFilter) args.push(`--diff-filter=${opts.diffFilter}`);
519
+ if (opts.pickaxeString) args.push(`-S${opts.pickaxeString}`);
520
+ if (opts.pickaxeRegex) args.push(`-G${opts.pickaxeRegex}`);
521
+ if (opts.pickaxeAll) args.push("--pickaxe-all");
522
+ if (opts.outputFile) args.push(`--output=${opts.outputFile}`);
523
+ if (opts.exitCode) args.push("--exit-code");
524
+ if (opts.quiet) args.push("--quiet");
525
+ if (opts.srcPrefix) args.push(`--src-prefix=${opts.srcPrefix}`);
526
+ if (opts.dstPrefix) args.push(`--dst-prefix=${opts.dstPrefix}`);
527
+ if (opts.noPrefix) args.push("--no-prefix");
528
+ if (opts.linePrefix) args.push(`--line-prefix=${opts.linePrefix}`);
529
+ if (opts.submodule) args.push(`--submodule=${opts.submodule}`);
530
+ if (opts.binary) args.push("--binary");
531
+ if (opts.fullIndex) args.push("--full-index");
532
+ if (opts.text) args.push("--text");
533
+ if (opts.textconv) args.push("--textconv");
534
+ if (opts.noExtDiff) args.push("--no-ext-diff");
535
+ if (opts.extDiff) args.push(`--ext-diff=${opts.extDiff}`);
536
+ if (opts.itaVisible) args.push("--ita-visible");
537
+ if (opts.relative !== void 0) args.push(opts.relative ? `--relative=${opts.relative}` : "--relative");
538
+ if (opts.noRelative) args.push("--no-relative");
539
+ if (opts.ignoreSubmodules) args.push(`--ignore-submodules=${opts.ignoreSubmodules}`);
540
+ for (const rev of opts.revisions) args.push(rev);
541
+ if (opts.pathspecs.length > 0) {
542
+ args.push("--");
543
+ args.push(...opts.pathspecs);
544
+ }
545
+ return args;
546
+ }
547
+ //#endregion
548
+ //#region src/lib/diff-engine.ts
549
+ const MAX_BUFFER = 50 * 1024 * 1024;
550
+ /**
551
+ * Determine whether to use the "standard" web mode (unstaged+staged+untracked)
552
+ * or a "custom" mode (specific revisions / pathspecs).
553
+ *
554
+ * Custom mode is used when the user provides explicit revisions or pathspecs
555
+ * beyond just toggling staged/untracked.
556
+ */
557
+ function isCustomMode$1(opts) {
558
+ return opts.revisions.length > 0 || opts.pathspecs.length > 0;
559
+ }
560
+ function parseFilePaths(patch) {
561
+ const paths = /* @__PURE__ */ new Set();
562
+ for (const line of patch.split("\n")) {
563
+ const match = line.match(/^diff --git a\/.+ b\/(.+)$/);
564
+ if (match) paths.add(match[1]);
565
+ }
566
+ return [...paths];
567
+ }
568
+ function parseBinaryFiles(patch, untrackedFiles) {
569
+ const binaryFiles = [];
570
+ const lines = patch.split("\n");
571
+ for (let i = 0; i < lines.length; i++) {
572
+ const line = lines[i];
573
+ if (!line.startsWith("Binary files ") || !line.includes(" differ")) continue;
574
+ let filePath = "";
575
+ for (let j = i - 1; j >= 0; j--) {
576
+ const match = lines[j].match(/^diff --git a\/.+ b\/(.+)$/);
577
+ if (match) {
578
+ filePath = match[1];
579
+ break;
580
+ }
581
+ }
582
+ if (!filePath) continue;
583
+ let changeType = "changed";
584
+ for (let j = i - 1; j >= 0; j--) {
585
+ if (lines[j].startsWith("diff --git")) break;
586
+ if (lines[j].startsWith("new file mode")) {
587
+ changeType = "added";
588
+ break;
589
+ }
590
+ if (lines[j].startsWith("deleted file mode")) {
591
+ changeType = "deleted";
592
+ break;
593
+ }
594
+ }
595
+ if (changeType === "added" && untrackedFiles?.has(filePath)) changeType = "untracked";
596
+ binaryFiles.push({
597
+ path: filePath,
598
+ type: changeType
599
+ });
600
+ }
601
+ return binaryFiles;
602
+ }
603
+ /**
604
+ * Execute a diff asynchronously — used by the server.
605
+ */
606
+ async function executeDiffAsync(opts) {
607
+ if (isCustomMode$1(opts)) {
608
+ const args = buildGitDiffArgs(opts);
609
+ return {
610
+ patch: await getCustomGitDiffAsync(args),
611
+ args
612
+ };
613
+ }
614
+ return {
615
+ patch: await getGitDiffAsync({
616
+ staged: opts.staged,
617
+ untracked: opts.includeUntracked
618
+ }),
619
+ args: []
620
+ };
621
+ }
622
+ /**
623
+ * Execute a diff and produce the full enriched result used by the web API.
624
+ */
625
+ async function executeDiffWithMeta(opts) {
626
+ const { patch } = await executeDiffAsync(opts);
627
+ const repoName = getRepoName();
628
+ const branch = getBranchName();
629
+ const untrackedFiles = opts.includeUntracked ? await getUntrackedFilePathsAsync() : [];
630
+ const binaryFiles = parseBinaryFiles(patch, new Set(untrackedFiles));
631
+ const filePaths = parseFilePaths(patch);
632
+ return {
633
+ patch,
634
+ binaryFiles,
635
+ filePaths,
636
+ tabSizeMap: getTabSizeForFiles(filePaths),
637
+ untrackedFiles,
638
+ repoName,
639
+ branch
640
+ };
641
+ }
642
+ /**
643
+ * Run the diff in terminal mode: forward to git diff with full flags,
644
+ * output to stdout. Behaves identically to `git diff`.
645
+ */
646
+ function runTerminalDiff(opts) {
647
+ const args = buildGitDiffArgs(opts);
648
+ const enforceDefaults = !opts.outputFormat && !opts.quiet;
649
+ const finalArgs = [];
650
+ if (enforceDefaults) {
651
+ if (!args.includes("--no-ext-diff") && !opts.extDiff) finalArgs.push("--no-ext-diff");
652
+ }
653
+ finalArgs.push(...args);
654
+ try {
655
+ execFileSync("git", ["diff", ...finalArgs], {
656
+ encoding: "utf-8",
657
+ maxBuffer: MAX_BUFFER,
658
+ stdio: "inherit"
659
+ });
660
+ return 0;
661
+ } catch (err) {
662
+ if (err.status === 1 && opts.exitCode) return 1;
663
+ return err.status ?? 1;
664
+ }
665
+ }
666
+ /**
667
+ * Validate the environment: check that we're in a git repo.
668
+ * Returns an error message or null.
669
+ */
670
+ function validateEnvironment() {
671
+ if (!isGitRepo()) return "Error: not inside a git repository";
672
+ return null;
673
+ }
674
+ //#endregion
675
+ //#region src/lib/symbols.ts
676
+ const SYMBOL_PATTERNS = [
677
+ {
678
+ pattern: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w[\w$]*)\s*[<(]/i,
679
+ kind: "function",
680
+ nameGroup: 1
681
+ },
682
+ {
683
+ pattern: /^\s*(?:export\s+)?(?:const|let|var)\s+(\w[\w$]*)\s*=\s*(?:async\s*)?\(/i,
684
+ kind: "function",
685
+ nameGroup: 1
686
+ },
687
+ {
688
+ pattern: /^\s*(?:export\s+)?(?:const|let|var)\s+(\w[\w$]*)\s*=\s*(?:async\s+)?function/i,
689
+ kind: "function",
690
+ nameGroup: 1
691
+ },
692
+ {
693
+ pattern: /^\s*(?:export\s+)?class\s+(\w[\w$]*)/i,
694
+ kind: "class",
695
+ nameGroup: 1
696
+ },
697
+ {
698
+ pattern: /^\s*(?:export\s+)?interface\s+(\w[\w$]*)/i,
699
+ kind: "interface",
700
+ nameGroup: 1
701
+ },
702
+ {
703
+ pattern: /^\s*(?:export\s+)?type\s+(\w[\w$]*)\s*=/i,
704
+ kind: "type",
705
+ nameGroup: 1
706
+ },
707
+ {
708
+ pattern: /^\s*(?:export\s+)?enum\s+(\w[\w$]*)/i,
709
+ kind: "enum",
710
+ nameGroup: 1
711
+ },
712
+ {
713
+ pattern: /^\s*(?:export\s+)?(?:const|let|var)\s+(\w[\w$]*)\s*=/i,
714
+ kind: "variable",
715
+ nameGroup: 1
716
+ },
717
+ {
718
+ pattern: /^\s*def\s+(\w[\w$]*)\s*\(/i,
719
+ kind: "function",
720
+ nameGroup: 1
721
+ },
722
+ {
723
+ pattern: /^\s*(?:pub(?:\s*\(\w+\))?\s+)?fn\s+(\w[\w$]*)\s*[<(]/i,
724
+ kind: "function",
725
+ nameGroup: 1
726
+ },
727
+ {
728
+ pattern: /^\s*(?:pub(?:\s*\(\w+\))?\s+)?struct\s+(\w[\w$]*)/i,
729
+ kind: "struct",
730
+ nameGroup: 1
731
+ },
732
+ {
733
+ pattern: /^\s*(?:pub(?:\s*\(\w+\))?\s+)?enum\s+(\w[\w$]*)/i,
734
+ kind: "enum",
735
+ nameGroup: 1
736
+ },
737
+ {
738
+ pattern: /^\s*(?:pub(?:\s*\(\w+\))?\s+)?impl\s+(\w[\w$]*)/i,
739
+ kind: "impl",
740
+ nameGroup: 1
741
+ },
742
+ {
743
+ pattern: /^\s*(?:pub(?:\s*\(\w+\))?\s+)?trait\s+(\w[\w$]*)/i,
744
+ kind: "trait",
745
+ nameGroup: 1
746
+ },
747
+ {
748
+ pattern: /^\s*func\s+(\w[\w$]*)\s*\(/i,
749
+ kind: "function",
750
+ nameGroup: 1
751
+ },
752
+ {
753
+ pattern: /^\s*(?:func\s+)?\((\w[\w$]*)\s+\*?\w+\)\s+(\w[\w$]*)\s*\(/i,
754
+ kind: "method",
755
+ nameGroup: 2
756
+ }
757
+ ];
758
+ /**
759
+ * Returns the symbol defined on this line (name + kind), or null if the line
760
+ * is not a recognizable definition. The first matching pattern wins.
761
+ */
762
+ function classifySymbolLine(line) {
763
+ for (const { pattern, kind, nameGroup } of SYMBOL_PATTERNS) {
764
+ const match = pattern.exec(line);
765
+ if (match) {
766
+ const name = match[nameGroup];
767
+ if (name) return {
768
+ name,
769
+ kind
770
+ };
771
+ }
772
+ }
773
+ return null;
774
+ }
775
+ //#endregion
776
+ //#region src/lib/search.ts
777
+ /**
778
+ * Server-side code search powered by `@ff-labs/fff-node` — a native (Rust)
779
+ * fuzzy file finder + live grep. Because it is a native Node addon it cannot
780
+ * run in the browser, so the React client calls the `/api/search` endpoint and
781
+ * this module owns the single long-lived `FileFinder` instance for the repo.
782
+ *
783
+ * Design notes:
784
+ * - The native binary is loaded lazily via dynamic import inside `init()` so a
785
+ * missing/incompatible platform binary degrades gracefully (search returns
786
+ * an error) instead of crashing the whole server at module load.
787
+ * - One finder per process, keyed implicitly to the repo root. Its frecency /
788
+ * history databases live under the project's `~/.diffit/<repo>` storage dir
789
+ * so we never write stray files into the user's repository.
790
+ * - fff runs its own file-system watcher, so the index stays fresh as the
791
+ * working tree changes during a review.
792
+ * - "Symbols" search is plain grep for the identifier, then each hit line is
793
+ * classified with the shared {@link classifySymbolLine} patterns — fff 0.8.x
794
+ * does not populate `isDefinition`, so we do the classification ourselves.
795
+ */
796
+ const DEFAULT_LIMIT = 60;
797
+ const MAX_LIMIT = 200;
798
+ /** Internal page size when no path filter — generous so diff-first client
799
+ * ranking has enough to work with. */
800
+ const SCAN_PAGE = 200;
801
+ /** Larger page size when filtering to a path set, so the post-filter is
802
+ * effectively complete for normal-sized diffs/repos. */
803
+ const SCAN_PAGE_FILTERED = 1e3;
804
+ let finder = null;
805
+ let initPromise = null;
806
+ let initError = null;
807
+ let shutdownHooked = false;
808
+ function clampLimit(limit) {
809
+ if (!limit || limit < 1) return DEFAULT_LIMIT;
810
+ return Math.min(limit, MAX_LIMIT);
811
+ }
812
+ async function init() {
813
+ try {
814
+ const { FileFinder } = await import("@ff-labs/fff-node");
815
+ const root = getRepoRoot();
816
+ const dbDir = join(getProjectStorageDir(root), "fff");
817
+ mkdirSync(dbDir, { recursive: true });
818
+ const created = FileFinder.create({
819
+ basePath: root,
820
+ frecencyDbPath: join(dbDir, "frecency.db"),
821
+ historyDbPath: join(dbDir, "history.db"),
822
+ logLevel: "error"
823
+ });
824
+ if (!created.ok) {
825
+ initError = created.error;
826
+ return null;
827
+ }
828
+ const f = created.value;
829
+ if (!shutdownHooked) {
830
+ shutdownHooked = true;
831
+ process.once("exit", () => {
832
+ try {
833
+ f.destroy();
834
+ } catch {}
835
+ });
836
+ }
837
+ await f.waitForScan(8e3);
838
+ finder = f;
839
+ return f;
840
+ } catch (err) {
841
+ initError = err?.message ?? String(err);
842
+ return null;
843
+ }
844
+ }
845
+ async function getFinder() {
846
+ if (finder) return finder;
847
+ if (initError) return null;
848
+ if (!initPromise) initPromise = init();
849
+ return initPromise;
850
+ }
851
+ function isIndexing(f) {
852
+ try {
853
+ return f.isScanning();
854
+ } catch {
855
+ return false;
856
+ }
857
+ }
858
+ async function searchFiles(query, opts = {}) {
859
+ const f = await getFinder();
860
+ if (!f) return {
861
+ scope: "files",
862
+ items: [],
863
+ total: 0,
864
+ indexing: false,
865
+ error: initError ?? "Search unavailable"
866
+ };
867
+ const limit = clampLimit(opts.limit);
868
+ const pathSet = opts.paths ? new Set(opts.paths) : null;
869
+ const pageSize = pathSet ? SCAN_PAGE_FILTERED : Math.max(SCAN_PAGE, limit);
870
+ const res = f.fileSearch(query, { pageSize });
871
+ if (!res.ok) return {
872
+ scope: "files",
873
+ items: [],
874
+ total: 0,
875
+ indexing: isIndexing(f),
876
+ error: res.error
877
+ };
878
+ const items = [];
879
+ const { items: raw, scores } = res.value;
880
+ for (let i = 0; i < raw.length && items.length < limit; i++) {
881
+ const it = raw[i];
882
+ if (pathSet && !pathSet.has(it.relativePath)) continue;
883
+ const score = scores[i];
884
+ items.push({
885
+ path: it.relativePath,
886
+ fileName: it.fileName,
887
+ gitStatus: it.gitStatus,
888
+ matchType: score?.matchType ?? "",
889
+ exact: !!score?.exactMatch
890
+ });
891
+ }
892
+ return {
893
+ scope: "files",
894
+ items,
895
+ total: res.value.totalMatched,
896
+ indexing: isIndexing(f)
897
+ };
898
+ }
899
+ async function searchContent(query, opts = {}) {
900
+ const f = await getFinder();
901
+ if (!f) return {
902
+ scope: "text",
903
+ items: [],
904
+ total: 0,
905
+ indexing: false,
906
+ error: initError ?? "Search unavailable"
907
+ };
908
+ if (!query) return {
909
+ scope: "text",
910
+ items: [],
911
+ total: 0,
912
+ indexing: isIndexing(f)
913
+ };
914
+ const limit = clampLimit(opts.limit);
915
+ const pathSet = opts.paths ? new Set(opts.paths) : null;
916
+ const pageSize = pathSet ? SCAN_PAGE_FILTERED : Math.max(SCAN_PAGE, limit);
917
+ const res = f.grep(query, {
918
+ mode: opts.regex ? "regex" : "plain",
919
+ smartCase: true,
920
+ beforeContext: 0,
921
+ afterContext: 0,
922
+ pageSize
923
+ });
924
+ if (!res.ok) return {
925
+ scope: "text",
926
+ items: [],
927
+ total: 0,
928
+ indexing: isIndexing(f),
929
+ error: res.error
930
+ };
931
+ const items = [];
932
+ for (const m of res.value.items) {
933
+ if (items.length >= limit) break;
934
+ if (pathSet && !pathSet.has(m.relativePath)) continue;
935
+ items.push(toContentHit(m));
936
+ }
937
+ return {
938
+ scope: "text",
939
+ items,
940
+ total: res.value.totalMatched,
941
+ indexing: isIndexing(f),
942
+ regexError: res.value.regexFallbackError || void 0
943
+ };
944
+ }
945
+ function toContentHit(m) {
946
+ return {
947
+ path: m.relativePath,
948
+ fileName: m.fileName,
949
+ line: m.lineNumber,
950
+ col: m.col,
951
+ content: m.lineContent,
952
+ matchRanges: m.matchRanges ?? [],
953
+ gitStatus: m.gitStatus
954
+ };
955
+ }
956
+ async function searchSymbols(query, opts = {}) {
957
+ const f = await getFinder();
958
+ if (!f) return {
959
+ scope: "symbols",
960
+ items: [],
961
+ total: 0,
962
+ indexing: false,
963
+ error: initError ?? "Search unavailable"
964
+ };
965
+ if (!query) return {
966
+ scope: "symbols",
967
+ items: [],
968
+ total: 0,
969
+ indexing: isIndexing(f)
970
+ };
971
+ const limit = clampLimit(opts.limit);
972
+ const pathSet = opts.paths ? new Set(opts.paths) : null;
973
+ const pageSize = pathSet ? SCAN_PAGE_FILTERED : Math.max(SCAN_PAGE_FILTERED, limit * 8);
974
+ const res = f.grep(query, {
975
+ mode: "plain",
976
+ smartCase: true,
977
+ beforeContext: 0,
978
+ afterContext: 0,
979
+ pageSize
980
+ });
981
+ if (!res.ok) return {
982
+ scope: "symbols",
983
+ items: [],
984
+ total: 0,
985
+ indexing: isIndexing(f),
986
+ error: res.error
987
+ };
988
+ const items = [];
989
+ const ql = query.toLowerCase();
990
+ const seen = /* @__PURE__ */ new Set();
991
+ for (const m of res.value.items) {
992
+ if (items.length >= limit) break;
993
+ if (pathSet && !pathSet.has(m.relativePath)) continue;
994
+ const sym = classifySymbolLine(m.lineContent);
995
+ if (!sym) continue;
996
+ if (!sym.name.toLowerCase().includes(ql)) continue;
997
+ const key = `${m.relativePath}:${m.lineNumber}:${sym.name}`;
998
+ if (seen.has(key)) continue;
999
+ seen.add(key);
1000
+ const nameIdx = m.lineContent.indexOf(sym.name);
1001
+ const matchRanges = nameIdx >= 0 ? [[nameIdx, nameIdx + sym.name.length]] : m.matchRanges ?? [];
1002
+ items.push({
1003
+ name: sym.name,
1004
+ kind: sym.kind,
1005
+ path: m.relativePath,
1006
+ fileName: m.fileName,
1007
+ line: m.lineNumber,
1008
+ content: m.lineContent,
1009
+ matchRanges,
1010
+ gitStatus: m.gitStatus
1011
+ });
1012
+ }
1013
+ return {
1014
+ scope: "symbols",
1015
+ items,
1016
+ total: items.length,
1017
+ indexing: isIndexing(f)
1018
+ };
1019
+ }
1020
+ async function searchAll(query, opts = {}) {
1021
+ const limit = clampLimit(opts.limit);
1022
+ const [filesRes, contentRes, symbolsRes] = await Promise.all([
1023
+ searchFiles(query, {
1024
+ ...opts,
1025
+ limit
1026
+ }),
1027
+ searchContent(query, {
1028
+ ...opts,
1029
+ limit
1030
+ }),
1031
+ searchSymbols(query, {
1032
+ ...opts,
1033
+ limit
1034
+ })
1035
+ ]);
1036
+ const items = [];
1037
+ for (const hit of filesRes.items) items.push({
1038
+ kind: "file",
1039
+ hit
1040
+ });
1041
+ for (const hit of symbolsRes.items) items.push({
1042
+ kind: "symbol",
1043
+ hit
1044
+ });
1045
+ for (const hit of contentRes.items) items.push({
1046
+ kind: "text",
1047
+ hit
1048
+ });
1049
+ const total = (filesRes.total || 0) + (contentRes.total || 0) + (symbolsRes.total || 0);
1050
+ const indexing = filesRes.indexing || contentRes.indexing || symbolsRes.indexing;
1051
+ const error = filesRes.error || contentRes.error || symbolsRes.error;
1052
+ const regexError = contentRes.regexError;
1053
+ return {
1054
+ scope: "all",
1055
+ items: items.slice(0, limit),
1056
+ total,
1057
+ indexing,
1058
+ error,
1059
+ regexError
1060
+ };
1061
+ }
1062
+ async function getSearchStatus() {
1063
+ const f = await getFinder();
1064
+ if (!f) return {
1065
+ available: false,
1066
+ indexing: false,
1067
+ indexedFiles: 0,
1068
+ error: initError ?? "Search unavailable"
1069
+ };
1070
+ let indexedFiles = 0;
1071
+ let indexing = false;
1072
+ try {
1073
+ const prog = f.getScanProgress();
1074
+ if (prog.ok) {
1075
+ indexedFiles = prog.value.scannedFilesCount;
1076
+ indexing = prog.value.isScanning;
1077
+ }
1078
+ } catch {}
1079
+ return {
1080
+ available: true,
1081
+ indexing,
1082
+ indexedFiles
1083
+ };
1084
+ }
1085
+ /** Record that the user opened `path` from a query, improving fff's frecency
1086
+ * ranking for future searches. Best-effort, never throws. */
1087
+ async function trackSelection(query, path) {
1088
+ const f = await getFinder();
1089
+ if (!f) return;
1090
+ try {
1091
+ f.trackQuery(query, path);
1092
+ } catch {}
1093
+ }
1094
+ //#endregion
1095
+ //#region src/lib/settings.ts
1096
+ const CONFIG_DIR = join(homedir(), ".config", "diffit");
1097
+ const SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
1098
+ const DEFAULTS = {
1099
+ staged: true,
1100
+ untracked: true,
1101
+ diffStyle: "split",
1102
+ defaultTabSize: 4,
1103
+ theme: "nord",
1104
+ editorIDE: "default",
1105
+ lineDiffType: "word",
1106
+ lineWrap: false,
1107
+ diffIndicators: "classic",
1108
+ showLineNumbers: true,
1109
+ hunkSeparators: "line-info",
1110
+ lineHoverHighlight: "both",
1111
+ fontSize: 13,
1112
+ expandContextByDefault: false,
1113
+ collapsedContextThreshold: 10,
1114
+ expansionLineCount: 20,
1115
+ haptics: true
1116
+ };
1117
+ function loadSettings() {
1118
+ try {
1119
+ const data = readFileSync(SETTINGS_FILE, "utf-8");
1120
+ return {
1121
+ ...DEFAULTS,
1122
+ ...JSON.parse(data)
1123
+ };
1124
+ } catch {
1125
+ return { ...DEFAULTS };
1126
+ }
1127
+ }
1128
+ function saveSettings(settings) {
1129
+ const merged = {
1130
+ ...loadSettings(),
1131
+ ...settings
1132
+ };
1133
+ mkdirSync(CONFIG_DIR, { recursive: true });
1134
+ writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2));
1135
+ return merged;
1136
+ }
1137
+ //#endregion
1138
+ //#region src/lib/comments.ts
1139
+ var FileCommentStore = class {
1140
+ dirPath;
1141
+ filePath;
1142
+ constructor(customRepoRoot) {
1143
+ if (customRepoRoot) this.dirPath = join(customRepoRoot, ".diffit");
1144
+ else this.dirPath = getProjectStorageDir();
1145
+ this.filePath = join(this.dirPath, "comments.json");
1146
+ }
1147
+ async getAll() {
1148
+ try {
1149
+ const data = await readFile(this.filePath, "utf-8");
1150
+ return JSON.parse(data);
1151
+ } catch {
1152
+ return [];
1153
+ }
1154
+ }
1155
+ async save(comments) {
1156
+ try {
1157
+ await mkdir(this.dirPath, { recursive: true });
1158
+ try {
1159
+ const repoRoot = getRepoRoot();
1160
+ await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1161
+ } catch {}
1162
+ await writeFile(this.filePath, JSON.stringify(comments, null, 2), "utf-8");
1163
+ } catch (err) {
1164
+ console.error("Failed to save comments to file:", err);
1165
+ }
1166
+ }
1167
+ async add(comment) {
1168
+ const comments = await this.getAll();
1169
+ comments.push(comment);
1170
+ await this.save(comments);
1171
+ return comment;
1172
+ }
1173
+ async update(id, fields) {
1174
+ const comments = await this.getAll();
1175
+ const index = comments.findIndex((c) => c.id === id);
1176
+ if (index === -1) return null;
1177
+ const comment = comments[index];
1178
+ if (fields.body !== void 0) comment.body = fields.body;
1179
+ if (fields.status !== void 0) comment.status = fields.status;
1180
+ await this.save(comments);
1181
+ return comment;
1182
+ }
1183
+ async remove(id) {
1184
+ const comments = await this.getAll();
1185
+ const index = comments.findIndex((c) => c.id === id);
1186
+ if (index === -1) return false;
1187
+ comments.splice(index, 1);
1188
+ await this.save(comments);
1189
+ return true;
1190
+ }
1191
+ async addReply(commentId, reply) {
1192
+ const comments = await this.getAll();
1193
+ const index = comments.findIndex((c) => c.id === commentId);
1194
+ if (index === -1) return null;
1195
+ const comment = comments[index];
1196
+ if (!comment.replies) comment.replies = [];
1197
+ comment.replies.push(reply);
1198
+ await this.save(comments);
1199
+ return comment;
1200
+ }
1201
+ async removeReply(commentId, replyId) {
1202
+ const comments = await this.getAll();
1203
+ const index = comments.findIndex((c) => c.id === commentId);
1204
+ if (index === -1) return null;
1205
+ const comment = comments[index];
1206
+ const replyIndex = comment.replies.findIndex((r) => r.id === replyId);
1207
+ if (replyIndex === -1) return null;
1208
+ comment.replies.splice(replyIndex, 1);
1209
+ await this.save(comments);
1210
+ return comment;
1211
+ }
1212
+ async updateReply(commentId, replyId, body) {
1213
+ const comments = await this.getAll();
1214
+ const index = comments.findIndex((c) => c.id === commentId);
1215
+ if (index === -1) return null;
1216
+ const comment = comments[index];
1217
+ const reply = comment.replies.find((r) => r.id === replyId);
1218
+ if (!reply) return null;
1219
+ reply.body = body;
1220
+ await this.save(comments);
1221
+ return comment;
1222
+ }
1223
+ };
1224
+ //#endregion
1225
+ //#region src/lib/review-session.ts
1226
+ var ReviewSession = class {
1227
+ round = 0;
1228
+ lastSentAt = null;
1229
+ lastPayload = null;
1230
+ waiters = /* @__PURE__ */ new Set();
1231
+ onStatusChange;
1232
+ constructor(onStatusChange) {
1233
+ this.onStatusChange = onStatusChange;
1234
+ }
1235
+ /**
1236
+ * Block until the next `send()` or until `timeoutMs` elapses.
1237
+ *
1238
+ * If `sinceRound` is provided and is behind the current round, a send already
1239
+ * happened since the client last checked → resolve immediately with the
1240
+ * cached payload (the race guard). Otherwise register a waiter.
1241
+ */
1242
+ await(opts) {
1243
+ const { sinceRound, timeoutMs, signal } = opts;
1244
+ if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
1245
+ status: "released",
1246
+ payload: this.lastPayload
1247
+ });
1248
+ if (signal?.aborted) return Promise.resolve({
1249
+ status: "keep-waiting",
1250
+ round: this.round
1251
+ });
1252
+ return new Promise((resolve) => {
1253
+ const waiter = {
1254
+ resolve,
1255
+ timer: setTimeout(() => {
1256
+ this.removeWaiter(waiter);
1257
+ resolve({
1258
+ status: "keep-waiting",
1259
+ round: this.round
1260
+ });
1261
+ }, timeoutMs),
1262
+ cleanup: () => {}
1263
+ };
1264
+ if (signal) {
1265
+ const onAbort = () => {
1266
+ this.removeWaiter(waiter);
1267
+ resolve({
1268
+ status: "keep-waiting",
1269
+ round: this.round
1270
+ });
1271
+ };
1272
+ signal.addEventListener("abort", onAbort, { once: true });
1273
+ waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
1274
+ }
1275
+ this.waiters.add(waiter);
1276
+ this.emitStatus();
1277
+ });
1278
+ }
1279
+ /** Capture a review batch and release every blocked waiter. */
1280
+ send(input) {
1281
+ this.round += 1;
1282
+ this.lastSentAt = input.sentAt;
1283
+ const payload = {
1284
+ round: this.round,
1285
+ sentAt: input.sentAt,
1286
+ commentXml: input.commentXml,
1287
+ openCount: input.openCount,
1288
+ comments: input.comments
1289
+ };
1290
+ this.lastPayload = payload;
1291
+ const waiters = [...this.waiters];
1292
+ this.waiters.clear();
1293
+ for (const waiter of waiters) {
1294
+ clearTimeout(waiter.timer);
1295
+ waiter.cleanup();
1296
+ waiter.resolve({
1297
+ status: "released",
1298
+ payload
1299
+ });
1300
+ }
1301
+ this.emitStatus();
1302
+ return payload;
1303
+ }
1304
+ snapshot() {
1305
+ return {
1306
+ round: this.round,
1307
+ waiters: this.waiters.size,
1308
+ lastSentAt: this.lastSentAt
1309
+ };
1310
+ }
1311
+ removeWaiter(waiter) {
1312
+ if (!this.waiters.delete(waiter)) return;
1313
+ clearTimeout(waiter.timer);
1314
+ waiter.cleanup();
1315
+ this.emitStatus();
1316
+ }
1317
+ emitStatus() {
1318
+ this.onStatusChange?.(this.snapshot());
1319
+ }
1320
+ };
1321
+ //#endregion
1322
+ //#region src/server.ts
1323
+ const MIME_TYPES = {
1324
+ ".html": "text/html",
1325
+ ".js": "application/javascript",
1326
+ ".css": "text/css",
1327
+ ".json": "application/json",
1328
+ ".svg": "image/svg+xml",
1329
+ ".png": "image/png",
1330
+ ".ico": "image/x-icon",
1331
+ ".jpg": "image/jpeg",
1332
+ ".jpeg": "image/jpeg",
1333
+ ".gif": "image/gif",
1334
+ ".webp": "image/webp",
1335
+ ".bmp": "image/bmp",
1336
+ ".avif": "image/avif"
1337
+ };
1338
+ function isCustomMode(opts) {
1339
+ return opts.revisions.length > 0 || opts.pathspecs.length > 0;
1340
+ }
1341
+ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
1342
+ const app = new Hono();
1343
+ const customMode = isCustomMode(diffOpts);
1344
+ const store = commentStore ?? new FileCommentStore();
1345
+ const viewedFiles = /* @__PURE__ */ new Set();
1346
+ const activeClients = /* @__PURE__ */ new Set();
1347
+ const broadcast = (event, data) => {
1348
+ for (const send of activeClients) try {
1349
+ send(event, data);
1350
+ } catch {}
1351
+ };
1352
+ const reviewSession = new ReviewSession((snapshot) => broadcast("agent-status", JSON.stringify(snapshot)));
1353
+ let repoRoot;
1354
+ try {
1355
+ repoRoot = getRepoRoot();
1356
+ } catch {
1357
+ repoRoot = process.cwd();
1358
+ }
1359
+ if (!commentStore) try {
1360
+ const storageDir = getProjectStorageDir();
1361
+ mkdirSync(storageDir, { recursive: true });
1362
+ let commentsDebounce = null;
1363
+ watch(storageDir, (_eventType, filename) => {
1364
+ if (filename && !filename.startsWith("comments.json")) return;
1365
+ if (commentsDebounce) clearTimeout(commentsDebounce);
1366
+ commentsDebounce = setTimeout(() => broadcast("comments", Date.now().toString()), 120);
1367
+ }).unref();
1368
+ } catch (err) {
1369
+ console.warn("Failed to initialize comment store watcher:", err);
1370
+ }
1371
+ let debounceTimeout = null;
1372
+ try {
1373
+ watch(repoRoot, { recursive: true }, (eventType, filename) => {
1374
+ if (!filename) return;
1375
+ const parts = filename.split(/[/\\]/);
1376
+ const isGit = parts.includes(".git");
1377
+ const isNodeModules = parts.includes("node_modules");
1378
+ const isDist = parts.includes("dist");
1379
+ const isChangeset = parts.includes(".changeset");
1380
+ let shouldTrigger = false;
1381
+ if (isGit) {
1382
+ const isIndex = filename.endsWith("index");
1383
+ const isHead = filename.endsWith("HEAD");
1384
+ const isRefs = filename.includes("refs/") || filename.includes("refs\\");
1385
+ if (isIndex || isHead || isRefs) shouldTrigger = true;
1386
+ } else if (!isNodeModules && !isDist && !isChangeset) shouldTrigger = true;
1387
+ if (shouldTrigger) {
1388
+ if (debounceTimeout) clearTimeout(debounceTimeout);
1389
+ debounceTimeout = setTimeout(() => broadcast("change", Date.now().toString()), 200);
1390
+ }
1391
+ }).unref();
1392
+ } catch (err) {
1393
+ console.warn("Failed to initialize repository watcher:", err);
1394
+ }
1395
+ app.get("/api/live", async (c) => {
1396
+ return streamSSE(c, async (stream) => {
1397
+ const sendUpdate = (event, data) => {
1398
+ stream.writeSSE({
1399
+ event,
1400
+ data
1401
+ });
1402
+ };
1403
+ activeClients.add(sendUpdate);
1404
+ await stream.writeSSE({
1405
+ event: "heartbeat",
1406
+ data: Date.now().toString()
1407
+ });
1408
+ const heartbeatInterval = setInterval(() => {
1409
+ stream.writeSSE({
1410
+ event: "heartbeat",
1411
+ data: Date.now().toString()
1412
+ }).catch(() => {});
1413
+ }, 15e3);
1414
+ await new Promise((resolve) => {
1415
+ stream.onAbort(() => {
1416
+ clearInterval(heartbeatInterval);
1417
+ activeClients.delete(sendUpdate);
1418
+ resolve();
1419
+ });
1420
+ });
1421
+ });
1422
+ });
1423
+ app.get("/api/diff", async (c) => {
1424
+ const staged = c.req.query("staged") === "true";
1425
+ const untracked = c.req.query("untracked") === "true";
1426
+ const result = await executeDiffWithMeta(customMode ? diffOpts : {
1427
+ ...diffOpts,
1428
+ staged,
1429
+ includeUntracked: untracked
1430
+ });
1431
+ return c.json({
1432
+ patch: result.patch,
1433
+ repoName: result.repoName,
1434
+ branch: result.branch,
1435
+ customMode,
1436
+ binaryFiles: result.binaryFiles,
1437
+ tabSizeMap: result.tabSizeMap,
1438
+ untrackedFiles: result.untrackedFiles
1439
+ });
1440
+ });
1441
+ app.get("/api/file-content", (c) => {
1442
+ const path = c.req.query("path");
1443
+ const version = c.req.query("version");
1444
+ if (!path || !version) return c.json({ error: "Missing path or version" }, 400);
1445
+ const content = getFileContent(path, version);
1446
+ if (!content) return c.json({ error: "File not found" }, 404);
1447
+ const contentType = MIME_TYPES[extname(path)] || "application/octet-stream";
1448
+ return new Response(new Uint8Array(content), { headers: { "Content-Type": contentType } });
1449
+ });
1450
+ app.get("/api/file-text", (c) => {
1451
+ const path = c.req.query("path");
1452
+ const version = c.req.query("version");
1453
+ if (!path || !version) return c.json({ error: "Missing path or version" }, 400);
1454
+ const buffer = getFileContent(path, version);
1455
+ if (!buffer) return c.json({
1456
+ content: "",
1457
+ missing: true
1458
+ });
1459
+ const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
1460
+ for (let i = 0; i < sample.length; i++) if (sample[i] === 0) return c.json({ error: "Binary file" }, 415);
1461
+ return c.json({
1462
+ content: buffer.toString("utf-8"),
1463
+ missing: false
1464
+ });
1465
+ });
1466
+ app.post("/api/open-file", async (c) => {
1467
+ const { filePath, editor } = await c.req.json();
1468
+ if (!filePath) return c.json({ error: "filePath is required" }, 400);
1469
+ try {
1470
+ const root = getRepoRoot();
1471
+ if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1472
+ const absolutePath = resolve(root, filePath);
1473
+ const { exec } = await import("node:child_process");
1474
+ if (editor === "vscode") exec(`code "${absolutePath}"`);
1475
+ else if (editor === "zed") exec(`zed "${absolutePath}"`);
1476
+ else if (editor === "vim") exec(`osascript -e 'tell application "Terminal" to do script "vim \\"${absolutePath}\\""'`);
1477
+ else if (editor === "neovim") exec(`osascript -e 'tell application "Terminal" to do script "nvim \\"${absolutePath}\\""'`);
1478
+ else await (await import("open")).default(absolutePath);
1479
+ return c.json({ ok: true });
1480
+ } catch (err) {
1481
+ return c.json({ error: `Failed to open file: ${err.message}` }, 500);
1482
+ }
1483
+ });
1484
+ app.get("/api/repo-files", (c) => {
1485
+ try {
1486
+ return c.json({ files: listRepoFiles() });
1487
+ } catch (err) {
1488
+ return c.json({ error: err.message }, 500);
1489
+ }
1490
+ });
1491
+ app.post("/api/search", async (c) => {
1492
+ const body = await c.req.json().catch(() => ({}));
1493
+ const scope = body.scope ?? "files";
1494
+ const query = typeof body.query === "string" ? body.query : "";
1495
+ const limit = typeof body.limit === "number" ? body.limit : void 0;
1496
+ const paths = Array.isArray(body.changedPaths) ? body.changedPaths : void 0;
1497
+ try {
1498
+ if (scope === "all") return c.json(await searchAll(query, {
1499
+ limit,
1500
+ regex: !!body.regex,
1501
+ paths
1502
+ }));
1503
+ if (scope === "text") return c.json(await searchContent(query, {
1504
+ limit,
1505
+ regex: !!body.regex,
1506
+ paths
1507
+ }));
1508
+ if (scope === "symbols") return c.json(await searchSymbols(query, {
1509
+ limit,
1510
+ paths
1511
+ }));
1512
+ return c.json(await searchFiles(query, {
1513
+ limit,
1514
+ paths
1515
+ }));
1516
+ } catch (err) {
1517
+ return c.json({
1518
+ scope,
1519
+ items: [],
1520
+ total: 0,
1521
+ indexing: false,
1522
+ error: err?.message ?? "Search failed"
1523
+ }, 500);
1524
+ }
1525
+ });
1526
+ app.get("/api/search/status", async (c) => {
1527
+ return c.json(await getSearchStatus());
1528
+ });
1529
+ app.post("/api/search/track", async (c) => {
1530
+ const { query, path } = await c.req.json().catch(() => ({}));
1531
+ if (path) await trackSelection(query ?? "", path);
1532
+ return c.json({ ok: true });
1533
+ });
1534
+ app.get("/api/merge-status", (c) => {
1535
+ try {
1536
+ const status = getMergeStatus();
1537
+ return c.json(status);
1538
+ } catch (err) {
1539
+ return c.json({ error: err.message }, 500);
1540
+ }
1541
+ });
1542
+ app.post("/api/revert-hunk", async (c) => {
1543
+ const { filePath, hunkIndex } = await c.req.json();
1544
+ if (!filePath || typeof hunkIndex !== "number") return c.json({ error: "Missing filePath or hunkIndex" }, 400);
1545
+ try {
1546
+ if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1547
+ revertHunk(filePath, hunkIndex);
1548
+ return c.json({ ok: true });
1549
+ } catch (err) {
1550
+ const stderr = typeof err?.stderr === "string" ? err.stderr : err?.stderr instanceof Buffer ? err.stderr.toString("utf-8") : "";
1551
+ return c.json({ error: stderr || err?.message || "Failed to revert hunk" }, 500);
1552
+ }
1553
+ });
1554
+ app.get("/api/hunk-history", async (c) => {
1555
+ const filePath = c.req.query("filePath");
1556
+ const deletionStart = Number(c.req.query("deletionStart"));
1557
+ const deletionCount = Number(c.req.query("deletionCount"));
1558
+ if (!filePath || Number.isNaN(deletionStart) || Number.isNaN(deletionCount)) return c.json({ error: "Missing or invalid parameters" }, 400);
1559
+ try {
1560
+ if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1561
+ const history = getHunkHistory(filePath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
1562
+ return c.json(history);
1563
+ } catch (err) {
1564
+ return c.json({ error: err.message || "Failed to fetch hunk history" }, 500);
1565
+ }
1566
+ });
1567
+ app.post("/api/save-file", async (c) => {
1568
+ const { filePath, content, gitAdd } = await c.req.json();
1569
+ if (!filePath || typeof content !== "string") return c.json({ error: "Missing filePath or content" }, 400);
1570
+ try {
1571
+ const root = getRepoRoot();
1572
+ if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1573
+ await writeFile(resolve(root, filePath), content, "utf-8");
1574
+ if (gitAdd) try {
1575
+ gitAddFile(filePath);
1576
+ } catch (err) {
1577
+ return c.json({
1578
+ ok: true,
1579
+ gitAddError: err.message
1580
+ });
1581
+ }
1582
+ return c.json({ ok: true });
1583
+ } catch (err) {
1584
+ return c.json({ error: `Failed to save file: ${err.message}` }, 500);
1585
+ }
1586
+ });
1587
+ app.get("/api/settings", (c) => {
1588
+ return c.json(loadSettings());
1589
+ });
1590
+ app.put("/api/settings", async (c) => {
1591
+ const settings = saveSettings(await c.req.json());
1592
+ return c.json(settings);
1593
+ });
1594
+ app.get("/api/viewed", (c) => {
1595
+ return c.json([...viewedFiles]);
1596
+ });
1597
+ app.put("/api/viewed", async (c) => {
1598
+ const { filePath, viewed } = await c.req.json();
1599
+ if (viewed) viewedFiles.add(filePath);
1600
+ else viewedFiles.delete(filePath);
1601
+ return c.json({ ok: true });
1602
+ });
1603
+ app.get("/api/comments", async (c) => {
1604
+ const comments = await store.getAll();
1605
+ return c.json(comments);
1606
+ });
1607
+ app.post("/api/comments", async (c) => {
1608
+ const body = await c.req.json();
1609
+ const comment = {
1610
+ id: crypto.randomUUID(),
1611
+ filePath: body.filePath,
1612
+ side: body.side,
1613
+ lineNumber: body.lineNumber,
1614
+ startLineNumber: body.startLineNumber,
1615
+ lineContent: body.lineContent,
1616
+ body: body.body,
1617
+ status: "open",
1618
+ createdAt: Date.now(),
1619
+ replies: []
1620
+ };
1621
+ const created = await store.add(comment);
1622
+ return c.json(created, 201);
1623
+ });
1624
+ app.put("/api/comments/:id", async (c) => {
1625
+ const id = c.req.param("id");
1626
+ const { body, status } = await c.req.json();
1627
+ const updated = await store.update(id, {
1628
+ body,
1629
+ status
1630
+ });
1631
+ if (!updated) return c.json({ error: "Comment not found" }, 404);
1632
+ return c.json(updated);
1633
+ });
1634
+ app.post("/api/comments/:id/replies", async (c) => {
1635
+ const commentId = c.req.param("id");
1636
+ const { body, role, model } = await c.req.json();
1637
+ const reply = {
1638
+ id: crypto.randomUUID(),
1639
+ body,
1640
+ createdAt: Date.now(),
1641
+ role: role || (model ? "agent" : "user"),
1642
+ model: model || void 0
1643
+ };
1644
+ const updated = await store.addReply(commentId, reply);
1645
+ if (!updated) return c.json({ error: "Comment not found" }, 404);
1646
+ return c.json(updated);
1647
+ });
1648
+ app.delete("/api/comments/:id/replies/:replyId", async (c) => {
1649
+ const commentId = c.req.param("id");
1650
+ const replyId = c.req.param("replyId");
1651
+ const updated = await store.removeReply(commentId, replyId);
1652
+ if (!updated) return c.json({ error: "Comment or reply not found" }, 404);
1653
+ return c.json(updated);
1654
+ });
1655
+ app.put("/api/comments/:id/replies/:replyId", async (c) => {
1656
+ const commentId = c.req.param("id");
1657
+ const replyId = c.req.param("replyId");
1658
+ const { body } = await c.req.json();
1659
+ if (!body) return c.json({ error: "Body is required" }, 400);
1660
+ const updated = await store.updateReply(commentId, replyId, body);
1661
+ if (!updated) return c.json({ error: "Comment or reply not found" }, 404);
1662
+ return c.json(updated);
1663
+ });
1664
+ app.post("/api/comments/:id/apply-suggestion", async (c) => {
1665
+ const id = c.req.param("id");
1666
+ const comment = (await store.getAll()).find((c) => c.id === id);
1667
+ if (!comment) return c.json({ error: "Comment not found" }, 404);
1668
+ if (comment.side !== "additions") return c.json({ error: "Suggestions can only be applied to added or modified lines" }, 400);
1669
+ const match = comment.body.match(/```suggestion\n([\s\S]*?)```/);
1670
+ if (!match) return c.json({ error: "No suggestion block found in comment body" }, 400);
1671
+ const suggestion = match[1];
1672
+ try {
1673
+ const root = getRepoRoot();
1674
+ if (!isSafePath(comment.filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1675
+ const absolutePath = resolve(root, comment.filePath);
1676
+ const lines = (await readFile(absolutePath, "utf-8")).split(/\r?\n/);
1677
+ const lineIdx = comment.lineNumber - 1;
1678
+ if (lineIdx < 0 || lineIdx >= lines.length) return c.json({ error: "Comment line number out of range" }, 400);
1679
+ lines[lineIdx] = suggestion.trimEnd();
1680
+ await writeFile(absolutePath, lines.join("\n"), "utf-8");
1681
+ await store.update(id, { status: "resolved" });
1682
+ return c.json({ ok: true });
1683
+ } catch (err) {
1684
+ return c.json({ error: `Failed to apply suggestion: ${err.message}` }, 500);
1685
+ }
1686
+ });
1687
+ app.delete("/api/comments/:id", async (c) => {
1688
+ const id = c.req.param("id");
1689
+ if (!await store.remove(id)) return c.json({ error: "Comment not found" }, 404);
1690
+ return c.json({ ok: true });
1691
+ });
1692
+ app.post("/api/review/send", async (c) => {
1693
+ const body = await c.req.json().catch(() => ({}));
1694
+ const generalComment = typeof body?.generalComment === "string" ? body.generalComment : void 0;
1695
+ const all = await store.getAll();
1696
+ const openCount = all.filter((x) => x.status === "open").length;
1697
+ const payload = reviewSession.send({
1698
+ sentAt: Date.now(),
1699
+ commentXml: formatComments(all, generalComment),
1700
+ openCount,
1701
+ comments: all
1702
+ });
1703
+ return c.json({
1704
+ ok: true,
1705
+ round: payload.round,
1706
+ openCount: payload.openCount,
1707
+ waiters: reviewSession.snapshot().waiters
1708
+ });
1709
+ });
1710
+ app.get("/api/review/await", async (c) => {
1711
+ const sinceRaw = c.req.query("sinceRound");
1712
+ const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
1713
+ const requested = Number(c.req.query("timeoutMs")) || 25e3;
1714
+ const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
1715
+ const result = await reviewSession.await({
1716
+ sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
1717
+ timeoutMs,
1718
+ signal: c.req.raw.signal
1719
+ });
1720
+ return c.json(result);
1721
+ });
1722
+ app.get("/api/review/status", (c) => {
1723
+ return c.json(reviewSession.snapshot());
1724
+ });
1725
+ app.post("/api/attachments", async (c) => {
1726
+ const file = (await c.req.parseBody())["file"];
1727
+ if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
1728
+ try {
1729
+ const storageDir = getProjectStorageDir();
1730
+ const attachmentsDir = join(storageDir, "attachments");
1731
+ await mkdir(attachmentsDir, { recursive: true });
1732
+ const repoRoot = getRepoRoot();
1733
+ await writeFile(join(storageDir, "repo_path.txt"), repoRoot, "utf-8");
1734
+ const ext = extname(file.name) || ".png";
1735
+ const filename = `pasted_image_${crypto.randomUUID()}${ext}`;
1736
+ const absolutePath = join(attachmentsDir, filename);
1737
+ const arrayBuffer = await file.arrayBuffer();
1738
+ await writeFile(absolutePath, new Uint8Array(arrayBuffer));
1739
+ return c.json({ url: `/api/attachments/${filename}` });
1740
+ } catch (err) {
1741
+ return c.json({ error: `Failed to save attachment: ${err.message}` }, 500);
1742
+ }
1743
+ });
1744
+ app.get("/api/attachments/:filename", async (c) => {
1745
+ const filename = c.req.param("filename");
1746
+ const attachmentsDir = join(getProjectStorageDir(), "attachments");
1747
+ const absolutePath = resolve(attachmentsDir, filename);
1748
+ if (!absolutePath.startsWith(attachmentsDir)) return c.text("Forbidden", 403);
1749
+ try {
1750
+ const content = await readFile(absolutePath);
1751
+ const contentType = MIME_TYPES[extname(absolutePath)] || "application/octet-stream";
1752
+ return new Response(content, { headers: { "Content-Type": contentType } });
1753
+ } catch {
1754
+ return c.text("Attachment not found", 404);
1755
+ }
1756
+ });
1757
+ app.get("/*", async (c) => {
1758
+ let filePath = c.req.path;
1759
+ if (filePath === "/") filePath = "/index.html";
1760
+ const relativePath = filePath.slice(1);
1761
+ if (!isSafePath(relativePath, clientDir)) return c.text("Forbidden", 403);
1762
+ const fullPath = resolve(clientDir, relativePath);
1763
+ try {
1764
+ const content = await readFile(fullPath);
1765
+ const contentType = MIME_TYPES[extname(fullPath)] || "application/octet-stream";
1766
+ return new Response(content, { headers: { "Content-Type": contentType } });
1767
+ } catch {
1768
+ const indexContent = await readFile(join(clientDir, "index.html"));
1769
+ return new Response(indexContent, { headers: { "Content-Type": "text/html" } });
1770
+ }
1771
+ });
1772
+ return app;
1773
+ }
1774
+ async function cleanupStaleProjects() {
1775
+ const baseDir = join(homedir(), ".diffit");
1776
+ if (!existsSync(baseDir)) return;
1777
+ try {
1778
+ const entries = await readdir(baseDir, { withFileTypes: true });
1779
+ const now = Date.now();
1780
+ const STALE_TIME = 336 * 60 * 60 * 1e3;
1781
+ for (const entry of entries) if (entry.isDirectory()) {
1782
+ const projectDir = join(baseDir, entry.name);
1783
+ const repoPathFile = join(projectDir, "repo_path.txt");
1784
+ const commentsFile = join(projectDir, "comments.json");
1785
+ let shouldDelete = false;
1786
+ if (existsSync(repoPathFile)) try {
1787
+ const repoPath = (await readFile(repoPathFile, "utf-8")).trim();
1788
+ if (!repoPath || !existsSync(repoPath)) shouldDelete = true;
1789
+ } catch {}
1790
+ if (!shouldDelete && existsSync(commentsFile)) try {
1791
+ if (now - (await stat(commentsFile)).mtimeMs > STALE_TIME) shouldDelete = true;
1792
+ } catch {}
1793
+ if (shouldDelete) await rm(projectDir, {
1794
+ recursive: true,
1795
+ force: true
1796
+ });
1797
+ }
1798
+ } catch (err) {
1799
+ console.error("Failed to cleanup stale projects:", err);
1800
+ }
1801
+ }
1802
+ function startServer(options) {
1803
+ cleanupStaleProjects().catch((err) => {
1804
+ console.error("Failed to clean up stale projects:", err);
1805
+ });
1806
+ const app = createApp(options.clientDir, options.diffOpts);
1807
+ return new Promise((resolve) => {
1808
+ serve({
1809
+ fetch: app.fetch,
1810
+ port: options.port,
1811
+ hostname: options.host
1812
+ }, (info) => {
1813
+ resolve({ port: info.port });
1814
+ });
1815
+ });
1816
+ }
1817
+ //#endregion
1818
+ //#region src/cli.ts
1819
+ const args = process.argv.slice(2);
1820
+ if (new Set([
1821
+ "await-review",
1822
+ "reply",
1823
+ "resolve",
1824
+ "comments",
1825
+ "url",
1826
+ "mcp"
1827
+ ]).has(args[0])) {
1828
+ if (args[0] === "mcp") {
1829
+ const { startMcpServer } = await import("./mcp-nMwTAut3.mjs");
1830
+ await startMcpServer();
1831
+ await new Promise(() => {});
1832
+ }
1833
+ const { runSubcommand } = await import("./cli-agent-CtNivjQW.mjs");
1834
+ process.exit(await runSubcommand(args[0], args.slice(1)));
1835
+ }
1836
+ const opts = parseDiffOptions(args);
1837
+ if (opts.help) {
1838
+ printHelp();
1839
+ process.exit(0);
1840
+ }
1841
+ if (opts.version) {
1842
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1843
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"));
1844
+ console.log(pkg.version);
1845
+ process.exit(0);
1846
+ }
1847
+ const envErr = validateEnvironment();
1848
+ if (envErr) {
1849
+ console.error(envErr);
1850
+ process.exit(1);
1851
+ }
1852
+ if (opts.outputMode === "terminal") {
1853
+ const exitCode = runTerminalDiff(opts);
1854
+ process.exit(exitCode);
1855
+ }
1856
+ const port = await getPort(opts.port ? { port: opts.port } : void 0);
1857
+ const host = opts.host;
1858
+ const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "client");
1859
+ const { existsSync: existsSync$1 } = await import("node:fs");
1860
+ const { port: actualPort } = await startServer({
1861
+ port,
1862
+ host,
1863
+ clientDir: existsSync$1(clientDir) ? clientDir : resolve(process.cwd(), "dist/client"),
1864
+ diffOpts: opts
1865
+ });
1866
+ const localUrl = `http://${host}:${actualPort}`;
1867
+ try {
1868
+ let repoRoot;
1869
+ try {
1870
+ repoRoot = getRepoRoot();
1871
+ } catch {
1872
+ repoRoot = process.cwd();
1873
+ }
1874
+ const __pkgDir = dirname(fileURLToPath(import.meta.url));
1875
+ const version = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
1876
+ writeServerLock({
1877
+ port: actualPort,
1878
+ host,
1879
+ pid: process.pid,
1880
+ repoRoot,
1881
+ startedAt: Date.now(),
1882
+ version
1883
+ });
1884
+ } catch {}
1885
+ console.log(`diffit server running at ${localUrl}`);
1886
+ if (!opts.noOpen) {
1887
+ const settings = loadSettings();
1888
+ const openUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
1889
+ const openModule = await import("open");
1890
+ let appName;
1891
+ if (settings.browser) appName = openModule.apps[settings.browser] || settings.browser;
1892
+ const options = appName ? { app: { name: appName } } : {};
1893
+ openModule.default(openUrl, options);
1894
+ }
1895
+ const shutdown = () => {
1896
+ console.log("\nShutting down...");
1897
+ removeServerLock();
1898
+ process.exit(0);
1899
+ };
1900
+ process.on("SIGINT", shutdown);
1901
+ process.on("SIGTERM", shutdown);
1902
+ //#endregion
1903
+ export {};