diffing 0.6.0 → 0.6.2

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 (154) hide show
  1. package/README.md +4 -0
  2. package/dist/cli-agent-BkkRbKqq.mjs +513 -0
  3. package/dist/cli-gh-DgqscIkg.mjs +147 -0
  4. package/dist/cli.mjs +4 -4
  5. package/dist/client/assets/{angular-html-BotC7B4-.js → angular-html-Dg8frIKA.js} +1 -1
  6. package/dist/client/assets/{angular-ts-DQTHeTJm.js → angular-ts-BgXWv16g.js} +1 -1
  7. package/dist/client/assets/{apl-Bwz5Dpd_.js → apl-uFl-_xI6.js} +1 -1
  8. package/dist/client/assets/{arc-D-TSgv-7.js → arc-2DyFlUoQ.js} +1 -1
  9. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DLe25t8t.js → architectureDiagram-3BPJPVTR-Cpo5XnQh.js} +1 -1
  10. package/dist/client/assets/{astro-DjkoD2fR.js → astro-CsSgohj_.js} +1 -1
  11. package/dist/client/assets/{blade-Cap2uJIA.js → blade-tmMbmea0.js} +1 -1
  12. package/dist/client/assets/{blockDiagram-GPEHLZMM-B-bihLYO.js → blockDiagram-GPEHLZMM-C2j4VeON.js} +1 -1
  13. package/dist/client/assets/{c-C7JoAATL.js → c-CYFchk-Y.js} +1 -1
  14. package/dist/client/assets/{c4Diagram-AAUBKEIU-CoCcjXEC.js → c4Diagram-AAUBKEIU-B9Iu60rg.js} +1 -1
  15. package/dist/client/assets/channel-Br3gV-0C.js +1 -0
  16. package/dist/client/assets/{chunk-2J33WTMH-Sm_uQrGq.js → chunk-2J33WTMH-CYF5wgmI.js} +1 -1
  17. package/dist/client/assets/{chunk-3OPIFGDE-Ct83Sp4P.js → chunk-3OPIFGDE-BsCKEfSk.js} +1 -1
  18. package/dist/client/assets/{chunk-4BX2VUAB-DrqChk1a.js → chunk-4BX2VUAB-CKaohpWW.js} +1 -1
  19. package/dist/client/assets/{chunk-55IACEB6-CodeFb9E.js → chunk-55IACEB6-CPg0c8fY.js} +1 -1
  20. package/dist/client/assets/{chunk-5ZQYHXKU-CpEx4dmS.js → chunk-5ZQYHXKU-qstslfP1.js} +1 -1
  21. package/dist/client/assets/{chunk-727SXJPM-D3U6TUmk.js → chunk-727SXJPM-C1WRwOi2.js} +1 -1
  22. package/dist/client/assets/{chunk-AQP2D5EJ-Dr-_DyBy.js → chunk-AQP2D5EJ-_jbfFh_H.js} +1 -1
  23. package/dist/client/assets/{chunk-BSJP7CBP-Bnyml8mU.js → chunk-BSJP7CBP-CjQkUxwj.js} +1 -1
  24. package/dist/client/assets/{chunk-CSCIHK7Q-BJSFGTlk.js → chunk-CSCIHK7Q-D4khYgW5.js} +1 -1
  25. package/dist/client/assets/{chunk-FMBD7UC4-BqiwDIwx.js → chunk-FMBD7UC4-B1r9As4X.js} +1 -1
  26. package/dist/client/assets/{chunk-KSCS5N6A-D-H-HBRn.js → chunk-KSCS5N6A-XehlazPT.js} +1 -1
  27. package/dist/client/assets/{chunk-L5ZTLDWV-CVMSAF7h.js → chunk-L5ZTLDWV-5KMCRmgT.js} +1 -1
  28. package/dist/client/assets/{chunk-LZXEDZCA-BQsF6HF3.js → chunk-LZXEDZCA--x7aE-36.js} +2 -2
  29. package/dist/client/assets/{chunk-ND2GUHAM-eoM20ySO.js → chunk-ND2GUHAM-BcUeNQF6.js} +1 -1
  30. package/dist/client/assets/{chunk-NZK2D7GU-B1iU0i4a.js → chunk-NZK2D7GU-HdA4zA81.js} +1 -1
  31. package/dist/client/assets/{chunk-O5CBEL6O-CcIPuHUC.js → chunk-O5CBEL6O-BbkpkgBW.js} +1 -1
  32. package/dist/client/assets/chunk-QZHKN3VN-yNyHmFLj.js +1 -0
  33. package/dist/client/assets/chunk-WU5MYG2G-DOSgjhZV.js +1 -0
  34. package/dist/client/assets/{chunk-XPW4576I-jSj3E63-.js → chunk-XPW4576I-BrjJtb92.js} +1 -1
  35. package/dist/client/assets/classDiagram-4FO5ZUOK-BV8x1oiD.js +1 -0
  36. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-AibFxRy2.js +1 -0
  37. package/dist/client/assets/{cobol-DkiK_msx.js → cobol-CbEdzJpe.js} +1 -1
  38. package/dist/client/assets/{coffee-f7z4wIqP.js → coffee-BYBPPvKf.js} +1 -1
  39. package/dist/client/assets/{cose-bilkent-S5V4N54A-Dikk-RXu.js → cose-bilkent-S5V4N54A-C2UlmvWG.js} +1 -1
  40. package/dist/client/assets/{cpp-DB_FV8ps.js → cpp-Cy1Wnp8d.js} +1 -1
  41. package/dist/client/assets/{crystal-BRBnyZFz.js → crystal-Bg1sUJya.js} +1 -1
  42. package/dist/client/assets/{css-GpNBLqRF.js → css-C6iyzRBi.js} +1 -1
  43. package/dist/client/assets/{dagre-BM42HDAG-tErJNBcm.js → dagre-BM42HDAG-CLP5IYO3.js} +1 -1
  44. package/dist/client/assets/{diagram-2AECGRRQ-D0-DgitC.js → diagram-2AECGRRQ-Dg8GXMUD.js} +1 -1
  45. package/dist/client/assets/{diagram-5GNKFQAL-DqMjQTtW.js → diagram-5GNKFQAL-D5eskvC7.js} +1 -1
  46. package/dist/client/assets/{diagram-KO2AKTUF-CP7V9tqS.js → diagram-KO2AKTUF-SO-9Apcj.js} +1 -1
  47. package/dist/client/assets/{diagram-LMA3HP47-tCGdsz4p.js → diagram-LMA3HP47-Dz15-yRy.js} +1 -1
  48. package/dist/client/assets/{diagram-OG6HWLK6-DtDnaqso.js → diagram-OG6HWLK6-BuQK0W1m.js} +1 -1
  49. package/dist/client/assets/{dist-B5Eb-OKZ.js → dist-CZ555WMx.js} +1 -1
  50. package/dist/client/assets/{edge-DqnWvViE.js → edge-CmOR4HCT.js} +1 -1
  51. package/dist/client/assets/{elixir-BSuxOIIa.js → elixir-CK9qhOZG.js} +1 -1
  52. package/dist/client/assets/{elm-B6U1Is0L.js → elm-DZWlTp8-.js} +1 -1
  53. package/dist/client/assets/{erDiagram-TEJ5UH35-C5S0U2x3.js → erDiagram-TEJ5UH35-D7vADGZy.js} +1 -1
  54. package/dist/client/assets/{erb-kK3QM18H.js → erb-CTTGdXJ0.js} +1 -1
  55. package/dist/client/assets/{flowDiagram-I6XJVG4X-NNqmBYk2.js → flowDiagram-I6XJVG4X-BwcgAtP3.js} +1 -1
  56. package/dist/client/assets/{ganttDiagram-6RSMTGT7-8ng09X8S.js → ganttDiagram-6RSMTGT7-kInginE9.js} +1 -1
  57. package/dist/client/assets/{git-rebase-BDKTWtxr.js → git-rebase-B_sSbVaY.js} +1 -1
  58. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-D_HBvwVl.js → gitGraphDiagram-PVQCEYII-C4pDDMLO.js} +1 -1
  59. package/dist/client/assets/{glimmer-js-BthGlRra.js → glimmer-js-BaLXlo90.js} +1 -1
  60. package/dist/client/assets/{glimmer-ts-CS8Z9z2h.js → glimmer-ts-ugNWY62Y.js} +1 -1
  61. package/dist/client/assets/{glsl-C5ERhU2v.js → glsl-CDofiyvf.js} +1 -1
  62. package/dist/client/assets/{graphql-sfgTqbIp.js → graphql-lRTsx6gb.js} +1 -1
  63. package/dist/client/assets/{hack-EeOmOf4N.js → hack-DG646xxf.js} +1 -1
  64. package/dist/client/assets/{haml-DVf5-Ndq.js → haml-CjNyEqTY.js} +1 -1
  65. package/dist/client/assets/{handlebars-BSLgW49o.js → handlebars-DmTlsycc.js} +1 -1
  66. package/dist/client/assets/{html-DoOSf-hz.js → html-CYKa0iGt.js} +1 -1
  67. package/dist/client/assets/{html-derivative-BM0DLd1Z.js → html-derivative-CJrmldEy.js} +1 -1
  68. package/dist/client/assets/{http-d2p18QI6.js → http-Dv_sB6KO.js} +1 -1
  69. package/dist/client/assets/{hurl-CH2I2f66.js → hurl-aIa6gC3s.js} +1 -1
  70. package/dist/client/assets/index-CkjX2qPW.css +1 -0
  71. package/dist/client/assets/{index-CJI1E0bV.js → index-DLwFuHoH.js} +96 -101
  72. package/dist/client/assets/{infoDiagram-5YYISTIA-Cgg2FsCN.js → infoDiagram-5YYISTIA-DE1yaqeG.js} +1 -1
  73. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-pYRQFecr.js → ishikawaDiagram-YF4QCWOH-D61KC690.js} +1 -1
  74. package/dist/client/assets/{java-BFiqhlWm.js → java--GCJC5Ht.js} +1 -1
  75. package/dist/client/assets/{javascript-YWa7zRPZ.js → javascript-CLU-vrsL.js} +1 -1
  76. package/dist/client/assets/{jinja-h_UdmDAn.js → jinja-DpKxO4hN.js} +1 -1
  77. package/dist/client/assets/{jison-cOvcjPYz.js → jison-DAzEKgwk.js} +1 -1
  78. package/dist/client/assets/{journeyDiagram-JHISSGLW-D3EBlW7m.js → journeyDiagram-JHISSGLW-Bt37oidm.js} +1 -1
  79. package/dist/client/assets/{json-DT0GjoRi.js → json-CoxC1kMn.js} +1 -1
  80. package/dist/client/assets/{jsx-BaYjgZVH.js → jsx-Bjv8cqXv.js} +1 -1
  81. package/dist/client/assets/{julia-Cg36ySxC.js → julia-2HI5Piu6.js} +1 -1
  82. package/dist/client/assets/{just-KYC4U1y4.js → just-AUrPjdB4.js} +1 -1
  83. package/dist/client/assets/{kanban-definition-UN3LZRKU-DA42mt4M.js → kanban-definition-UN3LZRKU-D46QdRYJ.js} +1 -1
  84. package/dist/client/assets/{latex-DpuXxlsH.js → latex-DjzDlfkY.js} +1 -1
  85. package/dist/client/assets/{line-D91vf_Yl.js → line-D_rKDTVX.js} +1 -1
  86. package/dist/client/assets/{linear-CL7WzrL5.js → linear-DCm3ZWOL.js} +1 -1
  87. package/dist/client/assets/{liquid-Dua3sjBU.js → liquid-DZ8JEV93.js} +1 -1
  88. package/dist/client/assets/{lua-CpDMEbOh.js → lua-iKYCK6sd.js} +1 -1
  89. package/dist/client/assets/{marko-rM0rxrJ5.js → marko-CrhrBZpj.js} +1 -1
  90. package/dist/client/assets/{mdc-D-E4W6ne.js → mdc-M6YkRqT_.js} +1 -1
  91. package/dist/client/assets/{mermaid-parser.core-BLQDdrsf.js → mermaid-parser.core-DOlZkAUl.js} +1 -1
  92. package/dist/client/assets/{mermaid.core-BBR2Otc_.js → mermaid.core-B5kraaYr.js} +3 -3
  93. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DMrApv-_.js → mindmap-definition-RKZ34NQL-CG-S_0Tm.js} +1 -1
  94. package/dist/client/assets/{nginx-wZWgcQLM.js → nginx-DGSv66H3.js} +1 -1
  95. package/dist/client/assets/{nim-Bh7OZsZh.js → nim-wTb9iFPK.js} +1 -1
  96. package/dist/client/assets/{perl-DtEqNC_V.js → perl-BrLOtih-.js} +1 -1
  97. package/dist/client/assets/{php-DX7bHce8.js → php-BpG59zED.js} +1 -1
  98. package/dist/client/assets/{pieDiagram-4H26LBE5-B2f9_PO6.js → pieDiagram-4H26LBE5-BSsvT950.js} +1 -1
  99. package/dist/client/assets/{pug-r67inScB.js → pug-L7QegceF.js} +1 -1
  100. package/dist/client/assets/{qml-f1bOqUwX.js → qml-Bhe1hZ7B.js} +1 -1
  101. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DgdKno5G.js → quadrantDiagram-W4KKPZXB-Bxs1ZIId.js} +1 -1
  102. package/dist/client/assets/{r-Cpm-_vkr.js → r-D1TBw_pw.js} +1 -1
  103. package/dist/client/assets/{razor-CEymfLfo.js → razor-CQm1uFKr.js} +1 -1
  104. package/dist/client/assets/{regexp-DCnAJwuE.js → regexp--jRne08M.js} +1 -1
  105. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BIRl56-J.js → requirementDiagram-4Y6WPE33-BvrwIHRn.js} +1 -1
  106. package/dist/client/assets/{rst-Cm1M2uc8.js → rst-DojkdR69.js} +1 -1
  107. package/dist/client/assets/{ruby-B49kkGT0.js → ruby-CihGsdWE.js} +1 -1
  108. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-7XZZmV9N.js → sankeyDiagram-5OEKKPKP-ChVx7-uL.js} +1 -1
  109. package/dist/client/assets/{sas-BOXC9Iq4.js → sas-De9KOrSF.js} +1 -1
  110. package/dist/client/assets/{scss-DU2HGWZP.js → scss-s1VsrZAM.js} +1 -1
  111. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CtIjo3i9.js → sequenceDiagram-3UESZ5HK-DfGXu7Db.js} +1 -1
  112. package/dist/client/assets/{shellscript-YFjx1NBH.js → shellscript-BQarhH5B.js} +1 -1
  113. package/dist/client/assets/{shellsession-4zfyiKm-.js → shellsession-DudKTKQ_.js} +1 -1
  114. package/dist/client/assets/{soy-CRExh-ql.js → soy-CQggW1_P.js} +1 -1
  115. package/dist/client/assets/{sql-2Iw6Ta1F.js → sql-CyxjmxEX.js} +1 -1
  116. package/dist/client/assets/{src-Dn3_YOHM.js → src-Be3tFbq6.js} +1 -1
  117. package/dist/client/assets/{stata-DflAcpzB.js → stata-Baq2oukf.js} +1 -1
  118. package/dist/client/assets/{stateDiagram-AJRCARHV-D_w9F0BN.js → stateDiagram-AJRCARHV-DUsSkWi-.js} +1 -1
  119. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DczGQieS.js +1 -0
  120. package/dist/client/assets/{surrealql-CMprrxj1.js → surrealql-DY_lPQU6.js} +1 -1
  121. package/dist/client/assets/{svelte-Bb_Bxt4a.js → svelte-oBaORrzD.js} +1 -1
  122. package/dist/client/assets/{templ-MJ_5gYAf.js → templ-WpkyVGPg.js} +1 -1
  123. package/dist/client/assets/{tex-BQ33E56K.js → tex-t_gMyObp.js} +1 -1
  124. package/dist/client/assets/{timeline-definition-PNZ67QCA-BzgsTjf8.js → timeline-definition-PNZ67QCA-DO30eyIk.js} +1 -1
  125. package/dist/client/assets/{ts-tags-Cddttr6f.js → ts-tags-Dn6NjJLA.js} +1 -1
  126. package/dist/client/assets/{tsx-BFchi_eS.js → tsx-DDWp_p5y.js} +1 -1
  127. package/dist/client/assets/{twig-CA8ssIYI.js → twig-FBY22xUa.js} +1 -1
  128. package/dist/client/assets/{typescript-tdiTR9sY.js → typescript-BHZBosYe.js} +1 -1
  129. package/dist/client/assets/{vennDiagram-CIIHVFJN-CMjPo4Qf.js → vennDiagram-CIIHVFJN-DbE8UKE1.js} +1 -1
  130. package/dist/client/assets/{vue-BbZNP0Rd.js → vue-CrOmWBlh.js} +1 -1
  131. package/dist/client/assets/{vue-html-02g5ipYx.js → vue-html-jBuGpXgg.js} +1 -1
  132. package/dist/client/assets/{vue-vine-9lCbaeLR.js → vue-vine-DEzqOkvG.js} +1 -1
  133. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cy6qwCgU.js → wardleyDiagram-YWT4CUSO-BqhSY5Qs.js} +1 -1
  134. package/dist/client/assets/{xml-BPPCB2P8.js → xml-DkWImCZV.js} +1 -1
  135. package/dist/client/assets/{xsl-1-u1Beet.js → xsl-BotuNSYU.js} +1 -1
  136. package/dist/client/assets/{xychartDiagram-2RQKCTM6-q9ysdRiS.js → xychartDiagram-2RQKCTM6-DA2pXGD9.js} +1 -1
  137. package/dist/client/assets/{yaml-79W-bav7.js → yaml-8lMTQUH5.js} +1 -1
  138. package/dist/client/favicon.svg +40 -0
  139. package/dist/client/index.html +7 -3
  140. package/dist/mcp-BgZO-iXI.mjs +1047 -0
  141. package/dist/mcp-BxBm3d5l.mjs +1047 -0
  142. package/dist/mcp-CphIUJUg.mjs +1047 -0
  143. package/dist/server-BZBFquG8.mjs +3875 -0
  144. package/dist/server-Bmt57wV5.mjs +3875 -0
  145. package/dist/server-D2xEbakj.mjs +3874 -0
  146. package/dist/server-lock-DaSx7DjW.mjs +1049 -0
  147. package/package.json +1 -1
  148. package/dist/client/assets/channel-h7e2aSOW.js +0 -1
  149. package/dist/client/assets/chunk-QZHKN3VN-HzjMGBiA.js +0 -1
  150. package/dist/client/assets/chunk-WU5MYG2G-D_yZ0nqc.js +0 -1
  151. package/dist/client/assets/classDiagram-4FO5ZUOK-eToeXGJV.js +0 -1
  152. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D_EfzX89.js +0 -1
  153. package/dist/client/assets/index-Ct9zZxJ5.css +0 -1
  154. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Brvg0TWH.js +0 -1
