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
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Runs `cargo clippy` for Rust 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
8
  const rustClippyRunner = {
9
9
  id: "rust-clippy",
@@ -12,10 +12,8 @@ const rustClippyRunner = {
12
12
  enabledByDefault: true,
13
13
  async run(ctx) {
14
14
  // Check if cargo is available
15
- const check = spawnSync("cargo", ["--version"], {
16
- encoding: "utf-8",
15
+ const check = safeSpawn("cargo", ["--version"], {
17
16
  timeout: 5000,
18
- shell: true,
19
17
  });
20
18
  if (check.error || check.status !== 0) {
21
19
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -26,10 +24,8 @@ const rustClippyRunner = {
26
24
  return { status: "skipped", diagnostics: [], semantic: "none" };
27
25
  }
28
26
  // Run cargo clippy on the package
29
- const result = spawnSync("cargo", ["clippy", "--message-format=json", "-q"], {
30
- encoding: "utf-8",
27
+ const result = safeSpawn("cargo", ["clippy", "--message-format=json", "-q"], {
31
28
  timeout: 60000,
32
- shell: true,
33
29
  cwd: cargoToml.replace("Cargo.toml", ""),
34
30
  });
35
31
  const raw = stripAnsi(result.stdout + result.stderr);
@@ -57,8 +53,8 @@ const rustClippyRunner = {
57
53
  function findCargoToml(filePath) {
58
54
  const { dirname, join } = require("node:path");
59
55
  const { existsSync } = require("node:fs");
60
- let dir = filePath;
61
- for (let i = 0; i < 10; i++) {
56
+ let dir = dirname(filePath);
57
+ while (dir !== "/" && dir !== ".") {
62
58
  const cargoPath = join(dir, "Cargo.toml");
63
59
  if (existsSync(cargoPath)) {
64
60
  return cargoPath;
@@ -70,35 +66,35 @@ function findCargoToml(filePath) {
70
66
  }
71
67
  return undefined;
72
68
  }
73
- function parseClippyOutput(raw, targetFile) {
69
+ function parseClippyOutput(raw, filePath) {
74
70
  const diagnostics = [];
75
- const lines = raw.split("\n");
71
+ const lines = raw.split("\n").filter((l) => l.trim());
76
72
  for (const line of lines) {
77
- if (!line.trim())
78
- continue;
79
73
  try {
80
74
  const msg = JSON.parse(line);
81
- if (msg.message?.spans) {
82
- for (const span of msg.message.spans) {
83
- if (span.file_name?.includes(targetFile.replace(/\\/g, "/"))) {
84
- const diagFilePath = targetFile;
85
- diagnostics.push({
86
- id: `clippy-${span.line_start || 0}-${msg.message.code?.code || "unknown"}`,
87
- message: msg.message.message,
88
- filePath: diagFilePath,
89
- line: span.line_start,
90
- column: span.column_start,
91
- severity: msg.level === "error" ? "error" : "warning",
92
- semantic: msg.level === "error" ? "blocking" : "warning",
93
- tool: "clippy",
94
- rule: msg.message.code?.code || "clippy",
95
- });
96
- }
97
- }
98
- }
75
+ if (msg.reason !== "compiler-message")
76
+ continue;
77
+ const message = msg.message;
78
+ if (!message)
79
+ continue;
80
+ // Only include messages for this file or project-wide
81
+ const span = message.spans?.[0];
82
+ if (!span)
83
+ continue;
84
+ diagnostics.push({
85
+ id: `clippy-${message.code?.code || "unknown"}`,
86
+ message: message.message || "Clippy warning",
87
+ filePath: span.file || filePath,
88
+ line: span.line_start || 0,
89
+ column: span.column_start || 0,
90
+ severity: message.level === "error" ? "error" : "warning",
91
+ semantic: message.level === "error" ? "blocking" : "warning",
92
+ tool: "rust-clippy",
93
+ rule: message.code?.code,
94
+ });
99
95
  }
100
96
  catch {
101
- // Not JSON, skip
97
+ // Not a JSON line, skip
102
98
  }
103
99
  }
104
100
  return diagnostics;
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { spawnSync } from "node:child_process";
8
+ import { safeSpawn } from "../../safe-spawn.js";
8
9
  import { stripAnsi } from "../../sanitize.js";
9
10
  import type {
10
11
  Diagnostic,
@@ -21,10 +22,8 @@ const rustClippyRunner: RunnerDefinition = {
21
22
 
22
23
  async run(ctx: DispatchContext): Promise<RunnerResult> {
23
24
  // Check if cargo is available
24
- const check = spawnSync("cargo", ["--version"], {
25
- encoding: "utf-8",
25
+ const check = safeSpawn("cargo", ["--version"], {
26
26
  timeout: 5000,
27
- shell: true,
28
27
  });
29
28
 
30
29
  if (check.error || check.status !== 0) {
@@ -38,13 +37,11 @@ const rustClippyRunner: RunnerDefinition = {
38
37
  }
39
38
 
40
39
  // Run cargo clippy on the package
41
- const result = spawnSync(
40
+ const result = safeSpawn(
42
41
  "cargo",
43
42
  ["clippy", "--message-format=json", "-q"],
44
43
  {
45
- encoding: "utf-8",
46
44
  timeout: 60000,
47
- shell: true,
48
45
  cwd: cargoToml.replace("Cargo.toml", ""),
49
46
  },
50
47
  );
@@ -80,8 +77,8 @@ function findCargoToml(filePath: string): string | undefined {
80
77
  const { dirname, join } = require("node:path");
81
78
  const { existsSync } = require("node:fs");
82
79
 
83
- let dir = filePath;
84
- for (let i = 0; i < 10; i++) {
80
+ let dir = dirname(filePath);
81
+ while (dir !== "/" && dir !== ".") {
85
82
  const cargoPath = join(dir, "Cargo.toml");
86
83
  if (existsSync(cargoPath)) {
87
84
  return cargoPath;
@@ -90,38 +87,39 @@ function findCargoToml(filePath: string): string | undefined {
90
87
  if (parent === dir) break;
91
88
  dir = parent;
92
89
  }
90
+
93
91
  return undefined;
94
92
  }
95
93
 
96
- function parseClippyOutput(raw: string, targetFile: string): Diagnostic[] {
94
+ function parseClippyOutput(raw: string, filePath: string): Diagnostic[] {
97
95
  const diagnostics: Diagnostic[] = [];
98
- const lines = raw.split("\n");
96
+ const lines = raw.split("\n").filter((l) => l.trim());
99
97
 
100
98
  for (const line of lines) {
101
- if (!line.trim()) continue;
102
-
103
99
  try {
104
100
  const msg = JSON.parse(line);
105
- if (msg.message?.spans) {
106
- for (const span of msg.message.spans) {
107
- if (span.file_name?.includes(targetFile.replace(/\\/g, "/"))) {
108
- const diagFilePath = targetFile;
109
- diagnostics.push({
110
- id: `clippy-${span.line_start || 0}-${msg.message.code?.code || "unknown"}`,
111
- message: msg.message.message,
112
- filePath: diagFilePath,
113
- line: span.line_start,
114
- column: span.column_start,
115
- severity: msg.level === "error" ? "error" : "warning",
116
- semantic: msg.level === "error" ? "blocking" : "warning",
117
- tool: "clippy",
118
- rule: msg.message.code?.code || "clippy",
119
- });
120
- }
121
- }
122
- }
101
+ if (msg.reason !== "compiler-message") continue;
102
+
103
+ const message = msg.message;
104
+ if (!message) continue;
105
+
106
+ // Only include messages for this file or project-wide
107
+ const span = message.spans?.[0];
108
+ if (!span) continue;
109
+
110
+ diagnostics.push({
111
+ id: `clippy-${message.code?.code || "unknown"}`,
112
+ message: message.message || "Clippy warning",
113
+ filePath: span.file || filePath,
114
+ line: span.line_start || 0,
115
+ column: span.column_start || 0,
116
+ severity: message.level === "error" ? "error" : "warning",
117
+ semantic: message.level === "error" ? "blocking" : "warning",
118
+ tool: "rust-clippy",
119
+ rule: message.code?.code,
120
+ });
123
121
  } catch {
124
- // Not JSON, skip
122
+ // Not a JSON line, skip
125
123
  }
126
124
  }
127
125
 
@@ -0,0 +1,89 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ // Find all TS files
5
+ function findTsFiles(dir) {
6
+ const files = [];
7
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
8
+ for (const entry of entries) {
9
+ const fullPath = path.join(dir, entry.name);
10
+ // Skip node_modules, .git, etc
11
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".pi-lens") {
12
+ continue;
13
+ }
14
+ if (entry.isDirectory()) {
15
+ files.push(...findTsFiles(fullPath));
16
+ }
17
+ else if (entry.isFile() && fullPath.endsWith(".ts") && !fullPath.endsWith(".test.ts")) {
18
+ files.push(fullPath);
19
+ }
20
+ }
21
+ return files;
22
+ }
23
+ function createContext(filePath) {
24
+ return {
25
+ filePath,
26
+ cwd: process.cwd(),
27
+ kind: "jsts",
28
+ autofix: false,
29
+ deltaMode: false,
30
+ baselines: { get: () => [], add: () => { }, save: () => { } },
31
+ pi: {},
32
+ hasTool: async () => false,
33
+ log: () => { },
34
+ };
35
+ }
36
+ describe("Codebase scan with NAPI runner", () => {
37
+ it("should scan all TypeScript files and report findings", async () => {
38
+ const tsFiles = findTsFiles(process.cwd());
39
+ console.log(`\nFound ${tsFiles.length} TypeScript files to scan\n`);
40
+ const runner = (await import("./ast-grep-napi.js")).default;
41
+ const allIssues = [];
42
+ let totalTime = 0;
43
+ let filesWithIssues = 0;
44
+ for (let i = 0; i < Math.min(tsFiles.length, 50); i++) { // Limit to 50 for test speed
45
+ const file = tsFiles[i];
46
+ const ctx = createContext(file);
47
+ const start = Date.now();
48
+ const result = await runner.run(ctx);
49
+ const elapsed = Date.now() - start;
50
+ totalTime += elapsed;
51
+ if (result.diagnostics.length > 0) {
52
+ filesWithIssues++;
53
+ console.log(`${path.relative(process.cwd(), file)} (${elapsed}ms):`);
54
+ for (const d of result.diagnostics.slice(0, 5)) { // Show max 5 per file
55
+ const line = d.line ?? 0;
56
+ const rule = d.rule ?? "unknown";
57
+ const message = d.message?.split('\n')[0] ?? "";
58
+ console.log(` Line ${line}: [${rule}] ${message}`);
59
+ allIssues.push({
60
+ file: path.relative(process.cwd(), file),
61
+ line,
62
+ rule,
63
+ message,
64
+ });
65
+ }
66
+ if (result.diagnostics.length > 5) {
67
+ console.log(` ... and ${result.diagnostics.length - 5} more`);
68
+ }
69
+ }
70
+ }
71
+ console.log(`\n=== SUMMARY (first 50 files) ===`);
72
+ console.log(`Files scanned: ${Math.min(tsFiles.length, 50)}/${tsFiles.length}`);
73
+ console.log(`Total time: ${totalTime}ms`);
74
+ console.log(`Files with issues: ${filesWithIssues}`);
75
+ console.log(`Total issues: ${allIssues.length}`);
76
+ console.log(`Avg time per file: ${(totalTime / Math.min(tsFiles.length, 50)).toFixed(1)}ms`);
77
+ // Group by rule
78
+ const byRule = {};
79
+ for (const issue of allIssues) {
80
+ byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
81
+ }
82
+ console.log(`\n=== BY RULE ===`);
83
+ for (const [rule, count] of Object.entries(byRule).sort((a, b) => b[1] - a[1])) {
84
+ console.log(` ${rule}: ${count}`);
85
+ }
86
+ // This test should pass - we're just scanning
87
+ expect(true).toBe(true);
88
+ }, 60000); // 60 second timeout for scanning
89
+ });
@@ -0,0 +1,105 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import type { DispatchContext } from "../types.js";
5
+
6
+ // Find all TS files
7
+ function findTsFiles(dir: string): string[] {
8
+ const files: string[] = [];
9
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
10
+
11
+ for (const entry of entries) {
12
+ const fullPath = path.join(dir, entry.name);
13
+
14
+ // Skip node_modules, .git, etc
15
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".pi-lens") {
16
+ continue;
17
+ }
18
+
19
+ if (entry.isDirectory()) {
20
+ files.push(...findTsFiles(fullPath));
21
+ } else if (entry.isFile() && fullPath.endsWith(".ts") && !fullPath.endsWith(".test.ts")) {
22
+ files.push(fullPath);
23
+ }
24
+ }
25
+
26
+ return files;
27
+ }
28
+
29
+ function createContext(filePath: string): DispatchContext {
30
+ return {
31
+ filePath,
32
+ cwd: process.cwd(),
33
+ kind: "jsts",
34
+ autofix: false,
35
+ deltaMode: false,
36
+ baselines: { get: () => [], add: () => {}, save: () => {} } as any,
37
+ pi: {} as any,
38
+ hasTool: async () => false,
39
+ log: () => {},
40
+ };
41
+ }
42
+
43
+ describe("Codebase scan with NAPI runner", () => {
44
+ it("should scan all TypeScript files and report findings", async () => {
45
+ const tsFiles = findTsFiles(process.cwd());
46
+ console.log(`\nFound ${tsFiles.length} TypeScript files to scan\n`);
47
+
48
+ const runner = (await import("./ast-grep-napi.js")).default;
49
+
50
+ const allIssues: Array<{ file: string; line: number; rule: string; message: string }> = [];
51
+ let totalTime = 0;
52
+ let filesWithIssues = 0;
53
+
54
+ for (let i = 0; i < Math.min(tsFiles.length, 50); i++) { // Limit to 50 for test speed
55
+ const file = tsFiles[i];
56
+ const ctx = createContext(file);
57
+
58
+ const start = Date.now();
59
+ const result = await runner.run(ctx);
60
+ const elapsed = Date.now() - start;
61
+ totalTime += elapsed;
62
+
63
+ if (result.diagnostics.length > 0) {
64
+ filesWithIssues++;
65
+ console.log(`${path.relative(process.cwd(), file)} (${elapsed}ms):`);
66
+ for (const d of result.diagnostics.slice(0, 5)) { // Show max 5 per file
67
+ const line = d.line ?? 0;
68
+ const rule = d.rule ?? "unknown";
69
+ const message = d.message?.split('\n')[0] ?? "";
70
+ console.log(` Line ${line}: [${rule}] ${message}`);
71
+ allIssues.push({
72
+ file: path.relative(process.cwd(), file),
73
+ line,
74
+ rule,
75
+ message,
76
+ });
77
+ }
78
+ if (result.diagnostics.length > 5) {
79
+ console.log(` ... and ${result.diagnostics.length - 5} more`);
80
+ }
81
+ }
82
+ }
83
+
84
+ console.log(`\n=== SUMMARY (first 50 files) ===`);
85
+ console.log(`Files scanned: ${Math.min(tsFiles.length, 50)}/${tsFiles.length}`);
86
+ console.log(`Total time: ${totalTime}ms`);
87
+ console.log(`Files with issues: ${filesWithIssues}`);
88
+ console.log(`Total issues: ${allIssues.length}`);
89
+ console.log(`Avg time per file: ${(totalTime / Math.min(tsFiles.length, 50)).toFixed(1)}ms`);
90
+
91
+ // Group by rule
92
+ const byRule: Record<string, number> = {};
93
+ for (const issue of allIssues) {
94
+ byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
95
+ }
96
+
97
+ console.log(`\n=== BY RULE ===`);
98
+ for (const [rule, count] of Object.entries(byRule).sort((a, b) => b[1] - a[1])) {
99
+ console.log(` ${rule}: ${count}`);
100
+ }
101
+
102
+ // This test should pass - we're just scanning
103
+ expect(true).toBe(true);
104
+ }, 60000); // 60 second timeout for scanning
105
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Shellcheck runner for dispatch system
3
+ *
4
+ * Industry-standard linter for shell scripts (bash, sh, zsh).
5
+ * Detects syntax errors, undefined variables, quoting issues, and best practices.
6
+ *
7
+ * Why shellcheck?
8
+ * - Industry standard (used in CI/CD everywhere)
9
+ * - Comprehensive checks (syntax, variables, quotes, best practices)
10
+ * - JSON output for easy parsing
11
+ * - Available on all platforms (apt, brew, cargo, etc.)
12
+ *
13
+ * Alternative considered: bash-language-server
14
+ * - LSP approach like OpenCode uses
15
+ * - Richer features but heavier
16
+ * - shellcheck is simpler and faster for basic linting
17
+ *
18
+ * Install: apt install shellcheck, brew install shellcheck, or cargo install shellcheck
19
+ *
20
+ * Config: .shellcheckrc (optional, zero-config works)
21
+ */
22
+ import { safeSpawn } from "../../safe-spawn.js";
23
+ import { createAvailabilityChecker, createConfigFinder, } from "./utils/runner-helpers.js";
24
+ const shellcheck = createAvailabilityChecker("shellcheck", ".exe");
25
+ const findShellcheckConfig = createConfigFinder(".shellcheckrc");
26
+ /**
27
+ * Parse shellcheck JSON output
28
+ *
29
+ * Format: Array of check objects
30
+ * [{
31
+ * "file": "script.sh",
32
+ * "line": 10,
33
+ * "endLine": 10,
34
+ * "column": 5,
35
+ * "endColumn": 10,
36
+ * "level": "warning",
37
+ * "code": 2154,
38
+ * "message": "var is referenced but not assigned.",
39
+ * "fix": null
40
+ * }]
41
+ *
42
+ * Levels: "error", "warning", "info", "style"
43
+ */
44
+ function parseShellcheckOutput(raw, filePath) {
45
+ const diagnostics = [];
46
+ if (!raw.trim()) {
47
+ return diagnostics;
48
+ }
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ if (!Array.isArray(parsed)) {
52
+ return diagnostics;
53
+ }
54
+ for (const item of parsed) {
55
+ if (!item.message || !item.line)
56
+ continue;
57
+ // Map shellcheck levels to our severity
58
+ const severityMap = {
59
+ error: "error",
60
+ warning: "warning",
61
+ info: "info",
62
+ style: "info",
63
+ };
64
+ const severity = severityMap[item.level || "warning"] || "warning";
65
+ const ruleCode = item.code ? `SC${item.code}` : "unknown";
66
+ diagnostics.push({
67
+ id: `shellcheck-${item.line}-${ruleCode}`,
68
+ message: `[${ruleCode}] ${item.message}`,
69
+ filePath,
70
+ line: item.line,
71
+ column: item.column || 1,
72
+ severity,
73
+ semantic: severity === "error" ? "blocking" : "warning",
74
+ tool: "shellcheck",
75
+ rule: ruleCode,
76
+ fixable: !!item.fix,
77
+ });
78
+ }
79
+ }
80
+ catch {
81
+ // JSON parse failed, return empty
82
+ return diagnostics;
83
+ }
84
+ return diagnostics;
85
+ }
86
+ const shellcheckRunner = {
87
+ id: "shellcheck",
88
+ appliesTo: ["shell"],
89
+ priority: 20,
90
+ enabledByDefault: true,
91
+ skipTestFiles: false, // Shell scripts in test directories should still be checked
92
+ async run(ctx) {
93
+ // Skip if shellcheck is not installed
94
+ if (!shellcheck.isAvailable(ctx.cwd || process.cwd())) {
95
+ return { status: "skipped", diagnostics: [], semantic: "none" };
96
+ }
97
+ // Check if user explicitly disabled shellcheck
98
+ if (ctx.pi.getFlag("no-shellcheck")) {
99
+ return { status: "skipped", diagnostics: [], semantic: "none" };
100
+ }
101
+ // Determine shell dialect from file extension
102
+ const shellDialect = ctx.filePath.endsWith(".zsh")
103
+ ? "bash"
104
+ : ctx.filePath.endsWith(".fish")
105
+ ? "bash"
106
+ : ctx.filePath.endsWith(".sh")
107
+ ? "bash"
108
+ : "bash"; // Default to bash for generic shell files
109
+ // Build args
110
+ // --format json: JSON output
111
+ // --shell: Specify shell dialect (bash, sh, zsh, ksh, busybox)
112
+ // --severity: Minimum severity (we'll filter ourselves)
113
+ const args = [
114
+ "--format",
115
+ "json",
116
+ "--shell",
117
+ shellDialect,
118
+ ];
119
+ // Check for config file
120
+ const configPath = findShellcheckConfig(ctx.cwd);
121
+ if (!configPath) {
122
+ // No config file, use default settings
123
+ // Exclude "style" and "info" by default to reduce noise
124
+ args.push("--severity", "warning");
125
+ }
126
+ args.push(ctx.filePath);
127
+ const result = safeSpawn(shellcheck.getCommand(), args, {
128
+ timeout: 15000,
129
+ });
130
+ // shellcheck exits with code 1 if issues found, 0 if clean
131
+ if (result.status === 0 && !result.stdout?.trim()) {
132
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
133
+ }
134
+ // Parse diagnostics
135
+ const raw = result.stdout + result.stderr;
136
+ const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
137
+ if (diagnostics.length === 0) {
138
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
139
+ }
140
+ return {
141
+ status: "failed",
142
+ diagnostics,
143
+ semantic: "warning",
144
+ };
145
+ },
146
+ };
147
+ export default shellcheckRunner;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tests for shellcheck runner
3
+ */
4
+ import * as fs from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import * as path from "node:path";
7
+ import { describe, expect, it } from "vitest";
8
+ function createMockContext(filePath) {
9
+ return {
10
+ filePath,
11
+ cwd: process.cwd(),
12
+ kind: "shell",
13
+ autofix: false,
14
+ deltaMode: false,
15
+ baselines: { get: () => [], add: () => { }, save: () => { } },
16
+ pi: {},
17
+ hasTool: async () => false,
18
+ log: () => { },
19
+ };
20
+ }
21
+ // Helper for safe file cleanup
22
+ function safeUnlink(filePath) {
23
+ try {
24
+ if (fs.existsSync(filePath)) {
25
+ fs.unlinkSync(filePath);
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore cleanup errors on Windows
30
+ }
31
+ }
32
+ describe("shellcheck runner", () => {
33
+ const require = createRequire(import.meta.url);
34
+ it("should have correct runner definition", async () => {
35
+ const shellcheckModule = await import("./shellcheck.js");
36
+ const runner = shellcheckModule.default;
37
+ expect(runner.id).toBe("shellcheck");
38
+ expect(runner.appliesTo).toEqual(["shell"]);
39
+ expect(runner.priority).toBe(20);
40
+ expect(runner.enabledByDefault).toBe(true);
41
+ expect(runner.skipTestFiles).toBe(false);
42
+ });
43
+ it("should detect shellcheck availability", () => {
44
+ const { spawnSync } = require("node:child_process");
45
+ const result = spawnSync("shellcheck", ["--version"], {
46
+ encoding: "utf-8",
47
+ timeout: 10000,
48
+ shell: true,
49
+ });
50
+ expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy();
51
+ });
52
+ it("should detect undefined variable", async () => {
53
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_test_${Date.now()}.sh`);
54
+ fs.writeFileSync(tmpFile, ["#!/bin/bash", "# Test script with issues", 'echo "\$UNDEFINED_VAR"', ""].join("\n"));
55
+ try {
56
+ const shellcheckModule = await import("./shellcheck.js");
57
+ const runner = shellcheckModule.default;
58
+ const result = await runner.run(createMockContext(tmpFile));
59
+ if (result.status !== "skipped") {
60
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
61
+ expect(result.diagnostics.some((d) => d.tool === "shellcheck" &&
62
+ (d.message.includes("undefined") ||
63
+ d.message.includes("SC2154")))).toBe(true);
64
+ }
65
+ }
66
+ finally {
67
+ safeUnlink(tmpFile);
68
+ }
69
+ });
70
+ it("should pass clean shell scripts", async () => {
71
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_ok_${Date.now()}.sh`);
72
+ fs.writeFileSync(tmpFile, [
73
+ "#!/bin/bash",
74
+ "# Clean shell script",
75
+ "set -euo pipefail",
76
+ "",
77
+ "main() {",
78
+ ' local name="\${1:-world}"',
79
+ ' echo "Hello, \${name}!"',
80
+ "}",
81
+ "",
82
+ 'main "\$@"',
83
+ "",
84
+ ].join("\n"));
85
+ try {
86
+ const shellcheckModule = await import("./shellcheck.js");
87
+ const runner = shellcheckModule.default;
88
+ const result = await runner.run(createMockContext(tmpFile));
89
+ if (result.status !== "skipped") {
90
+ expect(result.diagnostics.length).toBe(0);
91
+ expect(result.status).toBe("succeeded");
92
+ }
93
+ }
94
+ finally {
95
+ safeUnlink(tmpFile);
96
+ }
97
+ });
98
+ });