dev-prune 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -263,6 +263,7 @@ A few more worth knowing on day one:
263
263
  ```bash
264
264
  devp stats # how much has been reclaimed so far, and by which repositories
265
265
  devp caches # every package manager cache, sized. The report deletes nothing
266
+ devp caches docker # what Docker holds, and the prune commands. Read-only, permanently
266
267
  devp status --drift # anything installed that the lockfiles don't record?
267
268
  devp doctor . # why is this repository not being pruned?
268
269
  devp doctor --fix # repair a broken integration — never a first-time install
@@ -355,6 +356,13 @@ Package manager caches
355
356
  Total 21.99 GiB across 12 caches
356
357
 
357
358
  → Nothing above was deleted, and dev-prune never deletes any of it.
359
+
360
+ Container engines
361
+
362
+ docker 47.64 GiB 36.69 GiB reclaimable · devp caches docker
363
+
364
+ Container images, volumes and build cache are not package manager caches and are not in
365
+ the total above — dev-prune reports them and never deletes them.
358
366
  ```
359
367
 
360
368
  A cache lives outside every repository and is shared by all of them, so no single
@@ -371,6 +379,64 @@ a registered repository, and each such row names its store in the command it pri
371
379
 
372
380
  </details>
373
381
 
382
+ <details>
383
+ <summary><b><code>devp caches docker</code></b> — the engine that is usually bigger than all of them</summary>
384
+
385
+ ```console
386
+ $ devp caches docker
387
+
388
+ Container engines
389
+
390
+ docker
391
+
392
+ Images 37.36 GiB 29.28 GiB reclaimable 41 items, 9 in use
393
+ Containers 1.19 GiB 1.12 GiB reclaimable 12 items, 3 in use
394
+ Local Volumes 2.79 GiB 0 B reclaimable 5 items, 2 in use
395
+ Build Cache 6.29 GiB 6.29 GiB reclaimable 41 items, 0 in use
396
+
397
+ Total 47.64 GiB 36.69 GiB reclaimable
398
+
399
+ Reclaim it yourself what it takes with it
400
+ docker builder prune the build cache; costs a slower next build
401
+ docker image prune dangling images no tag points at any more
402
+ docker container prune stopped containers and each writable layer
403
+ docker system prune the three above at once; volumes untouched
404
+ docker system prune --volumes adds unused volumes — the one that deletes data
405
+
406
+ Nothing above was deleted, and nothing dev-prune runs on a schedule will ever delete it.
407
+ An image has no lockfile to prove it can be rebuilt, and a named volume is the one thing
408
+ here that cannot be rebuilt at all — so this command measures, prints the commands, and
409
+ leaves the decision with you.
410
+ ```
411
+
412
+ Clearing 6 GiB of npm cache while a Docker install nobody has looked at in a year sits on
413
+ 40 GiB is the mistake this exists to prevent. `devp caches podman` and `devp caches
414
+ nerdctl` are the same report for those engines, and `devp caches containers` runs every
415
+ one it finds and lists any local Kubernetes clusters (kind, k3d, minikube) by name.
416
+
417
+ **It is read-only, permanently.** There is no flag and no `clear` verb that makes
418
+ dev-prune run one of those commands — `devp caches clear docker` is a usage error that
419
+ says so — and no scheduler or Git hook reaches this code at all. That is the same rule
420
+ as everywhere else rather than extra caution: an image has no lockfile to prove it can be
421
+ rebuilt, the Dockerfile that built it may not be on this disk, and a named volume is the
422
+ one thing on the machine that cannot be rebuilt at all.
423
+
424
+ The figures come from the engine's own `system df`, not a walk of the disk. On Docker
425
+ Desktop and Podman the store lives inside a VM disk image the host filesystem cannot see,
426
+ so a size taken off the disk would be wrong by orders of magnitude in the reassuring
427
+ direction — and asking is the only way to learn what is *reclaimable*, which is the
428
+ figure that decides anything. 40 GB of images with 38 GB dangling is a different
429
+ situation from 40 GB with 2 GB dangling.
430
+
431
+ An engine that is installed with its daemon stopped is reported as exactly that, quoting
432
+ the engine's own first line, and contributes no figures: a blank, not a zero. An engine
433
+ that is not installed is absent from the report rather than listed as missing. Local
434
+ Kubernetes clusters are named and deliberately not sized — their nodes are containers
435
+ belonging to an engine already in the table, so a figure beside the cluster name would be
436
+ the same gigabytes counted twice.
437
+
438
+ </details>
439
+
374
440
  <details>
