332 lines
11 KiB
Rust
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()
|
|
}
|