/ internal / update / selfupdate.go
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  }