375
441
  <summary><b><code>devp doctor .</code></b> — the one reason a repository is being skipped</summary>
376
442
 
@@ -444,6 +510,7 @@ process that leaves a dirty working tree is a surprise.
444
510
  | 🧩 **Any number of ecosystems per repository** | uv, npm and cargo in one root, or spread across `frontend/`, `services/api/` and `tools/cli/` — each discovered, verified and pruned on its own terms |
445
511
  | ↩️ **One-command restore** | `devp restore .` reinstalls a tree; `devp restore --last-run` puts back exactly what the most recent pass deleted, across every repository it touched |
446
512
  | 🕒 **Activity-aware** | Combines `git log` timestamps with source-file `mtime`, so uncommitted work protects a repository just as a commit does |
513
+ | 🐳 **Container report** | `devp caches docker` (also `podman`, `nerdctl`, or `containers` for all of them plus local Kubernetes clusters) breaks a container engine's disk into images, containers, local volumes and build cache, each with what the engine itself calls reclaimable, and prints the prune commands narrowest first with what each takes with it. **Read-only, permanently** — `devp caches clear docker` is a usage error, and no scheduler or hook reaches this code: an image has no lockfile to prove it can be rebuilt, and a named volume cannot be rebuilt at all. `devp caches` carries a one-line summary per engine, outside its own total |
447
514
  | 📊 **Cache report** | `devp caches` sizes every package manager cache and store on the machine — npm to cargo to conda, Maven, Gradle, NuGet, vcpkg, Conan, Composer, CocoaPods and Hex — and prints the command that clears each. The report is read-only; `devp caches clear <manager>` runs that command for you, after asking. `devp config set cache_max_gb uv=10,npm=10` says how big is too big, per manager, and marks the ones past it — `devp caches clear --over-cap all` then empties exactly those, still only when you type it. Each manager also says how many of your registered repositories use it and what that works out to per repository, and `devp caches clear --unused all` empties the ones nothing uses at all. pnpm is reported once per filesystem, because a store it hardlinks into `node_modules` cannot cross one and projects kept off the system disk get a store of their own. Nothing on a schedule ever touches a cache, and Maven's `~/.m2/repository` is never cleared at all — it holds artifacts `mvn install:install-file` put there that no remote can hand back |
448
515
  | 🩺 **`devp doctor`** | One read-only pass that ends by naming the *single* reason a repository would or would not be pruned. Runs no package manager, repairs nothing, safe to run twice. `devp doctor --fix` then mends what it found — installed-but-broken only |
449
516
  | 🤖 **Self-installing automation** | OS-native scheduler (Task Scheduler, LaunchAgent, systemd user timer) and non-blocking Git hooks, installed at install time and restored after an upgrade. `auto_setup`, `auto_hooks`, `auto_daemon` or `DEV_PRUNE_NO_AUTO_SETUP=1` turn it off |
@@ -467,9 +534,10 @@ process that leaves a dirty working tree is a surprise.
467
534
  | `devp undo` | :------------------------------------------------------------------ | Reverts the most recent `init` or `link` |
468
535
  | `devp run [PATH]` | `--dry-run`, `--only`, `--skip`, `--except`, `--min-size`, `--json` | Prunes every registered repository, or one target |
469
536
  | `devp status` | `--top N`, `--drift`, `--json` | Interactive dashboard; a plain table when there is no TTY. `--top N` shows only the N biggest repositories; `--drift` lists every environment holding packages its lockfile never recorded |
470
- | `devp stats` | `--json` | What has already been reclaimed: lifetime total, prune passes, the last pass, and the biggest contributors |
537
+ | `devp stats` | `--json` | What has already been reclaimed: lifetime total from pruning, a separate lifetime total from `devp caches clear`, prune passes, the last pass, and the biggest contributors |
471
538
  | `devp completions` | `bash`, `zsh`, `fish`, `powershell`, `elvish` | Prints a shell completion script to stdout, generated from the same argument definitions the binary parses with |
472
539
  | `devp caches` | `clear <manager\|all>`, `--json` | Sizes every package manager cache on the machine and prints the command that clears each. The report deletes nothing and nothing on a schedule ever will; `clear` empties one when you type it, after asking |
540
+ | `devp caches docker` | `podman`, `nerdctl`, `containers [ENGINE]`, `--json` | What a container engine holds — images, containers, volumes, build cache — each sized, with what the engine calls reclaimable, then the prune commands. Read-only permanently: it prints them, you run them |
473
541
  | `devp trust` | `--json` | What dev-prune may do on this machine: the guarantees the code enforces, then the scheduler, hooks and settings read live. Read-only |
