use anyhow::{Result, anyhow};
use clap::{Args, Subcommand};
use colored::Colorize;
use regex::Regex;
use std::path::Path;
use crate::manifest::{
DetailedDependency, Manifest, ResourceDependency, find_manifest_with_optional,
};
use crate::models::{
AgentDependency, CommandDependency, DependencyType, HookDependency, McpServerDependency,
ScriptDependency, SnippetDependency, SourceSpec,
};
#[derive(Args)]
pub struct AddCommand {
#[command(subcommand)]
command: AddSubcommand,
}
#[derive(Subcommand)]
enum AddSubcommand {
Source {
name: String,
url: String,
},
#[command(subcommand)]
Dep(DependencySubcommand),
}
#[derive(Subcommand)]
enum DependencySubcommand {
Agent(AgentDependency),
Snippet(SnippetDependency),
Command(CommandDependency),
Script(ScriptDependency),
Hook(HookDependency),
McpServer(McpServerDependency),
}
impl AddCommand {
pub async fn execute_with_manifest_path(
self,
manifest_path: Option<std::path::PathBuf>,
) -> Result<()> {
match self.command {
AddSubcommand::Source {
name,
url,
} => {
add_source_with_manifest_path(
SourceSpec {
name,
url,
},
manifest_path,
)
.await
}
AddSubcommand::Dep(dep_command) => {
let dep_type = match dep_command {
DependencySubcommand::Agent(agent) => DependencyType::Agent(agent),
DependencySubcommand::Snippet(snippet) => DependencyType::Snippet(snippet),
DependencySubcommand::Command(command) => DependencyType::Command(command),
DependencySubcommand::Script(script) => DependencyType::Script(script),
DependencySubcommand::Hook(hook) => DependencyType::Hook(hook),
DependencySubcommand::McpServer(mcp) => DependencyType::McpServer(mcp),
};
add_dependency_with_manifest_path(dep_type, manifest_path).await
}
}
}
}
async fn add_source_with_manifest_path(
source: SourceSpec,
manifest_path: Option<std::path::PathBuf>,
) -> Result<()> {
let manifest_path = find_manifest_with_optional(manifest_path)?;
let mut manifest = Manifest::load(&manifest_path)?;
if manifest.sources.contains_key(&source.name) {
return Err(anyhow!("Source '{}' already exists in manifest", source.name));
}
manifest.sources.insert(source.name.clone(), source.url.clone());
manifest.save(&manifest_path)?;
println!("{}", format!("Added source '{}' → {}", source.name, source.url).green());
Ok(())
}
async fn add_dependency_with_manifest_path(
dep_type: DependencyType,
manifest_path: Option<std::path::PathBuf>,
) -> Result<()> {
let common = dep_type.common();
let manifest_path = find_manifest_with_optional(manifest_path)?;
let mut manifest = Manifest::load(&manifest_path)?;
let (name, mut dependency) =
parse_dependency_spec(&common.spec, &common.name, Some(&manifest))?;
let needs_detailed =
common.tool.is_some() || common.target.is_some() || common.filename.is_some();
if needs_detailed {
if let ResourceDependency::Simple(path) = &dependency {
let tool = common.tool.clone();
dependency = ResourceDependency::Detailed(Box::new(DetailedDependency {
source: None,
path: path.clone(),
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: common.target.clone(),
filename: common.filename.clone(),
dependencies: None,
tool,
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
}
}
if let ResourceDependency::Detailed(detailed) = &mut dependency {
if let Some(tool) = &common.tool {
detailed.tool = Some(tool.clone());
}
if let Some(target) = &common.target {
detailed.target = Some(target.clone());
}
if let Some(filename) = &common.filename {
detailed.filename = Some(filename.clone());
}
}
let resource_type = dep_type.resource_type();
if let DependencyType::McpServer(_) = &dep_type {
if manifest.mcp_servers.contains_key(&name) && !common.force {
return Err(anyhow!(
"MCP server '{name}' already exists in manifest. Use --force to overwrite"
));
}
manifest.mcp_servers.insert(name.clone(), dependency.clone());
} else {
let section = match &dep_type {
DependencyType::Agent(_) => &mut manifest.agents,
DependencyType::Snippet(_) => &mut manifest.snippets,
DependencyType::Command(_) => &mut manifest.commands,
DependencyType::Script(_) => &mut manifest.scripts,
DependencyType::Hook(_) => &mut manifest.hooks,
DependencyType::McpServer(_) => unreachable!(), };
if section.contains_key(&name) && !common.force {
return Err(anyhow!(
"{resource_type} '{name}' already exists in manifest. Use --force to overwrite"
));
}
section.insert(name.clone(), dependency.clone());
}
manifest.save(&manifest_path)?;
println!("{}", format!("Added {resource_type} '{name}'").green());
if !common.no_install {
println!("{}", "Installing dependency...".cyan());
install_single_dependency(&name, resource_type, &manifest, &manifest_path).await?;
}
Ok(())
}
#[allow(clippy::ref_option)]
fn parse_dependency_spec(
spec: &str,
custom_name: &Option<String>,
manifest: Option<&Manifest>,
) -> Result<(String, ResourceDependency)> {
let is_absolute_path = {
#[cfg(windows)]
{
spec.len() >= 3
&& spec.chars().nth(1) == Some(':')
&& spec.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
|| spec.starts_with("\\\\")
}
#[cfg(not(windows))]
{
spec.starts_with('/')
}
};
let is_local_path = is_absolute_path || spec.starts_with("file:") || Path::new(spec).exists();
let remote_pattern = Regex::new(r"^([^:]+):([^@]+)(?:@(.+))?$")?;
if !is_local_path && remote_pattern.is_match(spec) {
let captures = remote_pattern.captures(spec).unwrap();
let source = captures.get(1).unwrap().as_str().to_string();
let path = captures.get(2).unwrap().as_str().to_string();
let version = captures.get(3).map(|m| m.as_str().to_string());
let name = custom_name.clone().unwrap_or_else(|| {
Path::new(&path).file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string()
});
let source_is_local = if let Some(manifest) = manifest {
if let Some(source_url) = manifest.sources.get(&source) {
source_url.starts_with('/')
|| source_url.starts_with("./")
|| source_url.starts_with("../")
|| source_url.starts_with("file://")
|| (cfg!(windows)
&& source_url.len() >= 3
&& source_url.chars().nth(1) == Some(':'))
} else {
false
}
} else {
false
};
let final_version = if version.is_none() && !source_is_local {
Some("main".to_string())
} else {
version
};
Ok((
name,
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some(source),
path,
version: final_version,
branch: None,
rev: None,
command: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: None,
flatten: None,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
})),
))
} else if is_local_path {
let path = if spec.starts_with("file:") {
spec.trim_start_matches("file:")
} else {
spec
};
let name = custom_name.clone().unwrap_or_else(|| {
Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string()
});
Ok((name, ResourceDependency::Simple(path.to_string())))
} else {
let name = custom_name.clone().unwrap_or_else(|| {
Path::new(spec).file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string()
});
Ok((name, ResourceDependency::Simple(spec.to_string())))
}
}
async fn install_single_dependency(
name: &str,
resource_type: &str,
_manifest: &Manifest,
manifest_path: &Path,
) -> Result<()> {
println!("Installing dependency...");
let install_cmd = crate::cli::install::InstallCommand::new();
install_cmd.execute_with_manifest_path(Some(manifest_path.to_path_buf())).await?;
println!("{}", format!("✓ Installed {resource_type} '{name}' successfully").green());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::DependencySpec;
use crate::utils::normalize_path_for_storage;
use tempfile::TempDir;
fn create_test_manifest(manifest_path: &Path) {
let manifest_content = r#"[sources]
[agents]
[snippets]
[commands]
[mcp-servers]
"#;
if let Some(parent) = manifest_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(manifest_path, manifest_content).unwrap();
}
fn create_test_manifest_with_content(manifest_path: &Path) {
let manifest_content = r#"[sources]
existing = "https://github.com/existing/repo.git"
[agents]
existing-agent = "../local/agent.md"
[snippets]
existing-snippet = { source = "existing", path = "snippets/utils.md", version = "v1.0.0" }
[commands]
existing-command = { source = "existing", path = "commands/deploy.md", version = "v1.0.0" }
[mcp-servers]
existing-mcp = "../local/mcp-servers/existing.json"
"#;
if let Some(parent) = manifest_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(manifest_path, manifest_content).unwrap();
}
#[test]
fn test_parse_remote_dependency() {
let (name, dep) =
parse_dependency_spec("official:agents/reviewer.md@v1.0.0", &None, None).unwrap();
assert_eq!(name, "reviewer");
if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.source, Some("official".to_string()));
assert_eq!(detailed.path, "agents/reviewer.md");
assert_eq!(detailed.version, Some("v1.0.0".to_string()));
} else {
panic!("Expected detailed dependency");
}
}
#[test]
fn test_parse_local_dependency() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.md");
std::fs::write(&test_file, "# Test").unwrap();
let (name, dep) =
parse_dependency_spec(test_file.to_str().unwrap(), &Some("my-agent".to_string()), None)
.unwrap();
assert_eq!(name, "my-agent");
if let ResourceDependency::Simple(path) = dep {
assert_eq!(path, test_file.to_str().unwrap());
} else {
panic!("Expected simple dependency");
}
}
#[test]
fn test_parse_dependency_with_custom_name() {
let (name, _) = parse_dependency_spec(
"official:snippets/utils.md@v1.0.0",
&Some("my-utils".to_string()),
None,
)
.unwrap();
assert_eq!(name, "my-utils");
}
#[test]
fn test_parse_dependency_without_version() {
let (name, dep) = parse_dependency_spec("source:path/to/file.md", &None, None).unwrap();
assert_eq!(name, "file");
if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.source.as_deref(), Some("source"));
assert_eq!(detailed.path, "path/to/file.md");
assert_eq!(detailed.version.as_deref(), Some("main"));
} else {
panic!("Expected detailed dependency");
}
}
#[test]
fn test_parse_dependency_with_branch() {
let (name, dep) = parse_dependency_spec("src:file.md@main", &None, None).unwrap();
assert_eq!(name, "file");
if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.version.as_deref(), Some("main"));
} else {
panic!("Expected detailed dependency");
}
}
#[test]
fn test_parse_dependency_local_source_no_default_version() {
let mut manifest = Manifest::new();
manifest.sources.insert("local-src".to_string(), "/path/to/local".to_string());
let (name, dep) =
parse_dependency_spec("local-src:path/to/file.md", &None, Some(&manifest)).unwrap();
assert_eq!(name, "file");
if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.source.as_deref(), Some("local-src"));
assert_eq!(detailed.path, "path/to/file.md");
assert!(detailed.version.is_none());
} else {
panic!("Expected detailed dependency");
}
}
fn manifest_path_to_lockfile(manifest_path: &std::path::Path) -> std::path::PathBuf {
manifest_path.with_file_name("agpm.lock")
}
#[test]
fn test_manifest_path_to_lockfile() {
use std::path::PathBuf;
let manifest = PathBuf::from("/project/agpm.toml");
let lockfile = manifest_path_to_lockfile(&manifest);
assert_eq!(lockfile, PathBuf::from("/project/agpm.lock"));
let manifest2 = PathBuf::from("./agpm.toml");
let lockfile2 = manifest_path_to_lockfile(&manifest2);
assert_eq!(lockfile2, PathBuf::from("./agpm.lock"));
}
#[tokio::test]
async fn test_execute_add_source() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let add_command = AddCommand {
command: AddSubcommand::Source {
name: "test-source".to_string(),
url: "https://github.com/test/repo.git".to_string(),
},
};
let result = add_command.execute_with_manifest_path(Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to execute add source: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.sources.contains_key("test-source"));
assert_eq!(
manifest.sources.get("test-source").unwrap(),
"https://github.com/test/repo.git"
);
}
#[tokio::test]
async fn test_execute_add_agent_dependency() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let agent_file = temp_dir.path().join("test-agent.md");
std::fs::write(&agent_file, "# Test Agent\nThis is a test agent.").unwrap();
let add_command = AddCommand {
command: AddSubcommand::Dep(DependencySubcommand::Agent(AgentDependency {
common: DependencySpec {
spec: agent_file.to_string_lossy().to_string(),
name: Some("my-test-agent".to_string()),
tool: None,
target: None,
filename: None,
force: false,
no_install: false,
},
})),
};
let result = add_command.execute_with_manifest_path(Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add local agent: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.agents.contains_key("my-test-agent"));
}
#[tokio::test]
async fn test_execute_add_snippet_dependency() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let snippet_file = temp_dir.path().join("test-snippet.md");
std::fs::write(&snippet_file, "# Test Snippet\nUseful code snippet.").unwrap();
let add_command = AddCommand {
command: AddSubcommand::Dep(DependencySubcommand::Snippet(SnippetDependency {
common: DependencySpec {
spec: snippet_file.to_string_lossy().to_string(),
name: Some("my-snippet".to_string()),
tool: None,
target: None,
filename: None,
force: false,
no_install: false,
},
})),
};
let result = add_command.execute_with_manifest_path(Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add local snippet: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.snippets.contains_key("my-snippet"));
}
#[tokio::test]
async fn test_execute_add_command_dependency() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let command_file = temp_dir.path().join("test-command.md");
std::fs::write(&command_file, "# Test Command\nUseful command.").unwrap();
let add_command = AddCommand {
command: AddSubcommand::Dep(DependencySubcommand::Command(CommandDependency {
common: DependencySpec {
spec: command_file.to_string_lossy().to_string(),
name: Some("my-command".to_string()),
tool: None,
target: None,
filename: None,
force: false,
no_install: false,
},
})),
};
let result = add_command.execute_with_manifest_path(Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add local command: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.commands.contains_key("my-command"));
}
#[tokio::test]
async fn test_execute_add_mcp_server_dependency() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let mcp_config = serde_json::json!({
"command": "npx",
"args": ["-y", "@test/mcp-server"],
"env": {}
});
let mcp_file_path = temp_dir.path().join("test-mcp.json");
std::fs::write(&mcp_file_path, mcp_config.to_string()).unwrap();
let add_command = AddCommand {
command: AddSubcommand::Dep(DependencySubcommand::McpServer(McpServerDependency {
common: DependencySpec {
spec: mcp_file_path.to_string_lossy().to_string(),
name: Some("test-mcp".to_string()),
tool: None,
target: None,
filename: None,
force: false,
no_install: false,
},
})),
};
let result = add_command.execute_with_manifest_path(Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add MCP server: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.mcp_servers.contains_key("test-mcp"));
let mcp_config_path = temp_dir.path().join(".mcp.json");
assert!(mcp_config_path.exists(), "MCP config file should be created");
let mcp_config: serde_json::Value = crate::utils::read_json_file(&mcp_config_path).unwrap();
assert!(
mcp_config.get("mcpServers").and_then(|s| s.get("test-mcp")).is_some(),
"MCP server should be configured in .mcp.json"
);
}
#[tokio::test]
async fn test_add_source_success() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let source = SourceSpec {
name: "new-source".to_string(),
url: "https://github.com/new/repo.git".to_string(),
};
let result = add_source_with_manifest_path(source, Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add source: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.sources.contains_key("new-source"));
assert_eq!(manifest.sources.get("new-source").unwrap(), "https://github.com/new/repo.git");
}
#[tokio::test]
async fn test_add_source_already_exists() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest_with_content(&manifest_path);
let source = SourceSpec {
name: "existing".to_string(),
url: "https://github.com/different/repo.git".to_string(),
};
let result = add_source_with_manifest_path(source, Some(manifest_path.clone())).await;
assert!(result.is_err());
let error_msg = result.err().unwrap().to_string();
assert!(error_msg.contains("Source 'existing' already exists"));
}
#[test]
fn test_parse_dependency_spec_file_prefix() {
let (name, dep) = parse_dependency_spec("file:/path/to/agent.md", &None, None).unwrap();
assert_eq!(name, "agent");
if let ResourceDependency::Simple(path) = dep {
assert_eq!(path, "/path/to/agent.md"); } else {
panic!("Expected simple dependency");
}
}
#[test]
fn test_parse_dependency_spec_simple_path() {
let (name, dep) = parse_dependency_spec("nonexistent/path.md", &None, None).unwrap();
assert_eq!(name, "path");
if let ResourceDependency::Simple(path) = dep {
assert_eq!(path, "nonexistent/path.md");
} else {
panic!("Expected simple dependency");
}
}
#[test]
fn test_parse_dependency_spec_custom_name_simple() {
let (name, dep) =
parse_dependency_spec("simple/path.md", &Some("custom-name".to_string()), None)
.unwrap();
assert_eq!(name, "custom-name");
if let ResourceDependency::Simple(path) = dep {
assert_eq!(path, "simple/path.md");
} else {
panic!("Expected simple dependency");
}
}
#[test]
fn test_parse_dependency_spec_path_without_extension() {
let (name, dep) = parse_dependency_spec("source:agents/noext@v1.0", &None, None).unwrap();
assert_eq!(name, "noext");
if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.source, Some("source".to_string()));
assert_eq!(detailed.path, "agents/noext");
assert_eq!(detailed.version, Some("v1.0".to_string()));
} else {
panic!("Expected detailed dependency");
}
}
#[test]
fn test_parse_dependency_spec_unknown_fallback() {
let (name, dep) = parse_dependency_spec("malformed::", &None, None).unwrap();
assert_eq!(name, ":"); if let ResourceDependency::Detailed(detailed) = dep {
assert_eq!(detailed.source, Some("malformed".to_string())); assert_eq!(detailed.path, ":"); } else {
panic!("Expected detailed dependency");
}
}
#[tokio::test]
async fn test_install_single_dependency_mcp_server() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
let mcp_config = serde_json::json!({
"command": "node",
"args": ["server.js", "--port=3000"],
"env": {
"NODE_ENV": "production"
}
});
let mcp_file_path = temp_dir.path().join("test-mcp.json");
std::fs::write(&mcp_file_path, mcp_config.to_string()).unwrap();
let manifest_content = format!(
r#"[sources]
[agents]
[snippets]
[commands]
[mcp-servers]
test-mcp = "{}"
"#,
normalize_path_for_storage(&mcp_file_path)
);
std::fs::write(&manifest_path, manifest_content).unwrap();
let manifest = Manifest::load(&manifest_path).unwrap();
let result =
install_single_dependency("test-mcp", "mcp-server", &manifest, &manifest_path).await;
assert!(result.is_ok(), "MCP server installation should succeed: {result:?}");
let mcp_config_path = temp_dir.path().join(".mcp.json");
assert!(mcp_config_path.exists(), "MCP config file should be created");
let mcp_config: serde_json::Value = crate::utils::read_json_file(&mcp_config_path).unwrap();
assert!(
mcp_config.get("mcpServers").and_then(|s| s.get("test-mcp")).is_some(),
"MCP server should be configured in .mcp.json"
);
}
#[tokio::test]
async fn test_install_single_dependency_invalid_resource_type() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
let test_file = temp_dir.path().join("test.md");
std::fs::write(&test_file, "# Test content").unwrap();
let manifest_content = format!(
r#"[sources]
[agents]
test = "{}"
[snippets]
[commands]
"#,
normalize_path_for_storage(&test_file)
);
std::fs::write(&manifest_path, manifest_content).unwrap();
let manifest = Manifest::load(&manifest_path).unwrap();
let result = install_single_dependency(
"test",
"invalid-type", &manifest,
&manifest_path,
)
.await;
assert!(result.is_ok(), "Install should succeed with full command: {result:?}");
}
#[tokio::test]
async fn test_install_single_dependency_source_not_found() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
let manifest_content = r#"[sources]
[agents]
test-agent = { source = "nonexistent-source", path = "agents/test.md", version = "v1.0.0" }
[snippets]
[commands]
"#;
std::fs::write(&manifest_path, manifest_content).unwrap();
let manifest_result = Manifest::load(&manifest_path);
assert!(manifest_result.is_err(), "Should fail to load manifest with nonexistent source");
let error_msg = manifest_result.err().unwrap().to_string();
assert!(error_msg.contains("nonexistent-source") || error_msg.contains("not defined"));
}
#[tokio::test]
async fn test_add_dependency_agent_with_force() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
let original_agent_file = temp_dir.path().join("original-agent.md");
std::fs::write(&original_agent_file, "# Original Agent\nOriginal content.").unwrap();
let manifest_content = format!(
r#"[sources]
existing = "https://github.com/existing/repo.git"
[target]
agents = ".claude/agents"
snippets = ".agpm/snippets"
commands = ".claude/commands"
[agents]
existing-agent = "{}"
[snippets]
[commands]
"#,
normalize_path_for_storage(&original_agent_file)
);
std::fs::write(&manifest_path, manifest_content).unwrap();
let agent_file = temp_dir.path().join("new-agent.md");
std::fs::write(&agent_file, "# New Agent\nReplacement agent.").unwrap();
let dep_type = DependencyType::Agent(AgentDependency {
common: DependencySpec {
spec: agent_file.to_string_lossy().to_string(),
name: Some("existing-agent".to_string()), tool: None,
target: None,
filename: None,
force: true, no_install: false,
},
});
let result = add_dependency_with_manifest_path(dep_type, Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add agent with force flag: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.agents.contains_key("existing-agent"));
if let ResourceDependency::Simple(path) = manifest.agents.get("existing-agent").unwrap() {
assert!(path.contains("new-agent.md"));
} else {
panic!("Expected simple dependency");
}
}
#[tokio::test]
async fn test_add_dependency_mcp_server_without_force() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest_with_content(&manifest_path);
let dep_type = DependencyType::McpServer(McpServerDependency {
common: DependencySpec {
spec: "different-command different args".to_string(),
name: Some("existing-mcp".to_string()), tool: None,
target: None,
filename: None,
force: false, no_install: false,
},
});
let result = add_dependency_with_manifest_path(dep_type, Some(manifest_path.clone())).await;
assert!(result.is_err());
let error_msg = result.err().unwrap().to_string();
assert!(
error_msg.contains("existing-mcp")
&& (error_msg.contains("already exists") || error_msg.contains("force"))
);
}
#[tokio::test]
async fn test_add_dependency_snippet_without_force() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest_with_content(&manifest_path);
let snippet_file = temp_dir.path().join("new-snippet.md");
std::fs::write(&snippet_file, "# New Snippet\nReplacement snippet.").unwrap();
let dep_type = DependencyType::Snippet(SnippetDependency {
common: DependencySpec {
spec: snippet_file.to_string_lossy().to_string(),
name: Some("existing-snippet".to_string()), tool: None,
target: None,
filename: None,
force: false, no_install: false,
},
});
let result = add_dependency_with_manifest_path(dep_type, Some(manifest_path.clone())).await;
assert!(result.is_err());
let error_msg = result.err().unwrap().to_string();
assert!(
error_msg.contains("existing-snippet")
&& (error_msg.contains("already exists") || error_msg.contains("force"))
);
}
#[tokio::test]
async fn test_add_dependency_command_without_force() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest_with_content(&manifest_path);
let command_file = temp_dir.path().join("new-command.md");
std::fs::write(&command_file, "# New Command\nReplacement command.").unwrap();
let dep_type = DependencyType::Command(CommandDependency {
common: DependencySpec {
spec: command_file.to_string_lossy().to_string(),
name: Some("existing-command".to_string()), tool: None,
target: None,
filename: None,
force: false, no_install: false,
},
});
let result = add_dependency_with_manifest_path(dep_type, Some(manifest_path.clone())).await;
assert!(result.is_err());
let error_msg = result.err().unwrap().to_string();
assert!(
error_msg.contains("existing-command")
&& (error_msg.contains("already exists") || error_msg.contains("force"))
);
}
#[tokio::test]
async fn test_add_dependency_mcp_server_with_file() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
create_test_manifest(&manifest_path);
let mcp_config = serde_json::json!({
"command": "node",
"args": ["server.js", "--port=3000"],
"env": {
"NODE_ENV": "production"
}
});
let mcp_file_path = temp_dir.path().join("test-mcp.json");
std::fs::write(&mcp_file_path, mcp_config.to_string()).unwrap();
let dep_type = DependencyType::McpServer(McpServerDependency {
common: DependencySpec {
spec: mcp_file_path.to_string_lossy().to_string(),
name: Some("file-mcp".to_string()),
tool: None,
target: None,
filename: None,
force: false,
no_install: false,
},
});
let result = add_dependency_with_manifest_path(dep_type, Some(manifest_path.clone())).await;
assert!(result.is_ok(), "Failed to add MCP server with file: {result:?}");
let manifest = Manifest::load(&manifest_path).unwrap();
assert!(manifest.mcp_servers.contains_key("file-mcp"));
let mcp_config_path = temp_dir.path().join(".mcp.json");
assert!(mcp_config_path.exists(), "MCP config file should be created");
let mcp_config: serde_json::Value = crate::utils::read_json_file(&mcp_config_path).unwrap();
assert!(
mcp_config.get("mcpServers").and_then(|s| s.get("file-mcp")).is_some(),
"MCP server should be configured in .mcp.json"
);
}
}