Skip to content
Merged
55 changes: 50 additions & 5 deletions codex-cli/bin/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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",
Expand Down Expand Up @@ -98,7 +99,9 @@ function findCodexExecutable() {
const updateCommand =
packageManager === "bun"
? "bun install -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
: packageManager === "pnpm"
? "pnpm add -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
throw new Error(
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
);
Expand All @@ -112,11 +115,47 @@ const binaryPath = findCodexExecutable();
// and guarantees that when either the child terminates or the parent
// receives a fatal signal, both processes exit in a predictable manner.

function isPnpmOwnedCodexInstall(nodeModulesDir) {
if (!existsSync(path.join(nodeModulesDir, ".modules.yaml"))) {
return false;
}

try {
return (
realpathSync(path.join(nodeModulesDir, "@openai", "codex")) ===
codexPackageRoot
);
} catch {
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.
const entrypointDir = path.dirname(path.resolve(process.argv[1]));
for (const startDir of new Set([codexPackageRoot, entrypointDir])) {
const filesystemRoot = path.parse(startDir).root;
for (
let currentDir = startDir;
currentDir !== filesystemRoot;
currentDir = path.dirname(currentDir)
) {
if (isPnpmOwnedCodexInstall(path.join(currentDir, "node_modules"))) {
return "pnpm";
}
}

if (isPnpmOwnedCodexInstall(path.join(filesystemRoot, "node_modules"))) {
return "pnpm";
}
}

const userAgent = process.env.npm_config_user_agent || "";
if (/\bbun\//.test(userAgent)) {
return "bun";
Expand All @@ -137,15 +176,21 @@ function detectPackageManager() {
return userAgent ? "npm" : null;
}

const packageManager = detectPackageManager();
const packageManagerEnvVar =
detectPackageManager() === "bun"
packageManager === "bun"
? "CODEX_MANAGED_BY_BUN"
: "CODEX_MANAGED_BY_NPM";
: packageManager === "pnpm"
? "CODEX_MANAGED_BY_PNPM"
: "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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to delete the env variables newly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing bug on main (npm-managed Codex starts a shell, that shell invokes a Bun-managed Codex but inherits CODEX_MANAGED_BY_NPM=1). This PR makes it a bit worse since we now have even more combinations here. But it isn't strictly necessary, I can remove or carve into a separate PR if preferred.

delete env.CODEX_MANAGED_BY_BUN;
delete env.CODEX_MANAGED_BY_PNPM;
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 pnpm: {}",
env::var_os("CODEX_MANAGED_BY_PNPM").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_PNPM").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::Pnpm => {
describe_method_with_package_layout("pnpm", context.package_layout.as_ref())
}
InstallMethod::Brew => {
describe_method_with_package_layout("brew", context.package_layout.as_ref())
}
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/cli/src/doctor/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ Environment
LESS -FRX
✓ runtime running local build on darwin-arm64
✓ install consistent
managed by npm: no · bun: no · package root —
managed by npm: no · bun: no · pnpm: no · package root —
✓ search search is OK (bundled)
✓ git git version 2.54.0
selected git /usr/bin/git
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/cli/src/doctor/output/detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,15 @@ 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 package_root = value(parsed, "managed package root").unwrap_or("not set");
out.push(HumanDetail::Row {
label: "managed by".to_string(),
value: format!(
"npm: {} · bun: {} · package root {}",
"npm: {} · bun: {} · pnpm: {} · package root {}",
yes_no(managed_by_npm),
yes_no(managed_by_bun),
yes_no(managed_by_pnpm),
if is_falsy(package_root) {
"—".to_string()
} else {
Expand Down Expand Up @@ -263,6 +265,7 @@ fn install_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec<
"install context",
"managed by npm",
"managed by bun",
"managed by pnpm",
"managed package root",
"PATH codex entries",
],
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::Pnpm => "pnpm",
InstallMethod::Brew => "brew",
InstallMethod::Other => "local build",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Environment
LESS -FRX
✓ runtime running local build on darwin-arm64
✓ install consistent
managed by npm: no · bun: no · package root —
managed by npm: no · bun: no · pnpm: no · package root —
✓ search search is OK (bundled)
✓ git git version 2.54.0
selected git /usr/bin/git
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::Pnpm => "pnpm add -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::Pnpm
| 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::Pnpm,
package_layout: None,
}),
"pnpm add -g @openai/codex"
);
assert_eq!(
update_action_label(&InstallContext {
method: InstallMethod::Other,
Expand Down
Loading
Loading