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, 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 { 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, 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, 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, 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::::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)); } }