From 20202b1579487c07d36caf0ed0a930288975d540 Mon Sep 17 00:00:00 2001 From: SquarePots <46488165+squarepots@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:02:55 +0800 Subject: [PATCH] Fix Windows Codex configuration preflight for v1.0.9 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/app_server.rs | 145 ++++++++++++++++++++++++++++-------- src-tauri/src/switching.rs | 6 +- src-tauri/src/types.rs | 2 + src-tauri/tauri.conf.json | 2 +- src/App.test.tsx | 4 +- src/App.tsx | 4 + src/i18n.ts | 8 +- src/types.ts | 2 + 11 files changed, 139 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index f313927..da0afd4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gswitch", "private": true, - "version": "1.0.8", + "version": "1.0.9", "type": "module", "packageManager": "pnpm@10.17.1", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cff8eee..98fb68d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1968,7 +1968,7 @@ dependencies = [ [[package]] name = "gswitch" -version = "1.0.8" +version = "1.0.9" dependencies = [ "aes-gcm", "atomic-write-file", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d32f22..da860f2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gswitch" -version = "1.0.8" +version = "1.0.9" description = "A focused local Codex account switcher" authors = ["Ginbing"] edition = "2021" diff --git a/src-tauri/src/app_server.rs b/src-tauri/src/app_server.rs index d74a05d..c75bb46 100644 --- a/src-tauri/src/app_server.rs +++ b/src-tauri/src/app_server.rs @@ -363,44 +363,89 @@ fn app_server_command(codex_home: &Path) -> Command { #[cfg(windows)] fn windows_app_server_command() -> Command { if let Some(path) = configured_codex_binary() { - if path - .extension() - .is_some_and(|extension| extension.eq_ignore_ascii_case("cmd")) - { - if let Some(npm_dir) = path.parent() { - let script = npm_dir - .join("node_modules") - .join("@openai") - .join("codex") - .join("bin") - .join("codex.js"); - if script.is_file() { - // Calling a batch shim through cmd makes its child process - // lifecycle ambiguous and failed with redirected stdin in - // the installed Codex 0.144.5 runtime. Invoke its actual - // Node entrypoint instead. - let bundled_node = npm_dir.join("node.exe"); - let mut command = if bundled_node.is_file() { - Command::new(bundled_node) - } else { - Command::new("node.exe") - }; - command.args([script, PathBuf::from("app-server")]); - return command; - } + return windows_app_server_command_for(&path); + } + + let mut command = Command::new("codex"); + command.arg("app-server"); + command +} + +#[cfg(windows)] +fn windows_app_server_command_for(path: &Path) -> Command { + if path + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("cmd")) + { + if let Some(native) = windows_npm_codex_executable(path) { + let mut command = Command::new(native); + command.arg("app-server"); + return command; + } + if let Some(npm_dir) = path.parent() { + let script = npm_dir + .join("node_modules") + .join("@openai") + .join("codex") + .join("bin") + .join("codex.js"); + if script.is_file() { + // The npm shim is a batch file and cannot reliably carry the + // App Server's redirected stdin. Use its Node entrypoint when + // the package's native executable is unavailable. + let bundled_node = npm_dir.join("node.exe"); + let mut command = if bundled_node.is_file() { + Command::new(bundled_node) + } else { + Command::new("node.exe") + }; + command.args([script, PathBuf::from("app-server")]); + return command; } } - - let mut command = Command::new(path); - command.arg("app-server"); - return command; } - let mut command = Command::new("codex"); + let mut command = Command::new(path); command.arg("app-server"); command } +#[cfg(windows)] +fn windows_npm_codex_executable(shim: &Path) -> Option { + if !shim + .file_name() + .is_some_and(|name| name.eq_ignore_ascii_case("codex.cmd")) + { + return None; + } + let npm_dir = shim.parent()?; + let (package, target) = match std::env::consts::ARCH { + "x86_64" => ("codex-win32-x64", "x86_64-pc-windows-msvc"), + "aarch64" => ("codex-win32-arm64", "aarch64-pc-windows-msvc"), + _ => return None, + }; + let package_root = npm_dir.join("node_modules").join("@openai").join("codex"); + // The official npm launcher resolves the platform package through Node's + // package lookup and falls back to its own vendor directory. Match those + // shipped locations so a desktop launch does not depend on node.exe in PATH. + [ + package_root + .join("node_modules") + .join("@openai") + .join(package) + .join("vendor"), + npm_dir + .join("node_modules") + .join("@openai") + .join(package) + .join("vendor"), + package_root.join("vendor"), + ] + .into_iter() + .map(|vendor| vendor.join(target).join("bin").join("codex.exe")) + .find(|executable| executable.is_file()) +} + fn configured_codex_binary() -> Option { let explicit = env::var_os("GSWITCH_CODEX_BIN") .map(PathBuf::from) @@ -646,6 +691,40 @@ pub fn account_metadata(result: &Value) -> Result { mod tests { use super::*; + #[cfg(windows)] + #[test] + fn npm_codex_uses_its_native_executable_without_node_in_path() { + use std::ffi::OsStr; + + let root = test_profile_root(); + let npm_dir = root.join("npm"); + let shim = npm_dir.join("codex.cmd"); + let (package, target) = match std::env::consts::ARCH { + "x86_64" => ("codex-win32-x64", "x86_64-pc-windows-msvc"), + "aarch64" => ("codex-win32-arm64", "aarch64-pc-windows-msvc"), + _ => return, + }; + let native = npm_dir + .join("node_modules") + .join("@openai") + .join("codex") + .join("node_modules") + .join("@openai") + .join(package) + .join("vendor") + .join(target) + .join("bin") + .join("codex.exe"); + fs::create_dir_all(native.parent().expect("native parent")).expect("package tree"); + fs::write(&shim, b"npm shim fixture").expect("shim"); + fs::write(&native, b"native binary fixture").expect("native executable"); + + let command = windows_app_server_command_for(&shim); + assert_eq!(command.get_program(), native.as_os_str()); + assert_eq!(command.get_args().next(), Some(OsStr::new("app-server"))); + fs::remove_dir_all(root).expect("remove fixture"); + } + #[cfg(not(windows))] #[test] fn derives_the_official_unix_installer_location() { @@ -765,6 +844,7 @@ mod tests { .join(format!("gswitch-app-server-test-{}", Uuid::new_v4())); { let profile = TempCodexHome::create(&root).expect("profile"); + let profile_path = profile.path.clone(); let mut server = AppServer::start(&profile.path).expect("start app server"); let config = server.config_read(1).expect("read config"); let expected_version = config @@ -792,6 +872,11 @@ mod tests { .and_then(Value::as_str), Some("file") ); + drop(server); + profile + .cleanup() + .expect("remove completed isolated profile"); + assert!(!profile_path.exists()); } let _ = fs::remove_dir_all(root); } diff --git a/src-tauri/src/switching.rs b/src-tauri/src/switching.rs index 042c7a2..6d6cd38 100644 --- a/src-tauri/src/switching.rs +++ b/src-tauri/src/switching.rs @@ -478,8 +478,8 @@ fn check_effective_file_store( .map_err(|_| SwitchFailureCode::CodexConfigUnavailable)?, ) .map_err(|_| SwitchFailureCode::CodexConfigUnavailable)?; - let mut server = - AppServer::start(&profile.path).map_err(|_| SwitchFailureCode::CodexConfigUnavailable)?; + let mut server = AppServer::start(&profile.path) + .map_err(|_| SwitchFailureCode::CodexAppServerUnavailable)?; runtime::ensure_no_external_codex(&[server.pid()]).map_err(|_| SwitchFailureCode::CodexOpen)?; let config = server .config_read(1) @@ -491,7 +491,7 @@ fn check_effective_file_store( drop(server); profile .cleanup() - .map_err(|_| SwitchFailureCode::CodexConfigUnavailable)?; + .map_err(|_| SwitchFailureCode::CodexConfigCleanupFailed)?; Ok(()) } diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index d7ce8fb..119bab2 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -235,7 +235,9 @@ pub enum SwitchFailureCode { CredentialsChanged, RecoveryRequired, LocalVerificationFailed, + CodexAppServerUnavailable, CodexConfigUnavailable, + CodexConfigCleanupFailed, CurrentCredentialUnreadable, CurrentAccountNotSaved, TargetCheckUnavailable, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cfa7965..1e42a93 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "GSwitch", - "version": "1.0.8", + "version": "1.0.9", "identifier": "com.ginbing.gswitch", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.test.tsx b/src/App.test.tsx index 0fe5a1b..ac69828 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -813,7 +813,9 @@ describe("GSwitch account workspace", () => { ["file_store_required", /Enable file-backed Codex credentials/], ["credentials_changed", /Codex credentials changed during the switch/], ["recovery_required", /Complete the protected switch recovery/], - ["codex_config_unavailable", /could not check Codex configuration/], + ["codex_app_server_unavailable", /could not start the Codex CLI service/], + ["codex_config_unavailable", /could not finish checking Codex settings/], + ["codex_config_cleanup_failed", /could not finish the local Codex check/], ["current_credential_unreadable", /could not read the current Codex sign-in/], ["current_account_not_saved", /Save the current Codex account/], ["local_verification_failed", /could not read its local account state/], diff --git a/src/App.tsx b/src/App.tsx index dc43676..d8a6adc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -136,7 +136,9 @@ const switchFailureCodes = new Set([ "credentials_changed", "recovery_required", "local_verification_failed", + "codex_app_server_unavailable", "codex_config_unavailable", + "codex_config_cleanup_failed", "current_credential_unreadable", "current_account_not_saved", "target_check_unavailable", @@ -165,7 +167,9 @@ function switchFailureMessage(t: Translator, error: unknown, account: AccountVie credentials_changed: "switch.error.credentialsChanged", recovery_required: "switch.error.recoveryRequired", local_verification_failed: "switch.error.localVerificationFailed", + codex_app_server_unavailable: "switch.error.codexAppServerUnavailable", codex_config_unavailable: "switch.error.codexConfigUnavailable", + codex_config_cleanup_failed: "switch.error.codexConfigCleanupFailed", current_credential_unreadable: "switch.error.currentCredentialUnreadable", current_account_not_saved: "switch.error.currentAccountNotSaved", target_check_unavailable: "switch.error.targetCheckUnavailable", diff --git a/src/i18n.ts b/src/i18n.ts index 589efd2..e1c4932 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -124,7 +124,9 @@ const en = { "switch.error.credentialsChanged": "GSwitch stopped because Codex credentials changed during the switch. Review the current account, then retry {name}.", "switch.error.recoveryRequired": "GSwitch did not switch {name}. Complete the protected switch recovery before trying again.", "switch.error.localVerificationFailed": "GSwitch could not read its local account state before switching {name}. Your Codex account was not changed.", - "switch.error.codexConfigUnavailable": "GSwitch could not check Codex configuration before switching {name}. Make sure Codex is installed and try again. Your current account was not changed.", + "switch.error.codexAppServerUnavailable": "GSwitch could not start the Codex CLI service before switching {name}. Check or update your Codex CLI installation, then try again. Your current account was not changed.", + "switch.error.codexConfigUnavailable": "GSwitch could not finish checking Codex settings before switching {name}. Restart GSwitch and try again. Your current account was not changed.", + "switch.error.codexConfigCleanupFailed": "GSwitch could not finish the local Codex check before switching {name}. Restart GSwitch and try again. Your current account was not changed.", "switch.error.currentCredentialUnreadable": "GSwitch could not read the current Codex sign-in. Check the current Codex account before switching to {name}; nothing was changed.", "switch.error.currentAccountNotSaved": "Save the current Codex account in GSwitch before switching to {name}. Nothing was changed.", "switch.error.targetCheckUnavailable": "GSwitch could not check {name} with ChatGPT. Your Codex account was not changed. Retry when the connection is available.", @@ -437,7 +439,9 @@ const zhCN: Record = { "switch.error.credentialsChanged": "切换期间 Codex 凭据发生变化,GSwitch 已停止。请检查当前账户后重试 {name}。", "switch.error.recoveryRequired": "GSwitch 未切换到 {name}。请先完成受保护的切换恢复步骤。", "switch.error.localVerificationFailed": "切换到 {name} 前,GSwitch 无法读取本地账户状态。当前 Codex 账户未改变。", - "switch.error.codexConfigUnavailable": "切换到 {name} 前,GSwitch 无法检查 Codex 配置。请确认 Codex 已安装后重试;当前账户未改变。", + "switch.error.codexAppServerUnavailable": "切换到 {name} 前,GSwitch 无法启动 Codex CLI 的本地服务。请检查或更新 Codex CLI 后重试;当前账户未改变。", + "switch.error.codexConfigUnavailable": "切换到 {name} 前,GSwitch 未能完成 Codex 设置检查。请重启 GSwitch 后重试;当前账户未改变。", + "switch.error.codexConfigCleanupFailed": "切换到 {name} 前,GSwitch 未能结束本地 Codex 检查。请重启 GSwitch 后重试;当前账户未改变。", "switch.error.currentCredentialUnreadable": "GSwitch 无法读取当前 Codex 登录状态。请检查当前账户后再切换到 {name};账户未改变。", "switch.error.currentAccountNotSaved": "请先将当前 Codex 账户保存到 GSwitch,再切换到 {name}。当前账户未改变。", "switch.error.targetCheckUnavailable": "GSwitch 暂时无法向 ChatGPT 验证 {name}。当前 Codex 账户未改变,请在连接恢复后重试。", diff --git a/src/types.ts b/src/types.ts index fd50c01..17c18cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,7 +64,9 @@ export type SwitchFailureCode = | "credentials_changed" | "recovery_required" | "local_verification_failed" + | "codex_app_server_unavailable" | "codex_config_unavailable" + | "codex_config_cleanup_failed" | "current_credential_unreadable" | "current_account_not_saved" | "target_check_unavailable"