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