pi-lens 2.0.7 → 2.0.8
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/clients/ast-grep-client.test.ts +146 -116
- package/clients/ast-grep-client.ts +645 -551
- package/clients/biome-client.test.ts +154 -137
- package/clients/biome-client.ts +397 -337
- package/clients/complexity-client.test.ts +188 -200
- package/clients/complexity-client.ts +815 -667
- package/clients/dependency-checker.test.ts +55 -55
- package/clients/dependency-checker.ts +358 -333
- package/clients/go-client.test.ts +121 -111
- package/clients/go-client.ts +218 -216
- package/clients/jscpd-client.test.ts +132 -132
- package/clients/jscpd-client.ts +155 -118
- package/clients/knip-client.test.ts +123 -133
- package/clients/knip-client.ts +231 -218
- package/clients/metrics-client.test.ts +171 -167
- package/clients/metrics-client.ts +283 -252
- package/clients/ruff-client.test.ts +128 -117
- package/clients/ruff-client.ts +300 -269
- package/clients/rust-client.test.ts +104 -85
- package/clients/rust-client.ts +241 -234
- package/clients/subprocess-client.ts +1 -1
- package/clients/test-runner-client.test.ts +248 -215
- package/clients/test-runner-client.ts +728 -608
- package/clients/test-utils.ts +10 -3
- package/clients/todo-scanner.test.ts +288 -202
- package/clients/todo-scanner.ts +225 -187
- package/clients/type-coverage-client.test.ts +119 -119
- package/clients/type-coverage-client.ts +142 -115
- package/clients/types.ts +28 -28
- package/clients/typescript-client.test.ts +99 -93
- package/clients/typescript-client.ts +527 -502
- package/index.ts +662 -212
- package/package.json +1 -1
- package/tsconfig.json +12 -12
|
@@ -10,355 +10,380 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
|
-
import * as path from "node:path";
|
|
14
13
|
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
15
|
|
|
16
16
|
// --- Types ---
|
|
17
17
|
|
|
18
18
|
export interface CircularDep {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
file: string;
|
|
20
|
+
path: string[]; // The cycle path
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface DepCheckResult {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
hasCircular: boolean;
|
|
25
|
+
circular: CircularDep[];
|
|
26
|
+
checked: boolean;
|
|
27
|
+
cacheHit: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// --- Graph Cache ---
|
|
31
31
|
|
|
32
32
|
interface FileImports {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
imports: Set<string>;
|
|
34
|
+
timestamp: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// --- Client ---
|
|
38
38
|
|
|
39
39
|
export class DependencyChecker {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
40
|
+
private available: boolean | null = null;
|
|
41
|
+
private log: (msg: string) => void;
|
|
42
|
+
|
|
43
|
+
// Cache: file path -> its imports
|
|
44
|
+
private importCache = new Map<string, FileImports>();
|
|
45
|
+
|
|
46
|
+
// Circular deps: last known circular deps
|
|
47
|
+
private lastCircular: CircularDep[] = [];
|
|
48
|
+
|
|
49
|
+
// Files that are part of a circular dependency
|
|
50
|
+
private circularFiles = new Set<string>();
|
|
51
|
+
|
|
52
|
+
constructor(verbose = false) {
|
|
53
|
+
this.log = verbose
|
|
54
|
+
? (msg: string) => console.log(`[deps] ${msg}`)
|
|
55
|
+
: () => {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if madge is available
|
|
60
|
+
*/
|
|
61
|
+
isAvailable(): boolean {
|
|
62
|
+
if (this.available !== null) return this.available;
|
|
63
|
+
|
|
64
|
+
const result = spawnSync("npx", ["madge", "--version"], {
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
timeout: 10000,
|
|
67
|
+
shell: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.available = !result.error && result.status === 0;
|
|
71
|
+
if (this.available) {
|
|
72
|
+
this.log("Madge available for dependency checking");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.available;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a file is part of a circular dependency (from cache)
|
|
80
|
+
*/
|
|
81
|
+
isInCircular(filePath: string): boolean {
|
|
82
|
+
const normalized = path.resolve(filePath);
|
|
83
|
+
return this.circularFiles.has(normalized);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get circular deps for a specific file
|
|
88
|
+
*/
|
|
89
|
+
getCircularForFile(filePath: string): string[] {
|
|
90
|
+
const normalized = path.resolve(filePath);
|
|
91
|
+
const deps: string[] = [];
|
|
92
|
+
|
|
93
|
+
for (const dep of this.lastCircular) {
|
|
94
|
+
if (dep.file === normalized || dep.path.includes(normalized)) {
|
|
95
|
+
// Add the other files in the cycle
|
|
96
|
+
for (const f of dep.path) {
|
|
97
|
+
if (f !== normalized) {
|
|
98
|
+
deps.push(path.relative(process.cwd(), f));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [...new Set(deps)];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract imports from a TypeScript/JavaScript file
|
|
109
|
+
*/
|
|
110
|
+
extractImports(filePath: string): Set<string> {
|
|
111
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
112
|
+
const imports = new Set<string>();
|
|
113
|
+
|
|
114
|
+
// Match import statements: import ... from '...'
|
|
115
|
+
const importPattern =
|
|
116
|
+
/(?:import|export)\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
117
|
+
const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
118
|
+
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
121
|
+
if (match[1].startsWith(".")) {
|
|
122
|
+
imports.add(match[1]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
while ((match = requirePattern.exec(content)) !== null) {
|
|
127
|
+
if (match[1].startsWith(".")) {
|
|
128
|
+
imports.add(match[1]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return imports;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if imports have changed for a file
|
|
137
|
+
*/
|
|
138
|
+
importsChanged(filePath: string): boolean {
|
|
139
|
+
const normalized = path.resolve(filePath);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(normalized)) {
|
|
142
|
+
// File deleted, remove from cache
|
|
143
|
+
this.importCache.delete(normalized);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stat = fs.statSync(normalized);
|
|
148
|
+
const mtime = stat.mtimeMs;
|
|
149
|
+
|
|
150
|
+
const cached = this.importCache.get(normalized);
|
|
151
|
+
|
|
152
|
+
// If timestamp hasn't changed, imports haven't changed
|
|
153
|
+
if (cached && cached.timestamp >= mtime) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parse new imports
|
|
158
|
+
const newImports = this.extractImports(normalized);
|
|
159
|
+
const newEntry: FileImports = {
|
|
160
|
+
imports: newImports,
|
|
161
|
+
timestamp: mtime,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Check if imports actually changed
|
|
165
|
+
if (cached) {
|
|
166
|
+
if (cached.imports.size !== newImports.size) {
|
|
167
|
+
this.importCache.set(normalized, newEntry);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const imp of newImports) {
|
|
172
|
+
if (!cached.imports.has(imp)) {
|
|
173
|
+
this.importCache.set(normalized, newEntry);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const imp of cached.imports) {
|
|
179
|
+
if (!newImports.has(imp)) {
|
|
180
|
+
this.importCache.set(normalized, newEntry);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Imports are the same, just update timestamp
|
|
186
|
+
this.importCache.set(normalized, newEntry);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.importCache.set(normalized, newEntry);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Quick circular dependency check using DFS on cached graph.
|
|
196
|
+
* Only re-runs full madge check when imports change.
|
|
197
|
+
*/
|
|
198
|
+
checkFile(filePath: string, cwd?: string): DepCheckResult {
|
|
199
|
+
if (!this.isAvailable()) {
|
|
200
|
+
return {
|
|
201
|
+
hasCircular: false,
|
|
202
|
+
circular: [],
|
|
203
|
+
checked: false,
|
|
204
|
+
cacheHit: false,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const normalized = path.resolve(filePath);
|
|
209
|
+
const projectRoot = cwd || process.cwd();
|
|
210
|
+
|
|
211
|
+
// Check if imports changed
|
|
212
|
+
const importsChanged = this.importsChanged(normalized);
|
|
213
|
+
|
|
214
|
+
if (!importsChanged) {
|
|
215
|
+
// Return cached result
|
|
216
|
+
return {
|
|
217
|
+
hasCircular: this.circularFiles.has(normalized),
|
|
218
|
+
circular: this.lastCircular.filter(
|
|
219
|
+
(d) => d.file === normalized || d.path.includes(normalized),
|
|
220
|
+
),
|
|
221
|
+
checked: true,
|
|
222
|
+
cacheHit: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.log(
|
|
227
|
+
`Imports changed for ${path.basename(filePath)}, checking dependencies...`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Run madge on the specific file (fast)
|
|
231
|
+
try {
|
|
232
|
+
const result = spawnSync(
|
|
233
|
+
"npx",
|
|
234
|
+
[
|
|
235
|
+
"madge",
|
|
236
|
+
"--circular",
|
|
237
|
+
"--extensions",
|
|
238
|
+
"ts,tsx,js,jsx",
|
|
239
|
+
"--json",
|
|
240
|
+
normalized,
|
|
241
|
+
],
|
|
242
|
+
{
|
|
243
|
+
encoding: "utf-8",
|
|
244
|
+
timeout: 15000,
|
|
245
|
+
cwd: projectRoot,
|
|
246
|
+
shell: true,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const output = result.stdout || "[]";
|
|
251
|
+
const parsed = JSON.parse(output);
|
|
252
|
+
|
|
253
|
+
// Madge --circular --json returns array of cycle arrays: [["a.ts", "b.ts"], ...]
|
|
254
|
+
const cycles: string[][] = Array.isArray(parsed) ? parsed : [];
|
|
255
|
+
const circular: CircularDep[] = [];
|
|
256
|
+
const circularFiles = new Set<string>();
|
|
257
|
+
|
|
258
|
+
for (const cycle of cycles) {
|
|
259
|
+
const resolvedPaths = cycle.map((f: string) =>
|
|
260
|
+
path.resolve(projectRoot, f),
|
|
261
|
+
);
|
|
262
|
+
for (const f of resolvedPaths) {
|
|
263
|
+
circularFiles.add(f);
|
|
264
|
+
}
|
|
265
|
+
circular.push({
|
|
266
|
+
file: resolvedPaths[0],
|
|
267
|
+
path: resolvedPaths,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.lastCircular = circular;
|
|
272
|
+
this.circularFiles = circularFiles;
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
hasCircular: circular.length > 0,
|
|
276
|
+
circular: circular.filter(
|
|
277
|
+
(d) => d.file === normalized || d.path.includes(normalized),
|
|
278
|
+
),
|
|
279
|
+
checked: true,
|
|
280
|
+
cacheHit: false,
|
|
281
|
+
};
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
this.log(`Check error: ${err.message}`);
|
|
284
|
+
return {
|
|
285
|
+
hasCircular: false,
|
|
286
|
+
circular: [],
|
|
287
|
+
checked: false,
|
|
288
|
+
cacheHit: false,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Format circular dependency warning for LLM
|
|
295
|
+
*/
|
|
296
|
+
formatWarning(filePath: string, deps: string[]): string {
|
|
297
|
+
if (deps.length === 0) return "";
|
|
298
|
+
|
|
299
|
+
const filename = path.basename(filePath);
|
|
300
|
+
const depNames = deps.map((d) => path.basename(d));
|
|
301
|
+
|
|
302
|
+
let output = `[Circular Deps] ${filename} is in a cycle:\n`;
|
|
303
|
+
output += ` ${filename} ↔ ${depNames.join(", ")}\n`;
|
|
304
|
+
output += `\n Consider extracting shared code to a separate module.\n`;
|
|
305
|
+
|
|
306
|
+
return output;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Full project scan (for /check-deps command)
|
|
311
|
+
*/
|
|
312
|
+
scanProject(cwd?: string): { circular: CircularDep[]; count: number } {
|
|
313
|
+
if (!this.isAvailable()) {
|
|
314
|
+
return { circular: [], count: 0 };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const projectRoot = cwd || process.cwd();
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const result = spawnSync(
|
|
321
|
+
"npx",
|
|
322
|
+
[
|
|
323
|
+
"madge",
|
|
324
|
+
"--circular",
|
|
325
|
+
"--extensions",
|
|
326
|
+
"ts,tsx,js,jsx",
|
|
327
|
+
"--json",
|
|
328
|
+
projectRoot,
|
|
329
|
+
],
|
|
330
|
+
{
|
|
331
|
+
encoding: "utf-8",
|
|
332
|
+
timeout: 30000,
|
|
333
|
+
cwd: projectRoot,
|
|
334
|
+
shell: true,
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const output = result.stdout || "{}";
|
|
339
|
+
const data = JSON.parse(output);
|
|
340
|
+
|
|
341
|
+
const circular: CircularDep[] = [];
|
|
342
|
+
const circularFiles = new Set<string>();
|
|
343
|
+
|
|
344
|
+
for (const [file, deps] of Object.entries(data)) {
|
|
345
|
+
if (Array.isArray(deps) && deps.length > 0) {
|
|
346
|
+
const resolvedFile = path.resolve(file);
|
|
347
|
+
circularFiles.add(resolvedFile);
|
|
348
|
+
|
|
349
|
+
circular.push({
|
|
350
|
+
file: resolvedFile,
|
|
351
|
+
path: [resolvedFile, ...deps.map((d: string) => path.resolve(d))],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.lastCircular = circular;
|
|
357
|
+
this.circularFiles = circularFiles;
|
|
358
|
+
|
|
359
|
+
return { circular, count: circular.length };
|
|
360
|
+
} catch (err: any) {
|
|
361
|
+
this.log(`Scan error: ${err.message}`);
|
|
362
|
+
return { circular: [], count: 0 };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Format full scan results
|
|
368
|
+
*/
|
|
369
|
+
formatScanResult(circular: CircularDep[]): string {
|
|
370
|
+
if (circular.length === 0) return "";
|
|
371
|
+
|
|
372
|
+
// Group by cycle to avoid duplicate entries
|
|
373
|
+
const seen = new Set<string>();
|
|
374
|
+
let output = `[Circular Deps] ${circular.length} cycle(s) found:\n`;
|
|
375
|
+
|
|
376
|
+
for (const dep of circular) {
|
|
377
|
+
const cycleKey = dep.path.sort().join("→");
|
|
378
|
+
if (seen.has(cycleKey)) continue;
|
|
379
|
+
seen.add(cycleKey);
|
|
380
|
+
|
|
381
|
+
const names = dep.path.map((p) => path.relative(process.cwd(), p));
|
|
382
|
+
output += ` • ${names.join(" → ")}\n`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
output += "\n Consider extracting shared code to break cycles.\n";
|
|
386
|
+
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
364
389
|
}
|