Move from forgejo.zerova.net/sid/fgj-sid to forgejo.zerova.net/public/fgj-sid to reflect the new public org.
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"forgejo.zerova.net/public/fgj-sid/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: fgj 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: fgj 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)
|
|
}
|