pm: polish dedupe, prune, pm ls and pm licenses output

Merged
RO
3mo ago
+469-222

Problem

Five rough edges in the package manager command output, reported in #38925. The resolutions and lockfiles are correct in every case; only the output misleads.

  • bun dedupe prints a row like ↳ undici-types 7.24.6 with no arrow or target when a duplicate is dropped outright, which reads like truncated output next to name old → new rows (src/install/dedupe.rs, print_rows)
  • a multi-major downgrade like @types/node 25.9.1 → 22.19.19 is styled exactly like a patch bump
  • bun prune --production on a freshly installed tree warns node_modules/<pkg> is not the version bun.lock expects plus note: run 'bun install' first, claiming a correct tree is stale (src/install/prune.rs, HoistedTree::removable). Excluding dev deps changes which copy the hoist favors, so the hoisted position legitimately holds a different copy than the production view expects, and running bun install changes nothing
  • bun pm licenses prints no command banner and no closing summary, unlike every other pm command
  • summary lines count different things without naming them: dedupe counts lockfile entries, prune counts installed entries, bun pm ls counts tree positions, so the numbers look contradictory across commands

Fix

  • dedupe: a dropped duplicate now prints ↳ undici-types 7.24.6 → (removed); a row whose surviving versions are all a lower major is marked (downgrade) (yellow on a terminal). Same-major downgrades stay unmarked to keep the marker meaningful
  • prune: when pruning with --production/--omit and the hoisted position holds a different version, the copy is kept silently if that version exists in the lockfile under the same name (it is the copy a full install put there). A version the lockfile does not know anywhere is still a stale tree and keeps the existing warning and note. Plain bun prune behavior is unchanged; only the messages changed, never what is kept or removed
  • bun pm licenses: prints the standard bun pm licenses v<version> (<sha>) banner and closes with N packages across M licenses (checked K packages in bun.lock) [elapsed]. The banner prints after all validation exits so error output keeps a clean stdout, and --json output is byte-for-byte unchanged
  • units named: dedupe summaries say checked N packages in bun.lock (matching the existing bun pm licenses empty-case wording), prune says checked N installed packages, bun pm ls says node_modules (N installed). bun install/bun update's Saved bun.lock (N packages) is left alone: the count sits next to the file it describes, and changing it would churn bun install output wholesale
  • docs samples in docs/pm/cli/{dedupe,prune,pm}.mdx and the npm-migration guide updated to match

Verified by updating the existing output assertions in test/cli/install/bun-dedupe.test.ts, bun-prune.test.ts, bun-pm-licenses.test.ts, bun-pm.test.ts and bun-install-lifecycle-scripts.test.ts; all five files pass with the debug build. The previously asserted spurious --production warnings (two prune tests) now assert empty stderr, and one of them gains a stale-tree case proving the warning still fires when the installed version is foreign to the lockfile.

The reporter also mentioned bun outdated drawing a rule between every row; that predates the pm work (reproduces on 1.3.14) and is left out of this PR.

Background

  • The hoisted install tree assigns each package a position in node_modules; when two versions of one package are needed, one wins the top-level spot and the others nest under their dependents. Which one wins depends on which dependency edges exist, so excluding dev dependencies (--production) can flip the winner
  • bun prune compares the tree on disk against the positions the lockfile implies for the requested view; a keep decision on mismatch was already conservative, this PR only changes when it narrates that decision as a problem
  • bun dedupe collapses duplicate versions that a single surviving version can satisfy; a duplicate whose dependents all move elsewhere leaves no target, which is the (removed) case

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install-lifecycle-scripts.test.ts test/cli/install/bun-prune.test.ts test/cli/install/isolated-relink.test.ts