arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! Typed errors for `arc package` (AP2.1-10).

use std::fmt;
use std::path::PathBuf;

/// A failure during production artifact packaging. Preserved (not collapsed
/// to `String`) so the operator sees which step failed and why (AGENTS.md
/// §18). No secret content is ever carried by these variants — paths and
/// step names only.
#[derive(Debug)]
pub(crate) enum PackageError {
    /// Copying a file or directory into the bundle failed.
    Copy {
        what: &'static str,
        from: PathBuf,
        source: std::io::Error,
    },
    /// Reading a file failed.
    Read {
        path: PathBuf,
        source: std::io::Error,
    },
    /// Writing a bundle artifact (manifest, checksums, sbom, license) failed.
    Write {
        path: PathBuf,
        source: std::io::Error,
    },
    /// A required bundle input was missing (e.g. the release binary).
    Missing { what: &'static str, path: PathBuf },
    /// Serializing an artifact (manifest JSON, SPDX SBOM JSON) failed.
    Serialize {
        what: &'static str,
        source: serde_json::Error,
    },
    /// Parsing a TOML file (`Cargo.lock`, the framework `Cargo.toml`) for
    /// the SBOM / framework version failed. Distinct from `Serialize`
    /// (which is JSON) so the operator sees the right parser failed
    /// (AGENTS.md §18).
    Toml {
        what: &'static str,
        source: toml::de::Error,
    },
    /// A bundle path that would have leaked a secret / dev source / private
    /// file was detected and refused (defense in depth; the copy step also
    /// filters proactively).
    Forbidden { what: &'static str, path: PathBuf },
    /// Walking the bundle tree failed.
    Walk {
        root: PathBuf,
        source: std::io::Error,
    },
}

impl fmt::Display for PackageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Copy { what, from, source } => {
                write!(f, "cannot copy {what} from {}: {source}", from.display())
            }
            Self::Read { path, source } => {
                write!(f, "cannot read {}: {source}", path.display())
            }
            Self::Write { path, source } => {
                write!(f, "cannot write {}: {source}", path.display())
            }
            Self::Missing { what, path } => {
                write!(
                    f,
                    "missing {what} at {} (run `arc build` first)",
                    path.display()
                )
            }
            Self::Serialize { what, source } => {
                write!(f, "cannot serialize {what}: {source}")
            }
            Self::Toml { what, source } => {
                write!(f, "cannot parse {what}: {source}")
            }
            Self::Forbidden { what, path } => {
                write!(
                    f,
                    "refusing to package forbidden {what} at {}",
                    path.display()
                )
            }
            Self::Walk { root, source } => {
                write!(f, "cannot walk bundle root {}: {source}", root.display())
            }
        }
    }
}

impl std::error::Error for PackageError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Copy { source, .. }
            | Self::Read { source, .. }
            | Self::Write { source, .. }
            | Self::Walk { source, .. } => Some(source),
            Self::Serialize { source, .. } => Some(source),
            Self::Toml { source, .. } => Some(source),
            Self::Missing { .. } | Self::Forbidden { .. } => None,
        }
    }
}