Skip to content

fix: hint CLI update command on cli_upgrade_required errors#1794

Open
gtrrz-victor wants to merge 7 commits into
mainfrom
login-cli-upgrade-hint
Open

fix: hint CLI update command on cli_upgrade_required errors#1794
gtrrz-victor wants to merge 7 commits into
mainfrom
login-cli-upgrade-hint

Conversation

@gtrrz-victor

@gtrrz-victor gtrrz-victor commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 at main.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.PromptAllowed gate + versioncheck.RunUpdate execution 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" .-> Q2
Loading

Decision 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: &lt;cmd&gt;'"]
    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"]
Loading

What the user sees at each leaf

UPDATE+RERUN commands (no TTY / declined / kill switch / no runnable installer):

This Entire CLI version is no longer supported. Update it:

  curl -fsSL https://entire.io/install.sh | bash

Then rerun the command:

  entire login --device

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):

Updating Entire CLI: brew upgrade --yes entire
<installer output streams here>
Update complete. Rerunning the command:

  entire login --device
<original command runs with the new binary; process exits with its code>

UPDATE-FAILED (installer error):

Update failed: <error>
Try again later running:
  brew upgrade --yes entire

MANUAL-RERUN fallback (re-exec spawn failed):

Could not rerun automatically (<error>). Rerun the command:

  entire login --device

STALE-BINARY (rerun child rejected again — installer updated a different file than the binary that ran, e.g. a dev build):

The update installed, but this command still ran an unsupported binary (/tmp/entire-test).
Check that `entire` on your PATH points at the updated install, then rerun:

  entire login --device

This guard is what prevents an infinite update→rerun→update loop.

Implementation notes

  • One shared update flow for both triggers, in cmd/entire/cli/versioncheck/updateflow.go:
    PromptAllowed (single gate: kill switch, post-update-rerun loop guard, runnable
    installer, 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.
  • The version-check trigger passes nil argv to RunUpdate — it fires in
    PersistentPostRun, after the command already succeeded, so re-running would execute
    it 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.go keeps only the trigger-specific pieces:
    detection (IsCLIUpgradeRequired, string match; auth-go exposes no sentinel for
    unrecognised OAuth codes), the decision tree above (OfferCLIUpgradeIfRequired(...) bool — true means it took over the messaging and main.go skips the raw print), the
    Yes/No prompt, and the non-interactive command printout.
  • Re-exec is spawn-and-exit (portable incl. Windows), marked with
    ENTIRE_UPGRADE_RERUN=1 so 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).
  • Rerun command line is reconstructed from os.Args (basename + quoting args with spaces).
  • Known gaps (accepted): SilentError paths bypass the handler (they print curated
    messages, not raw OAuth codes); git-remote-entire is a separate binary and would
    need the same hook if the server ever gates repo-creds exchange.

Testing

  • Unit tests cover every branch above: detection, unhandled passthrough, non-interactive print, Yes (installer + re-exec argv), No, installer failure, re-exec failure, rerun loop guard (prompt and installer suppressed, stale-binary message), argv reconstruction.
  • mise run test:ci green (one unrelated flake, TestIsMetadataDisconnected_NoLocal in strategy, passes 4/4 in isolation).
  • Verified end-to-end against a fake AS returning cli_upgrade_required: non-interactive block, and the rerun-guard message via ENTIRE_UPGRADE_RERUN=1.
  • Refactor commit: single-implementation check (grep "Updating Entire CLI" → one
    non-test hit), all 13 existing TestMaybeAutoUpdate_* pass unchanged; unit +
    integration (436) + Vogon canary (117) green.

🤖 Generated with Claude Code

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
Copilot AI review requested due to automatic review settings July 17, 2026 09:04
@gtrrz-victor
gtrrz-victor requested a review from a team as a code owner July 17, 2026 09:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) in cmd/entire/main.go’s central error-printing path.
  • Introduce WithCLIUpgradeHint helper in cmd/entire/cli/errors.go to detect cli_upgrade_required via string match and append versioncheck.UpdateCommandForCurrentBinary(...).
  • Add unit tests for WithCLIUpgradeHint in cmd/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.

Comment thread cmd/entire/cli/errors_test.go Outdated
Comment thread cmd/entire/cli/errors_test.go Outdated
Comment thread cmd/entire/cli/errors_test.go Outdated
gtrrz-victor and others added 6 commits July 17, 2026 11:27
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants