pi-lens 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -3,9 +3,11 @@
3
3
  */
4
4
  import { registerRunner } from "../dispatcher.js";
5
5
  import architectRunner from "./architect.js";
6
+ import astGrepNapiRunner from "./ast-grep-napi.js";
6
7
  import biomeRunner from "./biome.js";
7
8
  import configValidationRunner from "./config-validation.js";
8
9
  import goVetRunner from "./go-vet.js";
10
+ import lspRunner from "./lsp.js";
9
11
  import oxlintRunner from "./oxlint.js";
10
12
  import pyrightRunner from "./pyright.js";
11
13
  import pythonSlopRunner from "./python-slop.js";
@@ -21,11 +23,13 @@ import tsLspRunner from "./ts-lsp.js";
21
23
  import tsSlopRunner from "./ts-slop.js";
22
24
  import typeSafetyRunner from "./type-safety.js";
23
25
  // Register all runners (ordered by priority)
24
- registerRunner(tsLspRunner); // TypeScript type-checking (priority 5)
25
- registerRunner(pyrightRunner); // Python type-checking (priority 5)
26
+ // Unified LSP runner for all languages (TypeScript, Python, Go, Rust, etc.) - priority 4
27
+ registerRunner(lspRunner); // Unified LSP type-checking for all languages (priority 4)
28
+ registerRunner(tsLspRunner); // TypeScript type-checking (priority 5) - fallback when --lens-lsp disabled
29
+ registerRunner(pyrightRunner); // Python type-checking (priority 5) - fallback when --lens-lsp disabled
26
30
  registerRunner(configValidationRunner); // Config/env validation (priority 8)
27
- // DISABLED: ast-grep-napi temporarily disabled for debugging
28
- // registerRunner(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15)
31
+ // DISABLED in post-write dispatch - ast-grep-napi can crash. Enabled via /lens-booboo plan only.
32
+ registerRunner(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15, post-write disabled)
29
33
  registerRunner(biomeRunner); // Biome formatting/linting (priority 10)
30
34
  registerRunner(oxlintRunner); // Oxlint fast JS/TS linter (priority 12)
31
35
  registerRunner(treeSitterRunner); // Tree-sitter structural analysis (priority 14)
@@ -4,9 +4,11 @@
4
4
 
5
5
  import { registerRunner } from "../dispatcher.js";
6
6
  import architectRunner from "./architect.js";
7
+ import astGrepNapiRunner from "./ast-grep-napi.js";
7
8
  import biomeRunner from "./biome.js";
8
9
  import configValidationRunner from "./config-validation.js";
9
10
  import goVetRunner from "./go-vet.js";
11
+ import lspRunner from "./lsp.js";
10
12
  import oxlintRunner from "./oxlint.js";
11
13
  import pyrightRunner from "./pyright.js";
12
14
  import pythonSlopRunner from "./python-slop.js";
@@ -23,11 +25,13 @@ import tsSlopRunner from "./ts-slop.js";
23
25
  import typeSafetyRunner from "./type-safety.js";
24
26
 
25
27
  // Register all runners (ordered by priority)
26
- registerRunner(tsLspRunner); // TypeScript type-checking (priority 5)
27
- registerRunner(pyrightRunner); // Python type-checking (priority 5)
28
+ // Unified LSP runner for all languages (TypeScript, Python, Go, Rust, etc.) - priority 4
29
+ registerRunner(lspRunner); // Unified LSP type-checking for all languages (priority 4)
30
+ registerRunner(tsLspRunner); // TypeScript type-checking (priority 5) - fallback when --lens-lsp disabled
31
+ registerRunner(pyrightRunner); // Python type-checking (priority 5) - fallback when --lens-lsp disabled
28
32
  registerRunner(configValidationRunner); // Config/env validation (priority 8)
29
- // DISABLED: ast-grep-napi temporarily disabled for debugging
30
- // registerRunner(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15)
33
+ // DISABLED in post-write dispatch - ast-grep-napi can crash. Enabled via /lens-booboo plan only.
34
+ registerRunner(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15, post-write disabled)
31
35
  registerRunner(biomeRunner); // Biome formatting/linting (priority 10)
32
36
  registerRunner(oxlintRunner); // Oxlint fast JS/TS linter (priority 12)
