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 rollout status <formation>` — poll formation state until terminal.
//!
//! Terminal states from the Session 16 state table: COMPLETED (cyan exit 0)
//! and FAILED (red, exit 2). RUNNING + DEGRADED + LAUNCHING + PENDING are
//! non-terminal and keep polling. Hitting Ctrl-C exits cleanly with code 1.

use std::time::Duration;

use indicatif::{ProgressBar, ProgressStyle};

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

const POLL_INTERVAL: Duration = Duration::from_millis(1500);

pub async fn status(client: &CellosClient, name: &str, timeout_secs: Option<u64>) -> CtlResult<()> {
    let bar = ProgressBar::new_spinner();
    bar.set_style(
        ProgressStyle::with_template("{spinner} {wide_msg}")
            .unwrap_or_else(|_| ProgressStyle::default_spinner()),
    );
    bar.enable_steady_tick(Duration::from_millis(120));

    let deadline = timeout_secs.map(|s| std::time::Instant::now() + Duration::from_secs(s));
    // CTL-002: UUID vs name routing — see `client::formation_path`.
    let path = formation_path(name);

    loop {
        if let Some(d) = deadline {
            if std::time::Instant::now() >= d {
                bar.finish_and_clear();
                return Err(CtlError::api(format!(
                    "timed out waiting for formation/{name}"
                )));
            }
        }

        let f: Formation = match client.get_json::<Formation>(&path).await {
            Ok(f) => f,
            Err(e) => {
                bar.finish_and_clear();
                return Err(e);
            }
        };

        let state = f.state.to_ascii_uppercase();
        let cells = f.cells.len();
        bar.set_message(format!(
            "formation/{name}: {state} ({cells} cell{s})",
            s = if cells == 1 { "" } else { "s" }
        ));

        match state.as_str() {
            "COMPLETED" => {
                bar.finish_and_clear();
                println!("formation/{name} COMPLETED");
                return Ok(());
            }
            "FAILED" => {
                bar.finish_and_clear();
                return Err(CtlError::api(format!("formation/{name} FAILED")));
            }
            _ => {
                tokio::select! {
                    _ = tokio::signal::ctrl_c() => {
                        bar.finish_and_clear();
                        return Err(CtlError::usage("rollout watch cancelled"));
                    }
                    _ = tokio::time::sleep(POLL_INTERVAL) => {}
                }
            }
        }
    }
}