fix(update): PATH-priority installer detection, asdf shims, --all#179
Conversation
🦋 Changeset detectedLatest commit: 78171e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 58 minutes and 7 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis PR makes Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
3446aa3 to
b364003
Compare
wyattjoh
left a comment
There was a problem hiding this comment.
Code Review — PR #179
Reviewed with Opus + Codex second-opinion validation.
Major
M1. brew upgrade clerk now auto-runs on stable; docs disagree (codex confirmed)
packages/cli-core/src/commands/update/index.ts:94-96,117-124,247 runs brew upgrade clerk via runGlobalInstall, but packages/cli-core/src/commands/update/README.md:23-31 still says Homebrew "prints brew upgrade clerk and exits (no auto-install)". Pick a semantic. If auto-upgrade is intended, update the docs and consider a separate confirmation step; running brew upgrade under a silenced spinner hides brew side-effects (dependency upgrades, autoremoves).
M2. npx clerk update / bunx clerk update silently regress (codex partial)
The old code had detectFromUserAgent returning "npm" when npm_config_user_agent=npm/.... New findClerkOnPath() cannot see the npx cache at ~/.npm/_npx/<hash>/... (not on PATH), and the fallback to process.execPath makes ownerOfBinary return null, so the command refuses with "outside any known package manager". Users who used to run npx clerk update now get a misleading install-sh error. Either preserve user-agent detection for this case, or emit an actionable "install globally first" message.
M3. Changeset is patch but PR adds --all and changes behavior
.changeset/clerk-update-path-aware.md bumps patch. A new flag plus the Homebrew behavior change lean minor.
Missed on first pass (codex caught)
- Cache poisoning on partial
--allfailure.writeUpdateCache()runs after the loop inupdate/index.ts:240-256,270even when some targets errored, so a failed run marks the latest version as cached and suppresses future update prompts until staleness. - Non-interactive auto-confirm without
--yes.README.md:77says agent mode requires--yes, butupdate/index.ts:222-223auto-confirms whenever!isHuman(), regardless of--yes.
Minor
process.env.ASDF_DATA_DIR ?? join(...)(installer.ts:167-169,191-196):??treats""as set. Use||or an explicit empty-check.queryBunPackageDiralways returns a path viasafeRealpath(installer.ts:156-159), contradicting thegetInstallerPackageDirsdocstring that says absent PMs are omitted.ownerOfBinary.startsWithon Windows (installer.ts:218-230): realpath case differences on drive letters can cause spurious "outside any known package manager" refusals.resolveTargetsfallback toprocess.execPath(update/index.ts:64-66) pairs with M2 to surface misleading errors for package-runner invocations.hashHint(update/index.ts:146-155) falls through to POSIXhash -ron Windows when$SHELLis unset.--allsummary doesn't distinguish primary-install status; a user whose shell-resolved clerk failed can miss that fact.
Positives
Excellent unit-test coverage for findClerkOnPath, ownerOfBinary, and the asdf helpers. The refuse-rather-than-guess ownership model is a real improvement over the prior silent npm fallback. The bun shim-vs-install-dir diagnosis is correct, and the realpath(mkdtemp(...)) trick to paper over /var -> /private/var is exactly the kind of detail that usually gets missed.
7fb8b88 to
f2ebbf4
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/cli-core/src/lib/installer.ts`:
- Around line 135-140: Replace the hardcoded POSIX path in queryNpmPackageDir()
with the output of `npm root -g` (call Bun.$`npm root -g`.quiet().nothrow(),
check exitCode, safeRealpath the stdout trimmed) so it works on Windows, and
update ownerOfBinary() to perform case-insensitive comparisons on Windows by
normalizing both paths (e.g., path.normalize()/path.resolve() and
.toLowerCase()) before matching when process.platform === 'win32'; ensure
existing null/exitCode handling remains intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0982a67d-4144-4122-91bb-ed5ad4b738c3
📒 Files selected for processing (7)
.changeset/clerk-update-path-aware.mdREADME.mdpackages/cli-core/src/cli-program.tspackages/cli-core/src/commands/update/README.mdpackages/cli-core/src/commands/update/index.tspackages/cli-core/src/lib/installer.test.tspackages/cli-core/src/lib/installer.ts
✅ Files skipped from review due to trivial changes (2)
- README.md
- .changeset/clerk-update-path-aware.md
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/cli-core/src/commands/update/README.md
- packages/cli-core/src/commands/update/index.ts
- Homebrew auto-upgrades on stable channel, docs said otherwise. Update README to reflect the actual behavior. - `npx clerk update` / `bunx clerk update` used to fall back to npm; after PATH-aware detection they hit the "unknown installer" refuse branch. Detect the runner via env vars and print a global-install hint instead of the install.sh message. - Agent mode was auto-confirming whenever stdout was not a TTY, which contradicted the documented `--yes`-required behavior. Refuse in agent mode without `--yes` and print the exact command to run. - Partial `--all` failures still refreshed the update cache; only write the cache when every attempted install succeeded. - Bump changeset from patch to minor (adds `--all`, changes Homebrew behavior). - Minor: `??` -> `||` for `ASDF_DATA_DIR` (empty string now treated as unset), require the bun install dir to exist before claiming ownership of paths under it, normalize case on Windows in `ownerOfBinary`, skip the POSIX `hash -r` hint on Windows, mark the primary target in the `--all` summary.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/cli-core/src/commands/update/index.ts (1)
206-269:⚠️ Potential issue | 🟠 MajorDon’t let primary-target state short-circuit
--all.With
--all, an unknown/Homebrew-canary primary should be skipped, not abort the whole run; likewise, an already-latest running CLI should not prevent updating older secondary installs. As written, both early returns happen beforeotherscan be updated.Possible control-flow fix
- if (compareSemver(latest, currentVersion) <= 0) { + const currentIsLatest = compareSemver(latest, currentVersion) <= 0; + if (currentIsLatest && !options.all) { log.info(`${green("✓")} Already on latest (${currentVersion})`); reportOtherInstalls(others, channel); if (isHuman()) outro("Up to date"); return; } @@ - if (primarySkip) { + if (primarySkip && !options.all) { const runner = detectPackageRunner(); @@ if (isHuman()) outro("Update required manual action"); return; } + if (primarySkip) { + log.warn(`Skipping primary target: ${primarySkip}`); + } @@ - const toUpdate: Target[] = [primary]; + const toUpdate: Target[] = primarySkip ? [] : [primary]; if (options.all) { for (const t of others) { if (whyCantUpdate(t, channel) === null) toUpdate.push(t); } } + + if (toUpdate.length === 0) { + log.warn("No updatable clerk installs found."); + if (isHuman()) outro("Update required manual action"); + return; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli-core/src/commands/update/index.ts` around lines 206 - 269, The current logic returns early when the running primary is already latest or when whyCantUpdate(primary, channel) is truthy, which aborts an --all update; change control flow so --all continues to update eligible items in others: for the compareSemver(latest, currentVersion) <= 0 branch, if options.all do not return — instead log "Already on latest" for primary but continue to build toUpdate from others (skip adding primary); similarly for the primarySkip branch, if options.all do not return — emit the same warnings/info for the primary but do not return, and ensure toUpdate is populated by pushing only targets from others where whyCantUpdate(t, channel) === null; keep existing behavior (warnings + early return) when --all is not set. Use the existing symbols primary, others, options.all, whyCantUpdate, compareSemver, toUpdate and preserve agent/yes/confirm logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/cli-core/src/commands/update/index.ts`:
- Around line 206-269: The current logic returns early when the running primary
is already latest or when whyCantUpdate(primary, channel) is truthy, which
aborts an --all update; change control flow so --all continues to update
eligible items in others: for the compareSemver(latest, currentVersion) <= 0
branch, if options.all do not return — instead log "Already on latest" for
primary but continue to build toUpdate from others (skip adding primary);
similarly for the primarySkip branch, if options.all do not return — emit the
same warnings/info for the primary but do not return, and ensure toUpdate is
populated by pushing only targets from others where whyCantUpdate(t, channel)
=== null; keep existing behavior (warnings + early return) when --all is not
set. Use the existing symbols primary, others, options.all, whyCantUpdate,
compareSemver, toUpdate and preserve agent/yes/confirm logic unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f82cdf74-16ce-49ce-a9b0-c5635d5b3b16
📒 Files selected for processing (4)
.changeset/clerk-update-path-aware.mdpackages/cli-core/src/commands/update/README.mdpackages/cli-core/src/commands/update/index.tspackages/cli-core/src/lib/installer.ts
✅ Files skipped from review due to trivial changes (1)
- .changeset/clerk-update-path-aware.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/cli-core/src/lib/installer.ts
- Homebrew auto-upgrades on stable channel, docs said otherwise. Update README to reflect the actual behavior. - `npx clerk update` / `bunx clerk update` used to fall back to npm; after PATH-aware detection they hit the "unknown installer" refuse branch. Detect the runner via env vars and print a global-install hint instead of the install.sh message. - Agent mode was auto-confirming whenever stdout was not a TTY, which contradicted the documented `--yes`-required behavior. Refuse in agent mode without `--yes` and print the exact command to run. - Partial `--all` failures still refreshed the update cache; only write the cache when every attempted install succeeded. - Bump changeset from patch to minor (adds `--all`, changes Homebrew behavior). - Minor: `??` -> `||` for `ASDF_DATA_DIR` (empty string now treated as unset), require the bun install dir to exist before claiming ownership of paths under it, normalize case on Windows in `ownerOfBinary`, skip the POSIX `hash -r` hint on Windows, mark the primary target in the `--all` summary.
eae52ae to
baf31a2
Compare
wyattjoh
left a comment
There was a problem hiding this comment.
Code review
Ambitious rewrite of clerk update's installer resolution. The PATH-walking and ownership-by-install-dir design is solid and the test coverage for ownerOfBinary, findClerkOnPath, and the asdf helpers is thorough. A few issues stood out: the homebrew branch silently misreports success when the tap lags, --all can no-op when the primary is a refuse-case, and the update-cache gate holds primary success hostage to ancillary failures.
Before this change, `clerk update` called `detectInstaller()` on the running binary's `process.execPath`. Two bugs compounded: 1. Bun detection compared against `bun pm bin -g` (the shim dir `~/.bun/bin`), but the Bun-compiled platform binary lives in `~/.bun/install/global/node_modules/@clerk/cli-<arch>`. The match never succeeded, so detection fell through to npm. 2. The fallback `npm install -g clerk@<new>` returned exit 0 and the spinner reported success — but on machines with asdf-managed node, the update landed in `~/.asdf/shims/` while the user's shell still resolved `clerk` to the (unchanged) `~/.bun/bin/clerk`. The fix splits resolution from execution: - New `findClerkOnPath()` walks `process.env.PATH` (shell-agnostic) and returns realpath'd, deduped, executable `clerk` binaries in PATH order. - New `getInstallerPackageDirs()` returns the dir where each PM stores *packages* (not shims); the bun entry uses `$BUN_INSTALL/install/ global/node_modules` — fixing bug 1. - New `ownerOfBinary()` maps a binary path to its owning installer, returning `null` on no match. The update command treats `null` as refuse-rather-than-guess, preventing bug 2. - The update command targets the *first* `clerk` on PATH (what the user's shell will actually execute), not whatever `process.execPath` reports. Also: - `--all` flag updates every `clerk` install on PATH in one run, skipping Homebrew on non-stable channels and `null`-owned binaries with a per-install warning and summary. - Other installs are reported after every run so shadowing is visible. - Post-update `hash -r` / `rehash` hint based on `$SHELL` (skipped for fish and PowerShell, which don't cache).
asdf shims are bash scripts (not symlinks), so realpath returns the shim path itself and ownerOfBinary can't match it against any PM prefix. The PATH survey marked asdf-installed clerks as "unknown installer" and --all would skip them with a warning. resolveAsdfShim() calls `asdf which <name>` to find the underlying binary, realpaths the result, and feeds it into ownerOfBinary. Non- shim paths pass through unchanged. When asdf is absent or can't resolve, the shim path is returned and ownerOfBinary correctly reports null — preserving the refuse-on-unknown behavior. After a successful install, runs `asdf reshim <plugin>` for every asdf plugin whose binary was updated. Safety net for asdf versions that don't auto-reshim on npm install. Target splits into displayPath (what's on PATH — the shim) and resolvedPath (what ownerOfBinary consults) so users see the familiar shim path in output while install logic operates on the resolved binary.
- Remove dead detectInstaller, detectFromUserAgent, matchPmFromExecPath,
and queryPmPrefix. They were the legacy runtime-based detection path,
superseded by ownerOfBinary + findClerkOnPath. Only installer.test.ts
still imported them.
- Drop the stale "TODO for a contributor" comment in the Strategy B
section header; ownerOfBinary has been implemented.
- Rewrite getInstallerPackageDirs with Promise.all so the parallelism
is obvious at the call site.
- Replace em-dashes with regular punctuation throughout installer.ts,
update/index.ts, and update/README.md.
- Fix formatTarget double-dim: dim("unknown") was being wrapped inside
another dim(...) in the null-owner case; the inner ANSI reset broke
the outer styling. Pass the raw string instead.
- Homebrew auto-upgrades on stable channel, docs said otherwise. Update README to reflect the actual behavior. - `npx clerk update` / `bunx clerk update` used to fall back to npm; after PATH-aware detection they hit the "unknown installer" refuse branch. Detect the runner via env vars and print a global-install hint instead of the install.sh message. - Agent mode was auto-confirming whenever stdout was not a TTY, which contradicted the documented `--yes`-required behavior. Refuse in agent mode without `--yes` and print the exact command to run. - Partial `--all` failures still refreshed the update cache; only write the cache when every attempted install succeeded. - Bump changeset from patch to minor (adds `--all`, changes Homebrew behavior). - Minor: `??` -> `||` for `ASDF_DATA_DIR` (empty string now treated as unset), require the bun install dir to exist before claiming ownership of paths under it, normalize case on Windows in `ownerOfBinary`, skip the POSIX `hash -r` hint on Windows, mark the primary target in the `--all` summary.
`npm prefix -g` + hardcoded `lib/node_modules` gives the wrong path on Windows, where npm stores global packages directly under the prefix (`%AppData%\npm\node_modules`, no `lib` segment). `npm root -g` reports the correct directory on both POSIX and Windows.
- brew upgrade: verify installed version post-install and fail when the tap lags the target; `brew upgrade` ignores the packageSpec pin and exits 0 even when it's a no-op, so the caller was silently left on the old version. - --all: when the primary target is blocked (e.g. Homebrew on canary, unknown owner), promote updatable others to effective primaries instead of refusing the whole run. Only refuse when every target is blocked. Surface the skipped primary in the summary so users can see why `clerk -v` didn't change. - detectPackageRunner: drop the `npm_config_user_agent` UA disjunct and key off npm_execpath only. The UA is set for every npm/bun invocation (e.g. `npm run build` that shells out to clerk), so matching `npm/` misfired on ordinary script use. - ownerOfBinary: strip Win32 extended-length (`\\?\`) and UNC (`\\?\UNC\`) prefixes before comparison so realpath-returned extended paths match plain install-dir prefixes. - hashHint: collapse the dead `powershell.exe` branch that was shadowed by the `process.platform === "win32"` early-return, keeping `$SHELL=/usr/bin/pwsh` (PowerShell on Linux) correctly opted out.
baf31a2 to
78171e3
Compare
Fixes
clerk updatesilently writing to the wrong installer on machines with multipleclerkbinaries on PATH. Replaces runtime-based installer detection with PATH-priority-aware resolution, adds asdf shim support, and introduces an opt-in--allbatch mode.Before / after
Reporter's setup (bun + asdf-managed npm + Homebrew):
Before:
clerk update --channel canaryreported success, butclerk -vstill showed the old version. The update had landed in~/.asdf/shims/clerk(via an npm fallback), not in~/.bun/bin/clerk(the one the shell actually resolves).After: the update writes to
~/.bun/install/global/node_modules/viabun add -g, andclerk -vreflects the new version. A post-run report lists the asdf and Homebrew installs with their owners;--allupdates the asdf install too (viaasdf which→ underlying npm, plusasdf reshim nodejsafter).What the new UX looks like
Default (first-on-PATH only, other installs reported):
--all(batch + per-install summary):install.shstandalone (refuse, don't guess):Fixed
clerkon PATH (what the shell actually executes) and runs the installer that owns it. If no known installer owns the primary target, refuses with reinstall guidance instead of silently writing to a different prefix.detectInstaller()Stage 2b comparedprocess.execPathagainstbun pm bin -g(the shim dir), which never matched the Bun-compiled platform binary under~/.bun/install/global/node_modules. Replaced with path-basedownerOfBinary()that matches the install dir.realpathreturns the shim itself and nothing matches. Now resolved viaasdf which <name>; the underlying binary is owner-matched normally, andasdf reshim <plugin>runs after install as a safety net for asdf versions that don't auto-reshim.Added
--allflag. Updates everyclerkinstall on PATH in one run. Skips Homebrew on non-stable channels andnull-owned binaries with per-install warnings; summary printed at the end.clerkinstalls with their owners so shadowing is visible.hash -r(bash/zsh/sh/dash/ksh),rehash(tcsh/csh), or nothing (fish auto-rehashes, PowerShell has no cache) based on$SHELL.Version managers
realpathchases into<nvm-version>/lib/node_modules, which matches activenpm prefix -g.resolveAsdfShim+ post-installasdf reshim. Honors$ASDF_DATA_DIR. Falls back to "unknown" (refuse) when asdf isn't installed.New exports in
packages/cli-core/src/lib/installer.tsfindClerkOnPath(name?)process.env.PATH, realpaths + dedupes, filters to executable regular files (POSIX X bit / Windows PATHEXT). Shell-agnostic.getInstallerPackageDirs()ownerOfBinary(path, installDirs)null.isAsdfShimPath(path)resolveAsdfShim(path)asdf which-based resolution. Returns input unchanged when asdf is absent or can't resolve.asdfPluginFromPath(path)<asdf>/installs/<plugin>/<ver>/....asdfReshim(plugin)The existing
detectInstaller()is preserved for backward compatibility but no longer used by the update command.Test plan
bun run typecheckpassesbun run lintpasses (oxlint, 0 warnings / 0 errors)bun run testpasses (71/71 files);installer.test.tshas 53 cases including 16 new ones coveringownerOfBinary,findClerkOnPath,isAsdfShimPath,asdfPluginFromPath, andresolveAsdfShimclerk update --channel canarywrites to~/.bun/install/global/node_modulesandclerk -vreflects the new version--allupdates both bun and asdf-npm (viaasdf which) and runsasdf reshim nodejsafter--allskips Homebrew with a channel-mismatch warning when--channel canaryis usedinstall.sh-installed clerk (standalone binary) is refused with reinstall guidancehash -rhint renders on zsh/bash;rehashon tcsh; no hint on fish/pwsh