cgx-core 0.0.3

Core library for cgx, the Rust equivalent of uvx or npx for running Rust crates quickly and easily
Documentation
use snafu::prelude::*;
use std::path::PathBuf;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
#[non_exhaustive]
pub enum Error {
    #[snafu(display("Crate name is required"))]
    MissingCrateParameter,

    #[snafu(display("Repository format must be 'owner/repo', got '{repo}'"))]
    InvalidRepoFormat { repo: String },

    #[snafu(display(
        "Git selectors (--branch, --tag, --rev) can only be used with git sources (--git, --github, \
         --gitlab)"
    ))]
    GitSelectorWithoutGitSource,

    #[snafu(display("Invalid version requirement '{version}': {source}"))]
    InvalidVersionReq { version: String, source: semver::Error },

    #[snafu(display("Invalid URL '{url}': {source}"))]
    InvalidUrl { url: String, source: url::ParseError },

    #[snafu(display(
        "Conflicting version specifications: @{at_version} in crate name vs --version {flag_version}. \
         Prefer using the @VERSION suffix in the crate name."
    ))]
    ConflictingVersions {
        at_version: String,
        flag_version: String,
    },

    // Resolution errors
    #[snafu(display("Crate '{name}' not found in registry"))]
    CrateNotFoundInRegistry { name: String },

    #[snafu(display("No version of crate '{name}' matches requirement '{requirement}'"))]
    NoMatchingVersion { name: String, requirement: String },

    #[snafu(display(
        "Package '{}' not found in workspace. Available packages: {}",
        name,
        available.join(", ")
    ))]
    PackageNotFoundInWorkspace { name: String, available: Vec<String> },

    #[snafu(display(
        "Ambiguous package name: found {count} packages in workspace, but no name was specified. Specify \
         which package to use with the 'name' field."
    ))]
    AmbiguousPackageName { count: usize },

    #[snafu(display("The crate '{krate}' does not have any binary targets so it cannot be executed"))]
    NoPackageBinaries { krate: String },

    #[snafu(display(
        "Package '{}' has multiple binary targets [{}], but no default was specified. Use --bin to \
         specify which binary to build, or set 'default-run' in Cargo.toml",
        package,
        available.join(", ")
    ))]
    AmbiguousBinaryTarget { package: String, available: Vec<String> },

    #[snafu(display(
        "Package '{package}' does not contain a {kind} target named '{target}'. Available {kind} targets: {}",
        available.join(", ")
    ))]
    RunnableTargetNotFound {
        kind: &'static str,
        package: String,
        target: String,
        available: Vec<String>,
    },

    #[snafu(display("Version mismatch: required version '{requirement}' but found '{found}'"))]
    VersionMismatch {
        requirement: String,
        found: semver::Version,
    },

    #[snafu(transparent)]
    Git {
        source: Box<dyn std::error::Error + Send + Sync>,
    },

    #[snafu(display("Failed to query registry: {source}"))]
    Registry { source: tame_index::Error },

    #[snafu(display("Error invoking `{}` to read metadata from source dir `{}`: {}",
        cargo_path.display(),
        source_dir.display(),
        source
    ))]
    CargoMetadata {
        cargo_path: PathBuf,
        source_dir: PathBuf,
        source: cargo_metadata::Error,
    },

    #[snafu(display("Cargo.toml not found in {}", source_dir.display()))]
    CargoTomlNotFound { source_dir: PathBuf },

    #[snafu(display("Failed to parse version '{version}': {source}"))]
    InvalidVersion { version: String, source: semver::Error },

    #[snafu(display("{}: {}", path.display(), source))]
    Io { path: PathBuf, source: std::io::Error },

    #[snafu(display("Failed to rename {} to {}: {}", src.display(), dst.display(), source))]
    RenameFile {
        src: PathBuf,
        dst: PathBuf,
        source: std::io::Error,
    },

    #[snafu(display("Failed to copy binary from {} to {}: {}", src.display(), dst.display(), source))]
    CopyBinary {
        src: PathBuf,
        dst: PathBuf,
        source: std::io::Error,
    },

    #[snafu(display("Failed to create temporary directory in {}: {}", parent.display(), source))]
    TempDirCreation { parent: PathBuf, source: std::io::Error },

    #[snafu(display("Failed to execute command: {}", source))]
    CommandExecution { source: std::io::Error },

    #[snafu(display("Failed to build SBOM component: {}", message))]
    SbomBuilder { message: String },

    #[snafu(display("Tokio runtime error: {source}"))]
    TokioRuntime { source: std::io::Error },

    #[snafu(display("Tokio task join error: {source}"))]
    TokioJoin { source: tokio::task::JoinError },

    #[snafu(display("JSON serialization error: {source}"))]
    Json { source: serde_json::Error },

    #[snafu(display("Cannot download '{name}' v{version}: network required but offline mode enabled"))]
    OfflineMode { name: String, version: String },

    #[snafu(display("Failed to download registry crate: {source}"))]
    RegistryDownload { source: reqwest::Error },

    #[snafu(display("Failed to extract crate tarball: {source}"))]
    TarExtraction { source: std::io::Error },

    #[snafu(display("Download URL not available for crate '{name}' version '{version}'"))]
    DownloadUrlUnavailable { name: String, version: String },

    #[snafu(display("Executable '{name}' not found in PATH or standard locations"))]
    ExecutableNotFound { name: String },

    #[snafu(display("Toolchain '{toolchain}' specified but rustup not found"))]
    RustupNotFound { toolchain: String },

    #[snafu(display("Expected binary not found in cargo build output"))]
    BinaryNotFoundInOutput,

    #[snafu(display(
        "cargo build failed with exit code {}: {}",
        exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string()),
        stderr
    ))]
    CargoBuildFailed { exit_code: Option<i32>, stderr: String },

    #[snafu(display("Failed to copy source tree from {} to {}: {}", src.display(), dst.display(), source))]
    CopySourceTree {
        src: PathBuf,
        dst: PathBuf,
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },

    // Configuration loading errors
    #[snafu(display("Failed to load configuration from {}: {}", path.display(), source))]
    ConfigLoad { path: PathBuf, source: figment::Error },

    #[snafu(display("Invalid configuration value for '{}': {}", field, message))]
    InvalidConfigValue { field: String, message: String },

    #[snafu(display("Failed to extract configuration: {}", source))]
    ConfigExtract { source: figment::Error },

    // Binary execution errors
    #[snafu(display("Failed to execute binary at {}: {source}", path.display()))]
    ExecFailed { path: PathBuf, source: std::io::Error },

    #[snafu(display("Failed to spawn process at {}: {source}", path.display()))]
    SpawnFailed { path: PathBuf, source: std::io::Error },

    #[snafu(display("Failed to wait for child process: {source}"))]
    WaitFailed { source: std::io::Error },

    #[cfg(windows)]
    #[snafu(display("Failed to set up Windows console control handler"))]
    ConsoleHandlerFailed { source: ctrlc::Error },

    #[snafu(display("Error determining home directory"))]
    Etcetera { source: etcetera::HomeDirError },
}

impl From<crate::git::Error> for Error {
    fn from(e: crate::git::Error) -> Self {
        Self::Git {
            source: Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
        }
    }
}

pub type Result<T> = std::result::Result<T, Error>;