agent-orchestrator/agor-pro/src/profiles.rs
Hibryda 03fe2e2237 feat(pro): add analytics, export, and multi-account commercial features
3 new agor-pro modules: analytics.rs (usage dashboard queries),
export.rs (session/project Markdown report generation),
profiles.rs (multi-account switching via accounts.json).
9 Tauri plugin commands. Frontend IPC bridge (pro-bridge.ts).
168 cargo tests, 14 commercial vitest tests.
2026-03-17 01:52:46 +01:00

143 lines
4.3 KiB
Rust

// 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<String>,
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<Vec<AccountProfile>, 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<AccountProfile> = 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<ActiveAccount, String> {
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<ActiveAccount, String> {
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<PathBuf, String> {
let config = agor_core::config::AppConfig::from_env();
Ok(config.config_dir.join("accounts.json"))
}
fn active_account_path() -> Result<PathBuf, String> {
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"));
}
}