Skip to content

feat: detect uv, pnpm and bun tool updates - #44

Merged
stanlyzoolo merged 3 commits into
mainfrom
feat/uv-bun-pnpm-updaters
Jul 27, 2026
Merged

feat: detect uv, pnpm and bun tool updates#44
stanlyzoolo merged 3 commits into
mainfrom
feat/uv-bun-pnpm-updaters

Conversation

@stanlyzoolo

Copy link
Copy Markdown
Owner

What

Adds uv, pnpm and bun to the update-detection chain, requested after the reddit announcement. Chain is now brew → go → cargo → pipx → uv → pnpm → bun → npm.

Along the way it fixes a misdetection that was already live: bun globals resolve into install/global/node_modules/<pkg>/…, so the npm step claimed them and offered npm install -g <pkg> — a duplicate copy under npm's prefix while the bun one keeps shadowing it on PATH.

  • uv<UV_TOOL_DIR>/<pkg>/bin/<bin>uv tool upgrade <pkg>
  • pnpm — globals are cmd-shim shell scripts, not symlinks, so EvalSymlinks resolves them to themselves and the path names nothing. The package comes from the # cmd-shim-target= line Detect reads off the binary (bounded, only under $PNPM_HOME), with the resolved path as the legacy-layout fallback → pnpm add -g <pkg>
  • bun<BUN_INSTALL>/install/global/node_modules/<pkg>bun add -g <pkg>

pnpm and bun go before npm: both layouts carry node_modules segments npm claims on sight. add -g rather than update -g, since update honours the range saved at install time and can refuse the major bump the card is offering.

npmPackage now skips .pnpm/.bin service segments, and the npm step refuses paths through /node_modules/.pnpm/ — offering npm install -g there is the duplicate install again, so the chain degrades to ErrUnknownManager + the update_cmd hint.

Found in review

Three defects in the change itself, each measured before fixing:

  1. Oversized shim parsed truncated. A cut inside the # cmd-shim-target= line shortens the path, and …/node_modules/typescript shortened to …/node_modules/types yields types — a real package on npm, so the chain would have offered a confidently wrong pnpm add -g types. Now reads cap+1 and rejects an overrunning file whole (the getReadme idiom).
  2. Manager roots compared unexpanded against a symlink-resolved binary path, so a relocated ~/.bun, a home on a secondary volume or /home under autofs missed silently. Measured across all five layouts: uv and pnpm shim/store degraded honestly, but bun and legacy pnpm fell back to npm install -g <pkg>. resolveManagerDirs now expands symlinks in every root, and managerDirs absorbed cargoBin/pipxVenvs so the fix is symmetrical — which also takes homeDir() out of detectFromPath, making its "no I/O" contract literal.
  3. The != "" root guard was six hand-copied copies of one convention, one omission away from claiming any relative path (filepath.Rel("", "bin/exa") succeeds). Moved into underDir/segmentUnder.

Not done, deliberately: $CARGO_HOME/$PIPX_HOME are still not consulted. Honouring them changes behaviour for anyone who sets them and belongs in its own commit.

Checks

  • go build / go vet / go test -race ./... (10 packages) / golangci-lint — green
  • Cross-compile GOOS=windows (build + vet) and GOOS=linux — green
  • internal/updater coverage 85.0% → 86.8%; every new function at 100%, the remaining gaps are pre-existing (brewPrefix, cargoCrate, runProbe)
  • Live smoke check against real uv 0.11.32: UV_TOOL_DIR=<tmp> uv tool install cowsayDetect yields uv tool upgrade cowsay. Temporary test and tmp dirs removed afterwards, $HOME untouched
  • Every fix mutation-checked: dropping the dirs threading reproduces Manager = "npm", want "bun"; dropping the symlink expansion, the cap guard, one field in the expansion loop, or either empty-dir guard each fails a specific test
  • docs-sync pass over CLAUDE.md / ARCHITECTURE.md / README

Known limits (documented in README)

  • Windows: uv/pnpm/bun/npm bins are .cmd shims keepkit does not parse — pre-existing npm limitation, inherited
  • A manager's own binary is out of scope (bun upgrade, pnpm self-update); those tools update themselves
  • Detection is path-convention based: a future layout change makes its check miss silently and degrade to the hint, never to a wrong command

🤖 Generated with Claude Code

stanlyzoolo and others added 3 commits July 27, 2026 23:39
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chain is now brew → go → cargo → pipx → uv → pnpm → bun → npm. pnpm
and bun sit before npm because both keep their globals behind
node_modules paths: a bun global resolved to `npm install -g <pkg>`,
which installs a duplicate under npm's prefix while the bun copy keeps
shadowing it on PATH. uv goes next to pipx — the same technique one
ecosystem over.

uv reads <UV_TOOL_DIR>/<pkg>/bin/<bin> and yields `uv tool upgrade
<pkg>`; bun reads <BUN_INSTALL>/install/global/node_modules/<pkg> and
yields `bun add -g <pkg>`. pnpm needs a second signal: its globals are
not symlinks at all but cmd-shim shell scripts, so the package name
comes from the `# cmd-shim-target=` line Detect reads off the binary,
with the resolved path as the legacy-layout fallback. `add -g` rather
than `update -g` for both, since update honours the range saved at
install time and can refuse the major bump the card is offering.

Two npm-side consequences of the pnpm layout: npmPackage skips a
post-node_modules segment starting with a dot (.pnpm is the virtual
store, .bin the shim dir) and keeps scanning, and the npm step refuses
any path through /node_modules/.pnpm/, where offering `npm install -g`
would be the duplicate install again — the chain degrades to
ErrUnknownManager and the update_cmd hint instead.

managerDirs carries all five path-convention roots, cargo and pipx
included, and resolveManagerDirs expands symlinks in each. Detect
matches these roots against a symlink-resolved binary path, so a
relocated ~/.bun, a home on a secondary volume or /home under autofs
missed silently, and bun and legacy pnpm globals then fell through to
npm — the misdetection above, back again. Carrying cargo/pipx there also
takes homeDir() out of detectFromPath, so its "no I/O, no environment"
contract holds literally. The empty-root check moved into
underDir/segmentUnder, where it belongs: filepath.Rel("", "bin/exa")
succeeds, so a relative path reads as living under every disabled root,
and the guard was six hand-copied copies of one convention.

Shim reads reject a file over the 8 KiB cap instead of parsing it
truncated. A cut inside the marker line shortens the path, and
.../node_modules/typescript shortened to .../node_modules/types yields
types, a real package on npm — a confidently wrong `pnpm add -g` where
the rest of the chain degrades honestly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
npmPackage returned the scope itself when nothing followed it, so
.../node_modules/@angular yielded `pnpm add -g @angular` — a wrong
command where the chain owes an honest miss. Reachable in practice: the
pnpm step feeds npmPackage a cmd-shim target, which is file content
nothing validated.

Also correct the README. It claimed a layout no check recognises always
degrades to the update_cmd hint, "never a wrong command". That is not
true and this branch is where it stops being true: pnpm and bun globals
sit behind node_modules paths, so an unresolved root still reads as a
plain npm install and the offer becomes `npm install -g <pkg>`. Say so,
and say which env vars keep it from happening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stanlyzoolo
stanlyzoolo merged commit 328c414 into main Jul 27, 2026
3 checks passed
@stanlyzoolo
stanlyzoolo deleted the feat/uv-bun-pnpm-updaters branch July 27, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant