pi-lens 3.6.3 → 3.6.4

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 (206) hide show
  1. package/package.json +3 -3
  2. package/tsconfig.json +1 -1
  3. package/clients/__tests__/file-time.test.js +0 -216
  4. package/clients/__tests__/file-time.test.ts +0 -276
  5. package/clients/__tests__/format-service.test.js +0 -245
  6. package/clients/__tests__/format-service.test.ts +0 -339
  7. package/clients/__tests__/formatters.test.js +0 -271
  8. package/clients/__tests__/formatters.test.ts +0 -401
  9. package/clients/agent-behavior-client.js +0 -110
  10. package/clients/agent-behavior-client.test.js +0 -94
  11. package/clients/agent-behavior-client.test.ts +0 -116
  12. package/clients/amain-types.js +0 -164
  13. package/clients/architect-client.js +0 -291
  14. package/clients/ast-grep-client.js +0 -253
  15. package/clients/ast-grep-parser.js +0 -84
  16. package/clients/ast-grep-rule-manager.js +0 -89
  17. package/clients/ast-grep-types.js +0 -9
  18. package/clients/auto-loop.js +0 -131
  19. package/clients/biome-client.js +0 -420
  20. package/clients/biome-client.test.js +0 -144
  21. package/clients/biome-client.test.ts +0 -163
  22. package/clients/cache/rule-cache.js +0 -72
  23. package/clients/cache-manager.js +0 -245
  24. package/clients/cache-manager.test.js +0 -197
  25. package/clients/cache-manager.test.ts +0 -299
  26. package/clients/complexity-client.js +0 -675
  27. package/clients/complexity-client.test.js +0 -234
  28. package/clients/complexity-client.test.ts +0 -255
  29. package/clients/config-validator.js +0 -465
  30. package/clients/dependency-checker.js +0 -325
  31. package/clients/dependency-checker.test.js +0 -60
  32. package/clients/dependency-checker.test.ts +0 -71
  33. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  34. package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
  35. package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
  36. package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
  37. package/clients/dispatch/debug.log +0 -1
  38. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  39. package/clients/dispatch/dispatcher.edge.test.ts +0 -100
  40. package/clients/dispatch/dispatcher.format.test.js +0 -46
  41. package/clients/dispatch/dispatcher.format.test.ts +0 -58
  42. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  43. package/clients/dispatch/dispatcher.inline.test.ts +0 -93
  44. package/clients/dispatch/dispatcher.js +0 -381
  45. package/clients/dispatch/dispatcher.test.js +0 -116
  46. package/clients/dispatch/dispatcher.test.ts +0 -149
  47. package/clients/dispatch/integration.js +0 -108
  48. package/clients/dispatch/plan.js +0 -183
  49. package/clients/dispatch/runners/architect.js +0 -83
  50. package/clients/dispatch/runners/architect.test.js +0 -138
  51. package/clients/dispatch/runners/architect.test.ts +0 -162
  52. package/clients/dispatch/runners/ast-grep-napi.js +0 -405
  53. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
  54. package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
  55. package/clients/dispatch/runners/ast-grep.js +0 -157
  56. package/clients/dispatch/runners/biome.js +0 -55
  57. package/clients/dispatch/runners/config-validation.js +0 -67
  58. package/clients/dispatch/runners/go-vet.js +0 -48
  59. package/clients/dispatch/runners/index.js +0 -47
  60. package/clients/dispatch/runners/lsp.js +0 -102
  61. package/clients/dispatch/runners/oxlint.js +0 -67
  62. package/clients/dispatch/runners/oxlint.test.js +0 -230
  63. package/clients/dispatch/runners/oxlint.test.ts +0 -303
  64. package/clients/dispatch/runners/pyright.js +0 -100
  65. package/clients/dispatch/runners/pyright.test.js +0 -98
  66. package/clients/dispatch/runners/pyright.test.ts +0 -121
  67. package/clients/dispatch/runners/python-slop.js +0 -97
  68. package/clients/dispatch/runners/python-slop.test.js +0 -203
  69. package/clients/dispatch/runners/python-slop.test.ts +0 -298
  70. package/clients/dispatch/runners/ruff.js +0 -48
  71. package/clients/dispatch/runners/rust-clippy.js +0 -102
  72. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  73. package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
  74. package/clients/dispatch/runners/shellcheck.js +0 -147
  75. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  76. package/clients/dispatch/runners/shellcheck.test.ts +0 -129
  77. package/clients/dispatch/runners/similarity.js +0 -230
  78. package/clients/dispatch/runners/spellcheck.js +0 -106
  79. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  80. package/clients/dispatch/runners/spellcheck.test.ts +0 -214
  81. package/clients/dispatch/runners/tree-sitter.js +0 -246
  82. package/clients/dispatch/runners/ts-lsp.js +0 -125
  83. package/clients/dispatch/runners/ts-slop.js +0 -113
  84. package/clients/dispatch/runners/type-safety.js +0 -142
  85. package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
  86. package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
  87. package/clients/dispatch/runners/utils.js +0 -51
  88. package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
  89. package/clients/dispatch/types.js +0 -16
  90. package/clients/dispatch/utils/format-utils.js +0 -44
  91. package/clients/dogfood.test.js +0 -201
  92. package/clients/dogfood.test.ts +0 -269
  93. package/clients/file-kinds.js +0 -177
  94. package/clients/file-kinds.test.js +0 -169
  95. package/clients/file-kinds.test.ts +0 -210
  96. package/clients/file-time.js +0 -152
  97. package/clients/file-utils.js +0 -40
  98. package/clients/fix-scanners.js +0 -204
  99. package/clients/format-service.js +0 -184
  100. package/clients/formatters.js +0 -488
  101. package/clients/go-client.js +0 -203
  102. package/clients/go-client.test.js +0 -127
  103. package/clients/go-client.test.ts +0 -143
  104. package/clients/installer/index.js +0 -403
  105. package/clients/interviewer-templates.js +0 -75
  106. package/clients/interviewer.js +0 -173
  107. package/clients/jscpd-client.js +0 -196
  108. package/clients/jscpd-client.test.js +0 -127
  109. package/clients/jscpd-client.test.ts +0 -145
  110. package/clients/knip-client.js +0 -239
  111. package/clients/knip-client.test.js +0 -112
  112. package/clients/knip-client.test.ts +0 -128
  113. package/clients/latency-logger.js +0 -40
  114. package/clients/lsp/__tests__/client.test.js +0 -310
  115. package/clients/lsp/__tests__/client.test.ts +0 -412
  116. package/clients/lsp/__tests__/config.test.js +0 -167
  117. package/clients/lsp/__tests__/config.test.ts +0 -217
  118. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  119. package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/integration.test.ts +0 -160
  122. package/clients/lsp/__tests__/launch.test.js +0 -313
  123. package/clients/lsp/__tests__/launch.test.ts +0 -394
  124. package/clients/lsp/__tests__/server.test.js +0 -259
  125. package/clients/lsp/__tests__/server.test.ts +0 -332
  126. package/clients/lsp/__tests__/service.test.js +0 -438
  127. package/clients/lsp/__tests__/service.test.ts +0 -530
  128. package/clients/lsp/client.js +0 -350
  129. package/clients/lsp/config.js +0 -112
  130. package/clients/lsp/index.js +0 -318
  131. package/clients/lsp/installer/index.js +0 -391
  132. package/clients/lsp/interactive-install.js +0 -221
  133. package/clients/lsp/language.js +0 -170
  134. package/clients/lsp/launch.js +0 -329
  135. package/clients/lsp/lsp/launch.js +0 -116
  136. package/clients/lsp/lsp/server.js +0 -532
  137. package/clients/lsp/lsp-index.js +0 -10
  138. package/clients/lsp/path-utils.js +0 -5
  139. package/clients/lsp/server.js +0 -725
  140. package/clients/lsp/test-py-spawn/requirements.txt +0 -1
  141. package/clients/lsp/test-py-spawn/test.py +0 -3
  142. package/clients/lsp/test-py-svc/requirements.txt +0 -1
  143. package/clients/lsp/test-py-svc/test.py +0 -3
  144. package/clients/lsp/test-python-project/requirements.txt +0 -1
  145. package/clients/lsp/test-python-project/test.py +0 -5
  146. package/clients/metrics-client.js +0 -107
  147. package/clients/metrics-client.test.js +0 -128
  148. package/clients/metrics-client.test.ts +0 -163
  149. package/clients/metrics-history.js +0 -367
  150. package/clients/path-utils.js +0 -142
  151. package/clients/pipeline.js +0 -272
  152. package/clients/production-readiness.js +0 -522
  153. package/clients/project-index.js +0 -255
  154. package/clients/project-metadata.js +0 -531
  155. package/clients/ruff-client.js +0 -325
  156. package/clients/ruff-client.test.js +0 -132
  157. package/clients/ruff-client.test.ts +0 -153
  158. package/clients/rules-scanner.js +0 -97
  159. package/clients/runner-tracker.js +0 -152
  160. package/clients/rust-client.js +0 -205
  161. package/clients/rust-client.test.js +0 -108
  162. package/clients/rust-client.test.ts +0 -130
  163. package/clients/safe-spawn-async.js +0 -163
  164. package/clients/safe-spawn.js +0 -241
  165. package/clients/sanitize.js +0 -291
  166. package/clients/sanitize.test.js +0 -177
  167. package/clients/sanitize.test.ts +0 -223
  168. package/clients/scan-architectural-debt.js +0 -167
  169. package/clients/scan-utils.js +0 -83
  170. package/clients/secrets-scanner.js +0 -119
  171. package/clients/secrets-scanner.test.js +0 -100
  172. package/clients/secrets-scanner.test.ts +0 -113
  173. package/clients/sg-runner.js +0 -292
  174. package/clients/state-matrix.js +0 -160
  175. package/clients/subprocess-client.js +0 -65
  176. package/clients/symbol-types.js +0 -5
  177. package/clients/test-runner-client.js +0 -523
  178. package/clients/test-runner-client.test.js +0 -192
  179. package/clients/test-runner-client.test.ts +0 -253
  180. package/clients/test-utils.js +0 -27
  181. package/clients/test-utils.ts +0 -36
  182. package/clients/todo-scanner.js +0 -200
  183. package/clients/todo-scanner.test.js +0 -301
  184. package/clients/todo-scanner.test.ts +0 -352
  185. package/clients/tool-availability.js +0 -207
  186. package/clients/tree-sitter-client.js +0 -601
  187. package/clients/tree-sitter-query-loader.js +0 -355
  188. package/clients/tree-sitter-symbol-extractor.js +0 -289
  189. package/clients/ts-service.js +0 -129
  190. package/clients/type-coverage-client.js +0 -127
  191. package/clients/type-coverage-client.test.js +0 -105
  192. package/clients/type-coverage-client.test.ts +0 -125
  193. package/clients/type-safety-client.js +0 -138
  194. package/clients/types.js +0 -11
  195. package/clients/typescript-client.codefix.test.js +0 -157
  196. package/clients/typescript-client.codefix.test.ts +0 -186
  197. package/clients/typescript-client.js +0 -509
  198. package/clients/typescript-client.test.js +0 -105
  199. package/clients/typescript-client.test.ts +0 -126
  200. package/commands/booboo.js +0 -1007
  201. package/commands/fix-from-booboo.js +0 -398
  202. package/commands/fix-simplified.js +0 -618
  203. package/commands/rate.js +0 -281
  204. package/commands/rate.test.js +0 -119
  205. package/commands/rate.test.ts +0 -131
  206. package/commands/refactor.js +0 -130
