pi-lens 3.1.2 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -2
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +83 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +67 -14
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.js +40 -20
- package/clients/lsp/__tests__/client.test.ts +62 -39
- package/clients/lsp/__tests__/launch.test.js +74 -25
- package/clients/lsp/__tests__/launch.test.ts +99 -39
- package/clients/lsp/__tests__/service.test.js +25 -7
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +149 -2
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +146 -129
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +376 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
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 (
|
|
209
|
-
| **ast-grep-napi** | TS/JS | 15 |
|
|
208
|
+
| **tree-sitter** | TS/JS, Python | 14 | Mixed | AST-based structural analysis (21 patterns) |
|
|
209
|
+
| **ast-grep-napi** | TS/JS | 15 | — | **Disabled by default** — causes random crashes in realtime dispatch. Full linter uses CLI ast-grep. |
|
|
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:** `
|
|
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 (
|
|
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 +
|
|
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
|
|
|
@@ -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:**
|
|
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;
|
package/clients/biome-client.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
const
|
|
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
|
|
180
|
+
this.log(`Fixed issue(s) in ${path.basename(filePath)}`);
|
|
162
181
|
}
|
|
163
|
-
return { success: true, changed, fixed:
|
|
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
|
|
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++;
|
package/clients/biome-client.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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
|
-
//
|
|
204
|
-
|
|
205
|
-
const
|
|
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:
|
|
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
|
|
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(
|
|
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;
|
|
@@ -110,10 +110,8 @@ describe("Runner Registration", () => {
|
|
|
110
110
|
"ruff-lint",
|
|
111
111
|
"shellcheck",
|
|
112
112
|
"spellcheck",
|
|
113
|
-
"ast-grep",
|
|
114
113
|
"ast-grep-napi",
|
|
115
114
|
"architect",
|
|
116
|
-
"ast-grep-napi",
|
|
117
115
|
"config-validation",
|
|
118
116
|
];
|
|
119
117
|
it("should have all expected critical runners", () => {
|