From d89efc55d999fde52ef6a9bbf6ae312dfe4bf96b Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sat, 22 Aug 2026 09:33:41 +0200 Subject: [PATCH] feat: add interactive update check on startup When starting kctl-tui without a subcommand, check GitHub Releases for a newer version and prompt the user to update. - Silent skip for dev builds and network errors - Prompt: 'Update now? [y/N]' - On 'y': apply update, then start TUI - On 'N'/Enter: start TUI immediately --- cmd/kctl-tui/main.go | 2 + cmd/kctl-tui/update.go | 115 ++++++++++++++++++++++++++++++++++++----- go.mod | 2 +- 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/cmd/kctl-tui/main.go b/cmd/kctl-tui/main.go index 679e997..a97520c 100644 --- a/cmd/kctl-tui/main.go +++ b/cmd/kctl-tui/main.go @@ -83,6 +83,8 @@ func main() { } } + checkForUpdateInteractive(verbose) + m := newFullModel() p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { diff --git a/cmd/kctl-tui/update.go b/cmd/kctl-tui/update.go index b7d519e..a4f4591 100644 --- a/cmd/kctl-tui/update.go +++ b/cmd/kctl-tui/update.go @@ -4,11 +4,14 @@ package main import ( + "bufio" "context" "fmt" "log" "os" + "strings" + "github.com/Masterminds/semver/v3" "github.com/creativeprojects/go-selfupdate" ) @@ -29,6 +32,103 @@ func runUpdate(verbose bool) error { fmt.Printf("Current version: %s\n", current) fmt.Println("Checking for updates...") + rel, err := detectLatest() + if err != nil { + return fmt.Errorf("failed to check for updates: %w", err) + } + + if rel == nil { + fmt.Println("Already up-to-date.") + return nil + } + + fmt.Printf("Found version %s. Updating...\n", rel.Version()) + + if err := applyUpdate(rel); err != nil { + return fmt.Errorf("update failed: %w", err) + } + + fmt.Printf("Updated from %s to %s\n", current, rel.Version()) + return nil +} + +// checkForUpdateInteractive checks for a new version and prompts the user to update. +// Returns true if an update was applied. +func checkForUpdateInteractive(verbose bool) bool { + if version == "dev" { + return false + } + + if verbose { + selfupdate.SetLogger(&verboseLogger{}) + } + + rel, err := detectLatest() + if err != nil { + // Silently ignore network errors — don't block startup + if verbose { + fmt.Fprintf(os.Stderr, "Update check failed: %v\n", err) + } + return false + } + + if rel == nil { + return false + } + + current, _ := semver.NewVersion(version) + newVersion := rel.Version() + + if current != nil && !rel.GreaterThan(current.String()) { + return false + } + + fmt.Printf("New version %s available (current: %s). Update now? [y/N] ", newVersion, version) + + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(strings.ToLower(answer)) + + if answer != "y" && answer != "yes" { + return false + } + + fmt.Println("Updating...") + if err := applyUpdate(rel); err != nil { + fmt.Fprintf(os.Stderr, "Update failed: %v\n", err) + return false + } + + fmt.Printf("Updated to %s. Starting kctl-tui...\n", newVersion) + return true +} + +func detectLatest() (*selfupdate.Release, error) { + source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to init GitHub source: %w", err) + } + + updater, err := selfupdate.NewUpdater(selfupdate.Config{ + Source: source, + }) + if err != nil { + return nil, fmt.Errorf("failed to create updater: %w", err) + } + + repo := selfupdate.ParseSlug(githubSlug) + rel, found, err := updater.DetectLatest(context.Background(), repo) + if err != nil { + return nil, err + } + if !found { + return nil, nil + } + + return rel, nil +} + +func applyUpdate(rel *selfupdate.Release) error { source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{}) if err != nil { return fmt.Errorf("failed to init GitHub source: %w", err) @@ -41,20 +141,7 @@ func runUpdate(verbose bool) error { return fmt.Errorf("failed to create updater: %w", err) } - repo := selfupdate.ParseSlug(githubSlug) - - rel, err := updater.UpdateSelf(context.Background(), current, repo) - if err != nil { - return fmt.Errorf("update failed: %w", err) - } - - if rel != nil && rel.Version() != current { - fmt.Printf("Updated from %s to %s\n", current, rel.Version()) - } else { - fmt.Println("Already up-to-date.") - } - - return nil + return updater.UpdateTo(context.Background(), rel, "") } type verboseLogger struct{} diff --git a/go.mod b/go.mod index 360dc7d..1abc16a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/skoelle/kctl-tui go 1.25.12 require ( + github.com/Masterminds/semver/v3 v3.5.0 github.com/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbletea v1.3.10 github.com/creativeprojects/go-selfupdate v1.6.0 @@ -12,7 +13,6 @@ require ( require ( code.gitea.io/sdk/gitea v0.23.2 // indirect github.com/42wim/httpsig v1.2.4 // indirect - github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect