Fix Duplicate Upgrade Prompt in CLI

i just tried to upgrade our CLI via the entire-upgrade plugin. it shells out to homebrew to perform the upgrade. but what's weird is at the end, the CLI asked me again "do you want to upgrade", but then called homebrew and caused a no-op. i wonder if what's happening is "homebrew info" or whatever the final summary is called is shelling out to entire --version or something, which triggers the upgrade check? see this transcript:
09:37:45 (git) main▲ $ entire upgrade --nightly --yes
Detected Entire CLI (homebrew install):
entire 0.8.43-nightly.202607081321.e009b7501 at /opt/homebrew/bin/entire
git-remote-entire 0.8.43-nightly.202607081321.e009b7501 at /opt/homebrew/bin/git-remote-entire
Latest nightly build is 0.8.43-nightly.202607110651.7cd666280
==> Auto-updating Homebrew...
Adjust how often this is run with $HOMEBREW_AUTO_UPDATE_SECS or disable with
$HOMEBREW_NO_AUTO_UPDATE=1. Hide these hints with $HOMEBREW_NO_ENV_HINTS=1 (see man brew).
==> Auto-updated Homebrew!
==> Updated Homebrew from 20901df0fa to 08a972812c.
Updated 3 taps (entireio/tap, homebrew/core and homebrew/cask).
==> New Formulae
==> Downloading https://formulae.brew.sh/api/formula.jws.json
[... elided ...]
You have 9 outdated formulae and 6 outdated casks installed.
==> Updating Homebrew... Already up-to-date. ==> Would upgrade 1 outdated package entireio/tap/entire@nightly 0.8.43-nightly.202607081321.e009b7501 -> 0.8.43-nightly.202607110651.7cd666280 ==> Do you want to proceed with the upgrade? [y/n] ==> Upgrading 1 outdated package: entireio/tap/entire@nightly 0.8.43-nightly.202607081321.e009b7501 -> 0.8.43-nightly.202607110651.7cd666280 ==> Fetching downloads for: entireio/tap/entire@nightly ✔︎ Cask entire@nightly (0.8.43-nightly.202607110651.7cd666280) Downloaded 20.2MB/ 20.2MB ==> Upgrading entire@nightly 0.8.43-nightly.202607081321.e009b7501 -> 0.8.43-nightly.202607110651.7cd666280 ==> Unlinking Binary '/opt/homebrew/bin/entire' ==> Unlinking Binary '/opt/homebrew/bin/git-remote-entire' ==> Linking Binary 'entire' to '/opt/homebrew/bin/entire' ==> Linking Binary 'git-remote-entire' to '/opt/homebrew/bin/git-remote-entire' ==> Purging files for version 0.8.43-nightly.202607081321.e009b7501 of Cask entire@nightly 🍺 entire@nightly was successfully upgraded! ==> Upgraded 1 outdated package entireio/tap/entire@nightly 0.8.43-nightly.202607081321.e009b7501 -> 0.8.43-nightly.202607110651.7cd666280 Entire CLI upgrade complete (homebrew install): entire 0.8.43-nightly.202607110651.7cd666280 installed to /opt/homebrew/bin/entire git-remote-entire 0.8.43-nightly.202607110651.7cd666280 installed to /opt/homebrew/bin/git-remote-entire
Updating Entire CLI: brew upgrade --yes entire@nightly ✔︎ JSON API packages.arm64_tahoe.jws.json Downloaded 15.2MB/ 15.2MB Warning: Not upgrading entire@nightly, the latest version is already installed Update complete. Re-run entire to use the new version.
Root-caused it — your hypothesis is close but the mechanism is different: nothing shells out to entire --version. It's the parent entire process's own post-command version check, running with its stale compiled-in version.
What happened, step by step:
upgradeisn't a built-in, so the old binary (0.8.43-nightly.20260708...) dispatched to theentire-upgradeplugin (cmd/entire/cli/plugin.go:41). The plugin ran brew and successfully upgraded the binary on disk.- When the plugin exits 0, the parent mirrors Cobra's PersistentPostRun and runs the version check:
versioncheck.CheckAndNotify(ctx, os.Stderr, versioninfo.Version)atplugin.go:52. But that parent process is still the old binary, soversioninfo.Versionis the pre-upgrade version baked in at compile time. - Your 24h check cache (
~/.config/entire/version_check.json) had expired, so it fetched the latest nightly, saw202607081321 < 202607110651→ "outdated" (versioncheck.go:86), and showed the interactive huh "Update available!" prompt. That's the second "do you want to upgrade" you saw (the huh TUI erases itself after selection, which is why it's not in your transcript). - You picked "Update now", which printed
Updating Entire CLI: brew upgrade --yes entire@nightly(autoupdate.go:87) and re-ran brew — a no-op, since the plugin had already installed the new version.
The bug: the post-plugin version check compares the running process's compiled-in version against latest, but the plugin may have just replaced the binary on disk. The check is answering "is this process stale?" when the question that matters is "is the install stale?" — and after entire upgrade, the answer to the first is always yes.
Fix options, in order of my preference:
- Skip
CheckAndNotifywhen the plugin isupgrade(or any official upgrade-ish plugin). One line inMaybeRunPlugin, trivially correct. Slight coupling to the plugin name, but the official allowlist already exists (IsOfficialPlugin). - Re-check the on-disk binary before prompting: before showing the prompt, stat/exec-resolve
os.Executable()isn't enough — you'd have to actually read the new binary's version, which means spawning it. Ironically that's theentire --versionsubprocess you hypothesized. More robust (covers a user runningbrew upgradein another terminal mid-session), but heavier. - Have the plugin bump the cache:
entire-upgradewritesLastCheckTime(and could clearSkippedVersion) intoversion_check.jsonon success. Works, but couples an external binary to the parent's cache format — fragile across versions.
Also worth noting: MaybeAutoUpdate unconditionally offers the brew path even though the fetched latestVersion is what the check just saw — after choosing "Update now" and getting brew's "already installed" warning, we still print "Update complete" (autoupdate.go:92), which is misleading in this case, but that's cosmetic once the root cause is fixed.
Want me to implement the skip-after-upgrade-plugin fix?
Done — committed as 5e12b0cac on inherited-snacking-squirrel (not pushed).
What changed:
cmd/entire/cli/plugin.go—MaybeRunPluginnow skipsversioncheck.CheckAndNotifywhen the plugin that just ran isupgrade(newselfUpdatePluginNameconst), since the parent process's compiled-in version is stale by definition after a self-update. The call goes through a newpostPluginVersionCheckseam var for testability.cmd/entire/cli/plugin_test.go— three new tests: version check fires after a successful non-upgrade plugin, is skipped afterentire-upgradesucceeds, and is skipped after any plugin fails (pre-existing behavior, now pinned).
Verification: all plugin tests pass; mise run fmt && mise run lint clean (0 issues), git diff after lint showed only my two files.
One tradeoff to be aware of: this is name-based coupling — a third-party entire-upgrade on PATH that doesn't upgrade the CLI would also suppress the check. I think that's fine since the name is de facto reserved for the official plugin, but flag it in the PR if you want it noted.