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
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Ruff Client for pi-lens
3
+ *
4
+ * Fast Python linting and formatting via Ruff CLI.
5
+ * Replaces flake8, pylint, isort, black, pyupgrade.
6
+ *
7
+ * Requires: pip install ruff
8
+ * Docs: https://docs.astral.sh/ruff/
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { isFileKind } from "./file-kinds.js";
13
+ import { safeSpawn } from "./safe-spawn.js";
14
+ // --- Client ---
15
+ export class RuffClient {
16
+ ruffAvailable = null;
17
+ log;
18
+ constructor(verbose = false) {
19
+ this.log = verbose
20
+ ? (msg) => console.error(`[ruff] ${msg}`)
21
+ : () => { };
22
+ }
23
+ /**
24
+ * Check if ruff CLI is available
25
+ */
26
+ isAvailable() {
27
+ if (this.ruffAvailable !== null)
28
+ return this.ruffAvailable;
29
+ try {
30
+ const result = safeSpawn("ruff", ["--version"], {
31
+ timeout: 5000,
32
+ });
33
+ this.ruffAvailable = !result.error && result.status === 0;
34
+ if (this.ruffAvailable) {
35
+ this.log(`Ruff found: ${result.stdout.trim()}`);
36
+ }
37
+ }
38
+ catch (err) {
39
+ void err;
40
+ this.ruffAvailable = false;
41
+ }
42
+ return this.ruffAvailable;
43
+ }
44
+ /**
45
+ * Check if a file is a Python file
46
+ */
47
+ isPythonFile(filePath) {
48
+ return isFileKind(filePath, "python");
49
+ }
50
+ /**
51
+ * Lint a Python file
52
+ */
53
+ checkFile(filePath) {
54
+ if (!this.isAvailable())
55
+ return [];
56
+ const absolutePath = path.resolve(filePath);
57
+ if (!fs.existsSync(absolutePath))
58
+ return [];
59
+ try {
60
+ const result = safeSpawn("ruff", [
61
+ "check",
62
+ "--output-format",
63
+ "json",
64
+ "--target-version",
65
+ "py310",
66
+ absolutePath,
67
+ ], {
68
+ timeout: 10000,
69
+ });
70
+ // ruff exits 1 when it finds issues (normal)
71
+ const output = result.stdout || "";
72
+ if (!output.trim())
73
+ return [];
74
+ return this.parseOutput(output, absolutePath);
75
+ }
76
+ catch (err) {
77
+ this.log(`Check error: ${err.message}`);
78
+ return [];
79
+ }
80
+ }
81
+ /**
82
+ * Check if file has formatting issues (ruff format --check)
83
+ */
84
+ checkFormatting(filePath) {
85
+ if (!this.isAvailable())
86
+ return "";
87
+ const absolutePath = path.resolve(filePath);
88
+ if (!fs.existsSync(absolutePath))
89
+ return "";
90
+ try {
91
+ const result = safeSpawn("ruff", ["format", "--check", "--diff", absolutePath], {
92
+ timeout: 10000,
93
+ });
94
+ // ruff format --check exits 1 when changes needed
95
+ if (result.status === 0)
96
+ return "";
97
+ const diff = result.stdout || "";
98
+ if (!diff.trim())
99
+ return "";
100
+ // Count lines that would change
101
+ const diffLines = diff
102
+ .split("\n")
103
+ .filter((l) => l.startsWith("+") || l.startsWith("-")).length;
104
+ return `[Ruff Format] ${diffLines} line(s) would change — run 'ruff format ${path.basename(filePath)}' to fix`;
105
+ }
106
+ catch (err) {
107
+ void err;
108
+ return "";
109
+ } // Intentionally return empty string on diff failure
110
+ }
111
+ /**
112
+ * Auto-fix linting issues (writes to disk)
113
+ */
114
+ fixFile(filePath) {
115
+ if (!this.isAvailable())
116
+ return {
117
+ success: false,
118
+ changed: false,
119
+ fixed: 0,
120
+ error: "Ruff not available",
121
+ };
122
+ const absolutePath = path.resolve(filePath);
123
+ if (!fs.existsSync(absolutePath))
124
+ return {
125
+ success: false,
126
+ changed: false,
127
+ fixed: 0,
128
+ error: "File not found",
129
+ };
130
+ const content = fs.readFileSync(absolutePath, "utf-8");
131
+ try {
132
+ const beforeDiags = this.checkFile(filePath);
133
+ const fixableCount = beforeDiags.filter((d) => d.fixable).length;
134
+ const result = safeSpawn("ruff", ["check", "--fix", absolutePath], {
135
+ timeout: 15000,
136
+ });
137
+ if (result.error) {
138
+ return {
139
+ success: false,
140
+ changed: false,
141
+ fixed: 0,
142
+ error: result.error.message,
143
+ };
144
+ }
145
+ const fixed = fs.readFileSync(absolutePath, "utf-8");
146
+ const changed = content !== fixed;
147
+ if (changed) {
148
+ this.log(`Fixed ${fixableCount} issue(s) in ${path.basename(filePath)}`);
149
+ }
150
+ return { success: true, changed, fixed: fixableCount };
151
+ }
152
+ catch (err) {
153
+ return { success: false, changed: false, fixed: 0, error: err.message };
154
+ }
155
+ }
156
+ /**
157
+ * Fix multiple Python files at once (much faster than file-by-file)
158
+ */
159
+ fixFiles(filePaths) {
160
+ if (!this.isAvailable()) {
161
+ return {
162
+ success: false,
163
+ fixed: 0,
164
+ changed: 0,
165
+ error: "Ruff not available",
166
+ };
167
+ }
168
+ // Filter to existing Python files
169
+ const validFiles = filePaths
170
+ .map(f => path.resolve(f))
171
+ .filter(f => fs.existsSync(f) && f.endsWith(".py"));
172
+ if (validFiles.length === 0) {
173
+ return { success: true, fixed: 0, changed: 0 };
174
+ }
175
+ try {
176
+ // Count fixable issues before fixing
177
+ let totalFixable = 0;
178
+ for (const file of validFiles) {
179
+ const diags = this.checkFile(file);
180
+ totalFixable += diags.filter(d => d.fixable).length;
181
+ }
182
+ // Run ruff once on all files - much faster than per file
183
+ const result = safeSpawn("ruff", ["check", "--fix", ...validFiles], {
184
+ timeout: 60000, // Longer timeout for batch
185
+ });
186
+ if (result.error) {
187
+ return {
188
+ success: false,
189
+ fixed: 0,
190
+ changed: 0,
191
+ error: result.error.message,
192
+ };
193
+ }
194
+ this.log(`Fixed ${totalFixable} issue(s) in ${validFiles.length} file(s)`);
195
+ return { success: true, fixed: totalFixable, changed: validFiles.length };
196
+ }
197
+ catch (err) {
198
+ return {
199
+ success: false,
200
+ fixed: 0,
201
+ changed: 0,
202
+ error: err.message,
203
+ };
204
+ }
205
+ }
206
+ /**
207
+ * Format a Python file (writes to disk)
208
+ */
209
+ formatFile(filePath) {
210
+ if (!this.isAvailable())
211
+ return { success: false, changed: false, error: "Ruff not available" };
212
+ const absolutePath = path.resolve(filePath);
213
+ if (!fs.existsSync(absolutePath))
214
+ return { success: false, changed: false, error: "File not found" };
215
+ const content = fs.readFileSync(absolutePath, "utf-8");
216
+ try {
217
+ const result = safeSpawn("ruff", ["format", absolutePath], {
218
+ timeout: 10000,
219
+ });
220
+ if (result.error) {
221
+ return { success: false, changed: false, error: result.error.message };
222
+ }
223
+ const formatted = fs.readFileSync(absolutePath, "utf-8");
224
+ const changed = content !== formatted;
225
+ if (changed) {
226
+ this.log(`Formatted ${path.basename(filePath)}`);
227
+ }
228
+ return { success: true, changed };
229
+ }
230
+ catch (err) {
231
+ return { success: false, changed: false, error: err.message };
232
+ }
233
+ }
234
+ /**
235
+ * Format diagnostics for LLM consumption
236
+ */
237
+ formatDiagnostics(diags) {
238
+ if (diags.length === 0)
239
+ return "";
240
+ const errors = diags.filter((d) => d.severity === "error");
241
+ const warnings = diags.filter((d) => d.severity === "warning");
242
+ const fixable = diags.filter((d) => d.fixable);
243
+ let result = `[Ruff] ${diags.length} issue(s)`;
244
+ if (errors.length)
245
+ result += ` — ${errors.length} error(s)`;
246
+ if (warnings.length)
247
+ result += ` — ${warnings.length} warning(s)`;
248
+ if (fixable.length)
249
+ result += ` — ${fixable.length} auto-fixable`;
250
+ result += ":\n";
251
+ for (const d of diags.slice(0, 15)) {
252
+ const loc = d.line === d.endLine
253
+ ? `L${d.line}:${d.column}-${d.endColumn}`
254
+ : `L${d.line}:${d.column}-L${d.endLine}:${d.endColumn}`;
255
+ const fix = d.fixable ? " [fixable]" : "";
256
+ result += ` [${d.rule}] ${loc} ${d.message}${fix}\n`;
257
+ }
258
+ if (diags.length > 15) {
259
+ result += ` ... and ${diags.length - 15} more\n`;
260
+ }
261
+ if (fixable.length > 0) {
262
+ result += `\n Run 'ruff check --fix ${path.basename(diags[0].file)}' to auto-fix ${fixable.length} issue(s)\n`;
263
+ }
264
+ return result;
265
+ }
266
+ // --- Internal ---
267
+ parseOutput(output, filterFile) {
268
+ if (!output.trim())
269
+ return [];
270
+ try {
271
+ const items = JSON.parse(output);
272
+ const diagnostics = [];
273
+ for (const item of items) {
274
+ // Filter to single file if requested
275
+ if (filterFile && path.resolve(item.filename) !== filterFile)
276
+ continue;
277
+ diagnostics.push({
278
+ line: item.location.row - 1, // ruff is 1-indexed
279
+ column: item.location.column - 1,
280
+ endLine: item.end_location.row - 1,
281
+ endColumn: item.end_location.column - 1,
282
+ severity: item.code?.startsWith("E") ? "error" : "warning",
283
+ message: item.message,
284
+ rule: item.code || "unknown",
285
+ file: item.filename,
286
+ fixable: item.fix !== null,
287
+ });
288
+ }
289
+ return diagnostics;
290
+ }
291
+ catch (err) {
292
+ void err;
293
+ this.log("Failed to parse ruff JSON output");
294
+ return [];
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,88 @@
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, and special characters.
16
+ */
17
+ function escapeWindowsArg(arg) {
18
+ // If no special characters, return as-is
19
+ if (!/[\s\"]/.test(arg))
20
+ return arg;
21
+ // Escape quotes by doubling them
22
+ return `"${arg.replace(/"/g, "\"\"")}"`;
23
+ }
24
+ /**
25
+ * Construct a command string for Windows shell execution.
26
+ * This avoids the deprecation warning by not passing an args array.
27
+ */
28
+ function buildWindowsCommand(command, args) {
29
+ const escapedArgs = args.map(escapeWindowsArg).join(" ");
30
+ return `${command} ${escapedArgs}`;
31
+ }
32
+ /**
33
+ * Safely spawn a process cross-platform without shell deprecation warnings.
34
+ *
35
+ * On Windows: Uses shell: true but constructs the command string manually
36
+ * to avoid the deprecation warning about unescaped args.
37
+ * On Unix: Uses shell: false for normal execution.
38
+ */
39
+ export function safeSpawn(command, args, options) {
40
+ if (process.platform === "win32") {
41
+ // On Windows, construct the full command string and use shell: true
42
+ // without an args array. This avoids the deprecation warning.
43
+ const fullCommand = buildWindowsCommand(command, args);
44
+ const result = spawnSync(fullCommand, {
45
+ ...options,
46
+ encoding: "utf-8",
47
+ shell: true,
48
+ windowsHide: true,
49
+ });
50
+ return {
51
+ stdout: result.stdout?.toString() || "",
52
+ stderr: result.stderr?.toString() || "",
53
+ status: result.status,
54
+ error: result.error,
55
+ };
56
+ }
57
+ // On Unix, use shell: false (the default) with normal args
58
+ const result = spawnSync(command, args, {
59
+ ...options,
60
+ encoding: "utf-8",
61
+ shell: false,
62
+ windowsHide: true,
63
+ });
64
+ return {
65
+ stdout: result.stdout?.toString() || "",
66
+ stderr: result.stderr?.toString() || "",
67
+ status: result.status,
68
+ error: result.error,
69
+ };
70
+ }
71
+ /**
72
+ * Check if a command is available in PATH
73
+ */
74
+ export function isCommandAvailable(command) {
75
+ const result = safeSpawn(process.platform === "win32" ? "where" : "which", [command], { timeout: 5000 });
76
+ return result.status === 0;
77
+ }
78
+ /**
79
+ * Find the full path to a command (npx, node, etc.)
80
+ */
81
+ export function findCommand(command) {
82
+ const finder = process.platform === "win32" ? "where" : "which";
83
+ const result = safeSpawn(finder, [command], { timeout: 5000 });
84
+ if (result.status !== 0)
85
+ return null;
86
+ // Take first line (first match)
87
+ return result.stdout.trim().split("\n")[0] || null;
88
+ }
@@ -0,0 +1,83 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { EXCLUDED_DIRS, isTestFile } from "./file-utils.js";
4
+ /**
5
+ * Common parsing logic for ast-grep JSON output (handles both array and NDJSON).
6
+ */
7
+ // biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
8
+ export function parseAstGrepJson(raw) {
9
+ if (!raw)
10
+ return [];
11
+ const trimmed = raw.trim();
12
+ if (trimmed.startsWith("[")) {
13
+ try {
14
+ return JSON.parse(trimmed);
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
20
+ return trimmed.split("\n").flatMap((l) => {
21
+ try {
22
+ return [JSON.parse(l)];
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ });
28
+ }
29
+ /**
30
+ * Check if a file should be ignored based on project type and common patterns.
31
+ */
32
+ export function shouldIgnoreFile(filePath, isTsProject) {
33
+ const relPath = filePath.replace(/\\/g, "/");
34
+ const _basename = path.basename(relPath);
35
+ // Ignore compiled JS in TS projects
36
+ const isJs = relPath.endsWith(".js") ||
37
+ relPath.endsWith(".mjs") ||
38
+ relPath.endsWith(".cjs");
39
+ if (isTsProject && isJs)
40
+ return true;
41
+ // Ignore test scripts and common test patterns
42
+ if (isTestFile(filePath))
43
+ return true;
44
+ // Ignore hidden directories and common build outputs
45
+ if (EXCLUDED_DIRS.some((d) => relPath.includes(`/${d}/`)))
46
+ return true;
47
+ return false;
48
+ }
49
+ /**
50
+ * Recursively find source files in a directory, respecting common excludes.
51
+ */
52
+ export function getSourceFiles(dir, isTsProject) {
53
+ const files = [];
54
+ if (!fs.existsSync(dir))
55
+ return files;
56
+ const scan = (d) => {
57
+ let entries = [];
58
+ try {
59
+ entries = fs.readdirSync(d, { withFileTypes: true });
60
+ }
61
+ catch {
62
+ return;
63
+ }
64
+ for (const entry of entries) {
65
+ const full = path.join(d, entry.name);
66
+ if (entry.isDirectory()) {
67
+ if (EXCLUDED_DIRS.includes(entry.name))
68
+ continue;
69
+ scan(full);
70
+ }
71
+ else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
72
+ // Skip compiled JS if it's a TS project
73
+ if (isTsProject &&
74
+ entry.name.endsWith(".js") &&
75
+ fs.existsSync(full.replace(/\.js$/, ".ts")))
76
+ continue;
77
+ files.push(full);
78
+ }
79
+ }
80
+ };
81
+ scan(dir);
82
+ return files;
83
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * SgRunner - encapsulates ast-grep subprocess management
3
+ *
4
+ * Extracted from AstGrepClient to simplify the main client.
5
+ * Handles: spawn, spawnSync, temp dir management, JSON parsing.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ import { safeSpawn } from "./safe-spawn.js";
12
+ export class SgRunner {
13
+ log;
14
+ constructor(verbose = false) {
15
+ this.log = verbose
16
+ ? (msg) => console.error(`[sg-runner] ${msg}`)
17
+ : () => { };
18
+ }
19
+ /**
20
+ * Check if ast-grep CLI is available
21
+ */
22
+ isAvailable() {
23
+ const result = safeSpawn("npx", ["sg", "--version"], {
24
+ timeout: 10000,
25
+ });
26
+ return !result.error && result.status === 0;
27
+ }
28
+ /**
29
+ * Run ast-grep asynchronously, return parsed matches
30
+ */
31
+ async exec(args) {
32
+ return new Promise((resolve) => {
33
+ // On Windows, construct full command string to avoid deprecation warning
34
+ const useShell = process.platform === "win32";
35
+ const fullCommand = useShell
36
+ ? `npx sg ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`
37
+ : undefined;
38
+ const proc = fullCommand
39
+ ? spawn(fullCommand, { stdio: ["ignore", "pipe", "pipe"], shell: true })
40
+ : spawn("npx", ["sg", ...args], { stdio: ["ignore", "pipe", "pipe"] });
41
+ let stdout = "";
42
+ let stderr = "";
43
+ proc.stdout.on("data", (data) => (stdout += data.toString()));
44
+ proc.stderr.on("data", (data) => (stderr += data.toString()));
45
+ proc.on("error", (err) => {
46
+ if (err.message.includes("ENOENT")) {
47
+ resolve({
48
+ matches: [],
49
+ error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
50
+ });
51
+ }
52
+ else {
53
+ resolve({ matches: [], error: err.message });
54
+ }
55
+ });
56
+ proc.on("close", (code) => {
57
+ if (code !== 0 && !stdout.trim()) {
58
+ resolve({
59
+ matches: [],
60
+ error: stderr.includes("No files found")
61
+ ? undefined
62
+ : stderr.trim() || `Exit code ${code}`,
63
+ });
64
+ return;
65
+ }
66
+ if (!stdout.trim()) {
67
+ resolve({ matches: [] });
68
+ return;
69
+ }
70
+ try {
71
+ const parsed = JSON.parse(stdout);
72
+ const matches = Array.isArray(parsed) ? parsed : [parsed];
73
+ resolve({ matches });
74
+ }
75
+ catch {
76
+ resolve({ matches: [], error: "Failed to parse output" });
77
+ }
78
+ });
79
+ });
80
+ }
81
+ /**
82
+ * Run ast-grep synchronously (for simple scans)
83
+ */
84
+ execSync(args) {
85
+ const result = safeSpawn("npx", ["sg", ...args], {
86
+ timeout: 30000,
87
+ });
88
+ if (result.error) {
89
+ return { output: "", error: result.error.message };
90
+ }
91
+ const output = result.stdout || result.stderr || "";
92
+ return { output };
93
+ }
94
+ /**
95
+ * Run a temporary rule scan (creates temp dir with rule file)
96
+ */
97
+ tempScan(dir, ruleId, ruleYaml, timeout = 30000) {
98
+ const tmpDir = os.tmpdir();
99
+ const ts = Date.now();
100
+ const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
101
+ const rulesSubdir = path.join(sessionDir, "rules");
102
+ const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
103
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
104
+ try {
105
+ fs.mkdirSync(rulesSubdir, { recursive: true });
106
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
107
+ fs.writeFileSync(ruleFile, ruleYaml);
108
+ const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
109
+ const output = result.stdout || result.stderr || "";
110
+ if (!output.trim())
111
+ return [];
112
+ const items = JSON.parse(output);
113
+ return Array.isArray(items) ? items : [items];
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ finally {
119
+ try {
120
+ fs.rmSync(sessionDir, { recursive: true, force: true });
121
+ }
122
+ catch (err) {
123
+ this.log(`Cleanup failed: ${err.message}`);
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Run a rule file scan (temporary config approach) - alias for tempScan
129
+ */
130
+ scanWithRule(ruleYaml, dir, timeout = 30000) {
131
+ const sessionDir = path.join(os.tmpdir(), `sg-scan-${Date.now()}`);
132
+ const rulesSubdir = path.join(sessionDir, "rules");
133
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
134
+ const ruleFile = path.join(rulesSubdir, "rule.yml");
135
+ try {
136
+ fs.mkdirSync(rulesSubdir, { recursive: true });
137
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
138
+ fs.writeFileSync(ruleFile, ruleYaml);
139
+ const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
140
+ const output = result.stdout || result.stderr || "";
141
+ if (!output.trim())
142
+ return [];
143
+ const items = JSON.parse(output);
144
+ return Array.isArray(items) ? items : [items];
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ finally {
150
+ try {
151
+ fs.rmSync(sessionDir, { recursive: true, force: true });
152
+ }
153
+ catch (err) {
154
+ this.log(`Cleanup failed: ${err.message}`);
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Format matches for display
160
+ */
161
+ formatMatches(matches, isDryRun = false, maxItems = 50, showModeIndicator = false) {
162
+ if (matches.length === 0) {
163
+ if (showModeIndicator) {
164
+ return isDryRun
165
+ ? "[DRY-RUN] No matches found."
166
+ : "[APPLIED] No changes made (no matches found).";
167
+ }
168
+ return "No matches found";
169
+ }
170
+ const shown = matches.slice(0, maxItems);
171
+ const lines = shown.map((m) => {
172
+ const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
173
+ const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
174
+ return isDryRun && m.replacement
175
+ ? `${loc}\n - ${text}\n + ${m.replacement}`
176
+ : `${loc}: ${text}`;
177
+ });
178
+ if (matches.length > maxItems) {
179
+ lines.unshift(`Found ${matches.length} matches (showing first ${maxItems}):`);
180
+ }
181
+ if (showModeIndicator) {
182
+ const prefix = isDryRun ? "[DRY-RUN]" : "[APPLIED]";
183
+ const suffix = isDryRun
184
+ ? "\n\n(Dry run — use apply=true to apply changes)"
185
+ : "";
186
+ return `${prefix} ${matches.length} replacement(s):\n\n${lines.join("\n")}${suffix}`;
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * LSP Types - Core types for the pi-local LSP client
3
+ * Simplified from the full LSP spec to what we actually need
4
+ */
5
+ export var DiagnosticSeverity;
6
+ (function (DiagnosticSeverity) {
7
+ DiagnosticSeverity[DiagnosticSeverity["Error"] = 1] = "Error";
8
+ DiagnosticSeverity[DiagnosticSeverity["Warning"] = 2] = "Warning";
9
+ DiagnosticSeverity[DiagnosticSeverity["Information"] = 3] = "Information";
10
+ DiagnosticSeverity[DiagnosticSeverity["Hint"] = 4] = "Hint";
11
+ })(DiagnosticSeverity || (DiagnosticSeverity = {}));