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.
This commit is contained in:
parent
6973c70c5a
commit
03fe2e2237
8 changed files with 805 additions and 3 deletions
143
agor-pro/src/profiles.rs
Normal file
143
agor-pro/src/profiles.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// 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"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue