pi-lens 1.3.13 → 2.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.
@@ -1,44 +1,31 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { DependencyChecker } from "./dependency-checker.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("DependencyChecker", () => {
8
- let checker: DependencyChecker;
6
+ let client: DependencyChecker;
9
7
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- const dir = path.dirname(filePath);
14
- if (!fs.existsSync(dir)) {
15
- fs.mkdirSync(dir, { recursive: true });
16
- }
17
- fs.writeFileSync(filePath, content);
18
- return filePath;
19
- }
8
+ let cleanup: () => void;
20
9
 
21
10
  beforeEach(() => {
22
- checker = new DependencyChecker();
23
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-dep-test-"));
11
+ client = new DependencyChecker();
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-dep-test-"));
24
13
  });
25
14
 
26
15
  afterEach(() => {
27
- if (tmpDir && fs.existsSync(tmpDir)) {
28
- fs.rmSync(tmpDir, { recursive: true });
29
- }
16
+ cleanup();
30
17
  });
31
18
 
32
19
  describe("isAvailable", () => {
33
20
  it("should check madge availability", () => {
34
- const available = checker.isAvailable();
21
+ const available = client.isAvailable();
35
22
  expect(typeof available).toBe("boolean");
36
23
  });
37
24
  });
38
25
 
39
26
  describe("checkFile", () => {
40
27
  it("should return no circular deps for non-existent files", () => {
41
- const result = checker.checkFile("/nonexistent/file.ts");
28
+ const result = client.checkFile("/nonexistent/file.ts");
42
29
  expect(result.hasCircular).toBe(false);
43
30
  expect(result.circular).toEqual([]);
44
31
  });
@@ -68,7 +55,7 @@ describe("DependencyChecker", () => {
68
55
  describe("formatWarning", () => {
69
56
  it("should format circular dependency warning", () => {
70
57
  const circularDeps = ["b.ts", "c.ts", "a.ts"];
71
- const formatted = checker.formatWarning("a.ts", circularDeps);
58
+ const formatted = client.formatWarning("a.ts", circularDeps);
72
59
 
73
60
  expect(formatted).toContain("cycle");
74
61
  expect(formatted).toContain("a.ts");
@@ -76,7 +63,7 @@ describe("DependencyChecker", () => {
76
63
 
77
64
  it("should show the circular path", () => {
78
65
  const circularDeps = ["b.ts", "a.ts"];
79
- const formatted = checker.formatWarning("a.ts", circularDeps);
66
+ const formatted = client.formatWarning("a.ts", circularDeps);
80
67
 
81
68
  expect(formatted).toContain("b.ts");
82
69
  });
@@ -1,22 +1,25 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { GoClient } from "./go-client.js";
3
+ import { setupTestEnvironment } from "./test-utils.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as path from "node:path";
5
- import * as os from "node:os";
6
6
 
7
7
  describe("GoClient", () => {
8
8
  let client: GoClient;
9
9
  let tmpDir: string;
10
+ let cleanup: () => void;
10
11
 
11
12
  beforeEach(() => {
12
13
  client = new GoClient();
13
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-go-test-"));
14
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-go-test-"));
14
15
  });
15
16
 
16
17
  afterEach(() => {
17
- if (tmpDir && fs.existsSync(tmpDir)) {
18
- fs.rmSync(tmpDir, { recursive: true });
19
- }
18
+ cleanup();
19
+ });
20
+
21
+ afterEach(() => {
22
+ cleanup();
20
23
  });
21
24
 
22
25
  describe("isGoFile", () => {
@@ -1,32 +1,23 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { JscpdClient } from "./jscpd-client.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("JscpdClient", () => {
8
6
  let client: JscpdClient;
9
7
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- const dir = path.dirname(filePath);
14
- if (!fs.existsSync(dir)) {
15
- fs.mkdirSync(dir, { recursive: true });
16
- }
17
- fs.writeFileSync(filePath, content);
18
- return filePath;
19
- }
8
+ let cleanup: () => void;
20
9
 
21
10
  beforeEach(() => {
22
11
  client = new JscpdClient();
23
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-jscpd-test-"));
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-jscpd-test-"));
13
+ });
14
+
15
+ afterEach(() => {
16
+ cleanup();
24
17
  });
25
18
 
26
19
  afterEach(() => {
27
- if (tmpDir && fs.existsSync(tmpDir)) {
28
- fs.rmSync(tmpDir, { recursive: true });
29
- }
20
+ cleanup();
30
21
  });
31
22
 
32
23
  describe("isAvailable", () => {
@@ -60,8 +51,8 @@ function processData(data: number[]): number {
60
51
  return sum;
61
52
  }
62
53
  `;
63
- createTempFile("file1.ts", duplicateCode);
64
- createTempFile("file2.ts", duplicateCode);
54
+ createTempFile(tmpDir, "file1.ts", duplicateCode);
55
+ createTempFile(tmpDir, "file2.ts", duplicateCode);
65
56
 
66
57
  const result = client.scan(tmpDir, 3, 20); // Lower thresholds for test
67
58
 
@@ -73,7 +73,7 @@ export class JscpdClient {
73
73
  "--min-tokens", String(minTokens),
74
74
  "--reporters", "json",
75
75
  "--output", outDir,
76
- "--ignore", "**/node_modules/**,**/dist/**,**/build/**,**/.git/**",
76
+ "--ignore", "**/node_modules/**,**/dist/**,**/build/**,**/.git/**,**/.pi-lens/**,**/*.md,**/*.txt,**/*.json,**/*.yaml,**/*.yml,**/*.toml,**/*.lock",
77
77
  ], {
78
78
  encoding: "utf-8",
79
79
  timeout: 30000,
@@ -1,22 +1,23 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { KnipClient } from "./knip-client.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("KnipClient", () => {
8
6
  let client: KnipClient;
9
7
  let tmpDir: string;
8
+ let cleanup: () => void;
10
9
 
11
10
  beforeEach(() => {
12
11
  client = new KnipClient();
13
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-knip-test-"));
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-knip-test-"));
14
13
  });
15
14
 
16
15
  afterEach(() => {
17
- if (tmpDir && fs.existsSync(tmpDir)) {
18
- fs.rmSync(tmpDir, { recursive: true });
19
- }
16
+ cleanup();
17
+ });
18
+
19
+ afterEach(() => {
20
+ cleanup();
20
21
  });
21
22
 
22
23
  describe("isAvailable", () => {
@@ -1,28 +1,25 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { MetricsClient } from "./metrics-client.js";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as path from "node:path";
5
- import * as os from "node:os";
6
6
 
7
7
  describe("MetricsClient", () => {
8
8
  let client: MetricsClient;
9
9
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- fs.writeFileSync(filePath, content);
14
- return filePath;
15
- }
10
+ let cleanup: () => void;
16
11
 
17
12
  beforeEach(() => {
18
13
  client = new MetricsClient();
19
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-metrics-test-"));
14
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-metrics-test-"));
20
15
  });
21
16
 
22
17
  afterEach(() => {
23
- if (tmpDir && fs.existsSync(tmpDir)) {
24
- fs.rmSync(tmpDir, { recursive: true });
25
- }
18
+ cleanup();
19
+ });
20
+
21
+ afterEach(() => {
22
+ cleanup();
26
23
  });
27
24
 
28
25
  describe("calculateEntropy", () => {
@@ -56,7 +53,7 @@ describe("MetricsClient", () => {
56
53
  describe("recordBaseline", () => {
57
54
  it("should record baseline for existing file", () => {
58
55
  const content = "const x = 1;\nconst y = 2;";
59
- const filePath = createTempFile("test.ts", content);
56
+ const filePath = createTempFile(tmpDir, "test.ts", content);
60
57
 
61
58
  client.recordBaseline(filePath);
62
59
 
@@ -74,7 +71,7 @@ describe("MetricsClient", () => {
74
71
  it("should not overwrite existing baseline", () => {
75
72
  const content1 = "const x = 1;\n";
76
73
  const content2 = "const x = 1;\nconst y = 2;\nconst z = 3;\n";
77
- const filePath = createTempFile("test.ts", content1);
74
+ const filePath = createTempFile(tmpDir, "test.ts", content1);
78
75
 
79
76
  client.recordBaseline(filePath);
80
77
 
@@ -92,7 +89,7 @@ describe("MetricsClient", () => {
92
89
  describe("recordWrite", () => {
93
90
  it("should track agent-written lines", () => {
94
91
  const original = "const x = 1;\n";
95
- const filePath = createTempFile("test.ts", original);
92
+ const filePath = createTempFile(tmpDir, "test.ts", original);
96
93
 
97
94
  client.recordBaseline(filePath);
98
95
 
@@ -105,8 +102,8 @@ describe("MetricsClient", () => {
105
102
  });
106
103
 
107
104
  it("should calculate AI code ratio", () => {
108
- const file1 = createTempFile("file1.ts", "original content line 1\noriginal content line 2\n");
109
- const file2 = createTempFile("file2.ts", "original\n");
105
+ const file1 = createTempFile(tmpDir, "file1.ts", "original content line 1\noriginal content line 2\n");
106
+ const file2 = createTempFile(tmpDir, "file2.ts", "original\n");
110
107
 
111
108
  client.recordBaseline(file1);
112
109
  client.recordBaseline(file2);
@@ -126,7 +123,7 @@ describe("MetricsClient", () => {
126
123
  describe("getEntropyDeltas", () => {
127
124
  it("should track entropy changes", () => {
128
125
  const simple = "const x = 1;\n";
129
- const filePath = createTempFile("test.ts", simple);
126
+ const filePath = createTempFile(tmpDir, "test.ts", simple);
130
127
 
131
128
  client.recordBaseline(filePath);
132
129
 
@@ -158,7 +155,7 @@ function complex(a: number, b: number, c: number): number {
158
155
  });
159
156
 
160
157
  it("should format AI code ratio when files are modified", () => {
161
- const filePath = createTempFile("test.ts", "original\n");
158
+ const filePath = createTempFile(tmpDir, "test.ts", "original\n");
162
159
  client.recordBaseline(filePath);
163
160
 
164
161
  const modified = "original\nnew line 1\nnew line 2\n";
@@ -173,7 +170,7 @@ function complex(a: number, b: number, c: number): number {
173
170
 
174
171
  describe("reset", () => {
175
172
  it("should clear all tracked data", () => {
176
- const filePath = createTempFile("test.ts", "content\n");
173
+ const filePath = createTempFile(tmpDir, "test.ts", "content\n");
177
174
  client.recordBaseline(filePath);
178
175
 
179
176
  client.reset();
@@ -1,28 +1,23 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { RuffClient } from "./ruff-client.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("RuffClient", () => {
8
6
  let client: RuffClient;
9
7
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- fs.writeFileSync(filePath, content);
14
- return filePath;
15
- }
8
+ let cleanup: () => void;
16
9
 
17
10
  beforeEach(() => {
18
11
  client = new RuffClient();
19
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-ruff-test-"));
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-ruff-test-"));
13
+ });
14
+
15
+ afterEach(() => {
16
+ cleanup();
20
17
  });
21
18
 
22
19
  afterEach(() => {
23
- if (tmpDir && fs.existsSync(tmpDir)) {
24
- fs.rmSync(tmpDir, { recursive: true });
25
- }
20
+ cleanup();
26
21
  });
27
22
 
28
23
  describe("isPythonFile", () => {
@@ -61,7 +56,7 @@ import sys
61
56
 
62
57
  x = 1
63
58
  `;
64
- const filePath = createTempFile("test.py", content);
59
+ const filePath = createTempFile(tmpDir, "test.py", content);
65
60
  const result = client.checkFile(filePath);
66
61
 
67
62
  // Should detect unused imports
@@ -76,7 +71,7 @@ def foo():
76
71
  x = undefined_variable
77
72
  return x
78
73
  `;
79
- const filePath = createTempFile("test.py", content);
74
+ const filePath = createTempFile(tmpDir, "test.py", content);
80
75
  const result = client.checkFile(filePath);
81
76
 
82
77
  // Should return an array
@@ -124,7 +119,7 @@ def foo():
124
119
  const content = `x=1
125
120
  y=2
126
121
  `;
127
- const filePath = createTempFile("test.py", content);
122
+ const filePath = createTempFile(tmpDir, "test.py", content);
128
123
  const result = client.checkFormatting(filePath);
129
124
 
130
125
  // Should suggest formatting (missing spaces around =)
@@ -137,7 +132,7 @@ y=2
137
132
  const content = `x = 1
138
133
  y = 2
139
134
  `;
140
- const filePath = createTempFile("test.py", content);
135
+ const filePath = createTempFile(tmpDir, "test.py", content);
141
136
  const result = client.checkFormatting(filePath);
142
137
 
143
138
  // Well-formatted code should return empty or minimal output
@@ -1,22 +1,25 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { RustClient } from "./rust-client.js";
3
+ import { setupTestEnvironment } from "./test-utils.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as path from "node:path";
5
- import * as os from "node:os";
6
6
 
7
7
  describe("RustClient", () => {
8
8
  let client: RustClient;
9
9
  let tmpDir: string;
10
+ let cleanup: () => void;
10
11
 
11
12
  beforeEach(() => {
12
13
  client = new RustClient();
13
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-rust-test-"));
14
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-rust-test-"));
14
15
  });
15
16
 
16
17
  afterEach(() => {
17
- if (tmpDir && fs.existsSync(tmpDir)) {
18
- fs.rmSync(tmpDir, { recursive: true });
19
- }
18
+ cleanup();
19
+ });
20
+
21
+ afterEach(() => {
22
+ cleanup();
20
23
  });
21
24
 
22
25
  describe("isRustFile", () => {
@@ -0,0 +1,158 @@
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
+ import { spawnSync } from "node:child_process";
18
+ import * as path from "node:path";
19
+ import * as fs from "node:fs";
20
+
21
+ export interface Diagnostic {
22
+ line: number;
23
+ column: number;
24
+ endLine: number;
25
+ endColumn: number;
26
+ severity: "error" | "warning" | "info" | "hint";
27
+ message: string;
28
+ rule?: string;
29
+ file: string;
30
+ fixable?: boolean;
31
+ }
32
+
33
+ export abstract class SubprocessClient<T extends Diagnostic> {
34
+ protected available: boolean | null = null;
35
+ protected log: (msg: string) => void;
36
+ private toolName: string;
37
+
38
+ constructor(verbose = false) {
39
+ this.toolName = this.getToolName();
40
+ this.log = verbose
41
+ ? (msg: string) => console.log(`[${this.toolName}] ${msg}`)
42
+ : () => {};
43
+ }
44
+
45
+ /**
46
+ * The name of the CLI tool (used in log messages)
47
+ */
48
+ 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
+ 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
+ protected abstract getSupportedExtensions(): string[];
61
+
62
+ /**
63
+ * Parse CLI output into diagnostics
64
+ */
65
+ protected abstract parseOutput(output: string, filePath: string): T[];
66
+
67
+ /**
68
+ * Check if the CLI tool is available (cached)
69
+ */
70
+ isAvailable(): boolean {
71
+ if (this.available !== null) return this.available;
72
+
73
+ const cmd = this.getCheckCommand();
74
+ try {
75
+ const result = spawnSync(cmd[0], cmd.slice(1), {
76
+ encoding: "utf-8",
77
+ timeout: 10000,
78
+ shell: true,
79
+ });
80
+
81
+ this.available = !result.error && result.status === 0;
82
+ if (this.available) {
83
+ this.log(`${this.toolName} found`);
84
+ } else {
85
+ this.log(`${this.toolName} not available`);
86
+ }
87
+ } catch {
88
+ this.available = false;
89
+ }
90
+
91
+ return this.available;
92
+ }
93
+
94
+ /**
95
+ * Check if a file is supported by this tool
96
+ */
97
+ isSupportedFile(filePath: string): boolean {
98
+ const ext = path.extname(filePath).toLowerCase();
99
+ return this.getSupportedExtensions().includes(ext);
100
+ }
101
+
102
+ /**
103
+ * Run the tool on a file and return diagnostics
104
+ */
105
+ abstract checkFile(filePath: string): T[];
106
+
107
+ /**
108
+ * Run a command and return the result
109
+ */
110
+ protected runCommand(
111
+ cmd: string[],
112
+ options: {
113
+ cwd?: string;
114
+ timeout?: number;
115
+ input?: string;
116
+ } = {},
117
+ ): ReturnType<typeof spawnSync> {
118
+ const { cwd, timeout = 15000, input } = options;
119
+
120
+ try {
121
+ const result = spawnSync(cmd[0], cmd.slice(1), {
122
+ encoding: "utf-8",
123
+ timeout,
124
+ cwd,
125
+ shell: true,
126
+ input,
127
+ });
128
+
129
+ if (result.error) {
130
+ this.log(`Command error: ${result.error.message}`);
131
+ }
132
+
133
+ return result;
134
+ } catch (err: any) {
135
+ this.log(`Command failed: ${err.message}`);
136
+ return {
137
+ error: err,
138
+ status: 1,
139
+ stdout: "",
140
+ stderr: err.message,
141
+ } as unknown as ReturnType<typeof spawnSync>;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Resolve a file path to absolute
147
+ */
148
+ protected resolvePath(filePath: string): string {
149
+ return path.resolve(filePath);
150
+ }
151
+
152
+ /**
153
+ * Check if a file exists
154
+ */
155
+ protected fileExists(filePath: string): boolean {
156
+ return fs.existsSync(filePath);
157
+ }
158
+ }