pi-lens 2.0.37 → 2.0.39

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/clients/architect-client.js +50 -20
  4. package/clients/architect-client.ts +61 -22
  5. package/clients/ast-grep-client.js +32 -127
  6. package/clients/ast-grep-client.test.js +2 -14
  7. package/clients/ast-grep-client.test.ts +2 -16
  8. package/clients/ast-grep-client.ts +34 -150
  9. package/clients/auto-loop.js +117 -0
  10. package/clients/auto-loop.ts +171 -0
  11. package/clients/biome-client.js +25 -22
  12. package/clients/biome-client.ts +28 -25
  13. package/clients/complexity-client.js +111 -74
  14. package/clients/complexity-client.ts +149 -105
  15. package/clients/dependency-checker.js +16 -30
  16. package/clients/dependency-checker.ts +15 -35
  17. package/clients/fix-scanners.js +195 -0
  18. package/clients/fix-scanners.ts +297 -0
  19. package/clients/interviewer-templates.js +75 -0
  20. package/clients/interviewer-templates.ts +90 -0
  21. package/clients/interviewer.js +73 -101
  22. package/clients/interviewer.ts +195 -140
  23. package/clients/knip-client.js +12 -4
  24. package/clients/knip-client.ts +21 -16
  25. package/clients/metrics-history.js +215 -0
  26. package/clients/metrics-history.ts +300 -0
  27. package/clients/scan-architectural-debt.js +62 -72
  28. package/clients/scan-architectural-debt.ts +79 -64
  29. package/clients/scan-utils.js +98 -0
  30. package/clients/scan-utils.ts +112 -0
  31. package/clients/sg-runner.js +138 -0
  32. package/clients/sg-runner.ts +168 -0
  33. package/clients/subprocess-client.js +0 -37
  34. package/clients/subprocess-client.ts +0 -60
  35. package/clients/ts-service.ts +1 -7
  36. package/clients/type-safety-client.js +3 -8
  37. package/clients/type-safety-client.ts +10 -14
  38. package/clients/typescript-client.js +55 -56
  39. package/clients/typescript-client.ts +94 -52
  40. package/default-architect.yaml +87 -0
  41. package/index.ts +143 -1165
  42. package/package.json +2 -1
  43. package/rules/ast-grep-rules/rules/large-class.yml +6 -2
  44. package/rules/ast-grep-rules/rules/long-method.yml +1 -1
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
  46. package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
