Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clerk-update-path-aware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Fix `clerk update` silently writing to the wrong installer when multiple `clerk` binaries exist on PATH. The command now walks PATH to identify the binary the user's shell will actually execute, determines which installer owns that specific path (via a new `ownerOfBinary()` check), and runs the corresponding installer. Binaries installed outside any known package manager (e.g. via `install.sh`) are refused with reinstall guidance rather than silently updated via npm. Also fixes bun detection, which previously matched the shim dir (`~/.bun/bin`) instead of the install dir (`~/.bun/install/global/node_modules`) and fell through to the npm fallback. Adds a `--all` flag to update every `clerk` install on PATH in one run, skipping Homebrew on non-stable channels and unknown-owner binaries with a warning. Prints a `hash -r` / `rehash` hint based on `$SHELL` after a successful update.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,12 @@ clerk completion <shell>
clerk update
--channel <tag> Release channel to update to (e.g. latest, canary)
-y, --yes Skip confirmation prompt
--all Update every clerk install found on PATH, not just the first
Examples:
$ clerk update Update to the latest stable release
$ clerk update --channel canary Update to the latest canary release
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,13 +532,15 @@ Tutorial — enable completions for your shell:
.description("Update the Clerk CLI to the latest version")
.option("--channel <tag>", "Release channel to update to (e.g. latest, canary)")
.option("-y, --yes", "Skip confirmation prompt")
.option("--all", "Update every clerk install found on PATH, not just the first")
.setExamples([
{ command: "clerk update", description: "Update to the latest stable release" },
{
command: "clerk update --channel canary",
description: "Update to the latest canary release",
},
{ command: "clerk update --yes", description: "Update without confirmation prompt" },
{ command: "clerk update --all", description: "Update every clerk install on PATH" },
])
.action(update);

Expand Down
51 changes: 34 additions & 17 deletions packages/cli-core/src/commands/update/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,45 @@ clerk update [options]
| ----------------- | --------------------------------------------------------------------------------- |
| `--channel <tag>` | Release channel to update from (default: `latest`; use `canary` for pre-releases) |
| `-y, --yes` | Skip confirmation prompt |
| `--all` | Update every clerk install found on PATH, not just the first one |

## Behavior

1. Detects the installer (npm, bun, pnpm, yarn, or Homebrew) in parallel with the version check
2. Fetches the latest version for the given channel from the npm registry
3. If already up to date, exits cleanly
4. For Homebrew installations, prints `brew upgrade clerk` and exits (no auto-install)
5. Prompts for confirmation (skipped with `--yes` or in non-interactive mode)
6. Runs the detected installer's global install command (e.g. `npm install -g clerk@<version>`, `bun add -g clerk@<version>`)
1. Fetches the latest version for the given channel from the npm registry
2. Walks `PATH` to find every `clerk` binary. For asdf shims (bash scripts, not symlinks), resolves through `asdf which <name>` so the underlying installer is visible. Picks the first one as the **primary target**
3. Determines which installer owns the primary target via `ownerOfBinary()`:
- Known installer (npm/bun/pnpm/yarn) → installs via that PM
- Homebrew → runs `brew upgrade clerk` after confirmation (stable channel only; refuses on `canary` since there is no canary tap). After the brew command succeeds, verifies the installed version matches the npm registry's `latest`; fails loudly if the tap is stale.
- `null` (binary not owned by any recognized installer, e.g. `install.sh`) → refuses and lists reinstall options
4. Prompts for confirmation (skipped with `--yes` or in non-interactive mode)
5. Runs the installer's global install command (e.g. `npm install -g clerk@<version>`, `bun add -g clerk@<version>`)
6. If any updated target was inside an asdf-managed tool, runs `asdf reshim <plugin>` so the shim picks up the new binary (safety net; modern asdf-nodejs auto-reshims)
7. With `--all`, updates every on-PATH `clerk` install whose owner the CLI can drive. If the primary itself is blocked (e.g. Homebrew on `canary`, or `null` owner), the primary is skipped with a warning and the remaining installs still run; the run only refuses when every target is blocked. Failures on individual installs are recorded in the summary but don't short-circuit the rest.
8. After a successful install, prints a shell-specific `hash -r` / `rehash` hint when applicable

## Why PATH-walking matters

