ReviewOS

stacks/system-cleaner

Stale node_modules reclamation (7.4 GB), gated on project inactivity and lockfile presence

#28
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Split out of #27 because the risk profile is genuinely different from other build output.

The case for it

7.4 GB across 18 node_modules directories on a working developer Mac — measured at -maxdepth 3, so the real figure is higher once nested workspace packages are counted. That makes it the second-largest reclaimable category after .zig-cache, and larger than every fixed-path developer target in categories.ts combined (~1.9 GB).

Most of it is dead weight. A machine with ~140 checked-out repositories has node_modules for projects last opened months ago, each carrying a full dependency tree.

Why it isn't just another pattern in #27

Everything in #27's "safe" tier regenerates locally and deterministically — zig build rebuilds .zig-cache from source already on disk. node_modules does not:

  • Reinstalling needs the network. Offline, on a plane, or on bad hotel wifi, the project is simply broken until connectivity returns.
  • Reinstalling is not reproducible without a lockfile. With one, it's close. Without, the restored tree can differ from what was deleted — and the project may build differently afterwards.
  • A registry can change under you. Unpublished, yanked or version-shifted packages mean a reinstall can fail or resolve differently than it did originally.
  • Install time is minutes, not seconds, and scales with the dependency count.

Offering it next to .zig-cache under one "clear build caches" button would misrepresent that.

Proposed

Treat it as stale dependency reclamation, not cache cleaning:

  • Gate on staleness. Only offer trees whose project has not been touched in 90+ days — use the newest mtime among the project's source files, not node_modules itself, which install timestamps make unreliable.
  • Require a lockfile. If bun.lock, package-lock.json, pnpm-lock.yaml or yarn.lock is present, reinstall is reasonably reproducible. If not, either skip it or flag it loudly — that tree may not be restorable to what it was.
  • Show the restore command per project (bun install, pnpm install, …), read from the lockfile that's actually present.
  • Group by project with last-activity dates. "old-client-site — 820 MB, untouched 7 months" is a decision someone can make; a flat list of paths isn't.
  • Never include in a one-click sweep. Explicit per-project selection only.
  • Consider warning when the machine is offline, since that's precisely when this is the wrong thing to do.
  • #27 — the pattern scanner this would build on; the roots/depth/age machinery is shared, only the risk treatment differs.
10 days ago

Implemented in 2c2313d (pushed straight to main).

packages/clean/src/stale-dependencies.ts, kept apart from the project-cache scanner exactly as this issue argued it should be.

The subtlety that decided the design: activity is read from sources, never from node_modules or build output. An install bumps the tree without anyone touching the code — so reading activity there would permanently exclude the one project most safely reclaimable. tests/stale-dependencies.test.ts pins this with an idle-fresh-install fixture: 200-day-old sources, dependencies reinstalled today, still correctly reported as idle.

Gates, as specified:

  • project inactivity, default 90 days
  • lockfile detected and mapped to its restore command (bun install, npm ci, pnpm install, yarn install), so the cost is visible before the decision
  • trees without a lockfile reported but flagged reproducible: false — what comes back may not match what was deleted — with includeWithoutLockfile: false to exclude them outright
  • a directory with no package.json is skipped
  • the activity walk is bounded, and a partial answer skews newer, so the error falls on the side of leaving a tree alone

Exposed as /stale-dependencies and /clean-stale-dependencies, validating requested paths against a fresh scan like the other two delete routes.

On the 7.4 GB figure: the real total on the development machine is 11.5 GB across 37 trees — the original estimate was measured at -maxdepth 3 and undercounted nested workspace packages, as the issue predicted.

But at the 90-day default this currently reports zero. The idle distribution is 34 of 37 trees touched within a week, 2 within a month, 1 within 90 days. That is the gate working rather than the scanner failing, and it is worth knowing that on an actively-used machine this feature reclaims nothing — its value is for the long tail of abandoned checkouts, which this particular machine does not have.

Full suite 534 pass / 0 fail across 59 files, typecheck clean, lint 0 errors.

Sign in to comment on this issue.