mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
@@ -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",
|
||||
@@ -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}`,
|
||||
);
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user