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 +2 -1
- package/bin/commands/doctor.js +24 -5
- package/bin/ima2.js +33 -15
- package/docs/migration/runtime-test-inventory.md +2 -1
- package/lib/codexDetect.js +72 -23
- package/lib/oauthLauncher.js +27 -7
- package/lib/packageCli.js +43 -0
- package/package.json +3 -2
- package/routes/auth.js +11 -7
- package/server.js +4 -0
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 **
|
|
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
|
|
package/bin/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
185
|
-
console.log(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
149
|
+
const hasAuth = auth.proxyReady;
|
|
136
150
|
if (!hasAuth) {
|
|
137
|
-
if (auth.
|
|
138
|
-
console.log("
|
|
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
|
-
|
|
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(
|
|
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
|
-
: "
|
|
249
|
+
: auth.probe === "error" ? "✗ codex CLI failed"
|
|
250
|
+
: "– codex CLI not found";
|
|
236
251
|
console.log(` codex login status ${probeLabel}`);
|
|
237
|
-
if (auth.
|
|
238
|
-
console.log("
|
|
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:
|
|
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`
|
package/lib/codexDetect.js
CHANGED
|
@@ -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
|
-
// -
|
|
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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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" ||
|
|
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
|
}
|
package/lib/oauthLauncher.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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": "
|
|
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
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
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({
|