Standardize CLI Error Handling (#318)
* Standardize CLI error handling and deprecate cli.Fatal - Updated `pkg/cli/output.go` to send error and warning output to `os.Stderr`. - Added `ErrorWrap`, `ErrorWrapVerb`, and `ErrorWrapAction` helpers to `pkg/cli/output.go`. - Deprecated `cli.Fatal` family of functions in `pkg/cli/errors.go`. - Introduced `cli.ExitError` and `cli.Exit` helper to allow commands to return specific exit codes. - Updated `pkg/cli/app.go` to silence Cobra errors and handle error printing and process exit in `Main`. - Refactored multiple commands (QA, SDK, CI, Updater) to return errors instead of exiting abruptly. - Replaced direct `os.Stderr` writes with standardized CLI or log helpers across the codebase. - Updated tests to accommodate changes in output destination. * Fix CI failure: remove unused fmt import in pkg/mcp/transport_tcp.go - Removed unused "fmt" import in `pkg/mcp/transport_tcp.go` that was causing CI failure. - Verified build and relevant tests pass. * Standardize CLI error handling and fix formatting issues - Updated `pkg/cli/output.go` to send error and warning output to `os.Stderr`. - Added `ErrorWrap`, `ErrorWrapVerb`, and `ErrorWrapAction` helpers to `pkg/cli/output.go`. - Deprecated `cli.Fatal` family of functions in `pkg/cli/errors.go`. - Introduced `cli.ExitError` and `cli.Exit` helper to allow commands to return specific exit codes. - Updated `pkg/cli/app.go` to silence Cobra errors and handle error printing and process exit in `Main`. - Refactored multiple commands (QA, SDK, CI, Updater) to return errors instead of exiting abruptly. - Replaced direct `os.Stderr` writes with standardized CLI or log helpers across the codebase. - Updated tests to accommodate changes in output destination. - Fixed formatting in `pkg/io/local/client.go`. - Removed unused `fmt` import in `pkg/mcp/transport_tcp.go`. * Standardize CLI error handling and fix CI issues - Updated `pkg/cli/output.go` to send error and warning output to `os.Stderr`. - Added `ErrorWrap`, `ErrorWrapVerb`, and `ErrorWrapAction` helpers to `pkg/cli/output.go`. - Deprecated `cli.Fatal` family of functions in `pkg/cli/errors.go`. - Introduced `cli.ExitError` and `cli.Exit` helper to allow commands to return specific exit codes. - Updated `pkg/cli/app.go` to silence Cobra errors and handle error printing and process exit in `Main`. - Refactored multiple commands (QA, SDK, CI, Updater) to return errors instead of exiting abruptly. - Replaced direct `os.Stderr` writes with standardized CLI or log helpers across the codebase. - Updated tests to accommodate changes in output destination. - Fixed formatting in `pkg/io/local/client.go`. - Removed unused `fmt` import in `pkg/mcp/transport_tcp.go`. - Fixed potential `gh` context issue in `.github/workflows/auto-merge.yml` by providing `GH_REPO`. --------- Co-authored-by: Claude <developers@lethean.io>
This commit is contained in:
parent
1d73209e89
commit
10ea31e586
12 changed files with 107 additions and 39 deletions
|
|
@ -308,7 +308,7 @@ func runGoQA(cmd *cli.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
os.Exit(1)
|
return cli.Err("QA checks failed: %d passed, %d failed", passed, failed)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ func runPHPCI() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !result.Passed {
|
if !result.Passed {
|
||||||
os.Exit(result.ExitCode)
|
return cli.Exit(result.ExitCode, cli.Err("CI pipeline failed"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ func CheckDocblockCoverage(patterns []string) (*DocblockResult, error) {
|
||||||
}, parser.ParseComments)
|
}, parser.ParseComments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log parse errors but continue to check other directories
|
// Log parse errors but continue to check other directories
|
||||||
fmt.Fprintf(os.Stderr, "warning: failed to parse %s: %v\n", dir, err)
|
cli.Warnf("failed to parse %s: %v", dir, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ func runSDKDiff(basePath, specPath string) error {
|
||||||
|
|
||||||
result, err := Diff(basePath, specPath)
|
result, err := Diff(basePath, specPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.Label("error")), err)
|
return cli.Exit(2, cli.Wrap(err, i18n.Label("error")))
|
||||||
os.Exit(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Breaking {
|
if result.Breaking {
|
||||||
|
|
@ -105,7 +104,7 @@ func runSDKDiff(basePath, specPath string) error {
|
||||||
for _, change := range result.Changes {
|
for _, change := range result.Changes {
|
||||||
fmt.Printf(" - %s\n", change)
|
fmt.Printf(" - %s\n", change)
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
return cli.Exit(1, cli.Err("%s", result.Summary))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), result.Summary)
|
fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), result.Summary)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package updater
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/cli"
|
"github.com/host-uk/core/pkg/cli"
|
||||||
|
|
@ -133,8 +132,6 @@ func runUpdate(cmd *cobra.Command, args []string) error {
|
||||||
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
||||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||||
|
|
||||||
// Exit so the watcher can restart us
|
|
||||||
os.Exit(0)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +176,6 @@ func handleDevUpdate(currentVersion string) error {
|
||||||
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
cli.Print("%s Updated to %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), release.TagName)
|
||||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +212,5 @@ func handleDevTagUpdate(currentVersion string) error {
|
||||||
cli.Print("%s Updated to latest dev build\n", cli.SuccessStyle.Render(cli.Glyph(":check:")))
|
cli.Print("%s Updated to latest dev build\n", cli.SuccessStyle.Render(cli.Glyph(":check:")))
|
||||||
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
cli.Print("%s Restarting...\n", cli.DimStyle.Render("→"))
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error {
|
||||||
if err := e.gatherFacts(ctx, host, play); err != nil {
|
if err := e.gatherFacts(ctx, host, play); err != nil {
|
||||||
// Non-fatal
|
// Non-fatal
|
||||||
if e.Verbose > 0 {
|
if e.Verbose > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: gather facts failed for %s: %v\n", host, err)
|
log.Warn("gather facts failed", "host", host, "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,23 @@ func Main() {
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
Fatal(err)
|
Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer Shutdown()
|
defer Shutdown()
|
||||||
|
|
||||||
// Add completion command to the CLI's root
|
// Add completion command to the CLI's root
|
||||||
RootCmd().AddCommand(completionCmd)
|
RootCmd().AddCommand(completionCmd)
|
||||||
|
|
||||||
Fatal(Execute())
|
if err := Execute(); err != nil {
|
||||||
|
code := 1
|
||||||
|
var exitErr *ExitError
|
||||||
|
if As(err, &exitErr) {
|
||||||
|
code = exitErr.Code
|
||||||
|
}
|
||||||
|
Error(err.Error())
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// completionCmd generates shell completion scripts.
|
// completionCmd generates shell completion scripts.
|
||||||
|
|
|
||||||
|
|
@ -77,41 +77,75 @@ func Join(errs ...error) error {
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExitError represents an error that should cause the CLI to exit with a specific code.
|
||||||
|
type ExitError struct {
|
||||||
|
Code int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExitError) Error() string {
|
||||||
|
if e.Err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExitError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit creates a new ExitError with the given code and error.
|
||||||
|
// Use this to return an error from a command with a specific exit code.
|
||||||
|
func Exit(code int, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ExitError{Code: code, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Fatal Functions (print and exit)
|
// Fatal Functions (Deprecated - return error from command instead)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Fatal prints an error message and exits with code 1.
|
// Fatal prints an error message to stderr and exits with code 1.
|
||||||
|
//
|
||||||
|
// Deprecated: return an error from the command instead.
|
||||||
func Fatal(err error) {
|
func Fatal(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + err.Error()))
|
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalf prints a formatted error message and exits with code 1.
|
// Fatalf prints a formatted error message to stderr and exits with code 1.
|
||||||
|
//
|
||||||
|
// Deprecated: return an error from the command instead.
|
||||||
func Fatalf(format string, args ...any) {
|
func Fatalf(format string, args ...any) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg))
|
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+msg))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FatalWrap prints a wrapped error message and exits with code 1.
|
// FatalWrap prints a wrapped error message to stderr and exits with code 1.
|
||||||
// Does nothing if err is nil.
|
// Does nothing if err is nil.
|
||||||
//
|
//
|
||||||
|
// Deprecated: return an error from the command instead.
|
||||||
|
//
|
||||||
// cli.FatalWrap(err, "load config") // Prints "✗ load config: <error>" and exits
|
// cli.FatalWrap(err, "load config") // Prints "✗ load config: <error>" and exits
|
||||||
func FatalWrap(err error, msg string) {
|
func FatalWrap(err error, msg string) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
||||||
fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + fullMsg))
|
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FatalWrapVerb prints a wrapped error using i18n grammar and exits with code 1.
|
// FatalWrapVerb prints a wrapped error using i18n grammar to stderr and exits with code 1.
|
||||||
// Does nothing if err is nil.
|
// Does nothing if err is nil.
|
||||||
//
|
//
|
||||||
|
// Deprecated: return an error from the command instead.
|
||||||
|
//
|
||||||
// cli.FatalWrapVerb(err, "load", "config") // Prints "✗ Failed to load config: <error>" and exits
|
// cli.FatalWrapVerb(err, "load", "config") // Prints "✗ Failed to load config: <error>" and exits
|
||||||
func FatalWrapVerb(err error, verb, subject string) {
|
func FatalWrapVerb(err error, verb, subject string) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -119,6 +153,6 @@ func FatalWrapVerb(err error, verb, subject string) {
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, subject)
|
msg := i18n.ActionFailed(verb, subject)
|
||||||
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
||||||
fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + fullMsg))
|
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
|
@ -45,22 +46,48 @@ func Successf(format string, args ...any) {
|
||||||
Success(fmt.Sprintf(format, args...))
|
Success(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error prints an error message with cross (red).
|
// Error prints an error message with cross (red) to stderr.
|
||||||
func Error(msg string) {
|
func Error(msg string) {
|
||||||
fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg))
|
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf prints a formatted error message.
|
// Errorf prints a formatted error message to stderr.
|
||||||
func Errorf(format string, args ...any) {
|
func Errorf(format string, args ...any) {
|
||||||
Error(fmt.Sprintf(format, args...))
|
Error(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn prints a warning message with warning symbol (amber).
|
// ErrorWrap prints a wrapped error message to stderr.
|
||||||
func Warn(msg string) {
|
func ErrorWrap(err error, msg string) {
|
||||||
fmt.Println(WarningStyle.Render(Glyph(":warn:") + " " + msg))
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Error(fmt.Sprintf("%s: %v", msg, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf prints a formatted warning message.
|
// ErrorWrapVerb prints a wrapped error using i18n grammar to stderr.
|
||||||
|
func ErrorWrapVerb(err error, verb, subject string) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := i18n.ActionFailed(verb, subject)
|
||||||
|
Error(fmt.Sprintf("%s: %v", msg, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWrapAction prints a wrapped error using i18n grammar to stderr.
|
||||||
|
func ErrorWrapAction(err error, verb string) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := i18n.ActionFailed(verb, "")
|
||||||
|
Error(fmt.Sprintf("%s: %v", msg, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn prints a warning message with warning symbol (amber) to stderr.
|
||||||
|
func Warn(msg string) {
|
||||||
|
fmt.Fprintln(os.Stderr, WarningStyle.Render(Glyph(":warn:")+" "+msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf prints a formatted warning message to stderr.
|
||||||
func Warnf(format string, args ...any) {
|
func Warnf(format string, args ...any) {
|
||||||
Warn(fmt.Sprintf(format, args...))
|
Warn(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func captureOutput(f func()) string {
|
func captureOutput(f func()) string {
|
||||||
old := os.Stdout
|
oldOut := os.Stdout
|
||||||
|
oldErr := os.Stderr
|
||||||
r, w, _ := os.Pipe()
|
r, w, _ := os.Pipe()
|
||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
f()
|
f()
|
||||||
|
|
||||||
_ = w.Close()
|
_ = w.Close()
|
||||||
os.Stdout = old
|
os.Stdout = oldOut
|
||||||
|
os.Stderr = oldErr
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
_, _ = io.Copy(&buf, r)
|
_, _ = io.Copy(&buf, r)
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ func Init(opts Options) error {
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: opts.AppName,
|
Use: opts.AppName,
|
||||||
Version: opts.Version,
|
Version: opts.Version,
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach all registered commands
|
// Attach all registered commands
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ package mcp
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/log"
|
||||||
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +48,7 @@ func (s *Service) ServeTCP(ctx context.Context, addr string) error {
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
addr = t.listener.Addr().String()
|
addr = t.listener.Addr().String()
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "MCP TCP server listening on %s\n", addr)
|
log.Info("MCP TCP server listening", "addr", addr)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := t.listener.Accept()
|
conn, err := t.listener.Accept()
|
||||||
|
|
@ -58,7 +57,7 @@ func (s *Service) ServeTCP(ctx context.Context, addr string) error {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "Accept error: %v\n", err)
|
log.Error("Accept error", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +83,7 @@ func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
|
||||||
// Run server (blocks until connection closed)
|
// Run server (blocks until connection closed)
|
||||||
// Server.Run calls Connect, then Read loop.
|
// Server.Run calls Connect, then Read loop.
|
||||||
if err := server.Run(ctx, transport); err != nil {
|
if err := server.Run(ctx, transport); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Connection error: %v\n", err)
|
log.Error("Connection error", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue