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