Find and delete node_modules directories to reclaim disk space.
Crate: oh-my-node-modules. Binary: onm.
Requires a Rust toolchain (1.88 or newer).
cargo install --path .Prebuilt onm binaries, when attached to a tagged release, use these names
(they are built and uploaded manually, see Releasing):
onm-x86_64-unknown-linux-gnu.tar.gzonm-x86_64-apple-darwin.tar.gzonm-aarch64-apple-darwin.tar.gzonm-x86_64-pc-windows-msvc.zip
onm [path] scans path (default: the current directory) for project-level
node_modules directories. Nested node_modules inside another node_modules
are pruned, so only top-level dependency trees are reported. Hidden directories
are skipped unless --hidden is given.
onm . # Scan the current directory and pick interactively
onm ~/projects # Scan a specific directoryThe scan and size passes run behind spinners. A summary line reports the total
size, project count, and stale count. An inquire multi-select then lists one
row per directory as [size] project [age] repo_path, with size and age
color-coded. Toggle entries with space, confirm with enter, and filter the list
by typing. Entries in .onmfavorites start deselected; the remaining entries
start selected. After selection, a confirm prompt reports how many directories
and how many bytes will be deleted.
onm --scan . # Colored table (top 20) plus a total
onm --scan --json . # JSON array of projects on stdout
onm --json . # Same as `--scan --json`--json writes a single JSON document to stdout; human-readable headers and
previews move to stderr so the output stays parseable.
onm --auto --min-size 500mb . # Delete every matching directory
onm --auto --min-size 1gb --dry-run . # Preview only
onm --auto --min-size 500mb --yes . # Skip the confirmation prompt--auto selects every scanned directory at or above --min-size and deletes
them without the picker. Without --yes it asks for confirmation first;
--dry-run reports what would be deleted and frees nothing.
| Flag | Effect |
|---|---|
--sort size|date|name|packages |
Sort key (default size) |
--asc |
Ascending order (default: descending) |
--max-depth N |
Limit directory descent depth |
--hidden |
Include hidden directories |
--dry-run |
Report without deleting |
--force |
Enable the removal fallbacks described below |
--yes |
Skip the confirmation prompt |
--no-check |
Skip the in-use lockfile check |
.onmignorein the current directory and in the home directory adds glob patterns to skip during discovery. Blank lines and#comments are ignored. Defaults (**/node_modules/**,**/.git/**) are always applied. An.onmignorepattern cannot exclude anode_modulesdirectory by name directly; exclude its parent directory instead (e.g.**/legacy/**)..onmfavoritesin the home directory lists one absolute path per line. Those entries are marked as favorites and start deselected in the picker.
Before removing a directory, onm checks that the normalized path ends in
node_modules, that the directory exists, and that it contains at least one
subdirectory or that its parent holds a package.json. Unless --no-check is
given, it also flags a target as in use when a lockfile (package-lock.json,
yarn.lock, or pnpm-lock.yaml) in the project was modified within the last
60 seconds.
Removal uses remove_dir_all first. With --force, a failure triggers a
recursive permission fix and a retry, then a rename to <path>.old.<timestamp>
followed by removal.
Source: docs/onm-flow.mmd, rendered with Mermaid.
- Discovery uses the
ignorewalker in parallel;node_modulessubtrees are pruned and.onmignorepatterns are applied throughglobset. - Sizes, recursive package counts,
package.jsonmetadata, and Git repository roots are computed per directory, then fanned out in parallel withrayonacross targets. - Deletion is parallelized across targets with
rayon. Each target's failure is recorded per item; a batch never aborts early.
The same checks run on every push and pull request (.github/workflows/ci.yml):
cargo fmt --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked --all-targets
cargo +1.88.0 check --locked --all-targets # MSRV
cargo build --release --locked
cargo publish --dry-run --locked # packaged crate builds
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --lockedPublishing is manual and local; CI never deploys. From a clean checkout:
# 1. Bump `version` in Cargo.toml and add a CHANGELOG entry.
# 2. Verify the same checks CI runs, then publish:
cargo publish --locked
# 3. Tag the commit that matches the published version:
git tag v0.1.1 && git push origin v0.1.1
# 4. Optional: build and attach the prebuilt binaries to the GitHub release.
cargo build --release --locked --target <target>Keep the tag and the crate version in sync (v<version>).
The crate restarted its version scheme at 0.1.0 when the TypeScript/Bun
implementation was replaced by Rust. The npm-era v1.x tags were removed on
2026-10-12 so the tag list reflects the current crate only; the tag-to-commit
map, and how to recreate any of them, is in
docs/version-history.md.
https://github.com/Lukasdias/oh-my-node-modules
MIT