Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions codex-cli/bin/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
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";

// __dirname equivalent in ESM
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",
Expand Down Expand Up @@ -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}`,
);
Expand All @@ -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";
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/cli/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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())
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/cli/src/doctor/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/cli/src/doctor/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(),
}
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 37 additions & 2 deletions codex-rs/install-context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -71,13 +73,15 @@ 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(
is_macos,
current_exe,
managed_by_npm,
managed_by_bun,
managed_by_vite_plus,
codex_home.as_deref(),
)
}
Expand All @@ -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
Expand All @@ -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,
)
})
}
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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!(
Expand All @@ -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!(
Expand All @@ -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!(
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/tui/src/update_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/tui/src/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading