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.
- package/CHANGELOG.md +27 -1
- package/README.md +19 -13
- package/clients/ast-grep-client.test.ts +8 -21
- package/clients/ast-grep-client.ts +2 -4
- package/clients/biome-client.test.ts +10 -17
- package/clients/complexity-client.test.ts +41 -44
- package/clients/complexity-client.ts +106 -0
- package/clients/dependency-checker.test.ts +10 -23
- package/clients/go-client.test.ts +8 -5
- package/clients/jscpd-client.test.ts +10 -19
- package/clients/jscpd-client.ts +1 -1
- package/clients/knip-client.test.ts +8 -7
- package/clients/metrics-client.test.ts +16 -19
- package/clients/ruff-client.test.ts +12 -17
- package/clients/rust-client.test.ts +8 -5
- package/clients/subprocess-client.ts +158 -0
- package/clients/test-runner-client.test.ts +39 -47
- package/clients/test-utils.ts +31 -0
- package/clients/todo-scanner.test.ts +47 -60
- package/clients/todo-scanner.ts +2 -0
- package/clients/type-coverage-client.test.ts +8 -7
- package/clients/typescript-client.test.ts +12 -21
- package/index.ts +501 -59
- package/package.json +2 -3
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { DependencyChecker } from "./dependency-checker.js";
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
23
|
-
tmpDir =
|
|
11
|
+
client = new DependencyChecker();
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-dep-test-"));
|
|
24
13
|
});
|
|
25
14
|
|
|
26
15
|
afterEach(() => {
|
|
27
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
14
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-go-test-"));
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
afterEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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 =
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-jscpd-test-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
24
17
|
});
|
|
25
18
|
|
|
26
19
|
afterEach(() => {
|
|
27
|
-
|
|
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
|
|
package/clients/jscpd-client.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-knip-test-"));
|
|
14
13
|
});
|
|
15
14
|
|
|
16
15
|
afterEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
14
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-metrics-test-"));
|
|
20
15
|
});
|
|
21
16
|
|
|
22
17
|
afterEach(() => {
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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 =
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-ruff-test-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
20
17
|
});
|
|
21
18
|
|
|
22
19
|
afterEach(() => {
|
|
23
|
-
|
|
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 =
|
|
14
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-rust-test-"));
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
afterEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
}
|