cellstate/commands/
status.rs

1//! `cellstate status` — Show session state and server connectivity.
2//!
3//! Displays the current session scope (tenant, agent, trajectory, scope),
4//! checks server connectivity via the health endpoint, and shows the
5//! server version if available.
6
7use crate::http;
8use crate::session::CliSession;
9use crate::ui;
10use console::style;
11use serde::Deserialize;
12
13// ============================================================================
14// API response types
15// ============================================================================
16
17#[derive(Debug, Deserialize)]
18struct HealthResponse {
19    status: String,
20    #[serde(default)]
21    version: Option<String>,
22}
23
24// ============================================================================
25// Command
26// ============================================================================
27
28pub fn run_with_json(json: bool) -> anyhow::Result<()> {
29    let session = CliSession::load()?;
30    let base_url = session.effective_base_url(None);
31    let client = http::default_http_client(5)?;
32    let health_url = format!("{base_url}/health/live");
33
34    let (server_status, server_version) = match client.get(&health_url).send() {
35        Ok(r) if r.status().is_success() => {
36            let health: HealthResponse = r.json().unwrap_or_else(|_| HealthResponse {
37                status: "unknown".into(),
38                version: None,
39            });
40            (health.status, health.version)
41        }
42        _ => ("disconnected".into(), None),
43    };
44
45    let has_key = session.effective_api_key(None).is_some();
46
47    if json {
48        let out = serde_json::json!({
49            "scope": {
50                "tenant_id": session.tenant_id,
51                "agent_id": session.agent_id,
52                "trajectory_id": session.trajectory_id,
53                "scope_id": session.scope_id,
54            },
55            "server": {
56                "url": base_url,
57                "status": server_status,
58                "version": server_version,
59            },
60            "auth": has_key,
61        });
62        println!("{}", serde_json::to_string_pretty(&out).unwrap());
63        return Ok(());
64    }
65
66    ui::print_banner();
67
68    // ── Scope ──
69    ui::section("Scope");
70    print_scope_field("Tenant", session.tenant_id.as_deref());
71    print_scope_field("Agent", session.agent_id.as_deref());
72    print_scope_field("Trajectory", session.trajectory_id.as_deref());
73    print_scope_field("Scope", session.scope_id.as_deref());
74
75    // ── Server ──
76    ui::section("Server");
77    ui::kv("URL", &base_url);
78
79    let status_str = match server_status.as_str() {
80        "ok" | "live" => format!("{}", style("connected").green()),
81        "degraded" => format!("{}", style("degraded").yellow()),
82        "disconnected" => {
83            ui::kv("Status", &format!("{}", style("disconnected").red()));
84            ui::info("Is CELLSTATE running? Try: cellstate start");
85            String::new()
86        }
87        _ => format!("{}", style(&server_status).red()),
88    };
89    if !status_str.is_empty() {
90        ui::kv("Status", &status_str);
91    }
92
93    if let Some(version) = &server_version {
94        ui::kv("Version", version);
95    }
96
97    // ── Auth ──
98    if has_key {
99        ui::kv("Auth", &format!("{}", style("API key configured").green()));
100    } else {
101        ui::kv("Auth", &format!("{}", style("no API key").dim()));
102    }
103
104    println!();
105
106    Ok(())
107}
108
109// ============================================================================
110// Helpers
111// ============================================================================
112
113fn print_scope_field(label: &str, value: Option<&str>) {
114    match value {
115        Some(v) => ui::kv(label, v),
116        None => ui::kv_missing(label),
117    }
118}