Skip to content

fix(installer): clear stale global installs and detect a shadowed upgrade - #169

Merged
ralyodio merged 1 commit into
masterfrom
worktree-installer-stale-shadow
Aug 30, 2026
Merged

fix(installer): clear stale global installs and detect a shadowed upgrade#169
ralyodio merged 1 commit into
masterfrom
worktree-installer-stale-shadow

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

Follow-up to #168. That fixed which package manager installs; this fixes what happens on a machine that already had the CLI installed by a different one.

Why

A prior pnpm add -g leaves its own shim on PATH, and it usually sorts ahead of npm's. The installer would write a current build into npm's global root, then report success using whatever threatcrush --version resolved to — the old shim:

✓ ThreatCrush 0.11.3 installed successfully!

…on a machine that just downloaded 0.11.4. Nothing told the user why the upgrade didn't take. Reported on macOS, reproduced on Linux.

There's a second flavour of the same trap: the pnpm global pnpm-lock.yaml pins the resolved version, so pnpm add -g reinstalls the locked release and exits 0 while handing back something older than latest.

Changes

  • remove_stale_global_installs() runs before installing and drops a copy of the package held by any package manager other than the one in use (pnpm, yarn, bun). Every probe is guarded — set -e is on and none of this is worth aborting a working install for. pnpm refusing to run because its global bin dir isn't exported is expected and benign here.
  • warn_if_shadowed() compares the version on PATH against the version actually written to npm's global root, read from the installed package.json so it stays correct offline and on a pinned install. On mismatch it names both versions plus the shadowing path, and suggests hash -r before rm.

Testing

  • shellcheck -s sh clean; sh -n clean.
  • Both new functions unit-tested against a stubbed environment: installed_version returns 0.11.4 for the real package and empty (not a crash) for a missing one; warn_if_shadowed stays silent for equal versions and for an empty expected version, and warns on a mismatch.
  • End-to-end on a real box: seeded a stale pnpm add -g install (which created a second global/5 root alongside global/v11), ran the installer, and watched it print → Removing an older @profullstack/threatcrush installed with pnpm..., install via npm, and land on 0.11.4. Confirmed the pnpm copy was gone afterwards and that an unrelated pnpm global on the same box was left untouched.
  • Pre-commit passed, including the full next build.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H3GDps1fD6ccfo93B3ePy1

…rade

Preferring npm (#168) fixed which package manager installs, but not what
happens on a machine that already had the CLI installed by another one.

A prior `pnpm add -g` leaves its own shim on PATH. The installer would put a
current build in npm's global root, then report success using whatever
`threatcrush --version` resolved to - the old shim. The output reads

  ✓ ThreatCrush 0.11.3 installed successfully!

on a machine that just downloaded 0.11.4, and nothing says why the upgrade
did not take. Reported on macOS, reproduced on Linux.

Two changes:

- remove_stale_global_installs() runs before the install and drops a copy of
  the package held by any package manager other than the one being used
  (pnpm, yarn, bun). Every probe is guarded - `set -e` is on, and none of it
  is worth aborting a working install for. pnpm refusing to run because its
  global bin dir is not exported is expected and benign here.

- warn_if_shadowed() compares the version on PATH against the version
  actually written to npm's global root, read from the installed package.json
  so it stays correct offline. On a mismatch it names both versions and the
  shadowing path, and suggests `hash -r` before `rm`.

Verified end to end: seeded a pnpm global install, ran the installer, watched
it remove the pnpm copy and land on 0.11.4 via npm. Unrelated pnpm globals on
the box were left alone. Both new functions unit-tested for the equal, empty
and mismatched cases; shellcheck -s sh is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GDps1fD6ccfo93B3ePy1
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:96
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:100
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM js-unescaped-html-sink apps/web/src/components/GuideReader.tsx:265
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
LOW secret-generic-credential PRD.md:269
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
LOW sh-remote-script-execution scripts/smoke-test.sh:47
LOW secret-aws-access-key scripts/smoke-test.sh:112

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit aaf0a41 into master Aug 30, 2026
10 of 11 checks passed
@ralyodio
ralyodio deleted the worktree-installer-stale-shadow branch August 30, 2026 13:28
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