use crate::agents::integrations::*;
use crate::agents::traits::AgentIntegration;
use crate::errors::TokenSaveError;
use std::path::Path;
pub fn get_integration(id: &str) -> crate::errors::Result<Box<dyn AgentIntegration>> {
match id {
"claude" => Ok(Box::new(ClaudeIntegration)),
"opencode" => Ok(Box::new(OpenCodeIntegration)),
"codex" => Ok(Box::new(CodexIntegration)),
"gemini" => Ok(Box::new(GeminiIntegration)),
"qwen" => Ok(Box::new(QwenIntegration)),
"copilot" => Ok(Box::new(CopilotIntegration)),
"cursor" => Ok(Box::new(CursorIntegration)),
"droid" => Ok(Box::new(DroidIntegration)),
"zed" => Ok(Box::new(ZedIntegration)),
"cline" => Ok(Box::new(ClineIntegration)),
"roo-code" => Ok(Box::new(RooCodeIntegration)),
"antigravity" => Ok(Box::new(AntigravityIntegration)),
"kilo" => Ok(Box::new(KiloIntegration)),
"kiro" => Ok(Box::new(KiroIntegration)),
"kimi" => Ok(Box::new(KimiIntegration)),
"vibe" => Ok(Box::new(VibeIntegration)),
"grok" => Ok(Box::new(GrokIntegration)),
"pi" => Ok(Box::new(PiIntegration)),
"plank" => Ok(Box::new(PlankIntegration)),
"auggie" => Ok(Box::new(AugmentIntegration)),
_ => Err(TokenSaveError::Config {
message: format!(
"unknown agent: \"{id}\". Available agents: {}",
available_integrations().join(", ")
),
}),
}
}
pub fn all_integrations() -> Vec<Box<dyn AgentIntegration>> {
vec![
Box::new(ClaudeIntegration),
Box::new(OpenCodeIntegration),
Box::new(CodexIntegration),
Box::new(GeminiIntegration),
Box::new(QwenIntegration),
Box::new(CopilotIntegration),
Box::new(CursorIntegration),
Box::new(DroidIntegration),
Box::new(ZedIntegration),
Box::new(ClineIntegration),
Box::new(RooCodeIntegration),
Box::new(AntigravityIntegration),
Box::new(KiloIntegration),
Box::new(KiroIntegration),
Box::new(KimiIntegration),
Box::new(VibeIntegration),
Box::new(GrokIntegration),
Box::new(PiIntegration),
Box::new(PlankIntegration),
Box::new(AugmentIntegration),
]
}
pub fn available_integrations() -> Vec<&'static str> {
vec![
"claude",
"opencode",
"codex",
"gemini",
"qwen",
"copilot",
"cursor",
"droid",
"zed",
"cline",
"roo-code",
"antigravity",
"kilo",
"kiro",
"kimi",
"vibe",
"grok",
"pi",
"plank",
"auggie",
]
}
pub fn detect_missing_installed_agents(home: &Path, current: &[String]) -> Vec<String> {
let mut additions = Vec::new();
for ag in all_integrations() {
let id = ag.id().to_string();
if ag.has_tokensave(home) && !current.contains(&id) {
additions.push(id);
}
}
additions
}
pub fn migrate_installed_agents(home: &Path, config: &mut crate::user_config::UserConfig) {
let additions = detect_missing_installed_agents(home, &config.installed_agents);
if additions.is_empty() {
return;
}
config.installed_agents.extend(additions);
config.save();
}
pub fn pick_integrations_interactive(
home: &Path,
installed: &[String],
) -> crate::errors::Result<(Vec<String>, Vec<String>)> {
let detected: Vec<Box<dyn AgentIntegration>> = all_integrations()
.into_iter()
.filter(|ag| ag.is_detected(home))
.collect();
if detected.is_empty() {
return Err(TokenSaveError::Config {
message: "No supported agents detected on this system".to_string(),
});
}
if detected.len() == 1 && !installed.contains(&detected[0].id().to_string()) {
let id = detected[0].id().to_string();
return Ok((vec![id], vec![]));
}
let mut to_install = Vec::new();
let mut to_uninstall = Vec::new();
for ag in &detected {
let id = ag.id().to_string();
let already = installed.contains(&id);
if already {
eprint!("Keep tokensave for {}? [Y/n] ", ag.name());
} else {
eprint!("Install tokensave for {}? [Y/n] ", ag.name());
}
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| TokenSaveError::Config {
message: format!("failed to read input: {e}"),
})?;
let answer = input.trim().to_lowercase();
let yes = answer.is_empty() || answer == "y" || answer == "yes";
if yes && !already {
to_install.push(id);
} else if !yes && already {
to_uninstall.push(id);
}
}
Ok((to_install, to_uninstall))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod migrate_tests {
use super::*;
use std::fs;
fn install_claude_marker(home: &Path) {
let claude_json = home.join(".claude.json");
fs::write(
&claude_json,
r#"{"mcpServers":{"tokensave":{"command":"tokensave","args":["serve"]}}}"#,
)
.unwrap();
}
#[test]
fn detects_claude_when_another_agent_already_tracked() {
let dir = tempfile::tempdir().unwrap();
install_claude_marker(dir.path());
let current = vec!["copilot".to_string()];
let additions = detect_missing_installed_agents(dir.path(), ¤t);
assert!(
additions.iter().any(|id| id == "claude"),
"claude must be detected even when copilot is already in the list, got {additions:?}"
);
}
#[test]
fn detects_claude_when_list_is_empty() {
let dir = tempfile::tempdir().unwrap();
install_claude_marker(dir.path());
let additions = detect_missing_installed_agents(dir.path(), &[]);
assert!(additions.iter().any(|id| id == "claude"));
}
#[test]
fn no_additions_when_claude_already_tracked() {
let dir = tempfile::tempdir().unwrap();
install_claude_marker(dir.path());
let current = vec!["claude".to_string()];
let additions = detect_missing_installed_agents(dir.path(), ¤t);
assert!(
!additions.contains(&"claude".to_string()),
"claude is already tracked; must not be re-added, got {additions:?}"
);
}
#[test]
fn empty_home_yields_no_additions() {
let dir = tempfile::tempdir().unwrap();
let additions = detect_missing_installed_agents(dir.path(), &[]);
assert!(
additions.is_empty(),
"no agent files in home → no additions, got {additions:?}"
);
}
}