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/README.md
CHANGED
|
@@ -22,11 +22,13 @@ codexm save <name>
|
|
|
22
22
|
codexm update
|
|
23
23
|
codexm switch <name>
|
|
24
24
|
codexm switch --auto --dry-run
|
|
25
|
+
codexm launch [name] [--json]
|
|
25
26
|
codexm remove <name> --yes
|
|
26
27
|
codexm rename <old> <new>
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Use `--json` on query and mutation commands when you need machine-readable output.
|
|
31
|
+
`codexm launch` starts Codex Desktop with the current auth, or switches to a saved account first when you pass a name. If Codex Desktop is already running, `codexm launch` asks before relaunching it. If the running Desktop was started by `codexm launch`, later `codexm switch` attempts to refresh that session automatically; if it was started outside `codexm`, `codexm switch` still warns that existing sessions may keep the previous login state. This refresh works by restarting the Codex app server, so any in-progress thread in that Desktop session will be interrupted.
|
|
30
32
|
|
|
31
33
|
## Typical flow
|
|
32
34
|
|
package/dist/cli.cjs
CHANGED
|
@@ -5,7 +5,9 @@ var __webpack_modules__ = {
|
|
|
5
5
|
__webpack_require__.d(__webpack_exports__, {
|
|
6
6
|
runCli: ()=>runCli
|
|
7
7
|
});
|
|
8
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
8
9
|
const external_node_process_namespaceObject = require("node:process");
|
|
10
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
9
11
|
const external_dayjs_namespaceObject = require("dayjs");
|
|
10
12
|
var external_dayjs_default = /*#__PURE__*/ __webpack_require__.n(external_dayjs_namespaceObject);
|
|
11
13
|
const timezone_js_namespaceObject = require("dayjs/plugin/timezone.js");
|
|
@@ -13,10 +15,9 @@ var __webpack_modules__ = {
|
|
|
13
15
|
const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
|
|
14
16
|
var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
|
|
15
17
|
var package_namespaceObject = {
|
|
16
|
-
rE: "0.0.
|
|
18
|
+
rE: "0.0.11"
|
|
17
19
|
};
|
|
18
20
|
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
19
|
-
const promises_namespaceObject = require("node:fs/promises");
|
|
20
21
|
function isRecord(value) {
|
|
21
22
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
22
23
|
}
|
|
@@ -221,11 +222,11 @@ var __webpack_modules__ = {
|
|
|
221
222
|
return parsed;
|
|
222
223
|
}
|
|
223
224
|
const external_node_os_namespaceObject = require("node:os");
|
|
224
|
-
const external_node_path_namespaceObject = require("node:path");
|
|
225
|
-
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
226
|
-
const external_node_util_namespaceObject = require("node:util");
|
|
227
225
|
const DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com";
|
|
228
226
|
const USER_AGENT = "codexm/0.1";
|
|
227
|
+
const USAGE_FETCH_ATTEMPTS = 3;
|
|
228
|
+
const USAGE_FETCH_RETRY_DELAY_MS = 250;
|
|
229
|
+
const USAGE_FETCH_TIMEOUT_MS = 15000;
|
|
229
230
|
function quota_client_isRecord(value) {
|
|
230
231
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
231
232
|
}
|
|
@@ -323,8 +324,6 @@ var __webpack_modules__ = {
|
|
|
323
324
|
const normalizedBaseUrl = baseUrl.replace(/\/+$/u, "");
|
|
324
325
|
const candidates = [
|
|
325
326
|
`${normalizedBaseUrl}/backend-api/wham/usage`,
|
|
326
|
-
`${normalizedBaseUrl}/wham/usage`,
|
|
327
|
-
`${normalizedBaseUrl}/api/codex/usage`,
|
|
328
327
|
"https://chatgpt.com/backend-api/wham/usage"
|
|
329
328
|
];
|
|
330
329
|
return [
|
|
@@ -334,6 +333,19 @@ var __webpack_modules__ = {
|
|
|
334
333
|
function normalizeFetchError(error) {
|
|
335
334
|
return error instanceof Error ? error.message : String(error);
|
|
336
335
|
}
|
|
336
|
+
async function delay(milliseconds) {
|
|
337
|
+
await new Promise((resolve)=>setTimeout(resolve, milliseconds));
|
|
338
|
+
}
|
|
339
|
+
function isTransientUsageStatus(status) {
|
|
340
|
+
return 408 === status || 429 === status || status >= 500 && status <= 599;
|
|
341
|
+
}
|
|
342
|
+
function isLastAttempt(attempt) {
|
|
343
|
+
return attempt >= USAGE_FETCH_ATTEMPTS;
|
|
344
|
+
}
|
|
345
|
+
function formatUsageAttemptError(url, attempt, message) {
|
|
346
|
+
const retryContext = USAGE_FETCH_ATTEMPTS > 1 ? ` attempt ${attempt}/${USAGE_FETCH_ATTEMPTS}` : "";
|
|
347
|
+
return `${url}${retryContext} -> ${message}`;
|
|
348
|
+
}
|
|
337
349
|
function shouldRetryWithTokenRefresh(message) {
|
|
338
350
|
const normalized = message.toLowerCase();
|
|
339
351
|
return normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("invalid_token") || normalized.includes("deactivated_workspace");
|
|
@@ -389,7 +401,9 @@ var __webpack_modules__ = {
|
|
|
389
401
|
const urls = await resolveUsageUrls(options.homeDir);
|
|
390
402
|
const now = (options.now ?? new Date()).toISOString();
|
|
391
403
|
const errors = [];
|
|
392
|
-
for (const url of urls){
|
|
404
|
+
for (const url of urls)for(let attempt = 1; attempt <= USAGE_FETCH_ATTEMPTS; attempt += 1){
|
|
405
|
+
const abortController = new AbortController();
|
|
406
|
+
const timeout = setTimeout(()=>abortController.abort(), USAGE_FETCH_TIMEOUT_MS);
|
|
393
407
|
let response;
|
|
394
408
|
try {
|
|
395
409
|
response = await fetchImpl(url, {
|
|
@@ -399,23 +413,35 @@ var __webpack_modules__ = {
|
|
|
399
413
|
"ChatGPT-Account-Id": extracted.accountId,
|
|
400
414
|
Accept: "application/json",
|
|
401
415
|
"User-Agent": USER_AGENT
|
|
402
|
-
}
|
|
416
|
+
},
|
|
417
|
+
signal: abortController.signal
|
|
403
418
|
});
|
|
404
419
|
} catch (error) {
|
|
405
|
-
|
|
406
|
-
|
|
420
|
+
clearTimeout(timeout);
|
|
421
|
+
const message = abortController.signal.aborted ? `timed out after ${USAGE_FETCH_TIMEOUT_MS}ms` : normalizeFetchError(error);
|
|
422
|
+
errors.push(formatUsageAttemptError(url, attempt, message));
|
|
423
|
+
if (!isLastAttempt(attempt)) {
|
|
424
|
+
await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
407
428
|
}
|
|
429
|
+
clearTimeout(timeout);
|
|
408
430
|
if (!response.ok) {
|
|
409
431
|
const body = await response.text();
|
|
410
|
-
errors.push(
|
|
411
|
-
|
|
432
|
+
errors.push(formatUsageAttemptError(url, attempt, `${response.status}: ${body.slice(0, 140).replace(/\s+/gu, " ").trim()}`));
|
|
433
|
+
if (isTransientUsageStatus(response.status) && !isLastAttempt(attempt)) {
|
|
434
|
+
await delay(USAGE_FETCH_RETRY_DELAY_MS * attempt);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
412
438
|
}
|
|
413
439
|
let payload;
|
|
414
440
|
try {
|
|
415
441
|
payload = await response.json();
|
|
416
442
|
} catch (error) {
|
|
417
|
-
errors.push(
|
|
418
|
-
|
|
443
|
+
errors.push(formatUsageAttemptError(url, attempt, `failed to parse JSON: ${normalizeFetchError(error)}`));
|
|
444
|
+
break;
|
|
419
445
|
}
|
|
420
446
|
return mapUsagePayload(payload, extracted.planType, now);
|
|
421
447
|
}
|
|
@@ -484,7 +510,6 @@ var __webpack_modules__ = {
|
|
|
484
510
|
};
|
|
485
511
|
}
|
|
486
512
|
}
|
|
487
|
-
const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
488
513
|
const DIRECTORY_MODE = 448;
|
|
489
514
|
const FILE_MODE = 384;
|
|
490
515
|
const SCHEMA_VERSION = 1;
|
|
@@ -557,25 +582,6 @@ var __webpack_modules__ = {
|
|
|
557
582
|
if (!snapshotUserId) return false;
|
|
558
583
|
return meta.account_id === getSnapshotAccountId(snapshot);
|
|
559
584
|
}
|
|
560
|
-
async function detectRunningCodexProcesses() {
|
|
561
|
-
try {
|
|
562
|
-
const { stdout } = await execFile("ps", [
|
|
563
|
-
"-Ao",
|
|
564
|
-
"pid=,command="
|
|
565
|
-
]);
|
|
566
|
-
const pids = [];
|
|
567
|
-
for (const line of stdout.split("\n")){
|
|
568
|
-
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
569
|
-
if (!match) continue;
|
|
570
|
-
const pid = Number(match[1]);
|
|
571
|
-
const command = match[2];
|
|
572
|
-
if (pid !== process.pid && /(^|\s|\/)codex(\s|$)/.test(command) && !command.includes("codex-team")) pids.push(pid);
|
|
573
|
-
}
|
|
574
|
-
return pids;
|
|
575
|
-
} catch {
|
|
576
|
-
return [];
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
585
|
class AccountStore {
|
|
580
586
|
paths;
|
|
581
587
|
fetchImpl;
|
|
@@ -871,8 +877,6 @@ var __webpack_modules__ = {
|
|
|
871
877
|
last_switched_account: name,
|
|
872
878
|
last_backup_path: backupPath
|
|
873
879
|
});
|
|
874
|
-
const runningCodexPids = await detectRunningCodexProcesses();
|
|
875
|
-
if (runningCodexPids.length > 0) warnings.push(`Detected running codex processes (${runningCodexPids.join(", ")}). Existing sessions may still hold the previous login state.`);
|
|
876
880
|
return {
|
|
877
881
|
account: await this.readManagedAccount(name),
|
|
878
882
|
warnings,
|
|
@@ -1046,6 +1050,252 @@ var __webpack_modules__ = {
|
|
|
1046
1050
|
fetchImpl: options?.fetchImpl
|
|
1047
1051
|
});
|
|
1048
1052
|
}
|
|
1053
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
1054
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
1055
|
+
const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
1056
|
+
const DEFAULT_CODEX_REMOTE_DEBUGGING_PORT = 9223;
|
|
1057
|
+
const DEFAULT_CODEX_DESKTOP_STATE_PATH = (0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.homedir)(), ".codex-team", "desktop-state.json");
|
|
1058
|
+
const CODEX_BINARY_SUFFIX = "/Contents/MacOS/Codex";
|
|
1059
|
+
const CODEX_APP_NAME = "Codex";
|
|
1060
|
+
const CODEX_LOCAL_HOST_ID = "local";
|
|
1061
|
+
const CODEX_APP_SERVER_RESTART_EXPRESSION = 'window.electronBridge.sendMessageFromView({ type: "codex-app-server-restart", hostId: "local" })';
|
|
1062
|
+
const DEVTOOLS_REQUEST_TIMEOUT_MS = 5000;
|
|
1063
|
+
function codex_desktop_launch_isRecord(value) {
|
|
1064
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
1065
|
+
}
|
|
1066
|
+
function isNonEmptyString(value) {
|
|
1067
|
+
return "string" == typeof value && "" !== value.trim();
|
|
1068
|
+
}
|
|
1069
|
+
async function codex_desktop_launch_delay(ms) {
|
|
1070
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1071
|
+
}
|
|
1072
|
+
async function pathExistsViaStat(execFileImpl, path) {
|
|
1073
|
+
try {
|
|
1074
|
+
await execFileImpl("stat", [
|
|
1075
|
+
"-f",
|
|
1076
|
+
"%N",
|
|
1077
|
+
path
|
|
1078
|
+
]);
|
|
1079
|
+
return true;
|
|
1080
|
+
} catch {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function parseManagedState(raw) {
|
|
1085
|
+
if ("" === raw.trim()) return null;
|
|
1086
|
+
let parsed;
|
|
1087
|
+
try {
|
|
1088
|
+
parsed = JSON.parse(raw);
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
if (!codex_desktop_launch_isRecord(parsed)) return null;
|
|
1093
|
+
const pid = parsed.pid;
|
|
1094
|
+
const appPath = parsed.app_path;
|
|
1095
|
+
const remoteDebuggingPort = parsed.remote_debugging_port;
|
|
1096
|
+
const managedByCodexm = parsed.managed_by_codexm;
|
|
1097
|
+
const startedAt = parsed.started_at;
|
|
1098
|
+
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;
|
|
1099
|
+
return {
|
|
1100
|
+
pid,
|
|
1101
|
+
app_path: appPath,
|
|
1102
|
+
remote_debugging_port: remoteDebuggingPort,
|
|
1103
|
+
managed_by_codexm: true,
|
|
1104
|
+
started_at: startedAt
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async function ensureStateDirectory(statePath) {
|
|
1108
|
+
await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(statePath), {
|
|
1109
|
+
recursive: true,
|
|
1110
|
+
mode: 448
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function createDefaultWebSocket(url) {
|
|
1114
|
+
return new WebSocket(url);
|
|
1115
|
+
}
|
|
1116
|
+
function isDevtoolsTarget(value) {
|
|
1117
|
+
return codex_desktop_launch_isRecord(value);
|
|
1118
|
+
}
|
|
1119
|
+
function isManagedDesktopProcess(runningApps, state) {
|
|
1120
|
+
const expectedBinaryPath = `${state.app_path}${CODEX_BINARY_SUFFIX}`;
|
|
1121
|
+
const expectedPort = `--remote-debugging-port=${state.remote_debugging_port}`;
|
|
1122
|
+
return runningApps.some((entry)=>entry.pid === state.pid && entry.command.includes(expectedBinaryPath) && entry.command.includes(expectedPort));
|
|
1123
|
+
}
|
|
1124
|
+
async function evaluateDevtoolsExpression(createWebSocketImpl, webSocketDebuggerUrl, expression) {
|
|
1125
|
+
const socket = createWebSocketImpl(webSocketDebuggerUrl);
|
|
1126
|
+
await new Promise((resolve, reject)=>{
|
|
1127
|
+
const requestId = 1;
|
|
1128
|
+
const timeout = setTimeout(()=>{
|
|
1129
|
+
cleanup();
|
|
1130
|
+
reject(new Error("Timed out waiting for Codex Desktop devtools response."));
|
|
1131
|
+
}, DEVTOOLS_REQUEST_TIMEOUT_MS);
|
|
1132
|
+
const cleanup = ()=>{
|
|
1133
|
+
clearTimeout(timeout);
|
|
1134
|
+
socket.onopen = null;
|
|
1135
|
+
socket.onmessage = null;
|
|
1136
|
+
socket.onerror = null;
|
|
1137
|
+
socket.onclose = null;
|
|
1138
|
+
socket.close();
|
|
1139
|
+
};
|
|
1140
|
+
socket.onopen = ()=>{
|
|
1141
|
+
socket.send(JSON.stringify({
|
|
1142
|
+
id: requestId,
|
|
1143
|
+
method: "Runtime.evaluate",
|
|
1144
|
+
params: {
|
|
1145
|
+
expression,
|
|
1146
|
+
awaitPromise: true
|
|
1147
|
+
}
|
|
1148
|
+
}));
|
|
1149
|
+
};
|
|
1150
|
+
socket.onmessage = (event)=>{
|
|
1151
|
+
if ("string" != typeof event.data) return;
|
|
1152
|
+
let payload;
|
|
1153
|
+
try {
|
|
1154
|
+
payload = JSON.parse(event.data);
|
|
1155
|
+
} catch {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (!codex_desktop_launch_isRecord(payload) || payload.id !== requestId) return;
|
|
1159
|
+
if (codex_desktop_launch_isRecord(payload.error)) {
|
|
1160
|
+
cleanup();
|
|
1161
|
+
reject(new Error(String(payload.error.message ?? "Codex Desktop devtools request failed.")));
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const result = codex_desktop_launch_isRecord(payload.result) ? payload.result : null;
|
|
1165
|
+
if (result && codex_desktop_launch_isRecord(result.exceptionDetails)) {
|
|
1166
|
+
cleanup();
|
|
1167
|
+
reject(new Error("Codex Desktop rejected the app-server restart request."));
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
cleanup();
|
|
1171
|
+
resolve();
|
|
1172
|
+
};
|
|
1173
|
+
socket.onerror = ()=>{
|
|
1174
|
+
cleanup();
|
|
1175
|
+
reject(new Error("Failed to communicate with Codex Desktop devtools."));
|
|
1176
|
+
};
|
|
1177
|
+
socket.onclose = ()=>{
|
|
1178
|
+
cleanup();
|
|
1179
|
+
reject(new Error("Codex Desktop devtools connection closed before replying."));
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function createCodexDesktopLauncher(options = {}) {
|
|
1184
|
+
const execFileImpl = options.execFileImpl ?? execFile;
|
|
1185
|
+
const statePath = options.statePath ?? DEFAULT_CODEX_DESKTOP_STATE_PATH;
|
|
1186
|
+
const readFileImpl = options.readFileImpl ?? (async (path)=>(0, promises_namespaceObject.readFile)(path, "utf8"));
|
|
1187
|
+
const writeFileImpl = options.writeFileImpl ?? promises_namespaceObject.writeFile;
|
|
1188
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1189
|
+
const createWebSocketImpl = options.createWebSocketImpl ?? createDefaultWebSocket;
|
|
1190
|
+
async function findInstalledApp() {
|
|
1191
|
+
const candidates = [
|
|
1192
|
+
"/Applications/Codex.app",
|
|
1193
|
+
(0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.homedir)(), "Applications", "Codex.app")
|
|
1194
|
+
];
|
|
1195
|
+
for (const candidate of candidates)if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
|
|
1196
|
+
try {
|
|
1197
|
+
const { stdout } = await execFileImpl("mdfind", [
|
|
1198
|
+
'kMDItemFSName == "Codex.app"'
|
|
1199
|
+
]);
|
|
1200
|
+
for (const line of stdout.split("\n")){
|
|
1201
|
+
const candidate = line.trim();
|
|
1202
|
+
if ("" !== candidate) {
|
|
1203
|
+
if (await pathExistsViaStat(execFileImpl, candidate)) return candidate;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
} catch {}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
async function listRunningApps() {
|
|
1210
|
+
const { stdout } = await execFileImpl("ps", [
|
|
1211
|
+
"-Ao",
|
|
1212
|
+
"pid=,command="
|
|
1213
|
+
]);
|
|
1214
|
+
const running = [];
|
|
1215
|
+
for (const line of stdout.split("\n")){
|
|
1216
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
1217
|
+
if (!match) continue;
|
|
1218
|
+
const pid = Number(match[1]);
|
|
1219
|
+
const command = match[2];
|
|
1220
|
+
if (pid !== process.pid && command.includes(CODEX_BINARY_SUFFIX)) running.push({
|
|
1221
|
+
pid,
|
|
1222
|
+
command
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
return running;
|
|
1226
|
+
}
|
|
1227
|
+
async function quitRunningApps() {
|
|
1228
|
+
const running = await listRunningApps();
|
|
1229
|
+
if (0 === running.length) return;
|
|
1230
|
+
await execFileImpl("osascript", [
|
|
1231
|
+
"-e",
|
|
1232
|
+
`tell application "${CODEX_APP_NAME}" to quit`
|
|
1233
|
+
]);
|
|
1234
|
+
for(let attempt = 0; attempt < 10; attempt += 1){
|
|
1235
|
+
const remaining = await listRunningApps();
|
|
1236
|
+
if (0 === remaining.length) return;
|
|
1237
|
+
await codex_desktop_launch_delay(300);
|
|
1238
|
+
}
|
|
1239
|
+
throw new Error("Timed out waiting for Codex Desktop to quit.");
|
|
1240
|
+
}
|
|
1241
|
+
async function launch(appPath) {
|
|
1242
|
+
await execFileImpl("open", [
|
|
1243
|
+
"-na",
|
|
1244
|
+
appPath,
|
|
1245
|
+
"--args",
|
|
1246
|
+
`--remote-debugging-port=${DEFAULT_CODEX_REMOTE_DEBUGGING_PORT}`
|
|
1247
|
+
]);
|
|
1248
|
+
}
|
|
1249
|
+
async function readManagedState() {
|
|
1250
|
+
try {
|
|
1251
|
+
return parseManagedState(await readFileImpl(statePath));
|
|
1252
|
+
} catch {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function writeManagedState(state) {
|
|
1257
|
+
await ensureStateDirectory(statePath);
|
|
1258
|
+
await writeFileImpl(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
1259
|
+
}
|
|
1260
|
+
async function clearManagedState() {
|
|
1261
|
+
await ensureStateDirectory(statePath);
|
|
1262
|
+
await writeFileImpl(statePath, "");
|
|
1263
|
+
}
|
|
1264
|
+
async function isManagedDesktopRunning() {
|
|
1265
|
+
const state = await readManagedState();
|
|
1266
|
+
if (!state) return false;
|
|
1267
|
+
const runningApps = await listRunningApps();
|
|
1268
|
+
return isManagedDesktopProcess(runningApps, state);
|
|
1269
|
+
}
|
|
1270
|
+
async function restartManagedAppServer() {
|
|
1271
|
+
const state = await readManagedState();
|
|
1272
|
+
if (!state) return false;
|
|
1273
|
+
const runningApps = await listRunningApps();
|
|
1274
|
+
if (!isManagedDesktopProcess(runningApps, state)) return false;
|
|
1275
|
+
const response = await fetchImpl(`http://127.0.0.1:${state.remote_debugging_port}/json/list`);
|
|
1276
|
+
if (!response.ok) throw new Error(`Failed to query Codex Desktop devtools targets (HTTP ${response.status}).`);
|
|
1277
|
+
const targets = await response.json();
|
|
1278
|
+
if (!Array.isArray(targets)) throw new Error("Codex Desktop devtools target list was not an array.");
|
|
1279
|
+
const localTarget = targets.find((target)=>{
|
|
1280
|
+
if (!isDevtoolsTarget(target)) return false;
|
|
1281
|
+
return "page" === target.type && target.url === `app://-/index.html?hostId=${CODEX_LOCAL_HOST_ID}` && isNonEmptyString(target.webSocketDebuggerUrl);
|
|
1282
|
+
});
|
|
1283
|
+
if (!localTarget || !isNonEmptyString(localTarget.webSocketDebuggerUrl)) throw new Error("Could not find the local Codex Desktop devtools target.");
|
|
1284
|
+
await evaluateDevtoolsExpression(createWebSocketImpl, localTarget.webSocketDebuggerUrl, CODEX_APP_SERVER_RESTART_EXPRESSION);
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
findInstalledApp,
|
|
1289
|
+
listRunningApps,
|
|
1290
|
+
quitRunningApps,
|
|
1291
|
+
launch,
|
|
1292
|
+
readManagedState,
|
|
1293
|
+
writeManagedState,
|
|
1294
|
+
clearManagedState,
|
|
1295
|
+
isManagedDesktopRunning,
|
|
1296
|
+
restartManagedAppServer
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1049
1299
|
external_dayjs_default().extend(utc_js_default());
|
|
1050
1300
|
external_dayjs_default().extend(timezone_js_default());
|
|
1051
1301
|
function parseArgs(argv) {
|
|
@@ -1089,12 +1339,29 @@ Usage:
|
|
|
1089
1339
|
codexm update [--json]
|
|
1090
1340
|
codexm switch <name> [--json]
|
|
1091
1341
|
codexm switch --auto [--dry-run] [--json]
|
|
1342
|
+
codexm launch [name] [--json]
|
|
1092
1343
|
codexm remove <name> [--yes] [--json]
|
|
1093
1344
|
codexm rename <old> <new> [--json]
|
|
1094
1345
|
|
|
1095
1346
|
Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
1096
1347
|
`);
|
|
1097
1348
|
}
|
|
1349
|
+
function stripManagedDesktopWarning(warnings) {
|
|
1350
|
+
return warnings.filter((warning)=>!warning.startsWith("Detected running codex processes (") || !warning.endsWith("Existing sessions may still hold the previous login state."));
|
|
1351
|
+
}
|
|
1352
|
+
async function refreshManagedDesktopAfterSwitch(warnings, desktopLauncher) {
|
|
1353
|
+
try {
|
|
1354
|
+
if (await desktopLauncher.restartManagedAppServer()) return;
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
warnings.push(`Failed to refresh the running codexm-managed Codex Desktop session: ${error.message}`);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1361
|
+
if (0 === runningApps.length) return;
|
|
1362
|
+
warnings.push(`Detected running codex processes (${runningApps.map((app)=>app.pid).join(", ")}). Existing sessions may still hold the previous login state.`);
|
|
1363
|
+
} catch {}
|
|
1364
|
+
}
|
|
1098
1365
|
function describeCurrentStatus(status) {
|
|
1099
1366
|
const lines = [];
|
|
1100
1367
|
if (status.exists) {
|
|
@@ -1289,6 +1556,67 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1289
1556
|
streams.stdin.on("data", onData);
|
|
1290
1557
|
});
|
|
1291
1558
|
}
|
|
1559
|
+
async function confirmDesktopRelaunch(streams) {
|
|
1560
|
+
if (!streams.stdin.isTTY) throw new Error("Refusing to relaunch Codex Desktop in a non-interactive terminal.");
|
|
1561
|
+
streams.stdout.write("Codex Desktop is already running. Close it and relaunch with the selected auth? [y/N] ");
|
|
1562
|
+
return await new Promise((resolve)=>{
|
|
1563
|
+
const cleanup = ()=>{
|
|
1564
|
+
streams.stdin.off("data", onData);
|
|
1565
|
+
streams.stdin.pause();
|
|
1566
|
+
};
|
|
1567
|
+
const onData = (buffer)=>{
|
|
1568
|
+
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1569
|
+
cleanup();
|
|
1570
|
+
streams.stdout.write("\n");
|
|
1571
|
+
resolve("y" === answer || "yes" === answer);
|
|
1572
|
+
};
|
|
1573
|
+
streams.stdin.resume();
|
|
1574
|
+
streams.stdin.on("data", onData);
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
async function sleep(ms) {
|
|
1578
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1579
|
+
}
|
|
1580
|
+
async function main_pathExists(path) {
|
|
1581
|
+
try {
|
|
1582
|
+
await (0, promises_namespaceObject.stat)(path);
|
|
1583
|
+
return true;
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
const nodeError = error;
|
|
1586
|
+
if ("ENOENT" === nodeError.code) return false;
|
|
1587
|
+
throw error;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function isRunningDesktopFromApp(app, appPath) {
|
|
1591
|
+
return app.command.includes(`${appPath}/Contents/MacOS/Codex`);
|
|
1592
|
+
}
|
|
1593
|
+
async function resolveManagedDesktopState(desktopLauncher, appPath, existingApps) {
|
|
1594
|
+
const existingPids = new Set(existingApps.map((app)=>app.pid));
|
|
1595
|
+
for(let attempt = 0; attempt < 10; attempt += 1){
|
|
1596
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1597
|
+
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;
|
|
1598
|
+
if (launchedApp) return {
|
|
1599
|
+
pid: launchedApp.pid,
|
|
1600
|
+
app_path: appPath,
|
|
1601
|
+
remote_debugging_port: DEFAULT_CODEX_REMOTE_DEBUGGING_PORT,
|
|
1602
|
+
managed_by_codexm: true,
|
|
1603
|
+
started_at: new Date().toISOString()
|
|
1604
|
+
};
|
|
1605
|
+
await sleep(300);
|
|
1606
|
+
}
|
|
1607
|
+
return null;
|
|
1608
|
+
}
|
|
1609
|
+
async function restoreLaunchBackup(store, backupPath) {
|
|
1610
|
+
if (backupPath && await main_pathExists(backupPath)) await (0, promises_namespaceObject.copyFile)(backupPath, store.paths.currentAuthPath);
|
|
1611
|
+
else await (0, promises_namespaceObject.rm)(store.paths.currentAuthPath, {
|
|
1612
|
+
force: true
|
|
1613
|
+
});
|
|
1614
|
+
const configBackupPath = (0, external_node_path_namespaceObject.join)(store.paths.backupsDir, "last-active-config.toml");
|
|
1615
|
+
if (await main_pathExists(configBackupPath)) await (0, promises_namespaceObject.copyFile)(configBackupPath, store.paths.currentConfigPath);
|
|
1616
|
+
else await (0, promises_namespaceObject.rm)(store.paths.currentConfigPath, {
|
|
1617
|
+
force: true
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1292
1620
|
async function runCli(argv, options = {}) {
|
|
1293
1621
|
const streams = {
|
|
1294
1622
|
stdin: options.stdin ?? external_node_process_namespaceObject.stdin,
|
|
@@ -1296,6 +1624,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1296
1624
|
stderr: options.stderr ?? external_node_process_namespaceObject.stderr
|
|
1297
1625
|
};
|
|
1298
1626
|
const store = options.store ?? createAccountStore();
|
|
1627
|
+
const desktopLauncher = options.desktopLauncher ?? createCodexDesktopLauncher();
|
|
1299
1628
|
const parsed = parseArgs(argv);
|
|
1300
1629
|
const json = parsed.flags.has("--json");
|
|
1301
1630
|
try {
|
|
@@ -1428,6 +1757,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1428
1757
|
}
|
|
1429
1758
|
const result = await store.switchAccount(selected.name);
|
|
1430
1759
|
for (const warning of warnings)result.warnings.push(warning);
|
|
1760
|
+
result.warnings = stripManagedDesktopWarning(result.warnings);
|
|
1761
|
+
await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
|
|
1431
1762
|
const payload = {
|
|
1432
1763
|
ok: true,
|
|
1433
1764
|
action: "switch",
|
|
@@ -1451,6 +1782,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1451
1782
|
}
|
|
1452
1783
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1453
1784
|
const result = await store.switchAccount(name);
|
|
1785
|
+
result.warnings = stripManagedDesktopWarning(result.warnings);
|
|
1786
|
+
await refreshManagedDesktopAfterSwitch(result.warnings, desktopLauncher);
|
|
1454
1787
|
let quota = null;
|
|
1455
1788
|
try {
|
|
1456
1789
|
await store.refreshQuotaForAccount(result.account.name);
|
|
@@ -1482,6 +1815,70 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1482
1815
|
}
|
|
1483
1816
|
return 0;
|
|
1484
1817
|
}
|
|
1818
|
+
case "launch":
|
|
1819
|
+
{
|
|
1820
|
+
const name = parsed.positionals[0] ?? null;
|
|
1821
|
+
if (parsed.positionals.length > 1) throw new Error("Usage: codexm launch [name] [--json]");
|
|
1822
|
+
const warnings = [];
|
|
1823
|
+
const appPath = await desktopLauncher.findInstalledApp();
|
|
1824
|
+
if (!appPath) throw new Error("Codex Desktop not found at /Applications/Codex.app.");
|
|
1825
|
+
const runningApps = await desktopLauncher.listRunningApps();
|
|
1826
|
+
if (runningApps.length > 0) {
|
|
1827
|
+
const confirmed = await confirmDesktopRelaunch(streams);
|
|
1828
|
+
if (!confirmed) {
|
|
1829
|
+
if (json) writeJson(streams.stdout, {
|
|
1830
|
+
ok: false,
|
|
1831
|
+
action: "launch",
|
|
1832
|
+
cancelled: true
|
|
1833
|
+
});
|
|
1834
|
+
else streams.stdout.write("Aborted.\n");
|
|
1835
|
+
return 1;
|
|
1836
|
+
}
|
|
1837
|
+
await desktopLauncher.quitRunningApps();
|
|
1838
|
+
}
|
|
1839
|
+
let switchedAccount = null;
|
|
1840
|
+
let switchBackupPath = null;
|
|
1841
|
+
if (name) {
|
|
1842
|
+
const switchResult = await store.switchAccount(name);
|
|
1843
|
+
warnings.push(...stripManagedDesktopWarning(switchResult.warnings));
|
|
1844
|
+
switchedAccount = switchResult.account;
|
|
1845
|
+
switchBackupPath = switchResult.backup_path;
|
|
1846
|
+
}
|
|
1847
|
+
try {
|
|
1848
|
+
await desktopLauncher.launch(appPath);
|
|
1849
|
+
const managedState = await resolveManagedDesktopState(desktopLauncher, appPath, runningApps);
|
|
1850
|
+
if (!managedState) {
|
|
1851
|
+
await desktopLauncher.clearManagedState().catch(()=>void 0);
|
|
1852
|
+
throw new Error("Failed to confirm the newly launched Codex Desktop process for managed-session tracking.");
|
|
1853
|
+
}
|
|
1854
|
+
await desktopLauncher.writeManagedState(managedState);
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
if (switchedAccount) await restoreLaunchBackup(store, switchBackupPath).catch(()=>void 0);
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
if (json) writeJson(streams.stdout, {
|
|
1860
|
+
ok: true,
|
|
1861
|
+
action: "launch",
|
|
1862
|
+
account: switchedAccount ? {
|
|
1863
|
+
name: switchedAccount.name,
|
|
1864
|
+
account_id: switchedAccount.account_id,
|
|
1865
|
+
user_id: switchedAccount.user_id ?? null,
|
|
1866
|
+
identity: switchedAccount.identity,
|
|
1867
|
+
auth_mode: switchedAccount.auth_mode
|
|
1868
|
+
} : null,
|
|
1869
|
+
launched_with_current_auth: null === switchedAccount,
|
|
1870
|
+
app_path: appPath,
|
|
1871
|
+
relaunched: runningApps.length > 0,
|
|
1872
|
+
warnings
|
|
1873
|
+
});
|
|
1874
|
+
else {
|
|
1875
|
+
if (switchedAccount) streams.stdout.write(`Switched to "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n`);
|
|
1876
|
+
if (runningApps.length > 0) streams.stdout.write("Closed existing Codex Desktop instance and launched a new one.\n");
|
|
1877
|
+
streams.stdout.write(switchedAccount ? `Launched Codex Desktop with "${switchedAccount.name}" (${maskAccountId(switchedAccount.identity)}).\n` : "Launched Codex Desktop with current auth.\n");
|
|
1878
|
+
for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
|
|
1879
|
+
}
|
|
1880
|
+
return 0;
|
|
1881
|
+
}
|
|
1485
1882
|
case "remove":
|
|
1486
1883
|
{
|
|
1487
1884
|
const name = parsed.positionals[0];
|