@@ -38,8 +38,61 @@ type zeuzRelease struct {
3838 TagName string `json:"tag_name"`
3939}
4040
41+ // versionCache persists the last GitHub version check result to disk so the
42+ // network call is skipped on subsequent startups within the cache window.
43+ type versionCache struct {
44+ CheckedAt time.Time `json:"checked_at"`
45+ Latest string `json:"latest"`
46+ }
47+
48+ const versionCheckInterval = 24 * time .Hour
49+
4150var errRateLimited = errors .New ("GitHub API rate limited — try again later" )
4251
52+ // versionCachePath returns the path to the version cache file.
53+ func versionCachePath () string {
54+ home , err := os .UserHomeDir ()
55+ if err != nil {
56+ return ""
57+ }
58+ return filepath .Join (home , ".zeuz" , "version_check_cache.json" )
59+ }
60+
61+ // loadVersionCache reads the cache file. Returns nil if missing, unreadable,
62+ // or older than versionCheckInterval.
63+ func loadVersionCache () * versionCache {
64+ path := versionCachePath ()
65+ if path == "" {
66+ return nil
67+ }
68+ data , err := os .ReadFile (path )
69+ if err != nil {
70+ return nil
71+ }
72+ var cache versionCache
73+ if err := json .Unmarshal (data , & cache ); err != nil {
74+ return nil
75+ }
76+ if time .Since (cache .CheckedAt ) > versionCheckInterval {
77+ return nil // expired
78+ }
79+ return & cache
80+ }
81+
82+ // saveVersionCache writes the latest version and current timestamp to disk.
83+ func saveVersionCache (latest string ) {
84+ path := versionCachePath ()
85+ if path == "" {
86+ return
87+ }
88+ os .MkdirAll (filepath .Dir (path ), 0755 )
89+ data , err := json .Marshal (versionCache {CheckedAt : time .Now (), Latest : latest })
90+ if err != nil {
91+ return
92+ }
93+ os .WriteFile (path , data , 0644 )
94+ }
95+
4396func effectiveVersion () string {
4497 if targetVersion != "" {
4598 return targetVersion
@@ -123,6 +176,9 @@ func runUpdate() error {
123176 return fmt .Errorf ("update failed: %w" , err )
124177 }
125178
179+ // Refresh the cache so the next startup doesn't re-check immediately.
180+ saveVersionCache (latest )
181+
126182 fmt .Printf (colorGreen + " Update complete (%s)" + colorReset + "\n " , latest )
127183 return nil
128184}
@@ -401,22 +457,42 @@ func runUVCommands(args []string) error {
401457 return runCmd .Run ()
402458}
403459
460+ // showUpdateBannerIfNeeded reads the version cache and prints the update
461+ // banner if the cached latest version differs from the running version.
462+ // This is called at startup and again after node_cli.py exits so the user
463+ // always sees a reminder. No network call — purely a cache file read.
464+ func showUpdateBannerIfNeeded () {
465+ cache := loadVersionCache ()
466+ if cache == nil {
467+ return
468+ }
469+ if cache .Latest != "" && cache .Latest != effectiveVersion () {
470+ printUpdateBanner (effectiveVersion (), cache .Latest )
471+ }
472+ }
473+
404474func main () {
405475 flag .Parse ()
406476
407477 fmt .Printf (colorGreen + colorBold + " ZeuZ Node %s" + colorReset + "\n " , version )
408478
409- // Launch background version check (non-blocking)
410- updateCh := make ( chan string , 1 )
479+ // Refresh the version cache in the background (once per 24h).
480+ // Just writes to the cache file — showUpdateBannerIfNeeded() reads it.
411481 go func () {
482+ if loadVersionCache () != nil {
483+ return // cache still fresh, nothing to do
484+ }
412485 latest , err := fetchLatestVersion ()
413486 if err != nil {
414- updateCh <- ""
415487 return
416488 }
417- updateCh <- latest
489+ saveVersionCache ( latest )
418490 }()
419491
492+ // Show update banner at startup using whatever is in the cache.
493+ // On first ever run the cache is empty so nothing shows yet.
494+ showUpdateBannerIfNeeded ()
495+
420496 zeuzDir := getZeuZNodeDir ()
421497
422498 if * cleanFlag {
@@ -461,24 +537,13 @@ func main() {
461537 os .Exit (1 )
462538 }
463539
464- // Change directory to ZeuZ Node (re -evaluate after potential targetVersion change)
540+ // Re -evaluate after potential targetVersion change by runUpdate.
465541 zeuzDir = getZeuZNodeDir ()
466542 if err := os .Chdir (zeuzDir ); err != nil {
467543 fmt .Printf (" Error changing to ZeuZ Node directory: %v\n " , err )
468544 os .Exit (1 )
469545 }
470546
471- // Drain the background update check (non-blocking)
472- select {
473- case latest := <- updateCh :
474- if latest != "" && latest != effectiveVersion () {
475- printUpdateBanner (effectiveVersion (), latest )
476- }
477- default :
478- // check still in flight or failed — continue silently
479- }
480-
481- // Update PATH before checking if UV is installed
482547 if err := updatePath (); err != nil {
483548 fmt .Printf (" Error updating path: %v\n " , err )
484549 }
@@ -496,10 +561,13 @@ func main() {
496561
497562 // Get remaining command line arguments after flag parsing
498563 args := flag .Args ()
564+ runErr := runUVCommands (args )
565+
566+ // Show update banner again after node_cli.py exits.
567+ showUpdateBannerIfNeeded ()
499568
500- // Run UV commands with arguments
501- if err := runUVCommands (args ); err != nil {
502- fmt .Printf (" Error running UV commands: %v\n " , err )
569+ if runErr != nil {
570+ fmt .Printf (" Error running UV commands: %v\n " , runErr )
503571 os .Exit (1 )
504572 }
505573}
0 commit comments