//! list: installed agents by default (managed, running and external), or //! the whole catalog with --all. use super::*; use crate::output::{boxed_table, print_json, status_style}; use crate::probe::ExternalInfo; use crate::state::InstalledEntry; use serde::Serialize; #[derive(Serialize)] struct AgentRow { name: String, display_name: String, category: Option, description: Option, status: String, version: Option, source: String, method: Option, path: Option, running: bool, pid: Option, installable: bool, note: Option, } /// run: installed agents only, unless all is set (whole catalog). pub fn run( app: &App, all: bool, category: Option<&str>, running: bool, sort: Option<&str>, ) -> Result { let entries = app.state.all().unwrap_or_default(); let probe = crate::probe::detect_externals(app); let mut rows: Vec = Vec::new(); for agent in app.catalog.agents() { if agent.hidden { continue; } if let Some(cat) = category { if agent.category.as_deref() != Some(cat) { continue; } } rows.push(row_for(agent, entries.get(&agent.name), &probe)); } if !all { rows.retain(|r| r.status == "running" || r.status == "installed" || r.status == "external"); } if running { rows.retain(|r| r.status == "running"); } match sort { Some("version") => rows.sort_by(|a, b| b.version.cmp(&a.version)), Some("status") => rows.sort_by(|a, b| a.status.cmp(&b.status)), Some(other) => bail!("invalid sort key '{other}' — expected name, version or status"), None => 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 { table_rows.push(vec![ r.display_name.clone(), r.name.clone(), r.version.clone().unwrap_or_else(|| "—".to_string()), status_style(&r.status, theme, color), r.source.clone(), r.path.clone().unwrap_or_else(|| "—".to_string()), ]); } print!( "{}", boxed_table( vec!["NAME", "AGENT", "VERSION", "STATUS", "SOURCE", "PATH"], table_rows, theme, color, ) ); if rows.is_empty() { app.log.info(if all { "no agents found" } else { "no installed agents found — run 'am list --all' for the full catalog" }); } Ok(0) } fn row_for(agent: &AgentDef, entry: Option<&InstalledEntry>, probe: &ExternalInfo) -> AgentRow { let (status, source, path, version, running, pid, method) = if let Some(e) = entry { let running = e.pid.map(crate::process::is_running).unwrap_or(false); let status = if running { "running" } else { "installed" }; let path = if e .bins .first() .map(|b| std::path::Path::new(b).exists()) .unwrap_or(false) { Some(e.bins[0].clone()) } else { Some(e.install_dir.clone()) }; ( status.to_string(), "managed".to_string(), path, e.version.clone(), running, e.pid, Some(e.method.clone()), ) } else if !agent.installable { ( "not-installable".to_string(), "-".to_string(), None, None, false, None, None, ) } else { match probe.path_of(&agent.name) { Some(p) => { let version = probe .version_of(&agent.name) .cloned() .flatten() .or_else(|| agent.version.clone()); ( "external".to_string(), "external".to_string(), Some(p.display().to_string()), version, false, None, None, ) } None => ( "not-installed".to_string(), "-".to_string(), None, None, false, None, None, ), } }; AgentRow { name: agent.name.clone(), display_name: agent.title().to_string(), category: agent.category.clone(), description: agent.description.clone(), status, version, source, method, path, running, pid, installable: agent.installable, note: agent.note.clone(), } }