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/README.md +2 -0
- package/dist/cli.cjs +434 -37
- package/dist/main.cjs +434 -37
- package/dist/main.js +432 -35
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
396
|
-
|
|
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(
|
|
401
|
-
|
|
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(
|
|
408
|
-
|
|
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];
|