node-modules-hunter 1.1.1 → 1.2.1
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 +9 -3
- package/dist/cli.js +240 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Node.js ≥ 18 required.
|
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
nmh # interactive wizard: arrow menu (delete all / pick / search / map / report / settings)
|
|
31
|
+
nmh wizard --dir=./a,./b # v1.2: the wizard on explicit targets (skips the auto-climb)
|
|
32
|
+
nmh wizard -- stats --dir=./a # v1.2: open a read-only view first, then the wizard menu
|
|
31
33
|
nmh <path> [path…] # shortcut: scan given path(s)
|
|
32
34
|
nmh scan [dir] # full scan (default: cwd)
|
|
33
35
|
nmh clean [dir] # scan + delete everything safe (trash by default)
|
|
@@ -44,7 +46,9 @@ nmh stats -g # global installs across npm/pnpm/yarn/bun
|
|
|
44
46
|
# aliases: `analyze` and `info` behave exactly like `stats` (recognition from PRD-era names)
|
|
45
47
|
nmh why <pkg> [dir…] # reverse depend: where is <pkg> and what requires it — read-only
|
|
46
48
|
nmh treemap [dir] # single-file interactive HTML treemap → nmh-treemap.html
|
|
47
|
-
nmh update
|
|
49
|
+
nmh update --check # v1.2: compare installed vs registry — nothing installs
|
|
50
|
+
nmh update --info # v1.2: full registry detail (npm view style)
|
|
51
|
+
nmh update -y # install the newer version without asking (-n declines, --force skips the ask)
|
|
48
52
|
|
|
49
53
|
nmh --dir=A,B,glob/*/node_modules # targets: comma list, globs, ~ — combine with flags below
|
|
50
54
|
nmh version | --version | -v # banner: tool version, node+abi, OS/arch/kernel distro, tty — --json for plain fields
|
|
@@ -322,6 +326,8 @@ pasang ulang semuanya sekali : ~6.7s
|
|
|
322
326
|
|
|
323
327
|
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.
|
|
324
328
|
|
|
329
|
+
**Explicit targeting (v1.2)**: `nmh wizard` is a registered command now (same menu as the bare invocation). Give it `--dir=./repo1,./repo2` and the menu runs on those targets as-is — the auto-climb is skipped for explicit scopes. `nmh --wizard` is the flag form. The **`--` router** opens a read-only view before the menu: `nmh wizard -- stats --dir=./repo1` runs the full stats report first (the view segment is parsed by the same dispatcher — all its flags/guards apply, and flags before `--` belong to the wizard), then drops you into the menu. Views: `stats` (aliases `analyze`, `info`), `map`, `search`, `why`, `treemap`. All explicit wizard forms need a TTY — a piped run refuses with exit `1` and directions (the bare `nmh` fallback scan is untouched).
|
|
330
|
+
|
|
325
331
|
## Safety first (the whole point)
|
|
326
332
|
|
|
327
333
|
- **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).
|
|
@@ -351,7 +357,7 @@ The classic question — *which of these `node_modules` can I actually kill?*
|
|
|
351
357
|
|
|
352
358
|
- **`--fast-size`** swaps the byte-exact walker for one `du -sk` per entry (KiB granularity — sizes are always multiples of 1024); anything `du` can't measure falls back to the exact walker silently. Scan-only knob: delete runs always measure exactly so the freed-bytes number stays honest. Windows has no `du`, so it always walks.
|
|
353
359
|
- **Default skip-list flip**: discovery now hides name-based system noise (`.Trash`, `.Trashes`, `.Trash-<uid>`, `Library`, `*.app`) wherever it sits in the tree; pass **`--no-skip`** to restore the bare full-tree v1.0 audit. `.git` stays skipped always (even with `--no-skip`), and direct targets are never filtered — the skip only shapes what discovery finds.
|
|
354
|
-
- **`nmh update
|
|
360
|
+
- **`nmh update` is consented (v1.2)**: the plain run shows the facts and asks before installing (`@clack` confirm on a TTY; a piped/`--json` run needs `-y` to install or `-n` to decline — without an answer it refuses with exit `1`, the same protocol as deletes). `--check` is the compact compare for scripts/CI (exit 0 whether or not an update exists, never installs), `--info` is the full `npm view`-style read-out (description, tarball + size + file count, engines, license, publish time), and the two are mutually exclusive. `--force` skips the ask when an update exists — an already-latest install is checked and reported, never blindly reinstalled. Non-npm-global installs keep the honest how-to note. `--json` payload: `{ current, latest, updated, method }` frozen + additive `{ updateAvailable, detail? }`.
|
|
355
361
|
- **`--json` stays additive**: new optional keys `idleDays`, `activitySource`, `backend`, `trashBatch`, `trashFallback` appear only when a v1.1 feature is active; every v1.0 payload key is unchanged (the frozen v1.0 contract, extended minor-version-safely).
|
|
356
362
|
|
|
357
363
|
## Exit codes
|
|
@@ -448,7 +454,7 @@ nmh config unset --default-exclude --animate # keys out of the file
|
|
|
448
454
|
|
|
449
455
|
## Roadmap
|
|
450
456
|
|
|
451
|
-
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. v1.1.0 is the **stale-hunting edition**: `--stale <days>` grades entries by newest-of git commit → manifest mtime → folder mtime and drops no-evidence stragglers, `--fast-size` measures with `du -sk` (exact-walker fallback), results gain an `Idle` column, a proportional Share bar and `--sort size|path|idle|mtime`, discovery hides system noise by default (`--no-skip` restores the bare audit), `nmh update` self-updates npm-global installs, and `--trash=system|nmh` picks the backend with a restorable `~/.nmh/trash` fallback — all with zero new dependencies and additive-only JSON keys, guarded by the 267-test suite. The v1.1.1 patch is a **polish-and-hardening pass driven by three user reports and a full live-exercise matrix** (26 sandbox fixtures × 223 real combinations): the measuring loader is a **time-driven spinner** (braille frames rotate on their own timer at 70 ms instead of per-update repaints, spinning on TTY, silent in pipes unless `--animate on`), `nmh help` renders a **complete manual** (own renderer — 14 commands + 5 stats subcommand views + grouped options + examples + the safety/exit footer, and the previously-missing `-h`/`--help`/`-v`/`--version` aliases finally work instead of slipping into the default scan), and ANSI colors are **TTY-only by design** (picocolors' `!!env.CI` guess had painted unknown escape bytes into piped GitHub Actions output and shredded substring assertions — CI is 9/9 green for the first time; local color comes from a real TTY or explicit `--color`/`FORCE_COLOR`). The same matrix surfaced two defects the suite had missed: an unknown first word (`nmh scanx`) used to fall through to a silent full-cwd scan with exit 0 — any non-path, non-command token is now a loud usage error (and a bare cwd-relative dir like `nmh node_modules` now correctly targets that folder); and `nmh config set` wrote `$HOME/.nmhrc` while scans walked cwd → `/` and never read it when the cwd lived outside the home tree — the home file is now the documented fallback (chain first). Meanwhile `os.homedir()` on Windows reads `USERPROFILE` (test helper pins both), `execFile('npm')` needs `shell: true` on win32 (`stats -g` exited 2 there), and system-trash-dependent flows run hermetically via `nmh trash` fallback. Suite: 44 files / 279 tests green.
|
|
457
|
+
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. v1.1.0 is the **stale-hunting edition**: `--stale <days>` grades entries by newest-of git commit → manifest mtime → folder mtime and drops no-evidence stragglers, `--fast-size` measures with `du -sk` (exact-walker fallback), results gain an `Idle` column, a proportional Share bar and `--sort size|path|idle|mtime`, discovery hides system noise by default (`--no-skip` restores the bare audit), `nmh update` self-updates npm-global installs, and `--trash=system|nmh` picks the backend with a restorable `~/.nmh/trash` fallback — all with zero new dependencies and additive-only JSON keys, guarded by the 267-test suite. The v1.1.1 patch is a **polish-and-hardening pass driven by three user reports and a full live-exercise matrix** (26 sandbox fixtures × 223 real combinations): the measuring loader is a **time-driven spinner** (braille frames rotate on their own timer at 70 ms instead of per-update repaints, spinning on TTY, silent in pipes unless `--animate on`), `nmh help` renders a **complete manual** (own renderer — 14 commands + 5 stats subcommand views + grouped options + examples + the safety/exit footer, and the previously-missing `-h`/`--help`/`-v`/`--version` aliases finally work instead of slipping into the default scan), and ANSI colors are **TTY-only by design** (picocolors' `!!env.CI` guess had painted unknown escape bytes into piped GitHub Actions output and shredded substring assertions — CI is 9/9 green for the first time; local color comes from a real TTY or explicit `--color`/`FORCE_COLOR`). The same matrix surfaced two defects the suite had missed: an unknown first word (`nmh scanx`) used to fall through to a silent full-cwd scan with exit 0 — any non-path, non-command token is now a loud usage error (and a bare cwd-relative dir like `nmh node_modules` now correctly targets that folder); and `nmh config set` wrote `$HOME/.nmhrc` while scans walked cwd → `/` and never read it when the cwd lived outside the home tree — the home file is now the documented fallback (chain first). Meanwhile `os.homedir()` on Windows reads `USERPROFILE` (test helper pins both), `execFile('npm')` needs `shell: true` on win32 (`stats -g` exited 2 there), and system-trash-dependent flows run hermetically via `nmh trash` fallback. Suite: 44 files / 279 tests green. v1.2.0 is the **consent-and-targeting edition**: `nmh update` stops installing on faith — the plain run shows the facts and asks (`-y`/`-n` on pipes, `--force` to skip the ask, and an already-latest install is never reinstalled), while `--check` and `--info` are read-only registry views (compact compare vs full `npm view` detail, mutually exclusive); and the wizard becomes a first-class command — `nmh wizard --dir=./a,./b` scopes the menu to explicit targets (no auto-climb), `nmh --wizard` is the flag form, and the `--` router (`nmh wizard -- stats --dir=./a`) opens a read-only view through the full dispatcher before the menu; pipes refuse every explicit wizard form with exit 1 and directions while the bare `nmh` fallback scan stays untouched. The update messages were also unified to Indonesian (the smoke matrix caught `--check` speaking English next to an Indonesian default). Suite: 46 files / 295 tests green. The v1.2.1 hotfix repairs what the wild caught the same day: modern npm prints `npm view <pkg> --json` as an **abbreviated packument array** (no top-level `version` key), which made every `nmh update` flavor exit "cannot read the latest version" — version detection is back on the plain-text `npm view <pkg> version` spawn, the `--info` detail is a separate best-effort fetch with shape normalization (array / object / garbage all tolerated — facts minus detail, never a failed run), and the new `NMH_UPDATE_VIEW_JSON` seam keeps both shapes hermetically testable. Suite: 46 files / 296 tests green.
|
|
452
458
|
|
|
453
459
|
## Development
|
|
454
460
|
|
package/dist/cli.js
CHANGED
|
@@ -248,6 +248,7 @@ function renderVersionDetail(detail) {
|
|
|
248
248
|
var COMMANDS = [
|
|
249
249
|
{ usage: "nmh", desc: "interactive wizard: arrow menu (delete all / pick / search / map / report / settings)" },
|
|
250
250
|
{ usage: "nmh <path> [path\u2026]", desc: "shortcut: scan given path(s); one-liner actions via flags (--delete, --search, \u2026)" },
|
|
251
|
+
{ usage: "nmh wizard [--dir \u2026]", desc: "v1.2: the wizard on explicit targets (--dir=./a,./b skips the auto-climb); -- <view> opens a view first" },
|
|
251
252
|
{ usage: "nmh scan [dir]", desc: "full scan across targets (default: cwd)" },
|
|
252
253
|
{ usage: "nmh clean [dir]", desc: "scan + delete everything safe (trash by default)" },
|
|
253
254
|
{ usage: "nmh search <pattern> [dir\u2026]", desc: "find installed packages by name (+ versions & sizes) \u2014 read-only" },
|
|
@@ -257,7 +258,7 @@ var COMMANDS = [
|
|
|
257
258
|
{ usage: "nmh treemap [dir]", desc: "single-file interactive HTML treemap \u2192 nmh-treemap.html" },
|
|
258
259
|
{ usage: "nmh stats -g", desc: "global installs across npm/pnpm/yarn/bun \u2014 read-only" },
|
|
259
260
|
{ usage: "nmh config <op>", desc: "manage .nmhrc from the CLI: info \xB7 set \xB7 add \xB7 remove \xB7 unset" },
|
|
260
|
-
{ usage: "nmh update", desc: "
|
|
261
|
+
{ usage: "nmh update", desc: "v1.2: shows the facts and asks before installing \u2014 --check / --info compare only; -y installs, -n declines, --force skips the ask" },
|
|
261
262
|
{ usage: "nmh version", desc: "banner: tool version, node+abi, OS/arch/kernel, tty (alias: -v / --version)" },
|
|
262
263
|
{ usage: "nmh help", desc: "this help (alias: -h / --help)" }
|
|
263
264
|
];
|
|
@@ -287,6 +288,8 @@ var GROUPS = [
|
|
|
287
288
|
{ usage: "-d, --dir <paths>", desc: "target dir(s), comma separated or repeated; globs and ~ allowed" },
|
|
288
289
|
{ usage: "--size <mode>", desc: "output shape: 'total' (just aggregate) or 'each' (default: per-folder table)" },
|
|
289
290
|
{ usage: "--save-report <file>", desc: "also write the printed report to a file (.json/.md/.txt; --report <file> aliases it)" },
|
|
291
|
+
{ usage: "--wizard", desc: "v1.2: open the wizard explicitly \u2014 same as bare `nmh`, but a pipe refuses with directions instead of a scan" },
|
|
292
|
+
{ usage: "update: --check \xB7 --info \xB7 -y \xB7 -n \xB7 -f", desc: "v1.2: read-only views vs consented install \u2014 see `nmh update` row above" },
|
|
290
293
|
{ usage: "--update-check", desc: "peek npm for a newer release (non-blocking, 24 h cache; also settable in .nmhrc)" }
|
|
291
294
|
]
|
|
292
295
|
},
|
|
@@ -349,11 +352,14 @@ var GROUPS = [
|
|
|
349
352
|
];
|
|
350
353
|
var EXAMPLES = [
|
|
351
354
|
["nmh", "wizard; the no-args menu auto-climbs to the nearest project root"],
|
|
355
|
+
["nmh wizard --dir=./repo1,./repo2", "wizard scoped to explicit targets \u2014 no auto-climb"],
|
|
356
|
+
["nmh wizard -- stats --dir=./repo1", "v1.2 router: stats view first, then the wizard menu"],
|
|
352
357
|
["nmh --dir=./proj,~/code/*/node_modules", "multi-target scan"],
|
|
353
358
|
["nmh . --stale 90 --delete=trash -y", "bury the stale boneyard, restorable trash"],
|
|
354
359
|
["nmh why react --expand", "reverse-depend: who declares + who requires"],
|
|
355
360
|
["nmh stats --by type --json", "dep-field breakdown as a machine payload"],
|
|
356
361
|
["nmh stats -g --top 20", "the 20 biggest global installs across PMs"],
|
|
362
|
+
["nmh update --check", "compare installed vs registry \u2014 nothing installs"],
|
|
357
363
|
["nmh config set default-mode trash", "persist a default delete mode in .nmhrc"]
|
|
358
364
|
];
|
|
359
365
|
var flagWidth = (groups) => {
|
|
@@ -2548,6 +2554,7 @@ function emptyResult(targets, unresolvable = []) {
|
|
|
2548
2554
|
import { promisify as promisify3 } from "util";
|
|
2549
2555
|
import { execFile as execFile3 } from "child_process";
|
|
2550
2556
|
import fs13 from "fs/promises";
|
|
2557
|
+
import { confirm } from "@clack/prompts";
|
|
2551
2558
|
|
|
2552
2559
|
// src/utils/update-check.ts
|
|
2553
2560
|
import fs12 from "fs/promises";
|
|
@@ -2652,17 +2659,44 @@ async function runNpm(args, timeoutMs) {
|
|
|
2652
2659
|
}
|
|
2653
2660
|
}
|
|
2654
2661
|
var VERSION_SHAPE = /^\d+\.\d+\.\d+/;
|
|
2655
|
-
|
|
2662
|
+
function manifestDetailFrom(parsed) {
|
|
2663
|
+
const detail = {};
|
|
2664
|
+
if (typeof parsed.description === "string") detail.description = parsed.description;
|
|
2665
|
+
if (typeof parsed.license === "string") detail.license = parsed.license;
|
|
2666
|
+
const dist = parsed.dist;
|
|
2667
|
+
if (dist !== null && typeof dist === "object") {
|
|
2668
|
+
const d = dist;
|
|
2669
|
+
if (typeof d.tarball === "string") detail.tarball = d.tarball;
|
|
2670
|
+
if (typeof d.unpackedSize === "number") detail.unpackedSize = formatBytes(d.unpackedSize);
|
|
2671
|
+
if (typeof d.fileCount === "number") detail.fileCount = d.fileCount;
|
|
2672
|
+
}
|
|
2673
|
+
const engines = parsed.engines;
|
|
2674
|
+
if (engines !== null && typeof engines === "object") {
|
|
2675
|
+
const e = engines;
|
|
2676
|
+
if (typeof e.node === "string") detail.engines = e.node;
|
|
2677
|
+
}
|
|
2678
|
+
return detail;
|
|
2679
|
+
}
|
|
2680
|
+
async function latestManifest() {
|
|
2656
2681
|
const override = process.env.NMH_UPDATE_LATEST_JSON;
|
|
2657
2682
|
if (typeof override === "string" && override.length > 0) {
|
|
2658
2683
|
try {
|
|
2659
2684
|
const parsed = JSON.parse(await fs13.readFile(override, "utf8"));
|
|
2660
|
-
|
|
2685
|
+
const version2 = parsed.version;
|
|
2686
|
+
if (typeof version2 !== "string" || !VERSION_SHAPE.test(version2)) return null;
|
|
2687
|
+
const base = manifestDetailFrom(parsed);
|
|
2688
|
+
const detail = Object.keys(base).length > 0 ? base : await fetchDetail(version2, false);
|
|
2689
|
+
return { version: version2, detail };
|
|
2661
2690
|
} catch (err) {
|
|
2662
2691
|
verboseLog(`update: override file unreadable (${err instanceof Error ? err.message : String(err)})`);
|
|
2663
2692
|
return null;
|
|
2664
2693
|
}
|
|
2665
2694
|
}
|
|
2695
|
+
const version = await latestVersionText();
|
|
2696
|
+
if (version === null) return null;
|
|
2697
|
+
return { version, detail: await fetchDetail(version, true) };
|
|
2698
|
+
}
|
|
2699
|
+
async function latestVersionText() {
|
|
2666
2700
|
const view = await runNpm(["view", "node-modules-hunter", "version"], 6e4);
|
|
2667
2701
|
if (view.code !== 0) {
|
|
2668
2702
|
verboseLog(`npm view failed (rc ${view.code}): ${view.stderr.trim().slice(0, 200)}`);
|
|
@@ -2671,6 +2705,48 @@ async function latestVersion() {
|
|
|
2671
2705
|
const version = view.stdout.trim();
|
|
2672
2706
|
return VERSION_SHAPE.test(version) ? version : null;
|
|
2673
2707
|
}
|
|
2708
|
+
function manifestFromView(raw2) {
|
|
2709
|
+
if (Array.isArray(raw2)) {
|
|
2710
|
+
const objects = raw2.filter((item) => item !== null && typeof item === "object");
|
|
2711
|
+
return objects.length > 0 ? objects[objects.length - 1] : null;
|
|
2712
|
+
}
|
|
2713
|
+
return raw2 !== null && typeof raw2 === "object" ? raw2 : null;
|
|
2714
|
+
}
|
|
2715
|
+
async function fetchDetail(version, allowSpawn) {
|
|
2716
|
+
const seam = process.env.NMH_UPDATE_VIEW_JSON;
|
|
2717
|
+
let raw2 = null;
|
|
2718
|
+
if (typeof seam === "string" && seam.length > 0) {
|
|
2719
|
+
try {
|
|
2720
|
+
raw2 = JSON.parse(await fs13.readFile(seam, "utf8"));
|
|
2721
|
+
} catch {
|
|
2722
|
+
return {};
|
|
2723
|
+
}
|
|
2724
|
+
} else {
|
|
2725
|
+
if (!allowSpawn) return {};
|
|
2726
|
+
const view = await runNpm(["view", `node-modules-hunter@${version}`, "--json"], 6e4);
|
|
2727
|
+
if (view.code !== 0) return {};
|
|
2728
|
+
try {
|
|
2729
|
+
raw2 = JSON.parse(view.stdout);
|
|
2730
|
+
} catch {
|
|
2731
|
+
return {};
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const manifest = manifestFromView(raw2);
|
|
2735
|
+
if (manifest === null) return {};
|
|
2736
|
+
const detail = manifestDetailFrom(manifest);
|
|
2737
|
+
if (typeof seam !== "string" || seam.length === 0) detail.publishedAt = await publishedTime(version);
|
|
2738
|
+
return detail;
|
|
2739
|
+
}
|
|
2740
|
+
async function publishedTime(version) {
|
|
2741
|
+
const times = await runNpm(["view", "node-modules-hunter", "time", "--json"], 3e4);
|
|
2742
|
+
if (times.code !== 0) return void 0;
|
|
2743
|
+
try {
|
|
2744
|
+
const parsed = JSON.parse(times.stdout);
|
|
2745
|
+
return typeof parsed[version] === "string" ? parsed[version] : void 0;
|
|
2746
|
+
} catch {
|
|
2747
|
+
return void 0;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2674
2750
|
async function isNpmGlobal() {
|
|
2675
2751
|
const forced = process.env.NMH_UPDATE_GLOBAL;
|
|
2676
2752
|
if (forced === "1") return true;
|
|
@@ -2698,41 +2774,86 @@ async function installLatest(version) {
|
|
|
2698
2774
|
}
|
|
2699
2775
|
return { ok: true, detail: installCommand };
|
|
2700
2776
|
}
|
|
2777
|
+
function renderInfo(current, manifest, updateAvailable, global) {
|
|
2778
|
+
const d = manifest.detail;
|
|
2779
|
+
const rows = [
|
|
2780
|
+
`installed ${current}`,
|
|
2781
|
+
`latest ${manifest.version}`,
|
|
2782
|
+
`update ${updateAvailable ? `tersedia \u2014 jalankan \`nmh update\`${global ? "" : " (atau: npm i -g node-modules-hunter)"}` : "tidak ada \u2014 ini versi terbaru"}`
|
|
2783
|
+
];
|
|
2784
|
+
if (d.description !== void 0) rows.push(`summary ${d.description}`);
|
|
2785
|
+
if (d.tarball !== void 0) {
|
|
2786
|
+
const size = d.unpackedSize === void 0 ? "" : ` (${d.unpackedSize}${d.fileCount === void 0 ? "" : `, ${d.fileCount} files`})`;
|
|
2787
|
+
rows.push(`tarball ${d.tarball}${size}`);
|
|
2788
|
+
}
|
|
2789
|
+
if (d.engines !== void 0) rows.push(`engines node ${d.engines}`);
|
|
2790
|
+
if (d.license !== void 0) rows.push(`license ${d.license}`);
|
|
2791
|
+
if (d.publishedAt !== void 0) rows.push(`published ${d.publishedAt}`);
|
|
2792
|
+
return rows;
|
|
2793
|
+
}
|
|
2701
2794
|
async function runUpdate(request) {
|
|
2702
2795
|
const current = toolVersion();
|
|
2703
|
-
const
|
|
2704
|
-
if (
|
|
2796
|
+
const manifest = await latestManifest();
|
|
2797
|
+
if (manifest === null) {
|
|
2705
2798
|
const message = "cannot read the latest version \u2014 is the registry reachable? (npm view node-modules-hunter version)";
|
|
2706
2799
|
if (request.json) errorLog(message);
|
|
2707
2800
|
else errorLog(`\u26A0 ${message}`);
|
|
2708
2801
|
return 1;
|
|
2709
2802
|
}
|
|
2803
|
+
const latest = manifest.version;
|
|
2710
2804
|
const global = await isNpmGlobal();
|
|
2711
2805
|
const method = global ? "npm-global" : "manual";
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2806
|
+
const updateAvailable = isNewer(current, latest);
|
|
2807
|
+
const result = (updated) => ({ current, latest, updated, method, updateAvailable });
|
|
2808
|
+
if (request.mode === "info") {
|
|
2809
|
+
if (request.json) payload(JSON.stringify({ ...result(false), detail: manifest.detail }, null, 2));
|
|
2810
|
+
else log(["nmh update \u2014 info registry (npm view node-modules-hunter)", ...renderInfo(current, manifest, updateAvailable, global)].join("\n"));
|
|
2811
|
+
return 0;
|
|
2812
|
+
}
|
|
2813
|
+
if (request.mode === "check") {
|
|
2814
|
+
if (request.json) payload(JSON.stringify(result(false), null, 2));
|
|
2815
|
+
else if (updateAvailable) log(`\u2191 update tersedia: ${current} \u2192 ${latest} \u2014 jalankan \`nmh update\` untuk menginstall`);
|
|
2816
|
+
else log(`\u2713 nmh ${current} sudah versi terbaru (registry: ${latest})`);
|
|
2817
|
+
return 0;
|
|
2818
|
+
}
|
|
2819
|
+
if (!updateAvailable) {
|
|
2820
|
+
if (request.json) payload(JSON.stringify(result(false), null, 2));
|
|
2821
|
+
else log(`\u2713 nmh ${current} sudah versi terbaru (registry: ${latest}) \u2014 tak ada yang diinstall`);
|
|
2716
2822
|
return 0;
|
|
2717
2823
|
}
|
|
2718
2824
|
if (!global) {
|
|
2719
|
-
const result2 = { current, latest, updated: false, method };
|
|
2720
2825
|
const advice = [
|
|
2721
2826
|
`nmh ${current} terpasang di luar npm global \u2014 update manual:`,
|
|
2722
2827
|
" npm i -g node-modules-hunter (atau: pnpm add -g / bunx / brew upgrade node-modules-hunter)"
|
|
2723
2828
|
];
|
|
2724
|
-
if (request.json) payload(JSON.stringify(
|
|
2829
|
+
if (request.json) payload(JSON.stringify(result(false), null, 2));
|
|
2725
2830
|
else for (const line of advice) note(line);
|
|
2726
2831
|
return 0;
|
|
2727
2832
|
}
|
|
2728
|
-
|
|
2729
|
-
if (!install.
|
|
2730
|
-
|
|
2833
|
+
let install = request.force || request.answer === "yes";
|
|
2834
|
+
if (!install && request.answer === "none") {
|
|
2835
|
+
if (request.json || !isTTYOut()) {
|
|
2836
|
+
errorLog("update butuh jawaban eksplisit \u2014 -y untuk menginstall, -n untuk menolak (--check/--info untuk lihat saja)");
|
|
2837
|
+
return 1;
|
|
2838
|
+
}
|
|
2839
|
+
const admitted = await confirm({
|
|
2840
|
+
message: `Update nmh ${current} \u2192 ${latest} via npm install -g?`,
|
|
2841
|
+
initialValue: true
|
|
2842
|
+
});
|
|
2843
|
+
install = admitted === true;
|
|
2844
|
+
}
|
|
2845
|
+
if (!install) {
|
|
2846
|
+
if (request.json) payload(JSON.stringify(result(false), null, 2));
|
|
2847
|
+
else log(`\u2715 tetap nmh ${current} \u2014 update ke ${latest} tidak diinstall`);
|
|
2848
|
+
return 0;
|
|
2849
|
+
}
|
|
2850
|
+
const installation = await installLatest(latest);
|
|
2851
|
+
if (!installation.ok) {
|
|
2852
|
+
errorLog(`\u26A0 update gagal: ${installation.detail}`);
|
|
2731
2853
|
errorLog(` coba manual: npm install -g node-modules-hunter@${latest}`);
|
|
2732
2854
|
return 1;
|
|
2733
2855
|
}
|
|
2734
|
-
|
|
2735
|
-
if (request.json) payload(JSON.stringify(result, null, 2));
|
|
2856
|
+
if (request.json) payload(JSON.stringify(result(true), null, 2));
|
|
2736
2857
|
else log(`\u2713 terupdate ${current} \u2192 ${latest} (npm install -g node-modules-hunter)`);
|
|
2737
2858
|
return 0;
|
|
2738
2859
|
}
|
|
@@ -2948,7 +3069,7 @@ var INFO_DEFAULTS = {
|
|
|
2948
3069
|
updateCheck: "off",
|
|
2949
3070
|
logDir: "~/.nmh"
|
|
2950
3071
|
};
|
|
2951
|
-
function
|
|
3072
|
+
function renderInfo2(source, values) {
|
|
2952
3073
|
const row = (key, value) => ` ${colors.dim(key.padEnd(12, " "))}${value}`;
|
|
2953
3074
|
const lines = [
|
|
2954
3075
|
"",
|
|
@@ -2985,7 +3106,7 @@ async function runConfigCommand(request) {
|
|
|
2985
3106
|
);
|
|
2986
3107
|
return 0;
|
|
2987
3108
|
}
|
|
2988
|
-
log(
|
|
3109
|
+
log(renderInfo2(loaded.source, loaded.values));
|
|
2989
3110
|
return 0;
|
|
2990
3111
|
}
|
|
2991
3112
|
const file = configWriteTarget(request.explicitFile, request.cwd);
|
|
@@ -4606,7 +4727,7 @@ async function saveBaselineQuiet(name, snapshot) {
|
|
|
4606
4727
|
}
|
|
4607
4728
|
|
|
4608
4729
|
// src/commands/delete.ts
|
|
4609
|
-
import { confirm, select, isCancel } from "@clack/prompts";
|
|
4730
|
+
import { confirm as confirm2, select, isCancel as isCancel2 } from "@clack/prompts";
|
|
4610
4731
|
|
|
4611
4732
|
// src/core/deleter.ts
|
|
4612
4733
|
import fs20 from "fs/promises";
|
|
@@ -4938,13 +5059,13 @@ async function resolveMode(requested, json, dryRun) {
|
|
|
4938
5059
|
],
|
|
4939
5060
|
initialValue: "trash"
|
|
4940
5061
|
});
|
|
4941
|
-
if (
|
|
5062
|
+
if (isCancel2(choice)) return "cancelled";
|
|
4942
5063
|
return choice;
|
|
4943
5064
|
}
|
|
4944
5065
|
return "trash";
|
|
4945
5066
|
}
|
|
4946
5067
|
async function confirmDeletion(count, size, mode) {
|
|
4947
|
-
const answer = await
|
|
5068
|
+
const answer = await confirm2({
|
|
4948
5069
|
message: mode === "hard" ? `Delete ${count} folder(s) \u2014 ${formatBytes(size)} \u2014 PERMANENTLY (no undo)?` : `Move ${count} folder(s) \u2014 ${formatBytes(size)} \u2014 to the OS trash?`,
|
|
4949
5070
|
initialValue: mode !== "hard"
|
|
4950
5071
|
});
|
|
@@ -4995,7 +5116,7 @@ async function runClean(request) {
|
|
|
4995
5116
|
// src/commands/wizard.ts
|
|
4996
5117
|
import fs21 from "fs/promises";
|
|
4997
5118
|
import path23 from "path";
|
|
4998
|
-
import { confirm as
|
|
5119
|
+
import { confirm as confirm3, isCancel as isCancel3, multiselect, outro, select as select2, text } from "@clack/prompts";
|
|
4999
5120
|
|
|
5000
5121
|
// src/utils/banner.ts
|
|
5001
5122
|
function widthOf(plain) {
|
|
@@ -5031,6 +5152,9 @@ async function runWizard(cwd, options = {}) {
|
|
|
5031
5152
|
if (!isTTYOut()) return runFallbackScan(cwd, options);
|
|
5032
5153
|
return runInteractiveWizard(cwd, options);
|
|
5033
5154
|
}
|
|
5155
|
+
function wizardTargets(options) {
|
|
5156
|
+
return options.targets !== void 0 && options.targets.length > 0 ? options.targets : ["."];
|
|
5157
|
+
}
|
|
5034
5158
|
var WIZARD_DEPTH = 3;
|
|
5035
5159
|
var WIZARD_MAX_CLIMB = 8;
|
|
5036
5160
|
var PROJECT_MANIFESTS = /* @__PURE__ */ new Set([
|
|
@@ -5069,12 +5193,13 @@ async function resolveWizardScope(cwd) {
|
|
|
5069
5193
|
}
|
|
5070
5194
|
async function runFallbackScan(cwd, options) {
|
|
5071
5195
|
if (!options.json) log(renderCompactBanner());
|
|
5072
|
-
const
|
|
5196
|
+
const explicit = options.targets !== void 0 && options.targets.length > 0;
|
|
5197
|
+
const scope = explicit ? { root: cwd, from: cwd, climbed: false, scouted: false } : await resolveWizardScope(cwd);
|
|
5073
5198
|
if (!options.json && scope.climbed) {
|
|
5074
5199
|
warn(`no node_modules within 3 levels of cwd \u2014 scoped to the project root above: ${scope.root}`);
|
|
5075
5200
|
}
|
|
5076
5201
|
const outcome = await collectScan({
|
|
5077
|
-
targets:
|
|
5202
|
+
targets: wizardTargets(options),
|
|
5078
5203
|
cwd: scope.root,
|
|
5079
5204
|
maxDepth: WIZARD_DEPTH,
|
|
5080
5205
|
excludes: options.excludes,
|
|
@@ -5093,14 +5218,15 @@ async function runFallbackScan(cwd, options) {
|
|
|
5093
5218
|
return outcome.code;
|
|
5094
5219
|
}
|
|
5095
5220
|
async function runInteractiveWizard(cwd, options) {
|
|
5096
|
-
|
|
5097
|
-
|
|
5221
|
+
const explicit = options.targets !== void 0 && options.targets.length > 0;
|
|
5222
|
+
if (options.banner !== false) log(renderFullBanner());
|
|
5223
|
+
const scope = explicit ? { root: cwd, from: cwd, climbed: false, scouted: false } : await resolveWizardScope(cwd);
|
|
5098
5224
|
if (scope.climbed) {
|
|
5099
5225
|
log(colors.dim(`cwd has no node_modules in 3 levels \u2014 scoped up to the project root ${scope.root}`));
|
|
5100
5226
|
}
|
|
5101
5227
|
for (; ; ) {
|
|
5102
5228
|
const scan = await collectScan({
|
|
5103
|
-
targets:
|
|
5229
|
+
targets: wizardTargets(options),
|
|
5104
5230
|
cwd: scope.root,
|
|
5105
5231
|
maxDepth: WIZARD_DEPTH,
|
|
5106
5232
|
excludes: options.excludes,
|
|
@@ -5124,7 +5250,7 @@ async function runInteractiveWizard(cwd, options) {
|
|
|
5124
5250
|
{ value: "quit", label: "\u23F9 Exit nmh" }
|
|
5125
5251
|
]
|
|
5126
5252
|
});
|
|
5127
|
-
if (
|
|
5253
|
+
if (isCancel3(choice) || choice === "quit") {
|
|
5128
5254
|
outro("bye \u{1F44B} \u2014 nothing deleted");
|
|
5129
5255
|
return 0;
|
|
5130
5256
|
}
|
|
@@ -5139,18 +5265,18 @@ async function runInteractiveWizard(cwd, options) {
|
|
|
5139
5265
|
placeholder: "e.g. lodash, react-dom, es*build, /react(-dom)?/i",
|
|
5140
5266
|
validate: (value) => value.trim().length === 0 ? "give a pattern \u2014 substring, glob, or /regex/" : void 0
|
|
5141
5267
|
});
|
|
5142
|
-
if (
|
|
5268
|
+
if (isCancel3(pattern)) {
|
|
5143
5269
|
warn("search skipped \u2014 back to the menu");
|
|
5144
5270
|
continue;
|
|
5145
5271
|
}
|
|
5146
|
-
await runSearch({ pattern: String(pattern).trim(), targets:
|
|
5272
|
+
await runSearch({ pattern: String(pattern).trim(), targets: wizardTargets(options), cwd: scope.root, maxDepth: WIZARD_DEPTH });
|
|
5147
5273
|
break;
|
|
5148
5274
|
}
|
|
5149
5275
|
case "map":
|
|
5150
|
-
await runMap({ targets:
|
|
5276
|
+
await runMap({ targets: wizardTargets(options), cwd: scope.root, maxDepth: WIZARD_DEPTH });
|
|
5151
5277
|
break;
|
|
5152
5278
|
case "stats":
|
|
5153
|
-
await runStats({ targets:
|
|
5279
|
+
await runStats({ targets: wizardTargets(options), cwd: scope.root, maxDepth: WIZARD_DEPTH });
|
|
5154
5280
|
break;
|
|
5155
5281
|
case "settings":
|
|
5156
5282
|
log(renderSettings(options.settings, scope));
|
|
@@ -5182,7 +5308,7 @@ async function deleteFlow2(kind, entries, options) {
|
|
|
5182
5308
|
})),
|
|
5183
5309
|
required: true
|
|
5184
5310
|
});
|
|
5185
|
-
if (
|
|
5311
|
+
if (isCancel3(picked)) {
|
|
5186
5312
|
outro("cancelled \u2014 nothing deleted");
|
|
5187
5313
|
return 0;
|
|
5188
5314
|
}
|
|
@@ -5197,11 +5323,11 @@ async function deleteFlow2(kind, entries, options) {
|
|
|
5197
5323
|
outro("cancelled \u2014 nothing deleted");
|
|
5198
5324
|
return 0;
|
|
5199
5325
|
}
|
|
5200
|
-
const admitted = await
|
|
5326
|
+
const admitted = await confirm3({
|
|
5201
5327
|
message: mode === "hard" ? `Delete ${selected.length} folder(s) \u2014 ${formatBytes(sum(selected))} \u2014 PERMANENTLY (no undo)?` : `Move ${selected.length} folder(s) \u2014 ${formatBytes(sum(selected))} \u2014 to the OS trash?`,
|
|
5202
5328
|
initialValue: mode !== "hard"
|
|
5203
5329
|
});
|
|
5204
|
-
if (
|
|
5330
|
+
if (isCancel3(admitted) || admitted === false) {
|
|
5205
5331
|
outro("cancelled \u2014 nothing deleted");
|
|
5206
5332
|
return 0;
|
|
5207
5333
|
}
|
|
@@ -5220,7 +5346,7 @@ async function pickDeleteMode(defaultMode) {
|
|
|
5220
5346
|
],
|
|
5221
5347
|
initialValue: defaultMode ?? "trash"
|
|
5222
5348
|
});
|
|
5223
|
-
return
|
|
5349
|
+
return isCancel3(modeChoice) ? null : modeChoice;
|
|
5224
5350
|
}
|
|
5225
5351
|
function renderSettings(settings, scope) {
|
|
5226
5352
|
const values = settings?.values ?? {};
|
|
@@ -5495,6 +5621,10 @@ cli.option("--reinstall", "stats only: naive npm-ci-style estimate of the time t
|
|
|
5495
5621
|
cli.option("--treemap [file]", "treemap export: single-file HTML (vanilla JS, no deps); default nmh-treemap.html in cwd");
|
|
5496
5622
|
cli.option("--out <file>", "treemap only: where to write the HTML file (same as --treemap <file>)");
|
|
5497
5623
|
cli.option("--update-check", "opt-in: peek npm for a newer release (non-blocking, 24 h cache; also settable in .nmhrc)");
|
|
5624
|
+
cli.option("--check", "update only: compare installed vs registry and exit \u2014 never installs");
|
|
5625
|
+
cli.option("--info", "update only: full registry detail (npm view style) \u2014 never installs");
|
|
5626
|
+
cli.option("-n, --no", "update only: decline the update on the command line (nothing installs)");
|
|
5627
|
+
cli.option("--wizard", "open the interactive wizard explicitly (same as the bare `nmh`); needs a TTY");
|
|
5498
5628
|
cli.option("--default-exclude <globs>", 'config family: manage the persisted exclude list (comma list; special value "self")');
|
|
5499
5629
|
cli.option("--default-keep <pkgs>", 'config family: manage the persisted keep list (comma list; special value "self")');
|
|
5500
5630
|
cli.option("--default-mode <mode>", "config family: set the persisted defaultMode (hard | soft | trash)");
|
|
@@ -5513,8 +5643,10 @@ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
5513
5643
|
"config",
|
|
5514
5644
|
"update",
|
|
5515
5645
|
"version",
|
|
5516
|
-
"help"
|
|
5646
|
+
"help",
|
|
5647
|
+
"wizard"
|
|
5517
5648
|
]);
|
|
5649
|
+
var WIZARD_VIEWS = /* @__PURE__ */ new Set(["stats", "analyze", "info", "map", "search", "why", "treemap"]);
|
|
5518
5650
|
function looksLikePath(text2) {
|
|
5519
5651
|
return text2.includes("/") || text2.includes("\\") || text2.startsWith(".") || text2.startsWith("~") || /^[A-Za-z]:/.test(text2);
|
|
5520
5652
|
}
|
|
@@ -5549,8 +5681,12 @@ function normalizeDepth(value) {
|
|
|
5549
5681
|
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
5550
5682
|
return "invalid";
|
|
5551
5683
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
5684
|
+
var updateNoticeShown = false;
|
|
5685
|
+
async function runCli(userArgs) {
|
|
5686
|
+
const dd = userArgs.indexOf("--");
|
|
5687
|
+
const routerTokens = dd >= 0 ? userArgs.slice(dd + 1) : null;
|
|
5688
|
+
const headArgs = dd >= 0 ? userArgs.slice(0, dd) : userArgs;
|
|
5689
|
+
const parsed = cli.parse(["node", "nmh", ...headArgs], { run: false });
|
|
5554
5690
|
const flags = parsed.options;
|
|
5555
5691
|
const positionals = (parsed.args ?? []).filter((token) => typeof token === "string");
|
|
5556
5692
|
if (flags.version === true) {
|
|
@@ -5599,7 +5735,8 @@ async function main() {
|
|
|
5599
5735
|
// --json shares stdout with the live counter → counter must stay silent
|
|
5600
5736
|
progress: json ? "off" : progress
|
|
5601
5737
|
});
|
|
5602
|
-
if (flags.updateCheck === true || config.updateCheck === true) {
|
|
5738
|
+
if (!updateNoticeShown && (flags.updateCheck === true || config.updateCheck === true)) {
|
|
5739
|
+
updateNoticeShown = true;
|
|
5603
5740
|
const notice = await checkForUpdate().catch(() => null);
|
|
5604
5741
|
if (notice !== null) note(notice);
|
|
5605
5742
|
}
|
|
@@ -5800,12 +5937,71 @@ async function main() {
|
|
|
5800
5937
|
printVersion(json);
|
|
5801
5938
|
return 0;
|
|
5802
5939
|
}
|
|
5940
|
+
if (flags.check !== void 0 && flags.check !== false && subcommand !== "update") {
|
|
5941
|
+
errorLog("--check only works with the update command \u2014 nmh update --check");
|
|
5942
|
+
return 1;
|
|
5943
|
+
}
|
|
5944
|
+
if (flags.info !== void 0 && flags.info !== false && subcommand !== "update") {
|
|
5945
|
+
errorLog("--info only works with the update command \u2014 nmh update --info");
|
|
5946
|
+
return 1;
|
|
5947
|
+
}
|
|
5803
5948
|
if (subcommand === "update") {
|
|
5804
5949
|
if (deleted.given || rest.length > 0) {
|
|
5805
5950
|
errorLog("update takes no arguments \u2014 plain `nmh update` (add --json for the payload)");
|
|
5806
5951
|
return 1;
|
|
5807
5952
|
}
|
|
5808
|
-
|
|
5953
|
+
if (flags.check === true && flags.info === true) {
|
|
5954
|
+
errorLog("--check and --info are two different read-only views \u2014 pick one (nmh update --check | --info)");
|
|
5955
|
+
return 1;
|
|
5956
|
+
}
|
|
5957
|
+
return runUpdate({
|
|
5958
|
+
json,
|
|
5959
|
+
mode: flags.check === true ? "check" : flags.info === true ? "info" : "default",
|
|
5960
|
+
answer: flags.yes === true ? "yes" : flags.no === true ? "no" : "none",
|
|
5961
|
+
force: flags.force === true
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5964
|
+
const wizardIntent = subcommand === "wizard" || flags.wizard === true;
|
|
5965
|
+
if (routerTokens !== null && !wizardIntent) {
|
|
5966
|
+
errorLog("the `--` router belongs to the wizard \u2014 nmh wizard -- stats|map|search|why|treemap \u2026");
|
|
5967
|
+
return 1;
|
|
5968
|
+
}
|
|
5969
|
+
let routerView = null;
|
|
5970
|
+
if (routerTokens !== null) {
|
|
5971
|
+
const view = routerTokens[0];
|
|
5972
|
+
if (typeof view !== "string" || !WIZARD_VIEWS.has(view)) {
|
|
5973
|
+
errorLog(`unknown wizard view: ${String(view ?? "(empty)")} \u2014 the router opens a read-only view: stats (analyze, info) | map | search | why | treemap`);
|
|
5974
|
+
return 1;
|
|
5975
|
+
}
|
|
5976
|
+
routerView = routerTokens;
|
|
5977
|
+
}
|
|
5978
|
+
if (wizardIntent) {
|
|
5979
|
+
if (rest.length > 0) {
|
|
5980
|
+
errorLog("wizard takes no positional targets \u2014 scope it with --dir=./repo1,./repo2 (or `nmh wizard -- <view> \u2026`)");
|
|
5981
|
+
return 1;
|
|
5982
|
+
}
|
|
5983
|
+
if (deleted.given) {
|
|
5984
|
+
errorLog("--delete does not belong to the wizard \u2014 the menu asks for the mode itself");
|
|
5985
|
+
return 1;
|
|
5986
|
+
}
|
|
5987
|
+
if (!isTTYOut()) {
|
|
5988
|
+
errorLog("the wizard needs a TTY (it is an arrow-key menu) \u2014 use nmh scan / --dir / the plain commands instead; bare `nmh` in a pipe stays a read-only scan");
|
|
5989
|
+
return 1;
|
|
5990
|
+
}
|
|
5991
|
+
if (routerView !== null) {
|
|
5992
|
+
log(renderFullBanner());
|
|
5993
|
+
const viewCode = await runCli(routerView);
|
|
5994
|
+
if (viewCode === 1) return 1;
|
|
5995
|
+
}
|
|
5996
|
+
return runWizard(cwd, {
|
|
5997
|
+
defaultMode: config.defaultMode,
|
|
5998
|
+
json,
|
|
5999
|
+
settings: { source: configSource, values: config },
|
|
6000
|
+
excludes: excludes.length > 0 ? excludes : void 0,
|
|
6001
|
+
keep: keep.length > 0 ? keep : void 0,
|
|
6002
|
+
targets: dirTargets.length > 0 ? dirTargets : void 0,
|
|
6003
|
+
banner: routerView === null
|
|
6004
|
+
});
|
|
5809
6005
|
}
|
|
5810
6006
|
if (subcommand === "config") {
|
|
5811
6007
|
const ops = /* @__PURE__ */ new Set(["info", "set", "add", "remove", "unset"]);
|
|
@@ -6128,6 +6324,9 @@ function normalizeAnimate(argv, configAnimate) {
|
|
|
6128
6324
|
if (argv.includes("--animate") || argv.includes("--animate=true")) return "on";
|
|
6129
6325
|
return configAnimate === false ? "off" : "auto";
|
|
6130
6326
|
}
|
|
6327
|
+
function main() {
|
|
6328
|
+
return runCli(process.argv.slice(2));
|
|
6329
|
+
}
|
|
6131
6330
|
main().then((code) => {
|
|
6132
6331
|
if (code !== 0) process.exitCode = code;
|
|
6133
6332
|
}).catch((err) => {
|
package/package.json
CHANGED