gitlink-cli/internal/errors/errors.go

151 lines
5.1 KiB
Go

package errors
import (
"fmt"
"strings"
)
// ErrorKind categorizes errors by user-actionability.
type ErrorKind string
const (
KindAuth ErrorKind = "auth" // Login/token issues — user can re-login
KindInput ErrorKind = "input" // Parameter issues — user can fix arguments
KindConfig ErrorKind = "config" // Config file issues — user can edit config
KindNetwork ErrorKind = "network" // Network issues — user can check/retry
KindGit ErrorKind = "git" // Git repo issues — user needs correct directory
KindServer ErrorKind = "server" // Server-side error — user should wait or contact admin
KindNotFound ErrorKind = "not_found" // Resource not found — user can check ID
KindForbidden ErrorKind = "forbidden" // Permission denied — user can request access
KindUnknown ErrorKind = "unknown" // Unclassified error
)
// CLIError is the unified CLI error type with multi-layered information.
type CLIError struct {
Kind ErrorKind // Error category for programmatic handling
Message string // Human-readable description of what went wrong
Detail string // Low-level technical detail (shown in debug mode)
Suggestion string // Actionable advice for the user
Command string // The command that triggered the error (e.g., "issue +create")
Cause error // The underlying error
}
func (e *CLIError) Error() string {
var b strings.Builder
// Header line: kind + command
b.WriteString(string(e.Kind))
b.WriteString(" error")
if e.Command != "" {
b.WriteString(" — ")
b.WriteString(e.Command)
}
// Body: message
if e.Message != "" {
b.WriteString("\n\n reason: ")
b.WriteString(e.Message)
}
// Suggestion
if e.Suggestion != "" {
b.WriteString("\n suggestion: ")
b.WriteString(e.Suggestion)
}
// Detail (always included in Error() so users see the raw cause)
if e.Detail != "" {
b.WriteString("\n detail: ")
b.WriteString(e.Detail)
}
return b.String()
}
func (e *CLIError) Unwrap() error {
return e.Cause
}
// New creates a CLIError with the given parameters.
func New(kind ErrorKind, message, suggestion string) *CLIError {
return &CLIError{
Kind: kind,
Message: message,
Suggestion: suggestion,
}
}
// Wrap creates a CLIError that wraps an underlying cause.
func Wrap(kind ErrorKind, message, suggestion string, cause error) *CLIError {
return &CLIError{
Kind: kind,
Message: message,
Suggestion: suggestion,
Cause: cause,
Detail: cause.Error(),
}
}
// WithCommand sets the command context on the error.
func (e *CLIError) WithCommand(cmd string) *CLIError {
e.Command = cmd
return e
}
// InputError is a convenience constructor for parameter errors.
func InputError(message, suggestion string) *CLIError {
return New(KindInput, message, suggestion)
}
// AuthError is a convenience constructor for authentication errors.
func AuthError(message, suggestion string) *CLIError {
return New(KindAuth, message, suggestion)
}
// ConfigError creates a config-related error with the config file path in the suggestion.
func ConfigError(message string, cause error) *CLIError {
return Wrap(KindConfig, message,
fmt.Sprintf("检查配置文件 %s 是否正确", configPathPlaceholder()), cause)
}
// OpError creates a unified operation-failure error.
// op is the English verb (e.g., "list", "create"), resource is the target (e.g., "issues").
// The Message is in English; Suggestion is in Chinese for user guidance.
func OpError(kind ErrorKind, op, resource string, cause error) *CLIError {
msg := fmt.Sprintf("failed to %s %s", op, resource)
sugg := opSuggestion(op, resource)
e := Wrap(kind, msg, sugg, cause)
return e
}
// opSuggestion returns a Chinese suggestion for the given operation.
func opSuggestion(op, resource string) string {
suggestions := map[string]string{
"list": "获取列表失败,请检查参数或网络连接,稍后重试",
"create": "创建失败,请检查必填参数是否正确(--help 查看用法)或 API 权限",
"view": "查看失败,请确认资源 ID 是否存在",
"update": "更新失败,请检查参数值或资源 ID 是否正确",
"delete": "删除失败,请确认资源是否存在或是否有删除权限",
"close": "关闭失败,请确认资源是否存在或已被关闭",
"reopen": "重新打开失败,请确认资源是否存在",
"merge": "合并失败,请检查是否有冲突或权限不足",
"comment": "添加评论失败,请确认资源是否存在",
"approve": "评审操作失败,请确认 PR 是否存在",
"scan": "扫描失败,请稍后重试",
"invite": "邀请失败,请确认用户 ID 是否正确",
"remove": "移除失败,请确认成员存在",
"fork": "Fork 失败,请确认仓库存在或有权限",
"search": "搜索失败,请稍后重试",
}
if s, ok := suggestions[op]; ok {
return s
}
return fmt.Sprintf("操作失败,请稍后重试或运行 --help 查看用法")
}
// configPathPlaceholder avoids circular import; the actual path will be resolved
// in output formatting.
func configPathPlaceholder() string {
return "~/.config/gitlink-cli/config.yaml"
}