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
25 changes: 23 additions & 2 deletions src-tauri/src/cli/codex_temp_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use serde_json::Value;
#[derive(Debug, Clone)]
pub(crate) struct PreparedCodexLaunch {
pub(crate) executable: PathBuf,
pub(crate) cc_switch_executable: PathBuf,
pub(crate) provider_id: String,
pub(crate) codex_home: PathBuf,
}

Expand All @@ -37,9 +39,18 @@ where
Resolve: FnOnce() -> Result<PathBuf, AppError>,
{
let executable = resolve_codex_binary()?;
let cc_switch_executable = std::env::current_exe().map_err(|err| {
AppError::localized(
"codex.temp_launch_current_exe_failed",
format!("解析当前 cc-switch 可执行文件路径失败: {err}"),
format!("Failed to resolve current cc-switch executable path: {err}"),
)
})?;
let codex_home = write_temp_codex_home(temp_dir, provider)?;
Ok(PreparedCodexLaunch {
executable,
cc_switch_executable,
provider_id: provider.id.clone(),
codex_home,
})
}
Expand Down Expand Up @@ -76,11 +87,13 @@ pub(crate) fn build_handoff_command(
) -> std::process::Command {
let mut command = std::process::Command::new("/bin/sh");
command.arg("-c").arg(
"codex_home=\"$1\"; codex_bin=\"$2\"; shift 2; exit_status=0; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; cleanup; exit \"$exit_status\"",
"codex_home=\"$1\"; codex_bin=\"$2\"; cc_switch_bin=\"$3\"; provider_id=\"$4\"; shift 4; exit_status=0; persist() { \"$cc_switch_bin\" internal capture-codex-temp \"$provider_id\" \"$codex_home\"; persist_status=$?; if [ \"$persist_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: 持久化供应商 $provider_id 的临时 Codex 登录状态失败(failed to persist temporary Codex login state)\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$persist_status; fi; fi; }; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; persist; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; persist; cleanup; exit \"$exit_status\"",
);
command.arg("cc-switch-codex-handoff");
command.arg(&prepared.codex_home);
command.arg(&prepared.executable);
command.arg(&prepared.cc_switch_executable);
command.arg(&prepared.provider_id);
command.args(native_args);
command
}
Expand Down Expand Up @@ -321,6 +334,8 @@ mod tests {
fn unix_handoff_command_exports_codex_home_and_cleans_up_temp_dir() {
let prepared = PreparedCodexLaunch {
executable: PathBuf::from("/usr/local/bin/codex"),
cc_switch_executable: PathBuf::from("/usr/local/bin/cc-switch"),
provider_id: "demo".to_string(),
codex_home: PathBuf::from("/tmp/cc-switch-codex-home"),
};
let native_args = vec![OsString::from("--model"), OsString::from("gpt-5.4")];
Expand All @@ -334,11 +349,13 @@ mod tests {
vec![
OsString::from("-c"),
OsString::from(
"codex_home=\"$1\"; codex_bin=\"$2\"; shift 2; exit_status=0; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; cleanup; exit \"$exit_status\""
"codex_home=\"$1\"; codex_bin=\"$2\"; cc_switch_bin=\"$3\"; provider_id=\"$4\"; shift 4; exit_status=0; persist() { \"$cc_switch_bin\" internal capture-codex-temp \"$provider_id\" \"$codex_home\"; persist_status=$?; if [ \"$persist_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: 持久化供应商 $provider_id 的临时 Codex 登录状态失败(failed to persist temporary Codex login state)\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$persist_status; fi; fi; }; cleanup() { rm -rf -- \"$codex_home\"; cleanup_status=$?; if [ \"$cleanup_status\" -ne 0 ]; then printf '%s\\n' \"cc-switch: failed to remove temporary Codex home: $codex_home\" >&2; if [ \"$exit_status\" -eq 0 ]; then exit_status=$cleanup_status; fi; fi; }; on_signal() { exit_status=\"$1\"; trap - INT TERM HUP; persist; cleanup; exit \"$exit_status\"; }; trap 'on_signal 130' INT; trap 'on_signal 143' TERM; trap 'on_signal 129' HUP; export CODEX_HOME=\"$codex_home\"; \"$codex_bin\" \"$@\"; exit_status=$?; persist; cleanup; exit \"$exit_status\""
),
OsString::from("cc-switch-codex-handoff"),
OsString::from("/tmp/cc-switch-codex-home"),
OsString::from("/usr/local/bin/codex"),
OsString::from("/usr/local/bin/cc-switch"),
OsString::from("demo"),
OsString::from("--model"),
OsString::from("gpt-5.4"),
]
Expand All @@ -361,6 +378,8 @@ mod tests {

let prepared = PreparedCodexLaunch {
executable,
cc_switch_executable: PathBuf::from("/bin/true"),
provider_id: "demo".to_string(),
codex_home: codex_home.clone(),
};
let mut command = build_handoff_command(&prepared, &[]);
Expand Down Expand Up @@ -394,6 +413,8 @@ mod tests {
let executable = write_test_executable(&temp_dir, "codex-stub.sh", "exit 0");
let prepared = PreparedCodexLaunch {
executable,
cc_switch_executable: PathBuf::from("/bin/true"),
provider_id: "demo".to_string(),
codex_home: PathBuf::from("."),
};

Expand Down
28 changes: 28 additions & 0 deletions src-tauri/src/cli/commands/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::path::PathBuf;

use clap::Subcommand;

use crate::error::AppError;
use crate::services::ProviderService;
use crate::store::AppState;

#[derive(Subcommand)]
pub enum InternalCommand {
/// Persist Codex files written during `cc-switch start codex`.
CaptureCodexTemp {
provider_id: String,
codex_home: PathBuf,
},
}

pub fn execute(cmd: InternalCommand) -> Result<(), AppError> {
match cmd {
InternalCommand::CaptureCodexTemp {
provider_id,
codex_home,
} => {
let state = AppState::try_new()?;
ProviderService::capture_codex_temp_launch_snapshot(&state, &provider_id, &codex_home)
}
}
}
1 change: 1 addition & 0 deletions src-tauri/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod config;
mod config_common;
pub mod config_webdav;
pub mod env;
pub mod internal;
pub mod mcp;
pub mod prompts;
pub mod provider;
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pub enum Commands {

/// Generate, install, inspect, or uninstall shell completions
Completions(commands::completions::CompletionsCommand),

#[command(name = "internal", hide = true, subcommand)]
Internal(commands::internal::InternalCommand),
}

/// Generate shell completions
Expand Down
24 changes: 21 additions & 3 deletions src-tauri/src/codex_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ pub fn delete_codex_provider_config(
pub fn write_codex_live_atomic(
auth: &Value,
config_text_opt: Option<&str>,
) -> Result<(), AppError> {
write_codex_live_atomic_optional_auth(Some(auth), config_text_opt)
}

pub fn write_codex_live_atomic_optional_auth(
auth: Option<&Value>,
config_text_opt: Option<&str>,
) -> Result<(), AppError> {
let auth_path = get_codex_auth_path();
let config_path = get_codex_config_path();
Expand Down Expand Up @@ -105,7 +112,11 @@ pub fn write_codex_live_atomic(
}

// 第一步:写 auth.json
write_json_file(&auth_path, auth)?;
if let Some(auth) = auth {
write_json_file(&auth_path, auth)?;
} else {
delete_file(&auth_path)?;
}

// 第二步:写 config.toml(失败则回滚 auth.json)
if let Err(e) = write_text_file(&config_path, &cfg_text) {
Expand Down Expand Up @@ -431,6 +442,13 @@ pub fn restore_codex_settings_config_model_provider_for_backfill(
pub fn write_codex_live_atomic_with_stable_provider(
auth: &Value,
config_text_opt: Option<&str>,
) -> Result<(), AppError> {
write_codex_live_atomic_optional_auth_with_stable_provider(Some(auth), config_text_opt)
}

pub fn write_codex_live_atomic_optional_auth_with_stable_provider(
auth: Option<&Value>,
config_text_opt: Option<&str>,
) -> Result<(), AppError> {
match config_text_opt {
Some(config_text) => {
Expand All @@ -442,9 +460,9 @@ pub fn write_codex_live_atomic_with_stable_provider(
.get("config")
.and_then(|value| value.as_str())
.unwrap_or(config_text);
write_codex_live_atomic(auth, Some(config_text))
write_codex_live_atomic_optional_auth(auth, Some(config_text))
}
None => write_codex_live_atomic(auth, None),
None => write_codex_live_atomic_optional_auth(auth, None),
}
}

Expand Down
31 changes: 30 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ fn run(cli: Cli) -> Result<(), AppError> {
Some(Commands::Env(cmd)) => cc_switch_lib::cli::commands::env::execute(cmd, cli.app),
Some(Commands::Update(cmd)) => cc_switch_lib::cli::commands::update::execute(cmd),
Some(Commands::Completions(cmd)) => cc_switch_lib::cli::commands::completions::execute(cmd),
Some(Commands::Internal(cmd)) => cc_switch_lib::cli::commands::internal::execute(cmd),
}
}

fn command_requires_startup_state(command: &Option<Commands>) -> bool {
match command {
Some(Commands::Completions(_)) | Some(Commands::Update(_)) => false,
Some(Commands::Completions(_))
| Some(Commands::Update(_))
| Some(Commands::Internal(_)) => false,
_ => true,
}
}
Expand Down Expand Up @@ -107,6 +110,13 @@ mod tests {
let completions_status = Cli::parse_from(["cc-switch", "completions", "status"]);
let completions_uninstall =
Cli::parse_from(["cc-switch", "completions", "uninstall", "--shell", "bash"]);
let internal_capture = Cli::parse_from([
"cc-switch",
"internal",
"capture-codex-temp",
"official",
"/tmp/codex-home",
]);
let provider = Cli::parse_from(["cc-switch", "provider", "list"]);

assert!(!command_requires_startup_state(&update.command));
Expand All @@ -120,6 +130,7 @@ mod tests {
assert!(!command_requires_startup_state(
&completions_uninstall.command
));
assert!(!command_requires_startup_state(&internal_capture.command));
assert!(command_requires_startup_state(&provider.command));
}

Expand All @@ -135,6 +146,24 @@ mod tests {
.expect("update should not touch startup state");
}

#[test]
#[serial]
fn internal_commands_bypass_future_schema_database_gate() {
let temp = tempfile::tempdir().expect("create temp dir");
seed_future_schema_database(temp.path());
let _guard = ConfigDirEnvGuard::set(temp.path());

let cli = Cli::parse_from([
"cc-switch",
"internal",
"capture-codex-temp",
"official",
"/tmp/codex-home",
]);
initialize_startup_state_if_needed(&cli.command)
.expect("internal commands should not touch startup state");
}

#[test]
#[serial]
fn provider_commands_still_fail_on_future_schema_database() {
Expand Down
84 changes: 82 additions & 2 deletions src-tauri/src/services/provider/codex.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,77 @@
use super::*;
use std::fs;
use std::path::Path;

impl ProviderService {
pub(crate) fn capture_codex_temp_launch_snapshot(
state: &AppState,
provider_id: &str,
codex_home: &Path,
) -> Result<(), AppError> {
let (provider, common_snippet) = {
let guard = state.config.read().map_err(AppError::from)?;
let provider = guard
.get_manager(&AppType::Codex)
.and_then(|manager| manager.providers.get(provider_id))
.cloned()
.ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {provider_id}"),
format!("Provider not found: {provider_id}"),
)
})?;
(provider, guard.common_config_snippets.codex.clone())
};

let config_path = codex_home.join("config.toml");
let cfg_text = if config_path.exists() {
fs::read_to_string(&config_path).map_err(|err| AppError::io(&config_path, err))?
} else {
provider
.settings_config
.get("config")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
};
crate::codex_config::validate_config_toml(&cfg_text)?;
let cfg_text_for_storage = Self::strip_codex_mcp_servers_from_snapshot_config(&cfg_text)?;

let auth_path = codex_home.join("auth.json");
let auth = if auth_path.exists() {
read_json_file::<Value>(&auth_path)?
} else {
Value::Object(serde_json::Map::new())
};

let mut raw_settings = serde_json::Map::new();
raw_settings.insert("auth".to_string(), auth);
raw_settings.insert("config".to_string(), Value::String(cfg_text_for_storage));

let mut settings_to_store = Self::normalize_settings_config_for_storage(
&AppType::Codex,
&provider,
Value::Object(raw_settings),
common_snippet.as_deref(),
)?;
Self::restore_codex_model_provider_for_storage_best_effort(
&provider,
&mut settings_to_store,
);

{
let mut guard = state.config.write().map_err(AppError::from)?;
if let Some(manager) = guard.get_manager_mut(&AppType::Codex) {
if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = settings_to_store;
}
}
}

state.save()
}

pub(super) fn extract_codex_common_config_from_config_toml(
config_toml: &str,
) -> Result<String, AppError> {
Expand Down Expand Up @@ -347,15 +418,24 @@ impl ProviderService {
let auth = settings
.get("auth")
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;

let cfg_text = settings
.get("config")
.and_then(Value::as_str)
.ok_or_else(|| {
AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string())
})?;

crate::codex_config::write_codex_live_atomic_with_stable_provider(auth, Some(cfg_text))?;
let auth_to_write = if Self::is_codex_official_provider(provider)
&& auth.as_object().is_some_and(|auth| auth.is_empty())
{
None
} else {
Some(auth)
};
crate::codex_config::write_codex_live_atomic_optional_auth_with_stable_provider(
auth_to_write,
Some(cfg_text),
)?;

Ok(())
}
Expand Down
Loading
Loading