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/dist/cli.js
CHANGED
|
@@ -489,6 +489,12 @@ ${entry}
|
|
|
489
489
|
`, "utf-8");
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
|
+
function formatBytes(bytes) {
|
|
493
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
494
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
495
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
496
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
497
|
+
}
|
|
492
498
|
function writeMarkdown(report, outputPath) {
|
|
493
499
|
const lines = [
|
|
494
500
|
`# ${report.title}`,
|
|
@@ -656,62 +662,136 @@ function getFileSize(filePath) {
|
|
|
656
662
|
return 0;
|
|
657
663
|
}
|
|
658
664
|
}
|
|
665
|
+
var RULE = "\u2500".repeat(78);
|
|
666
|
+
var RULE2 = "\u2550".repeat(78);
|
|
659
667
|
function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
|
|
660
668
|
const rel = (p) => path5.relative(rootDir, p).replaceAll("\\", "/");
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
const
|
|
669
|
+
const sortPaths = (paths) => [...paths].sort((a, b) => rel(a).localeCompare(rel(b), "en"));
|
|
670
|
+
const safeSorted = sortPaths(safeToDelete);
|
|
671
|
+
const transSorted = sortPaths(transitivelyDead);
|
|
672
|
+
const safeBytes = safeSorted.reduce((s, f) => s + getFileSize(f), 0);
|
|
673
|
+
const transBytes = transSorted.reduce((s, f) => s + getFileSize(f), 0);
|
|
674
|
+
const fileDeleteBytes = safeBytes + transBytes;
|
|
675
|
+
const totalKb = (fileDeleteBytes / 1024).toFixed(1);
|
|
676
|
+
const deadExportFiles = new Set(deadExports.map((e) => e.filePath));
|
|
677
|
+
const deadExportFileBytes = [...deadExportFiles].reduce((s, f) => s + getFileSize(f), 0);
|
|
664
678
|
const lines = [
|
|
665
|
-
|
|
679
|
+
RULE2,
|
|
666
680
|
" DEAD CODE REPORT",
|
|
681
|
+
RULE2,
|
|
667
682
|
` Safe to delete : ${safeToDelete.length}`,
|
|
668
683
|
` Transitively dead : ${transitivelyDead.length}`,
|
|
669
684
|
` Dead exports : ${deadExports.length}`,
|
|
670
|
-
` Recoverable
|
|
671
|
-
|
|
685
|
+
` Recoverable (files): ~${totalKb} KB (${formatBytes(fileDeleteBytes)})`,
|
|
686
|
+
RULE2,
|
|
687
|
+
"",
|
|
688
|
+
" HOW TO READ THIS",
|
|
689
|
+
"",
|
|
690
|
+
" 1. SAFE TO DELETE \u2014 No other source file imports these. You can delete them",
|
|
691
|
+
" first (after review).",
|
|
692
|
+
" 2. TRANSITIVELY DEAD \u2014 Only referenced by files that are already dead. After",
|
|
693
|
+
" removing safe-to-delete files, these become removable too.",
|
|
694
|
+
" 3. DEAD EXPORTS \u2014 The file is still used, but the listed export is never",
|
|
695
|
+
" imported by name. Remove the export (or use it) to tidy the API.",
|
|
696
|
+
" Framework routes (pages/, app/, etc.) are not listed here.",
|
|
697
|
+
"",
|
|
698
|
+
RULE,
|
|
672
699
|
""
|
|
673
700
|
];
|
|
674
|
-
if (
|
|
701
|
+
if (safeSorted.length > 0) {
|
|
675
702
|
lines.push(
|
|
676
703
|
"\u2500\u2500 SAFE TO DELETE \u2500\u2500",
|
|
677
|
-
"
|
|
704
|
+
"",
|
|
705
|
+
" Nothing in the project imports these paths. Review, then delete.",
|
|
678
706
|
""
|
|
679
707
|
);
|
|
680
|
-
for (const filePath of
|
|
708
|
+
for (const filePath of safeSorted) {
|
|
681
709
|
const chain = chains.get(filePath) ?? [];
|
|
682
|
-
const
|
|
683
|
-
lines.push(` ${rel(filePath)} (~${
|
|
710
|
+
const kb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
711
|
+
lines.push(` \u2022 ${rel(filePath)} (~${kb} KB)`);
|
|
684
712
|
if (chain.length > 0) {
|
|
685
|
-
lines.push(
|
|
713
|
+
lines.push(
|
|
714
|
+
` \u21B3 Also becomes orphaned if you delete the line above:`,
|
|
715
|
+
` ${chain.map(rel).join(", ")}`
|
|
716
|
+
);
|
|
686
717
|
}
|
|
687
718
|
}
|
|
688
|
-
lines.push(
|
|
719
|
+
lines.push(
|
|
720
|
+
"",
|
|
721
|
+
` SUBTOTAL \u2014 ${safeSorted.length} file(s) \xB7 ${formatBytes(safeBytes)}`,
|
|
722
|
+
"",
|
|
723
|
+
RULE,
|
|
724
|
+
""
|
|
725
|
+
);
|
|
689
726
|
}
|
|
690
|
-
if (
|
|
727
|
+
if (transSorted.length > 0) {
|
|
691
728
|
lines.push(
|
|
692
729
|
"\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500",
|
|
693
|
-
"
|
|
730
|
+
"",
|
|
731
|
+
" Every importer of these files is already in the dead set above.",
|
|
694
732
|
""
|
|
695
733
|
);
|
|
696
|
-
for (const filePath of
|
|
697
|
-
const
|
|
698
|
-
lines.push(` ${rel(filePath)} (~${
|
|
734
|
+
for (const filePath of transSorted) {
|
|
735
|
+
const kb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
736
|
+
lines.push(` \u2022 ${rel(filePath)} (~${kb} KB)`);
|
|
699
737
|
}
|
|
700
|
-
lines.push(
|
|
738
|
+
lines.push(
|
|
739
|
+
"",
|
|
740
|
+
` SUBTOTAL \u2014 ${transSorted.length} file(s) \xB7 ${formatBytes(transBytes)}`,
|
|
741
|
+
"",
|
|
742
|
+
RULE,
|
|
743
|
+
""
|
|
744
|
+
);
|
|
701
745
|
}
|
|
702
746
|
if (deadExports.length > 0) {
|
|
703
747
|
lines.push(
|
|
704
748
|
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
705
|
-
"
|
|
749
|
+
"",
|
|
750
|
+
" Symbol is exported but never imported by name elsewhere (namespace imports",
|
|
751
|
+
" count as \u201Call used\u201D). Route and framework files are excluded.",
|
|
706
752
|
""
|
|
707
753
|
);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
);
|
|
754
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
755
|
+
for (const e of deadExports) {
|
|
756
|
+
if (!byFile.has(e.filePath)) byFile.set(e.filePath, []);
|
|
757
|
+
byFile.get(e.filePath).push(e);
|
|
758
|
+
}
|
|
759
|
+
const fileKeys = [...byFile.keys()].sort((a, b) => rel(a).localeCompare(rel(b), "en"));
|
|
760
|
+
for (const fp of fileKeys) {
|
|
761
|
+
lines.push(` ${rel(fp)}`);
|
|
762
|
+
for (const entry of byFile.get(fp)) {
|
|
763
|
+
lines.push(` \u2022 export \u201C${entry.exportName}\u201D (line ${entry.line})`);
|
|
764
|
+
}
|
|
765
|
+
lines.push("");
|
|
712
766
|
}
|
|
713
|
-
lines.push(
|
|
767
|
+
lines.push(
|
|
768
|
+
` SUBTOTAL \u2014 ${deadExports.length} export(s) across ${deadExportFiles.size} file(s)`,
|
|
769
|
+
"",
|
|
770
|
+
RULE,
|
|
771
|
+
""
|
|
772
|
+
);
|
|
714
773
|
}
|
|
774
|
+
lines.push(
|
|
775
|
+
RULE2,
|
|
776
|
+
" END OF REPORT \u2014 DISK SPACE & CLEANUP SUMMARY",
|
|
777
|
+
RULE2,
|
|
778
|
+
"",
|
|
779
|
+
" If you DELETE every file under \u201CSafe to delete\u201D and \u201CTransitively dead\u201D:",
|
|
780
|
+
"",
|
|
781
|
+
` Files removed : ${safeSorted.length + transSorted.length}`,
|
|
782
|
+
` Disk freed : ${formatBytes(fileDeleteBytes)} (approx., from file sizes on disk)`,
|
|
783
|
+
"",
|
|
784
|
+
" Dead exports do not remove whole files by themselves. Cleaning them saves",
|
|
785
|
+
` maintainability first; the ${deadExportFiles.size} file(s) that contain them`,
|
|
786
|
+
` total about ${formatBytes(deadExportFileBytes)} on disk (entire files, not`,
|
|
787
|
+
" \u201Cexport-only\u201D savings).",
|
|
788
|
+
"",
|
|
789
|
+
" Suggested order: (1) Delete safe-to-delete files \u2192 (2) Delete transitively",
|
|
790
|
+
" dead files \u2192 (3) Remove or use dead exports in remaining files.",
|
|
791
|
+
"",
|
|
792
|
+
RULE2,
|
|
793
|
+
""
|
|
794
|
+
);
|
|
715
795
|
return lines.join("\n");
|
|
716
796
|
}
|
|
717
797
|
async function runDeadCode(dir, opts) {
|
|
@@ -1264,22 +1344,38 @@ async function runAssetCheck(rootDir, opts) {
|
|
|
1264
1344
|
writeMarkdown(
|
|
1265
1345
|
{
|
|
1266
1346
|
title: "Unused Assets Report",
|
|
1267
|
-
summary: `${result.unusedAssets.length} unused
|
|
1347
|
+
summary: `${result.unusedAssets.length} unused \xB7 ${formatBytes(result.unusedAssets.reduce((s, a) => s + a.sizeBytes, 0))} recoverable \u2014 see companion .txt for full detail`,
|
|
1268
1348
|
sections: [
|
|
1269
1349
|
{
|
|
1270
1350
|
title: "Unused Assets",
|
|
1271
1351
|
headers: ["Asset", "Size (KB)"],
|
|
1272
|
-
rows: result.unusedAssets.map((a) => [
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1352
|
+
rows: result.unusedAssets.slice().sort((a, b) => a.relativePath.localeCompare(b.relativePath, "en")).map((a) => [a.relativePath, (a.sizeBytes / 1024).toFixed(1)])
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
title: "If you delete all listed files",
|
|
1356
|
+
rows: [
|
|
1357
|
+
[
|
|
1358
|
+
"Files removed",
|
|
1359
|
+
String(result.unusedAssets.length)
|
|
1360
|
+
],
|
|
1361
|
+
[
|
|
1362
|
+
"Approx. disk freed",
|
|
1363
|
+
formatBytes(result.unusedAssets.reduce((s, a) => s + a.sizeBytes, 0))
|
|
1364
|
+
]
|
|
1365
|
+
]
|
|
1276
1366
|
}
|
|
1277
1367
|
],
|
|
1278
1368
|
generatedAt: /* @__PURE__ */ new Date()
|
|
1279
1369
|
},
|
|
1280
1370
|
opts.output
|
|
1281
1371
|
);
|
|
1282
|
-
|
|
1372
|
+
const txtPath = path8.join(
|
|
1373
|
+
path8.dirname(opts.output),
|
|
1374
|
+
`${path8.basename(opts.output, path8.extname(opts.output))}.txt`
|
|
1375
|
+
);
|
|
1376
|
+
fs8.writeFileSync(txtPath, result.report, "utf-8");
|
|
1377
|
+
console.log(chalk6.cyan(` Reports written \u2192 ${opts.output}`));
|
|
1378
|
+
console.log(chalk6.cyan(` \u2192 ${txtPath}`));
|
|
1283
1379
|
}
|
|
1284
1380
|
return result.unusedAssets;
|
|
1285
1381
|
} catch (err) {
|
|
@@ -1317,32 +1413,163 @@ function getFileSize2(filePath) {
|
|
|
1317
1413
|
}
|
|
1318
1414
|
function buildAssetReport(unused, totalAssets, rootDir) {
|
|
1319
1415
|
const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
|
|
1320
|
-
const
|
|
1416
|
+
const usedCount = totalAssets - unused.length;
|
|
1417
|
+
const pct = totalAssets > 0 ? (unused.length / totalAssets * 100).toFixed(1) : "0.0";
|
|
1418
|
+
const RULE22 = "\u2550".repeat(78);
|
|
1419
|
+
const RULE3 = "\u2500".repeat(78);
|
|
1321
1420
|
const lines = [
|
|
1322
|
-
|
|
1323
|
-
" UNUSED ASSETS REPORT",
|
|
1324
|
-
|
|
1325
|
-
`
|
|
1326
|
-
`
|
|
1327
|
-
|
|
1421
|
+
RULE22,
|
|
1422
|
+
" UNUSED ASSETS REPORT (public/)",
|
|
1423
|
+
RULE22,
|
|
1424
|
+
` Total files scanned : ${totalAssets}`,
|
|
1425
|
+
` Referenced in code : ${usedCount}`,
|
|
1426
|
+
` Unused (listed) : ${unused.length} (${pct}% of scanned)`,
|
|
1427
|
+
` Disk size (unused) : ${formatBytes(totalBytes)}`,
|
|
1428
|
+
RULE22,
|
|
1429
|
+
"",
|
|
1430
|
+
" HOW TO READ THIS",
|
|
1431
|
+
"",
|
|
1432
|
+
" Files below live under public/ but their filename or public URL path was not",
|
|
1433
|
+
" found in your source scan (TS, JS, CSS, HTML, JSON). They may still be used",
|
|
1434
|
+
" if loaded by URL from a CMS, CDN, or runtime string \u2014 verify before deleting.",
|
|
1435
|
+
"",
|
|
1436
|
+
RULE3,
|
|
1328
1437
|
""
|
|
1329
1438
|
];
|
|
1330
1439
|
if (unused.length === 0) {
|
|
1331
|
-
lines.push(" All public assets are referenced in source.", "");
|
|
1332
|
-
return lines.join("\n");
|
|
1333
|
-
}
|
|
1334
|
-
lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
|
|
1335
|
-
for (const asset of unused) {
|
|
1336
1440
|
lines.push(
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1441
|
+
" No unused assets detected \u2014 everything scanned appears referenced.",
|
|
1442
|
+
"",
|
|
1443
|
+
RULE22,
|
|
1444
|
+
" END OF REPORT",
|
|
1445
|
+
RULE22,
|
|
1446
|
+
"",
|
|
1447
|
+
" If you removed nothing: disk savings = 0 (already optimal for this scan).",
|
|
1448
|
+
"",
|
|
1449
|
+
RULE22,
|
|
1340
1450
|
""
|
|
1341
1451
|
);
|
|
1452
|
+
return lines.join("\n");
|
|
1453
|
+
}
|
|
1454
|
+
const sorted = [...unused].sort(
|
|
1455
|
+
(a, b) => a.relativePath.localeCompare(b.relativePath, "en")
|
|
1456
|
+
);
|
|
1457
|
+
lines.push("\u2500\u2500 UNUSED ASSET FILES \u2500\u2500", "", " Sorted by path.", "");
|
|
1458
|
+
for (const asset of sorted) {
|
|
1459
|
+
const kb = (asset.sizeBytes / 1024).toFixed(1);
|
|
1460
|
+
lines.push(` \u2022 ${asset.relativePath} (~${kb} KB)`);
|
|
1461
|
+
lines.push(` On disk: ${formatBytes(asset.sizeBytes)}`);
|
|
1462
|
+
lines.push("");
|
|
1342
1463
|
}
|
|
1464
|
+
lines.push(
|
|
1465
|
+
` SUBTOTAL \u2014 ${sorted.length} file(s) \xB7 ${formatBytes(totalBytes)}`,
|
|
1466
|
+
"",
|
|
1467
|
+
RULE3,
|
|
1468
|
+
"",
|
|
1469
|
+
RULE22,
|
|
1470
|
+
" END OF REPORT \u2014 DISK SPACE SUMMARY",
|
|
1471
|
+
RULE22,
|
|
1472
|
+
"",
|
|
1473
|
+
" If you DELETE every unused asset listed above from public/:",
|
|
1474
|
+
"",
|
|
1475
|
+
` Files removed : ${sorted.length}`,
|
|
1476
|
+
` Disk freed : ${formatBytes(totalBytes)} (sum of file sizes on disk)`,
|
|
1477
|
+
"",
|
|
1478
|
+
" This does not include savings from dead code \u2014 see dead-code.txt for source files.",
|
|
1479
|
+
"",
|
|
1480
|
+
RULE22,
|
|
1481
|
+
""
|
|
1482
|
+
);
|
|
1343
1483
|
return lines.join("\n");
|
|
1344
1484
|
}
|
|
1345
1485
|
|
|
1486
|
+
// src/core/agent-prompt.ts
|
|
1487
|
+
function buildPromptBody(meta) {
|
|
1488
|
+
const { prunifyVersion, generatedAtIso, projectRootDisplay, reportsDirDisplay } = meta;
|
|
1489
|
+
return `You are assisting with a **careful, incremental** cleanup of a TypeScript/JavaScript codebase.
|
|
1490
|
+
|
|
1491
|
+
## Context (prunify ${prunifyVersion})
|
|
1492
|
+
|
|
1493
|
+
- **Project root:** ${projectRootDisplay}
|
|
1494
|
+
- **Reports folder:** ${reportsDirDisplay}
|
|
1495
|
+
- **Report generated (this prompt file):** ${generatedAtIso}
|
|
1496
|
+
|
|
1497
|
+
The user has run **prunify** (static analysis). The reports are hints, not ground truth. **False positives are common.**
|
|
1498
|
+
|
|
1499
|
+
## Attach these files to the chat (if they exist)
|
|
1500
|
+
|
|
1501
|
+
From \`${reportsDirDisplay}\`:
|
|
1502
|
+
|
|
1503
|
+
| File | What it means |
|
|
1504
|
+
|------|----------------|
|
|
1505
|
+
| **dead-code.txt** | Orphan files (safe / transitive), dead exports, sizes. **--delete** uses this for whole-file removal. |
|
|
1506
|
+
| **circular.txt** | Import cycles \u2014 usually **refactor**, not delete. |
|
|
1507
|
+
| **dupes.md** | Repeated code **blocks** \u2014 merge only after **semantic** review. |
|
|
1508
|
+
| **deps.md** | package.json vs imports \u2014 **unused** / **missing** / dev issues. |
|
|
1509
|
+
| **assets.md** (+ **assets.txt**) | Unreferenced files under **public/** \u2014 **--delete-assets** uses the report. |
|
|
1510
|
+
|
|
1511
|
+
If a file is missing, skip that section of work.
|
|
1512
|
+
|
|
1513
|
+
---
|
|
1514
|
+
|
|
1515
|
+
## Mandatory safety rules (do not skip)
|
|
1516
|
+
|
|
1517
|
+
1. **No bulk deletes.** Work in small batches; one PR or one chat turn per logical change.
|
|
1518
|
+
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.
|
|
1519
|
+
3. **Dead code \u2014 exports:** Removing an export can break external consumers or barrels. After removal, run typecheck; fix barrel \`index.ts\` re-exports.
|
|
1520
|
+
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.
|
|
1521
|
+
5. **Transitively dead:** Deletion order matters \u2014 follow the report chains; remove importers before importers-only-used-by-dead-code where applicable.
|
|
1522
|
+
6. **Circular imports:** Prefer extracting shared code or inverting dependencies; **do not** "fix" by deleting arbitrary cycle members.
|
|
1523
|
+
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.
|
|
1524
|
+
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\`.
|
|
1525
|
+
9. **deps.md \u2014 missing:** May be transitive or tooling; verify before adding duplicates.
|
|
1526
|
+
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/**.
|
|
1527
|
+
|
|
1528
|
+
---
|
|
1529
|
+
|
|
1530
|
+
## Suggested order (lowest risk first)
|
|
1531
|
+
|
|
1532
|
+
1. **Verify deps.md** \u2014 search the whole repo; adjust package.json only with evidence.
|
|
1533
|
+
2. **Dead exports** \u2014 remove unused exports / tidy barrels; typecheck after each batch.
|
|
1534
|
+
3. **Safe-to-delete files** \u2014 small batches; run build after.
|
|
1535
|
+
4. **Unused assets** \u2014 after string search in app and config.
|
|
1536
|
+
5. **dupes.md** \u2014 last; highest judgment; add tests when merging.
|
|
1537
|
+
|
|
1538
|
+
---
|
|
1539
|
+
|
|
1540
|
+
## After every batch
|
|
1541
|
+
|
|
1542
|
+
- Run **lint** and **typecheck** (and **tests** if available).
|
|
1543
|
+
- Run **production build** before declaring done.
|
|
1544
|
+
- If something fails, **revert** that batch and narrow the change.
|
|
1545
|
+
|
|
1546
|
+
---
|
|
1547
|
+
|
|
1548
|
+
## What not to do
|
|
1549
|
+
|
|
1550
|
+
- Do not delete **pages**, **app routes**, **middleware**, or framework entry files unless the user explicitly asks and you verified they are unused.
|
|
1551
|
+
- Do not remove packages because deps.md says "unused" without a repo-wide check.
|
|
1552
|
+
- Do not merge duplicate blocks from dupes.md without reading surrounding code.
|
|
1553
|
+
|
|
1554
|
+
When uncertain, **list the risk** and ask the user to confirm before editing.`;
|
|
1555
|
+
}
|
|
1556
|
+
function buildAiPromptTsx(meta) {
|
|
1557
|
+
const body = buildPromptBody(meta);
|
|
1558
|
+
const literal = JSON.stringify(body);
|
|
1559
|
+
return `/* eslint-disable */
|
|
1560
|
+
/**
|
|
1561
|
+
* Auto-generated by prunify ${meta.prunifyVersion} \u2014 ${meta.generatedAtIso}
|
|
1562
|
+
* Do not edit by hand; re-run \`npx prunify\` to refresh.
|
|
1563
|
+
*
|
|
1564
|
+
* **For Cursor / Copilot:** attach the files listed inside PRUNIFY_CLEANUP_AGENT_PROMPT
|
|
1565
|
+
* (from prunify-reports/), then paste the prompt or use @-mention on those files plus this file.
|
|
1566
|
+
*/
|
|
1567
|
+
export const PRUNIFY_CLEANUP_AGENT_PROMPT = ${literal} as const
|
|
1568
|
+
|
|
1569
|
+
export default PRUNIFY_CLEANUP_AGENT_PROMPT
|
|
1570
|
+
`;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1346
1573
|
// src/core/report-parser.ts
|
|
1347
1574
|
import fs9 from "fs";
|
|
1348
1575
|
import path9 from "path";
|
|
@@ -1364,7 +1591,7 @@ function parseDeadCodeReportContent(content, rootDir) {
|
|
|
1364
1591
|
return [...new Set(paths)];
|
|
1365
1592
|
}
|
|
1366
1593
|
let section = "none";
|
|
1367
|
-
const fileLineRe = /^\s
|
|
1594
|
+
const fileLineRe = /^\s*(?:•\s+)?(.+?)\s+\(~[\d.]+\s+KB\)\s*$/;
|
|
1368
1595
|
for (const line of lines) {
|
|
1369
1596
|
if (line.includes("\u2500\u2500 SAFE TO DELETE \u2500\u2500")) {
|
|
1370
1597
|
section = "safe";
|
|
@@ -1385,7 +1612,9 @@ function parseDeadCodeReportContent(content, rootDir) {
|
|
|
1385
1612
|
continue;
|
|
1386
1613
|
}
|
|
1387
1614
|
if (section === "safe" || section === "transitive") {
|
|
1388
|
-
if (line.includes("\u2514\u2500") || line.includes("also makes dead"))
|
|
1615
|
+
if (line.includes("\u2514\u2500") || line.includes("\u21B3") || line.includes("also makes dead") || line.includes("SUBTOTAL") || line.includes("\u2500\u2500\u2500")) {
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1389
1618
|
const m = fileLineRe.exec(line);
|
|
1390
1619
|
if (m) {
|
|
1391
1620
|
const rel = m[1].trim();
|
|
@@ -1406,7 +1635,7 @@ function countDeadExportsInReport(content) {
|
|
|
1406
1635
|
continue;
|
|
1407
1636
|
}
|
|
1408
1637
|
if (section && line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) break;
|
|
1409
|
-
if (section && /\
|
|
1638
|
+
if (section && /\(line \d+\)/.test(line)) n++;
|
|
1410
1639
|
}
|
|
1411
1640
|
return n;
|
|
1412
1641
|
}
|
|
@@ -1414,7 +1643,7 @@ function parseDeadCodeReportSummary(content) {
|
|
|
1414
1643
|
const safe = content.match(/Safe to delete\s*:\s*(\d+)/);
|
|
1415
1644
|
const trans = content.match(/Transitively dead\s*:\s*(\d+)/);
|
|
1416
1645
|
const exp = content.match(/Dead exports\s*:\s*(\d+)/);
|
|
1417
|
-
const rec = content.match(/Recoverable
|
|
1646
|
+
const rec = content.match(/Recoverable[^:\n]*:\s*(~[\d.]+\s*KB)/i);
|
|
1418
1647
|
if (!safe || !trans || !exp) return null;
|
|
1419
1648
|
return {
|
|
1420
1649
|
safeToDelete: Number(safe[1]),
|
|
@@ -1582,7 +1811,8 @@ async function main(opts) {
|
|
|
1582
1811
|
}
|
|
1583
1812
|
}
|
|
1584
1813
|
if (loadAssetsFromCache) {
|
|
1585
|
-
|
|
1814
|
+
const assetsTxtPath = path10.join(reportsDir, "assets.txt");
|
|
1815
|
+
assetsReportFile = fs10.existsSync(assetsTxtPath) ? "assets.md, assets.txt" : "assets.md";
|
|
1586
1816
|
unusedAssetCount = unusedAssetPaths.length;
|
|
1587
1817
|
}
|
|
1588
1818
|
if (runDeadCode2) {
|
|
@@ -1654,7 +1884,7 @@ async function main(opts) {
|
|
|
1654
1884
|
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1655
1885
|
unusedAssetPaths = unusedAssets.map((a) => a.filePath);
|
|
1656
1886
|
unusedAssetCount = unusedAssets.length;
|
|
1657
|
-
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1887
|
+
if (unusedAssetCount > 0) assetsReportFile = "assets.md, assets.txt";
|
|
1658
1888
|
}
|
|
1659
1889
|
if (modules.includes("health")) {
|
|
1660
1890
|
const outputPath = path10.join(reportsDir, "health-report.md");
|
|
@@ -1694,6 +1924,16 @@ async function main(opts) {
|
|
|
1694
1924
|
);
|
|
1695
1925
|
console.log(table.toString());
|
|
1696
1926
|
console.log();
|
|
1927
|
+
const aiPromptContent = buildAiPromptTsx({
|
|
1928
|
+
prunifyVersion: PKG_VERSION,
|
|
1929
|
+
generatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1930
|
+
projectRootDisplay: path10.relative(process.cwd(), rootDir) || ".",
|
|
1931
|
+
reportsDirDisplay: path10.relative(rootDir, reportsDir).replaceAll("\\", "/") || "prunify-reports"
|
|
1932
|
+
});
|
|
1933
|
+
const aiPromptPath = path10.join(reportsDir, "ai_prompt.tsx");
|
|
1934
|
+
fs10.writeFileSync(aiPromptPath, aiPromptContent, "utf-8");
|
|
1935
|
+
console.log(chalk7.cyan(` Agent prompt saved \u2192 ${aiPromptPath}`));
|
|
1936
|
+
console.log();
|
|
1697
1937
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1698
1938
|
console.log(chalk7.yellow(`Dead code files to delete (${deadFilePaths.length}):`));
|
|
1699
1939
|
for (const f of deadFilePaths) {
|