pi-lens 3.6.2 → 3.6.4
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/CHANGELOG.md +10 -2
- package/package.json +4 -4
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/file-time.test.ts +0 -276
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/format-service.test.ts +0 -339
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/__tests__/formatters.test.ts +0 -401
- package/clients/agent-behavior-client.js +0 -110
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/agent-behavior-client.test.ts +0 -116
- package/clients/amain-types.js +0 -164
- package/clients/architect-client.js +0 -291
- package/clients/ast-grep-client.js +0 -253
- package/clients/ast-grep-parser.js +0 -84
- package/clients/ast-grep-rule-manager.js +0 -89
- package/clients/ast-grep-types.js +0 -9
- package/clients/auto-loop.js +0 -131
- package/clients/biome-client.js +0 -420
- package/clients/biome-client.test.js +0 -144
- package/clients/biome-client.test.ts +0 -163
- package/clients/cache/rule-cache.js +0 -72
- package/clients/cache-manager.js +0 -245
- package/clients/cache-manager.test.js +0 -197
- package/clients/cache-manager.test.ts +0 -299
- package/clients/complexity-client.js +0 -675
- package/clients/complexity-client.test.js +0 -234
- package/clients/complexity-client.test.ts +0 -255
- package/clients/config-validator.js +0 -465
- package/clients/dependency-checker.js +0 -325
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dependency-checker.test.ts +0 -71
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
- package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
- package/clients/dispatch/debug.log +0 -1
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.edge.test.ts +0 -100
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.format.test.ts +0 -58
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.inline.test.ts +0 -93
- package/clients/dispatch/dispatcher.js +0 -381
- package/clients/dispatch/dispatcher.test.js +0 -116
- package/clients/dispatch/dispatcher.test.ts +0 -149
- package/clients/dispatch/integration.js +0 -108
- package/clients/dispatch/plan.js +0 -183
- package/clients/dispatch/runners/architect.js +0 -83
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/architect.test.ts +0 -162
- package/clients/dispatch/runners/ast-grep-napi.js +0 -405
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
- package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
- package/clients/dispatch/runners/ast-grep.js +0 -157
- package/clients/dispatch/runners/biome.js +0 -55
- package/clients/dispatch/runners/config-validation.js +0 -67
- package/clients/dispatch/runners/go-vet.js +0 -48
- package/clients/dispatch/runners/index.js +0 -47
- package/clients/dispatch/runners/lsp.js +0 -102
- package/clients/dispatch/runners/oxlint.js +0 -67
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/oxlint.test.ts +0 -303
- package/clients/dispatch/runners/pyright.js +0 -100
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/pyright.test.ts +0 -121
- package/clients/dispatch/runners/python-slop.js +0 -97
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/python-slop.test.ts +0 -298
- package/clients/dispatch/runners/ruff.js +0 -48
- package/clients/dispatch/runners/rust-clippy.js +0 -102
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
- package/clients/dispatch/runners/shellcheck.js +0 -147
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/shellcheck.test.ts +0 -129
- package/clients/dispatch/runners/similarity.js +0 -230
- package/clients/dispatch/runners/spellcheck.js +0 -106
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/spellcheck.test.ts +0 -214
- package/clients/dispatch/runners/tree-sitter.js +0 -246
- package/clients/dispatch/runners/ts-lsp.js +0 -125
- package/clients/dispatch/runners/ts-slop.js +0 -113
- package/clients/dispatch/runners/type-safety.js +0 -142
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
- package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
- package/clients/dispatch/runners/utils.js +0 -51
- package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
- package/clients/dispatch/types.js +0 -16
- package/clients/dispatch/utils/format-utils.js +0 -44
- package/clients/dogfood.test.js +0 -201
- package/clients/dogfood.test.ts +0 -269
- package/clients/file-kinds.js +0 -177
- package/clients/file-kinds.test.js +0 -169
- package/clients/file-kinds.test.ts +0 -210
- package/clients/file-time.js +0 -152
- package/clients/file-utils.js +0 -40
- package/clients/fix-scanners.js +0 -204
- package/clients/format-service.js +0 -184
- package/clients/formatters.js +0 -488
- package/clients/go-client.js +0 -203
- package/clients/go-client.test.js +0 -127
- package/clients/go-client.test.ts +0 -143
- package/clients/installer/index.js +0 -403
- package/clients/interviewer-templates.js +0 -75
- package/clients/interviewer.js +0 -173
- package/clients/jscpd-client.js +0 -196
- package/clients/jscpd-client.test.js +0 -127
- package/clients/jscpd-client.test.ts +0 -145
- package/clients/knip-client.js +0 -239
- package/clients/knip-client.test.js +0 -112
- package/clients/knip-client.test.ts +0 -128
- package/clients/latency-logger.js +0 -40
- package/clients/lsp/__tests__/client.test.js +0 -310
- package/clients/lsp/__tests__/client.test.ts +0 -412
- package/clients/lsp/__tests__/config.test.js +0 -167
- package/clients/lsp/__tests__/config.test.ts +0 -217
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/integration.test.ts +0 -160
- package/clients/lsp/__tests__/launch.test.js +0 -313
- package/clients/lsp/__tests__/launch.test.ts +0 -394
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/server.test.ts +0 -332
- package/clients/lsp/__tests__/service.test.js +0 -438
- package/clients/lsp/__tests__/service.test.ts +0 -530
- package/clients/lsp/client.js +0 -350
- package/clients/lsp/config.js +0 -112
- package/clients/lsp/index.js +0 -318
- package/clients/lsp/installer/index.js +0 -391
- package/clients/lsp/interactive-install.js +0 -221
- package/clients/lsp/language.js +0 -170
- package/clients/lsp/launch.js +0 -329
- package/clients/lsp/lsp/launch.js +0 -116
- package/clients/lsp/lsp/server.js +0 -532
- package/clients/lsp/lsp-index.js +0 -10
- package/clients/lsp/path-utils.js +0 -5
- package/clients/lsp/server.js +0 -725
- package/clients/lsp/test-py-spawn/requirements.txt +0 -1
- package/clients/lsp/test-py-spawn/test.py +0 -3
- package/clients/lsp/test-py-svc/requirements.txt +0 -1
- package/clients/lsp/test-py-svc/test.py +0 -3
- package/clients/lsp/test-python-project/requirements.txt +0 -1
- package/clients/lsp/test-python-project/test.py +0 -5
- package/clients/metrics-client.js +0 -107
- package/clients/metrics-client.test.js +0 -128
- package/clients/metrics-client.test.ts +0 -163
- package/clients/metrics-history.js +0 -367
- package/clients/path-utils.js +0 -142
- package/clients/pipeline.js +0 -272
- package/clients/production-readiness.js +0 -522
- package/clients/project-index.js +0 -255
- package/clients/project-metadata.js +0 -531
- package/clients/ruff-client.js +0 -325
- package/clients/ruff-client.test.js +0 -132
- package/clients/ruff-client.test.ts +0 -153
- package/clients/rules-scanner.js +0 -97
- package/clients/runner-tracker.js +0 -152
- package/clients/rust-client.js +0 -205
- package/clients/rust-client.test.js +0 -108
- package/clients/rust-client.test.ts +0 -130
- package/clients/safe-spawn-async.js +0 -163
- package/clients/safe-spawn.js +0 -241
- package/clients/sanitize.js +0 -291
- package/clients/sanitize.test.js +0 -177
- package/clients/sanitize.test.ts +0 -223
- package/clients/scan-architectural-debt.js +0 -167
- package/clients/scan-utils.js +0 -83
- package/clients/secrets-scanner.js +0 -119
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/secrets-scanner.test.ts +0 -113
- package/clients/sg-runner.js +0 -292
- package/clients/state-matrix.js +0 -160
- package/clients/subprocess-client.js +0 -65
- package/clients/symbol-types.js +0 -5
- package/clients/test-runner-client.js +0 -523
- package/clients/test-runner-client.test.js +0 -192
- package/clients/test-runner-client.test.ts +0 -253
- package/clients/test-utils.js +0 -27
- package/clients/test-utils.ts +0 -36
- package/clients/todo-scanner.js +0 -200
- package/clients/todo-scanner.test.js +0 -301
- package/clients/todo-scanner.test.ts +0 -352
- package/clients/tool-availability.js +0 -207
- package/clients/tree-sitter-client.js +0 -601
- package/clients/tree-sitter-query-loader.js +0 -355
- package/clients/tree-sitter-symbol-extractor.js +0 -289
- package/clients/ts-service.js +0 -129
- package/clients/type-coverage-client.js +0 -127
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/type-coverage-client.test.ts +0 -125
- package/clients/type-safety-client.js +0 -138
- package/clients/types.js +0 -11
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.codefix.test.ts +0 -186
- package/clients/typescript-client.js +0 -509
- package/clients/typescript-client.test.js +0 -105
- package/clients/typescript-client.test.ts +0 -126
- package/commands/booboo.js +0 -1007
- package/commands/fix-from-booboo.js +0 -398
- package/commands/fix-simplified.js +0 -618
- package/commands/rate.js +0 -281
- package/commands/rate.test.js +0 -119
- package/commands/rate.test.ts +0 -131
- package/commands/refactor.js +0 -130
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
export class AstGrepRuleManager {
|
|
4
|
-
constructor(ruleDir, log) {
|
|
5
|
-
this.ruleDir = ruleDir;
|
|
6
|
-
this.log = log;
|
|
7
|
-
this.ruleDescriptions = null;
|
|
8
|
-
}
|
|
9
|
-
loadRuleDescriptions() {
|
|
10
|
-
if (this.ruleDescriptions !== null)
|
|
11
|
-
return this.ruleDescriptions;
|
|
12
|
-
const descriptions = new Map();
|
|
13
|
-
const possiblePaths = [
|
|
14
|
-
path.join(this.ruleDir, "ast-grep-rules", "rules"),
|
|
15
|
-
path.join(this.ruleDir, "rules"),
|
|
16
|
-
this.ruleDir,
|
|
17
|
-
];
|
|
18
|
-
const rulesPath = possiblePaths.find((p) => fs.existsSync(p));
|
|
19
|
-
if (!rulesPath) {
|
|
20
|
-
this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
|
|
21
|
-
this.ruleDescriptions = descriptions;
|
|
22
|
-
return descriptions;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const files = fs.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
|
|
26
|
-
this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
const filePath = path.join(rulesPath, file);
|
|
29
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
30
|
-
const rule = this.parseRuleYaml(content);
|
|
31
|
-
if (rule) {
|
|
32
|
-
descriptions.set(rule.id, rule);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
this.log(`Failed to load rule descriptions: ${err.message}`);
|
|
38
|
-
}
|
|
39
|
-
this.ruleDescriptions = descriptions;
|
|
40
|
-
return descriptions;
|
|
41
|
-
}
|
|
42
|
-
parseRuleYaml(content) {
|
|
43
|
-
const result = {};
|
|
44
|
-
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
45
|
-
if (idMatch)
|
|
46
|
-
result.id = idMatch[1].trim();
|
|
47
|
-
const msgMatch = content.match(/^message:\s*"([^"]+)"/m) ||
|
|
48
|
-
content.match(/^message:\s*'([^']+)'/m) ||
|
|
49
|
-
content.match(/^message:\s*(.+)$/m);
|
|
50
|
-
if (msgMatch)
|
|
51
|
-
result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
|
|
52
|
-
const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
|
|
53
|
-
if (noteMatch) {
|
|
54
|
-
result.note = noteMatch[1]
|
|
55
|
-
.split("\n")
|
|
56
|
-
.map((line) => line.trim())
|
|
57
|
-
.filter((line) => line.length > 0)
|
|
58
|
-
.join(" ");
|
|
59
|
-
}
|
|
60
|
-
const sevMatch = content.match(/^severity:\s*(.+)$/m);
|
|
61
|
-
if (sevMatch)
|
|
62
|
-
result.severity = this.mapSeverity(sevMatch[1].trim());
|
|
63
|
-
const gradeMatch = content.match(/Grade\s+(\d+\.\d+)/i);
|
|
64
|
-
if (gradeMatch)
|
|
65
|
-
result.grade = parseFloat(gradeMatch[1]);
|
|
66
|
-
const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
|
|
67
|
-
if (fixMatch) {
|
|
68
|
-
result.fix = fixMatch[1]
|
|
69
|
-
.split("\n")
|
|
70
|
-
.map((line) => line.replace(/^\s*\|?\s*/, ""))
|
|
71
|
-
.filter((line) => line.length > 0)
|
|
72
|
-
.join("\n");
|
|
73
|
-
}
|
|
74
|
-
if (result.id && result.message) {
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
mapSeverity(severity) {
|
|
80
|
-
const lower = severity.toLowerCase();
|
|
81
|
-
if (lower === "error")
|
|
82
|
-
return "error";
|
|
83
|
-
if (lower === "warning")
|
|
84
|
-
return "warning";
|
|
85
|
-
if (lower === "info")
|
|
86
|
-
return "info";
|
|
87
|
-
return "hint";
|
|
88
|
-
}
|
|
89
|
-
}
|
package/clients/auto-loop.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
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 = {
|
|
29
|
-
active: false,
|
|
30
|
-
iteration: 0,
|
|
31
|
-
maxIterations: config.maxIterations,
|
|
32
|
-
};
|
|
33
|
-
updateStatus(ctx);
|
|
34
|
-
if (wasActive) {
|
|
35
|
-
ctx.ui.notify(`✅ ${config.name} loop ${reason}`, "info");
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const complete = (ctx, reason) => {
|
|
39
|
-
stop(ctx, reason);
|
|
40
|
-
};
|
|
41
|
-
const start = (ctx) => {
|
|
42
|
-
if (state.active) {
|
|
43
|
-
ctx.ui.notify(`${config.name} loop is already running`, "warning");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
state = {
|
|
47
|
-
active: true,
|
|
48
|
-
iteration: 0,
|
|
49
|
-
maxIterations: config.maxIterations,
|
|
50
|
-
};
|
|
51
|
-
updateStatus(ctx);
|
|
52
|
-
ctx.ui.notify(`🔄 Starting ${config.name} auto-loop (max ${state.maxIterations} iterations)...`, "info");
|
|
53
|
-
};
|
|
54
|
-
const getState = () => ({ ...state });
|
|
55
|
-
// --- Event Handlers (registered at module load time) ---
|
|
56
|
-
// Handle user interruption (any manual input stops the loop)
|
|
57
|
-
pi.on("input", async (event, ctx) => {
|
|
58
|
-
if (!ctx.hasUI)
|
|
59
|
-
return { action: "continue" };
|
|
60
|
-
if (!state.active)
|
|
61
|
-
return { action: "continue" };
|
|
62
|
-
// User typed something manually → stop the auto-loop
|
|
63
|
-
if (event.source === "interactive") {
|
|
64
|
-
stop(ctx, "stopped (user interrupted)");
|
|
65
|
-
}
|
|
66
|
-
return { action: "continue" };
|
|
67
|
-
});
|
|
68
|
-
// Handle end of agent turn → check if we should continue
|
|
69
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
70
|
-
if (!ctx.hasUI)
|
|
71
|
-
return;
|
|
72
|
-
if (!state.active)
|
|
73
|
-
return;
|
|
74
|
-
const assistantMessages = event.messages.filter((m) => m.role === "assistant");
|
|
75
|
-
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
|
|
76
|
-
if (!lastAssistantMessage) {
|
|
77
|
-
stop(ctx, "stopped (no response)");
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const textContent = lastAssistantMessage.content
|
|
81
|
-
.filter((c) => c.type === "text")
|
|
82
|
-
.map((c) => c.text)
|
|
83
|
-
.join("\n");
|
|
84
|
-
if (!textContent.trim()) {
|
|
85
|
-
stop(ctx, "stopped (empty response)");
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// Check for completion patterns (explicit success)
|
|
89
|
-
if (config.completionPatterns) {
|
|
90
|
-
const hasCompletion = config.completionPatterns.some((p) => p.test(textContent));
|
|
91
|
-
if (hasCompletion) {
|
|
92
|
-
complete(ctx, "completed successfully");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Check for exit patterns (could be success or stopped)
|
|
97
|
-
const hasExit = config.exitPatterns.some((p) => p.test(textContent));
|
|
98
|
-
if (hasExit) {
|
|
99
|
-
complete(ctx, "completed - no more work");
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Check if agent is waiting for manual fixes (indicated in the prompt)
|
|
103
|
-
// If the last message says "When done, run..." we should NOT auto-continue
|
|
104
|
-
const awaitingManualFix = textContent.includes("When done, run");
|
|
105
|
-
if (awaitingManualFix) {
|
|
106
|
-
console.error("[auto-loop] Paused - awaiting agent manual fixes");
|
|
107
|
-
updateStatus(ctx);
|
|
108
|
-
// Don't send followUp - wait for agent to manually continue
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// Check max iterations
|
|
112
|
-
state.iteration++;
|
|
113
|
-
if (state.iteration >= state.maxIterations) {
|
|
114
|
-
stop(ctx, `stopped (max iterations ${state.maxIterations} reached)`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// Continue to next iteration - send command as follow-up
|
|
118
|
-
updateStatus(ctx);
|
|
119
|
-
const continueMsg = config.continuePrompt || `Run ${config.command} to continue.`;
|
|
120
|
-
console.error(`[auto-loop] Triggering iteration ${state.iteration + 1}/${state.maxIterations}: ${config.command}`);
|
|
121
|
-
pi.sendUserMessage(`🔄 Auto-loop (${state.iteration + 1}/${state.maxIterations}): ${continueMsg}`, { deliverAs: "followUp" });
|
|
122
|
-
});
|
|
123
|
-
return {
|
|
124
|
-
start,
|
|
125
|
-
stop,
|
|
126
|
-
getState,
|
|
127
|
-
setMaxIterations: (n) => {
|
|
128
|
-
state.maxIterations = n;
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
package/clients/biome-client.js
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Biome Client for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* All-in-one: formatting + linting for JS/TS/JSX/TSX/CSS/JSON
|
|
5
|
-
* Replaces Prettier with 15-50x faster Rust-based tool.
|
|
6
|
-
*
|
|
7
|
-
* Requires: npm install @biomejs/biome (or npx @biomejs/biome)
|
|
8
|
-
* Docs: https://biomejs.dev/
|
|
9
|
-
*/
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import { isFileKind } from "./file-kinds.js";
|
|
13
|
-
import { safeSpawn } from "./safe-spawn.js";
|
|
14
|
-
// --- Client ---
|
|
15
|
-
export class BiomeClient {
|
|
16
|
-
constructor(verbose = false) {
|
|
17
|
-
this.biomeAvailable = null;
|
|
18
|
-
this.localBinaryPath = null;
|
|
19
|
-
this.log = verbose
|
|
20
|
-
? (msg) => console.error(`[biome] ${msg}`)
|
|
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
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Check if biome CLI is available
|
|
61
|
-
*/
|
|
62
|
-
isAvailable() {
|
|
63
|
-
if (this.biomeAvailable !== null)
|
|
64
|
-
return this.biomeAvailable;
|
|
65
|
-
const result = this.spawnBiome(["--version"], 10000);
|
|
66
|
-
this.biomeAvailable = !result.error && result.status === 0;
|
|
67
|
-
if (this.biomeAvailable) {
|
|
68
|
-
const version = result.stdout?.trim() || "unknown";
|
|
69
|
-
this.log(`Biome found: ${version}`);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
this.log("Biome not available — install with: npm install -D @biomejs/biome");
|
|
73
|
-
}
|
|
74
|
-
return this.biomeAvailable;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Ensure Biome is available, auto-installing if necessary.
|
|
78
|
-
* Prefer this over isAvailable() for auto-install behavior.
|
|
79
|
-
*/
|
|
80
|
-
async ensureAvailable() {
|
|
81
|
-
if (this.biomeAvailable !== null)
|
|
82
|
-
return this.biomeAvailable;
|
|
83
|
-
// Check if already available
|
|
84
|
-
const result = this.spawnBiome(["--version"], 10000);
|
|
85
|
-
if (!result.error && result.status === 0) {
|
|
86
|
-
this.biomeAvailable = true;
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
// Auto-install via pi-lens installer
|
|
90
|
-
this.log("Biome not found, attempting auto-install...");
|
|
91
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
92
|
-
const installedPath = await ensureTool("biome");
|
|
93
|
-
if (installedPath) {
|
|
94
|
-
this.log(`Biome auto-installed: ${installedPath}`);
|
|
95
|
-
// Set the installed path as local binary to avoid npx overhead
|
|
96
|
-
this.localBinaryPath = installedPath;
|
|
97
|
-
this.biomeAvailable = true;
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
this.log("Biome auto-install failed");
|
|
101
|
-
this.biomeAvailable = false;
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Check if a file is supported by Biome
|
|
106
|
-
*/
|
|
107
|
-
isSupportedFile(filePath) {
|
|
108
|
-
return isFileKind(filePath, ["jsts", "json", "css"]);
|
|
109
|
-
}
|
|
110
|
-
// --- Internal helpers ---
|
|
111
|
-
/**
|
|
112
|
-
* Validate path and availability — returns path or null on failure
|
|
113
|
-
*/
|
|
114
|
-
withValidatedPath(filePath) {
|
|
115
|
-
if (!this.isAvailable())
|
|
116
|
-
return null;
|
|
117
|
-
const absolutePath = path.resolve(filePath);
|
|
118
|
-
if (!fs.existsSync(absolutePath))
|
|
119
|
-
return null;
|
|
120
|
-
return absolutePath;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Run biome check (format + lint) without fixing — returns diagnostics
|
|
124
|
-
*/
|
|
125
|
-
checkFile(filePath) {
|
|
126
|
-
const absolutePath = this.withValidatedPath(filePath);
|
|
127
|
-
if (!absolutePath)
|
|
128
|
-
return [];
|
|
129
|
-
try {
|
|
130
|
-
const result = this.spawnBiome([
|
|
131
|
-
"check",
|
|
132
|
-
"--reporter=json",
|
|
133
|
-
"--max-diagnostics=50",
|
|
134
|
-
absolutePath,
|
|
135
|
-
]);
|
|
136
|
-
// Biome exits 0 on success, 1 on issues found
|
|
137
|
-
const output = result.stdout || "";
|
|
138
|
-
if (!output.trim())
|
|
139
|
-
return [];
|
|
140
|
-
return this.parseDiagnostics(output, absolutePath);
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
this.log(`Check error: ${err instanceof Error ? err.message : String(err)}`);
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Format a file (writes to disk)
|
|
149
|
-
*/
|
|
150
|
-
formatFile(filePath) {
|
|
151
|
-
const absolutePath = this.withValidatedPath(filePath);
|
|
152
|
-
if (!absolutePath)
|
|
153
|
-
return {
|
|
154
|
-
success: false,
|
|
155
|
-
changed: false,
|
|
156
|
-
error: this.isAvailable() ? "File not found" : "Biome not available",
|
|
157
|
-
};
|
|
158
|
-
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
159
|
-
try {
|
|
160
|
-
const result = this.spawnBiome(["format", "--write", absolutePath]);
|
|
161
|
-
if (result.error) {
|
|
162
|
-
return { success: false, changed: false, error: result.error.message };
|
|
163
|
-
}
|
|
164
|
-
// Re-read to see if changed
|
|
165
|
-
const formatted = fs.readFileSync(absolutePath, "utf-8");
|
|
166
|
-
const changed = content !== formatted;
|
|
167
|
-
if (changed) {
|
|
168
|
-
this.log(`Formatted ${path.basename(filePath)}`);
|
|
169
|
-
}
|
|
170
|
-
return { success: true, changed };
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
changed: false,
|
|
176
|
-
error: err instanceof Error ? err.message : String(err),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Fix both formatting and linting issues (writes to disk)
|
|
182
|
-
*/
|
|
183
|
-
fixFile(filePath) {
|
|
184
|
-
const absolutePath = this.withValidatedPath(filePath);
|
|
185
|
-
if (!absolutePath)
|
|
186
|
-
return {
|
|
187
|
-
success: false,
|
|
188
|
-
changed: false,
|
|
189
|
-
fixed: 0,
|
|
190
|
-
error: this.isAvailable() ? "File not found" : "Biome not available",
|
|
191
|
-
};
|
|
192
|
-
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
193
|
-
try {
|
|
194
|
-
// Single invocation: check --write applies safe formatting + lint fixes.
|
|
195
|
-
// No pre-flight checkFile() needed — content diff tells us if anything changed.
|
|
196
|
-
const result = this.spawnBiome(["check", "--write", absolutePath]);
|
|
197
|
-
if (result.error) {
|
|
198
|
-
return {
|
|
199
|
-
success: false,
|
|
200
|
-
changed: false,
|
|
201
|
-
fixed: 0,
|
|
202
|
-
error: result.error.message,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
const fixed = fs.readFileSync(absolutePath, "utf-8");
|
|
206
|
-
const changed = content !== fixed;
|
|
207
|
-
if (changed) {
|
|
208
|
-
this.log(`Fixed issue(s) in ${path.basename(filePath)}`);
|
|
209
|
-
}
|
|
210
|
-
return { success: true, changed, fixed: changed ? 1 : 0 };
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
return {
|
|
214
|
-
success: false,
|
|
215
|
-
changed: false,
|
|
216
|
-
fixed: 0,
|
|
217
|
-
error: err instanceof Error ? err.message : String(err),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Fix multiple files at once (much faster than file-by-file)
|
|
223
|
-
*/
|
|
224
|
-
fixFiles(filePaths) {
|
|
225
|
-
if (!this.isAvailable()) {
|
|
226
|
-
return {
|
|
227
|
-
success: false,
|
|
228
|
-
fixed: 0,
|
|
229
|
-
changed: 0,
|
|
230
|
-
error: "Biome not available",
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
// Filter to existing files
|
|
234
|
-
const validFiles = filePaths
|
|
235
|
-
.map((f) => path.resolve(f))
|
|
236
|
-
.filter((f) => fs.existsSync(f));
|
|
237
|
-
if (validFiles.length === 0) {
|
|
238
|
-
return { success: true, fixed: 0, changed: 0 };
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
// Count fixable issues before fixing
|
|
242
|
-
let totalFixable = 0;
|
|
243
|
-
for (const file of validFiles) {
|
|
244
|
-
const diags = this.checkFile(file);
|
|
245
|
-
totalFixable += diags.filter((d) => d.fixable).length;
|
|
246
|
-
}
|
|
247
|
-
// Run biome once on all files - much faster than npx per file
|
|
248
|
-
const result = safeSpawn("npx", ["@biomejs/biome", "check", "--write", "--unsafe", ...validFiles], {
|
|
249
|
-
timeout: 60000, // Longer timeout for batch
|
|
250
|
-
});
|
|
251
|
-
if (result.error) {
|
|
252
|
-
return {
|
|
253
|
-
success: false,
|
|
254
|
-
fixed: 0,
|
|
255
|
-
changed: 0,
|
|
256
|
-
error: result.error.message,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
// Count how many files actually changed
|
|
260
|
-
let changedCount = 0;
|
|
261
|
-
for (const _file of validFiles) {
|
|
262
|
-
// We don't know exactly which files changed without re-reading,
|
|
263
|
-
// so we report total files processed
|
|
264
|
-
changedCount++;
|
|
265
|
-
}
|
|
266
|
-
this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
|
|
267
|
-
return { success: true, fixed: totalFixable, changed: changedCount };
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
return {
|
|
271
|
-
success: false,
|
|
272
|
-
fixed: 0,
|
|
273
|
-
changed: 0,
|
|
274
|
-
error: err instanceof Error ? err.message : String(err),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Format diagnostics for LLM consumption
|
|
280
|
-
*/
|
|
281
|
-
formatDiagnostics(diags, _filename) {
|
|
282
|
-
if (diags.length === 0)
|
|
283
|
-
return "";
|
|
284
|
-
const lintIssues = diags.filter((d) => d.category === "lint");
|
|
285
|
-
const formatIssues = diags.filter((d) => d.category === "format");
|
|
286
|
-
const errors = diags.filter((d) => d.severity === "error");
|
|
287
|
-
const fixable = diags.filter((d) => d.fixable);
|
|
288
|
-
let result = `[Biome] ${diags.length} issue(s)`;
|
|
289
|
-
if (lintIssues.length)
|
|
290
|
-
result += ` — ${lintIssues.length} lint`;
|
|
291
|
-
if (formatIssues.length)
|
|
292
|
-
result += ` — ${formatIssues.length} format`;
|
|
293
|
-
if (errors.length)
|
|
294
|
-
result += ` — ${errors.length} error(s)`;
|
|
295
|
-
if (fixable.length)
|
|
296
|
-
result += ` — ${fixable.length} fixable`;
|
|
297
|
-
result += ":\n";
|
|
298
|
-
for (const d of diags.slice(0, 15)) {
|
|
299
|
-
const loc = d.line === d.endLine
|
|
300
|
-
? `L${d.line}:${d.column}`
|
|
301
|
-
: `L${d.line}:${d.column}-L${d.endLine}:${d.endColumn}`;
|
|
302
|
-
const rule = d.rule ? ` [${d.rule}]` : "";
|
|
303
|
-
const fix = d.fixable ? " ✓" : "";
|
|
304
|
-
result += ` ${loc}${rule} ${d.message}${fix}\n`;
|
|
305
|
-
}
|
|
306
|
-
if (diags.length > 15) {
|
|
307
|
-
result += ` ... and ${diags.length - 15} more\n`;
|
|
308
|
-
}
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Generate a diff-like summary of formatting changes
|
|
313
|
-
*/
|
|
314
|
-
getFormatDiff(filePath) {
|
|
315
|
-
const absolutePath = this.withValidatedPath(filePath);
|
|
316
|
-
if (!absolutePath)
|
|
317
|
-
return "";
|
|
318
|
-
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
319
|
-
try {
|
|
320
|
-
// Get formatted output without writing
|
|
321
|
-
const result = safeSpawn("npx", ["@biomejs/biome", "format", absolutePath], {
|
|
322
|
-
timeout: 15000,
|
|
323
|
-
});
|
|
324
|
-
if (result.error || !result.stdout)
|
|
325
|
-
return "";
|
|
326
|
-
const formatted = result.stdout;
|
|
327
|
-
if (content === formatted)
|
|
328
|
-
return "";
|
|
329
|
-
return this.computeDiff(content, formatted);
|
|
330
|
-
}
|
|
331
|
-
catch (err) {
|
|
332
|
-
void err;
|
|
333
|
-
return "";
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// --- Internal ---
|
|
337
|
-
parseDiagnostics(output, filterFile) {
|
|
338
|
-
try {
|
|
339
|
-
// Biome JSON output: {"summary": {...}, "diagnostics": [...], ...}
|
|
340
|
-
const result = JSON.parse(output);
|
|
341
|
-
const diagnostics = [];
|
|
342
|
-
const diags = result.diagnostics || [];
|
|
343
|
-
const filterPath = path.resolve(filterFile);
|
|
344
|
-
for (const item of diags) {
|
|
345
|
-
// Filter to our file
|
|
346
|
-
const itemPath = item.location?.path;
|
|
347
|
-
if (itemPath && path.resolve(itemPath) !== filterPath)
|
|
348
|
-
continue;
|
|
349
|
-
const loc = item.location || {};
|
|
350
|
-
const start = loc.start || {};
|
|
351
|
-
const end = loc.end || start;
|
|
352
|
-
const isLint = item.category?.startsWith("lint/") || false;
|
|
353
|
-
const isFormat = item.category === "format";
|
|
354
|
-
const isAssist = item.category?.startsWith("assist/");
|
|
355
|
-
// Skip non-lint/format diagnostics (like summaries)
|
|
356
|
-
if (!isLint && !isFormat && !isAssist)
|
|
357
|
-
continue;
|
|
358
|
-
// Determine if fixable based on category
|
|
359
|
-
const fixable = isFormat ||
|
|
360
|
-
isAssist ||
|
|
361
|
-
item.category?.includes("organizeImports") ||
|
|
362
|
-
item.message?.includes("fix");
|
|
363
|
-
diagnostics.push({
|
|
364
|
-
line: start.line ?? 1,
|
|
365
|
-
column: start.column ?? 1,
|
|
366
|
-
endLine: end.line ?? start.line ?? 1,
|
|
367
|
-
endColumn: end.column ?? start.column ?? 1,
|
|
368
|
-
severity: item.severity || "warning",
|
|
369
|
-
message: item.message || "Unknown issue",
|
|
370
|
-
rule: isLint ? item.category?.replace("lint/", "") : undefined,
|
|
371
|
-
category: isLint ? "lint" : "format",
|
|
372
|
-
fixable,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
return diagnostics;
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
void err;
|
|
379
|
-
this.log("Failed to parse biome JSON output");
|
|
380
|
-
return [];
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
computeDiff(original, formatted) {
|
|
384
|
-
const origLines = original.split("\n");
|
|
385
|
-
const formLines = formatted.split("\n");
|
|
386
|
-
let changedLines = 0;
|
|
387
|
-
const changes = [];
|
|
388
|
-
const maxLen = Math.max(origLines.length, formLines.length);
|
|
389
|
-
for (let i = 0; i < maxLen; i++) {
|
|
390
|
-
const orig = origLines[i] ?? "";
|
|
391
|
-
const form = formLines[i] ?? "";
|
|
392
|
-
if (orig !== form) {
|
|
393
|
-
changedLines++;
|
|
394
|
-
if (changes.length < 5) {
|
|
395
|
-
if (orig && form) {
|
|
396
|
-
changes.push(` L${i + 1}: \`${orig.trim()}\` → \`${form.trim()}\``);
|
|
397
|
-
}
|
|
398
|
-
else if (!form) {
|
|
399
|
-
changes.push(` L${i + 1}: remove line`);
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
changes.push(` L${i + 1}: add line`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
let result = ` ${changedLines} line(s) would change`;
|
|
408
|
-
if (origLines.length !== formLines.length) {
|
|
409
|
-
result += ` (${origLines.length} → ${formLines.length} lines)`;
|
|
410
|
-
}
|
|
411
|
-
result += "\n";
|
|
412
|
-
for (const c of changes) {
|
|
413
|
-
result += `${c}\n`;
|
|
414
|
-
}
|
|
415
|
-
if (changedLines > 5) {
|
|
416
|
-
result += ` ... and ${changedLines - 5} more\n`;
|
|
417
|
-
}
|
|
418
|
-
return result;
|
|
419
|
-
}
|
|
420
|
-
}
|