Skip to content

Add inline auto-update prompt after version notification#997

Merged
gtrrz-victor merged 25 commits intomainfrom
auto-update
Apr 23, 2026
Merged

Add inline auto-update prompt after version notification#997
gtrrz-victor merged 25 commits intomainfrom
auto-update

Conversation

@gtrrz-victor
Copy link
Copy Markdown
Contributor

@gtrrz-victor gtrrz-victor commented Apr 21, 2026

https://entire.io/gh/entireio/cli/trails/e09fcff9bd93

Example

image

On error:

$ env ENTIRE_VERSION_CHECK_URL=http://localhost:8765/release.json ENTIRE_VERSION_CHECK_INSTALL_CMD='brew upgrade --cask entire-error' /tmp/entire-old version

Entire CLI 0.1.0 (unknown)
Go version: go1.26.2
OS/Arch: darwin/arm64

A newer version of Entire CLI is available: v99.0.0 (current: 0.1.0)

Updating Entire CLI: brew upgrade --cask entire-error
Error: Cask 'entire-error' is unavailable: No Cask with this name exists.
Update failed: installer exited: exit status 1
Try again later running:
  brew upgrade --cask entire-error

Flow

flowchart TD
  start(["entire <command>"]) --> dev{"version == 'dev' or empty?"}
  dev -- yes --> skip(["silent exit<br/>(no cache touched)"])
  dev -- no --> cache{"cache age &lt; 24h?"}
  cache -- yes --> skip
  cache -- no --> fetch["fetch GitHub /releases/latest<br/>(2s timeout)"]
  fetch --> bump["cache.LastCheckTime := now()<br/>saveCache()"]
  bump --> fok{fetch succeeded?}
  fok -- no --> done24
  fok -- yes --> outdated{isOutdated?}
  outdated -- no --> done24
  outdated -- yes --> banner["print banner:<br/>'A newer version of Entire CLI is available: vX (current: vY)'"]
  banner --> autoOK{"canAutoInstall()<br/>(Windows + scoop/mise OK;<br/>Windows + unknown blocked)"}
  autoOK -- "false<br/>(Windows + unknown installer)" --> winHint["print download hint:<br/>'To update, download the latest release from:'<br/>'  https://github.com/entireio/cli/releases'"]
  winHint --> done24
  autoOK -- true --> kill{"ENTIRE_NO_AUTO_UPDATE set?"}
  kill -- yes --> hint["print manual hint:<br/>'To update, run:'<br/>'  &lt;installer cmd&gt;'"]
  kill -- no --> tty{"CanPromptInteractively()<br/>&amp;&amp; isTerminalOut(stdout)"}
  tty -- "false<br/>(CI=true, no /dev/tty,<br/>agent subprocess, redirected stdout)" --> hint
  tty -- true --> prompt["huh prompt:<br/>'Install the new version now?'"]
  hint --> done24
  prompt --> ans{user answer}
  ans -- No --> done24
  ans -- Yes --> run["run installer command<br/>(brew / mise / scoop / curl)"]
  run --> res{installer exit code}
  res -- 0 --> ok["print:<br/>'Update complete. Re-run entire to use the new version.'"]
  res -- "non-zero" --> fail["print:<br/>'Update failed: &lt;err&gt;'<br/>'Try again later running:'<br/>'  &lt;installer cmd&gt;'"]
  ok --> done24
  fail --> done24
  done24(["24h cache is fresh<br/>no re-prompt until it expires"])
Loading

Invariants:

  • Every path that reaches the fetch step bumps the 24h cache — declining, a failed fetch, a "not outdated" result, a no-TTY exit, a Windows-unknown exit, and a failed installer all look the same to the cache. The only way to re-prompt sooner is to delete ~/.config/entire/version_check.json or wait 24h.
  • Dev builds (version == "dev") and invocations with a fresh cache short-circuit before ever touching the GitHub API.
  • The interactive prompt has three guardrails in front of it: canAutoInstall() (blocks Windows when no known install manager is detected, since the POSIX curl-pipe-bash fallback would fail from cmd.exe), the ENTIRE_NO_AUTO_UPDATE kill switch, and the combined CanPromptInteractively() && isTerminalOut(stdout) gate. The kill switch and interactive gates both print To update, run: <cmd>; the Windows-unknown path prints a download link instead (no POSIX command is surfaced on Windows).

