pi-lens 3.6.2 → 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 (207) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/package.json +4 -4
  3. package/tsconfig.json +1 -1
  4. package/clients/__tests__/file-time.test.js +0 -216
  5. package/clients/__tests__/file-time.test.ts +0 -276
  6. package/clients/__tests__/format-service.test.js +0 -245
  7. package/clients/__tests__/format-service.test.ts +0 -339
  8. package/clients/__tests__/formatters.test.js +0 -271
  9. package/clients/__tests__/formatters.test.ts +0 -401
  10. package/clients/agent-behavior-client.js +0 -110
  11. package/clients/agent-behavior-client.test.js +0 -94
  12. package/clients/agent-behavior-client.test.ts +0 -116
  13. package/clients/amain-types.js +0 -164
  14. package/clients/architect-client.js +0 -291
  15. package/clients/ast-grep-client.js +0 -253
  16. package/clients/ast-grep-parser.js +0 -84
  17. package/clients/ast-grep-rule-manager.js +0 -89
  18. package/clients/ast-grep-types.js +0 -9
  19. package/clients/auto-loop.js +0 -131
  20. package/clients/biome-client.js +0 -420
  21. package/clients/biome-client.test.js +0 -144
  22. package/clients/biome-client.test.ts +0 -163
  23. package/clients/cache/rule-cache.js +0 -72
  24. package/clients/cache-manager.js +0 -245
  25. package/clients/cache-manager.test.js +0 -197
  26. package/clients/cache-manager.test.ts +0 -299
  27. package/clients/complexity-client.js +0 -675
  28. package/clients/complexity-client.test.js +0 -234
  29. package/clients/complexity-client.test.ts +0 -255
  30. package/clients/config-validator.js +0 -465
  31. package/clients/dependency-checker.js +0 -325
  32. package/clients/dependency-checker.test.js +0 -60
  33. package/clients/dependency-checker.test.ts +0 -71
  34. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  35. package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
  36. package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
  37. package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
  38. package/clients/dispatch/debug.log +0 -1
  39. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  40. package/clients/dispatch/dispatcher.edge.test.ts +0 -100
  41. package/clients/dispatch/dispatcher.format.test.js +0 -46
  42. package/clients/dispatch/dispatcher.format.test.ts +0 -58
  43. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  44. package/clients/dispatch/dispatcher.inline.test.ts +0 -93
  45. package/clients/dispatch/dispatcher.js +0 -381
  46. package/clients/dispatch/dispatcher.test.js +0 -116
  47. package/clients/dispatch/dispatcher.test.ts +0 -149
  48. package/clients/dispatch/integration.js +0 -108
  49. package/clients/dispatch/plan.js +0 -183
  50. package/clients/dispatch/runners/architect.js +0 -83
  51. package/clients/dispatch/runners/architect.test.js +0 -138
  52. package/clients/dispatch/runners/architect.test.ts +0 -162
  53. package/clients/dispatch/runners/ast-grep-napi.js +0 -405
  54. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
  55. package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
  56. package/clients/dispatch/runners/ast-grep.js +0 -157
  57. package/clients/dispatch/runners/biome.js +0 -55
  58. package/clients/dispatch/runners/config-validation.js +0 -67
  59. package/clients/dispatch/runners/go-vet.js +0 -48
  60. package/clients/dispatch/runners/index.js +0 -47
  61. package/clients/dispatch/runners/lsp.js +0 -102
  62. package/clients/dispatch/runners/oxlint.js +0 -67
  63. package/clients/dispatch/runners/oxlint.test.js +0 -230
  64. package/clients/dispatch/runners/oxlint.test.ts +0 -303
  65. package/clients/dispatch/runners/pyright.js +0 -100
  66. package/clients/dispatch/runners/pyright.test.js +0 -98
  67. package/clients/dispatch/runners/pyright.test.ts +0 -121
  68. package/clients/dispatch/runners/python-slop.js +0 -97
  69. package/clients/dispatch/runners/python-slop.test.js +0 -203
  70. package/clients/dispatch/runners/python-slop.test.ts +0 -298
  71. package/clients/dispatch/runners/ruff.js +0 -48
  72. package/clients/dispatch/runners/rust-clippy.js +0 -102
  73. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  74. package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
  75. package/clients/dispatch/runners/shellcheck.js +0 -147
  76. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  77. package/clients/dispatch/runners/shellcheck.test.ts +0 -129
  78. package/clients/dispatch/runners/similarity.js +0 -230
  79. package/clients/dispatch/runners/spellcheck.js +0 -106
  80. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  81. package/clients/dispatch/runners/spellcheck.test.ts +0 -214
  82. package/clients/dispatch/runners/tree-sitter.js +0 -246
  83. package/clients/dispatch/runners/ts-lsp.js +0 -125
  84. package/clients/dispatch/runners/ts-slop.js +0 -113
  85. package/clients/dispatch/runners/type-safety.js +0 -142
  86. package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
  87. package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
  88. package/clients/dispatch/runners/utils.js +0 -51
  89. package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
  90. package/clients/dispatch/types.js +0 -16
  91. package/clients/dispatch/utils/format-utils.js +0 -44
  92. package/clients/dogfood.test.js +0 -201
  93. package/clients/dogfood.test.ts +0 -269
  94. package/clients/file-kinds.js +0 -177
  95. package/clients/file-kinds.test.js +0 -169
  96. package/clients/file-kinds.test.ts +0 -210
  97. package/clients/file-time.js +0 -152
  98. package/clients/file-utils.js +0 -40
  99. package/clients/fix-scanners.js +0 -204
  100. package/clients/format-service.js +0 -184
  101. package/clients/formatters.js +0 -488
  102. package/clients/go-client.js +0 -203
  103. package/clients/go-client.test.js +0 -127
  104. package/clients/go-client.test.ts +0 -143
  105. package/clients/installer/index.js +0 -403
  106. package/clients/interviewer-templates.js +0 -75
  107. package/clients/interviewer.js +0 -173
  108. package/clients/jscpd-client.js +0 -196
  109. package/clients/jscpd-client.test.js +0 -127
  110. package/clients/jscpd-client.test.ts +0 -145
  111. package/clients/knip-client.js +0 -239
  112. package/clients/knip-client.test.js +0 -112
  113. package/clients/knip-client.test.ts +0 -128
  114. package/clients/latency-logger.js +0 -40
  115. package/clients/lsp/__tests__/client.test.js +0 -310
  116. package/clients/lsp/__tests__/client.test.ts +0 -412
  117. package/clients/lsp/__tests__/config.test.js +0 -167
  118. package/clients/lsp/__tests__/config.test.ts +0 -217
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
  121. package/clients/lsp/__tests__/integration.test.js +0 -127
  122. package/clients/lsp/__tests__/integration.test.ts +0 -160
  123. package/clients/lsp/__tests__/launch.test.js +0 -313
  124. package/clients/lsp/__tests__/launch.test.ts +0 -394
  125. package/clients/lsp/__tests__/server.test.js +0 -259
  126. package/clients/lsp/__tests__/server.test.ts +0 -332
  127. package/clients/lsp/__tests__/service.test.js +0 -438
  128. package/clients/lsp/__tests__/service.test.ts +0 -530
  129. package/clients/lsp/client.js +0 -350
  130. package/clients/lsp/config.js +0 -112
  131. package/clients/lsp/index.js +0 -318
  132. package/clients/lsp/installer/index.js +0 -391
  133. package/clients/lsp/interactive-install.js +0 -221
  134. package/clients/lsp/language.js +0 -170
  135. package/clients/lsp/launch.js +0 -329
  136. package/clients/lsp/lsp/launch.js +0 -116
  137. package/clients/lsp/lsp/server.js +0 -532
  138. package/clients/lsp/lsp-index.js +0 -10
  139. package/clients/lsp/path-utils.js +0 -5
  140. package/clients/lsp/server.js +0 -725
  141. package/clients/lsp/test-py-spawn/requirements.txt +0 -1
  142. package/clients/lsp/test-py-spawn/test.py +0 -3
  143. package/clients/lsp/test-py-svc/requirements.txt +0 -1
  144. package/clients/lsp/test-py-svc/test.py +0 -3
  145. package/clients/lsp/test-python-project/requirements.txt +0 -1
  146. package/clients/lsp/test-python-project/test.py +0 -5
  147. package/clients/metrics-client.js +0 -107
  148. package/clients/metrics-client.test.js +0 -128
  149. package/clients/metrics-client.test.ts +0 -163
  150. package/clients/metrics-history.js +0 -367
  151. package/clients/path-utils.js +0 -142
  152. package/clients/pipeline.js +0 -272
  153. package/clients/production-readiness.js +0 -522
  154. package/clients/project-index.js +0 -255
  155. package/clients/project-metadata.js +0 -531
  156. package/clients/ruff-client.js +0 -325
  157. package/clients/ruff-client.test.js +0 -132
  158. package/clients/ruff-client.test.ts +0 -153
  159. package/clients/rules-scanner.js +0 -97
  160. package/clients/runner-tracker.js +0 -152
  161. package/clients/rust-client.js +0 -205
  162. package/clients/rust-client.test.js +0 -108
  163. package/clients/rust-client.test.ts +0 -130
  164. package/clients/safe-spawn-async.js +0 -163
  165. package/clients/safe-spawn.js +0 -241
  166. package/clients/sanitize.js +0 -291
  167. package/clients/sanitize.test.js +0 -177
  168. package/clients/sanitize.test.ts +0 -223
  169. package/clients/scan-architectural-debt.js +0 -167
  170. package/clients/scan-utils.js +0 -83
  171. package/clients/secrets-scanner.js +0 -119
  172. package/clients/secrets-scanner.test.js +0 -100
  173. package/clients/secrets-scanner.test.ts +0 -113
  174. package/clients/sg-runner.js +0 -292
  175. package/clients/state-matrix.js +0 -160
  176. package/clients/subprocess-client.js +0 -65
  177. package/clients/symbol-types.js +0 -5
  178. package/clients/test-runner-client.js +0 -523
  179. package/clients/test-runner-client.test.js +0 -192
  180. package/clients/test-runner-client.test.ts +0 -253
  181. package/clients/test-utils.js +0 -27
  182. package/clients/test-utils.ts +0 -36
  183. package/clients/todo-scanner.js +0 -200
  184. package/clients/todo-scanner.test.js +0 -301
  185. package/clients/todo-scanner.test.ts +0 -352
  186. package/clients/tool-availability.js +0 -207
  187. package/clients/tree-sitter-client.js +0 -601
  188. package/clients/tree-sitter-query-loader.js +0 -355
  189. package/clients/tree-sitter-symbol-extractor.js +0 -289
  190. package/clients/ts-service.js +0 -129
  191. package/clients/type-coverage-client.js +0 -127
  192. package/clients/type-coverage-client.test.js +0 -105
  193. package/clients/type-coverage-client.test.ts +0 -125
  194. package/clients/type-safety-client.js +0 -138
  195. package/clients/types.js +0 -11
  196. package/clients/typescript-client.codefix.test.js +0 -157
  197. package/clients/typescript-client.codefix.test.ts +0 -186
  198. package/clients/typescript-client.js +0 -509
  199. package/clients/typescript-client.test.js +0 -105
  200. package/clients/typescript-client.test.ts +0 -126
  201. package/commands/booboo.js +0 -1007
  202. package/commands/fix-from-booboo.js +0 -398
  203. package/commands/fix-simplified.js +0 -618
  204. package/commands/rate.js +0 -281
  205. package/commands/rate.test.js +0 -119
  206. package/commands/rate.test.ts +0 -131
  207. 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
- }