@eddacraft/anvil-runtime 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 (170) hide show
  1. package/LICENSE +14 -0
  2. package/dist/cache/cache-key.d.ts +45 -0
  3. package/dist/cache/cache-key.d.ts.map +1 -0
  4. package/dist/cache/cache-key.js +135 -0
  5. package/dist/cache/index.d.ts +27 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +38 -0
  8. package/dist/cache/providers/file-cache.d.ts +63 -0
  9. package/dist/cache/providers/file-cache.d.ts.map +1 -0
  10. package/dist/cache/providers/file-cache.js +369 -0
  11. package/dist/cache/providers/memory-cache.d.ts +52 -0
  12. package/dist/cache/providers/memory-cache.d.ts.map +1 -0
  13. package/dist/cache/providers/memory-cache.js +197 -0
  14. package/dist/cache/providers/null-cache.d.ts +26 -0
  15. package/dist/cache/providers/null-cache.d.ts.map +1 -0
  16. package/dist/cache/providers/null-cache.js +50 -0
  17. package/dist/cache/types.d.ts +114 -0
  18. package/dist/cache/types.d.ts.map +1 -0
  19. package/dist/cache/types.js +4 -0
  20. package/dist/concurrency/agent.d.ts +137 -0
  21. package/dist/concurrency/agent.d.ts.map +1 -0
  22. package/dist/concurrency/agent.js +440 -0
  23. package/dist/concurrency/atomic.d.ts +93 -0
  24. package/dist/concurrency/atomic.d.ts.map +1 -0
  25. package/dist/concurrency/atomic.js +281 -0
  26. package/dist/concurrency/git-agent.d.ts +114 -0
  27. package/dist/concurrency/git-agent.d.ts.map +1 -0
  28. package/dist/concurrency/git-agent.js +313 -0
  29. package/dist/concurrency/index.d.ts +95 -0
  30. package/dist/concurrency/index.d.ts.map +1 -0
  31. package/dist/concurrency/index.js +127 -0
  32. package/dist/concurrency/lock-manager.d.ts +170 -0
  33. package/dist/concurrency/lock-manager.d.ts.map +1 -0
  34. package/dist/concurrency/lock-manager.js +525 -0
  35. package/dist/concurrency/queue-manager.d.ts +166 -0
  36. package/dist/concurrency/queue-manager.d.ts.map +1 -0
  37. package/dist/concurrency/queue-manager.js +442 -0
  38. package/dist/concurrency/types.d.ts +382 -0
  39. package/dist/concurrency/types.d.ts.map +1 -0
  40. package/dist/concurrency/types.js +204 -0
  41. package/dist/export/constraint-collector.d.ts +175 -0
  42. package/dist/export/constraint-collector.d.ts.map +1 -0
  43. package/dist/export/constraint-collector.js +203 -0
  44. package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
  45. package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
  46. package/dist/export/formatters/llms-txt-formatter.js +249 -0
  47. package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
  48. package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
  49. package/dist/export/formatters/mcp-resource-formatter.js +139 -0
  50. package/dist/export/formatters/prompt-formatter.d.ts +83 -0
  51. package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
  52. package/dist/export/formatters/prompt-formatter.js +256 -0
  53. package/dist/export/index.d.ts +10 -0
  54. package/dist/export/index.d.ts.map +1 -0
  55. package/dist/export/index.js +9 -0
  56. package/dist/gate/check.interface.d.ts +15 -0
  57. package/dist/gate/check.interface.d.ts.map +1 -0
  58. package/dist/gate/check.interface.js +18 -0
  59. package/dist/gate/checks/antipattern.check.d.ts +27 -0
  60. package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
  61. package/dist/gate/checks/antipattern.check.js +140 -0
  62. package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
  63. package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
  64. package/dist/gate/checks/architecture/circular-detector.js +71 -0
  65. package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
  66. package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
  67. package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
  68. package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
  69. package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
  70. package/dist/gate/checks/architecture/layer-validator.js +193 -0
  71. package/dist/gate/checks/architecture.check.d.ts +56 -0
  72. package/dist/gate/checks/architecture.check.d.ts.map +1 -0
  73. package/dist/gate/checks/architecture.check.js +394 -0
  74. package/dist/gate/checks/command-safety.check.d.ts +12 -0
  75. package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
  76. package/dist/gate/checks/command-safety.check.js +230 -0
  77. package/dist/gate/checks/coverage.check.d.ts +9 -0
  78. package/dist/gate/checks/coverage.check.d.ts.map +1 -0
  79. package/dist/gate/checks/coverage.check.js +81 -0
  80. package/dist/gate/checks/dependency.check.d.ts +17 -0
  81. package/dist/gate/checks/dependency.check.d.ts.map +1 -0
  82. package/dist/gate/checks/dependency.check.js +342 -0
  83. package/dist/gate/checks/eslint.check.d.ts +14 -0
  84. package/dist/gate/checks/eslint.check.d.ts.map +1 -0
  85. package/dist/gate/checks/eslint.check.js +79 -0
  86. package/dist/gate/checks/policy.check.d.ts +78 -0
  87. package/dist/gate/checks/policy.check.d.ts.map +1 -0
  88. package/dist/gate/checks/policy.check.js +457 -0
  89. package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
  90. package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
  91. package/dist/gate/checks/secret/entropy-detector.js +76 -0
  92. package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
  93. package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
  94. package/dist/gate/checks/secret/git-scanner.js +90 -0
  95. package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
  96. package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
  97. package/dist/gate/checks/secret/secret-patterns.js +137 -0
  98. package/dist/gate/checks/secret.check.d.ts +56 -0
  99. package/dist/gate/checks/secret.check.d.ts.map +1 -0
  100. package/dist/gate/checks/secret.check.js +245 -0
  101. package/dist/gate/config/command-safety-config.d.ts +5 -0
  102. package/dist/gate/config/command-safety-config.d.ts.map +1 -0
  103. package/dist/gate/config/command-safety-config.js +69 -0
  104. package/dist/gate/config/index.d.ts +2 -0
  105. package/dist/gate/config/index.d.ts.map +1 -0
  106. package/dist/gate/config/index.js +1 -0
  107. package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
  108. package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
  109. package/dist/gate/formatters/command-safety-formatter.js +64 -0
  110. package/dist/gate/formatters/index.d.ts +2 -0
  111. package/dist/gate/formatters/index.d.ts.map +1 -0
  112. package/dist/gate/formatters/index.js +1 -0
  113. package/dist/gate/gate-config.d.ts +44 -0
  114. package/dist/gate/gate-config.d.ts.map +1 -0
  115. package/dist/gate/gate-config.js +334 -0
  116. package/dist/gate/gate-runner.d.ts +160 -0
  117. package/dist/gate/gate-runner.d.ts.map +1 -0
  118. package/dist/gate/gate-runner.js +531 -0
  119. package/dist/gate/index.d.ts +20 -0
  120. package/dist/gate/index.d.ts.map +1 -0
  121. package/dist/gate/index.js +14 -0
  122. package/dist/gate/parsers/command-parser.d.ts +18 -0
  123. package/dist/gate/parsers/command-parser.d.ts.map +1 -0
  124. package/dist/gate/parsers/command-parser.js +363 -0
  125. package/dist/gate/parsers/index.d.ts +2 -0
  126. package/dist/gate/parsers/index.d.ts.map +1 -0
  127. package/dist/gate/parsers/index.js +1 -0
  128. package/dist/gate/policy/index.d.ts +12 -0
  129. package/dist/gate/policy/index.d.ts.map +1 -0
  130. package/dist/gate/policy/index.js +10 -0
  131. package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
  132. package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
  133. package/dist/gate/rules/default-filesystem-rules.js +201 -0
  134. package/dist/gate/rules/default-git-rules.d.ts +3 -0
  135. package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
  136. package/dist/gate/rules/default-git-rules.js +192 -0
  137. package/dist/gate/rules/index.d.ts +5 -0
  138. package/dist/gate/rules/index.d.ts.map +1 -0
  139. package/dist/gate/rules/index.js +3 -0
  140. package/dist/gate/rules/rule-matcher.d.ts +27 -0
  141. package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
  142. package/dist/gate/rules/rule-matcher.js +228 -0
  143. package/dist/gate/rules/types.d.ts +250 -0
  144. package/dist/gate/rules/types.d.ts.map +1 -0
  145. package/dist/gate/rules/types.js +1 -0
  146. package/dist/index.d.ts +19 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +35 -0
  149. package/dist/types/gate.types.d.ts +42 -0
  150. package/dist/types/gate.types.d.ts.map +1 -0
  151. package/dist/types/gate.types.js +94 -0
  152. package/dist/watch/debouncer.d.ts +90 -0
  153. package/dist/watch/debouncer.d.ts.map +1 -0
  154. package/dist/watch/debouncer.js +135 -0
  155. package/dist/watch/file-watcher.d.ts +73 -0
  156. package/dist/watch/file-watcher.d.ts.map +1 -0
  157. package/dist/watch/file-watcher.js +121 -0
  158. package/dist/watch/git-status.d.ts +98 -0
  159. package/dist/watch/git-status.d.ts.map +1 -0
  160. package/dist/watch/git-status.js +266 -0
  161. package/dist/watch/index.d.ts +16 -0
  162. package/dist/watch/index.d.ts.map +1 -0
  163. package/dist/watch/index.js +15 -0
  164. package/dist/watch/orchestrator.d.ts +113 -0
  165. package/dist/watch/orchestrator.d.ts.map +1 -0
  166. package/dist/watch/orchestrator.js +409 -0
  167. package/dist/watch/types.d.ts +190 -0
  168. package/dist/watch/types.d.ts.map +1 -0
  169. package/dist/watch/types.js +76 -0
  170. package/package.json +60 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Change Debouncer
