fix: hint CLI update command on cli_upgrade_required errors#1794
Open
gtrrz-victor wants to merge 7 commits into
Open
fix: hint CLI update command on cli_upgrade_required errors#1794gtrrz-victor wants to merge 7 commits into
gtrrz-victor wants to merge 7 commits into
Conversation
The auth server rejects too-old builds with the cli_upgrade_required OAuth code, but the CLI printed only the raw code with no way forward. Append the install-manager-aware update command (same one the periodic version check advertises) in main.go's central error-print arm, so login, token refresh, and RFC 8693 exchange failures all get the hint without per-command wrapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQN2Y1CVD3F27KT6XX0A6AY
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves user-facing error output by appending an install-manager-aware CLI update command when server-side OAuth flows fail with the cli_upgrade_required error code, avoiding “dead end” errors across login/refresh/token-exchange paths.
Changes:
- Apply
cli.WithCLIUpgradeHint(err)incmd/entire/main.go’s central error-printing path. - Introduce
WithCLIUpgradeHinthelper incmd/entire/cli/errors.goto detectcli_upgrade_requiredvia string match and appendversioncheck.UpdateCommandForCurrentBinary(...). - Add unit tests for
WithCLIUpgradeHintincmd/entire/cli/errors_test.go.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cmd/entire/main.go | Adds centralized upgrade guidance to default error printing so all commands benefit. |
| cmd/entire/cli/errors.go | Adds WithCLIUpgradeHint helper that appends the appropriate update command when cli_upgrade_required is present. |
| cmd/entire/cli/errors_test.go | Adds tests covering hint appending, refresh-flow shaping, pass-through behavior, and nil handling. |
Replace the appended-hint approach with a handler on main.go's error arm: on an interactive terminal, ask Yes/No and run the installer (same runner the version-check prompt uses, honoring ENTIRE_NO_AUTO_UPDATE); otherwise print the update command plus the failed command so the user can copy both. Guidance text: 'This Entire CLI version is no longer supported. Update it, then rerun the command:' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQPEHCWP8C9DDJR8M5XR6CG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQPP0NCWKY9G2ZBY8TE8Z0Z
The wrapped OAuth chain adds nothing the guidance block doesn't say; the handler now replaces the raw print (SilentError-style) instead of following it. Exit code stays 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQPXWZFY96KNZMVB2555ZHN
After the installer succeeds, re-resolve argv[0] (picking up the fresh binary) and spawn the original invocation with the terminal attached, exiting with the child's code. ENTIRE_UPGRADE_RERUN guards the loop where the installer updated a different binary than the running one: the rerun then prints the commands instead of prompting again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQR0JH5CCDRA8V6K1AE42SQ
Telling the user to update again right after a successful update is wrong; the rerun child now names the binary it ran and points at PATH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQRE088AFVQZ2Q64H3EN6FD
The version-check prompt and the cli_upgrade_required handler each carried their own prompt gate and installer-run block, so the two could drift. Move the gate (PromptAllowed) and the accepted-update tail (RunUpdate: installer, messages, optional re-exec with loop guard) into versioncheck/updateflow.go; the triggers now differ only in detection and prompt shape. Version check passes nil argv — it fires after the command already succeeded, so rerunning would execute it twice — while cli_upgrade_required retries the failed command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXQWEBJM04P1607TDM5A9ZEK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Any command hitting a server that requires a newer CLI failed with only the raw OAuth code (
start login: start device auth: cli_upgrade_required) — no way forward for the user. The code can surface from any auth-server OAuth flow (login device/browser, the refresh grant under every authenticated command, RFC 8693 exchanges), so it is handled once atmain.go's central error-print arm, replacing the raw error. Exit code stays 1 on every failure leaf.There are two triggers that offer an update, and they share a single flow (
versioncheck.PromptAllowedgate +versioncheck.RunUpdateexecution tail) — they differ only in detection and prompt shape.Decision tree — cli_upgrade_required trigger (command failed)
flowchart TD ERR["Command fails"] --> Q1{"Error carries<br/>cli_upgrade_required?"} Q1 -- no --> RAW["Print raw error<br/>exit 1"] Q1 -- yes --> Q2{"Post-update rerun?<br/>(ENTIRE_UPGRADE_RERUN set)"} Q2 -- yes --> STALE["Print STALE-BINARY message<br/>exit 1"] Q2 -- no --> Q3{"PromptAllowed?<br/>no ENTIRE_NO_AUTO_UPDATE<br/>+ runnable installer<br/>+ interactive TTY"} Q3 -- no --> HINT["Print UPDATE+RERUN commands<br/>exit 1"] Q3 -- yes --> ASK{"Yes/No prompt:<br/>Update now?"} ASK -- "no / abort" --> HINT ASK -- yes --> INST["Shared RunUpdate (argv = os.Args)<br/>run installer, streaming output"] INST -- fails --> IFAIL["Print UPDATE-FAILED + retry command<br/>exit 1"] INST -- succeeds --> REEXEC["Re-exec original command<br/>with new binary<br/>(ENTIRE_UPGRADE_RERUN=1)"] REEXEC -- "spawn fails" --> RFAIL["Print MANUAL-RERUN fallback<br/>exit 1"] REEXEC -- runs --> CHILD["Exit with child's exit code"] CHILD -. "child hits the error again" .-> Q2Decision tree — version-check trigger (command succeeded)
flowchart TD OK["Command succeeds<br/>(PersistentPostRun)"] --> D1{"Dev build?"} D1 -- yes --> SKIP["No check"] D1 -- no --> D2{"Checked within 24h?"} D2 -- yes --> SKIP D2 -- no --> FETCH["Fetch latest version<br/>(stable or nightly channel)"] FETCH --> D3{"Outdated?"} D3 -- no --> SKIP D3 -- yes --> D4{"Version skipped earlier?<br/>(skip-until-next cache)"} D4 -- yes --> SKIP D4 -- no --> D5{"Runnable installer?"} D5 -- no --> DL["Print notification +<br/>downloads page URL"] D5 -- yes --> D6{"PromptAllowed?<br/>no ENTIRE_NO_AUTO_UPDATE<br/>+ not a post-update rerun<br/>+ interactive TTY"} D6 -- no --> MANUAL["Print notification +<br/>'To update, run: <cmd>'"] D6 -- yes --> ASK{"3-option prompt:<br/>Update now / Skip /<br/>Skip until next version"} ASK -- "Skip / abort" --> NOOP["Nothing<br/>(re-offer after next check)"] ASK -- "Skip until next version" --> CACHE["Cache skipped version<br/>(suppress until a newer release)"] ASK -- "Update now" --> INST["Shared RunUpdate (nil argv)<br/>run installer, streaming output"] INST -- fails --> IFAIL["Print UPDATE-FAILED + retry command"] INST -- succeeds --> DONE["'Update complete. Re-run entire<br/>to use the new version.'<br/>No auto-rerun: the command<br/>already ran — re-exec would<br/>execute it twice"]What the user sees at each leaf
UPDATE+RERUN commands (no TTY / declined / kill switch / no runnable installer):
Yes/No prompt (interactive): title "This Entire CLI version is no longer supported.", description "Update now? (runs
<update command>)" — accessible-form aware.Update + auto-rerun (Yes):
UPDATE-FAILED (installer error):
MANUAL-RERUN fallback (re-exec spawn failed):
STALE-BINARY (rerun child rejected again — installer updated a different file than the binary that ran, e.g. a dev build):
This guard is what prevents an infinite update→rerun→update loop.
Implementation notes
cmd/entire/cli/versioncheck/updateflow.go:PromptAllowed(single gate: kill switch, post-update-rerun loop guard, runnableinstaller, interactive TTY) and
RunUpdate(the accepted-update tail: announce,run installer, failure-with-retry message, then optional re-exec). The version-check
prompt (
MaybeAutoUpdate) and this handler differ only in detection and prompt shape.RunUpdate— it fires inPersistentPostRun, after the command already succeeded, so re-running would executeit twice; it keeps printing "Update complete. Re-run entire to use the new version."
This handler passes
os.Args, so the rerun is a retry of the failed command.cmd/entire/cli/upgrade_required.gokeeps only the trigger-specific pieces:detection (
IsCLIUpgradeRequired, string match; auth-go exposes no sentinel forunrecognised OAuth codes), the decision tree above (
OfferCLIUpgradeIfRequired(...) bool— true means it took over the messaging and main.go skips the raw print), theYes/No prompt, and the non-interactive command printout.
ENTIRE_UPGRADE_RERUN=1so a still-failing rerun suppresses all update prompts(both triggers) instead of looping. The update command stays install-manager-aware
(brew/mise/scoop/curl; downloads URL on Windows+unknown, which also disables the prompt).
os.Args(basename + quoting args with spaces).SilentErrorpaths bypass the handler (they print curatedmessages, not raw OAuth codes);
git-remote-entireis a separate binary and wouldneed the same hook if the server ever gates repo-creds exchange.
Testing
mise run test:cigreen (one unrelated flake,TestIsMetadataDisconnected_NoLocalin strategy, passes 4/4 in isolation).cli_upgrade_required: non-interactive block, and the rerun-guard message viaENTIRE_UPGRADE_RERUN=1.grep "Updating Entire CLI"→ onenon-test hit), all 13 existing
TestMaybeAutoUpdate_*pass unchanged; unit +integration (436) + Vogon canary (117) green.
🤖 Generated with Claude Code