driftdetect 0.1.7 → 0.2.1
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.
- package/dist/bin/drift.js +4 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/dashboard.d.ts +16 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +99 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +114 -5
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +103 -20
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +98 -14
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/watch.d.ts +6 -5
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +555 -126
- package/dist/commands/watch.js.map +1 -1
- package/dist/commands/where.d.ts +2 -0
- package/dist/commands/where.d.ts.map +1 -1
- package/dist/commands/where.js +79 -5
- package/dist/commands/where.js.map +1 -1
- package/dist/services/contract-scanner.d.ts +35 -0
- package/dist/services/contract-scanner.d.ts.map +1 -0
- package/dist/services/contract-scanner.js +183 -0
- package/dist/services/contract-scanner.js.map +1 -0
- package/dist/services/scanner-service.d.ts.map +1 -1
- package/dist/services/scanner-service.js +34 -1
- package/dist/services/scanner-service.js.map +1 -1
- package/package.json +3 -2
package/dist/commands/watch.js
CHANGED
|
@@ -1,111 +1,411 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drift Watch Command
|
|
3
3
|
*
|
|
4
|
-
* Real-time file watching with pattern detection.
|
|
5
|
-
* Monitors file changes
|
|
4
|
+
* Real-time file watching with pattern detection and persistence.
|
|
5
|
+
* Monitors file changes, detects patterns, persists to store, and emits events.
|
|
6
|
+
*
|
|
7
|
+
* @requirements Phase 1 - Watch mode should persist patterns to store
|
|
8
|
+
* @requirements Phase 2 - Smart merge strategy for pattern updates
|
|
9
|
+
* @requirements Phase 3 - File-level tracking for incremental updates
|
|
6
10
|
*/
|
|
7
11
|
import { Command } from 'commander';
|
|
8
12
|
import * as fs from 'node:fs';
|
|
13
|
+
import * as fsPromises from 'node:fs/promises';
|
|
9
14
|
import * as path from 'node:path';
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
10
16
|
import chalk from 'chalk';
|
|
11
17
|
import { createAllDetectorsArray } from 'driftdetect-detectors';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
import { PatternStore } from 'driftdetect-core';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Constants
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const DRIFT_DIR = '.drift';
|
|
23
|
+
const FILE_MAP_PATH = 'index/file-map.json';
|
|
24
|
+
const LOCK_FILE_PATH = 'index/.lock';
|
|
25
|
+
const LOCK_TIMEOUT_MS = 10000; // 10 seconds max lock hold time
|
|
26
|
+
const LOCK_RETRY_MS = 100; // Retry every 100ms
|
|
27
|
+
const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.css', '.scss', '.json', '.md'];
|
|
28
|
+
const IGNORE_PATTERNS = ['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo', '.drift'];
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Utility Functions
|
|
31
|
+
// ============================================================================
|
|
15
32
|
function timestamp() {
|
|
16
33
|
return chalk.gray(`[${new Date().toLocaleTimeString()}]`);
|
|
17
34
|
}
|
|
35
|
+
function generateFileHash(content) {
|
|
36
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
37
|
+
}
|
|
38
|
+
function generateStablePatternId(category, subcategory, detectorId, patternId) {
|
|
39
|
+
const key = `${category}:${subcategory}:${detectorId}:${patternId}`;
|
|
40
|
+
return crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
function mapToPatternCategory(category) {
|
|
43
|
+
const mapping = {
|
|
44
|
+
'api': 'api',
|
|
45
|
+
'auth': 'auth',
|
|
46
|
+
'security': 'security',
|
|
47
|
+
'errors': 'errors',
|
|
48
|
+
'structural': 'structural',
|
|
49
|
+
'components': 'components',
|
|
50
|
+
'styling': 'styling',
|
|
51
|
+
'logging': 'logging',
|
|
52
|
+
'testing': 'testing',
|
|
53
|
+
'data-access': 'data-access',
|
|
54
|
+
'config': 'config',
|
|
55
|
+
'types': 'types',
|
|
56
|
+
'performance': 'performance',
|
|
57
|
+
'accessibility': 'accessibility',
|
|
58
|
+
'documentation': 'documentation',
|
|
59
|
+
};
|
|
60
|
+
return mapping[category] || 'structural';
|
|
61
|
+
}
|
|
62
|
+
async function fileExists(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
await fsPromises.access(filePath);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function ensureDir(dirPath) {
|
|
72
|
+
await fsPromises.mkdir(dirPath, { recursive: true });
|
|
73
|
+
}
|
|
18
74
|
/**
|
|
19
|
-
*
|
|
75
|
+
* Acquire a file lock for exclusive access to .drift directory
|
|
76
|
+
* Uses a simple lock file with PID and timestamp
|
|
20
77
|
*/
|
|
21
|
-
async function
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Check if test file
|
|
42
|
-
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath) ||
|
|
43
|
-
filePath.includes('__tests__') ||
|
|
44
|
-
filePath.includes('/test/') ||
|
|
45
|
-
filePath.includes('/tests/');
|
|
46
|
-
// Check if type definition
|
|
47
|
-
const isTypeDefinition = ext === '.d.ts';
|
|
48
|
-
// Create minimal project context
|
|
49
|
-
const projectContext = {
|
|
50
|
-
rootDir: cwd,
|
|
51
|
-
files: [relativePath],
|
|
52
|
-
config: {},
|
|
53
|
-
};
|
|
54
|
-
// Run detectors
|
|
55
|
-
for (const detector of detectors) {
|
|
56
|
-
// Filter by category if specified
|
|
57
|
-
if (categories && !categories.includes(detector.category)) {
|
|
58
|
-
continue;
|
|
78
|
+
async function acquireLock(rootDir, holder) {
|
|
79
|
+
const lockPath = path.join(rootDir, DRIFT_DIR, LOCK_FILE_PATH);
|
|
80
|
+
await ensureDir(path.dirname(lockPath));
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
|
|
83
|
+
try {
|
|
84
|
+
// Check if lock exists and is stale
|
|
85
|
+
if (await fileExists(lockPath)) {
|
|
86
|
+
const content = await fsPromises.readFile(lockPath, 'utf-8');
|
|
87
|
+
const lockInfo = JSON.parse(content);
|
|
88
|
+
const lockAge = Date.now() - new Date(lockInfo.timestamp).getTime();
|
|
89
|
+
// If lock is older than timeout, it's stale - remove it
|
|
90
|
+
if (lockAge > LOCK_TIMEOUT_MS) {
|
|
91
|
+
await fsPromises.unlink(lockPath);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Lock is held by another process, wait and retry
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
59
98
|
}
|
|
60
|
-
//
|
|
61
|
-
|
|
99
|
+
// Try to create lock file exclusively
|
|
100
|
+
const lockInfo = {
|
|
101
|
+
pid: process.pid,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
holder,
|
|
104
|
+
};
|
|
105
|
+
await fsPromises.writeFile(lockPath, JSON.stringify(lockInfo), { flag: 'wx' });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const err = error;
|
|
110
|
+
if (err.code === 'EEXIST') {
|
|
111
|
+
// Lock file was created by another process, retry
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
|
|
62
113
|
continue;
|
|
63
114
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
// Other error, fail
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Timeout waiting for lock
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Release the file lock
|
|
124
|
+
*/
|
|
125
|
+
async function releaseLock(rootDir) {
|
|
126
|
+
const lockPath = path.join(rootDir, DRIFT_DIR, LOCK_FILE_PATH);
|
|
127
|
+
try {
|
|
128
|
+
// Only release if we own the lock
|
|
129
|
+
if (await fileExists(lockPath)) {
|
|
130
|
+
const content = await fsPromises.readFile(lockPath, 'utf-8');
|
|
131
|
+
const lockInfo = JSON.parse(content);
|
|
132
|
+
if (lockInfo.pid === process.pid) {
|
|
133
|
+
await fsPromises.unlink(lockPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Ignore errors during release
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Execute a function with file lock protection
|
|
143
|
+
*/
|
|
144
|
+
async function withLock(rootDir, holder, fn) {
|
|
145
|
+
const acquired = await acquireLock(rootDir, holder);
|
|
146
|
+
if (!acquired) {
|
|
147
|
+
throw new Error('Failed to acquire lock - another process may be writing');
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return await fn();
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await releaseLock(rootDir);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// File Map Management (Phase 3)
|
|
158
|
+
// ============================================================================
|
|
159
|
+
async function loadFileMap(rootDir) {
|
|
160
|
+
const mapPath = path.join(rootDir, DRIFT_DIR, FILE_MAP_PATH);
|
|
161
|
+
if (await fileExists(mapPath)) {
|
|
162
|
+
try {
|
|
163
|
+
const content = await fsPromises.readFile(mapPath, 'utf-8');
|
|
164
|
+
return JSON.parse(content);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Corrupted file, start fresh
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
version: '1.0.0',
|
|
172
|
+
files: {},
|
|
173
|
+
lastUpdated: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function saveFileMap(rootDir, fileMap) {
|
|
177
|
+
const mapPath = path.join(rootDir, DRIFT_DIR, FILE_MAP_PATH);
|
|
178
|
+
await ensureDir(path.dirname(mapPath));
|
|
179
|
+
fileMap.lastUpdated = new Date().toISOString();
|
|
180
|
+
// Atomic write: write to temp file, then rename
|
|
181
|
+
const tempPath = `${mapPath}.tmp`;
|
|
182
|
+
await fsPromises.writeFile(tempPath, JSON.stringify(fileMap, null, 2));
|
|
183
|
+
await fsPromises.rename(tempPath, mapPath);
|
|
184
|
+
}
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Pattern Detection
|
|
187
|
+
// ============================================================================
|
|
188
|
+
async function detectPatternsInFile(filePath, content, detectors, categories, rootDir) {
|
|
189
|
+
const patterns = [];
|
|
190
|
+
const violations = [];
|
|
191
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
192
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
193
|
+
let language = 'typescript';
|
|
194
|
+
if (['.ts', '.tsx'].includes(ext))
|
|
195
|
+
language = 'typescript';
|
|
196
|
+
else if (['.js', '.jsx'].includes(ext))
|
|
197
|
+
language = 'javascript';
|
|
198
|
+
else if (['.py'].includes(ext))
|
|
199
|
+
language = 'python';
|
|
200
|
+
else if (['.css', '.scss'].includes(ext))
|
|
201
|
+
language = 'css';
|
|
202
|
+
else if (['.json'].includes(ext))
|
|
203
|
+
language = 'json';
|
|
204
|
+
else if (['.md'].includes(ext))
|
|
205
|
+
language = 'markdown';
|
|
206
|
+
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath) ||
|
|
207
|
+
filePath.includes('__tests__') ||
|
|
208
|
+
filePath.includes('/test/') ||
|
|
209
|
+
filePath.includes('/tests/');
|
|
210
|
+
const isTypeDefinition = ext === '.d.ts';
|
|
211
|
+
const projectContext = {
|
|
212
|
+
rootDir,
|
|
213
|
+
files: [relativePath],
|
|
214
|
+
config: {},
|
|
215
|
+
};
|
|
216
|
+
// Aggregate patterns by detector+patternId
|
|
217
|
+
const patternMap = new Map();
|
|
218
|
+
for (const detector of detectors) {
|
|
219
|
+
if (categories && !categories.includes(detector.category)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!detector.supportsLanguage(language)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const context = {
|
|
227
|
+
file: relativePath,
|
|
228
|
+
content,
|
|
229
|
+
ast: null,
|
|
230
|
+
imports: [],
|
|
231
|
+
exports: [],
|
|
232
|
+
projectContext,
|
|
233
|
+
language,
|
|
234
|
+
extension: ext,
|
|
235
|
+
isTestFile,
|
|
236
|
+
isTypeDefinition,
|
|
237
|
+
};
|
|
238
|
+
const result = await detector.detect(context);
|
|
239
|
+
const info = detector.getInfo();
|
|
240
|
+
// Process matches (patterns)
|
|
241
|
+
if (result.patterns && result.patterns.length > 0) {
|
|
242
|
+
for (const match of result.patterns) {
|
|
243
|
+
const key = `${info.category}:${info.subcategory}:${detector.id}:${match.patternId}`;
|
|
244
|
+
if (!patternMap.has(key)) {
|
|
245
|
+
patternMap.set(key, {
|
|
246
|
+
patternId: match.patternId,
|
|
247
|
+
detectorId: detector.id,
|
|
248
|
+
category: info.category,
|
|
249
|
+
subcategory: info.subcategory,
|
|
250
|
+
name: info.name,
|
|
251
|
+
description: info.description,
|
|
252
|
+
confidence: match.confidence,
|
|
253
|
+
locations: [],
|
|
88
254
|
});
|
|
89
255
|
}
|
|
256
|
+
const pattern = patternMap.get(key);
|
|
257
|
+
const loc = {
|
|
258
|
+
file: relativePath,
|
|
259
|
+
line: match.location?.line ?? 1,
|
|
260
|
+
column: match.location?.column ?? 0,
|
|
261
|
+
};
|
|
262
|
+
if (match.location?.endLine !== undefined) {
|
|
263
|
+
loc.endLine = match.location.endLine;
|
|
264
|
+
}
|
|
265
|
+
if (match.location?.endColumn !== undefined) {
|
|
266
|
+
loc.endColumn = match.location.endColumn;
|
|
267
|
+
}
|
|
268
|
+
pattern.locations.push(loc);
|
|
90
269
|
}
|
|
91
270
|
}
|
|
92
|
-
|
|
93
|
-
|
|
271
|
+
// Process violations
|
|
272
|
+
if (result.violations && result.violations.length > 0) {
|
|
273
|
+
for (const v of result.violations) {
|
|
274
|
+
violations.push({
|
|
275
|
+
file: relativePath,
|
|
276
|
+
line: v.range?.start?.line ?? 1,
|
|
277
|
+
column: v.range?.start?.character ?? 0,
|
|
278
|
+
endLine: v.range?.end?.line,
|
|
279
|
+
endColumn: v.range?.end?.character,
|
|
280
|
+
message: v.message,
|
|
281
|
+
severity: v.severity,
|
|
282
|
+
patternId: v.patternId,
|
|
283
|
+
detectorId: detector.id,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
94
286
|
}
|
|
95
287
|
}
|
|
288
|
+
catch {
|
|
289
|
+
// Skip detector errors
|
|
290
|
+
}
|
|
96
291
|
}
|
|
97
|
-
|
|
98
|
-
|
|
292
|
+
patterns.push(...patternMap.values());
|
|
293
|
+
return { patterns, violations };
|
|
294
|
+
}
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Pattern Store Integration (Phase 1 & 2)
|
|
297
|
+
// ============================================================================
|
|
298
|
+
function mergePatternIntoStore(store, detected, violations, file) {
|
|
299
|
+
const stableId = generateStablePatternId(detected.category, detected.subcategory, detected.detectorId, detected.patternId);
|
|
300
|
+
const now = new Date().toISOString();
|
|
301
|
+
const existingPattern = store.get(stableId);
|
|
302
|
+
// Get violations for this pattern as outliers
|
|
303
|
+
const patternViolations = violations.filter(v => v.detectorId === detected.detectorId && v.patternId === detected.patternId);
|
|
304
|
+
const newOutliers = patternViolations.map(v => ({
|
|
305
|
+
file: v.file,
|
|
306
|
+
line: v.line,
|
|
307
|
+
column: v.column,
|
|
308
|
+
reason: v.message,
|
|
309
|
+
deviationScore: v.severity === 'error' ? 1.0 : v.severity === 'warning' ? 0.7 : 0.4,
|
|
310
|
+
}));
|
|
311
|
+
if (existingPattern) {
|
|
312
|
+
// Phase 2: Smart merge - update existing pattern
|
|
313
|
+
// Remove old locations from this file, add new ones
|
|
314
|
+
const otherFileLocations = existingPattern.locations.filter(loc => loc.file !== file);
|
|
315
|
+
const mergedLocations = [...otherFileLocations, ...detected.locations].slice(0, 100);
|
|
316
|
+
// Same for outliers - filter to only include required fields
|
|
317
|
+
const otherFileOutliers = existingPattern.outliers.filter(o => o.file !== file);
|
|
318
|
+
const mergedOutliers = [
|
|
319
|
+
...otherFileOutliers,
|
|
320
|
+
...newOutliers,
|
|
321
|
+
];
|
|
322
|
+
// Update pattern preserving status
|
|
323
|
+
store.update(stableId, {
|
|
324
|
+
locations: mergedLocations,
|
|
325
|
+
outliers: mergedOutliers,
|
|
326
|
+
metadata: {
|
|
327
|
+
...existingPattern.metadata,
|
|
328
|
+
lastSeen: now,
|
|
329
|
+
},
|
|
330
|
+
confidence: {
|
|
331
|
+
...existingPattern.confidence,
|
|
332
|
+
score: Math.max(existingPattern.confidence.score, detected.confidence),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// New pattern - add to store
|
|
338
|
+
const confidenceScore = Math.min(0.95, detected.confidence);
|
|
339
|
+
const newPattern = {
|
|
340
|
+
id: stableId,
|
|
341
|
+
category: mapToPatternCategory(detected.category),
|
|
342
|
+
subcategory: detected.subcategory,
|
|
343
|
+
name: detected.name,
|
|
344
|
+
description: detected.description,
|
|
345
|
+
detector: {
|
|
346
|
+
type: 'regex',
|
|
347
|
+
config: {
|
|
348
|
+
detectorId: detected.detectorId,
|
|
349
|
+
patternId: detected.patternId,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
confidence: {
|
|
353
|
+
frequency: Math.min(1, detected.locations.length / 10),
|
|
354
|
+
consistency: 0.9,
|
|
355
|
+
age: 0,
|
|
356
|
+
spread: 1,
|
|
357
|
+
score: confidenceScore,
|
|
358
|
+
level: confidenceScore >= 0.85 ? 'high' : confidenceScore >= 0.65 ? 'medium' : confidenceScore >= 0.45 ? 'low' : 'uncertain',
|
|
359
|
+
},
|
|
360
|
+
locations: detected.locations.slice(0, 100),
|
|
361
|
+
outliers: newOutliers,
|
|
362
|
+
metadata: {
|
|
363
|
+
firstSeen: now,
|
|
364
|
+
lastSeen: now,
|
|
365
|
+
source: 'auto-detected',
|
|
366
|
+
tags: [detected.category, detected.subcategory],
|
|
367
|
+
},
|
|
368
|
+
severity: patternViolations.length > 0
|
|
369
|
+
? (patternViolations.some(v => v.severity === 'error') ? 'error' : 'warning')
|
|
370
|
+
: 'info',
|
|
371
|
+
autoFixable: false,
|
|
372
|
+
status: 'discovered',
|
|
373
|
+
};
|
|
374
|
+
store.add(newPattern);
|
|
99
375
|
}
|
|
100
|
-
return
|
|
376
|
+
return stableId;
|
|
101
377
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
378
|
+
function removeFileFromStore(store, file) {
|
|
379
|
+
// Remove all locations and outliers for this file from all patterns
|
|
380
|
+
const allPatterns = store.getAll();
|
|
381
|
+
for (const pattern of allPatterns) {
|
|
382
|
+
const hasLocationsInFile = pattern.locations.some(loc => loc.file === file);
|
|
383
|
+
const hasOutliersInFile = pattern.outliers.some(o => o.file === file);
|
|
384
|
+
if (hasLocationsInFile || hasOutliersInFile) {
|
|
385
|
+
const newLocations = pattern.locations.filter(loc => loc.file !== file);
|
|
386
|
+
const newOutliers = pattern.outliers.filter(o => o.file !== file);
|
|
387
|
+
if (newLocations.length === 0 && newOutliers.length === 0) {
|
|
388
|
+
// Pattern has no more locations - delete it
|
|
389
|
+
store.delete(pattern.id);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Update pattern with remaining locations
|
|
393
|
+
store.update(pattern.id, {
|
|
394
|
+
locations: newLocations,
|
|
395
|
+
outliers: newOutliers,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// ============================================================================
|
|
402
|
+
// Console Output
|
|
403
|
+
// ============================================================================
|
|
404
|
+
function printViolations(filePath, violations, patternsUpdated, verbose) {
|
|
106
405
|
const relativePath = path.relative(process.cwd(), filePath);
|
|
107
406
|
if (violations.length === 0) {
|
|
108
|
-
|
|
407
|
+
const patternInfo = patternsUpdated > 0 ? chalk.cyan(` (${patternsUpdated} patterns)`) : '';
|
|
408
|
+
console.log(`${timestamp()} ${chalk.green('✓')} ${relativePath}${patternInfo}`);
|
|
109
409
|
return;
|
|
110
410
|
}
|
|
111
411
|
const errors = violations.filter(v => v.severity === 'error');
|
|
@@ -118,24 +418,25 @@ function printViolations(filePath, violations, verbose) {
|
|
|
118
418
|
summary += ', ';
|
|
119
419
|
summary += chalk.yellow(`${warnings.length} warning${warnings.length > 1 ? 's' : ''}`);
|
|
120
420
|
}
|
|
121
|
-
|
|
421
|
+
const patternInfo = patternsUpdated > 0 ? chalk.cyan(` | ${patternsUpdated} patterns`) : '';
|
|
422
|
+
console.log(`${timestamp()} ${chalk.red('✗')} ${relativePath} - ${summary}${patternInfo}`);
|
|
122
423
|
if (verbose) {
|
|
123
424
|
for (const v of violations) {
|
|
124
425
|
const icon = v.severity === 'error' ? chalk.red('●') : chalk.yellow('●');
|
|
125
426
|
console.log(` ${icon} Line ${v.line}: ${v.message}`);
|
|
126
|
-
console.log(` ${chalk.gray(`[${v.patternName}]`)}`);
|
|
127
427
|
}
|
|
128
428
|
}
|
|
129
429
|
}
|
|
130
|
-
|
|
131
|
-
* Update AI context file
|
|
132
|
-
*/
|
|
133
|
-
function updateContextFile(contextPath) {
|
|
430
|
+
function updateContextFile(contextPath, stats) {
|
|
134
431
|
try {
|
|
135
432
|
const content = `# Drift Context (Auto-updated)
|
|
136
433
|
|
|
137
434
|
Last updated: ${new Date().toISOString()}
|
|
138
435
|
|
|
436
|
+
## Current Stats
|
|
437
|
+
- Patterns tracked: ${stats.patterns}
|
|
438
|
+
- Active violations: ${stats.violations}
|
|
439
|
+
|
|
139
440
|
This file is auto-updated by \`drift watch\`.
|
|
140
441
|
Run \`drift export --format ai-context\` for full pattern details.
|
|
141
442
|
|
|
@@ -143,6 +444,7 @@ Run \`drift export --format ai-context\` for full pattern details.
|
|
|
143
444
|
- \`drift where <pattern>\` - Find pattern locations
|
|
144
445
|
- \`drift files <path>\` - See patterns in a specific file
|
|
145
446
|
- \`drift status\` - View pattern summary
|
|
447
|
+
- \`drift dashboard\` - Open web UI
|
|
146
448
|
`;
|
|
147
449
|
fs.writeFileSync(contextPath, content);
|
|
148
450
|
}
|
|
@@ -150,17 +452,18 @@ Run \`drift export --format ai-context\` for full pattern details.
|
|
|
150
452
|
// Silently fail context updates
|
|
151
453
|
}
|
|
152
454
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// Watch Command Implementation
|
|
457
|
+
// ============================================================================
|
|
156
458
|
async function watchCommand(options) {
|
|
157
|
-
const
|
|
459
|
+
const rootDir = process.cwd();
|
|
158
460
|
const verbose = options.verbose ?? false;
|
|
159
461
|
const contextPath = options.context;
|
|
160
462
|
const debounceMs = parseInt(options.debounce ?? '300', 10);
|
|
161
463
|
const categories = options.categories?.split(',').map(c => c.trim()) ?? null;
|
|
464
|
+
const persist = options.persist !== false; // Default to true
|
|
162
465
|
console.log(chalk.cyan('\n🔍 Drift Watch Mode\n'));
|
|
163
|
-
console.log(` Watching: ${chalk.white(
|
|
466
|
+
console.log(` Watching: ${chalk.white(rootDir)}`);
|
|
164
467
|
if (categories) {
|
|
165
468
|
console.log(` Categories: ${chalk.white(categories.join(', '))}`);
|
|
166
469
|
}
|
|
@@ -168,33 +471,72 @@ async function watchCommand(options) {
|
|
|
168
471
|
console.log(` Context file: ${chalk.white(contextPath)}`);
|
|
169
472
|
}
|
|
170
473
|
console.log(` Debounce: ${chalk.white(`${debounceMs}ms`)}`);
|
|
474
|
+
console.log(` Persistence: ${chalk.white(persist ? 'enabled' : 'disabled')}`);
|
|
171
475
|
console.log(chalk.gray('\n Press Ctrl+C to stop\n'));
|
|
172
476
|
console.log(chalk.gray('─'.repeat(50)));
|
|
477
|
+
// Initialize pattern store
|
|
478
|
+
let store = null;
|
|
479
|
+
let fileMap = null;
|
|
480
|
+
if (persist) {
|
|
481
|
+
try {
|
|
482
|
+
store = new PatternStore({ rootDir });
|
|
483
|
+
await store.initialize();
|
|
484
|
+
fileMap = await loadFileMap(rootDir);
|
|
485
|
+
const stats = store.getStats();
|
|
486
|
+
console.log(`${timestamp()} Loaded ${chalk.cyan(String(stats.totalPatterns))} existing patterns`);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.log(`${timestamp()} ${chalk.yellow('Warning: Could not initialize store, running without persistence')}`);
|
|
490
|
+
console.log(chalk.gray(` ${error.message}`));
|
|
491
|
+
store = null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
173
494
|
// Load detectors
|
|
174
495
|
const detectors = createAllDetectorsArray();
|
|
175
496
|
console.log(`${timestamp()} Loaded ${chalk.cyan(String(detectors.length))} detectors`);
|
|
176
497
|
// Track pending scans (for debouncing)
|
|
177
498
|
const pendingScans = new Map();
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
499
|
+
// Track save debounce
|
|
500
|
+
let saveTimeout = null;
|
|
501
|
+
const SAVE_DEBOUNCE_MS = 1000;
|
|
502
|
+
function scheduleSave() {
|
|
503
|
+
if (!store || !fileMap)
|
|
504
|
+
return;
|
|
505
|
+
if (saveTimeout) {
|
|
506
|
+
clearTimeout(saveTimeout);
|
|
507
|
+
}
|
|
508
|
+
saveTimeout = setTimeout(async () => {
|
|
509
|
+
try {
|
|
510
|
+
// Use file locking for concurrent write protection (Phase 4)
|
|
511
|
+
await withLock(rootDir, 'drift-watch', async () => {
|
|
512
|
+
await store.saveAll();
|
|
513
|
+
await saveFileMap(rootDir, fileMap);
|
|
514
|
+
});
|
|
515
|
+
if (verbose) {
|
|
516
|
+
console.log(`${timestamp()} ${chalk.gray('Saved patterns to disk')}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
console.log(`${timestamp()} ${chalk.red('Failed to save:')} ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
}, SAVE_DEBOUNCE_MS);
|
|
523
|
+
}
|
|
187
524
|
/**
|
|
188
525
|
* Handle file change
|
|
189
526
|
*/
|
|
190
|
-
function handleFileChange(filePath) {
|
|
527
|
+
async function handleFileChange(filePath) {
|
|
528
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
191
529
|
// Check if file should be ignored
|
|
192
|
-
const
|
|
193
|
-
for (const pattern of ignorePatterns) {
|
|
530
|
+
for (const pattern of IGNORE_PATTERNS) {
|
|
194
531
|
if (relativePath.includes(pattern)) {
|
|
195
532
|
return;
|
|
196
533
|
}
|
|
197
534
|
}
|
|
535
|
+
// Check extension
|
|
536
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
537
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
198
540
|
// Debounce
|
|
199
541
|
const existing = pendingScans.get(filePath);
|
|
200
542
|
if (existing) {
|
|
@@ -202,31 +544,100 @@ async function watchCommand(options) {
|
|
|
202
544
|
}
|
|
203
545
|
pendingScans.set(filePath, setTimeout(async () => {
|
|
204
546
|
pendingScans.delete(filePath);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
547
|
+
try {
|
|
548
|
+
// Check if file still exists
|
|
549
|
+
if (!fs.existsSync(filePath)) {
|
|
550
|
+
// File was deleted
|
|
551
|
+
if (store && fileMap) {
|
|
552
|
+
removeFileFromStore(store, relativePath);
|
|
553
|
+
delete fileMap.files[relativePath];
|
|
554
|
+
scheduleSave();
|
|
555
|
+
}
|
|
556
|
+
console.log(`${timestamp()} ${chalk.gray('Deleted:')} ${relativePath}`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Read file content
|
|
560
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
561
|
+
const fileHash = generateFileHash(content);
|
|
562
|
+
// Check if file actually changed (Phase 3)
|
|
563
|
+
if (fileMap && fileMap.files[relativePath]?.hash === fileHash) {
|
|
564
|
+
if (verbose) {
|
|
565
|
+
console.log(`${timestamp()} ${chalk.gray('Unchanged:')} ${relativePath}`);
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
// Detect patterns
|
|
570
|
+
const { patterns, violations } = await detectPatternsInFile(filePath, content, detectors, categories, rootDir);
|
|
571
|
+
// Update store (Phase 1 & 2)
|
|
572
|
+
let patternsUpdated = 0;
|
|
573
|
+
const patternIds = [];
|
|
574
|
+
if (store) {
|
|
575
|
+
// First, remove old data for this file
|
|
576
|
+
removeFileFromStore(store, relativePath);
|
|
577
|
+
// Then add new patterns
|
|
578
|
+
for (const detected of patterns) {
|
|
579
|
+
const patternId = mergePatternIntoStore(store, detected, violations, relativePath);
|
|
580
|
+
patternIds.push(patternId);
|
|
581
|
+
patternsUpdated++;
|
|
582
|
+
}
|
|
583
|
+
// Update file map
|
|
584
|
+
if (fileMap) {
|
|
585
|
+
fileMap.files[relativePath] = {
|
|
586
|
+
lastScanned: new Date().toISOString(),
|
|
587
|
+
hash: fileHash,
|
|
588
|
+
patterns: patternIds,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
scheduleSave();
|
|
592
|
+
}
|
|
593
|
+
// Print results
|
|
594
|
+
printViolations(filePath, violations, patternsUpdated, verbose);
|
|
595
|
+
// Update context file
|
|
596
|
+
if (contextPath && store) {
|
|
597
|
+
const stats = store.getStats();
|
|
598
|
+
updateContextFile(contextPath, {
|
|
599
|
+
patterns: stats.totalPatterns,
|
|
600
|
+
violations: stats.totalOutliers,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.log(`${timestamp()} ${chalk.red('Error processing')} ${relativePath}: ${error.message}`);
|
|
210
606
|
}
|
|
211
607
|
}, debounceMs));
|
|
212
608
|
}
|
|
213
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Handle file deletion
|
|
611
|
+
*/
|
|
612
|
+
function handleFileDelete(filePath) {
|
|
613
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
614
|
+
if (store && fileMap) {
|
|
615
|
+
removeFileFromStore(store, relativePath);
|
|
616
|
+
delete fileMap.files[relativePath];
|
|
617
|
+
scheduleSave();
|
|
618
|
+
}
|
|
619
|
+
console.log(`${timestamp()} ${chalk.gray('Removed:')} ${relativePath}`);
|
|
620
|
+
}
|
|
621
|
+
// Watch for file changes
|
|
214
622
|
const watchers = [];
|
|
215
623
|
function watchDirectory(dir) {
|
|
216
624
|
try {
|
|
217
|
-
const watcher = fs.watch(dir, { recursive: true }, (
|
|
625
|
+
const watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
218
626
|
if (!filename)
|
|
219
627
|
return;
|
|
220
628
|
const fullPath = path.join(dir, filename);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
629
|
+
if (eventType === 'rename') {
|
|
630
|
+
// Could be create or delete
|
|
631
|
+
if (fs.existsSync(fullPath)) {
|
|
632
|
+
handleFileChange(fullPath);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
handleFileDelete(fullPath);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
handleFileChange(fullPath);
|
|
640
|
+
}
|
|
230
641
|
});
|
|
231
642
|
watchers.push(watcher);
|
|
232
643
|
}
|
|
@@ -235,30 +646,48 @@ async function watchCommand(options) {
|
|
|
235
646
|
}
|
|
236
647
|
}
|
|
237
648
|
// Start watching
|
|
238
|
-
watchDirectory(
|
|
649
|
+
watchDirectory(rootDir);
|
|
239
650
|
console.log(`${timestamp()} ${chalk.green('Watching for changes...')}\n`);
|
|
240
651
|
// Handle shutdown
|
|
241
|
-
process.on('SIGINT', () => {
|
|
652
|
+
process.on('SIGINT', async () => {
|
|
242
653
|
console.log(chalk.gray('\n\nStopping watch mode...'));
|
|
654
|
+
// Clear pending operations
|
|
243
655
|
for (const watcher of watchers) {
|
|
244
656
|
watcher.close();
|
|
245
657
|
}
|
|
246
658
|
for (const timeout of pendingScans.values()) {
|
|
247
659
|
clearTimeout(timeout);
|
|
248
660
|
}
|
|
661
|
+
if (saveTimeout) {
|
|
662
|
+
clearTimeout(saveTimeout);
|
|
663
|
+
}
|
|
664
|
+
// Final save
|
|
665
|
+
if (store && fileMap) {
|
|
666
|
+
try {
|
|
667
|
+
await withLock(rootDir, 'drift-watch-exit', async () => {
|
|
668
|
+
await store.saveAll();
|
|
669
|
+
await saveFileMap(rootDir, fileMap);
|
|
670
|
+
});
|
|
671
|
+
console.log(chalk.green('Saved patterns before exit'));
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
console.log(chalk.red('Failed to save on exit:'), error.message);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
249
677
|
process.exit(0);
|
|
250
678
|
});
|
|
251
679
|
// Keep process alive
|
|
252
680
|
await new Promise(() => { });
|
|
253
681
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
682
|
+
// ============================================================================
|
|
683
|
+
// Command Definition
|
|
684
|
+
// ============================================================================
|
|
257
685
|
export const watchCommandDef = new Command('watch')
|
|
258
|
-
.description('Watch for file changes and
|
|
259
|
-
.option('--verbose', 'Show detailed
|
|
686
|
+
.description('Watch for file changes and detect patterns in real-time')
|
|
687
|
+
.option('--verbose', 'Show detailed output')
|
|
260
688
|
.option('--context <file>', 'Auto-update AI context file on changes')
|
|
261
689
|
.option('-c, --categories <categories>', 'Filter by categories (comma-separated)')
|
|
262
690
|
.option('--debounce <ms>', 'Debounce delay in milliseconds', '300')
|
|
691
|
+
.option('--no-persist', 'Disable pattern persistence (only show violations)')
|
|
263
692
|
.action(watchCommand);
|
|
264
693
|
//# sourceMappingURL=watch.js.map
|