arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
use std::fmt;

/// Errors from the `arc db` command family (Phase 4 spec §14).
///
/// These are Arcature-owned failures: missing configuration, connection
/// failure, advisory-lock failure, or a destructive command invoked without
/// `--force`. Migration-specific failures (a migration that errors on `up`)
/// are reported by the spawned migrator child process and surface as
/// [`crate::process::ProcessError`], not as this type.
#[derive(Debug)]
pub(crate) enum DbCommandError {
    /// `DATABASE_URL` is not set in the environment.
    MissingDatabaseUrl,
    /// The database configuration could not be parsed.
    Config(arcature_db::DbConfigError),
    /// The database connection could not be established.
    Connect(arcature_db::DbConnectError),
    /// The PostgreSQL advisory-lock session could not be acquired, queried, or
    /// closed safely.
    AdvisoryLock(arcature_db::sqlx::Error),
    /// The checked-out PostgreSQL session did not own the migration lock when
    /// the command attempted to release it.
    AdvisoryLockNotOwned,
    /// The Tokio runtime could not be created.
    Runtime(String),
    /// A destructive command (`fresh`/`reset`/`refresh`) was invoked without
    /// `--force`. The flag IS the explicit confirmation (Phase 4 spec §23).
    DestructiveRequiresForce(&'static str),
    /// `arc db lint` could not read the SQL from stdin (AP2.1-6).
    ReadStdin(std::io::Error),
    /// `arc db lint` found at least one critical migration risk (AP2.1-6). The
    /// report was printed; this error gates CI on destructive migrations.
    LintCritical,
}

impl fmt::Display for DbCommandError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingDatabaseUrl => {
                write!(formatter, "DATABASE_URL is not set")
            }
            Self::Config(error) => {
                write!(formatter, "database configuration error: {error}")
            }
            Self::Connect(error) => {
                write!(formatter, "database connection error: {error}")
            }
            Self::AdvisoryLock(error) => {
                write!(formatter, "advisory lock error: {error}")
            }
            Self::AdvisoryLockNotOwned => {
                write!(
                    formatter,
                    "migration advisory lock was not owned by its session"
                )
            }
            Self::Runtime(detail) => {
                write!(formatter, "cannot create async runtime: {detail}")
            }
            Self::DestructiveRequiresForce(command) => {
                write!(
                    formatter,
                    "arc db {command} is destructive — pass --force to confirm"
                )
            }
            Self::ReadStdin(error) => {
                write!(formatter, "cannot read migration SQL from stdin: {error}")
            }
            Self::LintCritical => {
                write!(
                    formatter,
                    "arc db lint found a critical migration risk — review the report above"
                )
            }
        }
    }
}

impl std::error::Error for DbCommandError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Config(error) => Some(error),
            Self::Connect(error) => Some(error),
            Self::AdvisoryLock(error) => Some(error),
            Self::ReadStdin(error) => Some(error),
            _ => None,
        }
    }
}

impl From<arcature_db::DbConfigError> for DbCommandError {
    fn from(value: arcature_db::DbConfigError) -> Self {
        Self::Config(value)
    }
}

impl From<arcature_db::DbConnectError> for DbCommandError {
    fn from(value: arcature_db::DbConnectError) -> Self {
        Self::Connect(value)
    }
}