node-modules-hunter 1.0.0 → 1.1.0
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 +21 -1
- package/dist/cli.js +688 -206
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { cac } from "cac";
|
|
5
|
-
import
|
|
5
|
+
import path25 from "path";
|
|
6
6
|
|
|
7
7
|
// src/utils/logger.ts
|
|
8
8
|
import pc from "picocolors";
|
|
@@ -354,8 +354,13 @@ async function writeDeleteLog(payload2) {
|
|
|
354
354
|
entries: payload2.results.map((row) => ({
|
|
355
355
|
path: row.path,
|
|
356
356
|
status: row.status,
|
|
357
|
-
error: row.error ?? null
|
|
358
|
-
|
|
357
|
+
error: row.error ?? null,
|
|
358
|
+
// v1.1 additive: which trash took it (system | nmh) — trash rows only
|
|
359
|
+
...row.backend !== void 0 ? { backend: row.backend } : {}
|
|
360
|
+
})),
|
|
361
|
+
// v1.1 additive: nmh-trash batch dir / fallback marker when applicable
|
|
362
|
+
...payload2.trashBatch !== void 0 ? { trashBatch: payload2.trashBatch } : {},
|
|
363
|
+
...payload2.trashFallback === true ? { trashFallback: true } : {}
|
|
359
364
|
};
|
|
360
365
|
await fs3.writeFile(file, JSON.stringify(body, null, 2));
|
|
361
366
|
return file;
|
|
@@ -558,6 +563,11 @@ function formatBytes(bytes) {
|
|
|
558
563
|
}
|
|
559
564
|
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
560
565
|
}
|
|
566
|
+
function formatIdle(days) {
|
|
567
|
+
if (days === void 0) return "\u2014";
|
|
568
|
+
if (days === 0) return "now";
|
|
569
|
+
return `${days.toLocaleString("en-US")}d`;
|
|
570
|
+
}
|
|
561
571
|
function formatAge(mtimeMs, now = Date.now()) {
|
|
562
572
|
if (mtimeMs == null) return "\u2014";
|
|
563
573
|
const diff = Math.max(0, now - mtimeMs);
|
|
@@ -692,9 +702,14 @@ function makeSearchMatcher(pattern) {
|
|
|
692
702
|
// src/core/walker.ts
|
|
693
703
|
import fs6 from "fs/promises";
|
|
694
704
|
import path6 from "path";
|
|
705
|
+
var SYSTEM_SKIP_NAMES = /* @__PURE__ */ new Set([".Trash", ".Trashes", "Library"]);
|
|
706
|
+
function isSystemNoise(name) {
|
|
707
|
+
return SYSTEM_SKIP_NAMES.has(name) || name.startsWith(".Trash-") || name.endsWith(".app");
|
|
708
|
+
}
|
|
695
709
|
async function scanForNodeModules(roots, options = {}) {
|
|
696
710
|
const maxDepth = options.maxDepth ?? Number.POSITIVE_INFINITY;
|
|
697
711
|
const recursive = options.recursive === true;
|
|
712
|
+
const skipSystem = options.skipSystem !== false;
|
|
698
713
|
const isExcluded = options.excludes && options.excludes.length > 0 ? makeGlobMatcher(options.excludes) : null;
|
|
699
714
|
const entries = [];
|
|
700
715
|
const permissionIssues = [];
|
|
@@ -730,6 +745,7 @@ async function scanForNodeModules(roots, options = {}) {
|
|
|
730
745
|
}
|
|
731
746
|
for (const dirent of dirents) {
|
|
732
747
|
if (dirent.name === ".git") continue;
|
|
748
|
+
if (skipSystem && isSystemNoise(dirent.name)) continue;
|
|
733
749
|
const child = path6.join(job.dir, dirent.name);
|
|
734
750
|
const st = await fs6.lstat(child).catch(() => null);
|
|
735
751
|
if (st === null) continue;
|
|
@@ -1000,11 +1016,11 @@ async function measureEntry(entryPath, permIssues = []) {
|
|
|
1000
1016
|
);
|
|
1001
1017
|
}
|
|
1002
1018
|
const packageCount = await countTopLevelPackages(entryPath);
|
|
1003
|
-
const
|
|
1019
|
+
const stat2 = await fs8.lstat(entryPath).then(
|
|
1004
1020
|
(value) => value,
|
|
1005
1021
|
() => null
|
|
1006
1022
|
);
|
|
1007
|
-
return { size, packageCount, mtimeMs:
|
|
1023
|
+
return { size, packageCount, mtimeMs: stat2?.mtimeMs ?? null };
|
|
1008
1024
|
}
|
|
1009
1025
|
async function countTopLevelPackages(entryPath) {
|
|
1010
1026
|
return (await listTopLevelPackages(entryPath)).length;
|
|
@@ -1031,8 +1047,105 @@ async function measureEntries(entryPaths, permIssues = [], options = {}) {
|
|
|
1031
1047
|
return results;
|
|
1032
1048
|
}
|
|
1033
1049
|
|
|
1034
|
-
// src/core/
|
|
1050
|
+
// src/core/du-size.ts
|
|
1051
|
+
import { promisify } from "util";
|
|
1052
|
+
import { execFile } from "child_process";
|
|
1035
1053
|
import fs9 from "fs/promises";
|
|
1054
|
+
var execFileP = promisify(execFile);
|
|
1055
|
+
async function measureEntryFast(entryPath) {
|
|
1056
|
+
try {
|
|
1057
|
+
const { stdout } = await execFileP("du", ["-sk", "--", entryPath], {
|
|
1058
|
+
maxBuffer: 8 * 1024,
|
|
1059
|
+
timeout: 3e4
|
|
1060
|
+
});
|
|
1061
|
+
const line = stdout.split("\n").find((value) => value.includes(" ")) ?? "";
|
|
1062
|
+
const tabIndex = line.lastIndexOf(" ");
|
|
1063
|
+
const kb = Number.parseInt(line.slice(0, tabIndex).trim(), 10);
|
|
1064
|
+
const echoed = line.slice(tabIndex + 1);
|
|
1065
|
+
if (!Number.isInteger(kb) || kb < 0 || echoed !== entryPath) return null;
|
|
1066
|
+
const [packageCount, stat2] = await Promise.all([
|
|
1067
|
+
countTopLevelPackages(entryPath),
|
|
1068
|
+
fs9.lstat(entryPath).catch(() => null)
|
|
1069
|
+
]);
|
|
1070
|
+
return { size: kb * 1024, packageCount, mtimeMs: stat2?.mtimeMs ?? null };
|
|
1071
|
+
} catch {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/core/activity.ts
|
|
1077
|
+
import { promisify as promisify2 } from "util";
|
|
1078
|
+
import { execFile as execFile2 } from "child_process";
|
|
1079
|
+
import { stat } from "fs/promises";
|
|
1080
|
+
import { dirname, join } from "path";
|
|
1081
|
+
var execFileP2 = promisify2(execFile2);
|
|
1082
|
+
var DAY_MS = 864e5;
|
|
1083
|
+
var GIT_TIMEOUT_MS = 4e3;
|
|
1084
|
+
async function hasGitDir(dir) {
|
|
1085
|
+
try {
|
|
1086
|
+
await stat(join(dir, ".git"));
|
|
1087
|
+
return true;
|
|
1088
|
+
} catch {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async function mtimeOf(dir, file) {
|
|
1093
|
+
try {
|
|
1094
|
+
return (await stat(join(dir, file))).mtimeMs;
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function findUp(start, test) {
|
|
1100
|
+
let dir = start;
|
|
1101
|
+
for (; ; ) {
|
|
1102
|
+
if (await test(dir)) return dir;
|
|
1103
|
+
const parent = dirname(dir);
|
|
1104
|
+
if (parent === dir) return null;
|
|
1105
|
+
dir = parent;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function lastCommitSeconds(gitDir) {
|
|
1109
|
+
try {
|
|
1110
|
+
const { stdout } = await execFileP2(
|
|
1111
|
+
"git",
|
|
1112
|
+
["-C", gitDir, "log", "-1", "--format=%ct"],
|
|
1113
|
+
{ maxBuffer: 64 * 1024, timeout: GIT_TIMEOUT_MS }
|
|
1114
|
+
);
|
|
1115
|
+
const seconds = Number.parseInt(stdout.trim(), 10);
|
|
1116
|
+
return Number.isNaN(seconds) ? null : seconds;
|
|
1117
|
+
} catch {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async function computeActivity(entryPath, now = Date.now()) {
|
|
1122
|
+
const startDir = dirname(entryPath);
|
|
1123
|
+
const gitDir = await findUp(startDir, hasGitDir);
|
|
1124
|
+
if (gitDir !== null) {
|
|
1125
|
+
const seconds = await lastCommitSeconds(gitDir);
|
|
1126
|
+
if (seconds !== null) {
|
|
1127
|
+
return { idleDays: idleFrom(seconds * 1e3, now), source: "git" };
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const manifestMtime = await findUp(startDir, async (dir) => await mtimeOf(dir, "package.json") !== null);
|
|
1131
|
+
if (manifestMtime !== null) {
|
|
1132
|
+
const mtime = await mtimeOf(manifestMtime, "package.json");
|
|
1133
|
+
if (mtime !== null) {
|
|
1134
|
+
return { idleDays: idleFrom(mtime, now), source: "manifest" };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const folderMtime = await mtimeOf(startDir, "node_modules");
|
|
1138
|
+
if (folderMtime !== null) {
|
|
1139
|
+
return { idleDays: idleFrom(folderMtime, now), source: "mtime" };
|
|
1140
|
+
}
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
function idleFrom(signalMs, now) {
|
|
1144
|
+
return Math.max(0, Math.floor((now - signalMs) / DAY_MS));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/core/pm-detect.ts
|
|
1148
|
+
import fs10 from "fs/promises";
|
|
1036
1149
|
import path9 from "path";
|
|
1037
1150
|
var LOCKFILE_PRIORITY = [
|
|
1038
1151
|
{ family: "pnpm", files: ["pnpm-workspace.yaml", "pnpm-lock.yaml"] },
|
|
@@ -1113,7 +1226,7 @@ async function detectLockfilesAt(dir) {
|
|
|
1113
1226
|
}
|
|
1114
1227
|
async function fileExists(p) {
|
|
1115
1228
|
try {
|
|
1116
|
-
const st = await
|
|
1229
|
+
const st = await fs10.stat(p);
|
|
1117
1230
|
return st.isFile();
|
|
1118
1231
|
} catch {
|
|
1119
1232
|
return false;
|
|
@@ -1150,9 +1263,9 @@ function knownFamily(name) {
|
|
|
1150
1263
|
// src/core/guards.ts
|
|
1151
1264
|
import os2 from "os";
|
|
1152
1265
|
import path10 from "path";
|
|
1153
|
-
import
|
|
1266
|
+
import fs11 from "fs/promises";
|
|
1154
1267
|
async function checkDeleteTarget(candidate) {
|
|
1155
|
-
const st = await
|
|
1268
|
+
const st = await fs11.lstat(candidate).then(
|
|
1156
1269
|
(value) => value,
|
|
1157
1270
|
() => null
|
|
1158
1271
|
);
|
|
@@ -1229,13 +1342,23 @@ function renderScanResult(result, options) {
|
|
|
1229
1342
|
if (options.sizeMode === "total" || isSilent()) return renderScanTotals(result);
|
|
1230
1343
|
return renderScanTable(result, options);
|
|
1231
1344
|
}
|
|
1345
|
+
var BAR_CELLS = 12;
|
|
1346
|
+
function renderBar(size, max) {
|
|
1347
|
+
if (max <= 0 || size <= 0) return colors.dim("\xB7");
|
|
1348
|
+
const cells = Math.max(1, Math.round(size / max * BAR_CELLS));
|
|
1349
|
+
return colors.cyan("\u2588".repeat(Math.min(cells, BAR_CELLS)));
|
|
1350
|
+
}
|
|
1232
1351
|
function renderScanTable(result, options) {
|
|
1352
|
+
const showIdle = result.entries.some((entry) => entry.idleDays !== void 0);
|
|
1353
|
+
const maxSize = result.entries.reduce((max, entry) => entry.nested === true ? max : Math.max(max, entry.size), 0);
|
|
1233
1354
|
const table = new Table({
|
|
1234
1355
|
head: [
|
|
1235
1356
|
colors.dim("Path"),
|
|
1236
1357
|
colors.dim("Size"),
|
|
1358
|
+
colors.dim("Share"),
|
|
1237
1359
|
colors.dim("Pkgs"),
|
|
1238
1360
|
colors.dim("PM"),
|
|
1361
|
+
...showIdle ? [colors.dim("Idle")] : [],
|
|
1239
1362
|
colors.dim("Modified")
|
|
1240
1363
|
],
|
|
1241
1364
|
style: { head: [], border: colorEnabled() ? ["gray"] : [] }
|
|
@@ -1246,8 +1369,10 @@ function renderScanTable(result, options) {
|
|
|
1246
1369
|
table.push([
|
|
1247
1370
|
label,
|
|
1248
1371
|
formatBytes(entry.size),
|
|
1372
|
+
nested ? colors.dim("\xB7") : renderBar(entry.size, maxSize),
|
|
1249
1373
|
entry.packageCount > 0 ? String(entry.packageCount) : "0",
|
|
1250
1374
|
pmLabel(entry.pm),
|
|
1375
|
+
...showIdle ? [formatIdle(entry.idleDays)] : [],
|
|
1251
1376
|
formatAge(entry.mtimeMs)
|
|
1252
1377
|
]);
|
|
1253
1378
|
};
|
|
@@ -1291,6 +1416,8 @@ function renderScanJson(result) {
|
|
|
1291
1416
|
pmProjectRoot: entry.pm.projectRoot,
|
|
1292
1417
|
nested: entry.nested === true,
|
|
1293
1418
|
ownerPath: entry.ownerPath ?? entry.path,
|
|
1419
|
+
// v1.1 additive — keys appear only when --stale/--sort idle ran (frozen-contract friendly)
|
|
1420
|
+
...entry.idleDays !== void 0 ? { idleDays: entry.idleDays, activitySource: entry.activitySource } : {},
|
|
1294
1421
|
modifiedAt: entry.mtimeMs == null ? null : new Date(entry.mtimeMs).toISOString()
|
|
1295
1422
|
}))
|
|
1296
1423
|
};
|
|
@@ -1348,8 +1475,13 @@ function renderDeleteJson(outcome, mode, dryRun) {
|
|
|
1348
1475
|
status: row.status,
|
|
1349
1476
|
size: row.size,
|
|
1350
1477
|
sizeFormatted: formatBytes(row.size),
|
|
1351
|
-
error: row.error ?? null
|
|
1352
|
-
|
|
1478
|
+
error: row.error ?? null,
|
|
1479
|
+
// v1.1 additive — trash backend per row, only when trash mode ran
|
|
1480
|
+
...row.backend !== void 0 ? { backend: row.backend } : {}
|
|
1481
|
+
})),
|
|
1482
|
+
// v1.1 additive — nmh-internal trash batch dir / fallback flag, only when used
|
|
1483
|
+
...outcome.trashBatch !== void 0 ? { trashBatch: outcome.trashBatch } : {},
|
|
1484
|
+
...outcome.trashFallback === true ? { trashFallback: true } : {}
|
|
1353
1485
|
};
|
|
1354
1486
|
return JSON.stringify(payload2, null, 2);
|
|
1355
1487
|
}
|
|
@@ -1989,7 +2121,8 @@ async function collectScan(request) {
|
|
|
1989
2121
|
const walk = await scanForNodeModules(walkRoots, {
|
|
1990
2122
|
maxDepth: request.maxDepth,
|
|
1991
2123
|
recursive: request.recursive,
|
|
1992
|
-
excludes: walkExcludes
|
|
2124
|
+
excludes: walkExcludes,
|
|
2125
|
+
skipSystem: request.skipSystem
|
|
1993
2126
|
});
|
|
1994
2127
|
const permIssues = [...walk.permissionIssues];
|
|
1995
2128
|
collected.push(...walk.entries);
|
|
@@ -1999,6 +2132,7 @@ async function collectScan(request) {
|
|
|
1999
2132
|
maxDepth: request.maxDepth,
|
|
2000
2133
|
recursive: true,
|
|
2001
2134
|
excludes: walkExcludes,
|
|
2135
|
+
skipSystem: request.skipSystem,
|
|
2002
2136
|
initialOwner: direct
|
|
2003
2137
|
});
|
|
2004
2138
|
permIssues.push(...inner.permissionIssues);
|
|
@@ -2008,28 +2142,39 @@ async function collectScan(request) {
|
|
|
2008
2142
|
const metaByPath = new Map(collected.map((entry) => [entry.path, entry]));
|
|
2009
2143
|
const entryPaths = [...metaByPath.keys()];
|
|
2010
2144
|
verboseLog(`found ${entryPaths.length} node_modules \u2014 measuring`);
|
|
2011
|
-
const
|
|
2012
|
-
let bytesSoFar = 0;
|
|
2013
|
-
let measuredCount = 0;
|
|
2014
|
-
const measures = await measureEntries(entryPaths, permIssues, {
|
|
2015
|
-
concurrency: 4,
|
|
2016
|
-
onProgress: (progress) => {
|
|
2017
|
-
measuredCount = progress.measuredEntries;
|
|
2018
|
-
bytesSoFar = progress.bytesSoFar;
|
|
2019
|
-
progressLine(
|
|
2020
|
-
`\u280B measuring ${measuredCount}/${entryCount} folders \xB7 ${formatBytes(bytesSoFar)}`
|
|
2021
|
-
);
|
|
2022
|
-
}
|
|
2023
|
-
});
|
|
2145
|
+
const measures = await collectMeasures(entryPaths, request.fastSize === true, permIssues);
|
|
2024
2146
|
clearProgress();
|
|
2025
2147
|
const detect = createPmDetector();
|
|
2026
2148
|
const pmLimiter = createLimiter(8);
|
|
2027
2149
|
const pmResults = await Promise.all(entryPaths.map((entryPath) => pmLimiter(() => detect(entryPath))));
|
|
2028
2150
|
verboseLog(`scan finished in ${Date.now() - started} ms`);
|
|
2151
|
+
const activityByPath = /* @__PURE__ */ new Map();
|
|
2152
|
+
if (request.stale !== void 0 || request.sort === "idle") {
|
|
2153
|
+
const activityLimiter = createLimiter(8);
|
|
2154
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2155
|
+
const activityFor = (entryPath) => {
|
|
2156
|
+
const startDir = path12.dirname(entryPath);
|
|
2157
|
+
let pending = cache.get(startDir);
|
|
2158
|
+
if (pending === void 0) {
|
|
2159
|
+
pending = computeActivity(entryPath);
|
|
2160
|
+
cache.set(startDir, pending);
|
|
2161
|
+
}
|
|
2162
|
+
return pending;
|
|
2163
|
+
};
|
|
2164
|
+
await Promise.all(
|
|
2165
|
+
entryPaths.map(
|
|
2166
|
+
(entryPath) => activityLimiter(async () => {
|
|
2167
|
+
const activity = await activityFor(entryPath);
|
|
2168
|
+
if (activity !== null) activityByPath.set(entryPath, activity);
|
|
2169
|
+
})
|
|
2170
|
+
)
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2029
2173
|
const cwd = request.cwd;
|
|
2030
2174
|
let entries = entryPaths.map((entryPath, index) => {
|
|
2031
2175
|
const measure = measures.get(entryPath) ?? { size: 0, packageCount: 0, mtimeMs: null };
|
|
2032
2176
|
const meta = metaByPath.get(entryPath);
|
|
2177
|
+
const activity = activityByPath.get(entryPath);
|
|
2033
2178
|
return {
|
|
2034
2179
|
path: entryPath,
|
|
2035
2180
|
displayPath: shortenPath(entryPath, cwd),
|
|
@@ -2037,11 +2182,19 @@ async function collectScan(request) {
|
|
|
2037
2182
|
packageCount: measure.packageCount,
|
|
2038
2183
|
pm: pmResults[index],
|
|
2039
2184
|
mtimeMs: measure.mtimeMs,
|
|
2185
|
+
...activity !== void 0 ? { idleDays: activity.idleDays, activitySource: activity.source } : {},
|
|
2040
2186
|
nested: meta?.nested,
|
|
2041
2187
|
ownerPath: meta?.ownerPath
|
|
2042
2188
|
};
|
|
2043
2189
|
});
|
|
2044
|
-
|
|
2190
|
+
const cmpStr = (x, y) => x < y ? -1 : x > y ? 1 : 0;
|
|
2191
|
+
const comparators = {
|
|
2192
|
+
size: (a, b) => b.size - a.size || cmpStr(a.path, b.path),
|
|
2193
|
+
path: (a, b) => cmpStr(a.path, b.path) || b.size - a.size,
|
|
2194
|
+
idle: (a, b) => (b.idleDays ?? -1) - (a.idleDays ?? -1) || b.size - a.size,
|
|
2195
|
+
mtime: (a, b) => (b.mtimeMs ?? Number.NEGATIVE_INFINITY) - (a.mtimeMs ?? Number.NEGATIVE_INFINITY) || b.size - a.size
|
|
2196
|
+
};
|
|
2197
|
+
entries.sort(comparators[request.sort ?? "size"]);
|
|
2045
2198
|
entries = await applyFilters(entries, request);
|
|
2046
2199
|
const topEntries = entries.filter((entry) => !entry.nested);
|
|
2047
2200
|
const totals = {
|
|
@@ -2065,6 +2218,59 @@ async function collectScan(request) {
|
|
|
2065
2218
|
else if (permIssues.length > 0) code = 4;
|
|
2066
2219
|
return { code, result };
|
|
2067
2220
|
}
|
|
2221
|
+
async function collectMeasures(entryPaths, fastSize, permIssues) {
|
|
2222
|
+
const entryCount = entryPaths.length;
|
|
2223
|
+
let measuredCount = 0;
|
|
2224
|
+
let bytesSoFar = 0;
|
|
2225
|
+
const bump = () => {
|
|
2226
|
+
progressLine(`\u280B measuring ${measuredCount}/${entryCount} folders \xB7 ${formatBytes(bytesSoFar)}`);
|
|
2227
|
+
};
|
|
2228
|
+
if (!fastSize) {
|
|
2229
|
+
return measureEntries(entryPaths, permIssues, {
|
|
2230
|
+
concurrency: 4,
|
|
2231
|
+
onProgress: (progress) => {
|
|
2232
|
+
measuredCount = progress.measuredEntries;
|
|
2233
|
+
bytesSoFar = progress.bytesSoFar;
|
|
2234
|
+
bump();
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
const measures = /* @__PURE__ */ new Map();
|
|
2239
|
+
const duLimiter = createLimiter(8);
|
|
2240
|
+
const fallbackPaths = [];
|
|
2241
|
+
await Promise.all(
|
|
2242
|
+
entryPaths.map(
|
|
2243
|
+
(entryPath) => duLimiter(async () => {
|
|
2244
|
+
const fast = await measureEntryFast(entryPath);
|
|
2245
|
+
if (fast === null) {
|
|
2246
|
+
fallbackPaths.push(entryPath);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
measures.set(entryPath, fast);
|
|
2250
|
+
measuredCount += 1;
|
|
2251
|
+
bytesSoFar += fast.size;
|
|
2252
|
+
bump();
|
|
2253
|
+
})
|
|
2254
|
+
)
|
|
2255
|
+
);
|
|
2256
|
+
verboseLog(
|
|
2257
|
+
`fast-size: ${measures.size} entr${measures.size === 1 ? "y" : "ies"} via du, ${fallbackPaths.length} via walker fallback`
|
|
2258
|
+
);
|
|
2259
|
+
if (fallbackPaths.length > 0) {
|
|
2260
|
+
const duBytes = bytesSoFar;
|
|
2261
|
+
const duCount = measures.size;
|
|
2262
|
+
const walked = await measureEntries(fallbackPaths, permIssues, {
|
|
2263
|
+
concurrency: 4,
|
|
2264
|
+
onProgress: (progress) => {
|
|
2265
|
+
progressLine(
|
|
2266
|
+
`\u280B measuring ${duCount + progress.measuredEntries}/${entryCount} folders \xB7 ${formatBytes(duBytes + progress.bytesSoFar)}`
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
for (const [entryPath, measure] of walked) measures.set(entryPath, measure);
|
|
2271
|
+
}
|
|
2272
|
+
return measures;
|
|
2273
|
+
}
|
|
2068
2274
|
async function applyFilters(entries, request) {
|
|
2069
2275
|
const wantsPm = request.pmFilter !== void 0 && request.pmFilter.length > 0;
|
|
2070
2276
|
const wantedFamilies = new Set(request.pmFilter ?? []);
|
|
@@ -2073,12 +2279,15 @@ async function applyFilters(entries, request) {
|
|
|
2073
2279
|
const excludeMatcher = request.excludes !== void 0 && request.excludes.length > 0 ? makeGlobMatcher(request.excludes) : null;
|
|
2074
2280
|
const selfGuard = (request.excludes?.includes(SELF_TOKEN) ?? false) || (request.keep?.includes(SELF_TOKEN) ?? false);
|
|
2075
2281
|
const guardRoot = selfGuard ? path12.resolve(request.cwd) : null;
|
|
2076
|
-
|
|
2282
|
+
const wantsStale = request.stale !== void 0;
|
|
2283
|
+
const isStale = (entry) => wantsStale && entry.idleDays !== void 0 && entry.idleDays >= (request.stale ?? 0);
|
|
2284
|
+
if (!wantsPm && !keepMatcher && !excludeMatcher && !selfGuard && !wantsStale) return entries;
|
|
2077
2285
|
const survivingTops = /* @__PURE__ */ new Set();
|
|
2078
2286
|
for (const entry of entries) {
|
|
2079
2287
|
if (entry.nested) continue;
|
|
2080
2288
|
if (guardRoot !== null && insideRoot(entry.path, guardRoot)) continue;
|
|
2081
2289
|
if (wantsPm && !matchPm(entry)) continue;
|
|
2290
|
+
if (wantsStale && !isStale(entry)) continue;
|
|
2082
2291
|
if (excludeMatcher && excludeMatcher(entry.path)) continue;
|
|
2083
2292
|
if (keepMatcher) {
|
|
2084
2293
|
const name = await projectNameFor(entry.path);
|
|
@@ -2137,10 +2346,203 @@ function emptyResult(targets, unresolvable = []) {
|
|
|
2137
2346
|
};
|
|
2138
2347
|
}
|
|
2139
2348
|
|
|
2140
|
-
// src/commands/
|
|
2141
|
-
import
|
|
2349
|
+
// src/commands/update.ts
|
|
2350
|
+
import { promisify as promisify3 } from "util";
|
|
2351
|
+
import { execFile as execFile3 } from "child_process";
|
|
2352
|
+
import fs13 from "fs/promises";
|
|
2353
|
+
|
|
2354
|
+
// src/utils/update-check.ts
|
|
2355
|
+
import fs12 from "fs/promises";
|
|
2142
2356
|
import path13 from "path";
|
|
2143
2357
|
import { homedir as homedir6 } from "os";
|
|
2358
|
+
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2359
|
+
function cacheFile() {
|
|
2360
|
+
return path13.join(homedir6(), ".nmh", "cache", "update-check.json");
|
|
2361
|
+
}
|
|
2362
|
+
function registryUrl() {
|
|
2363
|
+
return process.env.NMH_REGISTRY_JSON ?? "https://registry.npmjs.org/node-modules-hunter/latest";
|
|
2364
|
+
}
|
|
2365
|
+
function isNewer(current, latest) {
|
|
2366
|
+
const triple = (v) => {
|
|
2367
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(v.trim());
|
|
2368
|
+
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3]), match[4] ?? ""];
|
|
2369
|
+
};
|
|
2370
|
+
const now = triple(current);
|
|
2371
|
+
const next = triple(latest);
|
|
2372
|
+
if (now === null || next === null) return false;
|
|
2373
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2374
|
+
if (next[i] > now[i]) return true;
|
|
2375
|
+
if (next[i] < now[i]) return false;
|
|
2376
|
+
}
|
|
2377
|
+
return false;
|
|
2378
|
+
}
|
|
2379
|
+
function updateNote(latest, current) {
|
|
2380
|
+
return `update tersedia: ${current} \u2192 ${latest} \u2014 npm i -g node-modules-hunter (cek 24 jam sekali, opt-in via --update-check)`;
|
|
2381
|
+
}
|
|
2382
|
+
async function readFreshCache() {
|
|
2383
|
+
try {
|
|
2384
|
+
const parsed = JSON.parse(await fs12.readFile(cacheFile(), "utf8"));
|
|
2385
|
+
const age = Date.now() - new Date(parsed.checkedAt).getTime();
|
|
2386
|
+
if (Number.isFinite(age) && age >= 0 && age < UPDATE_CACHE_TTL_MS && typeof parsed.latest === "string") {
|
|
2387
|
+
return parsed.latest;
|
|
2388
|
+
}
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
return null;
|
|
2392
|
+
}
|
|
2393
|
+
async function writeCache(latest) {
|
|
2394
|
+
try {
|
|
2395
|
+
const entry = { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest };
|
|
2396
|
+
await fs12.mkdir(path13.dirname(cacheFile()), { recursive: true });
|
|
2397
|
+
await fs12.writeFile(cacheFile(), `${JSON.stringify(entry, null, 2)}
|
|
2398
|
+
`, "utf8");
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
verboseLog(`update-cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function parseLatestVersion(payload2) {
|
|
2404
|
+
if (typeof payload2 !== "object" || payload2 === null) return null;
|
|
2405
|
+
const version = payload2.version;
|
|
2406
|
+
if (typeof version !== "string" || version.length === 0) return null;
|
|
2407
|
+
return tripleOf(version) ? version : null;
|
|
2408
|
+
}
|
|
2409
|
+
function tripleOf(version) {
|
|
2410
|
+
return /^\d+\.\d+\.\d+/.test(version.trim());
|
|
2411
|
+
}
|
|
2412
|
+
async function checkForUpdate() {
|
|
2413
|
+
try {
|
|
2414
|
+
const fresh = await readFreshCache();
|
|
2415
|
+
const latest = fresh ?? await fetchLatest();
|
|
2416
|
+
if (latest === null) return null;
|
|
2417
|
+
return isNewer(toolVersion(), latest) ? updateNote(latest, toolVersion()) : null;
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
verboseLog(`update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
async function fetchLatest() {
|
|
2424
|
+
try {
|
|
2425
|
+
const response = await fetch(registryUrl(), { signal: AbortSignal.timeout(4e3) });
|
|
2426
|
+
if (!response.ok) {
|
|
2427
|
+
verboseLog(`update check: registry responded ${response.status}`);
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
const latest = parseLatestVersion(await response.json());
|
|
2431
|
+
if (latest !== null) await writeCache(latest);
|
|
2432
|
+
return latest;
|
|
2433
|
+
} catch (err) {
|
|
2434
|
+
verboseLog(`update check: registry unreachable (${err instanceof Error ? err.message : String(err)})`);
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/commands/update.ts
|
|
2440
|
+
var execFileP3 = promisify3(execFile3);
|
|
2441
|
+
async function runNpm(args, timeoutMs) {
|
|
2442
|
+
const win32 = process.platform === "win32";
|
|
2443
|
+
try {
|
|
2444
|
+
const { stdout } = await execFileP3(win32 ? "npm.cmd" : "npm", args, {
|
|
2445
|
+
timeout: timeoutMs,
|
|
2446
|
+
maxBuffer: 1024 * 1024,
|
|
2447
|
+
shell: win32,
|
|
2448
|
+
windowsHide: true
|
|
2449
|
+
});
|
|
2450
|
+
return { code: 0, stdout, stderr: "" };
|
|
2451
|
+
} catch (err) {
|
|
2452
|
+
const failure = err;
|
|
2453
|
+
return { code: failure.code ?? 1, stdout: failure.stdout ?? "", stderr: failure.stderr ?? "" };
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
var VERSION_SHAPE = /^\d+\.\d+\.\d+/;
|
|
2457
|
+
async function latestVersion() {
|
|
2458
|
+
const override = process.env.NMH_UPDATE_LATEST_JSON;
|
|
2459
|
+
if (typeof override === "string" && override.length > 0) {
|
|
2460
|
+
try {
|
|
2461
|
+
const parsed = JSON.parse(await fs13.readFile(override, "utf8"));
|
|
2462
|
+
return typeof parsed.version === "string" && VERSION_SHAPE.test(parsed.version) ? parsed.version : null;
|
|
2463
|
+
} catch (err) {
|
|
2464
|
+
verboseLog(`update: override file unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const view = await runNpm(["view", "node-modules-hunter", "version"], 6e4);
|
|
2469
|
+
if (view.code !== 0) {
|
|
2470
|
+
verboseLog(`npm view failed (rc ${view.code}): ${view.stderr.trim().slice(0, 200)}`);
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
const version = view.stdout.trim();
|
|
2474
|
+
return VERSION_SHAPE.test(version) ? version : null;
|
|
2475
|
+
}
|
|
2476
|
+
async function isNpmGlobal() {
|
|
2477
|
+
const forced = process.env.NMH_UPDATE_GLOBAL;
|
|
2478
|
+
if (forced === "1") return true;
|
|
2479
|
+
if (forced === "0") return false;
|
|
2480
|
+
const ls = await runNpm(["ls", "-g", "node-modules-hunter", "--depth=0"], 6e4);
|
|
2481
|
+
return ls.code === 0 && ls.stdout.includes("node-modules-hunter");
|
|
2482
|
+
}
|
|
2483
|
+
async function installLatest(version) {
|
|
2484
|
+
const installCommand = `npm install -g node-modules-hunter@${version}`;
|
|
2485
|
+
const fake = process.env.NMH_UPDATE_FAKE_INSTALL;
|
|
2486
|
+
if (typeof fake === "string" && fake.length > 0) {
|
|
2487
|
+
try {
|
|
2488
|
+
await fs13.writeFile(fake, `${installCommand}
|
|
2489
|
+
`, "utf8");
|
|
2490
|
+
return { ok: true, detail: installCommand };
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
progressLine("\u280B updating via npm\u2026");
|
|
2496
|
+
const install = await runNpm(["install", "-g", "node-modules-hunter@" + version], 3e5);
|
|
2497
|
+
clearProgress();
|
|
2498
|
+
if (install.code !== 0) {
|
|
2499
|
+
return { ok: false, detail: install.stderr.trim().slice(0, 300) || `npm exited ${install.code}` };
|
|
2500
|
+
}
|
|
2501
|
+
return { ok: true, detail: installCommand };
|
|
2502
|
+
}
|
|
2503
|
+
async function runUpdate(request) {
|
|
2504
|
+
const current = toolVersion();
|
|
2505
|
+
const latest = await latestVersion();
|
|
2506
|
+
if (latest === null) {
|
|
2507
|
+
const message = "cannot read the latest version \u2014 is the registry reachable? (npm view node-modules-hunter version)";
|
|
2508
|
+
if (request.json) errorLog(message);
|
|
2509
|
+
else errorLog(`\u26A0 ${message}`);
|
|
2510
|
+
return 1;
|
|
2511
|
+
}
|
|
2512
|
+
const global = await isNpmGlobal();
|
|
2513
|
+
const method = global ? "npm-global" : "manual";
|
|
2514
|
+
if (!isNewer(current, latest)) {
|
|
2515
|
+
const result2 = { current, latest, updated: false, method };
|
|
2516
|
+
if (request.json) payload(JSON.stringify(result2, null, 2));
|
|
2517
|
+
else log(`nmh ${current} sudah versi terbaru (registry: ${latest}) \u2014 tak ada yang diinstall`);
|
|
2518
|
+
return 0;
|
|
2519
|
+
}
|
|
2520
|
+
if (!global) {
|
|
2521
|
+
const result2 = { current, latest, updated: false, method };
|
|
2522
|
+
const advice = [
|
|
2523
|
+
`nmh ${current} terpasang di luar npm global \u2014 update manual:`,
|
|
2524
|
+
" npm i -g node-modules-hunter (atau: pnpm add -g / bunx / brew upgrade node-modules-hunter)"
|
|
2525
|
+
];
|
|
2526
|
+
if (request.json) payload(JSON.stringify(result2, null, 2));
|
|
2527
|
+
else for (const line of advice) note(line);
|
|
2528
|
+
return 0;
|
|
2529
|
+
}
|
|
2530
|
+
const install = await installLatest(latest);
|
|
2531
|
+
if (!install.ok) {
|
|
2532
|
+
errorLog(`\u26A0 update gagal: ${install.detail}`);
|
|
2533
|
+
errorLog(` coba manual: npm install -g node-modules-hunter@${latest}`);
|
|
2534
|
+
return 1;
|
|
2535
|
+
}
|
|
2536
|
+
const result = { current, latest, updated: true, method: "npm-global" };
|
|
2537
|
+
if (request.json) payload(JSON.stringify(result, null, 2));
|
|
2538
|
+
else log(`\u2713 terupdate ${current} \u2192 ${latest} (npm install -g node-modules-hunter)`);
|
|
2539
|
+
return 0;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/commands/config.ts
|
|
2543
|
+
import fs14 from "fs/promises";
|
|
2544
|
+
import path14 from "path";
|
|
2545
|
+
import { homedir as homedir7 } from "os";
|
|
2144
2546
|
var VALID_MODES2 = /* @__PURE__ */ new Set(["hard", "soft", "trash"]);
|
|
2145
2547
|
var KNOWN_KEYS = ["defaultMode", "depth", "exclude", "keep", "color", "animate", "updateCheck", "logDir"];
|
|
2146
2548
|
var KEY_TO_FLAG = {
|
|
@@ -2218,9 +2620,9 @@ function parseBool(raw2, label) {
|
|
|
2218
2620
|
function configWriteTarget(explicitFile, cwd) {
|
|
2219
2621
|
if (explicitFile !== void 0 && explicitFile.length > 0) {
|
|
2220
2622
|
const raw2 = expandTilde(explicitFile);
|
|
2221
|
-
return
|
|
2623
|
+
return path14.isAbsolute(raw2) ? raw2 : path14.resolve(cwd, raw2);
|
|
2222
2624
|
}
|
|
2223
|
-
return
|
|
2625
|
+
return path14.join(homedir7(), DEFAULT_TARGET);
|
|
2224
2626
|
}
|
|
2225
2627
|
function listMutation(op, flag, given) {
|
|
2226
2628
|
const key = LIST_KEYS[flag];
|
|
@@ -2287,7 +2689,7 @@ function assemble(request) {
|
|
|
2287
2689
|
async function readRawConfig(file) {
|
|
2288
2690
|
let body;
|
|
2289
2691
|
try {
|
|
2290
|
-
body = await
|
|
2692
|
+
body = await fs14.readFile(file, "utf8");
|
|
2291
2693
|
} catch (err) {
|
|
2292
2694
|
if (err.code === "ENOENT") return {};
|
|
2293
2695
|
throw new ConfigError(`config unreadable: ${file} (${err.code ?? String(err)})`);
|
|
@@ -2334,8 +2736,8 @@ async function writeConfigMutation(file, mutation) {
|
|
|
2334
2736
|
}
|
|
2335
2737
|
const body = `${JSON.stringify(raw2, null, 2)}
|
|
2336
2738
|
`;
|
|
2337
|
-
await
|
|
2338
|
-
await
|
|
2739
|
+
await fs14.mkdir(path14.dirname(file), { recursive: true });
|
|
2740
|
+
await fs14.writeFile(file, body, "utf8");
|
|
2339
2741
|
return applied;
|
|
2340
2742
|
}
|
|
2341
2743
|
var INFO_DEFAULTS = {
|
|
@@ -2426,7 +2828,7 @@ async function runConfigCommand(request) {
|
|
|
2426
2828
|
}
|
|
2427
2829
|
|
|
2428
2830
|
// src/commands/search.ts
|
|
2429
|
-
import
|
|
2831
|
+
import path15 from "path";
|
|
2430
2832
|
async function collectSearch(request) {
|
|
2431
2833
|
const scan = await collectScan({
|
|
2432
2834
|
targets: request.targets,
|
|
@@ -2444,7 +2846,7 @@ async function collectSearch(request) {
|
|
|
2444
2846
|
const outcomes = await Promise.all(
|
|
2445
2847
|
scan.result.entries.map(
|
|
2446
2848
|
(entry) => limiter(async () => {
|
|
2447
|
-
const pathMatch = match(entry.path) || match(
|
|
2849
|
+
const pathMatch = match(entry.path) || match(path15.relative(request.cwd, entry.path));
|
|
2448
2850
|
const packages = await listTopLevelPackages(entry.path);
|
|
2449
2851
|
const matched = packages.filter((pkg) => match(pkg.name));
|
|
2450
2852
|
const matches = await Promise.all(
|
|
@@ -2497,11 +2899,11 @@ async function runSearch(request) {
|
|
|
2497
2899
|
}
|
|
2498
2900
|
|
|
2499
2901
|
// src/commands/why.ts
|
|
2500
|
-
import
|
|
2902
|
+
import path17 from "path";
|
|
2501
2903
|
|
|
2502
2904
|
// src/core/lockfile.ts
|
|
2503
|
-
import
|
|
2504
|
-
import
|
|
2905
|
+
import fs15 from "fs/promises";
|
|
2906
|
+
import path16 from "path";
|
|
2505
2907
|
var ROOT = "\xABroot\xBB";
|
|
2506
2908
|
var MAX_LOCKFILE_BYTES = 8 * 1024 * 1024;
|
|
2507
2909
|
function nameOfPackageKey(key) {
|
|
@@ -2583,9 +2985,9 @@ function buildGraph(pm, file, parsed) {
|
|
|
2583
2985
|
async function readNpmLockGraph(projectDir, file = "package-lock.json") {
|
|
2584
2986
|
let text2;
|
|
2585
2987
|
try {
|
|
2586
|
-
const
|
|
2587
|
-
if (!
|
|
2588
|
-
text2 = await
|
|
2988
|
+
const stat2 = await fs15.stat(`${projectDir}/${file}`);
|
|
2989
|
+
if (!stat2.isFile() || stat2.size > MAX_LOCKFILE_BYTES) return null;
|
|
2990
|
+
text2 = await fs15.readFile(`${projectDir}/${file}`, "utf8");
|
|
2589
2991
|
} catch {
|
|
2590
2992
|
return null;
|
|
2591
2993
|
}
|
|
@@ -2606,9 +3008,9 @@ async function readProjectLockGraph(projectDir) {
|
|
|
2606
3008
|
async function readYarnLockGraph(projectDir) {
|
|
2607
3009
|
let text2;
|
|
2608
3010
|
try {
|
|
2609
|
-
const
|
|
2610
|
-
if (!
|
|
2611
|
-
text2 = await
|
|
3011
|
+
const stat2 = await fs15.stat(path16.join(projectDir, "yarn.lock"));
|
|
3012
|
+
if (!stat2.isFile() || stat2.size > MAX_LOCKFILE_BYTES) return null;
|
|
3013
|
+
text2 = await fs15.readFile(path16.join(projectDir, "yarn.lock"), "utf8");
|
|
2612
3014
|
} catch {
|
|
2613
3015
|
return null;
|
|
2614
3016
|
}
|
|
@@ -2616,7 +3018,7 @@ async function readYarnLockGraph(projectDir) {
|
|
|
2616
3018
|
if (parsed === null) return null;
|
|
2617
3019
|
let rootDirect = 0;
|
|
2618
3020
|
try {
|
|
2619
|
-
const manifest = JSON.parse(await
|
|
3021
|
+
const manifest = JSON.parse(await fs15.readFile(path16.join(projectDir, "package.json"), "utf8"));
|
|
2620
3022
|
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
2621
3023
|
const map = manifest[field];
|
|
2622
3024
|
if (map === void 0 || typeof map !== "object") continue;
|
|
@@ -2751,9 +3153,9 @@ function nameOfSpecKey(key) {
|
|
|
2751
3153
|
async function readPnpmLockGraph(projectDir) {
|
|
2752
3154
|
let text2;
|
|
2753
3155
|
try {
|
|
2754
|
-
const
|
|
2755
|
-
if (!
|
|
2756
|
-
text2 = await
|
|
3156
|
+
const stat2 = await fs15.stat(path16.join(projectDir, "pnpm-lock.yaml"));
|
|
3157
|
+
if (!stat2.isFile() || stat2.size > MAX_LOCKFILE_BYTES) return null;
|
|
3158
|
+
text2 = await fs15.readFile(path16.join(projectDir, "pnpm-lock.yaml"), "utf8");
|
|
2757
3159
|
} catch {
|
|
2758
3160
|
return null;
|
|
2759
3161
|
}
|
|
@@ -2810,9 +3212,9 @@ function collectPnpmNodes(section, edges, nodeNames) {
|
|
|
2810
3212
|
async function readBunLockGraph(projectDir) {
|
|
2811
3213
|
let text2;
|
|
2812
3214
|
try {
|
|
2813
|
-
const
|
|
2814
|
-
if (!
|
|
2815
|
-
text2 = await
|
|
3215
|
+
const stat2 = await fs15.stat(path16.join(projectDir, "bun.lock"));
|
|
3216
|
+
if (!stat2.isFile() || stat2.size > MAX_LOCKFILE_BYTES) return null;
|
|
3217
|
+
text2 = await fs15.readFile(path16.join(projectDir, "bun.lock"), "utf8");
|
|
2816
3218
|
} catch {
|
|
2817
3219
|
return null;
|
|
2818
3220
|
}
|
|
@@ -2957,7 +3359,7 @@ async function runWhy(request) {
|
|
|
2957
3359
|
if (target === void 0) return null;
|
|
2958
3360
|
const declaredBy = await classify(entry.path, request.pkg);
|
|
2959
3361
|
const targetVersion = await packageVersion(target.path);
|
|
2960
|
-
const lock = await readProjectLockGraph(
|
|
3362
|
+
const lock = await readProjectLockGraph(path17.dirname(entry.path));
|
|
2961
3363
|
let requiredBy;
|
|
2962
3364
|
let requiredByGrade;
|
|
2963
3365
|
if (lock !== null) {
|
|
@@ -3014,91 +3416,6 @@ async function runWhy(request) {
|
|
|
3014
3416
|
return code;
|
|
3015
3417
|
}
|
|
3016
3418
|
|
|
3017
|
-
// src/utils/update-check.ts
|
|
3018
|
-
import fs13 from "fs/promises";
|
|
3019
|
-
import path17 from "path";
|
|
3020
|
-
import { homedir as homedir7 } from "os";
|
|
3021
|
-
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3022
|
-
function cacheFile() {
|
|
3023
|
-
return path17.join(homedir7(), ".nmh", "cache", "update-check.json");
|
|
3024
|
-
}
|
|
3025
|
-
function registryUrl() {
|
|
3026
|
-
return process.env.NMH_REGISTRY_JSON ?? "https://registry.npmjs.org/node-modules-hunter/latest";
|
|
3027
|
-
}
|
|
3028
|
-
function isNewer(current, latest) {
|
|
3029
|
-
const triple = (v) => {
|
|
3030
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(v.trim());
|
|
3031
|
-
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3]), match[4] ?? ""];
|
|
3032
|
-
};
|
|
3033
|
-
const now = triple(current);
|
|
3034
|
-
const next = triple(latest);
|
|
3035
|
-
if (now === null || next === null) return false;
|
|
3036
|
-
for (let i = 0; i < 3; i += 1) {
|
|
3037
|
-
if (next[i] > now[i]) return true;
|
|
3038
|
-
if (next[i] < now[i]) return false;
|
|
3039
|
-
}
|
|
3040
|
-
return false;
|
|
3041
|
-
}
|
|
3042
|
-
function updateNote(latest, current) {
|
|
3043
|
-
return `update tersedia: ${current} \u2192 ${latest} \u2014 npm i -g node-modules-hunter (cek 24 jam sekali, opt-in via --update-check)`;
|
|
3044
|
-
}
|
|
3045
|
-
async function readFreshCache() {
|
|
3046
|
-
try {
|
|
3047
|
-
const parsed = JSON.parse(await fs13.readFile(cacheFile(), "utf8"));
|
|
3048
|
-
const age = Date.now() - new Date(parsed.checkedAt).getTime();
|
|
3049
|
-
if (Number.isFinite(age) && age >= 0 && age < UPDATE_CACHE_TTL_MS && typeof parsed.latest === "string") {
|
|
3050
|
-
return parsed.latest;
|
|
3051
|
-
}
|
|
3052
|
-
} catch {
|
|
3053
|
-
}
|
|
3054
|
-
return null;
|
|
3055
|
-
}
|
|
3056
|
-
async function writeCache(latest) {
|
|
3057
|
-
try {
|
|
3058
|
-
const entry = { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest };
|
|
3059
|
-
await fs13.mkdir(path17.dirname(cacheFile()), { recursive: true });
|
|
3060
|
-
await fs13.writeFile(cacheFile(), `${JSON.stringify(entry, null, 2)}
|
|
3061
|
-
`, "utf8");
|
|
3062
|
-
} catch (err) {
|
|
3063
|
-
verboseLog(`update-cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
function parseLatestVersion(payload2) {
|
|
3067
|
-
if (typeof payload2 !== "object" || payload2 === null) return null;
|
|
3068
|
-
const version = payload2.version;
|
|
3069
|
-
if (typeof version !== "string" || version.length === 0) return null;
|
|
3070
|
-
return tripleOf(version) ? version : null;
|
|
3071
|
-
}
|
|
3072
|
-
function tripleOf(version) {
|
|
3073
|
-
return /^\d+\.\d+\.\d+/.test(version.trim());
|
|
3074
|
-
}
|
|
3075
|
-
async function checkForUpdate() {
|
|
3076
|
-
try {
|
|
3077
|
-
const fresh = await readFreshCache();
|
|
3078
|
-
const latest = fresh ?? await fetchLatest();
|
|
3079
|
-
if (latest === null) return null;
|
|
3080
|
-
return isNewer(toolVersion(), latest) ? updateNote(latest, toolVersion()) : null;
|
|
3081
|
-
} catch (err) {
|
|
3082
|
-
verboseLog(`update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3083
|
-
return null;
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
async function fetchLatest() {
|
|
3087
|
-
try {
|
|
3088
|
-
const response = await fetch(registryUrl(), { signal: AbortSignal.timeout(4e3) });
|
|
3089
|
-
if (!response.ok) {
|
|
3090
|
-
verboseLog(`update check: registry responded ${response.status}`);
|
|
3091
|
-
return null;
|
|
3092
|
-
}
|
|
3093
|
-
const latest = parseLatestVersion(await response.json());
|
|
3094
|
-
if (latest !== null) await writeCache(latest);
|
|
3095
|
-
return latest;
|
|
3096
|
-
} catch (err) {
|
|
3097
|
-
verboseLog(`update check: registry unreachable (${err instanceof Error ? err.message : String(err)})`);
|
|
3098
|
-
return null;
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
|
|
3102
3419
|
// src/commands/map.ts
|
|
3103
3420
|
async function collectMap(request) {
|
|
3104
3421
|
const scan = await collectScan({
|
|
@@ -3190,7 +3507,7 @@ async function runMap(request) {
|
|
|
3190
3507
|
}
|
|
3191
3508
|
|
|
3192
3509
|
// src/commands/treemap.ts
|
|
3193
|
-
import
|
|
3510
|
+
import fs16 from "fs/promises";
|
|
3194
3511
|
import path18 from "path";
|
|
3195
3512
|
|
|
3196
3513
|
// src/core/treemap.ts
|
|
@@ -3421,8 +3738,8 @@ async function runTreemap(request) {
|
|
|
3421
3738
|
}
|
|
3422
3739
|
const outPath = path18.resolve(request.cwd, request.out);
|
|
3423
3740
|
try {
|
|
3424
|
-
await
|
|
3425
|
-
await
|
|
3741
|
+
await fs16.mkdir(path18.dirname(outPath), { recursive: true });
|
|
3742
|
+
await fs16.writeFile(outPath, renderTreemapHtml(data), "utf8");
|
|
3426
3743
|
} catch (err) {
|
|
3427
3744
|
errorLog(`\u{1F6D1} could not write the treemap file \u2192 ${outPath} \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
3428
3745
|
return 1;
|
|
@@ -3435,7 +3752,7 @@ async function runTreemap(request) {
|
|
|
3435
3752
|
// src/core/deps-audit.ts
|
|
3436
3753
|
import { glob as glob2 } from "tinyglobby";
|
|
3437
3754
|
import path19 from "path";
|
|
3438
|
-
import
|
|
3755
|
+
import fs17 from "fs/promises";
|
|
3439
3756
|
var SOURCE_GLOB = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,svelte,astro}";
|
|
3440
3757
|
var SOURCE_IGNORES = ["**/node_modules/**", "**/.git/**"];
|
|
3441
3758
|
var MAX_FILES_PER_PROJECT = 2e3;
|
|
@@ -3503,9 +3820,9 @@ async function collectDepsAudit(projects, installedNames) {
|
|
|
3503
3820
|
await limiter(async () => {
|
|
3504
3821
|
let code;
|
|
3505
3822
|
try {
|
|
3506
|
-
const
|
|
3507
|
-
if (
|
|
3508
|
-
code = await
|
|
3823
|
+
const stat2 = await fs17.stat(path19.join(project.dir, file));
|
|
3824
|
+
if (stat2.size > MAX_FILE_BYTES) return;
|
|
3825
|
+
code = await fs17.readFile(path19.join(project.dir, file), "utf8");
|
|
3509
3826
|
} catch {
|
|
3510
3827
|
return;
|
|
3511
3828
|
}
|
|
@@ -3546,7 +3863,7 @@ async function collectDepsAudit(projects, installedNames) {
|
|
|
3546
3863
|
import path21 from "path";
|
|
3547
3864
|
|
|
3548
3865
|
// src/utils/registry-deprecated.ts
|
|
3549
|
-
import
|
|
3866
|
+
import fs18 from "fs/promises";
|
|
3550
3867
|
import path20 from "path";
|
|
3551
3868
|
import os3 from "os";
|
|
3552
3869
|
var MAX_REGISTRY_DEPRECATED_FETCHES = 25;
|
|
@@ -3562,7 +3879,7 @@ async function registryDeprecated(name, version, cacheDir, base = "https://regis
|
|
|
3562
3879
|
if (name.length === 0 || version.length === 0) return null;
|
|
3563
3880
|
const file = cachePath(cacheDir, name, version);
|
|
3564
3881
|
try {
|
|
3565
|
-
const cached2 = JSON.parse(await
|
|
3882
|
+
const cached2 = JSON.parse(await fs18.readFile(file, "utf8"));
|
|
3566
3883
|
if (typeof cached2.checkedAt === "string") {
|
|
3567
3884
|
const age = Date.now() - Date.parse(cached2.checkedAt);
|
|
3568
3885
|
if (Number.isFinite(age) && age >= 0 && age < DEPRECATED_TTL_MS) {
|
|
@@ -3589,8 +3906,8 @@ async function registryDeprecated(name, version, cacheDir, base = "https://regis
|
|
|
3589
3906
|
}
|
|
3590
3907
|
async function writeDeprecatedCache(file, message) {
|
|
3591
3908
|
try {
|
|
3592
|
-
await
|
|
3593
|
-
await
|
|
3909
|
+
await fs18.mkdir(path20.dirname(file), { recursive: true });
|
|
3910
|
+
await fs18.writeFile(file, JSON.stringify({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), message }), "utf8");
|
|
3594
3911
|
} catch {
|
|
3595
3912
|
}
|
|
3596
3913
|
}
|
|
@@ -4094,13 +4411,20 @@ async function saveBaselineQuiet(name, snapshot) {
|
|
|
4094
4411
|
import { confirm, select, isCancel } from "@clack/prompts";
|
|
4095
4412
|
|
|
4096
4413
|
// src/core/deleter.ts
|
|
4097
|
-
import
|
|
4414
|
+
import fs20 from "fs/promises";
|
|
4098
4415
|
|
|
4099
4416
|
// src/utils/trash.ts
|
|
4100
|
-
import { execFile } from "child_process";
|
|
4101
|
-
import { promisify } from "util";
|
|
4102
|
-
|
|
4417
|
+
import { execFile as execFile4 } from "child_process";
|
|
4418
|
+
import { promisify as promisify4 } from "util";
|
|
4419
|
+
import fs19 from "fs/promises";
|
|
4420
|
+
import path22 from "path";
|
|
4421
|
+
import { homedir as homedir8 } from "os";
|
|
4422
|
+
var execFileAsync = promisify4(execFile4);
|
|
4103
4423
|
async function trashBackend() {
|
|
4424
|
+
const forced = process.env.NMH_TRASH_BACKEND;
|
|
4425
|
+
if (forced === "windows-com" || forced === "macos-finder" || forced === "linux-gio" || forced === "linux-trash-cli" || forced === "none") {
|
|
4426
|
+
return forced;
|
|
4427
|
+
}
|
|
4104
4428
|
switch (process.platform) {
|
|
4105
4429
|
case "win32":
|
|
4106
4430
|
return "windows-com";
|
|
@@ -4181,6 +4505,30 @@ async function commandExists(bin) {
|
|
|
4181
4505
|
return false;
|
|
4182
4506
|
}
|
|
4183
4507
|
}
|
|
4508
|
+
async function createTrashBatch(home) {
|
|
4509
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4510
|
+
const hex = Math.random().toString(16).slice(2, 5);
|
|
4511
|
+
const batchDir = path22.join(home ?? homedir8(), ".nmh", "trash", `trash-${stamp}-${hex}`);
|
|
4512
|
+
await fs19.mkdir(batchDir, { recursive: true });
|
|
4513
|
+
return batchDir;
|
|
4514
|
+
}
|
|
4515
|
+
async function nmhTrashMove(entryPath, batchDir) {
|
|
4516
|
+
const parent = path22.basename(path22.dirname(entryPath));
|
|
4517
|
+
const storedAs = `${parent}-node_modules-${Math.random().toString(16).slice(2, 5)}`;
|
|
4518
|
+
const destination = path22.join(batchDir, storedAs);
|
|
4519
|
+
try {
|
|
4520
|
+
await fs19.rename(entryPath, destination);
|
|
4521
|
+
} catch (err) {
|
|
4522
|
+
if (err?.code !== "EXDEV") throw err;
|
|
4523
|
+
await fs19.cp(entryPath, destination, { recursive: true, dereference: false, verbatimSymlinks: true });
|
|
4524
|
+
await fs19.rm(entryPath, { recursive: true, force: true });
|
|
4525
|
+
}
|
|
4526
|
+
return storedAs;
|
|
4527
|
+
}
|
|
4528
|
+
async function appendTrashManifest(batchDir, row) {
|
|
4529
|
+
await fs19.appendFile(path22.join(batchDir, "manifest.jsonl"), `${JSON.stringify(row)}
|
|
4530
|
+
`, "utf8");
|
|
4531
|
+
}
|
|
4184
4532
|
|
|
4185
4533
|
// src/core/deleter.ts
|
|
4186
4534
|
async function deleteEntries(entries, request) {
|
|
@@ -4188,39 +4536,76 @@ async function deleteEntries(entries, request) {
|
|
|
4188
4536
|
const limiter = createLimiter(request.concurrency ?? 8);
|
|
4189
4537
|
const rows = [];
|
|
4190
4538
|
let done = 0;
|
|
4191
|
-
const runOne = async (entry, attempt) => {
|
|
4539
|
+
const runOne = async (entry, attempt, backend) => {
|
|
4192
4540
|
const failure = await attempt().catch(
|
|
4193
4541
|
(err) => err instanceof Error ? err.message : String(err)
|
|
4194
4542
|
);
|
|
4195
|
-
const row = failure ? { path: entry.path, size: entry.size, status: "failed", error: failure } : {
|
|
4543
|
+
const row = failure ? { path: entry.path, size: entry.size, status: "failed", error: failure } : {
|
|
4544
|
+
path: entry.path,
|
|
4545
|
+
size: entry.size,
|
|
4546
|
+
status: request.mode === "hard" ? "deleted" : "trashed",
|
|
4547
|
+
...backend !== void 0 ? { backend } : {}
|
|
4548
|
+
};
|
|
4196
4549
|
rows.push(row);
|
|
4197
4550
|
done += 1;
|
|
4198
4551
|
request.onRow?.(row, done, total);
|
|
4199
4552
|
};
|
|
4553
|
+
const outcome = { results: rows, freed: 0, deleted: 0, failed: 0, permissionDenied: false };
|
|
4200
4554
|
if (request.mode === "hard") {
|
|
4201
4555
|
await Promise.all(entries.map((entry) => limiter(() => runOne(entry, () => removeHard(entry.path)))));
|
|
4202
4556
|
} else {
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
)
|
|
4209
|
-
|
|
4557
|
+
const trashMode = request.trashMode ?? "system";
|
|
4558
|
+
const systemBackend = trashMode === "system" ? await trashBackend() : null;
|
|
4559
|
+
const useNmhTrash = systemBackend === null || systemBackend === "none";
|
|
4560
|
+
if (useNmhTrash) {
|
|
4561
|
+
const via = request.trashMode === "nmh" ? "explicit" : "fallback";
|
|
4562
|
+
const batchDir = await createTrashBatch();
|
|
4563
|
+
outcome.trashBatch = batchDir;
|
|
4564
|
+
if (via === "fallback") outcome.trashFallback = true;
|
|
4565
|
+
await Promise.all(
|
|
4566
|
+
entries.map(
|
|
4567
|
+
(entry) => limiter(
|
|
4568
|
+
() => runOne(
|
|
4569
|
+
entry,
|
|
4570
|
+
async () => {
|
|
4571
|
+
const storedAs = await nmhTrashMove(entry.path, batchDir);
|
|
4572
|
+
await appendTrashManifest(batchDir, {
|
|
4573
|
+
originalPath: entry.path,
|
|
4574
|
+
storedAs,
|
|
4575
|
+
size: entry.size,
|
|
4576
|
+
trashedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4577
|
+
nmh: toolVersion(),
|
|
4578
|
+
via
|
|
4579
|
+
});
|
|
4580
|
+
return null;
|
|
4581
|
+
},
|
|
4582
|
+
"nmh"
|
|
4583
|
+
)
|
|
4584
|
+
)
|
|
4585
|
+
)
|
|
4586
|
+
);
|
|
4587
|
+
} else {
|
|
4588
|
+
const backend = systemBackend;
|
|
4589
|
+
await Promise.all(
|
|
4590
|
+
entries.map(
|
|
4591
|
+
(entry) => limiter(
|
|
4592
|
+
() => runOne(entry, () => moveToTrash([entry.path], backend).then(() => null), "system")
|
|
4593
|
+
)
|
|
4594
|
+
)
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
4210
4597
|
}
|
|
4211
4598
|
const completed = rows.filter((row) => row.status !== "failed");
|
|
4212
4599
|
const failedRows = rows.filter((row) => row.status === "failed");
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
permissionDenied: failedRows.some((row) => /EACCES|EPERM/.test(row.error ?? ""))
|
|
4219
|
-
};
|
|
4600
|
+
outcome.freed = completed.reduce((sum2, row) => sum2 + row.size, 0);
|
|
4601
|
+
outcome.deleted = completed.length;
|
|
4602
|
+
outcome.failed = failedRows.length;
|
|
4603
|
+
outcome.permissionDenied = failedRows.some((row) => /EACCES|EPERM/.test(row.error ?? ""));
|
|
4604
|
+
return outcome;
|
|
4220
4605
|
}
|
|
4221
4606
|
async function removeHard(target) {
|
|
4222
4607
|
try {
|
|
4223
|
-
await
|
|
4608
|
+
await fs20.rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 });
|
|
4224
4609
|
return null;
|
|
4225
4610
|
} catch (err) {
|
|
4226
4611
|
const code = err?.code ?? "ERR";
|
|
@@ -4233,6 +4618,7 @@ async function removeHard(target) {
|
|
|
4233
4618
|
async function executeAll(entries, request) {
|
|
4234
4619
|
const outcome = await deleteEntries(entries, {
|
|
4235
4620
|
mode: request.mode,
|
|
4621
|
+
trashMode: request.trashMode,
|
|
4236
4622
|
onRow: (_row, done, total) => {
|
|
4237
4623
|
progressLine(`\u{1F5D1} ${done}/${total} folders\u2026`);
|
|
4238
4624
|
}
|
|
@@ -4242,13 +4628,22 @@ async function executeAll(entries, request) {
|
|
|
4242
4628
|
else {
|
|
4243
4629
|
const summary = renderDeleteSummary(outcome, request.mode);
|
|
4244
4630
|
if (summary) log(summary);
|
|
4631
|
+
if (outcome.trashBatch !== void 0) {
|
|
4632
|
+
if (outcome.trashFallback === true) {
|
|
4633
|
+
note(`no OS trash backend (install gio or trash-cli) \u2014 fell back to the restorable nmh trash: ${outcome.trashBatch}`);
|
|
4634
|
+
} else {
|
|
4635
|
+
note(`restorable nmh trash: ${outcome.trashBatch} (manifest: manifest.jsonl \u2014 restore by moving folders back)`);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4245
4638
|
}
|
|
4246
4639
|
await writeDeleteLog({
|
|
4247
4640
|
mode: request.mode,
|
|
4248
4641
|
results: outcome.results,
|
|
4249
4642
|
freed: outcome.freed,
|
|
4250
4643
|
deleted: outcome.deleted,
|
|
4251
|
-
failed: outcome.failed
|
|
4644
|
+
failed: outcome.failed,
|
|
4645
|
+
...outcome.trashBatch !== void 0 ? { trashBatch: outcome.trashBatch } : {},
|
|
4646
|
+
...outcome.trashFallback === true ? { trashFallback: true } : {}
|
|
4252
4647
|
});
|
|
4253
4648
|
const code = outcome.permissionDenied ? 4 : outcome.failed > 0 ? 1 : 0;
|
|
4254
4649
|
return { code, outcome };
|
|
@@ -4264,7 +4659,9 @@ async function deleteFlow(request) {
|
|
|
4264
4659
|
recursive: request.recursive,
|
|
4265
4660
|
excludes: request.excludes,
|
|
4266
4661
|
keep: request.keep,
|
|
4267
|
-
pmFilter: request.pmFilter
|
|
4662
|
+
pmFilter: request.pmFilter,
|
|
4663
|
+
stale: request.stale,
|
|
4664
|
+
skipSystem: request.skipSystem
|
|
4268
4665
|
});
|
|
4269
4666
|
if (scan.result.unresolvable.length > 0) {
|
|
4270
4667
|
for (const suspect of scan.result.unresolvable) {
|
|
@@ -4309,7 +4706,7 @@ async function deleteFlow(request) {
|
|
|
4309
4706
|
return 1;
|
|
4310
4707
|
}
|
|
4311
4708
|
if (request.json) {
|
|
4312
|
-
const { code: code2 } = await executeAll(safeEntries, { mode, json: true });
|
|
4709
|
+
const { code: code2 } = await executeAll(safeEntries, { mode, json: true, trashMode: request.trashMode });
|
|
4313
4710
|
return code2;
|
|
4314
4711
|
}
|
|
4315
4712
|
if (!request.assumeYes) {
|
|
@@ -4324,7 +4721,7 @@ async function deleteFlow(request) {
|
|
|
4324
4721
|
return 0;
|
|
4325
4722
|
}
|
|
4326
4723
|
}
|
|
4327
|
-
const { code } = await executeAll(safeEntries, { mode, json: false });
|
|
4724
|
+
const { code } = await executeAll(safeEntries, { mode, json: false, trashMode: request.trashMode });
|
|
4328
4725
|
return code;
|
|
4329
4726
|
}
|
|
4330
4727
|
async function resolveMode(requested, json, dryRun) {
|
|
@@ -4389,14 +4786,17 @@ async function runClean(request) {
|
|
|
4389
4786
|
excludes: request.excludes,
|
|
4390
4787
|
keep: request.keep,
|
|
4391
4788
|
pmFilter: request.pmFilter,
|
|
4789
|
+
stale: request.stale,
|
|
4790
|
+
skipSystem: request.skipSystem,
|
|
4791
|
+
trashMode: request.trashMode,
|
|
4392
4792
|
maxDepth: request.maxDepth,
|
|
4393
4793
|
blockedMode: "filter"
|
|
4394
4794
|
});
|
|
4395
4795
|
}
|
|
4396
4796
|
|
|
4397
4797
|
// src/commands/wizard.ts
|
|
4398
|
-
import
|
|
4399
|
-
import
|
|
4798
|
+
import fs21 from "fs/promises";
|
|
4799
|
+
import path23 from "path";
|
|
4400
4800
|
import { confirm as confirm2, isCancel as isCancel2, multiselect, outro, select as select2, text } from "@clack/prompts";
|
|
4401
4801
|
|
|
4402
4802
|
// src/utils/banner.ts
|
|
@@ -4447,7 +4847,7 @@ var PROJECT_MANIFESTS = /* @__PURE__ */ new Set([
|
|
|
4447
4847
|
]);
|
|
4448
4848
|
async function hasProjectManifest(dir) {
|
|
4449
4849
|
try {
|
|
4450
|
-
const names = await
|
|
4850
|
+
const names = await fs21.readdir(dir);
|
|
4451
4851
|
for (const name of names) {
|
|
4452
4852
|
if (PROJECT_MANIFESTS.has(name)) return true;
|
|
4453
4853
|
}
|
|
@@ -4459,13 +4859,13 @@ async function hasProjectManifest(dir) {
|
|
|
4459
4859
|
async function resolveWizardScope(cwd) {
|
|
4460
4860
|
const here = await scanForNodeModules([cwd], { maxDepth: WIZARD_DEPTH });
|
|
4461
4861
|
if (here.entries.length > 0) return { root: cwd, from: cwd, climbed: false, scouted: false };
|
|
4462
|
-
let dir =
|
|
4463
|
-
for (let hop = 0; hop < WIZARD_MAX_CLIMB && dir !==
|
|
4862
|
+
let dir = path23.dirname(cwd);
|
|
4863
|
+
for (let hop = 0; hop < WIZARD_MAX_CLIMB && dir !== path23.dirname(dir); hop += 1) {
|
|
4464
4864
|
if (await hasProjectManifest(dir)) {
|
|
4465
4865
|
const probe = await scanForNodeModules([dir], { maxDepth: WIZARD_DEPTH });
|
|
4466
4866
|
if (probe.entries.length > 0) return { root: dir, from: cwd, climbed: true, scouted: true };
|
|
4467
4867
|
}
|
|
4468
|
-
dir =
|
|
4868
|
+
dir = path23.dirname(dir);
|
|
4469
4869
|
}
|
|
4470
4870
|
return { root: cwd, from: cwd, climbed: false, scouted: true };
|
|
4471
4871
|
}
|
|
@@ -4651,11 +5051,11 @@ function sum(entries) {
|
|
|
4651
5051
|
}
|
|
4652
5052
|
|
|
4653
5053
|
// src/commands/global.ts
|
|
4654
|
-
import { execFile as
|
|
4655
|
-
import { promisify as
|
|
4656
|
-
import
|
|
4657
|
-
import
|
|
4658
|
-
var execFileAsync2 =
|
|
5054
|
+
import { execFile as execFile5 } from "child_process";
|
|
5055
|
+
import { promisify as promisify5 } from "util";
|
|
5056
|
+
import fs22 from "fs/promises";
|
|
5057
|
+
import path24 from "path";
|
|
5058
|
+
var execFileAsync2 = promisify5(execFile5);
|
|
4659
5059
|
var PROBE_TIMEOUT = 8e3;
|
|
4660
5060
|
async function probeViaCommand(cmd, argv) {
|
|
4661
5061
|
try {
|
|
@@ -4678,7 +5078,7 @@ async function probeRoot(cmd, argv, shape, fallback) {
|
|
|
4678
5078
|
}
|
|
4679
5079
|
async function isDir(candidate) {
|
|
4680
5080
|
try {
|
|
4681
|
-
return (await
|
|
5081
|
+
return (await fs22.stat(candidate)).isDirectory();
|
|
4682
5082
|
} catch {
|
|
4683
5083
|
return false;
|
|
4684
5084
|
}
|
|
@@ -4704,12 +5104,12 @@ async function inspectDeadBins(root, binDir, refs) {
|
|
|
4704
5104
|
else if (binField && typeof binField === "object") binNames = Object.keys(binField);
|
|
4705
5105
|
else continue;
|
|
4706
5106
|
for (const binName of binNames) {
|
|
4707
|
-
const link =
|
|
5107
|
+
const link = path24.join(binDir, binName);
|
|
4708
5108
|
try {
|
|
4709
|
-
const linkStat = await
|
|
5109
|
+
const linkStat = await fs22.lstat(link);
|
|
4710
5110
|
if (!linkStat.isSymbolicLink()) continue;
|
|
4711
5111
|
try {
|
|
4712
|
-
await
|
|
5112
|
+
await fs22.stat(link);
|
|
4713
5113
|
} catch {
|
|
4714
5114
|
issues.push({ name: ref.name, kind: "broken bin symlink", detail: binName });
|
|
4715
5115
|
}
|
|
@@ -4722,40 +5122,40 @@ async function inspectDeadBins(root, binDir, refs) {
|
|
|
4722
5122
|
}
|
|
4723
5123
|
async function resolvePmRoots(pmFilter) {
|
|
4724
5124
|
const npmFallback = {
|
|
4725
|
-
root:
|
|
4726
|
-
binDir:
|
|
5125
|
+
root: path24.join(expandTilde("~"), "lib", "node_modules"),
|
|
5126
|
+
binDir: path24.join(expandTilde("~"), "bin"),
|
|
4727
5127
|
source: "fallback"
|
|
4728
5128
|
};
|
|
4729
5129
|
const pnpmFallback = await pnpmFallbackSpec();
|
|
4730
5130
|
const yarnFallback = {
|
|
4731
|
-
root:
|
|
4732
|
-
binDir:
|
|
5131
|
+
root: path24.join(expandTilde("~"), ".config", "yarn", "global", "node_modules"),
|
|
5132
|
+
binDir: path24.join(expandTilde("~"), ".config", "yarn", "global", "bin"),
|
|
4733
5133
|
source: "fallback"
|
|
4734
5134
|
};
|
|
4735
5135
|
const bunFallback = {
|
|
4736
|
-
root:
|
|
4737
|
-
binDir:
|
|
5136
|
+
root: path24.join(expandTilde("~"), ".bun", "install", "global", "node_modules"),
|
|
5137
|
+
binDir: path24.join(expandTilde("~"), ".bun", "bin"),
|
|
4738
5138
|
source: "fallback"
|
|
4739
5139
|
};
|
|
4740
5140
|
const wanted = pmFilter ?? ["npm", "pnpm", "yarn", "bun"];
|
|
4741
5141
|
const results = [];
|
|
4742
5142
|
for (const pm of wanted) {
|
|
4743
5143
|
let spec = null;
|
|
4744
|
-
if (pm === "npm") spec = await probeRoot("npm", ["root", "-g"], (dir) => ({ root: dir, binDir:
|
|
5144
|
+
if (pm === "npm") spec = await probeRoot("npm", ["root", "-g"], (dir) => ({ root: dir, binDir: path24.join(path24.dirname(path24.dirname(dir)), "bin"), source: "pm" }), npmFallback);
|
|
4745
5145
|
else if (pm === "pnpm") spec = await probeRoot("pnpm", ["root", "-g"], (dir) => ({ root: dir, binDir: null, source: "pm" }), pnpmFallback);
|
|
4746
|
-
else if (pm === "yarn") spec = await probeRoot("yarn", ["global", "dir"], (dir) => ({ root:
|
|
4747
|
-
else if (pm === "bun") spec = await probeRoot("bun", ["pm", "-g"], (dir) => ({ root: dir, binDir:
|
|
5146
|
+
else if (pm === "yarn") spec = await probeRoot("yarn", ["global", "dir"], (dir) => ({ root: path24.join(dir, "node_modules"), binDir: path24.join(dir, "bin"), source: "pm" }), yarnFallback);
|
|
5147
|
+
else if (pm === "bun") spec = await probeRoot("bun", ["pm", "-g"], (dir) => ({ root: dir, binDir: path24.join(path24.dirname(path24.dirname(dir)), "bin"), source: "pm" }), bunFallback);
|
|
4748
5148
|
if (spec) results.push({ pm, spec });
|
|
4749
5149
|
else verboseLog(`global: ${pm} \u2014 no global node_modules found`);
|
|
4750
5150
|
}
|
|
4751
5151
|
return results;
|
|
4752
5152
|
}
|
|
4753
5153
|
async function pnpmFallbackSpec() {
|
|
4754
|
-
const base =
|
|
5154
|
+
const base = path24.join(expandTilde("~"), ".local", "share", "pnpm", "global");
|
|
4755
5155
|
try {
|
|
4756
|
-
const versions = (await
|
|
5156
|
+
const versions = (await fs22.readdir(base)).filter((name) => /^\d+$/.test(name)).sort((a, b) => Number(b) - Number(a));
|
|
4757
5157
|
for (const version of versions) {
|
|
4758
|
-
const root =
|
|
5158
|
+
const root = path24.join(base, version, "node_modules");
|
|
4759
5159
|
if (await isDir(root)) return { root, binDir: null, source: "fallback" };
|
|
4760
5160
|
}
|
|
4761
5161
|
} catch {
|
|
@@ -4839,6 +5239,11 @@ cli.option("--dry-run", "Preview what would be deleted \u2014 nothing is touched
|
|
|
4839
5239
|
cli.option("-y, --yes", "Answer yes to every prompt");
|
|
4840
5240
|
cli.option("-f, --force", "Skip all confirmations (implies --yes)");
|
|
4841
5241
|
cli.option("--size <mode>", "'total' (just aggregate) or 'each' (default); numeric top-N lands later");
|
|
5242
|
+
cli.option("--stale <days>", "v1.1: only projects idle >= <days> (git activity signal \u2014 last commit, else manifest, else folder mtime)");
|
|
5243
|
+
cli.option("--fast-size", "v1.1: measure with one `du -sk` per entry (KiB granularity) instead of walking every file");
|
|
5244
|
+
cli.option("--sort <key>", "v1.1: order results \u2014 size (default, desc) | path | idle (oldest project first) | mtime (newest first)");
|
|
5245
|
+
cli.option("--no-skip", "v1.1: audit everything \u2014 stop hiding .Trash*, Library/, and *.app dirs during discovery (default: hidden)");
|
|
5246
|
+
cli.option("--trash <mode>", "v1.1: trash mode for delete ops \u2014 'system' (default: gio/Finder/Recycle Bin) or 'nmh' (restorable ~/.nmh/trash + manifest)");
|
|
4842
5247
|
cli.option("--json", "Machine-readable JSON output");
|
|
4843
5248
|
cli.option("-q, --quiet", "Minimal output \u2014 pretty tables/notes mute, but the --json payload still prints");
|
|
4844
5249
|
cli.option("--verbose", "Debug chatter on stderr");
|
|
@@ -4978,6 +5383,59 @@ async function main() {
|
|
|
4978
5383
|
errorLog("--size given more than once \u2014 pass a single value (total | each)");
|
|
4979
5384
|
return 1;
|
|
4980
5385
|
}
|
|
5386
|
+
let stale;
|
|
5387
|
+
if (flags.stale !== void 0) {
|
|
5388
|
+
if (flags.stale === true) {
|
|
5389
|
+
errorLog("--stale needs a value \u2014 use --stale=<days> (e.g. --stale=90)");
|
|
5390
|
+
return 1;
|
|
5391
|
+
}
|
|
5392
|
+
if (Array.isArray(flags.stale)) {
|
|
5393
|
+
errorLog("--stale given more than once \u2014 pass a single value (days)");
|
|
5394
|
+
return 1;
|
|
5395
|
+
}
|
|
5396
|
+
const days = Number(flags.stale);
|
|
5397
|
+
if (!Number.isInteger(days) || days < 1 || days > 365e3) {
|
|
5398
|
+
errorLog(`invalid --stale: ${String(flags.stale)} (expected a positive whole number of days)`);
|
|
5399
|
+
return 1;
|
|
5400
|
+
}
|
|
5401
|
+
stale = days;
|
|
5402
|
+
}
|
|
5403
|
+
const fastSize = flags.fastSize === true || Array.isArray(flags.fastSize) && flags.fastSize.includes(true);
|
|
5404
|
+
let sort;
|
|
5405
|
+
if (flags.sort !== void 0) {
|
|
5406
|
+
if (flags.sort === true) {
|
|
5407
|
+
errorLog("--sort needs a value \u2014 use --sort=size | path | idle | mtime");
|
|
5408
|
+
return 1;
|
|
5409
|
+
}
|
|
5410
|
+
if (Array.isArray(flags.sort)) {
|
|
5411
|
+
errorLog("--sort given more than once \u2014 pass a single key");
|
|
5412
|
+
return 1;
|
|
5413
|
+
}
|
|
5414
|
+
const key = String(flags.sort);
|
|
5415
|
+
if (key !== "size" && key !== "path" && key !== "idle" && key !== "mtime") {
|
|
5416
|
+
errorLog(`invalid --sort: ${key} (expected size | path | idle | mtime)`);
|
|
5417
|
+
return 1;
|
|
5418
|
+
}
|
|
5419
|
+
sort = key;
|
|
5420
|
+
}
|
|
5421
|
+
const skipSystem = flags.skip !== false;
|
|
5422
|
+
let trashMode;
|
|
5423
|
+
if (flags.trash !== void 0) {
|
|
5424
|
+
if (flags.trash === true) {
|
|
5425
|
+
errorLog("--trash needs a value \u2014 use --trash=system or --trash=nmh");
|
|
5426
|
+
return 1;
|
|
5427
|
+
}
|
|
5428
|
+
if (Array.isArray(flags.trash)) {
|
|
5429
|
+
errorLog("--trash given more than once \u2014 pass a single mode");
|
|
5430
|
+
return 1;
|
|
5431
|
+
}
|
|
5432
|
+
const mode = String(flags.trash);
|
|
5433
|
+
if (mode !== "system" && mode !== "nmh") {
|
|
5434
|
+
errorLog(`invalid --trash: ${mode} (expected system | nmh)`);
|
|
5435
|
+
return 1;
|
|
5436
|
+
}
|
|
5437
|
+
trashMode = mode;
|
|
5438
|
+
}
|
|
4981
5439
|
const deleted = normalizeDelete(flags.delete);
|
|
4982
5440
|
if (deleted === "invalid") {
|
|
4983
5441
|
errorLog(`invalid --delete: ${String(flags.delete)} (expected 'hard' | 'soft' | 'trash')`);
|
|
@@ -5023,7 +5481,7 @@ async function main() {
|
|
|
5023
5481
|
}
|
|
5024
5482
|
}
|
|
5025
5483
|
if (reportPath !== null) {
|
|
5026
|
-
const ext =
|
|
5484
|
+
const ext = path25.extname(reportPath).toLowerCase();
|
|
5027
5485
|
if (!REPORT_EXTENSIONS.includes(ext)) {
|
|
5028
5486
|
errorLog(`unknown report format: ${reportPath} (expected .json/.md/.txt)`);
|
|
5029
5487
|
return 1;
|
|
@@ -5064,9 +5522,12 @@ async function main() {
|
|
|
5064
5522
|
errorLog("--save-report rides along with scan \u2014 delete/clean runs are not scan reports");
|
|
5065
5523
|
return 1;
|
|
5066
5524
|
}
|
|
5525
|
+
if (trashMode !== void 0 && !deleted.given && subcommand !== "clean") {
|
|
5526
|
+
warn("--trash only matters for delete/clean runs \u2014 ignored for scans and read-only commands");
|
|
5527
|
+
}
|
|
5067
5528
|
if (deleted.given || subcommand === "clean") {
|
|
5068
5529
|
for (const candidate of targets.length > 0 ? targets : [""]) {
|
|
5069
|
-
if (isRootPath(
|
|
5530
|
+
if (isRootPath(path25.resolve(cwd, candidate))) {
|
|
5070
5531
|
errorLog("refusing a delete op over the filesystem root \u2014 point --dir at the project directory you want cleaned");
|
|
5071
5532
|
return 1;
|
|
5072
5533
|
}
|
|
@@ -5084,12 +5545,23 @@ async function main() {
|
|
|
5084
5545
|
pmFilter: pm.family !== null ? [pm.family] : void 0,
|
|
5085
5546
|
listPm: pm.given && pm.family === null,
|
|
5086
5547
|
expand,
|
|
5087
|
-
saveReport: reportPath ?? void 0
|
|
5548
|
+
saveReport: reportPath ?? void 0,
|
|
5549
|
+
stale,
|
|
5550
|
+
fastSize,
|
|
5551
|
+
sort,
|
|
5552
|
+
skipSystem
|
|
5088
5553
|
};
|
|
5089
5554
|
if (subcommand === "version") {
|
|
5090
5555
|
printVersion(json);
|
|
5091
5556
|
return 0;
|
|
5092
5557
|
}
|
|
5558
|
+
if (subcommand === "update") {
|
|
5559
|
+
if (deleted.given || rest.length > 0) {
|
|
5560
|
+
errorLog("update takes no arguments \u2014 plain `nmh update` (add --json for the payload)");
|
|
5561
|
+
return 1;
|
|
5562
|
+
}
|
|
5563
|
+
return runUpdate({ json });
|
|
5564
|
+
}
|
|
5093
5565
|
if (subcommand === "config") {
|
|
5094
5566
|
const ops = /* @__PURE__ */ new Set(["info", "set", "add", "remove", "unset"]);
|
|
5095
5567
|
const opRaw = rest[0];
|
|
@@ -5355,10 +5827,17 @@ async function main() {
|
|
|
5355
5827
|
excludes: excludes.length > 0 ? excludes : void 0,
|
|
5356
5828
|
keep: keep.length > 0 ? keep : void 0,
|
|
5357
5829
|
pmFilter: pm.family !== null ? [pm.family] : void 0,
|
|
5358
|
-
maxDepth: effectiveDepth
|
|
5830
|
+
maxDepth: effectiveDepth,
|
|
5831
|
+
stale,
|
|
5832
|
+
skipSystem,
|
|
5833
|
+
trashMode
|
|
5359
5834
|
});
|
|
5360
5835
|
}
|
|
5361
5836
|
if (deleted.given) {
|
|
5837
|
+
if (trashMode !== void 0 && deleted.mode === "hard") {
|
|
5838
|
+
errorLog("--trash applies to trash mode \u2014 hard delete is permanent by design, there is no trash to choose");
|
|
5839
|
+
return 1;
|
|
5840
|
+
}
|
|
5362
5841
|
return runDelete({
|
|
5363
5842
|
targets,
|
|
5364
5843
|
cwd,
|
|
@@ -5370,7 +5849,10 @@ async function main() {
|
|
|
5370
5849
|
excludes: excludes.length > 0 ? excludes : void 0,
|
|
5371
5850
|
keep: keep.length > 0 ? keep : void 0,
|
|
5372
5851
|
pmFilter: pm.family !== null ? [pm.family] : void 0,
|
|
5373
|
-
maxDepth: effectiveDepth
|
|
5852
|
+
maxDepth: effectiveDepth,
|
|
5853
|
+
stale,
|
|
5854
|
+
skipSystem,
|
|
5855
|
+
trashMode
|
|
5374
5856
|
});
|
|
5375
5857
|
}
|
|
5376
5858
|
if (positionals.length > 0 || dirTargets.length > 0) {
|