use anyhow::{Context, Result};
use clap::Args;
use colored::Colorize;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use crate::cache::Cache;
use crate::lockfile::LockFile;
use crate::lockfile::patch_display::extract_patch_displays;
use crate::manifest::{Manifest, find_manifest_with_optional};
#[derive(Debug, Clone)]
struct ListItem {
name: String,
source: Option<String>,
version: Option<String>,
path: Option<String>,
resource_type: String,
installed_at: Option<String>,
checksum: Option<String>,
resolved_commit: Option<String>,
tool: Option<String>,
applied_patches: std::collections::BTreeMap<String, toml::Value>,
}
#[derive(Args)]
pub struct ListCommand {
#[arg(long)]
agents: bool,
#[arg(long)]
snippets: bool,
#[arg(long)]
commands: 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().unwrap();
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" => {}
_ => {
return Err(anyhow::anyhow!(
"Invalid type '{t}'. Valid types are: agents, snippets"
));
}
}
}
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(),
});
}
}
}
}
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(),
});
}
}
}
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(self.lockentry_to_listitem(entry, &type_str));
}
}
}
self.sort_items(&mut items);
if self.detailed {
self.output_items_detailed(
&items,
"Installed resources from agpm.lock:",
&lockfile,
cache.as_ref(),
)
.await?;
} else {
self.output_items(&items, "Installed resources from agpm.lock:")?;
}
Ok(())
}
fn should_show_resource_type(&self, resource_type: crate::core::ResourceType) -> bool {
use crate::core::ResourceType;
if let Some(ref t) = self.r#type {
let type_str = resource_type.to_string();
return t == &type_str || t == &format!("{type_str}s");
}
match resource_type {
ResourceType::Agent => !self.snippets && !self.commands,
ResourceType::Snippet => !self.agents && !self.commands,
ResourceType::Command => !self.agents && !self.snippets,
ResourceType::Script => !self.agents && !self.snippets && !self.commands,
ResourceType::Hook => !self.agents && !self.snippets && !self.commands,
ResourceType::McpServer => !self.agents && !self.snippets && !self.commands,
}
}
fn matches_filters(
&self,
name: &str,
dep: Option<&crate::manifest::ResourceDependency>,
_resource_type: &str,
) -> bool {
if let Some(ref source_filter) = self.source
&& let Some(dep) = dep
{
if let Some(source) = dep.get_source() {
if source != source_filter {
return false;
}
} else {
return false; }
}
if let Some(ref search) = self.search
&& !name.contains(search)
{
return false;
}
true
}
fn sort_items(&self, items: &mut [ListItem]) {
if let Some(ref sort_field) = self.sort {
match sort_field.as_str() {
"name" => items.sort_by(|a, b| a.name.cmp(&b.name)),
"version" => items.sort_by(|a, b| {
a.version.as_deref().unwrap_or("").cmp(b.version.as_deref().unwrap_or(""))
}),
"source" => items.sort_by(|a, b| {
a.source
.as_deref()
.unwrap_or("local")
.cmp(b.source.as_deref().unwrap_or("local"))
}),
"type" => items.sort_by(|a, b| a.resource_type.cmp(&b.resource_type)),
_ => {} }
}
}
fn output_items(&self, items: &[ListItem], title: &str) -> Result<()> {
if items.is_empty() {
if self.format == "json" {
println!("{{}}");
} else {
println!("No installed resources found.");
}
return Ok(());
}
match self.format.as_str() {
"json" => self.output_json(items)?,
"yaml" => self.output_yaml(items)?,
"compact" => self.output_compact(items),
"simple" => self.output_simple(items),
_ => self.output_table(items, title),
}
Ok(())
}
fn output_json(&self, items: &[ListItem]) -> Result<()> {
let json_items: Vec<serde_json::Value> = items
.iter()
.map(|item| {
let mut obj = serde_json::json!({
"name": item.name,
"type": item.resource_type,
"tool": item.tool
});
if let Some(ref source) = item.source {
obj["source"] = serde_json::Value::String(source.clone());
}
if let Some(ref version) = item.version {
obj["version"] = serde_json::Value::String(version.clone());
}
if let Some(ref path) = item.path {
obj["path"] = serde_json::Value::String(path.clone());
}
if let Some(ref installed_at) = item.installed_at {
obj["installed_at"] = serde_json::Value::String(installed_at.clone());
}
if let Some(ref checksum) = item.checksum {
obj["checksum"] = serde_json::Value::String(checksum.clone());
}
obj
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_items)?);
Ok(())
}
fn output_yaml(&self, items: &[ListItem]) -> Result<()> {
let yaml_items: Vec<HashMap<String, serde_yaml::Value>> = items
.iter()
.map(|item| {
let mut obj = HashMap::new();
obj.insert("name".to_string(), serde_yaml::Value::String(item.name.clone()));
obj.insert(
"type".to_string(),
serde_yaml::Value::String(item.resource_type.clone()),
);
obj.insert(
"tool".to_string(),
serde_yaml::Value::String(
item.tool.clone().expect("Tool should always be set"),
),
);
if let Some(ref source) = item.source {
obj.insert("source".to_string(), serde_yaml::Value::String(source.clone()));
}
if let Some(ref version) = item.version {
obj.insert("version".to_string(), serde_yaml::Value::String(version.clone()));
}
if let Some(ref path) = item.path {
obj.insert("path".to_string(), serde_yaml::Value::String(path.clone()));
}
if let Some(ref installed_at) = item.installed_at {
obj.insert(
"installed_at".to_string(),
serde_yaml::Value::String(installed_at.clone()),
);
}
obj
})
.collect();
println!("{}", serde_yaml::to_string(&yaml_items)?);
Ok(())
}
fn output_compact(&self, items: &[ListItem]) {
for item in items {
let source = item.source.as_deref().unwrap_or("local");
let version = item.version.as_deref().unwrap_or("latest");
println!("{} {} {}", item.name, version, source);
}
}
fn output_simple(&self, items: &[ListItem]) {
for item in items {
println!("{} ({}))", item.name, item.resource_type);
}
}
fn output_table(&self, items: &[ListItem], title: &str) {
println!("{}", title.bold());
println!();
if !items.is_empty() && self.format == "table" && !self.verbose {
println!(
"{:<32} {:<15} {:<15} {:<12} {:<15}",
"Name".cyan().bold(),
"Version".cyan().bold(),
"Source".cyan().bold(),
"Type".cyan().bold(),
"Artifact".cyan().bold()
);
println!("{}", "-".repeat(92).bright_black());
}
if self.format == "table" && !self.files && !self.detailed && !self.verbose {
for item in items {
self.print_item(item);
}
} else {
let show_agents = self.should_show_resource_type(crate::core::ResourceType::Agent);
let show_snippets = self.should_show_resource_type(crate::core::ResourceType::Snippet);
if show_agents {
let agents: Vec<_> = items.iter().filter(|i| i.resource_type == "agent").collect();
if !agents.is_empty() {
println!("{}:", "Agents".cyan().bold());
for item in agents {
self.print_item(item);
}
println!();
}
}
if show_snippets {
let snippets: Vec<_> =
items.iter().filter(|i| i.resource_type == "snippet").collect();
if !snippets.is_empty() {
println!("{}:", "Snippets".cyan().bold());
for item in snippets {
self.print_item(item);
}
}
}
}
println!("{}: {} resources", "Total".green().bold(), items.len());
}
async fn output_items_detailed(
&self,
items: &[ListItem],
title: &str,
lockfile: &LockFile,
cache: Option<&Cache>,
) -> Result<()> {
if items.is_empty() {
if self.format == "json" {
println!("{{}}");
} else {
println!("No installed resources found.");
}
return Ok(());
}
println!("{}", title.bold());
println!();
let show_agents = self.should_show_resource_type(crate::core::ResourceType::Agent);
let show_snippets = self.should_show_resource_type(crate::core::ResourceType::Snippet);
if show_agents {
let agents: Vec<_> = items.iter().filter(|i| i.resource_type == "agent").collect();
if !agents.is_empty() {
println!("{}:", "Agents".cyan().bold());
for item in agents {
self.print_item_detailed(item, lockfile, cache).await;
}
println!();
}
}
if show_snippets {
let snippets: Vec<_> = items.iter().filter(|i| i.resource_type == "snippet").collect();
if !snippets.is_empty() {
println!("{}:", "Snippets".cyan().bold());
for item in snippets {
self.print_item_detailed(item, lockfile, cache).await;
}
}
}
println!("{}: {} resources", "Total".green().bold(), items.len());
Ok(())
}
async fn print_item_detailed(
&self,
item: &ListItem,
lockfile: &LockFile,
cache: Option<&Cache>,
) {
let source = item.source.as_deref().unwrap_or("local");
let version = item.version.as_deref().unwrap_or("latest");
println!(" {}", item.name.bright_white());
println!(" Source: {}", source.bright_black());
println!(" Version: {}", version.yellow());
if let Some(ref path) = item.path {
println!(" Path: {}", path.bright_black());
}
if let Some(ref installed_at) = item.installed_at {
println!(" Installed at: {}", installed_at.bright_black());
}
if let Some(ref checksum) = item.checksum {
println!(" Checksum: {}", checksum.bright_black());
}
if !item.applied_patches.is_empty() {
println!(" Applied patches:");
if let Some(cache) = cache {
if let Some(locked_resource) = self.find_locked_resource(item, lockfile) {
let patch_displays = extract_patch_displays(locked_resource, cache).await;
for display in patch_displays {
let formatted = display.format();
for (i, line) in formatted.lines().enumerate() {
if i == 0 {
println!(" • {}", line);
} else {
println!(" {}", line);
}
}
}
} else {
self.print_patches_fallback(&item.applied_patches);
}
} else {
self.print_patches_fallback(&item.applied_patches);
}
}
println!();
}
fn print_patches_fallback(&self, patches: &BTreeMap<String, toml::Value>) {
let mut patch_keys: Vec<_> = patches.keys().collect();
patch_keys.sort();
for key in patch_keys {
let value = &patches[key];
let formatted_value = format_patch_value(value);
println!(" • {}: {}", key.blue(), formatted_value);
}
}
fn find_locked_resource<'a>(
&self,
item: &ListItem,
lockfile: &'a LockFile,
) -> Option<&'a crate::lockfile::LockedResource> {
let resource_type = match item.resource_type.as_str() {
"agent" => crate::core::ResourceType::Agent,
"snippet" => crate::core::ResourceType::Snippet,
"command" => crate::core::ResourceType::Command,
"script" => crate::core::ResourceType::Script,
"hook" => crate::core::ResourceType::Hook,
"mcp-server" => crate::core::ResourceType::McpServer,
_ => return None,
};
lockfile.get_resources(&resource_type).iter().find(|r| r.name == item.name)
}
fn print_item(&self, item: &ListItem) {
let source = item.source.as_deref().unwrap_or("local");
let version = item.version.as_deref().unwrap_or("latest");
if self.format == "table" && !self.files && !self.detailed {
let name_with_indicator = if !item.applied_patches.is_empty() {
format!("{} (patched)", item.name)
} else {
item.name.clone()
};
let name_field = format!("{:<32}", name_with_indicator);
let colored_name = name_field.bright_white();
println!(
"{} {:<15} {:<15} {:<12} {:<15}",
colored_name,
version.yellow(),
source.bright_black(),
item.resource_type.bright_white(),
item.tool.clone().expect("Tool should always be set").bright_black()
);
} else if self.files {
if let Some(ref installed_at) = item.installed_at {
println!(" {}", installed_at.bright_black());
} else if let Some(ref path) = item.path {
println!(" {}", path.bright_black());
}
} else if self.detailed {
println!(" {}", item.name.bright_white());
println!(" Source: {}", source.bright_black());
println!(" Version: {}", version.yellow());
if let Some(ref path) = item.path {
println!(" Path: {}", path.bright_black());
}
if let Some(ref installed_at) = item.installed_at {
println!(" Installed at: {}", installed_at.bright_black());
}
if let Some(ref checksum) = item.checksum {
println!(" Checksum: {}", checksum.bright_black());
}
if !item.applied_patches.is_empty() {
println!(" {}", "Patches:".cyan());
let mut patch_keys: Vec<_> = item.applied_patches.keys().collect();
patch_keys.sort(); for key in patch_keys {
let value = &item.applied_patches[key];
let formatted_value = format_patch_value(value);
println!(" {}: {}", key.yellow(), formatted_value.green());
}
}
println!();
} else {
let commit_info = if let Some(ref commit) = item.resolved_commit {
format!("@{}", &commit[..7.min(commit.len())])
} else {
String::new()
};
println!(
" {} {} {} {}",
item.name.bright_white(),
format!("({source}))").bright_black(),
version.yellow(),
commit_info.bright_black()
);
if let Some(ref installed_at) = item.installed_at {
println!(" → {}", installed_at.bright_black());
}
}
}
fn matches_lockfile_filters(
&self,
name: &str,
entry: &crate::lockfile::LockedResource,
_resource_type: &str,
) -> bool {
if let Some(ref source_filter) = self.source {
if let Some(ref source) = entry.source {
if source != source_filter {
return false;
}
} else {
return false; }
}
if let Some(ref search) = self.search
&& !name.contains(search)
{
return false;
}
true
}
fn lockentry_to_listitem(
&self,
entry: &crate::lockfile::LockedResource,
resource_type: &str,
) -> ListItem {
ListItem {
name: entry.name.clone(),
source: entry.source.clone(),
version: entry.version.clone(),
path: Some(entry.path.clone()),
resource_type: resource_type.to_string(),
installed_at: Some(entry.installed_at.clone()),
checksum: Some(entry.checksum.clone()),
resolved_commit: entry.resolved_commit.clone(),
tool: Some(entry.tool.clone().unwrap_or_else(|| "claude-code".to_string())),
applied_patches: entry.applied_patches.clone(),
}
}
}
fn format_patch_value(value: &toml::Value) -> String {
match value {
toml::Value::String(s) => format!("\"{}\"", s),
toml::Value::Integer(i) => i.to_string(),
toml::Value::Float(f) => f.to_string(),
toml::Value::Boolean(b) => b.to_string(),
toml::Value::Array(arr) => {
let elements: Vec<String> = arr.iter().map(format_patch_value).collect();
format!("[{}]", elements.join(", "))
}
toml::Value::Table(_) | toml::Value::Datetime(_) => {
value.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lockfile::{LockedResource, LockedSource};
use crate::manifest::{DetailedDependency, ResourceDependency};
use tempfile::TempDir;
fn create_default_command() -> ListCommand {
ListCommand {
agents: false,
snippets: false,
commands: false,
format: "table".to_string(),
manifest: false,
r#type: None,
source: None,
search: None,
detailed: false,
files: false,
verbose: false,
sort: None,
}
}
fn create_test_manifest() -> crate::manifest::Manifest {
let mut manifest = crate::manifest::Manifest::new();
manifest
.sources
.insert("official".to_string(), "https://github.com/example/official.git".to_string());
manifest.sources.insert(
"community".to_string(),
"https://github.com/example/community.git".to_string(),
);
manifest.agents.insert(
"code-reviewer".to_string(),
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some("official".to_string()),
path: "agents/reviewer.md".to_string(),
version: Some("v1.0.0".to_string()),
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
})),
);
manifest.agents.insert(
"local-helper".to_string(),
ResourceDependency::Simple("../local/helper.md".to_string()),
);
manifest.snippets.insert(
"utils".to_string(),
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some("community".to_string()),
path: "snippets/utils.md".to_string(),
version: Some("v1.2.0".to_string()),
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
})),
);
manifest.snippets.insert(
"local-snippet".to_string(),
ResourceDependency::Simple("./snippets/local.md".to_string()),
);
manifest
}
fn create_test_lockfile() -> crate::lockfile::LockFile {
let mut lockfile = crate::lockfile::LockFile::new();
lockfile.sources.push(LockedSource {
name: "official".to_string(),
url: "https://github.com/example/official.git".to_string(),
fetched_at: "2024-01-01T00:00:00Z".to_string(),
});
lockfile.sources.push(LockedSource {
name: "community".to_string(),
url: "https://github.com/example/community.git".to_string(),
fetched_at: "2024-01-01T00:00:00Z".to_string(),
});
lockfile.agents.push(LockedResource {
name: "code-reviewer".to_string(),
source: Some("official".to_string()),
url: Some("https://github.com/example/official.git".to_string()),
path: "agents/reviewer.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("abc123def456".to_string()),
checksum: "sha256:abc123".to_string(),
installed_at: "agents/code-reviewer.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
});
lockfile.agents.push(LockedResource {
name: "local-helper".to_string(),
source: None,
url: None,
path: "../local/helper.md".to_string(),
version: None,
resolved_commit: None,
checksum: "sha256:def456".to_string(),
installed_at: "agents/local-helper.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
});
lockfile.snippets.push(LockedResource {
name: "utils".to_string(),
source: Some("community".to_string()),
url: Some("https://github.com/example/community.git".to_string()),
path: "snippets/utils.md".to_string(),
version: Some("v1.2.0".to_string()),
resolved_commit: Some("def456ghi789".to_string()),
checksum: "sha256:ghi789".to_string(),
installed_at: "snippets/utils.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Snippet,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
});
lockfile
}
#[tokio::test]
async fn test_list_no_manifest() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let cmd = create_default_command();
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_empty_manifest() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_from_manifest_with_resources() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_from_lockfile_no_lockfile() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = create_default_command();
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_from_lockfile_with_resources() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let lockfile = create_test_lockfile();
lockfile.save(&lockfile_path).unwrap();
let cmd = create_default_command();
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[test]
fn test_validate_arguments_valid_format() {
let valid_formats = ["table", "json", "yaml", "compact", "simple"];
for format in valid_formats {
let cmd = ListCommand {
format: format.to_string(),
..create_default_command()
};
assert!(cmd.validate_arguments().is_ok());
}
}
#[test]
fn test_validate_arguments_invalid_format() {
let cmd = ListCommand {
format: "invalid".to_string(),
..create_default_command()
};
let result = cmd.validate_arguments();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid format"));
}
#[test]
fn test_validate_arguments_valid_type() {
let valid_types = ["agents", "snippets"];
for type_name in valid_types {
let cmd = ListCommand {
r#type: Some(type_name.to_string()),
..create_default_command()
};
assert!(cmd.validate_arguments().is_ok());
}
}
#[test]
fn test_validate_arguments_invalid_type() {
let cmd = ListCommand {
r#type: Some("invalid".to_string()),
..create_default_command()
};
let result = cmd.validate_arguments();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid type"));
}
#[test]
fn test_validate_arguments_valid_sort() {
let valid_sorts = ["name", "version", "source", "type"];
for sort in valid_sorts {
let cmd = ListCommand {
sort: Some(sort.to_string()),
..create_default_command()
};
assert!(cmd.validate_arguments().is_ok());
}
}
#[test]
fn test_validate_arguments_invalid_sort() {
let cmd = ListCommand {
sort: Some("invalid".to_string()),
..create_default_command()
};
let result = cmd.validate_arguments();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid sort field"));
}
#[test]
fn test_should_show_agents() {
let cmd = create_default_command();
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Agent));
let cmd = ListCommand {
agents: true,
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Agent));
let cmd = ListCommand {
snippets: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Agent));
let cmd = ListCommand {
r#type: Some("agents".to_string()),
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Agent));
let cmd = ListCommand {
r#type: Some("snippets".to_string()),
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Agent));
}
#[test]
fn test_should_show_snippets() {
let cmd = create_default_command();
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
let cmd = ListCommand {
agents: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
let cmd = ListCommand {
snippets: true,
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
let cmd = ListCommand {
r#type: Some("agents".to_string()),
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
let cmd = ListCommand {
r#type: Some("snippets".to_string()),
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
}
#[test]
fn test_should_show_commands() {
let cmd = create_default_command();
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
agents: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
snippets: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
commands: true,
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Command));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Agent));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
let cmd = ListCommand {
r#type: Some("commands".to_string()),
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
r#type: Some("command".to_string()),
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Command));
}
#[test]
fn test_mutually_exclusive_type_filters() {
let cmd = ListCommand {
agents: true,
..create_default_command()
};
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Agent));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
snippets: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Agent));
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Command));
let cmd = ListCommand {
commands: true,
..create_default_command()
};
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Agent));
assert!(!cmd.should_show_resource_type(crate::core::ResourceType::Snippet));
assert!(cmd.should_show_resource_type(crate::core::ResourceType::Command));
}
#[test]
fn test_matches_filters_source() {
let cmd = ListCommand {
source: Some("official".to_string()),
..create_default_command()
};
let dep_with_source = ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some("official".to_string()),
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
let dep_with_different_source =
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some("community".to_string()),
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
let dep_without_source = ResourceDependency::Simple("local/file.md".to_string());
assert!(cmd.matches_filters("test", Some(&dep_with_source), "agent"));
assert!(!cmd.matches_filters("test", Some(&dep_with_different_source), "agent"));
assert!(!cmd.matches_filters("test", Some(&dep_without_source), "agent"));
}
#[test]
fn test_matches_filters_search() {
let cmd = ListCommand {
search: Some("code".to_string()),
..create_default_command()
};
assert!(cmd.matches_filters("code-reviewer", None, "agent"));
assert!(cmd.matches_filters("my-code-helper", None, "agent"));
assert!(!cmd.matches_filters("utils", None, "agent"));
}
#[test]
fn test_matches_lockfile_filters_source() {
let cmd = ListCommand {
source: Some("official".to_string()),
..create_default_command()
};
let entry_with_source = LockedResource {
name: "test".to_string(),
source: Some("official".to_string()),
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: "abc123".to_string(),
installed_at: "test.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
};
let entry_with_different_source = LockedResource {
name: "test".to_string(),
source: Some("community".to_string()),
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: "abc123".to_string(),
installed_at: "test.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
};
let entry_without_source = LockedResource {
name: "test".to_string(),
source: None,
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: "abc123".to_string(),
installed_at: "test.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
};
assert!(cmd.matches_lockfile_filters("test", &entry_with_source, "agent"));
assert!(!cmd.matches_lockfile_filters("test", &entry_with_different_source, "agent"));
assert!(!cmd.matches_lockfile_filters("test", &entry_without_source, "agent"));
}
#[test]
fn test_matches_lockfile_filters_search() {
let cmd = ListCommand {
search: Some("code".to_string()),
..create_default_command()
};
let entry = LockedResource {
name: "test".to_string(),
source: None,
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: "abc123".to_string(),
installed_at: "test.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
};
assert!(cmd.matches_lockfile_filters("code-reviewer", &entry, "agent"));
assert!(cmd.matches_lockfile_filters("my-code-helper", &entry, "agent"));
assert!(!cmd.matches_lockfile_filters("utils", &entry, "agent"));
}
#[test]
fn test_sort_items() {
let cmd = ListCommand {
sort: Some("name".to_string()),
..create_default_command()
};
let mut items = vec![
ListItem {
name: "zebra".to_string(),
source: None,
version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
ListItem {
name: "alpha".to_string(),
source: None,
version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
];
cmd.sort_items(&mut items);
assert_eq!(items[0].name, "alpha");
assert_eq!(items[1].name, "zebra");
}
#[test]
fn test_sort_items_by_version() {
let cmd = ListCommand {
sort: Some("version".to_string()),
..create_default_command()
};
let mut items = vec![
ListItem {
name: "test1".to_string(),
source: None,
version: Some("v2.0.0".to_string()),
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
ListItem {
name: "test2".to_string(),
source: None,
version: Some("v1.0.0".to_string()),
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
];
cmd.sort_items(&mut items);
assert_eq!(items[0].version, Some("v1.0.0".to_string()));
assert_eq!(items[1].version, Some("v2.0.0".to_string()));
}
#[test]
fn test_sort_items_by_source() {
let cmd = ListCommand {
sort: Some("source".to_string()),
..create_default_command()
};
let mut items = vec![
ListItem {
name: "test1".to_string(),
source: Some("zebra".to_string()),
version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
ListItem {
name: "test2".to_string(),
source: Some("alpha".to_string()),
version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
ListItem {
name: "test3".to_string(),
source: None, version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
];
cmd.sort_items(&mut items);
assert_eq!(items[0].source, Some("alpha".to_string()));
assert_eq!(items[1].source, None); assert_eq!(items[2].source, Some("zebra".to_string()));
}
#[test]
fn test_sort_items_by_type() {
let cmd = ListCommand {
sort: Some("type".to_string()),
..create_default_command()
};
let mut items = vec![
ListItem {
name: "test1".to_string(),
source: None,
version: None,
path: None,
resource_type: "snippet".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("agpm".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
ListItem {
name: "test2".to_string(),
source: None,
version: None,
path: None,
resource_type: "agent".to_string(),
installed_at: None,
checksum: None,
resolved_commit: None,
tool: Some("claude-code".to_string()),
applied_patches: std::collections::BTreeMap::new(),
},
];
cmd.sort_items(&mut items);
assert_eq!(items[0].resource_type, "agent");
assert_eq!(items[1].resource_type, "snippet");
}
#[test]
fn test_lockentry_to_listitem() {
let cmd = create_default_command();
let lock_entry = LockedResource {
name: "test-agent".to_string(),
source: Some("official".to_string()),
url: Some("https://example.com/repo.git".to_string()),
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:def456".to_string(),
installed_at: "agents/test-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
};
let list_item = cmd.lockentry_to_listitem(&lock_entry, "agent");
assert_eq!(list_item.name, "test-agent");
assert_eq!(list_item.source, Some("official".to_string()));
assert_eq!(list_item.version, Some("v1.0.0".to_string()));
assert_eq!(list_item.path, Some("agents/test.md".to_string()));
assert_eq!(list_item.resource_type, "agent");
assert_eq!(list_item.installed_at, Some("agents/test-agent.md".to_string()));
assert_eq!(list_item.checksum, Some("sha256:def456".to_string()));
assert_eq!(list_item.resolved_commit, Some("abc123".to_string()));
}
#[tokio::test]
async fn test_list_with_json_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
format: "json".to_string(),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_yaml_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
format: "yaml".to_string(),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_compact_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
format: "compact".to_string(),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_simple_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
format: "simple".to_string(),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_filter_by_agents_only() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
agents: true,
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_filter_by_snippets_only() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
snippets: true,
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_filter_by_type() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
r#type: Some("agents".to_string()),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path.clone()).await;
assert!(result.is_ok());
let cmd = ListCommand {
r#type: Some("snippets".to_string()),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_filter_by_source() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
source: Some("official".to_string()),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_search_by_pattern() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
search: Some("code".to_string()),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_detailed_flag() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
detailed: true,
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_files_flag() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
files: true,
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_verbose_flag() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
verbose: true,
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_with_sort() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
sort: Some("name".to_string()),
manifest: true,
..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_empty_lockfile_json_output() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = create_test_manifest();
manifest.save(&manifest_path).unwrap();
let cmd = ListCommand {
format: "json".to_string(),
manifest: false, ..create_default_command()
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
}