#!/usr/bin/env tsx /** * Post-Migration Cleanup Audit * * Read-only audit that scans a migrated site for dead code and obsolete * boilerplate that the framework now owns. Mirrors the human checklist at * `.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md` * but turns it into something CI can actually run. * * Usage (from a migrated site directory): * npx -p @decocms/blocks-cli deco-post-cleanup * npx -p @decocms/blocks-cli deco-post-cleanup --json * * Options: * --source Site directory to audit (default: current directory) * --json Emit machine-readable JSON instead of pretty text * --strict Exit code 2 if any warning-severity findings exist * --help, -h Show this help * * This script is intentionally read-only. Auto-fix support (`--fix`) is * a planned follow-up — see the SKILL doc. */ import * as path from "node:path"; import { banner, bold, cyan, gray, green, red, yellow } from "./migrate/colors"; import { realFsAdapter, realFsWriter, runAudit } from "./migrate/post-cleanup/runner"; import type { AuditReport, Severity } from "./migrate/post-cleanup/types"; interface CliOpts { source: string; json: boolean; strict: boolean; help: boolean; fix: boolean; } function parseArgs(args: string[]): CliOpts { let source = "."; let json = false; let strict = false; let help = false; let fix = false; for (let i = 0; i < args.length; i++) { switch (args[i]) { case "--source": source = args[++i]; break; case "--json": json = true; break; case "--strict": strict = true; break; case "--fix": fix = true; break; case "--help": case "-h": help = true; break; } } return { source, json, strict, help, fix }; } function showHelp() { console.log(` @decocms/blocks-cli — Post-Migration Cleanup Audit Scans a migrated site for dead code and obsolete boilerplate that the framework now owns. Read-only — prints findings, does not modify files. Usage: npx -p @decocms/blocks-cli deco-post-cleanup [options] Options: --source Site directory to audit (default: .) --fix Auto-apply mechanical fixes for the safe rules (dead-lib-shims, dead-runtime-shim, local-widgets-types, vtex-shim-regression swap subset, obsolete-vite-plugins, local-framework-duplicate auto-fixable subset, lockfile-multiple, package-manager-missing). Other rules — including htmx-residue, lockfile-missing, lockfile-drift, and the warn-only entries of local-framework-duplicate — stay detect-only. --json Emit machine-readable JSON instead of pretty text --strict Exit code 2 if any warning-severity findings exist --help, -h Show this help Examples: npx -p @decocms/blocks-cli deco-post-cleanup npx -p @decocms/blocks-cli deco-post-cleanup --source ./my-site --json npx -p @decocms/blocks-cli deco-post-cleanup --fix npx -p @decocms/blocks-cli deco-post-cleanup --fix --strict # fail CI if anything left See: .agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md `); } function severityColor(sev: Severity, text: string): string { if (sev === "warning") return yellow(text); return gray(text); } function printText(report: AuditReport, fixMode: boolean): void { banner(fixMode ? "Post-Migration Cleanup Audit (--fix)" : "Post-Migration Cleanup Audit"); console.log(` ${gray("Site:")} ${bold(report.site)}`); console.log(` ${gray("Findings:")} ${bold(String(report.totalFindings))}`); if (fixMode) { console.log(` ${gray("Auto-fixed:")} ${bold(String(report.totalFixActions))}`); } console.log(""); let idx = 0; for (const summary of report.rules) { idx++; const count = summary.findings.length; const fixCount = summary.fixes?.length ?? 0; const headColor = count === 0 ? green : yellow; const suffix = fixMode ? gray(`(${count} found, ${fixCount} fixed${summary.supportsAutoFix ? "" : ", manual"})`) : gray(`(${count} found)`); console.log(`${headColor(`[${idx}] ${summary.title}`)} ${suffix}`); for (const f of summary.findings) { const tag = severityColor(f.severity, `[${f.severity.toUpperCase()}]`); console.log(` ${tag} ${bold(f.file)} — ${f.message}`); if (f.fix && !summary.fixes) { console.log(` ${gray("fix:")} ${f.fix}`); } } if (summary.fixes) { for (const a of summary.fixes) { console.log(` ${cyan("[FIXED]")} ${bold(a.file)} — ${a.detail}`); } } if (count === 0) console.log(` ${gray("(nothing to clean up)")}`); console.log(""); } const warnings = report.rules .flatMap((r) => r.findings) .filter((f) => f.severity === "warning").length; const infos = report.totalFindings - warnings; const tail = fixMode ? `${report.totalFindings} finding(s) — ${cyan(`${report.totalFixActions} auto-fixed`)}, ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}` : `${report.totalFindings} finding(s) — ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}`; console.log(`${bold("Summary:")} ${tail}`); if (report.totalFindings > 0) { const hint = fixMode ? " Some rules require manual fixes — see post-migration-cleanup.md." : " Run with --fix to auto-correct the safe rules, or see post-migration-cleanup.md."; console.log(gray(hint)); } } function printJson(report: AuditReport): void { console.log(JSON.stringify(report, null, 2)); } function shouldFail(report: AuditReport, strict: boolean): boolean { if (!strict) return false; return report.rules.some((r) => r.findings.some((f) => f.severity === "warning")); } async function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { showHelp(); process.exit(0); } const siteDir = path.resolve(opts.source); const report = runAudit(siteDir, realFsAdapter, { writer: opts.fix ? realFsWriter : undefined, }); if (opts.json) { printJson(report); } else { printText(report, opts.fix); } if (shouldFail(report, opts.strict)) { process.exit(2); } } main().catch((err) => { console.error(red(`Audit failed: ${(err as Error).message}`)); process.exit(1); });