diff --git a/README.md b/README.md index 6866283f..f61fe3ad 100644 --- a/README.md +++ b/README.md @@ -444,6 +444,7 @@ When `CC_SWITCH_CONFIG_DIR` is set, CC-Switch uses that directory as its config **Live Configs:** - Claude: `~/.claude/settings.json` (provider/common config), `~/.claude.json` (MCP), `~/.claude/CLAUDE.md` (prompts) - Codex: `~/.codex/auth.json` (auth state), `~/.codex/config.toml` (provider/common config + MCP), `~/.codex/AGENTS.md` (prompts) + - Codex config directory uses CC-Switch's manual override first. If no override is configured, CC-Switch follows Codex's `$CODEX_HOME` when it points to an existing directory, otherwise it uses `$HOME/.codex`. - Gemini: `~/.gemini/.env` (provider env), `~/.gemini/settings.json` (settings + MCP), `~/.gemini/GEMINI.md` (prompts) - OpenCode: `~/.config/opencode/opencode.json` (providers + MCP + runtime config), `~/.config/opencode/AGENTS.md` (prompts) - OpenClaw: `~/.openclaw/openclaw.json` (providers + env/tools/agents defaults), `~/.openclaw/AGENTS.md` (prompts) diff --git a/README_ZH.md b/README_ZH.md index 5b2a4476..8ff95069 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -445,6 +445,7 @@ cc-switch update --version vX.Y.Z # 更新到指定版本 **实时配置:** - Claude: `~/.claude/settings.json`(供应商 / 通用配置), `~/.claude.json`(MCP), `~/.claude/CLAUDE.md`(提示词) - Codex: `~/.codex/auth.json`(认证状态), `~/.codex/config.toml`(供应商 / 通用配置 + MCP), `~/.codex/AGENTS.md`(提示词) + - Codex 配置目录优先使用 CC-Switch 的手动覆盖设置;未配置覆盖时,如果 `$CODEX_HOME` 指向已存在的目录则跟随 Codex 使用它,否则使用 `$HOME/.codex`。 - Gemini: `~/.gemini/.env`(供应商环境变量), `~/.gemini/settings.json`(设置 + MCP), `~/.gemini/GEMINI.md`(提示词) - OpenCode: `~/.config/opencode/opencode.json`(供应商 + MCP + 运行时配置), `~/.config/opencode/AGENTS.md`(提示词) - OpenClaw: `~/.openclaw/openclaw.json`(供应商 + Env/Tools/Agents Defaults), `~/.openclaw/AGENTS.md`(提示词) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 42f821b5..ae6431ae 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -30,6 +30,13 @@ pub fn get_codex_config_dir() -> PathBuf { return custom; } + if let Some(dir) = std::env::var_os("CODEX_HOME") { + let dir = PathBuf::from(dir); + if !dir.as_os_str().is_empty() && !dir.to_string_lossy().trim().is_empty() && dir.is_dir() { + return dir; + } + } + home_dir().expect("无法获取用户主目录").join(".codex") } @@ -499,6 +506,159 @@ pub fn clean_codex_provider_key(raw: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{lock_test_home_and_settings, set_test_home_override}; + use std::env; + use std::ffi::OsString; + + struct CodexHomeEnvGuard { + original: Option, + } + + impl CodexHomeEnvGuard { + fn new(value: Option<&str>) -> Self { + let original = env::var_os("CODEX_HOME"); + match value { + Some(value) => unsafe { env::set_var("CODEX_HOME", value) }, + None => unsafe { env::remove_var("CODEX_HOME") }, + } + Self { original } + } + } + + impl Drop for CodexHomeEnvGuard { + fn drop(&mut self) { + match self.original.as_ref() { + Some(value) => unsafe { env::set_var("CODEX_HOME", value) }, + None => unsafe { env::remove_var("CODEX_HOME") }, + } + } + } + + struct SettingsGuard { + original: crate::settings::AppSettings, + } + + impl SettingsGuard { + fn with_codex_config_dir(dir: Option<&str>) -> Self { + let original = crate::settings::get_settings(); + let mut settings = original.clone(); + settings.codex_config_dir = dir.map(str::to_string); + crate::settings::update_settings(settings).unwrap(); + Self { original } + } + } + + impl Drop for SettingsGuard { + fn drop(&mut self) { + let _ = crate::settings::update_settings(self.original.clone()); + } + } + + #[test] + fn get_codex_config_dir_respects_codex_home_env_var_when_directory_exists() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-home-env-home"))); + let _settings = SettingsGuard::with_codex_config_dir(None); + let codex_home = + std::env::temp_dir().join(format!("cc-switch-codex-home-env-{}", std::process::id())); + fs::create_dir_all(&codex_home).unwrap(); + let _env = CodexHomeEnvGuard::new(codex_home.to_str()); + + assert_eq!(get_codex_config_dir(), codex_home); + + set_test_home_override(None); + } + + #[test] + fn get_codex_config_dir_falls_back_to_home_dot_codex_when_codex_home_unset() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-default-home"))); + let _settings = SettingsGuard::with_codex_config_dir(None); + let _env = CodexHomeEnvGuard::new(None); + + assert_eq!( + get_codex_config_dir(), + PathBuf::from("/tmp/codex-default-home").join(".codex") + ); + + set_test_home_override(None); + } + + #[test] + fn get_codex_config_dir_blank_codex_home_uses_settings_override() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-blank-env-home"))); + let _settings = SettingsGuard::with_codex_config_dir(Some("/tmp/codex-settings-dir")); + let _env = CodexHomeEnvGuard::new(Some(" ")); + + assert_eq!( + get_codex_config_dir(), + PathBuf::from("/tmp/codex-settings-dir") + ); + + set_test_home_override(None); + } + + #[test] + fn get_codex_config_dir_nonexistent_codex_home_uses_settings_override() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-nonexistent-env-home"))); + let _settings = SettingsGuard::with_codex_config_dir(Some("/tmp/codex-settings-dir")); + let missing = std::env::temp_dir().join(format!( + "cc-switch-codex-missing-env-{}", + std::process::id() + )); + let _env = CodexHomeEnvGuard::new(missing.to_str()); + + assert_eq!( + get_codex_config_dir(), + PathBuf::from("/tmp/codex-settings-dir") + ); + + set_test_home_override(None); + } + + #[test] + fn get_codex_config_dir_file_codex_home_falls_back_to_home_dot_codex() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-file-env-home"))); + let _settings = SettingsGuard::with_codex_config_dir(None); + let codex_home_file = std::env::temp_dir().join(format!( + "cc-switch-codex-home-env-file-{}", + std::process::id() + )); + fs::write(&codex_home_file, "not a directory").unwrap(); + let _env = CodexHomeEnvGuard::new(codex_home_file.to_str()); + + assert_eq!( + get_codex_config_dir(), + PathBuf::from("/tmp/codex-file-env-home").join(".codex") + ); + + let _ = fs::remove_file(codex_home_file); + set_test_home_override(None); + } + + #[test] + fn get_codex_config_dir_settings_override_takes_precedence_over_codex_home() { + let _guard = lock_test_home_and_settings(); + set_test_home_override(Some(Path::new("/tmp/codex-precedence-home"))); + let _settings = SettingsGuard::with_codex_config_dir(Some("/tmp/codex-settings-dir")); + let codex_home = std::env::temp_dir().join(format!( + "cc-switch-codex-precedence-env-{}", + std::process::id() + )); + fs::create_dir_all(&codex_home).unwrap(); + let _env = CodexHomeEnvGuard::new(codex_home.to_str()); + + assert_eq!( + get_codex_config_dir(), + PathBuf::from("/tmp/codex-settings-dir") + ); + + let _ = fs::remove_dir_all(codex_home); + set_test_home_override(None); + } #[test] fn normalize_live_config_preserves_current_custom_model_provider_id() {