pi-lens 2.0.37 → 2.0.39

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/clients/architect-client.js +50 -20
  4. package/clients/architect-client.ts +61 -22
  5. package/clients/ast-grep-client.js +32 -127
  6. package/clients/ast-grep-client.test.js +2 -14
  7. package/clients/ast-grep-client.test.ts +2 -16
  8. package/clients/ast-grep-client.ts +34 -150
  9. package/clients/auto-loop.js +117 -0
  10. package/clients/auto-loop.ts +171 -0
  11. package/clients/biome-client.js +25 -22
  12. package/clients/biome-client.ts +28 -25
  13. package/clients/complexity-client.js +111 -74
  14. package/clients/complexity-client.ts +149 -105
  15. package/clients/dependency-checker.js +16 -30
  16. package/clients/dependency-checker.ts +15 -35
  17. package/clients/fix-scanners.js +195 -0
  18. package/clients/fix-scanners.ts +297 -0
  19. package/clients/interviewer-templates.js +75 -0
  20. package/clients/interviewer-templates.ts +90 -0
  21. package/clients/interviewer.js +73 -101
  22. package/clients/interviewer.ts +195 -140
  23. package/clients/knip-client.js +12 -4
  24. package/clients/knip-client.ts +21 -16
  25. package/clients/metrics-history.js +215 -0
  26. package/clients/metrics-history.ts +300 -0
  27. package/clients/scan-architectural-debt.js +62 -72
  28. package/clients/scan-architectural-debt.ts +79 -64
  29. package/clients/scan-utils.js +98 -0
  30. package/clients/scan-utils.ts +112 -0
  31. package/clients/sg-runner.js +138 -0
  32. package/clients/sg-runner.ts +168 -0
  33. package/clients/subprocess-client.js +0 -37
  34. package/clients/subprocess-client.ts +0 -60
  35. package/clients/ts-service.ts +1 -7
  36. package/clients/type-safety-client.js +3 -8
  37. package/clients/type-safety-client.ts +10 -14
  38. package/clients/typescript-client.js +55 -56
  39. package/clients/typescript-client.ts +94 -52
  40. package/default-architect.yaml +87 -0
  41. package/index.ts +143 -1165
  42. package/package.json +2 -1
  43. package/rules/ast-grep-rules/rules/large-class.yml +6 -2
  44. package/rules/ast-grep-rules/rules/long-method.yml +1 -1
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
  46. package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
