@@ -2,6 +2,8 @@ package main
22
33import (
44 "archive/zip"
5+ "encoding/json"
6+ "errors"
57 "flag"
68 "fmt"
79 "io"
@@ -11,16 +13,141 @@ import (
1113 "path/filepath"
1214 "runtime"
1315 "strings"
16+ "time"
1417
1518 "github.com/automationsolutionz/Zeuz_Python_Node/Apps/node_runner/uv_installer"
1619)
1720
1821var (
19- version = "dev"
20- branch = flag .String ("branch" , "" , "Branch to download (defaults to tagged version)" )
21- cleanFlag = flag .Bool ("clean" , false , "Remove ZeuZ Node directory and $HOME/.zeuz and exit" )
22+ version = "dev"
23+ targetVersion string // runtime override; empty = use build-time version
24+ branch = flag .String ("branch" , "" , "Branch to download (defaults to tagged version)" )
25+ cleanFlag = flag .Bool ("clean" , false , "Remove ZeuZ Node directory and $HOME/.zeuz and exit" )
26+ updateFlag = flag .Bool ("update" , false , "Download and install the latest ZeuZ Node version" )
2227)
2328
29+ const (
30+ colorReset = "\033 [0m"
31+ colorGreen = "\033 [32m"
32+ colorYellow = "\033 [33m"
33+ colorBold = "\033 [1m"
34+ )
35+
36+ type zeuzRelease struct {
37+ TagName string `json:"tag_name"`
38+ }
39+
40+ var errRateLimited = errors .New ("GitHub API rate limited — try again later" )
41+
42+ func effectiveVersion () string {
43+ if targetVersion != "" {
44+ return targetVersion
45+ }
46+ return version
47+ }
48+
49+ // fetchLatestVersion fetches the latest ZeuZ Node release tag from GitHub
50+ func fetchLatestVersion () (string , error ) {
51+ client := & http.Client {Timeout : 5 * time .Second }
52+ req , err := http .NewRequest ("GET" ,
53+ "https://api.github.com/repos/AutomationSolutionz/Zeuz_Python_Node/releases/latest" ,
54+ nil )
55+ if err != nil {
56+ return "" , err
57+ }
58+ req .Header .Set ("User-Agent" , "zeuz-node-runner/" + version )
59+ resp , err := client .Do (req )
60+ if err != nil {
61+ return "" , err
62+ }
63+ defer resp .Body .Close ()
64+ if resp .StatusCode == 403 {
65+ return "" , errRateLimited
66+ }
67+ if resp .StatusCode != 200 {
68+ return "" , fmt .Errorf ("HTTP %d" , resp .StatusCode )
69+ }
70+ var r zeuzRelease
71+ json .NewDecoder (resp .Body ).Decode (& r )
72+ return r .TagName , nil
73+ }
74+
75+ // printUpdateBanner prints a styled update available notice
76+ func printUpdateBanner (current , latest string ) {
77+ width := 52
78+ line1 := fmt .Sprintf (" Update available: %s → %s" , current , latest )
79+ line2 := " Run with --update to upgrade"
80+ pad := func (s string ) string {
81+ spaces := width - len (s ) - 1
82+ if spaces < 0 {
83+ spaces = 0
84+ }
85+ return s + strings .Repeat (" " , spaces ) + "║"
86+ }
87+ border := "╔" + strings .Repeat ("═" , width ) + "╗"
88+ bottom := "╚" + strings .Repeat ("═" , width ) + "╝"
89+ fmt .Println (colorYellow + border )
90+ fmt .Println ("║" + pad (line1 ))
91+ fmt .Println ("║" + pad (line2 ))
92+ fmt .Println (bottom + colorReset )
93+ }
94+
95+ // runUpdate fetches the latest release and replaces the current ZeuZ Node directory
96+ func runUpdate () error {
97+ if * branch != "" {
98+ return fmt .Errorf ("--update is not compatible with --branch" )
99+ }
100+ if version == "dev" || strings .HasPrefix (version , "dev-" ) {
101+ fmt .Println (" Dev build — skipping update check" )
102+ return nil
103+ }
104+
105+ fmt .Println (" Checking for updates..." )
106+ latest , err := fetchLatestVersion ()
107+ if err != nil {
108+ return fmt .Errorf ("could not check for updates: %w" , err )
109+ }
110+
111+ if effectiveVersion () == latest {
112+ fmt .Printf (colorGreen + " Already up to date (%s)" + colorReset + "\n " , latest )
113+ return nil
114+ }
115+
116+ fmt .Printf (" Updating %s → %s\n " , effectiveVersion (), latest )
117+ oldDir := getZeuZNodeDir ()
118+ os .RemoveAll (oldDir )
119+
120+ targetVersion = latest
121+ if err := setupZeuzNode (); err != nil {
122+ return fmt .Errorf ("update failed: %w" , err )
123+ }
124+
125+ fmt .Printf (colorGreen + " Update complete (%s)" + colorReset + "\n " , latest )
126+ return nil
127+ }
128+
129+ // progressReader wraps an io.Reader and prints download progress
130+ type progressReader struct {
131+ reader io.Reader
132+ total int64
133+ read int64
134+ }
135+
136+ func (p * progressReader ) Read (buf []byte ) (int , error ) {
137+ n , err := p .reader .Read (buf )
138+ p .read += int64 (n )
139+ if p .total > 0 {
140+ pct := (p .read * 100 ) / p .total
141+ fmt .Printf ("\r Downloading... %d%% " , pct )
142+ } else {
143+ fmt .Printf ("\r Downloading... %.1f MB" , float64 (p .read )/ 1e6 )
144+ }
145+ if err == io .EOF {
146+ fmt .Println ()
147+ }
148+ return n , err
149+ }
150+
24151func downloadFile (url , destPath string ) error {
25152 resp , err := http .Get (url )
26153 if err != nil {
@@ -38,7 +165,8 @@ func downloadFile(url, destPath string) error {
38165 }
39166 defer os .Remove (out .Name ())
40167
41- _ , err = io .Copy (out , resp .Body )
168+ pr := & progressReader {reader : resp .Body , total : resp .ContentLength }
169+ _ , err = io .Copy (out , pr )
42170 if err != nil {
43171 out .Close ()
44172 return fmt .Errorf ("failed to write file: %v" , err )
@@ -125,8 +253,9 @@ func getZeuZNodeURL() string {
125253 if * branch != "" {
126254 return fmt .Sprintf ("https://github.com/AutomationSolutionz/Zeuz_Python_Node/archive/refs/heads/%s.zip" , * branch )
127255 }
128- if version != "dev" && ! strings .HasPrefix (version , "dev-" ) {
129- return fmt .Sprintf ("https://github.com/AutomationSolutionz/Zeuz_Python_Node/archive/refs/tags/%s.zip" , version )
256+ ev := effectiveVersion ()
257+ if ev != "dev" && ! strings .HasPrefix (ev , "dev-" ) {
258+ return fmt .Sprintf ("https://github.com/AutomationSolutionz/Zeuz_Python_Node/archive/refs/tags/%s.zip" , ev )
130259 }
131260 return "https://github.com/AutomationSolutionz/Zeuz_Python_Node/archive/refs/heads/dev.zip"
132261}
@@ -135,9 +264,11 @@ func getZeuZNodeDir() string {
135264 selectedVersion := ""
136265 if * branch != "" {
137266 selectedVersion = * branch
138- }
139- if version != "dev" && ! strings .HasPrefix (version , "dev-" ) {
140- selectedVersion = version
267+ } else {
268+ ev := effectiveVersion ()
269+ if ev != "dev" && ! strings .HasPrefix (ev , "dev-" ) {
270+ selectedVersion = ev
271+ }
141272 }
142273
143274 return fmt .Sprintf ("ZeuZ_Node-%s" , selectedVersion )
@@ -159,7 +290,7 @@ func setupZeuzNode() error {
159290 }
160291 }
161292
162- fmt .Println ("Setting up ZeuZ Node..." )
293+ fmt .Println (" Setting up ZeuZ Node..." )
163294
164295 // Create temporary directory for zip file
165296 tempDir , err := os .MkdirTemp ("" , "zeuz-download" )
@@ -171,7 +302,7 @@ func setupZeuzNode() error {
171302 // Download zip file
172303 zipPath := filepath .Join (tempDir , "zeuz.zip" )
173304 zeuzURL := getZeuZNodeURL ()
174- fmt .Printf ("Downloading ZeuZ Node repository from: %s\n " , zeuzURL )
305+ fmt .Printf (" Fetching from: %s\n " , zeuzURL )
175306 if err := downloadFile (zeuzURL , zipPath ); err != nil {
176307 return err
177308 }
@@ -182,7 +313,7 @@ func setupZeuzNode() error {
182313 }
183314
184315 // Extract zip file
185- fmt .Println ("Extracting ZeuZ Node repository ..." )
316+ fmt .Println (" Extracting ..." )
186317 if err := unzip (zipPath , zeuzDir ); err != nil {
187318 return err
188319 }
@@ -272,73 +403,102 @@ func runUVCommands(args []string) error {
272403func main () {
273404 flag .Parse ()
274405
275- fmt .Printf ("✅ ZeuZ Node %s\n " , version )
406+ fmt .Printf (colorGreen + colorBold + " ZeuZ Node %s" + colorReset + "\n " , version )
407+
408+ // Launch background version check (non-blocking)
409+ updateCh := make (chan string , 1 )
410+ go func () {
411+ latest , err := fetchLatestVersion ()
412+ if err != nil {
413+ updateCh <- ""
414+ return
415+ }
416+ updateCh <- latest
417+ }()
276418
277419 zeuzDir := getZeuZNodeDir ()
278420
279421 if * cleanFlag {
280422 var removedAny bool
281423 if err := os .RemoveAll (zeuzDir ); err == nil {
282- fmt .Printf (" Removed %s\n " , zeuzDir )
424+ fmt .Printf (colorYellow + " Removed %s" + colorReset + " \n " , zeuzDir )
283425 removedAny = true
284426 } else if ! os .IsNotExist (err ) {
285- fmt .Printf ("Failed to remove %s: %v\n " , zeuzDir , err )
427+ fmt .Printf (" Failed to remove %s: %v\n " , zeuzDir , err )
286428 }
287429
288430 home , err := os .UserHomeDir ()
289431 if err == nil {
290432 zeuzHome := filepath .Join (home , ".zeuz" )
291433 if err := os .RemoveAll (zeuzHome ); err == nil {
292- fmt .Printf (" Removed %s\n " , zeuzHome )
434+ fmt .Printf (colorYellow + " Removed %s" + colorReset + " \n " , zeuzHome )
293435 removedAny = true
294436 } else if ! os .IsNotExist (err ) {
295- fmt .Printf ("Failed to remove %s: %v\n " , zeuzHome , err )
437+ fmt .Printf (" Failed to remove %s: %v\n " , zeuzHome , err )
296438 }
297439 } else {
298- fmt .Printf ("Could not determine user home dir: %v\n " , err )
440+ fmt .Printf (" Could not determine user home dir: %v\n " , err )
299441 }
300442
301443 if ! removedAny {
302- fmt .Println ("Nothing removed. No matching directories found." )
444+ fmt .Println (" Nothing removed. No matching directories found." )
303445 } else {
304- fmt .Println ("Cleanup complete — proceeding to download & install a fresh copy." )
446+ fmt .Println (colorGreen + " Cleanup complete — downloading fresh copy." + colorReset )
447+ }
448+ }
449+
450+ if * updateFlag {
451+ if err := runUpdate (); err != nil {
452+ fmt .Printf (colorYellow + " Warning: %v" + colorReset + "\n " , err )
453+ os .Exit (1 )
305454 }
306455 }
307456
308457 // Setup ZeuZ Node directory and change into it
309458 if err := setupZeuzNode (); err != nil {
310- fmt .Printf ("Error setting up ZeuZ Node: %v\n " , err )
459+ fmt .Printf (" Error setting up ZeuZ Node: %v\n " , err )
311460 os .Exit (1 )
312461 }
313462
314- // Change directory to ZeuZ Node
463+ // Change directory to ZeuZ Node (re-evaluate after potential targetVersion change)
464+ zeuzDir = getZeuZNodeDir ()
315465 if err := os .Chdir (zeuzDir ); err != nil {
316- fmt .Printf ("Error changing to ZeuZ Node directory: %v\n " , err )
466+ fmt .Printf (" Error changing to ZeuZ Node directory: %v\n " , err )
317467 os .Exit (1 )
318468 }
319469
470+ // Drain the background update check (non-blocking)
471+ select {
472+ case latest := <- updateCh :
473+ if latest != "" && latest != effectiveVersion () {
474+ printUpdateBanner (effectiveVersion (), latest )
475+ }
476+ default :
477+ // check still in flight or failed — continue silently
478+ }
479+
320480 // Update PATH before checking if UV is installed
321481 if err := updatePath (); err != nil {
322- fmt .Printf ("Error updating path: %v\n " , err )
482+ fmt .Printf (" Error updating path: %v\n " , err )
323483 }
324484
325485 // Install UV if needed
326486 if err := installUV (); err != nil {
327- fmt .Printf ("Error installing UV: %v\n " , err )
487+ fmt .Printf (" Error installing UV: %v\n " , err )
328488 os .Exit (1 )
329489 }
330490
331491 // Update PATH to ensure UV is available after installation
332492 if err := updatePath (); err != nil {
333- fmt .Printf ("Error updating path: %v\n " , err )
493+ fmt .Printf (" Error updating path: %v\n " , err )
334494 }
335495
336496 // Get remaining command line arguments after flag parsing
337497 args := flag .Args ()
338498
339499 // Run UV commands with arguments
340500 if err := runUVCommands (args ); err != nil {
341- fmt .Printf ("Error running UV commands: %v\n " , err )
501+ fmt .Printf (" Error running UV commands: %v\n " , err )
342502 os .Exit (1 )
343503 }
344504}
0 commit comments