ima2-gen 2.0.14 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
29
29
  - Release builds now pin Node `24.17.0`, npm `11.18.0`, and `@openai/codex` `0.144.1`; CI also performs clean npm 12 installs on Ubuntu and Windows.
30
30
  - npm 12 install-script approvals are explicit for root/UI lockfiles and one-click installers verify the installed native runtime with `ima2 doctor`.
31
31
  - GitHub Actions are pinned to full commit SHAs, Windows runs the installed-tarball smoke, and UI build dependencies are audited at high severity (`vite` 7.3.6, `esbuild` 0.28.1).
32
- - Test suite grew to **1087** cases across **206** files (72 runtime-importing, 134 contract-only).
32
+ - Test suite grew to **1094** cases across **207** files (73 runtime-importing, 134 contract-only).
33
33
 
34
34
  ### Fixed
35
35
 
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
43
43
  - Packaged `openai-oauth` now has its required `zod` peer available in consumer installs; package smoke executes both bundled CLIs from the installed tarball.
44
44
  - npm 12 keyed-object `npm pack --json` output is normalized alongside npm 11 array output, so package smoke and release packing share one cross-version artifact contract.
45
45
  - Windows npm subprocesses execute `npm-cli.js` through Node with preserved arguments, and packaged CLI smoke executes declared JS bins instead of fragile `.cmd` shell shims.
46
+ - Windows OAuth discovery now executes the package-local `@openai/codex` and bundled `openai-oauth` JavaScript entrypoints through the current Node runtime instead of relying on global PATH, `npx`, or direct `.cmd` spawning. Codex login explicitly uses file-backed credential storage required by the proxy, while keyring-only sessions receive a precise re-login diagnosis. Release publishing is gated by npm 11/12 Windows global-update smokes of the exact tarball.
46
47
 
47
48
  ## [2.0.4] - 2026-06-27
48
49
 
@@ -5,6 +5,7 @@ import { fileURLToPath } from "url";
5
5
  import { buildHardeningDoctorLines } from "../lib/doctor-checks.js";
6
6
  import { buildStorageDoctorLines } from "../lib/storage-doctor.js";
7
7
  import { detectCodexAuth } from "../../lib/codexDetect.js";
8
+ import { resolvePackageBin } from "../../lib/packageCli.js";
8
9
  import { runImageDoctorProbe } from "../../lib/responsesDoctor.js";
9
10
  import { config as runtimeConfig } from "../../config.js";
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -30,8 +31,8 @@ function loadConfig() {
30
31
  return {};
31
32
  }
