pi-lens 3.8.39 → 3.8.41
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 +84 -5
- package/README.md +37 -1
- package/clients/biome-client.ts +5 -4
- package/clients/cache/rule-cache.ts +1 -1
- package/clients/complexity-client.ts +1 -1
- package/clients/dependency-checker.ts +1 -1
- package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
- package/clients/dispatch/dispatcher.ts +9 -0
- package/clients/dispatch/fact-scheduler.ts +1 -1
- package/clients/dispatch/integration.ts +58 -3
- package/clients/dispatch/runners/index.ts +2 -0
- package/clients/dispatch/runners/semgrep.ts +269 -0
- package/clients/dispatch/runners/shellcheck.ts +2 -8
- package/clients/dispatch/runners/tree-sitter.ts +32 -11
- package/clients/dispatch/tool-profile.ts +1 -0
- package/clients/format-service.ts +10 -0
- package/clients/formatters.ts +22 -8
- package/clients/installer/index.ts +3 -3
- package/clients/knip-client.ts +360 -362
- package/clients/lsp/aggregation.ts +91 -0
- package/clients/lsp/client.ts +91 -38
- package/clients/lsp/index.ts +88 -72
- package/clients/lsp/launch.ts +107 -34
- package/clients/lsp/server-strategies.ts +71 -0
- package/clients/lsp/server.ts +76 -57
- package/clients/path-utils.ts +17 -0
- package/clients/pipeline.ts +23 -5
- package/clients/production-readiness.ts +2 -2
- package/clients/read-guard-logger.ts +41 -1
- package/clients/read-guard-tool-lines.ts +17 -4
- package/clients/read-guard.ts +95 -46
- package/clients/runtime-agent-end.ts +3 -0
- package/clients/runtime-session.ts +5 -0
- package/clients/runtime-tool-result.ts +48 -1
- package/clients/runtime-turn.ts +48 -4
- package/clients/sanitize.ts +1 -1
- package/clients/semgrep-config.ts +213 -0
- package/clients/tool-policy.ts +1982 -1936
- package/clients/tree-sitter-client.ts +1 -1
- package/clients/widget-state.ts +283 -0
- package/commands/booboo.ts +34 -2
- package/index.ts +231 -17
- package/package.json +3 -2
- package/rules/rule-catalog.json +25 -1
- package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
- package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
- package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
- package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
- package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
- package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
- package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
- package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
- package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
- package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
package/clients/lsp/launch.ts
CHANGED
|
@@ -402,7 +402,19 @@ function _attachErrorHandler(
|
|
|
402
402
|
proc.on("error", (err) => {
|
|
403
403
|
if (logContext) {
|
|
404
404
|
logSessionStart(
|
|
405
|
-
"lsp process " +
|
|
405
|
+
"lsp process " +
|
|
406
|
+
context +
|
|
407
|
+
": spawn-error command=" +
|
|
408
|
+
logContext.command +
|
|
409
|
+
" args=" +
|
|
410
|
+
JSON.stringify(logContext.args) +
|
|
411
|
+
" cwd=" +
|
|
412
|
+
logContext.cwd +
|
|
413
|
+
" pid=" +
|
|
414
|
+
(logContext.pid ?? 0) +
|
|
415
|
+
" error=" +
|
|
416
|
+
err.message +
|
|
417
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
406
418
|
);
|
|
407
419
|
}
|
|
408
420
|
|
|
@@ -422,12 +434,37 @@ function _attachErrorHandler(
|
|
|
422
434
|
if (code !== 0 && code !== null) {
|
|
423
435
|
if (logContext) {
|
|
424
436
|
logSessionStart(
|
|
425
|
-
"lsp process " +
|
|
437
|
+
"lsp process " +
|
|
438
|
+
context +
|
|
439
|
+
": closed code=" +
|
|
440
|
+
code +
|
|
441
|
+
(signal ? " signal=" + signal : "") +
|
|
442
|
+
" command=" +
|
|
443
|
+
logContext.command +
|
|
444
|
+
" args=" +
|
|
445
|
+
JSON.stringify(logContext.args) +
|
|
446
|
+
" cwd=" +
|
|
447
|
+
logContext.cwd +
|
|
448
|
+
" pid=" +
|
|
449
|
+
(logContext.pid ?? 0) +
|
|
450
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
426
451
|
);
|
|
427
452
|
}
|
|
428
453
|
} else if (signal && logContext) {
|
|
429
454
|
logSessionStart(
|
|
430
|
-
"lsp process " +
|
|
455
|
+
"lsp process " +
|
|
456
|
+
context +
|
|
457
|
+
": closed signal=" +
|
|
458
|
+
signal +
|
|
459
|
+
" command=" +
|
|
460
|
+
logContext.command +
|
|
461
|
+
" args=" +
|
|
462
|
+
JSON.stringify(logContext.args) +
|
|
463
|
+
" cwd=" +
|
|
464
|
+
logContext.cwd +
|
|
465
|
+
" pid=" +
|
|
466
|
+
(logContext.pid ?? 0) +
|
|
467
|
+
(stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
431
468
|
);
|
|
432
469
|
}
|
|
433
470
|
});
|
|
@@ -451,7 +488,7 @@ export async function launchLSP(
|
|
|
451
488
|
command: string,
|
|
452
489
|
args: string[] = [],
|
|
453
490
|
options: SpawnOptions & {
|
|
454
|
-
startupFailureWindowMs?: number
|
|
491
|
+
startupFailureWindowMs?: number;
|
|
455
492
|
} = {},
|
|
456
493
|
): Promise<LSPProcess> {
|
|
457
494
|
const cwd = String(options.cwd ?? process.cwd());
|
|
@@ -467,7 +504,9 @@ export async function launchLSP(
|
|
|
467
504
|
// - If already absolute, use as-is
|
|
468
505
|
// - If it's a simple command (no path separators), let system find it via PATH
|
|
469
506
|
// - Otherwise, resolve relative to cwd
|
|
470
|
-
const isRelativePath =
|
|
507
|
+
const isRelativePath =
|
|
508
|
+
!path.isAbsolute(command) &&
|
|
509
|
+
(command.includes(path.sep) || command.includes("/"));
|
|
471
510
|
const explicitCommand = isRelativePath ? path.resolve(cwd, command) : command;
|
|
472
511
|
const resolvedCommand =
|
|
473
512
|
!path.isAbsolute(command) &&
|
|
@@ -645,7 +684,7 @@ export async function launchLSP(
|
|
|
645
684
|
return DEFAULT_STARTUP_FAILURE_WINDOW_MS;
|
|
646
685
|
}
|
|
647
686
|
})();
|
|
648
|
-
|
|
687
|
+
|
|
649
688
|
// Give shell-backed Windows launches a slightly longer window because
|
|
650
689
|
// npm/cmd shims can fail asynchronously after the initial spawn succeeds.
|
|
651
690
|
setTimeout(() => {
|
|
@@ -803,38 +842,72 @@ export async function launchViaPython(
|
|
|
803
842
|
* Stop an LSP process gracefully
|
|
804
843
|
*/
|
|
805
844
|
export async function stopLSP(handle: LSPProcess): Promise<void> {
|
|
845
|
+
if (handle.process.exitCode !== null || handle.process.signalCode !== null) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
806
849
|
return new Promise((resolve) => {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
850
|
+
let settled = false;
|
|
851
|
+
let forceTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
852
|
+
let giveUpTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
853
|
+
|
|
854
|
+
const done = () => {
|
|
855
|
+
if (settled) return;
|
|
856
|
+
settled = true;
|
|
857
|
+
if (forceTimeout) clearTimeout(forceTimeout);
|
|
858
|
+
if (giveUpTimeout) clearTimeout(giveUpTimeout);
|
|
859
|
+
handle.process.off("exit", done);
|
|
860
|
+
handle.process.off("error", done);
|
|
861
|
+
resolve();
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
handle.process.once("exit", done);
|
|
865
|
+
handle.process.once("error", done);
|
|
866
|
+
|
|
867
|
+
const killWindowsTree = (): boolean => {
|
|
868
|
+
if (!isWindows || handle.pid <= 0) return false;
|
|
869
|
+
try {
|
|
870
|
+
// Absolute path avoids PATH-resolution substitution on Windows.
|
|
871
|
+
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
872
|
+
const killer = nodeSpawn(
|
|
873
|
+
taskkill,
|
|
874
|
+
["/F", "/T", "/PID", String(handle.pid)],
|
|
875
|
+
{
|
|
876
|
+
shell: false,
|
|
877
|
+
windowsHide: true,
|
|
878
|
+
},
|
|
879
|
+
);
|
|
880
|
+
killer.once("error", done);
|
|
881
|
+
return true;
|
|
882
|
+
} catch {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
// On Windows, kill the tree first; killing the direct child can orphan
|
|
889
|
+
// grandchildren (e.g. tsserver.js behind a cmd/npm shim).
|
|
890
|
+
if (!killWindowsTree()) {
|
|
891
|
+
handle.process.kill("SIGTERM");
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
done();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
forceTimeout = setTimeout(() => {
|
|
899
|
+
if (settled) return;
|
|
900
|
+
try {
|
|
901
|
+
if (!killWindowsTree()) {
|
|
825
902
|
handle.process.kill("SIGKILL");
|
|
826
903
|
}
|
|
904
|
+
} catch {
|
|
905
|
+
done();
|
|
906
|
+
return;
|
|
827
907
|
}
|
|
908
|
+
// If the process had already exited before listeners were attached, no
|
|
909
|
+
// exit event will arrive. Resolve rather than hanging test cleanup forever.
|
|
910
|
+
giveUpTimeout = setTimeout(done, 500);
|
|
828
911
|
}, 5000);
|
|
829
|
-
|
|
830
|
-
handle.process.on("exit", () => {
|
|
831
|
-
clearTimeout(timeout);
|
|
832
|
-
resolve();
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
handle.process.on("error", () => {
|
|
836
|
-
clearTimeout(timeout);
|
|
837
|
-
resolve();
|
|
838
|
-
});
|
|
839
912
|
});
|
|
840
913
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Server Diagnostic Strategies for pi-lens LSP
|
|
3
|
+
*
|
|
4
|
+
* Codifies known server behavior so timing decisions (debounce, retry budget,
|
|
5
|
+
* first-push seeding) are automatic rather than one-size-fits-all.
|
|
6
|
+
*
|
|
7
|
+
* Env var overrides (PI_LENS_LSP_*) always take precedence over strategy values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface DiagnosticStrategy {
|
|
11
|
+
/** Seed the push cache on the very first publishDiagnostics notification.
|
|
12
|
+
* True for servers whose first push is known to be complete. */
|
|
13
|
+
seedFirstPush: boolean;
|
|
14
|
+
/** Maximum ms to spend retrying pull diagnostics when the first pull returns
|
|
15
|
+
* empty. 0 = skip pull retry entirely, rely on push. */
|
|
16
|
+
pullRetryBudgetMs: number;
|
|
17
|
+
/** Debounce window for push diagnostics (ms). Applied in both the notification
|
|
18
|
+
* handler and the waitForDiagnostics listener. */
|
|
19
|
+
debounceMs: number;
|
|
20
|
+
/** The aggregate timeout for waitForDiagnostics per this server (ms).
|
|
21
|
+
* Overrides the global DIAGNOSTICS_AGGREGATE_WAIT_MS in the service layer. */
|
|
22
|
+
aggregateWaitMs: number;
|
|
23
|
+
/** Whether this server benefits from a second pull after an empty fast first
|
|
24
|
+
* pull. TypeScript: no (rely on push). rust-analyzer: yes (incremental). */
|
|
25
|
+
expectSemanticSecondPush: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SERVER_DIAGNOSTIC_STRATEGIES: Record<string, DiagnosticStrategy> =
|
|
29
|
+
{
|
|
30
|
+
typescript: {
|
|
31
|
+
seedFirstPush: true,
|
|
32
|
+
pullRetryBudgetMs: 0,
|
|
33
|
+
debounceMs: 50,
|
|
34
|
+
aggregateWaitMs: 1000,
|
|
35
|
+
expectSemanticSecondPush: false,
|
|
36
|
+
},
|
|
37
|
+
"rust-analyzer": {
|
|
38
|
+
seedFirstPush: false,
|
|
39
|
+
pullRetryBudgetMs: 500,
|
|
40
|
+
debounceMs: 150,
|
|
41
|
+
aggregateWaitMs: 3000,
|
|
42
|
+
expectSemanticSecondPush: true,
|
|
43
|
+
},
|
|
44
|
+
pyright: {
|
|
45
|
+
seedFirstPush: false,
|
|
46
|
+
pullRetryBudgetMs: 250,
|
|
47
|
+
debounceMs: 100,
|
|
48
|
+
aggregateWaitMs: 1500,
|
|
49
|
+
expectSemanticSecondPush: false,
|
|
50
|
+
},
|
|
51
|
+
eslint: {
|
|
52
|
+
seedFirstPush: true,
|
|
53
|
+
pullRetryBudgetMs: 0,
|
|
54
|
+
debounceMs: 200,
|
|
55
|
+
aggregateWaitMs: 2000,
|
|
56
|
+
expectSemanticSecondPush: false,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Fallback for unknown servers. Conservative defaults. */
|
|
61
|
+
export const DEFAULT_STRATEGY: DiagnosticStrategy = {
|
|
62
|
+
seedFirstPush: false,
|
|
63
|
+
pullRetryBudgetMs: 250,
|
|
64
|
+
debounceMs: 150,
|
|
65
|
+
aggregateWaitMs: 1500,
|
|
66
|
+
expectSemanticSecondPush: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function getStrategy(serverId: string): DiagnosticStrategy {
|
|
70
|
+
return SERVER_DIAGNOSTIC_STRATEGIES[serverId] ?? DEFAULT_STRATEGY;
|
|
71
|
+
}
|
package/clients/lsp/server.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
|
12
12
|
import { access, appendFile, mkdir, stat } from "node:fs/promises";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { KIND_EXTENSIONS } from "../file-kinds.js"
|
|
15
|
+
import { KIND_EXTENSIONS } from "../file-kinds.js";
|
|
16
16
|
import { ensureTool, getToolEnvironment } from "../installer/index.js";
|
|
17
17
|
import { logLatency } from "../latency-logger.js";
|
|
18
18
|
import { type LSPProcess, launchLSP } from "./launch.js";
|
|
19
|
+
import { normalizeMapKey } from "./path-utils.js";
|
|
19
20
|
|
|
20
21
|
// --- Types ---
|
|
21
22
|
|
|
@@ -489,56 +490,90 @@ export function NearestRoot(
|
|
|
489
490
|
excludePatterns?: string[],
|
|
490
491
|
stopDir?: string,
|
|
491
492
|
): RootFunction {
|
|
493
|
+
// Per-instance caches — each NearestRoot(markers) call gets its own Map so
|
|
494
|
+
// different servers (e.g. TypeScript vs Go) with different marker sets never
|
|
495
|
+
// share entries. vi.resetModules() in tests resets module state between cases.
|
|
496
|
+
const cache = new Map<string, string>();
|
|
497
|
+
const inFlight = new Map<string, Promise<string | undefined>>();
|
|
498
|
+
|
|
492
499
|
return async (file: string): Promise<string | undefined> => {
|
|
493
|
-
|
|
494
|
-
const
|
|
495
|
-
const
|
|
500
|
+
// Cache key is the resolved directory — all files in the same dir share a root.
|
|
501
|
+
const startDir = path.resolve(path.dirname(file));
|
|
502
|
+
const dirKey = normalizeMapKey(startDir);
|
|
503
|
+
|
|
504
|
+
// Fast path: already resolved for this directory.
|
|
505
|
+
const cached = cache.get(dirKey);
|
|
506
|
+
if (cached !== undefined) return cached;
|
|
507
|
+
|
|
508
|
+
// In-flight deduplication: if N parallel pipelines edit files in the same
|
|
509
|
+
// directory simultaneously, only one stat-walk runs; the rest await the same
|
|
510
|
+
// promise. This is the main fix for parallel-turn LSP timeout spikes.
|
|
511
|
+
const flying = inFlight.get(dirKey);
|
|
512
|
+
if (flying) return flying;
|
|
513
|
+
|
|
514
|
+
const promise = (async (): Promise<string | undefined> => {
|
|
515
|
+
let currentDir = startDir;
|
|
516
|
+
const fsRoot = path.parse(currentDir).root;
|
|
517
|
+
const stop = stopDir ? path.resolve(stopDir) : fsRoot;
|
|
518
|
+
|
|
519
|
+
while (true) {
|
|
520
|
+
if (
|
|
521
|
+
stop !== fsRoot &&
|
|
522
|
+
currentDir.startsWith(stop + path.sep) === false &&
|
|
523
|
+
currentDir !== stop
|
|
524
|
+
) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
496
527
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
528
|
+
// Check exclude patterns — skip this dir (but keep walking up)
|
|
529
|
+
if (excludePatterns) {
|
|
530
|
+
let excluded = false;
|
|
531
|
+
for (const pattern of excludePatterns) {
|
|
532
|
+
try {
|
|
533
|
+
await stat(path.join(currentDir, pattern));
|
|
534
|
+
excluded = true;
|
|
535
|
+
break;
|
|
536
|
+
} catch {
|
|
537
|
+
/* not found */
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (excluded) {
|
|
541
|
+
currentDir = path.dirname(currentDir);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
505
545
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
let excluded = false;
|
|
509
|
-
for (const pattern of excludePatterns) {
|
|
546
|
+
// Check include patterns
|
|
547
|
+
for (const pattern of includePatterns) {
|
|
510
548
|
try {
|
|
511
549
|
await stat(path.join(currentDir, pattern));
|
|
512
|
-
|
|
513
|
-
break;
|
|
550
|
+
return currentDir;
|
|
514
551
|
} catch {
|
|
515
552
|
/* not found */
|
|
516
553
|
}
|
|
517
554
|
}
|
|
518
|
-
if (excluded) {
|
|
519
|
-
currentDir = path.dirname(currentDir);
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
555
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
await stat(path.join(currentDir, pattern));
|
|
528
|
-
return currentDir;
|
|
529
|
-
} catch {
|
|
530
|
-
/* not found */
|
|
556
|
+
if (currentDir === stop || currentDir === fsRoot) {
|
|
557
|
+
break;
|
|
531
558
|
}
|
|
532
|
-
}
|
|
533
559
|
|
|
534
|
-
|
|
535
|
-
break;
|
|
560
|
+
currentDir = path.dirname(currentDir);
|
|
536
561
|
}
|
|
537
562
|
|
|
538
|
-
|
|
539
|
-
}
|
|
563
|
+
return undefined;
|
|
564
|
+
})();
|
|
540
565
|
|
|
541
|
-
|
|
566
|
+
inFlight.set(dirKey, promise);
|
|
567
|
+
try {
|
|
568
|
+
const result = await promise;
|
|
569
|
+
// Only cache successful hits. Undefined results are not cached so that
|
|
570
|
+
// a newly-created root marker (e.g. package.json added mid-session) is
|
|
571
|
+
// detected on the next call.
|
|
572
|
+
if (result !== undefined) cache.set(dirKey, result);
|
|
573
|
+
return result;
|
|
574
|
+
} finally {
|
|
575
|
+
inFlight.delete(dirKey);
|
|
576
|
+
}
|
|
542
577
|
};
|
|
543
578
|
}
|
|
544
579
|
|
|
@@ -1362,11 +1397,7 @@ export const NixServer = createInteractiveServer({
|
|
|
1362
1397
|
export const BashServer: LSPServerInfo = {
|
|
1363
1398
|
id: "bash",
|
|
1364
1399
|
name: "Bash Language Server",
|
|
1365
|
-
extensions: [
|
|
1366
|
-
".bash",
|
|
1367
|
-
".sh",
|
|
1368
|
-
".zsh",
|
|
1369
|
-
],
|
|
1400
|
+
extensions: [".bash", ".sh", ".zsh"],
|
|
1370
1401
|
root: FileDirRoot,
|
|
1371
1402
|
spawn(root, options) {
|
|
1372
1403
|
return resolveAndLaunch(
|
|
@@ -1384,10 +1415,7 @@ export const BashServer: LSPServerInfo = {
|
|
|
1384
1415
|
export const DockerServer: LSPServerInfo = {
|
|
1385
1416
|
id: "docker",
|
|
1386
1417
|
name: "Dockerfile Language Server",
|
|
1387
|
-
extensions: [
|
|
1388
|
-
".dockerfile",
|
|
1389
|
-
"Dockerfile",
|
|
1390
|
-
],
|
|
1418
|
+
extensions: [".dockerfile", "Dockerfile"],
|
|
1391
1419
|
root: RootWithFallback(
|
|
1392
1420
|
PriorityRoot([
|
|
1393
1421
|
[
|
|
@@ -1525,9 +1553,7 @@ export const PrismaServer: LSPServerInfo = {
|
|
|
1525
1553
|
export const VueServer: LSPServerInfo = {
|
|
1526
1554
|
id: "vue",
|
|
1527
1555
|
name: "Vue Language Server",
|
|
1528
|
-
extensions: [
|
|
1529
|
-
".vue",
|
|
1530
|
-
],
|
|
1556
|
+
extensions: [".vue"],
|
|
1531
1557
|
root: RootWithFallback(
|
|
1532
1558
|
IgnoreHomeRoot(
|
|
1533
1559
|
createRootDetector([
|
|
@@ -1577,9 +1603,7 @@ export const VueServer: LSPServerInfo = {
|
|
|
1577
1603
|
export const SvelteServer: LSPServerInfo = {
|
|
1578
1604
|
id: "svelte",
|
|
1579
1605
|
name: "Svelte Language Server",
|
|
1580
|
-
extensions: [
|
|
1581
|
-
".svelte",
|
|
1582
|
-
],
|
|
1606
|
+
extensions: [".svelte"],
|
|
1583
1607
|
root: RootWithFallback(
|
|
1584
1608
|
IgnoreHomeRoot(
|
|
1585
1609
|
createRootDetector([
|
|
@@ -1621,12 +1645,7 @@ export const ESLintServer: LSPServerInfo = {
|
|
|
1621
1645
|
id: "eslint",
|
|
1622
1646
|
name: "ESLint Language Server",
|
|
1623
1647
|
// Note: .ts/.tsx handled by TypeScript LSP + Biome
|
|
1624
|
-
extensions: [
|
|
1625
|
-
".js",
|
|
1626
|
-
".jsx",
|
|
1627
|
-
".svelte",
|
|
1628
|
-
".vue",
|
|
1629
|
-
],
|
|
1648
|
+
extensions: [".js", ".jsx", ".svelte", ".vue"],
|
|
1630
1649
|
root: IgnoreHomeRoot(
|
|
1631
1650
|
createRootDetector([
|
|
1632
1651
|
".eslintrc",
|
package/clients/path-utils.ts
CHANGED
|
@@ -151,3 +151,20 @@ export function isUnderDir(child: string, parent: string): boolean {
|
|
|
151
151
|
const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
|
|
152
152
|
return normChild === normParent || normChild.startsWith(parentPrefix);
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns true when a file should be treated as external/vendor and excluded
|
|
157
|
+
* from pipelines (LSP, diagnostics, complexity, read-guard, etc.).
|
|
158
|
+
*
|
|
159
|
+
* Two cases:
|
|
160
|
+
* 1. Outside the project root entirely (e.g. global npm packages, system files)
|
|
161
|
+
* 2. Inside the project but under node_modules (local deps, never user-editable)
|
|
162
|
+
*/
|
|
163
|
+
export function isExternalOrVendorFile(
|
|
164
|
+
filePath: string,
|
|
165
|
+
projectRoot: string,
|
|
166
|
+
): boolean {
|
|
167
|
+
if (!isUnderDir(filePath, projectRoot)) return true;
|
|
168
|
+
const normalized = normalizeFilePath(filePath);
|
|
169
|
+
return normalized.includes("/node_modules/");
|
|
170
|
+
}
|
package/clients/pipeline.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as nodeFs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import type { BiomeClient } from "./biome-client.js";
|
|
18
|
+
import { recordDiagnostics } from "./widget-state.js";
|
|
18
19
|
import { getDiagnosticLogger } from "./diagnostic-logger.js";
|
|
19
20
|
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
20
21
|
import {
|
|
@@ -725,7 +726,12 @@ export async function runFormatPhase(
|
|
|
725
726
|
if (result.anyChanged) {
|
|
726
727
|
formatChanged = true;
|
|
727
728
|
dbg(
|
|
728
|
-
"autoformat: " +
|
|
729
|
+
"autoformat: " +
|
|
730
|
+
result.formatters
|
|
731
|
+
.map(
|
|
732
|
+
(f) => f.name + "(" + (f.changed ? "changed" : "unchanged") + ")",
|
|
733
|
+
)
|
|
734
|
+
.join(", "),
|
|
729
735
|
);
|
|
730
736
|
}
|
|
731
737
|
if (!result.allSucceeded) {
|
|
@@ -734,7 +740,10 @@ export async function runFormatPhase(
|
|
|
734
740
|
...failures.map((f) => `${f.name}: ${f.error ?? "unknown error"}`),
|
|
735
741
|
);
|
|
736
742
|
dbg(
|
|
737
|
-
"autoformat: " +
|
|
743
|
+
"autoformat: " +
|
|
744
|
+
failures
|
|
745
|
+
.map((f) => f.name + " failed: " + (f.error ?? "unknown error"))
|
|
746
|
+
.join("; "),
|
|
738
747
|
);
|
|
739
748
|
}
|
|
740
749
|
} catch (err) {
|
|
@@ -889,6 +898,7 @@ export async function runPipeline(
|
|
|
889
898
|
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
890
899
|
},
|
|
891
900
|
);
|
|
901
|
+
recordDiagnostics(filePath, dispatchResult.diagnostics);
|
|
892
902
|
const hasBlockers = dispatchResult.hasBlockers;
|
|
893
903
|
|
|
894
904
|
if (dispatchResult.diagnostics.length > 0) {
|
|
@@ -946,9 +956,17 @@ export async function runPipeline(
|
|
|
946
956
|
const changedList = [...piChangedFiles].map((changedFile) =>
|
|
947
957
|
toRunnerDisplayPath(cwd, changedFile),
|
|
948
958
|
);
|
|
949
|
-
const topFiles = changedList
|
|
950
|
-
|
|
951
|
-
|
|
959
|
+
const topFiles = changedList
|
|
960
|
+
.slice(0, 8)
|
|
961
|
+
.map((f) => " - " + f)
|
|
962
|
+
.join("\n");
|
|
963
|
+
const overflow =
|
|
964
|
+
changedList.length > 8
|
|
965
|
+
? "\n - ... and " + (changedList.length - 8) + " more"
|
|
966
|
+
: "";
|
|
967
|
+
const fileList = changedList.length
|
|
968
|
+
? "\nModified files:\n" + topFiles + overflow
|
|
969
|
+
: "";
|
|
952
970
|
output += `\n\n⚠️ **File was modified by auto-format/fix. You MUST re-read modified file(s) before making any further edits — the content on disk has changed (whitespace, indentation, quotes, or code). Editing from memory will produce mismatches.**${fileList}`;
|
|
953
971
|
}
|
|
954
972
|
phase.end("dispatch_lint", {
|
|
@@ -256,9 +256,9 @@ function validateConfig(root: string): CategoryResult {
|
|
|
256
256
|
|
|
257
257
|
const found: string[] = [];
|
|
258
258
|
|
|
259
|
-
for (const { file, critical
|
|
259
|
+
for (const { file, critical } of checks) {
|
|
260
260
|
const filePath = path.join(root, file);
|
|
261
|
-
const exists =
|
|
261
|
+
const exists = fs.existsSync(filePath);
|
|
262
262
|
if (exists) {
|
|
263
263
|
found.push(file);
|
|
264
264
|
} else if (critical) {
|
|
@@ -4,6 +4,19 @@ import * as path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const READ_GUARD_LOG_DIR = path.join(os.homedir(), ".pi-lens");
|
|
6
6
|
const READ_GUARD_LOG_FILE = path.join(READ_GUARD_LOG_DIR, "read-guard.log");
|
|
7
|
+
const READ_GUARD_LOG_BACKUP_FILE = path.join(
|
|
8
|
+
READ_GUARD_LOG_DIR,
|
|
9
|
+
"read-guard.log.1",
|
|
10
|
+
);
|
|
11
|
+
const MAX_LOG_BYTES = Math.max(
|
|
12
|
+
128 * 1024,
|
|
13
|
+
Number.parseInt(process.env.PI_LENS_READ_GUARD_MAX_BYTES ?? "1048576", 10) ||
|
|
14
|
+
1048576,
|
|
15
|
+
);
|
|
16
|
+
const VERBOSE_READ_GUARD_LOG =
|
|
17
|
+
process.env.PI_LENS_READ_GUARD_VERBOSE === "1" ||
|
|
18
|
+
process.env.PI_LENS_READ_GUARD_LOG === "verbose";
|
|
19
|
+
const LOG_ALLOWED_EDITS = process.env.PI_LENS_READ_GUARD_LOG_ALLOWS === "1";
|
|
7
20
|
|
|
8
21
|
try {
|
|
9
22
|
if (!fs.existsSync(READ_GUARD_LOG_DIR)) {
|
|
@@ -26,15 +39,42 @@ export interface ReadGuardLogEntry {
|
|
|
26
39
|
metadata?: Record<string, unknown>;
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
function shouldLogEvent(event: string): boolean {
|
|
43
|
+
if (VERBOSE_READ_GUARD_LOG) return true;
|
|
44
|
+
if (event === "edit_allowed") return LOG_ALLOWED_EDITS;
|
|
45
|
+
return (
|
|
46
|
+
event === "edit_blocked" ||
|
|
47
|
+
event === "edit_warned" ||
|
|
48
|
+
event === "exemption_added" ||
|
|
49
|
+
event === "oldtext_not_found" ||
|
|
50
|
+
event === "oldtext_duplicate" ||
|
|
51
|
+
event === "touched_lines_missing"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rotateIfNeeded(): void {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(READ_GUARD_LOG_FILE)) return;
|
|
58
|
+
const size = fs.statSync(READ_GUARD_LOG_FILE).size;
|
|
59
|
+
if (size < MAX_LOG_BYTES) return;
|
|
60
|
+
try {
|
|
61
|
+
fs.rmSync(READ_GUARD_LOG_BACKUP_FILE, { force: true });
|
|
62
|
+
} catch {}
|
|
63
|
+
fs.renameSync(READ_GUARD_LOG_FILE, READ_GUARD_LOG_BACKUP_FILE);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
export function logReadGuardEvent(entry: ReadGuardLogEntry): void {
|
|
30
68
|
if (
|
|
31
69
|
process.env.PI_LENS_TEST_MODE === "1" ||
|
|
32
|
-
(process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0")
|
|
70
|
+
(process.env.VITEST && process.env.PI_LENS_TEST_MODE !== "0") ||
|
|
71
|
+
!shouldLogEvent(entry.event)
|
|
33
72
|
) {
|
|
34
73
|
return;
|
|
35
74
|
}
|
|
36
75
|
const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
|
|
37
76
|
try {
|
|
77
|
+
rotateIfNeeded();
|
|
38
78
|
fs.appendFileSync(READ_GUARD_LOG_FILE, line);
|
|
39
79
|
} catch {}
|
|
40
80
|
}
|