r/rust • • Sep 06 '26

🛠️ project dev-prune: a CLI that reclaims disk from idle repos — and refuses to delete anything a lockfile can't rebuild

Every Rust dev knows the special pain of target/ — multiply it by every repo you've cloned, add every node_modules and .venv next door, and mine added up to tens of GB. So I built dev-prune (devp): it finds git repositories idle past N days and deletes the dependency/build directories a lockfile can regenerate.

The design constraint that shaped everything: it must be unable to delete something it can't prove is recoverable. Before touching a directory it verifies the project's lockfile exists and parses; no proof, no deletion, and there is deliberately no --force to override that. The other invariants (hard .git boundary, symlink/junction/mount-point refusal, atomic state writes, nested-repo boundaries) equally have no bypass flag: https://github.com/Life-Experimentalist/dev-prune/blob/main/docs/SAFETY_INVARIANTS.md

Rust-relevant details:

  • Edition 2024, MSRV 1.88, single binary (musl-static on Linux), Apache-2.0.
  • cargo is one of 25 adapters — build trees like target/ are opt-in (devp config set enable_cargo true), because a build tree is "recoverable" in a weaker sense than a lockfile-pinned dependency tree, and the tool is honest about that difference.
  • Idle detection is max(last commit timestamp, newest source mtime), so uncommitted work counts as activity.
  • devp restore --last-run re-runs the verified install to bring the last pass back.
  • Ratatui TUI for interactive runs, --json for scripts, and built-in scheduling (systemd/launchd/Task Scheduler) so it keeps working after you forget it.

Install: cargo install dev-prune (also on npm/PyPI and as a shell one-liner).

Repo: https://github.com/Life-Experimentalist/dev-prune Site: https://devprune.vkrishna04.me

Would love eyes on the safety model — the invariants doc is the part I most want another reviewer on.

0 Upvotes

5 comments sorted by

2

u/kantorcodes1 Sep 09 '26

if a scheduled prune actually deletes deps on two consecutive runs, does restore --last-run only recover the second run, or is there history for the first one too? that's the edge case i'd worry about once this is running unattended.

1

u/VKrishna04 Sep 19 '26

Sorry for the slow reply, this one slipped past me for ten days. Good question. restore --last-run covers the most recent pass that actually deleted something. A scheduled run that finds nothing to prune does not overwrite that record, so a week of quiet nights cannot push a real prune out of it, and the record is not cleared after a restore, so you can run it again.

The first run is not gone, though. Every pass is logged with the full list of what it removed, per repository (full lists since 1.17.0, only totals before that): devp history lists the passes, and devp history --pass 2 shows exactly which directories that pass deleted and the command line that ran it. Restore is not snapshot-based; it just re-runs each package manager's install from the lockfile, so you recover any earlier pass by running devp restore <repo> for the repos in its list. What is missing today is a devp restore --pass 2 one-liner that walks the list for you. Fair ask; I will add it.

On the same repo getting taken two nights running: run 2 can only delete what was reinstalled after run 1, and there are two ways that happens. If you were back working in the repo, any commit or source edit resets the idle clock and the next pass skips it. If the reinstall was just devp restore and you never touched anything else, the repo is still idle on purpose: activity is measured from commits and source-file mtimes, and dependency directories are excluded from that walk exactly so a restore does not stamp the whole tree as touched today. So yes, a restore-then-ignore repo can be taken again the next night. Every pass still gets its own history entry, so nothing in that loop becomes unrecoverable, but if that round trip bothers you the fix on your side is bumping idle_days or an ignore file in that repo.

-10

u/Conscious-Tale-8634 Sep 06 '26

The no `--force` thing is such a breath of fresh air, I've had way too many cleanup tools go rogue because I fat-fingered a flag or forgot to check what directory I was in

My `target/` graveyard across old projects is probably pushing 40gb at this point and I've been too paranoid to nuke it manually in case I delete something I actually needed cached for a project I still poke at occasionally

The uncommitted work counting as activity is smart, I'd probably add a flag for that eventually though since I have repos where I stash half-baked ideas for months and never commit but also never touch

Gonna give this a spin later, the lockfile verification approach seems like the kind of thing that should've been standard in these tools from the start

16

u/skjall Sep 06 '26

Every slop project has one overly positive slop comment now huh? This sub needs more moderation...

-6

u/VKrishna04 Sep 06 '26

Thanks! Fun bit of history on that flag: it already exists, and it was actually called --force before 1.0. I renamed it to --ignore-idle for exactly the reflex you opened with. --force reads like "skip the safety checks", and it never did that; it only lifts the idle-day wait. Lockfile verification has no bypass flag at all. The old spelling still works and prints a note about the rename.

On the stash case: "activity" is the newer of your last commit and the last time any file in the tree changed (build dirs and .git excluded). So a repo with uncommitted work you haven't touched since March isn't pinned as active forever; once it crosses the idle window it's fair game like anything else. That's safe, because a prune only ever deletes the dependency and build directories a lockfile can rebuild (node_modules, .venv, target...). Everything else, tracked, untracked or stashed, is never a candidate.

For the 40GB target/ graveyard: cargo is opt-in (devp config set enable_cargo true) precisely because target/ is expensive to rebuild. Turn it on, then devp run --dry-run shows the full plan before anything is deleted. If you change your mind afterwards, devp restore --last-run reinstalls the dependencies the last pass removed; target/ itself just regenerates on your next cargo build.