diff --git a/veans/internal/output/errors.go b/veans/internal/output/errors.go new file mode 100644 index 000000000..ca1e955b4 --- /dev/null +++ b/veans/internal/output/errors.go @@ -0,0 +1,69 @@ +// Package output defines stable error codes and the JSON envelope +// veans uses for non-zero exits. +package output + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +type Code string + +const ( + CodeNotFound Code = "NOT_FOUND" + CodeConflict Code = "CONFLICT" + CodeValidation Code = "VALIDATION_ERROR" + CodeAuth Code = "AUTH_ERROR" + CodeRateLimited Code = "RATE_LIMITED" + CodeBotUsersUnavailable Code = "BOT_USERS_UNAVAILABLE" + CodeNotConfigured Code = "NOT_CONFIGURED" + CodeUnknown Code = "UNKNOWN" +) + +// Error is the structured error type used for both internal flow and the +// `--json` mutation envelope. Callers wrap underlying errors with codes via +// Wrap; the cobra runner converts unmapped errors to CodeUnknown. +type Error struct { + Code Code `json:"code"` + Message string `json:"error"` + Cause error `json:"-"` +} + +func (e *Error) Error() string { return e.Message } +func (e *Error) Unwrap() error { return e.Cause } + +func New(code Code, format string, args ...any) *Error { + return &Error{Code: code, Message: fmt.Sprintf(format, args...)} +} + +func Wrap(code Code, cause error, format string, args ...any) *Error { + return &Error{Code: code, Message: fmt.Sprintf(format, args...), Cause: cause} +} + +// AsError extracts an *Error from any error chain, returning a CodeUnknown +// wrapper for plain errors. +func AsError(err error) *Error { + if err == nil { + return nil + } + if e, ok := err.(*Error); ok { + return e + } + return &Error{Code: CodeUnknown, Message: err.Error(), Cause: err} +} + +// EmitError writes the JSON envelope when --json is set, or a plain message +// otherwise. Always to stderr so stdout stays parseable. +func EmitError(jsonMode bool, err error, w io.Writer) { + if w == nil { + w = os.Stderr + } + e := AsError(err) + if jsonMode { + _ = json.NewEncoder(w).Encode(e) + return + } + fmt.Fprintf(w, "veans: %s: %s\n", e.Code, e.Message) +}