pi-lens 2.2.9 → 3.0.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 (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { spawnSync } from "node:child_process";
11
11
  import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { safeSpawn } from "../../safe-spawn.js";
12
14
  import type {
13
15
  Diagnostic,
14
16
  DispatchContext,
@@ -16,6 +18,27 @@ import type {
16
18
  RunnerResult,
17
19
  } from "../types.js";
18
20
 
21
+ // Simple YAML fix: field extractor
22
+ function extractFixFromRule(ruleId: string, ruleDir: string): string | undefined {
23
+ try {
24
+ const rulePath = `${ruleDir}/${ruleId}.yml`;
25
+ if (!fs.existsSync(rulePath)) return undefined;
26
+
27
+ const content = fs.readFileSync(rulePath, "utf-8");
28
+ const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|\Z)/m);
29
+ if (fixMatch) {
30
+ return fixMatch[1]
31
+ .split("\n")
32
+ .map((line) => line.replace(/^\s*\|?\s*/, ""))
33
+ .filter((line) => line.length > 0)
34
+ .join("\n");
35
+ }
36
+ } catch {
37
+ // Ignore errors
38
+ }
39
+ return undefined;
40
+ }
41
+
19
42
  const astGrepRunner: RunnerDefinition = {
20
43
  id: "ast-grep",
21
44
  appliesTo: ["jsts", "python", "go", "rust", "cxx"],
@@ -24,11 +47,9 @@ const astGrepRunner: RunnerDefinition = {
24
47
  skipTestFiles: true, // Many rules are noisy in tests
25
48
 
26
49
  async run(ctx: DispatchContext): Promise<RunnerResult> {
27
- // Check if ast-grep is available
28
- const check = spawnSync("sg", ["--version"], {
29
- encoding: "utf-8",
50
+ // Check if ast-grep is available (use npx for local installs)
51
+ const check = safeSpawn("npx", ["sg", "--version"], {
30
52
  timeout: 5000,
31
- shell: true,
32
53
  });
33
54
 
34
55
  if (check.error || check.status !== 0) {
@@ -41,13 +62,11 @@ const astGrepRunner: RunnerDefinition = {
41
62
  return { status: "skipped", diagnostics: [], semantic: "none" };
42
63
  }
43
64
 
44
- // Run ast-grep scan on the file
45
- const args = ["scan", "--config", configPath, "--json", ctx.filePath];
65
+ // Run ast-grep scan on the file (use npx for local installs)
66
+ const args = ["sg", "scan", "--config", configPath, "--json", ctx.filePath];
46
67
 
47
- const result = spawnSync("sg", args, {
48
- encoding: "utf-8",
68
+ const result = safeSpawn("npx", args, {
49
69
  timeout: 30000,
50
- shell: true,
51
70
  });
52
71
 
53
72
  const raw = result.stdout + result.stderr;
@@ -57,7 +76,7 @@ const astGrepRunner: RunnerDefinition = {
57
76
  }
58
77
 
59
78
  // Parse results
60
- const diagnostics = parseAstGrepOutput(raw, ctx.filePath);
79
+ const diagnostics = parseAstGrepOutput(raw, ctx.filePath, configPath);
61
80
 
62
81
  if (diagnostics.length === 0) {
63
82
  return { status: "succeeded", diagnostics: [], semantic: "none" };
@@ -88,28 +107,61 @@ function findAstGrepConfig(cwd: string): string | undefined {
88
107
  return undefined;
89
108
  }
90
109
 
91
- function parseAstGrepOutput(raw: string, filePath: string): Diagnostic[] {
110
+ function parseAstGrepOutput(
111
+ raw: string,
112
+ filePath: string,
113
+ _configPath?: string,
114
+ ): Diagnostic[] {
92
115
  const diagnostics: Diagnostic[] = [];
93
116
 
94
117
  // Try to parse as JSON
118
+ // Determine rule directory for fix: extraction
119
+ const ruleDir = _configPath
120
+ ? path.dirname(_configPath).replace("/.sgconfig.yml", "/rules")
121
+ : path.join(process.cwd(), "rules", "ast-grep-rules", "rules");
122
+
95
123
  try {
96
124
  const parsed = JSON.parse(raw);
97
125
  if (Array.isArray(parsed)) {
98
126
  for (const item of parsed) {
99
127
  const line = item.range?.start?.line || 1;
128
+ const ruleId = item.rule || "unknown";
129
+
130
+ // Build message with inline fix suggestion
131
+ let message = item.message || item.lines || "";
132
+ let fixSuggestion: string | undefined;
133
+
134
+ if (item.replacement) {
135
+ // Show the actual code change inline in the message
136
+ const replacementPreview =
137
+ item.replacement.length > 40
138
+ ? `${item.replacement.substring(0, 40)}...`
139
+ : item.replacement;
140
+ message += `\n💡 Suggested fix: → "${replacementPreview}"`;
141
+ fixSuggestion = `Replace with: ${item.replacement}`;
142
+ } else {
143
+ // Try to get fix: from rule YAML
144
+ const ruleFix = extractFixFromRule(ruleId, ruleDir);
145
+ if (ruleFix) {
146
+ const fixPreview = ruleFix.length > 60
147
+ ? `${ruleFix.substring(0, 60)}...`
148
+ : ruleFix;
149
+ message += `\n💡 Suggested fix:\n${fixPreview}`;
150
+ fixSuggestion = ruleFix;
151
+ }
152
+ }
153
+
100
154
  diagnostics.push({
101
- id: `ast-grep-${line}-${item.rule || "unknown"}`,
102
- message: item.message || item.lines || "",
155
+ id: `ast-grep-${line}-${ruleId}`,
156
+ message,
103
157
  filePath,
104
158
  line,
105
159
  severity: item.severity === "error" ? "error" : "warning",
106
160
  semantic: item.severity === "error" ? "blocking" : "warning",
107
161
  tool: "ast-grep",
108
- rule: item.rule || "unknown",
109
- fixable: !!item.replacement,
110
- fixSuggestion: item.replacement
111
- ? "Run `sg fix` to auto-fix"
112
- : undefined,
162
+ rule: ruleId,
163
+ fixable: !!item.replacement || !!fixSuggestion,
164
+ fixSuggestion,
113
165
  });
114
166
  }
115
167
  }
@@ -3,46 +3,49 @@
3
3
  *
4
4
  * Requires: @biomejs/biome (npm install -D @biomejs/biome)
5
5
  */
6
- import { spawnSync } from "node:child_process";
7
- // Cache biome availability check
8
- let biomeAvailable = null;
9
- function isBiomeAvailable() {
10
- if (biomeAvailable !== null)
11
- return biomeAvailable;
12
- // Check if biome CLI is available (do NOT auto-install via npx)
13
- const check = spawnSync("biome", ["--version"], {
14
- encoding: "utf-8",
15
- timeout: 5000,
16
- shell: true,
17
- });
18
- biomeAvailable = !check.error && check.status === 0;
19
- return biomeAvailable;
20
- }
6
+ import { safeSpawn } from "../../safe-spawn.js";
7
+ import { createBiomeParser } from "./utils/diagnostic-parsers.js";
8
+ import { biome } from "./utils/runner-helpers.js";
21
9
  const biomeRunner = {
22
10
  id: "biome-lint",
23
11
  appliesTo: ["jsts", "json"],
24
12
  priority: 10,
25
13
  enabledByDefault: true,
26
14
  async run(ctx) {
27
- // Skip if biome is not installed
28
- if (!isBiomeAvailable()) {
29
- return { status: "skipped", diagnostics: [], semantic: "none" };
15
+ // Check if biome is available (via PATH, venv, or npx)
16
+ let cmd = biome.getCommand();
17
+ let useNpx = false;
18
+ if (!cmd || !biome.isAvailable(ctx.cwd)) {
19
+ // Try npx as fallback
20
+ const npxCheck = safeSpawn("npx", ["biome", "--version"], {
21
+ timeout: 5000,
22
+ });
23
+ if (!npxCheck.error && npxCheck.status === 0) {
24
+ cmd = "npx";
25
+ useNpx = true;
26
+ }
27
+ else {
28
+ return { status: "skipped", diagnostics: [], semantic: "none" };
29
+ }
30
30
  }
31
- // Run biome check (use direct command, not npx)
32
- const args = ctx.autofix
33
- ? ["check", "--write", ctx.filePath]
31
+ // IMPORTANT: Never use --write in dispatch runner to prevent infinite loops.
32
+ // Writing to the file would trigger another tool_result event, which would
33
+ // call dispatchLint again, creating a feedback loop.
34
+ // Use /lens-format command for explicit formatting, or autofix flags on
35
+ // the write/edit tools directly.
36
+ const args = useNpx
37
+ ? ["biome", "check", ctx.filePath]
34
38
  : ["check", ctx.filePath];
35
- const result = spawnSync("biome", args, {
36
- encoding: "utf-8",
39
+ const result = safeSpawn(cmd, args, {
37
40
  timeout: 30000,
38
- shell: true,
39
41
  });
40
42
  const output = result.stdout + result.stderr;
41
43
  if (result.status === 0) {
42
44
  return { status: "succeeded", diagnostics: [], semantic: "none" };
43
45
  }
44
- // Parse diagnostics
45
- const diagnostics = parseBiomeOutput(output, ctx.filePath, ctx.autofix);
46
+ // Parse diagnostics (never autofix in dispatch to prevent loops)
47
+ const parseBiomeOutput = createBiomeParser(false);
48
+ const diagnostics = parseBiomeOutput(output, ctx.filePath);
46
49
  return {
47
50
  status: "failed",
48
51
  diagnostics,
@@ -50,31 +53,4 @@ const biomeRunner = {
50
53
  };
51
54
  },
52
55
  };
53
- function parseBiomeOutput(raw, filePath, autofix) {
54
- const clean = raw.replace(/\x1b\[[0-9;]*m/g, "");
55
- const lines = clean.split("\n").filter((l) => l.trim());
56
- const diagnostics = [];
57
- for (const line of lines) {
58
- // Parse biome output format: file:line:col message (category)
59
- const match = line.match(/^(.+?):(\d+):(\d+)\s+(.+?)\s*\((.+?)\)/);
60
- if (match) {
61
- diagnostics.push({
62
- id: `biome-${match[2]}-${match[5]}`,
63
- message: `${match[5]}: ${match[4]}`,
64
- filePath,
65
- line: parseInt(match[2], 10),
66
- column: parseInt(match[3], 10),
67
- severity: line.includes("error") ? "error" : "warning",
68
- semantic: "warning",
69
- tool: "biome",
70
- rule: match[5],
71
- fixable: true,
72
- fixSuggestion: autofix
73
- ? "Auto-fix applied"
74
- : "Run with --autofix-biome to fix",
75
- });
76
- }
77
- }
78
- return diagnostics;
79
- }
80
56
  export default biomeRunner;
@@ -4,29 +4,14 @@
4
4
  * Requires: @biomejs/biome (npm install -D @biomejs/biome)
5
5
  */
6
6
 
7
- import { spawnSync } from "node:child_process";
7
+ import { safeSpawn } from "../../safe-spawn.js";
8
8
  import type {
9
- Diagnostic,
10
9
  DispatchContext,
11
10
  RunnerDefinition,
12
11
  RunnerResult,
13
12
  } from "../types.js";
14
-
15
- // Cache biome availability check
16
- let biomeAvailable: boolean | null = null;
17
-
18
- function isBiomeAvailable(): boolean {
19
- if (biomeAvailable !== null) return biomeAvailable;
20
-
21
- // Check if biome CLI is available (do NOT auto-install via npx)
22
- const check = spawnSync("biome", ["--version"], {
23
- encoding: "utf-8",
24
- timeout: 5000,
25
- shell: true,
26
- });
27
- biomeAvailable = !check.error && check.status === 0;
28
- return biomeAvailable;
29
- }
13
+ import { createBiomeParser } from "./utils/diagnostic-parsers.js";
14
+ import { biome } from "./utils/runner-helpers.js";
30
15
 
31
16
  const biomeRunner: RunnerDefinition = {
32
17
  id: "biome-lint",
@@ -35,20 +20,34 @@ const biomeRunner: RunnerDefinition = {
35
20
  enabledByDefault: true,
36
21
 
37
22
  async run(ctx: DispatchContext): Promise<RunnerResult> {
38
- // Skip if biome is not installed
39
- if (!isBiomeAvailable()) {
40
- return { status: "skipped", diagnostics: [], semantic: "none" };
23
+ // Check if biome is available (via PATH, venv, or npx)
24
+ let cmd = biome.getCommand();
25
+ let useNpx = false;
26
+
27
+ if (!cmd || !biome.isAvailable(ctx.cwd)) {
28
+ // Try npx as fallback
29
+ const npxCheck = safeSpawn("npx", ["biome", "--version"], {
30
+ timeout: 5000,
31
+ });
32
+ if (!npxCheck.error && npxCheck.status === 0) {
33
+ cmd = "npx";
34
+ useNpx = true;
35
+ } else {
36
+ return { status: "skipped", diagnostics: [], semantic: "none" };
37
+ }
41
38
  }
42
39
 
43
- // Run biome check (use direct command, not npx)
44
- const args = ctx.autofix
45
- ? ["check", "--write", ctx.filePath]
40
+ // IMPORTANT: Never use --write in dispatch runner to prevent infinite loops.
41
+ // Writing to the file would trigger another tool_result event, which would
42
+ // call dispatchLint again, creating a feedback loop.
43
+ // Use /lens-format command for explicit formatting, or autofix flags on
44
+ // the write/edit tools directly.
45
+ const args = useNpx
46
+ ? ["biome", "check", ctx.filePath]
46
47
  : ["check", ctx.filePath];
47
48
 
48
- const result = spawnSync("biome", args, {
49
- encoding: "utf-8",
49
+ const result = safeSpawn(cmd, args, {
50
50
  timeout: 30000,
51
- shell: true,
52
51
  });
53
52
 
54
53
  const output = result.stdout + result.stderr;
@@ -57,8 +56,9 @@ const biomeRunner: RunnerDefinition = {
57
56
  return { status: "succeeded", diagnostics: [], semantic: "none" };
58
57
  }
59
58
 
60
- // Parse diagnostics
61
- const diagnostics = parseBiomeOutput(output, ctx.filePath, ctx.autofix);
59
+ // Parse diagnostics (never autofix in dispatch to prevent loops)
60
+ const parseBiomeOutput = createBiomeParser(false);
61
+ const diagnostics = parseBiomeOutput(output, ctx.filePath);
62
62
 
63
63
  return {
64
64
  status: "failed",
@@ -68,38 +68,4 @@ const biomeRunner: RunnerDefinition = {
68
68
  },
69
69
  };
70
70
 
71
- function parseBiomeOutput(
72
- raw: string,
73
- filePath: string,
74
- autofix: boolean,
75
- ): Diagnostic[] {
76
- const clean = raw.replace(/\x1b\[[0-9;]*m/g, "");
77
- const lines = clean.split("\n").filter((l) => l.trim());
78
- const diagnostics: Diagnostic[] = [];
79
-
80
- for (const line of lines) {
81
- // Parse biome output format: file:line:col message (category)
82
- const match = line.match(/^(.+?):(\d+):(\d+)\s+(.+?)\s*\((.+?)\)/);
83
- if (match) {
84
- diagnostics.push({
85
- id: `biome-${match[2]}-${match[5]}`,
86
- message: `${match[5]}: ${match[4]}`,
87
- filePath,
88
- line: parseInt(match[2], 10),
89
- column: parseInt(match[3], 10),
90
- severity: line.includes("error") ? "error" : "warning",
91
- semantic: "warning",
92
- tool: "biome",
93
- rule: match[5],
94
- fixable: true,
95
- fixSuggestion: autofix
96
- ? "Auto-fix applied"
97
- : "Run with --autofix-biome to fix",
98
- });
99
- }
100
- }
101
-
102
- return diagnostics;
103
- }
104
-
105
71
  export default biomeRunner;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Config Validation Runner
3
+ *
4
+ * Validates config/environment variable access against actual config files.
5
+ * Detects undefined keys, typos, and missing env vars.
6
+ *
7
+ * Uses Tree-sitter to find config access patterns in code and validates
8
+ * against parsed config files (.env, config.ini, config.yaml, etc.)
9
+ */
10
+ import { createConfigValidator } from "../../config-validator.js";
11
+ const configValidationRunner = {
12
+ id: "config-validation",
13
+ appliesTo: ["jsts", "python", "go", "rust"],
14
+ priority: 8, // Run early, before other linters
15
+ enabledByDefault: true,
16
+ async run(ctx) {
17
+ // Only check supported file extensions
18
+ const ext = ctx.filePath.match(/\.(py|js|ts|tsx|go|rs)$/);
19
+ if (!ext) {
20
+ return { status: "skipped", diagnostics: [], semantic: "none" };
21
+ }
22
+ try {
23
+ const validator = await createConfigValidator(ctx.cwd);
24
+ const result = await validator.validateFile(ctx.filePath);
25
+ const diagnostics = [];
26
+ // Report undefined keys as warnings
27
+ for (const access of result.undefined) {
28
+ diagnostics.push({
29
+ id: `config-undefined:${access.key}`,
30
+ message: `Undefined config key: "${access.key}"`,
31
+ filePath: ctx.filePath,
32
+ line: access.line,
33
+ column: access.column,
34
+ severity: "warning",
35
+ semantic: "warning",
36
+ tool: "config-validation",
37
+ });
38
+ }
39
+ // Report typos with suggestion
40
+ for (const { access, suggestion } of result.typos) {
41
+ diagnostics.push({
42
+ id: `config-typo:${access.key}`,
43
+ message: `Possible typo: "${access.key}" — did you mean "${suggestion}"?`,
44
+ filePath: ctx.filePath,
45
+ line: access.line,
46
+ column: access.column,
47
+ severity: "warning",
48
+ semantic: "warning",
49
+ tool: "config-validation",
50
+ });
51
+ }
52
+ if (diagnostics.length === 0) {
53
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
54
+ }
55
+ return {
56
+ status: "failed",
57
+ diagnostics,
58
+ semantic: "warning",
59
+ };
60
+ }
61
+ catch (_err) {
62
+ // Silently fail if validator can't run (e.g., tree-sitter not available)
63
+ return { status: "skipped", diagnostics: [], semantic: "none" };
64
+ }
65
+ },
66
+ };
67
+ export default configValidationRunner;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Config Validation Runner
3
+ *
4
+ * Validates config/environment variable access against actual config files.
5
+ * Detects undefined keys, typos, and missing env vars.
6
+ *
7
+ * Uses Tree-sitter to find config access patterns in code and validates
8
+ * against parsed config files (.env, config.ini, config.yaml, etc.)
9
+ */
10
+
11
+ import { createConfigValidator } from "../../config-validator.js";
12
+ import type {
13
+ Diagnostic,
14
+ DispatchContext,
15
+ RunnerDefinition,
16
+ RunnerResult,
17
+ } from "../types.js";
18
+
19
+ const configValidationRunner: RunnerDefinition = {
20
+ id: "config-validation",
21
+ appliesTo: ["jsts", "python", "go", "rust"],
22
+ priority: 8, // Run early, before other linters
23
+ enabledByDefault: true,
24
+
25
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
26
+ // Only check supported file extensions
27
+ const ext = ctx.filePath.match(/\.(py|js|ts|tsx|go|rs)$/);
28
+ if (!ext) {
29
+ return { status: "skipped", diagnostics: [], semantic: "none" };
30
+ }
31
+
32
+ try {
33
+ const validator = await createConfigValidator(ctx.cwd);
34
+ const result = await validator.validateFile(ctx.filePath);
35
+
36
+ const diagnostics: Diagnostic[] = [];
37
+
38
+ // Report undefined keys as warnings
39
+ for (const access of result.undefined) {
40
+ diagnostics.push({
41
+ id: `config-undefined:${access.key}`,
42
+ message: `Undefined config key: "${access.key}"`,
43
+ filePath: ctx.filePath,
44
+ line: access.line,
45
+ column: access.column,
46
+ severity: "warning",
47
+ semantic: "warning",
48
+ tool: "config-validation",
49
+ });
50
+ }
51
+
52
+ // Report typos with suggestion
53
+ for (const { access, suggestion } of result.typos) {
54
+ diagnostics.push({
55
+ id: `config-typo:${access.key}`,
56
+ message: `Possible typo: "${access.key}" — did you mean "${suggestion}"?`,
57
+ filePath: ctx.filePath,
58
+ line: access.line,
59
+ column: access.column,
60
+ severity: "warning",
61
+ semantic: "warning",
62
+ tool: "config-validation",
63
+ });
64
+ }
65
+
66
+ if (diagnostics.length === 0) {
67
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
68
+ }
69
+
70
+ return {
71
+ status: "failed",
72
+ diagnostics,
73
+ semantic: "warning",
74
+ };
75
+ } catch (_err) {
76
+ // Silently fail if validator can't run (e.g., tree-sitter not available)
77
+ return { status: "skipped", diagnostics: [], semantic: "none" };
78
+ }
79
+ },
80
+ };
81
+
82
+ export default configValidationRunner;
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Runs `go vet` for Go files to catch common mistakes.
5
5
  */
6
- import { spawnSync } from "node:child_process";
6
+ import { safeSpawn } from "../../safe-spawn.js";
7
7
  import { stripAnsi } from "../../sanitize.js";
8
+ import { parseGoVetOutput } from "./utils/diagnostic-parsers.js";
8
9
  const goVetRunner = {
9
10
  id: "go-vet",
10
11
  appliesTo: ["go"],
@@ -12,19 +13,15 @@ const goVetRunner = {
12
13
  enabledByDefault: true,
13
14
  async run(ctx) {
14
15
  // Check if go is available
15
- const check = spawnSync("go", ["version"], {
16
- encoding: "utf-8",
16
+ const check = safeSpawn("go", ["version"], {
17
17
  timeout: 5000,
18
- shell: true,
19
18
  });
20
19
  if (check.error || check.status !== 0) {
21
20
  return { status: "skipped", diagnostics: [], semantic: "none" };
22
21
  }
23
22
  // Run go vet on the file
24
- const result = spawnSync("go", ["vet", ctx.filePath], {
25
- encoding: "utf-8",
23
+ const result = safeSpawn("go", ["vet", ctx.filePath], {
26
24
  timeout: 30000,
27
- shell: true,
28
25
  });
29
26
  const raw = stripAnsi(result.stdout + result.stderr);
30
27
  if (result.status === 0 && !raw.trim()) {
@@ -48,25 +45,4 @@ const goVetRunner = {
48
45
  };
49
46
  },
50
47
  };
51
- function parseGoVetOutput(raw, filePath) {
52
- const diagnostics = [];
53
- const lines = raw.split("\n");
54
- for (const line of lines) {
55
- // Parse go vet output: file:line:col: message
56
- const match = line.match(/^(.+?):(\d+):(\d+):\s*(.+)/);
57
- if (match) {
58
- diagnostics.push({
59
- id: `go-vet-${match[2]}`,
60
- message: match[4],
61
- filePath,
62
- line: parseInt(match[2], 10),
63
- column: parseInt(match[3], 10),
64
- severity: "warning",
65
- semantic: "warning",
66
- tool: "go-vet",
67
- });
68
- }
69
- }
70
- return diagnostics;
71
- }
72
48
  export default goVetRunner;
@@ -4,10 +4,10 @@
4
4
  * Runs `go vet` for Go files to catch common mistakes.
5
5
  */
6
6
 
7
- import { spawnSync } from "node:child_process";
7
+ import { safeSpawn } from "../../safe-spawn.js";
8
8
  import { stripAnsi } from "../../sanitize.js";
9
+ import { parseGoVetOutput } from "./utils/diagnostic-parsers.js";
9
10
  import type {
10
- Diagnostic,
11
11
  DispatchContext,
12
12
  RunnerDefinition,
13
13
  RunnerResult,
@@ -21,10 +21,8 @@ const goVetRunner: RunnerDefinition = {
21
21
 
22
22
  async run(ctx: DispatchContext): Promise<RunnerResult> {
23
23
  // Check if go is available
24
- const check = spawnSync("go", ["version"], {
25
- encoding: "utf-8",
24
+ const check = safeSpawn("go", ["version"], {
26
25
  timeout: 5000,
27
- shell: true,
28
26
  });
29
27
 
30
28
  if (check.error || check.status !== 0) {
@@ -32,10 +30,8 @@ const goVetRunner: RunnerDefinition = {
32
30
  }
33
31
 
34
32
  // Run go vet on the file
35
- const result = spawnSync("go", ["vet", ctx.filePath], {
36
- encoding: "utf-8",
33
+ const result = safeSpawn("go", ["vet", ctx.filePath], {
37
34
  timeout: 30000,
38
- shell: true,
39
35
  });
40
36
 
41
37
  const raw = stripAnsi(result.stdout + result.stderr);
@@ -65,28 +61,4 @@ const goVetRunner: RunnerDefinition = {
65
61
  },
66
62
  };
67
63
 
68
- function parseGoVetOutput(raw: string, filePath: string): Diagnostic[] {
69
- const diagnostics: Diagnostic[] = [];
70
- const lines = raw.split("\n");
71
-
72
- for (const line of lines) {
73
- // Parse go vet output: file:line:col: message
74
- const match = line.match(/^(.+?):(\d+):(\d+):\s*(.+)/);
75
- if (match) {
76
- diagnostics.push({
77
- id: `go-vet-${match[2]}`,
78
- message: match[4],
79
- filePath,
80
- line: parseInt(match[2], 10),
81
- column: parseInt(match[3], 10),
82
- severity: "warning",
83
- semantic: "warning",
84
- tool: "go-vet",
85
- });
86
- }
87
- }
88
-
89
- return diagnostics;
90
- }
91
-
92
64
  export default goVetRunner;