prunify 0.1.7 → 0.1.8
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/README.md +1 -0
- package/dist/cli.cjs +292 -52
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +292 -52
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ All reports are written to `prunify-reports/` (auto-added to `.gitignore`).
|
|
|
175
175
|
|
|
176
176
|
| File | Contents |
|
|
177
177
|
|------|----------|
|
|
178
|
+
| `ai_prompt.tsx` | **Agent handoff:** exports `PRUNIFY_CLEANUP_AGENT_PROMPT` — paste into Cursor/Copilot with the other reports attached; includes safety rules and cleanup order. Regenerated every run. |
|
|
178
179
|
| `dead-code.txt` | Header with tool version + time, then **Safe to delete**, **Transitively dead**, and **Dead exports** sections (sizes and optional chains). Used by `--delete` when re-running deletes. |
|
|
179
180
|
| `dupes.md` | Duplicate function clusters with AI-ready refactor prompts |
|
|
180
181
|
| `circular.txt` | Circular dependency chains (relative paths; header with tool version + time) |
|
package/dist/cli.cjs
CHANGED
|
@@ -512,6 +512,12 @@ ${entry}
|
|
|
512
512
|
`, "utf-8");
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
|
+
function formatBytes(bytes) {
|
|
516
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
517
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
518
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
519
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
520
|
+
}
|
|
515
521
|
function writeMarkdown(report, outputPath) {
|
|
516
522
|
const lines = [
|
|
517
523
|
`# ${report.title}`,
|
|
@@ -679,62 +685,136 @@ function getFileSize(filePath) {
|
|
|
679
685
|
return 0;
|
|
680
686
|
}
|
|
681
687
|
}
|
|
688
|
+
var RULE = "\u2500".repeat(78);
|
|
689
|
+
var RULE2 = "\u2550".repeat(78);
|
|
682
690
|
function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
|
|
683
691
|
const rel = (p) => import_node_path5.default.relative(rootDir, p).replaceAll("\\", "/");
|
|
684
|
-
const
|
|
685
|
-
const
|
|
686
|
-
const
|
|
692
|
+
const sortPaths = (paths) => [...paths].sort((a, b) => rel(a).localeCompare(rel(b), "en"));
|
|
693
|
+
const safeSorted = sortPaths(safeToDelete);
|
|
694
|
+
const transSorted = sortPaths(transitivelyDead);
|
|
695
|
+
const safeBytes = safeSorted.reduce((s, f) => s + getFileSize(f), 0);
|
|
696
|
+
const transBytes = transSorted.reduce((s, f) => s + getFileSize(f), 0);
|
|
697
|
+
const fileDeleteBytes = safeBytes + transBytes;
|
|
698
|
+
const totalKb = (fileDeleteBytes / 1024).toFixed(1);
|
|
699
|
+
const deadExportFiles = new Set(deadExports.map((e) => e.filePath));
|
|
700
|
+
const deadExportFileBytes = [...deadExportFiles].reduce((s, f) => s + getFileSize(f), 0);
|
|
687
701
|
const lines = [
|
|
688
|
-
|
|
702
|
+
RULE2,
|
|
689
703
|
" DEAD CODE REPORT",
|
|
704
|
+
RULE2,
|
|
690
705
|
` Safe to delete : ${safeToDelete.length}`,
|
|
691
706
|
` Transitively dead : ${transitivelyDead.length}`,
|
|
692
707
|
` Dead exports : ${deadExports.length}`,
|
|
693
|
-
` Recoverable
|
|
694
|
-
|
|
708
|
+
` Recoverable (files): ~${totalKb} KB (${formatBytes(fileDeleteBytes)})`,
|
|
709
|
+
RULE2,
|
|
710
|
+
"",
|
|
711
|
+
" HOW TO READ THIS",
|
|
712
|
+
"",
|
|
713
|
+
" 1. SAFE TO DELETE \u2014 No other source file imports these. You can delete them",
|
|
714
|
+
" first (after review).",
|
|
715
|
+
" 2. TRANSITIVELY DEAD \u2014 Only referenced by files that are already dead. After",
|
|
716
|
+
" removing safe-to-delete files, these become removable too.",
|
|
717
|
+
" 3. DEAD EXPORTS \u2014 The file is still used, but the listed export is never",
|
|
718
|
+
" imported by name. Remove the export (or use it) to tidy the API.",
|
|
719
|
+
" Framework routes (pages/, app/, etc.) are not listed here.",
|
|
720
|
+
"",
|
|
721
|
+
RULE,
|
|
695
722
|
""
|
|
696
723
|
];
|
|
697
|
-
if (
|
|
724
|
+
if (safeSorted.length > 0) {
|
|
698
725
|
lines.push(
|
|
699
726
|
"\u2500\u2500 SAFE TO DELETE \u2500\u2500",
|
|
700
|
-
"
|
|
727
|
+
"",
|
|
728
|
+
" Nothing in the project imports these paths. Review, then delete.",
|
|
701
729
|
""
|
|
702
730
|
);
|
|
703
|
-
for (const filePath of
|
|
731
|
+
for (const filePath of safeSorted) {
|
|
704
732
|
const chain = chains.get(filePath) ?? [];
|
|
705
|
-
const
|
|
706
|
-
lines.push(` ${rel(filePath)} (~${
|
|
733
|
+
const kb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
734
|
+
lines.push(` \u2022 ${rel(filePath)} (~${kb} KB)`);
|
|
707
735
|
if (chain.length > 0) {
|
|
708
|
-
lines.push(
|
|
736
|
+
lines.push(
|
|
737
|
+
` \u21B3 Also becomes orphaned if you delete the line above:`,
|
|
738
|
+
` ${chain.map(rel).join(", ")}`
|
|
739
|
+
);
|
|
709
740
|
}
|
|
710
741
|
}
|
|
711
|
-
lines.push(
|
|
742
|
+
lines.push(
|
|
743
|
+
"",
|
|
744
|
+
` SUBTOTAL \u2014 ${safeSorted.length} file(s) \xB7 ${formatBytes(safeBytes)}`,
|
|
745
|
+
"",
|
|
746
|
+
RULE,
|
|
747
|
+
""
|
|
748
|
+
);
|
|
712
749
|
}
|
|
713
|
-
if (
|
|
750
|
+
if (transSorted.length > 0) {
|
|
714
751
|
lines.push(
|
|
715
752
|
"\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500",
|
|
716
|
-
"
|
|
753
|
+
"",
|
|
754
|
+
" Every importer of these files is already in the dead set above.",
|
|
717
755
|
""
|
|
718
756
|
);
|
|
719
|
-
for (const filePath of
|
|
720
|
-
const
|
|
721
|
-
lines.push(` ${rel(filePath)} (~${
|
|
757
|
+
for (const filePath of transSorted) {
|
|
758
|
+
const kb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
759
|
+
lines.push(` \u2022 ${rel(filePath)} (~${kb} KB)`);
|
|
722
760
|
}
|
|
723
|
-
lines.push(
|
|
761
|
+
lines.push(
|
|
762
|
+
"",
|
|
763
|
+
` SUBTOTAL \u2014 ${transSorted.length} file(s) \xB7 ${formatBytes(transBytes)}`,
|
|
764
|
+
"",
|
|
765
|
+
RULE,
|
|
766
|
+
""
|
|
767
|
+
);
|
|
724
768
|
}
|
|
725
769
|
if (deadExports.length > 0) {
|
|
726
770
|
lines.push(
|
|
727
771
|
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
728
|
-
"
|
|
772
|
+
"",
|
|
773
|
+
" Symbol is exported but never imported by name elsewhere (namespace imports",
|
|
774
|
+
" count as \u201Call used\u201D). Route and framework files are excluded.",
|
|
729
775
|
""
|
|
730
776
|
);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
);
|
|
777
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const e of deadExports) {
|
|
779
|
+
if (!byFile.has(e.filePath)) byFile.set(e.filePath, []);
|
|
780
|
+
byFile.get(e.filePath).push(e);
|
|
781
|
+
}
|
|
782
|
+
const fileKeys = [...byFile.keys()].sort((a, b) => rel(a).localeCompare(rel(b), "en"));
|
|
783
|
+
for (const fp of fileKeys) {
|
|
784
|
+
lines.push(` ${rel(fp)}`);
|
|
785
|
+
for (const entry of byFile.get(fp)) {
|
|
786
|
+
lines.push(` \u2022 export \u201C${entry.exportName}\u201D (line ${entry.line})`);
|
|
787
|
+
}
|
|
788
|
+
lines.push("");
|
|
735
789
|
}
|
|
736
|
-
lines.push(
|
|
790
|
+
lines.push(
|
|
791
|
+
` SUBTOTAL \u2014 ${deadExports.length} export(s) across ${deadExportFiles.size} file(s)`,
|
|
792
|
+
"",
|
|
793
|
+
RULE,
|
|
794
|
+
""
|
|
795
|
+
);
|
|
737
796
|
}
|
|
797
|
+
lines.push(
|
|
798
|
+
RULE2,
|
|
799
|
+
" END OF REPORT \u2014 DISK SPACE & CLEANUP SUMMARY",
|
|
800
|
+
RULE2,
|
|
801
|
+
"",
|
|
802
|
+
" If you DELETE every file under \u201CSafe to delete\u201D and \u201CTransitively dead\u201D:",
|
|
803
|
+
"",
|
|
804
|
+
` Files removed : ${safeSorted.length + transSorted.length}`,
|
|
805
|
+
` Disk freed : ${formatBytes(fileDeleteBytes)} (approx., from file sizes on disk)`,
|
|
806
|
+
"",
|
|
807
|
+
" Dead exports do not remove whole files by themselves. Cleaning them saves",
|
|
808
|
+
` maintainability first; the ${deadExportFiles.size} file(s) that contain them`,
|
|
809
|
+
` total about ${formatBytes(deadExportFileBytes)} on disk (entire files, not`,
|
|
810
|
+
" \u201Cexport-only\u201D savings).",
|
|
811
|
+
"",
|
|
812
|
+
" Suggested order: (1) Delete safe-to-delete files \u2192 (2) Delete transitively",
|
|
813
|
+
" dead files \u2192 (3) Remove or use dead exports in remaining files.",
|
|
814
|
+
"",
|
|
815
|
+
RULE2,
|
|
816
|
+
""
|
|
817
|
+
);
|
|
738
818
|
return lines.join("\n");
|
|
739
819
|
}
|
|
740
820
|
async function runDeadCode(dir, opts) {
|
|
@@ -1284,22 +1364,38 @@ async function runAssetCheck(rootDir, opts) {
|
|
|
1284
1364
|
writeMarkdown(
|
|
1285
1365
|
{
|
|
1286
1366
|
title: "Unused Assets Report",
|
|
1287
|
-
summary: `${result.unusedAssets.length} unused
|
|
1367
|
+
summary: `${result.unusedAssets.length} unused \xB7 ${formatBytes(result.unusedAssets.reduce((s, a) => s + a.sizeBytes, 0))} recoverable \u2014 see companion .txt for full detail`,
|
|
1288
1368
|
sections: [
|
|
1289
1369
|
{
|
|
1290
1370
|
title: "Unused Assets",
|
|
1291
1371
|
headers: ["Asset", "Size (KB)"],
|
|
1292
|
-
rows: result.unusedAssets.map((a) => [
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1372
|
+
rows: result.unusedAssets.slice().sort((a, b) => a.relativePath.localeCompare(b.relativePath, "en")).map((a) => [a.relativePath, (a.sizeBytes / 1024).toFixed(1)])
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
title: "If you delete all listed files",
|
|
1376
|
+
rows: [
|
|
1377
|
+
[
|
|
1378
|
+
"Files removed",
|
|
1379
|
+
String(result.unusedAssets.length)
|
|
1380
|
+
],
|
|
1381
|
+
[
|
|
1382
|
+
"Approx. disk freed",
|
|
1383
|
+
formatBytes(result.unusedAssets.reduce((s, a) => s + a.sizeBytes, 0))
|
|
1384
|
+
]
|
|
1385
|
+
]
|
|
1296
1386
|
}
|
|
1297
1387
|
],
|
|
1298
1388
|
generatedAt: /* @__PURE__ */ new Date()
|
|
1299
1389
|
},
|
|
1300
1390
|
opts.output
|
|
1301
1391
|
);
|
|
1302
|
-
|
|
1392
|
+
const txtPath = import_node_path8.default.join(
|
|
1393
|
+
import_node_path8.default.dirname(opts.output),
|
|
1394
|
+
`${import_node_path8.default.basename(opts.output, import_node_path8.default.extname(opts.output))}.txt`
|
|
1395
|
+
);
|
|
1396
|
+
import_node_fs8.default.writeFileSync(txtPath, result.report, "utf-8");
|
|
1397
|
+
console.log(import_chalk6.default.cyan(` Reports written \u2192 ${opts.output}`));
|
|
1398
|
+
console.log(import_chalk6.default.cyan(` \u2192 ${txtPath}`));
|
|
1303
1399
|
}
|
|
1304
1400
|
return result.unusedAssets;
|
|
1305
1401
|
} catch (err) {
|
|
@@ -1337,32 +1433,163 @@ function getFileSize2(filePath) {
|
|
|
1337
1433
|
}
|
|
1338
1434
|
function buildAssetReport(unused, totalAssets, rootDir) {
|
|
1339
1435
|
const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
|
|
1340
|
-
const
|
|
1436
|
+
const usedCount = totalAssets - unused.length;
|
|
1437
|
+
const pct = totalAssets > 0 ? (unused.length / totalAssets * 100).toFixed(1) : "0.0";
|
|
1438
|
+
const RULE22 = "\u2550".repeat(78);
|
|
1439
|
+
const RULE3 = "\u2500".repeat(78);
|
|
1341
1440
|
const lines = [
|
|
1342
|
-
|
|
1343
|
-
" UNUSED ASSETS REPORT",
|
|
1344
|
-
|
|
1345
|
-
`
|
|
1346
|
-
`
|
|
1347
|
-
|
|
1441
|
+
RULE22,
|
|
1442
|
+
" UNUSED ASSETS REPORT (public/)",
|
|
1443
|
+
RULE22,
|
|
1444
|
+
` Total files scanned : ${totalAssets}`,
|
|
1445
|
+
` Referenced in code : ${usedCount}`,
|
|
1446
|
+
` Unused (listed) : ${unused.length} (${pct}% of scanned)`,
|
|
1447
|
+
` Disk size (unused) : ${formatBytes(totalBytes)}`,
|
|
1448
|
+
RULE22,
|
|
1449
|
+
"",
|
|
1450
|
+
" HOW TO READ THIS",
|
|
1451
|
+
"",
|
|
1452
|
+
" Files below live under public/ but their filename or public URL path was not",
|
|
1453
|
+
" found in your source scan (TS, JS, CSS, HTML, JSON). They may still be used",
|
|
1454
|
+
" if loaded by URL from a CMS, CDN, or runtime string \u2014 verify before deleting.",
|
|
1455
|
+
"",
|
|
1456
|
+
RULE3,
|
|
1348
1457
|
""
|
|
1349
1458
|
];
|
|
1350
1459
|
if (unused.length === 0) {
|
|
1351
|
-
lines.push(" All public assets are referenced in source.", "");
|
|
1352
|
-
return lines.join("\n");
|
|
1353
|
-
}
|
|
1354
|
-
lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
|
|
1355
|
-
for (const asset of unused) {
|
|
1356
1460
|
lines.push(
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1461
|
+
" No unused assets detected \u2014 everything scanned appears referenced.",
|
|
1462
|
+
"",
|
|
1463
|
+
RULE22,
|
|
1464
|
+
" END OF REPORT",
|
|
1465
|
+
RULE22,
|
|
1466
|
+
"",
|
|
1467
|
+
" If you removed nothing: disk savings = 0 (already optimal for this scan).",
|
|
1468
|
+
"",
|
|
1469
|
+
RULE22,
|
|
1360
1470
|
""
|
|
1361
1471
|
);
|
|
1472
|
+
return lines.join("\n");
|
|
1473
|
+
}
|
|
1474
|
+
const sorted = [...unused].sort(
|
|
1475
|
+
(a, b) => a.relativePath.localeCompare(b.relativePath, "en")
|
|
1476
|
+
);
|
|
1477
|
+
lines.push("\u2500\u2500 UNUSED ASSET FILES \u2500\u2500", "", " Sorted by path.", "");
|
|
1478
|
+
for (const asset of sorted) {
|
|
1479
|
+
const kb = (asset.sizeBytes / 1024).toFixed(1);
|
|
1480
|
+
lines.push(` \u2022 ${asset.relativePath} (~${kb} KB)`);
|
|
1481
|
+
lines.push(` On disk: ${formatBytes(asset.sizeBytes)}`);
|
|
1482
|
+
lines.push("");
|
|
1362
1483
|
}
|
|
1484
|
+
lines.push(
|
|
1485
|
+
` SUBTOTAL \u2014 ${sorted.length} file(s) \xB7 ${formatBytes(totalBytes)}`,
|
|
1486
|
+
"",
|
|
1487
|
+
RULE3,
|
|
1488
|
+
"",
|
|
1489
|
+
RULE22,
|
|
1490
|
+
" END OF REPORT \u2014 DISK SPACE SUMMARY",
|
|
1491
|
+
RULE22,
|
|
1492
|
+
"",
|
|
1493
|
+
" If you DELETE every unused asset listed above from public/:",
|
|
1494
|
+
"",
|
|
1495
|
+
` Files removed : ${sorted.length}`,
|
|
1496
|
+
` Disk freed : ${formatBytes(totalBytes)} (sum of file sizes on disk)`,
|
|
1497
|
+
"",
|
|
1498
|
+
" This does not include savings from dead code \u2014 see dead-code.txt for source files.",
|
|
1499
|
+
"",
|
|
1500
|
+
RULE22,
|
|
1501
|
+
""
|
|
1502
|
+
);
|
|
1363
1503
|
return lines.join("\n");
|
|
1364
1504
|
}
|
|
1365
1505
|
|
|
1506
|
+
// src/core/agent-prompt.ts
|
|
1507
|
+
function buildPromptBody(meta) {
|
|
1508
|
+
const { prunifyVersion, generatedAtIso, projectRootDisplay, reportsDirDisplay } = meta;
|
|
1509
|
+
return `You are assisting with a **careful, incremental** cleanup of a TypeScript/JavaScript codebase.
|
|
1510
|
+
|
|
1511
|
+
## Context (prunify ${prunifyVersion})
|
|
1512
|
+
|
|
1513
|
+
- **Project root:** ${projectRootDisplay}
|
|
1514
|
+
- **Reports folder:** ${reportsDirDisplay}
|
|
1515
|
+
- **Report generated (this prompt file):** ${generatedAtIso}
|
|
1516
|
+
|
|
1517
|
+
The user has run **prunify** (static analysis). The reports are hints, not ground truth. **False positives are common.**
|
|
1518
|
+
|
|
1519
|
+
## Attach these files to the chat (if they exist)
|
|
1520
|
+
|
|
1521
|
+
From \`${reportsDirDisplay}\`:
|
|
1522
|
+
|
|
1523
|
+
| File | What it means |
|
|
1524
|
+
|------|----------------|
|
|
1525
|
+
| **dead-code.txt** | Orphan files (safe / transitive), dead exports, sizes. **--delete** uses this for whole-file removal. |
|
|
1526
|
+
| **circular.txt** | Import cycles \u2014 usually **refactor**, not delete. |
|
|
1527
|
+
| **dupes.md** | Repeated code **blocks** \u2014 merge only after **semantic** review. |
|
|
1528
|
+
| **deps.md** | package.json vs imports \u2014 **unused** / **missing** / dev issues. |
|
|
1529
|
+
| **assets.md** (+ **assets.txt**) | Unreferenced files under **public/** \u2014 **--delete-assets** uses the report. |
|
|
1530
|
+
|
|
1531
|
+
If a file is missing, skip that section of work.
|
|
1532
|
+
|
|
1533
|
+
---
|
|
1534
|
+
|
|
1535
|
+
## Mandatory safety rules (do not skip)
|
|
1536
|
+
|
|
1537
|
+
1. **No bulk deletes.** Work in small batches; one PR or one chat turn per logical change.
|
|
1538
|
+
2. **Dead code \u2014 files:** Only delete files listed under **Safe to delete** (or explicitly marked removable in dead-code.txt) **after** you grep/search for dynamic imports, \`require(variable)\`, string-based paths, Next.js \`app/\`/\`pages/\` conventions, and config references.
|
|
1539
|
+
3. **Dead code \u2014 exports:** Removing an export can break external consumers or barrels. After removal, run typecheck; fix barrel \`index.ts\` re-exports.
|
|
1540
|
+
4. **Barrel files (\`index.ts\`):** A file may be "used" only via \`export * from './X'\`. Do not delete leaf modules until importers and barrels are updated.
|
|
1541
|
+
5. **Transitively dead:** Deletion order matters \u2014 follow the report chains; remove importers before importers-only-used-by-dead-code where applicable.
|
|
1542
|
+
6. **Circular imports:** Prefer extracting shared code or inverting dependencies; **do not** "fix" by deleting arbitrary cycle members.
|
|
1543
|
+
7. **dupes.md:** Identical text \u2260 identical behavior (closures, imports, generics). **Merge duplicates only** when you can prove equivalence; prefer extracting a shared function with tests.
|
|
1544
|
+
8. **deps.md \u2014 unused:** prunify scans **src/** imports only. Packages may still be used in **root config**, **scripts**, **CSS**, **tests outside src**, **Next.js** files outside \`src/\`, or **CLI**. Confirm with repo-wide search before \`npm uninstall\`.
|
|
1545
|
+
9. **deps.md \u2014 missing:** May be transitive or tooling; verify before adding duplicates.
|
|
1546
|
+
10. **assets:** URLs may be built at runtime, come from CMS, or live in strings prunify does not see. Confirm with search before deleting from **public/**.
|
|
1547
|
+
|
|
1548
|
+
---
|
|
1549
|
+
|
|
1550
|
+
## Suggested order (lowest risk first)
|
|
1551
|
+
|
|
1552
|
+
1. **Verify deps.md** \u2014 search the whole repo; adjust package.json only with evidence.
|
|
1553
|
+
2. **Dead exports** \u2014 remove unused exports / tidy barrels; typecheck after each batch.
|
|
1554
|
+
3. **Safe-to-delete files** \u2014 small batches; run build after.
|
|
1555
|
+
4. **Unused assets** \u2014 after string search in app and config.
|
|
1556
|
+
5. **dupes.md** \u2014 last; highest judgment; add tests when merging.
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
## After every batch
|
|
1561
|
+
|
|
1562
|
+
- Run **lint** and **typecheck** (and **tests** if available).
|
|
1563
|
+
- Run **production build** before declaring done.
|
|
1564
|
+
- If something fails, **revert** that batch and narrow the change.
|
|
1565
|
+
|
|
1566
|
+
---
|
|
1567
|
+
|
|
1568
|
+
## What not to do
|
|
1569
|
+
|
|
1570
|
+
- Do not delete **pages**, **app routes**, **middleware**, or framework entry files unless the user explicitly asks and you verified they are unused.
|
|
1571
|
+
- Do not remove packages because deps.md says "unused" without a repo-wide check.
|
|
1572
|
+
- Do not merge duplicate blocks from dupes.md without reading surrounding code.
|
|
1573
|
+
|
|
1574
|
+
When uncertain, **list the risk** and ask the user to confirm before editing.`;
|
|
1575
|
+
}
|
|
1576
|
+
function buildAiPromptTsx(meta) {
|
|
1577
|
+
const body = buildPromptBody(meta);
|
|
1578
|
+
const literal = JSON.stringify(body);
|
|
1579
|
+
return `/* eslint-disable */
|
|
1580
|
+
/**
|
|
1581
|
+
* Auto-generated by prunify ${meta.prunifyVersion} \u2014 ${meta.generatedAtIso}
|
|
1582
|
+
* Do not edit by hand; re-run \`npx prunify\` to refresh.
|
|
1583
|
+
*
|
|
1584
|
+
* **For Cursor / Copilot:** attach the files listed inside PRUNIFY_CLEANUP_AGENT_PROMPT
|
|
1585
|
+
* (from prunify-reports/), then paste the prompt or use @-mention on those files plus this file.
|
|
1586
|
+
*/
|
|
1587
|
+
export const PRUNIFY_CLEANUP_AGENT_PROMPT = ${literal} as const
|
|
1588
|
+
|
|
1589
|
+
export default PRUNIFY_CLEANUP_AGENT_PROMPT
|
|
1590
|
+
`;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1366
1593
|
// src/core/report-parser.ts
|
|
1367
1594
|
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
1368
1595
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
@@ -1384,7 +1611,7 @@ function parseDeadCodeReportContent(content, rootDir) {
|
|
|
1384
1611
|
return [...new Set(paths)];
|
|
1385
1612
|
}
|
|
1386
1613
|
let section = "none";
|
|
1387
|
-
const fileLineRe = /^\s
|
|
1614
|
+
const fileLineRe = /^\s*(?:•\s+)?(.+?)\s+\(~[\d.]+\s+KB\)\s*$/;
|
|
1388
1615
|
for (const line of lines) {
|
|
1389
1616
|
if (line.includes("\u2500\u2500 SAFE TO DELETE \u2500\u2500")) {
|
|
1390
1617
|
section = "safe";
|
|
@@ -1405,7 +1632,9 @@ function parseDeadCodeReportContent(content, rootDir) {
|
|
|
1405
1632
|
continue;
|
|
1406
1633
|
}
|
|
1407
1634
|
if (section === "safe" || section === "transitive") {
|
|
1408
|
-
if (line.includes("\u2514\u2500") || line.includes("also makes dead"))
|
|
1635
|
+
if (line.includes("\u2514\u2500") || line.includes("\u21B3") || line.includes("also makes dead") || line.includes("SUBTOTAL") || line.includes("\u2500\u2500\u2500")) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1409
1638
|
const m = fileLineRe.exec(line);
|
|
1410
1639
|
if (m) {
|
|
1411
1640
|
const rel = m[1].trim();
|
|
@@ -1426,7 +1655,7 @@ function countDeadExportsInReport(content) {
|
|
|
1426
1655
|
continue;
|
|
1427
1656
|
}
|
|
1428
1657
|
if (section && line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) break;
|
|
1429
|
-
if (section && /\
|
|
1658
|
+
if (section && /\(line \d+\)/.test(line)) n++;
|
|
1430
1659
|
}
|
|
1431
1660
|
return n;
|
|
1432
1661
|
}
|
|
@@ -1434,7 +1663,7 @@ function parseDeadCodeReportSummary(content) {
|
|
|
1434
1663
|
const safe = content.match(/Safe to delete\s*:\s*(\d+)/);
|
|
1435
1664
|
const trans = content.match(/Transitively dead\s*:\s*(\d+)/);
|
|
1436
1665
|
const exp = content.match(/Dead exports\s*:\s*(\d+)/);
|
|
1437
|
-
const rec = content.match(/Recoverable
|
|
1666
|
+
const rec = content.match(/Recoverable[^:\n]*:\s*(~[\d.]+\s*KB)/i);
|
|
1438
1667
|
if (!safe || !trans || !exp) return null;
|
|
1439
1668
|
return {
|
|
1440
1669
|
safeToDelete: Number(safe[1]),
|
|
@@ -1603,7 +1832,8 @@ async function main(opts) {
|
|
|
1603
1832
|
}
|
|
1604
1833
|
}
|
|
1605
1834
|
if (loadAssetsFromCache) {
|
|
1606
|
-
|
|
1835
|
+
const assetsTxtPath = import_node_path10.default.join(reportsDir, "assets.txt");
|
|
1836
|
+
assetsReportFile = import_node_fs10.default.existsSync(assetsTxtPath) ? "assets.md, assets.txt" : "assets.md";
|
|
1607
1837
|
unusedAssetCount = unusedAssetPaths.length;
|
|
1608
1838
|
}
|
|
1609
1839
|
if (runDeadCode2) {
|
|
@@ -1675,7 +1905,7 @@ async function main(opts) {
|
|
|
1675
1905
|
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1676
1906
|
unusedAssetPaths = unusedAssets.map((a) => a.filePath);
|
|
1677
1907
|
unusedAssetCount = unusedAssets.length;
|
|
1678
|
-
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1908
|
+
if (unusedAssetCount > 0) assetsReportFile = "assets.md, assets.txt";
|
|
1679
1909
|
}
|
|
1680
1910
|
if (modules.includes("health")) {
|
|
1681
1911
|
const outputPath = import_node_path10.default.join(reportsDir, "health-report.md");
|
|
@@ -1715,6 +1945,16 @@ async function main(opts) {
|
|
|
1715
1945
|
);
|
|
1716
1946
|
console.log(table.toString());
|
|
1717
1947
|
console.log();
|
|
1948
|
+
const aiPromptContent = buildAiPromptTsx({
|
|
1949
|
+
prunifyVersion: PKG_VERSION,
|
|
1950
|
+
generatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1951
|
+
projectRootDisplay: import_node_path10.default.relative(process.cwd(), rootDir) || ".",
|
|
1952
|
+
reportsDirDisplay: import_node_path10.default.relative(rootDir, reportsDir).replaceAll("\\", "/") || "prunify-reports"
|
|
1953
|
+
});
|
|
1954
|
+
const aiPromptPath = import_node_path10.default.join(reportsDir, "ai_prompt.tsx");
|
|
1955
|
+
import_node_fs10.default.writeFileSync(aiPromptPath, aiPromptContent, "utf-8");
|
|
1956
|
+
console.log(import_chalk7.default.cyan(` Agent prompt saved \u2192 ${aiPromptPath}`));
|
|
1957
|
+
console.log();
|
|
1718
1958
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1719
1959
|
console.log(import_chalk7.default.yellow(`Dead code files to delete (${deadFilePaths.length}):`));
|
|
1720
1960
|
for (const f of deadFilePaths) {
|