forked from Gitlink/gitlink-cli
151 lines
5.1 KiB
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"
|
|
}
|