@@ -0,0 +1,1049 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
+ import { mkdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { promisify } from "node:util";
6
+ import { readFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { parseSync } from "editorconfig";
9
+ //#region src/lib/path.ts
10
+ function decodeAndNormalize(p) {
11
+ let decoded = p;
12
+ try {
13
+ decoded = decodeURIComponent(p);
14
+ } catch {}
15
+ return decoded.replace(/\\/g, "/");
16
+ }
17
+ function toSafeRelativePath(filePath, baseDir) {
18
+ const normalized = decodeAndNormalize(filePath);
19
+ if (normalized.includes("..") || normalized.includes("\0")) return null;
20
+ const resolved = resolve(baseDir, normalized);
21
+ const resolvedBase = resolve(baseDir);
22
+ if (!(resolved === resolvedBase || resolved.startsWith(resolvedBase + sep))) return null;
23
+ return relative(resolvedBase, resolved);
24
+ }
25
+ function isSafePath(relativePath, baseDir) {
26
+ return toSafeRelativePath(relativePath, baseDir) !== null;
27
+ }
28
+ //#endregion
29
+ //#region src/lib/git.ts
30
+ const execFileAsync = promisify(execFile);
31
+ function splitLines(content) {
32
+ return content.split(/\r\n|\n|\r/);
33
+ }
34
+ function isBinaryFile(absolutePath) {
35
+ try {
36
+ const buffer = readFileSync(absolutePath);
37
+ const bytesToCheck = Math.min(buffer.length, 8192);
38
+ for (let i = 0; i < bytesToCheck; i++) if (buffer[i] === 0) return true;
39
+ return false;
40
+ } catch {
41
+ return true;
42
+ }
43
+ }
44
+ function getFileContent(filePath, version) {
45
+ const root = getRepoRoot();
46
+ const relPath = toSafeRelativePath(filePath, root);
47
+ if (!relPath) return null;
48
+ const resolved = resolve(root, relPath);
49
+ if (version === "new") try {
50
+ return readFileSync(resolved);
51
+ } catch {
52
+ return null;
53
+ }
54
+ try {
55
+ return execFileSync("git", ["show", `HEAD:${relPath}`], {
56
+ stdio: "pipe",
57
+ maxBuffer: 50 * 1024 * 1024
58
+ });
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ let cachedRepoRoot = null;
64
+ const editorConfigCache = /* @__PURE__ */ new Map();
65
+ function isGitRepo() {
66
+ try {
67
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ function getRepoRoot() {
74
+ if (cachedRepoRoot !== null) return cachedRepoRoot;
75
+ cachedRepoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
76
+ return cachedRepoRoot;
77
+ }
78
+ function getRepoName() {
79
+ return basename(getRepoRoot());
80
+ }
81
+ function getBranchName() {
82
+ try {
83
+ return execFileSync("git", [
84
+ "rev-parse",
85
+ "--abbrev-ref",
86
+ "HEAD"
87
+ ], {
88
+ stdio: "pipe",
89
+ encoding: "utf-8"
90
+ }).trim();
91
+ } catch {
92
+ return "";
93
+ }
94
+ }
95
+ const DIFF_FLAGS = [
96
+ "--no-ext-diff",
97
+ "--no-textconv",
98
+ "--no-color"
99
+ ];
100
+ function getTabSizeForFiles(filePaths) {
101
+ const root = getRepoRoot();
102
+ const result = {};
103
+ for (const filePath of filePaths) try {
104
+ const config = parseSync(join(root, filePath), { cache: editorConfigCache });
105
+ const size = config.tab_width ?? (config.indent_size === "tab" ? void 0 : config.indent_size);
106
+ if (typeof size === "number") result[filePath] = size;
107
+ } catch {}
108
+ return result;
109
+ }
110
+ async function getRepoRootAsync() {
111
+ if (cachedRepoRoot !== null) return cachedRepoRoot;
112
+ try {
113
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
114
+ cachedRepoRoot = stdout.trim();
115
+ return cachedRepoRoot;
116
+ } catch {
117
+ return "";
118
+ }
119
+ }
120
+ async function getUntrackedFilePathsAsync() {
121
+ try {
122
+ const { stdout } = await execFileAsync("git", [
123
+ "ls-files",
124
+ "--others",
125
+ "--exclude-standard"
126
+ ], {
127
+ encoding: "utf-8",
128
+ maxBuffer: 50 * 1024 * 1024
129
+ });
130
+ const trimmed = stdout.trim();
131
+ return trimmed ? trimmed.split("\n") : [];
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+ async function getUntrackedFilesDiffAsync() {
137
+ const root = await getRepoRootAsync();
138
+ const filePaths = await getUntrackedFilePathsAsync();
139
+ if (filePaths.length === 0) return "";
140
+ const patches = [];
141
+ const fileDiffPromises = filePaths.map(async (file) => {
142
+ const absolutePath = join(root, file);
143
+ if (isBinaryFile(absolutePath)) return [
144
+ `diff --git a/${file} b/${file}`,
145
+ "new file mode 100644",
146
+ "index 0000000..0000001",
147
+ `Binary files /dev/null and b/${file} differ`
148
+ ].join("\n");
149
+ else try {
150
+ const lines = splitLines(await readFile(absolutePath, "utf-8"));
151
+ const diffLines = lines.map((l) => `+${l}`);
152
+ return [
153
+ `diff --git a/${file} b/${file}`,
154
+ "new file mode 100644",
155
+ "index 0000000..0000001",
156
+ "--- /dev/null",
157
+ `+++ b/${file}`,
158
+ `@@ -0,0 +1,${lines.length} @@`,
159
+ ...diffLines
160
+ ].join("\n");
161
+ } catch {
162
+ return "";
163
+ }
164
+ });
165
+ const results = await Promise.all(fileDiffPromises);
166
+ for (const r of results) if (r) patches.push(r);
167
+ return patches.length > 0 ? "\n" + patches.join("\n") : "";
168
+ }
169
+ async function getCustomGitDiffAsync(args) {
170
+ try {
171
+ const { stdout } = await execFileAsync("git", [
172
+ "diff",
173
+ ...DIFF_FLAGS,
174
+ ...args
175
+ ], {
176
+ encoding: "utf-8",
177
+ maxBuffer: 50 * 1024 * 1024
178
+ });
179
+ return stdout;
180
+ } catch {
181
+ return "";
182
+ }
183
+ }
184
+ async function getGitDiffAsync(options = {}) {
185
+ const unstagedPromise = execFileAsync("git", ["diff", ...DIFF_FLAGS], {
186
+ encoding: "utf-8",
187
+ maxBuffer: 50 * 1024 * 1024
188
+ }).then(({ stdout }) => stdout).catch(() => "");
189
+ const stagedPromise = options.staged ? execFileAsync("git", [
190
+ "diff",
191
+ ...DIFF_FLAGS,
192
+ "--staged"
193
+ ], {
194
+ encoding: "utf-8",
195
+ maxBuffer: 50 * 1024 * 1024
196
+ }).then(({ stdout }) => stdout).catch(() => "") : Promise.resolve("");
197
+ const untrackedPromise = options.untracked ? getUntrackedFilesDiffAsync() : Promise.resolve("");
198
+ const [unstaged, staged, untrackedPatch] = await Promise.all([
199
+ unstagedPromise,
200
+ stagedPromise,
201
+ untrackedPromise
202
+ ]);
203
+ const parts = [];
204
+ if (unstaged) parts.push(unstaged);
205
+ if (staged) parts.push(staged);
206
+ if (untrackedPatch) parts.push(untrackedPatch);
207
+ return parts.join("\n");
208
+ }
209
+ /**
210
+ * Lists every file the working tree knows about (tracked + untracked,
211
+ * excluding standard-ignored paths). Used by the Phase D file viewer to
212
+ * power a path picker.
213
+ */
214
+ function listRepoFiles() {
215
+ try {
216
+ const tracked = execFileSync("git", ["ls-files"], {
217
+ encoding: "utf-8",
218
+ maxBuffer: 100 * 1024 * 1024
219
+ }).trim();
220
+ const untracked = execFileSync("git", [
221
+ "ls-files",
222
+ "--others",
223
+ "--exclude-standard"
224
+ ], {
225
+ encoding: "utf-8",
226
+ maxBuffer: 100 * 1024 * 1024
227
+ }).trim();
228
+ const set = /* @__PURE__ */ new Set();
229
+ for (const list of [tracked, untracked]) {
230
+ if (!list) continue;
231
+ for (const line of list.split("\n")) if (line) set.add(line);
232
+ }
233
+ return [...set].sort();
234
+ } catch {
235
+ return [];
236
+ }
237
+ }
238
+ /**
239
+ * Reports whether the repo is currently in the middle of a merge and which
240
+ * files have unresolved conflicts. Used by the Phase C merge-conflict UI to
241
+ * decide when to render the @pierre/diffs UnresolvedFile component.
242
+ */
243
+ function getMergeStatus() {
244
+ let root;
245
+ try {
246
+ root = getRepoRoot();
247
+ } catch {
248
+ return {
249
+ inMerge: false,
250
+ conflicts: []
251
+ };
252
+ }
253
+ const mergeHead = join(root, ".git", "MERGE_HEAD");
254
+ let inMerge = false;
255
+ try {
256
+ readFileSync(mergeHead);
257
+ inMerge = true;
258
+ } catch {
259
+ inMerge = false;
260
+ }
261
+ let conflicts = [];
262
+ try {
263
+ const out = execFileSync("git", [
264
+ "diff",
265
+ "--name-only",
266
+ "--diff-filter=U"
267
+ ], {
268
+ encoding: "utf-8",
269
+ maxBuffer: 50 * 1024 * 1024
270
+ }).trim();
271
+ conflicts = out ? out.split("\n") : [];
272
+ } catch {
273
+ conflicts = [];
274
+ }
275
+ return {
276
+ inMerge: inMerge || conflicts.length > 0,
277
+ conflicts
278
+ };
279
+ }
280
+ function gitAddFile(filePath) {
281
+ execFileSync("git", [
282
+ "add",
283
+ "--",
284
+ filePath
285
+ ], { stdio: "pipe" });
286
+ }
287
+ /**
288
+ * Returns a unified diff for a single working-tree file (covers unstaged
289
+ * and untracked changes). Used by the Phase E hunk-revert flow to derive
290
+ * the exact patch text we need to `git apply --reverse`.
291
+ */
292
+ function getFilePatch(filePath) {
293
+ try {
294
+ const unstaged = execFileSync("git", [
295
+ "diff",
296
+ ...DIFF_FLAGS,
297
+ "--",
298
+ filePath
299
+ ], {
300
+ encoding: "utf-8",
301
+ maxBuffer: 50 * 1024 * 1024
302
+ });
303
+ if (unstaged) return unstaged;
304
+ } catch {}
305
+ try {
306
+ const absolutePath = join(getRepoRoot(), filePath);
307
+ if (isBinaryFile(absolutePath)) return "";
308
+ const lines = splitLines(readFileSync(absolutePath, "utf-8"));
309
+ return [
310
+ `diff --git a/${filePath} b/${filePath}`,
311
+ "new file mode 100644",
312
+ "index 0000000..0000001",
313
+ "--- /dev/null",
314
+ `+++ b/${filePath}`,
315
+ `@@ -0,0 +1,${lines.length} @@`,
316
+ ...lines.map((l) => `+${l}`)
317
+ ].join("\n");
318
+ } catch {
319
+ return "";
320
+ }
321
+ }
322
+ /**
323
+ * Extracts the Nth hunk text from a single-file patch. Returns the lines
324
+ * from `@@ ... @@` through the line before the next `@@` (or EOF).
325
+ */
326
+ function extractHunk(patch, hunkIndex) {
327
+ const lines = patch.split("\n");
328
+ const hunkStarts = [];
329
+ for (let i = 0; i < lines.length; i++) if (lines[i].startsWith("@@ ")) hunkStarts.push(i);
330
+ if (hunkIndex < 0 || hunkIndex >= hunkStarts.length) return null;
331
+ const start = hunkStarts[hunkIndex];
332
+ const end = hunkIndex + 1 < hunkStarts.length ? hunkStarts[hunkIndex + 1] : lines.length;
333
+ return lines.slice(start, end).join("\n");
334
+ }
335
+ /** Header lines that the per-file patch needs in order for `git apply` to
336
+ * recognize the target. Stops just before the first `@@` hunk header. */
337
+ function extractPatchHeader(patch) {
338
+ const lines = patch.split("\n");
339
+ const out = [];
340
+ for (const line of lines) {
341
+ if (line.startsWith("@@ ")) break;
342
+ out.push(line);
343
+ }
344
+ return out.join("\n");
345
+ }
346
+ /**
347
+ * Reverts a single hunk from the working tree by piping a minimal patch to
348
+ * `git apply --reverse`. Throws if git rejects the apply (typically because
349
+ * the file has shifted since the diff was rendered).
350
+ */
351
+ function revertHunk(filePath, hunkIndex) {
352
+ const patch = getFilePatch(filePath);
353
+ if (!patch) throw new Error("No diff available for this file");
354
+ const header = extractPatchHeader(patch);
355
+ const hunk = extractHunk(patch, hunkIndex);
356
+ if (!hunk) throw new Error(`Hunk ${hunkIndex} not found`);
357
+ execFileSync("git", [
358
+ "apply",
359
+ "--reverse",
360
+ "-"
361
+ ], {
362
+ input: `${header}\n${hunk}\n`,
363
+ stdio: [
364
+ "pipe",
365
+ "pipe",
366
+ "pipe"
367
+ ]
368
+ });
369
+ }
370
+ function getProjectStorageDir(customRepoRoot) {
371
+ const root = customRepoRoot || getRepoRoot();
372
+ const hash = createHash("sha256").update(root).digest("hex").slice(0, 8);
373
+ const repoName = basename(root);
374
+ return join(homedir(), ".diffing", `${repoName}-${hash}`);
375
+ }
376
+ /**
377
+ * Retrieves git blame for deleted lines and recent log history for the file,
378
+ * providing context on what commit introduced the deleted code.
379
+ */
380
+ function getHunkHistory(filePath, deletionStart, deletionCount, revision = "HEAD") {
381
+ if (!isSafePath(filePath, getRepoRoot())) throw new Error("Forbidden file path");
382
+ const blame = [];
383
+ if (deletionCount > 0) try {
384
+ const lines = execFileSync("git", [
385
+ "blame",
386
+ "--date=format:%Y-%m-%d",
387
+ "-L",
388
+ `${deletionStart},+${deletionCount}`,
389
+ revision,
390
+ "--",
391
+ filePath
392
+ ], {
393
+ encoding: "utf-8",
394
+ maxBuffer: 10 * 1024 * 1024
395
+ }).split("\n");
396
+ const commitHashes = /* @__PURE__ */ new Set();
397
+ for (const line of lines) {
398
+ if (!line.trim()) continue;
399
+ const match = line.match(/^(\^?[0-9a-fA-F]+) \((.*?) (\d{4}-\d{2}-\d{2}) (\d+)\)(?: (.*))?$/);
400
+ if (match) {
401
+ const [_, commit, author, date, lineNum, content] = match;
402
+ const cleanCommit = commit.startsWith("^") ? commit.slice(1) : commit;
403
+ blame.push({
404
+ commit: cleanCommit,
405
+ author: author.trim(),
406
+ date,
407
+ line: parseInt(lineNum, 10),
408
+ content: content || "",
409
+ summary: ""
410
+ });
411
+ commitHashes.add(cleanCommit);
412
+ }
413
+ }
414
+ const commitSummaries = {};
415
+ for (const hash of commitHashes) try {
416
+ commitSummaries[hash] = execFileSync("git", [
417
+ "show",
418
+ "-s",
419
+ "--format=%s",
420
+ hash
421
+ ], { encoding: "utf-8" }).trim();
422
+ } catch {
423
+ commitSummaries[hash] = "Unknown commit";
424
+ }
425
+ for (const entry of blame) entry.summary = commitSummaries[entry.commit] || "Unknown commit";
426
+ } catch (err) {
427
+ console.warn("Failed to get git blame for hunk history:", err.message);
428
+ }
429
+ const recentCommits = [];
430
+ try {
431
+ const logOut = execFileSync("git", [
432
+ "log",
433
+ "-n",
434
+ "5",
435
+ "--date=format:%Y-%m-%d",
436
+ "--format=%h|%an|%ad|%s",
437
+ "--",
438
+ filePath
439
+ ], { encoding: "utf-8" });
440
+ for (const line of logOut.split("\n")) {
441
+ if (!line.trim()) continue;
442
+ const [hash, author, date, summary] = line.split("|");
443
+ if (hash && author && date && summary) recentCommits.push({
444
+ hash,
445
+ author: author.trim(),
446
+ date,
447
+ summary: summary.trim()
448
+ });
449
+ }
450
+ } catch (err) {
451
+ console.warn("Failed to get git log for hunk history:", err.message);
452
+ }
453
+ return {
454
+ blame,
455
+ recentCommits
456
+ };
457
+ }
458
+ /**
459
+ * Parse the output of `git log --pretty=raw -p` into `CommitInfo[]`.
460
+ *
461
+ * The `raw` format emits one record per commit:
462
+ *
463
+ * commit <sha>
464
+ * tree <sha>
465
+ * parent <sha> (zero or more; merges have ≥ 2)
466
+ * author <name> <email> <unix-time> <tz>
467
+ * committer <name> <email> <unix-time> <tz>
468
+ * gpgsig -----BEGIN ... (optional; continuation lines start with a single space)
469
+ * <indented body>
470
+ *
471
+ * <indented commit message — 4 spaces of indent>
472
+ *
473
+ * diff --git a/... b/... (the per-commit patch; may be empty)
474
+ * ...
475
+ *
476
+ * Records are separated by blank lines but the unambiguous boundary is the
477
+ * next line beginning with `commit <40-hex>` at column 0.
478
+ */
479
+ function parseGitShowRaw(raw) {
480
+ if (!raw) return [];
481
+ const out = [];
482
+ const lines = raw.split("\n");
483
+ const starts = [];
484
+ for (let i = 0; i < lines.length; i++) if (/^commit [0-9a-f]{40}\b/.test(lines[i])) starts.push(i);
485
+ if (starts.length === 0) return [];
486
+ for (let s = 0; s < starts.length; s++) {
487
+ const begin = starts[s];
488
+ const end = s + 1 < starts.length ? starts[s + 1] : lines.length;
489
+ const info = parseSingleCommitRaw(lines.slice(begin, end));
490
+ if (info) out.push(info);
491
+ }
492
+ return out;
493
+ }
494
+ function parseSingleCommitRaw(lines) {
495
+ const shaMatch = (lines[0] ?? "").match(/^commit ([0-9a-f]{40})/);
496
+ if (!shaMatch) return null;
497
+ const sha = shaMatch[1];
498
+ let i = 1;
499
+ const parents = [];
500
+ let authorName = "";
501
+ let authorEmail = "";
502
+ let authorDate = "";
503
+ let committerName = "";
504
+ let committerEmail = "";
505
+ let committerDate = "";
506
+ while (i < lines.length) {
507
+ const line = lines[i];
508
+ if (line === "") break;
509
+ if (line.startsWith(" ")) {
510
+ i++;
511
+ continue;
512
+ }
513
+ const idx = line.indexOf(" ");
514
+ const key = idx === -1 ? line : line.slice(0, idx);
515
+ const value = idx === -1 ? "" : line.slice(idx + 1);
516
+ switch (key) {
517
+ case "tree": break;
518
+ case "parent":
519
+ if (value) parents.push(value);
520
+ break;
521
+ case "author": {
522
+ const ident = parseIdent(value);
523
+ authorName = ident.name;
524
+ authorEmail = ident.email;
525
+ authorDate = ident.date;
526
+ break;
527
+ }
528
+ case "committer": {
529
+ const ident = parseIdent(value);
530
+ committerName = ident.name;
531
+ committerEmail = ident.email;
532
+ committerDate = ident.date;
533
+ break;
534
+ }
535
+ default: break;
536
+ }
537
+ i++;
538
+ }
539
+ if (i < lines.length && lines[i] === "") i++;
540
+ const msgLines = [];
541
+ while (i < lines.length) {
542
+ const line = lines[i];
543
+ if (line.startsWith(" ")) {
544
+ msgLines.push(line.slice(4));
545
+ i++;
546
+ continue;
547
+ }
548
+ if (line === "") {
549
+ msgLines.push("");
550
+ i++;
551
+ continue;
552
+ }
553
+ break;
554
+ }
555
+ const { subject, body } = splitMessage(msgLines);
556
+ let patchStart = -1;
557
+ for (let j = i; j < lines.length; j++) {
558
+ const line = lines[j];
559
+ if (line.startsWith("diff --git ") || line.startsWith("Binary files ")) {
560
+ patchStart = j;
561
+ break;
562
+ }
563
+ if (line === "") continue;
564
+ break;
565
+ }
566
+ const patch = patchStart === -1 ? "" : lines.slice(patchStart).join("\n").replace(/\n+$/, "\n");
567
+ return {
568
+ sha,
569
+ shortSha: sha.slice(0, 7),
570
+ parents,
571
+ subject,
572
+ body,
573
+ authorName,
574
+ authorEmail,
575
+ authorDate,
576
+ committerName,
577
+ committerEmail,
578
+ committerDate,
579
+ patch
580
+ };
581
+ }
582
+ /**
583
+ * Parse a `Name <email> <unix-seconds> <±HHMM>` ident line into structured
584
+ * fields. The email is bracketed; the timestamp is split out and converted to
585
+ * ISO 8601 for consumers.
586
+ */
587
+ function parseIdent(value) {
588
+ const match = value.match(/^(.*?)\s+<([^>]*)>\s+(\d+)\s+([+-]\d{4})$/);
589
+ if (!match) return {
590
+ name: value,
591
+ email: "",
592
+ date: ""
593
+ };
594
+ const [, name, email, secs, tz] = match;
595
+ return {
596
+ name,
597
+ email,
598
+ date: unixToIso(Number(secs), tz)
599
+ };
600
+ }
601
+ function unixToIso(secs, tz) {
602
+ if (!Number.isFinite(secs)) return "";
603
+ const sign = tz.startsWith("-") ? -1 : 1;
604
+ const tzHours = Number(tz.slice(1, 3));
605
+ const tzMins = Number(tz.slice(3, 5));
606
+ const offsetMs = sign * (tzHours * 60 + tzMins) * 6e4;
607
+ const d = new Date(secs * 1e3 + offsetMs);
608
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
609
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}${tz.slice(0, 3)}:${tz.slice(3)}`;
610
+ }
611
+ function splitMessage(msgLines) {
612
+ if (msgLines.length === 0) return {
613
+ subject: "",
614
+ body: ""
615
+ };
616
+ let start = 0;
617
+ while (start < msgLines.length && msgLines[start] === "") start++;
618
+ let stop = msgLines.length;
619
+ while (stop > start && msgLines[stop - 1] === "") stop--;
620
+ if (start >= stop) return {
621
+ subject: "",
622
+ body: ""
623
+ };
624
+ const subject = msgLines[start];
625
+ let bodyStart = start + 1;
626
+ if (bodyStart < stop && msgLines[bodyStart] === "") bodyStart++;
627
+ return {
628
+ subject,
629
+ body: msgLines.slice(bodyStart, stop).join("\n")
630
+ };
631
+ }
632
+ async function getShowDiff(revspecs, pathspecs = []) {
633
+ if (revspecs.length === 0) return {
634
+ commits: [],
635
+ patch: "",
636
+ truncated: 0
637
+ };
638
+ let shaList;
639
+ try {
640
+ const { stdout } = await execFileAsync("git", [
641
+ "rev-list",
642
+ "--no-walk",
643
+ "--reverse",
644
+ ...revspecs,
645
+ ...pathspecs.length ? ["--", ...pathspecs] : []
646
+ ], {
647
+ encoding: "utf-8",
648
+ maxBuffer: 50 * 1024 * 1024
649
+ });
650
+ shaList = stdout.split("\n").filter(Boolean);
651
+ } catch {
652
+ return {
653
+ commits: [],
654
+ patch: "",
655
+ truncated: 0
656
+ };
657
+ }
658
+ if (shaList.length === 0) return {
659
+ commits: [],
660
+ patch: "",
661
+ truncated: 0
662
+ };
663
+ const truncated = Math.max(0, shaList.length - 100);
664
+ const trimmed = truncated === 0 ? shaList : shaList.slice(0, 100);
665
+ let raw;
666
+ try {
667
+ const { stdout } = await execFileAsync("git", [
668
+ "log",
669
+ "--no-walk",
670
+ "--reverse",
671
+ "-p",
672
+ "--pretty=raw",
673
+ ...DIFF_FLAGS,
674
+ ...trimmed,
675
+ ...pathspecs.length ? ["--", ...pathspecs] : []
676
+ ], {
677
+ encoding: "utf-8",
678
+ maxBuffer: 200 * 1024 * 1024
679
+ });
680
+ raw = stdout;
681
+ } catch {
682
+ return {
683
+ commits: [],
684
+ patch: "",
685
+ truncated: 0
686
+ };
687
+ }
688
+ const commits = parseGitShowRaw(raw);
689
+ return {
690
+ commits,
691
+ patch: commits.map((c) => c.patch).filter(Boolean).join("\n"),
692
+ truncated
693
+ };
694
+ }
695
+ /**
696
+ * Cheap summary of a revspec series: just subject + author + date. Used by
697
+ * the diff overview banner to render "what am I looking at?" for custom
698
+ * non-show revisions (e.g. `diffing main..feature`).
699
+ *
700
+ * Uses `git log --no-walk --reverse --pretty=%s%x00%an%x00%aI`. The NUL
701
+ * separators avoid the `|` ambiguity the older `getHunkHistory` parser
702
+ * suffers from; we keep that call alone rather than refactoring it.
703
+ *
704
+ * `--no-walk` keeps single-SHA revspecs cheap (just that commit's metadata)
705
+ * while ranges still expand normally. Reuses the same `MAX_SHOW_COMMITS`
706
+ * cap as `getShowDiff` so the banner and the per-commit view agree.
707
+ */
708
+ async function getCommitSeriesSummary(revspecs, pathspecs = []) {
709
+ const empty = {
710
+ commitCount: 0,
711
+ truncated: 0,
712
+ subjects: [],
713
+ authors: []
714
+ };
715
+ if (revspecs.length === 0) return empty;
716
+ let shaList;
717
+ try {
718
+ const { stdout } = await execFileAsync("git", [
719
+ "rev-list",
720
+ "--no-walk",
721
+ "--reverse",
722
+ ...revspecs,
723
+ ...pathspecs.length ? ["--", ...pathspecs] : []
724
+ ], {
725
+ encoding: "utf-8",
726
+ maxBuffer: 50 * 1024 * 1024
727
+ });
728
+ shaList = stdout.split("\n").filter(Boolean);
729
+ } catch {
730
+ return empty;
731
+ }
732
+ const totalCommits = shaList.length;
733
+ if (totalCommits === 0) return empty;
734
+ const truncated = Math.max(0, totalCommits - 100);
735
+ const trimmed = truncated === 0 ? shaList : shaList.slice(0, 100);
736
+ let raw;
737
+ try {
738
+ const { stdout } = await execFileAsync("git", [
739
+ "log",
740
+ "--no-walk",
741
+ "--reverse",
742
+ "--pretty=%s%x00%an%x00%aI",
743
+ ...DIFF_FLAGS,
744
+ ...trimmed,
745
+ ...pathspecs.length ? ["--", ...pathspecs] : []
746
+ ], {
747
+ encoding: "utf-8",
748
+ maxBuffer: 50 * 1024 * 1024
749
+ });
750
+ raw = stdout;
751
+ } catch {
752
+ return {
753
+ ...empty,
754
+ commitCount: totalCommits,
755
+ truncated
756
+ };
757
+ }
758
+ const subjects = [];
759
+ const authors = [];
760
+ const seenAuthors = /* @__PURE__ */ new Set();
761
+ let fromDate;
762
+ let toDate;
763
+ for (const line of raw.split("\n")) {
764
+ if (!line) continue;
765
+ const parts = line.split("\0");
766
+ const subject = parts[0] ?? "";
767
+ const author = parts[1] ?? "";
768
+ const date = parts[2] ?? "";
769
+ if (subject) subjects.push(subject);
770
+ if (author && !seenAuthors.has(author)) {
771
+ seenAuthors.add(author);
772
+ authors.push(author);
773
+ }
774
+ if (date) {
775
+ if (!fromDate || date < fromDate) fromDate = date;
776
+ if (!toDate || date > toDate) toDate = date;
777
+ }
778
+ }
779
+ return {
780
+ commitCount: totalCommits,
781
+ truncated,
782
+ subjects,
783
+ authors,
784
+ fromDate,
785
+ toDate
786
+ };
787
+ }
788
+ //#endregion
789
+ //#region src/lib/comment-format.ts
790
+ /** Plain-language guidance the agent should act on, derived from the verdict. */
791
+ function reviewDecisionSummary(decision) {
792
+ switch (decision) {
793
+ case "approved": return "The reviewer APPROVED these changes. Address any open comments below, then proceed — the overall direction is good.";
794
+ case "changes-requested": return "The reviewer REQUESTED EDITS. Address every open comment below and apply the requested changes before considering this review done.";
795
+ case "rejected": return "The reviewer REJECTED these changes. Do NOT keep building on this approach; reconsider it in light of the comments below before continuing.";
796
+ case "comment-only": return "The reviewer chose COMMENT-ONLY mode. You MUST NOT edit any files. Only reply to the comments below — answer questions, provide clarification, or discuss. The general comment (if any) is your prompt for the chat.";
797
+ }
798
+ }
799
+ /**
800
+ * Render review comments as the `<code-review-comments>` XML envelope used to
801
+ * hand a review to an AI agent. Shared by the UI clipboard button
802
+ * (`useComments.formatAllComments`), the server's `/api/review/send` handoff,
803
+ * and the `diffing` CLI/MCP so every channel emits byte-identical output.
804
+ *
805
+ * A review may carry a headline `decision` (approve / request edits / reject / comment-only)
806
+ * and/or an overall comment, so the envelope is emitted even with zero inline
807
+ * comments — letting a reviewer submit a verdict without annotating any line.
808
+ */
809
+ function formatComments(comments, generalComment, decision, mode) {
810
+ const trimmedGeneral = generalComment?.trim();
811
+ if (comments.length === 0 && !decision && !trimmedGeneral) return "";
812
+ const grouped = /* @__PURE__ */ new Map();
813
+ for (const comment of comments) {
814
+ const list = grouped.get(comment.filePath) ?? [];
815
+ list.push(comment);
816
+ grouped.set(comment.filePath, list);
817
+ }
818
+ const sortedFilePaths = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
819
+ const modeAttr = mode && mode !== "standard" ? ` mode="${mode}"` : "";
820
+ const lines = [];
821
+ lines.push(decision ? `<code-review-comments decision="${decision}"${modeAttr}>` : `<code-review-comments${modeAttr}>`);
822
+ lines.push(" <instructions>");
823
+ lines.push(" You are an AI coding assistant. You are receiving a structured list of code review comments to address in the repository.");
824
+ if (mode === "comment-only") lines.push(" ⚠️ COMMENT-ONLY MODE: You MUST NOT edit any files. Your only task is to reply to the comments below — answer questions, provide clarification, or discuss. The general comment (if present) is your prompt for this chat.");
825
+ else lines.push(" For each file, review the inline comments and apply the changes requested.");
826
+ if (decision) {
827
+ lines.push(" - The \"decision\" attribute on the root element is the reviewer's headline verdict: \"approved\", \"changes-requested\", \"rejected\", or \"comment-only\".");
828
+ lines.push(" - <decision-summary> tells you, in plain language, what to do next based on that verdict.");
829
+ }
830
+ if (mode) lines.push(` - The "mode" attribute on the root element controls your behavior: "standard" (apply edits) or "comment-only" (reply only, no edits).`);
831
+ lines.push(" - Target lines are specified by the \"line\" attribute (e.g. line=\"10\" or line=\"10-15\").");
832
+ lines.push(" - \"side\" indicates whether the comment is on \"additions\" (added/modified lines) or \"deletions\" (deleted/old lines).");
833
+ lines.push(" - \"status\" indicates whether the comment is \"open\" or \"resolved\". Only address comments with status=\"open\".");
834
+ lines.push(" - The <code> block contains the specific code context at the reviewed lines, prefixed with \"+\" or \"-\".");
835
+ lines.push(" - The <body> tag contains the review feedback or request.");
836
+ lines.push(" - If developers have replied to the comment, their discussion is captured under the <replies> element.");
837
+ lines.push(" - The comment \"id\" attribute can be used to reference or update the comment via API if available.");
838
+ lines.push("");
839
+ lines.push(" HOW TO REPLY OR ASK FOR CLARIFICATION:");
840
+ lines.push(" If you need to ask for clarification, explain what you did, or reply to any comment:");
841
+ lines.push("");
842
+ lines.push(" Option A: Via the diffing CLI or MCP (Preferred — port-agnostic, no copy-paste)");
843
+ lines.push(" diffing reply <comment-id> --body \"Your response\" --model \"<your-model-name>\"");
844
+ lines.push(" diffing resolve <comment-id>");
845
+ lines.push(" (Or the equivalent MCP tools: reply_to_comment, resolve_comment.)");
846
+ lines.push("");
847
+ lines.push(" Option B: Via the local HTTP API (if you know the running port)");
848
+ lines.push(" POST http://localhost:<port>/api/comments/<comment-id>/replies");
849
+ lines.push(" Payload: { \"body\": \"Your response or clarification request here\", \"model\": \"<your-model-name>\" }");
850
+ lines.push(" PUT http://localhost:<port>/api/comments/<comment-id> Payload: { \"status\": \"resolved\" }");
851
+ lines.push("");
852
+ lines.push(" Option C: Via Text Response (Offline / Chat Copy-Paste)");
853
+ lines.push(" If you do not have local API access, output your comments/replies inside a structured XML block at the end of your response:");
854
+ lines.push(" <comment-replies>");
855
+ lines.push(" <reply to=\"<comment-id>\" model=\"<your-model-name>\"><![CDATA[Your reply or clarification request here]]></reply>");
856
+ lines.push(" </comment-replies>");
857
+ lines.push(" </instructions>");
858
+ if (decision) lines.push(` <decision-summary><![CDATA[${reviewDecisionSummary(decision)}]]></decision-summary>`);
859
+ if (trimmedGeneral) {
860
+ lines.push(" <general-comment>");
861
+ lines.push(` <![CDATA[${trimmedGeneral}]]>`);
862
+ lines.push(" </general-comment>");
863
+ }
864
+ for (const filePath of sortedFilePaths) {
865
+ const fileComments = grouped.get(filePath);
866
+ lines.push(` <file path="${filePath}">`);
867
+ for (const comment of fileComments) {
868
+ const lineAttr = comment.lineNumber === 0 ? "file" : comment.startLineNumber && comment.startLineNumber !== comment.lineNumber ? `${comment.startLineNumber}-${comment.lineNumber}` : `${comment.lineNumber}`;
869
+ const isoDate = new Date(comment.createdAt).toISOString();
870
+ lines.push(` <comment id="${comment.id}" line="${lineAttr}" side="${comment.side}" status="${comment.status}" created-at="${isoDate}">`);
871
+ if (comment.lineNumber !== 0) {
872
+ const prefix = comment.side === "additions" ? "+" : "-";
873
+ const isMultiLine = comment.lineContent && comment.lineContent.includes("\n");
874
+ let codeVal = "";
875
+ if (isMultiLine) codeVal = `\n${comment.lineContent.split("\n").map((l) => `${prefix} ${l}`).join("\n")}\n`;
876
+ else codeVal = `${prefix} ${comment.lineContent}`;
877
+ lines.push(` <code><![CDATA[${codeVal}]]></code>`);
878
+ }
879
+ lines.push(` <body><![CDATA[${comment.body}]]></body>`);
880
+ if (comment.replies && comment.replies.length > 0) {
881
+ lines.push(" <replies>");
882
+ for (const reply of comment.replies) {
883
+ const replyIsoDate = new Date(reply.createdAt).toISOString();
884
+ const roleAttr = reply.role ? ` role="${reply.role}"` : " role=\"agent\"";
885
+ const modelAttr = reply.model ? ` model="${reply.model}"` : "";
886
+ lines.push(` <reply id="${reply.id}" created-at="${replyIsoDate}"${roleAttr}${modelAttr}>`);
887
+ lines.push(` <![CDATA[${reply.body}]]>`);
888
+ lines.push(" </reply>");
889
+ }
890
+ lines.push(" </replies>");
891
+ }
892
+ lines.push(" </comment>");
893
+ }
894
+ lines.push(" </file>");
895
+ }
896
+ lines.push("</code-review-comments>");
897
+ return lines.join("\n");
898
+ }
899
+ //#endregion
900
+ //#region src/lib/server-lock.ts
901
+ const STARTUP_LEASE_STALE_MS = 3e4;
902
+ /**
903
+ * Produce a stable comparison key for the diff a server displays. Runtime-only
904
+ * web options are excluded so `--port`/`--no-open` do not create false scope
905
+ * mismatches between a user-started session and an MCP request.
906
+ */
907
+ function diffScopeKey(options) {
908
+ const { port: _port, host: _host, noOpen: _noOpen, help: _help, version: _version, outputMode: _outputMode, tui: _tui, gpu: _gpu, noExtDiff: _noExtDiff, ...scope } = options;
909
+ return JSON.stringify(scope);
910
+ }
911
+ function lockPath(repoRoot) {
912
+ return join(getProjectStorageDir(repoRoot), "server.json");
913
+ }
914
+ function writeServerLock(lock) {
915
+ const path = lockPath(lock.repoRoot);
916
+ mkdirSync(join(path, ".."), { recursive: true });
917
+ writeFileSync(path, JSON.stringify(lock, null, 2), "utf-8");
918
+ }
919
+ function readServerLock(repoRoot) {
920
+ try {
921
+ const raw = readFileSync(lockPath(repoRoot), "utf-8");
922
+ const lock = JSON.parse(raw);
923
+ if (typeof lock.port !== "number" || typeof lock.pid !== "number") return null;
924
+ return lock;
925
+ } catch {
926
+ return null;
927
+ }
928
+ }
929
+ /**
930
+ * True if the process named by the lock is still alive and the lock belongs to
931
+ * this repo. `process.kill(pid, 0)` sends no signal — it just probes existence.
932
+ */
933
+ function isLockAlive(lock, expectedRepoRoot) {
934
+ try {
935
+ process.kill(lock.pid, 0);
936
+ } catch {
937
+ return false;
938
+ }
939
+ try {
940
+ return lock.repoRoot === (expectedRepoRoot ?? getRepoRoot());
941
+ } catch {
942
+ return true;
943
+ }
944
+ }
945
+ function removeServerLock(repoRoot) {
946
+ try {
947
+ rmSync(lockPath(repoRoot), { force: true });
948
+ } catch {}
949
+ }
950
+ /** Remove server.json only while holding the startup lease and only if exact ownership still matches. */
951
+ function removeServerLockIfOwned(repoRoot, pid, ownerId) {
952
+ const cleanupLease = acquireServerStartupLease(repoRoot, `cleanup-${ownerId}`);
953
+ if (!cleanupLease) return false;
954
+ try {
955
+ const lock = readServerLock(repoRoot);
956
+ if (!lock || lock.pid !== pid || lock.ownerId !== ownerId) return false;
957
+ removeServerLock(repoRoot);
958
+ return true;
959
+ } finally {
960
+ cleanupLease.release();
961
+ }
962
+ }
963
+ /**
964
+ * Atomically reserve server startup for one repository. The exclusive-create
965
+ * directory is the cross-process serialization point; callers must re-read
966
+ * server.json after acquiring it because another process may have completed
967
+ * startup between their first lock check and this lease acquisition.
968
+ */
969
+ function acquireServerStartupLease(repoRoot, ownerId, now = Date.now()) {
970
+ const path = join(getProjectStorageDir(repoRoot), "server-startup.lock");
971
+ const recordPath = join(path, "lease.json");
972
+ const ownerMarkerPath = join(path, `owner-${encodeURIComponent(ownerId)}`);
973
+ mkdirSync(join(path, ".."), { recursive: true });
974
+ const tryCreate = () => {
975
+ try {
976
+ mkdirSync(path, {
977
+ recursive: false,
978
+ mode: 448
979
+ });
980
+ } catch (error) {
981
+ if (error?.code === "EEXIST") return false;
982
+ throw error;
983
+ }
984
+ try {
985
+ writeFileSync(recordPath, JSON.stringify({
986
+ ownerId,
987
+ createdAt: now,
988
+ pid: process.pid
989
+ }), "utf-8");
990
+ writeFileSync(ownerMarkerPath, "", "utf-8");
991
+ } catch (error) {
992
+ rmSync(path, {
993
+ recursive: true,
994
+ force: true
995
+ });
996
+ throw error;
997
+ }
998
+ return true;
999
+ };
1000
+ if (!tryCreate()) {
1001
+ let reclaim = false;
1002
+ let acquiredAfterRace = false;
1003
+ try {
1004
+ const record = JSON.parse(readFileSync(recordPath, "utf-8"));
1005
+ if (typeof record.createdAt !== "number" || typeof record.pid !== "number") throw new Error("Malformed startup lease");
1006
+ let ownerIsAlive = true;
1007
+ try {
1008
+ process.kill(record.pid, 0);
1009
+ } catch (error) {
1010
+ ownerIsAlive = error?.code !== "ESRCH";
1011
+ }
1012
+ reclaim = now - record.createdAt > STARTUP_LEASE_STALE_MS && !ownerIsAlive;
1013
+ if (!reclaim) return null;
1014
+ } catch {
1015
+ try {
1016
+ reclaim = now - statSync(path).mtimeMs > STARTUP_LEASE_STALE_MS;
1017
+ } catch (statError) {
1018
+ if (statError?.code !== "ENOENT") return null;
1019
+ if (!tryCreate()) return null;
1020
+ acquiredAfterRace = true;
1021
+ }
1022
+ if (!reclaim && !acquiredAfterRace) return null;
1023
+ }
1024
+ if (reclaim && !acquiredAfterRace) {
1025
+ const stalePath = `${path}.stale-${process.pid}-${now}`;
1026
+ renameSync(path, stalePath);
1027
+ rmSync(stalePath, {
1028
+ recursive: true,
1029
+ force: true
1030
+ });
1031
+ }
1032
+ if (!acquiredAfterRace && !tryCreate()) return null;
1033
+ }
1034
+ let released = false;
1035
+ return {
1036
+ ownerId,
1037
+ release() {
1038
+ if (released) return;
1039
+ released = true;
1040
+ try {
1041
+ unlinkSync(ownerMarkerPath);
1042
+ unlinkSync(recordPath);
1043
+ rmdirSync(path);
1044
+ } catch {}
1045
+ }
1046
+ };
1047
+ }
1048
+ //#endregion
1049
+ export { isGitRepo as C, toSafeRelativePath as D, isSafePath as E, gitAddFile as S, revertHunk as T, getRepoName as _, removeServerLock as a, getTabSizeForFiles as b, formatComments as c, getCustomGitDiffAsync as d, getFileContent as f, getProjectStorageDir as g, getMergeStatus as h, readServerLock as i, getBranchName as l, getHunkHistory as m, diffScopeKey as n, removeServerLockIfOwned as o, getGitDiffAsync as p, isLockAlive as r, writeServerLock as s, acquireServerStartupLease as t, getCommitSeriesSummary as u, getRepoRoot as v, listRepoFiles as w, getUntrackedFilePathsAsync as x, getShowDiff as y };