Skip to content

Commit f375eaa

Browse files
Sayan-cursoragent
andauthored
Add urgent upgrade warning when CLI is many versions behind (#163)
## Summary The CLI already shows a friendly "a new release is available" banner once a day via `update.MaybeShowMessage`. This adds a louder variant for users who are very far behind modeled on the Pulumi-style "you are running a very old version and should upgrade as soon as possible" prompt. A version is considered "very old" when either: - the latest release is a full major version ahead, or - the latest release is at least 5 minor versions ahead within the same major. When that condition fires, the banner switches from `pterm.Info` to `pterm.Warning` and uses urgent copy. Otherwise behavior is unchanged. ## Test plan - [x] `go test ./pkg/update/...` passes (added `TestIsVeryOldVersion` covering same-version, one/four/five minor behind, major bump, patch-only, v-prefix, non-semver). - [x] `go build ./...` succeeds. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to update-notification messaging and when the banner is shown (TTY-only), with no impact on core command execution beyond suppressing output in piped/redirected scenarios. > > **Overview** > Adds a new `IsVeryOldVersion` semver check (2+ major versions behind, or 5+ minors behind within the same major) and uses it to **escalate the update banner** from `pterm.Info` to a more urgent `pterm.Warning` message. > > Upgrade messaging is now consistently written to **stderr**, and `MaybeShowMessage` skips printing the banner when stdout is not a TTY (e.g., piped/redirected). A dedicated `TestIsVeryOldVersion` suite covers the new version-threshold logic and error cases. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 26c8729. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Sayan- <1415138+Sayan-@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent af19b8a commit f375eaa

2 files changed

Lines changed: 97 additions & 12 deletions

File tree

pkg/update/check.go

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ func IsNewerVersion(current, latest string) (bool, error) {
7777
return lv.GreaterThan(cv), nil
7878
}
7979

80+
// veryOldMinorGap is the number of minor versions behind (within the same
81+
// major) at which the upgrade prompt escalates from informational to urgent.
82+
const veryOldMinorGap = 5
83+
84+
// IsVeryOldVersion reports whether current is far enough behind latest to
85+
// warrant an urgent upgrade warning: two or more majors behind, or at least
86+
// veryOldMinorGap minor versions behind within the same major. A single
87+
// major bump intentionally does not escalate so v0.x users are not all
88+
// flagged "very old" the day v1.0 ships.
89+
func IsVeryOldVersion(current, latest string) (bool, error) {
90+
c := normalizeSemver(current)
91+
l := normalizeSemver(latest)
92+
if c == "" || l == "" {
93+
return false, errors.New("non-semver version")
94+
}
95+
cv, err := semver.NewVersion(c)
96+
if err != nil {
97+
return false, err
98+
}
99+
lv, err := semver.NewVersion(l)
100+
if err != nil {
101+
return false, err
102+
}
103+
if lv.Major() >= cv.Major()+2 {
104+
return true, nil
105+
}
106+
if lv.Major() == cv.Major() && lv.Minor() >= cv.Minor()+veryOldMinorGap {
107+
return true, nil
108+
}
109+
return false, nil
110+
}
111+
80112
// FetchLatest queries GitHub Releases and returns the latest stable tag and URL.
81113
// It expects that the GitHub API returns releases in descending chronological order
82114
// (newest first), which is standard behavior.
@@ -158,31 +190,38 @@ func isOnOldBrewTap() bool {
158190
return false
159191
}
160192

161-
// printUpgradeMessage prints a concise upgrade banner.
193+
// printUpgradeMessage prints a concise upgrade banner on stderr, escalating
194+
// to an urgent warning when the local version is far behind latest.
162195
func printUpgradeMessage(current, latest, url string) {
163196
cur := strings.TrimPrefix(current, "v")
164197
lat := strings.TrimPrefix(latest, "v")
165-
pterm.Println()
166-
pterm.Info.Printf("A new release of kernel is available: %s → %s\n", cur, lat)
198+
info := pterm.Info.WithWriter(os.Stderr)
199+
warn := pterm.Warning.WithWriter(os.Stderr)
200+
fmt.Fprintln(os.Stderr)
201+
if veryOld, err := IsVeryOldVersion(current, latest); err == nil && veryOld {
202+
warn.Printf("You are running a very old version of kernel (%s) and should upgrade as soon as possible. Latest: %s\n", cur, lat)
203+
} else {
204+
info.Printf("A new release of kernel is available: %s → %s\n", cur, lat)
205+
}
167206
if url != "" {
168-
pterm.Info.Printf("Release notes: %s\n", url)
207+
info.Printf("Release notes: %s\n", url)
169208
}
170209

171210
method, _ := DetectInstallMethod()
172211
if method == InstallMethodBrew && isOnOldBrewTap() {
173-
pterm.Println()
174-
pterm.Warning.Println("You have kernel installed from the old tap (onkernel/tap).")
175-
pterm.Warning.Println("To upgrade, switch to the new tap:")
176-
pterm.Println()
177-
pterm.Println(" brew uninstall kernel")
178-
pterm.Println(" brew install kernel/tap/kernel")
212+
fmt.Fprintln(os.Stderr)
213+
warn.Println("You have kernel installed from the old tap (onkernel/tap).")
214+
warn.Println("To upgrade, switch to the new tap:")
215+
fmt.Fprintln(os.Stderr)
216+
fmt.Fprintln(os.Stderr, " brew uninstall kernel")
217+
fmt.Fprintln(os.Stderr, " brew install kernel/tap/kernel")
179218
return
180219
}
181220

182221
if cmd := SuggestUpgradeCommand(); cmd != "" {
183-
pterm.Info.Printf("To upgrade, run: %s\n", cmd)
222+
info.Printf("To upgrade, run: %s\n", cmd)
184223
} else {
185-
pterm.Info.Println("To upgrade, visit the release page above or use your package manager.")
224+
info.Println("To upgrade, visit the release page above or use your package manager.")
186225
}
187226
}
188227

@@ -200,6 +239,9 @@ func MaybeShowMessage(ctx context.Context, currentVersion string, frequency time
200239
if invokedTrivialCommand() {
201240
return
202241
}
242+
if !stdoutIsTerminal() {
243+
return
244+
}
203245

204246
cachePath := filepath.Join(xdgCacheDir(), cacheRelPath)
205247
cache, _ := loadCache(cachePath)
@@ -415,3 +457,13 @@ func invokedTrivialCommand() bool {
415457
}
416458
return false
417459
}
460+
461+
// stdoutIsTerminal reports whether stdout is a TTY. Used to skip the upgrade
462+
// banner when stdout is piped or redirected.
463+
func stdoutIsTerminal() bool {
464+
fi, err := os.Stdout.Stat()
465+
if err != nil {
466+
return false
467+
}
468+
return (fi.Mode() & os.ModeCharDevice) != 0
469+
}

pkg/update/check_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,36 @@ func TestInstallMethodRulesPathPrecedence(t *testing.T) {
111111
assert.Equal(t, InstallMethodPNPM, detect("/home/user/.local/share/pnpm/kernel"))
112112
assert.Equal(t, InstallMethodUnknown, detect("/usr/local/bin/kernel"))
113113
}
114+
115+
func TestIsVeryOldVersion(t *testing.T) {
116+
tests := []struct {
117+
name string
118+
current string
119+
latest string
120+
want bool
121+
wantErr bool
122+
}{
123+
{"same version", "v0.19.1", "v0.19.1", false, false},
124+
{"one minor behind", "v0.18.0", "v0.19.0", false, false},
125+
{"four minor behind", "v0.15.0", "v0.19.0", false, false},
126+
{"five minor behind escalates", "v0.14.0", "v0.19.0", true, false},
127+
{"many minor behind", "v0.5.0", "v0.19.1", true, false},
128+
{"single major bump does not escalate", "v1.2.3", "v2.0.0", false, false},
129+
{"single major bump from 0.x does not escalate", "v0.19.2", "v1.0.0", false, false},
130+
{"two majors behind escalates", "v1.2.3", "v3.0.0", true, false},
131+
{"patch behind only", "v0.19.0", "v0.19.5", false, false},
132+
{"v prefix tolerated", "0.10.0", "v0.19.0", true, false},
133+
{"non-semver returns error", "dev", "v0.19.0", false, true},
134+
}
135+
for _, tt := range tests {
136+
t.Run(tt.name, func(t *testing.T) {
137+
got, err := IsVeryOldVersion(tt.current, tt.latest)
138+
if tt.wantErr {
139+
assert.Error(t, err)
140+
return
141+
}
142+
assert.NoError(t, err)
143+
assert.Equal(t, tt.want, got)
144+
})
145+
}
146+
}

0 commit comments

Comments
 (0)