diff --git a/codex-rs/app-server-daemon/src/managed_install.rs b/codex-rs/app-server-daemon/src/managed_install.rs index d5ad85b986..10f2c9c61c 100644 --- a/codex-rs/app-server-daemon/src/managed_install.rs +++ b/codex-rs/app-server-daemon/src/managed_install.rs @@ -1,3 +1,5 @@ +//! Resolves packaged and legacy standalone install layouts. + use std::path::Path; use std::path::PathBuf; @@ -16,12 +18,23 @@ use tokio::fs; #[cfg(unix)] use tokio::process::Command; +/// Returns the packaged executable when present, otherwise an existing legacy executable. +/// If neither exists, returns the expected packaged path on Windows and preserves the +/// historical legacy fallback on Unix. This is path selection, not existence validation: +/// launch operations reject a missing executable separately, while commands such as +/// stop can still run after the managed install has been removed. pub(crate) fn managed_codex_bin(codex_home: &Path) -> PathBuf { - codex_home + let current = codex_home .join("packages") .join("standalone") - .join("current") - .join(managed_codex_file_name()) + .join("current"); + let packaged = current.join("bin").join(managed_codex_file_name()); + let legacy = current.join(managed_codex_file_name()); + if packaged.is_file() || (cfg!(windows) && !legacy.is_file()) { + packaged + } else { + legacy + } } #[cfg(unix)] @@ -101,3 +114,7 @@ fn parse_codex_version(output: &str) -> Result { #[cfg(all(test, unix))] #[path = "managed_install_tests.rs"] mod tests; + +#[cfg(test)] +#[path = "managed_install_path_tests.rs"] +mod path_tests; diff --git a/codex-rs/app-server-daemon/src/managed_install_path_tests.rs b/codex-rs/app-server-daemon/src/managed_install_path_tests.rs new file mode 100644 index 0000000000..6c7ff5b3b0 --- /dev/null +++ b/codex-rs/app-server-daemon/src/managed_install_path_tests.rs @@ -0,0 +1,21 @@ +use pretty_assertions::assert_eq; + +#[test] +fn discovers_package_and_legacy_installs() { + let home = tempfile::TempDir::new().expect("home"); + let current = home.path().join("packages/standalone/current"); + let legacy = current.join(super::managed_codex_file_name()); + let expected = if cfg!(windows) { + current.join("bin").join("codex.exe") + } else { + legacy.clone() + }; + assert_eq!(super::managed_codex_bin(home.path()), expected); + std::fs::create_dir_all(¤t).expect("current directory"); + std::fs::write(&legacy, b"legacy").expect("legacy executable"); + assert_eq!(super::managed_codex_bin(home.path()), legacy); + let packaged = current.join("bin").join(super::managed_codex_file_name()); + std::fs::create_dir(current.join("bin")).expect("bin directory"); + std::fs::write(&packaged, b"packaged").expect("packaged executable"); + assert_eq!(super::managed_codex_bin(home.path()), packaged); +}