cellos-ctl 0.5.2

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation
//! `cellctl describe formation|cell <name>` — verbose human-readable detail.

use owo_colors::OwoColorize;

use crate::client::{formation_path, CellosClient};
use crate::exit::CtlResult;
use crate::model::{Cell, Formation};

pub async fn formation(client: &CellosClient, name: &str) -> CtlResult<()> {
    // CTL-002: route to `/v1/formations/{uuid}` or
    // `/v1/formations/by-name/{name}` depending on whether the operator
    // supplied a UUID or a kubectl-style name.
    let f: Formation = client.get_json(&formation_path(name)).await?;
    print_formation(&f);
    Ok(())
}

pub async fn cell(client: &CellosClient, name: &str) -> CtlResult<()> {
    let c: Cell = client
        .get_json(&format!("/v1/cells/{}", urlencode(name)))
        .await?;
    print_cell(&c);
    Ok(())
}

fn print_formation(f: &Formation) {
    let title = format!("Formation: {}", display_name(&f.name, &f.id));
    println!("{}", title.bold());
    field("ID", &f.id);
    field("State", &f.state);
    field("Tenant", f.tenant.as_deref().unwrap_or("-"));
    field("Created", f.created_at.as_deref().unwrap_or("-"));
    field("Updated", f.updated_at.as_deref().unwrap_or("-"));
    println!();
    println!("{}", "Cells:".bold());
    if f.cells.is_empty() {
        println!("  (none reported)");
    } else {
        for c in &f.cells {
            println!(
                "  - {} [{}] critical={}",
                display_name(&c.name, &c.id),
                c.state,
                c.critical
                    .map(|b| if b { "true" } else { "false" })
                    .unwrap_or("-"),
            );
        }
    }
    if !f.extra.is_empty() {
        println!();
        println!("{}", "Extra:".bold());
        println!(
            "  {}",
            serde_json::to_string_pretty(&f.extra).unwrap_or_default()
        );
    }
}

fn print_cell(c: &Cell) {
    let title = format!("Cell: {}", display_name(&c.name, &c.id));
    println!("{}", title.bold());
    field("ID", &c.id);
    field("State", &c.state);
    field("Formation", c.formation_id.as_deref().unwrap_or("-"));
    field("Image", c.image.as_deref().unwrap_or("-"));
    field(
        "Critical",
        c.critical
            .map(|b| if b { "true" } else { "false" })
            .unwrap_or("-"),
    );
    field("Created", c.created_at.as_deref().unwrap_or("-"));
    field("Started", c.started_at.as_deref().unwrap_or("-"));
    field("Finished", c.finished_at.as_deref().unwrap_or("-"));
    field("Outcome", c.outcome.as_deref().unwrap_or("-"));
    if !c.extra.is_empty() {
        println!();
        println!("{}", "Extra:".bold());
        println!(
            "  {}",
            serde_json::to_string_pretty(&c.extra).unwrap_or_default()
        );
    }
}

fn field(label: &str, value: &str) {
    println!("  {:<10} {}", format!("{}:", label), value);
}

fn display_name(name: &str, id: &str) -> String {
    if !name.is_empty() {
        name.to_string()
    } else {
        id.to_string()
    }
}

fn urlencode(s: &str) -> String {
    url::form_urlencoded::byte_serialize(s.as_bytes()).collect()
}