Summary

  • When the existing daily version check flags an outdated release, the CLI prints the usual notification and then asks "Install the new version now? [Y/n]".
  • Accepting runs whatever command versioncheck.updateCommand(current) resolves to (brew upgrade --cask entire[@nightly], mise upgrade entire, scoop update entire/cli, or the curl-pipe-bash fallback). stdin/stdout/stderr stream through so installer prompts and progress reach the user.
  • Declining or a failed installer both leave the 24h cache in place — the user is not re-prompted for 24h in either case. If the installer command fails, a retry hint ("Try again later running: <installer command>") is printed so the user can run it manually once the upstream issue is fixed, without being nagged every invocation.
  • Guardrails: stdout must be a TTY (both CanPromptInteractively() and isTerminalOut(w) must be true, so redirected output skips the prompt), CI must be empty or false, ENTIRE_NO_AUTO_UPDATE=1 is a kill switch, and on Windows the auto-run is blocked unless scoop or mise was detected (no POSIX curl-pipe-bash executed from cmd.exe).

Test plan

  • mise run check green locally (unit + integration + canary)
  • Unit: TTY gate, CI gate (CI=true blocks, CI=false overrides), kill-switch, user-declines, happy path, installer failure with retry hint, installer-failure keeps 24h cache fresh, kill-switch / no-TTY / CI paths all print manual-update hint, non-terminal writer skips prompt, Windows + unknown installer shows download link without auto-running, Windows + scoop still auto-runs
  • Manual end-to-end on a real outdated brew install (reviewer)
  • Windows scoop path verification (deferred)

Non-goals

  • No silent auto-install mode — prompt is always interactive.
  • No persisted preference / subcommands — kill-switch env is the escape hatch.
  • No self-built binary download; shells out to the native installer.

See docs/architecture/auto-update.md.

Replaces #981 (clean history — single commit on top of main with only the auto-update files).

🤖 Generated with Claude Code


Note

Medium Risk
Introduces a new interactive prompt and executes installer commands via sh -c/cmd /C, which could affect UX or fail in edge environments; guardrails and tests reduce but don’t eliminate runtime risk.

Overview
When the daily version check detects the CLI is outdated, it now follows the existing notification with an interactive Y/N prompt to install the new version, and on confirmation shells out to the resolved installer command (brew/mise/scoop/curl), streaming stdin/stdout/stderr so prompts and progress are visible.

The prompt is skipped silently when non-interactive (no TTY) or when ENTIRE_NO_AUTO_UPDATE is set, and failures are non-blocking (debug-logged or printed as a simple update failure message). Adds unit tests for the auto-update flow and documents the behavior in docs/architecture/auto-update.md.

Reviewed by Cursor Bugbot for commit 5872166. Configure here.

Update Since Original Description

  • Added a redirected-output guard to the auto-update path so the prompt is only shown when prompting is allowed and the command's output writer is terminal-backed.
  • Commands such as entire status > out.txt now skip the interactive confirm and print the manual update command instead of opening a prompt on /dev/tty.
  • Added unit coverage for the non-terminal writer case and adjusted the existing CheckAndNotify test seam so terminal vs redirected-output behavior is exercised explicitly.

Additional Verification

  • mise run lint
  • go test ./cmd/entire/cli/versioncheck

Remaining Follow-Up

  • The Windows fallback installer path still resolves to the POSIX curl -fsSL https://entire.io/install.sh | bash command for unknown install methods, so that path should not be auto-executed on Windows until it has a Windows-safe fallback. Resolved in d20d773: canAutoInstall() now returns false on Windows when the install manager is unknown, skipping the prompt and printing a download-page link instead.

