diff --git a/codex-cli/bin/coder.js b/codex-cli/bin/coder.js index ff56a0a8a8e..cb15147c48f 100755 --- a/codex-cli/bin/coder.js +++ b/codex-cli/bin/coder.js @@ -6,6 +6,7 @@ import { fileURLToPath } from "url"; import { platform as nodePlatform, arch as nodeArch } from "os"; import { execSync } from "child_process"; import { get as httpsGet } from "https"; +import { runPostinstall } from "../postinstall.js"; // __dirname equivalent in ESM const __filename = fileURLToPath(import.meta.url); @@ -270,12 +271,26 @@ const tryBootstrapBinary = async () => { }; // If missing, attempt to bootstrap into place (helps when Bun blocks postinstall) -if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) { - const ok = await tryBootstrapBinary(); - if (!ok) { - // retry legacy name in case archive provided coder-* - if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) { - binaryPath = legacyBinaryPath; +let binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); +if (!binaryReady) { + let runtimePostinstallError = null; + try { + await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true }); + } catch (err) { + runtimePostinstallError = err; + } + + binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); + if (!binaryReady) { + const ok = await tryBootstrapBinary(); + if (!ok) { + if (runtimePostinstallError && !lastBootstrapError) { + lastBootstrapError = runtimePostinstallError; + } + // retry legacy name in case archive provided coder-* + if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) { + binaryPath = legacyBinaryPath; + } } } } diff --git a/codex-cli/postinstall.js b/codex-cli/postinstall.js index 4664e82704f..e8b46789e79 100644 --- a/codex-cli/postinstall.js +++ b/codex-cli/postinstall.js @@ -2,7 +2,7 @@ // Non-functional change to trigger release workflow import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync, fsyncSync, renameSync, realpathSync } from 'fs'; -import { join, dirname } from 'path'; +import { join, dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; import { get } from 'https'; import { platform, arch, tmpdir } from 'os'; @@ -288,13 +288,21 @@ function validateDownloadedBinary(p) { } } -async function main() { +export async function runPostinstall(options = {}) { + const { skipGlobalAlias = false, invokedByRuntime = false } = options; + if (process.env.CODE_POSTINSTALL_DRY_RUN === '1') { + return { skipped: true }; + } + + if (invokedByRuntime) { + process.env.CODE_RUNTIME_POSTINSTALL = process.env.CODE_RUNTIME_POSTINSTALL || '1'; + } // Detect potential PATH conflict with an existing `code` command (e.g., VS Code) // Only relevant for global installs; skip for npx/local installs to keep postinstall fast. const ua = process.env.npm_config_user_agent || ''; const isNpx = ua.includes('npx'); const isGlobal = process.env.npm_config_global === 'true'; - if (isGlobal && !isNpx) { + if (!skipGlobalAlias && isGlobal && !isNpx) { try { const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true'; const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0]; @@ -760,7 +768,19 @@ async function main() { } } -main().catch(error => { - console.error('Installation failed:', error); - process.exit(1); -}); +function isExecutedDirectly() { + const entry = process.argv[1]; + if (!entry) return false; + try { + return resolve(entry) === fileURLToPath(import.meta.url); + } catch { + return false; + } +} + +if (isExecutedDirectly()) { + runPostinstall().catch(error => { + console.error('Installation failed:', error); + process.exit(1); + }); +} diff --git a/codex-cli/test/postinstall.test.js b/codex-cli/test/postinstall.test.js new file mode 100644 index 00000000000..596b64afcd1 --- /dev/null +++ b/codex-cli/test/postinstall.test.js @@ -0,0 +1,14 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +test('runPostinstall resolves in dry-run mode', async () => { + const { runPostinstall } = await import('../postinstall.js'); + process.env.CODE_POSTINSTALL_DRY_RUN = '1'; + try { + const result = await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true }); + assert.ok(result && result.skipped === true); + } finally { + delete process.env.CODE_POSTINSTALL_DRY_RUN; + delete process.env.CODE_RUNTIME_POSTINSTALL; + } +});