cellos-ctl 0.5.0

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.
#[derive(Debug)]
pub struct CtlError {
    pub kind: ErrorKind,
    pub message: String,
}

#[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(),
        }
    }
    pub fn api(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Api,
            message: msg.into(),
        }
    }
    pub fn validation(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Validation,
            message: msg.into(),
        }
    }

    /// 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>;