Detect Codex installs managed by Vite+

This commit is contained in:
Charlie Marsh
2026-07-01 20:02:38 -04:00
parent 042e61726d
commit dc174cdc4e
7 changed files with 102 additions and 9 deletions

View File

@@ -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",

View File

@@ -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())
}

View File

@@ -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",
}

View File

@@ -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<String, String> {
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,

View File

@@ -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!(

View File

@@ -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,

View File

@@ -79,7 +79,9 @@ async fn check_for_update(version_file: &Path, action: Option<UpdateAction>) ->
.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)