@tokensrc/codex 1.14.7 → 1.14.8

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/README.md CHANGED
@@ -127,8 +127,7 @@ const codex = await createCodexModule({
127
127
  },
128
128
  persistence: { configDirectory: applicationConfigDirectory },
129
129
  desktopApp: {
130
- inspect: inspectHostDesktopApp,
131
- prepare: prepareHostDesktopApp
130
+ inspect: inspectHostDesktopApp
132
131
  }
133
132
  });
134
133
 
@@ -138,9 +137,9 @@ const remote = await codex.status.loadRemote();
138
137
  ```
139
138
 
140
139
  The optional `desktopApp.inspect` hook adds host-provided desktop version details
141
- to local and `status` output. `desktopApp.prepare` runs immediately before the
142
- official `codex app` command, allowing an embedding host to validate or deploy an
143
- approved desktop build without coupling package-generic code to one installer.
140
+ to local and `status` output. The package never downloads, installs, replaces,
141
+ upgrades, or downgrades a desktop application; `codex app` only prepares the
142
+ selected official credentials and launches the existing official application.
144
143
 
145
144
  Hosts may instead inject a static access token or standalone OIDC settings, and
146
145
  can customize branding and launcher behavior. Injected access tokens and provider
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // NUMA_CODEX_SHORTCUT_BOOTSTRAP
5
+ // This file is copied beside managed shortcuts. Keep it dependency-free.
6
+
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const { spawnSync } = require("node:child_process");
10
+
11
+ const STATE_SCHEMA_VERSION = 1;
12
+ const SUCCESS_CHECK_INTERVAL_MS = 0;
13
+ const FAILURE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
14
+ const METADATA_TIMEOUT_MS = 8_000;
15
+ const INSTALL_TIMEOUT_MS = 120_000;
16
+ const LOCK_STALE_MS = 5 * 60 * 1000;
17
+
18
+ function fail(message) {
19
+ process.stderr.write(`Numa shortcut: ${message}\n`);
20
+ process.exitCode = 1;
21
+ }
22
+
23
+ function warn(message) {
24
+ process.stderr.write(`Numa shortcut update skipped: ${message}\n`);
25
+ }
26
+
27
+ function plainObject(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value);
29
+ }
30
+
31
+ function absoluteFile(value) {
32
+ return typeof value === "string" && value.length > 0 && path.isAbsolute(value);
33
+ }
34
+
35
+ function safeRegistry(value) {
36
+ try {
37
+ const parsed = new URL(value);
38
+ if (parsed.username || parsed.password || parsed.hash || parsed.search) return false;
39
+ if (parsed.protocol === "https:") return true;
40
+ return parsed.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function safePackageName(value) {
47
+ return typeof value === "string"
48
+ && /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(value);
49
+ }
50
+
51
+ function safeCommandName(value) {
52
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(value);
53
+ }
54
+
55
+ function parseVersion(value) {
56
+ if (typeof value !== "string") return undefined;
57
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
58
+ if (!match) return undefined;
59
+ return {
60
+ raw: value,
61
+ major: Number(match[1]),
62
+ minor: Number(match[2]),
63
+ patch: Number(match[3]),
64
+ prerelease: match[4] ? match[4].split(".") : []
65
+ };
66
+ }
67
+
68
+ function comparePrerelease(left, right) {
69
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
70
+ const length = Math.max(left.length, right.length);
71
+ for (let index = 0; index < length; index += 1) {
72
+ if (left[index] === undefined) return -1;
73
+ if (right[index] === undefined) return 1;
74
+ const leftNumber = /^\d+$/u.test(left[index]) ? Number(left[index]) : undefined;
75
+ const rightNumber = /^\d+$/u.test(right[index]) ? Number(right[index]) : undefined;
76
+ if (leftNumber !== undefined && rightNumber !== undefined && leftNumber !== rightNumber) {
77
+ return leftNumber < rightNumber ? -1 : 1;
78
+ }
79
+ if (leftNumber !== undefined && rightNumber === undefined) return -1;
80
+ if (leftNumber === undefined && rightNumber !== undefined) return 1;
81
+ if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ function compareVersions(left, right) {
87
+ for (const field of ["major", "minor", "patch"]) {
88
+ if (left[field] !== right[field]) return left[field] < right[field] ? -1 : 1;
89
+ }
90
+ return comparePrerelease(left.prerelease, right.prerelease);
91
+ }
92
+
93
+ function readJson(file) {
94
+ try {
95
+ return JSON.parse(fs.readFileSync(file, "utf8"));
96
+ } catch {
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ function writeJsonAtomic(file, value) {
102
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
103
+ const temporary = `${file}.${process.pid}.tmp`;
104
+ try {
105
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
106
+ if (process.platform === "win32") fs.rmSync(file, { force: true });
107
+ fs.renameSync(temporary, file);
108
+ } finally {
109
+ fs.rmSync(temporary, { force: true });
110
+ }
111
+ }
112
+
113
+ function loadConfig(file) {
114
+ const value = readJson(file);
115
+ if (!plainObject(value)
116
+ || value.schemaVersion !== 1
117
+ || typeof value.marker !== "string"
118
+ || !safePackageName(value.packageName)
119
+ || !safeCommandName(value.commandName)
120
+ || !safeRegistry(value.registryUrl)
121
+ || !parseVersion(value.installedVersion)
122
+ || !absoluteFile(value.nodeExecutable)
123
+ || (value.npmExecutable !== null && !absoluteFile(value.npmExecutable))
124
+ || (value.fallbackEntrypoint !== null && !absoluteFile(value.fallbackEntrypoint))
125
+ || !absoluteFile(value.cacheDirectory)) {
126
+ throw new Error(`invalid managed launcher configuration: ${file}`);
127
+ }
128
+ if (!fs.existsSync(value.nodeExecutable)) {
129
+ throw new Error("trusted Node.js runtime captured during shortcut installation is unavailable; reinstall the shortcut");
130
+ }
131
+ const configuredNode = fs.realpathSync(value.nodeExecutable);
132
+ const launchedNode = fs.realpathSync(process.execPath);
133
+ if (launchedNode !== configuredNode) {
134
+ throw new Error("managed shortcut was invoked by an unexpected Node.js runtime; reinstall the shortcut from a trusted terminal");
135
+ }
136
+ value.nodeExecutable = configuredNode;
137
+ value.npmExecutable = verifiedNpmCli(value.npmExecutable);
138
+ return value;
139
+ }
140
+
141
+ function verifiedNpmCli(file) {
142
+ if (!file || !fs.existsSync(file)) return null;
143
+ let actual;
144
+ try {
145
+ actual = fs.realpathSync(file);
146
+ } catch {
147
+ return null;
148
+ }
149
+ if (path.basename(actual).toLowerCase() !== "npm-cli.js") return null;
150
+ let current = path.dirname(actual);
151
+ const filesystemRoot = path.parse(current).root;
152
+ while (current !== filesystemRoot) {
153
+ const manifest = readJson(path.join(current, "package.json"));
154
+ if (manifest !== undefined) {
155
+ if (!plainObject(manifest) || manifest.name !== "npm") return null;
156
+ const declared = typeof manifest.bin === "string"
157
+ ? manifest.bin
158
+ : plainObject(manifest.bin) && typeof manifest.bin.npm === "string"
159
+ ? manifest.bin.npm
160
+ : undefined;
161
+ if (!declared || path.isAbsolute(declared)) return null;
162
+ try {
163
+ return fs.realpathSync(path.resolve(current, declared)) === actual ? actual : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ current = path.dirname(current);
169
+ }
170
+ return null;
171
+ }
172
+
173
+ function packageRoot(cacheDirectory, packageName, version) {
174
+ return path.join(cacheDirectory, "versions", version, "node_modules", ...packageName.split("/"));
175
+ }
176
+
177
+ function inside(parent, child) {
178
+ const relative = path.relative(parent, child);
179
+ return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
180
+ }
181
+
182
+ function packageEntrypoint(root, packageName, version, commandName) {
183
+ const manifest = readJson(path.join(root, "package.json"));
184
+ if (!plainObject(manifest) || manifest.name !== packageName || manifest.version !== version) return undefined;
185
+ const declared = typeof manifest.bin === "string"
186
+ ? manifest.bin
187
+ : plainObject(manifest.bin) && typeof manifest.bin[commandName] === "string"
188
+ ? manifest.bin[commandName]
189
+ : undefined;
190
+ if (!declared || path.isAbsolute(declared)) return undefined;
191
+ const entrypoint = path.resolve(root, declared);
192
+ if (!inside(root, entrypoint) || !fs.existsSync(entrypoint)) return undefined;
193
+ try {
194
+ const realRoot = fs.realpathSync(root);
195
+ const realEntrypoint = fs.realpathSync(entrypoint);
196
+ return inside(realRoot, realEntrypoint) ? realEntrypoint : undefined;
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
202
+ function cachedEntrypoint(config, version) {
203
+ return packageEntrypoint(
204
+ packageRoot(config.cacheDirectory, config.packageName, version),
205
+ config.packageName,
206
+ version,
207
+ config.commandName
208
+ );
209
+ }
210
+
211
+ function fallbackEntrypoint(config) {
212
+ if (!config.fallbackEntrypoint || !fs.existsSync(config.fallbackEntrypoint)) return undefined;
213
+ let current = path.dirname(config.fallbackEntrypoint);
214
+ const filesystemRoot = path.parse(current).root;
215
+ while (current !== filesystemRoot) {
216
+ const manifest = readJson(path.join(current, "package.json"));
217
+ if (plainObject(manifest) && manifest.name === config.packageName && manifest.version === config.installedVersion) {
218
+ const declared = typeof manifest.bin === "string"
219
+ ? manifest.bin
220
+ : plainObject(manifest.bin) && typeof manifest.bin[config.commandName] === "string"
221
+ ? manifest.bin[config.commandName]
222
+ : undefined;
223
+ if (!declared) return undefined;
224
+ try {
225
+ return fs.realpathSync(path.resolve(current, declared)) === fs.realpathSync(config.fallbackEntrypoint)
226
+ ? fs.realpathSync(config.fallbackEntrypoint)
227
+ : undefined;
228
+ } catch {
229
+ return undefined;
230
+ }
231
+ }
232
+ current = path.dirname(current);
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ function npmInvocation(config, args, timeout) {
238
+ if (!config.npmExecutable || !fs.existsSync(config.npmExecutable)) {
239
+ return { status: null, error: new Error("npm executable captured during shortcut installation is unavailable") };
240
+ }
241
+ const javascriptCli = /\.(?:c?js|mjs)$/iu.test(config.npmExecutable);
242
+ const command = javascriptCli ? config.nodeExecutable : config.npmExecutable;
243
+ const commandArgs = javascriptCli ? [config.npmExecutable, ...args] : args;
244
+ return spawnSync(command, commandArgs, {
245
+ encoding: "utf8",
246
+ stdio: ["ignore", "pipe", "pipe"],
247
+ timeout,
248
+ windowsHide: true,
249
+ env: {
250
+ ...process.env,
251
+ PATH: `${path.dirname(config.nodeExecutable)}${path.delimiter}${process.env.PATH ?? ""}`,
252
+ npm_config_audit: "false",
253
+ npm_config_fund: "false",
254
+ npm_config_update_notifier: "false"
255
+ }
256
+ });
257
+ }
258
+
259
+ function latestVersion(config) {
260
+ const result = npmInvocation(config, [
261
+ "view",
262
+ `${config.packageName}@latest`,
263
+ "version",
264
+ "--json",
265
+ `--registry=${config.registryUrl}`
266
+ ], METADATA_TIMEOUT_MS);
267
+ if (result.error || result.status !== 0) {
268
+ throw result.error ?? new Error((result.stderr || "npm view failed").trim());
269
+ }
270
+ const value = JSON.parse(result.stdout);
271
+ const parsed = parseVersion(value);
272
+ if (!parsed) throw new Error("registry latest tag did not resolve to an exact semantic version");
273
+ return parsed;
274
+ }
275
+
276
+ function installExact(config, version) {
277
+ const finalDirectory = path.join(config.cacheDirectory, "versions", version);
278
+ const existing = cachedEntrypoint(config, version);
279
+ if (existing) return existing;
280
+ const staging = path.join(config.cacheDirectory, "staging", `${version}-${process.pid}-${Date.now()}`);
281
+ fs.mkdirSync(staging, { recursive: true, mode: 0o700 });
282
+ try {
283
+ const result = npmInvocation(config, [
284
+ "install",
285
+ "--ignore-scripts",
286
+ "--no-audit",
287
+ "--no-fund",
288
+ "--omit=dev",
289
+ "--no-package-lock",
290
+ "--no-save",
291
+ `--prefix=${staging}`,
292
+ `--registry=${config.registryUrl}`,
293
+ `${config.packageName}@${version}`
294
+ ], INSTALL_TIMEOUT_MS);
295
+ if (result.error || result.status !== 0) {
296
+ throw result.error ?? new Error((result.stderr || "npm install failed").trim());
297
+ }
298
+ const stagedRoot = path.join(staging, "node_modules", ...config.packageName.split("/"));
299
+ if (!packageEntrypoint(stagedRoot, config.packageName, version, config.commandName)) {
300
+ throw new Error("downloaded package identity, version, or executable does not match the requested release");
301
+ }
302
+ fs.mkdirSync(path.dirname(finalDirectory), { recursive: true, mode: 0o700 });
303
+ if (fs.existsSync(finalDirectory)) fs.rmSync(finalDirectory, { recursive: true, force: true });
304
+ fs.renameSync(staging, finalDirectory);
305
+ const verified = cachedEntrypoint(config, version);
306
+ if (!verified) throw new Error("installed release failed exact readback verification");
307
+ return verified;
308
+ } finally {
309
+ fs.rmSync(staging, { recursive: true, force: true });
310
+ }
311
+ }
312
+
313
+ function readState(config) {
314
+ const value = readJson(path.join(config.cacheDirectory, "state.json"));
315
+ if (!plainObject(value)
316
+ || value.schemaVersion !== STATE_SCHEMA_VERSION
317
+ || value.packageName !== config.packageName
318
+ || value.registryUrl !== config.registryUrl
319
+ || (value.knownGoodVersion !== null && !parseVersion(value.knownGoodVersion))
320
+ || !Number.isSafeInteger(value.nextCheckAt)
321
+ || value.nextCheckAt < 0) {
322
+ return {
323
+ schemaVersion: STATE_SCHEMA_VERSION,
324
+ packageName: config.packageName,
325
+ registryUrl: config.registryUrl,
326
+ knownGoodVersion: null,
327
+ nextCheckAt: 0
328
+ };
329
+ }
330
+ return value;
331
+ }
332
+
333
+ function writeState(config, state) {
334
+ writeJsonAtomic(path.join(config.cacheDirectory, "state.json"), state);
335
+ }
336
+
337
+ function acquireLock(config) {
338
+ const lock = path.join(config.cacheDirectory, "update.lock");
339
+ fs.mkdirSync(config.cacheDirectory, { recursive: true, mode: 0o700 });
340
+ try {
341
+ const descriptor = fs.openSync(lock, "wx", 0o600);
342
+ fs.writeFileSync(descriptor, `${process.pid}\n`);
343
+ return () => {
344
+ try { fs.closeSync(descriptor); } catch { /* already closed */ }
345
+ fs.rmSync(lock, { force: true });
346
+ };
347
+ } catch (error) {
348
+ if (error && error.code === "EEXIST") {
349
+ try {
350
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
351
+ fs.rmSync(lock, { force: true });
352
+ return acquireLock(config);
353
+ }
354
+ } catch { /* another launcher owns or removed the lock */ }
355
+ return undefined;
356
+ }
357
+ throw error;
358
+ }
359
+ }
360
+
361
+ function refreshKnownGood(config, state) {
362
+ const release = acquireLock(config);
363
+ if (!release) return state;
364
+ try {
365
+ const current = readState(config);
366
+ if (Date.now() < current.nextCheckAt) return current;
367
+ const installed = parseVersion(config.installedVersion);
368
+ const known = current.knownGoodVersion ? parseVersion(current.knownGoodVersion) : undefined;
369
+ const floor = known && compareVersions(known, installed) > 0 ? known : installed;
370
+ try {
371
+ const latest = latestVersion(config);
372
+ if (compareVersions(latest, floor) < 0) {
373
+ warn(`registry latest ${latest.raw} is older than trusted ${floor.raw}; keeping the trusted release`);
374
+ const next = { ...current, nextCheckAt: Date.now() + SUCCESS_CHECK_INTERVAL_MS };
375
+ writeState(config, next);
376
+ return next;
377
+ }
378
+ installExact(config, latest.raw);
379
+ const next = {
380
+ ...current,
381
+ knownGoodVersion: latest.raw,
382
+ nextCheckAt: Date.now() + SUCCESS_CHECK_INTERVAL_MS
383
+ };
384
+ writeState(config, next);
385
+ return next;
386
+ } catch (error) {
387
+ warn(`${error instanceof Error ? error.message : String(error)}; using the last known-good local release`);
388
+ const next = { ...current, nextCheckAt: Date.now() + FAILURE_CHECK_INTERVAL_MS };
389
+ writeState(config, next);
390
+ return next;
391
+ }
392
+ } finally {
393
+ release();
394
+ }
395
+ }
396
+
397
+ function run(config, args) {
398
+ const state = refreshKnownGood(config, readState(config));
399
+ const installed = parseVersion(config.installedVersion);
400
+ const known = state.knownGoodVersion ? parseVersion(state.knownGoodVersion) : undefined;
401
+ const entrypoint = known && compareVersions(known, installed) >= 0
402
+ ? cachedEntrypoint(config, state.knownGoodVersion)
403
+ : undefined;
404
+ const fallbackAllowed = !known || compareVersions(known, installed) <= 0;
405
+ const selected = entrypoint ?? (fallbackAllowed ? fallbackEntrypoint(config) : undefined);
406
+ if (!selected) {
407
+ fail(known && compareVersions(known, installed) > 0
408
+ ? `trusted Numa ${known.raw} is unavailable or corrupt; refusing to downgrade to ${installed.raw}. Restore network access or reinstall the shortcut from a trusted terminal.`
409
+ : "no verified local Numa release is available. Re-run `npx -y --prefer-online @numa-tech/numa@latest codex shortcut install` from a terminal with Node and npm.");
410
+ return;
411
+ }
412
+ const child = spawnSync(config.nodeExecutable, [selected, ...args], {
413
+ stdio: "inherit",
414
+ windowsHide: false,
415
+ env: {
416
+ ...process.env,
417
+ PATH: `${path.dirname(config.nodeExecutable)}${path.delimiter}${process.env.PATH ?? ""}`
418
+ }
419
+ });
420
+ if (child.error) {
421
+ fail(`failed to start verified Numa release: ${child.error.message}`);
422
+ return;
423
+ }
424
+ process.exitCode = child.status === null ? 1 : child.status;
425
+ }
426
+
427
+ function main(argv = process.argv.slice(2)) {
428
+ const nodeVersion = /^(\d+)\.(\d+)\./u.exec(process.versions.node);
429
+ if (!nodeVersion || Number(nodeVersion[1]) < 20
430
+ || (Number(nodeVersion[1]) === 20 && Number(nodeVersion[2]) < 12)) {
431
+ fail(`Node.js 20.12 or newer is required; detected ${process.versions.node}`);
432
+ return;
433
+ }
434
+ if (argv.length < 1 || !absoluteFile(argv[0])) {
435
+ fail("managed launcher configuration path is missing or not absolute");
436
+ return;
437
+ }
438
+ try {
439
+ run(loadConfig(argv[0]), argv.slice(1));
440
+ } catch (error) {
441
+ fail(error instanceof Error ? error.message : String(error));
442
+ }
443
+ }
444
+
445
+ if (require.main === module) main();
446
+
447
+ module.exports = {
448
+ compareVersions,
449
+ loadConfig,
450
+ main,
451
+ parseVersion,
452
+ safeRegistry
453
+ };
@@ -6,8 +6,8 @@ const definitions = [
6
6
  { name: "upload", description: "Upload the legacy v1 local account pool", command: "codex upload [-u|--server <url>]", summary: "仅用于 v1 兼容账号批量导入;新账号统一运行 numa codex login 进入 v4 RT 池", access: "authenticated", examples: ["numa codex upload --storage ~/.codex/multi-auth/openai-codex-accounts.json"] },
7
7
  { name: "use", description: "Select an account and install official Codex credentials", command: "codex use [account]", summary: "选择服务端账号并写入官方 ChatGPT auth.json", access: "authenticated", examples: ["numa codex use acct_xxx"] },
8
8
  { name: "run", description: "Launch the official Codex CLI", command: "codex run [-s|--select-account|-a|--account <id>] [-u|--server <url>] [--] [codexArgs...]", summary: "选择或复用 ChatGPT 账号后启动官方 Codex;模型流量不经过宿主", access: "authenticated", examples: ["numa codex run -s", "numa codex run --account acct_xxx", "numa codex run -- exec \"完成长任务\""] },
9
- { name: "app", description: "Launch the official Codex desktop app", command: "codex app [workspace] [-s|--select-account|-a|--account <id>] [-u|--server <url>]", summary: "检查批准版本并使用官方 ChatGPT 登录启动 macOS/Windows Codex 桌面 App", access: "authenticated", examples: ["numa codex app . -s", "numa codex app . --account acct_xxx"] },
10
- { name: "shortcut", description: "Install or remove launcher shortcuts", command: "codex shortcut [install|remove] [-t|--target run|app|both] [-s|--select-account] [--no-cli-wrapper]", summary: "安装或删除 macOS/Windows 的自动更新 Numa launcher、Codex 快捷命令与桌面入口", access: "public", examples: ["numa codex shortcut install", "numa codex shortcut install --target app --workspace .", "numa codex shortcut remove"] },
9
+ { name: "app", description: "Launch the official Codex desktop app", command: "codex app [workspace] [-s|--select-account|-a|--account <id>] [-u|--server <url>]", summary: "准备官方 ChatGPT 登录并启动现有 macOS/Windows Codex 桌面 App;不安装或改动App版本", access: "authenticated", examples: ["numa codex app . -s", "numa codex app . --account acct_xxx"] },
10
+ { name: "shortcut", description: "Install or remove launcher shortcuts", command: "codex shortcut [install|remove] [-t|--target run|app|both] [-s|--select-account] [--no-cli-wrapper]", summary: "安装或删除macOS/Windows/Linux的原子自更新Numa launcher、Codex快捷命令与桌面入口", access: "public", examples: ["numa codex shortcut install", "numa codex shortcut install --target app --workspace .", "numa codex shortcut remove"] },
11
11
  { name: "auth-agent", description: "Continuously renew official Codex credentials", command: "codex auth-agent [account]", summary: "前台刷新短效 token 到官方 Codex 登录并输出完整状态日志", access: "authenticated", examples: ["numa codex auth-agent acct_xxx --replace-login", "numa codex auth-agent --replace-login --once"] },
12
12
  { name: "admin", description: "Manage server-side Codex accounts and assignments", command: "codex admin <accounts|assignments>", summary: "管理员查看账号与凭据版本、触发刷新,并管理 Keycloak 用户账号绑定", access: "authenticated", examples: ["numa codex admin accounts list", "numa codex admin accounts credential-versions acct_xxx", "numa codex admin accounts refresh acct_xxx", "numa codex admin assignments list", "numa codex admin assignments bind acct_xxx user-subject --default", "numa codex admin assignments unbind assignment_xxx"] },
13
13
  { name: "status", description: "Show selection, desktop app, and official credential status", command: "codex status", summary: "查看当前账号选择、ChatGPT Desktop 版本与官方 auth.json 状态", access: "public", examples: ["numa codex status --json"] },
@@ -1 +1 @@
1
- {"version":3,"file":"command-directory.js","sourceRoot":"","sources":["../src/command-directory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"command-directory.js","sourceRoot":"","sources":["../src/command-directory.ts"],"names":[],"mappings":"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"}
package/dist/commands.js CHANGED
@@ -12,7 +12,7 @@ import { cleanupLegacyCodexProvider } from "./legacy-cleanup.js";
12
12
  import { executeCodexEnrollmentLogin } from "./enrollment-flow.js";
13
13
  import { executeCodexMigrationLoginBatch } from "./migration-login-batch.js";
14
14
  import { executeCodexShortLivedCredentialDelivery } from "./short-lived-delivery-flow.js";
15
- import { executeCodexTokenPoolV4Delivery, reportManagedCodexV3Credentials } from "./managed-credential-v3-flow.js";
15
+ import { executeCodexTokenPoolV4Delivery, reconcilePendingManagedCodexV3Operation, reportManagedCodexV3Credentials } from "./managed-credential-v3-flow.js";
16
16
  import { resolveManagedCodexV3AssignmentTarget } from "./managed-credential-v3-assignment.js";
17
17
  import { executeCodexTokenPoolV4Login } from "./token-pool-v4-upload-flow.js";
18
18
  import { readManagedCodexV3State } from "./managed-credential-v3.js";
@@ -168,7 +168,7 @@ async function loadAccountData(deps, options = {}) {
168
168
  selectedAccountId: previous?.account.id ?? response.defaultAccountId ?? undefined
169
169
  };
170
170
  }
171
- async function chooseAccount(deps, requested, options = {}) {
171
+ async function chooseAccount(deps, requested, options = {}, persistSelection = true) {
172
172
  const data = await loadAccountData(deps, options);
173
173
  const { response, selectedAccountId } = data;
174
174
  const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
@@ -193,7 +193,7 @@ async function chooseAccount(deps, requested, options = {}) {
193
193
  ? previous.account.providerAccountId
194
194
  : undefined;
195
195
  const v3Assignment = data.v3Assignments?.items.find((item) => item.managedAccountId === account.id && item.assignmentId === account.accountId);
196
- const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
196
+ const selectionInput = {
197
197
  serviceUrl: options.server ?? deps.config.connection.serverUrl,
198
198
  allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
199
199
  account: v3Assignment ? {
@@ -220,9 +220,24 @@ async function chooseAccount(deps, requested, options = {}) {
220
220
  label: accountLabel,
221
221
  distributionMode: "SCHEMA_V1"
222
222
  }
223
- });
223
+ };
224
+ // V4 callers stage the selection in memory and persist it only after the
225
+ // official auth.json has been installed and the delivery has been ACKed.
226
+ // This keeps a failed account switch from pointing selection at credentials
227
+ // that were never installed.
228
+ const selection = persistSelection
229
+ ? await saveCodexSelection(deps.config.persistence.configDirectory, selectionInput)
230
+ : { schemaVersion: 1, ...selectionInput, updatedAt: new Date().toISOString() };
224
231
  return { account, selection };
225
232
  }
233
+ async function persistInstalledSelection(deps, installed) {
234
+ const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
235
+ serviceUrl: installed.selection.serviceUrl,
236
+ allowInsecurePoc: installed.selection.allowInsecurePoc,
237
+ account: installed.selection.account
238
+ });
239
+ return { ...installed, selection };
240
+ }
226
241
  async function installCredentials(deps, selection, service) {
227
242
  const client = deps.clientFor(service);
228
243
  await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
@@ -232,6 +247,13 @@ async function installCredentials(deps, selection, service) {
232
247
  || !selection.account.providerAccountId) {
233
248
  throw new CodexServiceError("The selected account is not a v4 token-pool assignment.", "codex_assignment_not_found", 404);
234
249
  }
250
+ await reconcilePendingManagedCodexV3Operation({
251
+ client,
252
+ tenantId: deps.config.connection.tenantId,
253
+ env: deps.env
254
+ });
255
+ // Recovery may ACK or renew a previous delivery and therefore advance the
256
+ // Server assignment tuple. Always fetch a fresh snapshot afterwards.
235
257
  const assignments = await client.listV3CredentialAssignments();
236
258
  const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
237
259
  && item.managedAccountId === selection.account.managedAccountId
@@ -301,10 +323,38 @@ async function installCredentials(deps, selection, service) {
301
323
  }
302
324
  async function selectedCredentials(deps, requested, options = {}) {
303
325
  if (accountTokenPoolV4Enabled(deps.config)) {
326
+ const client = deps.clientFor(serviceConnection(options));
327
+ const recovered = await reconcilePendingManagedCodexV3Operation({
328
+ client,
329
+ tenantId: deps.config.connection.tenantId,
330
+ env: deps.env
331
+ });
332
+ if (!requested && recovered) {
333
+ const assignments = await client.listV3CredentialAssignments();
334
+ const assignment = findV3Assignment({ assignments, ...recovered });
335
+ if (!assignment || assignment.currentGenerationVersion !== recovered.generationVersion
336
+ || assignment.authorityMode !== "CLIENT_MANAGED") {
337
+ throw new CodexServiceError("The recovered local v4 lineage no longer matches the server assignment.", "lineage_version_conflict", 409);
338
+ }
339
+ const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
340
+ serviceUrl: options.server ?? deps.config.connection.serverUrl,
341
+ allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
342
+ account: {
343
+ id: recovered.managedAccountId,
344
+ accountId: recovered.providerAccountId,
345
+ label: assignment.label,
346
+ distributionMode: "CLIENT_MANAGED_V4",
347
+ assignmentId: recovered.assignmentId,
348
+ managedAccountId: recovered.managedAccountId,
349
+ providerAccountId: recovered.providerAccountId
350
+ }
351
+ });
352
+ return { selection, expiresAt: Date.parse(recovered.credentialExpiresAt) };
353
+ }
304
354
  const state = await readManagedCodexV3State(deps.env);
305
355
  if (!state || state.phase !== "INSTALLED" || state.authorityMode !== "CLIENT_MANAGED") {
306
- const selection = (await chooseAccount(deps, requested, options)).selection;
307
- return installCredentials(deps, selection, serviceConnection(options));
356
+ const selection = (await chooseAccount(deps, requested, options, false)).selection;
357
+ return persistInstalledSelection(deps, await installCredentials(deps, selection, serviceConnection(options)));
308
358
  }
309
359
  if (requested && ![
310
360
  state.assignmentId,
@@ -312,10 +362,10 @@ async function selectedCredentials(deps, requested, options = {}) {
312
362
  state.providerAccountId,
313
363
  state.lineageId
314
364
  ].includes(requested)) {
315
- const selection = (await chooseAccount(deps, requested, options)).selection;
316
- return installCredentials(deps, selection, serviceConnection(options));
365
+ const selection = (await chooseAccount(deps, requested, options, false)).selection;
366
+ return persistInstalledSelection(deps, await installCredentials(deps, selection, serviceConnection(options)));
317
367
  }
318
- const assignments = await deps.clientFor(serviceConnection(options)).listV3CredentialAssignments();
368
+ const assignments = await client.listV3CredentialAssignments();
319
369
  const assignment = findV3Assignment({ assignments, ...state });
320
370
  if (!assignment || assignment.assignmentId !== state.assignmentId
321
371
  || assignment.managedAccountId !== state.managedAccountId
@@ -524,8 +574,11 @@ export function registerCodexCommands(program, deps, options = {}) {
524
574
  });
525
575
  serviceOptions(codex.command("use [account]").description("Select an account and update the official Codex auth.json"))
526
576
  .action(async (account, opts, command) => {
527
- const result = await chooseAccount(deps, account, opts);
528
- await installCredentials(deps, result.selection, serviceConnection(opts));
577
+ const v4 = accountTokenPoolV4Enabled(deps.config);
578
+ const result = await chooseAccount(deps, account, opts, !v4);
579
+ const installed = await installCredentials(deps, result.selection, serviceConnection(opts));
580
+ if (v4)
581
+ await persistInstalledSelection(deps, installed);
529
582
  if (await json(command, deps))
530
583
  print(deps.output, success({ account: result.account, auth_mode: "chatgpt" }));
531
584
  else
@@ -542,7 +595,7 @@ export function registerCodexCommands(program, deps, options = {}) {
542
595
  deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
543
596
  process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
544
597
  });
545
- serviceOptions(codex.command("app [workspace]").description("Validate and launch the official Codex desktop app"))
598
+ serviceOptions(codex.command("app [workspace]").description("Launch the existing official Codex desktop app without modifying its version"))
546
599
  .option("-a, --account <id>", "account id")
547
600
  .option("-s, --select-account", "select an account before launch")
548
601
  .action(async (workspace, opts) => {
@@ -576,9 +629,7 @@ export function registerCodexCommands(program, deps, options = {}) {
576
629
  path: local.desktopApp.path ?? null,
577
630
  bundle_id: local.desktopApp.bundleId ?? null,
578
631
  version: local.desktopApp.version ?? null,
579
- build: local.desktopApp.build ?? null,
580
- target_version: local.desktopApp.targetVersion ?? null,
581
- remediation_required: local.desktopApp.remediationRequired ?? false
632
+ build: local.desktopApp.build ?? null
582
633
  } : null,
583
634
  selection: local.selection ? {
584
635
  account: local.selection.account,
@@ -592,7 +643,7 @@ export function registerCodexCommands(program, deps, options = {}) {
592
643
  deps.output.write(`Official ChatGPT login: ${local.officialAuthMatchesSelection ? "matched" : "not matched"}\n`);
593
644
  if (local.desktopApp) {
594
645
  const desktop = !local.desktopApp.supported
595
- ? `not managed on ${local.desktopApp.platform}`
646
+ ? `not available on ${local.desktopApp.platform}`
596
647
  : local.desktopApp.installed
597
648
  ? `${local.desktopApp.version ?? "unknown version"}${local.desktopApp.build ? ` (${local.desktopApp.build})` : ""}`
598
649
  : "not installed";