3
+ *
4
+ * Coalesces rapid file changes into batches to prevent
5
+ * excessive action triggers during editor auto-save or
6
+ * multi-file operations.
7
+ */
8
+ import { createDebugger } from '@eddacraft/anvil-core';
9
+ const debug = createDebugger('watch');
10
+ /**
11
+ * Debouncer for file changes
12
+ *
13
+ * Accumulates file paths and flushes them as a batch
14
+ * after a configurable delay.
15
+ */
16
+ export class ChangeDebouncer {
17
+ delayMs;
18
+ onFlush;
19
+ pendingFiles = new Set();
20
+ timer = null;
21
+ /**
22
+ * Create a new debouncer
23
+ *
24
+ * @param delayMs - Debounce delay in milliseconds
25
+ * @param onFlush - Callback when changes are flushed
26
+ */
27
+ constructor(delayMs, onFlush) {
28
+ this.delayMs = delayMs;
29
+ this.onFlush = onFlush;
30
+ }
31
+ /**
32
+ * Add a file to the pending changes
33
+ *
34
+ * Resets the debounce timer each time a file is added.
35
+ *
36
+ * @param filePath - Absolute file path
37
+ */
38
+ add(filePath) {
39
+ debug(`debouncer add: path=${filePath} pending=${this.pendingFiles.size + 1}`);
40
+ this.pendingFiles.add(filePath);
41
+ this.resetTimer();
42
+ }
43
+ /**
44
+ * Add multiple files to pending changes
45
+ *
46
+ * @param filePaths - Array of absolute file paths
47
+ */
48
+ addMany(filePaths) {
49
+ for (const filePath of filePaths) {
50
+ this.pendingFiles.add(filePath);
51
+ }
52
+ this.resetTimer();
53
+ }
54
+ /**
55
+ * Immediately flush pending changes
56
+ *
57
+ * Clears the timer and invokes the callback with all
58
+ * accumulated files.
59
+ */
60
+ flush() {
61
+ this.clearTimer();
62
+ if (this.pendingFiles.size === 0) {
63
+ debug('debouncer flush: nothing pending');
64
+ return;
65
+ }
66
+ const files = Array.from(this.pendingFiles);
67
+ debug(`debouncer flush: dispatching batch of ${files.length} files`);
68
+ this.pendingFiles.clear();
69
+ this.onFlush({
70
+ files,
71
+ timestamp: new Date(),
72
+ });
73
+ }
74
+ /**
75
+ * Cancel pending flush and clear accumulated files
76
+ */
77
+ cancel() {
78
+ debug(`debouncer cancel: discarding ${this.pendingFiles.size} pending files`);
79
+ this.clearTimer();
80
+ this.pendingFiles.clear();
81
+ }
82
+ /**
83
+ * Get count of pending files
84
+ */
85
+ get pendingCount() {
86
+ return this.pendingFiles.size;
87
+ }
88
+ /**
89
+ * Check if there are pending changes
90
+ */
91
+ get hasPending() {
92
+ return this.pendingFiles.size > 0;
93
+ }
94
+ /**
95
+ * Get the current delay setting
96
+ */
97
+ get delay() {
98
+ return this.delayMs;
99
+ }
100
+ /**
101
+ * Update the debounce delay
102
+ *
103
+ * Takes effect on next add() call.
104
+ */
105
+ setDelay(delayMs) {
106
+ this.delayMs = delayMs;
107
+ }
108
+ /**
109
+ * Reset the debounce timer
110
+ */
111
+ resetTimer() {
112
+ this.clearTimer();
113
+ this.timer = setTimeout(() => {
114
+ this.flush();
115
+ }, this.delayMs);
116
+ }
117
+ /**
118
+ * Clear the debounce timer
119
+ */
120
+ clearTimer() {
121
+ if (this.timer) {
122
+ clearTimeout(this.timer);
123
+ this.timer = null;
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Create a change debouncer
129
+ *
130
+ * @param delayMs - Debounce delay in milliseconds (default: 300)
131
+ * @param onFlush - Callback when changes are flushed
132
+ */
133
+ export function createDebouncer(delayMs, onFlush) {
134
+ return new ChangeDebouncer(delayMs, onFlush);
135
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * File Watcher
3
+ *
4
+ * Chokidar wrapper with pattern matching and event normalisation.
5
+ * Provides a clean interface for watching file changes.
6
+ */
7
+ import { EventEmitter } from 'node:events';
8
+ import type { WatchChangeEvent } from './types.js';
9
+ /**
10
+ * File watcher options
11
+ */
12
+ export interface FileWatcherOptions {
13
+ /** Glob patterns to watch */
14
+ patterns: string[];
15
+ /** Glob patterns to exclude */
16
+ exclude: string[];
17
+ /** Working directory for relative patterns */
18
+ cwd: string;
19
+ /** Max directory depth to watch */
20
+ depth?: number;
21
+ }
22
+ /**
23
+ * File watcher events interface (for documentation)
24
+ */
25
+ export interface FileWatcherEvents {
26
+ change: [event: WatchChangeEvent];
27
+ error: [error: Error];
28
+ ready: [];
29
+ }
30
+ /**
31
+ * File watcher wrapping chokidar
32
+ *
33
+ * Emits normalised change events for file add/change/unlink.
34
+ * Uses method overloads for type-safe event handling.
35
+ */
36
+ export declare class FileWatcher extends EventEmitter {
37
+ private watcher;
38
+ private isReady;
39
+ private chokidar;
40
+ /**
41
+ * Start watching files
42
+ *
43
+ * @param options - Watcher options
44
+ */
45
+ start(options: FileWatcherOptions): Promise<void>;
46
+ /**
47
+ * Stop watching files
48
+ */
49
+ stop(): Promise<void>;
50
+ /**
51
+ * Check if watcher is ready
52
+ */
53
+ get ready(): boolean;
54
+ /**
55
+ * Check if watcher is running
56
+ */
57
+ get running(): boolean;
58
+ /**
59
+ * Emit normalised change event
60
+ */
61
+ private emitChange;
62
+ on(event: 'change', listener: (event: WatchChangeEvent) => void): this;
63
+ on(event: 'error', listener: (error: Error) => void): this;
64
+ on(event: 'ready', listener: () => void): this;
65
+ emit(event: 'change', watchEvent: WatchChangeEvent): boolean;
66
+ emit(event: 'error', error: Error): boolean;
67
+ emit(event: 'ready'): boolean;
68
+ }
69
+ /**
70
+ * Create a file watcher
71
+ */
72
+ export declare function createFileWatcher(): FileWatcher;
73
+ //# sourceMappingURL=file-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-watcher.d.ts","sourceRoot":"","sources":["../../src/watch/file-watcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA2BnD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAClC,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACtB,KAAK,EAAE,EAAE,CAAC;CACX;AAED;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAA+B;IAE/C;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDvD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B;;OAEG;IACH,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;OAEG;IACH,OAAO,CAAC,UAAU;IAkBT,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IACtE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAC1D,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAM9C,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO;IAC5D,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO;IAC3C,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;CAKvC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * File Watcher
3
+ *
4
+ * Chokidar wrapper with pattern matching and event normalisation.
5
+ * Provides a clean interface for watching file changes.
6
+ */
7
+ import { EventEmitter } from 'node:events';
8
+ import { createDebugger } from '@eddacraft/anvil-core';
9
+ const debug = createDebugger('watch');
10
+ /**
11
+ * File watcher wrapping chokidar
12
+ *
13
+ * Emits normalised change events for file add/change/unlink.
14
+ * Uses method overloads for type-safe event handling.
15
+ */
16
+ export class FileWatcher extends EventEmitter {
17
+ watcher = null;
18
+ isReady = false;
19
+ chokidar = null;
20
+ /**
21
+ * Start watching files
22
+ *
23
+ * @param options - Watcher options
24
+ */
25
+ async start(options) {
26
+ debug('file-watcher start', {
27
+ patterns: options.patterns,
28
+ exclude: options.exclude,
29
+ cwd: options.cwd,
30
+ });
31
+ if (this.watcher) {
32
+ throw new Error('Watcher already started. Call stop() first.');
33
+ }
34
+ // Dynamically import chokidar
35
+ try {
36
+ this.chokidar = (await import('chokidar'));
37
+ }
38
+ catch {
39
+ throw new Error('chokidar is not installed. Run: pnpm add chokidar in the cli package.');
40
+ }
41
+ const { patterns, exclude, cwd, depth } = options;
42
+ this.watcher = this.chokidar.watch(patterns, {
43
+ ignored: exclude,
44
+ persistent: true,
45
+ ignoreInitial: true,
46
+ cwd,
47
+ depth: depth ?? 10,
48
+ awaitWriteFinish: {
49
+ stabilityThreshold: 100,
50
+ pollInterval: 50,
51
+ },
52
+ });
53
+ this.watcher.on('add', (path) => {
54
+ this.emitChange('add', path, cwd);
55
+ });
56
+ this.watcher.on('change', (path) => {
57
+ this.emitChange('change', path, cwd);
58
+ });
59
+ this.watcher.on('unlink', (path) => {
60
+ this.emitChange('unlink', path, cwd);
61
+ });
62
+ this.watcher.on('error', (error) => {
63
+ this.emit('error', error);
64
+ });
65
+ this.watcher.on('ready', () => {
66
+ this.isReady = true;
67
+ this.emit('ready');
68
+ });
69
+ }
70
+ /**
71
+ * Stop watching files
72
+ */
73
+ async stop() {
74
+ debug(`file-watcher stop: running=${this.watcher !== null}`);
75
+ if (this.watcher) {
76
+ await this.watcher.close();
77
+ this.watcher = null;
78
+ this.isReady = false;
79
+ }
80
+ }
81
+ /**
82
+ * Check if watcher is ready
83
+ */
84
+ get ready() {
85
+ return this.isReady;
86
+ }
87
+ /**
88
+ * Check if watcher is running
89
+ */
90
+ get running() {
91
+ return this.watcher !== null;
92
+ }
93
+ /**
94
+ * Emit normalised change event
95
+ */
96
+ emitChange(type, path, cwd) {
97
+ debug(`file-watcher event: type=${type} path=${path}`);
98
+ // Convert relative path to absolute if needed
99
+ const absolutePath = path.startsWith('/') ? path : `${cwd}/${path}`;
100
+ const event = {
101
+ type,
102
+ path: absolutePath,
103
+ timestamp: new Date(),
104
+ };
105
+ this.emit('change', event);
106
+ }
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EventEmitter base requires any[]; independantly verified by codex 20260205
108
+ on(event, listener) {
109
+ return super.on(event, listener);
110
+ }
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EventEmitter base requires any[]; independantly verified by codex 20260205
112
+ emit(event, ...args) {
113
+ return super.emit(event, ...args);
114
+ }
115
+ }
116
+ /**
117
+ * Create a file watcher
118
+ */
119
+ export function createFileWatcher() {
120
+ return new FileWatcher();
121
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Git Status Checker
3
+ *
4
+ * Utilities for checking git status of files to filter watch events
5
+ * to only unstaged changes.
6
+ */
7
+ import type { GitFileStatus } from './types.js';
8
+ /**
9
+ * Git status checker for filtering watched files
10
+ */
11
+ export declare class GitStatusChecker {
12
+ private workspaceRoot;
13
+ constructor(workspaceRoot: string);
14
+ /**
15
+ * Check if directory is a git repository
16
+ */
17
+ isGitRepository(): Promise<boolean>;
18
+ /**
19
+ * Get git status for a specific file
20
+ *
21
+ * @param filePath - Absolute or relative file path
22
+ * @returns Git file status
23
+ */
24
+ getFileStatus(filePath: string): Promise<GitFileStatus>;
25
+ /**
26
+ * Check if file has unstaged changes
27
+ */
28
+ isUnstaged(filePath: string): Promise<boolean>;
29
+ /**
30
+ * Check if file is untracked
31
+ */
32
+ isUntracked(filePath: string): Promise<boolean>;
33
+ /**
34
+ * Get all files with unstaged changes
35
+ */
36
+ getUnstagedFiles(): Promise<string[]>;
37
+ /**
38
+ * Get all untracked files
39
+ */
40
+ getUntrackedFiles(): Promise<string[]>;
41
+ /**
42
+ * Filter file paths to only those with unstaged changes
43
+ *
44
+ * @param filePaths - Array of file paths to filter
45
+ * @param includeUntracked - Whether to include untracked files
46
+ * @returns Filtered array of file paths
47
+ */
48
+ filterUnstaged(filePaths: string[], includeUntracked?: boolean): Promise<string[]>;
49
+ /**
50
+ * Parse git status --porcelain output line
51
+ *
52
+ * Format: XY filename
53
+ * X = status in staging area
54
+ * Y = status in working tree
55
+ *
56
+ * Common codes:
57
+ * ' M' = modified in working tree (unstaged)
58
+ * 'M ' = modified in index (staged)
59
+ * 'MM' = modified in both (staged + unstaged changes)
60
+ * '??' = untracked
61
+ * 'A ' = added in index (staged new file)
62
+ * ' D' = deleted in working tree
63
+ * 'D ' = deleted in index
64
+ */
65
+ private parseStatusLine;
66
+ /**
67
+ * Convert absolute path to relative path from workspace root
68
+ */
69
+ private toRelativePath;
70
+ }
71
+ /**
72
+ * Create a git status checker for the workspace
73
+ */
74
+ export declare function createGitStatusChecker(workspaceRoot: string): GitStatusChecker;
75
+ /**
76
+ * Options for getChangedFiles
77
+ */
78
+ export interface GetChangedFilesOptions {
79
+ /** Include staged files (default: true) */
80
+ staged?: boolean;
81
+ /** Include unstaged files (default: true) */
82
+ unstaged?: boolean;
83
+ /** Include untracked files (default: false) */
84
+ untracked?: boolean;
85
+ /** Compare against git ref (e.g., 'main', 'HEAD~3') */
86
+ since?: string;
87
+ /** Filter to specific extensions (e.g., ['.ts', '.tsx']) */
88
+ extensions?: string[];
89
+ }
90
+ /**
91
+ * Get changed files from git with flexible filtering
92
+ *
93
+ * @param workspaceRoot - Root directory of the workspace
94
+ * @param options - Options for filtering changed files
95
+ * @returns Array of absolute file paths
96
+ */
97
+ export declare function getChangedFiles(workspaceRoot: string, options?: GetChangedFilesOptions): Promise<string[]>;
98
+ //# sourceMappingURL=git-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-status.d.ts","sourceRoot":"","sources":["../../src/watch/git-status.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAOhD;;GAEG;AACH,qBAAa,gBAAgB;IACf,OAAO,CAAC,aAAa;gBAAb,aAAa,EAAE,MAAM;IAEzC;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAYzC;;;;;OAKG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsB7D;;OAEG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpD;;OAEG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrD;;OAEG;IACG,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAuB3C;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAuB5C;;;;;;OAMG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,gBAAgB,UAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAgBtF;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,eAAe;IA8CvB;;OAEG;IACH,OAAO,CAAC,cAAc;CAMvB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,gBAAgB,CAE9E;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,2CAA2C;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,EAAE,CAAC,CAuDnB"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Git Status Checker
3
+ *
4
+ * Utilities for checking git status of files to filter watch events
5
+ * to only unstaged changes.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { relative, resolve } from 'node:path';
10
+ import { createDebugger } from '@eddacraft/anvil-core';
11
+ const debug = createDebugger('gate');
12
+ const execFileAsync = promisify(execFile);
13
+ /**
14
+ * Git status checker for filtering watched files
15
+ */
16
+ export class GitStatusChecker {
17
+ workspaceRoot;
18
+ constructor(workspaceRoot) {
19
+ this.workspaceRoot = workspaceRoot;
20
+ }
21
+ /**
22
+ * Check if directory is a git repository
23
+ */
24
+ async isGitRepository() {
25
+ try {
26
+ await execFileAsync('git', ['rev-parse', '--git-dir'], {
27
+ cwd: this.workspaceRoot,
28
+ });
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ debug('Failed to check if directory is a git repository', error);
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Get git status for a specific file
38
+ *
39
+ * @param filePath - Absolute or relative file path
40
+ * @returns Git file status
41
+ */
42
+ async getFileStatus(filePath) {
43
+ const relativePath = this.toRelativePath(filePath);
44
+ try {
45
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain', '--', relativePath], {
46
+ cwd: this.workspaceRoot,
47
+ });
48
+ return this.parseStatusLine(stdout.trim(), relativePath);
49
+ }
50
+ catch (error) {
51
+ debug('Git status command failed, treating file as untracked', error);
52
+ return {
53
+ path: relativePath,
54
+ isTracked: false,
55
+ isStaged: false,
56
+ isUnstaged: false,
57
+ isUntracked: true,
58
+ statusCode: '??',
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * Check if file has unstaged changes
64
+ */
65
+ async isUnstaged(filePath) {
66
+ const status = await this.getFileStatus(filePath);
67
+ return status.isUnstaged;
68
+ }
69
+ /**
70
+ * Check if file is untracked
71
+ */
72
+ async isUntracked(filePath) {
73
+ const status = await this.getFileStatus(filePath);
74
+ return status.isUntracked;
75
+ }
76
+ /**
77
+ * Get all files with unstaged changes
78
+ */
79
+ async getUnstagedFiles() {
80
+ try {
81
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
82
+ cwd: this.workspaceRoot,
83
+ });
84
+ const files = [];
85
+ const lines = stdout.trim().split('\n').filter(Boolean);
86
+ for (const line of lines) {
87
+ const status = this.parseStatusLine(line, '');
88
+ if (status.isUnstaged && status.path) {
89
+ files.push(resolve(this.workspaceRoot, status.path));
90
+ }
91
+ }
92
+ return files;
93
+ }
94
+ catch (error) {
95
+ debug('Failed to get unstaged files from git status', error);
96
+ return [];
97
+ }
98
+ }
99
+ /**
100
+ * Get all untracked files
101
+ */
102
+ async getUntrackedFiles() {
103
+ try {
104
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
105
+ cwd: this.workspaceRoot,
106
+ });
107
+ const files = [];
108
+ const lines = stdout.trim().split('\n').filter(Boolean);
109
+ for (const line of lines) {
110
+ const status = this.parseStatusLine(line, '');
111
+ if (status.isUntracked && status.path) {
112
+ files.push(resolve(this.workspaceRoot, status.path));
113
+ }
114
+ }
115
+ return files;
116
+ }
117
+ catch (error) {
118
+ debug('Failed to get untracked files from git status', error);
119
+ return [];
120
+ }
121
+ }
122
+ /**
123
+ * Filter file paths to only those with unstaged changes
124
+ *
125
+ * @param filePaths - Array of file paths to filter
126
+ * @param includeUntracked - Whether to include untracked files
127
+ * @returns Filtered array of file paths
128
+ */
129
+ async filterUnstaged(filePaths, includeUntracked = false) {
130
+ const results = [];
131
+ for (const filePath of filePaths) {
132
+ const status = await this.getFileStatus(filePath);
133
+ if (status.isUnstaged) {
134
+ results.push(filePath);
135
+ }
136
+ else if (includeUntracked && status.isUntracked) {
137
+ results.push(filePath);
138
+ }
139
+ }
140
+ return results;
141
+ }
142
+ /**
143
+ * Parse git status --porcelain output line
144
+ *
145
+ * Format: XY filename
146
+ * X = status in staging area
147
+ * Y = status in working tree
148
+ *
149
+ * Common codes:
150
+ * ' M' = modified in working tree (unstaged)
151
+ * 'M ' = modified in index (staged)
152
+ * 'MM' = modified in both (staged + unstaged changes)
153
+ * '??' = untracked
154
+ * 'A ' = added in index (staged new file)
155
+ * ' D' = deleted in working tree
156
+ * 'D ' = deleted in index
157
+ */
158
+ parseStatusLine(line, defaultPath) {
159
+ if (!line || line.length < 3) {
160
+ return {
161
+ path: defaultPath,
162
+ isTracked: true,
163
+ isStaged: false,
164
+ isUnstaged: false,
165
+ isUntracked: false,
166
+ statusCode: '',
167
+ };
168
+ }
169
+ const statusCode = line.substring(0, 2);
170
+ const path = line.substring(3).trim() || defaultPath;
171
+ const indexStatus = statusCode[0];
172
+ const workTreeStatus = statusCode[1];
173
+ // Check if untracked
174
+ if (statusCode === '??') {
175
+ return {
176
+ path,
177
+ isTracked: false,
178
+ isStaged: false,
179
+ isUnstaged: false,
180
+ isUntracked: true,
181
+ statusCode,
182
+ };
183
+ }
184
+ // Check staging area (index) status
185
+ const isStaged = indexStatus !== ' ' && indexStatus !== '?';
186
+ // Check working tree status
187
+ const isUnstaged = workTreeStatus !== ' ' && workTreeStatus !== '?';
188
+ return {
189
+ path,
190
+ isTracked: true,
191
+ isStaged,
192
+ isUnstaged,
193
+ isUntracked: false,
194
+ statusCode,
195
+ };
196
+ }
197
+ /**
198
+ * Convert absolute path to relative path from workspace root
199
+ */
200
+ toRelativePath(filePath) {
201
+ if (filePath.startsWith(this.workspaceRoot)) {
202
+ return relative(this.workspaceRoot, filePath);
203
+ }
204
+ return filePath;
205
+ }
206
+ }
207
+ /**
208
+ * Create a git status checker for the workspace
209
+ */
210
+ export function createGitStatusChecker(workspaceRoot) {
211
+ return new GitStatusChecker(workspaceRoot);
212
+ }
213
+ /**
214
+ * Get changed files from git with flexible filtering
215
+ *
216
+ * @param workspaceRoot - Root directory of the workspace
217
+ * @param options - Options for filtering changed files
218
+ * @returns Array of absolute file paths
219
+ */
220
+ export async function getChangedFiles(workspaceRoot, options = {}) {
221
+ const { staged = true, unstaged = true, untracked = false, since, extensions } = options;
222
+ const files = new Set();
223
+ try {
224
+ if (since) {
225
+ const { stdout } = await execFileAsync('git', ['diff', '--name-only', since], {
226
+ cwd: workspaceRoot,
227
+ });
228
+ const diffFiles = stdout.trim().split('\n').filter(Boolean);
229
+ for (const file of diffFiles) {
230
+ files.add(resolve(workspaceRoot, file));
231
+ }
232
+ }
233
+ else {
234
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
235
+ cwd: workspaceRoot,
236
+ });
237
+ const lines = stdout.trim().split('\n').filter(Boolean);
238
+ for (const line of lines) {
239
+ if (line.length < 3)
240
+ continue;
241
+ const statusCode = line.substring(0, 2);
242
+ const filePath = line.substring(3).trim();
243
+ const indexStatus = statusCode[0];
244
+ const workTreeStatus = statusCode[1];
245
+ const isFileStaged = indexStatus !== ' ' && indexStatus !== '?';
246
+ const isFileUnstaged = workTreeStatus !== ' ' && workTreeStatus !== '?';
247
+ const isFileUntracked = statusCode === '??';
248
+ const shouldInclude = (staged && isFileStaged) ||
249
+ (unstaged && isFileUnstaged) ||
250
+ (untracked && isFileUntracked);
251
+ if (shouldInclude) {
252
+ files.add(resolve(workspaceRoot, filePath));
253
+ }
254
+ }
255
+ }
256
+ }
257
+ catch (error) {
258
+ debug('Failed to get changed files from git', error);
259
+ return [];
260
+ }
261
+ let result = Array.from(files);
262
+ if (extensions && extensions.length > 0) {
263
+ result = result.filter((file) => extensions.some((ext) => file.endsWith(ext)));
264
+ }
265
+ return result.sort();
266
+ }