//! status: detailed state of one agent, or of every installed agent. use super::*; use crate::output::{boxed_table, kv_table, print_json, status_style}; use crate::probe::ExternalInfo; use crate::state::InstalledEntry; use serde::Serialize; #[derive(Serialize)] struct StatusDetail { name: String, display_name: String, status: String, version: Option, method: Option, path: Option, run: Option, pid: Option, started_at: Option, installed_at: Option, log: Option, managed: bool, } pub fn run(app: &App, agent_name: Option<&str>) -> Result { let probe = crate::probe::detect_externals(app); match agent_name { Some(name) => status_one(app, name, &probe), None => status_all(app, &probe), } } fn status_one(app: &App, name: &str, probe: &ExternalInfo) -> Result { let agent = require_agent(app, name)?; let entry: Option = app.state.get(&agent.name).ok().flatten(); let detail = detail_for(app, agent, entry.as_ref(), probe); if app.json() { print_json(&detail); return Ok(0); } let color = app.color(); let theme = app.theme(); // Favorite star (issue #39). let star = if crate::commands::annotations_cmd::is_favorite(app, &agent.name) { "★ " } else { "" }; println!("{}{} ({})", star, agent.title(), agent.name); let status_line = status_style(&detail.status, theme, color); let mut rows: Vec<(String, String)> = vec![ ("agent".to_string(), agent.name.clone()), ("status".to_string(), status_line), ( "version".to_string(), detail.version.clone().unwrap_or_else(|| "unknown".to_string()), ), ( "method".to_string(), detail.method.clone().unwrap_or_else(|| "—".to_string()), ), ( "path".to_string(), detail.path.clone().unwrap_or_else(|| "—".to_string()), ), ( "run".to_string(), detail.run.clone().unwrap_or_else(|| "—".to_string()), ), ]; if let Some(pid) = detail.pid { rows.push(("pid".to_string(), pid.to_string())); } if let Some(t) = &detail.started_at { rows.push(("started".to_string(), t.clone())); } if let Some(t) = &detail.installed_at { rows.push(("installed".to_string(), t.clone())); } if let Some(l) = &detail.log { rows.push(("log".to_string(), l.clone())); } print!("{}", kv_table(&rows, theme, color)); Ok(0) } fn status_all(app: &App, probe: &ExternalInfo) -> Result { let entries = app.state.all().unwrap_or_default(); let mut rows: Vec = Vec::new(); for agent in app.catalog.agents() { if agent.hidden { continue; } let entry = entries.get(&agent.name); let d = detail_for(app, agent, entry, probe); if d.status == "running" || d.status == "installed" || d.status == "external" { rows.push(AgentRow { name: agent.name.clone(), display_name: agent.title().to_string(), status: d.status, version: d.version, pid: d.pid, path: d.path, }); } } rows.sort_by(|a, b| a.name.cmp(&b.name)); if app.json() { print_json(&rows); return Ok(0); } let color = app.color(); let theme = app.theme(); let mut table_rows = Vec::new(); for r in &rows { let star = if crate::commands::annotations_cmd::is_favorite(app, &r.name) { "★ " } else { "" }; table_rows.push(vec![ format!("{star}{}", r.display_name), r.version.clone().unwrap_or_else(|| "—".to_string()), status_style(&r.status, theme, color), r.pid.map(|p| p.to_string()).unwrap_or_else(|| "—".to_string()), r.path.clone().unwrap_or_else(|| "—".to_string()), ]); } print!( "{}", boxed_table( vec!["NAME", "VERSION", "STATUS", "PID", "PATH"], table_rows, theme, color, ) ); if rows.is_empty() { app.log.info("no installed or running agents"); } Ok(0) } #[derive(Serialize)] struct AgentRow { name: String, display_name: String, status: String, version: Option, pid: Option, path: Option, } fn detail_for( app: &App, agent: &AgentDef, entry: Option<&InstalledEntry>, probe: &ExternalInfo, ) -> StatusDetail { match entry { Some(e) => { let running = e.pid.map(crate::process::is_running).unwrap_or(false); let status = if running { "running" } else { "installed" }; let path = e .bins .first() .filter(|b| std::path::Path::new(b).exists()) .cloned() .or_else(|| Some(e.install_dir.clone())); StatusDetail { name: agent.name.clone(), display_name: agent.title().to_string(), status: status.to_string(), version: e.version.clone(), method: Some(e.method.clone()), path, run: Some(e.run.clone()), pid: if running { e.pid } else { None }, started_at: if running { e.started_at.clone() } else { None }, installed_at: Some(e.installed_at.clone()), log: Some(crate::process::agent_log_path(app, &agent.name).display().to_string()), managed: true, } } None => { let external = probe.path_of(&agent.name).cloned(); let version = external .as_ref() .and_then(|_p| probe.version_of(&agent.name).cloned().flatten()) .or_else(|| agent.version.clone()); StatusDetail { name: agent.name.clone(), display_name: agent.title().to_string(), status: if external.is_some() { "external".to_string() } else if agent.installable { "not-installed".to_string() } else { "not-installable".to_string() }, version, method: None, path: external.map(|p| p.display().to_string()), run: agent.run.clone(), pid: None, started_at: None, installed_at: None, log: None, managed: false, } } } }