pi-lens 2.1.1 → 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.
@@ -0,0 +1,285 @@
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
+ import * as childProcess from "node:child_process";
8
+ import * as nodeFs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { getSourceFiles } from "../clients/scan-utils.js";
11
+ /**
12
+ * Run all scans and calculate scores
13
+ */
14
+ export async function gatherScores(targetPath, clients) {
15
+ const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
16
+ const files = getSourceFiles(targetPath, isTsProject);
17
+ const categories = [];
18
+ // ─── Type Safety ───
19
+ let typeCoverageScore = 100;
20
+ const typeIssues = [];
21
+ if (clients.typeCoverage.isAvailable()) {
22
+ const result = clients.typeCoverage.scan(targetPath);
23
+ if (result.success) {
24
+ typeCoverageScore = result.percentage;
25
+ if (result.percentage < 90) {
26
+ typeIssues.push(`${result.total - result.typed} untyped identifiers`);
27
+ }
28
+ }
29
+ }
30
+ categories.push({
31
+ name: "Type Safety",
32
+ score: Math.round(typeCoverageScore),
33
+ icon: "🔷",
34
+ issues: typeIssues,
35
+ });
36
+ // ─── Complexity ───
37
+ let complexityScore = 100;
38
+ const complexityIssues = [];
39
+ let totalScore = 0;
40
+ let fileCount = 0;
41
+ let worstFile = "";
42
+ let worstScore = 100;
43
+ for (const file of files.slice(0, 50)) {
44
+ if (clients.complexity.isSupportedFile(file)) {
45
+ const metrics = clients.complexity.analyzeFile(file);
46
+ if (metrics) {
47
+ totalScore += metrics.maintainabilityIndex;
48
+ fileCount++;
49
+ if (metrics.maintainabilityIndex < worstScore) {
50
+ worstScore = metrics.maintainabilityIndex;
51
+ worstFile = path.basename(file);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ if (fileCount > 0) {
57
+ complexityScore = totalScore / fileCount;
58
+ if (complexityScore < 70) {
59
+ complexityIssues.push(`High complexity: ${worstFile}`);
60
+ }
61
+ }
62
+ categories.push({
63
+ name: "Complexity",
64
+ score: Math.round(complexityScore),
65
+ icon: "🧩",
66
+ issues: complexityIssues,
67
+ });
68
+ // ─── Security ───
69
+ let securityScore = 100;
70
+ const securityIssues = [];
71
+ let secretsFound = 0;
72
+ // Check for secrets in source files
73
+ const secretPatterns = [
74
+ { name: "API Key (sk-)", pattern: /sk-[a-zA-Z0-9]{20,}/ },
75
+ { name: "GitHub Token", pattern: /ghp_[a-zA-Z0-9]{36}/ },
76
+ { name: "AWS Key", pattern: /AKIA[A-Z0-9]{16}/ },
77
+ { name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9]{20,}/ },
78
+ { name: "OpenAI Key", pattern: /sk-proj-[a-zA-Z0-9]{20,}/ },
79
+ {
80
+ name: "Private Key",
81
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
82
+ },
83
+ ];
84
+ for (const file of files.slice(0, 100)) {
85
+ try {
86
+ const content = nodeFs.readFileSync(file, "utf-8");
87
+ for (const line of content.split("\n")) {
88
+ if (line.trim().startsWith("//") || line.trim().startsWith("#"))
89
+ continue;
90
+ for (const { name, pattern } of secretPatterns) {
91
+ if (pattern.test(line)) {
92
+ secretsFound++;
93
+ if (securityIssues.length < 3) {
94
+ securityIssues.push(`${name} in ${path.basename(file)}`);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ // Skip unreadable files
102
+ }
103
+ }
104
+ securityScore = Math.max(0, 100 - secretsFound * 15);
105
+ categories.push({
106
+ name: "Security",
107
+ score: securityScore,
108
+ icon: "🔒",
109
+ issues: securityIssues,
110
+ });
111
+ // ─── Architecture ───
112
+ let archScore = 100;
113
+ const archIssues = [];
114
+ clients.architect.loadConfig(targetPath);
115
+ if (clients.architect.hasConfig()) {
116
+ let archViolations = 0;
117
+ const scanDir = (dir) => {
118
+ for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
119
+ const full = path.join(dir, entry.name);
120
+ if (entry.isDirectory()) {
121
+ if ([
122
+ "node_modules",
123
+ ".git",
124
+ "dist",
125
+ "build",
126
+ ".next",
127
+ ".pi-lens",
128
+ ].includes(entry.name))
129
+ continue;
130
+ scanDir(full);
131
+ }
132
+ else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
133
+ const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
134
+ const content = nodeFs.readFileSync(full, "utf-8");
135
+ const violations = clients.architect.checkFile(relPath, content);
136
+ archViolations += violations.length;
137
+ if (violations.length > 0 && archIssues.length < 3) {
138
+ archIssues.push(`${violations.length} in ${path.basename(full)}`);
139
+ }
140
+ const sizeV = clients.architect.checkFileSize(relPath, content.split("\n").length);
141
+ if (sizeV)
142
+ archViolations++;
143
+ }
144
+ }
145
+ };
146
+ scanDir(targetPath);
147
+ archScore = Math.max(0, 100 - archViolations * 10);
148
+ }
149
+ categories.push({
150
+ name: "Architecture",
151
+ score: archScore,
152
+ icon: "🏗️",
153
+ issues: archIssues,
154
+ });
155
+ // ─── Dead Code ───
156
+ let deadCodeScore = 100;
157
+ const deadCodeIssues = [];
158
+ if (clients.knip.isAvailable()) {
159
+ const result = clients.knip.analyze(targetPath);
160
+ if (result.success) {
161
+ const unusedExports = result.unusedExports.length;
162
+ const unusedFiles = result.unusedFiles.length;
163
+ const total = unusedExports + unusedFiles;
164
+ deadCodeScore = Math.max(0, 100 - total * 3);
165
+ if (unusedExports > 0) {
166
+ deadCodeIssues.push(`${unusedExports} unused export(s)`);
167
+ }
168
+ if (unusedFiles > 0) {
169
+ deadCodeIssues.push(`${unusedFiles} unused file(s)`);
170
+ }
171
+ }
172
+ }
173
+ categories.push({
174
+ name: "Dead Code",
175
+ score: deadCodeScore,
176
+ icon: "🗑️",
177
+ issues: deadCodeIssues,
178
+ });
179
+ // ─── Tests ───
180
+ let testScore = 100;
181
+ const testIssues = [];
182
+ // Quick test run
183
+ try {
184
+ const testResult = childProcess.spawnSync("npx", ["vitest", "run", "--reporter=basic"], {
185
+ encoding: "utf-8",
186
+ timeout: 60000,
187
+ shell: true,
188
+ cwd: targetPath,
189
+ });
190
+ if (testResult.status !== 0) {
191
+ const output = (testResult.stdout || "") + (testResult.stderr || "");
192
+ if (output.includes("failed")) {
193
+ // Count failing tests
194
+ const failMatch = output.match(/(\d+) failed/);
195
+ testScore = 50;
196
+ testIssues.push(failMatch ? `${failMatch[1]} test(s) failing` : "Some tests failing");
197
+ }
198
+ else {
199
+ testScore = 70;
200
+ testIssues.push("Tests timed out or errored");
201
+ }
202
+ }
203
+ }
204
+ catch {
205
+ testScore = 70;
206
+ testIssues.push("Could not run tests");
207
+ }
208
+ categories.push({
209
+ name: "Tests",
210
+ score: testScore,
211
+ icon: "✅",
212
+ issues: testIssues,
213
+ });
214
+ // ─── Calculate Overall ───
215
+ const overall = Math.round(categories.reduce((sum, c) => sum + c.score, 0) / categories.length);
216
+ return { overall, categories };
217
+ }
218
+ /**
219
+ * Format score as a bar
220
+ */
221
+ function scoreBar(score, width = 10) {
222
+ const filled = Math.round((score / 100) * width);
223
+ const empty = width - filled;
224
+ const color = score >= 80 ? "🟩" : score >= 60 ? "🟨" : "🟥";
225
+ return color.repeat(filled) + "⬜".repeat(empty);
226
+ }
227
+ /**
228
+ * Get grade from score
229
+ */
230
+ function getGrade(score) {
231
+ if (score >= 90)
232
+ return "A";
233
+ if (score >= 80)
234
+ return "B";
235
+ if (score >= 70)
236
+ return "C";
237
+ if (score >= 60)
238
+ return "D";
239
+ return "F";
240
+ }
241
+ /**
242
+ * Format rate result for terminal
243
+ */
244
+ export function formatRateResult(result) {
245
+ const lines = [];
246
+ lines.push("┌─────────────────────────────────────────────────────────┐");
247
+ const gradeText = ` (${getGrade(result.overall)})`;
248
+ const scoreText = `📊 CODE QUALITY SCORE: ${result.overall}/100${gradeText}`;
249
+ const padding = Math.max(0, 55 - scoreText.length);
250
+ lines.push(`│ ${scoreText}${" ".repeat(padding)}│`);
251
+ lines.push("├─────────────────────────────────────────────────────────┤");
252
+ for (const cat of result.categories) {
253
+ const name = cat.name.padEnd(14);
254
+ const bar = scoreBar(cat.score);
255
+ const score = String(cat.score).padStart(3);
256
+ lines.push(`│ ${cat.icon} ${name} ${bar} ${score} │`);
257
+ }
258
+ lines.push("└─────────────────────────────────────────────────────────┘");
259
+ // Show issues if any
260
+ const allIssues = result.categories
261
+ .filter((c) => c.issues.length > 0)
262
+ .flatMap((c) => c.issues.map((i) => `${c.icon} ${c.name}: ${i}`));
263
+ if (allIssues.length > 0) {
264
+ lines.push("");
265
+ lines.push("Issues to address:");
266
+ for (const issue of allIssues.slice(0, 5)) {
267
+ lines.push(` • ${issue}`);
268
+ }
269
+ if (allIssues.length > 5) {
270
+ lines.push(` ... and ${allIssues.length - 5} more`);
271
+ }
272
+ lines.push("");
273
+ lines.push("💡 Run /lens-booboo for full details");
274
+ }
275
+ return lines.join("\n");
276
+ }
277
+ /**
278
+ * Handle /lens-rate command
279
+ */
280
+ export async function handleRate(args, ctx, clients) {
281
+ const targetPath = args.trim() || ctx.cwd || process.cwd();
282
+ ctx.ui.notify("📊 Calculating code quality scores...", "info");
283
+ const result = await gatherScores(targetPath, clients);
284
+ return formatRateResult(result);
285
+ }
@@ -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
+ });