Skip to content

fix(installer): prefer npm over pnpm for the global install - #168

Merged
ralyodio merged 1 commit into
masterfrom
worktree-installer-prefer-npm
Aug 30, 2026
Merged

fix(installer): prefer npm over pnpm for the global install#168
ralyodio merged 1 commit into
masterfrom
worktree-installer-prefer-npm

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

detect_pm in apps/web/public/install.sh picked pnpm whenever it was on PATH. It now picks npm first, with pnpm → yarn → bun as fallbacks for machines without npm.

 detect_pm() {
-  if command_exists pnpm; then echo "pnpm"
+  if command_exists npm; then echo "npm"
+  elif command_exists pnpm; then echo "pnpm"
   elif command_exists yarn; then echo "yarn"
   elif command_exists bun; then echo "bun"
-  elif command_exists npm; then echo "npm"
   else echo ""; fi
 }

Why

curl -fsSL https://threatcrush.com/install.sh | sh was taking the least predictable global-install path available on the box. Two real failures, both pnpm's, neither anything to do with threatcrush:

ERR_PNPM_UNEXPECTED_VIRTUAL_STORE  Unexpected virtual store location
[ERROR] The configured global bin directory ".../pnpm/bin" is not in PATH
  • Versioned global dir. pnpm 9/10 use ~/.local/share/pnpm/global/5, pnpm 11 uses global/v11, and pnpm 10 refuses to reuse a global/5 tree that pnpm 9 laid out because it moved the virtual store. A box with more than one pnpm has them fighting over the same tree — and that is easy to end up with, because a corepack pnpm shim resolves a different version per directory from each repo's packageManager field. (This very repo pins pnpm@10.33.0, while the same shim serves 11.18.0 from $HOME.)
  • Global bin dir not on PATH. pnpm aborts add -g until its own bin dir is exported. A non-interactive sh from curl | sh sources no rc file, so the installer cannot guarantee that.
  • Silent version pinning. The global pnpm-lock.yaml pins the resolved version, so pnpm add -g can reinstall the locked release and exit 0 while handing back something older than latest. Reproduced: pnpm view reported 0.11.4, pnpm add -g installed 0.11.3, no warning.

npm ships with Node and its global install behaves the same everywhere, which is what an unattended installer wants. The mise bootstrap path is unaffected — it installs Node, which brings npm.

Testing

  • sh -n apps/web/public/install.sh — syntax clean.
  • detect_pm exercised against stubbed command_exists for four cases: real box → npm; npm absent → pnpm; only bun → bun; nothing → "".
  • Confirmed detect_pm exists in exactly one file, and that the deployed threatcrush.com/install.sh is byte-identical to repo HEAD, so this file is the single source.
  • Pre-commit passed, including the full next build.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H3GDps1fD6ccfo93B3ePy1

detect_pm picked pnpm whenever it was on PATH, so `curl | sh` took the
least predictable global-install path available on the machine.

Two real failures on a dev box, both from pnpm rather than threatcrush:

  ERR_PNPM_UNEXPECTED_VIRTUAL_STORE  Unexpected virtual store location
  [ERROR] The configured global bin directory ".../pnpm/bin" is not in PATH

The first is pnpm's versioned global dir - 9 and 10 use `global/5`, 11 uses
`global/v11` - so a box with more than one pnpm (a corepack shim resolves a
different version per directory) has them fighting over the same tree. The
second is pnpm refusing `add -g` until its own bin dir is exported, which a
non-interactive `sh` reading no rc file cannot guarantee.

There is also a silent variant: the global pnpm-lock.yaml pins the resolved
version, so `pnpm add -g` reinstalls the locked release and reports success
while handing back something older than `latest`.

npm ships with Node and its global install behaves the same everywhere, so
it is the right default for an unattended installer. pnpm, yarn and bun stay
as fallbacks in that order for machines without npm.

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 b20d3cb into master Aug 30, 2026
10 of 11 checks passed
@ralyodio
ralyodio deleted the worktree-installer-prefer-npm branch August 30, 2026 13:04
ralyodio added a commit that referenced this pull request Aug 30, 2026
…rade (#169)

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.


Claude-Session: https://claude.ai/code/session_01H3GDps1fD6ccfo93B3ePy1

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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