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
@@ -7,10 +7,10 @@
7
7
  * Requires: pip install ruff
8
8
  * Docs: https://docs.astral.sh/ruff/
9
9
  */
10
- import { spawnSync } from "node:child_process";
11
10
  import * as fs from "node:fs";
12
11
  import * as path from "node:path";
13
12
  import { isFileKind } from "./file-kinds.js";
13
+ import { safeSpawn } from "./safe-spawn.js";
14
14
  // --- Client ---
15
15
  export class RuffClient {
16
16
  constructor(verbose = false) {
@@ -26,10 +26,8 @@ export class RuffClient {
26
26
  if (this.ruffAvailable !== null)
27
27
  return this.ruffAvailable;
28
28
  try {
29
- const result = spawnSync("ruff", ["--version"], {
30
- encoding: "utf-8",
29
+ const result = safeSpawn("ruff", ["--version"], {
31
30
  timeout: 5000,
32
- shell: true,
33
31
  });
34
32
  this.ruffAvailable = !result.error && result.status === 0;
35
33
  if (this.ruffAvailable) {
@@ -58,7 +56,7 @@ export class RuffClient {
58
56
  if (!fs.existsSync(absolutePath))
59
57
  return [];
60
58
  try {
61
- const result = spawnSync("ruff", [
59
+ const result = safeSpawn("ruff", [
62
60
  "check",
63
61
  "--output-format",
64
62
  "json",
@@ -66,9 +64,7 @@ export class RuffClient {
66
64
  "py310",
67
65
  absolutePath,
68
66
  ], {
69
- encoding: "utf-8",
70
67
  timeout: 10000,
71
- shell: true,
72
68
  });
73
69
  // ruff exits 1 when it finds issues (normal)
74
70
  const output = result.stdout || "";
@@ -91,10 +87,8 @@ export class RuffClient {
91
87
  if (!fs.existsSync(absolutePath))
92
88
  return "";
93
89
  try {
94
- const result = spawnSync("ruff", ["format", "--check", "--diff", absolutePath], {
95
- encoding: "utf-8",
90
+ const result = safeSpawn("ruff", ["format", "--check", "--diff", absolutePath], {
96
91
  timeout: 10000,
97
- shell: true,
98
92
  });
99
93
  // ruff format --check exits 1 when changes needed
100
94
  if (result.status === 0)
@@ -136,10 +130,8 @@ export class RuffClient {
136
130
  try {
137
131
  const beforeDiags = this.checkFile(filePath);
138
132
  const fixableCount = beforeDiags.filter((d) => d.fixable).length;
139
- const result = spawnSync("ruff", ["check", "--fix", absolutePath], {
140
- encoding: "utf-8",
133
+ const result = safeSpawn("ruff", ["check", "--fix", absolutePath], {
141
134
  timeout: 15000,
142
- shell: true,
143
135
  });
144
136
  if (result.error) {
145
137
  return {
@@ -160,6 +152,56 @@ export class RuffClient {
160
152
  return { success: false, changed: false, fixed: 0, error: err.message };
161
153
  }
162
154
  }
155
+ /**
156
+ * Fix multiple Python files at once (much faster than file-by-file)
157
+ */
158
+ fixFiles(filePaths) {
159
+ if (!this.isAvailable()) {
160
+ return {
161
+ success: false,
162
+ fixed: 0,
163
+ changed: 0,
164
+ error: "Ruff not available",
165
+ };
166
+ }
167
+ // Filter to existing Python files
168
+ const validFiles = filePaths
169
+ .map(f => path.resolve(f))
170
+ .filter(f => fs.existsSync(f) && f.endsWith(".py"));
171
+ if (validFiles.length === 0) {
172
+ return { success: true, fixed: 0, changed: 0 };
173
+ }
174
+ try {
175
+ // Count fixable issues before fixing
176
+ let totalFixable = 0;
177
+ for (const file of validFiles) {
178
+ const diags = this.checkFile(file);
179
+ totalFixable += diags.filter(d => d.fixable).length;
180
+ }
181
+ // Run ruff once on all files - much faster than per file
182
+ const result = safeSpawn("ruff", ["check", "--fix", ...validFiles], {
183
+ timeout: 60000, // Longer timeout for batch
184
+ });
185
+ if (result.error) {
186
+ return {
187
+ success: false,
188
+ fixed: 0,
189
+ changed: 0,
190
+ error: result.error.message,
191
+ };
192
+ }
193
+ this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
194
+ return { success: true, fixed: totalFixable, changed: validFiles.length };
195
+ }
196
+ catch (err) {
197
+ return {
198
+ success: false,
199
+ fixed: 0,
200
+ changed: 0,
201
+ error: err.message,
202
+ };
203
+ }
204
+ }
163
205
  /**
164
206
  * Format a Python file (writes to disk)
165
207
  */
@@ -171,10 +213,8 @@ export class RuffClient {
171
213
  return { success: false, changed: false, error: "File not found" };
172
214
  const content = fs.readFileSync(absolutePath, "utf-8");
173
215
  try {
174
- const result = spawnSync("ruff", ["format", absolutePath], {
175
- encoding: "utf-8",
216
+ const result = safeSpawn("ruff", ["format", absolutePath], {
176
217
  timeout: 10000,
177
- shell: true,
178
218
  });
179
219
  if (result.error) {
180
220
  return { success: false, changed: false, error: result.error.message };
@@ -12,6 +12,7 @@ import { spawnSync } from "node:child_process";
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import { isFileKind } from "./file-kinds.js";
15
+ import { safeSpawn } from "./safe-spawn.js";
15
16
 
16
17
  // --- Types ---
17
18
 
@@ -56,10 +57,8 @@ export class RuffClient {
56
57
  if (this.ruffAvailable !== null) return this.ruffAvailable;
57
58
 
58
59
  try {
59
- const result = spawnSync("ruff", ["--version"], {
60
- encoding: "utf-8",
60
+ const result = safeSpawn("ruff", ["--version"], {
61
61
  timeout: 5000,
62
- shell: true,
63
62
  });
64
63
  this.ruffAvailable = !result.error && result.status === 0;
65
64
  if (this.ruffAvailable) {
@@ -90,7 +89,7 @@ export class RuffClient {
90
89
  if (!fs.existsSync(absolutePath)) return [];
91
90
 
92
91
  try {
93
- const result = spawnSync(
92
+ const result = safeSpawn(
94
93
  "ruff",
95
94
  [
96
95
  "check",
@@ -101,9 +100,7 @@ export class RuffClient {
101
100
  absolutePath,
102
101
  ],
103
102
  {
104
- encoding: "utf-8",
105
103
  timeout: 10000,
106
- shell: true,
107
104
  },
108
105
  );
109
106
 
@@ -128,13 +125,11 @@ export class RuffClient {
128
125
  if (!fs.existsSync(absolutePath)) return "";
129
126
 
130
127
  try {
131
- const result = spawnSync(
128
+ const result = safeSpawn(
132
129
  "ruff",
133
130
  ["format", "--check", "--diff", absolutePath],
134
131
  {
135
- encoding: "utf-8",
136
132
  timeout: 10000,
137
- shell: true,
138
133
  },
139
134
  );
140
135
 
@@ -187,10 +182,8 @@ export class RuffClient {
187
182
  const beforeDiags = this.checkFile(filePath);
188
183
  const fixableCount = beforeDiags.filter((d) => d.fixable).length;
189
184
 
190
- const result = spawnSync("ruff", ["check", "--fix", absolutePath], {
191
- encoding: "utf-8",
185
+ const result = safeSpawn("ruff", ["check", "--fix", absolutePath], {
192
186
  timeout: 15000,
193
- shell: true,
194
187
  });
195
188
 
196
189
  if (result.error) {
@@ -217,6 +210,72 @@ export class RuffClient {
217
210
  }
218
211
  }
219
212
 
213
+ /**
214
+ * Fix multiple Python files at once (much faster than file-by-file)
215
+ */
216
+ fixFiles(filePaths: string[]): {
217
+ success: boolean;
218
+ fixed: number;
219
+ changed: number;
220
+ error?: string;
221
+ } {
222
+ if (!this.isAvailable()) {
223
+ return {
224
+ success: false,
225
+ fixed: 0,
226
+ changed: 0,
227
+ error: "Ruff not available",
228
+ };
229
+ }
230
+
231
+ // Filter to existing Python files
232
+ const validFiles = filePaths
233
+ .map(f => path.resolve(f))
234
+ .filter(f => fs.existsSync(f) && f.endsWith(".py"));
235
+
236
+ if (validFiles.length === 0) {
237
+ return { success: true, fixed: 0, changed: 0 };
238
+ }
239
+
240
+ try {
241
+ // Count fixable issues before fixing
242
+ let totalFixable = 0;
243
+ for (const file of validFiles) {
244
+ const diags = this.checkFile(file);
245
+ totalFixable += diags.filter(d => d.fixable).length;
246
+ }
247
+
248
+ // Run ruff once on all files - much faster than per file
249
+ const result = safeSpawn(
250
+ "ruff",
251
+ ["check", "--fix", ...validFiles],
252
+ {
253
+ timeout: 60000, // Longer timeout for batch
254
+ },
255
+ );
256
+
257
+ if (result.error) {
258
+ return {
259
+ success: false,
260
+ fixed: 0,
261
+ changed: 0,
262
+ error: result.error.message,
263
+ };
264
+ }
265
+
266
+ this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
267
+
268
+ return { success: true, fixed: totalFixable, changed: validFiles.length };
269
+ } catch (err: any) {
270
+ return {
271
+ success: false,
272
+ fixed: 0,
273
+ changed: 0,
274
+ error: err.message,
275
+ };
276
+ }
277
+ }
278
+
220
279
  /**
221
280
  * Format a Python file (writes to disk)
222
281
  */
@@ -235,10 +294,8 @@ export class RuffClient {
235
294
  const content = fs.readFileSync(absolutePath, "utf-8");
236
295
 
237
296
  try {
238
- const result = spawnSync("ruff", ["format", absolutePath], {
239
- encoding: "utf-8",
297
+ const result = safeSpawn("ruff", ["format", absolutePath], {
240
298
  timeout: 10000,
241
- shell: true,
242
299
  });
243
300
 
244
301
  if (result.error) {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Runner tracker for /lens-booboo and related commands
3
+ *
4
+ * Tracks execution time and findings for each analysis runner,
5
+ * producing a summary of what each runner found.
6
+ */
7
+ export class RunnerTracker {
8
+ constructor(options) {
9
+ this.runners = [];
10
+ this.onProgress = options?.onProgress;
11
+ }
12
+ /**
13
+ * Run a function with timing and tracking
14
+ */
15
+ async run(name, runFn, options) {
16
+ const startMs = Date.now();
17
+ const index = this.runners.length;
18
+ const runner = {
19
+ name,
20
+ status: "running",
21
+ findings: 0,
22
+ elapsedMs: 0,
23
+ };
24
+ this.runners.push(runner);
25
+ // Notify start
26
+ this.onProgress?.(runner, index);
27
+ try {
28
+ const result = await runFn();
29
+ const elapsedMs = Date.now() - startMs;
30
+ // Extract findings if result has it
31
+ const findings = typeof result === "object" &&
32
+ result !== null &&
33
+ "findings" in result &&
34
+ typeof result.findings === "number"
35
+ ? result.findings
36
+ : 0;
37
+ runner.status = "done";
38
+ runner.elapsedMs = elapsedMs;
39
+ runner.findings = findings;
40
+ return result;
41
+ }
42
+ catch (err) {
43
+ const elapsedMs = Date.now() - startMs;
44
+ runner.status = "error";
45
+ runner.elapsedMs = elapsedMs;
46
+ runner.message = String(err);
47
+ throw err;
48
+ }
49
+ }
50
+ /**
51
+ * Mark a runner as skipped (for when preconditions aren't met)
52
+ */
53
+ skip(name, message) {
54
+ this.runners.push({
55
+ name,
56
+ status: "skipped",
57
+ findings: 0,
58
+ elapsedMs: 0,
59
+ message,
60
+ });
61
+ }
62
+ /**
63
+ * Update findings for a runner (useful when findings are discovered asynchronously)
64
+ */
65
+ updateFindings(runnerName, findings) {
66
+ const runner = this.runners.find((r) => r.name === runnerName);
67
+ if (runner) {
68
+ runner.findings = findings;
69
+ }
70
+ }
71
+ /**
72
+ * Get all tracked runners
73
+ */
74
+ getRunners() {
75
+ return [...this.runners];
76
+ }
77
+ /**
78
+ * Get summary statistics
79
+ */
80
+ getStats() {
81
+ return {
82
+ total: this.runners.length,
83
+ done: this.runners.filter((r) => r.status === "done").length,
84
+ skipped: this.runners.filter((r) => r.status === "skipped").length,
85
+ errors: this.runners.filter((r) => r.status === "error").length,
86
+ totalFindings: this.runners.reduce((sum, r) => sum + r.findings, 0),
87
+ totalTimeMs: this.runners.reduce((sum, r) => sum + r.elapsedMs, 0),
88
+ };
89
+ }
90
+ /**
91
+ * Format a single runner result for display
92
+ */
93
+ formatRunner(runner, index) {
94
+ const prefix = index !== undefined ? `[${index + 1}] ` : "";
95
+ const statusIcon = runner.status === "done"
96
+ ? "✓"
97
+ : runner.status === "skipped"
98
+ ? "⊘"
99
+ : runner.status === "error"
100
+ ? "✗"
101
+ : "○";
102
+ const findings = runner.findings > 0 ? ` (${runner.findings} findings)` : "";
103
+ const time = this.formatElapsed(runner.elapsedMs);
104
+ const message = runner.message ? ` — ${runner.message}` : "";
105
+ return `${prefix}${statusIcon} ${runner.name}${findings} — ${time}${message}`;
106
+ }
107
+ /**
108
+ * Format all runners as a summary table
109
+ */
110
+ formatSummary() {
111
+ const lines = ["📊 Runner Summary:", ""];
112
+ for (let i = 0; i < this.runners.length; i++) {
113
+ lines.push(` ${this.formatRunner(this.runners[i], i)}`);
114
+ }
115
+ const stats = this.getStats();
116
+ lines.push("");
117
+ lines.push(` Total: ${stats.totalFindings} findings in ${this.formatElapsed(stats.totalTimeMs)}`);
118
+ return lines.join("\n");
119
+ }
120
+ /**
121
+ * Format elapsed time in human-readable form
122
+ */
123
+ formatElapsed(ms) {
124
+ if (ms < 1000)
125
+ return `${ms}ms`;
126
+ if (ms < 60000)
127
+ return `${(ms / 1000).toFixed(1)}s`;
128
+ const mins = Math.floor(ms / 60000);
129
+ const secs = ((ms % 60000) / 1000).toFixed(0);
130
+ return `${mins}m${secs.padStart(2, "0")}s`;
131
+ }
132
+ }
133
+ /**
134
+ * Convenience function to create a tracker and run a sequence
135
+ */
136
+ export async function runSequence(sequence, onProgress) {
137
+ const tracker = new RunnerTracker({ onProgress });
138
+ const results = [];
139
+ for (const item of sequence) {
140
+ const result = await tracker.run(item.name, item.run, {
141
+ index: results.length,
142
+ total: sequence.length,
143
+ });
144
+ results.push(result);
145
+ // Update findings if handler provided
146
+ if (item.onFindings) {
147
+ const findings = item.onFindings(result);
148
+ tracker.updateFindings(item.name, findings);
149
+ }
150
+ }
151
+ return { results, tracker };
152
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Runner tracker for /lens-booboo and related commands
3
+ *
4
+ * Tracks execution time and findings for each analysis runner,
5
+ * producing a summary of what each runner found.
6
+ */
7
+
8
+ export interface TrackedRunner {
9
+ name: string;
10
+ status: "running" | "done" | "skipped" | "error";
11
+ findings: number;
12
+ elapsedMs: number;
13
+ message?: string;
14
+ /** Severity indicator for the runner (derived from findings) */
15
+ severity?: "error" | "warning" | "info";
16
+ }
17
+
18
+ export interface RunOptions {
19
+ /** Index in the sequence (for "[2/9]" style progress) */
20
+ index?: number;
21
+ /** Total number of runners (for "[2/9]" style progress) */
22
+ total?: number;
23
+ }
24
+
25
+ export class RunnerTracker {
26
+ private runners: TrackedRunner[] = [];
27
+ private onProgress?: (runner: TrackedRunner, index: number) => void;
28
+
29
+ constructor(options?: { onProgress?: (runner: TrackedRunner, index: number) => void }) {
30
+ this.onProgress = options?.onProgress;
31
+ }
32
+
33
+ /**
34
+ * Run a function with timing and tracking
35
+ */
36
+ async run<T>(
37
+ name: string,
38
+ runFn: () => Promise<T> | T,
39
+ options?: RunOptions,
40
+ ): Promise<T> {
41
+ const startMs = Date.now();
42
+ const index = this.runners.length;
43
+ const runner: TrackedRunner = {
44
+ name,
45
+ status: "running",
46
+ findings: 0,
47
+ elapsedMs: 0,
48
+ };
49
+ this.runners.push(runner);
50
+
51
+ // Notify start
52
+ this.onProgress?.(runner, index);
53
+
54
+ try {
55
+ const result = await runFn();
56
+ const elapsedMs = Date.now() - startMs;
57
+
58
+ // Extract findings if result has it
59
+ const findings =
60
+ typeof result === "object" &&
61
+ result !== null &&
62
+ "findings" in result &&
63
+ typeof (result as { findings?: number }).findings === "number"
64
+ ? (result as { findings: number }).findings
65
+ : 0;
66
+
67
+ runner.status = "done";
68
+ runner.elapsedMs = elapsedMs;
69
+ runner.findings = findings;
70
+
71
+ return result;
72
+ } catch (err) {
73
+ const elapsedMs = Date.now() - startMs;
74
+ runner.status = "error";
75
+ runner.elapsedMs = elapsedMs;
76
+ runner.message = String(err);
77
+ throw err;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Mark a runner as skipped (for when preconditions aren't met)
83
+ */
84
+ skip(name: string, message?: string): void {
85
+ this.runners.push({
86
+ name,
87
+ status: "skipped",
88
+ findings: 0,
89
+ elapsedMs: 0,
90
+ message,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Update findings for a runner (useful when findings are discovered asynchronously)
96
+ */
97
+ updateFindings(runnerName: string, findings: number): void {
98
+ const runner = this.runners.find((r) => r.name === runnerName);
99
+ if (runner) {
100
+ runner.findings = findings;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get all tracked runners
106
+ */
107
+ getRunners(): TrackedRunner[] {
108
+ return [...this.runners];
109
+ }
110
+
111
+ /**
112
+ * Get summary statistics
113
+ */
114
+ getStats(): {
115
+ total: number;
116
+ done: number;
117
+ skipped: number;
118
+ errors: number;
119
+ totalFindings: number;
120
+ totalTimeMs: number;
121
+ } {
122
+ return {
123
+ total: this.runners.length,
124
+ done: this.runners.filter((r) => r.status === "done").length,
125
+ skipped: this.runners.filter((r) => r.status === "skipped").length,
126
+ errors: this.runners.filter((r) => r.status === "error").length,
127
+ totalFindings: this.runners.reduce((sum, r) => sum + r.findings, 0),
128
+ totalTimeMs: this.runners.reduce((sum, r) => sum + r.elapsedMs, 0),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Format a single runner result for display
134
+ */
135
+ formatRunner(runner: TrackedRunner, index?: number): string {
136
+ const prefix = index !== undefined ? `[${index + 1}] ` : "";
137
+ const statusIcon =
138
+ runner.status === "done"
139
+ ? "✓"
140
+ : runner.status === "skipped"
141
+ ? "⊘"
142
+ : runner.status === "error"
143
+ ? "✗"
144
+ : "○";
145
+ const findings =
146
+ runner.findings > 0 ? ` (${runner.findings} findings)` : "";
147
+ const time = this.formatElapsed(runner.elapsedMs);
148
+ const message = runner.message ? ` — ${runner.message}` : "";
149
+
150
+ return `${prefix}${statusIcon} ${runner.name}${findings} — ${time}${message}`;
151
+ }
152
+
153
+ /**
154
+ * Format all runners as a summary table
155
+ */
156
+ formatSummary(): string {
157
+ const lines = ["📊 Runner Summary:", ""];
158
+
159
+ for (let i = 0; i < this.runners.length; i++) {
160
+ lines.push(` ${this.formatRunner(this.runners[i], i)}`);
161
+ }
162
+
163
+ const stats = this.getStats();
164
+ lines.push("");
165
+ lines.push(
166
+ ` Total: ${stats.totalFindings} findings in ${this.formatElapsed(stats.totalTimeMs)}`,
167
+ );
168
+
169
+ return lines.join("\n");
170
+ }
171
+
172
+ /**
173
+ * Format elapsed time in human-readable form
174
+ */
175
+ private formatElapsed(ms: number): string {
176
+ if (ms < 1000) return `${ms}ms`;
177
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
178
+ const mins = Math.floor(ms / 60000);
179
+ const secs = ((ms % 60000) / 1000).toFixed(0);
180
+ return `${mins}m${secs.padStart(2, "0")}s`;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Convenience function to create a tracker and run a sequence
186
+ */
187
+ export async function runSequence<T>(
188
+ sequence: Array<{
189
+ name: string;
190
+ run: () => Promise<T> | T;
191
+ onFindings?: (result: T) => number;
192
+ }>,
193
+ onProgress?: (runner: TrackedRunner, index: number) => void,
194
+ ): Promise<{ results: T[]; tracker: RunnerTracker }> {
195
+ const tracker = new RunnerTracker({ onProgress });
196
+ const results: T[] = [];
197
+
198
+ for (const item of sequence) {
199
+ const result = await tracker.run(item.name, item.run, {
200
+ index: results.length,
201
+ total: sequence.length,
202
+ });
203
+ results.push(result);
204
+
205
+ // Update findings if handler provided
206
+ if (item.onFindings) {
207
+ const findings = item.onFindings(result);
208
+ tracker.updateFindings(item.name, findings);
209
+ }
210
+ }
211
+
212
+ return { results, tracker };
213
+ }
@@ -6,9 +6,9 @@
6
6
  * Requires: cargo (rustup)
7
7
  * Docs: https://doc.rust-lang.org/cargo/
8
8
  */
9
- import { spawnSync } from "node:child_process";
10
9
  import * as fs from "node:fs";
11
10
  import * as path from "node:path";
11
+ import { safeSpawn } from "./safe-spawn.js";
12
12
  // --- Common install paths ---
13
13
  const CARGO_WINDOWS_PATHS = [
14
14
  path.join(process.env.USERPROFILE || "", ".cargo", "bin", "cargo.exe"),
@@ -46,10 +46,8 @@ export class RustClient {
46
46
  }
47
47
  }
48
48
  else {
49
- const result = spawnSync(p, ["--version"], {
50
- encoding: "utf-8",
49
+ const result = safeSpawn(p, ["--version"], {
51
50
  timeout: 3000,
52
- shell: true,
53
51
  });
54
52
  if (!result.error && result.status === 0) {
55
53
  this.cargoPath = p;
@@ -92,12 +90,9 @@ export class RustClient {
92
90
  if (!fs.existsSync(absolutePath))
93
91
  return [];
94
92
  try {
95
- const cargoCmd = cargoExe.includes(" ") ? `"${cargoExe}"` : cargoExe;
96
- const result = spawnSync(cargoCmd, ["check", "--message-format", "json"], {
97
- encoding: "utf-8",
93
+ const result = safeSpawn(cargoExe, ["check", "--message-format", "json"], {
98
94
  timeout: 60000,
99
95
  cwd,
100
- shell: true,
101
96
  });
102
97
  const output = result.stdout || "";
103
98
  return this.parseJsonOutput(output, absolutePath);
@@ -114,11 +109,9 @@ export class RustClient {
114
109
  if (!this.isAvailable())
115
110
  return [];
116
111
  try {
117
- const result = spawnSync("cargo", ["clippy", "--message-format", "json"], {
118
- encoding: "utf-8",
112
+ const result = safeSpawn("cargo", ["clippy", "--message-format", "json"], {
119
113
  timeout: 60000,
120
114
  cwd,
121
- shell: true,
122
115
  });
123
116
  const output = result.stdout || "";
124
117
  return this.parseJsonOutput(output, "");