xbp 10.17.2

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use clap::error::ErrorKind as ClapErrorKind;
use colored::Colorize;
use thiserror::Error;

/// Unified CLI error type to keep propagation consistent across handlers.
#[derive(Debug, Error)]
pub enum CliError {
    #[error("{0}")]
    Message(String),
    #[error(transparent)]
    Anyhow(#[from] anyhow::Error),
}

pub type CliResult<T> = Result<T, CliError>;

#[derive(Debug, Clone, Copy)]
pub enum ErrorKindTag {
    Parse,
    Validation,
    Dependency,
    Permission,
    Operation,
    Config,
    Runtime,
}

impl ErrorKindTag {
    fn label(self) -> &'static str {
        match self {
            ErrorKindTag::Parse => "PARSE",
            ErrorKindTag::Validation => "VALIDATION",
            ErrorKindTag::Dependency => "DEPENDENCY",
            ErrorKindTag::Permission => "PERMISSION",
            ErrorKindTag::Operation => "OPERATION",
            ErrorKindTag::Config => "CONFIG",
            ErrorKindTag::Runtime => "RUNTIME",
        }
    }
}

/// Centralized CLI error factory used across handlers for consistent UX.
pub struct ErrorFactory;

impl ErrorFactory {
    pub fn parse(message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Parse, "cli", message, hint, None)
    }

    pub fn validation(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Validation, component, message, hint, None)
    }

    pub fn dependency(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Dependency, component, message, hint, None)
    }

    pub fn permission(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Permission, component, message, hint, None)
    }

    pub fn config(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Config, component, message, hint, None)
    }

    pub fn operation(
        component: &str,
        action: &str,
        source: impl Into<String>,
        hint: Option<&str>,
    ) -> CliError {
        let message = format!("{} failed", action);
        Self::build(
            ErrorKindTag::Operation,
            component,
            message,
            hint,
            Some(source.into()),
        )
    }

    pub fn runtime(component: &str, message: impl Into<String>, hint: Option<&str>) -> CliError {
        Self::build(ErrorKindTag::Runtime, component, message, hint, None)
    }

    pub fn clap_parse(err: clap::Error) -> CliError {
        let hint = Self::clap_hint(err.kind());
        Self::parse(err.to_string(), hint)
    }

    fn clap_hint(kind: ClapErrorKind) -> Option<&'static str> {
        match kind {
            ClapErrorKind::InvalidSubcommand => {
                Some("Run `xbp --help` to list available commands.")
            }
            ClapErrorKind::UnknownArgument | ClapErrorKind::InvalidValue => {
                Some("Use `-h` with your subcommand to see supported options.")
            }
            ClapErrorKind::MissingRequiredArgument => {
                Some("Check the usage block above and provide required arguments.")
            }
            _ => None,
        }
    }

    fn build(
        kind: ErrorKindTag,
        component: &str,
        message: impl Into<String>,
        hint: Option<&str>,
        details: Option<String>,
    ) -> CliError {
        let header = format!("[{}] {}", kind.label(), component)
            .bright_red()
            .bold();
        let mut lines = vec![header.to_string(), format!("  {}", message.into())];
        if let Some(details) = details {
            lines.push(format!("  {} {}", "Details:".bright_black(), details));
        }
        if let Some(hint) = hint {
            lines.push(format!("  {} {}", "Hint:".bright_yellow().bold(), hint));
        }
        CliError::Message(lines.join("\n"))
    }
}

impl From<String> for CliError {
    fn from(value: String) -> Self {
        CliError::Message(value)
    }
}

impl From<&str> for CliError {
    fn from(value: &str) -> Self {
        CliError::Message(value.to_string())
    }
}