From 4486b6659640102dd542fea007f4c33ac02511ff Mon Sep 17 00:00:00 2001 From: s Date: Tue, 4 Nov 2025 11:06:35 -0500 Subject: feat: add version checking and auto-update functionality --- internal/utils/version.go | 115 +++++++++++++++++++++++++++++++++++++++++ internal/utils/version_test.go | 52 +++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 internal/utils/version.go create mode 100644 internal/utils/version_test.go (limited to 'internal/utils') diff --git a/internal/utils/version.go b/internal/utils/version.go new file mode 100644 index 0000000..00e6cc2 --- /dev/null +++ b/internal/utils/version.go @@ -0,0 +1,115 @@ +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/spf13/cobra" +) + +var Version = "dev" + +func CheckForUpdates(cmd *cobra.Command) error { + if !isTerminal() || Version == "dev" { + return nil + } + + latestVersion, err := getLatestRemoteTag() + if err != nil { + return nil + } + + if latestVersion == "" || latestVersion == Version { + return nil + } + + if isNewerVersion(latestVersion, Version) { + promptAndUpdate(latestVersion) + } + + return nil +} + +func getLatestRemoteTag() (string, error) { + cmd := exec.Command("git", "ls-remote", "--tags", "--refs", "--sort=-v:refname", "git.db.org.ai/dborg") + output, err := cmd.Output() + if err != nil { + return "", err + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) == 0 { + return "", fmt.Errorf("no tags found") + } + + parts := strings.Split(lines[0], "refs/tags/") + if len(parts) < 2 { + return "", fmt.Errorf("invalid tag format") + } + + return strings.TrimSpace(parts[1]), nil +} + +func isNewerVersion(remote, local string) bool { + remote = strings.TrimPrefix(remote, "v") + local = strings.TrimPrefix(local, "v") + + return remote != local && remote > local +} + +func promptAndUpdate(newVersion string) { + fmt.Fprintf(os.Stderr, "\nšŸ”” A new version of dborg is available: %s (current: %s)\n", newVersion, Version) + fmt.Fprintf(os.Stderr, "Would you like to update now? [Y/n]: ") + + var response string + fmt.Scanln(&response) + + response = strings.ToLower(strings.TrimSpace(response)) + if response != "" && response != "y" && response != "yes" { + fmt.Fprintf(os.Stderr, "Update skipped. Run 'go install git.db.org.ai/dborg@latest' to update manually.\n\n") + return + } + + fmt.Fprintf(os.Stderr, "Updating to %s...\n", newVersion) + + installCmd := exec.Command("go", "install", "git.db.org.ai/dborg") + installCmd.Stdout = os.Stderr + installCmd.Stderr = os.Stderr + + if err := installCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update: %v\n", err) + fmt.Fprintf(os.Stderr, "Please update manually: go install git.db.org.ai/dborg@latest\n\n") + return + } + + fmt.Fprintf(os.Stderr, "āœ“ Update successful! Restarting...\n\n") + + restartSelf() +} + +func restartSelf() { + executable, err := os.Executable() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get executable path: %v\n", err) + os.Exit(1) + } + + args := os.Args[1:] + + err = syscall.Exec(executable, append([]string{executable}, args...), os.Environ()) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to restart: %v\n", err) + os.Exit(1) + } +} + +func isTerminal() bool { + fileInfo, err := os.Stdout.Stat() + if err != nil { + return false + } + return (fileInfo.Mode() & os.ModeCharDevice) != 0 +} diff --git a/internal/utils/version_test.go b/internal/utils/version_test.go new file mode 100644 index 0000000..dc1627d --- /dev/null +++ b/internal/utils/version_test.go @@ -0,0 +1,52 @@ +package utils + +import "testing" + +func TestIsNewerVersion(t *testing.T) { + tests := []struct { + name string + remote string + local string + expected bool + }{ + { + name: "newer version available", + remote: "v0.2.0", + local: "v0.1.0", + expected: true, + }, + { + name: "same version", + remote: "v0.1.0", + local: "v0.1.0", + expected: false, + }, + { + name: "local is newer", + remote: "v0.1.0", + local: "v0.2.0", + expected: false, + }, + { + name: "without v prefix", + remote: "0.2.0", + local: "0.1.0", + expected: true, + }, + { + name: "mixed prefix", + remote: "v0.2.0", + local: "0.1.0", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isNewerVersion(tt.remote, tt.local) + if result != tt.expected { + t.Errorf("isNewerVersion(%s, %s) = %v, expected %v", tt.remote, tt.local, result, tt.expected) + } + }) + } +} -- cgit v1.2.3