use std::process::ExitCode;
use anyhow::{Context, Result};
use astrid_core::dirs::AstridHome;
use clap::Args;
use colored::Colorize;
use serde::Serialize;
use crate::context;
use crate::theme::Theme;
use crate::value_formatter::{ValueFormat, emit_structured};
#[derive(Args, Debug, Clone)]
pub(crate) struct ShowArgs {
pub name: String,
#[arg(short, long)]
pub agent: Option<String>,
#[arg(long, default_value = "pretty")]
pub format: String,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct CapsuleShow {
pub name: String,
pub version: String,
pub source: String,
pub wasm_hash: String,
pub installed_at: String,
pub updated_at: String,
pub manifest: String,
}
pub(crate) fn run(args: &ShowArgs) -> Result<ExitCode> {
let principal = context::resolve_agent(args.agent.as_deref())?;
let format = ValueFormat::parse(&args.format);
let home = AstridHome::resolve().context("Failed to resolve Astrid home directory")?;
let capsule_dir = home
.principal_home(&principal)
.root()
.join(".local")
.join("capsules")
.join(&args.name);
if !capsule_dir.exists() {
eprintln!(
"{}",
Theme::error(&format!(
"capsule '{}' is not installed for agent '{principal}'",
args.name
))
);
return Ok(ExitCode::from(1));
}
let manifest_path = capsule_dir.join("Capsule.toml");
let meta_path = capsule_dir.join("meta.json");
let manifest = std::fs::read_to_string(&manifest_path)
.with_context(|| format!("Failed to read {}", manifest_path.display()))?;
let meta_raw = std::fs::read_to_string(&meta_path)
.with_context(|| format!("Failed to read {}", meta_path.display()))?;
let meta: serde_json::Value = serde_json::from_str(&meta_raw)
.with_context(|| format!("{} is not valid JSON", meta_path.display()))?;
let record = CapsuleShow {
name: args.name.clone(),
version: meta
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string(),
source: meta
.get("source")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string(),
wasm_hash: meta
.get("wasm_hash")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
installed_at: meta
.get("installed_at")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
updated_at: meta
.get("updated_at")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
manifest: manifest.clone(),
};
if !format.is_pretty() {
emit_structured(&record, format)?;
return Ok(ExitCode::SUCCESS);
}
println!("{} {}", "Capsule".bold(), args.name.cyan());
println!(" Version: {}", record.version);
println!(" Source: {}", record.source);
println!(" Hash: {}", record.wasm_hash);
println!(" Installed: {}", record.installed_at);
println!(" Updated: {}", record.updated_at);
println!(" Agent: {principal}");
println!();
println!("{}", "Manifest".bold());
for line in manifest.lines() {
println!(" {line}");
}
Ok(ExitCode::SUCCESS)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_round_trips_to_json() {
let rec = CapsuleShow {
name: "x".into(),
version: "0.1.0".into(),
source: "local".into(),
wasm_hash: "abc".into(),
installed_at: "2026-04-28T00:00:00Z".into(),
updated_at: "2026-04-28T00:00:00Z".into(),
manifest: "[package]\nname = \"x\"\n".into(),
};
let json = serde_json::to_string(&rec).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["name"], "x");
assert_eq!(parsed["version"], "0.1.0");
}
}