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,33 @@
1
+ import { type Response } from 'express';
2
+ import { DiffMode, type WatchEvent } from '../types/watch.js';
3
+ export declare class FileWatcherService {
4
+ private subscriptions;
5
+ private clients;
6
+ private debounceTimer;
7
+ private config;
8
+ private git;
9
+ constructor();
10
+ start(diffMode: DiffMode, watchPath: string, debounceMs?: number, onCacheInvalidate?: () => void): Promise<void>;
11
+ private setupWatchers;
12
+ /**
13
+ * Resolve the actual git directory path, handling git worktrees.
14
+ * Uses git rev-parse --git-dir which correctly handles both normal repos and worktrees.
15
+ */
16
+ private resolveGitDir;
17
+ private shouldIgnoreEvent;
18
+ private matchesPattern;
19
+ private isRelevantGitFile;
20
+ private debouncedBroadcast;
21
+ stop(): Promise<void>;
22
+ addClient(res: Response): void;
23
+ removeClient(res: Response): void;
24
+ broadcast(event: WatchEvent): void;
25
+ private broadcastChange;
26
+ private sendToClient;
27
+ private determineChangeType;
28
+ /**
29
+ * Return the subset of the given absolute paths that git ignores, using a
30
+ * single `git check-ignore` invocation rather than one process per path.
31
+ */
32
+ private getGitignoredPaths;
33
+ }
@@ -0,0 +1,340 @@
1
+ import { join, resolve } from 'path';
2
+ import { subscribe } from '@parcel/watcher';
3
+ import { simpleGit } from 'simple-git';
4
+ import { DiffMode } from '../types/watch.js';
5
+ // Heavy/generated working-tree directories that are virtually always gitignored.
6
+ // Pre-ignoring them keeps the recursive watcher from registering thousands of
7
+ // watches and from running `git check-ignore` per event when another dev server
8
+ // (e.g. `rails server`, webpack) churns these paths. They're gitignored, so they
9
+ // never appear in diffs anyway — losing live-reload detection for them is moot.
10
+ const COMMON_WORKTREE_IGNORES = [
11
+ 'node_modules/**',
12
+ 'dist/**',
13
+ 'build/**',
14
+ 'out/**',
15
+ 'coverage/**',
16
+ '.next/**',
17
+ '.nuxt/**',
18
+ '.svelte-kit/**',
19
+ '.turbo/**',
20
+ '.cache/**',
21
+ '.parcel-cache/**',
22
+ 'tmp/**',
23
+ 'log/**',
24
+ 'logs/**',
25
+ 'vendor/bundle/**',
26
+ '.bundle/**',
27
+ 'public/assets/**',
28
+ 'public/packs/**',
29
+ 'public/packs-test/**',
30
+ 'storage/**',
31
+ 'target/**',
32
+ '__pycache__/**',
33
+ '.venv/**',
34
+ 'venv/**',
35
+ '.pytest_cache/**',
36
+ '.mypy_cache/**',
37
+ ];
38
+ const GIT_IGNORES = ['.git/objects/**', '.git/refs/**'];
39
+ const MODE_WATCH_CONFIGS = {
40
+ [DiffMode.DEFAULT]: {
41
+ watchPaths: ['.git'],
42
+ ignore: [...GIT_IGNORES, 'node_modules/**'],
43
+ },
44
+ [DiffMode.WORKING]: {
45
+ watchPaths: ['.', '.git'],
46
+ ignore: [...GIT_IGNORES, ...COMMON_WORKTREE_IGNORES],
47
+ },
48
+ [DiffMode.STAGED]: {
49
+ watchPaths: ['.git'],
50
+ ignore: [...GIT_IGNORES],
51
+ },
52
+ [DiffMode.DOT]: {
53
+ watchPaths: ['.', '.git'],
54
+ // Ignore noisy per-ref reflogs but NOT `.git/logs/HEAD`: it's appended on
55
+ // every HEAD move (commit / reset / checkout / merge), which is exactly what
56
+ // must invalidate a `HEAD..'.'` (working-tree / current-branch) diff. Without
57
+ // it, a commit produced no watch event and the diff went stale.
58
+ ignore: [
59
+ ...GIT_IGNORES,
60
+ '.git/FETCH_HEAD',
61
+ '.git/ORIG_HEAD',
62
+ '.git/logs/refs/**',
63
+ ...COMMON_WORKTREE_IGNORES,
64
+ ],
65
+ },
66
+ [DiffMode.SPECIFIC]: {
67
+ watchPaths: [], // No watching for specific commits
68
+ ignore: [],
69
+ },
70
+ };
71
+ export class FileWatcherService {
72
+ subscriptions = [];
73
+ clients = [];
74
+ debounceTimer = null;
75
+ config = null;
76
+ git = null;
77
+ constructor() { }
78
+ async start(diffMode, watchPath, debounceMs = 300, onCacheInvalidate) {
79
+ this.config = { watchPath, diffMode, debounceMs, onCacheInvalidate };
80
+ // Stop existing watchers
81
+ await this.stop();
82
+ // No watching for specific commit comparisons
83
+ if (diffMode === DiffMode.SPECIFIC) {
84
+ console.log('🔍 File watching disabled (specific commit comparison)');
85
+ return;
86
+ }
87
+ const modeConfig = MODE_WATCH_CONFIGS[diffMode];
88
+ // Initialize git instance for .gitignore checking
89
+ this.git = simpleGit(watchPath);
90
+ try {
91
+ await this.setupWatchers(modeConfig, watchPath);
92
+ }
93
+ catch (error) {
94
+ console.error('❌ Failed to start file watcher:', error);
95
+ throw error;
96
+ }
97
+ }
98
+ async setupWatchers(modeConfig, basePath) {
99
+ for (const watchPath of modeConfig.watchPaths) {
100
+ let fullPath = join(basePath, watchPath);
101
+ // Resolve git worktree path for .git directory
102
+ if (watchPath === '.git') {
103
+ fullPath = await this.resolveGitDir(basePath);
104
+ }
105
+ // The working-tree ('.') watch must not process .git internals. A repo with
106
+ // git's fsmonitor daemon enabled churns .git/fsmonitor--daemon/cookies/*
107
+ // continuously; those events aren't gitignored, so they'd stream into the
108
+ // handler and perpetually reset the debounce timer — starving the broadcast
109
+ // so auto-reload never fires (the "stale until restart" bug in large repos).
110
+ // The dedicated '.git' watch already covers the git-dir files we care about
111
+ // (HEAD / index / logs/HEAD), so the working-tree watch can ignore .git wholesale.
112
+ const effectiveIgnore = watchPath === '.' ? [...modeConfig.ignore, '.git/**'] : modeConfig.ignore;
113
+ try {
114
+ const subscription = (await subscribe(fullPath, async (err, events) => {
115
+ if (err) {
116
+ console.error(`Watch error for ${watchPath}:`, err);
117
+ return;
118
+ }
119
+ // First pass: drop events matching the static ignore patterns.
120
+ const candidates = events.filter((event) => !this.shouldIgnoreEvent(event.path, effectiveIgnore));
121
+ if (candidates.length === 0) {
122
+ return;
123
+ }
124
+ let relevantEvents = candidates;
125
+ if (watchPath === '.') {
126
+ // Working-tree watch: apply .gitignore with ONE `git check-ignore`
127
+ // for all paths, instead of spawning a git process per event —
128
+ // otherwise a file-churning build or dev server makes diff-easy
129
+ // fork git once per changed file.
130
+ const ignored = await this.getGitignoredPaths(candidates.map((event) => event.path), basePath);
131
+ relevantEvents = candidates.filter((event) => !ignored.has(event.path));
132
+ }
133
+ else if (watchPath === '.git') {
134
+ // Git-dir watch: only specific files (HEAD/index) matter.
135
+ relevantEvents = candidates.filter((event) => {
136
+ const fileName = event.path.replace(/.*[/\\]/, '');
137
+ return this.isRelevantGitFile(fileName, this.config?.diffMode);
138
+ });
139
+ }
140
+ if (relevantEvents.length > 0) {
141
+ this.debouncedBroadcast();
142
+ }
143
+ }, {
144
+ ignore: effectiveIgnore,
145
+ }));
146
+ this.subscriptions.push(subscription);
147
+ }
148
+ catch (error) {
149
+ console.warn(`⚠️ Could not watch ${fullPath}:`, error);
150
+ // Continue with other watchers even if one fails
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Resolve the actual git directory path, handling git worktrees.
156
+ * Uses git rev-parse --git-dir which correctly handles both normal repos and worktrees.
157
+ */
158
+ async resolveGitDir(basePath) {
159
+ try {
160
+ const git = simpleGit(basePath);
161
+ const gitDir = await git.revparse(['--git-dir']);
162
+ return resolve(basePath, gitDir.trim());
163
+ }
164
+ catch {
165
+ // Fallback to default .git path
166
+ return join(basePath, '.git');
167
+ }
168
+ }
169
+ shouldIgnoreEvent(filePath, ignorePatterns) {
170
+ return ignorePatterns.some((pattern) => {
171
+ // Handle negation patterns (e.g., "!.git/index")
172
+ if (pattern.startsWith('!')) {
173
+ const positivePattern = pattern.slice(1);
174
+ return !this.matchesPattern(filePath, positivePattern);
175
+ }
176
+ return this.matchesPattern(filePath, pattern);
177
+ });
178
+ }
179
+ matchesPattern(filePath, pattern) {
180
+ // Simple glob pattern matching
181
+ const regex = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
182
+ return new RegExp(regex).test(filePath);
183
+ }
184
+ isRelevantGitFile(fileName, diffMode) {
185
+ if (!diffMode)
186
+ return false;
187
+ switch (diffMode) {
188
+ case DiffMode.DEFAULT:
189
+ case DiffMode.DOT:
190
+ return fileName === 'HEAD';
191
+ case DiffMode.WORKING:
192
+ case DiffMode.STAGED:
193
+ return fileName === 'index' || fileName === 'HEAD';
194
+ default:
195
+ return false;
196
+ }
197
+ }
198
+ debouncedBroadcast() {
199
+ if (this.debounceTimer) {
200
+ clearTimeout(this.debounceTimer);
201
+ }
202
+ const debounceMs = this.config?.debounceMs || 300;
203
+ this.debounceTimer = setTimeout(() => {
204
+ // Invalidate cache before broadcasting change
205
+ if (this.config?.onCacheInvalidate) {
206
+ this.config.onCacheInvalidate();
207
+ }
208
+ this.broadcastChange();
209
+ }, debounceMs);
210
+ }
211
+ async stop() {
212
+ // Clear debounce timer
213
+ if (this.debounceTimer) {
214
+ clearTimeout(this.debounceTimer);
215
+ this.debounceTimer = null;
216
+ }
217
+ // Unsubscribe from all watchers
218
+ await Promise.all(this.subscriptions.map(async (subscription) => {
219
+ try {
220
+ await subscription.unsubscribe();
221
+ }
222
+ catch (error) {
223
+ console.warn('Error unsubscribing from file watcher:', error);
224
+ }
225
+ }));
226
+ this.subscriptions = [];
227
+ // NOTE: do NOT clear `this.clients` here. SSE clients are connection-scoped
228
+ // — they register in `addClient` on `/api/watch` and are removed in
229
+ // `removeClient` when that request closes. `stop()` only tears down the
230
+ // filesystem watchers; it is also called by `start()` on every hub workspace
231
+ // switch. Clearing clients there would drop the browser's live EventSource
232
+ // from the broadcast list without closing its stream, so it never gets an
233
+ // error, never reconnects, and silently stops receiving turn-complete/reload
234
+ // events — i.e. the agent-turn timeline would stop updating after the first
235
+ // workspace open. On full shutdown the process exits right after stop(), so
236
+ // there is nothing to clean up.
237
+ }
238
+ addClient(res) {
239
+ this.clients.push(res);
240
+ // Send initial connection event
241
+ this.sendToClient(res, {
242
+ type: 'connected',
243
+ diffMode: this.config?.diffMode || DiffMode.DEFAULT,
244
+ changeType: 'file',
245
+ timestamp: new Date().toISOString(),
246
+ message: `Connected to file watcher (${this.config?.diffMode} mode)`,
247
+ });
248
+ }
249
+ removeClient(res) {
250
+ const index = this.clients.indexOf(res);
251
+ if (index > -1) {
252
+ this.clients.splice(index, 1);
253
+ }
254
+ }
255
+ broadcast(event) {
256
+ if (this.clients.length === 0) {
257
+ return;
258
+ }
259
+ this.clients.forEach((client) => {
260
+ this.sendToClient(client, event);
261
+ });
262
+ }
263
+ broadcastChange() {
264
+ if (this.clients.length === 0 || !this.config) {
265
+ return;
266
+ }
267
+ const changeType = this.determineChangeType();
268
+ const event = {
269
+ type: 'reload',
270
+ diffMode: this.config.diffMode,
271
+ changeType,
272
+ timestamp: new Date().toISOString(),
273
+ message: `Changes detected in ${this.config.diffMode} mode`,
274
+ };
275
+ this.broadcast(event);
276
+ }
277
+ sendToClient(client, event) {
278
+ try {
279
+ client.write(`data: ${JSON.stringify(event)}\n\n`);
280
+ }
281
+ catch (error) {
282
+ console.error('Failed to send event to client:', error);
283
+ this.removeClient(client);
284
+ }
285
+ }
286
+ determineChangeType() {
287
+ if (!this.config)
288
+ return 'file';
289
+ switch (this.config.diffMode) {
290
+ case DiffMode.DEFAULT:
291
+ case DiffMode.DOT:
292
+ return 'commit'; // .git/HEAD changes indicate new commits
293
+ case DiffMode.STAGED:
294
+ return 'staging'; // .git/index changes
295
+ case DiffMode.WORKING:
296
+ return 'file'; // Both file and staging changes, default to file
297
+ default:
298
+ return 'file';
299
+ }
300
+ }
301
+ /**
302
+ * Return the subset of the given absolute paths that git ignores, using a
303
+ * single `git check-ignore` invocation rather than one process per path.
304
+ */
305
+ async getGitignoredPaths(filePaths, basePath) {
306
+ const ignored = new Set();
307
+ if (!this.git || filePaths.length === 0)
308
+ return ignored;
309
+ const prefix = `${basePath}/`;
310
+ const relToAbs = new Map();
311
+ for (const abs of filePaths) {
312
+ const rel = abs.startsWith(prefix) ? abs.slice(prefix.length) : abs;
313
+ relToAbs.set(rel, abs);
314
+ }
315
+ const rels = [...relToAbs.keys()];
316
+ // Chunk the paths so the argv stays well under the OS limit (ARG_MAX): a
317
+ // single `git check-ignore` with tens of thousands of args (e.g. a branch
318
+ // switch) would throw E2BIG and we'd lose ignore filtering for the batch.
319
+ // This still collapses N git spawns to ceil(N / CHUNK).
320
+ const CHUNK = 1000;
321
+ for (let i = 0; i < rels.length; i += CHUNK) {
322
+ const batch = rels.slice(i, i + CHUNK);
323
+ try {
324
+ // checkIgnore returns the subset of inputs that are ignored; it throws
325
+ // (git exits 1) when NONE are ignored — handled as "none" below.
326
+ const result = await this.git.checkIgnore(batch);
327
+ for (const rel of result) {
328
+ // git may quote paths with special chars (core.quotePath); strip them.
329
+ const abs = relToAbs.get(rel) ?? relToAbs.get(rel.replace(/^"(.*)"$/u, '$1'));
330
+ if (abs)
331
+ ignored.add(abs);
332
+ }
333
+ }
334
+ catch {
335
+ // None ignored in this batch (or a git error) — treat as not ignored.
336
+ }
337
+ }
338
+ return ignored;
339
+ }
340
+ }
@@ -0,0 +1,17 @@
1
+ interface GeneratedCheckResult {
2
+ isGenerated: boolean;
3
+ reason?: 'path' | 'universal' | 'language-specific';
4
+ matchedPattern?: string;
5
+ }
6
+ /**
7
+ * Checks if a file is generated.
8
+ * Uses a 3-layer approach:
9
+ * 1. Path/Filename patterns (fastest, no reading)
10
+ * 2. Universal header patterns (checks first 20 lines)
11
+ * 3. Language-specific header patterns (checks first 20 lines)
12
+ *
13
+ * @param parsedPath The decoded git path of the file
14
+ * @param getHeaderLines A function that lazily provides the first 20 lines of the file content
15
+ */
16
+ export declare function isGeneratedFile(parsedPath: string, getHeaderLines?: () => string[]): GeneratedCheckResult;
17
+ export {};
@@ -0,0 +1,280 @@
1
+ const EXACT_FILENAMES = new Set([
2
+ 'package-lock.json',
3
+ 'pnpm-lock.yaml',
4
+ 'yarn.lock',
5
+ 'Gemfile.lock',
6
+ 'Pipfile.lock',
7
+ 'composer.lock',
8
+ 'Cargo.lock',
9
+ 'poetry.lock',
10
+ 'go.sum',
11
+ 'go.mod',
12
+ 'pubspec.lock',
13
+ 'flake.lock',
14
+ 'Package.resolved',
15
+ 'packages.lock.json',
16
+ '.terraform.lock.hcl',
17
+ 'bun.lockb',
18
+ 'gradle.lockfile',
19
+ 'uv.lock',
20
+ 'pdm.lock',
21
+ ]);
22
+ const FILENAME_SUFFIXES = [
23
+ '.min.js',
24
+ '.min.css',
25
+ '.bundle.js',
26
+ '.bundle.css',
27
+ '-min.js',
28
+ '.map',
29
+ '.pb.go',
30
+ '.pb.rb',
31
+ '.pb.py',
32
+ '.pb.js',
33
+ '.pb.cc',
34
+ '.pb.h',
35
+ '.pb2.py',
36
+ '_pb2.py',
37
+ '.grpc.pb.go',
38
+ '_string.go',
39
+ '.graphql.ts',
40
+ '.graphql.js',
41
+ '.openapi.ts',
42
+ '.openapi.js',
43
+ '.msw.ts',
44
+ '.zod.ts',
45
+ '.api.ts',
46
+ '.g.dart',
47
+ '.freezed.dart',
48
+ '.g.cs',
49
+ '.designer.cs',
50
+ '_ide_helper.php',
51
+ ];
52
+ const ROOT_PATH_PREFIXES = ['vendor/', 'node_modules/', 'dist/', 'build/', 'out/'];
53
+ const PATH_SEGMENTS = ['/generated/', '/gen/', '/__generated__/'];
54
+ function matchesAnySuffix(fileName, suffixes) {
55
+ return suffixes.some((suffix) => fileName.endsWith(suffix));
56
+ }
57
+ function isWordCharacter(char) {
58
+ const code = char.charCodeAt(0);
59
+ return ((code >= 48 && code <= 57) ||
60
+ (code >= 65 && code <= 90) ||
61
+ (code >= 97 && code <= 122) ||
62
+ code === 95);
63
+ }
64
+ function isWordString(text) {
65
+ if (text.length === 0) {
66
+ return false;
67
+ }
68
+ for (const char of text) {
69
+ if (!isWordCharacter(char)) {
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ function hasGeneratedSuffix(fileName, marker) {
76
+ const markerIndex = fileName.lastIndexOf(marker);
77
+ if (markerIndex === -1) {
78
+ return false;
79
+ }
80
+ return isWordString(fileName.slice(markerIndex + marker.length));
81
+ }
82
+ // Layer 1: Path/Filename Patterns
83
+ const PATH_MATCHERS = [
84
+ {
85
+ description: 'exact-filename',
86
+ matches: (_parsedPath, fileName) => EXACT_FILENAMES.has(fileName),
87
+ },
88
+ {
89
+ description: 'minified-or-generated-suffix',
90
+ matches: (_parsedPath, fileName) => matchesAnySuffix(fileName, FILENAME_SUFFIXES),
91
+ },
92
+ {
93
+ description: 'generated-extension',
94
+ matches: (_parsedPath, fileName) => hasGeneratedSuffix(fileName, '.generated.') || hasGeneratedSuffix(fileName, '.gen.'),
95
+ },
96
+ {
97
+ description: 'mockgen-go',
98
+ matches: (_parsedPath, fileName) => fileName.startsWith('mock_') && fileName.endsWith('.go'),
99
+ },
100
+ {
101
+ description: 'mocks-go-directory',
102
+ matches: (parsedPath, fileName) => parsedPath.includes('mocks/') && fileName.endsWith('.go'),
103
+ },
104
+ {
105
+ description: 'root-generated-directory',
106
+ matches: (parsedPath) => ROOT_PATH_PREFIXES.some((prefix) => parsedPath.startsWith(prefix)),
107
+ },
108
+ {
109
+ description: 'generated-path-segment',
110
+ matches: (parsedPath) => PATH_SEGMENTS.some((segment) => parsedPath.includes(segment)),
111
+ },
112
+ ];
113
+ // Layer 2: Universal Header Patterns
114
+ const UNIVERSAL_PATTERNS = [
115
+ // Most common markers
116
+ /@generated\b/,
117
+ /\bDO NOT EDIT\b/i,
118
+ /\bDO NOT MODIFY\b/i,
119
+ /\bAUTO[- ]?GENERATED\b/i,
120
+ /\bTHIS FILE IS GENERATED\b/i,
121
+ /\bGENERATED CODE\b/i,
122
+ /\bMACHINE GENERATED\b/i,
123
+ // Common tool output phrases
124
+ /Generated by\b/i,
125
+ /Autogenerated by\b/i,
126
+ /Created by .* - DO NOT EDIT/i,
127
+ /This file was generated/i,
128
+ /This is a generated file/i,
129
+ // XML/HTML style
130
+ /<auto-generated\s*\/?>/i,
131
+ /<!-- Generated by/i,
132
+ ];
133
+ const LANGUAGE_SPECIFIC = [
134
+ {
135
+ extensions: ['.go'],
136
+ patterns: [
137
+ // Go official specification (https://golang.org/s/generatedcode)
138
+ /^\/\/ Code generated .* DO NOT EDIT\.$/m,
139
+ /Code generated by protoc-gen-go/,
140
+ /Code generated by Wire/,
141
+ /Code generated by ent/,
142
+ /Code generated by sqlc/,
143
+ /Code generated by MockGen/,
144
+ /Code generated by go-swagger/,
145
+ /Code generated by "stringer"/,
146
+ ],
147
+ },
148
+ {
149
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'],
150
+ patterns: [
151
+ /generated using openapi-typescript-codegen/i,
152
+ /This file was auto-generated by openapi/i,
153
+ /Generated by orval/i,
154
+ /THIS FILE WAS GENERATED/,
155
+ /Generated by graphql-codegen/i,
156
+ /Prisma Client JS/,
157
+ /Generated by the protocol buffer/,
158
+ /GENERATED CODE -- DO NOT EDIT/,
159
+ /@generated SignedSource/,
160
+ /Generated by Apollo/,
161
+ ],
162
+ },
163
+ {
164
+ extensions: ['.rb'],
165
+ patterns: [
166
+ /This file is auto-generated from the current state of the database/,
167
+ /This file is autogenerated/,
168
+ /Generated by the protocol buffer/,
169
+ /Autogenerated by Thrift/,
170
+ /DO NOT MODIFY.*schema/i,
171
+ ],
172
+ },
173
+ {
174
+ extensions: ['.php'],
175
+ patterns: [
176
+ /This class was generated by/i,
177
+ /Doctrine .* Generated/i,
178
+ /Generated by the protocol buffer/,
179
+ /auto-generated by Symfony/i,
180
+ ],
181
+ },
182
+ {
183
+ extensions: ['.py'],
184
+ patterns: [
185
+ /Generated by the protocol buffer compiler/,
186
+ /@generated by mypy-protobuf/,
187
+ /Generated by the gRPC/,
188
+ /Autogenerated by Thrift/,
189
+ /# Generated by Django/,
190
+ /autogenerated by sqlacodegen/i,
191
+ /generated by datamodel-codegen/,
192
+ ],
193
+ },
194
+ {
195
+ extensions: ['.java', '.kt'],
196
+ patterns: [
197
+ /Generated by the protocol buffer/,
198
+ /@javax\.annotation\.Generated/,
199
+ /@Generated\(/,
200
+ /Generated by the gRPC/,
201
+ /Generated by Dagger/,
202
+ /generated at.*by JAXB/i,
203
+ /OpenAPI Generator/,
204
+ ],
205
+ },
206
+ {
207
+ extensions: ['.cs'],
208
+ patterns: [
209
+ /<auto-generated>/,
210
+ /<autogenerated>/,
211
+ /Generated by the protocol buffer/,
212
+ /auto-generated by EF/i,
213
+ /This code was generated by a tool/,
214
+ ],
215
+ },
216
+ ];
217
+ /**
218
+ * Checks if a file is generated.
219
+ * Uses a 3-layer approach:
220
+ * 1. Path/Filename patterns (fastest, no reading)
221
+ * 2. Universal header patterns (checks first 20 lines)
222
+ * 3. Language-specific header patterns (checks first 20 lines)
223
+ *
224
+ * @param parsedPath The decoded git path of the file
225
+ * @param getHeaderLines A function that lazily provides the first 20 lines of the file content
226
+ */
227
+ export function isGeneratedFile(parsedPath, getHeaderLines) {
228
+ // Layer 1: Path-based check (no file read)
229
+ // We check if the filename matches any known generated patterns
230
+ // or if the path is in a known generated directory
231
+ const fileName = parsedPath.split('/').pop() || '';
232
+ for (const matcher of PATH_MATCHERS) {
233
+ if (matcher.matches(parsedPath, fileName)) {
234
+ return {
235
+ isGenerated: true,
236
+ reason: 'path',
237
+ matchedPattern: matcher.description,
238
+ };
239
+ }
240
+ }
241
+ // Optimization: Skip content checks for markdown files as they are documentation
242
+ // and often contain examples of generated headers which trigger false positives.
243
+ if (/\.(md|markdown)$/i.test(fileName)) {
244
+ return { isGenerated: false };
245
+ }
246
+ // If no content provider is available, we can't check further
247
+ if (!getHeaderLines) {
248
+ return { isGenerated: false };
249
+ }
250
+ // Layer 2 & 3: Header-based check
251
+ const lines = getHeaderLines();
252
+ const header = lines.join('\n');
253
+ // Universal patterns
254
+ for (const pattern of UNIVERSAL_PATTERNS) {
255
+ if (pattern.test(header)) {
256
+ return {
257
+ isGenerated: true,
258
+ reason: 'universal',
259
+ matchedPattern: pattern.source,
260
+ };
261
+ }
262
+ }
263
+ // Language-specific patterns
264
+ // Simple extension extraction
265
+ const extMatch = parsedPath.match(/\.[a-zA-Z0-9]+$/);
266
+ const ext = extMatch ? extMatch[0] : '';
267
+ const langPatterns = LANGUAGE_SPECIFIC.find((lang) => lang.extensions.includes(ext));
268
+ if (langPatterns) {
269
+ for (const pattern of langPatterns.patterns) {
270
+ if (pattern.test(header)) {
271
+ return {
272
+ isGenerated: true,
273
+ reason: 'language-specific',
274
+ matchedPattern: pattern.source,
275
+ };
276
+ }
277
+ }
278
+ }
279
+ return { isGenerated: false };
280
+ }