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 }