Skip to content

feat(NO-TASK): Add version detection and self-update, and document the CLI as a whole - #64

Merged
aaronware merged 3 commits into
mainfrom
feat/version-detection-and-update
Aug 26, 2026
Merged

feat(NO-TASK): Add version detection and self-update, and document the CLI as a whole#64
aaronware merged 3 commits into
mainfrom
feat/version-detection-and-update

Conversation

@aaronware

Copy link
Copy Markdown
Contributor

What this does

Three things, in three reviewable commits.

feat: version detection and self-update. linchpin version reports what is installed, whether a newer version is published, and how this copy was installed. linchpin update runs the install command for that install method. When a newer version exists, a notice appears on stderr after the command completes.

Update available: 1.1.3 → 1.2.0
  Run: linchpin update

ci: verify npm credentials before publishing. Diagnostics for the failure below.

docs: rewrite the README around the whole CLI. It opened as if wt were the product. It now leads with the command surface and each command's effect classification, and carries the install / update / uninstall walkthroughs it was missing. docs/updating.md covers the mechanism.

Why the release job kept failing

Every release from v1.1.0 to v1.1.3 failed at npm publish:

npm error 404 Not Found - PUT https://registry.npmjs.org/@linchpinagency%2fcli

That reads as if the package does not exist. It does — npm answers an unauthorized write with 404 rather than 403, so the message names the wrong problem. Evidence that this was always the credential:

  • npm holds only 1.1.1, published by a person with npm 11.11.0 / Node 24.14.1 and attestations: null — by hand from a workstation, since CI pins Node 22.12 and passes --provenance.
  • No CI publish has ever succeeded.
  • NPM_TOKEN was minted 2026-02-17, before this package first existed on npm (2026-08-08), and a granular npm token only ever covers packages that existed when it was created.

The token has since been rotated. This PR makes the next failure name itself: a preflight step fails on an empty or non-authenticating token before a build is spent on it, and a publish failure after a passing preflight says that a 404 can then only be a permissions problem. It also writes repository.url in the form npm was auto-correcting on every publish.

v1.1.3 is tagged and released on GitHub but absent from npm. Re-running the publish-npm job on run 32974737883 publishes it from the existing tag — no new release needed. Worth doing before this PR merges, or npm skips from 1.1.1 to 1.2.0.

Design notes worth a reviewer's attention

The notice costs no latency. It is read from a 24-hour cache file, never the network. A stale cache is refreshed by a detached, unref()ed process that outlives the command, marked with an env flag so it cannot spawn a refresh of its own.

stderr only, and never for machine readers. cd "$(linchpin wt switch)" and eval "$(linchpin shell-init)" both consume stdout, so a notice there would be executed. It is suppressed in --json and --quiet, in CI, and when an agent is driving — an agent asks linchpin version --check --json instead of paying tokens for a line it did not request. LINCHPIN_NO_UPDATE_NOTIFIER (or NO_UPDATE_NOTIFIER) turns it off entirely.

Install method is read from the path, resolved through realpathSync, not from npm_config_user_agent — which is only set while npm itself is the parent process, true during npm install and never when a user runs linchpin. Getting this wrong is not cosmetic: handing a pnpm or bun install an npm install -g leaves two copies on the machine and which one answers depends on PATH order.

Two ways to ask, deliberately. version --check always exits 0, so it is safe in a shell prompt or status line. update --check exits 3 when an update is pending, so it can gate a job with nothing to parse.

An unparseable version never reads as newer, or a registry answering with something odd would nag on every invocation with no version that could satisfy it.

Incidental fix

The mode flags were only accepted before a subcommand: linchpin version --json was an unknown-option error while linchpin --json version worked. They are now accepted at any depth — except on the wt passthrough, which must keep forwarding --json to the legacy dispatcher that reads it out of its own argv.

Verification

  • npm run typecheck clean, npm test 113/113 pass (13 new).
  • The suite never reaches the network: the checker runs against a local registry stub, and the shared fixture disables the notifier so no test can be perturbed by a real release.
  • Covered: semver precedence including prereleases, install-method detection for npm global/local, pnpm, bun, yarn, npx and source, corrupt/stale/future-dated caches, an unreachable registry, the exit-3 gate, and that the notice lands on stderr and nowhere else.

🤖 Generated with Claude Code

aaronware and others added 3 commits August 26, 2026 10:37
Two commands and a notifier, so a user finds out a new version exists
without asking and without paying for the check.

`linchpin version` reports what is installed, whether a newer version is
published, and how this copy was installed. `linchpin update` runs the
install command for *that* install method — derived from the path the
process is running from, because handing a pnpm or bun install an
`npm install -g` leaves two copies on the machine and which one answers
depends on PATH order.

The notice costs no latency: it is read from a 24-hour cache file, and a
stale cache is refreshed by a detached process that outlives the command.
It goes to stderr only, so `cd "$(linchpin wt switch)"` and
`eval "$(linchpin shell-init)"` keep working, and it is suppressed in
--json, --quiet, CI, and for agents — an agent asks
`linchpin version --check --json` instead of paying tokens for a line it
did not request.

Two ways to ask, deliberately: `version --check` always exits 0 so it is
safe in a prompt or status line, while `update --check` exits 3 when an
update is pending so it can gate a job with nothing to parse.

Also fixes a pre-existing gap the new commands made visible: the mode
flags were only accepted *before* a subcommand, so `linchpin version
--json` was an unknown-option error while `linchpin --json version`
worked. They are now accepted at any depth, except on the `wt`
passthrough which forwards them to the legacy dispatcher.

Commands receive the Output renderer and the package manifest through
CommandContext, so mode is still decided once per process.

The suite never reaches the network — the checker is exercised against a
local registry stub, and the shared fixture disables the notifier so no
test can be perturbed by a real release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every release from v1.1.0 to v1.1.3 failed at `npm publish` with

    npm error 404 Not Found - PUT https://registry.npmjs.org/@linchpinagency%2fcli

which reads as if the package did not exist. It does exist: npm answers an
*unauthorized* write with 404 rather than 403, so the message names the
wrong problem. 1.1.1 reached npm only because it was published by hand
from a workstation — it carries no provenance attestation, and no CI
publish has ever succeeded.

The cause is the credential: NPM_TOKEN was minted 2026-02-17, before this
package first existed on npm (2026-08-08), and a granular npm token only
ever covers the packages that existed when it was created.

Prove the credential before spending a build on it, and if the publish
still fails, say what a 404 after a passing preflight can only mean. Also
write repository.url in the form npm was auto-correcting on every publish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README opened as if `wt` were the product, so a reader could not tell
that this is one tool carrying every repeatable piece of the WordPress
workflow, with worktrees as one command group among several.

It now leads with the command surface and each command's effect
classification, and carries the three lifecycle walkthroughs it was
missing: install (per package manager, plus the shell wrapper and a source
checkout), update, and uninstall — including what deliberately *stays*
behind, since `.linchpin.json`, hooks, worktrees and symlinks all outlive
the CLI and a teammate still needs them.

docs/updating.md covers the mechanism rather than the walkthrough: how the
check is cached, who is told and why an agent is not, how the install
method is detected, and why `version --check` and `update --check` exit
differently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aaronware
aaronware merged commit bf13560 into main Aug 26, 2026
4 checks passed
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