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) } }