Skip to content

Commit f1f52aa

Browse files
authored
Merge pull request #391 from ndycode/feat/native-codex-cli-support
support native Codex CLI installs
2 parents 1f639d3 + 776c594 commit f1f52aa

6 files changed

Lines changed: 592 additions & 45 deletions

File tree

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![npm version](https://img.shields.io/npm/v/codex-multi-auth.svg)](https://www.npmjs.com/package/codex-multi-auth)
44
[![npm downloads](https://img.shields.io/npm/dw/codex-multi-auth.svg)](https://www.npmjs.com/package/codex-multi-auth)
55

6-
Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI.
6+
Codex CLI-first multi-account OAuth manager for the official Codex CLI, including the `@openai/codex` npm launcher and native `codex` installs.
77

88
<img width="1270" height="729" alt="2026-02-28 12_54_58-prompt txt ‎- Notepads" src="https://github.com/user-attachments/assets/0cecb77e-a6d3-432a-ba48-3577db0c7093" />
99

@@ -72,6 +72,8 @@ codex-multi-auth --version
7272
codex auth status
7373
```
7474

75+
Any official install path is fine as long as `codex` is on `PATH`: `npm i -g @openai/codex`, `brew install --cask codex`, or an official release binary.
76+
7577
</details>
7678

7779
<details>
@@ -106,6 +108,13 @@ npm i -g codex-multi-auth
106108
codex auth login
107109
```
108110

111+
If you already installed the official native CLI via Homebrew or a release binary, you only need:
112+
113+
```bash
114+
npm i -g codex-multi-auth
115+
codex auth login
116+
```
117+
109118
Verify the wrapper and the new account:
110119

111120
```bash
@@ -274,7 +283,7 @@ codex auth login
274283
<details>
275284
<summary><b>Common symptoms</b></summary>
276285

277-
- `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
286+
- `codex auth` unrecognized: run `where codex` or `which codex`, then follow `docs/troubleshooting.md` for routing fallback commands
278287
- Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
279288
- Requests fail fast with a pool cooldown message: wait for the cooldown window or inspect `codex auth status`
280289
- Requests fail fast after repeated upstream 5xx errors: inspect `codex auth report --json` for runtime traffic and cooldown details

scripts/codex-bin-resolver.js

Lines changed: 162 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
11
import { spawnSync } from "node:child_process";
2-
import { existsSync } from "node:fs";
2+
import { existsSync, realpathSync } from "node:fs";
33
import { createRequire } from "node:module";
4-
import { dirname, join } from "node:path";
4+
import { basename, delimiter, dirname, extname, join } from "node:path";
55
import process from "node:process";
66
import { fileURLToPath } from "node:url";
77

8+
function isJavaScriptEntryPath(candidate) {
9+
const extension = extname(candidate).toLowerCase();
10+
return extension === ".js" || extension === ".mjs" || extension === ".cjs";
11+
}
12+
13+
function createResolvedCodexBin(path) {
14+
return {
15+
path,
16+
launchWithNode: isJavaScriptEntryPath(path),
17+
};
18+
}
19+
20+
function normalizeResolvedPath(candidatePath, realpathSyncImpl) {
21+
try {
22+
return realpathSyncImpl(candidatePath);
23+
} catch {
24+
return candidatePath;
25+
}
26+
}
27+
28+
function resolveWrapperScriptPath(moduleUrl, realpathSyncImpl) {
29+
return normalizeResolvedPath(fileURLToPath(moduleUrl), realpathSyncImpl);
30+
}
31+
32+
const DEFAULT_WRAPPER_MODULE_URL = new URL("./codex.js", import.meta.url).href;
33+
834
function defaultResolvePackageBin(moduleUrl) {
935
try {
1036
const require = createRequire(moduleUrl);
@@ -26,26 +52,141 @@ function resolveWindowsCmdPath(env) {
2652
return "cmd.exe";
2753
}
2854

55+
export function splitPathEntries(pathValue) {
56+
if (typeof pathValue !== "string" || pathValue.trim().length === 0) {
57+
return [];
58+
}
59+
return pathValue
60+
.split(delimiter)
61+
.map((entry) => entry.trim())
62+
.filter((entry) => entry.length > 0);
63+
}
64+
65+
function resolvePathExecutableName(platform) {
66+
return platform === "win32" ? "codex.exe" : "codex";
67+
}
68+
69+
function resolveCandidateExecutableNames(platform) {
70+
if (platform !== "win32") {
71+
return [resolvePathExecutableName(platform)];
72+
}
73+
return ["codex.exe", "codex"];
74+
}
75+
76+
function resolveCodexExecutableFromPath(
77+
pathEntries,
78+
platform,
79+
existsSyncImpl,
80+
selfScriptPath,
81+
realpathSyncImpl,
82+
) {
83+
for (const entry of pathEntries) {
84+
for (const executableName of resolveCandidateExecutableNames(platform)) {
85+
const candidate = join(entry, executableName);
86+
if (!existsSyncImpl(candidate)) {
87+
continue;
88+
}
89+
if (
90+
selfScriptPath &&
91+
normalizeResolvedPath(candidate, realpathSyncImpl) === selfScriptPath
92+
) {
93+
continue;
94+
}
95+
return candidate;
96+
}
97+
}
98+
return null;
99+
}
100+
101+
function normalizeWhereOutput(stdout) {
102+
return stdout
103+
.split(/\r?\n/)
104+
.map((line) => line.trim())
105+
.filter((line) => line.length > 0);
106+
}
107+
108+
function resolveCodexExecutableFromSystemPath(
109+
env,
110+
platform,
111+
spawnSyncImpl,
112+
existsSyncImpl,
113+
selfScriptPath,
114+
realpathSyncImpl,
115+
) {
116+
const pathEntries = splitPathEntries(env.PATH ?? env.Path ?? "");
117+
const fromEnvPath = resolveCodexExecutableFromPath(
118+
pathEntries,
119+
platform,
120+
existsSyncImpl,
121+
selfScriptPath,
122+
realpathSyncImpl,
123+
);
124+
if (fromEnvPath) {
125+
return fromEnvPath;
126+
}
127+
128+
try {
129+
const lookupResult =
130+
platform === "win32"
131+
? spawnSyncImpl(resolveWindowsCmdPath(env), ["/d", "/s", "/c", "where codex"], {
132+
encoding: "utf8",
133+
env,
134+
stdio: ["ignore", "pipe", "ignore"],
135+
timeout: 5000,
136+
windowsHide: true,
137+
})
138+
: spawnSyncImpl("which", ["codex"], {
139+
encoding: "utf8",
140+
env,
141+
stdio: ["ignore", "pipe", "ignore"],
142+
timeout: 5000,
143+
});
144+
if (lookupResult.status !== 0) {
145+
return null;
146+
}
147+
for (const candidate of normalizeWhereOutput(lookupResult.stdout)) {
148+
if (!existsSyncImpl(candidate)) {
149+
continue;
150+
}
151+
if (
152+
selfScriptPath &&
153+
normalizeResolvedPath(candidate, realpathSyncImpl) === selfScriptPath
154+
) {
155+
continue;
156+
}
157+
const fileName = basename(candidate).toLowerCase();
158+
if (fileName === "codex" || fileName === "codex.exe") {
159+
return candidate;
160+
}
161+
}
162+
} catch {
163+
// Ignore and fall through.
164+
}
165+
166+
return null;
167+
}
168+
29169
export function resolveRealCodexBin(options = {}) {
30170
const {
31171
env = process.env,
32172
argv = process.argv,
33173
platform = process.platform,
34-
moduleUrl = import.meta.url,
174+
moduleUrl = DEFAULT_WRAPPER_MODULE_URL,
35175
existsSyncImpl = existsSync,
176+
realpathSyncImpl = realpathSync,
36177
spawnSyncImpl = spawnSync,
37178
resolvePackageBin = defaultResolvePackageBin,
38179
} = options;
39180

40181
const override = (env.CODEX_MULTI_AUTH_REAL_CODEX_BIN ?? "").trim();
41182
if (override.length > 0) {
42-
if (existsSyncImpl(override)) return override;
183+
if (existsSyncImpl(override)) return createResolvedCodexBin(override);
43184
return null;
44185
}
45186

46187
const resolved = resolvePackageBin(moduleUrl);
47188
if (typeof resolved === "string" && resolved.length > 0 && existsSyncImpl(resolved)) {
48-
return resolved;
189+
return createResolvedCodexBin(resolved);
49190
}
50191

51192
const searchRoots = [];
@@ -65,7 +206,7 @@ export function resolveRealCodexBin(options = {}) {
65206

66207
for (const root of searchRoots) {
67208
const candidate = join(root, "@openai", "codex", "bin", "codex.js");
68-
if (existsSyncImpl(candidate)) return candidate;
209+
if (existsSyncImpl(candidate)) return createResolvedCodexBin(candidate);
69210
}
70211

71212
try {
@@ -88,12 +229,26 @@ export function resolveRealCodexBin(options = {}) {
88229
const globalRoot = rootResult.stdout.trim();
89230
if (globalRoot.length > 0) {
90231
const globalBin = join(globalRoot, "@openai", "codex", "bin", "codex.js");
91-
if (existsSyncImpl(globalBin)) return globalBin;
232+
if (existsSyncImpl(globalBin)) return createResolvedCodexBin(globalBin);
92233
}
93234
}
94235
} catch {
95236
// Ignore and fall through to null.
96237
}
97238

239+
const selfScriptPath = resolveWrapperScriptPath(moduleUrl, realpathSyncImpl);
240+
241+
const nativeCodexBin = resolveCodexExecutableFromSystemPath(
242+
env,
243+
platform,
244+
spawnSyncImpl,
245+
existsSyncImpl,
246+
selfScriptPath,
247+
realpathSyncImpl,
248+
);
249+
if (nativeCodexBin) {
250+
return createResolvedCodexBin(nativeCodexBin);
251+
}
252+
98253
return null;
99254
}

scripts/codex.js

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import { homedir, tmpdir } from "node:os";
1818
import { basename, delimiter, dirname, join, resolve as resolvePath } from "node:path";
1919
import process from "node:process";
2020
import { fileURLToPath } from "node:url";
21-
import { resolveRealCodexBin as resolveRealCodexBinFromEnvironment } from "./codex-bin-resolver.js";
21+
import {
22+
resolveRealCodexBin as resolveRealCodexBinFromEnvironment,
23+
splitPathEntries,
24+
} from "./codex-bin-resolver.js";
2225
import { normalizeAuthAlias, shouldHandleMultiAuthAuth } from "./codex-routing.js";
2326

2427
const RETRYABLE_SHADOW_HOME_CLEANUP_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
@@ -124,11 +127,17 @@ async function autoSyncManagerActiveSelectionIfEnabled() {
124127
function resolveRealCodexBin() {
125128
const override = (process.env.CODEX_MULTI_AUTH_REAL_CODEX_BIN ?? "").trim();
126129
if (override.length > 0) {
127-
if (existsSync(override)) return override;
128-
console.error(
129-
`CODEX_MULTI_AUTH_REAL_CODEX_BIN is set but missing: ${override}`,
130-
);
131-
return null;
130+
if (!existsSync(override)) {
131+
console.error(
132+
`CODEX_MULTI_AUTH_REAL_CODEX_BIN is set but missing: ${override}`,
133+
);
134+
return null;
135+
}
136+
return resolveRealCodexBinFromEnvironment({
137+
moduleUrl: import.meta.url,
138+
env: process.env,
139+
existsSyncImpl: existsSync,
140+
});
132141
}
133142

134143
return resolveRealCodexBinFromEnvironment({ moduleUrl: import.meta.url });
@@ -145,7 +154,9 @@ function forwardToRealCodex(codexBin, args, env = process.env, cleanup) {
145154
resolve(exitCode);
146155
};
147156

148-
const child = spawn(process.execPath, [codexBin, ...args], {
157+
const command = codexBin.launchWithNode ? process.execPath : codexBin.path;
158+
const commandArgs = codexBin.launchWithNode ? [codexBin.path, ...args] : args;
159+
const child = spawn(command, commandArgs, {
149160
stdio: "inherit",
150161
env,
151162
});
@@ -993,16 +1004,6 @@ function shouldInstallWindowsBatchShimGuard() {
9931004
return override !== "0";
9941005
}
9951006

996-
function splitPathEntries(pathValue) {
997-
if (typeof pathValue !== "string" || pathValue.trim().length === 0) {
998-
return [];
999-
}
1000-
return pathValue
1001-
.split(delimiter)
1002-
.map((entry) => entry.trim())
1003-
.filter((entry) => entry.length > 0);
1004-
}
1005-
10061007
function resolveWindowsShimDirectoryFromInvocation() {
10071008
const invokedScript = (process.argv[1] ?? "").trim();
10081009
if (invokedScript.length === 0) return null;
@@ -1138,7 +1139,13 @@ function ensureWindowsShellShim(filePath, desiredContent, options = {}) {
11381139
}
11391140
const looksLikeStockOpenAiShim =
11401141
currentContent.includes("node_modules\\@openai\\codex\\bin\\codex.js") ||
1141-
currentContent.includes("node_modules/@openai/codex/bin/codex.js");
1142+
currentContent.includes("node_modules/@openai/codex/bin/codex.js") ||
1143+
currentContent.includes("@openai\\codex-win32-") ||
1144+
currentContent.includes("@openai/codex-win32-") ||
1145+
currentContent.includes("vendor\\x86_64-pc-windows-msvc\\codex\\codex.exe") ||
1146+
currentContent.includes("vendor\\aarch64-pc-windows-msvc\\codex\\codex.exe") ||
1147+
currentContent.includes("vendor/x86_64-pc-windows-msvc/codex/codex.exe") ||
1148+
currentContent.includes("vendor/aarch64-pc-windows-msvc/codex/codex.exe");
11421149
if (looksLikeStockOpenAiShim) {
11431150
try {
11441151
writeFileSync(filePath, desiredContent, { encoding: "utf8", mode: 0o755 });
@@ -1318,9 +1325,9 @@ async function main() {
13181325
if (!realCodexBin) {
13191326
console.error(
13201327
[
1321-
"Could not locate the official Codex CLI binary (@openai/codex).",
1322-
"Install it globally: npm install -g @openai/codex",
1323-
"Or set CODEX_MULTI_AUTH_REAL_CODEX_BIN to a full bin/codex.js path.",
1328+
"Could not locate the official Codex CLI.",
1329+
"Install it with npm, Homebrew, or an official native release so `codex` is on PATH.",
1330+
"Or set CODEX_MULTI_AUTH_REAL_CODEX_BIN to the full path of either codex or @openai/codex/bin/codex.js.",
13241331
].join("\n"),
13251332
);
13261333
return 1;

0 commit comments

Comments
 (0)