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.
Files changed (34) hide show
  1. package/clients/ast-grep-client.test.ts +146 -116
  2. package/clients/ast-grep-client.ts +645 -551
  3. package/clients/biome-client.test.ts +154 -137
  4. package/clients/biome-client.ts +397 -337
  5. package/clients/complexity-client.test.ts +188 -200
  6. package/clients/complexity-client.ts +815 -667
  7. package/clients/dependency-checker.test.ts +55 -55
  8. package/clients/dependency-checker.ts +358 -333
  9. package/clients/go-client.test.ts +121 -111
  10. package/clients/go-client.ts +218 -216
  11. package/clients/jscpd-client.test.ts +132 -132
  12. package/clients/jscpd-client.ts +155 -118
  13. package/clients/knip-client.test.ts +123 -133
  14. package/clients/knip-client.ts +231 -218
  15. package/clients/metrics-client.test.ts +171 -167
  16. package/clients/metrics-client.ts +283 -252
  17. package/clients/ruff-client.test.ts +128 -117
  18. package/clients/ruff-client.ts +300 -269
  19. package/clients/rust-client.test.ts +104 -85
  20. package/clients/rust-client.ts +241 -234
  21. package/clients/subprocess-client.ts +1 -1
  22. package/clients/test-runner-client.test.ts +248 -215
  23. package/clients/test-runner-client.ts +728 -608
  24. package/clients/test-utils.ts +10 -3
  25. package/clients/todo-scanner.test.ts +288 -202
  26. package/clients/todo-scanner.ts +225 -187
  27. package/clients/type-coverage-client.test.ts +119 -119
  28. package/clients/type-coverage-client.ts +142 -115
  29. package/clients/types.ts +28 -28
  30. package/clients/typescript-client.test.ts +99 -93
  31. package/clients/typescript-client.ts +527 -502
  32. package/index.ts +662 -212
  33. package/package.json +1 -1
  34. package/tsconfig.json +12 -12
@@ -9,354 +9,414 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import * as path from "node:path";
13
12
  import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
14
 
15
15
  // --- Types ---
16
16
 
17
17
  export interface BiomeDiagnostic {
18
- line: number;
19
- column: number;
20
- endLine: number;
21
- endColumn: number;
22
- severity: "error" | "warning" | "info" | "hint";
23
- message: string;
24
- rule?: string;
25
- category: "lint" | "format";
26
- fixable: boolean;
18
+ line: number;
19
+ column: number;
20
+ endLine: number;
21
+ endColumn: number;
22
+ severity: "error" | "warning" | "info" | "hint";
23
+ message: string;
24
+ rule?: string;
25
+ category: "lint" | "format";
26
+ fixable: boolean;
27
27
  }
28
28
 
29
29
  interface BiomeJsonDiagnostic {
30
- message: string;
31
- severity: "error" | "warning" | "info" | "hint";
32
- category: string;
33
- span?: {
34
- start: { line: number; column: number };
35
- end: { line: number; column: number };
36
- };
37
- advice?: Array<{ message: string }>;
30
+ message: string;
31
+ severity: "error" | "warning" | "info" | "hint";
32
+ category: string;
33
+ span?: {
34
+ start: { line: number; column: number };
35
+ end: { line: number; column: number };
36
+ };
37
+ advice?: Array<{ message: string }>;
38
38
  }
39
39
 
40
40
  // --- Client ---
41
41
 
