use anyhow::{Context, Result, bail};
use clap::Parser;
use colored::Colorize;
use std::path::{Path, PathBuf};
use crate::cli::install::InstallCommand;
#[derive(Parser, Debug)]
#[command(name = "migrate")]
pub struct MigrateCommand {
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
skip_install: bool,
}
impl MigrateCommand {
#[must_use]
pub fn new(path: Option<PathBuf>, dry_run: bool, skip_install: bool) -> Self {
Self {
path,
dry_run,
skip_install,
}
}
pub async fn execute(self) -> Result<()> {
let dir = self.path.as_deref().unwrap_or_else(|| Path::new("."));
let dir = dir.canonicalize().context("Failed to resolve directory path")?;
println!("🔍 Checking for legacy CCPM files in: {}", dir.display());
let ccpm_toml = dir.join("ccpm.toml");
let ccpm_lock = dir.join("ccpm.lock");
let agpm_toml = dir.join("agpm.toml");
let agpm_lock = dir.join("agpm.lock");
let ccpm_toml_exists = ccpm_toml.exists();
let ccpm_lock_exists = ccpm_lock.exists();
let agpm_toml_exists = agpm_toml.exists();
let agpm_lock_exists = agpm_lock.exists();
if !ccpm_toml_exists && !ccpm_lock_exists {
println!("✅ {}", "No legacy CCPM files found.".green());
return Ok(());
}
let mut conflicts = Vec::new();
if ccpm_toml_exists && agpm_toml_exists {
conflicts.push("agpm.toml already exists");
}
if ccpm_lock_exists && agpm_lock_exists {
conflicts.push("agpm.lock already exists");
}
if !conflicts.is_empty() {
bail!(
"Migration conflict: {}. Please resolve conflicts manually.",
conflicts.join(" and ")
);
}
println!("\n📦 Files to migrate:");
if ccpm_toml_exists {
println!(" • ccpm.toml → agpm.toml");
}
if ccpm_lock_exists {
println!(" • ccpm.lock → agpm.lock");
}
if self.dry_run {
println!(
"\n{} (use without --dry-run to perform migration)",
"Dry run complete".yellow()
);
return Ok(());
}
if ccpm_toml_exists {
std::fs::rename(&ccpm_toml, &agpm_toml)
.context("Failed to rename ccpm.toml to agpm.toml")?;
println!("✅ {}", "Renamed ccpm.toml → agpm.toml".green());
}
if ccpm_lock_exists {
std::fs::rename(&ccpm_lock, &agpm_lock)
.context("Failed to rename ccpm.lock to agpm.lock")?;
println!("✅ {}", "Renamed ccpm.lock → agpm.lock".green());
}
println!("\n🎉 {}", "File migration completed successfully!".green().bold());
if !self.skip_install {
println!("\n📦 {}", "Running installation to update artifact locations...".cyan());
let install_cmd = InstallCommand::new();
let manifest_path = dir.join("agpm.toml");
match install_cmd.execute_from_path(Some(&manifest_path)).await {
Ok(()) => {
println!("✅ {}", "Artifacts moved to correct locations".green());
}
Err(e) => {
eprintln!("\n⚠️ {}", "Warning: Installation failed".yellow());
eprintln!(" {}", format!("Error: {}", e).yellow());
eprintln!(" {}", "You may need to run 'agpm install' manually".yellow());
}
}
} else {
println!(
"\n💡 Next step: Run {} to move artifacts to correct locations",
"agpm install".cyan()
);
}
println!(
"\n💡 Remember to:\n • Review the changes\n • Run {} to verify\n • Commit the changes to version control",
"agpm validate".cyan()
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_migrate_no_files() {
let temp_dir = TempDir::new().unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_migrate_both_files() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
let ccpm_lock = temp_dir.path().join("ccpm.lock");
fs::write(&ccpm_toml, "[sources]\n").unwrap();
fs::write(&ccpm_lock, "# lockfile\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(!ccpm_toml.exists());
assert!(!ccpm_lock.exists());
assert!(temp_dir.path().join("agpm.toml").exists());
assert!(temp_dir.path().join("agpm.lock").exists());
}
#[tokio::test]
async fn test_migrate_dry_run() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
fs::write(&ccpm_toml, "[sources]\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: true,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(ccpm_toml.exists());
assert!(!temp_dir.path().join("agpm.toml").exists());
}
#[tokio::test]
async fn test_migrate_conflict() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
let agpm_toml = temp_dir.path().join("agpm.toml");
fs::write(&ccpm_toml, "[sources]\n").unwrap();
fs::write(&agpm_toml, "[sources]\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("conflict"));
}
#[tokio::test]
async fn test_migrate_only_toml() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
fs::write(&ccpm_toml, "[sources]\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(!ccpm_toml.exists());
assert!(temp_dir.path().join("agpm.toml").exists());
}
#[tokio::test]
async fn test_migrate_only_lock() {
let temp_dir = TempDir::new().unwrap();
let ccpm_lock = temp_dir.path().join("ccpm.lock");
fs::write(&ccpm_lock, "# lockfile\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: true,
};
let result = cmd.execute().await;
assert!(result.is_ok());
assert!(!ccpm_lock.exists());
assert!(temp_dir.path().join("agpm.lock").exists());
}
#[tokio::test]
async fn test_migrate_with_automatic_installation() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
fs::write(&ccpm_toml, "[sources]\n").unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: false, };
let result = cmd.execute().await;
assert!(result.is_ok(), "Migration with automatic installation should succeed");
assert!(!ccpm_toml.exists());
assert!(temp_dir.path().join("agpm.toml").exists());
assert!(temp_dir.path().join("agpm.lock").exists());
}
#[tokio::test]
async fn test_migrate_handles_installation_failure() {
let temp_dir = TempDir::new().unwrap();
let ccpm_toml = temp_dir.path().join("ccpm.toml");
fs::write(
&ccpm_toml,
"[sources]\ntest = \"https://github.com/nonexistent/repo.git\"\n\n\
[agents]\ntest-agent = { source = \"test\", path = \"agents/test.md\", version = \"v1.0.0\" }",
)
.unwrap();
let cmd = MigrateCommand {
path: Some(temp_dir.path().to_path_buf()),
dry_run: false,
skip_install: false, };
let result = cmd.execute().await;
assert!(result.is_ok(), "Migration should succeed even if installation fails");
assert!(!ccpm_toml.exists());
assert!(temp_dir.path().join("agpm.toml").exists());
}
}