A machine can host multiple `clerk` installs (bun + asdf-npm + Homebrew is common). `process.execPath` tells you what is running right now, but the binary the user's shell will resolve next may be a different one (e.g. `~/.bun/bin/clerk` shadowing `~/.asdf/shims/clerk`). To ensure the update actually affects the user's next `clerk` invocation, the command resolves the target from `PATH` order, not from `process.execPath`.

## Version managers (asdf, nvm)

- **nvm**: fully supported without special handling. nvm uses real symlinks, so `realpath` chases them into `<nvm-version>/lib/node_modules/clerk/bin/clerk`, which matches the active `npm root -g`; `ownerOfBinary` returns `"npm"`.
- **asdf**: handled via `resolveAsdfShim()`. asdf shims (`~/.asdf/shims/<name>`) are bash scripts, not symlinks, so `realpath` returns the shim itself. The update command calls `asdf which <name>` to find the underlying binary (e.g. `~/.asdf/installs/nodejs/22.16.0/bin/clerk`), realpaths it into the asdf-managed node's `lib/node_modules`, and treats it as `"npm"` with a post-install `asdf reshim <plugin>`. Honors `$ASDF_DATA_DIR` when set. If `asdf` isn't on PATH the shim path is returned unchanged and ownership falls back to `null`.

## Installer detection

Detection uses a multi-stage algorithm (see `lib/installer.ts`):
Detection uses path-based ownership (see `lib/installer.ts`). For a given binary path:

| Priority | Signal | What it detects |
| -------- | ----------------------------------------------- | ----------------------------------------------------------- |
| 1 | `npm_config_user_agent` env var | PM actively running the CLI (npx, bunx, pnpm dlx, yarn dlx) |
| 2 | `process.execPath` contains `/Cellar/clerk/` | Homebrew (macOS, Linuxbrew) |
| 3 | `process.execPath` matches a PM's global prefix | npm, bun, pnpm, or yarn global install |
| 4 | Fallback | npm |
| Check | Result |
| ---------------------------------------------------------------------------------------------- | ------------------------- |
| Contains `/Cellar/clerk/` | `homebrew` |
| Under `npm root -g` (`<prefix>/lib/node_modules` on POSIX, `<prefix>\node_modules` on Windows) | `npm` |
| Under `<bun install dir>/install/global/node_modules` | `bun` |
| Under `pnpm root -g` | `pnpm` |
| Under `<yarn global dir>/node_modules` | `yarn` |
| Nothing matches | `null` (refuse to update) |

`process.execPath` is the real, symlink-resolved path to the compiled binary. For Homebrew, this resolves through the symlink into the Cellar. For npm, this is the platform binary inside `node_modules/`. Symlinked binaries (e.g. `~/.local/bin/clerk` → Cellar) are correctly attributed to their installer.
When multiple PMs' dirs nest, the longest prefix wins. `null` is the signal to refuse rather than silently install via the wrong installer.

## Channels

Expand All @@ -44,7 +61,7 @@ Detection uses a multi-stage algorithm (see `lib/installer.ts`):
| Stable | `latest` | Production-ready releases (default) |
| Canary | `canary` | Pre-release builds for early adopters |

Set `CLERK_UPDATE_CHANNEL=canary` to make canary the default for all update checks.
Set `CLERK_UPDATE_CHANNEL=canary` to make canary the default for all update checks. Homebrew is updatable only on `latest` (no canary tap).

## npm registry endpoints

Expand All @@ -55,6 +72,6 @@ Set `CLERK_UPDATE_CHANNEL=canary` to make canary the default for all update chec
## Notes

- Supports 5 installers: npm, bun, pnpm, yarn, and Homebrew.
- Homebrew installations are not auto-updated. The command prints `brew upgrade clerk` and exits.
- Binaries installed via `install.sh` (direct GitHub Release download) are owned by no PM; the update command refuses and lists reinstall options instead of silently writing to a different prefix.
- Permission errors (EACCES) suggest retrying with `sudo` using the detected installer's command.
- This command does not perform the update itself in agent/non-interactive mode unless `--yes` is passed.
- This command does not perform the update itself in agent/non-interactive mode unless `--yes` is passed. In agent mode without `--yes`, it prints the command the caller needs to run and exits.
Loading
Loading