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,558 @@
1
+ /**
2
+ * Config Validation via Tree-sitter
3
+ *
4
+ * Detects config/environment variable access in code and validates against
5
+ * actual config files (INI, YAML, JSON, .env).
6
+ *
7
+ * Catches:
8
+ * - Undefined config keys
9
+ * - Typos in config keys
10
+ * - Missing environment variables
11
+ * - Deprecated/renamed keys
12
+ *
13
+ * Supported patterns:
14
+ * - Python: config.get("section.key"), os.environ.get("VAR")
15
+ * - JS/TS: process.env.VAR, config.get("key")
16
+ * - Go: os.Getenv("VAR")
17
+ * - Rust: env::var("VAR")
18
+ */
19
+
20
+ import * as fs from "node:fs/promises";
21
+ import * as path from "node:path";
22
+ import { TreeSitterClient } from "./tree-sitter-client.js";
23
+
24
+ // --- Types ---
25
+
26
+ export interface ConfigKey {
27
+ key: string;
28
+ file: string;
29
+ line: number;
30
+ value?: string;
31
+ }
32
+
33
+ export interface ConfigAccess {
34
+ key: string;
35
+ file: string;
36
+ line: number;
37
+ column: number;
38
+ pattern: string;
39
+ }
40
+
41
+ export interface ConfigValidationResult {
42
+ undefined: ConfigAccess[];
43
+ typos: Array<{ access: ConfigAccess; suggestion: string }>;
44
+ available: ConfigKey[];
45
+ }
46
+
47
+ // --- Tree-sitter Queries for Config Access Patterns ---
48
+
49
+ const CONFIG_QUERIES: Record<string, string> = {
50
+ // Python: config.get("section.key") or os.environ.get("VAR")
51
+ python: `
52
+ ; Config object access: config.get("key")
53
+ (call
54
+ function: (attribute
55
+ object: (identifier) @config_obj
56
+ attribute: (identifier) @method (#eq? @method "get") )
57
+ arguments: (argument_list
58
+ (string
59
+ (string_content) @config_key
60
+ )
61
+ )
62
+ )
63
+ (#match? @config_obj "^(config|cfg|settings|conf)$")
64
+
65
+ ; os.environ.get("VAR")
66
+ (call
67
+ function: (attribute
68
+ object: (attribute
69
+ object: (identifier) @os (#eq? @os "os")
70
+ attribute: (identifier) @environ (#eq? @environ "environ")
71
+ )
72
+ attribute: (identifier) @method (#eq? @method "get")
73
+ )
74
+ arguments: (argument_list
75
+ (string (string_content) @env_var)
76
+ )
77
+ )
78
+
79
+ ; os.getenv("VAR")
80
+ (call
81
+ function: (attribute
82
+ object: (identifier) @os (#eq? @os "os")
83
+ attribute: (identifier) @getenv (#eq? @getenv "getenv")
84
+ )
85
+ arguments: (argument_list
86
+ (string (string_content) @env_var)
87
+ )
88
+ )
89
+ `,
90
+
91
+ // JavaScript/TypeScript: process.env.VAR or config.get("key")
92
+ javascript: `
93
+ ; process.env.VAR or process.env["VAR"]
94
+ (member_expression
95
+ object: (member_expression
96
+ object: (identifier) @process (#eq? @process "process")
97
+ property: (property_identifier) @env (#eq? @env "env")
98
+ )
99
+ property: (property_identifier) @env_var
100
+ )
101
+
102
+ ; process.env["VAR"]
103
+ (member_expression
104
+ object: (member_expression
105
+ object: (identifier) @process (#eq? @process "process")
106
+ property: (property_identifier) @env (#eq? @env "env")
107
+ )
108
+ property: (computed_property_name
109
+ (string (string_fragment) @env_var)
110
+ )
111
+ )
112
+
113
+ ; config.get("key") or cfg.get("key")
114
+ (call_expression
115
+ function: (member_expression
116
+ object: (identifier) @config_obj
117
+ property: (property_identifier) @method (#eq? @method "get")
118
+ )
119
+ arguments: (arguments
120
+ (string (string_fragment) @config_key)
121
+ )
122
+ )
123
+ (#match? @config_obj "^(config|cfg|settings|conf)$")
124
+ `,
125
+
126
+ // Same for TypeScript (tsx)
127
+ tsx: `
128
+ ; process.env.VAR
129
+ (member_expression
130
+ object: (member_expression
131
+ object: (identifier) @process (#eq? @process "process")
132
+ property: (property_identifier) @env (#eq? @env "env")
133
+ )
134
+ property: (property_identifier) @env_var
135
+ )
136
+
137
+ ; config.get("key")
138
+ (call_expression
139
+ function: (member_expression
140
+ object: (identifier) @config_obj
141
+ property: (property_identifier) @method (#eq? @method "get")
142
+ )
143
+ arguments: (arguments
144
+ (string (string_fragment) @config_key)
145
+ )
146
+ )
147
+ (#match? @config_obj "^(config|cfg|settings|conf)$")
148
+ `,
149
+
150
+ // Go: os.Getenv("VAR")
151
+ go: `
152
+ (call_expression
153
+ function: (selector_expression
154
+ operand: (identifier) @os (#eq? @os "os")
155
+ field: (field_identifier) @getenv (#eq? @getenv "Getenv")
156
+ )
157
+ arguments: (argument_list
158
+ (raw_string_literal) @env_var
159
+ )
160
+ )
161
+ `,
162
+
163
+ // Rust: env::var("VAR") or std::env::var("VAR")
164
+ rust: `
165
+ (call_expression
166
+ function: (scoped_identifier
167
+ path: (identifier) @env (#eq? @env "env")
168
+ name: (identifier) @var (#eq? @var "var")
169
+ )
170
+ arguments: (arguments
171
+ (string_literal) @env_var
172
+ )
173
+ )
174
+ `,
175
+ };
176
+
177
+ // --- Config File Parsers ---
178
+
179
+ async function parseEnvFile(filePath: string): Promise<ConfigKey[]> {
180
+ const keys: ConfigKey[] = [];
181
+ try {
182
+ const content = await fs.readFile(filePath, "utf-8");
183
+ const lines = content.split("\n");
184
+ for (let i = 0; i < lines.length; i++) {
185
+ const line = lines[i].trim();
186
+ // Skip comments and empty lines
187
+ if (line.startsWith("#") || line.startsWith("//") || !line) continue;
188
+
189
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
190
+ if (match) {
191
+ keys.push({
192
+ key: match[1],
193
+ file: filePath,
194
+ line: i + 1,
195
+ value: match[2].trim(),
196
+ });
197
+ }
198
+ }
199
+ } catch {
200
+ // File doesn't exist or can't be read
201
+ }
202
+ return keys;
203
+ }
204
+
205
+ async function parseIniFile(filePath: string): Promise<ConfigKey[]> {
206
+ const keys: ConfigKey[] = [];
207
+ try {
208
+ const content = await fs.readFile(filePath, "utf-8");
209
+ const lines = content.split("\n");
210
+ let currentSection = "";
211
+
212
+ for (let i = 0; i < lines.length; i++) {
213
+ const line = lines[i].trim();
214
+ if (!line || line.startsWith(";") || line.startsWith("#")) continue;
215
+
216
+ // Section header: [section]
217
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
218
+ if (sectionMatch) {
219
+ currentSection = sectionMatch[1];
220
+ continue;
221
+ }
222
+
223
+ // Key = value
224
+ const keyMatch = line.match(/^([^=]+)\s*=\s*(.*)$/);
225
+ if (keyMatch) {
226
+ const key = keyMatch[1].trim();
227
+ const fullKey = currentSection ? `${currentSection}.${key}` : key;
228
+ keys.push({
229
+ key: fullKey,
230
+ file: filePath,
231
+ line: i + 1,
232
+ value: keyMatch[2].trim(),
233
+ });
234
+ }
235
+ }
236
+ } catch {
237
+ // File doesn't exist or can't be read
238
+ }
239
+ return keys;
240
+ }
241
+
242
+ async function parseYamlConfig(filePath: string): Promise<ConfigKey[]> {
243
+ const keys: ConfigKey[] = [];
244
+ try {
245
+ const content = await fs.readFile(filePath, "utf-8");
246
+ // Simple YAML parser for flat key: value or section.key format
247
+ const lines = content.split("\n");
248
+ let indentStack: { indent: number; key: string }[] = [];
249
+
250
+ for (let i = 0; i < lines.length; i++) {
251
+ const line = lines[i];
252
+ const trimmed = line.trim();
253
+
254
+ // Skip comments and empty lines
255
+ if (!trimmed || trimmed.startsWith("#")) continue;
256
+
257
+ // Calculate indent
258
+ const indent = line.search(/\S/);
259
+ const _indentMatch = indentStack.find((s) => s.indent === indent);
260
+
261
+ // key: value pattern
262
+ const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
263
+ if (match) {
264
+ const key = match[1];
265
+ const value = match[2].trim();
266
+
267
+ // Build full key path
268
+ const parentKeys = indentStack
269
+ .filter((s) => s.indent < indent)
270
+ .map((s) => s.key);
271
+ const fullKey = [...parentKeys, key].join(".");
272
+
273
+ // If has value, it's a config key
274
+ if (value && !value.endsWith(":")) {
275
+ keys.push({
276
+ key: fullKey,
277
+ file: filePath,
278
+ line: i + 1,
279
+ value: value,
280
+ });
281
+ }
282
+
283
+ // Update indent stack
284
+ indentStack = indentStack.filter((s) => s.indent < indent);
285
+ indentStack.push({ indent, key });
286
+ }
287
+ }
288
+ } catch {
289
+ // File doesn't exist or can't be read
290
+ }
291
+ return keys;
292
+ }
293
+
294
+ async function parseJsonConfig(filePath: string): Promise<ConfigKey[]> {
295
+ const keys: ConfigKey[] = [];
296
+ try {
297
+ const content = await fs.readFile(filePath, "utf-8");
298
+ const obj = JSON.parse(content);
299
+
300
+ function traverse(obj: unknown, path: string[] = []) {
301
+ if (typeof obj === "object" && obj !== null) {
302
+ for (const [key, value] of Object.entries(obj)) {
303
+ const newPath = [...path, key];
304
+ if (
305
+ typeof value === "string" ||
306
+ typeof value === "number" ||
307
+ typeof value === "boolean"
308
+ ) {
309
+ keys.push({
310
+ key: newPath.join("."),
311
+ file: filePath,
312
+ line: 0, // JSON doesn't preserve line numbers easily
313
+ value: String(value),
314
+ });
315
+ } else if (typeof value === "object") {
316
+ traverse(value, newPath);
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ traverse(obj);
323
+ } catch {
324
+ // File doesn't exist or invalid JSON
325
+ }
326
+ return keys;
327
+ }
328
+
329
+ // --- Main Config Validator ---
330
+
331
+ export class ConfigValidator {
332
+ private client: TreeSitterClient;
333
+ private availableKeys: Map<string, ConfigKey[]> = new Map();
334
+
335
+ constructor() {
336
+ this.client = new TreeSitterClient();
337
+ }
338
+
339
+ async init(): Promise<void> {
340
+ await this.client.init();
341
+ }
342
+
343
+ /**
344
+ * Scan project for config files
345
+ */
346
+ async scanConfigFiles(cwd: string): Promise<void> {
347
+ const configFiles = [
348
+ { pattern: ".env", parser: parseEnvFile },
349
+ { pattern: ".env.local", parser: parseEnvFile },
350
+ { pattern: ".env.development", parser: parseEnvFile },
351
+ { pattern: ".env.production", parser: parseEnvFile },
352
+ { pattern: "config.ini", parser: parseIniFile },
353
+ { pattern: "config.yaml", parser: parseYamlConfig },
354
+ { pattern: "config.yml", parser: parseYamlConfig },
355
+ { pattern: "config.json", parser: parseJsonConfig },
356
+ { pattern: "pyproject.toml", parser: parseIniFile }, // Simplified
357
+ { pattern: "package.json", parser: parseJsonConfig },
358
+ { pattern: "app.yaml", parser: parseYamlConfig },
359
+ { pattern: "application.yaml", parser: parseYamlConfig },
360
+ ];
361
+
362
+ for (const { pattern, parser } of configFiles) {
363
+ const filePath = path.join(cwd, pattern);
364
+ const keys = await parser(filePath);
365
+ if (keys.length > 0) {
366
+ this.availableKeys.set(pattern, keys);
367
+ }
368
+ }
369
+
370
+ // Also scan for any .env.* files
371
+ try {
372
+ const entries = await fs.readdir(cwd);
373
+ for (const entry of entries) {
374
+ if (entry.startsWith(".env.")) {
375
+ const filePath = path.join(cwd, entry);
376
+ const keys = await parseEnvFile(filePath);
377
+ if (keys.length > 0) {
378
+ this.availableKeys.set(entry, keys);
379
+ }
380
+ }
381
+ }
382
+ } catch {
383
+ // Can't read directory
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Validate config access in a source file
389
+ */
390
+ async validateFile(filePath: string): Promise<ConfigValidationResult> {
391
+ const languageId = this.getLanguageId(filePath);
392
+ if (!languageId || !CONFIG_QUERIES[languageId]) {
393
+ return { undefined: [], typos: [], available: [] };
394
+ }
395
+
396
+ // Get all config accesses in the file
397
+ const accesses = await this.findConfigAccesses(filePath, languageId);
398
+
399
+ // Get all available keys
400
+ const allAvailable: ConfigKey[] = [];
401
+ for (const keys of this.availableKeys.values()) {
402
+ allAvailable.push(...keys);
403
+ }
404
+
405
+ const undefined: ConfigAccess[] = [];
406
+ const typos: Array<{ access: ConfigAccess; suggestion: string }> = [];
407
+
408
+ for (const access of accesses) {
409
+ // Check if key exists
410
+ const exactMatch = allAvailable.find(
411
+ (k) => k.key.toLowerCase() === access.key.toLowerCase(),
412
+ );
413
+
414
+ if (!exactMatch) {
415
+ // Check for typos using fuzzy matching
416
+ const suggestion = this.findClosestMatch(
417
+ access.key,
418
+ allAvailable.map((k) => k.key),
419
+ );
420
+ if (
421
+ suggestion &&
422
+ this.calculateSimilarity(access.key, suggestion) > 0.7
423
+ ) {
424
+ typos.push({ access, suggestion });
425
+ } else {
426
+ undefined.push(access);
427
+ }
428
+ }
429
+ }
430
+
431
+ return { undefined, typos, available: allAvailable };
432
+ }
433
+
434
+ /**
435
+ * Find all config accesses in a file using tree-sitter
436
+ */
437
+ private async findConfigAccesses(
438
+ filePath: string,
439
+ languageId: string,
440
+ ): Promise<ConfigAccess[]> {
441
+ const query = CONFIG_QUERIES[languageId];
442
+ const matches = await this.client.structuralSearch(
443
+ query,
444
+ languageId,
445
+ path.dirname(filePath),
446
+ { fileFilter: (f) => f === filePath },
447
+ );
448
+
449
+ const accesses: ConfigAccess[] = [];
450
+
451
+ for (const match of matches) {
452
+ const configKeyCapture =
453
+ match.captures.config_key || match.captures.env_var;
454
+ if (configKeyCapture) {
455
+ // Clean up the key (remove quotes, etc.)
456
+ const key = configKeyCapture.replace(/^["'`]|["'`]$/g, "");
457
+
458
+ accesses.push({
459
+ key,
460
+ file: filePath,
461
+ line: match.line,
462
+ column: match.column,
463
+ pattern: match.matchedText,
464
+ });
465
+ }
466
+ }
467
+
468
+ return accesses;
469
+ }
470
+
471
+ /**
472
+ * Calculate string similarity (Levenshtein-based)
473
+ */
474
+ private calculateSimilarity(a: string, b: string): number {
475
+ const matrix: number[][] = [];
476
+
477
+ for (let i = 0; i <= b.length; i++) {
478
+ matrix[i] = [i];
479
+ }
480
+
481
+ for (let j = 0; j <= a.length; j++) {
482
+ matrix[0][j] = j;
483
+ }
484
+
485
+ for (let i = 1; i <= b.length; i++) {
486
+ for (let j = 1; j <= a.length; j++) {
487
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
488
+ matrix[i][j] = matrix[i - 1][j - 1];
489
+ } else {
490
+ matrix[i][j] = Math.min(
491
+ matrix[i - 1][j - 1] + 1, // substitution
492
+ matrix[i][j - 1] + 1, // insertion
493
+ matrix[i - 1][j] + 1, // deletion
494
+ );
495
+ }
496
+ }
497
+ }
498
+
499
+ const distance = matrix[b.length][a.length];
500
+ const maxLength = Math.max(a.length, b.length);
501
+ return 1 - distance / maxLength;
502
+ }
503
+
504
+ /**
505
+ * Find the closest matching key
506
+ */
507
+ private findClosestMatch(
508
+ key: string,
509
+ candidates: string[],
510
+ ): string | undefined {
511
+ let bestMatch: string | undefined;
512
+ let bestScore = 0;
513
+
514
+ for (const candidate of candidates) {
515
+ const score = this.calculateSimilarity(key, candidate);
516
+ if (score > bestScore && score > 0.5) {
517
+ bestScore = score;
518
+ bestMatch = candidate;
519
+ }
520
+ }
521
+
522
+ return bestMatch;
523
+ }
524
+
525
+ /**
526
+ * Map file extension to language ID
527
+ */
528
+ private getLanguageId(filePath: string): string | undefined {
529
+ const ext = path.extname(filePath);
530
+ switch (ext) {
531
+ case ".py":
532
+ return "python";
533
+ case ".js":
534
+ return "javascript";
535
+ case ".ts":
536
+ return "typescript";
537
+ case ".tsx":
538
+ return "tsx";
539
+ case ".go":
540
+ return "go";
541
+ case ".rs":
542
+ return "rust";
543
+ default:
544
+ return undefined;
545
+ }
546
+ }
547
+ }
548
+
549
+ // --- Simple factory function ---
550
+
551
+ export async function createConfigValidator(
552
+ cwd: string,
553
+ ): Promise<ConfigValidator> {
554
+ const validator = new ConfigValidator();
555
+ await validator.init();
556
+ await validator.scanConfigFiles(cwd);
557
+ return validator;
558
+ }
@@ -8,9 +8,9 @@
8
8
  * Requires: npm install -D madge
9
9
  * Docs: https://github.com/pahen/madge
10
10
  */
11
- import { spawnSync } from "node:child_process";
12
11
  import * as fs from "node:fs";
13
12
  import * as path from "node:path";
13
+ import { safeSpawn } from "./safe-spawn.js";
14
14
  // --- Client ---
15
15
  export class DependencyChecker {
16
16
  constructor(verbose = false) {
@@ -31,10 +31,8 @@ export class DependencyChecker {
31
31
  isAvailable() {
32
32
  if (this.available !== null)
33
33
  return this.available;
34
- const result = spawnSync("npx", ["madge", "--version"], {
35
- encoding: "utf-8",
34
+ const result = safeSpawn("npx", ["madge", "--version"], {
36
35
  timeout: 5000,
37
- shell: true,
38
36
  });
39
37
  this.available = !result.error && result.status === 0;
40
38
  if (this.available) {
@@ -164,7 +162,7 @@ export class DependencyChecker {
164
162
  this.log(`Imports changed for ${path.basename(filePath)}, checking dependencies...`);
165
163
  // Run madge on the specific file (fast)
166
164
  try {
167
- const result = spawnSync("npx", [
165
+ const result = safeSpawn("npx", [
168
166
  "madge",
169
167
  "--circular",
170
168
  "--extensions",
@@ -172,10 +170,8 @@ export class DependencyChecker {
172
170
  "--json",
173
171
  normalized,
174
172
  ], {
175
- encoding: "utf-8",
176
173
  timeout: 15000,
177
174
  cwd: projectRoot,
178
- shell: true,
179
175
  });
180
176
  const output = result.stdout || "[]";
181
177
  const parsed = JSON.parse(output);
@@ -243,7 +239,7 @@ export class DependencyChecker {
243
239
  return { circular: [], count: 0 };
244
240
  }
245
241
  try {
246
- const result = spawnSync("npx", [
242
+ const result = safeSpawn("npx", [
247
243
  "madge",
248
244
  "--circular",
249
245
  "--extensions",
@@ -251,10 +247,8 @@ export class DependencyChecker {
251
247
  "--json",
252
248
  projectRoot,
253
249
  ], {
254
- encoding: "utf-8",
255
250
  timeout: 30000,
256
251
  cwd: projectRoot,
257
- shell: true,
258
252
  });
259
253
  const output = result.stdout || "{}";
260
254
  const data = JSON.parse(output);
@@ -9,9 +9,9 @@
9
9
  * Docs: https://github.com/pahen/madge
10
10
  */
11
11
 
12
- import { spawnSync } from "node:child_process";
13
12
  import * as fs from "node:fs";
14
13
  import * as path from "node:path";
14
+ import { safeSpawn } from "./safe-spawn.js";
15
15
 
16
16
  // --- Types ---
17
17
 
@@ -61,10 +61,8 @@ export class DependencyChecker {
61
61
  isAvailable(): boolean {
62
62
  if (this.available !== null) return this.available;
63
63
 
64
- const result = spawnSync("npx", ["madge", "--version"], {
65
- encoding: "utf-8",
64
+ const result = safeSpawn("npx", ["madge", "--version"], {
66
65
  timeout: 5000,
67
- shell: true,
68
66
  });
69
67
 
70
68
  this.available = !result.error && result.status === 0;
@@ -223,7 +221,7 @@ export class DependencyChecker {
223
221
 
224
222
  // Run madge on the specific file (fast)
225
223
  try {
226
- const result = spawnSync(
224
+ const result = safeSpawn(
227
225
  "npx",
228
226
  [
229
227
  "madge",
@@ -234,10 +232,8 @@ export class DependencyChecker {
234
232
  normalized,
235
233
  ],
236
234
  {
237
- encoding: "utf-8",
238
235
  timeout: 15000,
239
236
  cwd: projectRoot,
240
- shell: true,
241
237
  },
242
238
  );
243
239
 
@@ -323,7 +319,7 @@ export class DependencyChecker {
323
319
  }
324
320
 
325
321
  try {
326
- const result = spawnSync(
322
+ const result = safeSpawn(
327
323
  "npx",
328
324
  [
329
325
  "madge",
@@ -334,10 +330,8 @@ export class DependencyChecker {
334
330
  projectRoot,
335
331
  ],
336
332
  {
337
- encoding: "utf-8",
338
333
  timeout: 30000,
339
334
  cwd: projectRoot,
340
- shell: true,
341
335
  },
342
336
  );
343
337