mirror of
https://github.com/openai/codex.git
synced 2026-09-28 08:43:01 +08:00
Detect Vite+-managed Codex installs (#42071)
## Why Vite+ launches Codex through the same JavaScript shim as npm, so Codex could misidentify Vite+-owned installations and recommend npm for updates. ## What changed - Detect Vite+ global installs from their package ownership metadata and pass a dedicated `CODEX_MANAGED_BY_VITE_PLUS` marker to the native binary. - Represent Vite+ in install context and `codex doctor` output. - Recommend `vp install -g @openai/codex` in update and repair flows for Vite+-managed installs. See https://github.com/openai/codex/pull/30880. GitOrigin-RevId: 8add724eefd31170a078086392609dbbd7f20b26
This commit is contained in:
+52
-6
@@ -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), {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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<String, String> {
|
||||
InstallMethod::Brew => fetch_homebrew_cask_version(),
|
||||
InstallMethod::Npm
|
||||
| InstallMethod::Bun
|
||||
| InstallMethod::VitePlus
|
||||
| InstallMethod::Pnpm
|
||||
| InstallMethod::Standalone { .. }
|
||||
| InstallMethod::Other => fetch_latest_github_release_version(),
|
||||
|
||||
@@ -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")),
|
||||
|
||||
+11
@@ -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 │
|
||||
╰─────────────────────────────────────────────────╯
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user