pi-lens 2.1.0 ā 2.2.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 -0
- package/README.md +70 -1
- package/clients/ast-grep-client.js +12 -12
- package/clients/ast-grep-client.ts +21 -11
- package/clients/dispatch/dispatcher.js +2 -2
- package/clients/dispatch/dispatcher.ts +2 -2
- package/clients/dispatch/runners/index.js +3 -1
- package/clients/dispatch/runners/index.ts +3 -1
- package/clients/dispatch/runners/pyright.js +68 -0
- package/clients/dispatch/runners/pyright.test.js +84 -0
- package/clients/dispatch/runners/pyright.test.ts +109 -0
- package/clients/dispatch/runners/pyright.ts +102 -0
- package/clients/dispatch/runners/secrets.js +109 -0
- package/clients/secrets-scanner.js +113 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/secrets-scanner.test.ts +113 -0
- package/clients/secrets-scanner.ts +134 -0
- package/clients/sg-runner.js +15 -2
- package/clients/sg-runner.ts +25 -2
- package/commands/fix.js +48 -50
- package/commands/fix.ts +71 -61
- package/commands/rate.js +285 -0
- package/commands/rate.test.js +119 -0
- package/commands/rate.test.ts +131 -0
- package/commands/rate.ts +348 -0
- package/commands/refactor.js +33 -9
- package/commands/refactor.ts +44 -11
- package/default-architect.yaml +7 -0
- package/index.ts +58 -10
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
- package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
- package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatRateResult } from "./rate.js";
|
|
3
|
+
// Test the formatting functions directly with mock data
|
|
4
|
+
describe("formatRateResult", () => {
|
|
5
|
+
it("should format a visual score breakdown", () => {
|
|
6
|
+
const result = {
|
|
7
|
+
overall: 75,
|
|
8
|
+
categories: [
|
|
9
|
+
{ name: "Type Safety", score: 85, icon: "š·", issues: [] },
|
|
10
|
+
{ name: "Complexity", score: 70, icon: "š§©", issues: [] },
|
|
11
|
+
{ name: "Security", score: 100, icon: "š", issues: [] },
|
|
12
|
+
{ name: "Architecture", score: 85, icon: "šļø", issues: [] },
|
|
13
|
+
{ name: "Dead Code", score: 100, icon: "šļø", issues: [] },
|
|
14
|
+
{ name: "Tests", score: 100, icon: "ā
", issues: [] },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
const output = formatRateResult(result);
|
|
18
|
+
expect(output).toContain("CODE QUALITY SCORE");
|
|
19
|
+
expect(output).toContain("75/100");
|
|
20
|
+
expect(output).toContain("Type Safety");
|
|
21
|
+
expect(output).toContain("Security");
|
|
22
|
+
expect(output).toContain("Tests");
|
|
23
|
+
});
|
|
24
|
+
it("should show correct grade for A", () => {
|
|
25
|
+
const result = {
|
|
26
|
+
overall: 95,
|
|
27
|
+
categories: Array(6).fill({
|
|
28
|
+
name: "Test",
|
|
29
|
+
score: 95,
|
|
30
|
+
icon: "ā
",
|
|
31
|
+
issues: [],
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
const output = formatRateResult(result);
|
|
35
|
+
expect(output).toContain("A");
|
|
36
|
+
});
|
|
37
|
+
it("should show correct grade for B", () => {
|
|
38
|
+
const result = {
|
|
39
|
+
overall: 85,
|
|
40
|
+
categories: Array(6).fill({
|
|
41
|
+
name: "Test",
|
|
42
|
+
score: 85,
|
|
43
|
+
icon: "ā
",
|
|
44
|
+
issues: [],
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
const output = formatRateResult(result);
|
|
48
|
+
expect(output).toContain("B");
|
|
49
|
+
});
|
|
50
|
+
it("should show correct grade for C", () => {
|
|
51
|
+
const result = {
|
|
52
|
+
overall: 75,
|
|
53
|
+
categories: Array(6).fill({
|
|
54
|
+
name: "Test",
|
|
55
|
+
score: 75,
|
|
56
|
+
icon: "ā
",
|
|
57
|
+
issues: [],
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
const output = formatRateResult(result);
|
|
61
|
+
expect(output).toContain("C");
|
|
62
|
+
});
|
|
63
|
+
it("should show issues section when there are problems", () => {
|
|
64
|
+
const result = {
|
|
65
|
+
overall: 50,
|
|
66
|
+
categories: [
|
|
67
|
+
{
|
|
68
|
+
name: "Type Safety",
|
|
69
|
+
score: 50,
|
|
70
|
+
icon: "š·",
|
|
71
|
+
issues: ["50 untyped identifiers"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Complexity",
|
|
75
|
+
score: 50,
|
|
76
|
+
icon: "š§©",
|
|
77
|
+
issues: ["High complexity: foo.ts"],
|
|
78
|
+
},
|
|
79
|
+
{ name: "Security", score: 100, icon: "š", issues: [] },
|
|
80
|
+
{ name: "Architecture", score: 100, icon: "šļø", issues: [] },
|
|
81
|
+
{ name: "Dead Code", score: 100, icon: "šļø", issues: [] },
|
|
82
|
+
{ name: "Tests", score: 100, icon: "ā
", issues: [] },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
const output = formatRateResult(result);
|
|
86
|
+
expect(output).toContain("Issues to address");
|
|
87
|
+
expect(output).toContain("Type Safety");
|
|
88
|
+
expect(output).toContain("/lens-booboo");
|
|
89
|
+
});
|
|
90
|
+
it("should not show issues section when clean", () => {
|
|
91
|
+
const result = {
|
|
92
|
+
overall: 100,
|
|
93
|
+
categories: Array(6).fill({
|
|
94
|
+
name: "Test",
|
|
95
|
+
score: 100,
|
|
96
|
+
icon: "ā
",
|
|
97
|
+
issues: [],
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
const output = formatRateResult(result);
|
|
101
|
+
expect(output).not.toContain("Issues to address");
|
|
102
|
+
});
|
|
103
|
+
it("should use colored bars based on score", () => {
|
|
104
|
+
const resultHigh = {
|
|
105
|
+
overall: 90,
|
|
106
|
+
categories: [{ name: "Test", score: 85, icon: "ā
", issues: [] }],
|
|
107
|
+
};
|
|
108
|
+
const resultLow = {
|
|
109
|
+
overall: 50,
|
|
110
|
+
categories: [{ name: "Test", score: 50, icon: "ā
", issues: [] }],
|
|
111
|
+
};
|
|
112
|
+
const outputHigh = formatRateResult(resultHigh);
|
|
113
|
+
const outputLow = formatRateResult(resultLow);
|
|
114
|
+
// High score should have green squares
|
|
115
|
+
expect(outputHigh).toContain("š©");
|
|
116
|
+
// Low score should have red squares
|
|
117
|
+
expect(outputLow).toContain("š„");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatRateResult } from "./rate.js";
|
|
3
|
+
|
|
4
|
+
// Test the formatting functions directly with mock data
|
|
5
|
+
|
|
6
|
+
describe("formatRateResult", () => {
|
|
7
|
+
it("should format a visual score breakdown", () => {
|
|
8
|
+
const result = {
|
|
9
|
+
overall: 75,
|
|
10
|
+
categories: [
|
|
11
|
+
{ name: "Type Safety", score: 85, icon: "š·", issues: [] },
|
|
12
|
+
{ name: "Complexity", score: 70, icon: "š§©", issues: [] },
|
|
13
|
+
{ name: "Security", score: 100, icon: "š", issues: [] },
|
|
14
|
+
{ name: "Architecture", score: 85, icon: "šļø", issues: [] },
|
|
15
|
+
{ name: "Dead Code", score: 100, icon: "šļø", issues: [] },
|
|
16
|
+
{ name: "Tests", score: 100, icon: "ā
", issues: [] },
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const output = formatRateResult(result);
|
|
21
|
+
|
|
22
|
+
expect(output).toContain("CODE QUALITY SCORE");
|
|
23
|
+
expect(output).toContain("75/100");
|
|
24
|
+
expect(output).toContain("Type Safety");
|
|
25
|
+
expect(output).toContain("Security");
|
|
26
|
+
expect(output).toContain("Tests");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should show correct grade for A", () => {
|
|
30
|
+
const result = {
|
|
31
|
+
overall: 95,
|
|
32
|
+
categories: Array(6).fill({
|
|
33
|
+
name: "Test",
|
|
34
|
+
score: 95,
|
|
35
|
+
icon: "ā
",
|
|
36
|
+
issues: [],
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
const output = formatRateResult(result);
|
|
40
|
+
expect(output).toContain("A");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should show correct grade for B", () => {
|
|
44
|
+
const result = {
|
|
45
|
+
overall: 85,
|
|
46
|
+
categories: Array(6).fill({
|
|
47
|
+
name: "Test",
|
|
48
|
+
score: 85,
|
|
49
|
+
icon: "ā
",
|
|
50
|
+
issues: [],
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
const output = formatRateResult(result);
|
|
54
|
+
expect(output).toContain("B");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should show correct grade for C", () => {
|
|
58
|
+
const result = {
|
|
59
|
+
overall: 75,
|
|
60
|
+
categories: Array(6).fill({
|
|
61
|
+
name: "Test",
|
|
62
|
+
score: 75,
|
|
63
|
+
icon: "ā
",
|
|
64
|
+
issues: [],
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
const output = formatRateResult(result);
|
|
68
|
+
expect(output).toContain("C");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should show issues section when there are problems", () => {
|
|
72
|
+
const result = {
|
|
73
|
+
overall: 50,
|
|
74
|
+
categories: [
|
|
75
|
+
{
|
|
76
|
+
name: "Type Safety",
|
|
77
|
+
score: 50,
|
|
78
|
+
icon: "š·",
|
|
79
|
+
issues: ["50 untyped identifiers"],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "Complexity",
|
|
83
|
+
score: 50,
|
|
84
|
+
icon: "š§©",
|
|
85
|
+
issues: ["High complexity: foo.ts"],
|
|
86
|
+
},
|
|
87
|
+
{ name: "Security", score: 100, icon: "š", issues: [] },
|
|
88
|
+
{ name: "Architecture", score: 100, icon: "šļø", issues: [] },
|
|
89
|
+
{ name: "Dead Code", score: 100, icon: "šļø", issues: [] },
|
|
90
|
+
{ name: "Tests", score: 100, icon: "ā
", issues: [] },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const output = formatRateResult(result);
|
|
94
|
+
expect(output).toContain("Issues to address");
|
|
95
|
+
expect(output).toContain("Type Safety");
|
|
96
|
+
expect(output).toContain("/lens-booboo");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should not show issues section when clean", () => {
|
|
100
|
+
const result = {
|
|
101
|
+
overall: 100,
|
|
102
|
+
categories: Array(6).fill({
|
|
103
|
+
name: "Test",
|
|
104
|
+
score: 100,
|
|
105
|
+
icon: "ā
",
|
|
106
|
+
issues: [],
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
const output = formatRateResult(result);
|
|
110
|
+
expect(output).not.toContain("Issues to address");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should use colored bars based on score", () => {
|
|
114
|
+
const resultHigh = {
|
|
115
|
+
overall: 90,
|
|
116
|
+
categories: [{ name: "Test", score: 85, icon: "ā
", issues: [] }],
|
|
117
|
+
};
|
|
118
|
+
const resultLow = {
|
|
119
|
+
overall: 50,
|
|
120
|
+
categories: [{ name: "Test", score: 50, icon: "ā
", issues: [] }],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const outputHigh = formatRateResult(resultHigh);
|
|
124
|
+
const outputLow = formatRateResult(resultLow);
|
|
125
|
+
|
|
126
|
+
// High score should have green squares
|
|
127
|
+
expect(outputHigh).toContain("š©");
|
|
128
|
+
// Low score should have red squares
|
|
129
|
+
expect(outputLow).toContain("š„");
|
|
130
|
+
});
|
|
131
|
+
});
|
package/commands/rate.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /lens-rate command
|
|
3
|
+
*
|
|
4
|
+
* Provides a visual scoring breakdown of code quality across multiple dimensions.
|
|
5
|
+
* Uses existing scan data to calculate scores.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as childProcess from "node:child_process";
|
|
9
|
+
import * as nodeFs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { ArchitectClient } from "../clients/architect-client.js";
|
|
13
|
+
import type { ComplexityClient } from "../clients/complexity-client.js";
|
|
14
|
+
import type { KnipClient } from "../clients/knip-client.js";
|
|
15
|
+
import { getSourceFiles } from "../clients/scan-utils.js";
|
|
16
|
+
import type { TypeCoverageClient } from "../clients/type-coverage-client.js";
|
|
17
|
+
|
|
18
|
+
interface CategoryScore {
|
|
19
|
+
name: string;
|
|
20
|
+
score: number; // 0-100
|
|
21
|
+
icon: string;
|
|
22
|
+
issues: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface RateResult {
|
|
26
|
+
overall: number;
|
|
27
|
+
categories: CategoryScore[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ScanClients {
|
|
31
|
+
complexity: ComplexityClient;
|
|
32
|
+
knip: KnipClient;
|
|
33
|
+
typeCoverage: TypeCoverageClient;
|
|
34
|
+
architect: ArchitectClient;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run all scans and calculate scores
|
|
39
|
+
*/
|
|
40
|
+
export async function gatherScores(
|
|
41
|
+
targetPath: string,
|
|
42
|
+
clients: ScanClients,
|
|
43
|
+
): Promise<RateResult> {
|
|
44
|
+
const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
|
|
45
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
46
|
+
const categories: CategoryScore[] = [];
|
|
47
|
+
|
|
48
|
+
// āāā Type Safety āāā
|
|
49
|
+
let typeCoverageScore = 100;
|
|
50
|
+
const typeIssues: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (clients.typeCoverage.isAvailable()) {
|
|
53
|
+
const result = clients.typeCoverage.scan(targetPath);
|
|
54
|
+
if (result.success) {
|
|
55
|
+
typeCoverageScore = result.percentage;
|
|
56
|
+
if (result.percentage < 90) {
|
|
57
|
+
typeIssues.push(`${result.total - result.typed} untyped identifiers`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
categories.push({
|
|
62
|
+
name: "Type Safety",
|
|
63
|
+
score: Math.round(typeCoverageScore),
|
|
64
|
+
icon: "š·",
|
|
65
|
+
issues: typeIssues,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// āāā Complexity āāā
|
|
69
|
+
let complexityScore = 100;
|
|
70
|
+
const complexityIssues: string[] = [];
|
|
71
|
+
|
|
72
|
+
let totalScore = 0;
|
|
73
|
+
let fileCount = 0;
|
|
74
|
+
let worstFile = "";
|
|
75
|
+
let worstScore = 100;
|
|
76
|
+
|
|
77
|
+
for (const file of files.slice(0, 50)) {
|
|
78
|
+
if (clients.complexity.isSupportedFile(file)) {
|
|
79
|
+
const metrics = clients.complexity.analyzeFile(file);
|
|
80
|
+
if (metrics) {
|
|
81
|
+
totalScore += metrics.maintainabilityIndex;
|
|
82
|
+
fileCount++;
|
|
83
|
+
if (metrics.maintainabilityIndex < worstScore) {
|
|
84
|
+
worstScore = metrics.maintainabilityIndex;
|
|
85
|
+
worstFile = path.basename(file);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (fileCount > 0) {
|
|
91
|
+
complexityScore = totalScore / fileCount;
|
|
92
|
+
if (complexityScore < 70) {
|
|
93
|
+
complexityIssues.push(`High complexity: ${worstFile}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
categories.push({
|
|
97
|
+
name: "Complexity",
|
|
98
|
+
score: Math.round(complexityScore),
|
|
99
|
+
icon: "š§©",
|
|
100
|
+
issues: complexityIssues,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// āāā Security āāā
|
|
104
|
+
let securityScore = 100;
|
|
105
|
+
const securityIssues: string[] = [];
|
|
106
|
+
let secretsFound = 0;
|
|
107
|
+
|
|
108
|
+
// Check for secrets in source files
|
|
109
|
+
const secretPatterns = [
|
|
110
|
+
{ name: "API Key (sk-)", pattern: /sk-[a-zA-Z0-9]{20,}/ },
|
|
111
|
+
{ name: "GitHub Token", pattern: /ghp_[a-zA-Z0-9]{36}/ },
|
|
112
|
+
{ name: "AWS Key", pattern: /AKIA[A-Z0-9]{16}/ },
|
|
113
|
+
{ name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9]{20,}/ },
|
|
114
|
+
{ name: "OpenAI Key", pattern: /sk-proj-[a-zA-Z0-9]{20,}/ },
|
|
115
|
+
{
|
|
116
|
+
name: "Private Key",
|
|
117
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const file of files.slice(0, 100)) {
|
|
122
|
+
try {
|
|
123
|
+
const content = nodeFs.readFileSync(file, "utf-8");
|
|
124
|
+
for (const line of content.split("\n")) {
|
|
125
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("#"))
|
|
126
|
+
continue;
|
|
127
|
+
for (const { name, pattern } of secretPatterns) {
|
|
128
|
+
if (pattern.test(line)) {
|
|
129
|
+
secretsFound++;
|
|
130
|
+
if (securityIssues.length < 3) {
|
|
131
|
+
securityIssues.push(`${name} in ${path.basename(file)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Skip unreadable files
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
securityScore = Math.max(0, 100 - secretsFound * 15);
|
|
141
|
+
categories.push({
|
|
142
|
+
name: "Security",
|
|
143
|
+
score: securityScore,
|
|
144
|
+
icon: "š",
|
|
145
|
+
issues: securityIssues,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// āāā Architecture āāā
|
|
149
|
+
let archScore = 100;
|
|
150
|
+
const archIssues: string[] = [];
|
|
151
|
+
|
|
152
|
+
clients.architect.loadConfig(targetPath);
|
|
153
|
+
if (clients.architect.hasConfig()) {
|
|
154
|
+
let archViolations = 0;
|
|
155
|
+
const scanDir = (dir: string) => {
|
|
156
|
+
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
|
|
157
|
+
const full = path.join(dir, entry.name);
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
if (
|
|
160
|
+
[
|
|
161
|
+
"node_modules",
|
|
162
|
+
".git",
|
|
163
|
+
"dist",
|
|
164
|
+
"build",
|
|
165
|
+
".next",
|
|
166
|
+
".pi-lens",
|
|
167
|
+
].includes(entry.name)
|
|
168
|
+
)
|
|
169
|
+
continue;
|
|
170
|
+
scanDir(full);
|
|
171
|
+
} else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
|
|
172
|
+
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
173
|
+
const content = nodeFs.readFileSync(full, "utf-8");
|
|
174
|
+
const violations = clients.architect.checkFile(relPath, content);
|
|
175
|
+
archViolations += violations.length;
|
|
176
|
+
if (violations.length > 0 && archIssues.length < 3) {
|
|
177
|
+
archIssues.push(`${violations.length} in ${path.basename(full)}`);
|
|
178
|
+
}
|
|
179
|
+
const sizeV = clients.architect.checkFileSize(
|
|
180
|
+
relPath,
|
|
181
|
+
content.split("\n").length,
|
|
182
|
+
);
|
|
183
|
+
if (sizeV) archViolations++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
scanDir(targetPath);
|
|
188
|
+
archScore = Math.max(0, 100 - archViolations * 10);
|
|
189
|
+
}
|
|
190
|
+
categories.push({
|
|
191
|
+
name: "Architecture",
|
|
192
|
+
score: archScore,
|
|
193
|
+
icon: "šļø",
|
|
194
|
+
issues: archIssues,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// āāā Dead Code āāā
|
|
198
|
+
let deadCodeScore = 100;
|
|
199
|
+
const deadCodeIssues: string[] = [];
|
|
200
|
+
|
|
201
|
+
if (clients.knip.isAvailable()) {
|
|
202
|
+
const result = clients.knip.analyze(targetPath);
|
|
203
|
+
if (result.success) {
|
|
204
|
+
const unusedExports = result.unusedExports.length;
|
|
205
|
+
const unusedFiles = result.unusedFiles.length;
|
|
206
|
+
const total = unusedExports + unusedFiles;
|
|
207
|
+
deadCodeScore = Math.max(0, 100 - total * 3);
|
|
208
|
+
if (unusedExports > 0) {
|
|
209
|
+
deadCodeIssues.push(`${unusedExports} unused export(s)`);
|
|
210
|
+
}
|
|
211
|
+
if (unusedFiles > 0) {
|
|
212
|
+
deadCodeIssues.push(`${unusedFiles} unused file(s)`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
categories.push({
|
|
217
|
+
name: "Dead Code",
|
|
218
|
+
score: deadCodeScore,
|
|
219
|
+
icon: "šļø",
|
|
220
|
+
issues: deadCodeIssues,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// āāā Tests āāā
|
|
224
|
+
let testScore = 100;
|
|
225
|
+
const testIssues: string[] = [];
|
|
226
|
+
|
|
227
|
+
// Quick test run
|
|
228
|
+
try {
|
|
229
|
+
const testResult = childProcess.spawnSync(
|
|
230
|
+
"npx",
|
|
231
|
+
["vitest", "run", "--reporter=basic"],
|
|
232
|
+
{
|
|
233
|
+
encoding: "utf-8",
|
|
234
|
+
timeout: 60000,
|
|
235
|
+
shell: true,
|
|
236
|
+
cwd: targetPath,
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
if (testResult.status !== 0) {
|
|
240
|
+
const output = (testResult.stdout || "") + (testResult.stderr || "");
|
|
241
|
+
if (output.includes("failed")) {
|
|
242
|
+
// Count failing tests
|
|
243
|
+
const failMatch = output.match(/(\d+) failed/);
|
|
244
|
+
testScore = 50;
|
|
245
|
+
testIssues.push(
|
|
246
|
+
failMatch ? `${failMatch[1]} test(s) failing` : "Some tests failing",
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
testScore = 70;
|
|
250
|
+
testIssues.push("Tests timed out or errored");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
testScore = 70;
|
|
255
|
+
testIssues.push("Could not run tests");
|
|
256
|
+
}
|
|
257
|
+
categories.push({
|
|
258
|
+
name: "Tests",
|
|
259
|
+
score: testScore,
|
|
260
|
+
icon: "ā
",
|
|
261
|
+
issues: testIssues,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// āāā Calculate Overall āāā
|
|
265
|
+
const overall = Math.round(
|
|
266
|
+
categories.reduce((sum, c) => sum + c.score, 0) / categories.length,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return { overall, categories };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Format score as a bar
|
|
274
|
+
*/
|
|
275
|
+
function scoreBar(score: number, width = 10): string {
|
|
276
|
+
const filled = Math.round((score / 100) * width);
|
|
277
|
+
const empty = width - filled;
|
|
278
|
+
const color = score >= 80 ? "š©" : score >= 60 ? "šØ" : "š„";
|
|
279
|
+
return color.repeat(filled) + "ā¬".repeat(empty);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get grade from score
|
|
284
|
+
*/
|
|
285
|
+
function getGrade(score: number): string {
|
|
286
|
+
if (score >= 90) return "A";
|
|
287
|
+
if (score >= 80) return "B";
|
|
288
|
+
if (score >= 70) return "C";
|
|
289
|
+
if (score >= 60) return "D";
|
|
290
|
+
return "F";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Format rate result for terminal
|
|
295
|
+
*/
|
|
296
|
+
export function formatRateResult(result: RateResult): string {
|
|
297
|
+
const lines: string[] = [];
|
|
298
|
+
|
|
299
|
+
lines.push("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
300
|
+
const gradeText = ` (${getGrade(result.overall)})`;
|
|
301
|
+
const scoreText = `š CODE QUALITY SCORE: ${result.overall}/100${gradeText}`;
|
|
302
|
+
const padding = Math.max(0, 55 - scoreText.length);
|
|
303
|
+
lines.push(`ā ${scoreText}${" ".repeat(padding)}ā`);
|
|
304
|
+
lines.push("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤");
|
|
305
|
+
|
|
306
|
+
for (const cat of result.categories) {
|
|
307
|
+
const name = cat.name.padEnd(14);
|
|
308
|
+
const bar = scoreBar(cat.score);
|
|
309
|
+
const score = String(cat.score).padStart(3);
|
|
310
|
+
lines.push(`ā ${cat.icon} ${name} ${bar} ${score} ā`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
lines.push("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
314
|
+
|
|
315
|
+
// Show issues if any
|
|
316
|
+
const allIssues = result.categories
|
|
317
|
+
.filter((c) => c.issues.length > 0)
|
|
318
|
+
.flatMap((c) => c.issues.map((i) => `${c.icon} ${c.name}: ${i}`));
|
|
319
|
+
|
|
320
|
+
if (allIssues.length > 0) {
|
|
321
|
+
lines.push("");
|
|
322
|
+
lines.push("Issues to address:");
|
|
323
|
+
for (const issue of allIssues.slice(0, 5)) {
|
|
324
|
+
lines.push(` ⢠${issue}`);
|
|
325
|
+
}
|
|
326
|
+
if (allIssues.length > 5) {
|
|
327
|
+
lines.push(` ... and ${allIssues.length - 5} more`);
|
|
328
|
+
}
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("š” Run /lens-booboo for full details");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle /lens-rate command
|
|
338
|
+
*/
|
|
339
|
+
export async function handleRate(
|
|
340
|
+
args: string,
|
|
341
|
+
ctx: ExtensionContext,
|
|
342
|
+
clients: ScanClients,
|
|
343
|
+
): Promise<string> {
|
|
344
|
+
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
345
|
+
ctx.ui.notify("š Calculating code quality scores...", "info");
|
|
346
|
+
const result = await gatherScores(targetPath, clients);
|
|
347
|
+
return formatRateResult(result);
|
|
348
|
+
}
|
package/commands/refactor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodeFs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { createAutoLoop } from "../clients/auto-loop.js";
|
|
4
|
-
import {
|
|
4
|
+
import { scanArchitectViolations, scanComplexityMetrics, scanSkipViolations, scoreFiles, } from "../clients/scan-architectural-debt.js";
|
|
5
5
|
// Auto-loop singleton for refactor command (initialized at module load)
|
|
6
6
|
let refactorLoop = null;
|
|
7
7
|
export function initRefactorLoop(pi) {
|
|
@@ -49,15 +49,28 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
|
|
|
49
49
|
ctx.ui.notify("ā
No architectural debt found ā codebase is clean.", "info");
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
// --- Write ranked list to TSV for agent reference ---
|
|
53
|
+
const reportDir = path.join(process.cwd(), ".pi-lens", "reports");
|
|
54
|
+
nodeFs.mkdirSync(reportDir, { recursive: true });
|
|
55
|
+
const reportPath = path.join(reportDir, "refactor-ranked.tsv");
|
|
56
|
+
const tsvRows = [
|
|
57
|
+
"rank\tfile\tscore\tmi\tcognitive\tnesting\tviolations",
|
|
58
|
+
];
|
|
59
|
+
scored.slice(0, 50).forEach((f, i) => {
|
|
60
|
+
const m = metricsByFile.get(f.file);
|
|
61
|
+
const skipCount = skipByFile.get(f.file)?.length ?? 0;
|
|
62
|
+
const archCount = architectViolations?.get(f.file)?.length ?? 0;
|
|
63
|
+
const totalViolations = skipCount + archCount;
|
|
64
|
+
const relPath = path.relative(targetPath, f.file).replace(/\\/g, "/");
|
|
65
|
+
tsvRows.push(`${i + 1}\t${relPath}\t${f.score}\t${m?.mi.toFixed(1) ?? "-"}\t${m?.cognitive ?? "-"}\t${m?.nesting ?? "-"}\t${totalViolations}`);
|
|
66
|
+
});
|
|
67
|
+
nodeFs.writeFileSync(reportPath, tsvRows.join("\n"), "utf-8");
|
|
68
|
+
// --- Current worst offender ---
|
|
52
69
|
const { file: worstFile, score } = scored[0];
|
|
53
70
|
const relFile = path.relative(targetPath, worstFile).replace(/\\/g, "/");
|
|
54
71
|
const issues = skipByFile.get(worstFile) ?? [];
|
|
55
72
|
const metrics = metricsByFile.get(worstFile);
|
|
56
73
|
const archIssues = architectViolations.get(worstFile) ?? [];
|
|
57
|
-
const snippetResult = issues.length > 0 ? extractCodeSnippet(worstFile, issues[0].line) : null;
|
|
58
|
-
const snippet = snippetResult?.snippet ?? "";
|
|
59
|
-
const snippetStart = snippetResult?.start ?? 1;
|
|
60
|
-
const snippetEnd = snippetResult?.end ?? 1;
|
|
61
74
|
const ruleGroups = new Map();
|
|
62
75
|
for (const i of issues)
|
|
63
76
|
ruleGroups.set(i.rule, (ruleGroups.get(i.rule) ?? 0) + 1);
|
|
@@ -68,6 +81,19 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
|
|
|
68
81
|
const metricsSummary = metrics
|
|
69
82
|
? `MI: ${metrics.mi.toFixed(1)}, Cognitive: ${metrics.cognitive}, Nesting: ${metrics.nesting}`
|
|
70
83
|
: "";
|
|
84
|
+
// First violation line for quick reference
|
|
85
|
+
const firstViolationLine = issues.length > 0 ? issues[0].line : null;
|
|
86
|
+
// --- Compact terminal summary ---
|
|
87
|
+
const topFiles = scored
|
|
88
|
+
.slice(0, 5)
|
|
89
|
+
.map((f, i) => {
|
|
90
|
+
const name = path.relative(targetPath, f.file).replace(/\\/g, "/");
|
|
91
|
+
return ` ${i + 1}. ${name} (score: ${f.score})`;
|
|
92
|
+
})
|
|
93
|
+
.join("\n");
|
|
94
|
+
ctx.ui.notify(`šļø Worst: ${relFile} (score: ${score}) ā ${scored.length} files with debt`, "info");
|
|
95
|
+
console.log(`\nš Top ${Math.min(scored.length, 5)} worst offenders:\n${topFiles}\nš Full ranked list: .pi-lens/reports/refactor-ranked.tsv\n`);
|
|
96
|
+
// --- Steer message for agent ---
|
|
71
97
|
const steer = [
|
|
72
98
|
`šļø BOOBOO REFACTOR ā worst offender identified`,
|
|
73
99
|
"",
|
|
@@ -79,11 +105,9 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
|
|
|
79
105
|
archIssues.length > 0
|
|
80
106
|
? `**Architectural rules violated**:\n${archSummary}`
|
|
81
107
|
: "",
|
|
108
|
+
firstViolationLine ? `First violation at line ${firstViolationLine}` : "",
|
|
82
109
|
"",
|
|
83
|
-
|
|
84
|
-
"```typescript",
|
|
85
|
-
snippet,
|
|
86
|
-
"```",
|
|
110
|
+
`š Full details: .pi-lens/reports/refactor-ranked.tsv ā read \`${relFile}\` when ready`,
|
|
87
111
|
"",
|
|
88
112
|
"**Your job**:",
|
|
89
113
|
"1. Analyze this code ā what's the most impactful refactoring for this file?",
|