selfupdate.go
1 package update 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "runtime" 8 9 "github.com/Masterminds/semver/v3" 10 "github.com/creativeprojects/go-selfupdate" 11 ) 12 13 const repoOwner = "Kocoro-lab" 14 const repoName = "ShanClaw" 15 16 func CheckForUpdate(currentVersion string) (*selfupdate.Release, bool, error) { 17 // Skip update check for non-semver versions (e.g. "dev") 18 if _, err := semver.NewVersion(currentVersion); err != nil { 19 return nil, false, nil 20 } 21 22 source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{}) 23 if err != nil { 24 return nil, false, err 25 } 26 27 updater, err := selfupdate.NewUpdater(selfupdate.Config{ 28 Source: source, 29 Validator: &selfupdate.ChecksumValidator{UniqueFilename: "checksums.txt"}, 30 }) 31 if err != nil { 32 return nil, false, err 33 } 34 35 release, found, err := updater.DetectLatest( 36 context.Background(), 37 selfupdate.NewRepositorySlug(repoOwner, repoName), 38 ) 39 if err != nil || !found { 40 return nil, false, err 41 } 42 43 if release.LessOrEqual(currentVersion) { 44 return nil, false, nil 45 } 46 47 return release, true, nil 48 } 49 50 func DoUpdate(currentVersion string) (string, error) { 51 // Reject non-semver versions (e.g. "dev") 52 if _, err := semver.NewVersion(currentVersion); err != nil { 53 return currentVersion, fmt.Errorf("cannot update from non-semver version: %s", currentVersion) 54 } 55 56 source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{}) 57 if err != nil { 58 return "", err 59 } 60 61 updater, err := selfupdate.NewUpdater(selfupdate.Config{Source: source}) 62 if err != nil { 63 return "", err 64 } 65 66 release, found, err := updater.DetectLatest( 67 context.Background(), 68 selfupdate.NewRepositorySlug(repoOwner, repoName), 69 ) 70 if err != nil { 71 return "", err 72 } 73 if !found || release.LessOrEqual(currentVersion) { 74 return currentVersion, fmt.Errorf("already up to date (%s)", currentVersion) 75 } 76 77 exe, err := selfupdate.ExecutablePath() 78 if err != nil { 79 return "", fmt.Errorf("find executable: %w", err) 80 } 81 82 if err := updater.UpdateTo(context.Background(), release, exe); err != nil { 83 return "", fmt.Errorf("update failed: %w", err) 84 } 85 86 return release.Version(), nil 87 } 88 89 func PlatformInfo() string { 90 return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) 91 } 92 93 // AutoUpdate performs a background-safe update check + download. 94 // Returns a user-facing message (empty if nothing to report). 95 // Skips if: dev build or cache is fresh. 96 func AutoUpdate(currentVersion, shannonDir string) string { 97 if _, err := semver.NewVersion(currentVersion); err != nil { 98 return "" 99 } 100 101 cachePath := filepath.Join(shannonDir, "update-check.json") 102 cache := NewUpdateCache(cachePath) 103 104 if !cache.ShouldCheck() { 105 return "" 106 } 107 108 release, found, err := CheckForUpdate(currentVersion) 109 if err != nil || !found { 110 // Still record the check to avoid hammering API on errors 111 cache.Record(currentVersion) 112 return "" 113 } 114 115 cache.Record(release.Version()) 116 117 exe, err := selfupdate.ExecutablePath() 118 if err != nil { 119 return fmt.Sprintf("Update available: v%s — run \"shan update\" or download from GitHub", release.Version()) 120 } 121 122 // Auto-download and replace 123 source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{}) 124 if err != nil { 125 return fmt.Sprintf("Update available: v%s — run \"shan update\"", release.Version()) 126 } 127 updater, err := selfupdate.NewUpdater(selfupdate.Config{Source: source}) 128 if err != nil { 129 return fmt.Sprintf("Update available: v%s — run \"shan update\"", release.Version()) 130 } 131 if err := updater.UpdateTo(context.Background(), release, exe); err != nil { 132 return fmt.Sprintf("Update available: v%s (auto-update failed: %v)", release.Version(), err) 133 } 134 135 return fmt.Sprintf("Updated to v%s (restart to use)", release.Version()) 136 }