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
@@ -8,11 +8,19 @@
8
8
  * Rules: ./rules/ directory
9
9
  */
10
10
 
11
- import { spawn, spawnSync } from "node:child_process";
11
+ import { spawnSync } from "node:child_process";
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import { AstGrepParser } from "./ast-grep-parser.js";
15
15
  import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
16
+ import { SgRunner, type SgMatch } from "./sg-runner.js";
17
+
18
+ const getExtensionDir = () => {
19
+ if (typeof __dirname !== "undefined") {
20
+ return __dirname;
21
+ }
22
+ return ".";
23
+ };
16
24
 
17
25
  // --- Types ---
18
26
 
@@ -54,19 +62,15 @@ export class AstGrepClient {
54
62
  private ruleDir: string;
55
63
  private log: (msg: string) => void;
56
64
  private ruleManager: AstGrepRuleManager;
65
+ private runner: SgRunner;
57
66
 
58
67
  constructor(ruleDir?: string, verbose = false) {
59
- this.ruleDir =
60
- ruleDir ||
61
- path.join(
62
- typeof __dirname !== "undefined" ? __dirname : ".",
63
- "..",
64
- "rules",
65
- );
68
+ this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
66
69
  this.log = verbose
67
70
  ? (msg: string) => console.error(`[ast-grep] ${msg}`)
68
71
  : () => {};
69
72
  this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
73
+ this.runner = new SgRunner(verbose);
70
74
  }
71
75
 
72
76
  /**
@@ -74,18 +78,10 @@ export class AstGrepClient {
74
78
  */
75
79
  isAvailable(): boolean {
76
80
  if (this.available !== null) return this.available;
77
-
78
- const result = spawnSync("npx", ["sg", "--version"], {
79
- encoding: "utf-8",
80
- timeout: 10000,
81
- shell: true,
82
- });
83
-
84
- this.available = !result.error && result.status === 0;
81
+ this.available = this.runner.isAvailable();
85
82
  if (this.available) {
86
83
  this.log("ast-grep available");
87
84
  }
88
-
89
85
  return this.available;
90
86
  }
91
87
 
@@ -97,7 +93,7 @@ export class AstGrepClient {
97
93
  lang: string,
98
94
  paths: string[],
99
95
  ): Promise<{ matches: AstGrepMatch[]; error?: string }> {
100
- return this.runSg([
96
+ return this.runner.exec([
101
97
  "run",
102
98
  "-p",
103
99
  pattern,
@@ -131,7 +127,7 @@ export class AstGrepClient {
131
127
  if (apply) args.push("--update-all");
132
128
  args.push(...paths);
133
129
 
134
- const result = await this.runSg(args);
130
+ const result = await this.runner.exec(args);
135
131
  return { matches: result.matches, applied: apply, error: result.error };
136
132
  }
137
133
 
@@ -145,44 +141,7 @@ export class AstGrepClient {
145
141
  timeout = 30000,
146
142
  ): any[] {
147
143
  if (!this.isAvailable()) return [];
148
-
149
- const tmpDir = require("node:os").tmpdir();
150
- const ts = Date.now();
151
- const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
152
- const rulesSubdir = path.join(sessionDir, "rules");
153
- const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
154
- const configFile = path.join(sessionDir, ".sgconfig.yml");
155
-
156
- try {
157
- fs.mkdirSync(rulesSubdir, { recursive: true });
158
- fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
159
- fs.writeFileSync(ruleFile, ruleYaml);
160
-
161
- const result = spawnSync(
162
- "npx",
163
- ["sg", "scan", "--config", configFile, "--json", dir],
164
- {
165
- encoding: "utf-8",
166
- timeout,
167
- shell: true,
168
- },
169
- );
170
-
171
- const output = result.stdout || result.stderr || "";
172
- if (!output.trim()) return [];
173
-
174
- const items = JSON.parse(output);
175
- return Array.isArray(items) ? items : [items];
176
- } catch (err) {
177
- void err;
178
- return [];
179
- } finally {
180
- try {
181
- fs.rmSync(sessionDir, { recursive: true, force: true });
182
- } catch (err) {
183
- void err;
184
- }
185
- }
144
+ return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
186
145
  }
187
146
 
188
147
  /**
@@ -215,42 +174,30 @@ message: found
215
174
  pattern: string;
216
175
  functions: Array<{ name: string; file: string; line: number }>;
217
176
  }> {
218
- const normalized = new Map<
219
- string,
220
- Array<{ name: string; file: string; line: number }>
221
- >();
177
+ const grouped = new Map<string, Array<{ name: string; file: string; line: number }>>();
222
178
 
223
179
  for (const item of matches) {
224
- const text = item.text || "";
225
- const nameMatch = text.match(/function\s+(\w+)/);
226
- if (!nameMatch?.[1]) continue;
180
+ const name = this.extractFunctionName(item.text);
181
+ if (!name) continue;
227
182
 
228
- const signature = this.normalizeFunction(text);
183
+ const signature = this.normalizeFunction(item.text);
184
+ const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) + 1;
229
185
 
230
- if (!normalized.has(signature)) {
231
- normalized.set(signature, []);
232
- }
233
-
234
- const line =
235
- item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0;
236
- normalized.get(signature)?.push({
237
- name: nameMatch[1],
238
- file: item.file,
239
- line: line + 1,
240
- });
186
+ const group = grouped.get(signature) ?? [];
187
+ group.push({ name, file: item.file, line });
188
+ grouped.set(signature, group);
241
189
  }
242
190
 
243
- const result_groups: Array<{
244
- pattern: string;
245
- functions: Array<{ name: string; file: string; line: number }>;
246
- }> = [];
247
- for (const [pattern, functions] of normalized) {
248
- if (functions.length > 1) {
249
- result_groups.push({ pattern, functions });
250
- }
251
- }
191
+ return Array.from(grouped.entries())
192
+ .filter(([_, functions]) => functions.length > 1)
193
+ .map(([pattern, functions]) => ({ pattern, functions }));
194
+ }
252
195
 
253
- return result_groups;
196
+ /**
197
+ * Extract function name from match text
198
+ */
199
+ private extractFunctionName(text: string): string | null {
200
+ return text.match(/function\s+(\w+)/)?.[1] ?? null;
254
201
  }
255
202
 
256
203
  private normalizeFunction(text: string): string {
@@ -305,71 +252,8 @@ message: found
305
252
  return exports;
306
253
  }
307
254
 
308
- private runSg(
309
- args: string[],
310
- ): Promise<{ matches: AstGrepMatch[]; error?: string }> {
311
- return new Promise((resolve) => {
312
- const proc = spawn("npx", ["sg", ...args], {
313
- stdio: ["ignore", "pipe", "pipe"],
314
- shell: true,
315
- });
316
- let stdout = "";
317
- let stderr = "";
318
-
319
- proc.stdout.on("data", (data: Buffer) => (stdout += data.toString()));
320
- proc.stderr.on("data", (data: Buffer) => (stderr += data.toString()));
321
-
322
- proc.on("error", (err: Error) => {
323
- if (err.message.includes("ENOENT")) {
324
- resolve({
325
- matches: [],
326
- error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
327
- });
328
- } else {
329
- resolve({ matches: [], error: err.message });
330
- }
331
- });
332
-
333
- proc.on("close", (code: number | null) => {
334
- if (code !== 0 && !stdout.trim()) {
335
- resolve({
336
- matches: [],
337
- error: stderr.includes("No files found")
338
- ? undefined
339
- : stderr.trim() || `Exit code ${code}`,
340
- });
341
- return;
342
- }
343
- if (!stdout.trim()) {
344
- resolve({ matches: [] });
345
- return;
346
- }
347
- try {
348
- const parsed = JSON.parse(stdout);
349
- const matches = Array.isArray(parsed) ? parsed : [parsed];
350
- resolve({ matches });
351
- } catch (err) {
352
- void err;
353
- resolve({ matches: [], error: "Failed to parse output" });
354
- }
355
- });
356
- });
357
- }
358
-
359
255
  formatMatches(matches: AstGrepMatch[], isDryRun = false): string {
360
- if (matches.length === 0) return "No matches found";
361
- const MAX = 50;
362
- const shown = matches.slice(0, MAX);
363
- const lines = shown.map((m) => {
364
- const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
365
- const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
366
- return isDryRun && m.replacement
367
- ? `${loc}\n - ${text}\n + ${m.replacement}`
368
- : `${loc}: ${text}`;
369
- });
370
- if (matches.length > MAX)
371
- lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
372
- return lines.join("\n");
256
+ return this.runner.formatMatches(matches as SgMatch[], isDryRun);
373
257
  }
374
258
 
375
259
  /**
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Auto-loop engine for pi-lens fix and refactor commands.
3
+ *
4
+ * Provides automatic iteration without requiring the user to manually
5
+ * re-run the command each time. Uses pi's event system (agent_end)
6
+ * to trigger the next iteration automatically.
7
+ *
8
+ * IMPORTANT: Must be initialized at extension load time (in index.ts),
9
+ * not lazily when the command is called. Event handlers need to be
10
+ * registered early to catch agent_end events.
11
+ */
12
+ export function createAutoLoop(pi, config) {
13
+ let state = {
14
+ active: false,
15
+ iteration: 0,
16
+ maxIterations: config.maxIterations,
17
+ };
18
+ const updateStatus = (ctx) => {
19
+ if (state.active) {
20
+ ctx.ui.setStatus(`loop-${config.name}`, `${config.name} (${state.iteration + 1}/${state.maxIterations})`);
21
+ }
22
+ else {
23
+ ctx.ui.setStatus(`loop-${config.name}`, undefined);
24
+ }
25
+ };
26
+ const stop = (ctx, reason) => {
27
+ const wasActive = state.active;
28
+ state = { active: false, iteration: 0, maxIterations: config.maxIterations };
29
+ updateStatus(ctx);
30
+ if (wasActive) {
31
+ ctx.ui.notify(`✅ ${config.name} loop ${reason}`, "info");
32
+ }
33
+ };
34
+ const complete = (ctx, reason) => {
35
+ stop(ctx, reason);
36
+ };
37
+ const start = (ctx) => {
38
+ if (state.active) {
39
+ ctx.ui.notify(`${config.name} loop is already running`, "warning");
40
+ return;
41
+ }
42
+ state = {
43
+ active: true,
44
+ iteration: 0,
45
+ maxIterations: config.maxIterations,
46
+ };
47
+ updateStatus(ctx);
48
+ ctx.ui.notify(`🔄 Starting ${config.name} auto-loop (max ${state.maxIterations} iterations)...`, "info");
49
+ };
50
+ const getState = () => ({ ...state });
51
+ // --- Event Handlers (registered at module load time) ---
52
+ // Handle user interruption (any manual input stops the loop)
53
+ pi.on("input", async (event, ctx) => {
54
+ if (!ctx.hasUI)
55
+ return { action: "continue" };
56
+ if (!state.active)
57
+ return { action: "continue" };
58
+ // User typed something manually → stop the auto-loop
59
+ if (event.source === "interactive") {
60
+ stop(ctx, "stopped (user interrupted)");
61
+ }
62
+ return { action: "continue" };
63
+ });
64
+ // Handle end of agent turn → check if we should continue
65
+ pi.on("agent_end", async (event, ctx) => {
66
+ if (!ctx.hasUI)
67
+ return;
68
+ if (!state.active)
69
+ return;
70
+ const assistantMessages = event.messages.filter((m) => m.role === "assistant");
71
+ const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
72
+ if (!lastAssistantMessage) {
73
+ stop(ctx, "stopped (no response)");
74
+ return;
75
+ }
76
+ const textContent = lastAssistantMessage.content
77
+ .filter((c) => c.type === "text")
78
+ .map((c) => c.text)
79
+ .join("\n");
80
+ if (!textContent.trim()) {
81
+ stop(ctx, "stopped (empty response)");
82
+ return;
83
+ }
84
+ // Check for completion patterns (explicit success)
85
+ if (config.completionPatterns) {
86
+ const hasCompletion = config.completionPatterns.some((p) => p.test(textContent));
87
+ if (hasCompletion) {
88
+ complete(ctx, "completed successfully");
89
+ return;
90
+ }
91
+ }
92
+ // Check for exit patterns (could be success or stopped)
93
+ const hasExit = config.exitPatterns.some((p) => p.test(textContent));
94
+ if (hasExit) {
95
+ complete(ctx, "completed - no more work");
96
+ return;
97
+ }
98
+ // Check max iterations
99
+ state.iteration++;
100
+ if (state.iteration >= state.maxIterations) {
101
+ stop(ctx, `stopped (max iterations ${state.maxIterations} reached)`);
102
+ return;
103
+ }
104
+ // Continue to next iteration - send command as follow-up
105
+ updateStatus(ctx);
106
+ const continueMsg = config.continuePrompt || `Run ${config.command} to continue.`;
107
+ pi.sendUserMessage(`🔄 Auto-loop (${state.iteration + 1}/${state.maxIterations}): ${continueMsg}`, { deliverAs: "followUp" });
108
+ });
109
+ return {
110
+ start,
111
+ stop,
112
+ getState,
113
+ setMaxIterations: (n) => {
114
+ state.maxIterations = n;
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Auto-loop engine for pi-lens fix and refactor commands.
3
+ *
4
+ * Provides automatic iteration without requiring the user to manually
5
+ * re-run the command each time. Uses pi's event system (agent_end)
6
+ * to trigger the next iteration automatically.
7
+ *
8
+ * IMPORTANT: Must be initialized at extension load time (in index.ts),
9
+ * not lazily when the command is called. Event handlers need to be
10
+ * registered early to catch agent_end events.
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+
15
+ export interface LoopConfig {
16
+ /** Unique identifier for this loop instance (e.g., "fix", "refactor") */
17
+ name: string;
18
+ /** Maximum iterations before stopping */
19
+ maxIterations: number;
20
+ /** Command to run for each iteration (e.g., "/lens-booboo-fix --loop") */
21
+ command: string;
22
+ /** Patterns that indicate the loop should exit (e.g., "no more fixable issues") */
23
+ exitPatterns: RegExp[];
24
+ /** Patterns that indicate completion with success */
25
+ completionPatterns?: RegExp[];
26
+ /** Additional text to include when prompting for next iteration */
27
+ continuePrompt?: string;
28
+ }
29
+
30
+ export interface LoopState {
31
+ active: boolean;
32
+ iteration: number;
33
+ maxIterations: number;
34
+ }
35
+
36
+ export function createAutoLoop(pi: ExtensionAPI, config: LoopConfig): {
37
+ start: (ctx: ExtensionContext) => void;
38
+ stop: (ctx: ExtensionContext, reason: string) => void;
39
+ getState: () => LoopState;
40
+ setMaxIterations: (n: number) => void;
41
+ } {
42
+ let state: LoopState = {
43
+ active: false,
44
+ iteration: 0,
45
+ maxIterations: config.maxIterations,
46
+ };
47
+
48
+ const updateStatus = (ctx: ExtensionContext) => {
49
+ if (state.active) {
50
+ ctx.ui.setStatus(
51
+ `loop-${config.name}`,
52
+ `${config.name} (${state.iteration + 1}/${state.maxIterations})`,
53
+ );
54
+ } else {
55
+ ctx.ui.setStatus(`loop-${config.name}`, undefined);
56
+ }
57
+ };
58
+
59
+ const stop = (ctx: ExtensionContext, reason: string) => {
60
+ const wasActive = state.active;
61
+ state = { active: false, iteration: 0, maxIterations: config.maxIterations };
62
+ updateStatus(ctx);
63
+ if (wasActive) {
64
+ ctx.ui.notify(`✅ ${config.name} loop ${reason}`, "info");
65
+ }
66
+ };
67
+
68
+ const complete = (ctx: ExtensionContext, reason: string) => {
69
+ stop(ctx, reason);
70
+ };
71
+
72
+ const start = (ctx: ExtensionContext) => {
73
+ if (state.active) {
74
+ ctx.ui.notify(`${config.name} loop is already running`, "warning");
75
+ return;
76
+ }
77
+
78
+ state = {
79
+ active: true,
80
+ iteration: 0,
81
+ maxIterations: config.maxIterations,
82
+ };
83
+
84
+ updateStatus(ctx);
85
+ ctx.ui.notify(
86
+ `🔄 Starting ${config.name} auto-loop (max ${state.maxIterations} iterations)...`,
87
+ "info",
88
+ );
89
+ };
90
+
91
+ const getState = (): LoopState => ({ ...state });
92
+
93
+ // --- Event Handlers (registered at module load time) ---
94
+
95
+ // Handle user interruption (any manual input stops the loop)
96
+ pi.on("input", async (event, ctx) => {
97
+ if (!ctx.hasUI) return { action: "continue" as const };
98
+ if (!state.active) return { action: "continue" as const };
99
+
100
+ // User typed something manually → stop the auto-loop
101
+ if (event.source === "interactive") {
102
+ stop(ctx, "stopped (user interrupted)");
103
+ }
104
+
105
+ return { action: "continue" as const };
106
+ });
107
+
108
+ // Handle end of agent turn → check if we should continue
109
+ pi.on("agent_end", async (event, ctx) => {
110
+ if (!ctx.hasUI) return;
111
+ if (!state.active) return;
112
+
113
+ const assistantMessages = event.messages.filter((m) => m.role === "assistant");
114
+ const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
115
+
116
+ if (!lastAssistantMessage) {
117
+ stop(ctx, "stopped (no response)");
118
+ return;
119
+ }
120
+
121
+ const textContent = lastAssistantMessage.content
122
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
123
+ .map((c) => c.text)
124
+ .join("\n");
125
+
126
+ if (!textContent.trim()) {
127
+ stop(ctx, "stopped (empty response)");
128
+ return;
129
+ }
130
+
131
+ // Check for completion patterns (explicit success)
132
+ if (config.completionPatterns) {
133
+ const hasCompletion = config.completionPatterns.some((p) => p.test(textContent));
134
+ if (hasCompletion) {
135
+ complete(ctx, "completed successfully");
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Check for exit patterns (could be success or stopped)
141
+ const hasExit = config.exitPatterns.some((p) => p.test(textContent));
142
+ if (hasExit) {
143
+ complete(ctx, "completed - no more work");
144
+ return;
145
+ }
146
+
147
+ // Check max iterations
148
+ state.iteration++;
149
+ if (state.iteration >= state.maxIterations) {
150
+ stop(ctx, `stopped (max iterations ${state.maxIterations} reached)`);
151
+ return;
152
+ }
153
+
154
+ // Continue to next iteration - send command as follow-up
155
+ updateStatus(ctx);
156
+ const continueMsg = config.continuePrompt || `Run ${config.command} to continue.`;
157
+ pi.sendUserMessage(
158
+ `🔄 Auto-loop (${state.iteration + 1}/${state.maxIterations}): ${continueMsg}`,
159
+ { deliverAs: "followUp" },
160
+ );
161
+ });
162
+
163
+ return {
164
+ start,
165
+ stop,
166
+ getState,
167
+ setMaxIterations: (n: number) => {
168
+ state.maxIterations = n;
169
+ },
170
+ };
171
+ }
@@ -56,14 +56,24 @@ export class BiomeClient {
56
56
  ".cjs",
57
57
  ].includes(ext);
58
58
  }
59
+ // --- Internal helpers ---
59
60
  /**
60
- * Run biome check (format + lint) without fixing returns diagnostics
61
+ * Validate path and availability returns path or null on failure
61
62
  */
62
- checkFile(filePath) {
63
+ withValidatedPath(filePath) {
63
64
  if (!this.isAvailable())
64
- return [];
65
+ return null;
65
66
  const absolutePath = path.resolve(filePath);
66
67
  if (!fs.existsSync(absolutePath))
68
+ return null;
69
+ return absolutePath;
70
+ }
71
+ /**
72
+ * Run biome check (format + lint) without fixing — returns diagnostics
73
+ */
74
+ checkFile(filePath) {
75
+ const absolutePath = this.withValidatedPath(filePath);
76
+ if (!absolutePath)
67
77
  return [];
68
78
  try {
69
79
  const result = spawnSync("npx", [
@@ -92,11 +102,13 @@ export class BiomeClient {
92
102
  * Format a file (writes to disk)
93
103
  */
94
104
  formatFile(filePath) {
95
- if (!this.isAvailable())
96
- return { success: false, changed: false, error: "Biome not available" };
97
- const absolutePath = path.resolve(filePath);
98
- if (!fs.existsSync(absolutePath))
99
- return { success: false, changed: false, error: "File not found" };
105
+ const absolutePath = this.withValidatedPath(filePath);
106
+ if (!absolutePath)
107
+ return {
108
+ success: false,
109
+ changed: false,
110
+ error: this.isAvailable() ? "File not found" : "Biome not available",
111
+ };
100
112
  const content = fs.readFileSync(absolutePath, "utf-8");
101
113
  try {
102
114
  const result = spawnSync("npx", ["@biomejs/biome", "format", "--write", absolutePath], {
@@ -123,20 +135,13 @@ export class BiomeClient {
123
135
  * Fix both formatting and linting issues (writes to disk)
124
136
  */
125
137
  fixFile(filePath) {
126
- if (!this.isAvailable())
138
+ const absolutePath = this.withValidatedPath(filePath);
139
+ if (!absolutePath)
127
140
  return {
128
141
  success: false,
129
142
  changed: false,
130
143
  fixed: 0,
131
- error: "Biome not available",
132
- };
133
- const absolutePath = path.resolve(filePath);
134
- if (!fs.existsSync(absolutePath))
135
- return {
136
- success: false,
137
- changed: false,
138
- fixed: 0,
139
- error: "File not found",
144
+ error: this.isAvailable() ? "File not found" : "Biome not available",
140
145
  };
141
146
  const content = fs.readFileSync(absolutePath, "utf-8");
142
147
  try {
@@ -211,10 +216,8 @@ export class BiomeClient {
211
216
  * Generate a diff-like summary of formatting changes
212
217
  */
213
218
  getFormatDiff(filePath) {
214
- if (!this.isAvailable())
215
- return "";
216
- const absolutePath = path.resolve(filePath);
217
- if (!fs.existsSync(absolutePath))
219
+ const absolutePath = this.withValidatedPath(filePath);
220
+ if (!absolutePath)
218
221
  return "";
219
222
  const content = fs.readFileSync(absolutePath, "utf-8");
220
223
  try {