When the existing daily version check detects a newer release, the CLI
prints the usual notification and then asks the user "Install the new
version now? [Y/n]". Accepting runs whatever command
versioncheck.updateCommand(current) resolves to, streaming stdio so the
user sees the installer. Declining skips; the 24h version-check cache
prevents re-prompts within a day.

Guardrails: TTY + empty CI env + ENTIRE_NO_AUTO_UPDATE kill switch.
No subcommands, no settings file, no silent auto-install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f880b34689b1
Copilot AI review requested due to automatic review settings April 21, 2026 15:29
@gtrrz-victor gtrrz-victor marked this pull request as ready for review April 21, 2026 15:32
@gtrrz-victor gtrrz-victor requested a review from a team as a code owner April 21, 2026 15:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an interactive inline auto-update flow to the existing daily version check so that, when an outdated release is detected, the CLI can optionally run the appropriate installer command (brew/mise/scoop/curl) immediately.

Changes:

  • Invoke a new MaybeAutoUpdate prompt after printing the “newer version available” notification.
  • Add the auto-update implementation that prompts the user and shells out to the resolved installer command with stdio passthrough.
  • Add unit tests and architecture documentation for the new behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/architecture/auto-update.md Documents the new post-notification auto-update prompt and its guardrails.
cmd/entire/cli/versioncheck/versioncheck.go Calls MaybeAutoUpdate after printing the outdated-version notification; minor comment tweaks.
cmd/entire/cli/versioncheck/autoupdate.go Implements interactive confirm prompt + installer subprocess execution.
cmd/entire/cli/versioncheck/autoupdate_test.go Adds unit tests covering guardrails, decline path, success, and installer failure messaging.
cmd/entire/cli/versioncheck/versioncheck_test.go Minor refactor in TestUpdateCommand (path constant reuse).

Comment thread cmd/entire/cli/versioncheck/autoupdate.go Outdated
Comment thread cmd/entire/cli/versioncheck/versioncheck.go
Comment thread cmd/entire/cli/versioncheck/autoupdate.go
Comment thread cmd/entire/cli/versioncheck/autoupdate.go Outdated
gtrrz-victor and others added 2 commits April 22, 2026 11:27
Pre-select the affirmative so Enter accepts, matching the (Y/n) UX
described in docs/architecture/auto-update.md. Previously the zero
value of the bound bool made "No" the default, contradicting the
prompt and surprising users who expected the implied accept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 7b8020d9f4cb
Replaces the ad-hoc stdoutIsTerminal seam with the shared
interactive.CanPromptInteractively() helper, which opens /dev/tty
directly. This handles three review items with a single change:

- Cobra redirects via cmd.SetOut(&bytes.Buffer{}) no longer matter;
  the TTY check is independent of the writer fd.
- Test binaries running from a real terminal skip the prompt via
  ENTIRE_TEST_TTY=0 set in setupCheckAndNotifyTest.
- Agent subprocesses that inherit the user's stdout but have no
  controlling terminal for stdin hit the /dev/tty open failure and
  are skipped; CI runners land on the same path.

Also drops the redundant os.Getenv("CI") check — the /dev/tty probe
already covers it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4c7de4acdbd6
@gtrrz-victor
Copy link
Copy Markdown
Contributor Author

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5872166. Configure here.

