npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cruftkill

v0.4.3

Published

Polyglot dev-cache reaper — find and delete node_modules, .venv, target, DerivedData and the rest of your build cruft from a fast terminal UI

Readme

cruftkill (cft)

Crates.io License: MIT Rust

 ██████╗███████╗████████╗
██╔════╝██╔════╝╚══██╔══╝
██║     █████╗     ██║
██║     ██╔══╝     ██║
╚██████╗██║        ██║
 ╚═════╝╚═╝        ╚═╝

Polyglot dev-cache reaper. Find and delete node_modules, .venv, target, DerivedData, __pycache__, obj, .gradle, .next, .turbo and the rest of your build cruft from a fast terminal UI.

Inspired by voidcosmos/npkill — but rewritten in Rust with a parallel async scanner and extended to 17 ecosystems.

⚠️ cft deletes recursively without a recycle bin. Always review the list before pressing d. Run with --dry-run first if you want to preview.

Visuals

Interactive TUI Mode

cft TUI Interface

Web Admin Dashboard Mode

cft Web UI Dashboard

Features (v0.4)

  • 🌐 17 hardcoded ecosystem profiles — node, python, rust, java, swift, dotnet, ruby, elixir, haskell, scala, cpp, unity, unreal, godot, infra, data-science (or pass --profile all)
  • 🔍 Parallel async directory scanner with CancellationToken
  • 📏 True on-disk size per folder (Unix blocks × 512)
  • 🛡️ Risk analyzer — flags paths inside ~/.config, AppData, .app bundles
  • 🧭 Per-result metadata — ecosystem, cleanup category, delete-risk verdict, and rebuild hint under each scanned path
  • 🗑️ Safe delete with two-layer guard (basename + canonicalize containment)
  • 🖥️ Three UX modes:
    • Interactive TUI (ratatui): navigate, sort by size/name/last-used, inspect cleanup metadata, delete with confirm, rescan
    • --no-tui mode: streams NDJSON for scripting / CI pipelines
    • cft ui web admin — localhost browser dashboard with live SSE streaming, bulk delete, saved scan presets, and a history page with "GB reclaimed over time" + "by ecosystem" charts. All destructive operations route through the same 2-layer guard as the TUI.

Install

From crates.io:

cargo install cruftkill
cft --help

From source:

git clone https://github.com/xuanphamdev/cruftkill
cd cruftkill
cargo install --path .

Requires Rust 1.85+ (edition 2024).

Usage

Interactive TUI

cft                            # scan current dir with `node` profile
cft ~/Projects                 # scan a specific directory
cft -p rust ~/code             # use the `rust` profile (matches `target/`)
cft -p node -p python ~/code   # combine profiles
cft -p all ~/                  # everything everywhere
cft --dry-run ~/Projects       # preview what would be deleted

Each result shows a metadata line under the folder path:

/work/app/.venv                         1.4 GB   3w
  data-science+python | virtual env | risk: low | Recreate with the project setup or package manager command

Keybinds:

| Key | Action | |---|---| | / k | move cursor up | | / j | move cursor down | | d / Space / Enter | open delete confirm | | y / Y | confirm delete | | n / N / Esc | cancel modal | | s | toggle sort by size (desc default) | | n | toggle sort by name (asc default) | | m | toggle sort by last-used (desc default) | | r / F5 | rescan | | q, Ctrl-C | quit |

Scriptable JSON output

cft --no-tui ~/Projects | jq '. | select(.size_bytes > 100000000)'

Each line is a JSON object:

{
  "path": "/home/me/proj-a/node_modules",
  "size_bytes": 314572800,
  "is_sensitive": false,
  "risk_reason": null,
  "target_name": "node_modules",
  "ecosystems": ["node"],
  "category": "dependency-tree",
  "delete_risk": "low",
  "delete_risk_reason": "Regenerable cache or build output outside sensitive paths",
  "rebuild_hint": "Reinstall dependencies before next build",
  "modified_unix": 1779867789,
  "dry_run": false
}

Metadata is advisory. Delete confirmation and the safety guards still decide what can actually be removed. If risk analysis is disabled, metadata uses a medium-risk advisory because sensitive-path checks were skipped.

When stdout is not a TTY (e.g. piped or in CI), cft auto-falls-back to --no-tui mode.

Web UI (cft ui)

cft ui                       # bind 127.0.0.1 on a random free port,
                             # auto-open the browser
cft ui --port 8080           # pin a specific port
cft ui --no-open             # serve only; don't launch the browser

A single-binary localhost admin — no Node toolchain, no separate static bundle, no network listener beyond 127.0.0.1. Press Ctrl-C in the terminal to stop the server gracefully.

