216 lines
5.2 KiB
Go
216 lines
5.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// Version is set during build
|
|
Version = "dev"
|
|
|
|
// BuildDate is set during build
|
|
BuildDate = ""
|
|
|
|
// For go install builds, this will be the go.mod version
|
|
versionFromMod = "$GOFLAGS: -ldflags=-X git.mvl.sh/vleeuwenmenno/sshtunnel/cmd.Version=${VERSION}"
|
|
|
|
checkForUpdates bool
|
|
)
|
|
|
|
var versionCmd = &cobra.Command{
|
|
Use: "version",
|
|
Short: "Show sshtunnel version information",
|
|
Long: `Display the current version of sshtunnel and check for updates.`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Display current version
|
|
showVersion()
|
|
|
|
// Check for updates if requested
|
|
if checkForUpdates {
|
|
checkUpdate()
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(versionCmd)
|
|
versionCmd.Flags().BoolVarP(&checkForUpdates, "check-updates", "c", false, "Check for updates")
|
|
|
|
// If version is still "dev", try to read from other sources
|
|
if Version == "dev" {
|
|
// Check if version was injected via go install's version info
|
|
if strings.Contains(versionFromMod, "${VERSION}") {
|
|
// Not replaced, try to read from the installed version file
|
|
Version = readInstalledVersion()
|
|
} else {
|
|
// Extract version from the injected string
|
|
re := regexp.MustCompile(`Version=(.+)$`)
|
|
matches := re.FindStringSubmatch(versionFromMod)
|
|
if len(matches) > 1 {
|
|
Version = matches[1]
|
|
} else {
|
|
// Fall back to reading from installed file
|
|
Version = readInstalledVersion()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func showVersion() {
|
|
fmt.Printf("sshtunnel %s\n", Version)
|
|
if BuildDate != "" {
|
|
fmt.Printf("Build date: %s\n", BuildDate)
|
|
}
|
|
fmt.Printf("Go version: %s\n", runtime.Version())
|
|
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
func readInstalledVersion() string {
|
|
// Check system-installed version file first
|
|
versionFile := "/usr/local/share/sshtunnel/sshtunnel.version"
|
|
if _, err := os.Stat(versionFile); err == nil {
|
|
content, err := os.ReadFile(versionFile)
|
|
if err == nil {
|
|
return strings.TrimSpace(string(content))
|
|
}
|
|
}
|
|
|
|
// Try user's home directory for go install version
|
|
homeDir, err := os.UserHomeDir()
|
|
if err == nil {
|
|
goPath := filepath.Join(homeDir, "go", "pkg", "mod", "git.mvl.sh", "vleeuwenmenno", "sshtunnel@*")
|
|
matches, err := filepath.Glob(goPath)
|
|
if err == nil && len(matches) > 0 {
|
|
// Sort matches to get the latest version
|
|
// Example: .../sshtunnel@v1.0.0 -> extract v1.0.0
|
|
latestMatch := matches[len(matches)-1]
|
|
parts := strings.Split(filepath.Base(latestMatch), "@")
|
|
if len(parts) > 1 {
|
|
return parts[1]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if executable name has version info
|
|
execPath, err := os.Executable()
|
|
if err == nil {
|
|
execName := filepath.Base(execPath)
|
|
if strings.Contains(execName, "sshtunnel-v") {
|
|
parts := strings.Split(execName, "-v")
|
|
if len(parts) > 1 {
|
|
return "v" + parts[1]
|
|
}
|
|
}
|
|
|
|
// Check if it's in GOBIN with a version suffix
|
|
re := regexp.MustCompile(`sshtunnel@(v[0-9]+\.[0-9]+\.[0-9]+)$`)
|
|
matches := re.FindStringSubmatch(execName)
|
|
if len(matches) > 1 {
|
|
return matches[1]
|
|
}
|
|
}
|
|
|
|
// Default version if we couldn't find any version information
|
|
return "dev"
|
|
}
|
|
|
|
func checkUpdate() {
|
|
fmt.Println("\nChecking for updates...")
|
|
client := http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get("https://git.mvl.sh/api/v1/repos/vleeuwenmenno/sshtunnel/tags")
|
|
if err != nil {
|
|
fmt.Printf("Error checking for updates: %s\n", err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Printf("Error reading response: %s\n", err)
|
|
return
|
|
}
|
|
|
|
var tags []struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.Unmarshal(body, &tags); err != nil {
|
|
fmt.Printf("Error parsing response: %s\n", err)
|
|
return
|
|
}
|
|
|
|
// Find the latest version tag
|
|
var latestTag string
|
|
for _, tag := range tags {
|
|
if strings.HasPrefix(tag.Name, "v") && (latestTag == "" || compareVersions(tag.Name, latestTag) > 0) {
|
|
latestTag = tag.Name
|
|
}
|
|
}
|
|
|
|
if latestTag == "" {
|
|
fmt.Println("No version tags found in the repository")
|
|
return
|
|
}
|
|
|
|
// Compare with current version
|
|
if compareVersions(latestTag, Version) > 0 {
|
|
fmt.Printf("A newer version is available: %s (you have %s)\n", latestTag, Version)
|
|
fmt.Println("To update, run: go install git.mvl.sh/vleeuwenmenno/sshtunnel@latest")
|
|
} else {
|
|
fmt.Println("You are running the latest version")
|
|
}
|
|
}
|
|
|
|
// compareVersions compares two semantic version strings (v1.2.3)
|
|
// returns: 1 if v1 > v2
|
|
// -1 if v1 < v2
|
|
// 0 if v1 == v2
|
|
func compareVersions(v1, v2 string) int {
|
|
// Remove 'v' prefix if any
|
|
v1 = strings.TrimPrefix(v1, "v")
|
|
v2 = strings.TrimPrefix(v2, "v")
|
|
|
|
// Split into parts
|
|
parts1 := strings.Split(v1, ".")
|
|
parts2 := strings.Split(v2, ".")
|
|
|
|
// Compare each part
|
|
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
|
// Skip non-numeric parts (like pre-release suffixes)
|
|
num1 := 0
|
|
fmt.Sscanf(parts1[i], "%d", &num1)
|
|
|
|
num2 := 0
|
|
fmt.Sscanf(parts2[i], "%d", &num2)
|
|
|
|
if num1 > num2 {
|
|
return 1
|
|
}
|
|
if num1 < num2 {
|
|
return -1
|
|
}
|
|
}
|
|
|
|
// If we get here, the common parts are equal
|
|
if len(parts1) > len(parts2) {
|
|
return 1
|
|
}
|
|
if len(parts1) < len(parts2) {
|
|
return -1
|
|
}
|
|
|
|
return 0
|
|
} |