1package cli
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "os"
8 "os/exec"
9 "regexp"
10 "runtime"
11 "strings"
12
13 "github.com/floatpane/matcha/internal/httpclient"
14)
15
16var v1RCRegex = regexp.MustCompile(`^v?1\.0\.0-rc\d+$`)
17
18// RunUpgradeV1 handles `matcha upgrade-v1` and upgrades a pre-v1 install (or
19// release candidate) to the v1 release.
20func RunUpgradeV1(args []string) error {
21 _ = args
22
23 fmt.Println("Checking for v1 release...")
24
25 rel, tag, err := fetchV1Release()
26 if err != nil {
27 return err
28 }
29
30 fmt.Printf("Target version: v%s\n", tag)
31
32 switch runtime.GOOS {
33 case goosDarwin:
34 if tryHomebrewV1Upgrade(true) {
35 return nil
36 }
37 case goosLinux:
38 if trySnapV1Refresh() {
39 return nil
40 }
41 if tryHomebrewV1Upgrade(false) {
42 return nil
43 }
44 }
45
46 // Windows or fallbacks: download the binary directly.
47 osName := runtime.GOOS
48 arch := runtime.GOARCH
49 assetName, assetURL, err := FindAsset(rel, osName, arch)
50 if err != nil {
51 return err
52 }
53 return UpgradeBinaryFromAsset(assetURL, assetName, "v"+tag, "matcha upgrade-v1")
54}
55
56func fetchV1Release() (*Release, string, error) {
57 client := httpclient.NewWithRedirectCap(httpclient.UpdateCheckTimeout, 5)
58
59 const apiTag = "https://api.github.com/repos/floatpane/matcha/releases/tags/v1.0.0"
60 resp, err := client.Get(apiTag) //nolint:noctx
61 if err == nil {
62 if resp.StatusCode == http.StatusOK {
63 defer resp.Body.Close() //nolint:errcheck
64 var rel Release
65 if err := json.NewDecoder(resp.Body).Decode(&rel); err == nil && !rel.Prerelease {
66 tag := strings.TrimPrefix(rel.TagName, "v")
67 return &rel, tag, nil
68 }
69 } else {
70 if err := resp.Body.Close(); err != nil {
71 fmt.Printf("warning: non-fatal response body close error: %v\n", err)
72 }
73 }
74 }
75
76 const apiList = "https://api.github.com/repos/floatpane/matcha/releases"
77 resp, err = client.Get(apiList) //nolint:noctx
78 if err != nil {
79 return nil, "", fmt.Errorf("could not query releases: %w", err)
80 }
81 defer resp.Body.Close() //nolint:errcheck
82
83 var rels []Release
84 if err := json.NewDecoder(resp.Body).Decode(&rels); err != nil {
85 return nil, "", fmt.Errorf("could not parse releases: %w", err)
86 }
87
88 for i := range rels {
89 tag := strings.TrimPrefix(rels[i].TagName, "v")
90 if strings.HasPrefix(tag, "1.0.") && !strings.Contains(tag, "-") && !rels[i].Prerelease {
91 return &rels[i], tag, nil
92 }
93 }
94 for i := range rels {
95 tag := strings.TrimPrefix(rels[i].TagName, "v")
96 if v1RCRegex.MatchString(tag) {
97 return &rels[i], tag, nil
98 }
99 }
100 return nil, "", fmt.Errorf("no v1 release found")
101}
102
103func tryHomebrewV1Upgrade(cask bool) bool {
104 if _, err := exec.LookPath("brew"); err != nil {
105 return false
106 }
107
108 formula := "floatpane/matcha/matcha@v1"
109 var installArgs, upgradeArgs []string
110 if cask {
111 installArgs = []string{"install", "--cask", formula}
112 upgradeArgs = []string{"upgrade", "--cask", formula}
113 fmt.Println("Attempting to upgrade via Homebrew cask to v1.")
114 } else {
115 installArgs = []string{"install", formula}
116 upgradeArgs = []string{"upgrade", formula}
117 fmt.Println("Attempting to upgrade via Homebrew to v1.")
118 }
119
120 cmd := exec.Command("brew", installArgs...) //nolint:noctx
121 cmd.Stdout = os.Stdout
122 cmd.Stderr = os.Stderr
123 if err := cmd.Run(); err == nil {
124 fmt.Println("Successfully upgraded via Homebrew.")
125 return true
126 }
127
128 cmd = exec.Command("brew", upgradeArgs...) //nolint:noctx
129 cmd.Stdout = os.Stdout
130 cmd.Stderr = os.Stderr
131 if err := cmd.Run(); err == nil {
132 fmt.Println("Successfully upgraded via Homebrew.")
133 return true
134 }
135
136 fmt.Println("Homebrew v1 upgrade failed.")
137 return false
138}
139
140func trySnapV1Refresh() bool {
141 if _, err := exec.LookPath("snap"); err != nil {
142 return false
143 }
144
145 cmdCheck := exec.Command("snap", "list", "matcha") //nolint:noctx
146 if err := cmdCheck.Run(); err != nil {
147 return false
148 }
149
150 fmt.Println("Detected Snap package — attempting to refresh to candidate v1.")
151 cmd := exec.Command("snap", "refresh", "matcha", "--candidate") //nolint:noctx
152 cmd.Stdout = os.Stdout
153 cmd.Stderr = os.Stderr
154 if err := cmd.Run(); err == nil {
155 fmt.Println("Successfully refreshed snap to candidate v1.")
156 return true
157 }
158
159 fmt.Println("Snap candidate refresh failed.")
160 return false
161}