feat: add self-update command

Add 'kctl-tui update' subcommand that checks GitHub Releases for
new versions and replaces the running binary.

Uses github.com/creativeprojects/go-selfupdate for safe binary
replacement with automatic OS/arch detection.

Supports --verbose flag for detailed update progress logging.
This commit is contained in:
2026-08-22 09:25:28 +02:00
parent b9439e428a
commit d743422d28
4 changed files with 172 additions and 29 deletions
+8
View File
@@ -70,6 +70,12 @@ func main() {
os.Exit(1)
}
return
case "update":
if err := runUpdate(verbose); err != nil {
fmt.Fprintln(os.Stderr, "kctl-tui update error:", err)
os.Exit(1)
}
return
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
printUsage()
@@ -92,6 +98,7 @@ https://github.com/skoelle/kctl-tui
Usage:
kctl-tui [flags] start the TUI (full navigation mode)
kctl-tui doctor check tools, config and connections
kctl-tui update update to the latest release
kctl-tui config check validate ~/.kctl-tui/config.yaml
kctl-tui panel [options] control pane (called internally by tmux)
@@ -103,6 +110,7 @@ Flags:
Examples:
kctl-tui # start the TUI
kctl-tui doctor # verify everything is installed
kctl-tui update # update to the latest version
kctl-tui --verbose 2>debug.log # log commands to a file
kctl-tui config check # validate config
`)
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/creativeprojects/go-selfupdate"
)
const githubSlug = "skoelle/kctl-tui"
func runUpdate(verbose bool) error {
if verbose {
selfupdate.SetLogger(&verboseLogger{})
}
current := version
if current == "dev" {
fmt.Fprintln(os.Stderr, "WARNING: running dev build — cannot compare versions")
fmt.Println("Skipping version check. Build from a tagged release to enable self-update.")
return nil
}
fmt.Printf("Current version: %s\n", current)
fmt.Println("Checking for updates...")
source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{})
if err != nil {
return fmt.Errorf("failed to init GitHub source: %w", err)
}
updater, err := selfupdate.NewUpdater(selfupdate.Config{
Source: source,
})
if err != nil {
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
}
type verboseLogger struct{}
func (l *verboseLogger) Print(v ...any) {
fmt.Fprint(os.Stderr, v...)
}
func (l *verboseLogger) Printf(format string, v ...any) {
fmt.Fprintf(os.Stderr, format, v...)
}
func init() {
log.SetOutput(os.Stderr)
}