go/pkg/cli/errors.go
Snider 74256fb708
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>
2026-02-05 06:55:49 +00:00

158 lines
5.4 KiB
Go

package cli
import (
"errors"
"fmt"
"os"
"github.com/host-uk/core/pkg/i18n"
)
// ─────────────────────────────────────────────────────────────────────────────
// Error Creation (replace fmt.Errorf)
// ─────────────────────────────────────────────────────────────────────────────
// Err creates a new error from a format string.
// This is a direct replacement for fmt.Errorf.
func Err(format string, args ...any) error {
return fmt.Errorf(format, args...)
}
// Wrap wraps an error with a message.
// Returns nil if err is nil.
//
// return cli.Wrap(err, "load config") // "load config: <original error>"
func Wrap(err error, msg string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", msg, err)
}
// WrapVerb wraps an error using i18n grammar for "Failed to verb subject".
// Uses the i18n.ActionFailed function for proper grammar composition.
// Returns nil if err is nil.
//
// return cli.WrapVerb(err, "load", "config") // "Failed to load config: <original error>"
func WrapVerb(err error, verb, subject string) error {
if err == nil {
return nil
}
msg := i18n.ActionFailed(verb, subject)
return fmt.Errorf("%s: %w", msg, err)
}
// WrapAction wraps an error using i18n grammar for "Failed to verb".
// Uses the i18n.ActionFailed function for proper grammar composition.
// Returns nil if err is nil.
//
// return cli.WrapAction(err, "connect") // "Failed to connect: <original error>"
func WrapAction(err error, verb string) error {
if err == nil {
return nil
}
msg := i18n.ActionFailed(verb, "")
return fmt.Errorf("%s: %w", msg, err)
}
// ─────────────────────────────────────────────────────────────────────────────
// Error Helpers
// ─────────────────────────────────────────────────────────────────────────────
// Is reports whether any error in err's tree matches target.
// This is a re-export of errors.Is for convenience.
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As finds the first error in err's tree that matches target.
// This is a re-export of errors.As for convenience.
func As(err error, target any) bool {
return errors.As(err, target)
}
// Join returns an error that wraps the given errors.
// This is a re-export of errors.Join for convenience.
func Join(errs ...error) error {
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 (Deprecated - return error from command instead)
// ─────────────────────────────────────────────────────────────────────────────
// Fatal prints an error message to stderr and exits with code 1.
//
// Deprecated: return an error from the command instead.
func Fatal(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+err.Error()))
os.Exit(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) {
msg := fmt.Sprintf(format, args...)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+msg))
os.Exit(1)
}
// FatalWrap prints a wrapped error message to stderr and exits with code 1.
// 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
func FatalWrap(err error, msg string) {
if err == nil {
return
}
fullMsg := fmt.Sprintf("%s: %v", msg, err)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
os.Exit(1)
}
// FatalWrapVerb prints a wrapped error using i18n grammar to stderr and exits with code 1.
// 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
func FatalWrapVerb(err error, verb, subject string) {
if err == nil {
return
}
msg := i18n.ActionFailed(verb, subject)
fullMsg := fmt.Sprintf("%s: %v", msg, err)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
os.Exit(1)
}