42
42
  export class BiomeClient {
43
- private biomeAvailable: boolean | null = null;
44
- private log: (msg: string) => void;
45
-
46
- constructor(verbose = false) {
47
- this.log = verbose
48
- ? (msg: string) => console.log(`[biome] ${msg}`)
49
- : () => {};
50
- }
51
-
52
- /**
53
- * Check if biome CLI is available
54
- */
55
- isAvailable(): boolean {
56
- if (this.biomeAvailable !== null) return this.biomeAvailable;
57
-
58
- // Try npx biome first (works without global install)
59
- const result = spawnSync("npx", ["@biomejs/biome", "--version"], {
60
- encoding: "utf-8",
61
- timeout: 10000,
62
- shell: true,
63
- });
64
-
65
- this.biomeAvailable = !result.error && result.status === 0;
66
- if (this.biomeAvailable) {
67
- const version = result.stdout?.trim() || "unknown";
68
- this.log(`Biome found: ${version}`);
69
- } else {
70
- this.log("Biome not available — install with: npm install -D @biomejs/biome");
71
- }
72
-
73
- return this.biomeAvailable;
74
- }
75
-
76
- /**
77
- * Check if a file is supported by Biome
78
- */
79
- isSupportedFile(filePath: string): boolean {
80
- const ext = path.extname(filePath).toLowerCase();
81
- return [".js", ".jsx", ".ts", ".tsx", ".css", ".json", ".mjs", ".cjs"].includes(ext);
82
- }
83
-
84
- /**
85
- * Run biome check (format + lint) without fixing — returns diagnostics
86
- */
87
- checkFile(filePath: string): BiomeDiagnostic[] {
88
- if (!this.isAvailable()) return [];
89
-
90
- const absolutePath = path.resolve(filePath);
91
- if (!fs.existsSync(absolutePath)) return [];
92
-
93
- try {
94
- const result = spawnSync("npx", [
95
- "@biomejs/biome", "check",
96
- "--reporter=json",
97
- "--max-diagnostics=50",
98
- absolutePath,
99
- ], {
100
- encoding: "utf-8",
101
- timeout: 15000,
102
- shell: true,
103
- });
104
-
105
- // Biome exits 0 on success, 1 on issues found
106
- const output = result.stdout || "";
107
- if (!output.trim()) return [];
108
-
109
- return this.parseDiagnostics(output, absolutePath);
110
- } catch (err: any) {
111
- this.log(`Check error: ${err.message}`);
112
- return [];
113
- }
114
- }
115
-
116
- /**
117
- * Format a file (writes to disk)
118
- */
119
- formatFile(filePath: string): { success: boolean; changed: boolean; error?: string } {
120
- if (!this.isAvailable()) return { success: false, changed: false, error: "Biome not available" };
121
-
122
- const absolutePath = path.resolve(filePath);
123
- if (!fs.existsSync(absolutePath)) return { success: false, changed: false, error: "File not found" };
124
-
125
- const content = fs.readFileSync(absolutePath, "utf-8");
126
-
127
- try {
128
- const result = spawnSync("npx", [
129
- "@biomejs/biome", "format",
130
- "--write",
131
- absolutePath,
132
- ], {
133
- encoding: "utf-8",
134
- timeout: 15000,
135
- shell: true,
136
- });
137
-
138
- if (result.error) {
139
- return { success: false, changed: false, error: result.error.message };
140
- }
141
-
142
- // Re-read to see if changed
143
- const formatted = fs.readFileSync(absolutePath, "utf-8");
144
- const changed = content !== formatted;
145
-
146
- if (changed) {
147
- this.log(`Formatted ${path.basename(filePath)}`);
148
- }
149
-
150
- return { success: true, changed };
151
- } catch (err: any) {
152
- return { success: false, changed: false, error: err.message };
153
- }
154
- }
155
-
156
- /**
157
- * Fix both formatting and linting issues (writes to disk)
158
- */
159
- fixFile(filePath: string): { success: boolean; changed: boolean; fixed: number; error?: string } {
160
- if (!this.isAvailable()) return { success: false, changed: false, fixed: 0, error: "Biome not available" };
161
-
162
- const absolutePath = path.resolve(filePath);
163
- if (!fs.existsSync(absolutePath)) return { success: false, changed: false, fixed: 0, error: "File not found" };
164
-
165
- const content = fs.readFileSync(absolutePath, "utf-8");
166
-
167
- try {
168
- // First, count issues before fixing
169
- const beforeDiags = this.checkFile(filePath);
170
- const fixableCount = beforeDiags.filter(d => d.fixable).length;
171
-
172
- // Apply fixes
173
- const result = spawnSync("npx", [
174
- "@biomejs/biome", "check",
175
- "--write",
176
- "--unsafe", // Apply unsafe fixes too
177
- absolutePath,
178
- ], {
179
- encoding: "utf-8",
180
- timeout: 15000,
181
- shell: true,
182
- });
183
-
184
- if (result.error) {
185
- return { success: false, changed: false, fixed: 0, error: result.error.message };
186
- }
187
-
188
- const fixed = fs.readFileSync(absolutePath, "utf-8");
189
- const changed = content !== fixed;
190
-
191
- if (changed) {
192
- this.log(`Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`);
193
- }
194
-
195
- return { success: true, changed, fixed: fixableCount };
196
- } catch (err: any) {
197
- return { success: false, changed: false, fixed: 0, error: err.message };
198
- }
199
- }
200
-
201
- /**
202
- * Format diagnostics for LLM consumption
203
- */
204
- formatDiagnostics(diags: BiomeDiagnostic[], filename: string): string {
205
- if (diags.length === 0) return "";
206
-
207
- const lintIssues = diags.filter(d => d.category === "lint");
208
- const formatIssues = diags.filter(d => d.category === "format");
209
- const errors = diags.filter(d => d.severity === "error");
210
- const fixable = diags.filter(d => d.fixable);
211
-
212
- let result = `[Biome] ${diags.length} issue(s)`;
213
- if (lintIssues.length) result += ` — ${lintIssues.length} lint`;
214
- if (formatIssues.length) result += ` — ${formatIssues.length} format`;
215
- if (errors.length) result += ` — ${errors.length} error(s)`;
216
- if (fixable.length) result += ` — ${fixable.length} fixable`;
217
- result += ":\n";
218
-
219
- for (const d of diags.slice(0, 15)) {
220
- const loc = d.line === d.endLine
221
- ? `L${d.line}:${d.column}`
222
- : `L${d.line}:${d.column}-L${d.endLine}:${d.endColumn}`;
223
- const rule = d.rule ? ` [${d.rule}]` : "";
224
- const fix = d.fixable ? " ✓" : "";
225
- result += ` ${loc}${rule} ${d.message}${fix}\n`;
226
- }
227
-
228
- if (diags.length > 15) {
229
- result += ` ... and ${diags.length - 15} more\n`;
230
- }
231
-
232
- return result;
233
- }
234
-
235
- /**
236
- * Generate a diff-like summary of formatting changes
237
- */
238
- getFormatDiff(filePath: string): string {
239
- if (!this.isAvailable()) return "";
240
-
241
- const absolutePath = path.resolve(filePath);
242
- if (!fs.existsSync(absolutePath)) return "";
243
-
244
- const content = fs.readFileSync(absolutePath, "utf-8");
245
-
246
- try {
247
- // Get formatted output without writing
248
- const result = spawnSync("npx", [
249
- "@biomejs/biome", "format",
250
- absolutePath,
251
- ], {
252
- encoding: "utf-8",
253
- timeout: 15000,
254
- shell: true,
255
- });
256
-
257
- if (result.error || !result.stdout) return "";
258
-
259
- const formatted = result.stdout;
260
- if (content === formatted) return "";
261
-
262
- return this.computeDiff(content, formatted);
263
- } catch {
264
- return "";
265
- }
266
- }
267
-
268
- // --- Internal ---
269
-
270
- private parseDiagnostics(output: string, filterFile: string): BiomeDiagnostic[] {
271
- try {
272
- // Biome JSON output: {"summary": {...}, "diagnostics": [...], ...}
273
- const result = JSON.parse(output);
274
- const diagnostics: BiomeDiagnostic[] = [];
275
-
276
- const diags = result.diagnostics || [];
277
- const filterPath = path.resolve(filterFile);
278
-
279
- for (const item of diags) {
280
- // Filter to our file
281
- const itemPath = item.location?.path;
282
- if (itemPath && path.resolve(itemPath) !== filterPath) continue;
283
-
284
- const loc = item.location || {};
285
- const start = loc.start || {};
286
- const end = loc.end || start;
287
- const isLint = item.category?.startsWith("lint/") || false;
288
- const isFormat = item.category === "format";
289
- const isAssist = item.category?.startsWith("assist/");
290
-
291
- // Skip non-lint/format diagnostics (like summaries)
292
- if (!isLint && !isFormat && !isAssist) continue;
293
-
294
- // Determine if fixable based on category
295
- const fixable = isFormat || isAssist ||
296
- item.category?.includes("organizeImports") ||
297
- item.message?.includes("fix");
298
-
299
- diagnostics.push({
300
- line: start.line ?? 1,
301
- column: start.column ?? 1,
302
- endLine: end.line ?? start.line ?? 1,
303
- endColumn: end.column ?? start.column ?? 1,
304
- severity: item.severity || "warning",
305
- message: item.message || "Unknown issue",
306
- rule: isLint ? item.category?.replace("lint/", "") : undefined,
307
- category: isLint ? "lint" : "format",
308
- fixable,
309
- });
310
- }
311
-
312
- return diagnostics;
313
- } catch {
314
- this.log("Failed to parse biome JSON output");
315
- return [];
316
- }
317
- }
318
-
319
- private computeDiff(original: string, formatted: string): string {
320
- const origLines = original.split("\n");
321
- const formLines = formatted.split("\n");
322
-
323
- let changedLines = 0;
324
- const changes: string[] = [];
325
-
326
- const maxLen = Math.max(origLines.length, formLines.length);
327
-
328
- for (let i = 0; i < maxLen; i++) {
329
- const orig = origLines[i] ?? "";
330
- const form = formLines[i] ?? "";
331
-
332
- if (orig !== form) {
333
- changedLines++;
334
- if (changes.length < 5) {
335
- if (orig && form) {
336
- changes.push(` L${i + 1}: \`${orig.trim()}\` → \`${form.trim()}\``);
337
- } else if (!form) {
338
- changes.push(` L${i + 1}: remove line`);
339
- } else {
340
- changes.push(` L${i + 1}: add line`);
341
- }
342
- }
343
- }
344
- }
345
-
346
- let result = ` ${changedLines} line(s) would change`;
347
- if (origLines.length !== formLines.length) {
348
- result += ` (${origLines.length} ${formLines.length} lines)`;
349
- }
350
- result += "\n";
351
-
352
- for (const c of changes) {
353
- result += `${c}\n`;
354
- }
355
-
356
- if (changedLines > 5) {
357
- result += ` ... and ${changedLines - 5} more\n`;
358
- }
359
-
360
- return result;
361
- }
43
+ private biomeAvailable: boolean | null = null;
44
+ private log: (msg: string) => void;
45
+
46
+ constructor(verbose = false) {
47
+ this.log = verbose
48
+ ? (msg: string) => console.log(`[biome] ${msg}`)
49
+ : () => {};
50
+ }
51
+
52
+ /**
53
+ * Check if biome CLI is available
54
+ */
55
+ isAvailable(): boolean {
56
+ if (this.biomeAvailable !== null) return this.biomeAvailable;
57
+
58
+ // Try npx biome first (works without global install)
59
+ const result = spawnSync("npx", ["@biomejs/biome", "--version"], {
60
+ encoding: "utf-8",
61
+ timeout: 10000,
62
+ shell: true,
63
+ });
64
+
65
+ this.biomeAvailable = !result.error && result.status === 0;
66
+ if (this.biomeAvailable) {
67
+ const version = result.stdout?.trim() || "unknown";
68
+ this.log(`Biome found: ${version}`);
69
+ } else {
70
+ this.log(
71
+ "Biome not available — install with: npm install -D @biomejs/biome",
72
+ );
73
+ }
74
+
75
+ return this.biomeAvailable;
76
+ }
77
+
78
+ /**
79
+ * Check if a file is supported by Biome
80
+ */
81
+ isSupportedFile(filePath: string): boolean {
82
+ const ext = path.extname(filePath).toLowerCase();
83
+ return [
84
+ ".js",
85
+ ".jsx",
86
+ ".ts",
87
+ ".tsx",
88
+ ".css",
89
+ ".json",
90
+ ".mjs",
91
+ ".cjs",
92
+ ].includes(ext);
93
+ }
94
+
95
+ /**
96
+ * Run biome check (format + lint) without fixing — returns diagnostics
97
+ */
98
+ checkFile(filePath: string): BiomeDiagnostic[] {
99
+ if (!this.isAvailable()) return [];
100
+
101
+ const absolutePath = path.resolve(filePath);
102
+ if (!fs.existsSync(absolutePath)) return [];
103
+
104
+ try {
105
+ const result = spawnSync(
106
+ "npx",
107
+ [
108
+ "@biomejs/biome",
109
+ "check",
110
+ "--reporter=json",
111
+ "--max-diagnostics=50",
112
+ absolutePath,
113
+ ],
114
+ {
115
+ encoding: "utf-8",
116
+ timeout: 15000,
117
+ shell: true,
118
+ },
119
+ );
120
+
121
+ // Biome exits 0 on success, 1 on issues found
122
+ const output = result.stdout || "";
123
+ if (!output.trim()) return [];
124
+
125
+ return this.parseDiagnostics(output, absolutePath);
126
+ } catch (err: any) {
127
+ this.log(`Check error: ${err.message}`);
128
+ return [];
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Format a file (writes to disk)
134
+ */
135
+ formatFile(filePath: string): {
136
+ success: boolean;
137
+ changed: boolean;
138
+ error?: string;
139
+ } {
140
+ if (!this.isAvailable())
141
+ return { success: false, changed: false, error: "Biome not available" };
142
+
143
+ const absolutePath = path.resolve(filePath);
144
+ if (!fs.existsSync(absolutePath))
145
+ return { success: false, changed: false, error: "File not found" };
146
+
147
+ const content = fs.readFileSync(absolutePath, "utf-8");
148
+
149
+ try {
150
+ const result = spawnSync(
151
+ "npx",
152
+ ["@biomejs/biome", "format", "--write", absolutePath],
153
+ {
154
+ encoding: "utf-8",
155
+ timeout: 15000,
156
+ shell: true,
157
+ },
158
+ );
159
+
160
+ if (result.error) {
161
+ return { success: false, changed: false, error: result.error.message };
162
+ }
163
+
164
+ // Re-read to see if changed
165
+ const formatted = fs.readFileSync(absolutePath, "utf-8");
166
+ const changed = content !== formatted;
167
+
168
+ if (changed) {
169
+ this.log(`Formatted ${path.basename(filePath)}`);
170
+ }
171
+
172
+ return { success: true, changed };
173
+ } catch (err: any) {
174
+ return { success: false, changed: false, error: err.message };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Fix both formatting and linting issues (writes to disk)
180
+ */
181
+ fixFile(filePath: string): {
182
+ success: boolean;
183
+ changed: boolean;
184
+ fixed: number;
185
+ error?: string;
186
+ } {
187
+ if (!this.isAvailable())
188
+ return {
189
+ success: false,
190
+ changed: false,
191
+ fixed: 0,
192
+ error: "Biome not available",
193
+ };
194
+
195
+ const absolutePath = path.resolve(filePath);
196
+ if (!fs.existsSync(absolutePath))
197
+ return {
198
+ success: false,
199
+ changed: false,
200
+ fixed: 0,
201
+ error: "File not found",
202
+ };
203
+
204
+ const content = fs.readFileSync(absolutePath, "utf-8");
205
+
206
+ try {
207
+ // First, count issues before fixing
208
+ const beforeDiags = this.checkFile(filePath);
209
+ const fixableCount = beforeDiags.filter((d) => d.fixable).length;
210
+
211
+ // Apply fixes
212
+ const result = spawnSync(
213
+ "npx",
214
+ [
215
+ "@biomejs/biome",
216
+ "check",
217
+ "--write",
218
+ "--unsafe", // Apply unsafe fixes too
219
+ absolutePath,
220
+ ],
221
+ {
222
+ encoding: "utf-8",
223
+ timeout: 15000,
224
+ shell: true,
225
+ },
226
+ );
227
+
228
+ if (result.error) {
229
+ return {
230
+ success: false,
231
+ changed: false,
232
+ fixed: 0,
233
+ error: result.error.message,
234
+ };
235
+ }
236
+
237
+ const fixed = fs.readFileSync(absolutePath, "utf-8");
238
+ const changed = content !== fixed;
239
+
240
+ if (changed) {
241
+ this.log(
242
+ `Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`,
243
+ );
244
+ }
245
+
246
+ return { success: true, changed, fixed: fixableCount };
247
+ } catch (err: any) {
248
+ return { success: false, changed: false, fixed: 0, error: err.message };
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Format diagnostics for LLM consumption
254
+ */
255
+ formatDiagnostics(diags: BiomeDiagnostic[], _filename: string): string {
256
+ if (diags.length === 0) return "";
257
+
258
+ const lintIssues = diags.filter((d) => d.category === "lint");
259
+ const formatIssues = diags.filter((d) => d.category === "format");
260
+ const errors = diags.filter((d) => d.severity === "error");
261
+ const fixable = diags.filter((d) => d.fixable);
262
+
263
+ let result = `[Biome] ${diags.length} issue(s)`;
264
+ if (lintIssues.length) result += ` — ${lintIssues.length} lint`;
265
+ if (formatIssues.length) result += ` — ${formatIssues.length} format`;
266
+ if (errors.length) result += ` — ${errors.length} error(s)`;
267
+ if (fixable.length) result += ` — ${fixable.length} fixable`;
268
+ result += ":\n";
269
+
270
+ for (const d of diags.slice(0, 15)) {
271
+ const loc =
272
+ d.line === d.endLine
273
+ ? `L${d.line}:${d.column}`
274
+ : `L${d.line}:${d.column}-L${d.endLine}:${d.endColumn}`;
275
+ const rule = d.rule ? ` [${d.rule}]` : "";
276
+ const fix = d.fixable ? " ✓" : "";
277
+ result += ` ${loc}${rule} ${d.message}${fix}\n`;
278
+ }
279
+
280
+ if (diags.length > 15) {
281
+ result += ` ... and ${diags.length - 15} more\n`;
282
+ }
283
+
284
+ return result;
285
+ }
286
+
287
+ /**
288
+ * Generate a diff-like summary of formatting changes
289
+ */
290
+ getFormatDiff(filePath: string): string {
291
+ if (!this.isAvailable()) return "";
292
+
293
+ const absolutePath = path.resolve(filePath);
294
+ if (!fs.existsSync(absolutePath)) return "";
295
+
296
+ const content = fs.readFileSync(absolutePath, "utf-8");
297
+
298
+ try {
299
+ // Get formatted output without writing
300
+ const result = spawnSync(
301
+ "npx",
302
+ ["@biomejs/biome", "format", absolutePath],
303
+ {
304
+ encoding: "utf-8",
305
+ timeout: 15000,
306
+ shell: true,
307
+ },
308
+ );
309
+
310
+ if (result.error || !result.stdout) return "";
311
+
312
+ const formatted = result.stdout;
313
+ if (content === formatted) return "";
314
+
315
+ return this.computeDiff(content, formatted);
316
+ } catch {
317
+ return "";
318
+ }
319
+ }
320
+
321
+ // --- Internal ---
322
+
323
+ private parseDiagnostics(
324
+ output: string,
325
+ filterFile: string,
326
+ ): BiomeDiagnostic[] {
327
+ try {
328
+ // Biome JSON output: {"summary": {...}, "diagnostics": [...], ...}
329
+ const result = JSON.parse(output);
330
+ const diagnostics: BiomeDiagnostic[] = [];
331
+
332
+ const diags = result.diagnostics || [];
333
+ const filterPath = path.resolve(filterFile);
334
+
335
+ for (const item of diags) {
336
+ // Filter to our file
337
+ const itemPath = item.location?.path;
338
+ if (itemPath && path.resolve(itemPath) !== filterPath) continue;
339
+
340
+ const loc = item.location || {};
341
+ const start = loc.start || {};
342
+ const end = loc.end || start;
343
+ const isLint = item.category?.startsWith("lint/") || false;
344
+ const isFormat = item.category === "format";
345
+ const isAssist = item.category?.startsWith("assist/");
346
+
347
+ // Skip non-lint/format diagnostics (like summaries)
348
+ if (!isLint && !isFormat && !isAssist) continue;
349
+
350
+ // Determine if fixable based on category
351
+ const fixable =
352
+ isFormat ||
353
+ isAssist ||
354
+ item.category?.includes("organizeImports") ||
355
+ item.message?.includes("fix");
356
+
357
+ diagnostics.push({
358
+ line: start.line ?? 1,
359
+ column: start.column ?? 1,
360
+ endLine: end.line ?? start.line ?? 1,
361
+ endColumn: end.column ?? start.column ?? 1,
362
+ severity: item.severity || "warning",
363
+ message: item.message || "Unknown issue",
364
+ rule: isLint ? item.category?.replace("lint/", "") : undefined,
365
+ category: isLint ? "lint" : "format",
366
+ fixable,
367
+ });
368
+ }
369
+
370
+ return diagnostics;
371
+ } catch {
372
+ this.log("Failed to parse biome JSON output");
373
+ return [];
374
+ }
375
+ }
376
+
377
+ private computeDiff(original: string, formatted: string): string {
378
+ const origLines = original.split("\n");
379
+ const formLines = formatted.split("\n");
380
+
381
+ let changedLines = 0;
382
+ const changes: string[] = [];
383
+
384
+ const maxLen = Math.max(origLines.length, formLines.length);
385
+
386
+ for (let i = 0; i < maxLen; i++) {
387
+ const orig = origLines[i] ?? "";
388
+ const form = formLines[i] ?? "";
389
+
390
+ if (orig !== form) {
391
+ changedLines++;
392
+ if (changes.length < 5) {
393
+ if (orig && form) {
394
+ changes.push(
395
+ ` L${i + 1}: \`${orig.trim()}\` → \`${form.trim()}\``,
396
+ );
397
+ } else if (!form) {
398
+ changes.push(` L${i + 1}: remove line`);
399
+ } else {
400
+ changes.push(` L${i + 1}: add line`);
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ let result = ` ${changedLines} line(s) would change`;
407
+ if (origLines.length !== formLines.length) {
408
+ result += ` (${origLines.length} → ${formLines.length} lines)`;
409
+ }
410
+ result += "\n";
411
+
412
+ for (const c of changes) {
413
+ result += `${c}\n`;
414
+ }
415
+
416
+ if (changedLines > 5) {
417
+ result += ` ... and ${changedLines - 5} more\n`;
418
+ }
419
+
420
+ return result;
421
+ }
362
422
  }