Add PR checks command, iostreams/text packages for colored table output, top-level run/workflow aliases matching gh CLI structure. Enhance actions, issues, PRs, releases, repos, labels, milestones, and wiki commands with improved flags, JSON output, and error handling.
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"forgejo.zerova.net/sid/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)
|
|
}
|