Detect Codex installs managed by pnpm (#31503)

## Why

The Codex JavaScript shim currently distinguishes npm and Bun installs,
but a global pnpm install falls back to npm. That causes the native CLI,
`codex doctor`, and update flows to report or run npm commands even
though pnpm owns the installation. pnpm also allows its global package
and bin directories to differ, so `PNPM_HOME` does not reliably identify
the package owner.

## What changed

- detect pnpm-managed installs by finding pnpm's
`node_modules/.modules.yaml` metadata from the launched JavaScript
entrypoint
- pass a mutually exclusive `CODEX_MANAGED_BY_PNPM` marker into the
native CLI
- represent pnpm in install context, doctor output, version checks, and
TUI update actions without changing the existing public
`InstallContext::from_exe` signature
- recommend `pnpm add -g @openai/codex` for pnpm-managed installs and
snapshot the rendered TUI update notice

Validated with the 9-test install-context suite, the focused pnpm TUI
snapshot test, and Node's syntax check for the launcher.

Closes https://github.com/openai/codex/issues/10294.
This commit is contained in:
Charlie Marsh
2026-07-08 12:17:58 -04:00
committed by GitHub
parent 0bbea86a6a
commit e212cc95b0
12 changed files with 152 additions and 50 deletions
@@ -0,0 +1,11 @@
---
source: tui/src/history_cell/tests.rs
expression: rendered
---
╭─────────────────────────────────────────────────╮
│ ✨ Update available! 0.0.0 -> 9.9.9 │
│ Run pnpm add -g @openai/codex to update. │
│ │
│ See full release notes: │
│ https://github.com/openai/codex/releases/latest │
╰─────────────────────────────────────────────────╯
+9
View File
@@ -1137,6 +1137,15 @@ fn standalone_windows_update_available_history_cell_snapshot() {
insta::assert_snapshot!(rendered);
}
#[test]
fn pnpm_update_available_history_cell_snapshot() {
let cell =
UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::PnpmGlobalLatest));
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);
+11
View File
@@ -12,6 +12,8 @@ pub enum UpdateAction {
NpmGlobalLatest,
/// Update via `bun install -g @openai/codex@latest`.
BunGlobalLatest,
/// Update via `pnpm add -g @openai/codex@latest`.
PnpmGlobalLatest,
/// 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::Pnpm => Some(UpdateAction::PnpmGlobalLatest),
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::PnpmGlobalLatest => ("pnpm", &["add", "-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::Pnpm,
package_layout: None,
}),
Some(UpdateAction::PnpmGlobalLatest)
);
assert_eq!(
UpdateAction::from_install_context(&InstallContext {
method: InstallMethod::Brew,
+3 -1
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::PnpmGlobalLatest) => {
let latest_version = fetch_latest_github_release_version().await?;
let package_info = create_client()
.get(npm_registry::PACKAGE_URL)