node-modules-hunter 0.0.0-stage β 1.0.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/LICENSE +21 -0
- package/README.md +445 -2
- package/dist/cli.js +5409 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 node-modules-hunter contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,446 @@
|
|
|
1
|
-
#
|
|
1
|
+
# π node-modules-hunter (nmh)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Hunt down bloated `node_modules` folders across your machine β scan, measure, and clean them **with one command**.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
nmh --dir=./my-project,./my-repo/packages/*/node_modules --delete=hard
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`node_modules` grows absurdly and piles up across every repo, sub-repo and artifact you forget about. Manual cleanup means cd-ing into each project, checking, deleting, praying. **nmh** does it in one line, safely, cross-platform (Linux Β· macOS Β· Windows), and cross-package-manager (npm Β· pnpm Β· Yarn Β· bun β *and* [Vite+ (`vp`)](https://viteplus.dev) projects, whose `vp` wrapper is detected through the underlying manager's lockfile the same way vp itself detects it).
|
|
10
|
+
|
|
11
|
+
For a plain interactive TUI alternative, check [npkill](https://github.com/voidcosmos/npkill) β nmh's differentiators are scriptable flags (CI-friendly), monorepo-aware package-manager detection, and accurate byte accounting that never follows symlinks.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# run once without installing
|
|
19
|
+
npx node-modules-hunter scan
|
|
20
|
+
|
|
21
|
+
# or install globally (bin: nmh, alias: node-modules-hunter)
|
|
22
|
+
npm i -g node-modules-hunter
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Node.js β₯ 18 required.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
nmh # interactive wizard: arrow menu (delete all / pick / search / map / report / settings)
|
|
31
|
+
nmh <path> [pathβ¦] # shortcut: scan given path(s)
|
|
32
|
+
nmh scan [dir] # full scan (default: cwd)
|
|
33
|
+
nmh clean [dir] # scan + delete everything safe (trash by default)
|
|
34
|
+
nmh search <pattern> [dirβ¦] # find installed packages by name (+ versions & sizes)
|
|
35
|
+
nmh map [dir] # project β PM β size β top packages β declared deps
|
|
36
|
+
nmh stats [dir] # size analytics: dup factor, waste size, top-N, per-package tree
|
|
37
|
+
nmh stats --by type [dir] # breakdown per dependency field (dep/dev/optional/peer/transitive)
|
|
38
|
+
nmh stats --licenses [dir] # license buckets (+β copyleft inside production deps)
|
|
39
|
+
nmh stats --score / --co2 # experimental: composite bloat score 0-100 / rough CO2 estimate
|
|
40
|
+
nmh stats --online [dir] # deprecated scan: offline manifests + lazy cached registry fallback
|
|
41
|
+
nmh stats --graph [dir] # dependency graph from the project lockfile β npm/yarn/pnpm/bun, no deps
|
|
42
|
+
nmh stats --reinstall [dir] # naive npm-ci-style "how long to reinstall everything" estimate
|
|
43
|
+
nmh stats -g # global installs across npm/pnpm/yarn/bun β read-only analysis
|
|
44
|
+
# aliases: `analyze` and `info` behave exactly like `stats` (recognition from PRD-era names)
|
|
45
|
+
nmh why <pkg> [dirβ¦] # reverse depend: where is <pkg> and what requires it β read-only
|
|
46
|
+
nmh treemap [dir] # single-file interactive HTML treemap β nmh-treemap.html
|
|
47
|
+
|
|
48
|
+
nmh --dir=A,B,glob/*/node_modules # targets: comma list, globs, ~ β combine with flags below
|
|
49
|
+
nmh version | --version | -v # banner: tool version, node+abi, OS/arch/kernel distro, tty β --json for plain fields
|
|
50
|
+
nmh help | --help | -h
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Search patterns: plain text is a case-insensitive substring (`nmh search lodash`), glob chars match whole names (`v*`, `@scope/*`), and `/re/flags` is a real regex (`/^vi/`).
|
|
54
|
+
|
|
55
|
+
| Flag | Effect |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `-d, --dir <paths>` | Target dirs β comma separated or repeated; globs (`./packages/*/node_modules`) and `~` allowed |
|
|
58
|
+
| `--delete=hard\|soft\|trash` | Delete mode: `hard` = permanent, `trash`/`soft` = OS recycle bin; no value = pick interactively |
|
|
59
|
+
| `--dry-run` | Preview what would be deleted β **nothing is touched** |
|
|
60
|
+
| `-f, --force` | Skip confirmations (implies `--yes`) |
|
|
61
|
+
| `-y, --yes` | Answer yes to every prompt |
|
|
62
|
+
| `--size total\|each` | Aggregate-only output (default: per-folder table) |
|
|
63
|
+
| `-r, --recursive` | Also look INSIDE found `node_modules` β nested entries are listed flagged and never counted twice in totals |
|
|
64
|
+
| `--pm <family>` | Keep only entries detected as `npm\|pnpm\|yarn\|bun\|vp`; bare `--pm` just lists what was detected |
|
|
65
|
+
| `--exclude <globs>` | Drop matching paths from results and traversal (comma list, e.g. `**/e2e/**`) |
|
|
66
|
+
| `--keep <pkgs>` | Refuse to delete the `node_modules` of these projects (comma list, glob ok) |
|
|
67
|
+
| `--depth <n>` | Hard cap on discovery depth (levels below the target) |
|
|
68
|
+
| `--expand` | Full absolute paths in output |
|
|
69
|
+
| `--search <pattern>` | Same as the `search` command, mixed into any read-only form (`nmh --dir=X --search lodash`) |
|
|
70
|
+
| `-m, --map [--tree]` | Project map table; `--tree` lists package names per project |
|
|
71
|
+
| `--depend [pkg]` | With `map`: show declared deps per project; with a value, list which projects declare it |
|
|
72
|
+
| `--stats` | Size analytics: instances vs unique names, duplication factor, dedupable waste, top-N list |
|
|
73
|
+
| `--tree` / `--top <n>` / `--by size\|type\|licenses` | With `stats`: per-package size tree / row count (default 10) / breakdown mode β package sizes, the declaring dep-field breakdown (`type`), or license buckets (`licenses`: historical field shapes normalized, `unknown` last, β copyleft in production deps) |
|
|
74
|
+
| `-g, --global` | With `stats`: analyze the global installs of npm/pnpm/yarn/bun (per-PM prefix scan, cross-PM duplicates, dead bin symlinks β read-only) |
|
|
75
|
+
| `--save-baseline <name>` / `--compare <name>` / `--history` | With `stats`: snapshot totals+per-package to `~/.nmh/baselines/<name>.json`, diff a later run against it (totals, per-node_modules tree, package movers, >30% growth warning), or just list snapshots |
|
|
76
|
+
| `--why <pkg>` | Same as the `why` command, mixed into any read-only form |
|
|
77
|
+
| `--phantom` / `--dead` | With `stats`: best-effort regex audit β imports declared nowhere / declared+installed packages nobody imports |
|
|
78
|
+
| `--score` / `--co2` / `--licenses` / `--online` | With `stats` (focused views): experimental bloat score 0β100 (waste % + dup factor + dead % + size, grade AβF) / rough CO2 estimate (g CO2e per GB-year + per redownload) / license buckets with copyleft flag / deprecated check (offline manifests + lazy cached registry fallback for missing notes) |
|
|
79
|
+
| `--graph` | With `stats` (focused view): dependency graph from the project lockfile β npm `package-lock.json` v2/v3, `yarn.lock` v1, `pnpm-lock.yaml`, `bun.lock` (zero new dependencies); edges + BFS depth histogram; unusable lockfiles stay honestly manifest-grade. Also powers `nmh why` (lockfile-grade required-by) |
|
|
80
|
+
| `--save-report <file>` / `--report <file>` | `scan`, `map`, `search`, `stats`: **also** write the printed report to a file β format picked by extension `.json` / `.md` / `.txt` (write failure = warning, exit unchanged) |
|
|
81
|
+
| `--treemap [file]` / `--out <file>` | `nmh treemap <dirs>`: export one self-contained interactive HTML treemap (default `nmh-treemap.html`); `--json` prints the raw data instead |
|
|
82
|
+
| `--reinstall` | With `stats` (focused view): naive npm-ci-style estimate of how long re-installing everything takes (pkg Γ 60 ms + bytes / 50 MB/s + 3 s per native-family build); assumptions surfaced in the JSON, labeled naif |
|
|
83
|
+
| `--animate` / `--no-animate` | Live size counter while measuring: auto = real TTY only, `--animate` forces it even when piped; always suppressed under `--json`; `.nmhrc animate:false` mutes it |
|
|
84
|
+
| `--update-check` | Opt-in: peek npm for a newer release (non-blocking, 24 h cache, notice on stderr; also `updateCheck` in `.nmhrc`) |
|
|
85
|
+
| `--config <file>` | Use this config file instead of the `.nmhrc` lookup (cwd β `$HOME`) |
|
|
86
|
+
| `--json` | Machine-readable output (pipe into `jq`) |
|
|
87
|
+
| `-q/--quiet`, `--verbose`, `--no-color` | Output control |
|
|
88
|
+
|
|
89
|
+
The `.nmhrc` lookup walks every ancestor from cwd up to `$HOME` β and it honors `logDir`, the directory where delete recovery logs are written. Only keep a `.nmhrc` in directories you control: in a shared checkout, a parent directory you don't own could steer your recovery logs and scan defaults; when in doubt, pass an explicit `--config`.
|
|
90
|
+
|
|
91
|
+
### Example
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
$ nmh scan ./my-repo
|
|
95
|
+
ββββββββββββββββββββββββββββββββββββββββ¬βββββββββββ¬βββββββ¬βββββββ
|
|
96
|
+
β Path β Size β Pkgs β PM β
|
|
97
|
+
ββββββββββββββββββββββββββββββββββββββββΌβββββββββββΌβββββββΌβββββββ€
|
|
98
|
+
β ./packages/web/node_modules β 1.20 GB β 1843 β pnpm β
|
|
99
|
+
β ./packages/api/node_modules β 640 MB β 912 β npm β
|
|
100
|
+
ββββββββββββββββββββββββββββββββββββββββ΄βββββββββββ΄βββββββ΄βββββββ
|
|
101
|
+
πΎ Total: 2.41 GB across 12 folder(s), 4820 packages
|
|
102
|
+
|
|
103
|
+
$ nmh --dir=./my-repo/packages/*/node_modules --delete=hard --dry-run
|
|
104
|
+
[DRY RUN] Would DELETE (permanent) 12 folder(s) (2.4 GB):
|
|
105
|
+
β ./packages/web/node_modules (1.2 GB, pnpm)
|
|
106
|
+
β¦
|
|
107
|
+
Run again without --dry-run to execute.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Size analytics (v0.3)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
$ nmh stats ./my-repo
|
|
114
|
+
π nmh stats β 2 node_modules Β· dedup potential detected
|
|
115
|
+
|
|
116
|
+
βββ OVERVIEW βββ
|
|
117
|
+
Total size : 11.4 KB across 2 node_modules
|
|
118
|
+
Packages : 4 instances / 3 unique (dup 1.3Γ)
|
|
119
|
+
Waste (dedupable) : 3.7 KB (32.9% of package sizes)
|
|
120
|
+
|
|
121
|
+
βββ TOP 10 SIZE βββ
|
|
122
|
+
1. lodash Γ2 copies 7.7 KB β dedup bisa hemat 3.7 KB
|
|
123
|
+
2. moment Γ1 copy 2.5 KB
|
|
124
|
+
|
|
125
|
+
$ nmh stats ./my-repo --tree # per-package sizes inside each node_modules (PM column: β = unknown)
|
|
126
|
+
web Β· ./web/node_modules Β· β Β· 6.4 KB
|
|
127
|
+
ββ lodash@4.17.21 (3.9 KB)
|
|
128
|
+
ββ moment@2.29.4 (2.5 KB)
|
|
129
|
+
|
|
130
|
+
$ nmh stats -g # every global install, per package manager
|
|
131
|
+
π nmh stats β global installation analysis
|
|
132
|
+
(β οΈ global mode β read-only; needs access to the PM prefixes β sudo is never auto-applied)
|
|
133
|
+
|
|
134
|
+
PM detected : npm (not found: pnpm, yarn, bun)
|
|
135
|
+
Total global : 2 packages β 11.8 KB
|
|
136
|
+
|
|
137
|
+
ββββββββββββββ¬ββββββββββ¬βββββββββ¬ββββββ
|
|
138
|
+
β Package β Version β Size β PM β
|
|
139
|
+
ββββββββββββββΌββββββββββΌβββββββββΌββββββ€
|
|
140
|
+
β @acme/beta β 2.3.4 β 3.0 KB β npm β
|
|
141
|
+
β alpha β 1.0.0 β 8.8 KB β npm β
|
|
142
|
+
ββββββββββββββ΄ββββββββββ΄βββββββββ΄ββββββ
|
|
143
|
+
β dead globals in npm: 1 (broken/missing bin symlink)
|
|
144
|
+
- alpha β alpha (missing bin symlink)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Stats rank the same package across many `node_modules` folders, so `dupFactor` grows with copies; "waste" counts only **exact-same-version** duplicates minus one copy each (never mind-measured, symlink-safe). `stats -g` discovers each PM's real global prefix (`npm root -g`, `pnpm root -g`, `yarn global dir`, `bun pm -g` with fallback paths), stays top-level-only and read-only, and flags packages whose declared `bin` commands are missing/broken in the global bin dir.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
$ nmh scan ./my-repo --save-report report.md # table on screen + file copy (also --report, .json/.md/.txt)
|
|
151
|
+
βΉ πΎ report saved β report.md (md)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Analytics deep-cut & audits (v0.4)
|
|
155
|
+
|
|
156
|
+
**Baseline & compare** β snapshot a repo before a big refactor, diff after:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
$ nmh stats ./my-repo --save-baseline before # β ~/.nmh/baselines/before.json
|
|
160
|
+
$ β¦ dikerjain nanti β¦
|
|
161
|
+
$ nmh stats ./my-repo --compare before
|
|
162
|
+
βββ BASELINE COMPARE βββ
|
|
163
|
+
Baseline : 'before' saved 2026-10-22T03:33:15.875Z
|
|
164
|
+
Total size : +2.4 KB (+44.8%) β was 5.4 KB, now 7.9 KB
|
|
165
|
+
Packages : Β±0 B
|
|
166
|
+
ββββββββββββββββββββ¬βββββββββ¬βββββββββ¬ββββββββββ
|
|
167
|
+
β Path β Before β Now β Ξ β
|
|
168
|
+
ββββββββββββββββββββΌβββββββββΌβββββββββΌββββββββββ€
|
|
169
|
+
β api/node_modules β 2.5 KB β 4.9 KB β +2.4 KB β
|
|
170
|
+
ββββββββββββββββββββ΄βββββββββ΄βββββββββ΄ββββββββββ
|
|
171
|
+
|
|
172
|
+
Package movers :
|
|
173
|
+
1. lodash +2.4 KB
|
|
174
|
+
π¨ bloat growth detected β node_modules grew by more than 30% since the baseline
|
|
175
|
+
|
|
176
|
+
$ nmh stats --history # list saved baselines (newest first) β no scan
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Dedup suggestion** β per-copy attribution so the waste number becomes actionable:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
βββ DEDUP SUGGESTION βββ
|
|
183
|
+
1. lodash@4.17.21 Γ2 copies +3.0 KB bisa hemat
|
|
184
|
+
Β· ./web/node_modules Β· 3.9 KB Β· web declared ^4.17.0 (dependencies)
|
|
185
|
+
Β· ./api/node_modules Β· 3.0 KB Β· api declared ^4.17.21 (devDependencies)
|
|
186
|
+
β³ range declared beda (^4.17.0 vs ^4.17.21) β samakan dulu di package.json, lalu install ulang
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Identical ranges get the hoist/dedupe hint (`npm dedupe` / `yarn dedupe`, pnpm virtual store), mixed ranges get the align-first warning, undeclared copies get the honest transitive note. Symlinked copies never count as a dedupable copy.
|
|
190
|
+
|
|
191
|
+
**`nmh why <pkg>`** β reverse depend at manifest level:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
$ nmh why lodash ./my-repo
|
|
195
|
+
π why 'lodash' β 1 instance(s)
|
|
196
|
+
|
|
197
|
+
./web/node_modules Β· lodash@4.17.21 Β· 3.9 KB
|
|
198
|
+
project : web declares ^4.17.21 (dependencies)
|
|
199
|
+
required by : 1 package(s)
|
|
200
|
+
Β· express@4.19.2 β ^4.17.0 (dependencies)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Required-by uses only each sibling's **own manifest** (no ancestor climb, so a project declaring lodash is never attributed to innocent siblings); orphan copies say so; symlinked copies show as `linked` without a size.
|
|
204
|
+
|
|
205
|
+
**`--by type` / `--phantom` / `--dead`** β the dependency-field breakdown plus the regex audit:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
βββ BY TYPE βββ
|
|
209
|
+
dependencies : 2.0 KB Β· 1 pkg
|
|
210
|
+
devDependencies : 1.5 KB Β· 1 pkg
|
|
211
|
+
peerDependencies : 437 B Β· 1 pkg
|
|
212
|
+
transitive : 735 B Β· 1 pkg
|
|
213
|
+
|
|
214
|
+
βββ DEPS AUDIT βββ
|
|
215
|
+
Β· best-effort regex scan β 1 source file(s) (lockfile graph lands later)
|
|
216
|
+
π§ phantom imports β imported in code, declared nowhere (1)
|
|
217
|
+
Β· chalk β src/index.ts
|
|
218
|
+
πͺ¦ dead declarations β installed + declared, never imported (1)
|
|
219
|
+
Β· unused β web (dependencies)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Only bare package specifiers count (`node:fs`, `./util`, `#alias` skipped; subpaths folded to the package name); `peerDependencies` never count as dead; both audits are honest best-effort, lockfile-grade graphs land later. All of this is stats-family state: read-only, and combining any of it with `--delete`/`clean` is refused (exit 1).
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
$ nmh stats ./my-repo --update-check # opt-in npm peek, 24 h cache
|
|
226
|
+
βΉ update tersedia: 0.4.0 β 0.5.0 β npm i -g node-modules-hunter (β¦)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Vision & hygiene extras (v0.5)
|
|
230
|
+
|
|
231
|
+
**License scan** β every installed instance gets bucketed by its manifest `license` field (all historical shapes normalized: string, `{type}`, `[{type}]`); the `unknown` bucket lands last, and copyleft families (GPL/AGPL/LGPL/MPL) get a β only when they ship inside production dependencies:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
$ nmh stats ./my-repo --licenses # (= --by licenses)
|
|
235
|
+
βββ LICENSES βββ
|
|
236
|
+
MIT : 8.1 MB Β· 42 pkg
|
|
237
|
+
Apache-2.0 : 2.0 MB Β· 11 pkg
|
|
238
|
+
GPL-3.0-or-later : 401 B Β· 1 pkg β copyleft di production dependencies
|
|
239
|
+
unknown : 41 B Β· 1 pkg
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Bloat Score & CO2** (experimental per decision #6) β one offline number for "how bloated is this folder" (waste % dominates at 40 pt, dup factor 20, dead declarations 20, absolute size vs 512 MB 20; grades AβF at 20-point steps), plus a clearly-labeled rough carbon estimate (~2 g CO2e per GB idle-year stored, ~10 g per GB redownloaded; small sizes stay honest to the third decimal):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
$ nmh stats ./my-repo --score --co2
|
|
246
|
+
βββ BLOAT SCORE βββ (experimental)
|
|
247
|
+
20 / 100 grade B [ββββββββββββββββββββ]
|
|
248
|
+
waste 0% β 0 pt
|
|
249
|
+
dup factor 1Γ β 0 pt
|
|
250
|
+
dead 50% (1 pkg) β 20 pt
|
|
251
|
+
size 3.5 MB β 0 pt
|
|
252
|
+
estimasi kasar, bukan audit karbon β simpan setahun: 0.030 g CO2e
|
|
253
|
+
kalau men-download ulang semuanya sekali: 0.310 g CO2e
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The score runs the deps audit implicitly for its deadness component (without printing it β add `--dead` for the actual list).
|
|
257
|
+
|
|
258
|
+
**Deprecated packages** β `--online` scans the installed manifests **offline first** (npm writes the `deprecated` note into package.json at install time); instances without a local note get a lazy, cached registry lookup β capped at 25 lookups per run, cached 30 days under `~/.nmh/cache/registry-deprecated/`, `NMH_REGISTRY_URL` points elsewhere (mirror/test), and network failures stay silent:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
$ nmh stats ./my-repo --online
|
|
262
|
+
βββ DEPRECATED βββ
|
|
263
|
+
1 instance deprecated:
|
|
264
|
+
old-lodash@4.17.19 ./web/node_modules β This version has a prototype pollution bug β please upgrade to 4.17.20+
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**`nmh treemap`** β one **self-contained interactive HTML** (vanilla JS, zero dependencies, no network): a squarified two-level map (node_modules β top packages) with hover tooltips for path+size; symlinked packages get dashed sliver cells so nothing silently disappears:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
$ nmh treemap ./my-repo # β ./nmh-treemap.html
|
|
271
|
+
$ nmh treemap ./my-repo --out viz.html # custom path; nmh treemap . --json = raw data
|
|
272
|
+
πΊοΈ treemap β /abs/nmh-treemap.html β buka di browser, hover cell buat detail
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**`--save-report` everywhere** β the report export is no longer scan-only: `map`, `search`, and `stats` accept `--save-report`/`--report` with the exact scan semantics (the printed report, format by extension `.json`/`.md`/`.txt`, ANSI stripped for text, write failure = warning and the exit code never changes). `treemap` refuses it β its HTML file IS the report.
|
|
276
|
+
|
|
277
|
+
### Lockfile graph & reinstall (v0.6)
|
|
278
|
+
|
|
279
|
+
The dependency graph finally stops being a heuristic. `--graph` parses the project lockfile (**zero new dependencies**): npm `package-lock.json` v2/v3, `yarn.lock` v1 (plus the sibling `package.json` for the root), `pnpm-lock.yaml` via a lockfile-subset mini-YAML parser (v9 `snapshots:` edges, v6 `packages:` edges, peer-suffixed keys reduced to the bare name), and Bun β₯1.2 `bun.lock` (jsonc-flavored text). The first usable lockfile wins (npm β yarn β pnpm β bun); anything outside the known shapes β including the `bun.lockb` binary β is honestly reported as **manifest-grade**:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
$ nmh stats ./my-repo --graph
|
|
283
|
+
βββ GRAPH βββ (lockfile-grade)
|
|
284
|
+
root direct : 9 declared β 145 tree nodes via 138 edges
|
|
285
|
+
depth 1 : βββββββββ 9
|
|
286
|
+
depth 2 : ββββββββββββββββββββ 39
|
|
287
|
+
depth 3 : ββββββββββββββββββββ 29
|
|
288
|
+
+ 54 node(s) unreachable from the root (optional/peer pruning)
|
|
289
|
+
Β· ~/my-repo/node_modules npm package-lock.json Β· 145 nodes Β· 138 edges Β· direct 9
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`nmh why` upgrades with it: when a lockfile exists, required-by is **lockfile-grade** β the full transitive requirer set (transitive packages stop being "orphans"), sibling manifests stay the fallback otherwise.
|
|
293
|
+
|
|
294
|
+
**`--reinstall`** answers the other classic question β "kalau aku copy-paste folder ini ke server baru, berapa lama pasang ulangnya?" β with a deliberately naive model (60 ms/package + flat 50 MB/s + 3 s per native-family build, `sharp`/`grpc`/`better-sqlite3` style proxies) and every assumption surfaced in the JSON:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
$ nmh stats ./my-repo --reinstall
|
|
298
|
+
βββ REINSTALL βββ (naif)
|
|
299
|
+
pasang ulang semuanya sekali : ~6.7s
|
|
300
|
+
Β· 94 pkg Γ 60 ms + 50.8 MB / 50 MB/s + 0 native Γ 3 s
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Wizard menu (v0.7 β PRD Β§7.5, the whole menu; v0.8 auto-scope)
|
|
304
|
+
|
|
305
|
+
`nmh` with no arguments opens with a compact banner box, quick-scans the cwd (depth 3 β and since v0.8, if nothing is found there, it scopes up once to the nearest manifest ancestor: a project root β€8 hops away) and offers an arrow-key menu:
|
|
306
|
+
|
|
307
|
+
| Key | |
|
|
308
|
+
| --- | --- |
|
|
309
|
+
| ποΈ Delete everything found | guards run first (blocked targets are surfaced), then trash/hard mode + confirm + guarded execution |
|
|
310
|
+
| βοΈ Pick manually | the v0.1 checkbox flow β space to select, then mode + confirm |
|
|
311
|
+
| π Search packages | type a pattern (substring, glob, `/regex/`) β same output as `nmh search` |
|
|
312
|
+
| πΊοΈ Map dependencies | same project β PM β declared-deps table as `nmh map` |
|
|
313
|
+
| π Detailed report | the full `nmh stats` read-out for this tree |
|
|
314
|
+
| βοΈ Settings & integration | the active `.nmhrc` (source path + values) β read-only |
|
|
315
|
+
| βΉ Exit | `esc` also exits |
|
|
316
|
+
|
|
317
|
+
Read-only items return to the menu; a delete run ends with the freed-space summary and the rescue-log pointer. Non-TTY (piped/CI): no menu β a compact one-line banner + a read-only scan, and `--json` suppresses even that for clean machine output. The v0.8 auto-scope climbs to the nearest project root (package.json / any PM lockfile / `pnpm-workspace.yaml`, β€8 hops of ancestors β empty manifest dirs are skipped so the monorepo root still wins) and sticks for the whole session; the TTY shows a dim note, non-TTY a warn line, `--json` stays a pure payload.
|
|
318
|
+
|
|
319
|
+
## Safety first (the whole point)
|
|
320
|
+
|
|
321
|
+
- **Guards refuse to delete** (exit code `3`): the filesystem root, `$HOME` itself, a `node_modules` sitting **directly inside** `/` or `$HOME`, anything not named exactly `node_modules`, and targets with unresolvable paths (symlink loops).
|
|
322
|
+
- **pnpm-safe accounting & deletion**: symlinks are never followed when measuring (`lstat` discipline β and `lstat` is also authoritative for file/dir/link classification: some filesystems lie about `d_type`, e.g. Android FUSE reporting hardlinked pnpm-store files as symlinks), never followed when deleting (`fs.rm(rm recursive, force)` unlinks links β the pnpm link farm and virtual store die together, nothing outside the target is touched). Hardlinks inside one entry count once per inode (`du -sb`-style dedupe), so pnpm's multi-linked virtual store is not double-counted.
|
|
323
|
+
- **`--dry-run` exists** and is the safe default when running from a non-interactive shell without `--yes` β in that case nmh refuses to execute (it is a CLI misuse, exit `1`), it never hangs waiting for input.
|
|
324
|
+
- **`--json` deletes demand explicit consent** (v0.9.2): machine output has no interactive confirm slot, so `--json --delete` without `--yes`/`--force` refuses on any shell β TTY or not (exit `1`, nothing deleted). Since v0.9.1 the non-TTY path was guarded; v0.9.2 closes the same hole for TTY runs (previously a TTY `--json --delete=hard` executed with zero confirmation).
|
|
325
|
+
- **Recovery log**: every verified delete writes `~/.nmh/logs/delete-<timestamp>-<suffix>.json` (mode, paths, sizes, failures) so discoveries can be traced/undone manually.
|
|
326
|
+
- **Trash by default**: wizard pre-highlights `trash`; `soft` is an alias of `trash`.
|
|
327
|
+
- **Windows EPERM**: `fs.rm` retries 3Γ with backoff before reporting a failure.
|
|
328
|
+
|
|
329
|
+
### Trash backends
|
|
330
|
+
|
|
331
|
+
| Platform | Backend |
|
|
332
|
+
| --- | --- |
|
|
333
|
+
| Linux | `gio trash` (gvfs) β `trash-put` (trash-cli) β honest error if neither exists (never silently falls back to hard delete) |
|
|
334
|
+
| macOS | Finder via `osascript` |
|
|
335
|
+
| Windows | Recycle Bin via PowerShell (`Microsoft.VisualBasic.FileIO`) |
|
|
336
|
+
|
|
337
|
+
## Exit codes
|
|
338
|
+
|
|
339
|
+
| Code | Meaning |
|
|
340
|
+
| --- | --- |
|
|
341
|
+
| `0` | success (incl. dry-run, cancelled) |
|
|
342
|
+
| `1` | general / usage error (e.g. non-TTY or `--json` without `--yes`) |
|
|
343
|
+
| `2` | no `node_modules` targets found |
|
|
344
|
+
| `3` | safety guard refused |
|
|
345
|
+
| `4` | permission denied somewhere (results still listed) |
|
|
346
|
+
|
|
347
|
+
## Package-manager detection (mirrors Vite+)
|
|
348
|
+
|
|
349
|
+
Nearest-evidence-first, ordered like the [`vp`](https://viteplus.dev/guide/install) wrapper does it:
|
|
350
|
+
|
|
351
|
+
1. `packageManager` field in `package.json`
|
|
352
|
+
2. `devEngines.packageManager` field
|
|
353
|
+
3. lockfiles: `pnpm-workspace.yaml`/`pnpm-lock.yaml` β pnpm, `yarn.lock`/`.yarnrc.yml` β yarn, `package-lock.json` β npm, `bun.lock`/`bun.lockb` β bun
|
|
354
|
+
4. nothing conclusive β `unknown` (shown as `β`; nmh never guesses for deletion)
|
|
355
|
+
|
|
356
|
+
A `vp`-scoped project tags the underlying manager as `pnpm [vp]` / `npm [vp]` (etc.); the node_modules layout and cleanup strategy are always the underlying PM's. Filter such projects with `--pm vp`.
|
|
357
|
+
|
|
358
|
+
## Monorepo-aware extras (v0.2)
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
$ nmh search lodash ./my-repo
|
|
362
|
+
π 'lodash' β 2 package(s) in 1 node_modules
|
|
363
|
+
|
|
364
|
+
./my-repo/packages/web/node_modules Β· pnpm Β· 1.20 GB
|
|
365
|
+
β’ lodash@4.17.21 528.0 KB
|
|
366
|
+
|
|
367
|
+
$ nmh map ./my-repo
|
|
368
|
+
βββββββββββ¬βββββββββββββ¬ββββββββββ¬ββββββββββ¬βββββββββββ
|
|
369
|
+
β Project β PM β Size β TopPkgs β Declared β
|
|
370
|
+
βββββββββββΌβββββββββββββΌββββββββββΌββββββββββΌβββββββββββ€
|
|
371
|
+
β web β pnpm [vp] β 1.20 GB β 1843 β 42 β
|
|
372
|
+
β api β npm β 640 MB β 912 β 18 β
|
|
373
|
+
βββββββββββ΄βββββββββββββ΄ββββββββββ΄ββββββββββ΄βββββββββββ
|
|
374
|
+
|
|
375
|
+
$ nmh map ./my-repo --tree # package names per project
|
|
376
|
+
$ nmh map ./my-repo --depend vite # who declares vite? (declared level)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Config file (.nmhrc)
|
|
380
|
+
|
|
381
|
+
For settings you always repeat, drop a `.nmhrc` (or `.nmhrc.json`) next to your project or in `$HOME`:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"depth": 4,
|
|
386
|
+
"exclude": ["**/e2e/**", "**/.cache/**"],
|
|
387
|
+
"keep": ["my-fragile-app"],
|
|
388
|
+
"defaultMode": "trash",
|
|
389
|
+
"color": true,
|
|
390
|
+
"animate": false,
|
|
391
|
+
"updateCheck": true,
|
|
392
|
+
"logDir": "~/.cache/nmh-logs"
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- Nearest file wins: looks from the current dir up **to** `$HOME` (inclusive), not beyond.
|
|
397
|
+
- `--config <file>` replaces the lookup entirely.
|
|
398
|
+
- CLI flags always win; `exclude`/`keep` **merge** (config first, CLI appended); `defaultMode` pre-fills a bare `--delete` and the wizard's initial choice; unknown keys are warned about and ignored.
|
|
399
|
+
- `--config` isn't a delete-mode switch β deleting still asks permission unless `--yes`/`--force`.
|
|
400
|
+
|
|
401
|
+
### `nmh config` β manage the file from the CLI (v0.9)
|
|
402
|
+
|
|
403
|
+
Hand-editing JSON is optional now:
|
|
404
|
+
|
|
405
|
+
| Command | What it does |
|
|
406
|
+
|---|---|
|
|
407
|
+
| `nmh config info` | Which file is active (or "none β defaults"), its values, defaults for everything else (`--json` payload works) |
|
|
408
|
+
| `nmh config set β¦` | Write/replace keys β list keys are **replaced whole** |
|
|
409
|
+
| `nmh config add β¦` | Append to a list key (deduped) |
|
|
410
|
+
| `nmh config remove β¦` | Drop listed items; the **bare flag clears the whole list** |
|
|
411
|
+
| `nmh config unset β¦` | Delete keys from the file (back to defaults) |
|
|
412
|
+
|
|
413
|
+
List keys use self-describing flag names: `--default-exclude` (β the `exclude` key) and `--default-keep` (β `keep`). Scalars keep their `.nmhrc` names: `--depth`, `--default-mode`, `--log-dir`, `--animate`, `--color`, `--update-check` (negations like `--no-animate` work).
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
nmh config add --default-exclude=.npm,.nvm,self # the classic homedir silencer
|
|
417
|
+
nmh config set --default-exclude=self # lists replace on set
|
|
418
|
+
nmh config set --depth=2 --default-mode=trash
|
|
419
|
+
nmh config remove --default-exclude=.nvm # one item out
|
|
420
|
+
nmh config remove --default-exclude # whole list cleared
|
|
421
|
+
nmh config unset --default-exclude --animate # keys out of the file
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- Writes go to `$HOME/.nmhrc` by default; `--config <file>` manages that exact file (created when missing).
|
|
425
|
+
- Unknown keys already in the file are **preserved on write** (the same tolerance the loader has).
|
|
426
|
+
- Validation mirrors the loader: bad values (`--depth=-1`, `--default-mode=bogus`) exit 1 and touch nothing.
|
|
427
|
+
- **Special value `self`** in either list is a scan-level guard: entries inside (or exactly at) the folder nmh runs from are dropped before filters, delete pipelines and wizard picks. Point `--dir` wherever you like β run nmh from inside a project and that project refuses to be scanned or deleted wholesale. Works identically via the config file (`"exclude": ["self"]`) and the runtime `--exclude self` / `--keep self`.
|
|
428
|
+
|
|
429
|
+
## Roadmap
|
|
430
|
+
|
|
431
|
+
v0.1 shipped the MVP (scan Β· size Β· delete Β· wizard Β· guards Β· PM detection). v0.2 added **search, map, `--depend`, `-r` recursive audit and the `--pm` / `--exclude` / `--keep` / `--depth` / `--expand` filters**. v0.3 added the **`stats` suite (dup factor, waste, top-N, per-package tree), `stats -g` global analysis across PMs, report export (`--save-report`/`--report`), `--animate` and the `.nmhrc` config file**. v0.4 added the **analytics deep-cut: baselines (`--save-baseline`/`--compare`/`--history`), dedup suggestions with per-copy declared ranges, `nmh why`, the real `--by type` breakdown, the `--phantom`/`--dead` best-effort import audit and the opt-in `--update-check`**. v0.5 added the **vision & hygiene extras: license scan (`--licenses`/`--by licenses` with the copyleft-in-prod β ), the experimental Bloat Score (`--score`) and CO2 estimate (`--co2`), the deprecated check (`--online`, offline manifests + lazy cached registry fallback), the `nmh treemap` single-file HTML export and `--save-report` for map/search/stats**. v0.6 added the **lockfile-grade graph: `--graph` parses npm/yarn/pnpm/bun lockfiles (zero new dependencies) into a real dependency graph with a BFS depth histogram, `nmh why` upgrades to lockfile-grade required-by (transitive requirers, no more false "orphans"), and `--reinstall` estimates the npm-ci-style reinstall time**. v0.7 added the **full PRD Β§7.5 wizard menu (delete all / pick / search / map / detailed report / settings, with loop-backs), a serious banner box (TTY) + compact one-liner (non-TTY, `--json`-clean), and a richer `nmh version` (node + abi + OS/arch/kernel + tty, `--json`)**. The v0.7.1 patch re-scheduled the folder walker to a slot-per-readdir work queue: the old recursive limiter could deadlock (circular wait) on wide bushy trees like a 12-package pnpm workspace β freezing the scan with **zero output and exit code 0 mid-walk**. Trees of that shape are covered by a regression test now. The same patch hardened measurement: children are classified with `fs.lstat` instead of dirent `d_type`, because some filesystems (Android FUSE/sdcardfs) misreport hardlinked pnpm-store files as symlinks β one `typescript/lib` dir had 112 of 125 dirents lying, which made a whole pnpm virtual store measure 650 KB while `du` saw 66 MB β and entry sizes now dedupe hardlinks per entry (`du -sb`-style), keeping walk numbers in line with `du` (91 β 63 MB on the same tree). v0.8 makes the no-arg wizard scope-aware: when the cwd has no node_modules within depth 3, it climbs once to the nearest manifest ancestor (package.json, any PM lockfile, or `pnpm-workspace.yaml`, β€8 hops β empty manifest dirs are skipped so the monorepo root still wins) and the whole menu runs there; the scope is frozen for the session (rescans never re-climb), a TTY note reveals the lift, non-TTY gets a warn line, and `--json` stays a pure payload. The v0.9 minor introduces **`nmh config` (info Β· set Β· add Β· remove Β· unset)** β manage the `.nmhrc` from the CLI instead of hand-editing JSON (`--default-exclude`/`--default-keep` list discipline with dedupe and clear, loader-grade validation, unknown-key preservation on write, `$HOME/.nmhrc` default target) β and the **`self` token** for `exclude`/`keep`: a scan-level guard that drops every entry inside the folder nmh runs from, wholesale, whatever target you point at (your own project can never be hunted by accident). v0.9.1 is a **live-audit hardening patch**: a scripted fixture farm Γ ~140 invoked combination runs surfaced five defects that are now fixed β delete guards additionally refuse a `node_modules` sitting *directly inside* `/` or `$HOME` (previously a `--dir ~ --delete -y` could sweep it away), the wizard's non-TTY fallback scan now honors `exclude`/`keep` from config and flags, a bare `--depth` failure message no longer swallows the value into `true`, the `--depend` guard messages are reconciled as map-only with actionable hints, and a failed `treemap --out` write exits 1 with a friendly π instead of a raw errno string. Audited-and-confirmed-as-designed along the way: positional targets are never comma-split (only `--dir` is), `--save-report` write failures warn and keep the exit code, `nmh search` matches top-level package names per PRD Β§7.3, and `nmh why` takes pkg-first arguments. v0.9.2 is an **independent-review hardening patch**: it closes the biggest hole the v0.9.1 audit missed β a TTY `--json --delete` used to execute with **zero confirmation** (machine output has no interactive prompt slot, and the non-TTY guard didn't cover the TTY branch; a pseudo-TTY reproduction proved rc=0 with the victim gone) β now `--json --delete` requires `--yes`/`--force` on every shell, TTY included. The same pass fixed the config family under `--quiet --json` (its payload was the one survivor the sweep missed β printed empty with rc 0), turned a silent bare/multi `--size` into a loud usage error like its sibling guards, and cleaned the last 10 pre-existing `tsc` errors (a leftover `depth` sentinel union) so `npm run typecheck` is finally clean. v0.9.3 is a **no-behavior-change cleanliness patch** from the same review: six dead symbols removed (`measurePackageRefs`, `SizedPackageRef`, `readLicense`, `listHasSelf`, `isVerbose`, `formatCount`), the write-only `MeasureProgress.currentEntry` field dropped, the twin `DepsAudit` interfaces collapsed to one canonical definition (re-exported from `core/result.ts`), the `self` guard moved to its semantic home (`core/guards.ts`), `ConfigError` now carries a machine-readable `code` instead of message string-matching, scan's private report writer now shares `writeReportSnapshot` with map/search/stats, the `analyze`/`info` stats aliases are documented, and `npm run typecheck` stays at zero errors. v0.9.4 is a **hardening patch for the review findings that change data/behavior**: `stripJsonC` is now fully quote-aware (a trailing comma inside a string value is no longer eaten β silent corruption fixed), `makeGlobMatcher` caps pathological patterns (512 chars / 8 `**`) up front, a delete op aimed at `/` refuses before scanning, the deprecated `truncated` flag means "a 201st item was refused" instead of "exactly at the cap", recovery logs get a random suffix (no same-millisecond overwrites), deprecated notes are ANSI-stripped at the source, `nmh why` reads its manifests/locks with bounded concurrency of 4, the `$HOME` guard ignores letter-case on macOS, the `.nmhrc` ancestor trust is documented in the README, and the self-guard-vs-symlinked-cwd claim is verified as a non-issue (Node resolves `process.cwd()` physically). v1.0.0 is the **stable-API release**: every `--json` payload is a frozen contract guarded by the 245-test suite, the exit-code table (0/1/2/3/4) is locked, `npm run typecheck` and `npm audit` are both clean, and the package ships to npm with a `v1.0.0` git tag once the remote is configured.
|
|
432
|
+
|
|
433
|
+
## Development
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
npm ci
|
|
437
|
+
npm test # build + vitest (unit + e2e through the real CLI binary)
|
|
438
|
+
npm run typecheck
|
|
439
|
+
npm run build
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
CI runs the full suite on Linux/macOS/Windows Γ Node 20/22/24 (the runtime `engines` stays `>=18`).
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
[MIT](./LICENSE) β node-modules-hunter contributors.
|