Skip to content

Commit 090e90d

Browse files
authored
feat: add kernel upgrade command (#68)
## Summary - Add a new `kernel upgrade` command (with `update` alias) that automatically detects how kernel was installed (brew, pnpm, npm, bun) and runs the appropriate upgrade command - Falls back to manual wget/tar instructions for unknown installation methods - Shows release URL so users can view release notes - Add `--dry-run` flag to preview what would be executed (shows detected method, binary path, and command) - Detects Homebrew upgrade failures and suggests uninstall/untap/reinstall for old tap users - Move `DetectInstallMethod` to `pkg/update` and refactor `SuggestUpgradeCommand` to use it - Export `FetchLatest` and `IsNewerVersion` from `pkg/update` for reuse - Remove obsolete old-tap migration logic (taps now mirrored on both orgs) - Add `upgrade` to auth-exempt commands list (no authentication required) ## Test plan - [x] `go build ./...` passes - [x] `go test ./...` passes - [x] `kernel upgrade --help` displays correct usage information - [x] `kernel update --help` works (alias) - [x] `kernel upgrade --dry-run` shows the detected installation method, binary path, and upgrade command - [ ] Manual testing: run `kernel upgrade` with different installation methods (brew, npm, pnpm, bun) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a user-facing upgrade flow and exposes update utilities for reuse. > > - New `kernel upgrade` command (alias `update`) with `--dry-run`, release notes display, and method-aware execution (brew, pnpm, npm, bun); falls back to manual instructions when unknown > - Detects Homebrew old-tap error and prints uninstall/untap/reinstall guidance > - Wires `upgrade` into root command and marks it auth-exempt > - Refactors update package: export `FetchLatest` and `IsNewerVersion`; add `DetectInstallMethod` and `InstallMethod` enum; update `SuggestUpgradeCommand` to use detection > - Update periodic update check to use exported helpers > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3168d20. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent cfda96e commit 090e90d

File tree

3 files changed

+255
-63
lines changed

3 files changed

+255
-63
lines changed

cmd/root.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func isAuthExempt(cmd *cobra.Command) bool {
9090

9191
// Check if the top-level command is in the exempt list
9292
switch topLevel.Name() {
93-
case "login", "logout", "auth", "help", "completion", "create", "mcp":
93+
case "login", "logout", "auth", "help", "completion", "create", "mcp", "upgrade":
9494
return true
9595
}
9696

@@ -141,6 +141,7 @@ func init() {
141141
rootCmd.AddCommand(extensionsCmd)
142142
rootCmd.AddCommand(createCmd)
143143
rootCmd.AddCommand(mcp.MCPCmd)
144+
rootCmd.AddCommand(upgradeCmd)
144145

145146
rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error {
146147
// running synchronously so we never slow the command

cmd/upgrade.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
"os/exec"
10+
"runtime"
11+
"strings"
12+
"time"
13+
14+
"github.com/kernel/cli/pkg/update"
15+
"github.com/pterm/pterm"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
// UpgradeInput holds the input parameters for the upgrade command.
20+
type UpgradeInput struct {
21+
DryRun bool
22+
}
23+
24+
// UpgradeCmd handles the upgrade command logic, separated from cobra.
25+
type UpgradeCmd struct {
26+
currentVersion string
27+
}
28+
29+
// Run executes the upgrade command logic.
30+
func (u UpgradeCmd) Run(ctx context.Context, in UpgradeInput) error {
31+
// Fetch latest version from GitHub
32+
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
33+
defer cancel()
34+
35+
pterm.Info.Println("Checking for updates...")
36+
37+
latestTag, releaseURL, err := update.FetchLatest(ctx)
38+
if err != nil {
39+
return fmt.Errorf("failed to check for updates: %w", err)
40+
}
41+
42+
// Compare versions
43+
isNewer, err := update.IsNewerVersion(u.currentVersion, latestTag)
44+
if err != nil {
45+
// If version comparison fails (e.g., dev version), still allow upgrade
46+
pterm.Warning.Printf("Could not compare versions (%s vs %s): %v\n", u.currentVersion, latestTag, err)
47+
pterm.Info.Println("Proceeding with upgrade...")
48+
} else if !isNewer {
49+
pterm.Success.Printf("You are already on the latest version (%s)\n", strings.TrimPrefix(u.currentVersion, "v"))
50+
return nil
51+
} else {
52+
pterm.Info.Printf("New version available: %s → %s\n", strings.TrimPrefix(u.currentVersion, "v"), strings.TrimPrefix(latestTag, "v"))
53+
if releaseURL != "" {
54+
pterm.Info.Printf("Release notes: %s\n", releaseURL)
55+
}
56+
}
57+
58+
// Detect installation method
59+
method, binaryPath := update.DetectInstallMethod()
60+
61+
if method == update.InstallMethodUnknown {
62+
printManualUpgradeInstructions(latestTag, binaryPath)
63+
// Return nil since we've provided manual instructions - don't fail scripts
64+
return nil
65+
}
66+
67+
if in.DryRun {
68+
pterm.Info.Printf("Detected installation method: %s\n", method)
69+
pterm.Info.Printf("Binary path: %s\n", binaryPath)
70+
pterm.Info.Printf("Would run: %s\n", getUpgradeCommand(method))
71+
return nil
72+
}
73+
74+
pterm.Info.Printf("Upgrading via %s...\n", method)
75+
stderr, err := executeUpgrade(method)
76+
if err != nil {
77+
// If Homebrew upgrade fails, check if it's due to old tap installation
78+
if method == update.InstallMethodBrew && isOldTapError(stderr) {
79+
pterm.Println()
80+
pterm.Error.Println("Homebrew upgrade failed due to old tap installation.")
81+
pterm.Info.Println("Run these commands to fix:")
82+
pterm.Println()
83+
fmt.Println(" brew uninstall kernel")
84+
fmt.Println(" brew untap onkernel/tap")
85+
fmt.Println(" brew install kernel/tap/kernel")
86+
pterm.Println()
87+
}
88+
return err
89+
}
90+
return nil
91+
}
92+
93+
// isOldTapError checks if the brew error output indicates the user has the old onkernel/tap
94+
// installed and needs to migrate to kernel/tap.
95+
func isOldTapError(stderr string) bool {
96+
// When a user has onkernel/tap/kernel installed and runs `brew upgrade kernel/tap/kernel`,
97+
// Homebrew will suggest: "Please tap it and then try again: brew tap kernel/tap"
98+
return strings.Contains(stderr, "brew tap kernel/tap")
99+
}
100+
101+
// upgradeCommandArgs returns the command and arguments for a given installation method.
102+
// Returns nil if the method is unknown.
103+
func upgradeCommandArgs(method update.InstallMethod) []string {
104+
switch method {
105+
case update.InstallMethodBrew:
106+
return []string{"brew", "upgrade", "kernel/tap/kernel"}
107+
case update.InstallMethodPNPM:
108+
return []string{"pnpm", "add", "-g", "@onkernel/cli@latest"}
109+
case update.InstallMethodNPM:
110+
return []string{"npm", "i", "-g", "@onkernel/cli@latest"}
111+
case update.InstallMethodBun:
112+
return []string{"bun", "add", "-g", "@onkernel/cli@latest"}
113+
default:
114+
return nil
115+
}
116+
}
117+
118+
// getUpgradeCommand returns the command string for display (e.g., dry-run output).
119+
func getUpgradeCommand(method update.InstallMethod) string {
120+
args := upgradeCommandArgs(method)
121+
if args == nil {
122+
return ""
123+
}
124+
return strings.Join(args, " ")
125+
}
126+
127+
// executeUpgrade runs the appropriate upgrade command based on the installation method.
128+
// Returns the captured stderr (for error diagnosis) and any error.
129+
func executeUpgrade(method update.InstallMethod) (stderr string, err error) {
130+
args := upgradeCommandArgs(method)
131+
if args == nil {
132+
return "", fmt.Errorf("unknown installation method")
133+
}
134+
135+
cmd := exec.Command(args[0], args[1:]...)
136+
cmd.Stdout = os.Stdout
137+
cmd.Stdin = os.Stdin
138+
139+
// Capture stderr while also displaying it to the user
140+
var stderrBuf bytes.Buffer
141+
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
142+
143+
err = cmd.Run()
144+
return stderrBuf.String(), err
145+
}
146+
147+
// printManualUpgradeInstructions prints instructions for manually upgrading kernel
148+
func printManualUpgradeInstructions(version, binaryPath string) {
149+
// Normalize version (remove 'v' prefix if present)
150+
version = strings.TrimPrefix(version, "v")
151+
152+
goos := runtime.GOOS
153+
goarch := runtime.GOARCH
154+
155+
downloadURL := fmt.Sprintf(
156+
"https://github.com/kernel/cli/releases/download/v%s/kernel_%s_%s_%s.tar.gz",
157+
version, version, goos, goarch,
158+
)
159+
160+
if binaryPath == "" {
161+
binaryPath = "/usr/local/bin/kernel"
162+
}
163+
164+
pterm.Warning.Println("Could not detect installation method.")
165+
pterm.Info.Println("To upgrade manually, run:")
166+
pterm.Println()
167+
fmt.Printf(" wget %s -O /tmp/kernel.tar.gz\n", downloadURL)
168+
fmt.Printf(" tar -xzf /tmp/kernel.tar.gz -C /tmp\n")
169+
fmt.Printf(" sudo cp /tmp/kernel %q\n", binaryPath)
170+
pterm.Println()
171+
}
172+
173+
var upgradeCmd = &cobra.Command{
174+
Use: "upgrade",
175+
Aliases: []string{"update"},
176+
Short: "Upgrade the Kernel CLI to the latest version",
177+
Long: `Upgrade the Kernel CLI to the latest version.
178+
179+
Supported installation methods:
180+
- Homebrew (brew)
181+
- pnpm
182+
- npm
183+
- bun
184+
185+
If your installation method cannot be detected, manual upgrade instructions will be provided.`,
186+
RunE: runUpgrade,
187+
}
188+
189+
func init() {
190+
upgradeCmd.Flags().Bool("dry-run", false, "Show what would be executed without running")
191+
}
192+
193+
func runUpgrade(cmd *cobra.Command, args []string) error {
194+
dryRun, _ := cmd.Flags().GetBool("dry-run")
195+
196+
u := UpgradeCmd{
197+
currentVersion: metadata.Version,
198+
}
199+
return u.Run(cmd.Context(), UpgradeInput{
200+
DryRun: dryRun,
201+
})
202+
}

0 commit comments

Comments
 (0)