pi-lens 2.0.5 → 2.0.7
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/index.ts +272 -0
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -580,6 +580,278 @@ export default function (pi: ExtensionAPI) {
|
|
|
580
580
|
},
|
|
581
581
|
});
|
|
582
582
|
|
|
583
|
+
// --- Rule action map for lens-booboo-fix ---
|
|
584
|
+
const RULE_ACTIONS: Record<string, { type: "biome" | "agent" | "skip"; note: string }> = {
|
|
585
|
+
"no-var": { type: "biome", note: "auto-fixed by Biome --write" },
|
|
586
|
+
"prefer-template": { type: "biome", note: "auto-fixed by Biome --write" },
|
|
587
|
+
"no-useless-concat": { type: "biome", note: "auto-fixed by Biome --write" },
|
|
588
|
+
"no-lonely-if": { type: "biome", note: "auto-fixed by Biome --write" },
|
|
589
|
+
"prefer-const": { type: "biome", note: "auto-fixed by Biome --write" },
|
|
590
|
+
"empty-catch": { type: "agent", note: "Add this.log('Error: ' + err.message) to the catch block" },
|
|
591
|
+
"silent-failure": { type: "agent", note: "Add this.log('Error: ' + err.message) or rethrow" },
|
|
592
|
+
"no-console-log": { type: "agent", note: "Remove or replace with class logger method" },
|
|
593
|
+
"no-debugger": { type: "agent", note: "Remove the debugger statement" },
|
|
594
|
+
"no-return-await": { type: "agent", note: "Remove the unnecessary `return await`" },
|
|
595
|
+
"nested-ternary": { type: "agent", note: "Extract to if/else or a named variable" },
|
|
596
|
+
"no-throw-string": { type: "agent", note: "Wrap in `new Error(...)` instead of throwing a string" },
|
|
597
|
+
"no-star-imports": { type: "skip", note: "Requires knowing which exports are actually used." },
|
|
598
|
+
"no-as-any": { type: "skip", note: "Replacing `as any` requires knowing the correct type." },
|
|
599
|
+
"no-non-null-assertion": { type: "skip", note: "Each `!` needs nullability analysis in context." },
|
|
600
|
+
"large-class": { type: "skip", note: "Splitting a class requires architectural decisions." },
|
|
601
|
+
"long-method": { type: "skip", note: "Extraction requires understanding the function's purpose." },
|
|
602
|
+
"long-parameter-list": { type: "skip", note: "Redesigning the signature requires an API decision." },
|
|
603
|
+
"no-shadow": { type: "skip", note: "Renaming requires understanding all variable scopes." },
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
pi.registerCommand("lens-booboo-fix", {
|
|
607
|
+
description:
|
|
608
|
+
"Iterative fix loop: auto-fixes Biome/Ruff, then generates a per-issue plan for agent to execute. Run repeatedly until clean. Usage: /lens-booboo-fix [path]",
|
|
609
|
+
handler: async (args, ctx) => {
|
|
610
|
+
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
611
|
+
const fs = require("node:fs") as typeof import("node:fs");
|
|
612
|
+
const sessionFile = path.join(process.cwd(), ".pi-lens", "fix-session.json");
|
|
613
|
+
const configPath = path.join(
|
|
614
|
+
typeof __dirname !== "undefined" ? __dirname : ".",
|
|
615
|
+
"rules", "ast-grep-rules", ".sgconfig.yml",
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
ctx.ui.notify("🔧 Running booboo fix loop...", "info");
|
|
619
|
+
|
|
620
|
+
const MAX_ITERATIONS = 3;
|
|
621
|
+
|
|
622
|
+
// Load session state
|
|
623
|
+
let session: { iteration: number; counts: Record<string, number> } = { iteration: 0, counts: {} };
|
|
624
|
+
try { if (fs.existsSync(sessionFile)) session = JSON.parse(fs.readFileSync(sessionFile, "utf-8")); } catch (e) { dbg(`fix-session load failed: ${e}`); }
|
|
625
|
+
session.iteration++;
|
|
626
|
+
|
|
627
|
+
// Hard stop at max iterations
|
|
628
|
+
if (session.iteration > MAX_ITERATIONS) {
|
|
629
|
+
ctx.ui.notify(`⛔ Max iterations (${MAX_ITERATIONS}) reached. Run /lens-booboo for full remaining report, or delete .pi-lens/fix-session.json to reset.`, "warning");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const prevCounts = { ...session.counts };
|
|
633
|
+
|
|
634
|
+
// --- Step 1: Auto-fix with Biome ---
|
|
635
|
+
let biomeRan = false;
|
|
636
|
+
if (!pi.getFlag("no-biome") && biomeClient.isAvailable()) {
|
|
637
|
+
require("node:child_process").spawnSync("npx", [
|
|
638
|
+
"@biomejs/biome", "check", "--write", "--unsafe", targetPath,
|
|
639
|
+
], { encoding: "utf-8", timeout: 30000, shell: true });
|
|
640
|
+
biomeRan = true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// --- Step 2: Auto-fix with Ruff ---
|
|
644
|
+
let ruffRan = false;
|
|
645
|
+
if (!pi.getFlag("no-ruff") && ruffClient.isAvailable()) {
|
|
646
|
+
require("node:child_process").spawnSync("ruff", ["check", "--fix", targetPath], { encoding: "utf-8", timeout: 15000, shell: true });
|
|
647
|
+
require("node:child_process").spawnSync("ruff", ["format", targetPath], { encoding: "utf-8", timeout: 15000, shell: true });
|
|
648
|
+
ruffRan = true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// --- Step 3: ast-grep scan (after auto-fix) ---
|
|
652
|
+
type AstIssue = { rule: string; file: string; line: number; message: string };
|
|
653
|
+
const astIssues: AstIssue[] = [];
|
|
654
|
+
if (astGrepClient.isAvailable()) {
|
|
655
|
+
const result = require("node:child_process").spawnSync("npx", [
|
|
656
|
+
"sg", "scan", "--config", configPath, "--json",
|
|
657
|
+
"--globs", "!**/*.test.ts", "--globs", "!**/*.spec.ts",
|
|
658
|
+
"--globs", "!**/test-utils.ts", "--globs", "!**/.pi-lens/**",
|
|
659
|
+
targetPath,
|
|
660
|
+
], { encoding: "utf-8", timeout: 30000, shell: true, maxBuffer: 32 * 1024 * 1024 });
|
|
661
|
+
|
|
662
|
+
const raw = result.stdout?.trim() ?? "";
|
|
663
|
+
// biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
|
|
664
|
+
const items: Record<string, any>[] = raw.startsWith("[")
|
|
665
|
+
? (() => { try { return JSON.parse(raw); } catch (e) { dbg(`ast-grep parse failed: ${e}`); return []; } })()
|
|
666
|
+
: raw.split("\n").flatMap((l: string) => { try { return [JSON.parse(l)]; } catch { return []; } });
|
|
667
|
+
|
|
668
|
+
for (const item of items) {
|
|
669
|
+
const rule = item.ruleId || item.rule?.title || item.name || "unknown";
|
|
670
|
+
const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) + 1;
|
|
671
|
+
const relFile = path.relative(targetPath, item.file ?? "").replace(/\\/g, "/");
|
|
672
|
+
astIssues.push({ rule, file: relFile, line, message: item.message ?? rule });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// --- Step 4: AI slop from complexity scan ---
|
|
677
|
+
const slopFiles: Array<{ file: string; warnings: string[] }> = [];
|
|
678
|
+
const slopScanDir = (dir: string) => {
|
|
679
|
+
if (!fs.existsSync(dir)) return;
|
|
680
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
681
|
+
const fullPath = path.join(dir, entry.name);
|
|
682
|
+
if (entry.isDirectory()) {
|
|
683
|
+
if (["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(entry.name)) continue;
|
|
684
|
+
slopScanDir(fullPath);
|
|
685
|
+
} else if (complexityClient.isSupportedFile(fullPath) && !/\.(test|spec)\.[jt]sx?$/.test(entry.name)) {
|
|
686
|
+
const metrics = complexityClient.analyzeFile(fullPath);
|
|
687
|
+
if (metrics) {
|
|
688
|
+
const warnings = complexityClient.checkThresholds(metrics).filter(w =>
|
|
689
|
+
w.includes("AI-style") || w.includes("try/catch") || w.includes("single-use") || w.includes("Excessive comments")
|
|
690
|
+
);
|
|
691
|
+
if (warnings.length > 0) {
|
|
692
|
+
slopFiles.push({ file: path.relative(targetPath, fullPath).replace(/\\/g, "/"), warnings });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
slopScanDir(targetPath);
|
|
699
|
+
|
|
700
|
+
// --- Step 5: Remaining Biome lint ---
|
|
701
|
+
const remainingBiome: Array<{ file: string; line: number; rule: string; message: string; fixable: boolean }> = [];
|
|
702
|
+
if (!pi.getFlag("no-biome") && biomeClient.isAvailable()) {
|
|
703
|
+
// Sample a few key files for remaining lint (not whole project — Biome just ran)
|
|
704
|
+
// Just report counts from a quick scan
|
|
705
|
+
const checkResult = require("node:child_process").spawnSync("npx", [
|
|
706
|
+
"@biomejs/biome", "check", "--reporter=json", "--max-diagnostics=50", targetPath,
|
|
707
|
+
], { encoding: "utf-8", timeout: 20000, shell: true });
|
|
708
|
+
try {
|
|
709
|
+
const data = JSON.parse(checkResult.stdout ?? "{}");
|
|
710
|
+
for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
|
|
711
|
+
if (!diag.category?.startsWith("lint/")) continue;
|
|
712
|
+
const filePath = diag.location?.path?.file ?? "";
|
|
713
|
+
const line = diag.location?.span?.start?.line ?? 0;
|
|
714
|
+
const rule = diag.category ?? "lint";
|
|
715
|
+
const fixable = diag.tags?.includes("fixable") ?? false;
|
|
716
|
+
remainingBiome.push({
|
|
717
|
+
file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
|
|
718
|
+
line: line + 1, rule, message: diag.message ?? rule, fixable,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} catch (e) { dbg(`biome lint parse failed: ${e}`); }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// --- Categorize ast-grep issues ---
|
|
725
|
+
const agentTasks: AstIssue[] = [];
|
|
726
|
+
const skipRules = new Map<string, { note: string; count: number }>();
|
|
727
|
+
|
|
728
|
+
// Group by rule
|
|
729
|
+
const byRule = new Map<string, AstIssue[]>();
|
|
730
|
+
for (const issue of astIssues) {
|
|
731
|
+
const list = byRule.get(issue.rule) ?? [];
|
|
732
|
+
list.push(issue);
|
|
733
|
+
byRule.set(issue.rule, list);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for (const [rule, issues] of byRule) {
|
|
737
|
+
const action = RULE_ACTIONS[rule];
|
|
738
|
+
if (!action || action.type === "agent") {
|
|
739
|
+
agentTasks.push(...issues);
|
|
740
|
+
} else if (action.type === "skip") {
|
|
741
|
+
skipRules.set(rule, { note: action.note, count: issues.length });
|
|
742
|
+
}
|
|
743
|
+
// biome type → already handled by Step 1
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// --- Update session counts ---
|
|
747
|
+
const currentCounts = {
|
|
748
|
+
agent_ast: agentTasks.length,
|
|
749
|
+
biome_lint: remainingBiome.length,
|
|
750
|
+
slop_files: slopFiles.length,
|
|
751
|
+
};
|
|
752
|
+
session.counts = currentCounts;
|
|
753
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
754
|
+
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2), "utf-8");
|
|
755
|
+
|
|
756
|
+
// --- Check if done ---
|
|
757
|
+
const totalFixable = agentTasks.length + remainingBiome.length + slopFiles.length;
|
|
758
|
+
if (totalFixable === 0) {
|
|
759
|
+
const msg = `✅ BOOBOO FIX LOOP COMPLETE — No more fixable issues found after ${session.iteration} iteration(s).\n\nRemaining skipped items are architectural — see /lens-booboo for full report.`;
|
|
760
|
+
ctx.ui.notify(msg, "info");
|
|
761
|
+
fs.unlinkSync(sessionFile);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// --- Build delta line ---
|
|
766
|
+
let deltaLine = "";
|
|
767
|
+
if (session.iteration > 1 && Object.keys(prevCounts).length > 0) {
|
|
768
|
+
const prevTotal = Object.values(prevCounts).reduce((a, b) => a + b, 0);
|
|
769
|
+
const currTotal = totalFixable;
|
|
770
|
+
const fixed = prevTotal - currTotal;
|
|
771
|
+
deltaLine = fixed > 0 ? `✅ Fixed ${fixed} issues since last iteration.` : `⚠️ No change since last iteration — check if fixes were applied.`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- Build the fix plan message ---
|
|
775
|
+
const lines: string[] = [];
|
|
776
|
+
lines.push(`📋 BOOBOO FIX PLAN — Iteration ${session.iteration}/${MAX_ITERATIONS} (${totalFixable} fixable items remaining)`);
|
|
777
|
+
if (deltaLine) lines.push(deltaLine);
|
|
778
|
+
lines.push("");
|
|
779
|
+
|
|
780
|
+
// Auto-fix note
|
|
781
|
+
if (biomeRan || ruffRan) {
|
|
782
|
+
lines.push(`⚡ Auto-fixed: ${[biomeRan && "Biome --write --unsafe", ruffRan && "Ruff --fix + format"].filter(Boolean).join(", ")} already ran.`);
|
|
783
|
+
lines.push("");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Agent tasks — grouped by rule
|
|
787
|
+
if (agentTasks.length > 0) {
|
|
788
|
+
lines.push(`## 🔨 Fix these [${agentTasks.length} items]`);
|
|
789
|
+
lines.push("");
|
|
790
|
+
const groupedAgent = new Map<string, AstIssue[]>();
|
|
791
|
+
for (const t of agentTasks) {
|
|
792
|
+
const g = groupedAgent.get(t.rule) ?? [];
|
|
793
|
+
g.push(t);
|
|
794
|
+
groupedAgent.set(t.rule, g);
|
|
795
|
+
}
|
|
796
|
+
for (const [rule, issues] of groupedAgent) {
|
|
797
|
+
const action = RULE_ACTIONS[rule];
|
|
798
|
+
const note = action?.note ?? "Fix this violation";
|
|
799
|
+
lines.push(`### ${rule} (${issues.length})`);
|
|
800
|
+
lines.push(`→ ${note}`);
|
|
801
|
+
for (const issue of issues.slice(0, 15)) {
|
|
802
|
+
lines.push(` - \`${issue.file}:${issue.line}\``);
|
|
803
|
+
}
|
|
804
|
+
if (issues.length > 15) lines.push(` ... and ${issues.length - 15} more`);
|
|
805
|
+
lines.push("");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Remaining Biome lint
|
|
810
|
+
if (remainingBiome.length > 0) {
|
|
811
|
+
lines.push(`## 🟠 Remaining Biome lint [${remainingBiome.length} items]`);
|
|
812
|
+
lines.push("→ Fix each manually or run `/lens-format` from the UI:");
|
|
813
|
+
for (const d of remainingBiome.slice(0, 10)) {
|
|
814
|
+
lines.push(` - \`${d.file}:${d.line}\` [${d.rule}] ${d.message}`);
|
|
815
|
+
}
|
|
816
|
+
if (remainingBiome.length > 10) lines.push(` ... and ${remainingBiome.length - 10} more`);
|
|
817
|
+
lines.push("");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// AI slop
|
|
821
|
+
if (slopFiles.length > 0) {
|
|
822
|
+
lines.push(`## 🤖 AI Slop indicators [${slopFiles.length} files]`);
|
|
823
|
+
for (const { file, warnings } of slopFiles.slice(0, 10)) {
|
|
824
|
+
lines.push(` - \`${file}\`: ${warnings.map(w => w.split(" — ")[0]).join(", ")}`);
|
|
825
|
+
}
|
|
826
|
+
if (slopFiles.length > 10) lines.push(` ... and ${slopFiles.length - 10} more`);
|
|
827
|
+
lines.push("");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Skips
|
|
831
|
+
if (skipRules.size > 0) {
|
|
832
|
+
lines.push(`## ⏭️ Skip [${[...skipRules.values()].reduce((a, b) => a + b.count, 0)} items — architectural]`);
|
|
833
|
+
for (const [rule, { note, count }] of skipRules) {
|
|
834
|
+
lines.push(` - **${rule}** (${count}): ${note}`);
|
|
835
|
+
}
|
|
836
|
+
lines.push("");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
lines.push("---");
|
|
840
|
+
lines.push("Fix the items above, then run `/lens-booboo-fix` again for the next iteration.");
|
|
841
|
+
lines.push("If an item in '🔨 Fix these' is not safe to fix, skip it with one sentence why.");
|
|
842
|
+
|
|
843
|
+
const fixPlan = lines.join("\n");
|
|
844
|
+
|
|
845
|
+
// Save plan for reference
|
|
846
|
+
const planPath = path.join(process.cwd(), ".pi-lens", "fix-plan.md");
|
|
847
|
+
fs.writeFileSync(planPath, `# Fix Plan — Iteration ${session.iteration}\n\n${fixPlan}`, "utf-8");
|
|
848
|
+
|
|
849
|
+
// Notify and inject into conversation
|
|
850
|
+
ctx.ui.notify(`📄 Fix plan saved: ${planPath}`, "info");
|
|
851
|
+
pi.sendUserMessage(fixPlan);
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
|
|
583
855
|
pi.registerCommand("lens-metrics", {
|
|
584
856
|
description:
|
|
585
857
|
"Measure complexity metrics for all files and export to report.md. Usage: /lens-metrics [path]",
|
package/package.json
CHANGED