The dashboard:

  • Form-driven scan launch with profile chips + custom-target/exclude inputs.
  • Live results table — each match streams in via Server-Sent Events from a broadcast::Sender fan-out, so multiple browser tabs can watch the same scan in real time.
  • Inline sort (size / name / age, asc + desc) + 300ms-debounced substring filter — re-rendered by the server, swapped in via HTMX.
  • Per-row delete with a confirm modal that shows the risk verdict.
  • Bulk select + bulk delete (live "N selected — SIZE reclaimable" summary bar, single-confirm modal for the whole batch).
  • Saved presets — reusable profile+targets+exclude triples; click Run to pre-fill the dashboard form.
  • History page — paginated past scans, plus two Chart.js panels: reclaimed bytes per day (line) and reclaimed bytes per ecosystem (donut). Each scan row drills into a full result list.

Persistence: scan history + presets live in a SQLite DB at dirs::cache_dir()/cruftkill/cft.db (e.g. ~/Library/Caches/cruftkill/cft.db on macOS, ~/.cache/cruftkill/cft.db on Linux, %LOCALAPPDATA%\cruftkill\Cache\cft.db on Windows).

Safety model: every destructive op — single delete, bulk delete, drill-down delete — routes through core::safe_delete (basename guard) + core::delete (canonicalize + containment). Same 2-layer guard the TUI uses; the web UI never bypasses it.

Profiles

Run cft --help or read src/core/profiles.rs for the full target list. Highlights:

| Profile | Matches | |---|---| | node (default) | node_modules, .npm, .pnpm-store, .next, .nuxt, .turbo, .cache, coverage, … | | python | __pycache__, .pytest_cache, .venv, .tox, .mypy_cache, … | | rust | target | | java | target, .gradle, out | | swift | DerivedData, .swiftpm | | dotnet | obj, TestResults, .vs | | cpp | CMakeFiles, cmake-build-debug, cmake-build-release | | unity | Library, Temp, Obj | | unreal | Intermediate, DerivedDataCache, Binaries | | godot | .import, .godot | | data-science | .ipynb_checkpoints, .dvc, .mlruns, … | | infra | .serverless, .vercel, .netlify, .terraform, … | | all | union of every profile |

Add ad-hoc targets with -t:

cft -p node -t my_custom_cache ~/code

Safety model

Two layered guards run before any FS mutation:

  1. Basename guard — the path's basename must appear in the resolved target list. Catches the "wrong path" mistake.
  2. Containment guard — both the scan root and the target path are canonicalized (symlinks resolved). The canonical target must starts_with the canonical root. Catches symlink escape attacks even if the link is named like a target.

std::fs::remove_dir_all is hardened against symlink-traversal (CVE-2022-21658) — Rust does not follow symlinks when removing a directory.

Architecture

cft binary (src/main.rs)
   │
   ├── (no subcommand) ── scan mode
   │     ├── TUI    ──►  src/tui/    (ratatui + crossterm + tokio::select!)
   │     └── --no-tui ──► run_no_tui → NDJSON
   │
   └── `cft ui` ──►  src/web/  (axum + Askama + HTMX + SQLite)
         ├── server.rs   bind 127.0.0.1:port + graceful shutdown
         ├── state.rs    AppState (db + active scan + broadcast)
         ├── db.rs       rusqlite + spawn_blocking + schema
         ├── render.rs   shared row + table templates
         ├── routes/     {pages, scans, sse, results, presets, history, assets}
         └── assets/     embedded HTMX + HTMX-SSE + Chart.js + CSS

All three modes call into:
   src/core/
      ├── scanner    (parallel tokio worker pool, unbounded dispatch)
      ├── size       (refcounted async sum, 60s timeout)
      ├── risk       (pure path classifier)
      ├── metadata   (ecosystem/category/delete-risk labels)
      ├── safe_delete  (basename guard)
      ├── delete     (canonicalize + remove_dir_all)
      ├── profiles   (17 profiles, resolve_targets)
      ├── sort       (path / size / age comparators)
      ├── filter     (case-insensitive substring)
      ├── ignore     (GLOBAL_IGNORE set)
      ├── types      (ScanOptions, FolderResult, …)
      └── error      (CruftError)

Templates live at the crate-root templates/ directory (Askama default). See docs/architecture.md for the full layout.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo build --release

History

This crate was originally published as nodemoduleskiller v0.1.0 (binary nmk). It was renamed to cruftkill (binary cft) in v0.2.0 because the scope had outgrown "node_modules only" — the tool now wipes 17 different language ecosystems' build cruft from a single command. The GitHub repository was renamed too; the old URL xuanphamdev/nodemoduleskiller auto-redirects to xuanphamdev/cruftkill.

Third-party embedded assets

cft ui ships with a small set of client-side libraries vendored into the binary. Full attribution + license texts live in NOTICE. Summary:

  • HTMX (2.0.4) — 0BSD
  • HTMX SSE extension (2.2.2) — 0BSD
  • Chart.js (4.4.6) — MIT

Attribution

This project was inspired by — and ported from — voidcosmos/npkill (© voidcosmos, MIT license). Many design decisions (target detection rules, risk-analysis heuristics, profile definitions, behavioural invariants) are preserved verbatim.

License

MIT — see LICENSE.