diff-easy 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 (172) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +142 -0
  3. package/dist/cli/cmux.d.ts +9 -0
  4. package/dist/cli/cmux.js +274 -0
  5. package/dist/cli/comment.d.ts +2 -0
  6. package/dist/cli/comment.js +91 -0
  7. package/dist/cli/github.d.ts +84 -0
  8. package/dist/cli/github.js +364 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +397 -0
  11. package/dist/cli/turns.d.ts +14 -0
  12. package/dist/cli/turns.js +333 -0
  13. package/dist/cli/utils.d.ts +27 -0
  14. package/dist/cli/utils.js +204 -0
  15. package/dist/client/assets/arc-CHYjdFTk.js +1 -0
  16. package/dist/client/assets/architecture-7EHR7CIX-weHBYDgM.js +1 -0
  17. package/dist/client/assets/architectureDiagram-3BPJPVTR-DRa2msuJ.js +36 -0
  18. package/dist/client/assets/array-BifhSqXX.js +1 -0
  19. package/dist/client/assets/blockDiagram-GPEHLZMM-BTdKsXy5.js +132 -0
  20. package/dist/client/assets/c4Diagram-AAUBKEIU-CwTvCkOg.js +10 -0
  21. package/dist/client/assets/channel-Cv40nQW1.js +1 -0
  22. package/dist/client/assets/chunk-2J33WTMH-BFsjrTtc.js +1 -0
  23. package/dist/client/assets/chunk-3OPIFGDE-V_xVw1dq.js +62 -0
  24. package/dist/client/assets/chunk-4BX2VUAB-DoLO6LLy.js +1 -0
  25. package/dist/client/assets/chunk-55IACEB6-CHgBwbdB.js +1 -0
  26. package/dist/client/assets/chunk-5ZQYHXKU-DMixAgQG.js +2 -0
  27. package/dist/client/assets/chunk-727SXJPM-C-3FZTmm.js +206 -0
  28. package/dist/client/assets/chunk-AQP2D5EJ-DpQZhXt-.js +231 -0
  29. package/dist/client/assets/chunk-BSJP7CBP-BnO9E_G4.js +1 -0
  30. package/dist/client/assets/chunk-CSCIHK7Q-BxItphyO.js +124 -0
  31. package/dist/client/assets/chunk-FMBD7UC4-4hEcfvfT.js +15 -0
  32. package/dist/client/assets/chunk-KSCS5N6A-DnArkpOI.js +10 -0
  33. package/dist/client/assets/chunk-L5ZTLDWV-DQoYJ1BW.js +1 -0
  34. package/dist/client/assets/chunk-LZXEDZCA-CdCevBX-.js +2 -0
  35. package/dist/client/assets/chunk-ND2GUHAM-Dt89U_nU.js +1 -0
  36. package/dist/client/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
  37. package/dist/client/assets/chunk-NZK2D7GU-C2Ev_f_w.js +1 -0
  38. package/dist/client/assets/chunk-O5CBEL6O-BeZ3MFnJ.js +70 -0
  39. package/dist/client/assets/chunk-QZHKN3VN-BmDOSWHp.js +1 -0
  40. package/dist/client/assets/chunk-WU5MYG2G-gLuInTk4.js +1 -0
  41. package/dist/client/assets/chunk-XPW4576I-DPbDc8_T.js +32 -0
  42. package/dist/client/assets/classDiagram-4FO5ZUOK-DOpVKUrb.js +1 -0
  43. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DOpVKUrb.js +1 -0
  44. package/dist/client/assets/cose-bilkent-S5V4N54A-CCPyalms.js +1 -0
  45. package/dist/client/assets/cytoscape.esm-FqbQrHcz.js +321 -0
  46. package/dist/client/assets/dagre-BM42HDAG-6jrYMjX3.js +4 -0
  47. package/dist/client/assets/dagre-Bx709z4p.js +1 -0
  48. package/dist/client/assets/defaultLocale-C8Fc0cco.js +1 -0
  49. package/dist/client/assets/diagram-2AECGRRQ-dCQ_KoXl.js +43 -0
  50. package/dist/client/assets/diagram-5GNKFQAL-B3Xj4-XE.js +10 -0
  51. package/dist/client/assets/diagram-KO2AKTUF-DaGgF2yL.js +3 -0
  52. package/dist/client/assets/diagram-LMA3HP47-CF_c4tPT.js +24 -0
  53. package/dist/client/assets/diagram-OG6HWLK6-BgBHbJkT.js +24 -0
  54. package/dist/client/assets/dist-7wVkzZne.js +1 -0
  55. package/dist/client/assets/erDiagram-TEJ5UH35-BZgX1grR.js +85 -0
  56. package/dist/client/assets/eventmodeling-FCH6USID-BaI6RNKr.js +1 -0
  57. package/dist/client/assets/flowDiagram-I6XJVG4X-BFNUa12V.js +162 -0
  58. package/dist/client/assets/ganttDiagram-6RSMTGT7-C8t6BThI.js +292 -0
  59. package/dist/client/assets/gitGraph-WXDBUCRP-BlicfDkM.js +1 -0
  60. package/dist/client/assets/gitGraphDiagram-PVQCEYII-BmnIXuXP.js +106 -0
  61. package/dist/client/assets/graphlib-B8gBHxth.js +1 -0
  62. package/dist/client/assets/index-BfVtbKTZ.css +2 -0
  63. package/dist/client/assets/index-p8zzfjdu.js +87 -0
  64. package/dist/client/assets/info-J43DQDTF-DHLG38N3.js +1 -0
  65. package/dist/client/assets/infoDiagram-5YYISTIA-oQ-lq-45.js +2 -0
  66. package/dist/client/assets/init-D6jRqBbL.js +1 -0
  67. package/dist/client/assets/ishikawaDiagram-YF4QCWOH-DrODlBLD.js +70 -0
  68. package/dist/client/assets/journeyDiagram-JHISSGLW-Bxg5wCQz.js +139 -0
  69. package/dist/client/assets/kanban-definition-UN3LZRKU-DAdvUDIB.js +89 -0
  70. package/dist/client/assets/katex-Vhh-h91d.js +257 -0
  71. package/dist/client/assets/line-D8kNd46u.js +1 -0
  72. package/dist/client/assets/linear-jwGZSrg4.js +1 -0
  73. package/dist/client/assets/mermaid-parser.core-PVPfUfyU.js +4 -0
  74. package/dist/client/assets/mermaid.core-DwdP6CFg.js +9 -0
  75. package/dist/client/assets/mindmap-definition-RKZ34NQL-DeOoL56Q.js +96 -0
  76. package/dist/client/assets/ordinal-hYBb2elL.js +1 -0
  77. package/dist/client/assets/packet-YPE3B663-BBEaxdJP.js +1 -0
  78. package/dist/client/assets/path-BWPyau1x.js +1 -0
  79. package/dist/client/assets/pie-LRSECV5Y-fvTRFm-2.js +1 -0
  80. package/dist/client/assets/pieDiagram-4H26LBE5-oep1px2r.js +30 -0
  81. package/dist/client/assets/prism-bash-D6zCJ74D.js +1 -0
  82. package/dist/client/assets/prism-clojure-_z-zoNcE.js +1 -0
  83. package/dist/client/assets/prism-csharp-XPn2bw0d.js +1 -0
  84. package/dist/client/assets/prism-dart-CDv16Fbu.js +1 -0
  85. package/dist/client/assets/prism-elixir-DhHhV5Rj.js +1 -0
  86. package/dist/client/assets/prism-haskell-5hq1Nh9c.js +1 -0
  87. package/dist/client/assets/prism-hcl-B6Z0Ouoo.js +1 -0
  88. package/dist/client/assets/prism-java-6wtusvIl.js +1 -0
  89. package/dist/client/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
  90. package/dist/client/assets/prism-nix-C88dTX93.js +1 -0
  91. package/dist/client/assets/prism-perl-Lsbi34sH.js +1 -0
  92. package/dist/client/assets/prism-php-Co4fM40K.js +1 -0
  93. package/dist/client/assets/prism-protobuf-yzSHlhwy.js +1 -0
  94. package/dist/client/assets/prism-ruby-DFCASkuD.js +1 -0
  95. package/dist/client/assets/prism-scala-VkM3wCOL.js +1 -0
  96. package/dist/client/assets/prism-solidity-DTom4k5p.js +1 -0
  97. package/dist/client/assets/prism-sql-CYhVDMEE.js +1 -0
  98. package/dist/client/assets/prism-vim-BgpkjCK5.js +1 -0
  99. package/dist/client/assets/quadrantDiagram-W4KKPZXB-D9l66CHb.js +7 -0
  100. package/dist/client/assets/radar-GUYGQ44K-DAU5EPVd.js +1 -0
  101. package/dist/client/assets/requirementDiagram-4Y6WPE33-helpQlwb.js +84 -0
  102. package/dist/client/assets/rough.esm-CSKSodPl.js +1 -0
  103. package/dist/client/assets/sankeyDiagram-5OEKKPKP-C9sNuuvA.js +40 -0
  104. package/dist/client/assets/sequenceDiagram-3UESZ5HK-BP0bhCwQ.js +162 -0
  105. package/dist/client/assets/src-CDAPGf2l.js +1 -0
  106. package/dist/client/assets/stateDiagram-AJRCARHV-CsFizpla.js +1 -0
  107. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Bvn8tanl.js +1 -0
  108. package/dist/client/assets/timeline-definition-PNZ67QCA-BnkmHs1F.js +120 -0
  109. package/dist/client/assets/treeView-BLDUP644-DD9HZO5W.js +1 -0
  110. package/dist/client/assets/treemap-LRROVOQU-BBKuQ1lo.js +1 -0
  111. package/dist/client/assets/vennDiagram-CIIHVFJN-BPFTHflF.js +34 -0
  112. package/dist/client/assets/wardley-L42UT6IY-y88gDkwb.js +1 -0
  113. package/dist/client/assets/wardleyDiagram-YWT4CUSO-BlcA7IVh.js +78 -0
  114. package/dist/client/assets/xychartDiagram-2RQKCTM6-DX5vUCRH.js +7 -0
  115. package/dist/client/favicon.png +0 -0
  116. package/dist/client/index.html +14 -0
  117. package/dist/client/logo.png +0 -0
  118. package/dist/client/site-data/blobs/080c0e6/cHVibGljL3NpdGUtZGF0YS9vZy1pbWFnZS5wbmc.png +0 -0
  119. package/dist/client/site-data/blobs/55f23a1/bGFuZGluZy9wdWJsaWMvZGlmaXQvbG9nby5wbmc.png +0 -0
  120. package/dist/client/site-data/blobs/66ff7c6/cHVibGljL2xvZ28ucG5n.png +0 -0
  121. package/dist/client/site-data/blobs/e6977fe/cHVibGljL2xvZ28ucG5n.png +0 -0
  122. package/dist/client/site-data/manifest.json +1 -0
  123. package/dist/client/site-data/og-image.png +0 -0
  124. package/dist/client/site-data/snapshots/55f23a1...080c0e6.json +1 -0
  125. package/dist/client/site-data/snapshots/66ff7c6...e6977fe.json +1 -0
  126. package/dist/client/site-data/snapshots/7d40fd4...a72112f-comments.json +1 -0
  127. package/dist/client/site-data/snapshots/7d40fd4...a72112f.json +1 -0
  128. package/dist/server/file-watcher.d.ts +33 -0
  129. package/dist/server/file-watcher.js +340 -0
  130. package/dist/server/generated-file-check.d.ts +17 -0
  131. package/dist/server/generated-file-check.js +280 -0
  132. package/dist/server/git-diff.d.ts +94 -0
  133. package/dist/server/git-diff.js +909 -0
  134. package/dist/server/grouping/cache.d.ts +3 -0
  135. package/dist/server/grouping/cache.js +22 -0
  136. package/dist/server/grouping/claudeCli.d.ts +10 -0
  137. package/dist/server/grouping/claudeCli.js +95 -0
  138. package/dist/server/grouping/digest.d.ts +15 -0
  139. package/dist/server/grouping/digest.js +38 -0
  140. package/dist/server/grouping/reconcile.d.ts +6 -0
  141. package/dist/server/grouping/reconcile.js +34 -0
  142. package/dist/server/grouping/types.d.ts +9 -0
  143. package/dist/server/grouping/types.js +1 -0
  144. package/dist/server/server.d.ts +37 -0
  145. package/dist/server/server.js +1283 -0
  146. package/dist/shared/grouping.d.ts +4 -0
  147. package/dist/shared/grouping.js +170 -0
  148. package/dist/types/diff.d.ts +263 -0
  149. package/dist/types/diff.js +1 -0
  150. package/dist/types/watch.d.ts +59 -0
  151. package/dist/types/watch.js +8 -0
  152. package/dist/utils/commentFormatting.d.ts +6 -0
  153. package/dist/utils/commentFormatting.js +90 -0
  154. package/dist/utils/commentImports.d.ts +11 -0
  155. package/dist/utils/commentImports.js +386 -0
  156. package/dist/utils/createId.d.ts +1 -0
  157. package/dist/utils/createId.js +5 -0
  158. package/dist/utils/diffMode.d.ts +3 -0
  159. package/dist/utils/diffMode.js +13 -0
  160. package/dist/utils/diffSelection.d.ts +13 -0
  161. package/dist/utils/diffSelection.js +39 -0
  162. package/dist/utils/editorOptions.d.ts +60 -0
  163. package/dist/utils/editorOptions.js +171 -0
  164. package/dist/utils/fileUtils.d.ts +12 -0
  165. package/dist/utils/fileUtils.js +21 -0
  166. package/dist/utils/hubConfig.d.ts +32 -0
  167. package/dist/utils/hubConfig.js +51 -0
  168. package/dist/utils/suggestionUtils.d.ts +19 -0
  169. package/dist/utils/suggestionUtils.js +31 -0
  170. package/dist/utils/turnLog.d.ts +47 -0
  171. package/dist/utils/turnLog.js +133 -0
  172. package/package.json +105 -0