@@ -0,0 +1,168 @@
1
+ /**
2
+ * SgRunner - encapsulates ast-grep subprocess management
3
+ *
4
+ * Extracted from AstGrepClient to simplify the main client.
5
+ * Handles: spawn, spawnSync, temp dir management, JSON parsing.
6
+ */
7
+
8
+ import { spawn, spawnSync } from "node:child_process";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ export interface SgMatch {
14
+ file: string;
15
+ range: {
16
+ start: { line: number; column: number };
17
+ end: { line: number; column: number };
18
+ };
19
+ text: string;
20
+ replacement?: string;
21
+ }
22
+
23
+ export interface SgResult {
24
+ matches: SgMatch[];
25
+ error?: string;
26
+ }
27
+
28
+ export class SgRunner {
29
+ private log: (msg: string) => void;
30
+
31
+ constructor(verbose = false) {
32
+ this.log = verbose ? (msg: string) => console.error(`[sg-runner] ${msg}`) : () => {};
33
+ }
34
+
35
+ /**
36
+ * Check if ast-grep CLI is available
37
+ */
38
+ isAvailable(): boolean {
39
+ const result = spawnSync("npx", ["sg", "--version"], {
40
+ encoding: "utf-8",
41
+ timeout: 10000,
42
+ shell: true,
43
+ });
44
+ return !result.error && result.status === 0;
45
+ }
46
+
47
+ /**
48
+ * Run ast-grep asynchronously, return parsed matches
49
+ */
50
+ async exec(args: string[]): Promise<SgResult> {
51
+ return new Promise((resolve) => {
52
+ const proc = spawn("npx", ["sg", ...args], {
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ shell: true,
55
+ });
56
+ let stdout = "";
57
+ let stderr = "";
58
+
59
+ proc.stdout.on("data", (data: Buffer) => (stdout += data.toString()));
60
+ proc.stderr.on("data", (data: Buffer) => (stderr += data.toString()));
61
+
62
+ proc.on("error", (err: Error) => {
63
+ if (err.message.includes("ENOENT")) {
64
+ resolve({ matches: [], error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli" });
65
+ } else {
66
+ resolve({ matches: [], error: err.message });
67
+ }
68
+ });
69
+
70
+ proc.on("close", (code: number | null) => {
71
+ if (code !== 0 && !stdout.trim()) {
72
+ resolve({
73
+ matches: [],
74
+ error: stderr.includes("No files found") ? undefined : stderr.trim() || `Exit code ${code}`,
75
+ });
76
+ return;
77
+ }
78
+ if (!stdout.trim()) {
79
+ resolve({ matches: [] });
80
+ return;
81
+ }
82
+ try {
83
+ const parsed = JSON.parse(stdout);
84
+ const matches = Array.isArray(parsed) ? parsed : [parsed];
85
+ resolve({ matches });
86
+ } catch {
87
+ resolve({ matches: [], error: "Failed to parse output" });
88
+ }
89
+ });
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Run ast-grep synchronously (for simple scans)
95
+ */
96
+ execSync(args: string[]): { output: string; error?: string } {
97
+ const result = spawnSync("npx", ["sg", ...args], {
98
+ encoding: "utf-8",
99
+ timeout: 30000,
100
+ shell: true,
101
+ maxBuffer: 32 * 1024 * 1024,
102
+ });
103
+
104
+ if (result.error) {
105
+ return { output: "", error: result.error.message };
106
+ }
107
+
108
+ const output = result.stdout || result.stderr || "";
109
+ return { output };
110
+ }
111
+
112
+ /**
113
+ * Run a temporary rule scan (creates temp dir with rule file)
114
+ */
115
+ tempScan(dir: string, ruleId: string, ruleYaml: string, timeout = 30000): SgMatch[] {
116
+ const tmpDir = os.tmpdir();
117
+ const ts = Date.now();
118
+ const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
119
+ const rulesSubdir = path.join(sessionDir, "rules");
120
+ const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
121
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
122
+
123
+ try {
124
+ fs.mkdirSync(rulesSubdir, { recursive: true });
125
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
126
+ fs.writeFileSync(ruleFile, ruleYaml);
127
+
128
+ const result = spawnSync(
129
+ "npx",
130
+ ["sg", "scan", "--config", configFile, "--json", dir],
131
+ { encoding: "utf-8", timeout, shell: true },
132
+ );
133
+
134
+ const output = result.stdout || result.stderr || "";
135
+ if (!output.trim()) return [];
136
+
137
+ const items = JSON.parse(output);
138
+ return Array.isArray(items) ? items : [items];
139
+ } catch {
140
+ return [];
141
+ } finally {
142
+ try {
143
+ fs.rmSync(sessionDir, { recursive: true, force: true });
144
+ } catch (err) {
145
+ this.log(`Cleanup failed: ${(err as Error).message}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Format matches for display
152
+ */
153
+ formatMatches(matches: SgMatch[], isDryRun = false, maxItems = 50): string {
154
+ if (matches.length === 0) return "No matches found";
155
+ const shown = matches.slice(0, maxItems);
156
+ const lines = shown.map((m) => {
157
+ const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
158
+ const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
159
+ return isDryRun && m.replacement
160
+ ? `${loc}\n - ${text}\n + ${m.replacement}`
161
+ : `${loc}: ${text}`;
162
+ });
163
+ if (matches.length > maxItems) {
164
+ lines.unshift(`Found ${matches.length} matches (showing first ${maxItems}):`);
165
+ }
166
+ return lines.join("\n");
167
+ }
168
+ }
@@ -1,20 +1,4 @@
1
- /**
2
- * Base class for CLI tool clients that communicate via subprocess.
3
- *
4
- * Provides common patterns for:
5
- * - Availability checking (cached)
6
- * - File type detection
7
- * - Running CLI commands
8
- * - Logging
9
- *
10
- * Subclasses implement:
11
- * - getToolName(): The CLI tool name
12
- * - getCheckCommand(): Command to check availability
13
- * - isSupportedFile(): File extensions the tool handles
14
- * - parseOutput(): Parse CLI output into diagnostics
15
- */
16
1
  import { spawnSync } from "node:child_process";
17
- import * as fs from "node:fs";
18
2
  import * as path from "node:path";
19
3
  export class SubprocessClient {
20
4
  constructor(verbose = false) {
@@ -24,9 +8,6 @@ export class SubprocessClient {
24
8
  ? (msg) => console.error(`[${this.toolName}] ${msg}`)
25
9
  : () => { };
26
10
  }
27
- /**
28
- * Check if the CLI tool is available (cached)
29
- */
30
11
  isAvailable() {
31
12
  if (this.available !== null)
32
13
  return this.available;
@@ -51,16 +32,10 @@ export class SubprocessClient {
51
32
  }
52
33
  return this.available;
53
34
  }
54
- /**
55
- * Check if a file is supported by this tool
56
- */
57
35
  isSupportedFile(filePath) {
58
36
  const ext = path.extname(filePath).toLowerCase();
59
37
  return this.getSupportedExtensions().includes(ext);
60
38
  }
61
- /**
62
- * Run a command and return the result
63
- */
64
39
  runCommand(cmd, options = {}) {
65
40
  const { cwd, timeout = 15000, input } = options;
66
41
  try {
@@ -86,16 +61,4 @@ export class SubprocessClient {
86
61
  };
87
62
  }
88
63
  }
89
- /**
90
- * Resolve a file path to absolute
91
- */
92
- resolvePath(filePath) {
93
- return path.resolve(filePath);
94
- }
95
- /**
96
- * Check if a file exists
97
- */
98
- fileExists(filePath) {
99
- return fs.existsSync(filePath);
100
- }
101
64
  }
@@ -1,21 +1,4 @@
1
- /**
2
- * Base class for CLI tool clients that communicate via subprocess.
3
- *
4
- * Provides common patterns for:
5
- * - Availability checking (cached)
6
- * - File type detection
7
- * - Running CLI commands
8
- * - Logging
9
- *
10
- * Subclasses implement:
11
- * - getToolName(): The CLI tool name
12
- * - getCheckCommand(): Command to check availability
13
- * - isSupportedFile(): File extensions the tool handles
14
- * - parseOutput(): Parse CLI output into diagnostics
15
- */
16
-
17
1
  import { spawnSync } from "node:child_process";
18
- import * as fs from "node:fs";
19
2
  import * as path from "node:path";
20
3
 
21
4
  export interface Diagnostic {
@@ -42,31 +25,11 @@ export abstract class SubprocessClient<T extends Diagnostic> {
42
25
  : () => {};
43
26
  }
44
27
 
45
- /**
46
- * The name of the CLI tool (used in log messages)
47
- */
48
28
  protected abstract getToolName(): string;
49
-
50
- /**
51
- * Command and args to check if the tool is available
52
- * e.g., ["ruff", "--version"] or ["npx", "@biomejs/biome", "--version"]
53
- */
54
29
  protected abstract getCheckCommand(): string[];
55
-
56
- /**
57
- * File extensions this tool supports (with dots)
58
- * e.g., [".py"] or [".ts", ".tsx", ".js", ".jsx"]
59
- */
60
30
  protected abstract getSupportedExtensions(): string[];
61
-
62
- /**
63
- * Parse CLI output into diagnostics
64
- */
65
31
  protected abstract parseOutput(output: string, filePath: string): T[];
66
32
 
67
- /**
68
- * Check if the CLI tool is available (cached)
69
- */
70
33
  isAvailable(): boolean {
71
34
  if (this.available !== null) return this.available;
72
35
 
@@ -92,22 +55,13 @@ export abstract class SubprocessClient<T extends Diagnostic> {
92
55
  return this.available;
93
56
  }
94
57
 
95
- /**
96
- * Check if a file is supported by this tool
97
- */
98
58
  isSupportedFile(filePath: string): boolean {
99
59
  const ext = path.extname(filePath).toLowerCase();
100
60
  return this.getSupportedExtensions().includes(ext);
101
61
  }
102
62
 
103
- /**
104
- * Run the tool on a file and return diagnostics
105
- */
106
63
  abstract checkFile(filePath: string): T[];
107
64
 
108
- /**
109
- * Run a command and return the result
110
- */
111
65
  protected runCommand(
112
66
  cmd: string[],
113
67
  options: {
@@ -142,18 +96,4 @@ export abstract class SubprocessClient<T extends Diagnostic> {
142
96
  } as unknown as ReturnType<typeof spawnSync>;
143
97
  }
144
98
  }
145
-
146
- /**
147
- * Resolve a file path to absolute
148
- */
149
- protected resolvePath(filePath: string): string {
150
- return path.resolve(filePath);
151
- }
152
-
153
- /**
154
- * Check if a file exists
155
- */
156
- protected fileExists(filePath: string): boolean {
157
- return fs.existsSync(filePath);
158
- }
159
99
  }
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import * as fs from "node:fs";
9
- import * as path from "node:path";
10
9
  import * as ts from "typescript";
11
10
 
12
11
  export class TypeScriptService {
@@ -120,12 +119,7 @@ export class TypeScriptService {
120
119
  return null;
121
120
  }
122
121
  }
123
- return ts.createSourceFile(
124
- filePath,
125
- content,
126
- ts.ScriptTarget.Latest,
127
- true,
128
- );
122
+ return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
129
123
  }
130
124
 
131
125
  /**
@@ -100,7 +100,8 @@ export class TypeSafetyClient {
100
100
  // Find missing cases
101
101
  const missingCases = literalValues.filter((v) => !coveredCases.has(v));
102
102
  if (missingCases.length > 0 && !hasDefault) {
103
- const line = programSourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
103
+ const line = programSourceFile.getLineAndCharacterOfPosition(node.getStart())
104
+ .line + 1;
104
105
  const exprText = node.expression.getText(programSourceFile);
105
106
  const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
106
107
  issues.push({
@@ -134,10 +135,4 @@ export class TypeSafetyClient {
134
135
  }
135
136
  }
136
137
  // --- Singleton ---
137
- let instance = null;
138
- export function getTypeSafetyClient(verbose = false) {
139
- if (!instance) {
140
- instance = new TypeSafetyClient(verbose);
141
- }
142
- return instance;
143
- }
138
+ const _instance = null;
@@ -90,7 +90,9 @@ export class TypeSafetyClient {
90
90
 
91
91
  // Use the source file from the program (has proper type information)
92
92
  const tsService = getTypeScriptService();
93
- const programSourceFile = tsService.getSourceFileFromProgram(sourceFile.fileName);
93
+ const programSourceFile = tsService.getSourceFileFromProgram(
94
+ sourceFile.fileName,
95
+ );
94
96
  if (!programSourceFile) return;
95
97
 
96
98
  const visit = (node: ts.Node) => {
@@ -103,7 +105,9 @@ export class TypeSafetyClient {
103
105
 
104
106
  // Get all literal values from the union
105
107
  const literalValues = unionTypes
106
- .filter((t) => t.isLiteral() || t.flags & ts.TypeFlags.BooleanLiteral)
108
+ .filter(
109
+ (t) => t.isLiteral() || t.flags & ts.TypeFlags.BooleanLiteral,
110
+ )
107
111
  .map((t) => {
108
112
  if (t.isLiteral()) {
109
113
  return String(t.value);
@@ -143,9 +147,9 @@ export class TypeSafetyClient {
143
147
  );
144
148
 
145
149
  if (missingCases.length > 0 && !hasDefault) {
146
- const line = programSourceFile.getLineAndCharacterOfPosition(
147
- node.getStart(),
148
- ).line + 1;
150
+ const line =
151
+ programSourceFile.getLineAndCharacterOfPosition(node.getStart())
152
+ .line + 1;
149
153
 
150
154
  const exprText = node.expression.getText(programSourceFile);
151
155
  const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
@@ -184,16 +188,8 @@ export class TypeSafetyClient {
184
188
  }
185
189
  return checker;
186
190
  }
187
-
188
191
  }
189
192
 
190
193
  // --- Singleton ---
191
194
 
192
- let instance: TypeSafetyClient | null = null;
193
-
194
- export function getTypeSafetyClient(verbose = false): TypeSafetyClient {
195
- if (!instance) {
196
- instance = new TypeSafetyClient(verbose);
197
- }
198
- return instance;
199
- }
195
+ const _instance: TypeSafetyClient | null = null;
@@ -264,58 +264,61 @@ export class TypeScriptClient {
264
264
  ls: this.languageService,
265
265
  };
266
266
  }
267
- /**
268
- * Go to definition
269
- */
270
- getDefinition(filePath, line, character) {
267
+ withPosition(filePath, line, character, cb) {
271
268
  const resolved = this.resolvePosition(filePath, line, character);
272
269
  if (!resolved)
273
270
  return [];
274
271
  const { normalized, position, ls } = resolved;
275
- const definitions = ls.getDefinitionAtPosition(normalized, position);
276
- if (!definitions)
277
- return [];
278
- return definitions.map((def) => {
279
- if (def.textSpan) {
280
- const defFile = def.fileName || normalized;
281
- const defContent = this.fileContents.get(defFile) || "";
282
- if (defContent) {
283
- const lines = defContent.substring(0, def.textSpan.start).split("\n");
284
- return {
285
- file: defFile,
286
- line: lines.length - 1,
287
- character: lines[lines.length - 1].length,
288
- };
272
+ return cb(normalized, position, ls) ?? [];
273
+ }
274
+ /**
275
+ * Go to definition
276
+ */
277
+ getDefinition(filePath, line, character) {
278
+ return this.withPosition(filePath, line, character, (normalized, position, ls) => {
279
+ const definitions = ls.getDefinitionAtPosition(normalized, position);
280
+ if (!definitions)
281
+ return undefined;
282
+ return definitions.map((def) => {
283
+ if (def.textSpan) {
284
+ const defFile = def.fileName || normalized;
285
+ const defContent = this.fileContents.get(defFile) || "";
286
+ if (defContent) {
287
+ const lines = defContent
288
+ .substring(0, def.textSpan.start)
289
+ .split("\n");
290
+ return {
291
+ file: defFile,
292
+ line: lines.length - 1,
293
+ character: lines[lines.length - 1].length,
294
+ };
295
+ }
289
296
  }
290
- }
291
- return { file: def.fileName, line: 0, character: 0 };
297
+ return { file: def.fileName, line: 0, character: 0 };
298
+ });
292
299
  });
293
300
  }
294
301
  /**
295
302
  * Get type definition
296
303
  */
297
304
  getTypeDefinition(filePath, line, character) {
298
- const resolved = this.resolvePosition(filePath, line, character);
299
- if (!resolved)
300
- return [];
301
- const { normalized, position, ls } = resolved;
302
- const defs = ls.getTypeDefinitionAtPosition(normalized, position);
303
- if (!defs)
304
- return [];
305
- return this.toLocations(defs, normalized);
305
+ return this.withPosition(filePath, line, character, (normalized, position, ls) => {
306
+ const defs = ls.getTypeDefinitionAtPosition(normalized, position);
307
+ if (!defs)
308
+ return undefined;
309
+ return this.toLocations(defs, normalized);
310
+ });
306
311
  }
307
312
  /**
308
313
  * Find references
309
314
  */
310
315
  getReferences(filePath, line, character) {
311
- const resolved = this.resolvePosition(filePath, line, character);
312
- if (!resolved)
313
- return [];
314
- const { normalized, position, ls } = resolved;
315
- const references = ls.getReferencesAtPosition(normalized, position);
316
- if (!references)
317
- return [];
318
- return this.toLocations(references);
316
+ return this.withPosition(filePath, line, character, (normalized, position, ls) => {
317
+ const references = ls.getReferencesAtPosition(normalized, position);
318
+ if (!references)
319
+ return undefined;
320
+ return this.toLocations(references);
321
+ });
319
322
  }
320
323
  /** Map TS definition/reference entries to Location objects. */
321
324
  toLocations(entries, fallbackFile) {
@@ -369,31 +372,27 @@ export class TypeScriptClient {
369
372
  * Get completions at a position
370
373
  */
371
374
  getCompletions(filePath, line, character) {
372
- const resolved = this.resolvePosition(filePath, line, character);
373
- if (!resolved)
374
- return [];
375
- const { normalized, position, ls } = resolved;
376
- const completions = ls.getCompletionsAtPosition(normalized, position, {});
377
- if (!completions)
378
- return [];
379
- return completions.entries.slice(0, 50).map((entry) => ({
380
- name: entry.name,
381
- kind: this.completionKind(entry.kind),
382
- sortText: entry.sortText,
383
- }));
375
+ return this.withPosition(filePath, line, character, (normalized, position, ls) => {
376
+ const completions = ls.getCompletionsAtPosition(normalized, position, {});
377
+ if (!completions)
378
+ return undefined;
379
+ return completions.entries.slice(0, 50).map((entry) => ({
380
+ name: entry.name,
381
+ kind: this.completionKind(entry.kind),
382
+ sortText: entry.sortText,
383
+ }));
384
+ });
384
385
  }
385
386
  /**
386
387
  * Go to implementation
387
388
  */
388
389
  getImplementation(filePath, line, character) {
389
- const resolved = this.resolvePosition(filePath, line, character);
390
- if (!resolved)
391
- return [];
392
- const { normalized, position, ls } = resolved;
393
- const implementations = ls.getImplementationAtPosition(normalized, position);
394
- if (!implementations)
395
- return [];
396
- return this.toLocations(implementations);
390
+ return this.withPosition(filePath, line, character, (normalized, position, ls) => {
391
+ const implementations = ls.getImplementationAtPosition(normalized, position);
392
+ if (!implementations)
393
+ return undefined;
394
+ return this.toLocations(implementations);
395
+ });
397
396
  }
398
397
  /**
399
398
  * Get folding ranges