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
@@ -10,6 +10,7 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
+ import { safeSpawn } from "./safe-spawn.js";
13
14
 
14
15
  // --- Types ---
15
16
 
@@ -85,10 +86,8 @@ export class RustClient {
85
86
  return p;
86
87
  }
87
88
  } else {
88
- const result = spawnSync(p, ["--version"], {
89
- encoding: "utf-8",
89
+ const result = safeSpawn(p, ["--version"], {
90
90
  timeout: 3000,
91
- shell: true,
92
91
  });
93
92
  if (!result.error && result.status === 0) {
94
93
  this.cargoPath = p;
@@ -133,15 +132,12 @@ export class RustClient {
133
132
  if (!fs.existsSync(absolutePath)) return [];
134
133
 
135
134
  try {
136
- const cargoCmd = cargoExe.includes(" ") ? `"${cargoExe}"` : cargoExe;
137
- const result = spawnSync(
138
- cargoCmd,
135
+ const result = safeSpawn(
136
+ cargoExe,
139
137
  ["check", "--message-format", "json"],
140
138
  {
141
- encoding: "utf-8",
142
139
  timeout: 60000,
143
140
  cwd,
144
- shell: true,
145
141
  },
146
142
  );
147
143
 
@@ -160,14 +156,12 @@ export class RustClient {
160
156
  if (!this.isAvailable()) return [];
161
157
 
162
158
  try {
163
- const result = spawnSync(
159
+ const result = safeSpawn(
164
160
  "cargo",
165
161
  ["clippy", "--message-format", "json"],
166
162
  {
167
- encoding: "utf-8",
168
163
  timeout: 60000,
169
164
  cwd,
170
- shell: true,
171
165
  },
172
166
  );
173
167
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Safe cross-platform spawn utilities
3
+ *
4
+ * Wraps child_process.spawnSync to handle Windows execution safely
5
+ * without triggering deprecation warnings.
6
+ *
7
+ * Strategy:
8
+ * - Unix: Use shell: false with normal args
9
+ * - Windows: Manually construct command string to avoid deprecation warning,
10
+ * then use shell: true with no args array
11
+ */
12
+ import { spawnSync } from "node:child_process";
13
+ /**
14
+ * Escape an argument for Windows shell execution.
15
+ * Handles spaces, quotes, $variables, and special characters.
16
+ */
17
+ function escapeWindowsArg(arg) {
18
+ // Check if this looks like an ast-grep pattern with meta-variables ($NAME)
19
+ // In Git Bash/MSYS2 on Windows, $VAR gets expanded by the shell
20
+ // We need to use single quotes to prevent expansion
21
+ if (arg.includes("$")) {
22
+ // Use single quotes for arguments with $variables
23
+ // Escape single quotes within the argument
24
+ return `'${arg.replace(/'/g, "'\\''")}'`;
25
+ }
26
+ // If no special characters, return as-is
27
+ if (!/[\s\"]/.test(arg))
28
+ return arg;
29
+ // Escape quotes by doubling them
30
+ return `"${arg.replace(/"/g, "\"\"")}"`;
31
+ }
32
+ /**
33
+ * Construct a command string for Windows shell execution.
34
+ * This avoids the deprecation warning by not passing an args array.
35
+ */
36
+ function buildWindowsCommand(command, args) {
37
+ const escapedArgs = args.map(escapeWindowsArg).join(" ");
38
+ return `${command} ${escapedArgs}`;
39
+ }
40
+ /**
41
+ * Safely spawn a process cross-platform without shell deprecation warnings.
42
+ *
43
+ * On Windows: Uses shell: true but constructs the command string manually
44
+ * to avoid the deprecation warning about unescaped args.
45
+ * On Unix: Uses shell: false for normal execution.
46
+ */
47
+ export function safeSpawn(command, args, options) {
48
+ if (process.platform === "win32") {
49
+ // On Windows, construct the full command string and use shell: true
50
+ // without an args array. This avoids the deprecation warning.
51
+ const fullCommand = buildWindowsCommand(command, args);
52
+ const result = spawnSync(fullCommand, {
53
+ ...options,
54
+ encoding: "utf-8",
55
+ shell: true,
56
+ windowsHide: true,
57
+ });
58
+ return {
59
+ stdout: result.stdout?.toString() || "",
60
+ stderr: result.stderr?.toString() || "",
61
+ status: result.status,
62
+ error: result.error,
63
+ };
64
+ }
65
+ // On Unix, use shell: false (the default) with normal args
66
+ const result = spawnSync(command, args, {
67
+ ...options,
68
+ encoding: "utf-8",
69
+ shell: false,
70
+ windowsHide: true,
71
+ });
72
+ return {
73
+ stdout: result.stdout?.toString() || "",
74
+ stderr: result.stderr?.toString() || "",
75
+ status: result.status,
76
+ error: result.error,
77
+ };
78
+ }
79
+ /**
80
+ * Check if a command is available in PATH
81
+ */
82
+ export function isCommandAvailable(command) {
83
+ const result = safeSpawn(process.platform === "win32" ? "where" : "which", [command], { timeout: 5000 });
84
+ return result.status === 0;
85
+ }
86
+ /**
87
+ * Find the full path to a command (npx, node, etc.)
88
+ */
89
+ export function findCommand(command) {
90
+ const finder = process.platform === "win32" ? "where" : "which";
91
+ const result = safeSpawn(finder, [command], { timeout: 5000 });
92
+ if (result.status !== 0)
93
+ return null;
94
+ // Take first line (first match)
95
+ return result.stdout.trim().split("\n")[0] || null;
96
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Safe cross-platform spawn utilities
3
+ *
4
+ * Wraps child_process.spawnSync to handle Windows execution safely
5
+ * without triggering deprecation warnings.
6
+ *
7
+ * Strategy:
8
+ * - Unix: Use shell: false with normal args
9
+ * - Windows: Manually construct command string to avoid deprecation warning,
10
+ * then use shell: true with no args array
11
+ */
12
+
13
+ import { spawnSync, type SpawnOptions } from "node:child_process";
14
+
15
+ export interface SpawnResult {
16
+ stdout: string;
17
+ stderr: string;
18
+ status: number | null;
19
+ error?: Error;
20
+ }
21
+
22
+ export interface SafeSpawnOptions {
23
+ timeout?: number;
24
+ cwd?: string;
25
+ env?: NodeJS.ProcessEnv;
26
+ }
27
+
28
+ /**
29
+ * Escape an argument for Windows shell execution.
30
+ * Handles spaces, quotes, $variables, and special characters.
31
+ */
32
+ function escapeWindowsArg(arg: string): string {
33
+ // Check if this looks like an ast-grep pattern with meta-variables ($NAME)
34
+ // In Git Bash/MSYS2 on Windows, $VAR gets expanded by the shell
35
+ // We need to use single quotes to prevent expansion
36
+ if (arg.includes("$")) {
37
+ // Use single quotes for arguments with $variables
38
+ // Escape single quotes within the argument
39
+ return `'${arg.replace(/'/g, "'\\''")}'`;
40
+ }
41
+
42
+ // If no special characters, return as-is
43
+ if (!/[\s\"]/.test(arg)) return arg;
44
+
45
+ // Escape quotes by doubling them
46
+ return `"${arg.replace(/"/g, "\"\"")}"`;
47
+ }
48
+
49
+ /**
50
+ * Construct a command string for Windows shell execution.
51
+ * This avoids the deprecation warning by not passing an args array.
52
+ */
53
+ function buildWindowsCommand(command: string, args: string[]): string {
54
+ const escapedArgs = args.map(escapeWindowsArg).join(" ");
55
+ return `${command} ${escapedArgs}`;
56
+ }
57
+
58
+ /**
59
+ * Safely spawn a process cross-platform without shell deprecation warnings.
60
+ *
61
+ * On Windows: Uses shell: true but constructs the command string manually
62
+ * to avoid the deprecation warning about unescaped args.
63
+ * On Unix: Uses shell: false for normal execution.
64
+ */
65
+ export function safeSpawn(
66
+ command: string,
67
+ args: string[],
68
+ options?: SafeSpawnOptions,
69
+ ): SpawnResult {
70
+ if (process.platform === "win32") {
71
+ // On Windows, construct the full command string and use shell: true
72
+ // without an args array. This avoids the deprecation warning.
73
+ const fullCommand = buildWindowsCommand(command, args);
74
+ const result = spawnSync(fullCommand, {
75
+ ...(options as SpawnOptions),
76
+ encoding: "utf-8",
77
+ shell: true,
78
+ windowsHide: true,
79
+ });
80
+
81
+ return {
82
+ stdout: result.stdout?.toString() || "",
83
+ stderr: result.stderr?.toString() || "",
84
+ status: result.status,
85
+ error: result.error,
86
+ };
87
+ }
88
+
89
+ // On Unix, use shell: false (the default) with normal args
90
+ const result = spawnSync(command, args, {
91
+ ...(options as SpawnOptions),
92
+ encoding: "utf-8",
93
+ shell: false,
94
+ windowsHide: true,
95
+ });
96
+
97
+ return {
98
+ stdout: result.stdout?.toString() || "",
99
+ stderr: result.stderr?.toString() || "",
100
+ status: result.status,
101
+ error: result.error,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Check if a command is available in PATH
107
+ */
108
+ export function isCommandAvailable(command: string): boolean {
109
+ const result = safeSpawn(
110
+ process.platform === "win32" ? "where" : "which",
111
+ [command],
112
+ { timeout: 5000 },
113
+ );
114
+ return result.status === 0;
115
+ }
116
+
117
+ /**
118
+ * Find the full path to a command (npx, node, etc.)
119
+ */
120
+ export function findCommand(command: string): string | null {
121
+ const finder = process.platform === "win32" ? "where" : "which";
122
+ const result = safeSpawn(finder, [command], { timeout: 5000 });
123
+
124
+ if (result.status !== 0) return null;
125
+
126
+ // Take first line (first match)
127
+ return result.stdout.trim().split("\n")[0] || null;
128
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Shared architectural debt scanning — used by booboo-fix and booboo-refactor.
2
+ * Shared architectural debt scanning.
3
3
  * Scans ast-grep skip rules + complexity metrics + architect.yaml rules.
4
4
  */
5
- import { spawnSync } from "node:child_process";
6
5
  import * as fs from "node:fs";
7
6
  import * as path from "node:path";
7
+ import { safeSpawn } from "./safe-spawn.js";
8
8
  import { getSourceFiles, parseAstGrepJson } from "./scan-utils.js";
9
9
  /**
10
10
  * Scan for skip-category ast-grep violations grouped by absolute file path.
@@ -13,7 +13,7 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
13
13
  const skipByFile = new Map();
14
14
  if (!astGrepClient.isAvailable())
15
15
  return skipByFile;
16
- const sgResult = spawnSync("npx", [
16
+ const sgResult = safeSpawn("npx", [
17
17
  "sg",
18
18
  "scan",
19
19
  "--config",
@@ -30,10 +30,7 @@ export function scanSkipViolations(astGrepClient, configPath, targetPath, isTsPr
30
30
  ...(isTsProject ? ["--globs", "!**/*.js"] : []),
31
31
  targetPath,
32
32
  ], {
33
- encoding: "utf-8",
34
33
  timeout: 30000,
35
- shell: true,
36
- maxBuffer: 32 * 1024 * 1024,
37
34
  });
38
35
  const items = parseAstGrepJson(sgResult.stdout?.trim() ?? "");
39
36
  for (const item of items) {
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Shared architectural debt scanning — used by booboo-fix and booboo-refactor.
2
+ * Shared architectural debt scanning.
3
3
  * Scans ast-grep skip rules + complexity metrics + architect.yaml rules.
4
4
  */
5
5
 
6
- import { spawnSync } from "node:child_process";
7
6
  import * as fs from "node:fs";
8
7
  import * as path from "node:path";
9
8
  import type { ArchitectClient } from "./architect-client.js";
10
9
  import type { AstGrepClient } from "./ast-grep-client.js";
11
10
  import type { ComplexityClient } from "./complexity-client.js";
11
+ import { safeSpawn } from "./safe-spawn.js";
12
12
  import { getSourceFiles, parseAstGrepJson } from "./scan-utils.js";
13
13
 
14
14
  export type SkipIssue = { rule: string; line: number; note: string };
@@ -28,7 +28,7 @@ export function scanSkipViolations(
28
28
  const skipByFile = new Map<string, SkipIssue[]>();
29
29
  if (!astGrepClient.isAvailable()) return skipByFile;
30
30
 
31
- const sgResult = spawnSync(
31
+ const sgResult = safeSpawn(
32
32
  "npx",
33
33
  [
34
34
  "sg",
@@ -48,10 +48,7 @@ export function scanSkipViolations(
48
48
  targetPath,
49
49
  ],
50
50
  {
51
- encoding: "utf-8",
52
51
  timeout: 30000,
53
- shell: true,
54
- maxBuffer: 32 * 1024 * 1024,
55
52
  },
56
53
  );
57
54
 
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { EXCLUDED_DIRS, isTestFile } from "./file-utils.js";
3
4
  /**
4
5
  * Common parsing logic for ast-grep JSON output (handles both array and NDJSON).
5
6
  */
@@ -30,7 +31,7 @@ export function parseAstGrepJson(raw) {
30
31
  */
31
32
  export function shouldIgnoreFile(filePath, isTsProject) {
32
33
  const relPath = filePath.replace(/\\/g, "/");
33
- const basename = path.basename(relPath);
34
+ const _basename = path.basename(relPath);
34
35
  // Ignore compiled JS in TS projects
35
36
  const isJs = relPath.endsWith(".js") ||
36
37
  relPath.endsWith(".mjs") ||
@@ -38,20 +39,11 @@ export function shouldIgnoreFile(filePath, isTsProject) {
38
39
  if (isTsProject && isJs)
39
40
  return true;
40
41
  // Ignore test scripts and common test patterns
41
- if (basename.startsWith("test-") ||
42
- basename.includes(".test.") ||
43
- basename.includes(".spec.")) {
42
+ if (isTestFile(filePath))
44
43
  return true;
45
- }
46
44
  // Ignore hidden directories and common build outputs
47
- if (relPath.includes("/node_modules/") ||
48
- relPath.includes("/.git/") ||
49
- relPath.includes("/dist/") ||
50
- relPath.includes("/build/") ||
51
- relPath.includes("/.next/") ||
52
- relPath.includes("/.pi-lens/")) {
45
+ if (EXCLUDED_DIRS.some((d) => relPath.includes(`/${d}/`)))
53
46
  return true;
54
- }
55
47
  return false;
56
48
  }
57
49
  /**
@@ -72,14 +64,7 @@ export function getSourceFiles(dir, isTsProject) {
72
64
  for (const entry of entries) {
73
65
  const full = path.join(d, entry.name);
74
66
  if (entry.isDirectory()) {
75
- if ([
76
- "node_modules",
77
- ".git",
78
- "dist",
79
- "build",
80
- ".next",
81
- ".pi-lens",
82
- ].includes(entry.name))
67
+ if (EXCLUDED_DIRS.includes(entry.name))
83
68
  continue;
84
69
  scan(full);
85
70
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { EXCLUDED_DIRS, isTestFile } from "./file-utils.js";
3
4
 
4
5
  /**
5
6
  * Common parsing logic for ast-grep JSON output (handles both array and NDJSON).
@@ -32,7 +33,7 @@ export function shouldIgnoreFile(
32
33
  isTsProject: boolean,
33
34
  ): boolean {
34
35
  const relPath = filePath.replace(/\\/g, "/");
35
- const basename = path.basename(relPath);
36
+ const _basename = path.basename(relPath);
36
37
 
37
38
  // Ignore compiled JS in TS projects
38
39
  const isJs =
@@ -42,25 +43,10 @@ export function shouldIgnoreFile(
42
43
  if (isTsProject && isJs) return true;
43
44
 
44
45
  // Ignore test scripts and common test patterns
45
- if (
46
- basename.startsWith("test-") ||
47
- basename.includes(".test.") ||
48
- basename.includes(".spec.")
49
- ) {
50
- return true;
51
- }
46
+ if (isTestFile(filePath)) return true;
52
47
 
53
48
  // Ignore hidden directories and common build outputs
54
- if (
55
- relPath.includes("/node_modules/") ||
56
- relPath.includes("/.git/") ||
57
- relPath.includes("/dist/") ||
58
- relPath.includes("/build/") ||
59
- relPath.includes("/.next/") ||
60
- relPath.includes("/.pi-lens/")
61
- ) {
62
- return true;
63
- }
49
+ if (EXCLUDED_DIRS.some((d) => relPath.includes(`/${d}/`))) return true;
64
50
 
65
51
  return false;
66
52
  }
@@ -83,17 +69,7 @@ export function getSourceFiles(dir: string, isTsProject: boolean): string[] {
83
69
  for (const entry of entries) {
84
70
  const full = path.join(d, entry.name);
85
71
  if (entry.isDirectory()) {
86
- if (
87
- [
88
- "node_modules",
89
- ".git",
90
- "dist",
91
- "build",
92
- ".next",
93
- ".pi-lens",
94
- ].includes(entry.name)
95
- )
96
- continue;
72
+ if (EXCLUDED_DIRS.includes(entry.name)) continue;
97
73
  scan(full);
98
74
  } else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
99
75
  // Skip compiled JS if it's a TS project
@@ -12,6 +12,7 @@
12
12
  * - Private keys (BEGIN PRIVATE KEY)
13
13
  * - Generic API key/password patterns
14
14
  */
15
+ import { isTestFile } from "./file-utils.js";
15
16
  // Patterns ordered by specificity - first match wins per line
16
17
  const SECRET_PATTERNS = [
17
18
  // High-confidence: specific key prefixes
@@ -68,21 +69,6 @@ const SECRET_PATTERNS = [
68
69
  message: "Possible secret in .env format",
69
70
  },
70
71
  ];
71
- /**
72
- * Check if file path is a test file (should skip secrets scan)
73
- */
74
- function isTestFile(filePath) {
75
- const normalized = filePath.replace(/\\/g, "/");
76
- return (normalized.includes(".test.") ||
77
- normalized.includes(".spec.") ||
78
- normalized.includes("/test/") ||
79
- normalized.includes("/tests/") ||
80
- normalized.includes("__tests__/") ||
81
- normalized.includes("test-utils") ||
82
- normalized.startsWith("test-") ||
83
- normalized.includes(".fixture.") ||
84
- normalized.includes(".mock."));
85
- }
86
72
  /**
87
73
  * Scan content for potential secrets
88
74
  * Returns findings with line numbers.
@@ -97,7 +83,7 @@ export function scanForSecrets(content, filePath) {
97
83
  const lines = content.split("\n");
98
84
  for (let i = 0; i < lines.length; i++) {
99
85
  const line = lines[i];
100
- let matched = false;
86
+ let _matched = false;
101
87
  for (const pattern of SECRET_PATTERNS) {
102
88
  // Reset lastIndex before each test (important for global regex)
103
89
  const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
@@ -106,7 +92,7 @@ export function scanForSecrets(content, filePath) {
106
92
  line: i + 1,
107
93
  message: pattern.message,
108
94
  });
109
- matched = true;
95
+ _matched = true;
110
96
  break; // One finding per line
111
97
  }
112
98
  }
@@ -13,6 +13,8 @@
13
13
  * - Generic API key/password patterns
14
14
  */
15
15
 
16
+ import { isTestFile } from "./file-utils.js";
17
+
16
18
  interface SecretPattern {
17
19
  pattern: RegExp;
18
20
  name: string;
@@ -83,24 +85,6 @@ export interface SecretFinding {
83
85
  message: string;
84
86
  }
85
87
 
86
- /**
87
- * Check if file path is a test file (should skip secrets scan)
88
- */
89
- function isTestFile(filePath: string): boolean {
90
- const normalized = filePath.replace(/\\/g, "/");
91
- return (
92
- normalized.includes(".test.") ||
93
- normalized.includes(".spec.") ||
94
- normalized.includes("/test/") ||
95
- normalized.includes("/tests/") ||
96
- normalized.includes("__tests__/") ||
97
- normalized.includes("test-utils") ||
98
- normalized.startsWith("test-") ||
99
- normalized.includes(".fixture.") ||
100
- normalized.includes(".mock.")
101
- );
102
- }
103
-
104
88
  /**
105
89
  * Scan content for potential secrets
106
90
  * Returns findings with line numbers.
@@ -120,7 +104,7 @@ export function scanForSecrets(
120
104
 
121
105
  for (let i = 0; i < lines.length; i++) {
122
106
  const line = lines[i];
123
- let matched = false;
107
+ let _matched = false;
124
108
  for (const pattern of SECRET_PATTERNS) {
125
109
  // Reset lastIndex before each test (important for global regex)
126
110
  const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
@@ -129,7 +113,7 @@ export function scanForSecrets(
129
113
  line: i + 1,
130
114
  message: pattern.message,
131
115
  });
132
- matched = true;
116
+ _matched = true;
133
117
  break; // One finding per line
134
118
  }
135
119
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Effect Integration Tests
3
+ *
4
+ * Tests for Effect-TS concurrent runner execution.
5
+ */
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { clearRunnerRegistry, listRunners, registerRunner, } from "../../dispatch/dispatcher.js";
8
+ import { dispatchLintWithEffect, dispatchWithEffect, } from "../effect-integration.js";
9
+ describe("Effect Integration", () => {
10
+ beforeEach(async () => {
11
+ clearRunnerRegistry();
12
+ // Register a simple test runner
13
+ const testRunner = {
14
+ id: "test-runner",
15
+ appliesTo: ["jsts"],
16
+ priority: 10,
17
+ enabledByDefault: true,
18
+ async run(ctx) {
19
+ return {
20
+ status: "succeeded",
21
+ diagnostics: [{
22
+ id: "test:1",
23
+ message: "Test diagnostic",
24
+ filePath: ctx.filePath,
25
+ severity: "info",
26
+ semantic: "silent",
27
+ tool: "test-runner",
28
+ }],
29
+ semantic: "none",
30
+ };
31
+ },
32
+ };
33
+ registerRunner(testRunner);
34
+ });
35
+ it("should have runners registered", () => {
36
+ const runners = listRunners();
37
+ expect(runners.length).toBeGreaterThan(0);
38
+ expect(runners.some(r => r.id === "test-runner")).toBe(true);
39
+ });
40
+ it("should run effect integration with real runners", async () => {
41
+ const ctx = {
42
+ filePath: "test.ts",
43
+ cwd: "/test",
44
+ kind: "jsts",
45
+ pi: {
46
+ getFlag: vi.fn(() => false),
47
+ },
48
+ autofix: true,
49
+ deltaMode: false,
50
+ baselines: new Map(),
51
+ hasTool: vi.fn(() => Promise.resolve(false)),
52
+ log: vi.fn(),
53
+ };
54
+ // Get actual runners for jsts
55
+ const { getRunnersForKind } = await import("../../dispatch/dispatcher.js");
56
+ const runners = getRunnersForKind("jsts");
57
+ const group = {
58
+ runnerIds: runners.slice(0, 2).map(r => r.id), // Use first 2 runners
59
+ mode: "all",
60
+ };
61
+ const result = await dispatchWithEffect(ctx, [group]);
62
+ // Just verify it doesn't crash and returns valid result
63
+ expect(result).toBeDefined();
64
+ expect(typeof result.durationMs).toBe("number");
65
+ });
66
+ it("should handle --lens-effect flag path", async () => {
67
+ const mockPi = {
68
+ getFlag: vi.fn((flag) => flag === "lens-effect"),
69
+ readFile: vi.fn(),
70
+ writeFile: vi.fn(),
71
+ editFile: vi.fn(),
72
+ bash: vi.fn(),
73
+ ui: {
74
+ notify: vi.fn(),
75
+ progress: vi.fn(),
76
+ prompt: vi.fn(),
77
+ },
78
+ llm: {
79
+ stream: vi.fn(),
80
+ createMessage: vi.fn(),
81
+ },
82
+ };
83
+ const output = await dispatchLintWithEffect("test.ts", "/test", mockPi);
84
+ expect(typeof output).toBe("string");
85
+ });
86
+ });