@@ -0,0 +1,333 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
4
+ import { homedir, tmpdir } from 'os';
5
+ import { dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { Command } from 'commander';
8
+ import { deleteHubInfo, deleteServerInfo, readHubInfo, readServerInfo, readTurnLog, writeTurnLog, } from '../utils/turnLog.js';
9
+ import { readStdin } from './utils.js';
10
+ /** Git ref that anchors all live snapshot commits so their objects survive GC. */
11
+ const SNAPSHOT_REF = 'refs/difit/snapshots';
12
+ const PROMPT_PREVIEW_MAX = 120;
13
+ /** Per-session retention: keep at most this many most-recent turns. */
14
+ const MAX_TURNS_PER_SESSION = 50;
15
+ function gitEnv(extra = {}) {
16
+ return {
17
+ ...process.env,
18
+ GIT_AUTHOR_NAME: 'diff-easy',
19
+ GIT_AUTHOR_EMAIL: 'diff-easy@local',
20
+ GIT_COMMITTER_NAME: 'diff-easy',
21
+ GIT_COMMITTER_EMAIL: 'diff-easy@local',
22
+ ...extra,
23
+ };
24
+ }
25
+ function resolveRepoRoot(cwd) {
26
+ try {
27
+ return execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
28
+ stdio: ['ignore', 'pipe', 'ignore'],
29
+ })
30
+ .toString()
31
+ .trim();
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Capture the entire current working tree (tracked + untracked, honoring
39
+ * .gitignore) as a standalone git commit, without touching the user's real
40
+ * index or working tree. The commit is kept alive by reanchorSnapshots().
41
+ */
42
+ function snapshotWorkingTree(repoRoot) {
43
+ const tmpIndex = join(tmpdir(), `difit-index-${process.pid}-${randomUUID()}`);
44
+ // Seed the temp index from the repo's real index so `git add -A` reuses the
45
+ // stat cache and only re-hashes actually-changed files. Without this, a fresh
46
+ // (empty) index forces git to lstat + hash the ENTIRE working tree every turn
47
+ // — seconds on a large repo, twice per turn, on the agent's critical path.
48
+ try {
49
+ const gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], {
50
+ cwd: repoRoot,
51
+ stdio: ['ignore', 'pipe', 'ignore'],
52
+ })
53
+ .toString()
54
+ .trim();
55
+ const realIndex = join(gitDir, 'index');
56
+ if (gitDir && existsSync(realIndex)) {
57
+ copyFileSync(realIndex, tmpIndex);
58
+ }
59
+ }
60
+ catch {
61
+ // fall back to an empty temp index (slower but correct)
62
+ }
63
+ const env = gitEnv({ GIT_INDEX_FILE: tmpIndex });
64
+ try {
65
+ execFileSync('git', ['add', '-A'], { cwd: repoRoot, env, stdio: 'ignore' });
66
+ const tree = execFileSync('git', ['write-tree'], { cwd: repoRoot, env }).toString().trim();
67
+ const commit = execFileSync('git', ['commit-tree', tree, '-m', 'diff-easy turn snapshot'], {
68
+ cwd: repoRoot,
69
+ env,
70
+ })
71
+ .toString()
72
+ .trim();
73
+ return { commit, tree };
74
+ }
75
+ finally {
76
+ try {
77
+ unlinkSync(tmpIndex);
78
+ }
79
+ catch {
80
+ // best-effort cleanup
81
+ }
82
+ }
83
+ }
84
+ /** Keep at most MAX_TURNS_PER_SESSION most-recent turns per session. */
85
+ function pruneLog(log) {
86
+ const bySession = new Map();
87
+ for (const turn of log) {
88
+ const arr = bySession.get(turn.sessionId) ?? [];
89
+ arr.push(turn);
90
+ bySession.set(turn.sessionId, arr);
91
+ }
92
+ if ([...bySession.values()].every((arr) => arr.length <= MAX_TURNS_PER_SESSION)) {
93
+ return log;
94
+ }
95
+ const keep = new Set();
96
+ for (const arr of bySession.values()) {
97
+ // log is chronological (appended in order), so the tail is most-recent.
98
+ for (const turn of arr.slice(-MAX_TURNS_PER_SESSION))
99
+ keep.add(turn.id);
100
+ }
101
+ return log.filter((turn) => keep.has(turn.id));
102
+ }
103
+ /**
104
+ * Point SNAPSHOT_REF at a single anchor commit whose parents are every snapshot
105
+ * commit still referenced by the (pruned) log. Snapshot commits dropped by
106
+ * pruning are no longer reachable from any ref, so a routine `git gc` reclaims
107
+ * them — bounding `.git` growth.
108
+ */
109
+ function reanchorSnapshots(repoRoot, log, tree) {
110
+ const commits = new Set();
111
+ for (const turn of log) {
112
+ if (turn.commitBefore)
113
+ commits.add(turn.commitBefore);
114
+ if (turn.commitAfter)
115
+ commits.add(turn.commitAfter);
116
+ }
117
+ const env = gitEnv();
118
+ if (commits.size === 0) {
119
+ try {
120
+ execFileSync('git', ['update-ref', '-d', SNAPSHOT_REF], { cwd: repoRoot, stdio: 'ignore' });
121
+ }
122
+ catch {
123
+ // ref may not exist yet
124
+ }
125
+ return;
126
+ }
127
+ const parentArgs = [];
128
+ for (const commit of commits)
129
+ parentArgs.push('-p', commit);
130
+ const anchor = execFileSync('git', ['commit-tree', tree, ...parentArgs, '-m', 'diff-easy snapshot anchor'], { cwd: repoRoot, env })
131
+ .toString()
132
+ .trim();
133
+ execFileSync('git', ['update-ref', SNAPSHOT_REF, anchor], { cwd: repoRoot, env });
134
+ }
135
+ function previewOf(prompt) {
136
+ if (!prompt)
137
+ return null;
138
+ const oneLine = prompt.replace(/\s+/gu, ' ').trim();
139
+ if (oneLine.length <= PROMPT_PREVIEW_MAX)
140
+ return oneLine;
141
+ return `${oneLine.slice(0, PROMPT_PREVIEW_MAX - 1)}…`;
142
+ }
143
+ /** True if a process with this pid currently exists (EPERM still means alive). */
144
+ function isPidAlive(pid) {
145
+ try {
146
+ process.kill(pid, 0);
147
+ return true;
148
+ }
149
+ catch (err) {
150
+ return err.code === 'EPERM';
151
+ }
152
+ }
153
+ async function pingTurnComplete(port) {
154
+ try {
155
+ const controller = new AbortController();
156
+ const timer = setTimeout(() => controller.abort(), 300);
157
+ await fetch(`http://127.0.0.1:${port}/events/turn-complete`, {
158
+ method: 'POST',
159
+ signal: controller.signal,
160
+ }).catch(() => undefined);
161
+ clearTimeout(timer);
162
+ }
163
+ catch {
164
+ // server not running / unreachable — fine, capture is decoupled from viewing
165
+ }
166
+ }
167
+ /**
168
+ * Best-effort live-refresh ping. Capped so it never slows the agent's turn.
169
+ *
170
+ * A viewer advertises itself in one of two ways: a per-repo `server.json` (a
171
+ * standalone `difit` opened on this repo) or the global `hub.json` (the
172
+ * always-on hub, which serves many repos and never writes a per-repo file).
173
+ * Ping every live advertisement so the timeline refreshes regardless of which
174
+ * one the user has open — without this, hub users only saw new turns after a
175
+ * manual refresh.
176
+ */
177
+ export async function notifyServer(repoRoot) {
178
+ const targets = [];
179
+ const repoInfo = readServerInfo(repoRoot);
180
+ if (repoInfo) {
181
+ targets.push({ info: repoInfo, clearStale: () => deleteServerInfo(repoRoot) });
182
+ }
183
+ const hubInfo = readHubInfo();
184
+ if (hubInfo) {
185
+ targets.push({ info: hubInfo, clearStale: () => deleteHubInfo() });
186
+ }
187
+ const pingedPorts = new Set();
188
+ for (const { info, clearStale } of targets) {
189
+ // A stale advertisement (the viewer exited without cleaning up) would make
190
+ // us POST to whatever process now holds that port. If the recorded pid is
191
+ // dead, skip the ping and clear the stale file.
192
+ if (typeof info.pid === 'number' && !isPidAlive(info.pid)) {
193
+ clearStale();
194
+ continue;
195
+ }
196
+ if (pingedPorts.has(info.port)) {
197
+ continue;
198
+ }
199
+ pingedPorts.add(info.port);
200
+ await pingTurnComplete(info.port);
201
+ }
202
+ }
203
+ async function runSnapshot() {
204
+ // Skip when this Claude session was spawned internally by diff-easy itself
205
+ // (e.g. the file-grouping provider's `claude -p` call). Otherwise diff-easy
206
+ // would record its own tool invocations as spurious agent turns — which also
207
+ // fires the turn-complete SSE and hijacks the live view away from the diff
208
+ // the user is grouping. See src/server/grouping/claudeCli.ts.
209
+ if (process.env.DIFIT_INTERNAL_CLAUDE === '1')
210
+ return;
211
+ // Never throw: a failing hook must not block the agent.
212
+ try {
213
+ const input = await readStdin().catch(() => '');
214
+ let payload = {};
215
+ try {
216
+ payload = JSON.parse(input || '{}');
217
+ }
218
+ catch {
219
+ payload = {};
220
+ }
221
+ const cwd = payload.cwd || process.cwd();
222
+ const repoRoot = resolveRepoRoot(cwd);
223
+ if (!repoRoot)
224
+ return; // no-op outside a git repo
225
+ const { commit, tree } = snapshotWorkingTree(repoRoot);
226
+ const sessionId = payload.session_id || 'unknown';
227
+ const event = payload.hook_event_name || '';
228
+ const now = Date.now();
229
+ const log = readTurnLog(repoRoot);
230
+ if (event === 'UserPromptSubmit') {
231
+ const prompt = typeof payload.prompt === 'string' ? payload.prompt : null;
232
+ const record = {
233
+ id: randomUUID(),
234
+ sessionId,
235
+ prompt,
236
+ promptPreview: previewOf(prompt),
237
+ commitBefore: commit,
238
+ commitAfter: null,
239
+ startedAt: now,
240
+ endedAt: null,
241
+ status: 'open',
242
+ };
243
+ log.push(record);
244
+ }
245
+ else {
246
+ // Stop / SubagentStop / anything else closes the current open turn.
247
+ const openTurn = [...log]
248
+ .reverse()
249
+ .find((t) => t.sessionId === sessionId && t.status === 'open');
250
+ if (openTurn) {
251
+ openTurn.commitAfter = commit;
252
+ openTurn.endedAt = now;
253
+ openTurn.status = 'closed';
254
+ }
255
+ else {
256
+ const lastForSession = [...log].reverse().find((t) => t.sessionId === sessionId);
257
+ log.push({
258
+ id: randomUUID(),
259
+ sessionId,
260
+ prompt: null,
261
+ promptPreview: null,
262
+ commitBefore: lastForSession?.commitAfter ?? commit,
263
+ commitAfter: commit,
264
+ startedAt: now,
265
+ endedAt: now,
266
+ status: 'closed',
267
+ });
268
+ }
269
+ }
270
+ const pruned = pruneLog(log);
271
+ writeTurnLog(repoRoot, pruned);
272
+ reanchorSnapshots(repoRoot, pruned, tree);
273
+ await notifyServer(repoRoot);
274
+ }
275
+ catch {
276
+ // swallow — hook must always exit 0
277
+ }
278
+ }
279
+ export function createSnapshotCommand() {
280
+ return new Command('snapshot')
281
+ .description('Internal: capture a working-tree snapshot for a Claude Code turn (hook entrypoint)')
282
+ .action(async () => {
283
+ await runSnapshot();
284
+ process.exit(0);
285
+ });
286
+ }
287
+ const HOOK_EVENTS = ['UserPromptSubmit', 'Stop'];
288
+ function snapshotCommandString() {
289
+ // Absolute, install-independent invocation of this very CLI.
290
+ const cliPath = fileURLToPath(import.meta.url).replace(/turns\.js$/u, 'index.js');
291
+ return `"${process.execPath}" "${cliPath}" snapshot`;
292
+ }
293
+ function readSettings(path) {
294
+ try {
295
+ return JSON.parse(readFileSync(path, 'utf8'));
296
+ }
297
+ catch {
298
+ return {};
299
+ }
300
+ }
301
+ export function createInitCommand() {
302
+ return new Command('init')
303
+ .description('Install the Claude Code hooks that capture per-turn diffs')
304
+ .option('--project', 'install into ./.claude/settings.json instead of ~/.claude/settings.json')
305
+ .action((options) => {
306
+ const settingsPath = options.project
307
+ ? join(process.cwd(), '.claude', 'settings.json')
308
+ : join(homedir(), '.claude', 'settings.json');
309
+ const settings = readSettings(settingsPath);
310
+ settings.hooks ??= {};
311
+ const command = snapshotCommandString();
312
+ let added = 0;
313
+ let already = 0;
314
+ for (const event of HOOK_EVENTS) {
315
+ const entries = (settings.hooks[event] ??= []);
316
+ const exists = entries.some((entry) => entry.hooks?.some((h) => h.command?.includes('snapshot') && h.command?.includes('index.js')));
317
+ if (exists) {
318
+ already += 1;
319
+ continue;
320
+ }
321
+ entries.push({ hooks: [{ type: 'command', command }] });
322
+ added += 1;
323
+ }
324
+ const dir = dirname(settingsPath);
325
+ if (!existsSync(dir))
326
+ mkdirSync(dir, { recursive: true });
327
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
328
+ console.log(`diff-easy: hooks written to ${settingsPath}`);
329
+ console.log(` added ${added} hook(s), ${already} already present`);
330
+ console.log(' events: UserPromptSubmit, Stop');
331
+ console.log('\nStart (or restart) Claude Code in your repo, then run `diff-easy` to review per-turn diffs.');
332
+ });
333
+ }
@@ -0,0 +1,27 @@
1
+ import { type Stats } from 'node:fs';
2
+ import type { SimpleGit } from 'simple-git';
3
+ import type { CommentImport } from '../types/diff.js';
4
+ type StdinStat = Pick<Stats, 'isFIFO' | 'isFile' | 'isSocket'>;
5
+ type StdinSource = 'pipe' | 'file' | 'socket' | 'tty';
6
+ export declare function detectStdinSource(stdinStat?: StdinStat): StdinSource;
7
+ interface ShouldReadStdinOptions {
8
+ commitish: string;
9
+ hasPositionalArgs: boolean;
10
+ hasPrOption: boolean;
11
+ stdinSource?: StdinSource;
12
+ }
13
+ export declare function shouldReadStdin(options: ShouldReadStdinOptions): boolean;
14
+ export declare function getGitRoot(): string;
15
+ export declare function validateCommitish(commitish: string): boolean;
16
+ export declare function shortHash(hash: string): string;
17
+ export declare function createCommitRangeString(baseHash: string, targetHash: string): string;
18
+ export declare function parseCommentOptions(commentValues: string[]): CommentImport[];
19
+ export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
20
+ valid: boolean;
21
+ error?: string;
22
+ };
23
+ export declare function findUntrackedFiles(git: SimpleGit): Promise<string[]>;
24
+ export declare function markFilesIntentToAdd(git: SimpleGit, files: string[]): Promise<void>;
25
+ export declare function promptUser(message: string): Promise<boolean>;
26
+ export declare function readStdin(): Promise<string>;
27
+ export {};
@@ -0,0 +1,204 @@
1
+ import { execSync } from 'child_process';
2
+ import { fstatSync } from 'node:fs';
3
+ import { createInterface } from 'readline/promises';
4
+ import { parseCommentImportValue } from '../utils/commentImports.js';
5
+ export function detectStdinSource(stdinStat = fstatSync(0)) {
6
+ if (stdinStat.isFIFO()) {
7
+ return 'pipe';
8
+ }
9
+ if (stdinStat.isFile()) {
10
+ return 'file';
11
+ }
12
+ if (stdinStat.isSocket()) {
13
+ return 'socket';
14
+ }
15
+ return 'tty';
16
+ }
17
+ export function shouldReadStdin(options) {
18
+ if (options.commitish === '-') {
19
+ return true;
20
+ }
21
+ if (options.hasPositionalArgs || options.hasPrOption) {
22
+ return false;
23
+ }
24
+ const stdinSource = options.stdinSource ?? detectStdinSource();
25
+ return stdinSource === 'pipe' || stdinSource === 'file' || stdinSource === 'socket';
26
+ }
27
+ export function getGitRoot() {
28
+ try {
29
+ const result = execSync('git rev-parse --show-toplevel', {
30
+ encoding: 'utf8',
31
+ stdio: 'pipe',
32
+ });
33
+ return result.trim();
34
+ }
35
+ catch {
36
+ throw new Error('Not a git repository (or any of the parent directories)');
37
+ }
38
+ }
39
+ export function validateCommitish(commitish) {
40
+ if (!commitish || typeof commitish !== 'string') {
41
+ return false;
42
+ }
43
+ const trimmed = commitish.trim();
44
+ if (trimmed.length === 0) {
45
+ return false;
46
+ }
47
+ if (trimmed === '.' || trimmed === 'working' || trimmed === 'staged') {
48
+ return true; // Allow special keywords for working directory and staging area diff
49
+ }
50
+ const baseCommitish = stripRevisionSuffix(trimmed);
51
+ if (baseCommitish.length === 0) {
52
+ return false;
53
+ }
54
+ return isValidCommitishBase(baseCommitish);
55
+ }
56
+ function isValidCommitishBase(baseCommitish) {
57
+ const validBasePatterns = [
58
+ /^[a-f0-9]{4,40}$/i, // SHA hashes
59
+ /^HEAD$/, // HEAD
60
+ /^@$/, // @ is Git alias for HEAD
61
+ ];
62
+ if (validBasePatterns.some((pattern) => pattern.test(baseCommitish))) {
63
+ return true;
64
+ }
65
+ // For branch, tag, and remote refs, use git's ref naming rules.
66
+ return isValidBranchName(baseCommitish);
67
+ }
68
+ function stripRevisionSuffix(commitish) {
69
+ let suffixStart = commitish.length;
70
+ while (suffixStart > 0) {
71
+ const current = commitish[suffixStart - 1];
72
+ if (current === '^') {
73
+ suffixStart--;
74
+ continue;
75
+ }
76
+ if (current === '~') {
77
+ suffixStart--;
78
+ continue;
79
+ }
80
+ if (!isAsciiDigit(current)) {
81
+ break;
82
+ }
83
+ let digitStart = suffixStart - 1;
84
+ while (digitStart > 0 && isAsciiDigit(commitish[digitStart - 1])) {
85
+ digitStart--;
86
+ }
87
+ const operator = commitish[digitStart - 1];
88
+ if (operator !== '^' && operator !== '~') {
89
+ break;
90
+ }
91
+ suffixStart = digitStart - 1;
92
+ }
93
+ return commitish.slice(0, suffixStart);
94
+ }
95
+ function isAsciiDigit(char) {
96
+ return char >= '0' && char <= '9';
97
+ }
98
+ function isValidBranchName(name) {
99
+ // Git branch name rules
100
+ if (name.startsWith('-'))
101
+ return false; // Cannot start with dash
102
+ if (name.endsWith('.'))
103
+ return false; // Cannot end with dot
104
+ // @ is a valid Git alias for HEAD, so we should allow it
105
+ if (name.includes('..'))
106
+ return false; // No consecutive dots
107
+ if (name.includes('@{'))
108
+ return false; // No @{ sequence
109
+ if (name.includes('//'))
110
+ return false; // No consecutive slashes
111
+ if (name.startsWith('/') || name.endsWith('/'))
112
+ return false; // No leading/trailing slashes
113
+ if (name.endsWith('.lock'))
114
+ return false; // Cannot end with .lock
115
+ // Check for forbidden characters
116
+ const forbiddenChars = /[~^:?*[\\\x00-\x20\x7F]/;
117
+ if (forbiddenChars.test(name))
118
+ return false;
119
+ // Check path components
120
+ const components = name.split('/');
121
+ for (const component of components) {
122
+ if (component === '')
123
+ return false; // Empty component
124
+ if (component.startsWith('.'))
125
+ return false; // Component cannot start with dot
126
+ if (component.endsWith('.lock'))
127
+ return false; // Component cannot end with .lock
128
+ }
129
+ return true;
130
+ }
131
+ export function shortHash(hash) {
132
+ return hash.substring(0, 7);
133
+ }
134
+ export function createCommitRangeString(baseHash, targetHash) {
135
+ return `${baseHash}...${targetHash}`;
136
+ }
137
+ export function parseCommentOptions(commentValues) {
138
+ return commentValues.flatMap((value) => parseCommentImportValue(value));
139
+ }
140
+ export function validateDiffArguments(targetCommitish, baseCommitish) {
141
+ // Validate target commitish format
142
+ if (!validateCommitish(targetCommitish)) {
143
+ return { valid: false, error: 'Invalid target commit-ish format' };
144
+ }
145
+ // Validate base commitish format if provided
146
+ if (baseCommitish !== undefined && !validateCommitish(baseCommitish)) {
147
+ return { valid: false, error: 'Invalid base commit-ish format' };
148
+ }
149
+ // Special arguments are only allowed in target, not base (except staged with working)
150
+ const specialArgs = ['working', 'staged', '.'];
151
+ if (baseCommitish && specialArgs.includes(baseCommitish)) {
152
+ // Allow 'staged' as base only when target is 'working'
153
+ if (baseCommitish === 'staged' && targetCommitish === 'working') {
154
+ // This is valid: working vs staged
155
+ }
156
+ else {
157
+ return {
158
+ valid: false,
159
+ error: `Special arguments (working, staged, .) are only allowed as target, not base. Got base: ${baseCommitish}`,
160
+ };
161
+ }
162
+ }
163
+ // Cannot compare same values
164
+ if (targetCommitish === baseCommitish) {
165
+ return {
166
+ valid: false,
167
+ error: `Cannot compare ${targetCommitish} with itself`,
168
+ };
169
+ }
170
+ // "working" shows unstaged changes and can only be compared with staging area
171
+ if (targetCommitish === 'working' && baseCommitish && baseCommitish !== 'staged') {
172
+ return {
173
+ valid: false,
174
+ error: '"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.',
175
+ };
176
+ }
177
+ return { valid: true };
178
+ }
179
+ export async function findUntrackedFiles(git) {
180
+ const status = await git.status();
181
+ return status.not_added;
182
+ }
183
+ // Add files with --intent-to-add to make them visible in `git diff` without staging content
184
+ export async function markFilesIntentToAdd(git, files) {
185
+ await git.add(['--intent-to-add', ...files]);
186
+ }
187
+ export async function promptUser(message) {
188
+ const rl = createInterface({
189
+ input: process.stdin,
190
+ output: process.stdout,
191
+ });
192
+ const answer = await rl.question(message);
193
+ rl.close();
194
+ // Empty string (Enter) or 'y', 'yes' return true
195
+ const trimmed = answer.trim().toLowerCase();
196
+ return trimmed === '' || ['y', 'yes'].includes(trimmed);
197
+ }
198
+ export async function readStdin() {
199
+ const chunks = [];
200
+ for await (const chunk of process.stdin) {
201
+ chunks.push(chunk);
202
+ }
203
+ return Buffer.concat(chunks).toString('utf8');
204
+ }
@@ -0,0 +1 @@
1
+ import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-7wVkzZne.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
@@ -0,0 +1 @@
1
+ import"./chunk-NNHCCRGN-DlpIbxXb.js";import{x as e}from"./mermaid-parser.core-PVPfUfyU.js";export{e as createArchitectureServices};