//! 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, category: Option, website: Option, tags: Vec, status: String, version: Option, managed: bool, running: bool, pid: Option, installable: bool, note: Option, /// Personal note (issue #39). personal_note: Option, /// Personal tags (issue #39). personal_tags: Vec, /// Favorite (issue #39). favorite: bool, platforms: Vec, run: Option, args: Vec, env: std::collections::BTreeMap, install_methods: Vec, post_install: Vec, dependencies: Vec, path: Option, } #[derive(Serialize)] struct DepJson { name: String, min_version: Option, install_hint: Option, } pub fn run(app: &App, name: &str) -> Result { 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 = agent .install .as_ref() .map(|spec| spec.all_methods().iter().map(|m| describe_method(m)).collect()) .unwrap_or_default(); let deps: Vec = 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 = 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 = 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::>() .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::>() .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 { app.state.annotation(name).ok().and_then(|a| a.note) } fn annotations_tags(app: &App, name: &str) -> Vec { 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 { let first = agent.first_token()?; which::which(first).ok() }