Files
agent-manager/src/commands/list_cmd.rs
T

181 lines
5.1 KiB
Rust

//! 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<String>,
description: Option<String>,
status: String,
version: Option<String>,
source: String,
method: Option<String>,
path: Option<String>,
running: bool,
pid: Option<u32>,
installable: bool,
note: Option<String>,
/// Personal favorite (issue #39).
favorite: bool,
}
/// 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<i32> {
let entries = app.state.all().unwrap_or_default();
let probe = crate::probe::detect_externals(app);
let mut rows: Vec<AgentRow> = 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(app, 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 {
// Favorite star (issue #39).
let star = if r.favorite { "★ " } else { "" };
table_rows.push(vec![
format!("{star}{}", r.display_name),
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(app: &App, 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(),
favorite: crate::commands::annotations_cmd::is_favorite(app, &agent.name),
}
}