mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 03:32:25 +08:00
c49cf96a16
* fix(grokbuild): complete proxy integration * fix(deeplink): preview GrokBuild configs safely * test(app): stabilize provider integration suite * fix(grokbuild): address review feedback * fix(grokbuild): resolve remaining review findings * fix(grokbuild): use native sessions and harden previews
236 lines
8.2 KiB
Rust
236 lines
8.2 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
#[cfg(not(target_os = "windows"))]
|
|
use std::fs;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EnvConflict {
|
|
pub var_name: String,
|
|
pub var_value: String,
|
|
pub source_type: String, // "system" | "file"
|
|
pub source_path: String, // Registry path or file path
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::enums::*;
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::RegKey;
|
|
|
|
/// Check environment variables for conflicts
|
|
pub fn check_env_conflicts(app: &str) -> Result<Vec<EnvConflict>, String> {
|
|
let keywords = get_keywords_for_app(app);
|
|
let mut conflicts = Vec::new();
|
|
|
|
// Check system environment variables
|
|
conflicts.extend(check_system_env(&keywords)?);
|
|
|
|
// Check shell configuration files (Unix only)
|
|
#[cfg(not(target_os = "windows"))]
|
|
conflicts.extend(check_shell_configs(&keywords)?);
|
|
|
|
Ok(conflicts)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum EnvKeyword {
|
|
Exact(&'static str),
|
|
Prefix(&'static str),
|
|
}
|
|
|
|
/// Get relevant keywords for each app
|
|
fn get_keywords_for_app(app: &str) -> Vec<EnvKeyword> {
|
|
match app.to_lowercase().as_str() {
|
|
"claude" => vec![EnvKeyword::Prefix("ANTHROPIC")],
|
|
"codex" => vec![EnvKeyword::Prefix("OPENAI")],
|
|
"gemini" => vec![
|
|
EnvKeyword::Prefix("GEMINI"),
|
|
EnvKeyword::Prefix("GOOGLE_GEMINI"),
|
|
],
|
|
"grokbuild" | "grok" => vec![
|
|
EnvKeyword::Exact("XAI_API_KEY"),
|
|
EnvKeyword::Exact("GROK_DEFAULT_MODEL"),
|
|
],
|
|
_ => vec![],
|
|
}
|
|
}
|
|
|
|
fn matches_env_keyword(name: &str, keywords: &[EnvKeyword]) -> bool {
|
|
let upper_name = name.to_uppercase();
|
|
keywords.iter().any(|keyword| match keyword {
|
|
EnvKeyword::Exact(name) => upper_name == *name,
|
|
EnvKeyword::Prefix(prefix) => upper_name.starts_with(prefix),
|
|
})
|
|
}
|
|
|
|
/// Check system environment variables (Windows Registry or Unix env)
|
|
#[cfg(target_os = "windows")]
|
|
fn check_system_env(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
|
let mut conflicts = Vec::new();
|
|
|
|
// Check HKEY_CURRENT_USER\Environment
|
|
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
|
|
for (name, value) in hkcu.enum_values().filter_map(Result::ok) {
|
|
if matches_env_keyword(&name, keywords) {
|
|
conflicts.push(EnvConflict {
|
|
var_name: name.clone(),
|
|
var_value: value.to_string(),
|
|
source_type: "system".to_string(),
|
|
source_path: "HKEY_CURRENT_USER\\Environment".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
|
|
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
|
.open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
|
|
{
|
|
for (name, value) in hklm.enum_values().filter_map(Result::ok) {
|
|
if matches_env_keyword(&name, keywords) {
|
|
conflicts.push(EnvConflict {
|
|
var_name: name.clone(),
|
|
var_value: value.to_string(),
|
|
source_type: "system".to_string(),
|
|
source_path: "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(conflicts)
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn check_system_env(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
|
let mut conflicts = Vec::new();
|
|
|
|
// Check current process environment
|
|
for (key, value) in std::env::vars() {
|
|
if matches_env_keyword(&key, keywords) {
|
|
conflicts.push(EnvConflict {
|
|
var_name: key,
|
|
var_value: value,
|
|
source_type: "system".to_string(),
|
|
source_path: "Process Environment".to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(conflicts)
|
|
}
|
|
|
|
/// Check shell configuration files for environment variable exports (Unix only)
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn check_shell_configs(keywords: &[EnvKeyword]) -> Result<Vec<EnvConflict>, String> {
|
|
let mut conflicts = Vec::new();
|
|
|
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
let config_files = vec![
|
|
format!("{}/.bashrc", home),
|
|
format!("{}/.bash_profile", home),
|
|
format!("{}/.zshrc", home),
|
|
format!("{}/.zprofile", home),
|
|
format!("{}/.profile", home),
|
|
"/etc/profile".to_string(),
|
|
"/etc/bashrc".to_string(),
|
|
];
|
|
|
|
for file_path in config_files {
|
|
if let Ok(content) = fs::read_to_string(&file_path) {
|
|
// Parse lines for export statements
|
|
for (line_num, line) in content.lines().enumerate() {
|
|
let trimmed = line.trim();
|
|
|
|
// Match patterns like: export VAR=value or VAR=value
|
|
if trimmed.starts_with("export ")
|
|
|| (!trimmed.starts_with('#') && trimmed.contains('='))
|
|
{
|
|
let export_line = trimmed.strip_prefix("export ").unwrap_or(trimmed);
|
|
|
|
if let Some(eq_pos) = export_line.find('=') {
|
|
let var_name = export_line[..eq_pos].trim();
|
|
let var_value = export_line[eq_pos + 1..].trim();
|
|
|
|
// Check if variable name contains any keyword
|
|
if matches_env_keyword(var_name, keywords) {
|
|
conflicts.push(EnvConflict {
|
|
var_name: var_name.to_string(),
|
|
var_value: var_value
|
|
.trim_matches('"')
|
|
.trim_matches('\'')
|
|
.to_string(),
|
|
source_type: "file".to_string(),
|
|
source_path: format!("{}:{}", file_path, line_num + 1),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(conflicts)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_get_keywords() {
|
|
assert_eq!(
|
|
get_keywords_for_app("claude"),
|
|
vec![EnvKeyword::Prefix("ANTHROPIC")]
|
|
);
|
|
assert_eq!(
|
|
get_keywords_for_app("codex"),
|
|
vec![EnvKeyword::Prefix("OPENAI")]
|
|
);
|
|
assert_eq!(
|
|
get_keywords_for_app("gemini"),
|
|
vec![
|
|
EnvKeyword::Prefix("GEMINI"),
|
|
EnvKeyword::Prefix("GOOGLE_GEMINI")
|
|
]
|
|
);
|
|
assert_eq!(
|
|
get_keywords_for_app("grokbuild"),
|
|
vec![
|
|
EnvKeyword::Exact("XAI_API_KEY"),
|
|
EnvKeyword::Exact("GROK_DEFAULT_MODEL")
|
|
]
|
|
);
|
|
assert_eq!(
|
|
get_keywords_for_app("grok"),
|
|
get_keywords_for_app("grokbuild")
|
|
);
|
|
assert_eq!(get_keywords_for_app("unknown"), Vec::<EnvKeyword>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn grok_keywords_only_match_credentials() {
|
|
let keywords = get_keywords_for_app("grokbuild");
|
|
|
|
assert!(matches_env_keyword("XAI_API_KEY", &keywords));
|
|
assert!(matches_env_keyword("xai_api_key", &keywords));
|
|
assert!(matches_env_keyword("GROK_DEFAULT_MODEL", &keywords));
|
|
assert!(matches_env_keyword("grok_default_model", &keywords));
|
|
assert!(!matches_env_keyword("MY_XAI_API_KEY", &keywords));
|
|
assert!(!matches_env_keyword("XAI_API_KEY_BACKUP", &keywords));
|
|
assert!(!matches_env_keyword("MY_GROK_DEFAULT_MODEL", &keywords));
|
|
assert!(!matches_env_keyword("GROK_DEFAULT_MODEL_BACKUP", &keywords));
|
|
assert!(!matches_env_keyword("GROK_BIN_DIR", &keywords));
|
|
assert!(!matches_env_keyword("GROK_HOME", &keywords));
|
|
}
|
|
|
|
#[test]
|
|
fn broad_app_keywords_match_only_at_the_start() {
|
|
let keywords = get_keywords_for_app("claude");
|
|
|
|
assert!(matches_env_keyword("ANTHROPIC_API_KEY", &keywords));
|
|
assert!(matches_env_keyword("anthropic_base_url", &keywords));
|
|
assert!(!matches_env_keyword("MY_ANTHROPIC_API_KEY", &keywords));
|
|
assert!(!matches_env_keyword("NOT_ANTHROPIC", &keywords));
|
|
}
|
|
}
|