diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index 5cc519418300..7a0922ca8511 100755 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -4,6 +4,7 @@ import { spawn } from "node:child_process"; import { existsSync, realpathSync } from "fs"; import { createRequire } from "node:module"; +import os from "os"; import path from "path"; import { fileURLToPath } from "url"; @@ -11,6 +12,7 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const require = createRequire(import.meta.url); +const codexPackageRoot = realpathSync(path.join(__dirname, "..")); const PLATFORM_PACKAGE_BY_TARGET = { "x86_64-unknown-linux-musl": "@openai/codex-linux-x64", @@ -98,7 +100,9 @@ function findCodexExecutable() { const updateCommand = packageManager === "bun" ? "bun install -g @openai/codex@latest" - : "npm install -g @openai/codex@latest"; + : packageManager === "vite-plus" + ? "vp install -g @openai/codex@latest" + : "npm install -g @openai/codex@latest"; throw new Error( `Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`, ); @@ -117,6 +121,15 @@ const binaryPath = findCodexExecutable(); * in order to give the user a hint about how to update it. */ function detectPackageManager() { + const vpHome = process.env.VP_HOME || path.join(os.homedir(), ".vite-plus"); + const vpPackagesRoot = path.join(vpHome, "packages"); + const resolvedVpPackagesRoot = existsSync(vpPackagesRoot) + ? realpathSync(vpPackagesRoot) + : path.resolve(vpPackagesRoot); + if (isPathInside(resolvedVpPackagesRoot, codexPackageRoot)) { + return "vite-plus"; + } + const userAgent = process.env.npm_config_user_agent || ""; if (/\bbun\//.test(userAgent)) { return "bun"; @@ -137,15 +150,29 @@ function detectPackageManager() { return userAgent ? "npm" : null; } +function isPathInside(parent, child) { + const relative = path.relative(parent, child); + return ( + relative === "" || + (relative !== ".." && + !relative.startsWith(`..${path.sep}`) && + !path.isAbsolute(relative)) + ); +} + const packageManagerEnvVar = - detectPackageManager() === "bun" - ? "CODEX_MANAGED_BY_BUN" - : "CODEX_MANAGED_BY_NPM"; + { + "bun": "CODEX_MANAGED_BY_BUN", + "vite-plus": "CODEX_MANAGED_BY_VITE_PLUS", + }[detectPackageManager()] ?? "CODEX_MANAGED_BY_NPM"; const env = { ...process.env, - [packageManagerEnvVar]: "1", - CODEX_MANAGED_PACKAGE_ROOT: realpathSync(path.join(__dirname, "..")), + CODEX_MANAGED_PACKAGE_ROOT: codexPackageRoot, }; +delete env.CODEX_MANAGED_BY_NPM; +delete env.CODEX_MANAGED_BY_BUN; +delete env.CODEX_MANAGED_BY_VITE_PLUS; +env[packageManagerEnvVar] = "1"; const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index ef587cd1d4eb..ac9a6ac66aad 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -797,6 +797,10 @@ fn installation_check(show_details: bool) -> DoctorCheck { "managed by bun: {}", env::var_os("CODEX_MANAGED_BY_BUN").is_some() )); + details.push(format!( + "managed by Vite+: {}", + env::var_os("CODEX_MANAGED_BY_VITE_PLUS").is_some() + )); push_env_path_detail( &mut details, "managed package root", @@ -885,6 +889,7 @@ fn doctor_managed_by_npm(current_exe: Option<&Path>) -> bool { fn inherited_managed_env_for_cargo_binary(current_exe: Option<&Path>) -> bool { if env::var_os("CODEX_MANAGED_BY_NPM").is_none() && env::var_os("CODEX_MANAGED_BY_BUN").is_none() + && env::var_os("CODEX_MANAGED_BY_VITE_PLUS").is_none() { return false; } @@ -937,6 +942,9 @@ fn describe_install_context(context: &InstallContext) -> String { InstallMethod::Bun => { describe_method_with_package_layout("bun", context.package_layout.as_ref()) } + InstallMethod::VitePlus => { + describe_method_with_package_layout("vite+", context.package_layout.as_ref()) + } InstallMethod::Brew => { describe_method_with_package_layout("brew", context.package_layout.as_ref()) } diff --git a/codex-rs/cli/src/doctor/runtime.rs b/codex-rs/cli/src/doctor/runtime.rs index 14afa70e4c53..8add3b4f43d9 100644 --- a/codex-rs/cli/src/doctor/runtime.rs +++ b/codex-rs/cli/src/doctor/runtime.rs @@ -121,6 +121,7 @@ fn install_method_name(context: &InstallContext) -> &'static str { InstallMethod::Standalone { .. } => "standalone", InstallMethod::Npm => "npm", InstallMethod::Bun => "bun", + InstallMethod::VitePlus => "vite+", InstallMethod::Brew => "brew", InstallMethod::Other => "local build", } diff --git a/codex-rs/cli/src/doctor/updates.rs b/codex-rs/cli/src/doctor/updates.rs index 246eac2b39fa..c1c0986bcd92 100644 --- a/codex-rs/cli/src/doctor/updates.rs +++ b/codex-rs/cli/src/doctor/updates.rs @@ -133,6 +133,7 @@ fn update_action_label(context: &InstallContext) -> &'static str { match &context.method { InstallMethod::Npm => "npm install -g @openai/codex", InstallMethod::Bun => "bun install -g @openai/codex", + InstallMethod::VitePlus => "vp install -g @openai/codex", InstallMethod::Brew => "brew upgrade --cask codex", InstallMethod::Standalone { .. } => "standalone installer", InstallMethod::Other => "manual or unknown", @@ -144,6 +145,7 @@ fn fetch_latest_version(context: &InstallContext) -> Result { InstallMethod::Brew => fetch_homebrew_cask_version(), InstallMethod::Npm | InstallMethod::Bun + | InstallMethod::VitePlus | InstallMethod::Standalone { .. } | InstallMethod::Other => fetch_latest_github_release_version(), } @@ -223,6 +225,13 @@ mod tests { }), "npm install -g @openai/codex" ); + assert_eq!( + update_action_label(&InstallContext { + method: InstallMethod::VitePlus, + package_layout: None, + }), + "vp install -g @openai/codex" + ); assert_eq!( update_action_label(&InstallContext { method: InstallMethod::Other, diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index 63694c5d6259..47a1d368b4b0 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -56,6 +56,8 @@ pub enum InstallMethod { Npm, /// A Codex binary launched through the bun-managed `codex.js` shim. Bun, + /// A Codex binary launched through the Vite+-managed `codex.js` shim. + VitePlus, /// A Codex binary that appears to come from a Homebrew install prefix. Brew, /// Any other execution environment. @@ -71,6 +73,7 @@ impl InstallContext { current_exe: Option<&Path>, managed_by_npm: bool, managed_by_bun: bool, + managed_by_vite_plus: bool, ) -> Self { let codex_home = codex_utils_home_dir::find_codex_home().ok(); Self::from_exe_with_codex_home( @@ -78,6 +81,7 @@ impl InstallContext { current_exe, managed_by_npm, managed_by_bun, + managed_by_vite_plus, codex_home.as_deref(), ) } @@ -87,10 +91,13 @@ impl InstallContext { current_exe: Option<&Path>, managed_by_npm: bool, managed_by_bun: bool, + managed_by_vite_plus: bool, codex_home: Option<&Path>, ) -> Self { let package_layout = current_exe.and_then(CodexPackageLayout::from_exe); - let method = if managed_by_npm { + let method = if managed_by_vite_plus { + InstallMethod::VitePlus + } else if managed_by_npm { InstallMethod::Npm } else if managed_by_bun { InstallMethod::Bun @@ -111,11 +118,13 @@ impl InstallContext { let current_exe = std::env::current_exe().ok(); let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some(); let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some(); + let managed_by_vite_plus = std::env::var_os("CODEX_MANAGED_BY_VITE_PLUS").is_some(); Self::from_exe( cfg!(target_os = "macos"), current_exe.as_deref(), managed_by_npm, managed_by_bun, + managed_by_vite_plus, ) }) } @@ -310,6 +319,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ Some(codex_home.path()), ); assert_eq!( @@ -345,6 +355,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ Some(codex_home.path()), ); assert_eq!(context.rg_command(), default_rg_command()); @@ -388,6 +399,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!( @@ -452,6 +464,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ Some(codex_home.path()), ); assert_eq!( @@ -501,6 +514,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ true, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!(context.method, InstallMethod::Npm); @@ -528,6 +542,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!(context.rg_command(), default_rg_command()); @@ -552,6 +567,7 @@ mod tests { /*current_exe*/ Some(&exe_path), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!(context.rg_command(), default_rg_command()); @@ -560,12 +576,29 @@ mod tests { } #[test] - fn npm_and_bun_take_precedence() { + fn package_manager_markers_take_precedence() { + let vite_plus_context = InstallContext::from_exe_with_codex_home( + /*is_macos*/ false, + /*current_exe*/ Some(Path::new("/tmp/codex")), + /*managed_by_npm*/ false, + /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ true, + /*codex_home*/ None, + ); + assert_eq!( + vite_plus_context, + InstallContext { + method: InstallMethod::VitePlus, + package_layout: None, + } + ); + let npm_context = InstallContext::from_exe_with_codex_home( /*is_macos*/ false, /*current_exe*/ Some(Path::new("/tmp/codex")), /*managed_by_npm*/ true, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!( @@ -581,6 +614,7 @@ mod tests { /*current_exe*/ Some(Path::new("/tmp/codex")), /*managed_by_npm*/ false, /*managed_by_bun*/ true, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!( @@ -599,6 +633,7 @@ mod tests { /*current_exe*/ Some(Path::new("/opt/homebrew/bin/codex")), /*managed_by_npm*/ false, /*managed_by_bun*/ false, + /*managed_by_vite_plus*/ false, /*codex_home*/ None, ); assert_eq!( diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index 420562f13b68..c38dc9a9a0dd 100644 --- a/codex-rs/tui/src/update_action.rs +++ b/codex-rs/tui/src/update_action.rs @@ -12,6 +12,8 @@ pub enum UpdateAction { NpmGlobalLatest, /// Update via `bun install -g @openai/codex@latest`. BunGlobalLatest, + /// Update via `vp install -g @openai/codex@latest`. + VitePlusGlobalLatest, /// Update via `brew upgrade codex`. BrewUpgrade, /// Update via `curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh`. @@ -26,6 +28,7 @@ impl UpdateAction { match &context.method { InstallMethod::Npm => Some(UpdateAction::NpmGlobalLatest), InstallMethod::Bun => Some(UpdateAction::BunGlobalLatest), + InstallMethod::VitePlus => Some(UpdateAction::VitePlusGlobalLatest), InstallMethod::Brew => Some(UpdateAction::BrewUpgrade), InstallMethod::Standalone { platform, .. } => Some(match platform { StandalonePlatform::Unix => UpdateAction::StandaloneUnix, @@ -40,6 +43,7 @@ impl UpdateAction { match self { UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex"]), UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex"]), + UpdateAction::VitePlusGlobalLatest => ("vp", &["install", "-g", "@openai/codex"]), UpdateAction::BrewUpgrade => ("brew", &["upgrade", "--cask", "codex"]), UpdateAction::StandaloneUnix => ( "sh", @@ -106,6 +110,13 @@ mod tests { }), Some(UpdateAction::BunGlobalLatest) ); + assert_eq!( + UpdateAction::from_install_context(&InstallContext { + method: InstallMethod::VitePlus, + package_layout: None, + }), + Some(UpdateAction::VitePlusGlobalLatest) + ); assert_eq!( UpdateAction::from_install_context(&InstallContext { method: InstallMethod::Brew, diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index 65a2317560ab..0b1a47aa190a 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -79,7 +79,9 @@ async fn check_for_update(version_file: &Path, action: Option) -> .await?; version } - Some(UpdateAction::NpmGlobalLatest) | Some(UpdateAction::BunGlobalLatest) => { + Some(UpdateAction::NpmGlobalLatest) + | Some(UpdateAction::BunGlobalLatest) + | Some(UpdateAction::VitePlusGlobalLatest) => { let latest_version = fetch_latest_github_release_version().await?; let package_info = create_client() .get(npm_registry::PACKAGE_URL)