// SPDX-License-Identifier: LicenseRef-Commercial // Multi-Account Profile Switching — manage multiple Claude/provider accounts. use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AccountProfile { pub id: String, pub display_name: String, pub email: Option, pub provider: String, pub config_dir: String, pub is_active: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ActiveAccount { pub profile_id: String, pub provider: String, pub config_dir: String, } /// List all configured account profiles across providers. /// Scans ~/.config/agor/accounts.json for profile definitions. #[tauri::command] pub fn pro_list_accounts() -> Result, String> { let accounts_path = accounts_file_path()?; let active = load_active_id(); if !accounts_path.exists() { // Return default account derived from Claude profiles let home = dirs::home_dir().unwrap_or_default(); let default_dir = home.join(".claude").to_string_lossy().to_string(); return Ok(vec![AccountProfile { id: "default".into(), display_name: "Default".into(), email: None, provider: "claude".into(), config_dir: default_dir, is_active: true, }]); } let content = std::fs::read_to_string(&accounts_path) .map_err(|e| format!("Failed to read accounts.json: {e}"))?; let mut profiles: Vec = serde_json::from_str(&content) .map_err(|e| format!("Failed to parse accounts.json: {e}"))?; for p in &mut profiles { p.is_active = p.id == active; } Ok(profiles) } /// Get the currently active account. #[tauri::command] pub fn pro_get_active_account() -> Result { let profiles = pro_list_accounts()?; let active = profiles.iter() .find(|p| p.is_active) .or_else(|| profiles.first()) .ok_or("No accounts configured")?; Ok(ActiveAccount { profile_id: active.id.clone(), provider: active.provider.clone(), config_dir: active.config_dir.clone(), }) } /// Switch the active account. Updates ~/.config/agor/active-account. #[tauri::command] pub fn pro_set_active_account(profile_id: String) -> Result { let profiles = pro_list_accounts()?; let target = profiles.iter() .find(|p| p.id == profile_id) .ok_or_else(|| format!("Account '{}' not found", profile_id))?; let active_path = active_account_path()?; std::fs::write(&active_path, &profile_id) .map_err(|e| format!("Failed to write active account: {e}"))?; Ok(ActiveAccount { profile_id: target.id.clone(), provider: target.provider.clone(), config_dir: target.config_dir.clone(), }) } fn accounts_file_path() -> Result { let config = agor_core::config::AppConfig::from_env(); Ok(config.config_dir.join("accounts.json")) } fn active_account_path() -> Result { let config = agor_core::config::AppConfig::from_env(); Ok(config.config_dir.join("active-account")) } fn load_active_id() -> String { active_account_path() .ok() .and_then(|p| std::fs::read_to_string(p).ok()) .map(|s| s.trim().to_string()) .unwrap_or_else(|| "default".into()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_account_profile_serializes_camel_case() { let p = AccountProfile { id: "work".into(), display_name: "Work Account".into(), email: Some("work@example.com".into()), provider: "claude".into(), config_dir: "/home/user/.claude-work".into(), is_active: true, }; let json = serde_json::to_string(&p).unwrap(); assert!(json.contains("displayName")); assert!(json.contains("isActive")); assert!(json.contains("configDir")); } #[test] fn test_active_account_struct() { let a = ActiveAccount { profile_id: "test".into(), provider: "claude".into(), config_dir: "/tmp/test".into(), }; let json = serde_json::to_string(&a).unwrap(); assert!(json.contains("profileId")); } }