474
542
  | `devp restore [PATH]` | `--last-run` | Reinstalls dependencies for every project in a tree; `--last-run` undoes the last prune pass |
475
543
  | `devp doctor [PATH]` | `--fix` | Diagnoses the installation, or one repository — ending with the single reason a pass would or would not touch it. `--fix` repairs what the checks found; it never performs a first-time install |
package/bin/dev-prune.js CHANGED
@@ -66,7 +66,18 @@ const fallbacks = [
66
66
  const binaryPath =
67
67
  bundledBinary() || fallbacks.find((c) => fs.existsSync(c)) || exeName;
68
68
 
69
- const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
69
+ let child;
70
+ try {
71
+ child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
72
+ } catch (err) {
73
+ // Windows throws out of `spawn` itself when the loader refuses the image — a
74
+ // wrong-architecture or truncated binary — because there is no child to emit an
75
+ // 'error' event on. Everywhere else the same failure arrives asynchronously. Without
76
+ // this branch the message written for exactly that case never runs, and the user gets
77
+ // a raw Node stack trace instead. `reportLaunchFailure` is a hoisted function
78
+ // declaration, so it is callable from above its definition; it never returns.
79
+ reportLaunchFailure(err);
80
+ }
70
81
 
71
82
  // Forward termination to the child. Ctrl+C already reaches it through the shared
72
83
  // process group, but a bare `kill` of this wrapper — process managers, CI timeouts —
@@ -85,11 +96,19 @@ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
85
96
 
86
97
  // Without this, a missing binary surfaces as an unhandled 'error' event and a Node
87
98
  // stack trace instead of an actionable message.
88
- child.on('error', (err) => {
89
- if (err.code === 'ENOENT' && binaryPath !== exeName && fs.existsSync(binaryPath)) {
90
- // The file is right there, yet the loader said "no such file" — the kernel's
91
- // report for a binary it cannot load, typically one built for a different
92
- // architecture. "Install what you already installed" would loop the user.
99
+ child.on('error', reportLaunchFailure);
100
+
101
+ function reportLaunchFailure(err) {
102
+ // ENOENT on a file that is demonstrably there, or Windows' UNKNOWN: both are a loader
103
+ // refusing the image rather than a file that is missing.
104
+ if (
105
+ (err.code === 'ENOENT' || err.code === 'UNKNOWN') &&
106
+ binaryPath !== exeName &&
107
+ fs.existsSync(binaryPath)
108
+ ) {
109
+ // The file is right there, yet the loader would not take it — typically a binary
110
+ // built for a different architecture. "Install what you already installed" would
111
+ // loop the user.
93
112
  console.error(
94
113
  `dev-prune: '${binaryPath}' exists but could not be executed.\n\n` +
95
114
  `It may be built for a different architecture than ${process.platform}-${process.arch},\n` +
@@ -116,7 +135,7 @@ child.on('error', (err) => {
116
135
  console.error(`dev-prune: failed to launch ${binaryPath}: ${err.message}`);
117
136
  }
118
137
  process.exit(127);
119
- });
138
+ }
120
139
 
121
140
  // `code` is null when the child was killed by a signal; `code || 0` reported success
122
141
  // in that case. Mirror the shell convention of 128 + signal number instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-prune",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Universal, lockfile-safe workspace pruner. Reclaims disk space from idle Git repositories by deleting only dependency and build directories a lockfile can rebuild.",
5
5
  "bin": {
6
6
  "dev-prune": "./bin/dev-prune.js",
@@ -12,13 +12,13 @@
12
12
  "LICENSE.md"
13
13
  ],
14
14
  "optionalDependencies": {
15
- "dev-prune-darwin-arm64": "1.8.0",
16
- "dev-prune-darwin-x64": "1.8.0",
17
- "dev-prune-linux-arm64": "1.8.0",
18
- "dev-prune-linux-x64": "1.8.0",
19
- "dev-prune-windows-arm64": "1.8.0",
20
- "dev-prune-windows-x64": "1.8.0",
21
- "dev-prune-windows-x86": "1.8.0"
15
+ "dev-prune-darwin-arm64": "1.9.0",
16
+ "dev-prune-darwin-x64": "1.9.0",
17
+ "dev-prune-linux-arm64": "1.9.0",
18
+ "dev-prune-linux-x64": "1.9.0",
19
+ "dev-prune-windows-arm64": "1.9.0",
20
+ "dev-prune-windows-x64": "1.9.0",
21
+ "dev-prune-windows-x86": "1.9.0"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=20"