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