diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1950939e65..09c5786a9c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2997,6 +2997,7 @@ dependencies = [ name = "codex-install-context" version = "0.0.0" dependencies = [ + "codex-utils-absolute-path", "codex-utils-home-dir", "pretty_assertions", "tempfile", diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index aec57c1e82..3f8c3ff094 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -39,7 +39,9 @@ use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::find_codex_home; use codex_features::FEATURES; +use codex_install_context::CodexPackageLayout; use codex_install_context::InstallContext; +use codex_install_context::InstallMethod; use codex_install_context::StandalonePlatform; use codex_login::AuthDotJson; use codex_login::AuthManager; @@ -812,7 +814,10 @@ fn installation_check(show_details: bool) -> DoctorCheck { fn doctor_install_context(current_exe: Option<&Path>) -> InstallContext { if inherited_managed_env_for_cargo_binary(current_exe) { - InstallContext::Other + InstallContext { + method: InstallMethod::Other, + package_layout: None, + } } else { InstallContext::current().clone() } @@ -843,8 +848,8 @@ fn inherited_managed_env_for_cargo_binary(current_exe: Option<&Path>) -> bool { } fn describe_install_context(context: &InstallContext) -> String { - match context { - InstallContext::Standalone { + match &context.method { + InstallMethod::Standalone { release_dir, resources_dir, platform, @@ -853,22 +858,63 @@ fn describe_install_context(context: &InstallContext) -> String { StandalonePlatform::Unix => "unix", StandalonePlatform::Windows => "windows", }; - let resources = resources_dir - .as_ref() - .map(|path| path.display().to_string()) - .unwrap_or_else(|| "none".to_string()); + match &context.package_layout { + Some(package_layout) => { + let resources = display_optional_path(package_layout.resources_dir.as_deref()); + let path = display_optional_path(package_layout.path_dir.as_deref()); + format!( + "standalone ({platform}, package {}, bin {}, resources {resources}, path {path})", + package_layout.package_dir.display(), + package_layout.bin_dir.display() + ) + } + None => { + let resources = display_optional_path(resources_dir.as_deref()); + format!( + "standalone ({platform}, release {}, resources {resources})", + release_dir.display() + ) + } + } + } + InstallMethod::Npm => { + describe_method_with_package_layout("npm", context.package_layout.as_ref()) + } + InstallMethod::Bun => { + describe_method_with_package_layout("bun", context.package_layout.as_ref()) + } + InstallMethod::Brew => { + describe_method_with_package_layout("brew", context.package_layout.as_ref()) + } + InstallMethod::Other => { + describe_method_with_package_layout("other", context.package_layout.as_ref()) + } + } +} + +fn describe_method_with_package_layout( + method: &str, + package_layout: Option<&CodexPackageLayout>, +) -> String { + match package_layout { + Some(package_layout) => { + let resources = display_optional_path(package_layout.resources_dir.as_deref()); + let path = display_optional_path(package_layout.path_dir.as_deref()); format!( - "standalone ({platform}, release {}, resources {resources})", - release_dir.display() + "{method} (package {}, bin {}, resources {resources}, path {path})", + package_layout.package_dir.display(), + package_layout.bin_dir.display() ) } - InstallContext::Npm => "npm".to_string(), - InstallContext::Bun => "bun".to_string(), - InstallContext::Brew => "brew".to_string(), - InstallContext::Other => "other".to_string(), + None => method.to_string(), } } +fn display_optional_path(path: Option<&Path>) -> String { + path.map(|path| path.display().to_string()) + .unwrap_or_else(|| "none".to_string()) +} + #[derive(Debug, PartialEq, Eq)] enum NpmRootCheck { Match { @@ -2791,13 +2837,14 @@ fn path_readiness(details: &mut Vec, label: &str, path: &Path) { } fn standalone_release_cache_details(details: &mut Vec) { - let InstallContext::Standalone { release_dir, .. } = InstallContext::current() else { + let context = InstallContext::current(); + let InstallMethod::Standalone { release_dir, .. } = &context.method else { return; }; let Some(releases_dir) = release_dir.parent() else { return; }; - let Ok(entries) = std::fs::read_dir(releases_dir) else { + let Ok(entries) = std::fs::read_dir(&releases_dir) else { return; }; let release_count = entries.filter_map(Result::ok).count(); diff --git a/codex-rs/cli/src/doctor/runtime.rs b/codex-rs/cli/src/doctor/runtime.rs index 96e806762c..14afa70e4c 100644 --- a/codex-rs/cli/src/doctor/runtime.rs +++ b/codex-rs/cli/src/doctor/runtime.rs @@ -3,12 +3,13 @@ //! Runtime diagnostics answer provenance questions that are hard to infer from //! user reports: which binary is running, which install channel it resembles, //! which platform it targets, and whether the search command comes from bundled -//! standalone resources or from PATH. +//! package files or from PATH. use std::env; use std::process::Command; use codex_install_context::InstallContext; +use codex_install_context::InstallMethod; use super::CheckStatus; use super::DoctorCheck; @@ -49,9 +50,10 @@ pub(super) fn runtime_check() -> DoctorCheck { /// Verifies that the search command selected by the install context is usable. /// -/// Standalone installs should point at a bundled ripgrep binary, while local or -/// package-managed installs usually resolve rg from PATH. A warning here means -/// features that depend on file search may degrade even when the CLI launches. +/// Package-layout installs should point at a bundled ripgrep binary, while local +/// installs without that layout usually resolve rg from PATH. A warning here +/// means features that depend on file search may degrade even when the CLI +/// launches. pub(super) fn search_check() -> DoctorCheck { let current_exe = env::current_exe().ok(); let install_context = doctor_install_context(current_exe.as_deref()); @@ -109,28 +111,40 @@ pub(super) fn search_check() -> DoctorCheck { }; let mut check = DoctorCheck::new("runtime.search", "search", status, summary).details(details); if status != CheckStatus::Ok { - check = check.remediation("Install ripgrep or repair the bundled standalone resources."); + check = check.remediation("Install ripgrep or repair the bundled Codex package."); } check } fn install_method_name(context: &InstallContext) -> &'static str { - match context { - InstallContext::Standalone { .. } => "standalone", - InstallContext::Npm => "npm", - InstallContext::Bun => "bun", - InstallContext::Brew => "brew", - InstallContext::Other => "local build", + match &context.method { + InstallMethod::Standalone { .. } => "standalone", + InstallMethod::Npm => "npm", + InstallMethod::Bun => "bun", + InstallMethod::Brew => "brew", + InstallMethod::Other => "local build", } } fn search_provider(context: &InstallContext) -> &'static str { - match context { - InstallContext::Standalone { + let rg_command = context.rg_command(); + let from_package_layout = context + .package_layout + .as_ref() + .and_then(|package_layout| package_layout.path_dir.as_ref()) + .is_some_and(|path_dir| rg_command.starts_with(path_dir)); + let from_legacy_standalone = matches!( + &context.method, + InstallMethod::Standalone { resources_dir: Some(resources_dir), .. - } if context.rg_command().starts_with(resources_dir) => "bundled", - _ => "system", + } if rg_command.starts_with(resources_dir) + ); + + if from_package_layout || from_legacy_standalone { + "bundled" + } else { + "system" } } diff --git a/codex-rs/cli/src/doctor/updates.rs b/codex-rs/cli/src/doctor/updates.rs index 3d728fc07b..246eac2b39 100644 --- a/codex-rs/cli/src/doctor/updates.rs +++ b/codex-rs/cli/src/doctor/updates.rs @@ -10,6 +10,7 @@ use std::path::Path; use codex_core::config::Config; use codex_install_context::InstallContext; +use codex_install_context::InstallMethod; use serde::Deserialize; use super::CheckStatus; @@ -129,22 +130,22 @@ fn push_cached_version_details(details: &mut Vec, version_file: &Path) { } fn update_action_label(context: &InstallContext) -> &'static str { - match context { - InstallContext::Npm => "npm install -g @openai/codex", - InstallContext::Bun => "bun install -g @openai/codex", - InstallContext::Brew => "brew upgrade --cask codex", - InstallContext::Standalone { .. } => "standalone installer", - InstallContext::Other => "manual or unknown", + match &context.method { + InstallMethod::Npm => "npm install -g @openai/codex", + InstallMethod::Bun => "bun install -g @openai/codex", + InstallMethod::Brew => "brew upgrade --cask codex", + InstallMethod::Standalone { .. } => "standalone installer", + InstallMethod::Other => "manual or unknown", } } fn fetch_latest_version(context: &InstallContext) -> Result { - match context { - InstallContext::Brew => fetch_homebrew_cask_version(), - InstallContext::Npm - | InstallContext::Bun - | InstallContext::Standalone { .. } - | InstallContext::Other => fetch_latest_github_release_version(), + match &context.method { + InstallMethod::Brew => fetch_homebrew_cask_version(), + InstallMethod::Npm + | InstallMethod::Bun + | InstallMethod::Standalone { .. } + | InstallMethod::Other => fetch_latest_github_release_version(), } } @@ -216,11 +217,17 @@ mod tests { #[test] fn update_action_labels_install_contexts() { assert_eq!( - update_action_label(&InstallContext::Npm), + update_action_label(&InstallContext { + method: InstallMethod::Npm, + package_layout: None, + }), "npm install -g @openai/codex" ); assert_eq!( - update_action_label(&InstallContext::Other), + update_action_label(&InstallContext { + method: InstallMethod::Other, + package_layout: None, + }), "manual or unknown" ); } diff --git a/codex-rs/install-context/Cargo.toml b/codex-rs/install-context/Cargo.toml index 52938a0812..5882a19763 100644 --- a/codex-rs/install-context/Cargo.toml +++ b/codex-rs/install-context/Cargo.toml @@ -13,6 +13,7 @@ doctest = false workspace = true [dependencies] +codex-utils-absolute-path = { workspace = true } codex-utils-home-dir = { workspace = true } [dev-dependencies] diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index 980fc3f546..d1f8d3839b 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -1,7 +1,13 @@ +use std::ffi::OsStr; use std::path::Path; use std::path::PathBuf; use std::sync::OnceLock; +use codex_utils_absolute_path::AbsolutePathBuf; + +const BIN_DIRNAME: &str = "bin"; +const PACKAGE_METADATA_FILENAME: &str = "codex-package.json"; +const PATH_DIRNAME: &str = "codex-path"; const RELEASES_DIRNAME: &str = "releases"; const RESOURCES_DIRNAME: &str = "codex-resources"; const STANDALONE_PACKAGES_DIRNAME: &str = "standalone"; @@ -14,14 +20,34 @@ pub enum StandalonePlatform { } #[derive(Clone, Debug, Eq, PartialEq)] -pub enum InstallContext { +pub struct CodexPackageLayout { + /// The package root that contains the metadata file and layout directories. + pub package_dir: AbsolutePathBuf, + /// Directory containing the Codex entrypoint executable. + pub bin_dir: AbsolutePathBuf, + /// Directory containing managed helper binaries and data files, when present. + pub resources_dir: Option, + /// Folder that should be prepended to the PATH, when present. + pub path_dir: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InstallContext { + pub method: InstallMethod, + pub package_layout: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum InstallMethod { Standalone { - /// The managed standalone release directory, for example + /// The managed standalone release directory. Legacy installs use paths + /// such as /// `~/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl`. - release_dir: PathBuf, - /// The bundled resource directory that sits next to the executable when - /// this install ships managed dependencies. - resources_dir: Option, + /// Package-layout installs use the package root that contains `bin/`, + /// `codex-resources/`, and `codex-path/`. + release_dir: AbsolutePathBuf, + /// The bundled resource directory for managed dependencies. + resources_dir: Option, /// The platform of the standalone release, either `Unix` or `Windows`. platform: StandalonePlatform, }, @@ -62,28 +88,21 @@ impl InstallContext { managed_by_bun: bool, codex_home: Option<&Path>, ) -> Self { - if managed_by_npm { - return Self::Npm; - } + let package_layout = current_exe.and_then(CodexPackageLayout::from_exe); + let method = if managed_by_npm { + InstallMethod::Npm + } else if managed_by_bun { + InstallMethod::Bun + } else if let Some(exe_path) = current_exe { + install_method_from_exe(exe_path, codex_home, package_layout.as_ref(), is_macos) + } else { + InstallMethod::Other + }; - if managed_by_bun { - return Self::Bun; + Self { + method, + package_layout, } - - if let Some(exe_path) = current_exe - && let Some(standalone_context) = standalone_install_context(exe_path, codex_home) - { - return standalone_context; - } - - if is_macos - && let Some(exe_path) = current_exe - && (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local")) - { - return Self::Brew; - } - - Self::Other } pub fn current() -> &'static Self { @@ -101,53 +120,102 @@ impl InstallContext { } pub fn rg_command(&self) -> PathBuf { - match self { - Self::Standalone { - resources_dir: Some(resources_dir), - .. - } => { - let bundled_rg = resources_dir.join(default_rg_command()); - if bundled_rg.exists() { - bundled_rg - } else { - default_rg_command() - } + if let Some(package_layout) = &self.package_layout + && let Some(path_dir) = &package_layout.path_dir + { + let bundled_rg = path_dir.join(default_rg_command()); + if bundled_rg.exists() { + return bundled_rg.into_path_buf(); } - Self::Standalone { - resources_dir: None, - .. - } - | Self::Npm - | Self::Bun - | Self::Brew - | Self::Other => default_rg_command(), } + + if let InstallMethod::Standalone { + resources_dir: Some(resources_dir), + .. + } = &self.method + { + let bundled_rg = resources_dir.join(default_rg_command()); + if bundled_rg.exists() { + return bundled_rg.into_path_buf(); + } + } + + default_rg_command() } } -fn standalone_install_context( +impl CodexPackageLayout { + fn from_exe(exe_path: &Path) -> Option { + let canonical_exe = canonical_absolute_path(exe_path)?; + let bin_dir = canonical_exe.parent()?; + if bin_dir.file_name() != Some(OsStr::new(BIN_DIRNAME)) { + return None; + } + + let package_dir = bin_dir.parent()?; + if !package_dir.join(PACKAGE_METADATA_FILENAME).is_file() { + return None; + } + + Some(Self { + resources_dir: existing_dir(package_dir.join(RESOURCES_DIRNAME)), + path_dir: existing_dir(package_dir.join(PATH_DIRNAME)), + package_dir, + bin_dir, + }) + } +} + +fn install_method_from_exe( exe_path: &Path, codex_home: Option<&Path>, -) -> Option { - let canonical_exe = std::fs::canonicalize(exe_path).ok()?; - let canonical_codex_home = std::fs::canonicalize(codex_home?).ok()?; - let release_dir = canonical_exe.parent()?.to_path_buf(); + package_layout: Option<&CodexPackageLayout>, + is_macos: bool, +) -> InstallMethod { + if let Some(standalone_method) = standalone_install_method(exe_path, codex_home, package_layout) + { + return standalone_method; + } + + if is_macos && (exe_path.starts_with("/opt/homebrew") || exe_path.starts_with("/usr/local")) { + InstallMethod::Brew + } else { + InstallMethod::Other + } +} + +fn standalone_install_method( + exe_path: &Path, + codex_home: Option<&Path>, + package_layout: Option<&CodexPackageLayout>, +) -> Option { + let canonical_codex_home = canonical_absolute_path(codex_home?)?; + let release_dir = if let Some(package_layout) = package_layout { + package_layout.package_dir.clone() + } else { + canonical_absolute_path(exe_path)?.parent()? + }; let releases_root = canonical_codex_home .join("packages") .join(STANDALONE_PACKAGES_DIRNAME) .join(RELEASES_DIRNAME); - if !release_dir.starts_with(releases_root) { + if !release_dir.starts_with(releases_root.as_path()) { return None; } let resources_dir = release_dir.join(RESOURCES_DIRNAME); - Some(InstallContext::Standalone { + Some(InstallMethod::Standalone { release_dir, resources_dir: resources_dir.is_dir().then_some(resources_dir), platform: standalone_platform(), }) } +fn canonical_absolute_path(path: &Path) -> Option { + let canonical_path = std::fs::canonicalize(path).ok()?; + AbsolutePathBuf::from_absolute_path(canonical_path).ok() +} + fn standalone_platform() -> StandalonePlatform { if cfg!(windows) { StandalonePlatform::Windows @@ -156,6 +224,10 @@ fn standalone_platform() -> StandalonePlatform { } } +fn existing_dir(path: AbsolutePathBuf) -> Option { + path.is_dir().then_some(path) +} + fn default_rg_command() -> PathBuf { if cfg!(windows) { PathBuf::from("rg.exe") @@ -181,8 +253,10 @@ mod tests { let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); fs::write(&exe_path, "")?; fs::write(resources_dir.join(default_rg_command()), "")?; - let canonical_release_dir = release_dir.canonicalize()?; - let canonical_resources_dir = resources_dir.canonicalize()?; + let canonical_release_dir = + AbsolutePathBuf::from_absolute_path(release_dir.canonicalize()?)?; + let canonical_resources_dir = + AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?; let context = InstallContext::from_exe_with_codex_home( /*is_macos*/ false, @@ -193,10 +267,13 @@ mod tests { ); assert_eq!( context, - InstallContext::Standalone { - release_dir: canonical_release_dir, - resources_dir: Some(canonical_resources_dir), - platform: standalone_platform(), + InstallContext { + method: InstallMethod::Standalone { + release_dir: canonical_release_dir, + resources_dir: Some(canonical_resources_dir), + platform: standalone_platform(), + }, + package_layout: None, } ); Ok(()) @@ -223,6 +300,161 @@ mod tests { Ok(()) } + #[test] + fn detects_package_layout_independently_from_install_method() -> std::io::Result<()> { + let package_dir = tempfile::tempdir()?; + let bin_dir = package_dir.path().join(BIN_DIRNAME); + let resources_dir = package_dir.path().join(RESOURCES_DIRNAME); + let path_dir = package_dir.path().join(PATH_DIRNAME); + fs::create_dir_all(&bin_dir)?; + fs::create_dir_all(&resources_dir)?; + fs::create_dir_all(&path_dir)?; + fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?; + let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); + fs::write(&exe_path, "")?; + fs::write(path_dir.join(default_rg_command()), "")?; + let canonical_package_dir = + AbsolutePathBuf::from_absolute_path(package_dir.path().canonicalize()?)?; + let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?; + let canonical_resources_dir = + AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?; + let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?; + let package_layout = CodexPackageLayout { + package_dir: canonical_package_dir, + bin_dir: canonical_bin_dir, + resources_dir: Some(canonical_resources_dir), + path_dir: Some(canonical_path_dir.clone()), + }; + + let context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*managed_by_npm*/ false, + /*managed_by_bun*/ false, + /*codex_home*/ None, + ); + assert_eq!( + context, + InstallContext { + method: InstallMethod::Other, + package_layout: Some(package_layout), + } + ); + assert_eq!( + context.rg_command(), + canonical_path_dir + .join(default_rg_command()) + .into_path_buf() + ); + Ok(()) + } + + #[test] + fn standalone_package_layout_keeps_standalone_install_method() -> std::io::Result<()> { + let codex_home = tempfile::tempdir()?; + let package_dir = codex_home + .path() + .join("packages/standalone/releases/1.2.3-x86_64-unknown-linux-musl"); + let bin_dir = package_dir.join(BIN_DIRNAME); + let resources_dir = package_dir.join(RESOURCES_DIRNAME); + let path_dir = package_dir.join(PATH_DIRNAME); + fs::create_dir_all(&bin_dir)?; + fs::create_dir_all(&resources_dir)?; + fs::create_dir_all(&path_dir)?; + fs::write(package_dir.join(PACKAGE_METADATA_FILENAME), "{}")?; + let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); + fs::write(&exe_path, "")?; + fs::write(path_dir.join(default_rg_command()), "")?; + let canonical_package_dir = + AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?; + let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?; + let canonical_resources_dir = + AbsolutePathBuf::from_absolute_path(resources_dir.canonicalize()?)?; + let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?; + + let context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*managed_by_npm*/ false, + /*managed_by_bun*/ false, + /*codex_home*/ Some(codex_home.path()), + ); + assert_eq!( + context, + InstallContext { + method: InstallMethod::Standalone { + release_dir: canonical_package_dir.clone(), + resources_dir: Some(canonical_resources_dir.clone()), + platform: standalone_platform(), + }, + package_layout: Some(CodexPackageLayout { + package_dir: canonical_package_dir, + bin_dir: canonical_bin_dir, + resources_dir: Some(canonical_resources_dir), + path_dir: Some(canonical_path_dir.clone()), + }), + } + ); + assert_eq!( + context.rg_command(), + canonical_path_dir + .join(default_rg_command()) + .into_path_buf() + ); + Ok(()) + } + + #[test] + fn npm_managed_package_keeps_package_layout() -> std::io::Result<()> { + let package_dir = tempfile::tempdir()?; + let bin_dir = package_dir.path().join(BIN_DIRNAME); + let path_dir = package_dir.path().join(PATH_DIRNAME); + fs::create_dir_all(&bin_dir)?; + fs::create_dir_all(&path_dir)?; + fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?; + let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); + fs::write(&exe_path, "")?; + fs::write(path_dir.join(default_rg_command()), "")?; + let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?; + + let context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*managed_by_npm*/ true, + /*managed_by_bun*/ false, + /*codex_home*/ None, + ); + assert_eq!(context.method, InstallMethod::Npm); + assert!(context.package_layout.is_some()); + assert_eq!( + context.rg_command(), + canonical_path_dir + .join(default_rg_command()) + .into_path_buf() + ); + Ok(()) + } + + #[test] + fn standalone_package_rg_falls_back_when_codex_path_is_missing() -> std::io::Result<()> { + let package_dir = tempfile::tempdir()?; + let bin_dir = package_dir.path().join(BIN_DIRNAME); + fs::create_dir_all(&bin_dir)?; + fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?; + let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" }); + fs::write(&exe_path, "")?; + + let context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(&exe_path), + /*managed_by_npm*/ false, + /*managed_by_bun*/ false, + /*codex_home*/ None, + ); + assert_eq!(context.rg_command(), default_rg_command()); + Ok(()) + } + #[test] fn npm_and_bun_take_precedence() { let npm_context = InstallContext::from_exe_with_codex_home( @@ -232,7 +464,13 @@ mod tests { /*managed_by_bun*/ false, /*codex_home*/ None, ); - assert_eq!(npm_context, InstallContext::Npm); + assert_eq!( + npm_context, + InstallContext { + method: InstallMethod::Npm, + package_layout: None, + } + ); let bun_context = InstallContext::from_exe_with_codex_home( /*is_macos*/ false, @@ -241,7 +479,13 @@ mod tests { /*managed_by_bun*/ true, /*codex_home*/ None, ); - assert_eq!(bun_context, InstallContext::Bun); + assert_eq!( + bun_context, + InstallContext { + method: InstallMethod::Bun, + package_layout: None, + } + ); } #[test] @@ -253,6 +497,12 @@ mod tests { /*managed_by_bun*/ false, /*codex_home*/ None, ); - assert_eq!(context, InstallContext::Brew); + assert_eq!( + context, + InstallContext { + method: InstallMethod::Brew, + package_layout: None, + } + ); } } diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index aca065440c..cb4aa662ca 100644 --- a/codex-rs/tui/src/update_action.rs +++ b/codex-rs/tui/src/update_action.rs @@ -1,6 +1,8 @@ #[cfg(any(not(debug_assertions), test))] use codex_install_context::InstallContext; #[cfg(any(not(debug_assertions), test))] +use codex_install_context::InstallMethod; +#[cfg(any(not(debug_assertions), test))] use codex_install_context::StandalonePlatform; /// Update action the CLI should perform after the TUI exits. @@ -21,15 +23,15 @@ pub enum UpdateAction { impl UpdateAction { #[cfg(any(not(debug_assertions), test))] pub(crate) fn from_install_context(context: &InstallContext) -> Option { - match context { - InstallContext::Npm => Some(UpdateAction::NpmGlobalLatest), - InstallContext::Bun => Some(UpdateAction::BunGlobalLatest), - InstallContext::Brew => Some(UpdateAction::BrewUpgrade), - InstallContext::Standalone { platform, .. } => Some(match platform { + match &context.method { + InstallMethod::Npm => Some(UpdateAction::NpmGlobalLatest), + InstallMethod::Bun => Some(UpdateAction::BunGlobalLatest), + InstallMethod::Brew => Some(UpdateAction::BrewUpgrade), + InstallMethod::Standalone { platform, .. } => Some(match platform { StandalonePlatform::Unix => UpdateAction::StandaloneUnix, StandalonePlatform::Windows => UpdateAction::StandaloneWindows, }), - InstallContext::Other => None, + InstallMethod::Other => None, } } @@ -66,42 +68,62 @@ pub fn get_update_action() -> Option { #[cfg(test)] mod tests { use super::*; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; - use std::path::PathBuf; #[test] fn maps_install_context_to_update_action() { - let native_release_dir = PathBuf::from("/tmp/native-release"); + let native_release_dir = + AbsolutePathBuf::from_absolute_path(std::env::temp_dir().join("native-release")) + .expect("temp dir path should be absolute"); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Other), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Other, + package_layout: None, + }), None ); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Npm), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Npm, + package_layout: None, + }), Some(UpdateAction::NpmGlobalLatest) ); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Bun), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Bun, + package_layout: None, + }), Some(UpdateAction::BunGlobalLatest) ); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Brew), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Brew, + package_layout: None, + }), Some(UpdateAction::BrewUpgrade) ); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Standalone { - platform: StandalonePlatform::Unix, - release_dir: native_release_dir.clone(), - resources_dir: Some(native_release_dir.join("codex-resources")), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Standalone { + platform: StandalonePlatform::Unix, + release_dir: native_release_dir.clone(), + resources_dir: Some(native_release_dir.join("codex-resources")), + }, + package_layout: None, }), Some(UpdateAction::StandaloneUnix) ); assert_eq!( - UpdateAction::from_install_context(&InstallContext::Standalone { - platform: StandalonePlatform::Windows, - release_dir: native_release_dir.clone(), - resources_dir: Some(native_release_dir.join("codex-resources")), + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::Standalone { + platform: StandalonePlatform::Windows, + release_dir: native_release_dir.clone(), + resources_dir: Some(native_release_dir.join("codex-resources")), + }, + package_layout: None, }), Some(UpdateAction::StandaloneWindows) );