Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/cli/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -38,6 +47,9 @@ pub async fn run(command: Option<ProfileCommands>) -> 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
Expand Down Expand Up @@ -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] ",
Expand Down
31 changes: 31 additions & 0 deletions src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
119 changes: 117 additions & 2 deletions tests/profile_management.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<()> {
Expand Down
Loading