33
37
  registerRunner(treeSitterRunner); // Tree-sitter structural analysis (priority 14)
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Unified LSP Runner for pi-lens
3
+ *
4
+ * Handles type checking for ALL LSP-supported languages:
5
+ * - TypeScript/JavaScript (typescript-language-server)
6
+ * - Python (pyright/pylsp)
7
+ * - Go (gopls)
8
+ * - Rust (rust-analyzer)
9
+ * - Ruby, PHP, C#, Java, Kotlin, Swift, Dart, etc.
10
+ *
11
+ * Replaces language-specific runners (ts-lsp, pyright) with a single
12
+ * unified runner that delegates to the LSP service.
13
+ */
14
+ import { getLSPService } from "../../lsp/index.js";
15
+ import { readFileContent } from "./utils.js";
16
+ const lspRunner = {
17
+ id: "lsp",
18
+ appliesTo: ["jsts", "python", "go", "rust"], // Core LSP languages
19
+ priority: 4, // Run before everything (even ts-lsp was priority 5)
20
+ enabledByDefault: true,
21
+ async run(ctx) {
22
+ // Only run if --lens-lsp flag is enabled
23
+ if (!ctx.pi.getFlag("lens-lsp")) {
24
+ return { status: "skipped", diagnostics: [], semantic: "none" };
25
+ }
26
+ const lspService = getLSPService();
27
+ // Check if we have LSP available for this file
28
+ const hasLSP = await lspService.hasLSP(ctx.filePath);
29
+ if (!hasLSP) {
30
+ return { status: "skipped", diagnostics: [], semantic: "none" };
31
+ }
32
+ // Read file content
33
+ const content = readFileContent(ctx.filePath);
34
+ if (!content) {
35
+ return { status: "skipped", diagnostics: [], semantic: "none" };
36
+ }
37
+ // Open file in LSP and get diagnostics
38
+ await lspService.openFile(ctx.filePath, content);
39
+ // getDiagnostics() internally calls waitForDiagnostics() with bus
40
+ // subscription + 150ms debounce + 3s timeout
41
+ const lspDiags = await lspService.getDiagnostics(ctx.filePath);
42
+ // Convert LSP diagnostics to our format
43
+ // Defensive: filter out malformed diagnostics that may lack range
44
+ const diagnostics = lspDiags
45
+ .filter((d) => d.range?.start?.line !== undefined)
46
+ .map((d) => ({
47
+ id: `lsp:${d.code ?? "unknown"}:${d.range.start.line}`,
48
+ message: d.message,
49
+ filePath: ctx.filePath,
50
+ line: d.range.start.line + 1,
51
+ column: d.range.start.character + 1,
52
+ severity: d.severity === 1 ? "error" : d.severity === 2 ? "warning" : "info",
53
+ semantic: d.severity === 1 ? "blocking" : "warning",
54
+ tool: "lsp",
55
+ code: String(d.code ?? ""),
56
+ }));
57
+ const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
58
+ return {
59
+ status: hasErrors ? "failed" : "succeeded",
60
+ diagnostics,
61
+ semantic: hasErrors ? "blocking" : "warning",
62
+ };
63
+ },
64
+ };
65
+ export default lspRunner;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Unified LSP Runner for pi-lens
3
+ *
4
+ * Handles type checking for ALL LSP-supported languages:
5
+ * - TypeScript/JavaScript (typescript-language-server)
6
+ * - Python (pyright/pylsp)
7
+ * - Go (gopls)
8
+ * - Rust (rust-analyzer)
9
+ * - Ruby, PHP, C#, Java, Kotlin, Swift, Dart, etc.
10
+ *
11
+ * Replaces language-specific runners (ts-lsp, pyright) with a single
12
+ * unified runner that delegates to the LSP service.
13
+ */
14
+
15
+ import { getLSPService } from "../../lsp/index.js";
16
+ import type {
17
+ Diagnostic,
18
+ DispatchContext,
19
+ RunnerDefinition,
20
+ RunnerResult,
21
+ } from "../types.js";
22
+ import { readFileContent } from "./utils.js";
23
+
24
+ const lspRunner: RunnerDefinition = {
25
+ id: "lsp",
26
+ appliesTo: ["jsts", "python", "go", "rust"], // Core LSP languages
27
+ priority: 4, // Run before everything (even ts-lsp was priority 5)
28
+ enabledByDefault: true,
29
+
30
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
31
+ // Only run if --lens-lsp flag is enabled
32
+ if (!ctx.pi.getFlag("lens-lsp")) {
33
+ return { status: "skipped", diagnostics: [], semantic: "none" };
34
+ }
35
+
36
+ const lspService = getLSPService();
37
+
38
+ // Check if we have LSP available for this file
39
+ const hasLSP = await lspService.hasLSP(ctx.filePath);
40
+ if (!hasLSP) {
41
+ return { status: "skipped", diagnostics: [], semantic: "none" };
42
+ }
43
+
44
+ // Read file content
45
+ const content = readFileContent(ctx.filePath);
46
+ if (!content) {
47
+ return { status: "skipped", diagnostics: [], semantic: "none" };
48
+ }
49
+
50
+ // Try to open file in LSP and get diagnostics
51
+ // If the server fails to spawn or crashes, this will be caught
52
+ let lspDiags: import("../../lsp/client.js").LSPDiagnostic[] = [];
53
+ let serverFailed = false;
54
+ let failureReason = "";
55
+
56
+ try {
57
+ await lspService.openFile(ctx.filePath, content);
58
+ // getDiagnostics() internally calls waitForDiagnostics() with bus
59
+ // subscription + 150ms debounce + 3s timeout
60
+ lspDiags = await lspService.getDiagnostics(ctx.filePath);
61
+ } catch (err) {
62
+ serverFailed = true;
63
+ failureReason = err instanceof Error ? err.message : String(err);
64
+ // Check if this is a server spawn/connection error
65
+ if (
66
+ failureReason.includes("spawn") ||
67
+ failureReason.includes("exited") ||
68
+ failureReason.includes("connection") ||
69
+ failureReason.includes("JSON RPC")
70
+ ) {
71
+ // Mark this server as broken so we don't keep trying
72
+ console.error(
73
+ `[lsp-runner] LSP server failed for ${ctx.filePath}: ${failureReason}`,
74
+ );
75
+ }
76
+ }
77
+
78
+ // If server failed to provide diagnostics, report as failed status
79
+ if (serverFailed) {
80
+ return {
81
+ status: "failed",
82
+ diagnostics: [
83
+ {
84
+ id: `lsp:server-error:0`,
85
+ message: `LSP server failed: ${failureReason}`,
86
+ filePath: ctx.filePath,
87
+ line: 1,
88
+ column: 1,
89
+ severity: "error",
90
+ semantic: "warning", // Don't block - fallback to other runners
91
+ tool: "lsp",
92
+ },
93
+ ],
94
+ semantic: "warning",
95
+ };
96
+ }
97
+
98
+ // Convert LSP diagnostics to our format
99
+ // Defensive: filter out malformed diagnostics that may lack range
100
+ const diagnostics: Diagnostic[] = lspDiags
101
+ .filter((d) => d.range?.start?.line !== undefined)
102
+ .map((d) => ({
103
+ id: `lsp:${d.code ?? "unknown"}:${d.range.start.line}`,
104
+ message: d.message,
105
+ filePath: ctx.filePath,
106
+ line: d.range.start.line + 1,
107
+ column: d.range.start.character + 1,
108
+ severity:
109
+ d.severity === 1 ? "error" : d.severity === 2 ? "warning" : "info",
110
+ semantic: d.severity === 1 ? "blocking" : "warning",
111
+ tool: "lsp",
112
+ code: String(d.code ?? ""),
113
+ }));
114
+
115
+ const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
116
+
117
+ return {
118
+ status: hasErrors ? "failed" : "succeeded",
119
+ diagnostics,
120
+ semantic: hasErrors ? "blocking" : "warning",
121
+ };
122
+ },
123
+ };
124
+
125
+ export default lspRunner;
@@ -13,8 +13,8 @@ const oxlintRunner = {
13
13
  id: "oxlint",
14
14
  appliesTo: ["jsts"],
15
15
  priority: 12,
16
- enabledByDefault: true,
17
- skipTestFiles: false,
16
+ enabledByDefault: false, // Opt-in: may conflict with ESLint in existing projects
17
+ skipTestFiles: true,
18
18
  async run(ctx) {
19
19
  const cwd = ctx.cwd || process.cwd();
20
20
  // Check if oxlint is available
@@ -22,8 +22,8 @@ const oxlintRunner: RunnerDefinition = {
22
22
  id: "oxlint",
23
23
  appliesTo: ["jsts"],
24
24
  priority: 12,
25
- enabledByDefault: true,
26
- skipTestFiles: false,
25
+ enabledByDefault: false, // Opt-in: may conflict with ESLint in existing projects
26
+ skipTestFiles: true,
27
27
 
28
28
  async run(ctx: DispatchContext): Promise<RunnerResult> {
29
29
  const cwd = ctx.cwd || process.cwd();
@@ -17,15 +17,31 @@ const pyrightRunner = {
17
17
  enabledByDefault: true,
18
18
  async run(ctx) {
19
19
  const cwd = ctx.cwd || process.cwd();
20
- // Auto-install pyright if not available (it's one of the 4 auto-install tools)
21
- if (!pyright.isAvailable(cwd)) {
22
- const installed = await ensureTool("pyright");
23
- if (!installed) {
24
- return { status: "skipped", diagnostics: [], semantic: "none" };
25
- }
20
+ // Get pyright command - try multiple strategies
21
+ let cmd = null;
22
+ // Strategy 1: Check cached availability (fast path)
23
+ if (pyright.isAvailable(cwd)) {
24
+ cmd = pyright.getCommand();
25
+ }
26
+ // Strategy 2: Try to find pyright via ensureTool (installs if needed)
27
+ if (!cmd) {
28
+ const installedPath = await ensureTool("pyright");
29
+ if (installedPath)
30
+ cmd = installedPath;
31
+ }
32
+ // Strategy 3: Direct PATH check (handles module cache staleness)
33
+ if (!cmd) {
34
+ const { findCommandAsync } = await import("../../safe-spawn.js");
35
+ const foundCmd = await findCommandAsync("pyright");
36
+ if (foundCmd)
37
+ cmd = foundCmd;
38
+ }
39
+ // If still no pyright, skip this runner
40
+ if (!cmd) {
41
+ return { status: "skipped", diagnostics: [], semantic: "none" };
26
42
  }
27
- // Run pyright with JSON output (use venv-local or global command)
28
- const result = await safeSpawnAsync(pyright.getCommand(), ["--outputjson", ctx.filePath], {
43
+ // Run pyright with JSON output
44
+ const result = await safeSpawnAsync(cmd, ["--outputjson", ctx.filePath], {
29
45
  timeout: 60000,
30
46
  });
31
47
  // Pyright returns non-zero when errors found, that's OK
@@ -28,22 +28,36 @@ const pyrightRunner: RunnerDefinition = {
28
28
  async run(ctx: DispatchContext): Promise<RunnerResult> {
29
29
  const cwd = ctx.cwd || process.cwd();
30
30
 
31
- // Auto-install pyright if not available (it's one of the 4 auto-install tools)
32
- if (!pyright.isAvailable(cwd)) {
33
- const installed = await ensureTool("pyright");
34
- if (!installed) {
35
- return { status: "skipped", diagnostics: [], semantic: "none" };
36
- }
31
+ // Get pyright command - try multiple strategies
32
+ let cmd: string | null = null;
33
+
34
+ // Strategy 1: Check cached availability (fast path)
35
+ if (pyright.isAvailable(cwd)) {
36
+ cmd = pyright.getCommand();
37
+ }
38
+
39
+ // Strategy 2: Try to find pyright via ensureTool (installs if needed)
40
+ if (!cmd) {
41
+ const installedPath = await ensureTool("pyright");
42
+ if (installedPath) cmd = installedPath;
43
+ }
44
+
45
+ // Strategy 3: Direct PATH check (handles module cache staleness)
46
+ if (!cmd) {
47
+ const { findCommandAsync } = await import("../../safe-spawn.js");
48
+ const foundCmd: string | null = await findCommandAsync("pyright");
49
+ if (foundCmd) cmd = foundCmd;
37
50
  }
38
51
 
39
- // Run pyright with JSON output (use venv-local or global command)
40
- const result = await safeSpawnAsync(
41
- pyright.getCommand()!,
42
- ["--outputjson", ctx.filePath],
43
- {
44
- timeout: 60000,
45
- },
46
- );
52
+ // If still no pyright, skip this runner
53
+ if (!cmd) {
54
+ return { status: "skipped", diagnostics: [], semantic: "none" };
55
+ }
56
+
57
+ // Run pyright with JSON output
58
+ const result = await safeSpawnAsync(cmd, ["--outputjson", ctx.filePath], {
59
+ timeout: 60000,
60
+ });
47
61
 
48
62
  // Pyright returns non-zero when errors found, that's OK
49
63
  if (result.error) {
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Runs `cargo clippy` for Rust files to catch common mistakes.
5
5
  */
6
+ import { existsSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
6
8
  import { safeSpawn } from "../../safe-spawn.js";
7
9
  import { stripAnsi } from "../../sanitize.js";
8
10
  const rustClippyRunner = {
@@ -51,8 +53,6 @@ const rustClippyRunner = {
51
53
  },
52
54
  };
53
55
  function findCargoToml(filePath) {
54
- const { dirname, join } = require("node:path");
55
- const { existsSync } = require("node:fs");
56
56
  let dir = dirname(filePath);
57
57
  while (dir !== "/" && dir !== ".") {
58
58
  const cargoPath = join(dir, "Cargo.toml");
@@ -4,7 +4,8 @@
4
4
  * Runs `cargo clippy` for Rust files to catch common mistakes.
5
5
  */
6
6
 
7
- import { spawnSync } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
8
9
  import { safeSpawn } from "../../safe-spawn.js";
9
10
  import { stripAnsi } from "../../sanitize.js";
10
11
  import type {
@@ -74,9 +75,6 @@ const rustClippyRunner: RunnerDefinition = {
74
75
  };
75
76
 
76
77
  function findCargoToml(filePath: string): string | undefined {
77
- const { dirname, join } = require("node:path");
78
- const { existsSync } = require("node:fs");
79
-
80
78
  let dir = dirname(filePath);
81
79
  while (dir !== "/" && dir !== ".") {
82
80
  const cargoPath = join(dir, "Cargo.toml");
@@ -3,10 +3,21 @@
3
3
  *
4
4
  * Executes all loaded tree-sitter query files from rules/tree-sitter-queries/
5
5
  * for fast AST-based pattern matching.
6
+ * Updated: ast-grep-napi test
6
7
  */
7
8
  import path from "node:path";
8
9
  import { TreeSitterClient } from "../../tree-sitter-client.js";
9
10
  import { queryLoader } from "../../tree-sitter-query-loader.js";
11
+ // Module-level singleton: web-tree-sitter WASM must only be initialized once per process.
12
+ // Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
13
+ // WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
14
+ let _sharedClient = null;
15
+ function getSharedClient() {
16
+ if (!_sharedClient) {
17
+ _sharedClient = new TreeSitterClient();
18
+ }
19
+ return _sharedClient;
20
+ }
10
21
  const treeSitterRunner = {
11
22
  id: "tree-sitter",
12
23
  appliesTo: ["jsts", "python"],
@@ -14,8 +25,8 @@ const treeSitterRunner = {
14
25
  enabledByDefault: true,
15
26
  skipTestFiles: false, // Run on test files too (structural issues matter there)
16
27
  async run(ctx) {
17
- // Initialize tree-sitter client
18
- const client = new TreeSitterClient();
28
+ // Use singleton client — WASM must never be re-initialized after first call
29
+ const client = getSharedClient();
19
30
  if (!client.isAvailable()) {
20
31
  return { status: "skipped", diagnostics: [], semantic: "none" };
21
32
  }
@@ -105,3 +116,4 @@ const treeSitterRunner = {
105
116
  },
106
117
  };
107
118
  export default treeSitterRunner;
119
+ // test ast-grep-napi re-enable
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Executes all loaded tree-sitter query files from rules/tree-sitter-queries/
5
5
  * for fast AST-based pattern matching.
6
+ * Updated: ast-grep-napi test
6
7
  */
7
8
 
8
9
  import path from "node:path";
@@ -15,6 +16,17 @@ import type {
15
16
  RunnerResult,
16
17
  } from "../types.js";
17
18
 
19
+ // Module-level singleton: web-tree-sitter WASM must only be initialized once per process.
20
+ // Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
21
+ // WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
22
+ let _sharedClient: TreeSitterClient | null = null;
23
+ function getSharedClient(): TreeSitterClient {
24
+ if (!_sharedClient) {
25
+ _sharedClient = new TreeSitterClient();
26
+ }
27
+ return _sharedClient;
28
+ }
29
+
18
30
  const treeSitterRunner: RunnerDefinition = {
19
31
  id: "tree-sitter",
20
32
  appliesTo: ["jsts", "python"],
@@ -23,8 +35,8 @@ const treeSitterRunner: RunnerDefinition = {
23
35
  skipTestFiles: false, // Run on test files too (structural issues matter there)
24
36
 
25
37
  async run(ctx: DispatchContext): Promise<RunnerResult> {
26
- // Initialize tree-sitter client
27
- const client = new TreeSitterClient();
38
+ // Use singleton client — WASM must never be re-initialized after first call
39
+ const client = getSharedClient();
28
40
  if (!client.isAvailable()) {
29
41
  return { status: "skipped", diagnostics: [], semantic: "none" };
30
42
  }
@@ -133,3 +145,4 @@ const treeSitterRunner: RunnerDefinition = {
133
145
  };
134
146
 
135
147
  export default treeSitterRunner;
148
+ // test ast-grep-napi re-enable
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @deprecated The built-in TypeScriptClient is deprecated. Use --lens-lsp for full LSP support.
8
8
  */
9
- import { TypeScriptClient } from "../../typescript-client.js";
10
9
  import { getLSPService } from "../../lsp/index.js";
10
+ import { TypeScriptClient } from "../../typescript-client.js";
11
11
  import { readFileContent } from "./utils.js";
12
12
  const tsLspRunner = {
13
13
  id: "ts-lsp",
@@ -45,8 +45,8 @@ async function runWithLSPClient(ctx) {
45
45
  }
46
46
  // Open file in LSP and get diagnostics
47
47
  await lspService.openFile(ctx.filePath, content);
48
- // Small delay to let diagnostics propagate
49
- await new Promise(r => setTimeout(r, 500));
48
+ // getDiagnostics() internally calls waitForDiagnostics() with bus
49
+ // subscription + 150ms debounce + 3s timeout
50
50
  const lspDiags = await lspService.getDiagnostics(ctx.filePath);
51
51
  // Convert LSP diagnostics to our format
52
52
  // Defensive: filter out malformed diagnostics that may lack range
@@ -7,8 +7,8 @@
7
7
  * @deprecated The built-in TypeScriptClient is deprecated. Use --lens-lsp for full LSP support.
8
8
  */
9
9
 
10
- import { TypeScriptClient } from "../../typescript-client.js";
11
10
  import { getLSPService } from "../../lsp/index.js";
11
+ import { TypeScriptClient } from "../../typescript-client.js";
12
12
  import type {
13
13
  Diagnostic,
14
14
  DispatchContext,
@@ -60,8 +60,8 @@ async function runWithLSPClient(ctx: DispatchContext): Promise<RunnerResult> {
60
60
 
61
61
  // Open file in LSP and get diagnostics
62
62
  await lspService.openFile(ctx.filePath, content);
63
- // Small delay to let diagnostics propagate
64
- await new Promise(r => setTimeout(r, 500));
63
+ // getDiagnostics() internally calls waitForDiagnostics() with bus
64
+ // subscription + 150ms debounce + 3s timeout
65
65
  const lspDiags = await lspService.getDiagnostics(ctx.filePath);
66
66
 
67
67
  // Convert LSP diagnostics to our format
@@ -74,7 +74,8 @@ async function runWithLSPClient(ctx: DispatchContext): Promise<RunnerResult> {
74
74
  filePath: ctx.filePath,
75
75
  line: d.range.start.line + 1,
76
76
  column: d.range.start.character + 1,
77
- severity: d.severity === 1 ? "error" : d.severity === 2 ? "warning" : "info",
77
+ severity:
78
+ d.severity === 1 ? "error" : d.severity === 2 ? "warning" : "info",
78
79
  semantic: d.severity === 1 ? "blocking" : "warning",
79
80
  tool: "ts-lsp",
80
81
  code: String(d.code ?? ""),
@@ -91,7 +92,9 @@ async function runWithLSPClient(ctx: DispatchContext): Promise<RunnerResult> {
91
92
  * Run with deprecated built-in TypeScriptClient
92
93
  * @deprecated Use runWithLSPClient instead
93
94
  */
94
- async function runWithBuiltinClient(ctx: DispatchContext): Promise<RunnerResult> {
95
+ async function runWithBuiltinClient(
96
+ ctx: DispatchContext,
97
+ ): Promise<RunnerResult> {
95
98
  const tsClient = new TypeScriptClient();
96
99
 
97
100
  const content = readFileContent(ctx.filePath);