pi-lens 3.8.18 → 3.8.19
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 +5 -0
- package/README.md +20 -14
- package/clients/dispatch/runners/sqlfluff.ts +41 -0
- package/clients/dispatch/runners/yamllint.ts +49 -0
- package/clients/pipeline.ts +71 -40
- package/clients/type-coverage-client.ts +1 -1
- package/commands/booboo.ts +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.8.19] - 2026-04-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Biome autofix gating** — Biome autofix/auto-install now runs only when the project has Biome configuration (`biome.json`/`biome.jsonc`) or `@biomejs/biome` in `devDependencies`, preventing unwanted Biome installs in non-Biome JS/TS projects.
|
|
9
|
+
|
|
5
10
|
## [3.8.18] - 2026-04-07
|
|
6
11
|
|
|
7
12
|
### Changed
|
package/README.md
CHANGED
|
@@ -82,19 +82,25 @@ Some runners are language/config-gated and may skip when not applicable.
|
|
|
82
82
|
|
|
83
83
|
## Dependencies
|
|
84
84
|
|
|
85
|
-
Auto-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
| `@biomejs/biome` | JS/TS lint/format/autofix | Yes |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
|
|
|
97
|
-
| `
|
|
85
|
+
Auto-install behavior depends on gate type:
|
|
86
|
+
|
|
87
|
+
- **Config-gated**: installs only when project config/deps indicate usage.
|
|
88
|
+
- **Flow/language-gated**: installs when the runtime path needs it for the current file/session flow.
|
|
89
|
+
- **Operational prewarm**: installs during session warm scans / turn-end analysis paths.
|
|
90
|
+
|
|
91
|
+
| Tool | Purpose | Auto-installed | Gate |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated (`biome.json`/`biome.jsonc` or `@biomejs/biome` dep) |
|
|
94
|
+
| `prettier` | Formatting fallback | Yes | Config-gated (Prettier dep or `package.json#prettier`) |
|
|
95
|
+
| `yamllint` | YAML linting | Yes | Config-gated (`.yamllint*` / tool section / dep hint) |
|
|
96
|
+
| `sqlfluff` | SQL linting/formatting | Yes | Config-gated (`.sqlfluff` / tool section / dep hint) |
|
|
97
|
+
| `ruff` | Python lint/format/autofix | Yes | Flow/language-gated (Python file paths/runners) |
|
|
98
|
+
| `typescript-language-server` | Unified LSP diagnostics | Yes | Flow-gated (LSP enabled; default on unless `--no-lsp`) |
|
|
99
|
+
| `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated (Python fallback paths) |
|
|
100
|
+
| `@ast-grep/cli` (`sg`) | AST scans/search/replace | Yes | Operational prewarm + analysis flows |
|
|
101
|
+
| `knip` | Dead code analysis | Yes | Operational prewarm + turn-end flows |
|
|
102
|
+
| `jscpd` | Duplicate code detection | Yes | Operational prewarm + turn-end flows |
|
|
103
|
+
| `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
|
|
98
104
|
|
|
99
105
|
LSP is enabled by default. pi-lens includes many language-server definitions (including up to 31+ servers), and activates them when the server is installed and the project/root detection matches the file.
|
|
100
106
|
|
|
@@ -105,5 +111,5 @@ Optional safety switch:
|
|
|
105
111
|
|
|
106
112
|
## Notes
|
|
107
113
|
|
|
108
|
-
-
|
|
114
|
+
- Not every auto-install runs in every project: gate type decides when install is attempted.
|
|
109
115
|
- Rule packs are customizable via project-level rule directories.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { ensureTool } from "../../installer/index.js";
|
|
2
4
|
import { safeSpawn } from "../../safe-spawn.js";
|
|
3
5
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
@@ -10,6 +12,41 @@ import type {
|
|
|
10
12
|
|
|
11
13
|
const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
|
|
12
14
|
|
|
15
|
+
const SQLFLUFF_CONFIGS = [".sqlfluff", "pyproject.toml", "setup.cfg", "tox.ini"];
|
|
16
|
+
|
|
17
|
+
export function hasSqlfluffConfig(cwd: string): boolean {
|
|
18
|
+
for (const cfg of SQLFLUFF_CONFIGS) {
|
|
19
|
+
const cfgPath = path.join(cwd, cfg);
|
|
20
|
+
if (!nodeFs.existsSync(cfgPath)) continue;
|
|
21
|
+
if (cfg === "pyproject.toml") {
|
|
22
|
+
try {
|
|
23
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
24
|
+
if (content.includes("[tool.sqlfluff]")) return true;
|
|
25
|
+
} catch {}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (cfg === "setup.cfg" || cfg === "tox.ini") {
|
|
29
|
+
try {
|
|
30
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
31
|
+
if (content.includes("[sqlfluff]")) return true;
|
|
32
|
+
} catch {}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
|
|
39
|
+
const depPath = path.join(cwd, depFile);
|
|
40
|
+
if (!nodeFs.existsSync(depPath)) continue;
|
|
41
|
+
try {
|
|
42
|
+
const content = nodeFs.readFileSync(depPath, "utf-8").toLowerCase();
|
|
43
|
+
if (content.includes("sqlfluff")) return true;
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
type SqlfluffJson = Array<{
|
|
14
51
|
filepath?: string;
|
|
15
52
|
violations?: Array<{
|
|
@@ -59,6 +96,10 @@ const sqlfluffRunner: RunnerDefinition = {
|
|
|
59
96
|
|
|
60
97
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
61
98
|
const cwd = ctx.cwd || process.cwd();
|
|
99
|
+
if (!hasSqlfluffConfig(cwd)) {
|
|
100
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
let cmd: string | null = null;
|
|
63
104
|
if (sqlfluff.isAvailable(cwd)) {
|
|
64
105
|
cmd = sqlfluff.getCommand(cwd);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { ensureTool } from "../../installer/index.js";
|
|
2
4
|
import { safeSpawn } from "../../safe-spawn.js";
|
|
3
5
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
@@ -10,6 +12,49 @@ import type {
|
|
|
10
12
|
|
|
11
13
|
const yamllint = createAvailabilityChecker("yamllint", ".exe");
|
|
12
14
|
|
|
15
|
+
const YAMLLINT_CONFIGS = [
|
|
16
|
+
".yamllint",
|
|
17
|
+
".yamllint.yml",
|
|
18
|
+
".yamllint.yaml",
|
|
19
|
+
"pyproject.toml",
|
|
20
|
+
"setup.cfg",
|
|
21
|
+
"tox.ini",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function hasYamllintConfig(cwd: string): boolean {
|
|
25
|
+
for (const cfg of YAMLLINT_CONFIGS) {
|
|
26
|
+
const cfgPath = path.join(cwd, cfg);
|
|
27
|
+
if (!nodeFs.existsSync(cfgPath)) continue;
|
|
28
|
+
if (cfg === "pyproject.toml") {
|
|
29
|
+
try {
|
|
30
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
31
|
+
if (content.includes("[tool.yamllint]")) return true;
|
|
32
|
+
} catch {}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (cfg === "setup.cfg" || cfg === "tox.ini") {
|
|
36
|
+
try {
|
|
37
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
38
|
+
if (content.includes("[yamllint]")) return true;
|
|
39
|
+
} catch {}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Dependency hint fallback for Python projects.
|
|
46
|
+
for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
|
|
47
|
+
const depPath = path.join(cwd, depFile);
|
|
48
|
+
if (!nodeFs.existsSync(depPath)) continue;
|
|
49
|
+
try {
|
|
50
|
+
const content = nodeFs.readFileSync(depPath, "utf-8").toLowerCase();
|
|
51
|
+
if (content.includes("yamllint")) return true;
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
function parseYamllintParsable(raw: string, filePath: string): Diagnostic[] {
|
|
14
59
|
const diagnostics: Diagnostic[] = [];
|
|
15
60
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -44,6 +89,10 @@ const yamllintRunner: RunnerDefinition = {
|
|
|
44
89
|
|
|
45
90
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
46
91
|
const cwd = ctx.cwd || process.cwd();
|
|
92
|
+
if (!hasYamllintConfig(cwd)) {
|
|
93
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
let cmd: string | null = null;
|
|
48
97
|
if (yamllint.isAvailable(cwd)) {
|
|
49
98
|
cmd = yamllint.getCommand(cwd);
|
package/clients/pipeline.ts
CHANGED
|
@@ -18,24 +18,30 @@ import type { BiomeClient } from "./biome-client.js";
|
|
|
18
18
|
import { getDiagnosticLogger } from "./diagnostic-logger.js";
|
|
19
19
|
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
20
20
|
import { dispatchLintWithResult } from "./dispatch/integration.js";
|
|
21
|
+
import {
|
|
22
|
+
resolveRunnerPath,
|
|
23
|
+
toRunnerDisplayPath,
|
|
24
|
+
} from "./dispatch/runner-context.js";
|
|
21
25
|
import type { PiAgentAPI } from "./dispatch/types.js";
|
|
22
26
|
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
23
27
|
import type { FormatService } from "./format-service.js";
|
|
24
28
|
import { logLatency } from "./latency-logger.js";
|
|
25
29
|
import { getLSPService } from "./lsp/index.js";
|
|
26
30
|
import type { MetricsClient } from "./metrics-client.js";
|
|
31
|
+
import { normalizeMapKey } from "./path-utils.js";
|
|
27
32
|
import type { RuffClient } from "./ruff-client.js";
|
|
28
33
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
29
34
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
30
35
|
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
31
36
|
import type { TestRunnerClient } from "./test-runner-client.js";
|
|
32
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
33
|
-
import { resolveRunnerPath, toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
34
37
|
|
|
35
38
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
36
39
|
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
37
40
|
|
|
38
|
-
function exceedsLspSyncLimits(
|
|
41
|
+
function exceedsLspSyncLimits(
|
|
42
|
+
filePath: string,
|
|
43
|
+
content: string,
|
|
44
|
+
): {
|
|
39
45
|
tooLarge: boolean;
|
|
40
46
|
reason: string;
|
|
41
47
|
} {
|
|
@@ -140,6 +146,8 @@ function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
|
|
|
140
146
|
|
|
141
147
|
// --- ESLint autofix helpers ---
|
|
142
148
|
|
|
149
|
+
const BIOME_CONFIGS = ["biome.json", "biome.jsonc"];
|
|
150
|
+
|
|
143
151
|
const ESLINT_CONFIGS = [
|
|
144
152
|
".eslintrc",
|
|
145
153
|
".eslintrc.js",
|
|
@@ -158,6 +166,19 @@ function isJsTs(filePath: string): boolean {
|
|
|
158
166
|
return JSTS_EXTS.has(path.extname(filePath).toLowerCase());
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
function hasBiomeConfig(cwd: string): boolean {
|
|
170
|
+
for (const cfg of BIOME_CONFIGS) {
|
|
171
|
+
if (nodeFs.existsSync(path.join(cwd, cfg))) return true;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const pkg = JSON.parse(
|
|
175
|
+
nodeFs.readFileSync(path.join(cwd, "package.json"), "utf-8"),
|
|
176
|
+
);
|
|
177
|
+
if (pkg.devDependencies?.["@biomejs/biome"]) return true;
|
|
178
|
+
} catch {}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
function hasEslintConfig(cwd: string): boolean {
|
|
162
183
|
for (const cfg of ESLINT_CONFIGS) {
|
|
163
184
|
if (nodeFs.existsSync(path.join(cwd, cfg))) return true;
|
|
@@ -171,7 +192,10 @@ function hasEslintConfig(cwd: string): boolean {
|
|
|
171
192
|
return false;
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
const _eslintCache = new Map<
|
|
195
|
+
const _eslintCache = new Map<
|
|
196
|
+
string,
|
|
197
|
+
{ available: boolean; bin: string | null }
|
|
198
|
+
>();
|
|
175
199
|
|
|
176
200
|
function findEslintBin(cwd: string): string {
|
|
177
201
|
const isWin = process.platform === "win32";
|
|
@@ -331,7 +355,7 @@ export async function runPipeline(
|
|
|
331
355
|
const deferLspSync =
|
|
332
356
|
!getFlag("no-autofix") &&
|
|
333
357
|
(ruffClient.isPythonFile(filePath) ||
|
|
334
|
-
biomeClient.isSupportedFile(filePath) ||
|
|
358
|
+
(biomeClient.isSupportedFile(filePath) && hasBiomeConfig(cwd)) ||
|
|
335
359
|
isJsTs(filePath));
|
|
336
360
|
|
|
337
361
|
if (deferLspSync) {
|
|
@@ -386,7 +410,9 @@ export async function runPipeline(
|
|
|
386
410
|
!noAutofixRuff && ruffClient.isPythonFile(filePath)
|
|
387
411
|
? ruffClient.ensureAvailable()
|
|
388
412
|
: Promise.resolve(false),
|
|
389
|
-
!noAutofixBiome &&
|
|
413
|
+
!noAutofixBiome &&
|
|
414
|
+
biomeClient.isSupportedFile(filePath) &&
|
|
415
|
+
hasBiomeConfig(cwd)
|
|
390
416
|
? biomeClient.ensureAvailable()
|
|
391
417
|
: Promise.resolve(false),
|
|
392
418
|
]);
|
|
@@ -494,12 +520,16 @@ export async function runPipeline(
|
|
|
494
520
|
);
|
|
495
521
|
for (const d of dispatchResult.diagnostics) {
|
|
496
522
|
const shownInline = inlineKeys.has(toKey(d));
|
|
497
|
-
logger.logCaught(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
523
|
+
logger.logCaught(
|
|
524
|
+
d,
|
|
525
|
+
{
|
|
526
|
+
model: ctx.telemetry?.model ?? "unknown",
|
|
527
|
+
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
528
|
+
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
529
|
+
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
530
|
+
},
|
|
531
|
+
shownInline,
|
|
532
|
+
);
|
|
503
533
|
}
|
|
504
534
|
}
|
|
505
535
|
|
|
@@ -552,34 +582,34 @@ export async function runPipeline(
|
|
|
552
582
|
target.runner,
|
|
553
583
|
target.config,
|
|
554
584
|
);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
585
|
+
const testDuration = Date.now() - testStart;
|
|
586
|
+
logLatency({
|
|
587
|
+
type: "phase",
|
|
588
|
+
toolName,
|
|
589
|
+
filePath,
|
|
590
|
+
phase: "test_runner",
|
|
591
|
+
durationMs: testDuration,
|
|
592
|
+
metadata: {
|
|
593
|
+
testFile: target.testFile,
|
|
594
|
+
runner: target.runner,
|
|
595
|
+
strategy: target.strategy,
|
|
596
|
+
success: !testResult?.error,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
if (testResult && !testResult.error) {
|
|
600
|
+
testSummary = {
|
|
601
|
+
passed: testResult.passed,
|
|
602
|
+
total: testResult.passed + testResult.failed + testResult.skipped,
|
|
603
|
+
failed: testResult.failed,
|
|
604
|
+
};
|
|
605
|
+
if (testSummary.failed > 0) {
|
|
606
|
+
hasBlockers = true;
|
|
607
|
+
}
|
|
608
|
+
const testOutput = testRunnerClient.formatResult(testResult);
|
|
609
|
+
if (testOutput) {
|
|
610
|
+
output += `\n\n${testOutput}`;
|
|
582
611
|
}
|
|
612
|
+
}
|
|
583
613
|
}
|
|
584
614
|
}
|
|
585
615
|
phase.end("test_runner", { found: testInfoFound, ran: testRunnerRan });
|
|
@@ -608,7 +638,8 @@ export async function runPipeline(
|
|
|
608
638
|
|
|
609
639
|
for (const [diagPath, diags] of allDiags) {
|
|
610
640
|
const normalizedDiagPath = resolveRunnerPath(cwd, diagPath);
|
|
611
|
-
if (normalizeMapKey(normalizedDiagPath) === normalizedEditedPath)
|
|
641
|
+
if (normalizeMapKey(normalizedDiagPath) === normalizedEditedPath)
|
|
642
|
+
continue;
|
|
612
643
|
|
|
613
644
|
if (!nodeFs.existsSync(normalizedDiagPath)) {
|
|
614
645
|
stalePathsSkipped++;
|
|
@@ -105,7 +105,7 @@ export class TypeCoverageClient {
|
|
|
105
105
|
if (result.percentage >= 95) icon = "✓";
|
|
106
106
|
else if (result.percentage >= 80) icon = "⚠";
|
|
107
107
|
|
|
108
|
-
let output = `[type-coverage] ${icon} ${pct}% typed (${result.typed}/${result.total} identifiers)`;
|
|
108
|
+
let output = `[type-coverage] ${icon} ${pct}% typed (${result.typed}/${result.total} identifiers; any-typed flagged)`;
|
|
109
109
|
|
|
110
110
|
if (result.untypedLocations.length === 0) {
|
|
111
111
|
output += " — fully typed\n";
|
package/commands/booboo.ts
CHANGED
|
@@ -910,6 +910,8 @@ export async function handleBooboo(
|
|
|
910
910
|
});
|
|
911
911
|
|
|
912
912
|
let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
913
|
+
fullSection +=
|
|
914
|
+
"Type coverage highlights identifiers that resolve to `any` (implicit or explicit). Inferred non-`any` types are treated as typed.\n\n";
|
|
913
915
|
const byFile: Record<string, number> = {};
|
|
914
916
|
for (const u of filteredLocations) {
|
|
915
917
|
byFile[u.file] = (byFile[u.file] || 0) + 1;
|
|
@@ -920,7 +922,7 @@ export async function handleBooboo(
|
|
|
920
922
|
.slice(0, 10);
|
|
921
923
|
|
|
922
924
|
if (sortedFiles.length > 0) {
|
|
923
|
-
fullSection += `### Top Files by
|
|
925
|
+
fullSection += `### Top Files by Any-Typed Identifier Count\n\n| File | Any-Typed Count |\n|------|-----------------|\n`;
|
|
924
926
|
for (const [file, count] of sortedFiles) {
|
|
925
927
|
fullSection += `| ${file} | ${count} |\n`;
|
|
926
928
|
}
|