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
@@ -8,9 +8,10 @@
8
8
  * - scanBiome: Remaining Biome lint issues
9
9
  * - scanSlop: AI slop indicators (high complexity patterns)
10
10
  */
11
- import * as childProcess from "node:child_process";
12
11
  import * as nodeFs from "node:fs";
13
12
  import * as path from "node:path";
13
+ import { EXCLUDED_DIRS } from "./file-utils.js";
14
+ import { safeSpawn } from "./safe-spawn.js";
14
15
  import { shouldIgnoreFile } from "./scan-utils.js";
15
16
  const DEBUG_LOG = path.join(process.env.HOME || process.env.USERPROFILE || ".", "pi-lens-debug.log");
16
17
  function dbg(msg) {
@@ -18,8 +19,9 @@ function dbg(msg) {
18
19
  try {
19
20
  nodeFs.appendFileSync(DEBUG_LOG, line);
20
21
  }
21
- catch {
22
- // Ignored
22
+ catch (err) {
23
+ // Debug logging failed, silently ignore to avoid recursive errors
24
+ void err;
23
25
  }
24
26
  }
25
27
  /**
@@ -53,14 +55,12 @@ export function scanDeadCode(knip, targetPath, isTsProject) {
53
55
  */
54
56
  export function scanAstGrep(targetPath, isTsProject, configPath) {
55
57
  const hasSg = nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
56
- childProcess.spawnSync("npx", ["sg", "--version"], {
57
- encoding: "utf-8",
58
+ safeSpawn("npx", ["sg", "--version"], {
58
59
  timeout: 5000,
59
- shell: true,
60
60
  }).status === 0;
61
61
  if (!hasSg)
62
62
  return [];
63
- const result = childProcess.spawnSync("npx", [
63
+ const result = safeSpawn("npx", [
64
64
  "sg",
65
65
  "scan",
66
66
  "--config",
@@ -77,10 +77,7 @@ export function scanAstGrep(targetPath, isTsProject, configPath) {
77
77
  ...(isTsProject ? ["--globs", "!**/*.js"] : []),
78
78
  targetPath,
79
79
  ], {
80
- encoding: "utf-8",
81
80
  timeout: 30000,
82
- shell: true,
83
- maxBuffer: 32 * 1024 * 1024,
84
81
  });
85
82
  const raw = result.stdout?.trim() ?? "";
86
83
  const items = raw.startsWith("[")
@@ -125,13 +122,13 @@ export function scanAstGrep(targetPath, isTsProject, configPath) {
125
122
  export function scanBiomeIssues(biome, targetPath) {
126
123
  if (!biome.isAvailable())
127
124
  return [];
128
- const checkResult = childProcess.spawnSync("npx", [
125
+ const checkResult = safeSpawn("npx", [
129
126
  "@biomejs/biome",
130
127
  "check",
131
128
  "--reporter=json",
132
129
  "--max-diagnostics=50",
133
130
  targetPath,
134
- ], { encoding: "utf-8", timeout: 20000, shell: true });
131
+ ], { timeout: 20000 });
135
132
  const remainingBiome = [];
136
133
  try {
137
134
  const data = JSON.parse(checkResult.stdout ?? "{}");
@@ -165,14 +162,7 @@ export function scanSlop(complexity, targetPath, isTsProject) {
165
162
  for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
166
163
  const fullPath = path.join(dir, entry.name);
167
164
  if (entry.isDirectory()) {
168
- if ([
169
- "node_modules",
170
- ".git",
171
- "dist",
172
- "build",
173
- ".next",
174
- ".pi-lens",
175
- ].includes(entry.name))
165
+ if (EXCLUDED_DIRS.includes(entry.name))
176
166
  continue;
177
167
  scanDir(fullPath);
178
168
  }
@@ -14,8 +14,10 @@ import * as nodeFs from "node:fs";
14
14
  import * as path from "node:path";
15
15
  import type { BiomeClient } from "./biome-client.js";
16
16
  import type { ComplexityClient } from "./complexity-client.js";
17
+ import { EXCLUDED_DIRS } from "./file-utils.js";
17
18
  import type { JscpdClient } from "./jscpd-client.js";
18
19
  import type { KnipClient } from "./knip-client.js";
20
+ import { safeSpawn } from "./safe-spawn.js";
19
21
  import { shouldIgnoreFile } from "./scan-utils.js";
20
22
 
21
23
  export interface DuplicateClone {
@@ -68,8 +70,9 @@ function dbg(msg: string) {
68
70
  const line = `[${new Date().toISOString()}] ${msg}\n`;
69
71
  try {
70
72
  nodeFs.appendFileSync(DEBUG_LOG, line);
71
- } catch {
72
- // Ignored
73
+ } catch (err) {
74
+ // Debug logging failed, silently ignore to avoid recursive errors
75
+ void err;
73
76
  }
74
77
  }
75
78
 
@@ -118,15 +121,13 @@ export function scanAstGrep(
118
121
  ): AstIssue[] {
119
122
  const hasSg =
120
123
  nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
121
- childProcess.spawnSync("npx", ["sg", "--version"], {
122
- encoding: "utf-8",
124
+ safeSpawn("npx", ["sg", "--version"], {
123
125
  timeout: 5000,
124
- shell: true,
125
126
  }).status === 0;
126
127
 
127
128
  if (!hasSg) return [];
128
129
 
129
- const result = childProcess.spawnSync(
130
+ const result = safeSpawn(
130
131
  "npx",
131
132
  [
132
133
  "sg",
@@ -146,10 +147,7 @@ export function scanAstGrep(
146
147
  targetPath,
147
148
  ],
148
149
  {
149
- encoding: "utf-8",
150
150
  timeout: 30000,
151
- shell: true,
152
- maxBuffer: 32 * 1024 * 1024,
153
151
  },
154
152
  );
155
153
 
@@ -202,7 +200,7 @@ export function scanBiomeIssues(
202
200
  ): BiomeIssue[] {
203
201
  if (!biome.isAvailable()) return [];
204
202
 
205
- const checkResult = childProcess.spawnSync(
203
+ const checkResult = safeSpawn(
206
204
  "npx",
207
205
  [
208
206
  "@biomejs/biome",
@@ -211,7 +209,7 @@ export function scanBiomeIssues(
211
209
  "--max-diagnostics=50",
212
210
  targetPath,
213
211
  ],
214
- { encoding: "utf-8", timeout: 20000, shell: true },
212
+ { timeout: 20000 },
215
213
  );
216
214
 
217
215
  const remainingBiome: BiomeIssue[] = [];
@@ -251,17 +249,7 @@ export function scanSlop(
251
249
  for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
252
250
  const fullPath = path.join(dir, entry.name);
253
251
  if (entry.isDirectory()) {
254
- if (
255
- [
256
- "node_modules",
257
- ".git",
258
- "dist",
259
- "build",
260
- ".next",
261
- ".pi-lens",
262
- ].includes(entry.name)
263
- )
264
- continue;
252
+ if (EXCLUDED_DIRS.includes(entry.name)) continue;
265
253
  scanDir(fullPath);
266
254
  } else if (complexity.isSupportedFile(fullPath)) {
267
255
  const metrics = complexity.analyzeFile(fullPath);
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Format Service for pi-lens
3
+ *
4
+ * Concurrent formatter execution using Effect-TS.
5
+ * Auto-formats files on write with multiple formatters per file.
6
+ *
7
+ * Key features:
8
+ * - Auto-detects formatters based on project config
9
+ * - Runs multiple formatters concurrently via Effect.all
10
+ * - FileTime integration for safety
11
+ * - Multiple formatters per file (e.g., biome + prettier both run)
12
+ */
13
+ import * as path from "node:path";
14
+ import { Effect, pipe } from "effect";
15
+ import { FileTime } from "./file-time.js";
16
+ import { clearFormatterCache, formatFile, getFormattersForFile, } from "./formatters.js";
17
+ // --- Format Service ---
18
+ export class FormatService {
19
+ constructor(sessionID, enabled = true) {
20
+ this.fileTime = new FileTime(sessionID);
21
+ this.enabled = enabled;
22
+ }
23
+ /**
24
+ * Format a file with all detected formatters
25
+ * Runs formatters concurrently via Effect-TS
26
+ */
27
+ async formatFile(filePath, options = {}) {
28
+ const absolutePath = path.resolve(filePath);
29
+ const cwd = path.dirname(absolutePath);
30
+ // Skip if disabled
31
+ if (options.skip || !this.enabled) {
32
+ return {
33
+ filePath: absolutePath,
34
+ formatters: [],
35
+ anyChanged: false,
36
+ allSucceeded: true,
37
+ };
38
+ }
39
+ // Check if file was modified externally (safety check)
40
+ if (this.fileTime.hasChanged(absolutePath)) {
41
+ console.warn(`[format] File ${absolutePath} modified externally, skipping format`);
42
+ return {
43
+ filePath: absolutePath,
44
+ formatters: [],
45
+ anyChanged: false,
46
+ allSucceeded: false,
47
+ };
48
+ }
49
+ // Get formatters for this file
50
+ const formatters = options.formatters
51
+ ? await this.getFormattersByName(options.formatters)
52
+ : await getFormattersForFile(absolutePath, cwd);
53
+ if (formatters.length === 0) {
54
+ return {
55
+ filePath: absolutePath,
56
+ formatters: [],
57
+ anyChanged: false,
58
+ allSucceeded: true,
59
+ };
60
+ }
61
+ // Run all formatters concurrently via Effect-TS
62
+ const results = await this.runFormattersConcurrently(absolutePath, formatters);
63
+ // Record new file state after formatting
64
+ this.fileTime.read(absolutePath);
65
+ // Build summary
66
+ const anyChanged = results.some((r) => r.changed);
67
+ const allSucceeded = results.every((r) => r.success);
68
+ return {
69
+ filePath: absolutePath,
70
+ formatters: results.map((r, i) => ({
71
+ name: formatters[i].name,
72
+ success: r.success,
73
+ changed: r.changed,
74
+ error: r.error,
75
+ })),
76
+ anyChanged,
77
+ allSucceeded,
78
+ };
79
+ }
80
+ /**
81
+ * Run formatters concurrently using Effect-TS
82
+ */
83
+ async runFormattersConcurrently(filePath, formatters) {
84
+ // Create Effect for each formatter
85
+ const effects = formatters.map((formatter) => Effect.tryPromise({
86
+ try: () => formatFile(filePath, formatter),
87
+ catch: (error) => ({
88
+ success: false,
89
+ changed: false,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ }),
92
+ }));
93
+ // Run all concurrently with Effect.all
94
+ const program = pipe(Effect.all(effects, { concurrency: "unbounded" }), Effect.timeout(30000), // 30s total timeout for all formatters
95
+ Effect.catchAll((error) => {
96
+ console.error("[format] Concurrent formatting failed:", error);
97
+ return Effect.succeed(formatters.map(() => ({
98
+ success: false,
99
+ changed: false,
100
+ error: "Timeout or concurrent execution failed",
101
+ })));
102
+ }));
103
+ return Effect.runPromise(program);
104
+ }
105
+ /**
106
+ * Get formatters by name (for explicit formatter selection)
107
+ */
108
+ async getFormattersByName(names) {
109
+ const { listAllFormatters, ...formatters } = await import("./formatters.js");
110
+ const allNames = listAllFormatters();
111
+ return names
112
+ .filter((name) => allNames.includes(name))
113
+ .map((name) => {
114
+ // Access formatter by name from the exports
115
+ const key = `${name}Formatter`;
116
+ return formatters[key];
117
+ })
118
+ .filter(Boolean);
119
+ }
120
+ /**
121
+ * Assert file hasn't changed before editing
122
+ * Throws FileTimeError if file modified externally
123
+ */
124
+ assertUnchanged(filePath) {
125
+ this.fileTime.assert(filePath);
126
+ }
127
+ /**
128
+ * Check if file has changed externally
129
+ */
130
+ hasChanged(filePath) {
131
+ return this.fileTime.hasChanged(filePath);
132
+ }
133
+ /**
134
+ * Record file read (after agent reads file)
135
+ */
136
+ recordRead(filePath) {
137
+ this.fileTime.read(filePath);
138
+ }
139
+ /**
140
+ * Clear detection cache
141
+ */
142
+ clearCache() {
143
+ clearFormatterCache();
144
+ }
145
+ }
146
+ // --- Singleton Instance ---
147
+ let globalFormatService = null;
148
+ let currentSessionID = null;
149
+ export function getFormatService(sessionID, enabled = true) {
150
+ // Create new instance if:
151
+ // 1. No service exists yet
152
+ // 2. Session ID changed (different session)
153
+ const shouldCreateNew = !globalFormatService || (sessionID && sessionID !== currentSessionID);
154
+ if (shouldCreateNew) {
155
+ globalFormatService = new FormatService(sessionID ?? "default", enabled);
156
+ currentSessionID = sessionID ?? "default";
157
+ }
158
+ return globalFormatService;
159
+ }
160
+ export function resetFormatService() {
161
+ globalFormatService = null;
162
+ currentSessionID = null;
163
+ }
164
+ /**
165
+ * Reset format service and clear all file tracking state.
166
+ * Use this in tests to ensure complete isolation.
167
+ */
168
+ export function clearFormatServiceAndFileState() {
169
+ resetFormatService();
170
+ }
171
+ // Re-export for convenience
172
+ export { clearAllSessions } from "./file-time.js";
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Format Service for pi-lens
3
+ *
4
+ * Concurrent formatter execution using Effect-TS.
5
+ * Auto-formats files on write with multiple formatters per file.
6
+ *
7
+ * Key features:
8
+ * - Auto-detects formatters based on project config
9
+ * - Runs multiple formatters concurrently via Effect.all
10
+ * - FileTime integration for safety
11
+ * - Multiple formatters per file (e.g., biome + prettier both run)
12
+ */
13
+
14
+ import * as path from "node:path";
15
+ import { Effect, pipe } from "effect";
16
+ import { FileTime } from "./file-time.js";
17
+ import {
18
+ clearFormatterCache,
19
+ type FormatterInfo,
20
+ type FormatterResult,
21
+ formatFile,
22
+ getFormattersForFile,
23
+ } from "./formatters.js";
24
+
25
+ // --- Types ---
26
+
27
+ export interface FormatOptions {
28
+ /** Skip auto-format even if enabled (manual mode) */
29
+ skip?: boolean;
30
+ /** Specific formatters to use (overrides detection) */
31
+ formatters?: string[];
32
+ }
33
+
34
+ export interface FormatSummary {
35
+ filePath: string;
36
+ formatters: Array<{
37
+ name: string;
38
+ success: boolean;
39
+ changed: boolean;
40
+ error?: string;
41
+ }>;
42
+ anyChanged: boolean;
43
+ allSucceeded: boolean;
44
+ }
45
+
46
+ // --- Format Service ---
47
+
48
+ export class FormatService {
49
+ private fileTime: FileTime;
50
+ private enabled: boolean;
51
+
52
+ constructor(sessionID: string, enabled: boolean = true) {
53
+ this.fileTime = new FileTime(sessionID);
54
+ this.enabled = enabled;
55
+ }
56
+
57
+ /**
58
+ * Format a file with all detected formatters
59
+ * Runs formatters concurrently via Effect-TS
60
+ */
61
+ async formatFile(
62
+ filePath: string,
63
+ options: FormatOptions = {},
64
+ ): Promise<FormatSummary> {
65
+ const absolutePath = path.resolve(filePath);
66
+ const cwd = path.dirname(absolutePath);
67
+
68
+ // Skip if disabled
69
+ if (options.skip || !this.enabled) {
70
+ return {
71
+ filePath: absolutePath,
72
+ formatters: [],
73
+ anyChanged: false,
74
+ allSucceeded: true,
75
+ };
76
+ }
77
+
78
+ // Check if file was modified externally (safety check)
79
+ if (this.fileTime.hasChanged(absolutePath)) {
80
+ console.warn(
81
+ `[format] File ${absolutePath} modified externally, skipping format`,
82
+ );
83
+ return {
84
+ filePath: absolutePath,
85
+ formatters: [],
86
+ anyChanged: false,
87
+ allSucceeded: false,
88
+ };
89
+ }
90
+
91
+ // Get formatters for this file
92
+ const formatters = options.formatters
93
+ ? await this.getFormattersByName(options.formatters)
94
+ : await getFormattersForFile(absolutePath, cwd);
95
+
96
+ if (formatters.length === 0) {
97
+ return {
98
+ filePath: absolutePath,
99
+ formatters: [],
100
+ anyChanged: false,
101
+ allSucceeded: true,
102
+ };
103
+ }
104
+
105
+ // Run all formatters concurrently via Effect-TS
106
+ const results = await this.runFormattersConcurrently(
107
+ absolutePath,
108
+ formatters,
109
+ );
110
+
111
+ // Record new file state after formatting
112
+ this.fileTime.read(absolutePath);
113
+
114
+ // Build summary
115
+ const anyChanged = results.some((r) => r.changed);
116
+ const allSucceeded = results.every((r) => r.success);
117
+
118
+ return {
119
+ filePath: absolutePath,
120
+ formatters: results.map((r, i) => ({
121
+ name: formatters[i].name,
122
+ success: r.success,
123
+ changed: r.changed,
124
+ error: r.error,
125
+ })),
126
+ anyChanged,
127
+ allSucceeded,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Run formatters concurrently using Effect-TS
133
+ */
134
+ private async runFormattersConcurrently(
135
+ filePath: string,
136
+ formatters: FormatterInfo[],
137
+ ): Promise<FormatterResult[]> {
138
+ // Create Effect for each formatter
139
+ const effects = formatters.map((formatter) =>
140
+ Effect.tryPromise({
141
+ try: () => formatFile(filePath, formatter),
142
+ catch: (error): FormatterResult => ({
143
+ success: false,
144
+ changed: false,
145
+ error: error instanceof Error ? error.message : String(error),
146
+ }),
147
+ }),
148
+ );
149
+
150
+ // Run all concurrently with Effect.all
151
+ const program = pipe(
152
+ Effect.all(effects, { concurrency: "unbounded" }),
153
+ Effect.timeout(30000), // 30s total timeout for all formatters
154
+ Effect.catchAll((error): Effect.Effect<FormatterResult[]> => {
155
+ console.error("[format] Concurrent formatting failed:", error);
156
+ return Effect.succeed(
157
+ formatters.map(() => ({
158
+ success: false,
159
+ changed: false,
160
+ error: "Timeout or concurrent execution failed",
161
+ })),
162
+ );
163
+ }),
164
+ );
165
+
166
+ return Effect.runPromise(program);
167
+ }
168
+
169
+ /**
170
+ * Get formatters by name (for explicit formatter selection)
171
+ */
172
+ private async getFormattersByName(names: string[]): Promise<FormatterInfo[]> {
173
+ const { listAllFormatters, ...formatters } = await import(
174
+ "./formatters.js"
175
+ );
176
+ const allNames = listAllFormatters();
177
+
178
+ return names
179
+ .filter((name) => allNames.includes(name))
180
+ .map((name) => {
181
+ // Access formatter by name from the exports
182
+ const key = `${name}Formatter` as keyof typeof formatters;
183
+ return formatters[key] as FormatterInfo;
184
+ })
185
+ .filter(Boolean);
186
+ }
187
+
188
+ /**
189
+ * Assert file hasn't changed before editing
190
+ * Throws FileTimeError if file modified externally
191
+ */
192
+ assertUnchanged(filePath: string): void {
193
+ this.fileTime.assert(filePath);
194
+ }
195
+
196
+ /**
197
+ * Check if file has changed externally
198
+ */
199
+ hasChanged(filePath: string): boolean {
200
+ return this.fileTime.hasChanged(filePath);
201
+ }
202
+
203
+ /**
204
+ * Record file read (after agent reads file)
205
+ */
206
+ recordRead(filePath: string): void {
207
+ this.fileTime.read(filePath);
208
+ }
209
+
210
+ /**
211
+ * Clear detection cache
212
+ */
213
+ clearCache(): void {
214
+ clearFormatterCache();
215
+ }
216
+ }
217
+
218
+ // --- Singleton Instance ---
219
+
220
+ let globalFormatService: FormatService | null = null;
221
+ let currentSessionID: string | null = null;
222
+
223
+ export function getFormatService(
224
+ sessionID?: string,
225
+ enabled: boolean = true,
226
+ ): FormatService {
227
+ // Create new instance if:
228
+ // 1. No service exists yet
229
+ // 2. Session ID changed (different session)
230
+ const shouldCreateNew =
231
+ !globalFormatService || (sessionID && sessionID !== currentSessionID);
232
+
233
+ if (shouldCreateNew) {
234
+ globalFormatService = new FormatService(sessionID ?? "default", enabled);
235
+ currentSessionID = sessionID ?? "default";
236
+ }
237
+ return globalFormatService!;
238
+ }
239
+
240
+ export function resetFormatService(): void {
241
+ globalFormatService = null;
242
+ currentSessionID = null;
243
+ }
244
+
245
+ /**
246
+ * Reset format service and clear all file tracking state.
247
+ * Use this in tests to ensure complete isolation.
248
+ */
249
+ export function clearFormatServiceAndFileState(): void {
250
+ resetFormatService();
251
+ }
252
+
253
+ // Re-export for convenience
254
+ export { clearAllSessions } from "./file-time.js";