Skip to content

fix(update): install @latest instead of a range-bound update -g - #170

Merged
ralyodio merged 3 commits into
masterfrom
worktree-fix-update-latest
Aug 30, 2026
Merged

fix(update): install @latest instead of a range-bound update -g#170
ralyodio merged 3 commits into
masterfrom
worktree-fix-update-latest

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The bug

threatcrush update on a machine still running 0.2.0 upgraded it to 0.2.2 and printed ✓ ThreatCrush updated successfully! — while pnpm, in the same output, said (0.11.5 is available).

/home/ettinger/.local/share/pnpm/global/5:
- @profullstack/threatcrush 0.2.0
+ @profullstack/threatcrush 0.2.2 (0.11.5 is available)

Why

The update shelled out to pnpm update -g / npm update -g. Both only move a package within the semver range recorded when it was first installed. A 0.2.0 install is recorded as ^0.2.0, and ^0.x cannot cross a minor — so that box is mathematically incapable of reaching 0.11.x via update -g, no matter how many times it is run. It will report success every time.

The fix

Every upgrade path is now a plain install of an explicit @latest, which ignores both the recorded range and pnpm's global lockfile pin:

before after
npm npm update -g pkg npm i -g pkg@latest
pnpm pnpm update -g pkg pnpm add -g pkg@latest
yarn yarn global upgrade pkg yarn global add pkg@latest
bun bun update -g pkg bun add -g pkg@latest

Alongside that, three things that let the same symptom survive a correct install:

  • Every holder gets updated, not just the detected one. update and remove now run against each package manager that reports a global copy. A second install under another manager was left sitting on PATH, so the user reran update, watched it succeed, and still got the old binary.
  • The success message is now verified. After installing, the CLI compares the registry's latest against what threatcrush --version on PATH actually reports; on a mismatch it names both versions, the shadowing path, and hash -r — instead of an unconditional "✓ updated successfully".
  • install.sh installs @latest too. A bare package name let pnpm's global lockfile (and npm's satisfying tree) hand back the copy already on disk. Its installed_version also now reads pnpm's global root as well as npm's — after a pnpm install npm root -g was empty, which silently disabled the shadowed-install warning on exactly the machines that needed it.

Structure

The package-manager command builders move out of apps/cli/src/index.ts (which runs the program on import and so is untestable) into apps/cli/src/upgrade.ts.

Testing

  • 19 new unit tests in apps/cli/src/__tests__/upgrade.test.ts, including a regression test asserting no upgrade command for any package manager contains update -g or global upgrade.
  • 2 new install.sh assertions covering the @latest spec and the two-root version read.
  • Full CLI suite: 57 passed. Web install-script suite: 8 passed. tsc --noEmit clean, sh -n install.sh clean.
  • Dry-ran the new resolution logic on a real box: correctly picks the npm holder, resolves npm i -g @profullstack/threatcrush@latest, reads registry latest 0.11.5, and stays quiet when PATH is already current.

Note for anyone currently stuck

This fix ships in a new release, so a box running the old update still cannot upgrade itself out of it. That machine needs a one-time manual escape:

pnpm add -g @profullstack/threatcrush@latest && hash -r

After that, threatcrush update works normally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QrdMghdnykcfDVSr7tdKDG

`threatcrush update` ran `pnpm update -g` / `npm update -g`, which only
moves a package within the semver range recorded at first install. A box
that installed 0.2.0 is pinned to `^0.2.0` and can never cross a 0.x
minor, so the update walked 0.2.0 -> 0.2.2 while pnpm printed
"(0.11.5 is available)" in the same output, and reported success.

Every upgrade path is now a plain install of an explicit `@latest`,
which ignores both the recorded range and pnpm's global lockfile pin.

Also:

- Update (and remove) every package manager that holds a global copy,
  not just the first one detected. A second install under another
  manager kept shadowing the new binary on PATH.
- After the install, compare the registry's `latest` against what PATH
  actually resolves to and print the offending path plus `hash -r` when
  they differ, instead of a bare "updated successfully".
- install.sh installs `pkg@latest` too - a bare name let pnpm's global
  lockfile hand back the copy already on disk.
- install.sh reads the installed version from pnpm's global root as well
  as npm's; after a pnpm install `npm root -g` was empty, which silently
  disabled the shadowed-install warning on the machines that needed it.

The package-manager command builders move to apps/cli/src/upgrade.ts
with 19 unit tests, including a regression test that no upgrade command
contains `update -g`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrdMghdnykcfDVSr7tdKDG
@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.

Comment thread apps/cli/src/upgrade.ts Fixed
ralyodio and others added 2 commits August 30, 2026 14:03
`pkgName.replace("/", "%2F")` replaces only the first occurrence
(js/incomplete-sanitization, high). The registry serves a scoped name
unescaped — `https://registry.npmjs.org/@scope/name/latest` returns 200 —
so the encoding was never needed. Verified the lookup still resolves
0.11.5 against the live registry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrdMghdnykcfDVSr7tdKDG
…g 21

The GitHub webhook tests signed with a 64-hex `const SECRET`, which
gitleaks reads as a leaked webhook secret. It was never issued by
anything — the same test stubs it into GITHUB_APP_WEBHOOK_SECRET a line
later, so both halves of the signature check are the fixture — but the
literal reached master directly in 20cc664, and because the job scans
all refs with fetch-depth 0 it has failed every PR opened since.

Both halves of the house remedy:

- The test generates the secret per run with randomBytes, so no literal
  survives in the tree for any scanner to find.
- A fingerprint for 20cc664 in .gitleaksignore, since history keeps its
  copy. Placed at the head of the file: two branches both appending at
  the foot conflict on the second merge.

Verified with the exact CI command (gitleaks 8.21.2, `detect --source .
--redact --verbose --no-banner --exit-code 1`) scoped to remote refs:
456 commits, no leaks, exit 0. Unscoped, a local scan also reads
refs/stash, which CI never checks out — that accounts for the 474 vs 455
commit counts and one finding that exists only in a stash entry.

Webhook tests: 9/9 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrdMghdnykcfDVSr7tdKDG
@ralyodio
ralyodio merged commit 162e247 into master Aug 30, 2026
11 checks passed
@ralyodio
ralyodio deleted the worktree-fix-update-latest branch August 30, 2026 14:18
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.

2 participants