fix: update re-execs with sudo; version output with color and formatting

- 'update' now re-execs with sudo when not root (same pattern as status)
- 'version' output: name and version on separate lines, cyan version number,
  green for up-to-date, yellow for available update
- 'update' output: matching color treatment, red on failure
- Deduplicate color constants — single package-level set used everywhere
This commit is contained in:
Joel Male
2026-02-25 15:02:32 +10:00
parent c0541f7bc9
commit f4e5bafa73
+51 -50
View File
@@ -29,6 +29,16 @@ import (
// version is set at build time via -ldflags "-X main.version=x.y.z" // version is set at build time via -ldflags "-X main.version=x.y.z"
var version = "dev" var version = "dev"
const (
ansiReset = "\033[0m"
ansiBold = "\033[1m"
ansiDim = "\033[2m"
ansiCyan = "\033[0;36m"
ansiGreen = "\033[0;32m"
ansiYellow = "\033[1;33m"
ansiRed = "\033[0;31m"
)
const ( const (
defaultURL = "https://pulsewise.app/api/collect" defaultURL = "https://pulsewise.app/api/collect"
versionCheckURL = "https://pulsewise.app/collector/release/latest" versionCheckURL = "https://pulsewise.app/collector/release/latest"
@@ -216,47 +226,55 @@ func main() {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
func runVersionCommand() { func runVersionCommand() {
fmt.Printf("pulsewise-collector v%s\n\n", version) fmt.Printf("\n %spulsewise-collector%s\n", ansiBold, ansiReset)
fmt.Printf(" %s%sv%s%s\n\n", ansiBold, ansiCyan, version, ansiReset)
fmt.Print("Checking for updates... ") fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset)
latest, err := fetchLatestVersion() latest, err := fetchLatestVersion()
if err != nil { if err != nil {
fmt.Printf("(update check failed: %v)\n", err) fmt.Printf("%s(update check failed: %v)%s\n\n", ansiDim, err, ansiReset)
return return
} }
if latest == version { if latest == version {
fmt.Println("You are on the latest version.") fmt.Printf("%sYou are on the latest version.%s\n\n", ansiGreen, ansiReset)
} else { } else {
fmt.Printf("v%s is available.\n", latest) fmt.Printf("%sv%s is available%s\n", ansiYellow, latest, ansiReset)
fmt.Println("Run 'pulsewise-collector update' to upgrade.") fmt.Printf(" %sRun 'pulsewise-collector update' to upgrade.%s\n\n", ansiDim, ansiReset)
} }
} }
func runUpdateCommand() { func runUpdateCommand() {
fmt.Printf("pulsewise-collector v%s\n\n", version) if os.Getuid() != 0 {
reexecWithSudo()
return
}
fmt.Print("Checking for updates... ") fmt.Printf("\n %spulsewise-collector%s\n", ansiBold, ansiReset)
fmt.Printf(" %s%sv%s%s\n\n", ansiBold, ansiCyan, version, ansiReset)
fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset)
latest, err := fetchLatestVersion() latest, err := fetchLatestVersion()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "update check failed: %v\n", err) fmt.Fprintf(os.Stderr, "%supdate check failed: %v%s\n\n", ansiRed, err, ansiReset)
os.Exit(1) os.Exit(1)
} }
if latest == version { if latest == version {
fmt.Printf("Already on the latest version (v%s).\n", version) fmt.Printf("%sAlready on the latest version.%s\n\n", ansiGreen, ansiReset)
return return
} }
fmt.Printf("v%s is available.\n\n", latest) fmt.Printf("%sv%s is available%s\n\n", ansiYellow, latest, ansiReset)
fmt.Print("Downloading... ") fmt.Printf(" %sDownloading...%s ", ansiDim, ansiReset)
if err := downloadAndReplace(); err != nil { if err := downloadAndReplace(); err != nil {
fmt.Fprintf(os.Stderr, "\nUpdate failed: %v\n", err) fmt.Fprintf(os.Stderr, "\n %sUpdate failed: %v%s\n\n", ansiRed, err, ansiReset)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Done.\nSuccessfully updated to v%s.\n", latest) fmt.Printf("%sDone.%s\n", ansiGreen, ansiReset)
fmt.Printf(" Successfully updated to %sv%s%s.\n\n", ansiCyan, latest, ansiReset)
} }
func reexecWithSudo() { func reexecWithSudo() {
@@ -280,22 +298,14 @@ func runStatusCommand() {
return return
} }
const ( label := func(s string) string { return fmt.Sprintf(" %s%-16s%s", ansiDim, s, ansiReset) }
bold = "\033[1m"
dim = "\033[2m"
green = "\033[0;32m"
red = "\033[0;31m"
nc = "\033[0m"
)
label := func(s string) string { return fmt.Sprintf(" %s%-16s%s", dim, s, nc) } fmt.Printf("\n%spulsewise-collector%s v%s\n\n", ansiBold, ansiReset, version)
fmt.Printf("\n%spulsewise-collector%s v%s\n\n", bold, nc, version)
// Config // Config
token, hostname, url, _, autoUpdate, err := loadConfigFromFile() token, hostname, url, _, autoUpdate, err := loadConfigFromFile()
if err != nil { if err != nil {
fmt.Printf("%s Not configured — %v\n\n", red, nc) fmt.Printf("%s Not configured — %v\n\n", ansiRed, ansiReset)
} else { } else {
maskedToken := token maskedToken := token
if len(token) >= 8 { if len(token) >= 8 {
@@ -303,11 +313,11 @@ func runStatusCommand() {
} else { } else {
maskedToken = "****" maskedToken = "****"
} }
autoUpdateStr := green + "enabled" + nc autoUpdateStr := ansiGreen + "enabled" + ansiReset
if !autoUpdate { if !autoUpdate {
autoUpdateStr = dim + "disabled" + nc autoUpdateStr = ansiDim + "disabled" + ansiReset
} }
fmt.Printf("%sCollector%s\n", bold, nc) fmt.Printf("%sCollector%s\n", ansiBold, ansiReset)
fmt.Printf("%s %s\n", label("Hostname"), hostname) fmt.Printf("%s %s\n", label("Hostname"), hostname)
fmt.Printf("%s %s\n", label("Token"), maskedToken) fmt.Printf("%s %s\n", label("Token"), maskedToken)
fmt.Printf("%s %s\n", label("API endpoint"), url) fmt.Printf("%s %s\n", label("API endpoint"), url)
@@ -317,22 +327,22 @@ func runStatusCommand() {
} }
// Service status // Service status
fmt.Printf("%sService%s\n", bold, nc) fmt.Printf("%sService%s\n", ansiBold, ansiReset)
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
out, err := exec.Command("systemctl", "is-active", systemdUnit).Output() out, err := exec.Command("systemctl", "is-active", systemdUnit).Output()
state := strings.TrimSpace(string(out)) state := strings.TrimSpace(string(out))
if err == nil && state == "active" { if err == nil && state == "active" {
fmt.Printf("%s %srunning%s (systemd)\n", label("Status"), green, nc) fmt.Printf("%s %srunning%s (systemd)\n", label("Status"), ansiGreen, ansiReset)
} else { } else {
fmt.Printf("%s %sstopped%s (systemd)\n", label("Status"), red, nc) fmt.Printf("%s %sstopped%s (systemd)\n", label("Status"), ansiRed, ansiReset)
} }
case "darwin": case "darwin":
err := exec.Command("launchctl", "list", launchdLabel).Run() err := exec.Command("launchctl", "list", launchdLabel).Run()
if err == nil { if err == nil {
fmt.Printf("%s %srunning%s (launchd)\n", label("Status"), green, nc) fmt.Printf("%s %srunning%s (launchd)\n", label("Status"), ansiGreen, ansiReset)
} else { } else {
fmt.Printf("%s %sstopped%s (launchd)\n", label("Status"), red, nc) fmt.Printf("%s %sstopped%s (launchd)\n", label("Status"), ansiRed, ansiReset)
} }
default: default:
fmt.Printf("%s unknown platform\n", label("Status")) fmt.Printf("%s unknown platform\n", label("Status"))
@@ -341,7 +351,7 @@ func runStatusCommand() {
// Last pulse // Last pulse
if t, err := readLastPulse(); err == nil { if t, err := readLastPulse(); err == nil {
ago := time.Since(t).Round(time.Second) ago := time.Since(t).Round(time.Second)
fmt.Printf("%s %s %s(%s ago)%s\n", label("Last pulse"), t.Format("2006-01-02 15:04:05"), dim, ago, nc) fmt.Printf("%s %s %s(%s ago)%s\n", label("Last pulse"), t.Format("2006-01-02 15:04:05"), ansiDim, ago, ansiReset)
} else { } else {
fmt.Printf("%s never\n", label("Last pulse")) fmt.Printf("%s never\n", label("Last pulse"))
} }
@@ -349,25 +359,16 @@ func runStatusCommand() {
} }
func runUninstallCommand() { func runUninstallCommand() {
const (
bold = "\033[1m"
dim = "\033[2m"
green = "\033[0;32m"
red = "\033[0;31m"
yellow = "\033[1;33m"
nc = "\033[0m"
)
if os.Getuid() != 0 { if os.Getuid() != 0 {
fmt.Fprintf(os.Stderr, "%sError:%s uninstall must be run as root (try sudo pulsewise-collector uninstall)\n", red, nc) fmt.Fprintf(os.Stderr, "%sError:%s uninstall must be run as root (try sudo pulsewise-collector uninstall)\n", ansiRed, ansiReset)
os.Exit(1) os.Exit(1)
} }
selfPath, _ := os.Executable() selfPath, _ := os.Executable()
selfPath, _ = filepath.EvalSymlinks(selfPath) selfPath, _ = filepath.EvalSymlinks(selfPath)
fmt.Printf("\n%sUninstall pulsewise-collector%s\n\n", bold, nc) fmt.Printf("\n%sUninstall pulsewise-collector%s\n\n", ansiBold, ansiReset)
fmt.Printf(" %sThe following will be removed:%s\n", dim, nc) fmt.Printf(" %sThe following will be removed:%s\n", ansiDim, ansiReset)
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
@@ -380,7 +381,7 @@ func runUninstallCommand() {
fmt.Printf(" Config %s/\n", configDir) fmt.Printf(" Config %s/\n", configDir)
fmt.Println() fmt.Println()
fmt.Printf(" %sAre you sure?%s [y/N]: ", yellow, nc) fmt.Printf(" %sAre you sure?%s [y/N]: ", ansiYellow, ansiReset)
var reply string var reply string
fmt.Scanln(&reply) fmt.Scanln(&reply)
if strings.ToLower(strings.TrimSpace(reply)) != "y" { if strings.ToLower(strings.TrimSpace(reply)) != "y" {
@@ -392,9 +393,9 @@ func runUninstallCommand() {
step := func(label, path string, fn func() error) { step := func(label, path string, fn func() error) {
fmt.Printf(" %-28s", label+"...") fmt.Printf(" %-28s", label+"...")
if err := fn(); err != nil { if err := fn(); err != nil {
fmt.Printf("%sfailed%s (%v)\n", red, nc, err) fmt.Printf("%sfailed%s (%v)\n", ansiRed, ansiReset, err)
} else { } else {
fmt.Printf("%sdone%s\n", green, nc) fmt.Printf("%sdone%s\n", ansiGreen, ansiReset)
} }
} }
@@ -440,7 +441,7 @@ func runUninstallCommand() {
return os.Remove(selfPath) return os.Remove(selfPath)
}) })
fmt.Printf("\n %sPulsewise Collector has been uninstalled.%s\n\n", green, nc) fmt.Printf("\n %sPulsewise Collector has been uninstalled.%s\n\n", ansiGreen, ansiReset)
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────