32
33
  function missingRuntimeDeps() {
33
- const deps = ["express", "better-sqlite3", "openai", "openai-oauth", "progrok/package.json"];
34
- return deps.filter((dep) => {
34
+ const deps = ["express", "better-sqlite3", "openai", "openai-oauth", "progrok/package.json", "@openai/codex/package.json", "zod"];
35
+ const missing = deps.filter((dep) => {
35
36
  try {
36
37
  requireFromRoot.resolve(dep);
37
38
  return false;
@@ -39,7 +40,17 @@ function missingRuntimeDeps() {
39
40
  catch {
40
41
  return true;
41
42
  }
42
- }).map((dep) => dep === "progrok/package.json" ? "progrok" : dep);
43
+ }).map((dep) => dep.endsWith("/package.json") ? dep.slice(0, -"/package.json".length) : dep);
44
+ for (const [packageName, binName] of [["openai-oauth", "openai-oauth"], ["@openai/codex", "codex"]]) {
45
+ try {
46
+ resolvePackageBin(packageName, binName);
47
+ }
48
+ catch {
49
+ if (!missing.includes(packageName))
50
+ missing.push(packageName);
51
+ }
52
+ }
53
+ return missing;
43
54
  }
44
55
  function valueAfter(args, name) {
45
56
  const index = args.indexOf(name);
@@ -181,8 +192,16 @@ async function standardDoctor() {
181
192
  for (const line of storageLines)
182
193
  console.log(line);
183
194
  const auth = detectCodexAuth();
184
- if (auth.platform === "win32")
185
- console.log(" ℹ Windows GPT OAuth note: use WSL2 for Codex login.");
195
+ if (fileConfig.provider === "oauth" && !auth.proxyReady) {
196
+ console.log(auth.authed
197
+ ? " ✗ Codex is keyring-authenticated, but GPT OAuth needs a file-backed session; run 'ima2 login'"
198
+ : " ✗ GPT OAuth has no file-backed Codex session; run 'ima2 login'");
199
+ fail++;
200
+ }
201
+ else if (auth.proxyReady) {
202
+ console.log(" ✓ GPT OAuth file-backed Codex session is ready");
203
+ ok++;
204
+ }
186
205
  console.log(`\n ${ok} passed, ${fail} failed\n`);
187
206
  process.exit(fail > 0 ? 1 : 0);
188
207
  }
package/bin/ima2.js CHANGED
@@ -3,13 +3,14 @@ import { createInterface } from "readline/promises";
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { spawn, execSync } from "child_process";
6
+ import { spawn, execFileSync } from "child_process";
7
7
  import { confirmDestructiveAction } from "./lib/destructive-confirm.js";
8
8
  import { doctor } from "./commands/doctor.js";
9
- import { openUrl, resolveBin, killProcessTree } from "./lib/platform.js";
9
+ import { openUrl, killProcessTree } from "./lib/platform.js";
10
10
  import { maybePromptGithubStar } from "./lib/star-prompt.js";
11
11
  import { ensureFreshUiDist } from "./lib/ui-build.js";
12
- import { detectCodexAuth } from "../lib/codexDetect.js";
12
+ import { codexFileLoginArgs, detectCodexAuth } from "../lib/codexDetect.js";
13
+ import { packageCliCommand } from "../lib/packageCli.js";
13
14
  import { config as runtimeConfig } from "../config.js";
14
15
  import { errInfo } from "../lib/errInfo.js";
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -20,6 +21,16 @@ const ROOT = join(__dirname, "..");
20
21
  const CONFIG_DIR = runtimeConfig.storage.configDir;
21
22
  const CONFIG_FILE = runtimeConfig.storage.configFile;
22
23
  const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
24
+ function runSelf(args) {
25
+ execFileSync(process.execPath, [join(ROOT, "bin", "ima2.js"), ...args], { stdio: "inherit" });
26
+ }
27
+ function runCodexLogin() {
28
+ const codex = packageCliCommand("@openai/codex", "codex", codexFileLoginArgs());
29
+ execFileSync(codex.command, codex.args, { stdio: "inherit", windowsHide: true });
30
+ if (!detectCodexAuth().proxyReady) {
31
+ throw new Error("Codex login completed without a file-backed session for the GPT OAuth proxy");
32
+ }
33
+ }
23
34
  // Load package.json for version
24
35
  let pkg = { version: "?", name: "ima2-gen" };
25
36
  try {
@@ -83,7 +94,7 @@ async function setup() {
83
94
  saveConfig(config);
84
95
  console.log("\n Starting Grok OAuth login...\n");
85
96
  try {
86
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
97
+ runSelf(["grok", "login", "--manual-paste"]);
87
98
  }
88
99
  catch {
89
100
  console.log("\n Grok login failed or cancelled. You can retry with 'ima2 grok login'.\n");
@@ -101,10 +112,13 @@ async function setup() {
101
112
  console.log("\n Setting up both GPT OAuth + Grok OAuth...\n");
102
113
  // GPT OAuth
103
114
  const auth = detectCodexAuth();
104
- if (!auth.authed) {
115
+ if (!auth.proxyReady) {
116
+ if (auth.authed) {
117
+ console.log(" Codex is signed in through the OS keyring; ima2 needs a file-backed session.\n");
118
+ }
105
119
  console.log(" Running GPT OAuth login...\n");
106
120
  try {
107
- execSync(`${resolveBin("npx")} @openai/codex login`, { stdio: "inherit" });
121
+ runCodexLogin();
108
122
  }
109
123
  catch {
110
124
  console.log("\n GPT login failed. Continuing with Grok...\n");
@@ -116,7 +130,7 @@ async function setup() {
116
130
  // Grok OAuth
117
131
  console.log(" Running Grok OAuth login...\n");
118
132
  try {
119
- execSync(`node ${JSON.stringify(join(ROOT, "bin", "ima2.js"))} grok login --manual-paste`, { stdio: "inherit" });
133
+ runSelf(["grok", "login", "--manual-paste"]);
120
134
  }
121
135
  catch {
122
136
  console.log("\n Grok login failed. You can retry with 'ima2 grok login'.\n");
@@ -132,14 +146,14 @@ async function setup() {
132
146
  saveConfig(config);
133
147
  console.log("\n Starting GPT OAuth login...\n");
134
148
  const auth = detectCodexAuth();
135
- const hasAuth = auth.authed;
149
+ const hasAuth = auth.proxyReady;
136
150
  if (!hasAuth) {
137
- if (auth.platform === "win32") {
138
- console.log(" Windows note: OpenAI Codex has no documented native installer. Use WSL2 for best results.\n");
151
+ if (auth.authed) {
152
+ console.log(" Codex is signed in through the OS keyring; ima2 needs a file-backed session.\n");
139
153
  }
140
154
  console.log(" Running 'codex login' — follow the browser prompt.\n");
141
155
  try {
142
- execSync(`${resolveBin("npx")} @openai/codex login`, { stdio: "inherit" });
156
+ runCodexLogin();
143
157
  }
144
158
  catch {
145
159
  console.log("\n Login failed or cancelled. You can retry with 'ima2 serve'.\n");
@@ -187,7 +201,7 @@ async function serve(serveArgs = []) {
187
201
  env.OPENAI_API_KEY = config.apiKey;
188
202
  }
189
203
  const serverPath = join(ROOT, "server.js");
190
- const child = spawn("node", [serverPath], {
204
+ const child = spawn(process.execPath, [serverPath], {
191
205
  stdio: "inherit",
192
206
  env,
193
207
  cwd: ROOT,
@@ -232,10 +246,14 @@ async function showStatus() {
232
246
  }
233
247
  const probeLabel = auth.probe === "authed" ? "✓ authed"
234
248
  : auth.probe === "unauthed" ? "✗ not logged in"
235
- : " codex CLI not found";
249
+ : auth.probe === "error" ? "✗ codex CLI failed"
250
+ : "– codex CLI not found";
236
251
  console.log(` codex login status ${probeLabel}`);
237
- if (auth.platform === "win32") {
238
- console.log(" (Windows: no native codex installer use WSL2)");
252
+ if (auth.authed && !auth.proxyReady) {
253
+ console.log(" GPT OAuth proxy ✗ keyring-only; run 'ima2 login'");
254
+ }
255
+ else if (auth.proxyReady) {
256
+ console.log(" GPT OAuth proxy ✓ file-backed session ready");
239
257
  }
240
258
  console.log("");
241
259
  }
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
4
4
 
5
5
  _Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
6
6
 
7
- Total: 206 (runtime: 72, contract: 134)
7
+ Total: 207 (runtime: 73, contract: 134)
8
8
 
9
9
  ## Runtime-importing tests
10
10
  - `tests/agent-mode-auto-planner-contract.test.ts`
@@ -55,6 +55,7 @@ Total: 206 (runtime: 72, contract: 134)
55
55
  - `tests/node-route-refs.test.ts`
56
56
  - `tests/node-streaming-sse.test.ts`
57
57
  - `tests/node-validation-error-contract.test.ts`
58
+ - `tests/oauth-cli-dependencies.test.ts`
58
59
  - `tests/oauth-normalize.test.ts`
59
60
  - `tests/oauth-proxy-error-safety.test.ts`
60
61
  - `tests/open-directory.test.ts`
@@ -3,13 +3,23 @@
3
3
  // - OpenAI Codex stores auth under CODEX_HOME (default ~/.codex/auth.json).
4
4
  // - Legacy chatgpt-local stores auth under ~/.chatgpt-local/auth.json.
5
5
  // - Auth may live in OS keyring instead of a file (file absence ≠ unauth).
6
- // - Windows has no documented native install path; WSL is the supported path.
6
+ // - openai-oauth can only consume a file-backed Codex session.
7
7
  import { existsSync } from "node:fs";
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { errInfo } from "./errInfo.js";
12
+ import { resolvePackageBin } from "./packageCli.js";
12
13
  const HOME = homedir();
14
+ export const CODEX_FILE_AUTH_CONFIG = 'cli_auth_credentials_store="file"';
15
+ export function codexFileLoginArgs(options = {}) {
16
+ return [
17
+ "login",
18
+ ...(options.deviceAuth ? ["--device-auth"] : []),
19
+ "-c",
20
+ CODEX_FILE_AUTH_CONFIG,
21
+ ];
22
+ }
13
23
  export function codexAuthPaths() {
14
24
  const codexHome = process.env.CODEX_HOME || join(HOME, ".codex");
15
25
  return {
@@ -22,31 +32,62 @@ export function hasAuthFile() {
22
32
  const p = codexAuthPaths();
23
33
  return existsSync(p.codex) || existsSync(p.chatgpt) || existsSync(p.xdgCodex);
24
34
  }
35
+ function commandErrorText(error) {
36
+ if (!error || typeof error !== "object")
37
+ return String(error || "");
38
+ const value = error;
39
+ return [value.message, value.stdout, value.stderr]
40
+ .map((part) => Buffer.isBuffer(part) ? part.toString("utf8") : typeof part === "string" ? part : "")
41
+ .filter(Boolean)
42
+ .join("\n");
43
+ }
25
44
  // Non-invasive probe: `codex login status` returns 0 when authed (file OR keyring).
26
- // Returns: "authed" | "unauthed" | "missing" (codex binary not found)
27
- export function codexLoginStatus(timeoutMs = 2000) {
28
- const candidates = process.platform === "win32"
45
+ // "error" means the wrapper was found but failed before reporting auth state.
46
+ function probeCodex(command, args, timeoutMs, execFileSyncImpl, shell = false) {
47
+ try {
48
+ execFileSyncImpl(command, args, {
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ timeout: timeoutMs,
51
+ windowsHide: true,
52
+ shell,
53
+ });
54
+ return "authed";
55
+ }
56
+ catch (e) {
57
+ const err = errInfo(e);
58
+ if (err.code === "ENOENT")
59
+ return "missing";
60
+ if (typeof err.status === "number" && /not logged in/i.test(commandErrorText(e))) {
61
+ return "unauthed";
62
+ }
63
+ return "error";
64
+ }
65
+ }
66
+ export function codexLoginStatus(timeoutMs = 2000, options = {}) {
67
+ const platform = options.platform || process.platform;
68
+ const execPath = options.execPath || process.execPath;
69
+ const execFileSyncImpl = options.execFileSyncImpl || execFileSync;
70
+ let sawError = false;
71
+ try {
72
+ const codexBin = (options.resolveCodexBin || (() => resolvePackageBin("@openai/codex", "codex")))();
73
+ const bundled = probeCodex(execPath, [codexBin, "login", "status"], timeoutMs, execFileSyncImpl, false);
74
+ if (bundled === "authed" || bundled === "unauthed")
75
+ return bundled;
76
+ sawError ||= bundled === "error";
77
+ }
78
+ catch {
79
+ // Fall through to a user-managed Codex CLI when the package dependency is unavailable.
80
+ }
81
+ const candidates = platform === "win32"
29
82
  ? ["codex.cmd", "codex.exe", "codex"]
30
83
  : ["codex"];
31
84
  for (const bin of candidates) {
32
- try {
33
- execFileSync(bin, ["login", "status"], {
34
- stdio: "ignore",
35
- timeout: timeoutMs,
36
- windowsHide: true,
37
- });
38
- return "authed";
39
- }
40
- catch (e) {
41
- const err = errInfo(e);
42
- if (err.raw && err.code === "ENOENT")
43
- continue;
44
- // non-zero exit = binary exists but not authed
45
- if (err.raw && typeof err.status === "number")
46
- return "unauthed";
47
- }
85
+ const result = probeCodex(bin, ["login", "status"], timeoutMs, execFileSyncImpl, platform === "win32" && bin.endsWith(".cmd"));
86
+ if (result === "authed" || result === "unauthed")
87
+ return result;
88
+ sawError ||= result === "error";
48
89
  }
49
- return "missing";
90
+ return sawError ? "error" : "missing";
50
91
  }
51
92
  export function detectCodexAuth() {
52
93
  const files = codexAuthPaths();
@@ -55,14 +96,22 @@ export function detectCodexAuth() {
55
96
  chatgpt: existsSync(files.chatgpt),
56
97
  xdgCodex: existsSync(files.xdgCodex),
57
98
  };
99
+ const proxyAuthFile = fileHits.codex
100
+ ? files.codex
101
+ : fileHits.chatgpt
102
+ ? files.chatgpt
103
+ : fileHits.xdgCodex
104
+ ? files.xdgCodex
105
+ : null;
58
106
  const probe = codexLoginStatus();
59
- const authed = probe === "authed" || fileHits.codex || fileHits.chatgpt || fileHits.xdgCodex;
107
+ const authed = probe === "authed" || proxyAuthFile !== null;
60
108
  return {
61
109
  authed,
110
+ proxyReady: proxyAuthFile !== null,
111
+ proxyAuthFile,
62
112
  probe,
63
113
  files,
64
114
  fileHits,
65
115
  platform: process.platform,
66
- wslHint: process.platform === "win32",
67
116
  };
68
117
  }
@@ -1,7 +1,7 @@
1
- import { isWin } from "../bin/lib/platform.js";
2
1
  import { config } from "../config.js";
3
2
  import { parseLocalhostPortFromUrl, parseOAuthReadyUrl } from "./runtimePorts.js";
4
- import { hasAuthFile } from "./codexDetect.js";
3
+ import { detectCodexAuth } from "./codexDetect.js";
4
+ import { resolvePackageBin } from "./packageCli.js";
5
5
  import { spawn } from "node:child_process";
6
6
  export function startOAuthProxy(options = {}) {
7
7
  const oauthPort = options.oauthPort ?? config.oauth.proxyPort;
@@ -12,19 +12,39 @@ export function startOAuthProxy(options = {}) {
12
12
  let hasBeenReady = false;
13
13
  let restartCount = 0;
14
14
  const MAX_RESTARTS = 3;
15
+ const detectAuth = options.detectAuth ?? detectCodexAuth;
16
+ const execPath = options.execPath ?? process.execPath;
17
+ const resolveOAuthBin = options.resolveOAuthBin ?? (() => resolvePackageBin("openai-oauth", "openai-oauth"));
18
+ const spawnImpl = options.spawnImpl ?? spawn;
15
19
  const spawnProxy = () => {
16
20
  // Guard: don't start if no auth file exists (avoids pointless crash loops
17
21
  // and prevents openai-oauth from corrupting state on refresh failure)
18
- if (!hasAuthFile()) {
19
- console.log("[gpt-oauth] No Codex auth file found. Skipping GPT OAuth proxy.");
20
- options.onExit?.({ code: 0 });
22
+ const auth = detectAuth();
23
+ if (!auth.proxyReady || typeof auth.proxyAuthFile !== "string") {
24
+ console.log("[gpt-oauth] No file-backed Codex session found. Run `ima2 login` to enable GPT OAuth.");
25
+ options.onExit?.({ code: 0, reason: "missing-auth-file" });
21
26
  return;
22
27
  }
23
28
  console.log(`Starting GPT OAuth proxy (openai-oauth) on port ${oauthPort}...`);
24
29
  const spawnedAt = Date.now();
25
- const child = spawn("npx", ["openai-oauth", "--port", String(oauthPort)], {
30
+ let oauthBin;
31
+ try {
32
+ oauthBin = resolveOAuthBin();
33
+ }
34
+ catch (error) {
35
+ console.error(`[gpt-oauth] failed to resolve bundled proxy: ${error.message}`);
36
+ options.onExit?.({ code: 1 });
37
+ return;
38
+ }
39
+ const child = spawnImpl(execPath, [
40
+ oauthBin,
41
+ "--port",
42
+ String(oauthPort),
43
+ "--oauth-file",
44
+ auth.proxyAuthFile,
45
+ ], {
26
46
  stdio: ["ignore", "pipe", "pipe"],
27
- shell: isWin,
47
+ shell: false,
28
48
  windowsHide: true,
29
49
  env: { ...process.env },
30
50
  });
@@ -0,0 +1,43 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, resolve } from "node:path";
4
+ const requireFromPackage = createRequire(import.meta.url);
5
+ function readManifest(path) {
6
+ return JSON.parse(readFileSync(path, "utf8"));
7
+ }
8
+ function resolvePackageManifest(packageName) {
9
+ try {
10
+ return requireFromPackage.resolve(`${packageName}/package.json`);
11
+ }
12
+ catch (error) {
13
+ if (error.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED")
14
+ throw error;
15
+ }
16
+ let current = dirname(requireFromPackage.resolve(packageName));
17
+ while (true) {
18
+ const candidate = join(current, "package.json");
19
+ if (existsSync(candidate) && readManifest(candidate).name === packageName)
20
+ return candidate;
21
+ const parent = dirname(current);
22
+ if (parent === current)
23
+ throw new Error(`Could not locate ${packageName}/package.json`);
24
+ current = parent;
25
+ }
26
+ }
27
+ export function resolvePackageBin(packageName, binName) {
28
+ const manifestPath = resolvePackageManifest(packageName);
29
+ const manifest = readManifest(manifestPath);
30
+ const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binName];
31
+ if (!entry)
32
+ throw new Error(`${packageName} does not declare the ${binName} CLI`);
33
+ const binPath = resolve(dirname(manifestPath), entry);
34
+ if (!existsSync(binPath))
35
+ throw new Error(`${packageName} CLI entry is missing: ${binPath}`);
36
+ return binPath;
37
+ }
38
+ export function packageCliCommand(packageName, binName, args = [], options = {}) {
39
+ return {
40
+ command: options.execPath || process.execPath,
41
+ args: [resolvePackageBin(packageName, binName), ...args],
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "packageManager": "npm@11.18.0",
5
5
  "description": "Local OAuth image generation studio with classic and node workflows",
6
6
  "type": "module",
@@ -20,6 +20,7 @@
20
20
  "test:install-policy": "node scripts/check-install-policy.mjs",
21
21
  "test:install-policy:npm12": "node scripts/check-install-policy.mjs --npm-pending",
22
22
  "test:package-install": "node --test tests/package-install-smoke.mjs",
23
+ "test:package-global-update": "node scripts/package-global-update-smoke.mjs",
23
24
  "test:inventory": "node scripts/classify-tests.mjs --check --fail-js-runtime",
24
25
  "setup": "node bin/ima2.js setup",
25
26
  "prepack": "npm run ui:build && npm run build:server && npm run build:cli",
@@ -104,5 +105,5 @@
104
105
  "tsx": "^4.21.0",
105
106
  "typescript": "^5.9.3"
106
107
  },
107
- "gitHead": "bea7ae5b5ea0a6e9039732794d2bdeb939e429b3"
108
+ "gitHead": "ab86116ee1d23040e7ab63e49d8a6d4e19eddabd"
108
109
  }
package/routes/auth.js CHANGED
@@ -2,15 +2,13 @@ import { spawn } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { writeFileSync, renameSync, mkdirSync, existsSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
- import { join, dirname } from "node:path";
6
- import { fileURLToPath } from "node:url";
5
+ import { join } from "node:path";
6
+ import { codexFileLoginArgs, detectCodexAuth } from "../lib/codexDetect.js";
7
+ import { packageCliCommand } from "../lib/packageCli.js";
7
8
  const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
8
9
  const GROK_SCOPE = "openid profile email offline_access grok-cli:access api:access";
9
10
  const GROK_TOKEN_URL = "https://auth.x.ai/oauth2/token";
10
11
  const CODEX_DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
11
- // Bundled @openai/codex binary (npm dependency), resolved relative to this
12
- // module so device-auth login works even when `codex` is not on the user's PATH.
13
- const CODEX_BIN = join(dirname(fileURLToPath(import.meta.url)), "..", "node_modules", ".bin", process.platform === "win32" ? "codex.cmd" : "codex");
14
12
  const MAX_CONCURRENT_SESSIONS = 20;
15
13
  const sessions = new Map();
16
14
  function sid() {
@@ -129,9 +127,12 @@ function startCodexDeviceCode() {
129
127
  for (const k of ["OPENAI_API_KEY", "XAI_API_KEY", "GEMINI_API_KEY", "ANTHROPIC_API_KEY", "VERTEX_SERVICE_ACCOUNT_JSON"]) {
130
128
  delete childEnv[k];
131
129
  }
132
- const child = spawn(CODEX_BIN, ["login", "--device-auth"], {
130
+ const codex = packageCliCommand("@openai/codex", "codex", codexFileLoginArgs({ deviceAuth: true }));
131
+ const child = spawn(codex.command, codex.args, {
133
132
  stdio: ["ignore", "pipe", "pipe"],
134
133
  env: childEnv,
134
+ shell: false,
135
+ windowsHide: true,
135
136
  });
136
137
  let stdout = "";
137
138
  let resolved = false;
@@ -180,9 +181,12 @@ function startCodexDeviceCode() {
180
181
  reject(new Error(`codex login exited with code ${code} before providing device code`));
181
182
  return;
182
183
  }
183
- session.status = code === 0 ? "complete" : "error";
184
+ const proxyReady = code === 0 && detectCodexAuth().proxyReady;
185
+ session.status = proxyReady ? "complete" : "error";
184
186
  if (code !== 0)
185
187
  session.error = `codex exited with code ${code}`;
188
+ else if (!proxyReady)
189
+ session.error = "Codex login did not create a file-backed GPT OAuth session";
186
190
  cleanup(id);
187
191
  });
188
192
  child.on("error", (err) => {
package/server.js CHANGED
@@ -181,6 +181,10 @@ function runtimeHostUrl(host) {
181
181
  return host;
182
182
  }
183
183
  function advertise(ctx) {
184
+ // Proxy readiness can arrive before the backend has bound. Publishing that
185
+ // intermediate state makes consumers treat the configured port as live.
186
+ if (!ctx.serverActualPort)
187
+ return;
184
188
  try {
185
189
  mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true });
186
190
  writeFileSync(ctx.config.storage.advertiseFile, JSON.stringify({