pmpx 0.0.0

One command surface for a project's package managers. Detects the project type, routes the verb, and spawns the real tool (cargo, pnpm, ...).
//! 错误类型与退出码。
//!
//! # 为什么错误要带退出码
//!
//! 设计文档 6.5 把退出码定成了一张表,而那张表是**用户脚本能依赖的接口**:
//! `pmpx test || echo failed` 必须能工作,`pmpx install` 检测不到项目时也不该报成
//! "pmpx 崩了"。所以退出码不能在 `main` 里靠匹配字符串猜 —— 它得是错误类型的一部分。
//!
//! 唯一的例外是**后端进程的退出码原样透传**(`pmpx test` 返回测试失败的码)。
//! 那不是错误路径,是正常流程,所以它走 `Ok(code)` 而不是 `Err`。

/// pmpx 的成功/失败结果。
pub type Result<T> = std::result::Result<T, PmpxError>;

/// 退出码 0:成功。
pub const EXIT_OK: u8 = 0;
/// 退出码 1:pmpx 自身错误(配置解析失败、文件锁超时、插件删除冲突、self update 失败)。
pub const EXIT_INTERNAL: u8 = 1;
/// 退出码 2:用法错误 / 不支持的动词。
pub const EXIT_USAGE: u8 = 2;
/// 退出码 3:检测不到项目类型 / 缺少能处理该生态的插件 / 后端可执行文件找不到。
pub const EXIT_NOT_FOUND: u8 = 3;

/// pmpx 的错误。
///
/// 三个变体对应 6.5 表里那三个**由 pmpx 自己产生**的码。第四个码(后端退出码)
/// 不是错误,见文件头。
#[derive(Debug, thiserror::Error)]
pub enum PmpxError {
    /// 用法错误,或者插件明确不支持这个动词。
    ///
    /// 「不支持」也归这里:用户敲的动词是合法的,是**这个后端**做不到 ——
    /// 但那仍然是"你让我做的事做不了",与"pmpx 自己坏了"是两回事。
    #[error("{0}")]
    Usage(String),

    /// 检测不到项目类型 / 缺少能处理该生态的插件 / 后端可执行文件找不到。
    ///
    /// 这三种情况的共同点是:**pmpx 本身没问题,是环境里缺东西**。
    /// 它们都带着"下一步该做什么"的信息,所以调用方应当把 `message` 完整打出来。
    #[error("{0}")]
    NotFound(String),

    /// pmpx 自身出错了。这里是兜底,正常情况下不该走到。
    #[error(transparent)]
    Other(#[from] anyhow::Error),

    /// **后端**报出来的失败,退出码由它决定。
    ///
    /// 单独一个变体而不是塞进 [`PmpxError::Usage`]:后端的失败有自己的码
    /// ([`crate::runtime::BackendError::exit_code`]),而把它一律折成 2
    /// 会让"插件不支持这个动词"和"插件自己炸了"变得无法区分 ——
    /// 前者是用户敲错了,后者是插件坏了,脚本要能分开处理。
    #[error("{0}")]
    Backend(String, u8),
}

impl PmpxError {
    /// 这条错误该让进程以什么码退出。
    pub fn exit_code(&self) -> u8 {
        match self {
            PmpxError::Usage(_) => EXIT_USAGE,
            PmpxError::NotFound(_) => EXIT_NOT_FOUND,
            PmpxError::Other(_) => EXIT_INTERNAL,
            PmpxError::Backend(_, code) => *code,
        }
    }

    /// 构造一条"环境里缺东西"。
    pub fn not_found(message: impl Into<String>) -> Self {
        PmpxError::NotFound(message.into())
    }
}