diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index a307e38610..934600714b 100755 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -2,7 +2,7 @@ // Unified entry point for the Codex CLI. import { spawn } from "node:child_process"; -import { existsSync, realpathSync } from "fs"; +import { existsSync, readFileSync, realpathSync } from "fs"; import { createRequire } from "node:module"; import path from "path"; import { fileURLToPath } from "url"; @@ -101,7 +101,9 @@ function findCodexExecutable() { ? "bun install -g @openai/codex@latest" : packageManager === "pnpm" ? "pnpm add -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}`, ); @@ -130,14 +132,52 @@ function isPnpmOwnedCodexInstall(nodeModulesDir) { } } +function isVitePlusOwnedCodexInstall(packagesDir) { + if (path.basename(packagesDir) !== "packages") { + return false; + } + + try { + const metadata = JSON.parse( + readFileSync(path.join(packagesDir, "@openai", "codex.json"), "utf8"), + ); + if (metadata.name !== "@openai/codex") { + return false; + } + + // Vite+ records the active global installation in packages/@openai/codex.json. + // Older installs have no ID or append a #-prefixed ID to the package name; + // newer installs put the ID in a subdirectory of the package prefix. + const installId = metadata.installId || ""; + const installDir = installId.startsWith("#") + ? path.join(packagesDir, `@openai/codex${installId}`) + : path.join(packagesDir, "@openai/codex", installId); + for (const nodeModulesDir of [ + path.join(installDir, "lib", "node_modules"), + path.join(installDir, "node_modules"), + ]) { + const packageRoot = path.join(nodeModulesDir, "@openai", "codex"); + if ( + existsSync(packageRoot) && + realpathSync(packageRoot) === codexPackageRoot + ) { + return true; + } + } + } catch { + // Missing or unreadable ownership metadata must not prevent Codex starting. + } + return false; +} + /** * Use heuristics to detect the package manager that was used to install Codex * in order to give the user a hint about how to update it. */ function detectPackageManager() { - // pnpm's owning node_modules directory can be several parents above the - // package in isolated global layouts. Search ancestors of both the canonical - // package root and lexical entrypoint because pnpm may link either path. + // Package-manager ownership metadata can be several parents above the package. + // Search ancestors of both the canonical package root and lexical entrypoint + // because the package manager may link either path. const entrypointDir = path.dirname(path.resolve(process.argv[1])); for (const startDir of new Set([codexPackageRoot, entrypointDir])) { const filesystemRoot = path.parse(startDir).root; @@ -146,6 +186,9 @@ function detectPackageManager() { currentDir !== filesystemRoot; currentDir = path.dirname(currentDir) ) { + if (isVitePlusOwnedCodexInstall(currentDir)) { + return "vite-plus"; + } if (isPnpmOwnedCodexInstall(path.join(currentDir, "node_modules"))) { return "pnpm"; } @@ -182,7 +225,9 @@ const packageManagerEnvVar = ? "CODEX_MANAGED_BY_BUN" : packageManager === "pnpm" ? "CODEX_MANAGED_BY_PNPM" - : "CODEX_MANAGED_BY_NPM"; + : packageManager === "vite-plus" + ? "CODEX_MANAGED_BY_VITE_PLUS" + : "CODEX_MANAGED_BY_NPM"; const env = { ...process.env, CODEX_MANAGED_PACKAGE_ROOT: codexPackageRoot, @@ -190,6 +235,7 @@ const env = { delete env.CODEX_MANAGED_BY_NPM; delete env.CODEX_MANAGED_BY_BUN; delete env.CODEX_MANAGED_BY_PNPM; +delete env.CODEX_MANAGED_BY_VITE_PLUS; env[packageManagerEnvVar] = "1"; const child = spawn(binaryPath, process.argv.slice(2), { diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 5735044303..0e0e06b3e5 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -875,6 +875,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() + )); details.push(format!( "managed by pnpm: {}", env::var_os("CODEX_MANAGED_BY_PNPM").is_some() @@ -967,6 +971,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() && env::var_os("CODEX_MANAGED_BY_PNPM").is_none() { return false; @@ -1020,6 +1025,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::Pnpm => { describe_method_with_package_layout("pnpm", context.package_layout.as_ref()) } diff --git a/codex-rs/cli/src/doctor/output.rs b/codex-rs/cli/src/doctor/output.rs index 74c40f7f7d..d3d7dfd914 100644 --- a/codex-rs/cli/src/doctor/output.rs +++ b/codex-rs/cli/src/doctor/output.rs @@ -1282,7 +1282,7 @@ Environment LESS -FRX ✓ runtime running local build on darwin-arm64 ✓ install consistent - managed by npm: no · bun: no · pnpm: no · package root — + managed by npm: no · bun: no · pnpm: no · Vite+: no · package root — ✓ search search is OK (bundled) ✓ git git version 2.54.0 selected git /usr/bin/git diff --git a/codex-rs/cli/src/doctor/output/detail.rs b/codex-rs/cli/src/doctor/output/detail.rs index 0ee5add05e..bca2baeaa4 100644 --- a/codex-rs/cli/src/doctor/output/detail.rs +++ b/codex-rs/cli/src/doctor/output/detail.rs @@ -212,14 +212,16 @@ fn install_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec< let managed_by_npm = value(parsed, "managed by npm").unwrap_or("false"); let managed_by_bun = value(parsed, "managed by bun").unwrap_or("false"); let managed_by_pnpm = value(parsed, "managed by pnpm").unwrap_or("false"); + let managed_by_vite_plus = value(parsed, "managed by Vite+").unwrap_or("false"); let package_root = value(parsed, "managed package root").unwrap_or("not set"); out.push(HumanDetail::Row { label: "managed by".to_string(), value: format!( - "npm: {} · bun: {} · pnpm: {} · package root {}", + "npm: {} · bun: {} · pnpm: {} · Vite+: {} · package root {}", yes_no(managed_by_npm), yes_no(managed_by_bun), yes_no(managed_by_pnpm), + yes_no(managed_by_vite_plus), if is_falsy(package_root) { "—".to_string() } else { @@ -266,6 +268,7 @@ fn install_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec< "managed by npm", "managed by bun", "managed by pnpm", + "managed by Vite+", "managed package root", "PATH codex entries", ], diff --git a/codex-rs/cli/src/doctor/runtime.rs b/codex-rs/cli/src/doctor/runtime.rs index d805c7c6b2..2eb176189a 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::Pnpm => "pnpm", InstallMethod::Brew => "brew", InstallMethod::Other => "local build", diff --git a/codex-rs/cli/src/doctor/snapshots/codex__doctor__output__tests__doctor_human_report_environment_rows.snap b/codex-rs/cli/src/doctor/snapshots/codex__doctor__output__tests__doctor_human_report_environment_rows.snap index ec93326efa..7aec6928df 100644 --- a/codex-rs/cli/src/doctor/snapshots/codex__doctor__output__tests__doctor_human_report_environment_rows.snap +++ b/codex-rs/cli/src/doctor/snapshots/codex__doctor__output__tests__doctor_human_report_environment_rows.snap @@ -30,7 +30,7 @@ Environment → Microsoft Defender: Add a certificate or executable-path exclusion for Codex and its helpers. If Attack Surface Reduction blocks Codex, add a rule exclusion. If Controlled Folder Access blocks Codex, allow the app. ✓ runtime running local build on darwin-arm64 ✓ install consistent - managed by npm: no · bun: no · pnpm: no · package root — + managed by npm: no · bun: no · pnpm: no · Vite+: no · package root — ✓ search search is OK (bundled) ✓ git git version 2.54.0 selected git /usr/bin/git diff --git a/codex-rs/cli/src/doctor/updates.rs b/codex-rs/cli/src/doctor/updates.rs index 7c2add2308..08d0d3d276 100644 --- a/codex-rs/cli/src/doctor/updates.rs +++ b/codex-rs/cli/src/doctor/updates.rs @@ -432,6 +432,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::Pnpm => "pnpm add -g @openai/codex", InstallMethod::Brew => "brew upgrade --cask codex", InstallMethod::Standalone { .. } => "standalone installer", @@ -444,6 +445,7 @@ fn fetch_latest_version(context: &InstallContext) -> Result { InstallMethod::Brew => fetch_homebrew_cask_version(), InstallMethod::Npm | InstallMethod::Bun + | InstallMethod::VitePlus | InstallMethod::Pnpm | InstallMethod::Standalone { .. } | InstallMethod::Other => fetch_latest_github_release_version(), diff --git a/codex-rs/install-context/src/lib.rs b/codex-rs/install-context/src/lib.rs index 20f27515c8..699add63db 100644 --- a/codex-rs/install-context/src/lib.rs +++ b/codex-rs/install-context/src/lib.rs @@ -71,6 +71,8 @@ pub enum InstallMethod { Bun, /// A Codex binary launched through the pnpm-managed `codex.js` shim. Pnpm, + /// 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. @@ -119,7 +121,9 @@ impl InstallContext { pub fn current() -> &'static Self { INSTALL_CONTEXT.get_or_init(|| { let current_exe = std::env::current_exe().ok(); - let method_override = if std::env::var_os("CODEX_MANAGED_BY_PNPM").is_some() { + let method_override = if std::env::var_os("CODEX_MANAGED_BY_VITE_PLUS").is_some() { + Some(InstallMethod::VitePlus) + } else if std::env::var_os("CODEX_MANAGED_BY_PNPM").is_some() { Some(InstallMethod::Pnpm) } else if std::env::var_os("CODEX_MANAGED_BY_NPM").is_some() { Some(InstallMethod::Npm) @@ -824,6 +828,19 @@ mod tests { #[test] fn package_manager_method_overrides_take_precedence() { + let vite_plus_context = InstallContext::from_exe( + /*is_macos*/ false, + /*current_exe*/ Some(Path::new("/tmp/codex")), + /*method_override*/ Some(InstallMethod::VitePlus), + ); + assert_eq!( + vite_plus_context, + InstallContext { + method: InstallMethod::VitePlus, + package_layout: None, + } + ); + let pnpm_context = InstallContext::from_exe( /*is_macos*/ false, /*current_exe*/ Some(Path::new("/tmp/codex")), diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__vite_plus_update_available_history_cell_snapshot.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__vite_plus_update_available_history_cell_snapshot.snap new file mode 100644 index 0000000000..1a2df82a9d --- /dev/null +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__vite_plus_update_available_history_cell_snapshot.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/history_cell/tests.rs +expression: rendered +--- +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.0.0 -> 9.9.9 │ +│ Run vp install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 5890c3ecaf..13ca45cf06 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -1463,6 +1463,17 @@ fn pnpm_update_available_history_cell_snapshot() { insta::assert_snapshot!(rendered); } +#[test] +fn vite_plus_update_available_history_cell_snapshot() { + let cell = UpdateAvailableHistoryCell::new( + "9.9.9".to_string(), + Some(UpdateAction::VitePlusGlobalLatest), + ); + let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n"); + + insta::assert_snapshot!(rendered); +} + #[test] fn web_search_history_cell_without_detail_snapshot() { let cell = new_web_search_call("call-1".to_string(), String::new(), WebSearchAction::Other); diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index 0fe4408444..225c457086 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 `pnpm add -g @openai/codex@latest`. PnpmGlobalLatest, /// Update via `brew upgrade codex`. @@ -28,6 +30,7 @@ impl UpdateAction { match &context.method { InstallMethod::Npm => Some(UpdateAction::NpmGlobalLatest), InstallMethod::Bun => Some(UpdateAction::BunGlobalLatest), + InstallMethod::VitePlus => Some(UpdateAction::VitePlusGlobalLatest), InstallMethod::Pnpm => Some(UpdateAction::PnpmGlobalLatest), InstallMethod::Brew => Some(UpdateAction::BrewUpgrade), InstallMethod::Standalone { platform, .. } => Some(match platform { @@ -43,6 +46,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::PnpmGlobalLatest => ("pnpm", &["add", "-g", "@openai/codex"]), UpdateAction::BrewUpgrade => ("brew", &["upgrade", "--cask", "codex"]), UpdateAction::StandaloneUnix => ( diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index 948551a65e..b801426552 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -95,6 +95,7 @@ async fn check_for_update( } Some(UpdateAction::NpmGlobalLatest) | Some(UpdateAction::BunGlobalLatest) + | Some(UpdateAction::VitePlusGlobalLatest) | Some(UpdateAction::PnpmGlobalLatest) => { let latest_version = fetch_latest_github_release_version(&client_pool).await?; let package_info = client_pool