-
Notifications
You must be signed in to change notification settings - Fork 332
Add inline auto-update prompt after version notification #997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
a6f62a8
Add inline "update now?" prompt after version notification
gtrrz-victor ce3acf9
Default confirm prompt to Yes
gtrrz-victor 5872166
Gate auto-update prompt on /dev/tty, not stdout
gtrrz-victor b7d13a4
Add CI guardrail to CanPromptInteractively
gtrrz-victor 55c7f89
Drop docs/architecture/auto-update.md from PR
gtrrz-victor dfa4f39
Merge branch 'main' into auto-update
gtrrz-victor 1d1e012
Set ENTIRE_TEST_TTY=1 in pty-based integration tests
gtrrz-victor 7c3d8a7
Consolidate TTY gating on interactive.HasTTY
gtrrz-victor f67d983
Rename interactive.HasTTY back to CanPromptInteractively
gtrrz-victor fc3d0e3
Skip interactive prompts when CI env var is set
gtrrz-victor f2e0063
Invalidate version-check cache when installer fails
gtrrz-victor bd23fc5
Merge remote-tracking branch 'origin/main' into auto-update
gtrrz-victor 879a3cf
Simplify auto-update decline/failure UX
gtrrz-victor f666a09
Force ENTIRE_TEST_TTY=1 in pty-based integration tests
gtrrz-victor 649bd22
Allow overriding version-check URLs via env vars for local testing
gtrrz-victor 7fbc348
Tighten auto-update banner and failure-hint wording
gtrrz-victor 69db6fa
Drop ENTIRE_VERSION_CHECK_URL env overrides
gtrrz-victor 99b5315
Merge branch 'main' into auto-update
gtrrz-victor eb78adf
Print manual-update hint when prompt can't be shown
gtrrz-victor 176ca2d
Fix build: qualify isTerminalWriter in activity_cmd.go
gtrrz-victor 59918d0
Reword manual-update hint to "To update, run:"
gtrrz-victor eb9b0df
Treat CI=false as "not CI" for prompt gating
gtrrz-victor c8d4abf
Skip auto-update prompts on redirected output
gtrrz-victor d20d773
Skip auto-run on Windows when install manager is unknown
gtrrz-victor 28c05ec
Merge branch 'main' into auto-update
gtrrz-victor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package versioncheck | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/exec" | ||
| "runtime" | ||
|
|
||
| "github.com/charmbracelet/huh" | ||
|
|
||
| "github.com/entireio/cli/cmd/entire/cli/interactive" | ||
| "github.com/entireio/cli/cmd/entire/cli/logging" | ||
| ) | ||
|
|
||
| // envKillSwitch disables the interactive update prompt regardless of TTY. | ||
| const envKillSwitch = "ENTIRE_NO_AUTO_UPDATE" | ||
|
|
||
| // Test seams. | ||
| var ( | ||
| runInstaller = realRunInstaller | ||
| confirmUpdate = realConfirmUpdate | ||
| ) | ||
|
|
||
| // MaybeAutoUpdate offers an interactive upgrade after the standard | ||
| // "version available" notification has been printed. Silent on every | ||
| // failure path — it must never interrupt the CLI. | ||
| func MaybeAutoUpdate(ctx context.Context, w io.Writer, currentVersion string) { | ||
| if os.Getenv(envKillSwitch) != "" { | ||
| return | ||
| } | ||
| if !interactive.CanPromptInteractively() { | ||
| return | ||
| } | ||
|
|
||
| confirmed, err := confirmUpdate() | ||
| if err != nil { | ||
| logging.Debug(ctx, "auto-update: prompt failed", "error", err.Error()) | ||
| return | ||
| } | ||
| if !confirmed { | ||
| return | ||
| } | ||
|
|
||
| cmdStr := updateCommand(currentVersion) | ||
| fmt.Fprintf(w, "\nUpdating Entire CLI: %s\n", cmdStr) | ||
| if err := runInstaller(ctx, cmdStr); err != nil { | ||
| fmt.Fprintf(w, "Update failed: %v\n", err) | ||
| return | ||
| } | ||
| fmt.Fprintln(w, "Update complete. Re-run entire to use the new version.") | ||
| } | ||
|
|
||
| func realConfirmUpdate() (bool, error) { | ||
| // Pre-select "Yes" so pressing Enter accepts — matches the (Y/n) UX. | ||
| confirmed := true | ||
| form := huh.NewForm( | ||
| huh.NewGroup( | ||
| huh.NewConfirm(). | ||
| Title("Install the new version now?"). | ||
| Affirmative("Yes"). | ||
| Negative("No"). | ||
| Value(&confirmed), | ||
| ), | ||
|
gtrrz-victor marked this conversation as resolved.
|
||
| ).WithTheme(huh.ThemeDracula()) | ||
| if os.Getenv("ACCESSIBLE") != "" { | ||
| form = form.WithAccessible(true) | ||
| } | ||
| if err := form.Run(); err != nil { | ||
| if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, huh.ErrTimeout) { | ||
| return false, nil | ||
| } | ||
| return false, fmt.Errorf("confirm form: %w", err) | ||
| } | ||
| return confirmed, nil | ||
| } | ||
|
|
||
| // realRunInstaller shells out to the installer command, streaming stdin/stdout/stderr | ||
| // so password prompts and progress output reach the user. | ||
| func realRunInstaller(ctx context.Context, cmdStr string) error { | ||
| var c *exec.Cmd | ||
| if runtime.GOOS == "windows" { | ||
| c = exec.CommandContext(ctx, "cmd", "/C", cmdStr) | ||
| } else { | ||
| c = exec.CommandContext(ctx, "sh", "-c", cmdStr) | ||
| } | ||
| c.Stdin = os.Stdin | ||
| c.Stdout = os.Stdout | ||
| c.Stderr = os.Stderr | ||
| if err := c.Run(); err != nil { | ||
| return fmt.Errorf("installer exited: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| package versioncheck | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "errors" | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| // autoUpdateFixture wires the test seams for MaybeAutoUpdate. | ||
| type autoUpdateFixture struct { | ||
| installCalls int | ||
| installErr error | ||
| lastCommand string | ||
| confirmValue bool | ||
| confirmErr error | ||
| } | ||
|
|
||
| func newAutoUpdateFixture(t *testing.T) *autoUpdateFixture { | ||
| t.Helper() | ||
| t.Setenv("HOME", t.TempDir()) | ||
| t.Setenv(envKillSwitch, "") | ||
| // Force interactive mode on by default; individual tests can opt out. | ||
| t.Setenv("ENTIRE_TEST_TTY", "1") | ||
|
|
||
| f := &autoUpdateFixture{confirmValue: true} | ||
|
|
||
| origRun := runInstaller | ||
| runInstaller = func(_ context.Context, cmd string) error { | ||
| f.installCalls++ | ||
| f.lastCommand = cmd | ||
| return f.installErr | ||
| } | ||
| origConfirm := confirmUpdate | ||
| confirmUpdate = func() (bool, error) { return f.confirmValue, f.confirmErr } | ||
|
|
||
| t.Cleanup(func() { | ||
| runInstaller = origRun | ||
| confirmUpdate = origConfirm | ||
| }) | ||
| return f | ||
| } | ||
|
|
||
| // useBrewExecutable points the install-manager detector at a brew cellar path. | ||
| func useBrewExecutable(t *testing.T) { | ||
| t.Helper() | ||
| orig := executablePath | ||
| executablePath = func() (string, error) { | ||
| return "/opt/homebrew/Cellar/entire/1.0.0/bin/entire", nil | ||
| } | ||
| t.Cleanup(func() { executablePath = orig }) | ||
| } | ||
|
|
||
| func TestMaybeAutoUpdate_KillSwitch(t *testing.T) { | ||
| f := newAutoUpdateFixture(t) | ||
| useBrewExecutable(t) | ||
| t.Setenv(envKillSwitch, "1") | ||
|
|
||
| var buf bytes.Buffer | ||
| MaybeAutoUpdate(context.Background(), &buf, "1.0.0") | ||
|
|
||
| if f.installCalls != 0 { | ||
| t.Errorf("installer called with kill-switch set") | ||
| } | ||
| } | ||
|
|
||
| func TestMaybeAutoUpdate_NoTTY(t *testing.T) { | ||
| f := newAutoUpdateFixture(t) | ||
| useBrewExecutable(t) | ||
| t.Setenv("ENTIRE_TEST_TTY", "0") | ||
|
|
||
| var buf bytes.Buffer | ||
| MaybeAutoUpdate(context.Background(), &buf, "1.0.0") | ||
|
|
||
| if f.installCalls != 0 { | ||
| t.Errorf("installer called without TTY") | ||
| } | ||
| } | ||
|
|
||
| func TestMaybeAutoUpdate_UserDeclines(t *testing.T) { | ||
| f := newAutoUpdateFixture(t) | ||
| useBrewExecutable(t) | ||
| f.confirmValue = false | ||
|
|
||
| var buf bytes.Buffer | ||
| MaybeAutoUpdate(context.Background(), &buf, "1.0.0") | ||
|
|
||
| if f.installCalls != 0 { | ||
| t.Errorf("installer called after user declined") | ||
| } | ||
| } | ||
|
|
||
| func TestMaybeAutoUpdate_HappyPath(t *testing.T) { | ||
| f := newAutoUpdateFixture(t) | ||
| useBrewExecutable(t) | ||
|
|
||
| var buf bytes.Buffer | ||
| MaybeAutoUpdate(context.Background(), &buf, "1.0.0") | ||
|
|
||
| if f.installCalls != 1 { | ||
| t.Fatalf("installer called %d times, want 1", f.installCalls) | ||
| } | ||
| if f.lastCommand != "brew upgrade --cask entire" { | ||
| t.Errorf("installer got %q, want brew upgrade --cask entire", f.lastCommand) | ||
| } | ||
| if !strings.Contains(buf.String(), "Update complete") { | ||
| t.Errorf("missing success message: %q", buf.String()) | ||
| } | ||
| } | ||
|
|
||
| func TestMaybeAutoUpdate_InstallerFailurePrintedToUser(t *testing.T) { | ||
| f := newAutoUpdateFixture(t) | ||
| useBrewExecutable(t) | ||
| f.installErr = errors.New("boom") | ||
|
|
||
| var buf bytes.Buffer | ||
| MaybeAutoUpdate(context.Background(), &buf, "1.0.0") | ||
|
|
||
| if f.installCalls != 1 { | ||
| t.Fatalf("installer called %d times, want 1", f.installCalls) | ||
| } | ||
| if !strings.Contains(buf.String(), "Update failed") { | ||
| t.Errorf("missing failure message: %q", buf.String()) | ||
| } | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Auto-Update | ||
|
|
||
| After the Entire CLI's daily version check detects a newer release, the | ||
| standard notification is followed by an interactive Y/N prompt to run the | ||
| installer. | ||
|
|
||
| ## UX | ||
|
|
||
| ``` | ||
| A newer version of Entire CLI is available: v1.2.3 (current: v1.0.0) | ||
| Run 'brew upgrade --cask entire' to update. | ||
|
|
||
| ? Install the new version now? (Y/n) | ||
| ``` | ||
|
|
||
| - Declining simply skips the upgrade. The 24-hour version-check cache means | ||
| the prompt will not reappear until the next day. | ||
| - The installer command is whatever `versioncheck.updateCommand(current)` | ||
| returns — `brew upgrade --cask ...`, `mise upgrade entire`, | ||
| `scoop update entire/cli`, or the curl-pipe-bash fallback — including the | ||
| `--channel nightly` variant for nightly builds. | ||
| - stdin, stdout and stderr are wired through so the user sees installer | ||
| output and can answer any password prompt. | ||
|
|
||
| ## Guardrails | ||
|
|
||
| The prompt is skipped silently when any of the following holds: | ||
|
|
||
| - stdout is not a terminal. | ||
| - `CI` environment variable is set. | ||
| - `ENTIRE_NO_AUTO_UPDATE` environment variable is set (kill switch). | ||
|
|
||
| In those cases the user still sees the existing notification line pointing | ||
| to the installer command. | ||
|
|
||
| ## Not in scope | ||
|
|
||
| - No "silent auto-install" mode — the prompt is always interactive. | ||
| - No persisted preference — the kill-switch env is the escape hatch. | ||
| - No dedicated `entire auto-update` / `entire update` subcommands; the | ||
| notification + prompt replaces them. |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.