mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
62a4ab2ad3
Use /K instead of /C flag to prevent cmd window from closing immediately after batch file execution. Also remove the buggy errorlevel check that was checking del command's return value instead of claude's. Fixes #726
760 lines
24 KiB
Rust
760 lines
24 KiB
Rust
#![allow(non_snake_case)]
|
|
|
|
use crate::app_config::AppType;
|
|
use crate::init_status::{InitErrorPayload, SkillsMigrationPayload};
|
|
use crate::services::ProviderService;
|
|
use once_cell::sync::Lazy;
|
|
use regex::Regex;
|
|
use std::path::Path;
|
|
use std::str::FromStr;
|
|
use tauri::AppHandle;
|
|
use tauri::State;
|
|
use tauri_plugin_opener::OpenerExt;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
|
|
/// 打开外部链接
|
|
#[tauri::command]
|
|
pub async fn open_external(app: AppHandle, url: String) -> Result<bool, String> {
|
|
let url = if url.starts_with("http://") || url.starts_with("https://") {
|
|
url
|
|
} else {
|
|
format!("https://{url}")
|
|
};
|
|
|
|
app.opener()
|
|
.open_url(&url, None::<String>)
|
|
.map_err(|e| format!("打开链接失败: {e}"))?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// 检查更新
|
|
#[tauri::command]
|
|
pub async fn check_for_updates(handle: AppHandle) -> Result<bool, String> {
|
|
handle
|
|
.opener()
|
|
.open_url(
|
|
"https://github.com/farion1231/cc-switch/releases/latest",
|
|
None::<String>,
|
|
)
|
|
.map_err(|e| format!("打开更新页面失败: {e}"))?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// 判断是否为便携版(绿色版)运行
|
|
#[tauri::command]
|
|
pub async fn is_portable_mode() -> Result<bool, String> {
|
|
let exe_path = std::env::current_exe().map_err(|e| format!("获取可执行路径失败: {e}"))?;
|
|
if let Some(dir) = exe_path.parent() {
|
|
Ok(dir.join("portable.ini").is_file())
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
/// 获取应用启动阶段的初始化错误(若有)。
|
|
/// 用于前端在早期主动拉取,避免事件订阅竞态导致的提示缺失。
|
|
#[tauri::command]
|
|
pub async fn get_init_error() -> Result<Option<InitErrorPayload>, String> {
|
|
Ok(crate::init_status::get_init_error())
|
|
}
|
|
|
|
/// 获取 JSON→SQLite 迁移结果(若有)。
|
|
/// 只返回一次 true,之后返回 false,用于前端显示一次性 Toast 通知。
|
|
#[tauri::command]
|
|
pub async fn get_migration_result() -> Result<bool, String> {
|
|
Ok(crate::init_status::take_migration_success())
|
|
}
|
|
|
|
/// 获取 Skills 自动导入(SSOT)迁移结果(若有)。
|
|
/// 只返回一次 Some({count}),之后返回 None,用于前端显示一次性 Toast 通知。
|
|
#[tauri::command]
|
|
pub async fn get_skills_migration_result() -> Result<Option<SkillsMigrationPayload>, String> {
|
|
Ok(crate::init_status::take_skills_migration_result())
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
pub struct ToolVersion {
|
|
name: String,
|
|
version: Option<String>,
|
|
latest_version: Option<String>, // 新增字段:最新版本
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
|
let tools = vec!["claude", "codex", "gemini"];
|
|
let mut results = Vec::new();
|
|
|
|
// 使用全局 HTTP 客户端(已包含代理配置)
|
|
let client = crate::proxy::http_client::get();
|
|
|
|
for tool in tools {
|
|
// 1. 获取本地版本 - 先尝试直接执行,失败则扫描常见路径
|
|
let (local_version, local_error) = if let Some(distro) = wsl_distro_for_tool(tool) {
|
|
try_get_version_wsl(tool, &distro)
|
|
} else {
|
|
// 先尝试直接执行
|
|
let direct_result = try_get_version(tool);
|
|
|
|
if direct_result.0.is_some() {
|
|
direct_result
|
|
} else {
|
|
// 扫描常见的 npm 全局安装路径
|
|
scan_cli_version(tool)
|
|
}
|
|
};
|
|
|
|
// 2. 获取远程最新版本
|
|
let latest_version = match tool {
|
|
"claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await,
|
|
"codex" => fetch_npm_latest_version(&client, "@openai/codex").await,
|
|
"gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await,
|
|
_ => None,
|
|
};
|
|
|
|
results.push(ToolVersion {
|
|
name: tool.to_string(),
|
|
version: local_version,
|
|
latest_version,
|
|
error: local_error,
|
|
});
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
/// Helper function to fetch latest version from npm registry
|
|
async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Option<String> {
|
|
let url = format!("https://registry.npmjs.org/{package}");
|
|
match client.get(&url).send().await {
|
|
Ok(resp) => {
|
|
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
|
json.get("dist-tags")
|
|
.and_then(|tags| tags.get("latest"))
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
|
|
/// 预编译的版本号正则表达式
|
|
static VERSION_RE: Lazy<Regex> =
|
|
Lazy::new(|| Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").expect("Invalid version regex"));
|
|
|
|
/// 从版本输出中提取纯版本号
|
|
fn extract_version(raw: &str) -> String {
|
|
VERSION_RE
|
|
.find(raw)
|
|
.map(|m| m.as_str().to_string())
|
|
.unwrap_or_else(|| raw.to_string())
|
|
}
|
|
|
|
/// 尝试直接执行命令获取版本
|
|
fn try_get_version(tool: &str) -> (Option<String>, Option<String>) {
|
|
use std::process::Command;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let output = {
|
|
Command::new("cmd")
|
|
.args(["/C", &format!("{tool} --version")])
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
.output()
|
|
};
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let output = {
|
|
Command::new("sh")
|
|
.arg("-c")
|
|
.arg(format!("{tool} --version"))
|
|
.output()
|
|
};
|
|
|
|
match output {
|
|
Ok(out) => {
|
|
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
if out.status.success() {
|
|
let raw = if stdout.is_empty() { &stderr } else { &stdout };
|
|
if raw.is_empty() {
|
|
(None, Some("not installed or not executable".to_string()))
|
|
} else {
|
|
(Some(extract_version(raw)), None)
|
|
}
|
|
} else {
|
|
let err = if stderr.is_empty() { stdout } else { stderr };
|
|
(
|
|
None,
|
|
Some(if err.is_empty() {
|
|
"not installed or not executable".to_string()
|
|
} else {
|
|
err
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
Err(e) => (None, Some(e.to_string())),
|
|
}
|
|
}
|
|
|
|
/// 校验 WSL 发行版名称是否合法
|
|
/// WSL 发行版名称只允许字母、数字、连字符和下划线
|
|
#[cfg(target_os = "windows")]
|
|
fn is_valid_wsl_distro_name(name: &str) -> bool {
|
|
!name.is_empty()
|
|
&& name.len() <= 64
|
|
&& name
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn try_get_version_wsl(tool: &str, distro: &str) -> (Option<String>, Option<String>) {
|
|
use std::process::Command;
|
|
|
|
// 防御性断言:tool 只能是预定义的值
|
|
debug_assert!(
|
|
["claude", "codex", "gemini"].contains(&tool),
|
|
"unexpected tool name: {tool}"
|
|
);
|
|
|
|
// 校验 distro 名称,防止命令注入
|
|
if !is_valid_wsl_distro_name(distro) {
|
|
return (None, Some(format!("[WSL:{distro}] invalid distro name")));
|
|
}
|
|
|
|
let output = Command::new("wsl.exe")
|
|
.args([
|
|
"-d",
|
|
distro,
|
|
"--",
|
|
"sh",
|
|
"-lc",
|
|
&format!("{tool} --version"),
|
|
])
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
.output();
|
|
|
|
match output {
|
|
Ok(out) => {
|
|
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
if out.status.success() {
|
|
let raw = if stdout.is_empty() { &stderr } else { &stdout };
|
|
if raw.is_empty() {
|
|
(
|
|
None,
|
|
Some(format!("[WSL:{distro}] not installed or not executable")),
|
|
)
|
|
} else {
|
|
(Some(extract_version(raw)), None)
|
|
}
|
|
} else {
|
|
let err = if stderr.is_empty() { stdout } else { stderr };
|
|
(
|
|
None,
|
|
Some(format!(
|
|
"[WSL:{distro}] {}",
|
|
if err.is_empty() {
|
|
"not installed or not executable".to_string()
|
|
} else {
|
|
err
|
|
}
|
|
)),
|
|
)
|
|
}
|
|
}
|
|
Err(e) => (None, Some(format!("[WSL:{distro}] exec failed: {e}"))),
|
|
}
|
|
}
|
|
|
|
/// 非 Windows 平台的 WSL 版本检测存根
|
|
/// 注意:此函数实际上不会被调用,因为 `wsl_distro_from_path` 在非 Windows 平台总是返回 None。
|
|
/// 保留此函数是为了保持 API 一致性,防止未来重构时遗漏。
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn try_get_version_wsl(_tool: &str, _distro: &str) -> (Option<String>, Option<String>) {
|
|
(
|
|
None,
|
|
Some("WSL check not supported on this platform".to_string()),
|
|
)
|
|
}
|
|
|
|
/// 扫描常见路径查找 CLI
|
|
fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
|
use std::process::Command;
|
|
|
|
let home = dirs::home_dir().unwrap_or_default();
|
|
|
|
// 常见的 npm 全局安装路径
|
|
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
|
home.join(".npm-global/bin"),
|
|
home.join(".local/bin"),
|
|
home.join("n/bin"), // n version manager
|
|
];
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin"));
|
|
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
|
search_paths.push(std::path::PathBuf::from("/usr/bin"));
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Some(appdata) = dirs::data_dir() {
|
|
search_paths.push(appdata.join("npm"));
|
|
}
|
|
search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs"));
|
|
}
|
|
|
|
// 添加 fnm 路径支持
|
|
let fnm_base = home.join(".local/state/fnm_multishells");
|
|
if fnm_base.exists() {
|
|
if let Ok(entries) = std::fs::read_dir(&fnm_base) {
|
|
for entry in entries.flatten() {
|
|
let bin_path = entry.path().join("bin");
|
|
if bin_path.exists() {
|
|
search_paths.push(bin_path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 扫描 nvm 目录下的所有 node 版本
|
|
let nvm_base = home.join(".nvm/versions/node");
|
|
if nvm_base.exists() {
|
|
if let Ok(entries) = std::fs::read_dir(&nvm_base) {
|
|
for entry in entries.flatten() {
|
|
let bin_path = entry.path().join("bin");
|
|
if bin_path.exists() {
|
|
search_paths.push(bin_path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 在每个路径中查找工具
|
|
for path in &search_paths {
|
|
let tool_path = if cfg!(target_os = "windows") {
|
|
path.join(format!("{tool}.cmd"))
|
|
} else {
|
|
path.join(tool)
|
|
};
|
|
|
|
if tool_path.exists() {
|
|
// 构建 PATH 环境变量,确保 node 可被找到
|
|
let current_path = std::env::var("PATH").unwrap_or_default();
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let new_path = format!("{};{}", path.display(), current_path);
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let new_path = format!("{}:{}", path.display(), current_path);
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let output = {
|
|
// 使用 cmd /C 包装执行,确保子进程也在隐藏的控制台中运行
|
|
Command::new("cmd")
|
|
.args(["/C", &format!("\"{}\" --version", tool_path.display())])
|
|
.env("PATH", &new_path)
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
.output()
|
|
};
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let output = {
|
|
Command::new(&tool_path)
|
|
.arg("--version")
|
|
.env("PATH", &new_path)
|
|
.output()
|
|
};
|
|
|
|
if let Ok(out) = output {
|
|
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
if out.status.success() {
|
|
let raw = if stdout.is_empty() { &stderr } else { &stdout };
|
|
if !raw.is_empty() {
|
|
return (Some(extract_version(raw)), None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(None, Some("not installed or not executable".to_string()))
|
|
}
|
|
|
|
fn wsl_distro_for_tool(tool: &str) -> Option<String> {
|
|
let override_dir = match tool {
|
|
"claude" => crate::settings::get_claude_override_dir(),
|
|
"codex" => crate::settings::get_codex_override_dir(),
|
|
"gemini" => crate::settings::get_gemini_override_dir(),
|
|
_ => None,
|
|
}?;
|
|
|
|
wsl_distro_from_path(&override_dir)
|
|
}
|
|
|
|
/// 从 UNC 路径中提取 WSL 发行版名称
|
|
/// 支持 `\\wsl$\Ubuntu\...` 和 `\\wsl.localhost\Ubuntu\...` 两种格式
|
|
#[cfg(target_os = "windows")]
|
|
fn wsl_distro_from_path(path: &Path) -> Option<String> {
|
|
use std::path::{Component, Prefix};
|
|
let Some(Component::Prefix(prefix)) = path.components().next() else {
|
|
return None;
|
|
};
|
|
match prefix.kind() {
|
|
Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => {
|
|
let server_name = server.to_string_lossy();
|
|
if server_name.eq_ignore_ascii_case("wsl$")
|
|
|| server_name.eq_ignore_ascii_case("wsl.localhost")
|
|
{
|
|
let distro = share.to_string_lossy().to_string();
|
|
if !distro.is_empty() {
|
|
return Some(distro);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// 非 Windows 平台不支持 WSL 路径解析
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn wsl_distro_from_path(_path: &Path) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
/// 打开指定提供商的终端
|
|
///
|
|
/// 根据提供商配置的环境变量启动一个带有该提供商特定设置的终端
|
|
/// 无需检查是否为当前激活的提供商,任何提供商都可以打开终端
|
|
#[allow(non_snake_case)]
|
|
#[tauri::command]
|
|
pub async fn open_provider_terminal(
|
|
state: State<'_, crate::store::AppState>,
|
|
app: String,
|
|
#[allow(non_snake_case)] providerId: String,
|
|
) -> Result<bool, String> {
|
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
|
|
|
// 获取提供商配置
|
|
let providers = ProviderService::list(state.inner(), app_type.clone())
|
|
.map_err(|e| format!("获取提供商列表失败: {e}"))?;
|
|
|
|
let provider = providers
|
|
.get(&providerId)
|
|
.ok_or_else(|| format!("提供商 {providerId} 不存在"))?;
|
|
|
|
// 从提供商配置中提取环境变量
|
|
let config = &provider.settings_config;
|
|
let env_vars = extract_env_vars_from_config(config, &app_type);
|
|
|
|
// 根据平台启动终端,传入提供商ID用于生成唯一的配置文件名
|
|
launch_terminal_with_env(env_vars, &providerId).map_err(|e| format!("启动终端失败: {e}"))?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// 从提供商配置中提取环境变量
|
|
fn extract_env_vars_from_config(
|
|
config: &serde_json::Value,
|
|
app_type: &AppType,
|
|
) -> Vec<(String, String)> {
|
|
let mut env_vars = Vec::new();
|
|
|
|
let Some(obj) = config.as_object() else {
|
|
return env_vars;
|
|
};
|
|
|
|
// 处理 env 字段(Claude/Gemini 通用)
|
|
if let Some(env) = obj.get("env").and_then(|v| v.as_object()) {
|
|
for (key, value) in env {
|
|
if let Some(str_val) = value.as_str() {
|
|
env_vars.push((key.clone(), str_val.to_string()));
|
|
}
|
|
}
|
|
|
|
// 处理 base_url: 根据应用类型添加对应的环境变量
|
|
let base_url_key = match app_type {
|
|
AppType::Claude => Some("ANTHROPIC_BASE_URL"),
|
|
AppType::Gemini => Some("GOOGLE_GEMINI_BASE_URL"),
|
|
_ => None,
|
|
};
|
|
|
|
if let Some(key) = base_url_key {
|
|
if let Some(url_str) = env.get(key).and_then(|v| v.as_str()) {
|
|
env_vars.push((key.to_string(), url_str.to_string()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Codex 使用 auth 字段转换为 OPENAI_API_KEY
|
|
if *app_type == AppType::Codex {
|
|
if let Some(auth) = obj.get("auth").and_then(|v| v.as_str()) {
|
|
env_vars.push(("OPENAI_API_KEY".to_string(), auth.to_string()));
|
|
}
|
|
}
|
|
|
|
// Gemini 使用 api_key 字段转换为 GEMINI_API_KEY
|
|
if *app_type == AppType::Gemini {
|
|
if let Some(api_key) = obj.get("api_key").and_then(|v| v.as_str()) {
|
|
env_vars.push(("GEMINI_API_KEY".to_string(), api_key.to_string()));
|
|
}
|
|
}
|
|
|
|
env_vars
|
|
}
|
|
|
|
/// 创建临时配置文件并启动 claude 终端
|
|
/// 使用 --settings 参数传入提供商特定的 API 配置
|
|
fn launch_terminal_with_env(
|
|
env_vars: Vec<(String, String)>,
|
|
provider_id: &str,
|
|
) -> Result<(), String> {
|
|
let temp_dir = std::env::temp_dir();
|
|
let config_file = temp_dir.join(format!(
|
|
"claude_{}_{}.json",
|
|
provider_id,
|
|
std::process::id()
|
|
));
|
|
|
|
// 创建并写入配置文件
|
|
write_claude_config(&config_file, &env_vars)?;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
launch_macos_terminal(&config_file)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
launch_linux_terminal(&config_file)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
launch_windows_terminal(&temp_dir, &config_file)?;
|
|
return Ok(());
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
|
Err("不支持的操作系统".to_string())
|
|
}
|
|
|
|
/// 写入 claude 配置文件
|
|
fn write_claude_config(
|
|
config_file: &std::path::Path,
|
|
env_vars: &[(String, String)],
|
|
) -> Result<(), String> {
|
|
let mut config_obj = serde_json::Map::new();
|
|
let mut env_obj = serde_json::Map::new();
|
|
|
|
for (key, value) in env_vars {
|
|
env_obj.insert(key.clone(), serde_json::Value::String(value.clone()));
|
|
}
|
|
|
|
config_obj.insert("env".to_string(), serde_json::Value::Object(env_obj));
|
|
|
|
let config_json =
|
|
serde_json::to_string_pretty(&config_obj).map_err(|e| format!("序列化配置失败: {e}"))?;
|
|
|
|
std::fs::write(config_file, config_json).map_err(|e| format!("写入配置文件失败: {e}"))
|
|
}
|
|
|
|
/// macOS: 使用 Terminal.app 启动
|
|
#[cfg(target_os = "macos")]
|
|
fn launch_macos_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::process::Command;
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
|
|
|
let config_path = config_file.to_string_lossy();
|
|
|
|
// Write the shell script to a temp file (no escaping needed!)
|
|
let script_content = format!(
|
|
r#"#!/bin/bash
|
|
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
|
echo "Using provider-specific claude config:"
|
|
echo "{config_path}"
|
|
claude --settings "{config_path}"
|
|
exec bash --norc --noprofile
|
|
"#,
|
|
config_path = config_path,
|
|
script_file = script_file.display()
|
|
);
|
|
|
|
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
|
|
|
// Make script executable
|
|
std::fs::set_permissions(&script_file, std::fs::Permissions::from_mode(0o755))
|
|
.map_err(|e| format!("设置脚本权限失败: {e}"))?;
|
|
|
|
// Simple AppleScript - just execute the script file
|
|
let applescript = format!(
|
|
r#"tell application "Terminal"
|
|
activate
|
|
do script "bash '{}'"
|
|
end tell"#,
|
|
script_file.display()
|
|
);
|
|
|
|
let output = Command::new("osascript")
|
|
.arg("-e")
|
|
.arg(&applescript)
|
|
.output()
|
|
.map_err(|e| format!("执行 osascript 失败: {e}"))?;
|
|
|
|
if !output.status.success() {
|
|
// Clean up on failure
|
|
let _ = std::fs::remove_file(&script_file);
|
|
let _ = std::fs::remove_file(config_file);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!(
|
|
"AppleScript 执行失败 (exit code: {:?}): {}",
|
|
output.status.code(),
|
|
stderr
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Linux: 尝试使用常见终端启动
|
|
#[cfg(target_os = "linux")]
|
|
fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::process::Command;
|
|
|
|
let terminals = [
|
|
("gnome-terminal", vec!["--"]),
|
|
("konsole", vec!["-e"]),
|
|
("xfce4-terminal", vec!["-e"]),
|
|
("mate-terminal", vec!["--"]),
|
|
("lxterminal", vec!["-e"]),
|
|
("alacritty", vec!["-e"]),
|
|
("kitty", vec!["-e"]),
|
|
];
|
|
|
|
// Create temp script file (same approach as macOS)
|
|
let temp_dir = std::env::temp_dir();
|
|
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
|
let config_path = config_file.to_string_lossy();
|
|
|
|
let script_content = format!(
|
|
r#"#!/bin/bash
|
|
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
|
echo "Using provider-specific claude config:"
|
|
echo "{config_path}"
|
|
claude --settings "{config_path}"
|
|
exec bash --norc --noprofile
|
|
"#,
|
|
config_path = config_path,
|
|
script_file = script_file.display()
|
|
);
|
|
|
|
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
|
|
|
std::fs::set_permissions(&script_file, std::fs::Permissions::from_mode(0o755))
|
|
.map_err(|e| format!("设置脚本权限失败: {e}"))?;
|
|
|
|
let mut last_error = String::from("未找到可用的终端");
|
|
|
|
for (terminal, args) in terminals {
|
|
// Check if terminal exists
|
|
if std::path::Path::new(&format!("/usr/bin/{}", terminal)).exists()
|
|
|| std::path::Path::new(&format!("/bin/{}", terminal)).exists()
|
|
{
|
|
let result = Command::new(terminal)
|
|
.args(&args)
|
|
.arg("bash")
|
|
.arg(script_file.to_string_lossy().as_ref())
|
|
.output();
|
|
|
|
match result {
|
|
Ok(output) if output.status.success() => return Ok(()),
|
|
Ok(output) => {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
last_error = format!("启动 {} 失败: {}", terminal, stderr);
|
|
}
|
|
Err(e) => {
|
|
last_error = format!("执行 {} 失败: {}", terminal, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up on failure
|
|
let _ = std::fs::remove_file(&script_file);
|
|
let _ = std::fs::remove_file(config_file);
|
|
Err(last_error)
|
|
}
|
|
|
|
/// Windows: 创建临时批处理文件启动
|
|
#[cfg(target_os = "windows")]
|
|
fn launch_windows_terminal(
|
|
temp_dir: &std::path::Path,
|
|
config_file: &std::path::Path,
|
|
) -> Result<(), String> {
|
|
use std::process::Command;
|
|
|
|
let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id()));
|
|
let config_path_for_batch = config_file.to_string_lossy().replace('&', "^&");
|
|
|
|
let content = format!(
|
|
"@echo off
|
|
echo Using provider-specific claude config:
|
|
echo {}
|
|
claude --settings \"{}\"
|
|
del \"{}\" >nul 2>&1
|
|
del \"%~f0\" >nul 2>&1
|
|
",
|
|
config_path_for_batch, config_path_for_batch, config_path_for_batch
|
|
);
|
|
|
|
std::fs::write(&bat_file, content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
|
|
|
|
// Use output() to capture errors from the start command
|
|
// Use /K instead of /C to keep the window open after execution
|
|
let output = Command::new("cmd")
|
|
.args(["/C", "start", "cmd", "/K", &bat_file.to_string_lossy()])
|
|
.creation_flags(CREATE_NO_WINDOW)
|
|
.output()
|
|
.map_err(|e| format!("执行 cmd 失败: {e}"))?;
|
|
|
|
if !output.status.success() {
|
|
// Clean up on failure
|
|
let _ = std::fs::remove_file(&bat_file);
|
|
let _ = std::fs::remove_file(config_file);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!(
|
|
"启动 Windows 终端失败 (exit code: {:?}): {}",
|
|
output.status.code(),
|
|
stderr
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|