@@ -95,46 +95,32 @@ export class DependencyChecker {
95
95
  importsChanged(filePath) {
96
96
  const normalized = path.resolve(filePath);
97
97
  if (!fs.existsSync(normalized)) {
98
- // File deleted, remove from cache
99
98
  this.importCache.delete(normalized);
100
99
  return true;
101
100
  }
102
101
  const stat = fs.statSync(normalized);
103
- const mtime = stat.mtimeMs;
104
102
  const cached = this.importCache.get(normalized);
105
- // If timestamp hasn't changed, imports haven't changed
106
- if (cached && cached.timestamp >= mtime) {
103
+ // Fast path: timestamp hasn't changed
104
+ if (cached && cached.timestamp >= stat.mtimeMs) {
107
105
  return false;
108
106
  }
109
- // Parse new imports
107
+ // Compare actual imports
110
108
  const newImports = this.extractImports(normalized);
111
- const newEntry = {
112
- imports: newImports,
113
- timestamp: mtime,
114
- };
115
- // Check if imports actually changed
116
- if (cached) {
117
- if (cached.imports.size !== newImports.size) {
118
- this.importCache.set(normalized, newEntry);
119
- return true;
120
- }
121
- for (const imp of newImports) {
122
- if (!cached.imports.has(imp)) {
123
- this.importCache.set(normalized, newEntry);
124
- return true;
125
- }
126
- }
127
- for (const imp of cached.imports) {
128
- if (!newImports.has(imp)) {
129
- this.importCache.set(normalized, newEntry);
130
- return true;
131
- }
132
- }
133
- // Imports are the same, just update timestamp
134
- this.importCache.set(normalized, newEntry);
109
+ const hasChanged = !cached || !this.setsEqual(cached.imports, newImports);
110
+ // Update cache
111
+ this.importCache.set(normalized, { imports: newImports, timestamp: stat.mtimeMs });
112
+ return hasChanged;
113
+ }
114
+ /**
115
+ * Check if two sets have the same elements
116
+ */
117
+ setsEqual(a, b) {
118
+ if (a.size !== b.size)
135
119
  return false;
120
+ for (const item of a) {
121
+ if (!b.has(item))
122
+ return false;
136
123
  }
137
- this.importCache.set(normalized, newEntry);
138
124
  return true;
139
125
  }
140
126
  /**
@@ -139,55 +139,35 @@ export class DependencyChecker {
139
139
  const normalized = path.resolve(filePath);
140
140
 
141
141
  if (!fs.existsSync(normalized)) {
142
- // File deleted, remove from cache
143
142
  this.importCache.delete(normalized);
144
143
  return true;
145
144
  }
146
145
 
147
146
  const stat = fs.statSync(normalized);
148
- const mtime = stat.mtimeMs;
149
-
150
147
  const cached = this.importCache.get(normalized);
151
148
 
152
- // If timestamp hasn't changed, imports haven't changed
153
- if (cached && cached.timestamp >= mtime) {
149
+ // Fast path: timestamp hasn't changed
150
+ if (cached && cached.timestamp >= stat.mtimeMs) {
154
151
  return false;
155
152
  }
156
153
 
157
- // Parse new imports
154
+ // Compare actual imports
158
155
  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
- }
156
+ const hasChanged = !cached || !this.setsEqual(cached.imports, newImports);
177
157
 
178
- for (const imp of cached.imports) {
179
- if (!newImports.has(imp)) {
180
- this.importCache.set(normalized, newEntry);
181
- return true;
182
- }
183
- }
158
+ // Update cache
159
+ this.importCache.set(normalized, { imports: newImports, timestamp: stat.mtimeMs });
160
+ return hasChanged;
161
+ }
184
162
 
185
- // Imports are the same, just update timestamp
186
- this.importCache.set(normalized, newEntry);
187
- return false;
163
+ /**
164
+ * Check if two sets have the same elements
165
+ */
166
+ private setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
167
+ if (a.size !== b.size) return false;
168
+ for (const item of a) {
169
+ if (!b.has(item)) return false;
188
170
  }
189
-
190
- this.importCache.set(normalized, newEntry);
191
171
  return true;
192
172
  }
193
173
 
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Scanner functions for fix.ts
3
+ *
4
+ * Each scanner encapsulates one type of issue detection:
5
+ * - scanDuplicates: JSCPD duplicate code detection
6
+ * - scanDeadCode: Knip dead code detection
7
+ * - scanAstGrep: Structural linting via ast-grep
8
+ * - scanBiome: Remaining Biome lint issues
9
+ * - scanSlop: AI slop indicators (high complexity patterns)
10
+ */
11
+ import * as childProcess from "node:child_process";
12
+ import * as nodeFs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { shouldIgnoreFile } from "./scan-utils.js";
15
+ const DEBUG_LOG = path.join(process.env.HOME || process.env.USERPROFILE || ".", "pi-lens-debug.log");
16
+ function dbg(msg) {
17
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
18
+ try {
19
+ nodeFs.appendFileSync(DEBUG_LOG, line);
20
+ }
21
+ catch {
22
+ // Ignored
23
+ }
24
+ }
25
+ /**
26
+ * Scan for duplicate code blocks using JSCPD
27
+ */
28
+ export function scanDuplicates(jscpd, targetPath, isTsProject) {
29
+ if (!jscpd.isAvailable())
30
+ return [];
31
+ const jscpdResult = jscpd.scan(targetPath);
32
+ return jscpdResult.clones.filter((c) => {
33
+ if (isTsProject && (c.fileA.endsWith(".js") || c.fileB.endsWith(".js")))
34
+ return false;
35
+ return path.resolve(c.fileA) !== path.resolve(c.fileB);
36
+ });
37
+ }
38
+ /**
39
+ * Scan for dead code using Knip
40
+ */
41
+ export function scanDeadCode(knip, targetPath, isTsProject) {
42
+ if (!knip.isAvailable())
43
+ return [];
44
+ const knipResult = knip.analyze(targetPath);
45
+ return knipResult.issues.filter((i) => {
46
+ if (!i.file)
47
+ return true;
48
+ return !shouldIgnoreFile(i.file, isTsProject);
49
+ });
50
+ }
51
+ /**
52
+ * Scan for structural issues using ast-grep
53
+ */
54
+ export function scanAstGrep(targetPath, isTsProject, configPath) {
55
+ const hasSg = nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
56
+ childProcess.spawnSync("npx", ["sg", "--version"], { encoding: "utf-8", timeout: 5000, shell: true }).status === 0;
57
+ if (!hasSg)
58
+ return [];
59
+ const result = childProcess.spawnSync("npx", [
60
+ "sg",
61
+ "scan",
62
+ "--config",
63
+ configPath,
64
+ "--json",
65
+ "--globs",
66
+ "!**/*.test.ts",
67
+ "--globs",
68
+ "!**/*.spec.ts",
69
+ "--globs",
70
+ "!**/test-utils.ts",
71
+ "--globs",
72
+ "!**/.pi-lens/**",
73
+ ...(isTsProject ? ["--globs", "!**/*.js"] : []),
74
+ targetPath,
75
+ ], {
76
+ encoding: "utf-8",
77
+ timeout: 30000,
78
+ shell: true,
79
+ maxBuffer: 32 * 1024 * 1024,
80
+ });
81
+ const raw = result.stdout?.trim() ?? "";
82
+ const items = raw.startsWith("[")
83
+ ? (() => {
84
+ try {
85
+ return JSON.parse(raw);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ })()
91
+ : raw.split("\n").flatMap((l) => {
92
+ try {
93
+ return [JSON.parse(l)];
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ });
99
+ const astIssues = [];
100
+ for (const item of items) {
101
+ const rule = item.ruleId || item.rule?.title || item.name || "unknown";
102
+ const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
103
+ 1;
104
+ const relFile = path.relative(targetPath, item.file ?? "").replace(/\\/g, "/");
105
+ if (shouldIgnoreFile(relFile, isTsProject))
106
+ continue;
107
+ astIssues.push({
108
+ rule,
109
+ file: relFile,
110
+ line,
111
+ message: item.message ?? rule,
112
+ });
113
+ }
114
+ return astIssues;
115
+ }
116
+ /**
117
+ * Scan for remaining Biome lint issues (couldn't be auto-fixed)
118
+ */
119
+ export function scanBiomeIssues(biome, targetPath) {
120
+ if (!biome.isAvailable())
121
+ return [];
122
+ const checkResult = childProcess.spawnSync("npx", ["@biomejs/biome", "check", "--reporter=json", "--max-diagnostics=50", targetPath], { encoding: "utf-8", timeout: 20000, shell: true });
123
+ const remainingBiome = [];
124
+ try {
125
+ const data = JSON.parse(checkResult.stdout ?? "{}");
126
+ for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
127
+ if (!diag.category?.startsWith("lint/"))
128
+ continue;
129
+ const filePath = diag.location?.path?.file ?? "";
130
+ const line = diag.location?.span?.start?.line ?? 0;
131
+ const rule = diag.category ?? "lint";
132
+ remainingBiome.push({
133
+ file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
134
+ line: line + 1,
135
+ rule,
136
+ message: diag.message ?? rule,
137
+ });
138
+ }
139
+ }
140
+ catch (e) {
141
+ dbg(`biome lint parse failed: ${e}`);
142
+ }
143
+ return remainingBiome;
144
+ }
145
+ /**
146
+ * Scan for AI slop indicators (high complexity patterns)
147
+ */
148
+ export function scanSlop(complexity, targetPath, isTsProject) {
149
+ const slopFiles = [];
150
+ const scanDir = (dir) => {
151
+ if (!nodeFs.existsSync(dir))
152
+ return;
153
+ for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
154
+ const fullPath = path.join(dir, entry.name);
155
+ if (entry.isDirectory()) {
156
+ if (["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(entry.name))
157
+ continue;
158
+ scanDir(fullPath);
159
+ }
160
+ else if (complexity.isSupportedFile(fullPath)) {
161
+ const metrics = complexity.analyzeFile(fullPath);
162
+ if (metrics) {
163
+ const warnings = complexity
164
+ .checkThresholds(metrics)
165
+ .filter((w) => w.includes("AI-style") ||
166
+ w.includes("try/catch") ||
167
+ w.includes("single-use") ||
168
+ w.includes("Excessive comments"));
169
+ const relFile = path
170
+ .relative(targetPath, fullPath)
171
+ .replace(/\\/g, "/");
172
+ if (shouldIgnoreFile(relFile, isTsProject))
173
+ continue;
174
+ if (warnings.length >= 2) {
175
+ slopFiles.push({ file: relFile, warnings });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ };
181
+ scanDir(targetPath);
182
+ return slopFiles;
183
+ }
184
+ /**
185
+ * Run all scanners and return combined results
186
+ */
187
+ export function scanAll(clients, targetPath, isTsProject, configPath) {
188
+ return {
189
+ duplicates: scanDuplicates(clients.jscpd, targetPath, isTsProject),
190
+ deadCode: scanDeadCode(clients.knip, targetPath, isTsProject),
191
+ astIssues: scanAstGrep(targetPath, isTsProject, configPath),
192
+ biomeIssues: scanBiomeIssues(clients.biome, targetPath),
193
+ slopFiles: scanSlop(clients.complexity, targetPath, isTsProject),
194
+ };
195
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Scanner functions for fix.ts
3
+ *
4
+ * Each scanner encapsulates one type of issue detection:
5
+ * - scanDuplicates: JSCPD duplicate code detection
6
+ * - scanDeadCode: Knip dead code detection
7
+ * - scanAstGrep: Structural linting via ast-grep
8
+ * - scanBiome: Remaining Biome lint issues
9
+ * - scanSlop: AI slop indicators (high complexity patterns)
10
+ */
11
+
12
+ import * as childProcess from "node:child_process";
13
+ import * as nodeFs from "node:fs";
14
+ import * as path from "node:path";
15
+ import type { BiomeClient } from "./biome-client.js";
16
+ import type { ComplexityClient } from "./complexity-client.js";
17
+ import type { JscpdClient } from "./jscpd-client.js";
18
+ import type { KnipClient } from "./knip-client.js";
19
+ import { shouldIgnoreFile } from "./scan-utils.js";
20
+
21
+ export interface DuplicateClone {
22
+ fileA: string;
23
+ fileB: string;
24
+ startA: number;
25
+ startB: number;
26
+ lines: number;
27
+ }
28
+
29
+ export interface DeadCodeIssue {
30
+ type: string;
31
+ name: string;
32
+ file?: string;
33
+ }
34
+
35
+ export interface AstIssue {
36
+ rule: string;
37
+ file: string;
38
+ line: number;
39
+ message: string;
40
+ }
41
+
42
+ export interface BiomeIssue {
43
+ file: string;
44
+ line: number;
45
+ rule: string;
46
+ message: string;
47
+ }
48
+
49
+ export interface SlopFile {
50
+ file: string;
51
+ warnings: string[];
52
+ }
53
+
54
+ export interface FixScanResults {
55
+ duplicates: DuplicateClone[];
56
+ deadCode: DeadCodeIssue[];
57
+ astIssues: AstIssue[];
58
+ biomeIssues: BiomeIssue[];
59
+ slopFiles: SlopFile[];
60
+ }
61
+
62
+ const DEBUG_LOG = path.join(
63
+ process.env.HOME || process.env.USERPROFILE || ".",
64
+ "pi-lens-debug.log",
65
+ );
66
+
67
+ function dbg(msg: string) {
68
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
69
+ try {
70
+ nodeFs.appendFileSync(DEBUG_LOG, line);
71
+ } catch {
72
+ // Ignored
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Scan for duplicate code blocks using JSCPD
78
+ */
79
+ export function scanDuplicates(
80
+ jscpd: JscpdClient,
81
+ targetPath: string,
82
+ isTsProject: boolean,
83
+ ): DuplicateClone[] {
84
+ if (!jscpd.isAvailable()) return [];
85
+
86
+ const jscpdResult = jscpd.scan(targetPath);
87
+ return jscpdResult.clones.filter((c) => {
88
+ if (isTsProject && (c.fileA.endsWith(".js") || c.fileB.endsWith(".js")))
89
+ return false;
90
+ return path.resolve(c.fileA) !== path.resolve(c.fileB);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Scan for dead code using Knip
96
+ */
97
+ export function scanDeadCode(
98
+ knip: KnipClient,
99
+ targetPath: string,
100
+ isTsProject: boolean,
101
+ ): DeadCodeIssue[] {
102
+ if (!knip.isAvailable()) return [];
103
+
104
+ const knipResult = knip.analyze(targetPath);
105
+ return knipResult.issues.filter((i) => {
106
+ if (!i.file) return true;
107
+ return !shouldIgnoreFile(i.file, isTsProject);
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Scan for structural issues using ast-grep
113
+ */
114
+ export function scanAstGrep(
115
+ targetPath: string,
116
+ isTsProject: boolean,
117
+ configPath: string,
118
+ ): AstIssue[] {
119
+ const hasSg = nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
120
+ childProcess.spawnSync("npx", ["sg", "--version"], { encoding: "utf-8", timeout: 5000, shell: true }).status === 0;
121
+
122
+ if (!hasSg) return [];
123
+
124
+ const result = childProcess.spawnSync(
125
+ "npx",
126
+ [
127
+ "sg",
128
+ "scan",
129
+ "--config",
130
+ configPath,
131
+ "--json",
132
+ "--globs",
133
+ "!**/*.test.ts",
134
+ "--globs",
135
+ "!**/*.spec.ts",
136
+ "--globs",
137
+ "!**/test-utils.ts",
138
+ "--globs",
139
+ "!**/.pi-lens/**",
140
+ ...(isTsProject ? ["--globs", "!**/*.js"] : []),
141
+ targetPath,
142
+ ],
143
+ {
144
+ encoding: "utf-8",
145
+ timeout: 30000,
146
+ shell: true,
147
+ maxBuffer: 32 * 1024 * 1024,
148
+ },
149
+ );
150
+
151
+ const raw = result.stdout?.trim() ?? "";
152
+ const items: any[] = raw.startsWith("[")
153
+ ? (() => {
154
+ try {
155
+ return JSON.parse(raw);
156
+ } catch {
157
+ return [];
158
+ }
159
+ })()
160
+ : raw.split("\n").flatMap((l: string) => {
161
+ try {
162
+ return [JSON.parse(l)];
163
+ } catch {
164
+ return [];
165
+ }
166
+ });
167
+
168
+ const astIssues: AstIssue[] = [];
169
+ for (const item of items) {
170
+ const rule = item.ruleId || item.rule?.title || item.name || "unknown";
171
+ const line =
172
+ (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
173
+ 1;
174
+ const relFile = path.relative(targetPath, item.file ?? "").replace(/\\/g, "/");
175
+
176
+ if (shouldIgnoreFile(relFile, isTsProject)) continue;
177
+
178
+ astIssues.push({
179
+ rule,
180
+ file: relFile,
181
+ line,
182
+ message: item.message ?? rule,
183
+ });
184
+ }
185
+
186
+ return astIssues;
187
+ }
188
+
189
+ /**
190
+ * Scan for remaining Biome lint issues (couldn't be auto-fixed)
191
+ */
192
+ export function scanBiomeIssues(
193
+ biome: BiomeClient,
194
+ targetPath: string,
195
+ ): BiomeIssue[] {
196
+ if (!biome.isAvailable()) return [];
197
+
198
+ const checkResult = childProcess.spawnSync(
199
+ "npx",
200
+ ["@biomejs/biome", "check", "--reporter=json", "--max-diagnostics=50", targetPath],
201
+ { encoding: "utf-8", timeout: 20000, shell: true },
202
+ );
203
+
204
+ const remainingBiome: BiomeIssue[] = [];
205
+ try {
206
+ const data = JSON.parse(checkResult.stdout ?? "{}");
207
+ for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
208
+ if (!diag.category?.startsWith("lint/")) continue;
209
+ const filePath = diag.location?.path?.file ?? "";
210
+ const line = diag.location?.span?.start?.line ?? 0;
211
+ const rule = diag.category ?? "lint";
212
+ remainingBiome.push({
213
+ file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
214
+ line: line + 1,
215
+ rule,
216
+ message: diag.message ?? rule,
217
+ });
218
+ }
219
+ } catch (e) {
220
+ dbg(`biome lint parse failed: ${e}`);
221
+ }
222
+
223
+ return remainingBiome;
224
+ }
225
+
226
+ /**
227
+ * Scan for AI slop indicators (high complexity patterns)
228
+ */
229
+ export function scanSlop(
230
+ complexity: ComplexityClient,
231
+ targetPath: string,
232
+ isTsProject: boolean,
233
+ ): SlopFile[] {
234
+ const slopFiles: SlopFile[] = [];
235
+
236
+ const scanDir = (dir: string) => {
237
+ if (!nodeFs.existsSync(dir)) return;
238
+ for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
239
+ const fullPath = path.join(dir, entry.name);
240
+ if (entry.isDirectory()) {
241
+ if (
242
+ ["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(
243
+ entry.name,
244
+ )
245
+ )
246
+ continue;
247
+ scanDir(fullPath);
248
+ } else if (complexity.isSupportedFile(fullPath)) {
249
+ const metrics = complexity.analyzeFile(fullPath);
250
+ if (metrics) {
251
+ const warnings = complexity
252
+ .checkThresholds(metrics)
253
+ .filter(
254
+ (w) =>
255
+ w.includes("AI-style") ||
256
+ w.includes("try/catch") ||
257
+ w.includes("single-use") ||
258
+ w.includes("Excessive comments"),
259
+ );
260
+ const relFile = path
261
+ .relative(targetPath, fullPath)
262
+ .replace(/\\/g, "/");
263
+ if (shouldIgnoreFile(relFile, isTsProject)) continue;
264
+ if (warnings.length >= 2) {
265
+ slopFiles.push({ file: relFile, warnings });
266
+ }
267
+ }
268
+ }
269
+ }
270
+ };
271
+
272
+ scanDir(targetPath);
273
+ return slopFiles;
274
+ }
275
+
276
+ /**
277
+ * Run all scanners and return combined results
278
+ */
279
+ export function scanAll(
280
+ clients: {
281
+ jscpd: JscpdClient;
282
+ knip: KnipClient;
283
+ biome: BiomeClient;
284
+ complexity: ComplexityClient;
285
+ },
286
+ targetPath: string,
287
+ isTsProject: boolean,
288
+ configPath: string,
289
+ ): FixScanResults {
290
+ return {
291
+ duplicates: scanDuplicates(clients.jscpd, targetPath, isTsProject),
292
+ deadCode: scanDeadCode(clients.knip, targetPath, isTsProject),
293
+ astIssues: scanAstGrep(targetPath, isTsProject, configPath),
294
+ biomeIssues: scanBiomeIssues(clients.biome, targetPath),
295
+ slopFiles: scanSlop(clients.complexity, targetPath, isTsProject),
296
+ };
297
+ }