Skip to content

Commit 21f932a

Browse files
committed
Added new ZeuZ Node update mechanism with version check and progress reporting
1 parent 629e8b5 commit 21f932a

1 file changed

Lines changed: 187 additions & 27 deletions

File tree

Apps/node_runner/main.go

Lines changed: 187 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
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

1821
var (
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+
24151
func 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 {
272403
func 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

Comments
 (0)