prunify 0.1.1 → 0.1.2

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/dist/cli.cjs CHANGED
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli.ts
27
27
  var import_commander = require("commander");
28
- var import_chalk6 = __toESM(require("chalk"), 1);
29
- var import_cli_table35 = __toESM(require("cli-table3"), 1);
30
- var import_node_fs8 = __toESM(require("fs"), 1);
31
- var import_node_path8 = __toESM(require("path"), 1);
28
+ var import_chalk7 = __toESM(require("chalk"), 1);
29
+ var import_cli_table36 = __toESM(require("cli-table3"), 1);
30
+ var import_node_fs9 = __toESM(require("fs"), 1);
31
+ var import_node_path9 = __toESM(require("path"), 1);
32
32
  var import_node_url = require("url");
33
33
  var import_node_readline = __toESM(require("readline"), 1);
34
34
 
@@ -80,7 +80,14 @@ var DEFAULT_IGNORE = [
80
80
  "coverage",
81
81
  "coverage/**",
82
82
  "**/*.test.ts",
83
+ "**/*.test.tsx",
84
+ "**/*.test.js",
85
+ "**/*.test.jsx",
83
86
  "**/*.spec.ts",
87
+ "**/*.spec.tsx",
88
+ "**/*.spec.js",
89
+ "**/*.spec.jsx",
90
+ "**/*.stories.ts",
84
91
  "**/*.stories.tsx",
85
92
  "**/*.d.ts"
86
93
  ];
@@ -225,14 +232,19 @@ function buildGraph(files, getImports) {
225
232
  function findEntryPoints(rootDir, packageJson) {
226
233
  const entries = [
227
234
  ...resolveNextJsEntries(rootDir),
228
- ...resolvePkgFieldEntries(rootDir, packageJson)
235
+ ...resolvePkgFieldEntries(rootDir, packageJson),
236
+ ...resolveFallbackEntries(rootDir)
229
237
  ];
230
- if (entries.length === 0) {
231
- const fallback = resolveFallbackEntry(rootDir);
232
- if (fallback) entries.push(fallback);
233
- }
234
238
  return [...new Set(entries)];
235
239
  }
240
+ function findRootFiles(graph) {
241
+ const imported = /* @__PURE__ */ new Set();
242
+ for (const deps of graph.values()) {
243
+ for (const dep of deps) imported.add(dep);
244
+ }
245
+ const roots = [...graph.keys()].filter((f) => !imported.has(f));
246
+ return roots.length > 0 ? roots : [...graph.keys()];
247
+ }
236
248
  function runDFS(graph, entryPoints) {
237
249
  const visited = /* @__PURE__ */ new Set();
238
250
  const stack = [...entryPoints];
@@ -259,11 +271,11 @@ function detectCycles(graph) {
259
271
  const seenKeys = /* @__PURE__ */ new Set();
260
272
  const visited = /* @__PURE__ */ new Set();
261
273
  const inStack = /* @__PURE__ */ new Set();
262
- const path9 = [];
274
+ const path10 = [];
263
275
  const acc = { seenKeys, cycles };
264
276
  for (const start of graph.keys()) {
265
277
  if (!visited.has(start)) {
266
- dfsForCycles(start, graph, visited, inStack, path9, acc);
278
+ dfsForCycles(start, graph, visited, inStack, path10, acc);
267
279
  }
268
280
  }
269
281
  return cycles;
@@ -288,18 +300,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
288
300
  }
289
301
  return entries;
290
302
  }
291
- function resolveFallbackEntry(rootDir) {
292
- const fallbacks = ["src/main.ts", "src/main.tsx", "src/index.ts", "src/index.tsx"];
293
- for (const rel of fallbacks) {
294
- const abs = import_node_path3.default.join(rootDir, rel);
295
- if (import_node_fs3.default.existsSync(abs)) return abs;
296
- }
297
- return void 0;
303
+ function resolveFallbackEntries(rootDir) {
304
+ const candidates = [
305
+ "src/main.ts",
306
+ "src/main.tsx",
307
+ "src/main.js",
308
+ "src/main.jsx",
309
+ "src/index.ts",
310
+ "src/index.tsx",
311
+ "src/index.js",
312
+ "src/index.jsx",
313
+ "src/App.ts",
314
+ "src/App.tsx",
315
+ "src/App.js",
316
+ "src/App.jsx",
317
+ "index.ts",
318
+ "index.tsx",
319
+ "index.js",
320
+ "index.jsx"
321
+ ];
322
+ return candidates.map((rel) => import_node_path3.default.join(rootDir, rel)).filter((abs) => import_node_fs3.default.existsSync(abs));
298
323
  }
299
324
  function mkFrame(node, graph) {
300
325
  return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
301
326
  }
302
- function dfsForCycles(start, graph, visited, inStack, path9, acc) {
327
+ function dfsForCycles(start, graph, visited, inStack, path10, acc) {
303
328
  const stack = [mkFrame(start, graph)];
304
329
  while (stack.length > 0) {
305
330
  const frame = stack.at(-1);
@@ -311,30 +336,30 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
311
336
  }
312
337
  frame.entered = true;
313
338
  inStack.add(frame.node);
314
- path9.push(frame.node);
339
+ path10.push(frame.node);
315
340
  }
316
341
  const { done, value: neighbor } = frame.neighbors.next();
317
342
  if (done) {
318
343
  stack.pop();
319
- path9.pop();
344
+ path10.pop();
320
345
  inStack.delete(frame.node);
321
346
  visited.add(frame.node);
322
347
  } else {
323
- handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph);
348
+ handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
324
349
  }
325
350
  }
326
351
  }
