@@ -0,0 +1,114 @@
1+package update
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "strings"
10+ "time"
11+)
12+
13+const (
14+ githubAPIURL = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
15+ userAgent = "crush/1.0"
16+)
17+
18+// Default is the default [Client].
19+var Default Client = &github{}
20+
21+// Info contains information about an available update.
22+type Info struct {
23+ Current string
24+ Latest string
25+ URL string
26+}
27+
28+// Available returns true if there's an update available.
29+//
30+// If both current and latest are stable versions, returns true if versions are
31+// different.
32+// If current is a pre-release and latest isn't, returns true.
33+// If latest is a pre-release and current isn't, returns false.
34+func (i Info) Available() bool {
35+ cpr := strings.Contains(i.Current, "-")
36+ lpr := strings.Contains(i.Latest, "-")
37+ // current is pre release
38+ if cpr {
39+ // latest isn't a prerelease
40+ if !lpr {
41+ return true
42+ }
43+ }
44+ if lpr && !cpr {
45+ return false
46+ }
47+ return i.Current != i.Latest
48+}
49+
50+// Check checks if a new version is available.
51+func Check(ctx context.Context, current string, client Client) (Info, error) {
52+ info := Info{
53+ Current: current,
54+ Latest: current,
55+ }
56+
57+ if info.Current == "devel" || info.Current == "unknown" {
58+ return info, nil
59+ }
60+
61+ release, err := client.Latest(ctx)
62+ if err != nil {
63+ return info, fmt.Errorf("failed to fetch latest release: %w", err)
64+ }
65+
66+ info.Latest = strings.TrimPrefix(release.TagName, "v")
67+ info.URL = release.HTMLURL
68+ return info, nil
69+}
70+
71+// Release represents a GitHub release.
72+type Release struct {
73+ TagName string `json:"tag_name"`
74+ HTMLURL string `json:"html_url"`
75+}
76+
77+// Client is a client that can get the latest release.
78+type Client interface {
79+ Latest(ctx context.Context) (*Release, error)
80+}
81+
82+type github struct{}
83+
84+// Latest implements [Client].
85+func (c *github) Latest(ctx context.Context) (*Release, error) {
86+ client := &http.Client{
87+ Timeout: 30 * time.Second,
88+ }
89+
90+ req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
91+ if err != nil {
92+ return nil, err
93+ }
94+ req.Header.Set("User-Agent", userAgent)
95+ req.Header.Set("Accept", "application/vnd.github.v3+json")
96+
97+ resp, err := client.Do(req)
98+ if err != nil {
99+ return nil, err
100+ }
101+ defer resp.Body.Close()
102+
103+ if resp.StatusCode != http.StatusOK {
104+ body, _ := io.ReadAll(resp.Body)
105+ return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
106+ }
107+
108+ var release Release
109+ if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
110+ return nil, err
111+ }
112+
113+ return &release, nil
114+}