fj/cmd/errors.go
sid bc43f6e5a5 rename fgj to fj
Module path, binary name, config dir, help text, and docs
all updated from fgj-sid/fgj to fj.
2026-04-26 08:16:52 -06:00

116 lines
3.2 KiB
Go

package cmd
import (
"encoding/json"
"errors"
"fmt"
"strings"
"forgejo.zerova.net/public/fj/internal/api"
)
// Error codes for structured error output.
const (
ErrAuthRequired = "auth_required"
ErrNotFound = "not_found"
ErrAPIError = "api_error"
ErrInvalidInput = "invalid_input"
ErrGitDetectionFailed = "git_detection_failed"
ErrNetworkError = "network_error"
)
// CLIError is a structured error type for machine-readable output.
type CLIError struct {
Code string `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
Status int `json:"status,omitempty"`
}
func (e *CLIError) Error() string {
return e.Message
}
// NewCLIError creates a new CLIError with the given code and message.
func NewCLIError(code, message string) *CLIError {
return &CLIError{Code: code, Message: message}
}
// NewAPIError creates a CLIError from an HTTP status and message.
func NewAPIError(status int, message string) *CLIError {
return &CLIError{Code: ErrAPIError, Message: message, Status: status}
}
// ContextualError wraps common errors with helpful hints.
func ContextualError(err error) error {
if err == nil {
return nil
}
msg := err.Error()
// Check for API errors with status codes
var apiErr *api.APIError
if errors.As(err, &apiErr) {
switch {
case apiErr.StatusCode == 401 || apiErr.StatusCode == 403:
return fmt.Errorf("%w\nHint: Try authenticating with: fj auth login", err)
case apiErr.StatusCode == 404:
return fmt.Errorf("%w\nHint: Resource not found. Check the repository and number are correct.", err)
}
return err
}
// Check for network/connection errors
switch {
case strings.Contains(msg, "no such host"):
return fmt.Errorf("%w\nHint: Check your internet connection and that the host is correct.", err)
case strings.Contains(msg, "connection refused"):
return fmt.Errorf("%w\nHint: Check your internet connection and that the host is correct.", err)
}
// Check for string-based status code patterns (from wrapped errors)
switch {
case strings.Contains(msg, "401") || strings.Contains(msg, "403"):
if strings.Contains(msg, "authentication") || strings.Contains(msg, "unauthorized") || strings.Contains(msg, "forbidden") {
return fmt.Errorf("%w\nHint: Try authenticating with: fj auth login", err)
}
}
return err
}
// writeJSONError writes a structured JSON error to stderr.
// It attempts to extract structured info from known error types.
// WriteJSONError writes a structured JSON error to stderr.
// It is exported for use from main.go.
func WriteJSONError(err error) {
cliErr := &CLIError{
Code: ErrAPIError,
Message: err.Error(),
}
// Try to extract structured info from the error chain.
var apiErr *api.APIError
var cErr *CLIError
switch {
case errors.As(err, &cErr):
cliErr = cErr
case errors.As(err, &apiErr):
cliErr.Status = apiErr.StatusCode
cliErr.Detail = apiErr.Body
switch {
case apiErr.StatusCode == 401 || apiErr.StatusCode == 403:
cliErr.Code = ErrAuthRequired
case apiErr.StatusCode == 404:
cliErr.Code = ErrNotFound
default:
cliErr.Code = ErrAPIError
}
}
enc := json.NewEncoder(ios.ErrOut)
enc.SetIndent("", " ")
_ = enc.Encode(cliErr)
}