use std::collections::HashMap;
use astrid_capsule_install::mismatching_contracts;
use astrid_core::dirs::AstridHome;
use colored::Colorize;
use super::meta::scan_installed_capsules_in_home_with_layout;
use crate::theme::Theme;
pub(crate) fn list_capsules(verbose: bool) -> anyhow::Result<()> {
let home = AstridHome::resolve()?;
let capsules =
scan_installed_capsules_in_home_with_layout(&home, crate::workspace_layout::current())?;
if capsules.is_empty() {
println!("{}", Theme::info("No capsules installed."));
return Ok(());
}
println!(
"{} ({})",
Theme::header("Installed Capsules"),
capsules.len()
);
println!("{}", Theme::separator());
if verbose {
print_verbose(&home, &capsules);
} else {
print_compact(&capsules);
}
println!(
"\n{} capsule(s) installed",
capsules.len().to_string().bold()
);
let mismatched = mismatching_contracts(&home, &capsules);
if !mismatched.is_empty() {
println!();
println!(
"{}",
Theme::warning(&format!(
"Contracts skew: astrid-contracts.wit pin differs from the daemon canonical for {}: {}.",
if mismatched.len() == 1 {
"capsule"
} else {
"capsules"
},
mismatched.join(", ")
))
);
println!(
"{}",
Theme::dimmed(
" Run `astrid capsule show <name>` (or `list --verbose`) for pins. Warning only."
)
);
}
Ok(())
}
fn print_compact(capsules: &[super::meta::InstalledCapsule]) {
let max_name_len = capsules.iter().map(|c| c.name.len()).max().unwrap_or(30);
let max_version_len = capsules
.iter()
.map(|c| c.meta.as_ref().map_or(7, |m| m.version.len()))
.max()
.unwrap_or(7);
for cap in capsules {
let (version, exports_count, imports_count) = match &cap.meta {
Some(meta) => (
meta.version.as_str(),
meta.exports.values().map(HashMap::len).sum::<usize>(),
meta.imports.values().map(HashMap::len).sum::<usize>(),
),
None => ("unknown", 0, 0),
};
let location_tag = format!("[{}]", cap.location);
let caps_summary = format!("exports: {exports_count}, imports: {imports_count}");
let padded_name = format!("{:<width$}", cap.name, width = max_name_len);
println!(
" {} {:<width$} {:<13} {}",
padded_name.bold(),
version,
Theme::dimmed(&location_tag),
Theme::dimmed(&caps_summary),
width = max_version_len,
);
}
}
fn print_verbose(home: &AstridHome, capsules: &[super::meta::InstalledCapsule]) {
for (i, cap) in capsules.iter().enumerate() {
if i > 0 {
println!();
}
let Some(meta) = &cap.meta else {
let version = "unknown";
println!(
"{} {} {}",
cap.name.bold(),
version,
Theme::dimmed(&format!("[{}]", cap.location)),
);
println!(" {}", Theme::dimmed("(no metadata)"));
continue;
};
let (version, source) = (meta.version.as_str(), meta.source.as_deref());
println!(
"{} {} {}",
cap.name.bold(),
version,
Theme::dimmed(&format!("[{}]", cap.location)),
);
if let Some(src) = source {
println!(" {}", Theme::kv("Source", src));
}
let skew = astrid_capsule_install::contracts_skew(home, &meta.wit_files);
if let Some(line) = super::show::contracts_line(&skew) {
println!(" {}: {line}", "Contracts".bold());
}
print_interface_map("Exports", &meta.exports);
print_interface_map("Imports", &meta.imports);
}
}
fn print_interface_map(
label: &str,
map: &std::collections::HashMap<String, std::collections::HashMap<String, String>>,
) {
if map.is_empty() {
println!(" {}: {}", label.bold(), Theme::dimmed("(none)"));
} else {
println!(" {}:", label.bold());
for (ns, ifaces) in map {
for (name, version) in ifaces {
println!(" {ns}/{name} {version}");
}
}
}
}