use anyhow::{Context, Result};
use clap::Args;
use std::path::PathBuf;
use crate::cache::Cache;
use crate::manifest::{Manifest, find_manifest_with_optional};
mod converters;
mod filters;
mod formatters;
#[cfg(test)]
mod list_tests;
pub use formatters::{ListItem, OutputConfig};
#[derive(Args)]
pub struct ListCommand {
#[arg(long)]
agents: bool,
#[arg(long)]
snippets: bool,
#[arg(long)]
commands: bool,
#[arg(long)]
skills: bool,
#[arg(short = 'f', long, default_value = "table")]
format: String,
#[arg(long)]
manifest: bool,
#[arg(long, value_name = "TYPE")]
r#type: Option<String>,
#[arg(long, value_name = "SOURCE")]
source: Option<String>,
#[arg(long, value_name = "PATTERN")]
search: Option<String>,
#[arg(long)]
detailed: bool,
#[arg(long)]
files: bool,
#[arg(short = 'v', long)]
verbose: bool,
#[arg(long, value_name = "FIELD")]
sort: Option<String>,
}
impl ListCommand {
pub async fn execute_with_manifest_path(self, manifest_path: Option<PathBuf>) -> Result<()> {
self.validate_arguments()?;
let manifest_path = find_manifest_with_optional(manifest_path)
.context("No agpm.toml found. Please create one to define your dependencies.")?;
self.execute_from_path(manifest_path).await
}
pub async fn execute_from_path(self, manifest_path: PathBuf) -> Result<()> {
self.validate_arguments()?;
if !manifest_path.exists() {
return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
}
let project_dir = manifest_path.parent().ok_or_else(|| {
anyhow::anyhow!("Manifest file has no parent directory: {}", manifest_path.display())
})?;
if self.manifest {
self.list_from_manifest(&manifest_path)?;
} else {
self.list_from_lockfile(project_dir).await?;
}
Ok(())
}
fn validate_arguments(&self) -> Result<()> {
match self.format.as_str() {
"table" | "json" | "yaml" | "compact" | "simple" => {}
_ => {
return Err(anyhow::anyhow!(
"Invalid format '{}'. Valid formats are: table, json, yaml, compact, simple",
self.format
));
}
}
if let Some(ref t) = self.r#type {
match t.as_str() {
"agents" | "snippets" | "commands" | "scripts" | "hooks" | "mcp-servers"
| "skills" | "agent" | "snippet" | "command" | "script" | "hook" | "mcp-server"
| "skill" => {}
_ => {
return Err(anyhow::anyhow!(
"Invalid type '{t}'. Valid types are: agents, snippets, commands, scripts, hooks, mcp-servers, skills"
));
}
}
}
if let Some(ref field) = self.sort {
match field.as_str() {
"name" | "version" | "source" | "type" => {}
_ => {
return Err(anyhow::anyhow!(
"Invalid sort field '{field}'. Valid fields are: name, version, source, type"
));
}
}
}
Ok(())
}
fn list_from_manifest(&self, manifest_path: &std::path::Path) -> Result<()> {
let manifest = Manifest::load(manifest_path)?;
let mut items = Vec::new();
for resource_type in crate::core::ResourceType::all() {
if !self.should_show_resource_type(*resource_type) {
continue;
}
let type_str = resource_type.to_string();
if *resource_type == crate::core::ResourceType::McpServer {
continue;
}
if let Some(deps) = manifest.get_dependencies(*resource_type) {
for (name, dep) in deps {
if self.matches_filters(name, Some(dep), &type_str) {
items.push(ListItem {
name: name.clone(),
source: dep.get_source().map(std::string::ToString::to_string),
version: dep.get_version().map(std::string::ToString::to_string),
path: Some(dep.get_path().to_string()),
resource_type: type_str.clone(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some(
dep.get_tool()
.map(|s| s.to_string())
.unwrap_or_else(|| manifest.get_default_tool(*resource_type)),
),
applied_patches: std::collections::BTreeMap::new(),
approximate_token_count: None,
});
}
}
}
}
if self.should_show_resource_type(crate::core::ResourceType::McpServer) {
for (name, mcp_dep) in &manifest.mcp_servers {
if self.matches_filters(name, Some(mcp_dep), "mcp-server") {
items.push(ListItem {
name: name.clone(),
source: mcp_dep.get_source().map(std::string::ToString::to_string),
version: mcp_dep.get_version().map(std::string::ToString::to_string),
path: Some(mcp_dep.get_path().to_string()),
resource_type: "mcp-server".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some(mcp_dep.get_tool().map(|s| s.to_string()).unwrap_or_else(
|| manifest.get_default_tool(crate::core::ResourceType::McpServer),
)),
applied_patches: std::collections::BTreeMap::new(),
approximate_token_count: None,
});
}
}
}
self.sort_items(&mut items);
self.output_items(&items, "Dependencies from agpm.toml:")?;
Ok(())
}
async fn list_from_lockfile(&self, project_dir: &std::path::Path) -> Result<()> {
let lockfile_path = project_dir.join("agpm.lock");
if !lockfile_path.exists() {
if self.format == "json" {
println!("{{}}");
} else {
println!("No installed resources found.");
println!("⚠️ agpm.lock not found. Run 'agpm install' first.");
}
return Ok(());
}
let manifest_path = project_dir.join("agpm.toml");
let manifest = crate::manifest::Manifest::load(&manifest_path)?;
let command_context =
crate::cli::common::CommandContext::new(manifest, project_dir.to_path_buf())?;
let lockfile = match command_context.load_lockfile_with_regeneration(true, "list")? {
Some(lockfile) => lockfile,
None => {
if self.format == "json" {
println!("{{}}");
} else {
println!("No installed resources found.");
println!(
"⚠️ Lockfile was invalid and has been removed. Run 'agpm install' to regenerate it."
);
}
return Ok(());
}
};
let cache = if self.detailed {
Some(Cache::new().context("Failed to initialize cache")?)
} else {
None
};
let mut items = Vec::new();
for resource_type in crate::core::ResourceType::all() {
if !self.should_show_resource_type(*resource_type) {
continue;
}
let type_str = resource_type.to_string();
for entry in lockfile.get_resources(resource_type) {
if self.matches_lockfile_filters(&entry.name, entry, &type_str) {
items.push(converters::lockentry_to_listitem(entry, &type_str));
}
}
}
self.sort_items(&mut items);
if self.detailed {
formatters::output_items_detailed(
&items,
"Installed resources from agpm.lock:",
&lockfile,
cache.as_ref(),
self.should_show_resource_type(crate::core::ResourceType::Agent),
self.should_show_resource_type(crate::core::ResourceType::Snippet),
)
.await?;
} else {
self.output_items(&items, "Installed resources from agpm.lock:")?;
}
Ok(())
}
fn should_show_resource_type(&self, resource_type: crate::core::ResourceType) -> bool {
filters::should_show_resource_type(
resource_type,
self.agents,
self.snippets,
self.commands,
self.skills,
self.r#type.as_ref(),
)
}
fn matches_filters(
&self,
name: &str,
dep: Option<&crate::manifest::ResourceDependency>,
resource_type: &str,
) -> bool {
filters::matches_filters(
name,
dep,
resource_type,
self.source.as_ref(),
self.search.as_ref(),
)
}
fn matches_lockfile_filters(
&self,
name: &str,
entry: &crate::lockfile::LockedResource,
resource_type: &str,
) -> bool {
filters::matches_lockfile_filters(
name,
entry,
resource_type,
self.source.as_ref(),
self.search.as_ref(),
)
}
fn sort_items(&self, items: &mut [ListItem]) {
filters::sort_items(items, self.sort.as_ref());
}
fn output_items(&self, items: &[ListItem], title: &str) -> Result<()> {
let config = OutputConfig {
title: title.to_string(),
format: self.format.clone(),
files: self.files,
detailed: self.detailed,
verbose: self.verbose,
should_show_agents: self.should_show_resource_type(crate::core::ResourceType::Agent),
should_show_snippets: self
.should_show_resource_type(crate::core::ResourceType::Snippet),
};
formatters::output_items(items, &config)
}
}