pi-lens 3.1.2 → 3.2.0

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 (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [3.2.0] - 2026-04-02
6
+
7
+ ### Fixed
8
+ - **LSP server initialization errors** — Fixed `workspaceFolders` capability format that caused gopls and rust-analyzer to crash with JSON RPC parse errors. Changed from object `{supported: true, changeNotifications: true}` to simple boolean `true` for broader compatibility.
9
+ - **Formatter cwd not passed** — `formatFile` now passes `cwd` to `safeSpawn`, fixing Biome's "nested root configuration" error when formatting files in subdirectories.
10
+ - **LSP runner error handling** — Added try-catch around LSP operations to properly detect and report server spawn/connection failures instead of silently returning empty success.
11
+
12
+ ### Changed
13
+ - **Go/Rust LSP initialization** — Added server-specific initialization options for better compatibility.
14
+
15
+ ---
16
+
17
+ ## [3.1.3] - 2026-04-02
18
+
19
+ ### Fixed
20
+ - **Biome autofix: removed `--unsafe` flag** — `--unsafe` silently deleted unused variables
21
+ and interfaces, removing code the agent was mid-way through writing (e.g. a new interface
22
+ not yet wired up). Only safe fixes (`--write`) are now applied automatically on every write.
23
+ Unsafe fixes require explicit opt-in.
24
+ - **Tree-sitter WASM crash on concurrent writes** — The tree-sitter runner was creating a
25
+ `new TreeSitterClient()` on every post-write event. Each construction re-invoked
26
+ `Parser.init()` → `C._ts_init()`, which resets the module-level `TRANSFER_BUFFER` pointer
27
+ used by all active WASM operations. Concurrent writes (fast multi-file edits) raced on
28
+ `_ts_init()` and corrupted shared WASM state → process crash. Fixed with a module-level
29
+ singleton (`getSharedClient()`). Also fixes the secondary bug where each fresh client had
30
+ an empty internal `queryLoader`, making the tree-sitter runner a silent no-op.
31
+ - **`blockingOnly` missing in bus/effect dispatchers** — `dispatchLintWithBus` and
32
+ `dispatchLintWithEffect` were not passing `blockingOnly: true` to `createDispatchContext`,
33
+ causing warning-level runners to execute on every write when `--lens-bus` or `--lens-effect`
34
+ was active. Now consistent with the standard `dispatchLint` behaviour.
35
+ - **Async `when` condition silently ignored in bus dispatcher** — `dispatchConcurrent` was
36
+ filtering runners with `.filter(r => r.when ? r.when(ctx) : true)`. Since `r.when(ctx)`
37
+ returns `Promise<boolean>`, a truthy promise object was always passing the filter regardless
38
+ of the actual condition. The check is now awaited properly inside `runRunner()`.
39
+
40
+ ### Performance
41
+ - **Biome: local binary instead of npx** — `BiomeClient` now resolves
42
+ `node_modules/.bin/biome.cmd` (Windows) or `node_modules/.bin/biome` before falling back
43
+ to `npx @biomejs/biome`. Eliminates ~1 s npx startup overhead per invocation.
44
+ Result: `checkFile` 1029 ms → **176 ms**, `fixFile` 2012 ms → **158 ms**.
45
+ - **Biome: eliminated redundant pre-flight `checkFile` in `fixFile`** — `fixFile` was calling
46
+ `checkFile` (a full `biome check --reporter=json`) solely to count fixable issues for
47
+ logging, then running `biome check --write` anyway. The count is now derived from the
48
+ content diff (`changed ? 1 : 0`), saving one full biome invocation per write.
49
+ Combined with the format phase, biome now runs at most **2×** per write (format + fix)
50
+ instead of 3×.
51
+ - **TypeScript pre-write check: halved `getSemanticDiagnostics` calls** — `getAllCodeFixes()`
52
+ was calling `getDiagnostics()` internally, but `index.ts` also called `getDiagnostics()`
53
+ immediately before it — running the full TypeScript semantic analysis twice per pre-write
54
+ event (~1.2 s each on a 1700-line file). `getAllCodeFixes` now accepts an optional
55
+ `precomputedDiags` parameter; `index.ts` passes the already-computed result.
56
+ `ts_pre_check` latency: ~2400 ms → **~1200 ms**.
57
+
58
+ ---
59
+
5
60
  ## [3.1.1] - 2026-04-01
6
61
 
7
62
  ### Added
package/README.md CHANGED
@@ -205,8 +205,8 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
205
205
  | **biome** | TS/JS | 10 | Warning | Linting issues (delta-tracked) |
206
206
  | **ruff** | Python | 10 | Warning | Python linting (delta-tracked) |
207
207
  | **oxlint** | TS/JS | 12 | Warning | Fast Rust-based JS/TS linter |
208
- | **tree-sitter** | TS/JS, Python | 14 | Mixed | AST-based structural analysis (17 patterns) |
209
- | **ast-grep-napi** | TS/JS | 15 | Warning | **Unified structural analysis** (104 rules) ⚠️ Temporarily disabled for debugging |
208
+ | **tree-sitter** | TS/JS, Python | 14 | Mixed | AST-based structural analysis (21 patterns) — **singleton WASM client** |
209
+ | **ast-grep-napi** | TS/JS | 15 | | **Disabled by default** — heavy; use `/lens-booboo` for full analysis |
210
210
  | **type-safety** | TS | 20 | Mixed | Switch exhaustiveness (blocking), other (warning) |
211
211
  | **shellcheck** | Shell | 20 | Warning | Bash/sh/zsh/fish linting |
212
212
  | **python-slop** | Python | 25 | Warning | AI slop detection (~40 patterns) |
@@ -228,13 +228,17 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
228
228
  - **Warning** — Shown in `/lens-booboo`, not inline (noise reduction)
229
229
  - **Silent** — Tracked in metrics only, never shown
230
230
 
231
- **Consolidated runners:** `ast-grep` (CLI) and `ts-slop` merged into `ast-grep-napi` — unified 104-rule set
231
+ **Consolidated runners:** `ts-slop` merged into `ast-grep-napi` (disabled by default) CLI ast-grep used for full linter only
232
232
 
233
233
  **Tree-sitter runner patterns** (priority 14, AST-based structural analysis):
234
234
 
235
- TypeScript/JavaScript (12 patterns):
235
+ TypeScript/JavaScript (13 patterns):
236
236
  - 🔴 **Error**: empty-catch, hardcoded-secrets, eval
237
- - 🟡 **Warning**: debugger, await-in-loop, console-statement, long-parameter-list, nested-ternary, deep-promise-chain, mixed-async-styles, deep-nesting
237
+ - 🟡 **Warning**: debugger, await-in-loop, console-statement, long-parameter-list, nested-ternary, deep-promise-chain, mixed-async-styles, deep-nesting, constructor-super, no-dupe-class-members
238
+
239
+ TSX (2 patterns):
240
+ - 🔴 **Error**: dangerously-set-inner-html
241
+ - 🟡 **Warning**: no-nested-links
238
242
 
239
243
  Python (6 patterns):
240
244
  - 🔴 **Error**: bare-except, mutable-default-arg, eval-exec, unreachable-except
@@ -244,7 +248,7 @@ Python (6 patterns):
244
248
 
245
249
  **AI Slop Detection:**
246
250
  - `python-slop` runner (priority 25): ~40 patterns for Python code quality
247
- - `ast-grep-napi` runner (priority 15): 33 slop patterns + 71 security/architecture rules for TypeScript/JavaScript
251
+ - `ast-grep-napi` runner (priority 15): 33 slop patterns + 68 security/architecture rules for TypeScript/JavaScript (disabled by default — use `/lens-booboo` for full ast-grep analysis via CLI)
248
252
 
249
253
  ---
250
254
 
@@ -429,7 +433,7 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
429
433
  | `knip` | `npm i -D knip` | Dead code / unused exports |
430
434
  | `jscpd` | `npm i -D jscpd` | Copy-paste detection |
431
435
  | `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage % |
432
- | `@ast-grep/napi` | `npm i -D @ast-grep/napi` | Fast structural analysis (TS/JS) |
436
+ | `@ast-grep/napi` | `npm i -D @ast-grep/napi` | Fast structural analysis (TS/JS) — currently disabled in realtime |
433
437
  | `@ast-grep/cli` | `npm i -D @ast-grep/cli` | Structural pattern matching (all languages) |
434
438
  | `typos-cli` | `cargo install typos-cli` | Spellcheck for Markdown |
435
439
 
@@ -487,7 +491,7 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
487
491
  | `--lens-effect` | Run all runners **concurrently** (faster) instead of sequentially (Experimental) |
488
492
  | `--lens-verbose` | Enable detailed console logging |
489
493
  | `--no-autoformat` | Disable automatic formatting (formatting is **enabled by default**) |
490
- | `--no-autofix` | Disable all auto-fixing (Biome + Ruff autofix is **enabled by default**) |
494
+ | `--no-autofix` | Disable all auto-fixing (Biome safe fixes + Ruff autofix **enabled by default**). Unsafe fixes (e.g. removing unused vars) are never applied automatically — use `/lens-booboo` with explicit confirmation. |
491
495
  | `--no-autofix-biome` | Disable Biome auto-fix only |
492
496
  | `--no-autofix-ruff` | Disable Ruff auto-fix only |
493
497
  | `--no-oxlint` | Skip Oxlint linting |
@@ -640,7 +644,7 @@ Tracks which files were edited in the current agent turn for:
640
644
 
641
645
  | Runner | Cache | Notes |
642
646
  |--------|-------|-------|
643
- | `ast-grep-napi` | Rule descriptions | Loaded once per session |
647
+ | `ast-grep-napi` | Rule descriptions | Loaded once per session (disabled by default) |
644
648
  | `biome` | Tool availability | Checked once, cached |
645
649
  | `pyright` | Command path | Venv lookup cached |
646
650
  | `ruff` | Command path | Venv lookup cached |
@@ -659,7 +663,7 @@ pi-lens/
659
663
  │ ├── dispatch/ # Dispatcher and runners
660
664
  │ │ ├── dispatcher.ts
661
665
  │ │ └── runners/ # Individual runners
662
- │ │ ├── ast-grep-napi.ts # Fast TS/JS runner
666
+ │ │ ├── ast-grep-napi.ts # Fast TS/JS runner (disabled by default)
663
667
  │ │ ├── python-slop.ts # Python slop detection
664
668
  │ │ ├── ts-lsp.ts # TS type checking
665
669
  │ │ ├── biome.ts
@@ -702,8 +706,8 @@ See [CHANGELOG.md](CHANGELOG.md) for full history.
702
706
 
703
707
  - **LSP Support:** 31 Language Server Protocol clients (4 core auto-installed, others via npx or manual)
704
708
  - **Concurrent Execution:** Effect-TS-based parallel runner execution with `--lens-effect`
705
- - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms)
706
- - **Slop Detection:** 30+ TypeScript and 40+ Python patterns for AI-generated code quality issues
709
+ - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms) — currently disabled in realtime due to stability
710
+ - **Slop Detection:** 33+ TypeScript and 40+ Python patterns for AI-generated code quality issues
707
711
 
708
712
  ---
709
713
 
@@ -31,7 +31,14 @@ export class AstGrepClient {
31
31
  this.runner = new SgRunner(verbose);
32
32
  }
33
33
  /**
34
- * Check if ast-grep CLI is available
34
+ * Check if ast-grep CLI is available, auto-install if not
35
+ */
36
+ async ensureAvailable() {
37
+ return this.runner.ensureAvailable();
38
+ }
39
+ /**
40
+ * Check if ast-grep CLI is available (legacy sync method)
41
+ * Prefer ensureAvailable() for auto-install behavior
35
42
  */
36
43
  isAvailable() {
37
44
  if (this.available !== null)
@@ -47,7 +47,15 @@ export class AstGrepClient {
47
47
  }
48
48
 
49
49
  /**
50
- * Check if ast-grep CLI is available
50
+ * Check if ast-grep CLI is available, auto-install if not
51
+ */
52
+ async ensureAvailable(): Promise<boolean> {
53
+ return this.runner.ensureAvailable();
54
+ }
55
+
56
+ /**
57
+ * Check if ast-grep CLI is available (legacy sync method)
58
+ * Prefer ensureAvailable() for auto-install behavior
51
59
  */
52
60
  isAvailable(): boolean {
53
61
  if (this.available !== null) return this.available;
@@ -15,20 +15,54 @@ import { safeSpawn } from "./safe-spawn.js";
15
15
  export class BiomeClient {
16
16
  constructor(verbose = false) {
17
17
  this.biomeAvailable = null;
18
+ this.localBinaryPath = null;
18
19
  this.log = verbose
19
20
  ? (msg) => console.error(`[biome] ${msg}`)
20
21
  : () => { };
21
22
  }
23
+ /**
24
+ * Resolve the fastest available biome binary.
25
+ * Prefers local node_modules/.bin/biome (skip npx overhead ~1s).
26
+ * Falls back to global biome, then npx.
27
+ */
28
+ getBiomeBinary() {
29
+ if (this.localBinaryPath)
30
+ return { cmd: this.localBinaryPath, args: [] };
31
+ // Walk up from cwd looking for node_modules/.bin/biome.
32
+ // On Windows prefer .cmd (native batch) over the sh wrapper — 2x faster.
33
+ const isWin = process.platform === "win32";
34
+ const candidates = isWin
35
+ ? [
36
+ path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
37
+ path.join(process.cwd(), "node_modules", ".bin", "biome"),
38
+ ]
39
+ : [
40
+ path.join(process.cwd(), "node_modules", ".bin", "biome"),
41
+ path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
42
+ ];
43
+ for (const p of candidates) {
44
+ if (fs.existsSync(p)) {
45
+ this.localBinaryPath = p;
46
+ return { cmd: p, args: [] };
47
+ }
48
+ }
49
+ // Fallback: npx (slower but works anywhere)
50
+ return { cmd: "npx", args: ["@biomejs/biome"] };
51
+ }
52
+ /**
53
+ * Spawn biome with the fastest available binary.
54
+ */
55
+ spawnBiome(args, timeout = 15000) {
56
+ const { cmd, args: prefix } = this.getBiomeBinary();
57
+ return safeSpawn(cmd, [...prefix, ...args], { timeout });
58
+ }
22
59
  /**
23
60
  * Check if biome CLI is available
24
61
  */
25
62
  isAvailable() {
26
63
  if (this.biomeAvailable !== null)
27
64
  return this.biomeAvailable;
28
- // Try npx biome first (works without global install)
29
- const result = safeSpawn("npx", ["@biomejs/biome", "--version"], {
30
- timeout: 10000,
31
- });
65
+ const result = this.spawnBiome(["--version"], 10000);
32
66
  this.biomeAvailable = !result.error && result.status === 0;
33
67
  if (this.biomeAvailable) {
34
68
  const version = result.stdout?.trim() || "unknown";
@@ -65,15 +99,12 @@ export class BiomeClient {
65
99
  if (!absolutePath)
66
100
  return [];
67
101
  try {
68
- const result = safeSpawn("npx", [
69
- "@biomejs/biome",
102
+ const result = this.spawnBiome([
70
103
  "check",
71
104
  "--reporter=json",
72
105
  "--max-diagnostics=50",
73
106
  absolutePath,
74
- ], {
75
- timeout: 15000,
76
- });
107
+ ]);
77
108
  // Biome exits 0 on success, 1 on issues found
78
109
  const output = result.stdout || "";
79
110
  if (!output.trim())
@@ -98,9 +129,7 @@ export class BiomeClient {
98
129
  };
99
130
  const content = fs.readFileSync(absolutePath, "utf-8");
100
131
  try {
101
- const result = safeSpawn("npx", ["@biomejs/biome", "format", "--write", absolutePath], {
102
- timeout: 15000,
103
- });
132
+ const result = this.spawnBiome(["format", "--write", absolutePath]);
104
133
  if (result.error) {
105
134
  return { success: false, changed: false, error: result.error.message };
106
135
  }
@@ -134,19 +163,9 @@ export class BiomeClient {
134
163
  };
135
164
  const content = fs.readFileSync(absolutePath, "utf-8");
136
165
  try {
137
- // First, count issues before fixing
138
- const beforeDiags = this.checkFile(filePath);
139
- const fixableCount = beforeDiags.filter((d) => d.fixable).length;
140
- // Apply fixes
141
- const result = safeSpawn("npx", [
142
- "@biomejs/biome",
143
- "check",
144
- "--write",
145
- "--unsafe", // Apply unsafe fixes too
146
- absolutePath,
147
- ], {
148
- timeout: 15000,
149
- });
166
+ // Single invocation: check --write applies safe formatting + lint fixes.
167
+ // No pre-flight checkFile() needed — content diff tells us if anything changed.
168
+ const result = this.spawnBiome(["check", "--write", absolutePath]);
150
169
  if (result.error) {
151
170
  return {
152
171
  success: false,
@@ -158,9 +177,9 @@ export class BiomeClient {
158
177
  const fixed = fs.readFileSync(absolutePath, "utf-8");
159
178
  const changed = content !== fixed;
160
179
  if (changed) {
161
- this.log(`Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`);
180
+ this.log(`Fixed issue(s) in ${path.basename(filePath)}`);
162
181
  }
163
- return { success: true, changed, fixed: fixableCount };
182
+ return { success: true, changed, fixed: changed ? 1 : 0 };
164
183
  }
165
184
  catch (err) {
166
185
  return {
@@ -185,8 +204,8 @@ export class BiomeClient {
185
204
  }
186
205
  // Filter to existing files
187
206
  const validFiles = filePaths
188
- .map(f => path.resolve(f))
189
- .filter(f => fs.existsSync(f));
207
+ .map((f) => path.resolve(f))
208
+ .filter((f) => fs.existsSync(f));
190
209
  if (validFiles.length === 0) {
191
210
  return { success: true, fixed: 0, changed: 0 };
192
211
  }
@@ -195,16 +214,10 @@ export class BiomeClient {
195
214
  let totalFixable = 0;
196
215
  for (const file of validFiles) {
197
216
  const diags = this.checkFile(file);
198
- totalFixable += diags.filter(d => d.fixable).length;
217
+ totalFixable += diags.filter((d) => d.fixable).length;
199
218
  }
200
219
  // Run biome once on all files - much faster than npx per file
201
- const result = safeSpawn("npx", [
202
- "@biomejs/biome",
203
- "check",
204
- "--write",
205
- "--unsafe",
206
- ...validFiles,
207
- ], {
220
+ const result = safeSpawn("npx", ["@biomejs/biome", "check", "--write", "--unsafe", ...validFiles], {
208
221
  timeout: 60000, // Longer timeout for batch
209
222
  });
210
223
  if (result.error) {
@@ -217,7 +230,7 @@ export class BiomeClient {
217
230
  }
218
231
  // Count how many files actually changed
219
232
  let changedCount = 0;
220
- for (const file of validFiles) {
233
+ for (const _file of validFiles) {
221
234
  // We don't know exactly which files changed without re-reading,
222
235
  // so we report total files processed
223
236
  changedCount++;
@@ -42,6 +42,7 @@ interface BiomeJsonDiagnostic {
42
42
 
43
43
  export class BiomeClient {
44
44
  private biomeAvailable: boolean | null = null;
45
+ private localBinaryPath: string | null = null;
45
46
  private log: (msg: string) => void;
46
47
 
47
48
  constructor(verbose = false) {
@@ -50,16 +51,51 @@ export class BiomeClient {
50
51
  : () => {};
51
52
  }
52
53
 
54
+ /**
55
+ * Resolve the fastest available biome binary.
56
+ * Prefers local node_modules/.bin/biome (skip npx overhead ~1s).
57
+ * Falls back to global biome, then npx.
58
+ */
59
+ private getBiomeBinary(): { cmd: string; args: string[] } {
60
+ if (this.localBinaryPath) return { cmd: this.localBinaryPath, args: [] };
61
+
62
+ // Walk up from cwd looking for node_modules/.bin/biome.
63
+ // On Windows prefer .cmd (native batch) over the sh wrapper — 2x faster.
64
+ const isWin = process.platform === "win32";
65
+ const candidates = isWin
66
+ ? [
67
+ path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
68
+ path.join(process.cwd(), "node_modules", ".bin", "biome"),
69
+ ]
70
+ : [
71
+ path.join(process.cwd(), "node_modules", ".bin", "biome"),
72
+ path.join(process.cwd(), "node_modules", ".bin", "biome.cmd"),
73
+ ];
74
+ for (const p of candidates) {
75
+ if (fs.existsSync(p)) {
76
+ this.localBinaryPath = p;
77
+ return { cmd: p, args: [] };
78
+ }
79
+ }
80
+ // Fallback: npx (slower but works anywhere)
81
+ return { cmd: "npx", args: ["@biomejs/biome"] };
82
+ }
83
+
84
+ /**
85
+ * Spawn biome with the fastest available binary.
86
+ */
87
+ private spawnBiome(args: string[], timeout = 15000) {
88
+ const { cmd, args: prefix } = this.getBiomeBinary();
89
+ return safeSpawn(cmd, [...prefix, ...args], { timeout });
90
+ }
91
+
53
92
  /**
54
93
  * Check if biome CLI is available
55
94
  */
56
95
  isAvailable(): boolean {
57
96
  if (this.biomeAvailable !== null) return this.biomeAvailable;
58
97
 
59
- // Try npx biome first (works without global install)
60
- const result = safeSpawn("npx", ["@biomejs/biome", "--version"], {
61
- timeout: 10000,
62
- });
98
+ const result = this.spawnBiome(["--version"], 10000);
63
99
 
64
100
  this.biomeAvailable = !result.error && result.status === 0;
65
101
  if (this.biomeAvailable) {
@@ -103,19 +139,12 @@ export class BiomeClient {
103
139
  if (!absolutePath) return [];
104
140
 
105
141
  try {
106
- const result = safeSpawn(
107
- "npx",
108
- [
109
- "@biomejs/biome",
110
- "check",
111
- "--reporter=json",
112
- "--max-diagnostics=50",
113
- absolutePath,
114
- ],
115
- {
116
- timeout: 15000,
117
- },
118
- );
142
+ const result = this.spawnBiome([
143
+ "check",
144
+ "--reporter=json",
145
+ "--max-diagnostics=50",
146
+ absolutePath,
147
+ ]);
119
148
 
120
149
  // Biome exits 0 on success, 1 on issues found
121
150
  const output = result.stdout || "";
@@ -149,13 +178,7 @@ export class BiomeClient {
149
178
  const content = fs.readFileSync(absolutePath, "utf-8");
150
179
 
151
180
  try {
152
- const result = safeSpawn(
153
- "npx",
154
- ["@biomejs/biome", "format", "--write", absolutePath],
155
- {
156
- timeout: 15000,
157
- },
158
- );
181
+ const result = this.spawnBiome(["format", "--write", absolutePath]);
159
182
 
160
183
  if (result.error) {
161
184
  return { success: false, changed: false, error: result.error.message };
@@ -200,24 +223,9 @@ export class BiomeClient {
200
223
  const content = fs.readFileSync(absolutePath, "utf-8");
201
224
 
202
225
  try {
203
- // First, count issues before fixing
204
- const beforeDiags = this.checkFile(filePath);
205
- const fixableCount = beforeDiags.filter((d) => d.fixable).length;
206
-
207
- // Apply fixes
208
- const result = safeSpawn(
209
- "npx",
210
- [
211
- "@biomejs/biome",
212
- "check",
213
- "--write",
214
- "--unsafe", // Apply unsafe fixes too
215
- absolutePath,
216
- ],
217
- {
218
- timeout: 15000,
219
- },
220
- );
226
+ // Single invocation: check --write applies safe formatting + lint fixes.
227
+ // No pre-flight checkFile() needed — content diff tells us if anything changed.
228
+ const result = this.spawnBiome(["check", "--write", absolutePath]);
221
229
 
222
230
  if (result.error) {
223
231
  return {
@@ -232,12 +240,10 @@ export class BiomeClient {
232
240
  const changed = content !== fixed;
233
241
 
234
242
  if (changed) {
235
- this.log(
236
- `Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`,
237
- );
243
+ this.log(`Fixed issue(s) in ${path.basename(filePath)}`);
238
244
  }
239
245
 
240
- return { success: true, changed, fixed: fixableCount };
246
+ return { success: true, changed, fixed: changed ? 1 : 0 };
241
247
  } catch (err) {
242
248
  return {
243
249
  success: false,
@@ -268,8 +274,8 @@ export class BiomeClient {
268
274
 
269
275
  // Filter to existing files
270
276
  const validFiles = filePaths
271
- .map(f => path.resolve(f))
272
- .filter(f => fs.existsSync(f));
277
+ .map((f) => path.resolve(f))
278
+ .filter((f) => fs.existsSync(f));
273
279
 
274
280
  if (validFiles.length === 0) {
275
281
  return { success: true, fixed: 0, changed: 0 };
@@ -280,19 +286,13 @@ export class BiomeClient {
280
286
  let totalFixable = 0;
281
287
  for (const file of validFiles) {
282
288
  const diags = this.checkFile(file);
283
- totalFixable += diags.filter(d => d.fixable).length;
289
+ totalFixable += diags.filter((d) => d.fixable).length;
284
290
  }
285
291
 
286
292
  // Run biome once on all files - much faster than npx per file
287
293
  const result = safeSpawn(
288
294
  "npx",
289
- [
290
- "@biomejs/biome",
291
- "check",
292
- "--write",
293
- "--unsafe",
294
- ...validFiles,
295
- ],
295
+ ["@biomejs/biome", "check", "--write", "--unsafe", ...validFiles],
296
296
  {
297
297
  timeout: 60000, // Longer timeout for batch
298
298
  },
@@ -309,13 +309,15 @@ export class BiomeClient {
309
309
 
310
310
  // Count how many files actually changed
311
311
  let changedCount = 0;
312
- for (const file of validFiles) {
312
+ for (const _file of validFiles) {
313
313
  // We don't know exactly which files changed without re-reading,
314
314
  // so we report total files processed
315
315
  changedCount++;
316
316
  }
317
317
 
318
- this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
318
+ this.log(
319
+ `Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`,
320
+ );
319
321
 
320
322
  return { success: true, fixed: totalFixable, changed: changedCount };
321
323
  } catch (err) {
@@ -26,7 +26,36 @@ export class DependencyChecker {
26
26
  : () => { };
27
27
  }
28
28
  /**
29
- * Check if madge is available
29
+ * Check if madge is available, auto-install if not
30
+ */
31
+ async ensureAvailable() {
32
+ // Fast path: already checked
33
+ if (this.available !== null)
34
+ return this.available;
35
+ // Check if available in PATH
36
+ const result = safeSpawn("madge", ["--version"], {
37
+ timeout: 5000,
38
+ });
39
+ this.available = !result.error && result.status === 0;
40
+ if (this.available) {
41
+ this.log(`Madge found: ${result.stdout?.trim()}`);
42
+ return true;
43
+ }
44
+ // Auto-install via pi-lens installer
45
+ this.log("Madge not found, attempting auto-install...");
46
+ const { ensureTool } = await import("./installer/index.js");
47
+ const installedPath = await ensureTool("madge");
48
+ if (installedPath) {
49
+ this.log(`Madge auto-installed: ${installedPath}`);
50
+ this.available = true;
51
+ return true;
52
+ }
53
+ this.log("Madge auto-install failed");
54
+ return false;
55
+ }
56
+ /**
57
+ * Check if madge is available (legacy sync method)
58
+ * Prefer ensureAvailable() for auto-install behavior
30
59
  */
31
60
  isAvailable() {
32
61
  if (this.available !== null)
@@ -56,7 +56,41 @@ export class DependencyChecker {
56
56
  }
57
57
 
58
58
  /**
59
- * Check if madge is available
59
+ * Check if madge is available, auto-install if not
60
+ */
61
+ async ensureAvailable(): Promise<boolean> {
62
+ // Fast path: already checked
63
+ if (this.available !== null) return this.available;
64
+
65
+ // Check if available in PATH
66
+ const result = safeSpawn("madge", ["--version"], {
67
+ timeout: 5000,
68
+ });
69
+ this.available = !result.error && result.status === 0;
70
+
71
+ if (this.available) {
72
+ this.log(`Madge found: ${result.stdout?.trim()}`);
73
+ return true;
74
+ }
75
+
76
+ // Auto-install via pi-lens installer
77
+ this.log("Madge not found, attempting auto-install...");
78
+ const { ensureTool } = await import("./installer/index.js");
79
+ const installedPath = await ensureTool("madge");
80
+
81
+ if (installedPath) {
82
+ this.log(`Madge auto-installed: ${installedPath}`);
83
+ this.available = true;
84
+ return true;
85
+ }
86
+
87
+ this.log("Madge auto-install failed");
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * Check if madge is available (legacy sync method)
93
+ * Prefer ensureAvailable() for auto-install behavior
60
94
  */
61
95
  isAvailable(): boolean {
62
96
  if (this.available !== null) return this.available;