From 7e915146a8268265010a12527f4b61d70cb837e2 Mon Sep 17 00:00:00 2001 From: Laurence Date: Fri, 29 May 2026 15:36:50 +0100 Subject: [PATCH] fix: send CLI errors to stderr instead of stdout Shell completion redirects stdout, so runtime init warnings and command errors must not be written there. Skip directory setup for completion, route logger warnings to stderr, and return errors via Cobra RunE so diagnostics land on stderr with correct exit codes. Fixes fosrl/cli#74 --- cmd/auth/login/login.go | 6 +- cmd/auth/logout/logout.go | 7 +- cmd/auth/status/status.go | 7 +- cmd/authdaemon/authdaemon.go | 27 +++--- cmd/down/client/client.go | 7 +- cmd/logs/client/client.go | 6 +- cmd/root.go | 53 +++++++----- cmd/select/account/account.go | 7 +- cmd/select/org/org.go | 7 +- cmd/ssh/sign.go | 124 ++++++++++++++------------- cmd/ssh/ssh.go | 156 ++++++++++++++++++---------------- cmd/status/client/client.go | 7 +- cmd/up/client/client.go | 6 +- cmd/update/update_unix.go | 6 +- cmd/update/update_windows.go | 6 +- cmd/version/version.go | 3 +- internal/logger/logger.go | 4 +- 17 files changed, 213 insertions(+), 226 deletions(-) diff --git a/cmd/auth/login/login.go b/cmd/auth/login/login.go index ad3070e..2d2eea5 100644 --- a/cmd/auth/login/login.go +++ b/cmd/auth/login/login.go @@ -163,10 +163,8 @@ func LoginCmd() *cobra.Command { return nil }, - Run: func(cmd *cobra.Command, args []string) { - if err := loginMain(cmd, &opts); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return loginMain(cmd, &opts) }, } diff --git a/cmd/auth/logout/logout.go b/cmd/auth/logout/logout.go index 0d3f4bd..4c6f352 100644 --- a/cmd/auth/logout/logout.go +++ b/cmd/auth/logout/logout.go @@ -2,7 +2,6 @@ package logout import ( "errors" - "os" "time" "github.com/charmbracelet/huh" @@ -18,10 +17,8 @@ func LogoutCmd() *cobra.Command { Use: "logout", Short: "Logout from Pangolin", Long: "Logout and clear your session", - Run: func(cmd *cobra.Command, args []string) { - if err := logoutMain(cmd); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return logoutMain(cmd) }, } diff --git a/cmd/auth/status/status.go b/cmd/auth/status/status.go index c3a234f..5c589f1 100644 --- a/cmd/auth/status/status.go +++ b/cmd/auth/status/status.go @@ -2,7 +2,6 @@ package status import ( "fmt" - "os" "strings" "github.com/fosrl/cli/internal/api" @@ -17,10 +16,8 @@ func StatusCmd() *cobra.Command { Use: "status", Short: "Check authentication status", Long: "Check if you are logged in and view your account information", - Run: func(cmd *cobra.Command, args []string) { - if err := statusMain(cmd); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return statusMain(cmd) }, } diff --git a/cmd/authdaemon/authdaemon.go b/cmd/authdaemon/authdaemon.go index 25992f6..bb171d9 100644 --- a/cmd/authdaemon/authdaemon.go +++ b/cmd/authdaemon/authdaemon.go @@ -10,7 +10,6 @@ import ( "os/signal" "syscall" - "github.com/fosrl/cli/internal/logger" authdaemonpkg "github.com/fosrl/newt/authdaemon" "github.com/spf13/cobra" ) @@ -51,8 +50,8 @@ func AuthDaemonCmd() *cobra.Command { } return nil }, - Run: func(c *cobra.Command, args []string) { - runAuthDaemon(opts) + RunE: func(c *cobra.Command, args []string) error { + return runAuthDaemon(opts) }, } @@ -84,12 +83,12 @@ func PrincipalsCmd() *cobra.Command { } return nil }, - Run: func(c *cobra.Command, args []string) { + RunE: func(c *cobra.Command, args []string) error { path := opts.PrincipalsFile if path == "" { path = defaultPrincipalsPath } - runPrincipals(path, opts.Username) + return runPrincipals(path, opts.Username) }, } @@ -100,19 +99,19 @@ func PrincipalsCmd() *cobra.Command { return cmd } -func runPrincipals(principalsPath, username string) { +func runPrincipals(principalsPath, username string) error { list, err := authdaemonpkg.GetPrincipals(principalsPath, username) if err != nil { - logger.Error("%v", err) - os.Exit(1) + return err } if len(list) == 0 { fmt.Println("") - return + return nil } for _, principal := range list { fmt.Println(principal) } + return nil } func runAuthDaemon(opts struct { @@ -121,7 +120,7 @@ func runAuthDaemon(opts struct { PrincipalsFile string CACertPath string GenerateRandomPassword bool -}) { +}) error { cfg := authdaemonpkg.Config{ Port: opts.Port, PresharedKey: opts.PreSharedKey, @@ -133,15 +132,11 @@ func runAuthDaemon(opts struct { srv, err := authdaemonpkg.NewServer(cfg) if err != nil { - logger.Error("%v", err) - os.Exit(1) + return err } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - if err := srv.Run(ctx); err != nil { - logger.Error("%v", err) - os.Exit(1) - } + return srv.Run(ctx) } diff --git a/cmd/down/client/client.go b/cmd/down/client/client.go index 6629481..3cdf7ce 100644 --- a/cmd/down/client/client.go +++ b/cmd/down/client/client.go @@ -2,7 +2,6 @@ package client import ( "errors" - "os" "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/logger" @@ -16,10 +15,8 @@ func ClientDownCmd() *cobra.Command { Use: "client", Short: "Stop the client connection", Long: "Stop the currently running client connection", - Run: func(cmd *cobra.Command, args []string) { - if err := clientDownMain(cmd); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return clientDownMain(cmd) }, } diff --git a/cmd/logs/client/client.go b/cmd/logs/client/client.go index db98a97..b19be8a 100644 --- a/cmd/logs/client/client.go +++ b/cmd/logs/client/client.go @@ -26,10 +26,8 @@ func ClientLogsCmd() *cobra.Command { Use: "client", Short: "View client logs", Long: "View client logs. Use -f to follow log output.", - Run: func(cmd *cobra.Command, args []string) { - if err := clientLogsMain(cmd, &opts); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return clientLogsMain(cmd, &opts) }, } diff --git a/cmd/root.go b/cmd/root.go index 94ef259..43ec44a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,6 @@ import ( "github.com/fosrl/cli/cmd/auth/logout" "github.com/fosrl/cli/cmd/authdaemon" "github.com/fosrl/cli/cmd/down" - "github.com/fosrl/cli/cmd/list" "github.com/fosrl/cli/cmd/logs" selectcmd "github.com/fosrl/cli/cmd/select" "github.com/fosrl/cli/cmd/ssh" @@ -50,7 +49,6 @@ func RootCommand(initResources bool) (*cobra.Command, error) { } cmd.AddCommand(apply.ApplyCommand()) cmd.AddCommand(selectcmd.SelectCmd()) - cmd.AddCommand(list.ListCmd()) // Platform-specific commands - nil on unsupported platforms if upCmd := up.UpCmd(); upCmd != nil { @@ -117,16 +115,15 @@ func RootCommand(initResources bool) (*cobra.Command, error) { } func mainCommandPreRun(cmd *cobra.Command, args []string) error { - cfg := config.ConfigFromContext(cmd.Context()) - - // Skip init/update check for version and update commands - // Check both the command name and if it's one of these specific commands - cmdName := cmd.Name() - if cmdName == "version" || cmdName == "update" { + if shouldSkipRuntimeInit(cmd) { return nil } - ensureRuntimeDirs(cfg) + cfg := config.ConfigFromContext(cmd.Context()) + + if err := ensureRuntimeDirs(cfg); err != nil { + return err + } // Check for updates asynchronously if !cfg.DisableUpdateCheck { @@ -140,34 +137,44 @@ func mainCommandPreRun(cmd *cobra.Command, args []string) error { return nil } -// Make sure all required directories exist once -// before executing any subcommands. -func ensureRuntimeDirs(cfg *config.Config) { +// shouldSkipRuntimeInit returns true for commands that must not touch runtime +// directories or emit diagnostics to stdout (for example shell completion). +func shouldSkipRuntimeInit(cmd *cobra.Command) bool { + for c := cmd; c != nil; c = c.Parent() { + switch c.Name() { + case "completion", "version", "update": + return true + } + } + return false +} + +// Make sure all required directories exist once before executing subcommands. +func ensureRuntimeDirs(cfg *config.Config) error { configDir, err := config.GetPangolinConfigDir() if err != nil { - logger.Warning("failed to create pangolin configuration directory: %v", err) - } else { - err = os.MkdirAll(configDir, 0o755) - if err != nil { - logger.Warning("failed to create %s: %v", configDir, err) - } + return fmt.Errorf("failed to create pangolin configuration directory: %w", err) + } + + if err := os.MkdirAll(configDir, 0o755); err != nil { + return fmt.Errorf("failed to create %s: %w", configDir, err) } if cfg.LogFile != "" { logPathDirname := filepath.Dir(cfg.LogFile) - - err = os.MkdirAll(logPathDirname, 0o755) - if err != nil { - logger.Warning("failed to create %s: %v", logPathDirname, err) + if err := os.MkdirAll(logPathDirname, 0o755); err != nil { + return fmt.Errorf("failed to create %s: %w", logPathDirname, err) } } + + return nil } // Execute is called by main.go func Execute() { cmd, err := RootCommand(true) if err != nil { - logger.Error("%v", err) + fmt.Fprintln(os.Stderr, err) os.Exit(1) } diff --git a/cmd/select/account/account.go b/cmd/select/account/account.go index 2189e0d..60fc106 100644 --- a/cmd/select/account/account.go +++ b/cmd/select/account/account.go @@ -3,7 +3,6 @@ package account import ( "errors" "fmt" - "os" "strings" "github.com/charmbracelet/huh" @@ -27,10 +26,8 @@ func AccountCmd() *cobra.Command { Use: "account", Short: "Select an account", Long: "List your logged-in accounts and select active one", - Run: func(cmd *cobra.Command, args []string) { - if err := accountMain(cmd, &opts); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return accountMain(cmd, &opts) }, } diff --git a/cmd/select/org/org.go b/cmd/select/org/org.go index 5faf467..67153d0 100644 --- a/cmd/select/org/org.go +++ b/cmd/select/org/org.go @@ -2,7 +2,6 @@ package org import ( "fmt" - "os" "github.com/fosrl/cli/internal/api" "github.com/fosrl/cli/internal/config" @@ -24,10 +23,8 @@ func OrgCmd() *cobra.Command { Use: "org", Short: "Select an organization", Long: "List your organizations and select one to use", - Run: func(cmd *cobra.Command, args []string) { - if err := orgMain(cmd, &opts); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return orgMain(cmd, &opts) }, } diff --git a/cmd/ssh/sign.go b/cmd/ssh/sign.go index 671fef1..7046c51 100644 --- a/cmd/ssh/sign.go +++ b/cmd/ssh/sign.go @@ -38,74 +38,80 @@ func SignCmd() *cobra.Command { opts.ResourceID = args[0] return nil }, - Run: func(c *cobra.Command, args []string) { - apiClient := api.FromContext(c.Context()) - accountStore := config.AccountStoreFromContext(c.Context()) - - orgID, err := utils.ResolveOrgID(accountStore, "") - if err != nil { - logger.Error("%v", err) - os.Exit(1) - } + RunE: func(c *cobra.Command, args []string) error { + return signRun(c, &opts) + }, + } - privPEM, _, cert, signData, err := GenerateAndSignKey(apiClient, orgID, opts.ResourceID) - if err != nil { - logger.Error("%v", err) - os.Exit(1) - } + cmd.Flags().StringVar(&opts.KeyFile, "key-file", "", "Path to write the private key (required)") + cmd.Args = cobra.ExactArgs(1) + cmd.Flags().StringVar(&opts.CertFile, "cert-file", "", "Path to write the certificate (default: -cert.pub)") - keyPath, err := filepath.Abs(opts.KeyFile) - if err != nil { - keyPath = opts.KeyFile - } - certPath := opts.CertFile - if certPath == "" { - certPath = keyPath + "-cert.pub" - } else { - certPath, err = filepath.Abs(certPath) - if err != nil { - certPath = opts.CertFile - } - } + return cmd +} - if err := os.WriteFile(keyPath, []byte(privPEM), 0o600); err != nil { - logger.Error("write key file: %v", err) - os.Exit(1) - } - if err := os.WriteFile(certPath, []byte(cert), 0o644); err != nil { - os.Remove(keyPath) - logger.Error("write certificate file: %v", err) - os.Exit(1) - } +func signRun(c *cobra.Command, opts *struct { + ResourceID string + KeyFile string + CertFile string +}) error { + apiClient := api.FromContext(c.Context()) + accountStore := config.AccountStoreFromContext(c.Context()) - logger.Success("Private key: %s", keyPath) - logger.Success("Certificate: %s", certPath) - fmt.Println() + orgID, err := utils.ResolveOrgID(accountStore, "") + if err != nil { + return err + } + + privPEM, _, cert, signData, err := GenerateAndSignKey(apiClient, orgID, opts.ResourceID) + if err != nil { + return err + } - // Certificate details table - utils.PrintTable([]string{"Field", "Value"}, signCertTableRows(signData)) - fmt.Println() + keyPath, err := filepath.Abs(opts.KeyFile) + if err != nil { + keyPath = opts.KeyFile + } + certPath := opts.CertFile + if certPath == "" { + certPath = keyPath + "-cert.pub" + } else { + certPath, err = filepath.Abs(certPath) + if err != nil { + certPath = opts.CertFile + } + } - hostname := signData.Hostname - if hostname == "" { - hostname = "" - } - user := signData.User - if user == "" { - user = "" - } - fmt.Println("Usage with system ssh (scp, tunnels, etc.):") - fmt.Printf(" ssh -i %q -o CertificateFile=%q %s@%s\n", keyPath, certPath, user, hostname) - fmt.Printf(" ssh -i %q -o CertificateFile=%q -L 8080:127.0.0.1:80 -N %s@%s\n", keyPath, certPath, user, hostname) - fmt.Printf(" scp -i %q -o CertificateFile=%q ...\n", keyPath, certPath) - }, + if err := os.WriteFile(keyPath, []byte(privPEM), 0o600); err != nil { + return fmt.Errorf("write key file: %w", err) + } + if err := os.WriteFile(certPath, []byte(cert), 0o644); err != nil { + os.Remove(keyPath) + return fmt.Errorf("write certificate file: %w", err) } - cmd.Flags().StringVar(&opts.KeyFile, "key-file", "", "Path to write the private key (required)") - cmd.Args = cobra.ExactArgs(1) - cmd.Flags().StringVar(&opts.CertFile, "cert-file", "", "Path to write the certificate (default: -cert.pub)") + logger.Success("Private key: %s", keyPath) + logger.Success("Certificate: %s", certPath) + fmt.Println() - return cmd + // Certificate details table + utils.PrintTable([]string{"Field", "Value"}, signCertTableRows(signData)) + fmt.Println() + + hostname := signData.Hostname + if hostname == "" { + hostname = "" + } + user := signData.User + if user == "" { + user = "" + } + fmt.Println("Usage with system ssh (scp, tunnels, etc.):") + fmt.Printf(" ssh -i %q -o CertificateFile=%q %s@%s\n", keyPath, certPath, user, hostname) + fmt.Printf(" ssh -i %q -o CertificateFile=%q -L 8080:127.0.0.1:80 -N %s@%s\n", keyPath, certPath, user, hostname) + fmt.Printf(" scp -i %q -o CertificateFile=%q ...\n", keyPath, certPath) + + return nil } // signCertTableRows builds table rows for certificate metadata (Key ID, principals, valid after/before, expires in). diff --git a/cmd/ssh/ssh.go b/cmd/ssh/ssh.go index 6880d48..4adde46 100644 --- a/cmd/ssh/ssh.go +++ b/cmd/ssh/ssh.go @@ -13,9 +13,9 @@ import ( ) var ( - errHostnameRequired = errors.New("API did not return a hostname for the connection") - errResourceIDRequired = errors.New("Resource (alias or identifier) is required; example: pangolin ssh my-server.internal") - errNoClientRunning = errors.New("No client is currently running. Start the client first.") + errHostnameRequired = errors.New("API did not return a hostname for the connection") + errResourceIDRequired = errors.New("Resource (alias or identifier) is required; example: pangolin ssh my-server.internal") + errNoClientRunning = errors.New("No client is currently running. Start the client first.") ) func SSHCmd() *cobra.Command { @@ -43,88 +43,94 @@ Set PANGOLIN_SSH_BINARY to the full path of ssh(1) to override PATH lookup on al opts.ResourceID = args[0] return nil }, - Run: func(c *cobra.Command, args []string) { - client := olm.NewClient("") - if !client.IsRunning() { - logger.Error("%v", errNoClientRunning) - os.Exit(1) - } + RunE: func(c *cobra.Command, args []string) error { + return sshRun(c, &opts, args) + }, + } - apiClient := api.FromContext(c.Context()) - accountStore := config.AccountStoreFromContext(c.Context()) + cmd.Flags().BoolVar(&opts.Builtin, "builtin", false, "Use the built-in SSH client instead of the system OpenSSH binary (interactive shell only)") + cmd.Flags().IntVarP(&opts.Port, "port", "p", 0, "Remote SSH port (default: 22)") - // init a jit connection to the site if we need to because we might not be connected - _, err := client.JITConnectByResourceID(opts.ResourceID) - if err != nil { - logger.Warning("%v", err) // we pass through this warning for backward compatibility with older olm api servers - } + cmd.AddCommand(SignCmd()) - orgID, err := utils.ResolveOrgID(accountStore, "") - if err != nil { - logger.Error("%v", err) - os.Exit(1) - } + return cmd +} - privPEM, _, cert, signData, err := GenerateAndSignKey(apiClient, orgID, opts.ResourceID) - if err != nil { - logger.Error("%v", err) - os.Exit(1) - } - if signData == nil || signData.Hostname == "" { - logger.Error("%v", errHostnameRequired) - os.Exit(1) - } +func sshRun(c *cobra.Command, opts *struct { + ResourceID string + Builtin bool + Port int +}, args []string) error { + client := olm.NewClient("") + if !client.IsRunning() { + return errNoClientRunning + } - siteIDs := []int{} - if signData.SiteID != 0 { - siteIDs = append(siteIDs, signData.SiteID) - } - for _, id := range signData.SiteIDs { - if id != 0 { - siteIDs = append(siteIDs, id) - } - } + apiClient := api.FromContext(c.Context()) + accountStore := config.AccountStoreFromContext(c.Context()) - if len(siteIDs) > 0 { // older versions of the server did not send back the site id so we need to check for backward compatibility - if err := waitForAnySiteConnection(client, siteIDs); err != nil { - logger.Error("%v", err) - os.Exit(1) - } - } + // init a jit connection to the site if we need to because we might not be connected + _, err := client.JITConnectByResourceID(opts.ResourceID) + if err != nil { + logger.Warning("%v", err) // we pass through this warning for backward compatibility with older olm api servers + } - passThrough := mergePassThrough(os.Args, opts.ResourceID, args[1:]) - pt := ParseOpenSSHPassThrough(passThrough) - runOpts := RunOpts{ - User: signData.User, - Hostname: signData.Hostname, - Port: opts.Port, - PrivateKeyPEM: privPEM, - Certificate: cert, - SSHPassthrough: pt, - } + orgID, err := utils.ResolveOrgID(accountStore, "") + if err != nil { + return err + } - useBuiltin := opts.Builtin - if len(passThrough) > 0 && useBuiltin { - logger.Warning("Extra arguments after the resource are ignored by the built-in client (port forwarding, remote commands, and other ssh(1) options). Omit --builtin to use the system OpenSSH client.") - } - var exitCode int - if useBuiltin { - exitCode, err = RunNative(runOpts) - } else { - exitCode, err = RunExec(runOpts) - } - if err != nil { - logger.Error("%v", err) - os.Exit(1) - } - os.Exit(exitCode) - }, + privPEM, _, cert, signData, err := GenerateAndSignKey(apiClient, orgID, opts.ResourceID) + if err != nil { + return err + } + if signData == nil || signData.Hostname == "" { + return errHostnameRequired } - cmd.Flags().BoolVar(&opts.Builtin, "builtin", false, "Use the built-in SSH client instead of the system OpenSSH binary (interactive shell only)") - cmd.Flags().IntVarP(&opts.Port, "port", "p", 0, "Remote SSH port (default: 22)") + siteIDs := []int{} + if signData.SiteID != 0 { + siteIDs = append(siteIDs, signData.SiteID) + } + for _, id := range signData.SiteIDs { + if id != 0 { + siteIDs = append(siteIDs, id) + } + } - cmd.AddCommand(SignCmd()) + if len(siteIDs) > 0 { // older versions of the server did not send back the site id so we need to check for backward compatibility + if err := waitForAnySiteConnection(client, siteIDs); err != nil { + return err + } + } - return cmd + passThrough := mergePassThrough(os.Args, opts.ResourceID, args[1:]) + pt := ParseOpenSSHPassThrough(passThrough) + runOpts := RunOpts{ + User: signData.User, + Hostname: signData.Hostname, + Port: opts.Port, + PrivateKeyPEM: privPEM, + Certificate: cert, + SSHPassthrough: pt, + } + + useBuiltin := opts.Builtin + if len(passThrough) > 0 && useBuiltin { + logger.Warning("Extra arguments after the resource are ignored by the built-in client (port forwarding, remote commands, and other ssh(1) options). Omit --builtin to use the system OpenSSH client.") + } + + var exitCode int + if useBuiltin { + exitCode, err = RunNative(runOpts) + } else { + exitCode, err = RunExec(runOpts) + } + if err != nil { + return err + } + if exitCode != 0 { + os.Exit(exitCode) + } + return nil } diff --git a/cmd/status/client/client.go b/cmd/status/client/client.go index 1ccd925..e5b982c 100644 --- a/cmd/status/client/client.go +++ b/cmd/status/client/client.go @@ -3,7 +3,6 @@ package client import ( "encoding/json" "fmt" - "os" "time" "github.com/fosrl/cli/internal/logger" @@ -23,10 +22,8 @@ func ClientStatusCmd() *cobra.Command { Use: "client", Short: "Show client status", Long: "Display current client connection status and peer information", - Run: func(cmd *cobra.Command, args []string) { - if err := clientStatusMain(&opts); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return clientStatusMain(&opts) }, } diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index f497abb..209d637 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -105,10 +105,8 @@ func ClientUpCmd() *cobra.Command { return nil }, - Run: func(cmd *cobra.Command, args []string) { - if err := clientUpMain(cmd, &opts, args); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return clientUpMain(cmd, &opts, args) }, } diff --git a/cmd/update/update_unix.go b/cmd/update/update_unix.go index 23f3d36..ed63341 100644 --- a/cmd/update/update_unix.go +++ b/cmd/update/update_unix.go @@ -15,10 +15,8 @@ func UpdateCmd() *cobra.Command { Use: "update", Short: "Update Pangolin CLI to the latest version", Long: "Update Pangolin CLI to the latest version by downloading and running the installation script", - Run: func(cmd *cobra.Command, args []string) { - if err := updateMain(); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return updateMain() }, } diff --git a/cmd/update/update_windows.go b/cmd/update/update_windows.go index 97f158b..3f78e7f 100644 --- a/cmd/update/update_windows.go +++ b/cmd/update/update_windows.go @@ -39,10 +39,8 @@ func UpdateCmd() *cobra.Command { Use: "update", Short: "Update Pangolin CLI to the latest version", Long: "Update Pangolin CLI to the latest version by downloading the new installer from GitHub", - Run: func(cmd *cobra.Command, args []string) { - if err := updateMain(windowsUpdateRepo); err != nil { - os.Exit(1) - } + RunE: func(cmd *cobra.Command, args []string) error { + return updateMain(windowsUpdateRepo) }, } cmd.Flags().StringVar(&windowsUpdateRepo, "repo", windowsUpdateRepo, "GitHub repository in owner/name format") diff --git a/cmd/version/version.go b/cmd/version/version.go index 2ab9a8c..8519c4d 100644 --- a/cmd/version/version.go +++ b/cmd/version/version.go @@ -13,8 +13,9 @@ func VersionCmd() *cobra.Command { Use: "version", Short: "Print the version number", Long: "Print the version number and check for updates", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { versionMain() + return nil }, } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 44744f1..83c73cd 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -121,9 +121,9 @@ func (l *Logger) Error(format string, args ...any) { func (l *Logger) Warning(format string, args ...any) { message := fmt.Sprintf(format, args...) // icon := colorWarningStyle.Render(iconWarning) - fmt.Printf("%s", message) + fmt.Fprintf(os.Stderr, "%s", message) if !strings.HasSuffix(message, "\n") { - fmt.Println() + fmt.Fprintln(os.Stderr) } }