thuban 0.3.2 → 0.3.3

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.
@@ -1,323 +1,323 @@
1
- /**
2
- * File Watcher
3
- *
4
- * @purpose Event-driven FileWatcher component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system reads, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel File Watcher
15
- *
16
- * Real-time file system monitoring for the Orion codebase.
17
- * Detects file changes, additions, and deletions to trigger scans.
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const EventEmitter = require('events');
23
-
24
- class FileWatcher extends EventEmitter {
25
- constructor(config = {}) {
26
- super();
27
-
28
- this.config = {
29
- rootPath: config.rootPath || process.cwd(),
30
- watchPatterns: config.watchPatterns || ['**/*.js', '**/*.ts', '**/*.json'],
31
- ignorePatterns: config.ignorePatterns || [
32
- 'node_modules/**',
33
- '.git/**',
34
- 'dist/**',
35
- 'build/**',
36
- '.orion/snapshots/**',
37
- 'coverage/**',
38
- '*.log'
39
- ],
40
- debounceMs: config.debounceMs || 300,
41
- ...config
42
- };
43
-
44
- this.watchers = [];
45
- this.watchedFiles = new Set();
46
- this.debounceTimers = new Map();
47
- this.isRunning = false;
48
- }
49
-
50
- /**
51
- * Start watching files
52
- */
53
- async start() {
54
- if (this.isRunning) {
55
- return { success: true, message: 'Already watching' };
56
- }
57
-
58
- console.log('[FILE-WATCHER] Starting file watcher...');
59
- console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);
60
-
61
- try {
62
- // Discover files to watch
63
- await this._discoverFiles(this.config.rootPath);
64
-
65
- // Set up watchers
66
- this._setupWatchers();
67
-
68
- this.isRunning = true;
69
- console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`);
70
-
71
- return {
72
- success: true,
73
- filesWatched: this.watchedFiles.size
74
- };
75
- } catch (error) {
76
- console.error('[FILE-WATCHER] Start failed:', error.message);
77
- return { success: false, error: error.message };
78
- }
79
- }
80
-
81
- /**
82
- * Stop watching files
83
- */
84
- async stop() {
85
- console.log('[FILE-WATCHER] Stopping file watcher...');
86
-
87
- // Close all watchers
88
- for (const watcher of this.watchers) {
89
- try {
90
- watcher.close();
91
- } catch (e) {
92
- // Ignore close errors
93
- }
94
- }
95
-
96
- // Clear debounce timers
97
- for (const timer of this.debounceTimers.values()) {
98
- clearTimeout(timer);
99
- }
100
-
101
- this.watchers = [];
102
- this.debounceTimers.clear();
103
- this.isRunning = false;
104
-
105
- console.log('[FILE-WATCHER] Stopped');
106
- return { success: true };
107
- }
108
-
109
- /**
110
- * Get count of watched files
111
- */
112
- getWatchedCount() {
113
- return this.watchedFiles.size;
114
- }
115
-
116
- /**
117
- * Get list of watched files
118
- */
119
- getWatchedFiles() {
120
- return Array.from(this.watchedFiles);
121
- }
122
-
123
- /**
124
- * Check if a file path should be ignored
125
- */
126
- shouldIgnore(filePath) {
127
- const relativePath = path.relative(this.config.rootPath, filePath);
128
-
129
- for (const pattern of this.config.ignorePatterns) {
130
- if (this._matchPattern(relativePath, pattern)) {
131
- return true;
132
- }
133
- }
134
-
135
- return false;
136
- }
137
-
138
- /**
139
- * Check if a file path matches watch patterns
140
- */
141
- shouldWatch(filePath) {
142
- const relativePath = path.relative(this.config.rootPath, filePath);
143
-
144
- // First check if ignored
145
- if (this.shouldIgnore(filePath)) {
146
- return false;
147
- }
148
-
149
- // Then check if matches watch patterns
150
- for (const pattern of this.config.watchPatterns) {
151
- if (this._matchPattern(relativePath, pattern)) {
152
- return true;
153
- }
154
- }
155
-
156
- return false;
157
- }
158
-
159
- // Private methods
160
-
161
- async _discoverFiles(dir) {
162
- try {
163
- const entries = fs.readdirSync(dir, { withFileTypes: true });
164
-
165
- for (const entry of entries) {
166
- const fullPath = path.join(dir, entry.name);
167
-
168
- // Skip ignored directories early
169
- if (entry.isDirectory()) {
170
- if (!this.shouldIgnore(fullPath)) {
171
- await this._discoverFiles(fullPath);
172
- }
173
- } else if (entry.isFile()) {
174
- if (this.shouldWatch(fullPath)) {
175
- this.watchedFiles.add(fullPath);
176
- }
177
- }
178
- }
179
- } catch (error) {
180
- // Directory might not exist or be inaccessible
181
- if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
182
- console.warn(`[FILE-WATCHER] Error scanning ${dir}:`, error.message);
183
- }
184
- }
185
- }
186
-
187
- _setupWatchers() {
188
- // Group files by directory for efficient watching
189
- const directories = new Set();
190
- for (const file of this.watchedFiles) {
191
- directories.add(path.dirname(file));
192
- }
193
-
194
- // Watch each directory
195
- for (const dir of directories) {
196
- try {
197
- const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
198
- if (filename) {
199
- const fullPath = path.join(dir, filename);
200
- this._handleFileEvent(eventType, fullPath);
201
- }
202
- });
203
-
204
- watcher.on('error', (error) => {
205
- console.warn(`[FILE-WATCHER] Watcher error for ${dir}:`, error.message);
206
- });
207
-
208
- this.watchers.push(watcher);
209
- } catch (error) {
210
- console.warn(`[FILE-WATCHER] Could not watch ${dir}:`, error.message);
211
- }
212
- }
213
-
214
- // Also set up a recursive watcher for the root (if supported)
215
- try {
216
- const rootWatcher = fs.watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
217
- if (filename) {
218
- const fullPath = path.join(this.config.rootPath, filename);
219
- this._handleFileEvent(eventType, fullPath);
220
- }
221
- });
222
-
223
- rootWatcher.on('error', (error) => {
224
- // Recursive watch might not be supported on all platforms
225
- if (error.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
226
- console.warn('[FILE-WATCHER] Root watcher error:', error.message);
227
- }
228
- });
229
-
230
- this.watchers.push(rootWatcher);
231
- } catch (error) {
232
- // Recursive watching might not be available
233
- }
234
- }
235
-
236
- _handleFileEvent(eventType, filePath) {
237
- // Skip if ignored
238
- if (this.shouldIgnore(filePath)) {
239
- return;
240
- }
241
-
242
- // Debounce rapid events
243
- if (this.debounceTimers.has(filePath)) {
244
- clearTimeout(this.debounceTimers.get(filePath));
245
- }
246
-
247
- this.debounceTimers.set(filePath, setTimeout(() => {
248
- this.debounceTimers.delete(filePath);
249
- this._processFileEvent(eventType, filePath);
250
- }, this.config.debounceMs));
251
- }
252
-
253
- _processFileEvent(eventType, filePath) {
254
- // Check if file exists
255
- const exists = fs.existsSync(filePath);
256
- const isFile = exists && fs.statSync(filePath).isFile();
257
-
258
- let event;
259
-
260
- if (!exists) {
261
- // File was deleted
262
- if (this.watchedFiles.has(filePath)) {
263
- this.watchedFiles.delete(filePath);
264
- event = {
265
- type: 'delete',
266
- filePath,
267
- timestamp: new Date().toISOString()
268
- };
269
- }
270
- } else if (isFile && this.shouldWatch(filePath)) {
271
- if (this.watchedFiles.has(filePath)) {
272
- // File was modified
273
- event = {
274
- type: 'change',
275
- filePath,
276
- timestamp: new Date().toISOString()
277
- };
278
- } else {
279
- // New file added
280
- this.watchedFiles.add(filePath);
281
- event = {
282
- type: 'add',
283
- filePath,
284
- timestamp: new Date().toISOString()
285
- };
286
- }
287
- }
288
-
289
- if (event) {
290
- this.emit(event.type, event);
291
- this.emit('any', event);
292
- }
293
- }
294
-
295
- _matchPattern(filePath, pattern) {
296
- // Simple glob-like pattern matching
297
- // Supports: *, **, ?
298
-
299
- // Normalize separators
300
- const normalizedPath = filePath.replace(/\\/g, '/');
301
- const normalizedPattern = pattern.replace(/\\/g, '/');
302
-
303
- // Convert glob pattern to regex
304
- let regexPattern = normalizedPattern
305
- .replace(/\./g, '\\.') // Escape dots
306
- .replace(/\*\*/g, '<<<GLOBSTAR>>>') // Temporarily replace **
307
- .replace(/\*/g, '[^/]*') // * matches anything except /
308
- .replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
309
- .replace(/\?/g, '.'); // ? matches single char
310
-
311
- // Anchor the pattern
312
- regexPattern = '^' + regexPattern + '$';
313
-
314
- try {
315
- const regex = new RegExp(regexPattern);
316
- return regex.test(normalizedPath);
317
- } catch (e) {
318
- return false;
319
- }
320
- }
321
- }
322
-
323
- module.exports = FileWatcher;
1
+ /**
2
+ * File Watcher
3
+ *
4
+ * @purpose Event-driven FileWatcher component
5
+ * @module sentinel
6
+ * @layer application
7
+ * @imports-from fs, path, events
8
+ * @exports-to sentinel
9
+ * @side-effects File system reads, Console output, Event emission
10
+ * @critical-for sentinel
11
+ */
12
+
13
+ /**
14
+ * Sentinel File Watcher
15
+ *
16
+ * Real-time file system monitoring for the Orion codebase.
17
+ * Detects file changes, additions, and deletions to trigger scans.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const EventEmitter = require('events');
23
+
24
+ class FileWatcher extends EventEmitter {
25
+ constructor(config = {}) {
26
+ super();
27
+
28
+ this.config = {
29
+ rootPath: config.rootPath || process.cwd(),
30
+ watchPatterns: config.watchPatterns || ['**/*.js', '**/*.ts', '**/*.json'],
31
+ ignorePatterns: config.ignorePatterns || [
32
+ 'node_modules/**',
33
+ '.git/**',
34
+ 'dist/**',
35
+ 'build/**',
36
+ '.orion/snapshots/**',
37
+ 'coverage/**',
38
+ '*.log'
39
+ ],
40
+ debounceMs: config.debounceMs || 300,
41
+ ...config
42
+ };
43
+
44
+ this.watchers = [];
45
+ this.watchedFiles = new Set();
46
+ this.debounceTimers = new Map();
47
+ this.isRunning = false;
48
+ }
49
+
50
+ /**
51
+ * Start watching files
52
+ */
53
+ async start() {
54
+ if (this.isRunning) {
55
+ return { success: true, message: 'Already watching' };
56
+ }
57
+
58
+ console.log('[FILE-WATCHER] Starting file watcher...');
59
+ console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);
60
+
61
+ try {
62
+ // Discover files to watch
63
+ await this._discoverFiles(this.config.rootPath);
64
+
65
+ // Set up watchers
66
+ this._setupWatchers();
67
+
68
+ this.isRunning = true;
69
+ console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`);
70
+
71
+ return {
72
+ success: true,
73
+ filesWatched: this.watchedFiles.size
74
+ };
75
+ } catch (error) {
76
+ console.error('[FILE-WATCHER] Start failed:', error.message);
77
+ return { success: false, error: error.message };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Stop watching files
83
+ */
84
+ async stop() {
85
+ console.log('[FILE-WATCHER] Stopping file watcher...');
86
+
87
+ // Close all watchers
88
+ for (const watcher of this.watchers) {
89
+ try {
90
+ watcher.close();
91
+ } catch (e) {
92
+ // Ignore close errors
93
+ }
94
+ }
95
+
96
+ // Clear debounce timers
97
+ for (const timer of this.debounceTimers.values()) {
98
+ clearTimeout(timer);
99
+ }
100
+
101
+ this.watchers = [];
102
+ this.debounceTimers.clear();
103
+ this.isRunning = false;
104
+
105
+ console.log('[FILE-WATCHER] Stopped');
106
+ return { success: true };
107
+ }
108
+
109
+ /**
110
+ * Get count of watched files
111
+ */
112
+ getWatchedCount() {
113
+ return this.watchedFiles.size;
114
+ }
115
+
116
+ /**
117
+ * Get list of watched files
118
+ */
119
+ getWatchedFiles() {
120
+ return Array.from(this.watchedFiles);
121
+ }
122
+
123
+ /**
124
+ * Check if a file path should be ignored
125
+ */
126
+ shouldIgnore(filePath) {
127
+ const relativePath = path.relative(this.config.rootPath, filePath);
128
+
129
+ for (const pattern of this.config.ignorePatterns) {
130
+ if (this._matchPattern(relativePath, pattern)) {
131
+ return true;
132
+ }
133
+ }
134
+
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * Check if a file path matches watch patterns
140
+ */
141
+ shouldWatch(filePath) {
142
+ const relativePath = path.relative(this.config.rootPath, filePath);
143
+
144
+ // First check if ignored
145
+ if (this.shouldIgnore(filePath)) {
146
+ return false;
147
+ }
148
+
149
+ // Then check if matches watch patterns
150
+ for (const pattern of this.config.watchPatterns) {
151
+ if (this._matchPattern(relativePath, pattern)) {
152
+ return true;
153
+ }
154
+ }
155
+
156
+ return false;
157
+ }
158
+
159
+ // Private methods
160
+
161
+ async _discoverFiles(dir) {
162
+ try {
163
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
164
+
165
+ for (const entry of entries) {
166
+ const fullPath = path.join(dir, entry.name);
167
+
168
+ // Skip ignored directories early
169
+ if (entry.isDirectory()) {
170
+ if (!this.shouldIgnore(fullPath)) {
171
+ await this._discoverFiles(fullPath);
172
+ }
173
+ } else if (entry.isFile()) {
174
+ if (this.shouldWatch(fullPath)) {
175
+ this.watchedFiles.add(fullPath);
176
+ }
177
+ }
178
+ }
179
+ } catch (error) {
180
+ // Directory might not exist or be inaccessible
181
+ if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
182
+ console.warn(`[FILE-WATCHER] Error scanning ${dir}:`, error.message);
183
+ }
184
+ }
185
+ }
186
+
187
+ _setupWatchers() {
188
+ // Group files by directory for efficient watching
189
+ const directories = new Set();
190
+ for (const file of this.watchedFiles) {
191
+ directories.add(path.dirname(file));
192
+ }
193
+
194
+ // Watch each directory
195
+ for (const dir of directories) {
196
+ try {
197
+ const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
198
+ if (filename) {
199
+ const fullPath = path.join(dir, filename);
200
+ this._handleFileEvent(eventType, fullPath);
201
+ }
202
+ });
203
+
204
+ watcher.on('error', (error) => {
205
+ console.warn(`[FILE-WATCHER] Watcher error for ${dir}:`, error.message);
206
+ });
207
+
208
+ this.watchers.push(watcher);
209
+ } catch (error) {
210
+ console.warn(`[FILE-WATCHER] Could not watch ${dir}:`, error.message);
211
+ }
212
+ }
213
+
214
+ // Also set up a recursive watcher for the root (if supported)
215
+ try {
216
+ const rootWatcher = fs.watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
217
+ if (filename) {
218
+ const fullPath = path.join(this.config.rootPath, filename);
219
+ this._handleFileEvent(eventType, fullPath);
220
+ }
221
+ });
222
+
223
+ rootWatcher.on('error', (error) => {
224
+ // Recursive watch might not be supported on all platforms
225
+ if (error.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
226
+ console.warn('[FILE-WATCHER] Root watcher error:', error.message);
227
+ }
228
+ });
229
+
230
+ this.watchers.push(rootWatcher);
231
+ } catch (error) {
232
+ // Recursive watching might not be available
233
+ }
234
+ }
235
+
236
+ _handleFileEvent(eventType, filePath) {
237
+ // Skip if ignored
238
+ if (this.shouldIgnore(filePath)) {
239
+ return;
240
+ }
241
+
242
+ // Debounce rapid events
243
+ if (this.debounceTimers.has(filePath)) {
244
+ clearTimeout(this.debounceTimers.get(filePath));
245
+ }
246
+
247
+ this.debounceTimers.set(filePath, setTimeout(() => {
248
+ this.debounceTimers.delete(filePath);
249
+ this._processFileEvent(eventType, filePath);
250
+ }, this.config.debounceMs));
251
+ }
252
+
253
+ _processFileEvent(eventType, filePath) {
254
+ // Check if file exists
255
+ const exists = fs.existsSync(filePath);
256
+ const isFile = exists && fs.statSync(filePath).isFile();
257
+
258
+ let event;
259
+
260
+ if (!exists) {
261
+ // File was deleted
262
+ if (this.watchedFiles.has(filePath)) {
263
+ this.watchedFiles.delete(filePath);
264
+ event = {
265
+ type: 'delete',
266
+ filePath,
267
+ timestamp: new Date().toISOString()
268
+ };
269
+ }
270
+ } else if (isFile && this.shouldWatch(filePath)) {
271
+ if (this.watchedFiles.has(filePath)) {
272
+ // File was modified
273
+ event = {
274
+ type: 'change',
275
+ filePath,
276
+ timestamp: new Date().toISOString()
277
+ };
278
+ } else {
279
+ // New file added
280
+ this.watchedFiles.add(filePath);
281
+ event = {
282
+ type: 'add',
283
+ filePath,
284
+ timestamp: new Date().toISOString()
285
+ };
286
+ }
287
+ }
288
+
289
+ if (event) {
290
+ this.emit(event.type, event);
291
+ this.emit('any', event);
292
+ }
293
+ }
294
+
295
+ _matchPattern(filePath, pattern) {
296
+ // Simple glob-like pattern matching
297
+ // Supports: *, **, ?
298
+
299
+ // Normalize separators
300
+ const normalizedPath = filePath.replace(/\\/g, '/');
301
+ const normalizedPattern = pattern.replace(/\\/g, '/');
302
+
303
+ // Convert glob pattern to regex
304
+ let regexPattern = normalizedPattern
305
+ .replace(/\./g, '\\.') // Escape dots
306
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>') // Temporarily replace **
307
+ .replace(/\*/g, '[^/]*') // * matches anything except /
308
+ .replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
309
+ .replace(/\?/g, '.'); // ? matches single char
310
+
311
+ // Anchor the pattern
312
+ regexPattern = '^' + regexPattern + '$';
313
+
314
+ try {
315
+ const regex = new RegExp(regexPattern);
316
+ return regex.test(normalizedPath);
317
+ } catch (e) {
318
+ return false;
319
+ }
320
+ }
321
+ }
322
+
323
+ module.exports = FileWatcher;