11package updater
22
33import (
4- "context"
54 "encoding/json"
65 "fmt"
6+ "io"
77 "net/http"
88 "os"
99 "path/filepath"
10+ "runtime"
1011 "sync"
1112 "time"
1213
@@ -30,7 +31,135 @@ type CheckState struct {
3031 UpdateAvailable bool `json:"update_available"`
3132}
3233
33- func ShowUpdateNotificationIfAvailable (currentVersion string ) {
34+ type AutoUpdateMode string
35+
36+ const (
37+ AutoUpdateEnabled AutoUpdateMode = "true"
38+ AutoUpdateNotify AutoUpdateMode = "notify"
39+ AutoUpdateDisabled AutoUpdateMode = "false"
40+ )
41+
42+ type UserConfig struct {
43+ AutoUpdate AutoUpdateMode `json:"autoupdate"`
44+ }
45+
46+ func loadUserConfig () UserConfig {
47+ cfg := UserConfig {AutoUpdate : AutoUpdateEnabled }
48+ path , err := getUserConfigPath ()
49+ if err != nil {
50+ return cfg
51+ }
52+ data , err := os .ReadFile (path )
53+ if err != nil {
54+ return cfg
55+ }
56+ json .Unmarshal (data , & cfg )
57+ if cfg .AutoUpdate == "" {
58+ cfg .AutoUpdate = AutoUpdateEnabled
59+ }
60+ return cfg
61+ }
62+
63+ func getUserConfigPath () (string , error ) {
64+ home , err := os .UserHomeDir ()
65+ if err != nil {
66+ return "" , err
67+ }
68+ return filepath .Join (home , ".openboot" , "config.json" ), nil
69+ }
70+
71+ func AutoUpgrade (currentVersion string ) {
72+ if os .Getenv ("OPENBOOT_DISABLE_AUTOUPDATE" ) == "1" {
73+ return
74+ }
75+
76+ cfg := loadUserConfig ()
77+
78+ switch cfg .AutoUpdate {
79+ case AutoUpdateDisabled :
80+ return
81+ case AutoUpdateNotify :
82+ notifyIfUpdateAvailable (currentVersion )
83+ checkForUpdatesAsync (currentVersion )
84+ return
85+ default :
86+ latest , err := getLatestVersion ()
87+ if err != nil {
88+ return
89+ }
90+ if ! isNewerVersion (latest , currentVersion ) {
91+ return
92+ }
93+
94+ latestClean := trimVersionPrefix (latest )
95+ ui .Info (fmt .Sprintf ("Updating OpenBoot v%s → v%s..." , currentVersion , latestClean ))
96+ if err := DownloadAndReplace (); err != nil {
97+ ui .Warn (fmt .Sprintf ("Auto-update failed: %v" , err ))
98+ ui .Muted ("Run 'openboot update --self' to update manually" )
99+ fmt .Println ()
100+ return
101+ }
102+ ui .Success (fmt .Sprintf ("Updated to v%s. Restart openboot to use the new version." , latestClean ))
103+ fmt .Println ()
104+ }
105+ }
106+
107+ func DownloadAndReplace () error {
108+ arch := runtime .GOARCH
109+ if arch == "" {
110+ arch = "arm64"
111+ }
112+
113+ url := fmt .Sprintf ("https://github.com/openbootdotdev/openboot/releases/latest/download/openboot-darwin-%s" , arch )
114+
115+ client := getHTTPClient ()
116+ resp , err := client .Get (url )
117+ if err != nil {
118+ return fmt .Errorf ("download failed: %w" , err )
119+ }
120+ defer resp .Body .Close ()
121+
122+ if resp .StatusCode != http .StatusOK {
123+ return fmt .Errorf ("download failed: HTTP %d" , resp .StatusCode )
124+ }
125+
126+ binPath , err := os .Executable ()
127+ if err != nil {
128+ return fmt .Errorf ("cannot determine binary path: %w" , err )
129+ }
130+
131+ binPath , err = filepath .EvalSymlinks (binPath )
132+ if err != nil {
133+ return fmt .Errorf ("cannot resolve binary path: %w" , err )
134+ }
135+
136+ tmpPath := binPath + ".tmp"
137+ f , err := os .Create (tmpPath )
138+ if err != nil {
139+ return fmt .Errorf ("failed to create temp file: %w" , err )
140+ }
141+
142+ if _ , err := io .Copy (f , resp .Body ); err != nil {
143+ f .Close ()
144+ os .Remove (tmpPath )
145+ return fmt .Errorf ("failed to write binary: %w" , err )
146+ }
147+ f .Close ()
148+
149+ if err := os .Chmod (tmpPath , 0755 ); err != nil {
150+ os .Remove (tmpPath )
151+ return fmt .Errorf ("failed to set permissions: %w" , err )
152+ }
153+
154+ if err := os .Rename (tmpPath , binPath ); err != nil {
155+ os .Remove (tmpPath )
156+ return fmt .Errorf ("failed to replace binary: %w" , err )
157+ }
158+
159+ return nil
160+ }
161+
162+ func notifyIfUpdateAvailable (currentVersion string ) {
34163 state , err := loadState ()
35164 if err != nil {
36165 return
@@ -43,16 +172,9 @@ func ShowUpdateNotificationIfAvailable(currentVersion string) {
43172 }
44173}
45174
46- func CheckForUpdatesAsync ( ctx context. Context , currentVersion string ) {
175+ func checkForUpdatesAsync ( currentVersion string ) {
47176 go func () {
48- select {
49- case <- ctx .Done ():
50- return
51- default :
52- }
53-
54177 state , _ := loadState ()
55-
56178 if state != nil && time .Since (state .LastCheck ) < checkInterval {
57179 return
58180 }
@@ -62,12 +184,10 @@ func CheckForUpdatesAsync(ctx context.Context, currentVersion string) {
62184 return
63185 }
64186
65- updateAvailable := isNewerVersion (latestVersion , currentVersion )
66-
67187 saveState (& CheckState {
68188 LastCheck : time .Now (),
69189 LatestVersion : latestVersion ,
70- UpdateAvailable : updateAvailable ,
190+ UpdateAvailable : isNewerVersion ( latestVersion , currentVersion ) ,
71191 })
72192 }()
73193}
@@ -76,10 +196,8 @@ func isNewerVersion(latest, current string) bool {
76196 if latest == "" {
77197 return false
78198 }
79-
80199 latestClean := trimVersionPrefix (latest )
81200 currentClean := trimVersionPrefix (current )
82-
83201 return latestClean != currentClean && latestClean > currentClean
84202}
85203
@@ -92,7 +210,7 @@ func trimVersionPrefix(v string) string {
92210
93211func getHTTPClient () * http.Client {
94212 httpClientOnce .Do (func () {
95- httpClient = & http.Client {Timeout : 5 * time .Second }
213+ httpClient = & http.Client {Timeout : 15 * time .Second }
96214 })
97215 return httpClient
98216}
0 commit comments