cellos-ctl 0.5.3

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation
//! Exit-code discipline — kubectl/POSIX-aligned, aligned with CellOS engineering doctrine.
//!
//! All non-success exits in `cellctl` MUST go through `CtlError::exit()` so the contract
//! between the CLI and shell scripts/CI is stable:
//!
//! | code | meaning                                            |
//! |------|----------------------------------------------------|
//! | 0    | success                                            |
//! | 1    | usage / config error (bad flag, missing file, …)   |
//! | 2    | API error (cellos-server returned 4xx/5xx, network)|
//! | 3    | validation error (local schema check failed)       |
//!
//! Errors are always written to **stderr**. Machine-readable output (JSON, names)
//! always goes to **stdout** so it can be piped through `jq`, `xargs`, etc.

use std::fmt;
use std::process;

/// Structured CLI error carrying both a message and the doctrine-mandated exit code.
///
/// `status` carries the HTTP status code when the error originated from a real
/// HTTP response with a non-success status. Transport / network errors leave
/// `status` as `None` so callers can distinguish "the server said no" from
/// "we never reached the server". See SMOKE-TEST report Finding #1:
/// `cellctl diff` must only treat HTTP 404 as "would create", not every
/// `ErrorKind::Api`.
#[derive(Debug)]
pub struct CtlError {
    pub kind: ErrorKind,
    pub message: String,
    pub status: Option<u16>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
    /// Usage / config error — bad CLI invocation, missing config, malformed YAML on read.
    Usage = 1,
    /// API error — cellos-server returned an error status, or the network call failed.
    Api = 2,
    /// Validation error — local schema check failed before the API call.
    Validation = 3,
}

impl ErrorKind {
    pub fn code(self) -> i32 {
        self as i32
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Usage => f.write_str("usage"),
            Self::Api => f.write_str("api"),
            Self::Validation => f.write_str("validation"),
        }
    }
}

impl CtlError {
    pub fn usage(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Usage,
            message: msg.into(),
            status: None,
        }
    }
    pub fn api(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Api,
            message: msg.into(),
            status: None,
        }
    }
    pub fn validation(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Validation,
            message: msg.into(),
            status: None,
        }
    }

    /// Attach an HTTP status code. Use this in client error paths that map
    /// a real HTTP response into a `CtlError` so downstream callers
    /// (e.g. `cellctl diff`'s 404→"would create" branch) can distinguish
    /// "server returned 404" from "transport failure".
    pub fn with_status(mut self, status: u16) -> Self {
        self.status = Some(status);
        self
    }

    /// Emit the error to stderr and exit with the doctrine-mandated code.
    /// Never returns.
    pub fn exit(&self) -> ! {
        eprintln!("cellctl: {}: {}", self.kind, self.message);
        process::exit(self.kind.code())
    }
}

impl fmt::Display for CtlError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.kind, self.message)
    }
}

impl std::error::Error for CtlError {}

/// Convenience: convert anyhow errors into a generic API error.
impl From<anyhow::Error> for CtlError {
    fn from(e: anyhow::Error) -> Self {
        Self::api(e.to_string())
    }
}

impl From<reqwest::Error> for CtlError {
    fn from(e: reqwest::Error) -> Self {
        Self::api(format!("http: {e}"))
    }
}

impl From<std::io::Error> for CtlError {
    fn from(e: std::io::Error) -> Self {
        Self::usage(format!("io: {e}"))
    }
}

impl From<serde_yaml::Error> for CtlError {
    fn from(e: serde_yaml::Error) -> Self {
        Self::validation(format!("yaml: {e}"))
    }
}

impl From<serde_json::Error> for CtlError {
    fn from(e: serde_json::Error) -> Self {
        Self::api(format!("json: {e}"))
    }
}

pub type CtlResult<T> = Result<T, CtlError>;