Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`(提示词)
Expand Down
160 changes: 160 additions & 0 deletions src-tauri/src/codex_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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<OsString>,
}

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() {
Expand Down
Loading