// Package errors defines nightme's unified error code surface.
//
// Every command exits with a small, stable integer code defined
// here. The mapping lets shell scripts (CI, cron, systemd) branch
// on what went wrong without parsing free-form error strings.
//
// Convention:
//
// - 0 success
// - 1 generic / unmapped error (default fallback)
// - 2..9 stable category codes
//
// Callers should:
//
// - Wrap concrete failures in CodedError{Code: ..., Message: ..., Cause: err}
// at the lowest layer that knows the category.
// - Use ExitCode(err) at the top-level to derive the process
// exit code (unknown errors fall back to CodeGenericError).
package errors
import (
stderrors "errors"
"fmt"
)
// Exit code constants. Keep the list narrow and stable — adding
// new codes mid-cycle would break consumers that pattern-match on
// these integers.
const (
CodeSuccess = 0
CodeGenericError = 1
CodeConfigError = 2
CodeAuthError = 3
CodeChannelError = 4
CodeSessionError = 5
CodeAgentError = 6
CodeBridgeError = 7
CodeValidationError = 8
CodeNotFound = 9
)
// CodedError carries an exit code alongside a human message and an
// optional underlying cause. It satisfies the standard error
// interface and unwraps to its Cause so errors.Is / errors.As work
// as callers expect.
type CodedError struct {
Code int
Message string
Cause error
}
// Error renders the message plus, when present, the cause. The
// code is intentionally omitted from the string — the exit code is
// metadata, not diagnostic text.
func (e *CodedError) Error() string {
if e == nil {
return "<nil coded error>"
}
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
}
return e.Message
}
// Unwrap returns the wrapped cause so errors.Is / errors.As can
// reach through a CodedError to the original failure.
func (e *CodedError) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
// New constructs a CodedError with the given code and message.
// Use Wrap to attach an underlying cause.
func New(code int, message string) *CodedError {
return &CodedError{Code: code, Message: message}
}
// Wrap attaches a cause to a code + message. A nil cause returns
// a plain CodedError (no wrap needed).
func Wrap(code int, message string, cause error) *CodedError {
return &CodedError{Code: code, Message: message, Cause: cause}
}
// ExitCode walks err and any wrapped cause, returning the first
// CodedError's Code it encounters. A nil error yields
// CodeSuccess; a non-coded error falls back to CodeGenericError.
//
// Multiple nested CodedErrors return the OUTERMOST code — the
// one closest to the call site, which is what shell users expect.
func ExitCode(err error) int {
if err == nil {
return CodeSuccess
}
var coded *CodedError
if stderrors.As(err, &coded) {
return coded.Code
}
return CodeGenericError
}
// From is a convenience for one-call construction where the caller
// already has a code and cause. Returns nil when cause is nil so
// callers can use it inline without a guard.
func From(code int, cause error) error {
if cause == nil {
return nil
}
return Wrap(code, codeMessage(code), cause)
}
// codeMessage returns a stable human label for a code. The label
// is also used by From so callers don't need to invent their own
// wording. Unknown codes fall back to "error".
func codeMessage(code int) string {
switch code {
case CodeConfigError:
return "config error"
case CodeAuthError:
return "auth error"
case CodeChannelError:
return "channel error"
case CodeSessionError:
return "session error"
case CodeAgentError:
return "agent error"
case CodeBridgeError:
return "bridge error"
case CodeValidationError:
return "validation error"
case CodeNotFound:
return "not found"
default:
return "error"
}
}