Comment thread cmd/entire/cli/versioncheck/autoupdate.go
gtrrz-victor and others added 6 commits April 22, 2026 16:49
Self-hosted runners and some Docker configurations leave /dev/tty
attached, so the prior /dev/tty-only probe would let huh forms run
in CI and hang the pipeline. Gate on CI in addition to /dev/tty;
ENTIRE_TEST_TTY still overrides both so existing tests that exercise
the interactive path keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 096757b44493
Remove the design-doc file from this PR as requested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 23fdeb653d1e
CI guardrail in CanPromptInteractively short-circuits to false when CI=1,
which broke TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite in
GitHub Actions — the pty is real but the env check wins. Override with
ENTIRE_TEST_TTY=1 in RunCommandInteractive so interactive prompts work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: a1da08f67a46
Move hasTTY from strategy package to interactive package (its
documented home) and replace all interactive.CanPromptInteractively
callers with interactive.HasTTY. One check path for agent-subprocess
and /dev/tty detection across cli, strategy, and versioncheck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1f83680e3b77
Keep the consolidated body (agent-subprocess + /dev/tty checks) under
the original name so call sites and tests read the same as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: df1ddfe6b261
pfleidi
pfleidi previously approved these changes Apr 22, 2026
Self-hosted runners and some Docker CI configs expose /dev/tty, so the
existing TTY probe alone would let the auto-update huh prompt fire and
hang the pipeline. Add CI=<non-empty> as a central guardrail in
CanPromptInteractively so every interactive caller (auto-update, setup,
login, etc.) stays silent on CI.

Entire-Checkpoint: e1608c493997
Previously the 24h cache was bumped before the installer ran, so if
`brew upgrade` / `mise upgrade` / `scoop update` failed (network loss
mid-install, auth, etc.), the user had to wait a full day for the
auto-update prompt to re-arm.

MaybeAutoUpdate now reports installer failure back to CheckAndNotify,
which zeros LastCheckTime so the next CLI invocation re-fetches and
re-prompts. Fetch-failure behavior (cache still bumped to avoid
hammering GitHub during outages) is unchanged.

Entire-Checkpoint: 47637d280ce6
# Conflicts:
#	cmd/entire/cli/integration_test/testenv.go
#	cmd/entire/cli/interactive/interactive.go
#	cmd/entire/cli/interactive/interactive_test.go
#	cmd/entire/cli/strategy/manual_commit_hooks.go
#	cmd/entire/cli/strategy/preparecommitmsg_bench_test.go
#	e2e/testutil/repo.go
Revert the cache-invalidation-on-installer-failure branch and keep the
24h window uniform for every terminal state:

- User declines → silent for 24h (unchanged).
- User accepts and installer succeeds → no re-prompt until the next
  outdated release is detected 24h later (unchanged).
- User accepts and installer fails → print the failure plus a manual
  retry hint ("Try again later with: <installer command>") and stay
  silent for 24h. Avoids re-prompting every invocation while an upstream
  issue (network, auth, repo outage) is still in place; the user has
  the exact command to retry manually when ready.

Drops the now-unused installerFailed return value from MaybeAutoUpdate
and the corresponding cache-reset path in CheckAndNotify. Matches the
simpler "every failure path keeps the 24h cache fresh" invariant (only
a fetch failure leaves the cache fresh as before, same as accept/decline
paths).

Entire-Checkpoint: a3cbe08bf582
The new CI=* guardrail in CanPromptInteractively short-circuits the
interactive gate when the runner sets CI=true, even for subprocesses
that have a real pty attached. This broke resume_interactive_test on
GitHub Actions: the spawned CLI inherited CI=true, returned false from
CanPromptInteractively, and bailed with "cannot prompt to overwrite
local session logs in non-interactive mode".

Set ENTIRE_TEST_TTY=1 in the pty test helper so the subprocess stays
interactive regardless of the runner's CI env. Matches the existing
pattern established in 1d1e012.

Entire-Checkpoint: bbb8fed6c7ce
Manual testing of the auto-update prompt requires hitting the real
GitHub API, which rate-limits unauthenticated clients at 60 req/h per
IP. Iterating on the prompt UX (scenarios × retries) blows through that
quickly, especially when the 24h cache is cleared between attempts.

Introduce two env-var overrides:
  - ENTIRE_VERSION_CHECK_URL          → overrides the /releases/latest endpoint
  - ENTIRE_VERSION_CHECK_RELEASES_URL → overrides the /releases list endpoint