@@ -1,291 +0,0 @@
1
- /**
2
- * Architect Client for pi-lens
3
- *
4
- * Loads path-based architectural rules from .pi-lens/architect.yaml
5
- * and checks file paths against them.
6
- *
7
- * Provides:
8
- * - Pre-write hints: what rules apply before the agent edits
9
- * - Post-write validation: check for violations after edits
10
- */
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { minimatch } from "minimatch";
14
- // --- Client ---
15
- export class ArchitectClient {
16
- constructor(verbose = false) {
17
- this.config = null;
18
- this.isUserConfig = false;
19
- this.log = verbose
20
- ? (msg) => console.error(`[architect] ${msg}`)
21
- : () => { };
22
- }
23
- /**
24
- * Load architect config from project root.
25
- * Falls back to built-in default if no user config exists.
26
- */
27
- loadConfig(projectRoot) {
28
- // Try user config locations first
29
- const userCandidates = [
30
- path.join(projectRoot, ".pi-lens", "architect.yaml"),
31
- path.join(projectRoot, "architect.yaml"),
32
- path.join(projectRoot, ".pi-lens", "architect.yml"),
33
- ];
34
- for (const configPath of userCandidates) {
35
- try {
36
- const content = fs.readFileSync(configPath, "utf-8");
37
- this.config = this.parseYaml(content);
38
- this.configPath = configPath;
39
- this.isUserConfig = true;
40
- this.log(`Loaded user architect config from ${configPath}`);
41
- return true;
42
- }
43
- catch (error) {
44
- this.log(`Could not read ${configPath}: ${error}`);
45
- }
46
- }
47
- // Fall back to built-in default
48
- try {
49
- // Try multiple possible locations for the default config
50
- const possibleDefaultPaths = [
51
- path.join(projectRoot, "default-architect.yaml"),
52
- path.join(projectRoot, "..", "default-architect.yaml"),
53
- path.join(process.cwd(), "default-architect.yaml"),
54
- ];
55
- // Handle both CommonJS and ESM environments
56
- if (typeof __dirname !== "undefined") {
57
- possibleDefaultPaths.push(path.join(__dirname, "..", "default-architect.yaml"));
58
- possibleDefaultPaths.push(path.join(__dirname, "..", "..", "default-architect.yaml"));
59
- }
60
- for (const defaultPath of possibleDefaultPaths) {
61
- try {
62
- const content = fs.readFileSync(defaultPath, "utf-8");
63
- this.config = this.parseYaml(content);
64
- this.configPath = defaultPath;
65
- this.isUserConfig = false;
66
- this.log("Using default architect rules (create .pi-lens/architect.yaml to customize)");
67
- return true;
68
- }
69
- catch {
70
- // Try next path
71
- }
72
- }
73
- this.log("No architect config available");
74
- return false;
75
- }
76
- catch {
77
- this.log("No architect config available");
78
- return false;
79
- }
80
- }
81
- /**
82
- * Check if the loaded config is user-defined (not default)
83
- */
84
- isUserDefined() {
85
- return this.isUserConfig;
86
- }
87
- /**
88
- * Check if config is loaded
89
- */
90
- hasConfig() {
91
- return this.config !== null;
92
- }
93
- /**
94
- * Get rules that apply to a file path
95
- */
96
- getRulesForFile(filePath) {
97
- if (!this.config)
98
- return [];
99
- const matched = [];
100
- for (const rule of this.config.rules) {
101
- if (minimatch(filePath, rule.pattern, { matchBase: true })) {
102
- matched.push(rule);
103
- }
104
- }
105
- return matched;
106
- }
107
- /**
108
- * Check code content against rules for a file path
109
- * Returns violations found
110
- */
111
- checkFile(filePath, content) {
112
- const rules = this.getRulesForFile(filePath);
113
- const violations = [];
114
- for (const rule of rules) {
115
- if (!rule.must_not)
116
- continue;
117
- for (const check of rule.must_not) {
118
- // We use 'g' to find all occurrences and correctly report line numbers
119
- const regex = new RegExp(check.pattern, "gi");
120
- let match;
121
- // biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
122
- while ((match = regex.exec(content)) !== null) {
123
- // Convert index to line number
124
- const lineNum = content.slice(0, match.index).split("\n").length;
125
- violations.push({
126
- pattern: rule.pattern,
127
- message: check.message,
128
- line: lineNum,
129
- fix: check.fix,
130
- note: check.note,
131
- });
132
- // Prevent infinite loop on empty matches
133
- if (match.index === regex.lastIndex) {
134
- regex.lastIndex++;
135
- }
136
- }
137
- }
138
- }
139
- return violations;
140
- }
141
- /**
142
- * Check file size against max_lines rule
143
- * Returns violation if file exceeds the limit
144
- */
145
- checkFileSize(filePath, lineCount) {
146
- const rules = this.getRulesForFile(filePath);
147
- for (const rule of rules) {
148
- if (rule.max_lines && lineCount > rule.max_lines) {
149
- return {
150
- pattern: rule.pattern,
151
- message: `File is ${lineCount} lines — exceeds ${rule.max_lines} line limit. Split into smaller modules.`,
152
- };
153
- }
154
- }
155
- return null;
156
- }
157
- /**
158
- * Get pre-write hints for a file path
159
- * Returns rules that will apply to the file being written
160
- */
161
- getHints(filePath) {
162
- const rules = this.getRulesForFile(filePath);
163
- const hints = [];
164
- for (const rule of rules) {
165
- if (rule.must_not) {
166
- for (const check of rule.must_not) {
167
- hints.push(check.message);
168
- }
169
- }
170
- if (rule.must) {
171
- for (const req of rule.must) {
172
- hints.push(`Must: ${req}`);
173
- }
174
- }
175
- }
176
- return hints;
177
- }
178
- /**
179
- * Simple YAML parser for architect.yaml format
180
- * Handles the specific structure we need
181
- */
182
- parseYaml(content) {
183
- const config = { rules: [] };
184
- // Split into top-level rule blocks (4-space indent "- pattern:")
185
- const ruleBlocks = content.split(/(?=^ {2}- pattern:)/m);
186
- for (const block of ruleBlocks) {
187
- const lines = block.split("\n");
188
- let rule = null;
189
- let section = null;
190
- let violation = null;
191
- for (const line of lines) {
192
- const trimmed = line.trim();
193
- if (trimmed.startsWith("#") || !trimmed)
194
- continue;
195
- // Version (top-level)
196
- if (trimmed.startsWith("version:") && !rule) {
197
- config.version = trimmed.split(":")[1]?.trim().replace(/['"]/g, "");
198
- continue;
199
- }
200
- // Rule pattern
201
- const ruleMatch = trimmed.match(/^-?\s*pattern:\s*["'](.+?)["']/);
202
- if (ruleMatch && trimmed.startsWith("-") && !section) {
203
- rule = { pattern: ruleMatch[1], must_not: [], must: [] };
204
- continue;
205
- }
206
- // Nested pattern inside must_not (may start with "- ")
207
- if ((trimmed.startsWith("pattern:") ||
208
- trimmed.startsWith("- pattern:")) &&
209
- section === "must_not") {
210
- // Extract everything after "pattern:" and unquote
211
- const raw = trimmed.replace(/^-?\s*pattern:\s*/, "").trim();
212
- const unquoted = raw.replace(/^["']|["']$/g, "");
213
- if (unquoted) {
214
- violation = { pattern: unquoted, message: "" };
215
- }
216
- continue;
217
- }
218
- // Section headers
219
- if (trimmed === "must_not:" || trimmed.startsWith("must_not:")) {
220
- section = "must_not";
221
- continue;
222
- }
223
- if (trimmed === "must:") {
224
- section = "must";
225
- continue;
226
- }
227
- // Message for current violation (handle nested quotes)
228
- if (trimmed.startsWith("message:") && violation) {
229
- // Match "..." or '...' allowing the other quote type inside
230
- const dquoteMatch = trimmed.match(/message:\s*"([^"]*)"/);
231
- const squoteMatch = !dquoteMatch
232
- ? trimmed.match(/message:\s*'([^']*)'/)
233
- : null;
234
- const match = dquoteMatch || squoteMatch;
235
- if (match) {
236
- violation.message = match[1];
237
- if (rule) {
238
- rule.must_not = rule.must_not ?? [];
239
- rule.must_not.push(violation);
240
- }
241
- violation = null;
242
- }
243
- continue;
244
- }
245
- // Fix guidance for current violation
246
- if (trimmed.startsWith("fix:") && violation) {
247
- const dquoteMatch = trimmed.match(/fix:\s*"([^"]*)"/);
248
- const squoteMatch = !dquoteMatch
249
- ? trimmed.match(/fix:\s*'([^']*)'/)
250
- : null;
251
- const match = dquoteMatch || squoteMatch;
252
- if (match) {
253
- violation.fix = match[1];
254
- }
255
- continue;
256
- }
257
- // Note guidance for current violation
258
- if (trimmed.startsWith("note:") && violation) {
259
- const dquoteMatch = trimmed.match(/note:\s*"([^"]*)"/);
260
- const squoteMatch = !dquoteMatch
261
- ? trimmed.match(/note:\s*'([^']*)'/)
262
- : null;
263
- const match = dquoteMatch || squoteMatch;
264
- if (match) {
265
- violation.note = match[1];
266
- }
267
- continue;
268
- }
269
- // Must items (simple strings)
270
- if (section === "must" && trimmed.startsWith("- ") && rule) {
271
- const item = trimmed.slice(2).replace(/^["']|["']$/g, "");
272
- rule.must = rule.must ?? [];
273
- rule.must.push(item);
274
- }
275
- // max_lines setting
276
- if (trimmed.startsWith("max_lines:") && rule) {
277
- const num = parseInt(trimmed.split(":")[1]?.trim(), 10);
278
- if (!Number.isNaN(num)) {
279
- rule.max_lines = num;
280
- }
281
- }
282
- }
283
- if (rule) {
284
- config.rules.push(rule);
285
- }
286
- }
287
- return config;
288
- }
289
- }
290
- // --- Singleton ---
291
- const _instance = null;
@@ -1,253 +0,0 @@
1
- /**
2
- * AstGrep Client for pi-lens
3
- *
4
- * Structural code analysis using ast-grep CLI.
5
- * Scans files against YAML rule definitions.
6
- *
7
- * Requires: npm install -D @ast-grep/cli
8
- * Rules: ./rules/ directory
9
- */
10
- import { spawnSync } from "node:child_process";
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { AstGrepParser } from "./ast-grep-parser.js";
14
- import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
- import { SgRunner } from "./sg-runner.js";
16
- const _getExtensionDir = () => {
17
- if (typeof __dirname !== "undefined") {
18
- return __dirname;
19
- }
20
- return ".";
21
- };
22
- // --- Client ---
23
- export class AstGrepClient {
24
- constructor(ruleDir, verbose = false) {
25
- this.available = null;
26
- this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
27
- this.log = verbose
28
- ? (msg) => console.error(`[ast-grep] ${msg}`)
29
- : () => { };
30
- this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
31
- this.runner = new SgRunner(verbose);
32
- }
33
- /**
34
- * Check if ast-grep CLI is available, auto-install if not
35
- */
36
- async ensureAvailable() {
37
- return this.runner.ensureAvailable();
38
- }
39
- /**
40
- * Check if ast-grep CLI is available (legacy sync method)
41
- * Prefer ensureAvailable() for auto-install behavior
42
- */
43
- isAvailable() {
44
- if (this.available !== null)
45
- return this.available;
46
- this.available = this.runner.isAvailable();
47
- if (this.available) {
48
- this.log("ast-grep available");
49
- }
50
- return this.available;
51
- }
52
- /**
53
- * Search for AST patterns in files
54
- */
55
- async search(pattern, lang, paths, options) {
56
- const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
57
- if (options?.selector) {
58
- args.push("--selector", options.selector);
59
- }
60
- if (options?.context !== undefined) {
61
- args.push("--context", String(options.context));
62
- }
63
- args.push(...paths);
64
- return this.runner.exec(args);
65
- }
66
- /**
67
- * Search and replace AST patterns
68
- */
69
- async replace(pattern, rewrite, lang, paths, apply = false) {
70
- const args = [
71
- "run",
72
- "-p",
73
- pattern,
74
- "-r",
75
- rewrite,
76
- "--lang",
77
- lang,
78
- "--json=compact",
79
- ];
80
- if (apply)
81
- args.push("--update-all");
82
- args.push(...paths);
83
- const result = await this.runner.exec(args);
84
- return { matches: result.matches, applied: apply, error: result.error };
85
- }
86
- /**
87
- * Run a one-off scan with a temporary rule and configuration
88
- */
89
- runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
90
- if (!this.isAvailable())
91
- return [];
92
- return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
93
- }
94
- /**
95
- * Find similar functions by comparing normalized AST structure
96
- */
97
- async findSimilarFunctions(dir, lang = "typescript") {
98
- const ruleYaml = `id: find-functions
99
- language: ${lang}
100
- rule:
101
- kind: function_declaration
102
- severity: info
103
- message: found
104
- `;
105
- const matches = this.runTempScan(dir, "find-functions", ruleYaml);
106
- if (matches.length === 0)
107
- return [];
108
- return this.groupSimilarFunctions(matches);
109
- }
110
- groupSimilarFunctions(matches) {
111
- const grouped = new Map();
112
- for (const item of matches) {
113
- const name = this.extractFunctionName(item.text);
114
- if (!name)
115
- continue;
116
- const signature = this.normalizeFunction(item.text);
117
- const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
118
- 1;
119
- const group = grouped.get(signature) ?? [];
120
- group.push({ name, file: item.file, line });
121
- grouped.set(signature, group);
122
- }
123
- return Array.from(grouped.entries())
124
- .filter(([_, functions]) => functions.length > 1)
125
- .map(([pattern, functions]) => ({ pattern, functions }));
126
- }
127
- /**
128
- * Extract function name from match text
129
- */
130
- extractFunctionName(text) {
131
- return text.match(/function\s+(\w+)/)?.[1] ?? null;
132
- }
133
- normalizeFunction(text) {
134
- const normalizedText = text
135
- .replace(/function\s+\w+/, "function FN")
136
- .replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
137
- .replace(/["'].*?["']/g, "STR")
138
- .replace(/`[^`]*`/g, "TMPL")
139
- .replace(/\b\d+\b/g, "NUM")
140
- .replace(/\btrue\b|\bfalse\b/g, "BOOL")
141
- .replace(/\/\/.*/g, "")
142
- .replace(/\/\*[\s\S]*?\*\//g, "")
143
- .replace(/\s+/g, " ")
144
- .trim();
145
- // Extract just the body structure
146
- const bodyMatch = normalizedText.match(/\{(.*)\}/);
147
- const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
148
- // Use first 200 chars as signature
149
- return body.slice(0, 200);
150
- }
151
- /**
152
- * Scan for exported function names in a directory
153
- */
154
- async scanExports(dir, lang = "typescript") {
155
- const exports = new Map();
156
- const ruleYaml = `id: find-functions
157
- language: ${lang}
158
- rule:
159
- kind: function_declaration
160
- severity: info
161
- message: found
162
- `;
163
- const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
164
- this.log(`scanExports output length: ${matches.length}`);
165
- for (const item of matches) {
166
- const text = item.text || "";
167
- const nameMatch = text.match(/function\s+(\w+)/);
168
- if (nameMatch?.[1]) {
169
- this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
170
- exports.set(nameMatch[1], item.file);
171
- }
172
- }
173
- return exports;
174
- }
175
- formatMatches(matches, isDryRun = false, showModeIndicator = false) {
176
- return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
177
- }
178
- /**
179
- * Scan a file against all rules
180
- */
181
- scanFile(filePath) {
182
- if (!this.isAvailable())
183
- return [];
184
- const absolutePath = path.resolve(filePath);
185
- if (!fs.existsSync(absolutePath))
186
- return [];
187
- const configPath = path.join(this.ruleDir, ".sgconfig.yml");
188
- try {
189
- const result = spawnSync("npx", ["sg", "scan", "--config", configPath, "--json", absolutePath], {
190
- encoding: "utf-8",
191
- timeout: 15000,
192
- shell: process.platform === "win32",
193
- });
194
- // ast-grep exits 1 when it finds issues
195
- const output = result.stdout || result.stderr || "";
196
- if (!output.trim())
197
- return [];
198
- const parser = new AstGrepParser((id) => this.getRuleDescription(id), (sev) => this.mapSeverity(sev));
199
- return parser.parseOutput(output, absolutePath);
200
- }
201
- catch (err) {
202
- this.log(`Scan error: ${err instanceof Error ? err.message : String(err)}`);
203
- return [];
204
- }
205
- }
206
- /**
207
- * Format diagnostics for LLM consumption
208
- */
209
- formatDiagnostics(diags) {
210
- if (diags.length === 0)
211
- return "";
212
- const errors = diags.filter((d) => d.severity === "error");
213
- const warnings = diags.filter((d) => d.severity === "warning");
214
- const hints = diags.filter((d) => d.severity === "hint");
215
- let output = `[ast-grep] ${diags.length} structural issue(s)`;
216
- if (errors.length)
217
- output += ` — ${errors.length} error(s)`;
218
- if (warnings.length)
219
- output += ` — ${warnings.length} warning(s)`;
220
- if (hints.length)
221
- output += ` — ${hints.length} hint(s)`;
222
- output += ":\n";
223
- for (const d of diags.slice(0, 10)) {
224
- const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
225
- const ruleInfo = d.ruleDescription
226
- ? `${d.rule}: ${d.ruleDescription.message}`
227
- : d.rule;
228
- const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
229
- output += ` ${ruleInfo} (${loc})${fix}\n`;
230
- if (d.ruleDescription?.note) {
231
- const shortNote = d.ruleDescription.note.split("\n")[0];
232
- output += ` → ${shortNote}\n`;
233
- }
234
- }
235
- if (diags.length > 10) {
236
- output += ` ... and ${diags.length - 10} more\n`;
237
- }
238
- return output;
239
- }
240
- getRuleDescription(ruleId) {
241
- return this.ruleManager.loadRuleDescriptions().get(ruleId);
242
- }
243
- mapSeverity(severity) {
244
- const lower = severity.toLowerCase();
245
- if (lower === "error")
246
- return "error";
247
- if (lower === "warning")
248
- return "warning";
249
- if (lower === "info")
250
- return "info";
251
- return "hint";
252
- }
253
- }
@@ -1,84 +0,0 @@
1
- import * as path from "node:path";
2
- export class AstGrepParser {
3
- constructor(getRuleDescription, mapSeverity) {
4
- this.getRuleDescription = getRuleDescription;
5
- this.mapSeverity = mapSeverity;
6
- }
7
- parseOutput(output, filterFile) {
8
- const resolvedFilterFile = path.resolve(filterFile);
9
- try {
10
- const items = JSON.parse(output);
11
- if (Array.isArray(items)) {
12
- return items
13
- .map((item) => this.parseDiagnostic(item, resolvedFilterFile))
14
- .filter((d) => d !== null);
15
- }
16
- }
17
- catch (err) {
18
- void err;
19
- }
20
- return output
21
- .split("\n")
22
- .filter((l) => l.trim())
23
- .map((line) => {
24
- try {
25
- return this.parseDiagnostic(JSON.parse(line), resolvedFilterFile);
26
- }
27
- catch (err) {
28
- void err;
29
- return null;
30
- }
31
- })
32
- .filter((d) => d !== null);
33
- }
34
- parseDiagnostic(item, filterFile) {
35
- if (item.labels?.length) {
36
- return this.parseNewFormat(item, filterFile);
37
- }
38
- if (item.spans?.length) {
39
- return this.parseLegacyFormat(item, filterFile);
40
- }
41
- return null;
42
- }
43
- parseNewFormat(item, filterFile) {
44
- const label = item.labels.find((l) => l.style === "primary") || item.labels[0];
45
- const filePath = path.resolve(label.file || filterFile);
46
- if (filePath !== filterFile)
47
- return null;
48
- const start = label.range?.start || { line: 0, column: 0 };
49
- const end = label.range?.end || start;
50
- return {
51
- line: start.line + 1,
52
- column: start.column,
53
- endLine: end.line + 1,
54
- endColumn: end.column,
55
- severity: this.mapSeverity(item.severity),
56
- message: item.message || "Unknown issue",
57
- rule: item.ruleId || "unknown",
58
- ruleDescription: this.getRuleDescription(item.ruleId || "unknown"),
59
- file: filePath,
60
- };
61
- }
62
- parseLegacyFormat(item, filterFile) {
63
- const span = item.spans?.[0];
64
- if (!span)
65
- return null;
66
- const filePath = path.resolve(span.file || filterFile);
67
- if (filePath !== filterFile)
68
- return null;
69
- const start = span.range?.start || { line: 0, column: 0 };
70
- const end = span.range?.end || start;
71
- const ruleId = item.name || item.ruleId || "unknown";
72
- return {
73
- line: start.line + 1,
74
- column: start.column,
75
- endLine: end.line + 1,
76
- endColumn: end.column,
77
- severity: this.mapSeverity(item.severity || item.Severity || "warning"),
78
- message: item.Message?.text || item.message || "Unknown issue",
79
- rule: ruleId,
80
- ruleDescription: this.getRuleDescription(ruleId),
81
- file: filePath,
82
- };
83
- }
84
- }