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

332 lines
11 KiB
Rust

//! info: everything known about one agent.
use super::*;
use crate::output::print_json;
use colored::Colorize;
use serde::Serialize;
#[derive(Serialize)]
struct InfoJson {
name: String,
display_name: String,
description: Option<String>,
category: Option<String>,
website: Option<String>,
tags: Vec<String>,
status: String,
version: Option<String>,
managed: bool,
running: bool,
pid: Option<u32>,
installable: bool,
note: Option<String>,
/// Personal note (issue #39).
personal_note: Option<String>,
/// Personal tags (issue #39).
personal_tags: Vec<String>,
/// Favorite (issue #39).
favorite: bool,
platforms: Vec<String>,
run: Option<String>,
args: Vec<String>,
env: std::collections::BTreeMap<String, String>,
install_methods: Vec<String>,
post_install: Vec<String>,
dependencies: Vec<DepJson>,
path: Option<String>,
}
#[derive(Serialize)]
struct DepJson {
name: String,
min_version: Option<String>,
install_hint: Option<String>,
}
pub fn run(app: &App, name: &str) -> Result<i32> {
let agent = require_agent(app, name)?;
let entry = app.state.get(&agent.name).ok().flatten();
let running = entry
.as_ref()
.and_then(|e| e.pid)
.map(crate::process::is_running)
.unwrap_or(false);
let methods: Vec<String> = agent
.install
.as_ref()
.map(|spec| spec.all_methods().iter().map(|m| describe_method(m)).collect())
.unwrap_or_default();
let deps: Vec<DepJson> = agent
.dependencies
.iter()
.map(|d| DepJson {
name: d.name.clone(),
min_version: d.min_version.clone(),
install_hint: d.install_hint.clone(),
})
.collect();
if app.json() {
print_json(&InfoJson {
name: agent.name.clone(),
display_name: agent.title().to_string(),
description: agent.description.clone(),
category: agent.category.clone(),
website: agent.website.clone(),
tags: agent.tags.clone(),
status: if running {
"running".to_string()
} else if entry.is_some() {
"installed".to_string()
} else if !agent.installable {
"not-installable".to_string()
} else {
"not-installed".to_string()
},
version: entry.as_ref().and_then(|e| e.version.clone()),
managed: entry.is_some(),
running,
pid: if running { entry.as_ref().and_then(|e| e.pid) } else { None },
installable: agent.installable,
note: agent.note.clone(),
personal_note: annotations_note(app, &agent.name),
personal_tags: annotations_tags(app, &agent.name),
favorite: annotations_favorite(app, &agent.name),
platforms: agent.platforms.clone(),
run: agent.run.clone(),
args: agent.args.clone(),
env: agent.env.clone(),
install_methods: methods,
post_install: agent
.install
.as_ref()
.map(|s| s.post_install.clone())
.unwrap_or_default(),
dependencies: deps,
path: entry
.as_ref()
.map(|e| e.install_dir.clone())
.or_else(|| external_path(agent).map(|p| p.display().to_string())),
});
return Ok(0);
}
let color = app.color();
let dim = |s: &str| -> String {
if color {
s.dimmed().to_string()
} else {
s.to_string()
}
};
let bold = |s: &str| -> String {
if color {
s.bold().to_string()
} else {
s.to_string()
}
};
// --- Header -------------------------------------------------------------
println!("{}", bold(agent.title()));
let mut sub: Vec<String> = vec![agent.name.clone()];
if let Some(c) = &agent.category {
sub.push(c.clone());
}
println!(" {}", dim(&sub.join(" · ")));
if let Some(d) = &agent.description {
println!();
println!(" {d}");
}
if let Some(w) = &agent.website {
println!();
println!(" {} {}", dim("website"), w);
}
println!();
// --- Status --------------------------------------------------------------
let external = external_path(agent);
let (status_word, detail): (&str, String) = if running {
let pid = entry.as_ref().and_then(|e| e.pid).unwrap_or(0);
("running", format!(" (pid {pid})"))
} else if entry.is_some() {
let v = entry
.as_ref()
.and_then(|e| e.version.clone())
.map(|v| format!(" — v{v}"))
.unwrap_or_default();
("installed", v)
} else if !agent.installable {
("not-installable", String::new())
} else if external.is_some() {
("external", " (found on PATH — not managed)".to_string())
} else {
("not-installed", String::new())
};
let word = crate::output::status_style(status_word, app.theme(), color);
println!(" {} {}", dim("● status"), format!("{word}{detail}"));
// Version: recorded for managed installs, detected for external ones.
let version_display: Option<String> = entry
.as_ref()
.and_then(|e| e.version.clone())
.or_else(|| external.as_ref().and_then(|p| crate::probe::detect_external_version(p)));
if let Some(v) = version_display {
println!(" {} {}", dim("● version"), v);
}
if let Some(t) = entry.as_ref().map(|e| e.installed_at.clone()) {
println!(" {} {}", dim("● installed"), t);
}
if let Some(t) = entry.as_ref().and_then(|e| e.updated_at.clone()) {
println!(" {} {}", dim("● updated"), t);
}
if let Some(p) = entry
.as_ref()
.map(|e| e.install_dir.clone())
.or_else(|| external.clone().map(|p| p.display().to_string()))
{
println!(" {} {}", dim("● path"), p);
}
if let Some(e) = &entry {
if let Some(bin) = e.bins.first() {
println!(" {} {}", dim("● bin"), bin);
}
}
// --- How to run it -------------------------------------------------------
if let Some(run) = &agent.run {
let args = if agent.args.is_empty() {
String::new()
} else {
format!(" {}", agent.args.join(" "))
};
println!();
println!(" {} {}", dim("▶ run"), format!("{run}{args}"));
}
if !agent.env.is_empty() {
println!(
" {} {}",
dim("⚙ env"),
agent
.env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ")
);
}
// --- Installation ---------------------------------------------------------
if let Some(spec) = &agent.install {
println!();
println!(" {}", bold("install"));
for (i, m) in spec.all_methods().iter().enumerate() {
println!(" {}) {}", i + 1, describe_method(m));
if let Some(n) = &m.note {
println!(" {}", dim(n));
}
}
if !spec.post_install.is_empty() {
println!(
" {}",
dim(&format!("post-install: {}", spec.post_install.join(" && ")))
);
}
}
// --- Dependencies, checked live ------------------------------------------
if !agent.dependencies.is_empty() {
println!();
println!(" {}", bold("dependencies"));
let runner = crate::runner::SystemRunner::new(false, app.cli.verbose, &app.log);
let checks = crate::deps::check_agent(agent, &runner);
for c in &checks {
let mark = if c.ok { "✓" } else { "✗" };
let mark = if color {
if c.ok {
mark.green().to_string()
} else {
mark.red().to_string()
}
} else {
mark.to_string()
};
let mut line = format!(" {mark} {}", c.name);
if let Some(v) = &c.version {
line.push_str(&format!(" {v}"));
}
if let Some(min) = &c.min {
line.push_str(&format!(" (min {min})"));
}
if !c.ok {
if let Some(hint) = agent
.dependencies
.iter()
.find(|d| d.name == c.name)
.and_then(|d| d.install_hint.clone())
{
line.push_str(&format!(" — {hint}"));
} else if !c.detail.is_empty() && c.detail != "not found on PATH" {
line.push_str(&format!(" — {}", c.detail));
}
}
println!("{line}");
}
}
// --- Tags and note ---------------------------------------------------------
if !agent.tags.is_empty() {
println!();
println!(" {}", dim(&format!("tags {}", agent.tags.join(" · "))));
}
if let Some(n) = &agent.note {
println!();
println!(" {}", dim(&format!("note {n}")));
}
// --- Personal annotations (issue #39) -------------------------------------
let p_note = annotations_note(app, &agent.name);
let p_tags = annotations_tags(app, &agent.name);
let p_fav = annotations_favorite(app, &agent.name);
if p_fav || p_tags.iter().any(|t| !agent.tags.contains(t)) || p_note.is_some() {
println!();
println!(" {}", bold("personal"));
if p_fav {
println!(" {} {}", dim("● favorite"), "★");
}
if !p_tags.is_empty() {
println!(
" {} {}",
dim("● tags"),
p_tags
.iter()
.map(|t| format!("#{t}"))
.collect::<Vec<_>>()
.join(" ")
);
}
if let Some(n) = &p_note {
println!(" {} {}", dim("● note"), n);
}
}
Ok(0)
}
// Personal annotations helpers (issue #39) — never fail.
fn annotations_note(app: &App, name: &str) -> Option<String> {
app.state.annotation(name).ok().and_then(|a| a.note)
}
fn annotations_tags(app: &App, name: &str) -> Vec<String> {
app.state.annotation(name).map(|a| a.tags).unwrap_or_default()
}
fn annotations_favorite(app: &App, name: &str) -> bool {
app.state
.annotation(name)
.map(|a| a.favorite)
.unwrap_or(false)
}
fn external_path(agent: &AgentDef) -> Option<std::path::PathBuf> {
let first = agent.first_token()?;
which::which(first).ok()
}