pi-lens 2.0.42 ā 2.0.44
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/commands/booboo.js +363 -0
- package/commands/booboo.ts +446 -0
- package/commands/fix.js +222 -0
- package/commands/fix.ts +310 -0
- package/commands/refactor.js +106 -0
- package/commands/refactor.ts +169 -0
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/array-callback-return.yml +33 -33
- package/rules/ast-grep-rules/rules/constructor-super.yml +22 -22
- package/rules/ast-grep-rules/rules/getter-return.yml +59 -59
- package/rules/ast-grep-rules/rules/in-correct-optional-input-type.yml +63 -63
- package/rules/ast-grep-rules/rules/missing-component-decorator.yml +29 -29
- package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +15 -15
- package/rules/ast-grep-rules/rules/no-await-in-loop.yml +30 -30
- package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +13 -13
- package/rules/ast-grep-rules/rules/no-cond-assign.yml +36 -36
- package/rules/ast-grep-rules/rules/no-constructor-return.yml +28 -28
- package/rules/ast-grep-rules/rules/no-debugger.yml +10 -10
- package/rules/ast-grep-rules/rules/no-dupe-args.yml +15 -15
- package/rules/ast-grep-rules/rules/no-dupe-class-members.yml +76 -76
- package/rules/ast-grep-rules/rules/no-dupe-keys.yml +73 -73
- package/rules/ast-grep-rules/rules/no-new-symbol.yml +8 -8
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import * as nodeFs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getSourceFiles } from "../clients/scan-utils.js";
|
|
5
|
+
const getExtensionDir = () => {
|
|
6
|
+
if (typeof __dirname !== "undefined") {
|
|
7
|
+
return __dirname;
|
|
8
|
+
}
|
|
9
|
+
return ".";
|
|
10
|
+
};
|
|
11
|
+
export async function handleBooboo(args, ctx, clients, pi) {
|
|
12
|
+
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
13
|
+
ctx.ui.notify("š Running full codebase review...", "info");
|
|
14
|
+
const parts = [];
|
|
15
|
+
const fullReport = [];
|
|
16
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
17
|
+
const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
|
|
18
|
+
// Part 1: Design smells via ast-grep
|
|
19
|
+
if (clients.astGrep.isAvailable()) {
|
|
20
|
+
const configPath = path.join(getExtensionDir(), "..", "rules", "ast-grep-rules", ".sgconfig.yml");
|
|
21
|
+
try {
|
|
22
|
+
const result = childProcess.spawnSync("npx", [
|
|
23
|
+
"sg",
|
|
24
|
+
"scan",
|
|
25
|
+
"--config",
|
|
26
|
+
configPath,
|
|
27
|
+
"--json",
|
|
28
|
+
"--globs",
|
|
29
|
+
"!**/*.test.ts",
|
|
30
|
+
"--globs",
|
|
31
|
+
"!**/*.spec.ts",
|
|
32
|
+
"--globs",
|
|
33
|
+
"!**/test-utils.ts",
|
|
34
|
+
"--globs",
|
|
35
|
+
"!**/.pi-lens/**",
|
|
36
|
+
targetPath,
|
|
37
|
+
], {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
timeout: 30000,
|
|
40
|
+
shell: true,
|
|
41
|
+
maxBuffer: 32 * 1024 * 1024, // 32MB
|
|
42
|
+
});
|
|
43
|
+
const output = result.stdout || result.stderr || "";
|
|
44
|
+
if (output.trim() && result.status !== undefined) {
|
|
45
|
+
const issues = [];
|
|
46
|
+
const parseItems = (raw) => {
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
if (trimmed.startsWith("[")) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(trimmed);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
void err;
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return raw.split("\n").flatMap((l) => {
|
|
58
|
+
try {
|
|
59
|
+
return [JSON.parse(l)];
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
void err;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
for (const item of parseItems(output)) {
|
|
68
|
+
const ruleId = item.ruleId || item.rule?.title || item.name || "unknown";
|
|
69
|
+
const ruleDesc = clients.astGrep.getRuleDescription?.(ruleId);
|
|
70
|
+
const message = ruleDesc?.message || item.message || ruleId;
|
|
71
|
+
const lineNum = item.labels?.[0]?.range?.start?.line ||
|
|
72
|
+
item.spans?.[0]?.range?.start?.line ||
|
|
73
|
+
item.range?.start?.line ||
|
|
74
|
+
0;
|
|
75
|
+
issues.push({
|
|
76
|
+
line: lineNum + 1,
|
|
77
|
+
rule: ruleId,
|
|
78
|
+
message: message,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (issues.length > 0) {
|
|
82
|
+
let report = `[ast-grep] ${issues.length} issue(s) found:\n`;
|
|
83
|
+
for (const issue of issues.slice(0, 20)) {
|
|
84
|
+
report += ` L${issue.line}: ${issue.rule} ā ${issue.message}\n`;
|
|
85
|
+
}
|
|
86
|
+
if (issues.length > 20) {
|
|
87
|
+
report += ` ... and ${issues.length - 20} more\n`;
|
|
88
|
+
}
|
|
89
|
+
parts.push(report);
|
|
90
|
+
let fullSection = `## ast-grep (Structural Issues)\n\n**${issues.length} issue(s) found**\n\n`;
|
|
91
|
+
fullSection +=
|
|
92
|
+
"| Line | Rule | Message |\n|------|------|--------|\n";
|
|
93
|
+
for (const issue of issues) {
|
|
94
|
+
fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
|
|
95
|
+
}
|
|
96
|
+
fullReport.push(fullSection);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const _err = err;
|
|
102
|
+
// Ignored
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Part 2: Similar functions
|
|
106
|
+
if (clients.astGrep.isAvailable()) {
|
|
107
|
+
const similarGroups = await clients.astGrep.findSimilarFunctions(targetPath, "typescript");
|
|
108
|
+
if (similarGroups.length > 0) {
|
|
109
|
+
let report = `[Similar Functions] ${similarGroups.length} group(s) of structurally similar functions:\n`;
|
|
110
|
+
for (const group of similarGroups.slice(0, 5)) {
|
|
111
|
+
report += ` Pattern: ${group.functions.map((f) => f.name).join(", ")}\n`;
|
|
112
|
+
for (const fn of group.functions) {
|
|
113
|
+
report += ` ${fn.name} (${path.basename(fn.file)}:${fn.line})\n`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (similarGroups.length > 5) {
|
|
117
|
+
report += ` ... and ${similarGroups.length - 5} more groups\n`;
|
|
118
|
+
}
|
|
119
|
+
parts.push(report);
|
|
120
|
+
let fullSection = `## Similar Functions\n\n**${similarGroups.length} group(s) of structurally similar functions**\n\n`;
|
|
121
|
+
for (const group of similarGroups) {
|
|
122
|
+
fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
|
|
123
|
+
fullSection +=
|
|
124
|
+
"| Function | File | Line |\n|----------|------|------|\n";
|
|
125
|
+
for (const fn of group.functions) {
|
|
126
|
+
fullSection += `| ${fn.name} | ${fn.file} | ${fn.line} |\n`;
|
|
127
|
+
}
|
|
128
|
+
fullSection += "\n";
|
|
129
|
+
}
|
|
130
|
+
fullReport.push(fullSection);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Part 3: Complexity metrics
|
|
134
|
+
const results = [];
|
|
135
|
+
const aiSlopIssues = [];
|
|
136
|
+
const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
|
|
137
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
138
|
+
for (const fullPath of files) {
|
|
139
|
+
if (clients.complexity.isSupportedFile(fullPath)) {
|
|
140
|
+
const metrics = clients.complexity.analyzeFile(fullPath);
|
|
141
|
+
if (metrics) {
|
|
142
|
+
results.push(metrics);
|
|
143
|
+
if (!/\.(test|spec)\.[jt]sx?$/.test(path.basename(fullPath))) {
|
|
144
|
+
const warnings = clients.complexity.checkThresholds(metrics);
|
|
145
|
+
if (warnings.length > 0) {
|
|
146
|
+
aiSlopIssues.push(` ${metrics.filePath}:`);
|
|
147
|
+
for (const w of warnings) {
|
|
148
|
+
aiSlopIssues.push(` ā ${w}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (results.length > 0) {
|
|
156
|
+
const avgMI = results.reduce((a, b) => a + b.maintainabilityIndex, 0) / results.length;
|
|
157
|
+
const avgCognitive = results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
|
|
158
|
+
const avgCyclomatic = results.reduce((a, b) => a + b.cyclomaticComplexity, 0) / results.length;
|
|
159
|
+
const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
|
|
160
|
+
const maxCognitive = Math.max(...results.map((r) => r.cognitiveComplexity));
|
|
161
|
+
const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
|
|
162
|
+
const lowMI = results
|
|
163
|
+
.filter((r) => r.maintainabilityIndex < 60)
|
|
164
|
+
.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
|
|
165
|
+
const highCognitive = results
|
|
166
|
+
.filter((r) => r.cognitiveComplexity > 20)
|
|
167
|
+
.sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
|
|
168
|
+
let summary = `[Complexity] ${results.length} file(s) scanned\n`;
|
|
169
|
+
summary += ` Maintainability: ${avgMI.toFixed(1)} avg | Cognitive: ${avgCognitive.toFixed(1)} avg | Max Nesting: ${maxNesting} levels\n`;
|
|
170
|
+
if (lowMI.length > 0) {
|
|
171
|
+
summary += `\n Low Maintainability (MI < 60):\n`;
|
|
172
|
+
for (const f of lowMI.slice(0, 5)) {
|
|
173
|
+
summary += ` ā ${f.filePath}: MI ${f.maintainabilityIndex.toFixed(1)}\n`;
|
|
174
|
+
}
|
|
175
|
+
if (lowMI.length > 5)
|
|
176
|
+
summary += ` ... and ${lowMI.length - 5} more\n`;
|
|
177
|
+
}
|
|
178
|
+
if (highCognitive.length > 0) {
|
|
179
|
+
summary += `\n High Cognitive Complexity (> 20):\n`;
|
|
180
|
+
for (const f of highCognitive.slice(0, 5)) {
|
|
181
|
+
summary += ` ā ${f.filePath}: ${f.cognitiveComplexity}\n`;
|
|
182
|
+
}
|
|
183
|
+
if (highCognitive.length > 5)
|
|
184
|
+
summary += ` ... and ${highCognitive.length - 5} more\n`;
|
|
185
|
+
}
|
|
186
|
+
if (aiSlopIssues.length > 0) {
|
|
187
|
+
summary += `\n[AI Slop Indicators]\n${aiSlopIssues.join("\n")}`;
|
|
188
|
+
}
|
|
189
|
+
parts.push(summary);
|
|
190
|
+
let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
|
|
191
|
+
fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n| Min Maintainability Index | ${minMI.toFixed(1)} |\n| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n| Max Cognitive Complexity | ${maxCognitive} |\n| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n| Max Nesting Depth | ${maxNesting} |\n| Total Files | ${results.length} |\n\n`;
|
|
192
|
+
if (lowMI.length > 0) {
|
|
193
|
+
fullSection += `### Low Maintainability (MI < 60)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
|
|
194
|
+
for (const f of lowMI) {
|
|
195
|
+
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
196
|
+
}
|
|
197
|
+
fullSection += "\n";
|
|
198
|
+
}
|
|
199
|
+
if (highCognitive.length > 0) {
|
|
200
|
+
fullSection += `### High Cognitive Complexity (> 20)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
|
|
201
|
+
for (const f of highCognitive) {
|
|
202
|
+
fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
203
|
+
}
|
|
204
|
+
fullSection += "\n";
|
|
205
|
+
}
|
|
206
|
+
fullSection += `### All Files\n\n| File | MI | Cognitive | Cyclomatic | Nesting | Entropy |\n|------|-----|-----------|------------|---------|--------|\n`;
|
|
207
|
+
for (const f of results.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex)) {
|
|
208
|
+
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} | ${f.codeEntropy.toFixed(2)} |\n`;
|
|
209
|
+
}
|
|
210
|
+
fullSection += "\n";
|
|
211
|
+
if (aiSlopIssues.length > 0) {
|
|
212
|
+
fullSection += `### AI Slop Indicators\n\n`;
|
|
213
|
+
for (const issue of aiSlopIssues) {
|
|
214
|
+
fullSection += `${issue}\n`;
|
|
215
|
+
}
|
|
216
|
+
fullSection += "\n";
|
|
217
|
+
}
|
|
218
|
+
fullReport.push(fullSection);
|
|
219
|
+
}
|
|
220
|
+
// Part 4: TODOs
|
|
221
|
+
const todoResult = clients.todo.scanDirectory(targetPath);
|
|
222
|
+
const todoReport = clients.todo.formatResult(todoResult);
|
|
223
|
+
if (todoReport) {
|
|
224
|
+
parts.push(todoReport);
|
|
225
|
+
let fullSection = `## TODOs / Annotations\n\n`;
|
|
226
|
+
if (todoResult.items.length > 0) {
|
|
227
|
+
fullSection += `**${todoResult.items.length} annotation(s) found**\n\n| Type | File | Line | Text |\n|------|------|------|------|\n`;
|
|
228
|
+
for (const item of todoResult.items) {
|
|
229
|
+
fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
fullSection += `No annotations found.\n`;
|
|
234
|
+
}
|
|
235
|
+
fullSection += "\n";
|
|
236
|
+
fullReport.push(fullSection);
|
|
237
|
+
}
|
|
238
|
+
// Part 5: Dead code
|
|
239
|
+
if (clients.knip.isAvailable()) {
|
|
240
|
+
const knipResult = clients.knip.analyze(targetPath);
|
|
241
|
+
const knipReport = clients.knip.formatResult(knipResult);
|
|
242
|
+
if (knipReport) {
|
|
243
|
+
parts.push(knipReport);
|
|
244
|
+
let fullSection = `## Dead Code (Knip)\n\n`;
|
|
245
|
+
if (knipResult.issues.length > 0) {
|
|
246
|
+
fullSection += `**${knipResult.issues.length} issue(s) found**\n\n| Type | Name | File |\n|------|------|------|\n`;
|
|
247
|
+
for (const issue of knipResult.issues) {
|
|
248
|
+
fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
fullSection += `No dead code issues found.\n`;
|
|
253
|
+
}
|
|
254
|
+
fullSection += "\n";
|
|
255
|
+
fullReport.push(fullSection);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Part 6: Duplicate code
|
|
259
|
+
if (clients.jscpd.isAvailable()) {
|
|
260
|
+
const jscpdResult = clients.jscpd.scan(targetPath);
|
|
261
|
+
const jscpdReport = clients.jscpd.formatResult(jscpdResult);
|
|
262
|
+
if (jscpdReport) {
|
|
263
|
+
parts.push(jscpdReport);
|
|
264
|
+
let fullSection = `## Code Duplication (jscpd)\n\n`;
|
|
265
|
+
if (jscpdResult.clones.length > 0) {
|
|
266
|
+
fullSection += `**${jscpdResult.clones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n`;
|
|
267
|
+
for (const dup of jscpdResult.clones) {
|
|
268
|
+
fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
fullSection += `No duplicate code found.\n`;
|
|
273
|
+
}
|
|
274
|
+
fullSection += "\n";
|
|
275
|
+
fullReport.push(fullSection);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Part 7: Type coverage
|
|
279
|
+
if (clients.typeCoverage.isAvailable()) {
|
|
280
|
+
const tcResult = clients.typeCoverage.scan(targetPath);
|
|
281
|
+
const tcReport = clients.typeCoverage.formatResult(tcResult);
|
|
282
|
+
if (tcReport) {
|
|
283
|
+
parts.push(tcReport);
|
|
284
|
+
let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
285
|
+
if (tcResult.untypedLocations.length > 0) {
|
|
286
|
+
fullSection += `### Untyped Identifiers\n\n| File | Line | Column | Name |\n|------|------|--------|------|\n`;
|
|
287
|
+
for (const u of tcResult.untypedLocations) {
|
|
288
|
+
fullSection += `| ${u.file} | ${u.line} | ${u.column} | ${u.name} |\n`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
fullSection += "\n";
|
|
292
|
+
fullReport.push(fullSection);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Part 8: Circular deps
|
|
296
|
+
if (!pi.getFlag("no-madge") && clients.depChecker.isAvailable()) {
|
|
297
|
+
const { circular } = clients.depChecker.scanProject(targetPath);
|
|
298
|
+
const depReport = clients.depChecker.formatScanResult(circular);
|
|
299
|
+
if (depReport) {
|
|
300
|
+
parts.push(depReport);
|
|
301
|
+
let fullSection = `## Circular Dependencies (Madge)\n\n**${circular.length} circular chain(s) found**\n\n`;
|
|
302
|
+
for (const dep of circular) {
|
|
303
|
+
fullSection += `- ${dep.path.join(" ā ")}\n`;
|
|
304
|
+
}
|
|
305
|
+
fullReport.push(`${fullSection}\n`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Part 9: Arch rules
|
|
309
|
+
if (!clients.architect.hasConfig()) {
|
|
310
|
+
clients.architect.loadConfig(process.cwd());
|
|
311
|
+
}
|
|
312
|
+
if (clients.architect.hasConfig()) {
|
|
313
|
+
const archViolations = [];
|
|
314
|
+
const archScanDir = (dir) => {
|
|
315
|
+
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
|
|
316
|
+
const full = path.join(dir, entry.name);
|
|
317
|
+
if (entry.isDirectory()) {
|
|
318
|
+
if ([
|
|
319
|
+
"node_modules",
|
|
320
|
+
".git",
|
|
321
|
+
"dist",
|
|
322
|
+
"build",
|
|
323
|
+
".next",
|
|
324
|
+
".pi-lens",
|
|
325
|
+
].includes(entry.name))
|
|
326
|
+
continue;
|
|
327
|
+
archScanDir(full);
|
|
328
|
+
}
|
|
329
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
|
|
330
|
+
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
331
|
+
const content = nodeFs.readFileSync(full, "utf-8");
|
|
332
|
+
const lineCount = content.split("\n").length;
|
|
333
|
+
for (const v of clients.architect.checkFile(relPath, content)) {
|
|
334
|
+
archViolations.push({ file: relPath, message: v.message });
|
|
335
|
+
}
|
|
336
|
+
const sizeV = clients.architect.checkFileSize(relPath, lineCount);
|
|
337
|
+
if (sizeV)
|
|
338
|
+
archViolations.push({ file: relPath, message: sizeV.message });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
archScanDir(targetPath);
|
|
343
|
+
if (archViolations.length > 0) {
|
|
344
|
+
parts.push(`š“ ${archViolations.length} architectural violation(s) ā fix before adding new code`);
|
|
345
|
+
let fullSection = `## Architectural Rules\n\n**${archViolations.length} violation(s) found**\n\n`;
|
|
346
|
+
for (const v of archViolations) {
|
|
347
|
+
fullSection += `- **${v.file}**: ${v.message}\n`;
|
|
348
|
+
}
|
|
349
|
+
fullReport.push(`${fullSection}\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
nodeFs.mkdirSync(reviewDir, { recursive: true });
|
|
353
|
+
const projectName = path.basename(process.cwd());
|
|
354
|
+
const mdReport = `# Code Review: ${projectName}\n\n**Scanned:** ${new Date().toISOString()}\n\n**Path:** \`${targetPath}\`\n\n---\n\n${fullReport.join("\n")}`;
|
|
355
|
+
const reportPath = path.join(reviewDir, `booboo-${timestamp}.md`);
|
|
356
|
+
nodeFs.writeFileSync(reportPath, mdReport, "utf-8");
|
|
357
|
+
if (parts.length === 0) {
|
|
358
|
+
ctx.ui.notify("ā Code review clean ā saved to .pi-lens/reviews/", "info");
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
ctx.ui.notify(`${parts.join("\n\n")}\n\nš Full report: ${reportPath}`, "info");
|
|
362
|
+
}
|
|
363
|
+
}
|