use anyhow::{Result, anyhow};
use clap::Args;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
#[derive(Args)]
pub struct InitCommand {
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short, long)]
force: bool,
}
impl InitCommand {
pub async fn execute_with_manifest_path(
self,
_manifest_path: Option<std::path::PathBuf>,
) -> Result<()> {
self.execute().await
}
pub async fn execute(self) -> Result<()> {
let target_dir = self.path.unwrap_or_else(|| PathBuf::from("."));
let manifest_path = target_dir.join("agpm.toml");
let gitignore_path = target_dir.join(".gitignore");
if manifest_path.exists() && !self.force {
return Err(anyhow!(
"Manifest already exists at {}. Use --force to overwrite",
manifest_path.display()
));
}
if !target_dir.exists() {
fs::create_dir_all(&target_dir)?;
}
let template = r#"# AGPM Manifest
# This file defines your Claude Code resource dependencies
[sources]
# Add your Git repository sources here
# Example: official = "https://github.com/aig787/agpm-community.git"
# Tool type configurations (multi-tool support)
[tools.claude-code]
path = ".claude"
resources = { agents = { path = "agents" }, snippets = { path = "agpm/snippets" }, commands = { path = "commands" }, scripts = { path = "scripts" }, hooks = { path = "hooks" }, mcp-servers = { path = "mcp-servers" } }
[tools.opencode]
path = ".opencode"
resources = { agents = { path = "agent" }, commands = { path = "command" } }
# Note: OpenCode MCP servers merge into opencode.json (no file installation)
[tools.agpm]
path = ".agpm"
resources = { snippets = { path = "snippets" } }
[agents]
# Add your agent dependencies here
# Example: my-agent = { source = "official", path = "agents/my-agent.md", version = "v1.0.0" }
# For OpenCode: my-agent = { source = "official", path = "agents/my-agent.md", version = "v1.0.0", tool = "opencode" }
[snippets]
# Add your snippet dependencies here (AGPM-specific resources)
# Example: utils = { source = "official", path = "snippets/utils.md", tool = "agpm" }
[commands]
# Add your command dependencies here
# Example: deploy = { source = "official", path = "commands/deploy.md" }
[scripts]
# Add your script dependencies here
# Example: build = { source = "official", path = "scripts/build.sh" }
[hooks]
# Add your hook dependencies here
# Example: pre-commit = { source = "official", path = "hooks/pre-commit.json" }
[mcp-servers]
# Add your MCP server dependencies here
# Example: filesystem = { source = "official", path = "mcp-servers/filesystem.json" }
"#;
fs::write(&manifest_path, template)?;
let gitignore_entries = vec![".claude/agpm/"];
let mut gitignore_content = if gitignore_path.exists() {
fs::read_to_string(&gitignore_path)?
} else {
String::new()
};
if !gitignore_content.contains("# AGPM managed directories") {
if !gitignore_content.is_empty() && !gitignore_content.ends_with('\n') {
gitignore_content.push('\n');
}
if !gitignore_content.is_empty() {
gitignore_content.push('\n');
}
gitignore_content.push_str("# AGPM managed directories\n");
for entry in gitignore_entries {
if !gitignore_content.lines().any(|line| line.trim() == entry) {
gitignore_content.push_str(entry);
gitignore_content.push('\n');
}
}
fs::write(&gitignore_path, gitignore_content)?;
println!("{} Updated .gitignore with AGPM entries", "✓".green());
}
println!("{} Initialized agpm.toml at {}", "✓".green(), manifest_path.display());
println!("\n{}", "Next steps:".cyan());
println!(" Add dependencies with {}:", "agpm add".bright_white());
println!(
" agpm add agent my-agent --source https://github.com/org/repo.git --path agents/my-agent.md"
);
println!(" agpm add snippet utils --path ../local/snippets/utils.md");
println!("\n Then run {} to install", "agpm install".bright_white());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_init_creates_manifest() {
let temp_dir = TempDir::new().unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let manifest_path = temp_dir.path().join("agpm.toml");
assert!(manifest_path.exists());
let content = fs::read_to_string(&manifest_path).unwrap();
assert!(content.contains("[sources]"));
assert!(content.contains("[agents]"));
assert!(content.contains("[snippets]"));
}
#[tokio::test]
async fn test_init_creates_directory_if_not_exists() {
let temp_dir = TempDir::new().unwrap();
let new_dir = temp_dir.path().join("new_project");
let cmd = InitCommand {
path: Some(new_dir.clone()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(new_dir.exists());
assert!(new_dir.join("agpm.toml").exists());
}
#[tokio::test]
async fn test_init_fails_if_manifest_exists() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
fs::write(&manifest_path, "existing content").unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[tokio::test]
async fn test_init_force_overwrites_existing() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
fs::write(&manifest_path, "old content").unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let content = fs::read_to_string(&manifest_path).unwrap();
assert!(content.contains("[sources]"));
assert!(!content.contains("old content"));
}
#[tokio::test]
async fn test_init_uses_current_dir_by_default() {
let temp_dir = TempDir::new().unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(temp_dir.path().join("agpm.toml").exists());
}
#[tokio::test]
async fn test_init_template_content() {
let temp_dir = TempDir::new().unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let manifest_path = temp_dir.path().join("agpm.toml");
let content = fs::read_to_string(&manifest_path).unwrap();
assert!(content.contains("# AGPM Manifest"));
assert!(content.contains("# This file defines your Claude Code resource dependencies"));
assert!(content.contains("# Add your Git repository sources here"));
assert!(content.contains("# Example: official ="));
assert!(content.contains("# Add your agent dependencies here"));
assert!(content.contains("# Example: my-agent ="));
assert!(content.contains("# Add your snippet dependencies here"));
assert!(content.contains("# Example: utils ="));
}
#[tokio::test]
async fn test_init_nested_directory_creation() {
let temp_dir = TempDir::new().unwrap();
let nested_path = temp_dir.path().join("a").join("b").join("c");
let cmd = InitCommand {
path: Some(nested_path.clone()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(nested_path.exists());
assert!(nested_path.join("agpm.toml").exists());
}
#[tokio::test]
async fn test_init_force_flag_behavior() {
let temp_dir = TempDir::new().unwrap();
let manifest_path = temp_dir.path().join("agpm.toml");
let initial_content = "# Old manifest\n[sources]\n";
fs::write(&manifest_path, initial_content).unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
let content = fs::read_to_string(&manifest_path).unwrap();
assert_eq!(content, initial_content);
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let new_content = fs::read_to_string(&manifest_path).unwrap();
assert!(new_content.contains("# AGPM Manifest"));
assert!(!new_content.contains("# Old manifest"));
}
#[tokio::test]
async fn test_init_creates_gitignore() {
let temp_dir = TempDir::new().unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let gitignore_path = temp_dir.path().join(".gitignore");
assert!(gitignore_path.exists());
let content = fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains("# AGPM managed directories"));
assert!(content.contains(".claude/agpm/"));
}
#[tokio::test]
async fn test_init_updates_existing_gitignore() {
let temp_dir = TempDir::new().unwrap();
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, "node_modules/\n*.log\n").unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let content = fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains("node_modules/"));
assert!(content.contains("*.log"));
assert!(content.contains("# AGPM managed directories"));
assert!(content.contains(".claude/agpm/"));
}
#[tokio::test]
async fn test_init_does_not_duplicate_gitignore_entries() {
let temp_dir = TempDir::new().unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let gitignore_path = temp_dir.path().join(".gitignore");
let first_content = fs::read_to_string(&gitignore_path).unwrap();
let cmd = InitCommand {
path: Some(temp_dir.path().to_path_buf()),
force: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
let second_content = fs::read_to_string(&gitignore_path).unwrap();
assert_eq!(
first_content.matches("# AGPM managed directories").count(),
second_content.matches("# AGPM managed directories").count()
);
}
}