codex-team 0.0.9 → 0.0.11

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/dist/main.js CHANGED
@@ -1,15 +1,15 @@
1
+ import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
1
2
  import { stderr, stdin, stdout as external_node_process_stdout } from "node:process";
3
+ import { basename, dirname, join } from "node:path";
2
4
  import dayjs from "dayjs";
3
5
  import timezone from "dayjs/plugin/timezone.js";
4
6
  import utc from "dayjs/plugin/utc.js";
5
7
  import { createHash } from "node:crypto";
6
- import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
8
  import { homedir } from "node:os";
8
- import { basename, dirname, join } from "node:path";
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  var package_namespaceObject = {
12
- rE: "0.0.9"
12
+ rE: "0.0.11"
13
13
  };
14
14
  function isRecord(value) {
15
15
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -216,6 +216,9 @@ function decodeJwtPayload(token) {
216
216
  }
217
217
  const DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com";
218
218
  const USER_AGENT = "codexm/0.1";
219
+ const USAGE_FETCH_ATTEMPTS = 3;
220
+ const USAGE_FETCH_RETRY_DELAY_MS = 250;
221
+ const USAGE_FETCH_TIMEOUT_MS = 15000;
219
222
  function quota_client_isRecord(value) {
220
223
  return "object" == typeof value && null !== value && !Array.isArray(value);
221
224
  }
@@ -313,8 +316,6 @@ async function resolveUsageUrls(homeDir) {
313
316
  const normalizedBaseUrl = baseUrl.replace(/\/+$/u, "");
314
317
  const candidates = [
315
318
  `${normalizedBaseUrl}/backend-api/wham/usage`,
316
- `${normalizedBaseUrl}/wham/usage`,
317
- `${normalizedBaseUrl}/api/codex/usage`,
318
319
  "https://chatgpt.com/backend-api/wham/usage"
319
320
  ];
320
321
  return [
@@ -324,6 +325,19 @@ async function resolveUsageUrls(homeDir) {
324
325
  function normalizeFetchError(error) {
325
326
  return error instanceof Error ? error.message : String(error);
326
327
  }
328
+ async function delay(milliseconds) {
329
+ await new Promise((resolve)=>setTimeout(resolve, milliseconds));
330
+ }
331
+ function isTransientUsageStatus(status) {
332
+ return 408 === status || 429 === status || status >= 500 && status <= 599;
333
+ }
334
+ function isLastAttempt(attempt) {
335
+ return attempt >= USAGE_FETCH_ATTEMPTS;
336
+ }
337
+ function formatUsageAttemptError(url, attempt, message) {
338
+ const retryContext = USAGE_FETCH_ATTEMPTS > 1 ? ` attempt ${attempt}/${USAGE_FETCH_ATTEMPTS}` : "";
339
+ return `${url}${retryContext} -> ${message}`;
340
+ }
327
341
  function shouldRetryWithTokenRefresh(message) {
328
342
  const normalized = message.toLowerCase();
329
343
  return normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("invalid_token") || normalized.includes("deactivated_workspace");
@@ -379,7 +393,9 @@ async function requestUsage(snapshot, options) {
379
393
  const urls = await resolveUsageUrls(options.homeDir);
380
394
  const now = (options.now ?? new Date()).toISOString();
381
395
  const errors = [];
382
- for (const url of urls){
396
+ for (const url of urls)for(let attempt = 1; attempt <= USAGE_FETCH_ATTEMPTS; attempt += 1){
397
+ const abortController = new AbortController();
398
+ const timeout = setTimeout(()=>abortController.abort(), USAGE_FETCH_TIMEOUT_MS);
383
399
  let response;
384
400
  try {
385
401
  response = await fetchImpl(url, {
@@ -389,23 +405,35 @@ async function requestUsage(snapshot, options) {
389
405
  "ChatGPT-Account-Id": extracted.accountId,
390
406
  Accept: "application/json",
391
407
  "User-Agent": USER_AGENT
392
- }
408
+ },
409
+ signal: abortController.signal
393
410
  });
394
411
  } catch (error) {
395
- errors.push(`${url} -> ${normalizeFetchError(error)}`);
396
- continue;
412
+ clearTimeout(timeout);
413
+ const message = abortController.signal.aborted ? `timed out after ${USAGE_FETCH_TIMEOUT_MS}ms` : normalizeFetchError(error);
414
+ errors.push(formatUsageAttemptError(url, attempt, message));
415
+ if (!isLastAttempt(attempt)) {
416
+ await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
417
+ continue;
418
+ }
419
+ break;
397
420
  }
421
+ clearTimeout(timeout);
398
422
  if (!response.ok) {
399
423
  const body = await response.text();
400
- errors.push(`${url} -> ${response.status}: ${body.slice(0, 140).replace(/\s+/gu, " ").trim()}`);
401
- continue;
424
+ errors.push(formatUsageAttemptError(url, attempt, `${response.status}: ${body.slice(0, 140).replace(/\s+/gu, " ").trim()}`));
425
+ if (isTransientUsageStatus(response.status) && !isLastAttempt(attempt)) {
426
+ await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
427
+ continue;
428
+ }
429
+ break;
402
430
  }
403
431
  let payload;
404
432
  try {
405
433
  payload = await response.json();
406
434
  } catch (error) {
407
- errors.push(`${url} -> failed to parse JSON: ${normalizeFetchError(error)}`);
408
- continue;
435
+ errors.push(formatUsageAttemptError(url, attempt, `failed to parse JSON: ${normalizeFetchError(error)}`));
436
+ break;
409
437
  }
410
438
  return mapUsagePayload(payload, extracted.planType, now);
411
439
  }
@@ -474,7 +502,6 @@ async function fetchQuotaSnapshot(snapshot, options = {}) {
474
502
  };
475
503
  }
476
504
  }
477
- const account_store_execFile = promisify(execFile);
478
505
  const DIRECTORY_MODE = 448;
479
506
  const FILE_MODE = 384;
480
507
  const SCHEMA_VERSION = 1;
@@ -547,25 +574,6 @@ function canAutoMigrateLegacyChatGPTMeta(meta, snapshot) {
547
574
  if (!snapshotUserId) return false;
548
575
  return meta.account_id === getSnapshotAccountId(snapshot);
549
576
  }
550
- async function detectRunningCodexProcesses() {
551
- try {
552
- const { stdout } = await account_store_execFile("ps", [
553
- "-Ao",
554
- "pid=,command="
555
- ]);
556
- const pids = [];
557
- for (const line of stdout.split("\n")){
558
- const match = line.trim().match(/^(\d+)\s+(.+)$/);
559
- if (!match) continue;
560
- const pid = Number(match[1]);
561
- const command = match[2];
562
- if (pid !== process.pid && /(^|\s|\/)codex(\s|$)/.test(command) && !command.includes("codex-team")) pids.push(pid);
563
- }
564
- return pids;
565
- } catch {
566
- return [];
567
- }
568
- }
569
577
  class AccountStore {
570
578
  paths;
571
579
  fetchImpl;
@@ -861,8 +869,6 @@ class AccountStore {
861
869
  last_switched_account: name,
862
870
  last_backup_path: backupPath
863
871
  });
864
- const runningCodexPids = await detectRunningCodexProcesses();
865
- if (runningCodexPids.length > 0) warnings.push(`Detected running codex processes (${runningCodexPids.join(", ")}). Existing sessions may still hold the previous login state.`);
866
872
  return {
867
873
  account: await this.readManagedAccount(name),
868
874
  warnings,
@@ -1036,6 +1042,250 @@ function createAccountStore(homeDir, options) {
1036
1042
  fetchImpl: options?.fetchImpl
1037
1043
  });
1038
1044
  }
1045
+ const codex_desktop_launch_execFile = promisify(execFile);
1046
+ const DEFAULT_CODEX_REMOTE_DEBUGGING_PORT = 9223;
1047
+ const DEFAULT_CODEX_DESKTOP_STATE_PATH = join(homedir(), ".codex-team", "desktop-state.json");
1048
+ const CODEX_BINARY_SUFFIX = "/Contents/MacOS/Codex";
1049
+ const CODEX_APP_NAME = "Codex";
1050
+ const CODEX_LOCAL_HOST_ID = "local";
1051
+ const CODEX_APP_SERVER_RESTART_EXPRESSION = 'window.electronBridge.sendMessageFromView({ type: "codex-app-server-restart", hostId: "local" })';
1052
+ const DEVTOOLS_REQUEST_TIMEOUT_MS = 5000;
1053
+ function codex_desktop_launch_isRecord(value) {
1054
+ return "object" == typeof value && null !== value && !Array.isArray(value);
1055
+ }
1056
+ function isNonEmptyString(value) {
1057
+ return "string" == typeof value && "" !== value.trim();
1058
+ }
1059
+ async function codex_desktop_launch_delay(ms) {
1060
+ await new Promise((resolve)=>setTimeout(resolve, ms));
1061
+ }
1062
+ async function pathExistsViaStat(execFileImpl, path) {
1063
+ try {
1064
+ await execFileImpl("stat", [
1065
+ "-f",
1066
+ "%N",
1067
+ path
1068
+ ]);
1069
+ return true;
1070
+ } catch {
1071
+ return false;
1072
+ }
1073
+ }
1074
+ function parseManagedState(raw) {
1075
+ if ("" === raw.trim()) return null;
1076
+ let parsed;
1077
+ try {
1078
+ parsed = JSON.parse(raw);
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ if (!codex_desktop_launch_isRecord(parsed)) return null;
1083
+ const pid = parsed.pid;
1084
+ const appPath = parsed.app_path;
1085
+ const remoteDebuggingPort = parsed.remote_debugging_port;
1086
+ const managedByCodexm = parsed.managed_by_codexm;
1087
+ const startedAt = parsed.started_at;
1088
+ if ("number" != typeof pid || !Number.isInteger(pid) || pid <= 0 || !isNonEmptyString(appPath) || "number" != typeof remoteDebuggingPort || !Number.isInteger(remoteDebuggingPort) || remoteDebuggingPort <= 0 || true !== managedByCodexm || !isNonEmptyString(startedAt)) return null;
1089
+ return {
1090
+ pid,
1091
+ app_path: appPath,
1092
+ remote_debugging_port: remoteDebuggingPort,
1093
+ managed_by_codexm: true,
1094
+ started_at: startedAt
1095
+ };
1096
+ }
1097
+ async function ensureStateDirectory(statePath) {
1098
+ await mkdir(dirname(statePath), {
1099
+ recursive: true,
1100
+ mode: 448
1101
+ });
1102
+ }
1103
+ function createDefaultWebSocket(url) {
1104
+ return new WebSocket(url);
1105
+ }
1106
+ function isDevtoolsTarget(value) {
1107
+ return codex_desktop_launch_isRecord(value);
1108
+ }
1109
+ function isManagedDesktopProcess(runningApps, state) {
1110
+ const expectedBinaryPath = `${state.app_path}${CODEX_BINARY_SUFFIX}`;
1111
+ const expectedPort = `--remote-debugging-port=${state.remote_debugging_port}`;
1112
+ return runningApps.some((entry)=>entry.pid === state.pid && entry.command.includes(expectedBinaryPath) && entry.command.includes(expectedPort));
1113
+ }
1114
+ async function evaluateDevtoolsExpression(createWebSocketImpl, webSocketDebuggerUrl, expression) {
1115
+ const socket = createWebSocketImpl(webSocketDebuggerUrl);
1116
+ await new Promise((resolve, reject)=>{
1117
+ const requestId = 1;
1118
+ const timeout = setTimeout(()=>{
1119
+ cleanup();
1120
+ reject(new Error("Timed out waiting for Codex Desktop devtools response."));
1121
+ }, DEVTOOLS_REQUEST_TIMEOUT_MS);
1122
+ const cleanup = ()=>{
1123
+ clearTimeout(timeout);
1124
+ socket.onopen = null;
1125
+ socket.onmessage = null;
1126
+ socket.onerror = null;
1127
+ socket.onclose = null;
1128
+ socket.close();
1129
+ };
1130
+ socket.onopen = ()=>{
1131
+ socket.send(JSON.stringify({
1132
+ id: requestId,
1133
+ method: "Runtime.evaluate",
1134
+ params: {
1135
+ expression,
1136
+ awaitPromise: true
1137
+ }
1138
+ }));
1139
+ };
1140
+ socket.onmessage = (event)=>{
1141
+ if ("string" != typeof event.data) return;
1142
+ let payload;
1143
+ try {
1144
+ payload = JSON.parse(event.data);
1145
+ } catch {
1146
+ return;
1147
+ }
1148
+ if (!codex_desktop_launch_isRecord(payload) || payload.id !== requestId) return;
1149
+ if (codex_desktop_launch_isRecord(payload.error)) {
1150
+ cleanup();
1151
+ reject(new Error(String(payload.error.message ?? "Codex Desktop devtools request failed.")));
1152
+ return;
1153
+ }
1154
+ const result = codex_desktop_launch_isRecord(payload.result) ? payload.result : null;
1155
+ if (result && codex_desktop_launch_isRecord(result.exceptionDetails)) {
1156
+ cleanup();
1157
+ reject(new Error("Codex Desktop rejected the app-server restart request."));
1158
+ return;
1159
+ }
1160
+ cleanup();
1161
+ resolve();
1162
+ };
1163
+ socket.onerror = ()=>{
1164
+ cleanup();
1165
+ reject(new Error("Failed to communicate with Codex Desktop devtools."));
1166
+ };
1167
+ socket.onclose = ()=>{
1168
+ cleanup();
1169
+ reject(new Error("Codex Desktop devtools connection closed before replying."));
1170
+ };
1171
+ });
1172
+ }
1173
+ function createCodexDesktopLauncher(options = {}) {
1174
+ const execFileImpl = options.execFileImpl ?? codex_desktop_launch_execFile;
1175
+ const statePath = options.statePath ?? DEFAULT_CODEX_DESKTOP_STATE_PATH;
1176
+ const readFileImpl = options.readFileImpl ?? (async (path)=>readFile(path, "utf8"));
1177
+ const writeFileImpl = options.writeFileImpl ?? writeFile;
1178
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
1179
+ const createWebSocketImpl = options.createWebSocketImpl ?? createDefaultWebSocket;
1180
+ async function findInstalledApp() {
1181
+ const candidates = [
1182
+ "/Applications/Codex.app",
1183
+ join(homedir(), "Applications", "Codex.app")
1184
+ ];
1185
+ for (const candidate of candidates)if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
1186
+ try {
1187
+ const { stdout } = await execFileImpl("mdfind", [
1188
+ 'kMDItemFSName == "Codex.app"'
1189
+ ]);
1190
+ for (const line of stdout.split("\n")){
1191
+ const candidate = line.trim();
1192
+ if ("" !== candidate) {
1193
+ if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
1194
+ }
1195
+ }
1196
+ } catch {}
1197
+ return null;
1198
+ }
1199
+ async function listRunningApps() {
1200
+ const { stdout } = await execFileImpl("ps", [
1201
+ "-Ao",
1202
+ "pid=,command="
1203
+ ]);
1204
+ const running = [];
1205
+ for (const line of stdout.split("\n")){
1206
+ const match = line.trim().match(/^(\d+)\s+(.+)$/);
1207
+ if (!match) continue;
1208
+ const pid = Number(match[1]);
1209
+ const command = match[2];
1210
+ if (pid !== process.pid && command.includes(CODEX_BINARY_SUFFIX)) running.push({
1211
+ pid,
1212
+ command
1213
+ });
1214
+ }
1215
+ return running;
1216
+ }
1217
+ async function quitRunningApps() {
1218
+ const running = await listRunningApps();
1219
+ if (0 === running.length) return;
1220
+ await execFileImpl("osascript", [
1221
+ "-e",
1222
+ `tell application "${CODEX_APP_NAME}" to quit`
1223
+ ]);
1224
+ for(let attempt = 0; attempt < 10; attempt += 1){
1225
+ const remaining = await listRunningApps();
1226
+ if (0 === remaining.length) return;
1227
+ await codex_desktop_launch_delay(300);
1228
+ }
1229
+ throw new Error("Timed out waiting for Codex Desktop to quit.");
1230
+ }
1231
+ async function launch(appPath) {
1232
+ await execFileImpl("open", [
1233
+ "-na",
1234
+ appPath,
1235
+ "--args",
1236
+ `--remote-debugging-port=${DEFAULT_CODEX_REMOTE_DEBUGGING_PORT}`
1237
+ ]);
1238
+ }
1239
+ async function readManagedState() {
1240
+ try {
1241
+ return parseManagedState(await readFileImpl(statePath));
1242
+ } catch {
1243
+ return null;
1244
+ }
1245
+ }
1246
+ async function writeManagedState(state) {
1247
+ await ensureStateDirectory(statePath);
1248
+ await writeFileImpl(statePath, `${JSON.stringify(state, null, 2)}\n`);
1249
+ }
1250
+ async function clearManagedState() {
1251
+ await ensureStateDirectory(statePath);
1252
+ await writeFileImpl(statePath, "");
1253
+ }
1254
+ async function isManagedDesktopRunning() {
1255
+ const state = await readManagedState();
1256
+ if (!state) return false;
1257
+ const runningApps = await listRunningApps();
1258
+ return isManagedDesktopProcess(runningApps, state);
1259
+ }
1260
+ async function restartManagedAppServer() {
1261
+ const state = await readManagedState();
1262
+ if (!state) return false;
1263
+ const runningApps = await listRunningApps();
1264
+ if (!isManagedDesktopProcess(runningApps, state)) return false;
1265
+ const response = await fetchImpl(`http://127.0.0.1:${state.remote_debugging_port}/json/list`);
1266
+ if (!response.ok) throw new Error(`Failed to query Codex Desktop devtools targets (HTTP ${response.status}).`);
1267
+ const targets = await response.json();
1268
+ if (!Array.isArray(targets)) throw new Error("Codex Desktop devtools target list was not an array.");
1269
+ const localTarget = targets.find((target)=>{
1270
+ if (!isDevtoolsTarget(target)) return false;
1271
+ return "page" === target.type && target.url === `app://-/index.html?hostId=${CODEX_LOCAL_HOST_ID}` && isNonEmptyString(target.webSocketDebuggerUrl);
1272
+ });
1273
+ if (!localTarget || !isNonEmptyString(localTarget.webSocketDebuggerUrl)) throw new Error("Could not find the local Codex Desktop devtools target.");
1274
+ await evaluateDevtoolsExpression(createWebSocketImpl, localTarget.webSocketDebuggerUrl, CODEX_APP_SERVER_RESTART_EXPRESSION);
1275
+ return true;
1276
+ }
1277
+ return {
1278
+ findInstalledApp,
1279
+ listRunningApps,
1280
+ quitRunningApps,
1281
+ launch,
1282
+ readManagedState,
1283
+ writeManagedState,
1284
+ clearManagedState,
1285
+ isManagedDesktopRunning,
1286
+ restartManagedAppServer
1287
+ };
1288
+ }
1039
1289
  dayjs.extend(utc);
1040
1290
  dayjs.extend(timezone);
1041
1291
  function parseArgs(argv) {
@@ -1079,12 +1329,29 @@ Usage:
1079
1329
  codexm update [--json]
1080
1330
  codexm switch <name> [--json]
1081
1331
  codexm switch --auto [--dry-run] [--json]
1332
+ codexm launch [name] [--json]
1082
1333
  codexm remove <name> [--yes] [--json]
1083
1334
  codexm rename <old> <new> [--json]
1084
1335
 
1085
1336
  Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1086
1337
  `);
1087
1338
  }
1339
+ function stripManagedDesktopWarning(warnings) {
1340
+ return warnings.filter((warning)=>!warning.startsWith("Detected running codex processes (") || !warning.endsWith("Existing sessions may still hold the previous login state."));
1341
+ }
1342
+ async function refreshManagedDesktopAfterSwitch(warnings, desktopLauncher) {
1343
+ try {
1344
+ if (await desktopLauncher.restartManagedAppServer()) return;
1345
+ } catch (error) {
1346
+ warnings.push(`Failed to refresh the running codexm-managed Codex Desktop session: ${error.message}`);
1347
+ return;
1348
+ }
1349
+ try {
1350
+ const runningApps = await desktopLauncher.listRunningApps();
1351
+ if (0 === runningApps.length) return;
1352
+ warnings.push(`Detected running codex processes (${runningApps.map((app)=>app.pid).join(", ")}). Existing sessions may still hold the previous login state.`);
1353
+ } catch {}
1354
+ }
1088
1355
  function describeCurrentStatus(status) {
1089
1356
  const lines = [];
1090
1357
  if (status.exists) {
@@ -1279,6 +1546,67 @@ async function confirmRemoval(name, streams) {
1279
1546
  streams.stdin.on("data", onData);
1280
1547
  });
1281
1548
  }
1549
+ async function confirmDesktopRelaunch(streams) {
1550
+ if (!streams.stdin.isTTY) throw new Error("Refusing to relaunch Codex Desktop in a non-interactive terminal.");
1551
+ streams.stdout.write("Codex Desktop is already running. Close it and relaunch with the selected auth? [y/N] ");
1552
+ return await new Promise((resolve)=>{
1553
+ const cleanup = ()=>{
1554
+ streams.stdin.off("data", onData);
1555
+ streams.stdin.pause();
1556
+ };
1557
+ const onData = (buffer)=>{
1558
+ const answer = buffer.toString("utf8").trim().toLowerCase();
1559
+ cleanup();
1560
+ streams.stdout.write("\n");
1561
+ resolve("y" === answer || "yes" === answer);
1562
+ };
1563
+ streams.stdin.resume();
1564
+ streams.stdin.on("data", onData);
1565
+ });
1566
+ }
1567
+ async function sleep(ms) {
1568
+ await new Promise((resolve)=>setTimeout(resolve, ms));
1569
+ }
1570
+ async function main_pathExists(path) {
1571
+ try {
1572
+ await stat(path);
1573
+ return true;
1574
+ } catch (error) {
1575
+ const nodeError = error;
1576
+ if ("ENOENT" === nodeError.code) return false;
1577
+ throw error;
1578
+ }
1579
+ }
1580
+ function isRunningDesktopFromApp(app, appPath) {
1581
+ return app.command.includes(`${appPath}/Contents/MacOS/Codex`);
1582
+ }
1583
+ async function resolveManagedDesktopState(desktopLauncher, appPath, existingApps) {
1584
+ const existingPids = new Set(existingApps.map((app)=>app.pid));
1585
+ for(let attempt = 0; attempt < 10; attempt += 1){
1586
+ const runningApps = await desktopLauncher.listRunningApps();
1587
+ const launchedApp = runningApps.filter((app)=>isRunningDesktopFromApp(app, appPath) && !existingPids.has(app.pid)).sort((left, right)=>right.pid - left.pid)[0] ?? runningApps.filter((app)=>isRunningDesktopFromApp(app, appPath)).sort((left, right)=>right.pid - left.pid)[0] ?? null;
1588
+ if (launchedApp) return {
1589
+ pid: launchedApp.pid,
1590
+ app_path: appPath,
1591
+ remote_debugging_port: DEFAULT_CODEX_REMOTE_DEBUGGING_PORT,
1592
+ managed_by_codexm: true,
1593
+ started_at: new Date().toISOString()
1594
+ };
1595
+ await sleep(300);
1596
+ }
1597
+ return null;
1598
+ }
1599
+ async function restoreLaunchBackup(store, backupPath) {
1600
+ if (backupPath && await main_pathExists(backupPath)) await copyFile(backupPath, store.paths.currentAuthPath);
1601
+ else await rm(store.paths.currentAuthPath, {
1602
+ force: true
1603
+ });
1604
+ const configBackupPath = join(store.paths.backupsDir, "last-active-config.toml");
1605
+ if (await main_pathExists(configBackupPath)) await copyFile(configBackupPath, store.paths.currentConfigPath);
1606
+ else await rm(store.paths.currentConfigPath, {
1607
+ force: true
1608
+ });
1609
+ }
1282
1610
  async function runCli(argv, options = {}) {
1283
1611
  const streams = {
1284
1612
  stdin: options.stdin ?? stdin,
@@ -1286,6 +1614,7 @@ async function runCli(argv, options = {}) {
1286
1614
  stderr: options.stderr ?? stderr
1287
1615
  };
1288
1616
  const store = options.store ?? createAccountStore();
1617
+ const desktopLauncher = options.desktopLauncher ?? createCodexDesktopLauncher();
1289
1618
  const parsed = parseArgs(argv);
1290
1619
  const json = parsed.flags.has("--json");
1291
1620
  try {
@@ -1418,6 +1747,8 @@ async function runCli(argv, options = {}) {
1418
1747
  }
1419
1748
  const result = await store.switchAccount(selected.name);
1420
1749
  for (const warning of warnings)result.warnings.push(warning);
1750
+ result.warnings = stripManagedDesktopWarning(result.warnings);
1751
+ await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
1421
1752
  const payload = {
1422
1753
  ok: true,
1423
1754
  action: "switch",
@@ -1441,6 +1772,8 @@ async function runCli(argv, options = {}) {
1441
1772
  }
1442
1773
  if (!name) throw new Error("Usage: codexm switch <name>");
1443
1774
  const result = await store.switchAccount(name);
1775
+ result.warnings = stripManagedDesktopWarning(result.warnings);
1776
+ await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
1444
1777
  let quota = null;
1445
1778
  try {
1446
1779
  await store.refreshQuotaForAccount(result.account.name);
@@ -1472,6 +1805,70 @@ async function runCli(argv, options = {}) {
1472
1805
  }
1473
1806
  return 0;
1474
1807
  }
1808
+ case "launch":
1809
+ {
1810
+ const name = parsed.positionals[0] ?? null;
1811
+ if (parsed.positionals.length > 1) throw new Error("Usage: codexm launch [name] [--json]");
1812
+ const warnings = [];
1813
+ const appPath = await desktopLauncher.findInstalledApp();
1814
+ if (!appPath) throw new Error("Codex Desktop not found at /Applications/Codex.app.");
1815
+ const runningApps = await desktopLauncher.listRunningApps();
1816
+ if (runningApps.length > 0) {
1817
+ const confirmed = await confirmDesktopRelaunch(streams);
1818
+ if (!confirmed) {
1819
+ if (json) writeJson(streams.stdout, {
1820
+ ok: false,
1821
+ action: "launch",
1822
+ cancelled: true
1823
+ });
1824
+ else streams.stdout.write("Aborted.\n");
1825
+ return 1;
1826
+ }
1827
+ await desktopLauncher.quitRunningApps();
1828
+ }
1829
+ let switchedAccount = null;
1830
+ let switchBackupPath = null;
1831
+ if (name) {
1832
+ const switchResult = await store.switchAccount(name);
1833
+ warnings.push(...stripManagedDesktopWarning(switchResult.warnings));
1834
+ switchedAccount = switchResult.account;
1835
+ switchBackupPath = switchResult.backup_path;
1836
+ }
1837
+ try {
1838
+ await desktopLauncher.launch(appPath);
1839
+ const managedState = await resolveManagedDesktopState(desktopLauncher, appPath, runningApps);
1840
+ if (!managedState) {
1841
+ await desktopLauncher.clearManagedState().catch(()=>void 0);
1842
+ throw new Error("Failed to confirm the newly launched Codex Desktop process for managed-session tracking.");
1843
+ }
1844
+ await desktopLauncher.writeManagedState(managedState);
1845
+ } catch (error) {
1846
+ if (switchedAccount) await restoreLaunchBackup(store, switchBackupPath).catch(()=>void 0);
1847
+ throw error;
1848
+ }
1849
+ if (json) writeJson(streams.stdout, {
1850
+ ok: true,
1851
+ action: "launch",
1852
+ account: switchedAccount ? {
1853
+ name: switchedAccount.name,
1854
+ account_id: switchedAccount.account_id,
1855
+ user_id: switchedAccount.user_id ?? null,
1856
+ identity: switchedAccount.identity,
1857
+ auth_mode: switchedAccount.auth_mode
1858
+ } : null,
1859
+ launched_with_current_auth: null === switchedAccount,
1860
+ app_path: appPath,
1861
+ relaunched: runningApps.length > 0,
1862
+ warnings
1863
+ });
1864
+ else {
1865
+ if (switchedAccount) streams.stdout.write(`Switched to "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n`);
1866
+ if (runningApps.length > 0) streams.stdout.write("Closed existing Codex Desktop instance and launched a new one.\n");
1867
+ streams.stdout.write(switchedAccount ? `Launched Codex Desktop with "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n` : "Launched Codex Desktop with current auth.\n");
1868
+ for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
1869
+ }
1870
+ return 0;
1871
+ }
1475
1872
  case "remove":
1476
1873
  {
1477
1874
  const name = parsed.positionals[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",