Point them at any mock HTTP server that returns a GitHubRelease JSON
payload. Tests continue to override the vars directly; production
callers are unaffected (env unset → real GitHub URLs).

Entire-Checkpoint: b05ec25035ac
Banner drops the "Run '<cmd>' to update." line — the interactive prompt
that immediately follows already offers to run the installer, so the
extra sentence is redundant noise. On non-interactive environments (CI,
agent subprocess) the banner still shows what's new; the user can infer
the upgrade command from their install manager.

Installer-failure hint becomes "Try again later running:\n  <cmd>" — the
command now sits on its own indented line, which is easier to copy when
the installer string is long (e.g. the curl fallback).

Entire-Checkpoint: 4384cb5889d6
The URL override env vars were added to make local manual testing
easier without hammering the GitHub API. They're scaffolding — not
something end users should rely on — so keep them out of the shipped
binary. Tests still override the package-level vars directly.

Entire-Checkpoint: bf252e83f2e8
When ENTIRE_NO_AUTO_UPDATE is set or CanPromptInteractively returns
false (CI, agent subprocess, no /dev/tty), MaybeAutoUpdate used to
silently return after the banner — leaving the user with no indication
of how to upgrade manually. Now the resolved installer command is
printed:

  To update entire run:
    brew upgrade --cask entire

This restores the info that was removed from the banner itself (where
it was redundant, because the interactive prompt offers to run it).
Callers with the prompt available still see only the prompt; the hint
is only for paths that skip it.

Entire-Checkpoint: 99cd58ca1c60
main is currently broken — PR #999 (`Add entire activity`) merged with
an unresolved reference to `isTerminalWriter` in activity_cmd.go, which
doesn't exist in the cli package. The sibling call sites
(status_style.go, search_cmd.go, explain.go) correctly use the exported
interactive.IsTerminalWriter helper. Apply the same qualification here.

Entire-Checkpoint: 4cb94a44f29b
"To update entire run: <cmd>" read ambiguously — "entire" could be
parsed as an adverb ("run the update entirely"). The new wording is
grammatically clean and parallels the installer-failure hint
("Try again later running: <cmd>"), so the two non-happy-path
messages now read as a pair.

Entire-Checkpoint: 29f6ae5b8238
Reviewer flag: `os.Getenv("CI") != ""` also blocks when a developer sets
CI=false to override an inherited value — a common sandbox/devcontainer
workaround. Match the widely used `is-ci` convention: any non-"false"
value blocks, "false" doesn't.

Inlined the two-line check (no helper) and added a targeted test pair:
CI=true blocks, CI=false doesn't. The CI=false assertion relies on
ENTIRE_TEST_TTY=1 to stand in for a real /dev/tty in the host.

Entire-Checkpoint: ae4dc0e79f92
Entire-Checkpoint: 81adb7a7c7f2
The fallback installer command is POSIX (`curl -fsSL ... | bash`), so
auto-running it on Windows without a detected install manager would
either fail (cmd.exe doesn't know curl-pipe-bash) or look confusing
(partial execution, stray bash spawn via WSL, etc.).

Introduce `canAutoInstall()` which returns false on Windows unless the
binary is under a scoop/mise install prefix. When false, MaybeAutoUpdate
skips the prompt and points the user at the releases page instead of
printing the unusable POSIX command.

A `goos` test seam lets non-Windows hosts exercise both the blocked and
permitted Windows paths. New tests:
- WindowsUnknownInstallerNoAutoRun: verifies no installer call, download
  hint shown, POSIX command not surfaced.
- WindowsScoopStillAutoRuns: verifies scoop detection on Windows still
  takes the interactive path.

Entire-Checkpoint: f6e622ab0da2
@gtrrz-victor gtrrz-victor enabled auto-merge April 23, 2026 14:50
@gtrrz-victor gtrrz-victor merged commit ebbe1d8 into main Apr 23, 2026
9 checks passed
@gtrrz-victor gtrrz-victor deleted the auto-update branch April 23, 2026 15:02
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.

4 participants