A TUI tool for finding and removing bloated project directories that waste disk space. Scan projects, see exactly what's eating your disk, and delete with a single keystroke.
Requires Node.js or pnpm installed on your system.
# for npm user
npx dirsweep# for pnpm user
pnpx dirsweepRequires the Rust toolchain installed via rustup.
cargo install dirsweepcurl -sSfL https://github.com/farrelaby/dirsweep/raw/main/install.sh | shInstalls to ~/.local/bin (no sudo needed).
curl -sSfL https://github.com/farrelaby/dirsweep/raw/main/install.sh | shInstalls to /usr/local/bin (may prompt for sudo).
irm https://github.com/farrelaby/dirsweep/raw/main/install.ps1 | iexInstalls to %USERPROFILE%\.local\bin and adds to user PATH.
# Scan current directory
dirsweep
# Scan a specific directory
dirsweep --dir /path/to/projects
# Update to latest version
dirsweep update
# Uninstall
dirsweep uninstall
dirsweep uninstall --force # skip confirmation dirsweep — /home/user/projects · 6.8 GiB you can free · scanned in 0.6s
my-app (pnpm)
└ [●] node_modules 1.2 GiB 3 days ago
[●] dist 800 MiB 2 days ago
rust-project (cargo)
└ [●] target 4.5 GiB 10 days ago
my-other-app (npm)
[ ] .next 350 MiB just now
[Space] Toggle [a] All [d] None [o] Sort [?] Help [Enter] Delete (3 · 6.50 GiB) [q] Quit 1/4
Everything on the bar is clickable — and so are the rows and the dialog
options. Press ? in the app to see every key at once.
| Directory | Language / Framework |
|---|---|
node_modules |
Node.js |
target |
Rust, Maven (Java) |
.next |
Next.js |
__pycache__ |
Python |
.venv / venv |
Python virtual environments |
dist / build |
JS/TS bundlers, Gradle (Java/Kotlin) |
vendor |
Go, PHP (Composer) |
Pods |
iOS (CocoaPods) |
bower_components |
Legacy JavaScript |
.tox |
Python (tox) |
out |
Kotlin/Java |
dirsweep detects the language/package manager by scanning for lock files and manifests in the parent directory:
| Lock file | Package manager |
|---|---|
package-lock.json |
npm |
pnpm-lock.yaml |
pnpm |
bun.lockb |
bun |
deno.lock |
deno |
yarn.lock |
yarn |
Cargo.lock |
Cargo (Rust) |
go.sum |
Go |
Gemfile.lock |
Bundler (Ruby) |
composer.lock |
Composer (PHP) |
dirsweep identifies monorepo structures to help you understand what you're deleting:
| File | Monorepo tool |
|---|---|
pnpm-workspace.yaml |
pnpm workspaces |
turbo.json |
Turborepo |
nx.json |
Nx |
lerna.json |
Lerna |
workspaces in package.json |
npm/yarn workspaces |
Cargo.toml with members |
Rust workspace |
Root-level node_modules in monorepos are highlighted as they may be shared across packages — deleting them could break the entire workspace.
dirsweep is benchmarked against npkill — the most popular CLI tool for finding and removing node_modules directories. Both tools run in TUI mode through a PTY, measuring wall time and peak RSS.
| Scenario | ds (s) | np (s) | ds (MB) | np (MB) | Result |
|---|---|---|---|---|---|
| Node monorepo | 0.064 | 0.272 | 7 | 171 | dirsweep 4.2× faster |
| Multi-language | 0.062 | 0.136 | 7 | 170 | dirsweep 2.2× faster |
| 500 tiny projs | 0.098 | 4.430 | 7 | 188 | dirsweep 45.1× faster |
| Deep nesting | 0.067 | 0.973 | 7 | 176 | dirsweep 14.5× faster |
| Large targets | 0.061 | 0.112 | 7 | 169 | dirsweep 1.8× faster |
Averages of 10 runs on synthetic fixtures (low variance). ds = dirsweep v0.3.0, np = npkill v0.12.2.
| # | Name | Description |
|---|---|---|
| 1 | Node monorepo | Root project + 20 JS sub-packages, .next build output |
| 2 | Multi-language | 5 JS, 5 Rust, 5 Python projects with their respective junk dirs |
| 3 | 500 tiny projs | 500 minimal JS projects, each with 1 KB node_modules |
| 4 | Deep nesting | 10 nested projects + 100 random-depth tiny projects |
| 5 | Large targets | 3 JS projects with 100, 200, 300 MB node_modules |
Requirements:
- dirsweep — build from source:
cargo build --release - npkill — install globally:
npm install -g npkill(or viapnpm/yarn) - Python 3 — used for PTY automation
- bash,
find,truncate,awk— standard POSIX tools
# From the repo root:
bash bench/bench.sh
# Only create fixtures (skip the benchmark):
bash bench/bench.sh --prepare
# Customise:
DIRSWEEP=target/debug/dirsweep RUNS=5 SCENARIOS="1 3" bash bench/bench.shFixtures are created in bench/fixtures/ and left after the run for inspection.
Remove them with rm -rf bench/fixtures.
bench.sh measures dirsweep end-to-end against npkill — one wall-clock number.
That answers "is dirsweep fast?", but not "which phase got slower?". For
algorithmic changes to the scan core there is a separate Criterion harness that
times each phase in isolation, plus size computation, which bench.sh never
measures because the TUI discards it before it finishes.
# Build without running (used in CI to stop benches rotting):
cargo bench --bench scan --no-run
# Run everything:
cargo bench --bench scan
# One phase, one scenario:
cargo bench --bench scan -- "scan/many-tiny"Five groups, each over five deterministic scenarios:
| Group | Measures |
|---|---|
pipeline/ |
scan → build_tree → first rendered frame. The headline number |
scan/ |
scanner::scan alone — discovery and target assignment |
build_tree/ |
AppState::build_tree — flattening scan output into the render tree |
size/ |
scan_target_size across every target, replicating the worker in main.rs |
render/ |
ui::render into a fixed 120x40 TestBackend, tree pre-built |
Scenarios: node-monorepo, multi-lang, many-tiny (500 projects),
deep-nesting, dense-targets (few targets, many files — the one that
stresses size/).
Unlike bench.sh, fixtures are deterministic: a seeded PRNG replaces
$RANDOM, so the same scenario always produces the same tree. That is what
makes a before/after comparison meaningful rather than partly noise.
Before starting a rewrite, save a baseline; afterwards, compare against it:
git checkout main && cargo bench --bench scan -- --save-baseline before
git checkout <rewrite> && cargo bench --bench scan -- --baseline beforeRun the two back to back. The same code measured 1.87× apart across two
sessions on this laptop — a uniform shift from CPU clock and thermal state, not
an algorithmic change. A stale baseline will report a false speedup in every
benchmark. bench/BASELINE.md has the measurements and the full procedure.
Criterion reports a significance test per benchmark (regressed / improved /
no change) rather than a bare percentage. HTML reports land in
target/criterion/.
Each run also prints a work counters table — directories entered, files seen, subtrees pruned, detection files matched:
scenario targets files disc_d disc_f pruned detect tgt_d
many-tiny 500 1500 501 500 500 500 500
dense-targets 3 753 4 3 3 3 3
Wall-clock time on a loaded machine is noisy; work done is not. If a rewrite
cuts disc_d by a third, that is a real result even when the timer cannot
resolve it. Note dense-targets: 753 files exist but discovery only enters
three, because node_modules is pruned rather than walked.
Benches build under [profile.bench] with opt-level = 3, while the shipped
binary is opt-level = "z". Both sides of a comparison use the bench profile,
so the result isolates algorithmic change from optimizer behaviour — but it is
not a measurement of the binary you ship.