diff --git a/src/cli/profile.rs b/src/cli/profile.rs index ae6d62651..07acc9f18 100644 --- a/src/cli/profile.rs +++ b/src/cli/profile.rs @@ -26,6 +26,15 @@ pub enum ProfileCommands { name: String, }, + /// Rename a profile + #[command(alias = "mv")] + Rename { + /// Current profile name + old_name: String, + /// New profile name + new_name: String, + }, + /// Show or set default profile Default { /// Profile name (optional, shows current if not provided) @@ -38,6 +47,9 @@ pub async fn run(command: Option) -> Result<()> { Some(ProfileCommands::List) | None => list_profiles().await, Some(ProfileCommands::Create { name }) => create_profile(&name).await, Some(ProfileCommands::Delete { name }) => delete_profile(&name).await, + Some(ProfileCommands::Rename { old_name, new_name }) => { + rename_profile(&old_name, &new_name).await + } Some(ProfileCommands::Default { name }) => { if let Some(n) = name { set_default_profile(&n).await @@ -82,6 +94,12 @@ async fn create_profile(name: &str) -> Result<()> { Ok(()) } +async fn rename_profile(old_name: &str, new_name: &str) -> Result<()> { + session::rename_profile(old_name, new_name)?; + println!("✓ Renamed profile: {} -> {}", old_name, new_name); + Ok(()) +} + async fn delete_profile(name: &str) -> Result<()> { print!( "Are you sure you want to delete profile '{}'? This will remove all sessions in this profile. [y/N] ", diff --git a/src/session/mod.rs b/src/session/mod.rs index 9d6d2ddfc..c260bbbba 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -129,6 +129,37 @@ pub fn delete_profile(name: &str) -> Result<()> { Ok(()) } +pub fn rename_profile(old_name: &str, new_name: &str) -> Result<()> { + if new_name.is_empty() { + anyhow::bail!("New profile name cannot be empty"); + } + if new_name.contains('/') || new_name.contains('\\') { + anyhow::bail!("Profile name cannot contain path separators"); + } + + let base = get_app_dir()?; + let old_dir = base.join("profiles").join(old_name); + let new_dir = base.join("profiles").join(new_name); + + if !old_dir.exists() { + anyhow::bail!("Profile '{}' does not exist", old_name); + } + if new_dir.exists() { + anyhow::bail!("Profile '{}' already exists", new_name); + } + + fs::rename(&old_dir, &new_dir)?; + + // Update default profile if the renamed profile was the default + if let Some(config) = load_config()? { + if config.default_profile == old_name { + set_default_profile(new_name)?; + } + } + + Ok(()) +} + pub fn set_default_profile(name: &str) -> Result<()> { let mut config = load_config()?.unwrap_or_default(); config.default_profile = name.to_string(); diff --git a/tests/profile_management.rs b/tests/profile_management.rs index 9137b4329..5431fafb7 100644 --- a/tests/profile_management.rs +++ b/tests/profile_management.rs @@ -1,7 +1,8 @@ -//! Integration tests for profile management: create, delete, list, default, and isolation. +//! Integration tests for profile management: create, delete, list, default, rename, and isolation. use agent_of_empires::session::{ - create_profile, delete_profile, list_profiles, set_default_profile, Config, Instance, Storage, + create_profile, delete_profile, list_profiles, rename_profile, set_default_profile, Config, + Instance, Storage, }; use anyhow::Result; use serial_test::serial; @@ -111,6 +112,120 @@ fn test_profile_session_isolation() -> Result<()> { Ok(()) } +#[test] +#[serial] +fn test_rename_profile() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("old_name")?; + // Add a session so we can verify data moves with the rename + let storage = Storage::new("old_name")?; + let instance = Instance::new("Test Session", "/path/test"); + storage.save(&[instance])?; + + rename_profile("old_name", "new_name")?; + + let profiles = list_profiles()?; + assert!(!profiles.contains(&"old_name".to_string())); + assert!(profiles.contains(&"new_name".to_string())); + + // Verify sessions moved with the profile + let new_storage = Storage::new("new_name")?; + let sessions = new_storage.load()?; + assert_eq!(sessions.len(), 1); + assert_eq!(sessions[0].title, "Test Session"); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_profile_updates_default() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("primary")?; + set_default_profile("primary")?; + + rename_profile("primary", "renamed")?; + + let config = Config::load()?; + assert_eq!(config.default_profile, "renamed"); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_profile_nondefault_keeps_default() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("main_profile")?; + create_profile("other")?; + set_default_profile("main_profile")?; + + rename_profile("other", "renamed_other")?; + + let config = Config::load()?; + assert_eq!(config.default_profile, "main_profile"); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_profile_to_existing_fails() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("first")?; + create_profile("second")?; + + let result = rename_profile("first", "second"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("already exists")); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_nonexistent_profile_fails() -> Result<()> { + let _temp = setup_temp_home(); + + let result = rename_profile("nonexistent", "new_name"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("does not exist")); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_profile_empty_name_fails() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("valid")?; + + let result = rename_profile("valid", ""); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("cannot be empty")); + + Ok(()) +} + +#[test] +#[serial] +fn test_rename_profile_with_path_separator_fails() -> Result<()> { + let _temp = setup_temp_home(); + + create_profile("valid")?; + + let result = rename_profile("valid", "bad/name"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("path separators")); + + Ok(()) +} + #[test] #[serial] fn test_profile_config_isolation() -> Result<()> {