327
- function handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph) {
352
+ function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
328
353
  if (inStack.has(neighbor)) {
329
- recordCycle(neighbor, path9, acc);
354
+ recordCycle(neighbor, path10, acc);
330
355
  } else if (!visited.has(neighbor)) {
331
356
  stack.push(mkFrame(neighbor, graph));
332
357
  }
333
358
  }
334
- function recordCycle(cycleStart, path9, acc) {
335
- const idx = path9.indexOf(cycleStart);
359
+ function recordCycle(cycleStart, path10, acc) {
360
+ const idx = path10.indexOf(cycleStart);
336
361
  if (idx === -1) return;
337
- const cycle = normalizeCycle(path9.slice(idx));
362
+ const cycle = normalizeCycle(path10.slice(idx));
338
363
  const key = cycle.join("\0");
339
364
  if (!acc.seenKeys.has(key)) {
340
365
  acc.seenKeys.add(key);
@@ -490,7 +515,7 @@ function ensureDir(dir) {
490
515
  // src/modules/dead-code.ts
491
516
  function runDeadCodeModule(project, graph, entryPoints, rootDir) {
492
517
  const allFiles = [...graph.keys()];
493
- const effectiveEntries = entryPoints.length > 0 ? entryPoints : allFiles.slice(0, 1);
518
+ const effectiveEntries = entryPoints.length > 0 ? entryPoints : findRootFiles(graph);
494
519
  const liveFiles = runDFS(graph, effectiveEntries);
495
520
  const deadFiles = allFiles.filter((f) => !liveFiles.has(f));
496
521
  const deadSet = new Set(deadFiles);
@@ -987,27 +1012,219 @@ async function runHealthReport(dir, opts) {
987
1012
  );
988
1013
  }
989
1014
 
1015
+ // src/modules/assets.ts
1016
+ var import_node_fs8 = __toESM(require("fs"), 1);
1017
+ var import_node_path8 = __toESM(require("path"), 1);
1018
+ var import_ora6 = __toESM(require("ora"), 1);
1019
+ var import_chalk6 = __toESM(require("chalk"), 1);
1020
+ var import_cli_table35 = __toESM(require("cli-table3"), 1);
1021
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
1022
+ ".png",
1023
+ ".jpg",
1024
+ ".jpeg",
1025
+ ".gif",
1026
+ ".svg",
1027
+ ".webp",
1028
+ ".avif",
1029
+ ".ico",
1030
+ ".bmp",
1031
+ ".woff",
1032
+ ".woff2",
1033
+ ".ttf",
1034
+ ".eot",
1035
+ ".otf",
1036
+ ".mp4",
1037
+ ".webm",
1038
+ ".ogg",
1039
+ ".mp3",
1040
+ ".wav",
1041
+ ".pdf"
1042
+ ]);
1043
+ var SOURCE_PATTERNS2 = [
1044
+ "**/*.ts",
1045
+ "**/*.tsx",
1046
+ "**/*.js",
1047
+ "**/*.jsx",
1048
+ "**/*.css",
1049
+ "**/*.scss",
1050
+ "**/*.sass",
1051
+ "**/*.less",
1052
+ "**/*.html",
1053
+ "**/*.json"
1054
+ ];
1055
+ var SOURCE_IGNORE = [
1056
+ "node_modules",
1057
+ "node_modules/**",
1058
+ "dist",
1059
+ "dist/**",
1060
+ ".next",
1061
+ ".next/**",
1062
+ "coverage",
1063
+ "coverage/**",
1064
+ "public",
1065
+ "public/**"
1066
+ ];
1067
+ function runAssetCheckModule(rootDir) {
1068
+ const publicDir = import_node_path8.default.join(rootDir, "public");
1069
+ if (!import_node_fs8.default.existsSync(publicDir)) {
1070
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1071
+ }
1072
+ const assets = collectAssets(publicDir);
1073
+ if (assets.length === 0) {
1074
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1075
+ }
1076
+ const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
1077
+ const sourceContent = sourceFiles.reduce((acc, f) => {
1078
+ try {
1079
+ return acc + import_node_fs8.default.readFileSync(f, "utf-8") + "\n";
1080
+ } catch {
1081
+ return acc;
1082
+ }
1083
+ }, "");
1084
+ const unused = [];
1085
+ for (const assetAbs of assets) {
1086
+ const fileName = import_node_path8.default.basename(assetAbs);
1087
+ const relFromPublic = "/" + import_node_path8.default.relative(publicDir, assetAbs).replaceAll("\\", "/");
1088
+ const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
1089
+ if (!referenced) {
1090
+ unused.push({
1091
+ filePath: assetAbs,
1092
+ relativePath: import_node_path8.default.relative(rootDir, assetAbs).replaceAll("\\", "/"),
1093
+ sizeBytes: getFileSize2(assetAbs)
1094
+ });
1095
+ }
1096
+ }
1097
+ const report = buildAssetReport(unused, assets.length, rootDir);
1098
+ return { unusedAssets: unused, totalAssets: assets.length, report };
1099
+ }
1100
+ async function runAssetCheck(rootDir, opts) {
1101
+ const publicDir = import_node_path8.default.join(rootDir, "public");
1102
+ if (!import_node_fs8.default.existsSync(publicDir)) {
1103
+ console.log(import_chalk6.default.dim(" No public/ folder found \u2014 skipping asset check"));
1104
+ return [];
1105
+ }
1106
+ const spinner = (0, import_ora6.default)(import_chalk6.default.cyan("Scanning public/ for unused assets\u2026")).start();
1107
+ try {
1108
+ const result = runAssetCheckModule(rootDir);
1109
+ spinner.succeed(
1110
+ import_chalk6.default.green(
1111
+ `Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
1112
+ )
1113
+ );
1114
+ if (result.unusedAssets.length === 0) {
1115
+ console.log(import_chalk6.default.green(" All public assets are referenced in source."));
1116
+ return [];
1117
+ }
1118
+ const table = new import_cli_table35.default({ head: ["Asset", "Size"] });
1119
+ for (const asset of result.unusedAssets) {
1120
+ const kb = (asset.sizeBytes / 1024).toFixed(1);
1121
+ table.push([import_chalk6.default.gray(asset.relativePath), `${kb} KB`]);
1122
+ }
1123
+ console.log(table.toString());
1124
+ if (opts.output) {
1125
+ writeMarkdown(
1126
+ {
1127
+ title: "Unused Assets Report",
1128
+ summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
1129
+ sections: [
1130
+ {
1131
+ title: "Unused Assets",
1132
+ headers: ["Asset", "Size (KB)"],
1133
+ rows: result.unusedAssets.map((a) => [
1134
+ a.relativePath,
1135
+ (a.sizeBytes / 1024).toFixed(1)
1136
+ ])
1137
+ }
1138
+ ],
1139
+ generatedAt: /* @__PURE__ */ new Date()
1140
+ },
1141
+ opts.output
1142
+ );
1143
+ console.log(import_chalk6.default.cyan(` Report written to ${opts.output}`));
1144
+ }
1145
+ return result.unusedAssets;
1146
+ } catch (err) {
1147
+ spinner.fail(import_chalk6.default.red("Asset scan failed"));
1148
+ throw err;
1149
+ }
1150
+ }
1151
+ function collectAssets(dir) {
1152
+ const results = [];
1153
+ function walk(current) {
1154
+ let entries;
1155
+ try {
1156
+ entries = import_node_fs8.default.readdirSync(current, { withFileTypes: true });
1157
+ } catch {
1158
+ return;
1159
+ }
1160
+ for (const entry of entries) {
1161
+ const full = import_node_path8.default.join(current, entry.name);
1162
+ if (entry.isDirectory()) {
1163
+ walk(full);
1164
+ } else if (entry.isFile() && ASSET_EXTENSIONS.has(import_node_path8.default.extname(entry.name).toLowerCase())) {
1165
+ results.push(full);
1166
+ }
1167
+ }
1168
+ }
1169
+ walk(dir);
1170
+ return results;
1171
+ }
1172
+ function getFileSize2(filePath) {
1173
+ try {
1174
+ return import_node_fs8.default.statSync(filePath).size;
1175
+ } catch {
1176
+ return 0;
1177
+ }
1178
+ }
1179
+ function buildAssetReport(unused, totalAssets, rootDir) {
1180
+ const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
1181
+ const totalKb = (totalBytes / 1024).toFixed(1);
1182
+ const lines = [
1183
+ "========================================",
1184
+ " UNUSED ASSETS REPORT",
1185
+ ` Total assets : ${totalAssets}`,
1186
+ ` Unused assets : ${unused.length}`,
1187
+ ` Recoverable : ~${totalKb} KB`,
1188
+ "========================================",
1189
+ ""
1190
+ ];
1191
+ if (unused.length === 0) {
1192
+ lines.push(" All public assets are referenced in source.", "");
1193
+ return lines.join("\n");
1194
+ }
1195
+ lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
1196
+ for (const asset of unused) {
1197
+ lines.push(
1198
+ `UNUSED \u2014 ${asset.relativePath}`,
1199
+ `Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
1200
+ `Action: Safe to delete if not served directly via URL`,
1201
+ ""
1202
+ );
1203
+ }
1204
+ return lines.join("\n");
1205
+ }
1206
+
990
1207
  // src/cli.ts
991
1208
  var import_meta = {};
992
1209
  function readPkgVersion() {
993
1210
  try {
994
1211
  if (typeof import_meta !== "undefined" && import_meta.url) {
995
- const dir = import_node_path8.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
996
- const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
997
- return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
1212
+ const dir = import_node_path9.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1213
+ const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
1214
+ return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
998
1215
  }
999
1216
  } catch {
1000
1217
  }
1001
1218
  try {
1002
1219
  const dir = globalThis.__dirname ?? __dirname;
1003
- const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
1004
- return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
1220
+ const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
1221
+ return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
1005
1222
  } catch {
1006
1223
  return "0.0.0";
1007
1224
  }
1008
1225
  }
1009
1226
  var PKG_VERSION = readPkgVersion();
1010
- var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
1227
+ var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
1011
1228
  var program = new import_commander.Command();
1012
1229
  program.name("prunify").description("npm run clean. ship with confidence.").version(PKG_VERSION, "-v, --version").option("--dir <path>", "Root directory to analyze", process.cwd()).option("--entry <path>", "Override entry point").option("--only <modules>", "Comma-separated: dead-code,dupes,circular,deps,health").option(
1013
1230
  "--ignore <pattern>",
@@ -1017,29 +1234,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
1017
1234
  ).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "Prompt to delete dead files after analysis").option("--ci", "CI mode: exit 1 if issues found, no interactive prompts").action(main);
1018
1235
  program.parse();
1019
1236
  async function main(opts) {
1020
- const rootDir = import_node_path8.default.resolve(opts.dir);
1021
- if (!import_node_fs8.default.existsSync(import_node_path8.default.join(rootDir, "package.json"))) {
1022
- console.error(import_chalk6.default.red(`\u2717 No package.json found in ${rootDir}`));
1023
- console.error(import_chalk6.default.dim(" Use --dir <path> to point to your project root."));
1237
+ const rootDir = import_node_path9.default.resolve(opts.dir);
1238
+ if (!import_node_fs9.default.existsSync(import_node_path9.default.join(rootDir, "package.json"))) {
1239
+ console.error(import_chalk7.default.red(`\u2717 No package.json found in ${rootDir}`));
1240
+ console.error(import_chalk7.default.dim(" Use --dir <path> to point to your project root."));
1024
1241
  process.exit(1);
1025
1242
  }
1026
1243
  const modules = resolveModules(opts.only);
1027
1244
  console.log();
1028
- console.log(import_chalk6.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1245
+ console.log(import_chalk7.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1029
1246
  console.log();
1030
- const parseSpinner = createSpinner(import_chalk6.default.cyan("Parsing codebase\u2026"));
1247
+ const parseSpinner = createSpinner(import_chalk7.default.cyan("Parsing codebase\u2026"));
1031
1248
  const files = discoverFiles(rootDir, opts.ignore);
1032
- parseSpinner.succeed(import_chalk6.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1033
- const graphSpinner = createSpinner(import_chalk6.default.cyan("Building import graph\u2026"));
1249
+ parseSpinner.succeed(import_chalk7.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1250
+ const graphSpinner = createSpinner(import_chalk7.default.cyan("Building import graph\u2026"));
1034
1251
  const project = buildProject(files);
1035
1252
  const graph = buildGraph(files, (f) => {
1036
1253
  const sf = project.getSourceFile(f);
1037
1254
  return sf ? getImportsForFile(sf) : [];
1038
1255
  });
1039
1256
  const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
1040
- graphSpinner.succeed(import_chalk6.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1257
+ graphSpinner.succeed(import_chalk7.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1041
1258
  const packageJson = loadPackageJson2(rootDir);
1042
- const entryPoints = opts.entry ? [import_node_path8.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1259
+ const entryPoints = opts.entry ? [import_node_path9.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1043
1260
  const reportsDir = ensureReportsDir(rootDir, opts.out);
1044
1261
  appendToGitignore(rootDir);
1045
1262
  console.log();
@@ -1047,33 +1264,35 @@ async function main(opts) {
1047
1264
  let dupeCount = 0;
1048
1265
  let unusedPkgCount = 0;
1049
1266
  let circularCount = 0;
1267
+ let unusedAssetCount = 0;
1050
1268
  let deadReportFile = "";
1051
1269
  let dupesReportFile = "";
1052
1270
  let depsReportFile = "";
1053
1271
  let circularReportFile = "";
1272
+ let assetsReportFile = "";
1054
1273
  const deadFilePaths = [];
1055
1274
  if (modules.includes("dead-code")) {
1056
- const spinner = createSpinner(import_chalk6.default.cyan("Analysing dead code\u2026"));
1275
+ const spinner = createSpinner(import_chalk7.default.cyan("Analysing dead code\u2026"));
1057
1276
  const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
1058
1277
  deadFileCount = result.deadFiles.length + result.deadExports.length;
1059
1278
  deadFilePaths.push(...result.deadFiles);
1060
- spinner.succeed(import_chalk6.default.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
1279
+ spinner.succeed(import_chalk7.default.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
1061
1280
  if (result.report) {
1062
1281
  deadReportFile = "dead-code.txt";
1063
1282
  writeReport(reportsDir, deadReportFile, result.report);
1064
1283
  }
1065
1284
  }
1066
1285
  if (modules.includes("dupes")) {
1067
- const outputPath = import_node_path8.default.join(reportsDir, "dupes.md");
1286
+ const outputPath = import_node_path9.default.join(reportsDir, "dupes.md");
1068
1287
  const dupes = await runDupeFinder(rootDir, { output: outputPath });
1069
1288
  dupeCount = dupes.length;
1070
1289
  if (dupeCount > 0) dupesReportFile = "dupes.md";
1071
1290
  }
1072
1291
  if (modules.includes("circular")) {
1073
- const spinner = createSpinner(import_chalk6.default.cyan("Analysing circular imports\u2026"));
1292
+ const spinner = createSpinner(import_chalk7.default.cyan("Analysing circular imports\u2026"));
1074
1293
  const cycles = detectCycles(graph);
1075
1294
  circularCount = cycles.length;
1076
- spinner.succeed(import_chalk6.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1295
+ spinner.succeed(import_chalk7.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1077
1296
  if (circularCount > 0) {
1078
1297
  circularReportFile = "circular.txt";
1079
1298
  const cycleText = cycles.map((c, i) => `Cycle ${i + 1}: ${c.join(" \u2192 ")}`).join("\n");
@@ -1081,56 +1300,63 @@ async function main(opts) {
1081
1300
  }
1082
1301
  }
1083
1302
  if (modules.includes("deps")) {
1084
- const outputPath = import_node_path8.default.join(reportsDir, "deps.md");
1303
+ const outputPath = import_node_path9.default.join(reportsDir, "deps.md");
1085
1304
  const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
1086
1305
  unusedPkgCount = issues.filter((i) => i.type === "unused").length;
1087
1306
  if (issues.length > 0) depsReportFile = "deps.md";
1088
1307
  }
1308
+ if (modules.includes("assets")) {
1309
+ const outputPath = import_node_path9.default.join(reportsDir, "assets.md");
1310
+ const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
1311
+ unusedAssetCount = unusedAssets.length;
1312
+ if (unusedAssetCount > 0) assetsReportFile = "assets.md";
1313
+ }
1089
1314
  if (modules.includes("health")) {
1090
- const outputPath = import_node_path8.default.join(reportsDir, "health-report.md");
1315
+ const outputPath = import_node_path9.default.join(reportsDir, "health-report.md");
1091
1316
  await runHealthReport(rootDir, { output: outputPath });
1092
1317
  }
1093
1318
  if (opts.html) {
1094
- const htmlPath = import_node_path8.default.join(reportsDir, "code_health.html");
1319
+ const htmlPath = import_node_path9.default.join(reportsDir, "code_health.html");
1095
1320
  writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
1096
- console.log(import_chalk6.default.cyan(` HTML report written to ${htmlPath}`));
1321
+ console.log(import_chalk7.default.cyan(` HTML report written to ${htmlPath}`));
1097
1322
  }
1098
1323
  console.log();
1099
- console.log(import_chalk6.default.bold("Summary"));
1324
+ console.log(import_chalk7.default.bold("Summary"));
1100
1325
  console.log();
1101
- const table = new import_cli_table35.default({
1102
- head: [import_chalk6.default.bold("Check"), import_chalk6.default.bold("Found"), import_chalk6.default.bold("Output File")],
1326
+ const table = new import_cli_table36.default({
1327
+ head: [import_chalk7.default.bold("Check"), import_chalk7.default.bold("Found"), import_chalk7.default.bold("Output File")],
1103
1328
  style: { head: [], border: [] }
1104
1329
  });
1105
- const fmt = (n) => n > 0 ? import_chalk6.default.yellow(String(n)) : import_chalk6.default.green("0");
1330
+ const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
1106
1331
  table.push(
1107
1332
  ["Dead Files / Exports", fmt(deadFileCount), deadReportFile || "\u2014"],
1108
1333
  ["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
1109
1334
  ["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
1110
- ["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"]
1335
+ ["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"],
1336
+ ["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
1111
1337
  );
1112
1338
  console.log(table.toString());
1113
1339
  console.log();
1114
1340
  if (opts.delete && deadFilePaths.length > 0) {
1115
- console.log(import_chalk6.default.yellow(`Dead files (${deadFilePaths.length}):`));
1341
+ console.log(import_chalk7.default.yellow(`Dead files (${deadFilePaths.length}):`));
1116
1342
  for (const f of deadFilePaths) {
1117
- console.log(import_chalk6.default.dim(` ${import_node_path8.default.relative(rootDir, f)}`));
1343
+ console.log(import_chalk7.default.dim(` ${import_node_path9.default.relative(rootDir, f)}`));
1118
1344
  }
1119
1345
  console.log();
1120
1346
  if (!opts.ci) {
1121
1347
  const confirmed = await confirmPrompt("Delete these files? (y/N) ");
1122
1348
  if (confirmed) {
1123
1349
  for (const f of deadFilePaths) {
1124
- import_node_fs8.default.rmSync(f, { force: true });
1350
+ import_node_fs9.default.rmSync(f, { force: true });
1125
1351
  }
1126
- console.log(import_chalk6.default.green(` Deleted ${deadFilePaths.length} file(s).`));
1352
+ console.log(import_chalk7.default.green(` Deleted ${deadFilePaths.length} file(s).`));
1127
1353
  } else {
1128
- console.log(import_chalk6.default.dim(" Skipped."));
1354
+ console.log(import_chalk7.default.dim(" Skipped."));
1129
1355
  }
1130
1356
  }
1131
1357
  }
1132
1358
  if (opts.ci) {
1133
- const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
1359
+ const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
1134
1360
  if (hasIssues) process.exit(1);
1135
1361
  }
1136
1362
  }
@@ -1141,7 +1367,7 @@ function resolveModules(only) {
1141
1367
  }
1142
1368
  function loadPackageJson2(dir) {
1143
1369
  try {
1144
- return JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(dir, "package.json"), "utf-8"));
1370
+ return JSON.parse(import_node_fs9.default.readFileSync(import_node_path9.default.join(dir, "package.json"), "utf-8"));
1145
1371
  } catch {
1146
1372
  return null;
1147
1373
  }
@@ -1162,7 +1388,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
1162
1388
  ["Circular Dependencies", String(circularCount)],
1163
1389
  ["Unused Packages", String(unusedPkgCount)]
1164
1390
  ].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
1165
- const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path8.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1391
+ const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path9.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1166
1392
  const html = `<!DOCTYPE html>
1167
1393
  <html lang="en">
1168
1394
  <head>
@@ -1189,7 +1415,7 @@ ${rows}
1189
1415
  ${deadList}
1190
1416
  </body>
1191
1417
  </html>`;
1192
- import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(outputPath), { recursive: true });
1193
- import_node_fs8.default.writeFileSync(outputPath, html, "utf-8");
1418
+ import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(outputPath), { recursive: true });
1419
+ import_node_fs9.default.writeFileSync(outputPath, html, "utf-8");
1194
1420
  }
1195
1421
  //# sourceMappingURL=cli.cjs.map