Rpsl/repo-cleaner

Tool for safely removing package-manager directories like vendor/, node_modules/, .venv/ ...

★ 0Forks 0GoGitHub ↗Compare

README

repo-cleaner — Go gopher vacuuming dependency directories

repo-cleaner

Safely reclaim gigabytes eaten by dormant repositories.
Finds dependency directories (node_modules, vendor, .venv, target…) and removes only those that can be fully restored by the package manager.

Go 1.21+ Platform No external deps


The problem

You have dozens of cloned repos that haven't been touched in months. Each one carries a dependency directory packed with packages that can be reinstalled in seconds:

~/code/
  old-api/         node_modules/  →  1.2 GB    45 000 files
  php-legacy/      vendor/        →  120 MB     8 000 files
  ml-experiment/   .venv/         →  2.1 GB    15 000 files
  rust-prototype/  target/        →  3.4 GB    85 000 files

repo-cleaner scans your code directory, determines which ones are safe to delete, and removes them — in one command.

Features

  • Deep lock-file validation — parses lock files to confirm every dependency is downloadable before touching anything
  • Non-destructive by default — dry-run mode only reports; deletion requires an explicit flag
  • Archive mode — zips a dependency directory into a single file before removing it
  • Modular — adding support for a new package manager takes one file and a registration line
  • No external dependencies — pure Go stdlib, compiles to a single static binary

Installation

go install github.com/rpsl/repo-cleaner/cmd/repo-cleaner@latest

Or build from source:

git clone https://github.com/rpsl/repo-cleaner
cd repo-cleaner
go build -o repo-cleaner ./cmd/repo-cleaner

Usage

# Scan and report — nothing is deleted
repo-cleaner --path ~/code

# Delete safe dependency directories
repo-cleaner --path ~/code --delete

# Zip each directory into one archive, then delete the originals
repo-cleaner --path ~/code --archive

# Scan only specific package managers
repo-cleaner --path ~/code --managers js,python

# Override safety checks (dangerous — use with care)
repo-cleaner --path ~/code --delete --force

Example output

Scanning /home/user/code...

[go    ] /home/user/code/api/vendor                              245.3 MB      1.2K files  SAFE
[js    ] /home/user/code/frontend/node_modules                     1.2 GB     45.1K files  SAFE
[php   ] /home/user/code/legacy/vendor                           120.0 MB      8.0K files  UNSAFE
         ! composer.json has path repository: ../local-pkg
[python] /home/user/code/ml-exp/.venv                              2.1 GB     14.8K files  SAFE
[rust  ] /home/user/code/cli/target                                3.4 GB     85.3K files  SAFE
────────────────────────────────────────────────────────────────────────────────────────────────────
Reclaimable (safe): 7.0 GB across 146.4K files, 4 candidate(s)
Skipped (unsafe):   1 candidate(s) — use --force to override

Run with --delete to remove safe candidates, --archive to zip them first.

Safety model

A dependency directory is only eligible for removal if its lock file passes deep validation. Unsafe candidates are always reported but never touched without --force.

Manager Target dir Lock file Flagged as unsafe when
php (Composer) vendor/ composer.lock path-type repository in composer.json; package with no downloadable dist/source URL
js (npm · yarn · pnpm) node_modules/ package-lock.json / yarn.lock / pnpm-lock.yaml file: or link: local dependency; no lock file present
python .venv/ venv/ env/ poetry.lock / Pipfile.lock / requirements.txt no reproducible source found; unpinned versions; local path dependency
go vendor/ go.mod + go.sum replace directive targeting a local path
rust target/ Cargo.lock Cargo.lock missing (target/ is build artifacts — always regeneratable otherwise)

Archive safety: when using --archive, the zip is fully verified (every entry re-read) before the original is removed. If verification fails, the zip is discarded and the original is preserved.

Adding a new package manager

  1. Create internal/manager/mypm.go implementing the Manager interface:
type MyPM struct{}

func (m *MyPM) Name() string         { return "mypm" }
func (m *MyPM) TargetDirs() []string { return []string{"packages"} }

func (m *MyPM) Inspect(dir, parent string) (*manager.Candidate, error) {
    if !fileExists(filepath.Join(parent, "mypm.lock")) {
        return nil, nil // manifest absent — not our project type
    }
    reasons := parseLock(filepath.Join(parent, "mypm.lock"))
    return candidate(m.Name(), dir, parent, reasons), nil
}
  1. Register &MyPM{} in buildRegistry in cmd/repo-cleaner/main.go.
  2. Add tests using the makeFixture / assertSafe / assertUnsafe helpers from internal/manager/testhelper_test.go.

License

MIT

Contributors

Rpsl

Issues