codex-team 0.0.1
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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/cli.cjs +1293 -0
- package/dist/cli.js +8 -0
- package/dist/main.cjs +1273 -0
- package/dist/main.js +1227 -0
- package/package.json +63 -0
package/dist/main.cjs
ADDED
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
14
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: definition[key]
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
(()=>{
|
|
21
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
})();
|
|
23
|
+
(()=>{
|
|
24
|
+
__webpack_require__.r = (exports1)=>{
|
|
25
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
26
|
+
value: 'Module'
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
29
|
+
value: true
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
})();
|
|
33
|
+
var __webpack_exports__ = {};
|
|
34
|
+
__webpack_require__.r(__webpack_exports__);
|
|
35
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
+
runCli: ()=>runCli
|
|
37
|
+
});
|
|
38
|
+
const external_node_process_namespaceObject = require("node:process");
|
|
39
|
+
const external_dayjs_namespaceObject = require("dayjs");
|
|
40
|
+
var external_dayjs_default = /*#__PURE__*/ __webpack_require__.n(external_dayjs_namespaceObject);
|
|
41
|
+
const timezone_js_namespaceObject = require("dayjs/plugin/timezone.js");
|
|
42
|
+
var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namespaceObject);
|
|
43
|
+
const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
|
|
44
|
+
var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
|
|
45
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
function asNonEmptyString(value, fieldName) {
|
|
50
|
+
if ("string" != typeof value || "" === value.trim()) throw new Error(`Field "${fieldName}" must be a non-empty string.`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function asOptionalString(value, fieldName) {
|
|
54
|
+
if (null == value) return;
|
|
55
|
+
return asNonEmptyString(value, fieldName);
|
|
56
|
+
}
|
|
57
|
+
function asOptionalBoolean(value, fieldName) {
|
|
58
|
+
if (null == value) return;
|
|
59
|
+
if ("boolean" != typeof value) throw new Error(`Field "${fieldName}" must be a boolean.`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function asOptionalNumber(value, fieldName) {
|
|
63
|
+
if (null == value) return;
|
|
64
|
+
if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function defaultQuotaSnapshot() {
|
|
68
|
+
return {
|
|
69
|
+
status: "stale"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function parseQuotaSnapshot(raw) {
|
|
73
|
+
if (null == raw) return defaultQuotaSnapshot();
|
|
74
|
+
if (!isRecord(raw)) throw new Error('Field "quota" must be an object.');
|
|
75
|
+
const status = raw.status;
|
|
76
|
+
if ("ok" !== status && "stale" !== status && "error" !== status && "unsupported" !== status) throw new Error('Field "quota.status" must be one of ok/stale/error/unsupported.');
|
|
77
|
+
return {
|
|
78
|
+
status,
|
|
79
|
+
plan_type: asOptionalString(raw.plan_type, "quota.plan_type"),
|
|
80
|
+
credits_balance: asOptionalNumber(raw.credits_balance, "quota.credits_balance"),
|
|
81
|
+
fetched_at: asOptionalString(raw.fetched_at, "quota.fetched_at"),
|
|
82
|
+
error_message: asOptionalString(raw.error_message, "quota.error_message"),
|
|
83
|
+
unlimited: asOptionalBoolean(raw.unlimited, "quota.unlimited"),
|
|
84
|
+
five_hour: parseQuotaWindowSnapshot(raw.five_hour, "quota.five_hour"),
|
|
85
|
+
one_week: parseQuotaWindowSnapshot(raw.one_week, "quota.one_week")
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseQuotaWindowSnapshot(raw, fieldName) {
|
|
89
|
+
if (null == raw) return;
|
|
90
|
+
if (!isRecord(raw)) throw new Error(`Field "${fieldName}" must be an object.`);
|
|
91
|
+
return {
|
|
92
|
+
used_percent: asNonEmptyNumber(raw.used_percent, `${fieldName}.used_percent`),
|
|
93
|
+
window_seconds: asNonEmptyNumber(raw.window_seconds, `${fieldName}.window_seconds`),
|
|
94
|
+
reset_after_seconds: asOptionalNumber(raw.reset_after_seconds, `${fieldName}.reset_after_seconds`),
|
|
95
|
+
reset_at: asOptionalString(raw.reset_at, `${fieldName}.reset_at`)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function asNonEmptyNumber(value, fieldName) {
|
|
99
|
+
if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function parseAuthSnapshot(raw) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(raw);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Failed to parse auth snapshot JSON: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
|
|
110
|
+
const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
|
|
111
|
+
if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
|
|
112
|
+
const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
|
|
113
|
+
return {
|
|
114
|
+
...parsed,
|
|
115
|
+
auth_mode: authMode,
|
|
116
|
+
tokens: {
|
|
117
|
+
...parsed.tokens,
|
|
118
|
+
account_id: accountId
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function readAuthSnapshotFile(filePath) {
|
|
123
|
+
const raw = await (0, promises_namespaceObject.readFile)(filePath, "utf8");
|
|
124
|
+
return parseAuthSnapshot(raw);
|
|
125
|
+
}
|
|
126
|
+
function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
|
|
127
|
+
const timestamp = now.toISOString();
|
|
128
|
+
return {
|
|
129
|
+
name,
|
|
130
|
+
auth_mode: snapshot.auth_mode,
|
|
131
|
+
account_id: snapshot.tokens.account_id,
|
|
132
|
+
created_at: existingCreatedAt ?? timestamp,
|
|
133
|
+
updated_at: timestamp,
|
|
134
|
+
last_switched_at: null,
|
|
135
|
+
quota: defaultQuotaSnapshot()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function parseSnapshotMeta(raw) {
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(raw);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`Failed to parse account metadata JSON: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(parsed)) throw new Error("Account metadata must be a JSON object.");
|
|
146
|
+
const lastSwitchedAt = parsed.last_switched_at;
|
|
147
|
+
if (null !== lastSwitchedAt && "string" != typeof lastSwitchedAt) throw new Error('Field "last_switched_at" must be a string or null.');
|
|
148
|
+
return {
|
|
149
|
+
name: asNonEmptyString(parsed.name, "name"),
|
|
150
|
+
auth_mode: asNonEmptyString(parsed.auth_mode, "auth_mode"),
|
|
151
|
+
account_id: asNonEmptyString(parsed.account_id, "account_id"),
|
|
152
|
+
created_at: asNonEmptyString(parsed.created_at, "created_at"),
|
|
153
|
+
updated_at: asNonEmptyString(parsed.updated_at, "updated_at"),
|
|
154
|
+
last_switched_at: lastSwitchedAt,
|
|
155
|
+
quota: parseQuotaSnapshot(parsed.quota)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function maskAccountId(accountId) {
|
|
159
|
+
if (accountId.length <= 10) return accountId;
|
|
160
|
+
return `${accountId.slice(0, 6)}...${accountId.slice(-4)}`;
|
|
161
|
+
}
|
|
162
|
+
function decodeJwtPayload(token) {
|
|
163
|
+
const payload = token.split(".")[1];
|
|
164
|
+
if (!payload) throw new Error("Token payload is missing.");
|
|
165
|
+
const padded = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, "=");
|
|
166
|
+
const decoded = Buffer.from(padded, "base64url").toString("utf8");
|
|
167
|
+
const parsed = JSON.parse(decoded);
|
|
168
|
+
if (!isRecord(parsed)) throw new Error("Token payload must be a JSON object.");
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
172
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
173
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
174
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
175
|
+
const DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com";
|
|
176
|
+
const USER_AGENT = "codexm/0.1";
|
|
177
|
+
function quota_client_isRecord(value) {
|
|
178
|
+
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function extractAuthClaim(payload) {
|
|
181
|
+
const value = payload["https://api.openai.com/auth"];
|
|
182
|
+
return quota_client_isRecord(value) ? value : void 0;
|
|
183
|
+
}
|
|
184
|
+
function extractStringClaim(payload, key) {
|
|
185
|
+
const value = payload[key];
|
|
186
|
+
return "string" == typeof value && "" !== value.trim() ? value : void 0;
|
|
187
|
+
}
|
|
188
|
+
function isSupportedChatGPTMode(authMode) {
|
|
189
|
+
const normalized = authMode.trim().toLowerCase();
|
|
190
|
+
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
191
|
+
}
|
|
192
|
+
function parsePlanType(snapshot) {
|
|
193
|
+
for (const tokenName of [
|
|
194
|
+
"id_token",
|
|
195
|
+
"access_token"
|
|
196
|
+
]){
|
|
197
|
+
const token = snapshot.tokens[tokenName];
|
|
198
|
+
if ("string" == typeof token && "" !== token.trim()) try {
|
|
199
|
+
const payload = decodeJwtPayload(token);
|
|
200
|
+
const authClaim = extractAuthClaim(payload);
|
|
201
|
+
const planType = authClaim?.chatgpt_plan_type;
|
|
202
|
+
if ("string" == typeof planType && "" !== planType.trim()) return planType;
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function extractChatGPTAuth(snapshot) {
|
|
207
|
+
const authMode = snapshot.auth_mode ?? "";
|
|
208
|
+
const supported = isSupportedChatGPTMode(authMode);
|
|
209
|
+
const accessTokenValue = snapshot.tokens.access_token;
|
|
210
|
+
const refreshTokenValue = snapshot.tokens.refresh_token;
|
|
211
|
+
const directAccountId = snapshot.tokens.account_id;
|
|
212
|
+
let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
|
|
213
|
+
let planType;
|
|
214
|
+
let issuer;
|
|
215
|
+
let clientId;
|
|
216
|
+
for (const tokenName of [
|
|
217
|
+
"id_token",
|
|
218
|
+
"access_token"
|
|
219
|
+
]){
|
|
220
|
+
const token = snapshot.tokens[tokenName];
|
|
221
|
+
if ("string" == typeof token && "" !== token.trim()) try {
|
|
222
|
+
const payload = decodeJwtPayload(token);
|
|
223
|
+
const authClaim = extractAuthClaim(payload);
|
|
224
|
+
if (!accountId) {
|
|
225
|
+
const maybeAccountId = authClaim?.chatgpt_account_id;
|
|
226
|
+
if ("string" == typeof maybeAccountId && "" !== maybeAccountId.trim()) accountId = maybeAccountId;
|
|
227
|
+
}
|
|
228
|
+
if (!planType) {
|
|
229
|
+
const maybePlanType = authClaim?.chatgpt_plan_type;
|
|
230
|
+
if ("string" == typeof maybePlanType && "" !== maybePlanType.trim()) planType = maybePlanType;
|
|
231
|
+
}
|
|
232
|
+
issuer ??= extractStringClaim(payload, "iss");
|
|
233
|
+
clientId ??= extractStringClaim(payload, "client_id") ?? extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
if (!supported) return {
|
|
237
|
+
accessToken: "string" == typeof accessTokenValue ? accessTokenValue : "",
|
|
238
|
+
accountId: accountId ?? "",
|
|
239
|
+
refreshToken: "string" == typeof refreshTokenValue && "" !== refreshTokenValue.trim() ? refreshTokenValue : void 0,
|
|
240
|
+
planType,
|
|
241
|
+
issuer,
|
|
242
|
+
clientId,
|
|
243
|
+
supported: false
|
|
244
|
+
};
|
|
245
|
+
if ("string" != typeof accessTokenValue || "" === accessTokenValue.trim()) throw new Error("auth.json is missing access_token.");
|
|
246
|
+
if (!accountId) throw new Error("auth.json is missing ChatGPT account_id.");
|
|
247
|
+
return {
|
|
248
|
+
accessToken: accessTokenValue,
|
|
249
|
+
accountId,
|
|
250
|
+
refreshToken: "string" == typeof refreshTokenValue && "" !== refreshTokenValue.trim() ? refreshTokenValue : void 0,
|
|
251
|
+
planType,
|
|
252
|
+
issuer,
|
|
253
|
+
clientId,
|
|
254
|
+
supported: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function readChatGPTBaseUrl(homeDir) {
|
|
258
|
+
if (!homeDir) return DEFAULT_CHATGPT_BASE_URL;
|
|
259
|
+
try {
|
|
260
|
+
const config = await (0, promises_namespaceObject.readFile)((0, external_node_path_namespaceObject.join)(homeDir, ".codex", "config.toml"), "utf8");
|
|
261
|
+
for (const line of config.split(/\r?\n/u)){
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
if (!trimmed.startsWith("chatgpt_base_url")) continue;
|
|
264
|
+
const [, rawValue] = trimmed.split("=", 2);
|
|
265
|
+
const value = rawValue?.trim().replace(/^['"]|['"]$/gu, "");
|
|
266
|
+
if (value) return value.replace(/\/+$/u, "");
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
return DEFAULT_CHATGPT_BASE_URL;
|
|
270
|
+
}
|
|
271
|
+
async function resolveUsageUrls(homeDir) {
|
|
272
|
+
const baseUrl = await readChatGPTBaseUrl(homeDir);
|
|
273
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/u, "");
|
|
274
|
+
const candidates = [
|
|
275
|
+
`${normalizedBaseUrl}/backend-api/wham/usage`,
|
|
276
|
+
`${normalizedBaseUrl}/wham/usage`,
|
|
277
|
+
`${normalizedBaseUrl}/api/codex/usage`,
|
|
278
|
+
"https://chatgpt.com/backend-api/wham/usage"
|
|
279
|
+
];
|
|
280
|
+
return [
|
|
281
|
+
...new Set(candidates)
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
function normalizeFetchError(error) {
|
|
285
|
+
return error instanceof Error ? error.message : String(error);
|
|
286
|
+
}
|
|
287
|
+
function shouldRetryWithTokenRefresh(message) {
|
|
288
|
+
const normalized = message.toLowerCase();
|
|
289
|
+
return normalized.includes("401") || normalized.includes("403") || normalized.includes("unauthorized") || normalized.includes("invalid_token") || normalized.includes("deactivated_workspace");
|
|
290
|
+
}
|
|
291
|
+
function parseCreditsBalance(balance) {
|
|
292
|
+
if (null == balance) return;
|
|
293
|
+
const normalized = balance.trim().toLowerCase();
|
|
294
|
+
if ("" === normalized || "null" === normalized || "none" === normalized || "nan" === normalized) return;
|
|
295
|
+
const numeric = Number.parseFloat(balance);
|
|
296
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
297
|
+
throw new Error(`Invalid credits balance "${balance}".`);
|
|
298
|
+
}
|
|
299
|
+
function mapUsagePayload(payload, fallbackPlanType, fetchedAt) {
|
|
300
|
+
if (!payload.credits) throw new Error('Usage response is missing the "credits" field.');
|
|
301
|
+
const windows = collectUsageWindows(payload);
|
|
302
|
+
return {
|
|
303
|
+
status: "ok",
|
|
304
|
+
plan_type: payload.plan_type ?? fallbackPlanType,
|
|
305
|
+
credits_balance: parseCreditsBalance(payload.credits.balance),
|
|
306
|
+
fetched_at: fetchedAt,
|
|
307
|
+
unlimited: true === payload.credits.unlimited,
|
|
308
|
+
five_hour: pickNearestWindow(windows, 18000),
|
|
309
|
+
one_week: pickNearestWindow(windows, 604800)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function collectUsageWindows(payload) {
|
|
313
|
+
const windows = [];
|
|
314
|
+
const pushRateLimit = (rateLimit)=>{
|
|
315
|
+
if (!rateLimit) return;
|
|
316
|
+
if (rateLimit.primary_window) windows.push(rateLimit.primary_window);
|
|
317
|
+
if (rateLimit.secondary_window) windows.push(rateLimit.secondary_window);
|
|
318
|
+
};
|
|
319
|
+
pushRateLimit(payload.rate_limit);
|
|
320
|
+
for (const additional of payload.additional_rate_limits ?? [])pushRateLimit(additional.rate_limit);
|
|
321
|
+
return windows;
|
|
322
|
+
}
|
|
323
|
+
function pickNearestWindow(windows, targetSeconds) {
|
|
324
|
+
const nearest = windows.reduce((best, current)=>{
|
|
325
|
+
if (!best) return current;
|
|
326
|
+
return Math.abs(current.limit_window_seconds - targetSeconds) < Math.abs(best.limit_window_seconds - targetSeconds) ? current : best;
|
|
327
|
+
}, void 0);
|
|
328
|
+
if (!nearest) return;
|
|
329
|
+
return {
|
|
330
|
+
used_percent: nearest.used_percent,
|
|
331
|
+
window_seconds: nearest.limit_window_seconds,
|
|
332
|
+
reset_after_seconds: nearest.reset_after_seconds,
|
|
333
|
+
reset_at: "number" == typeof nearest.reset_at ? new Date(1000 * nearest.reset_at).toISOString() : void 0
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function requestUsage(snapshot, options) {
|
|
337
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
338
|
+
const extracted = extractChatGPTAuth(snapshot);
|
|
339
|
+
const urls = await resolveUsageUrls(options.homeDir);
|
|
340
|
+
const now = (options.now ?? new Date()).toISOString();
|
|
341
|
+
const errors = [];
|
|
342
|
+
for (const url of urls){
|
|
343
|
+
let response;
|
|
344
|
+
try {
|
|
345
|
+
response = await fetchImpl(url, {
|
|
346
|
+
method: "GET",
|
|
347
|
+
headers: {
|
|
348
|
+
Authorization: `Bearer ${extracted.accessToken}`,
|
|
349
|
+
"ChatGPT-Account-Id": extracted.accountId,
|
|
350
|
+
Accept: "application/json",
|
|
351
|
+
"User-Agent": USER_AGENT
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
errors.push(`${url} -> ${normalizeFetchError(error)}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
const body = await response.text();
|
|
360
|
+
errors.push(`${url} -> ${response.status}: ${body.slice(0, 140).replace(/\s+/gu, " ").trim()}`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
let payload;
|
|
364
|
+
try {
|
|
365
|
+
payload = await response.json();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
errors.push(`${url} -> failed to parse JSON: ${normalizeFetchError(error)}`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
return mapUsagePayload(payload, extracted.planType, now);
|
|
371
|
+
}
|
|
372
|
+
throw new Error(0 === errors.length ? "Usage request failed: no candidate URL was attempted." : `Usage request failed: ${errors.join(" | ")}`);
|
|
373
|
+
}
|
|
374
|
+
async function refreshChatGPTAuthTokens(snapshot, options) {
|
|
375
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
376
|
+
const extracted = extractChatGPTAuth(snapshot);
|
|
377
|
+
if (!extracted.refreshToken) throw new Error("auth.json is missing refresh_token.");
|
|
378
|
+
const tokenUrl = `${(extracted.issuer ?? "https://auth.openai.com").replace(/\/+$/u, "")}/oauth/token`;
|
|
379
|
+
const body = new URLSearchParams({
|
|
380
|
+
grant_type: "refresh_token",
|
|
381
|
+
refresh_token: extracted.refreshToken
|
|
382
|
+
});
|
|
383
|
+
if (extracted.clientId) body.set("client_id", extracted.clientId);
|
|
384
|
+
const response = await fetchImpl(tokenUrl, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: {
|
|
387
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
388
|
+
Accept: "application/json",
|
|
389
|
+
"User-Agent": USER_AGENT
|
|
390
|
+
},
|
|
391
|
+
body
|
|
392
|
+
});
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
const errorText = await response.text();
|
|
395
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText.slice(0, 140).replace(/\s+/gu, " ").trim()}`);
|
|
396
|
+
}
|
|
397
|
+
const payload = await response.json();
|
|
398
|
+
const nextSnapshot = {
|
|
399
|
+
...snapshot,
|
|
400
|
+
last_refresh: (options.now ?? new Date()).toISOString(),
|
|
401
|
+
tokens: {
|
|
402
|
+
...snapshot.tokens,
|
|
403
|
+
access_token: payload.access_token,
|
|
404
|
+
id_token: payload.id_token,
|
|
405
|
+
refresh_token: payload.refresh_token ?? extracted.refreshToken,
|
|
406
|
+
account_id: extracted.accountId
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return nextSnapshot;
|
|
410
|
+
}
|
|
411
|
+
async function fetchQuotaSnapshot(snapshot, options = {}) {
|
|
412
|
+
const fetchedAt = (options.now ?? new Date()).toISOString();
|
|
413
|
+
const extracted = extractChatGPTAuth(snapshot);
|
|
414
|
+
if (!extracted.supported) return {
|
|
415
|
+
quota: {
|
|
416
|
+
status: "unsupported",
|
|
417
|
+
plan_type: extracted.planType ?? parsePlanType(snapshot),
|
|
418
|
+
fetched_at: fetchedAt
|
|
419
|
+
},
|
|
420
|
+
authSnapshot: snapshot
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
return {
|
|
424
|
+
quota: await requestUsage(snapshot, options),
|
|
425
|
+
authSnapshot: snapshot
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const message = normalizeFetchError(error);
|
|
429
|
+
if (!extracted.refreshToken || !shouldRetryWithTokenRefresh(message)) throw error;
|
|
430
|
+
const refreshedSnapshot = await refreshChatGPTAuthTokens(snapshot, options);
|
|
431
|
+
return {
|
|
432
|
+
quota: await requestUsage(refreshedSnapshot, options),
|
|
433
|
+
authSnapshot: refreshedSnapshot
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const execFile = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.execFile);
|
|
438
|
+
const DIRECTORY_MODE = 448;
|
|
439
|
+
const FILE_MODE = 384;
|
|
440
|
+
const SCHEMA_VERSION = 1;
|
|
441
|
+
const ACCOUNT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
442
|
+
function defaultPaths(homeDir = (0, external_node_os_namespaceObject.homedir)()) {
|
|
443
|
+
const codexDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex");
|
|
444
|
+
const codexTeamDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex-team");
|
|
445
|
+
return {
|
|
446
|
+
homeDir,
|
|
447
|
+
codexDir,
|
|
448
|
+
codexTeamDir,
|
|
449
|
+
currentAuthPath: (0, external_node_path_namespaceObject.join)(codexDir, "auth.json"),
|
|
450
|
+
accountsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "accounts"),
|
|
451
|
+
backupsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "backups"),
|
|
452
|
+
statePath: (0, external_node_path_namespaceObject.join)(codexTeamDir, "state.json")
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function chmodIfPossible(path, mode) {
|
|
456
|
+
try {
|
|
457
|
+
await (0, promises_namespaceObject.chmod)(path, mode);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const nodeError = error;
|
|
460
|
+
if ("ENOENT" !== nodeError.code) throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function ensureDirectory(path, mode) {
|
|
464
|
+
await (0, promises_namespaceObject.mkdir)(path, {
|
|
465
|
+
recursive: true,
|
|
466
|
+
mode
|
|
467
|
+
});
|
|
468
|
+
await chmodIfPossible(path, mode);
|
|
469
|
+
}
|
|
470
|
+
async function atomicWriteFile(path, content, mode = FILE_MODE) {
|
|
471
|
+
const directory = (0, external_node_path_namespaceObject.dirname)(path);
|
|
472
|
+
const tempPath = (0, external_node_path_namespaceObject.join)(directory, `.${(0, external_node_path_namespaceObject.basename)(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
473
|
+
await ensureDirectory(directory, DIRECTORY_MODE);
|
|
474
|
+
await (0, promises_namespaceObject.writeFile)(tempPath, content, {
|
|
475
|
+
encoding: "utf8",
|
|
476
|
+
mode
|
|
477
|
+
});
|
|
478
|
+
await chmodIfPossible(tempPath, mode);
|
|
479
|
+
await (0, promises_namespaceObject.rename)(tempPath, path);
|
|
480
|
+
await chmodIfPossible(path, mode);
|
|
481
|
+
}
|
|
482
|
+
function stringifyJson(value) {
|
|
483
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
484
|
+
}
|
|
485
|
+
function ensureAccountName(name) {
|
|
486
|
+
if (!ACCOUNT_NAME_PATTERN.test(name)) throw new Error('Account name must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/ and cannot contain path separators.');
|
|
487
|
+
}
|
|
488
|
+
async function pathExists(path) {
|
|
489
|
+
try {
|
|
490
|
+
await (0, promises_namespaceObject.stat)(path);
|
|
491
|
+
return true;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const nodeError = error;
|
|
494
|
+
if ("ENOENT" === nodeError.code) return false;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function readJsonFile(path) {
|
|
499
|
+
return (0, promises_namespaceObject.readFile)(path, "utf8");
|
|
500
|
+
}
|
|
501
|
+
async function detectRunningCodexProcesses() {
|
|
502
|
+
try {
|
|
503
|
+
const { stdout } = await execFile("ps", [
|
|
504
|
+
"-Ao",
|
|
505
|
+
"pid=,command="
|
|
506
|
+
]);
|
|
507
|
+
const pids = [];
|
|
508
|
+
for (const line of stdout.split("\n")){
|
|
509
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
510
|
+
if (!match) continue;
|
|
511
|
+
const pid = Number(match[1]);
|
|
512
|
+
const command = match[2];
|
|
513
|
+
if (pid !== process.pid && /(^|\s|\/)codex(\s|$)/.test(command) && !command.includes("codex-team")) pids.push(pid);
|
|
514
|
+
}
|
|
515
|
+
return pids;
|
|
516
|
+
} catch {
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
class AccountStore {
|
|
521
|
+
paths;
|
|
522
|
+
fetchImpl;
|
|
523
|
+
constructor(paths){
|
|
524
|
+
const resolved = defaultPaths(paths?.homeDir);
|
|
525
|
+
this.paths = {
|
|
526
|
+
...resolved,
|
|
527
|
+
...paths,
|
|
528
|
+
homeDir: paths?.homeDir ?? resolved.homeDir
|
|
529
|
+
};
|
|
530
|
+
this.fetchImpl = paths?.fetchImpl;
|
|
531
|
+
}
|
|
532
|
+
accountDirectory(name) {
|
|
533
|
+
ensureAccountName(name);
|
|
534
|
+
return (0, external_node_path_namespaceObject.join)(this.paths.accountsDir, name);
|
|
535
|
+
}
|
|
536
|
+
accountAuthPath(name) {
|
|
537
|
+
return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "auth.json");
|
|
538
|
+
}
|
|
539
|
+
accountMetaPath(name) {
|
|
540
|
+
return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "meta.json");
|
|
541
|
+
}
|
|
542
|
+
async writeAccountAuthSnapshot(name, snapshot) {
|
|
543
|
+
await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
|
|
544
|
+
}
|
|
545
|
+
async writeAccountMeta(name, meta) {
|
|
546
|
+
await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
|
|
547
|
+
}
|
|
548
|
+
async syncCurrentAuthIfMatching(snapshot) {
|
|
549
|
+
if (!await pathExists(this.paths.currentAuthPath)) return;
|
|
550
|
+
try {
|
|
551
|
+
const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
552
|
+
if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
|
|
553
|
+
await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
|
|
554
|
+
} catch {}
|
|
555
|
+
}
|
|
556
|
+
async quotaSummaryForAccount(account) {
|
|
557
|
+
let planType = account.quota.plan_type ?? null;
|
|
558
|
+
try {
|
|
559
|
+
const snapshot = await readAuthSnapshotFile(account.authPath);
|
|
560
|
+
const extracted = extractChatGPTAuth(snapshot);
|
|
561
|
+
planType ??= extracted.planType ?? null;
|
|
562
|
+
} catch {}
|
|
563
|
+
return {
|
|
564
|
+
name: account.name,
|
|
565
|
+
account_id: account.account_id,
|
|
566
|
+
plan_type: planType,
|
|
567
|
+
credits_balance: account.quota.credits_balance ?? null,
|
|
568
|
+
status: account.quota.status,
|
|
569
|
+
fetched_at: account.quota.fetched_at ?? null,
|
|
570
|
+
error_message: account.quota.error_message ?? null,
|
|
571
|
+
unlimited: true === account.quota.unlimited,
|
|
572
|
+
five_hour: account.quota.five_hour ?? null,
|
|
573
|
+
one_week: account.quota.one_week ?? null
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async ensureLayout() {
|
|
577
|
+
await ensureDirectory(this.paths.codexTeamDir, DIRECTORY_MODE);
|
|
578
|
+
await ensureDirectory(this.paths.accountsDir, DIRECTORY_MODE);
|
|
579
|
+
await ensureDirectory(this.paths.backupsDir, DIRECTORY_MODE);
|
|
580
|
+
}
|
|
581
|
+
async readState() {
|
|
582
|
+
if (!await pathExists(this.paths.statePath)) return {
|
|
583
|
+
schema_version: SCHEMA_VERSION,
|
|
584
|
+
last_switched_account: null,
|
|
585
|
+
last_backup_path: null
|
|
586
|
+
};
|
|
587
|
+
const raw = await readJsonFile(this.paths.statePath);
|
|
588
|
+
const parsed = JSON.parse(raw);
|
|
589
|
+
return {
|
|
590
|
+
schema_version: parsed.schema_version ?? SCHEMA_VERSION,
|
|
591
|
+
last_switched_account: parsed.last_switched_account ?? null,
|
|
592
|
+
last_backup_path: parsed.last_backup_path ?? null
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async writeState(state) {
|
|
596
|
+
await this.ensureLayout();
|
|
597
|
+
await atomicWriteFile(this.paths.statePath, stringifyJson(state));
|
|
598
|
+
}
|
|
599
|
+
async readManagedAccount(name) {
|
|
600
|
+
const metaPath = this.accountMetaPath(name);
|
|
601
|
+
const authPath = this.accountAuthPath(name);
|
|
602
|
+
const [rawMeta, snapshot] = await Promise.all([
|
|
603
|
+
readJsonFile(metaPath),
|
|
604
|
+
readAuthSnapshotFile(authPath)
|
|
605
|
+
]);
|
|
606
|
+
const meta = parseSnapshotMeta(rawMeta);
|
|
607
|
+
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
608
|
+
if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
|
|
609
|
+
return {
|
|
610
|
+
...meta,
|
|
611
|
+
authPath,
|
|
612
|
+
metaPath,
|
|
613
|
+
duplicateAccountId: false
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async listAccounts() {
|
|
617
|
+
await this.ensureLayout();
|
|
618
|
+
const entries = await (0, promises_namespaceObject.readdir)(this.paths.accountsDir, {
|
|
619
|
+
withFileTypes: true
|
|
620
|
+
});
|
|
621
|
+
const accounts = [];
|
|
622
|
+
const warnings = [];
|
|
623
|
+
for (const entry of entries)if (entry.isDirectory()) try {
|
|
624
|
+
accounts.push(await this.readManagedAccount(entry.name));
|
|
625
|
+
} catch (error) {
|
|
626
|
+
warnings.push(`Account "${entry.name}" is invalid: ${error.message}`);
|
|
627
|
+
}
|
|
628
|
+
const counts = new Map();
|
|
629
|
+
for (const account of accounts)counts.set(account.account_id, (counts.get(account.account_id) ?? 0) + 1);
|
|
630
|
+
accounts.sort((left, right)=>left.name.localeCompare(right.name));
|
|
631
|
+
return {
|
|
632
|
+
accounts: accounts.map((account)=>({
|
|
633
|
+
...account,
|
|
634
|
+
duplicateAccountId: (counts.get(account.account_id) ?? 0) > 1
|
|
635
|
+
})),
|
|
636
|
+
warnings
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async getCurrentStatus() {
|
|
640
|
+
const { accounts, warnings } = await this.listAccounts();
|
|
641
|
+
if (!await pathExists(this.paths.currentAuthPath)) return {
|
|
642
|
+
exists: false,
|
|
643
|
+
auth_mode: null,
|
|
644
|
+
account_id: null,
|
|
645
|
+
matched_accounts: [],
|
|
646
|
+
managed: false,
|
|
647
|
+
duplicate_match: false,
|
|
648
|
+
warnings
|
|
649
|
+
};
|
|
650
|
+
const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
651
|
+
const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
|
|
652
|
+
return {
|
|
653
|
+
exists: true,
|
|
654
|
+
auth_mode: snapshot.auth_mode,
|
|
655
|
+
account_id: snapshot.tokens.account_id,
|
|
656
|
+
matched_accounts: matchedAccounts,
|
|
657
|
+
managed: matchedAccounts.length > 0,
|
|
658
|
+
duplicate_match: matchedAccounts.length > 1,
|
|
659
|
+
warnings
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async saveCurrentAccount(name, force = false) {
|
|
663
|
+
ensureAccountName(name);
|
|
664
|
+
await this.ensureLayout();
|
|
665
|
+
if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
|
|
666
|
+
const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
|
|
667
|
+
const snapshot = parseAuthSnapshot(rawSnapshot);
|
|
668
|
+
const accountDir = this.accountDirectory(name);
|
|
669
|
+
const authPath = this.accountAuthPath(name);
|
|
670
|
+
const metaPath = this.accountMetaPath(name);
|
|
671
|
+
const accountExists = await pathExists(accountDir);
|
|
672
|
+
const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
|
|
673
|
+
if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
|
|
674
|
+
await ensureDirectory(accountDir, DIRECTORY_MODE);
|
|
675
|
+
await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
|
|
676
|
+
const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
|
|
677
|
+
meta.last_switched_at = existingMeta?.last_switched_at ?? null;
|
|
678
|
+
meta.quota = existingMeta?.quota ?? meta.quota;
|
|
679
|
+
await atomicWriteFile(metaPath, stringifyJson(meta));
|
|
680
|
+
return this.readManagedAccount(name);
|
|
681
|
+
}
|
|
682
|
+
async updateCurrentManagedAccount() {
|
|
683
|
+
await this.ensureLayout();
|
|
684
|
+
if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
|
|
685
|
+
const current = await this.getCurrentStatus();
|
|
686
|
+
if (!current.managed) throw new Error("Current account is not managed.");
|
|
687
|
+
if (current.duplicate_match || 1 !== current.matched_accounts.length) throw new Error(`Current account matches multiple managed accounts: ${current.matched_accounts.join(", ")}.`);
|
|
688
|
+
const name = current.matched_accounts[0];
|
|
689
|
+
const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
|
|
690
|
+
const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
|
|
691
|
+
const metaPath = this.accountMetaPath(name);
|
|
692
|
+
const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
|
|
693
|
+
await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
|
|
694
|
+
await atomicWriteFile(metaPath, stringifyJson({
|
|
695
|
+
...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
|
|
696
|
+
last_switched_at: existingMeta.last_switched_at,
|
|
697
|
+
quota: existingMeta.quota
|
|
698
|
+
}));
|
|
699
|
+
return {
|
|
700
|
+
account: await this.readManagedAccount(name)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
async switchAccount(name) {
|
|
704
|
+
ensureAccountName(name);
|
|
705
|
+
await this.ensureLayout();
|
|
706
|
+
const account = await this.readManagedAccount(name);
|
|
707
|
+
const warnings = [];
|
|
708
|
+
let backupPath = null;
|
|
709
|
+
await ensureDirectory(this.paths.codexDir, DIRECTORY_MODE);
|
|
710
|
+
if (await pathExists(this.paths.currentAuthPath)) {
|
|
711
|
+
backupPath = (0, external_node_path_namespaceObject.join)(this.paths.backupsDir, "last-active-auth.json");
|
|
712
|
+
await (0, promises_namespaceObject.copyFile)(this.paths.currentAuthPath, backupPath);
|
|
713
|
+
await chmodIfPossible(backupPath, FILE_MODE);
|
|
714
|
+
}
|
|
715
|
+
const rawAuth = await readJsonFile(account.authPath);
|
|
716
|
+
await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
|
|
717
|
+
const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
718
|
+
if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
|
|
719
|
+
const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
|
|
720
|
+
meta.last_switched_at = new Date().toISOString();
|
|
721
|
+
meta.updated_at = meta.last_switched_at;
|
|
722
|
+
await atomicWriteFile(account.metaPath, stringifyJson(meta));
|
|
723
|
+
await this.writeState({
|
|
724
|
+
schema_version: SCHEMA_VERSION,
|
|
725
|
+
last_switched_account: name,
|
|
726
|
+
last_backup_path: backupPath
|
|
727
|
+
});
|
|
728
|
+
const runningCodexPids = await detectRunningCodexProcesses();
|
|
729
|
+
if (runningCodexPids.length > 0) warnings.push(`Detected running codex processes (${runningCodexPids.join(", ")}). Existing sessions may still hold the previous login state.`);
|
|
730
|
+
return {
|
|
731
|
+
account: await this.readManagedAccount(name),
|
|
732
|
+
warnings,
|
|
733
|
+
backup_path: backupPath
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async listQuotaSummaries() {
|
|
737
|
+
const { accounts, warnings } = await this.listAccounts();
|
|
738
|
+
const summaries = await Promise.all(accounts.map((account)=>this.quotaSummaryForAccount(account)));
|
|
739
|
+
return {
|
|
740
|
+
accounts: summaries,
|
|
741
|
+
warnings
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async refreshQuotaForAccount(name) {
|
|
745
|
+
ensureAccountName(name);
|
|
746
|
+
await this.ensureLayout();
|
|
747
|
+
const account = await this.readManagedAccount(name);
|
|
748
|
+
const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
|
|
749
|
+
const snapshot = await readAuthSnapshotFile(account.authPath);
|
|
750
|
+
const now = new Date();
|
|
751
|
+
try {
|
|
752
|
+
const result = await fetchQuotaSnapshot(snapshot, {
|
|
753
|
+
homeDir: this.paths.homeDir,
|
|
754
|
+
fetchImpl: this.fetchImpl,
|
|
755
|
+
now
|
|
756
|
+
});
|
|
757
|
+
if (JSON.stringify(result.authSnapshot) !== JSON.stringify(snapshot)) {
|
|
758
|
+
await this.writeAccountAuthSnapshot(name, result.authSnapshot);
|
|
759
|
+
await this.syncCurrentAuthIfMatching(result.authSnapshot);
|
|
760
|
+
}
|
|
761
|
+
meta.auth_mode = result.authSnapshot.auth_mode;
|
|
762
|
+
meta.account_id = result.authSnapshot.tokens.account_id;
|
|
763
|
+
meta.updated_at = now.toISOString();
|
|
764
|
+
meta.quota = result.quota;
|
|
765
|
+
await this.writeAccountMeta(name, meta);
|
|
766
|
+
return {
|
|
767
|
+
account: await this.readManagedAccount(name),
|
|
768
|
+
quota: meta.quota
|
|
769
|
+
};
|
|
770
|
+
} catch (error) {
|
|
771
|
+
let planType = meta.quota.plan_type;
|
|
772
|
+
try {
|
|
773
|
+
const extracted = extractChatGPTAuth(snapshot);
|
|
774
|
+
planType ??= extracted.planType;
|
|
775
|
+
} catch {}
|
|
776
|
+
meta.updated_at = now.toISOString();
|
|
777
|
+
meta.quota = {
|
|
778
|
+
...meta.quota,
|
|
779
|
+
status: "error",
|
|
780
|
+
plan_type: planType,
|
|
781
|
+
fetched_at: now.toISOString(),
|
|
782
|
+
error_message: error.message
|
|
783
|
+
};
|
|
784
|
+
await this.writeAccountMeta(name, meta);
|
|
785
|
+
throw new Error(`Failed to refresh quota for "${name}": ${error.message}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async refreshAllQuotas(targetName) {
|
|
789
|
+
const { accounts } = await this.listAccounts();
|
|
790
|
+
const targets = targetName ? accounts.filter((account)=>account.name === targetName) : accounts;
|
|
791
|
+
if (targetName && 0 === targets.length) throw new Error(`Account "${targetName}" does not exist.`);
|
|
792
|
+
const successes = [];
|
|
793
|
+
const failures = [];
|
|
794
|
+
for (const account of targets)try {
|
|
795
|
+
const refreshed = await this.refreshQuotaForAccount(account.name);
|
|
796
|
+
successes.push(await this.quotaSummaryForAccount(refreshed.account));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
failures.push({
|
|
799
|
+
name: account.name,
|
|
800
|
+
error: error.message
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
successes,
|
|
805
|
+
failures
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async removeAccount(name) {
|
|
809
|
+
ensureAccountName(name);
|
|
810
|
+
const accountDir = this.accountDirectory(name);
|
|
811
|
+
if (!await pathExists(accountDir)) throw new Error(`Account "${name}" does not exist.`);
|
|
812
|
+
await (0, promises_namespaceObject.rm)(accountDir, {
|
|
813
|
+
recursive: true,
|
|
814
|
+
force: false
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
async renameAccount(oldName, newName) {
|
|
818
|
+
ensureAccountName(oldName);
|
|
819
|
+
ensureAccountName(newName);
|
|
820
|
+
const oldDir = this.accountDirectory(oldName);
|
|
821
|
+
const newDir = this.accountDirectory(newName);
|
|
822
|
+
if (!await pathExists(oldDir)) throw new Error(`Account "${oldName}" does not exist.`);
|
|
823
|
+
if (await pathExists(newDir)) throw new Error(`Account "${newName}" already exists.`);
|
|
824
|
+
await (0, promises_namespaceObject.rename)(oldDir, newDir);
|
|
825
|
+
const metaPath = this.accountMetaPath(newName);
|
|
826
|
+
const meta = parseSnapshotMeta(await readJsonFile(metaPath));
|
|
827
|
+
meta.name = newName;
|
|
828
|
+
meta.updated_at = new Date().toISOString();
|
|
829
|
+
await atomicWriteFile(metaPath, stringifyJson(meta));
|
|
830
|
+
return this.readManagedAccount(newName);
|
|
831
|
+
}
|
|
832
|
+
async doctor() {
|
|
833
|
+
await this.ensureLayout();
|
|
834
|
+
const issues = [];
|
|
835
|
+
const warnings = [];
|
|
836
|
+
const invalidAccounts = [];
|
|
837
|
+
const rootStat = await (0, promises_namespaceObject.stat)(this.paths.codexTeamDir);
|
|
838
|
+
if ((511 & rootStat.mode) !== DIRECTORY_MODE) issues.push(`Store directory permissions are ${(511 & rootStat.mode).toString(8)}, expected 700.`);
|
|
839
|
+
if (await pathExists(this.paths.statePath)) {
|
|
840
|
+
const stateStat = await (0, promises_namespaceObject.stat)(this.paths.statePath);
|
|
841
|
+
if ((511 & stateStat.mode) !== FILE_MODE) issues.push(`State file permissions are ${(511 & stateStat.mode).toString(8)}, expected 600.`);
|
|
842
|
+
await this.readState();
|
|
843
|
+
}
|
|
844
|
+
const { accounts, warnings: accountWarnings } = await this.listAccounts();
|
|
845
|
+
for (const warning of accountWarnings){
|
|
846
|
+
warnings.push(warning);
|
|
847
|
+
const match = warning.match(/^Account "(.+)" is invalid:/);
|
|
848
|
+
if (match) invalidAccounts.push(match[1]);
|
|
849
|
+
}
|
|
850
|
+
for (const account of accounts){
|
|
851
|
+
const authStat = await (0, promises_namespaceObject.stat)(account.authPath);
|
|
852
|
+
const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
|
|
853
|
+
if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
|
|
854
|
+
if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
|
|
855
|
+
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
|
|
856
|
+
}
|
|
857
|
+
let currentAuthPresent = false;
|
|
858
|
+
if (await pathExists(this.paths.currentAuthPath)) {
|
|
859
|
+
currentAuthPresent = true;
|
|
860
|
+
try {
|
|
861
|
+
await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
issues.push(`Current auth.json is invalid: ${error.message}`);
|
|
864
|
+
}
|
|
865
|
+
} else warnings.push("Current ~/.codex/auth.json is missing.");
|
|
866
|
+
return {
|
|
867
|
+
healthy: 0 === issues.length,
|
|
868
|
+
warnings,
|
|
869
|
+
issues,
|
|
870
|
+
account_count: accounts.length,
|
|
871
|
+
invalid_accounts: invalidAccounts,
|
|
872
|
+
current_auth_present: currentAuthPresent
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function createAccountStore(homeDir, options) {
|
|
877
|
+
return new AccountStore({
|
|
878
|
+
homeDir,
|
|
879
|
+
fetchImpl: options?.fetchImpl
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
external_dayjs_default().extend(utc_js_default());
|
|
883
|
+
external_dayjs_default().extend(timezone_js_default());
|
|
884
|
+
function parseArgs(argv) {
|
|
885
|
+
const flags = new Set();
|
|
886
|
+
const positionals = [];
|
|
887
|
+
for (const arg of argv)if (arg.startsWith("--")) flags.add(arg);
|
|
888
|
+
else positionals.push(arg);
|
|
889
|
+
return {
|
|
890
|
+
command: positionals[0] ?? null,
|
|
891
|
+
positionals: positionals.slice(1),
|
|
892
|
+
flags
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function writeJson(stream, value) {
|
|
896
|
+
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
897
|
+
}
|
|
898
|
+
function formatTable(rows, columns) {
|
|
899
|
+
if (0 === rows.length) return "";
|
|
900
|
+
const widths = columns.map(({ key, label })=>Math.max(label.length, ...rows.map((row)=>row[key].length)));
|
|
901
|
+
const renderRow = (row)=>columns.map(({ key }, index)=>row[key].padEnd(widths[index])).join(" ").trimEnd();
|
|
902
|
+
const header = renderRow(Object.fromEntries(columns.map(({ key, label })=>[
|
|
903
|
+
key,
|
|
904
|
+
label
|
|
905
|
+
])));
|
|
906
|
+
const separator = widths.map((width)=>"-".repeat(width)).join(" ");
|
|
907
|
+
return [
|
|
908
|
+
header,
|
|
909
|
+
separator,
|
|
910
|
+
...rows.map(renderRow)
|
|
911
|
+
].join("\n");
|
|
912
|
+
}
|
|
913
|
+
function printHelp(stream) {
|
|
914
|
+
stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
|
|
915
|
+
|
|
916
|
+
Usage:
|
|
917
|
+
codexm current [--json]
|
|
918
|
+
codexm list [--json]
|
|
919
|
+
codexm save <name> [--force] [--json]
|
|
920
|
+
codexm update [--json]
|
|
921
|
+
codexm quota refresh [name] [--json]
|
|
922
|
+
codexm quota list [--json]
|
|
923
|
+
codexm switch <name> [--json]
|
|
924
|
+
codexm remove <name> [--yes] [--json]
|
|
925
|
+
codexm rename <old> <new> [--json]
|
|
926
|
+
codexm doctor [--json]
|
|
927
|
+
|
|
928
|
+
Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
929
|
+
`);
|
|
930
|
+
}
|
|
931
|
+
function describeCurrentStatus(status) {
|
|
932
|
+
const lines = [];
|
|
933
|
+
if (status.exists) {
|
|
934
|
+
lines.push("Current auth: present");
|
|
935
|
+
lines.push(`Auth mode: ${status.auth_mode}`);
|
|
936
|
+
lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
|
|
937
|
+
if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
|
|
938
|
+
else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
|
|
939
|
+
else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
|
|
940
|
+
} else lines.push("Current auth: missing");
|
|
941
|
+
for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
|
|
942
|
+
return lines.join("\n");
|
|
943
|
+
}
|
|
944
|
+
function describeAccounts(accounts, warnings) {
|
|
945
|
+
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
946
|
+
const table = formatTable(accounts.map((account)=>({
|
|
947
|
+
name: account.name,
|
|
948
|
+
account_id: maskAccountId(account.account_id),
|
|
949
|
+
auth_mode: account.auth_mode,
|
|
950
|
+
saved: account.created_at,
|
|
951
|
+
switched: account.last_switched_at ?? "-",
|
|
952
|
+
flags: account.duplicateAccountId ? "duplicate-account-id" : "-"
|
|
953
|
+
})), [
|
|
954
|
+
{
|
|
955
|
+
key: "name",
|
|
956
|
+
label: "NAME"
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
key: "account_id",
|
|
960
|
+
label: "ACCOUNT ID"
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
key: "auth_mode",
|
|
964
|
+
label: "AUTH MODE"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
key: "saved",
|
|
968
|
+
label: "SAVED AT"
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
key: "switched",
|
|
972
|
+
label: "LAST SWITCHED"
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
key: "flags",
|
|
976
|
+
label: "FLAGS"
|
|
977
|
+
}
|
|
978
|
+
]);
|
|
979
|
+
const lines = [
|
|
980
|
+
table
|
|
981
|
+
];
|
|
982
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
983
|
+
return lines.join("\n");
|
|
984
|
+
}
|
|
985
|
+
function describeDoctor(report) {
|
|
986
|
+
const lines = [
|
|
987
|
+
report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
|
|
988
|
+
`Saved accounts: ${report.account_count}`,
|
|
989
|
+
`Current auth present: ${report.current_auth_present ? "yes" : "no"}`
|
|
990
|
+
];
|
|
991
|
+
for (const issue of report.issues)lines.push(`Issue: ${issue}`);
|
|
992
|
+
for (const warning of report.warnings)lines.push(`Warning: ${warning}`);
|
|
993
|
+
return lines.join("\n");
|
|
994
|
+
}
|
|
995
|
+
function formatUsagePercent(window) {
|
|
996
|
+
if (!window) return "-";
|
|
997
|
+
return `${window.used_percent}%`;
|
|
998
|
+
}
|
|
999
|
+
function formatResetAt(window) {
|
|
1000
|
+
if (!window?.reset_at) return "-";
|
|
1001
|
+
return external_dayjs_default().utc(window.reset_at).tz(external_dayjs_default().tz.guess()).format("MM-DD HH:mm");
|
|
1002
|
+
}
|
|
1003
|
+
function describeQuotaAccounts(accounts, warnings) {
|
|
1004
|
+
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
1005
|
+
const table = formatTable(accounts.map((account)=>({
|
|
1006
|
+
name: account.name,
|
|
1007
|
+
account_id: maskAccountId(account.account_id),
|
|
1008
|
+
plan_type: account.plan_type ?? "-",
|
|
1009
|
+
five_hour: formatUsagePercent(account.five_hour),
|
|
1010
|
+
five_hour_reset: formatResetAt(account.five_hour),
|
|
1011
|
+
one_week: formatUsagePercent(account.one_week),
|
|
1012
|
+
one_week_reset: formatResetAt(account.one_week),
|
|
1013
|
+
status: account.status
|
|
1014
|
+
})), [
|
|
1015
|
+
{
|
|
1016
|
+
key: "name",
|
|
1017
|
+
label: "NAME"
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
key: "account_id",
|
|
1021
|
+
label: "ACCOUNT ID"
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
key: "plan_type",
|
|
1025
|
+
label: "PLAN TYPE"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
key: "five_hour",
|
|
1029
|
+
label: "5H USED"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
key: "five_hour_reset",
|
|
1033
|
+
label: "5H RESET AT"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
key: "one_week",
|
|
1037
|
+
label: "1W USED"
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
key: "one_week_reset",
|
|
1041
|
+
label: "1W RESET AT"
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
key: "status",
|
|
1045
|
+
label: "STATUS"
|
|
1046
|
+
}
|
|
1047
|
+
]);
|
|
1048
|
+
const lines = [
|
|
1049
|
+
table
|
|
1050
|
+
];
|
|
1051
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1052
|
+
return lines.join("\n");
|
|
1053
|
+
}
|
|
1054
|
+
function describeQuotaRefresh(result) {
|
|
1055
|
+
const lines = [];
|
|
1056
|
+
if (result.successes.length > 0) {
|
|
1057
|
+
lines.push("Refreshed quotas:");
|
|
1058
|
+
lines.push(describeQuotaAccounts(result.successes, []));
|
|
1059
|
+
}
|
|
1060
|
+
for (const failure of result.failures)lines.push(`Failure: ${failure.name}: ${failure.error}`);
|
|
1061
|
+
if (0 === lines.length) lines.push("No accounts were refreshed.");
|
|
1062
|
+
return lines.join("\n");
|
|
1063
|
+
}
|
|
1064
|
+
async function confirmRemoval(name, streams) {
|
|
1065
|
+
if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
|
|
1066
|
+
streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
|
|
1067
|
+
return await new Promise((resolve)=>{
|
|
1068
|
+
const onData = (buffer)=>{
|
|
1069
|
+
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1070
|
+
streams.stdin.off("data", onData);
|
|
1071
|
+
streams.stdout.write("\n");
|
|
1072
|
+
resolve("y" === answer || "yes" === answer);
|
|
1073
|
+
};
|
|
1074
|
+
streams.stdin.on("data", onData);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
async function runCli(argv, options = {}) {
|
|
1078
|
+
const streams = {
|
|
1079
|
+
stdin: options.stdin ?? external_node_process_namespaceObject.stdin,
|
|
1080
|
+
stdout: options.stdout ?? external_node_process_namespaceObject.stdout,
|
|
1081
|
+
stderr: options.stderr ?? external_node_process_namespaceObject.stderr
|
|
1082
|
+
};
|
|
1083
|
+
const store = options.store ?? createAccountStore();
|
|
1084
|
+
const parsed = parseArgs(argv);
|
|
1085
|
+
const json = parsed.flags.has("--json");
|
|
1086
|
+
try {
|
|
1087
|
+
if (!parsed.command || parsed.flags.has("--help")) {
|
|
1088
|
+
printHelp(streams.stdout);
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
switch(parsed.command){
|
|
1092
|
+
case "current":
|
|
1093
|
+
{
|
|
1094
|
+
const result = await store.getCurrentStatus();
|
|
1095
|
+
if (json) writeJson(streams.stdout, result);
|
|
1096
|
+
else streams.stdout.write(`${describeCurrentStatus(result)}\n`);
|
|
1097
|
+
return 0;
|
|
1098
|
+
}
|
|
1099
|
+
case "list":
|
|
1100
|
+
{
|
|
1101
|
+
const result = await store.listAccounts();
|
|
1102
|
+
if (json) writeJson(streams.stdout, result);
|
|
1103
|
+
else streams.stdout.write(`${describeAccounts(result.accounts, result.warnings)}\n`);
|
|
1104
|
+
return 0;
|
|
1105
|
+
}
|
|
1106
|
+
case "save":
|
|
1107
|
+
{
|
|
1108
|
+
const name = parsed.positionals[0];
|
|
1109
|
+
if (!name) throw new Error("Usage: codexm save <name> [--force]");
|
|
1110
|
+
const account = await store.saveCurrentAccount(name, parsed.flags.has("--force"));
|
|
1111
|
+
const payload = {
|
|
1112
|
+
ok: true,
|
|
1113
|
+
action: "save",
|
|
1114
|
+
account: {
|
|
1115
|
+
name: account.name,
|
|
1116
|
+
account_id: account.account_id,
|
|
1117
|
+
auth_mode: account.auth_mode
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1121
|
+
else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.account_id)}).\n`);
|
|
1122
|
+
return 0;
|
|
1123
|
+
}
|
|
1124
|
+
case "update":
|
|
1125
|
+
{
|
|
1126
|
+
const result = await store.updateCurrentManagedAccount();
|
|
1127
|
+
const warnings = [];
|
|
1128
|
+
let quota = null;
|
|
1129
|
+
try {
|
|
1130
|
+
const quotaResult = await store.refreshQuotaForAccount(result.account.name);
|
|
1131
|
+
const quotaList = await store.listQuotaSummaries();
|
|
1132
|
+
quota = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
warnings.push(error.message);
|
|
1135
|
+
}
|
|
1136
|
+
const payload = {
|
|
1137
|
+
ok: true,
|
|
1138
|
+
action: "update",
|
|
1139
|
+
account: {
|
|
1140
|
+
name: result.account.name,
|
|
1141
|
+
account_id: result.account.account_id,
|
|
1142
|
+
auth_mode: result.account.auth_mode
|
|
1143
|
+
},
|
|
1144
|
+
quota,
|
|
1145
|
+
warnings
|
|
1146
|
+
};
|
|
1147
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1148
|
+
else {
|
|
1149
|
+
streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
|
|
1150
|
+
for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
|
|
1151
|
+
}
|
|
1152
|
+
return 0;
|
|
1153
|
+
}
|
|
1154
|
+
case "quota":
|
|
1155
|
+
{
|
|
1156
|
+
const quotaCommand = parsed.positionals[0];
|
|
1157
|
+
if ("list" === quotaCommand) {
|
|
1158
|
+
const result = await store.listQuotaSummaries();
|
|
1159
|
+
if (json) writeJson(streams.stdout, result);
|
|
1160
|
+
else streams.stdout.write(`${describeQuotaAccounts(result.accounts, result.warnings)}\n`);
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
1163
|
+
if ("refresh" === quotaCommand) {
|
|
1164
|
+
const targetName = parsed.positionals[1];
|
|
1165
|
+
const result = await store.refreshAllQuotas(targetName);
|
|
1166
|
+
if (json) writeJson(streams.stdout, result);
|
|
1167
|
+
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1168
|
+
return 0 === result.failures.length ? 0 : 1;
|
|
1169
|
+
}
|
|
1170
|
+
throw new Error("Usage: codexm quota <refresh [name] | list> [--json]");
|
|
1171
|
+
}
|
|
1172
|
+
case "switch":
|
|
1173
|
+
{
|
|
1174
|
+
const name = parsed.positionals[0];
|
|
1175
|
+
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1176
|
+
const result = await store.switchAccount(name);
|
|
1177
|
+
let quota = null;
|
|
1178
|
+
try {
|
|
1179
|
+
await store.refreshQuotaForAccount(result.account.name);
|
|
1180
|
+
const quotaList = await store.listQuotaSummaries();
|
|
1181
|
+
quota = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
result.warnings.push(error.message);
|
|
1184
|
+
}
|
|
1185
|
+
const payload = {
|
|
1186
|
+
ok: true,
|
|
1187
|
+
action: "switch",
|
|
1188
|
+
account: {
|
|
1189
|
+
name: result.account.name,
|
|
1190
|
+
account_id: result.account.account_id,
|
|
1191
|
+
auth_mode: result.account.auth_mode
|
|
1192
|
+
},
|
|
1193
|
+
quota,
|
|
1194
|
+
backup_path: result.backup_path,
|
|
1195
|
+
warnings: result.warnings
|
|
1196
|
+
};
|
|
1197
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1198
|
+
else {
|
|
1199
|
+
streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
|
|
1200
|
+
if (result.backup_path) streams.stdout.write(`Backup: ${result.backup_path}\n`);
|
|
1201
|
+
for (const warning of result.warnings)streams.stdout.write(`Warning: ${warning}\n`);
|
|
1202
|
+
}
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1205
|
+
case "remove":
|
|
1206
|
+
{
|
|
1207
|
+
const name = parsed.positionals[0];
|
|
1208
|
+
if (!name) throw new Error("Usage: codexm remove <name> [--yes]");
|
|
1209
|
+
const confirmed = parsed.flags.has("--yes") || await confirmRemoval(name, streams);
|
|
1210
|
+
if (!confirmed) {
|
|
1211
|
+
if (json) writeJson(streams.stdout, {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
action: "remove",
|
|
1214
|
+
account: name,
|
|
1215
|
+
cancelled: true
|
|
1216
|
+
});
|
|
1217
|
+
else streams.stdout.write("Cancelled.\n");
|
|
1218
|
+
return 1;
|
|
1219
|
+
}
|
|
1220
|
+
await store.removeAccount(name);
|
|
1221
|
+
if (json) writeJson(streams.stdout, {
|
|
1222
|
+
ok: true,
|
|
1223
|
+
action: "remove",
|
|
1224
|
+
account: name
|
|
1225
|
+
});
|
|
1226
|
+
else streams.stdout.write(`Removed account "${name}".\n`);
|
|
1227
|
+
return 0;
|
|
1228
|
+
}
|
|
1229
|
+
case "rename":
|
|
1230
|
+
{
|
|
1231
|
+
const oldName = parsed.positionals[0];
|
|
1232
|
+
const newName = parsed.positionals[1];
|
|
1233
|
+
if (!oldName || !newName) throw new Error("Usage: codexm rename <old> <new>");
|
|
1234
|
+
const account = await store.renameAccount(oldName, newName);
|
|
1235
|
+
if (json) writeJson(streams.stdout, {
|
|
1236
|
+
ok: true,
|
|
1237
|
+
action: "rename",
|
|
1238
|
+
account: {
|
|
1239
|
+
name: account.name,
|
|
1240
|
+
account_id: account.account_id,
|
|
1241
|
+
auth_mode: account.auth_mode
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
else streams.stdout.write(`Renamed "${oldName}" to "${newName}".\n`);
|
|
1245
|
+
return 0;
|
|
1246
|
+
}
|
|
1247
|
+
case "doctor":
|
|
1248
|
+
{
|
|
1249
|
+
const report = await store.doctor();
|
|
1250
|
+
if (json) writeJson(streams.stdout, report);
|
|
1251
|
+
else streams.stdout.write(`${describeDoctor(report)}\n`);
|
|
1252
|
+
return report.healthy ? 0 : 1;
|
|
1253
|
+
}
|
|
1254
|
+
default:
|
|
1255
|
+
throw new Error(`Unknown command "${parsed.command}".`);
|
|
1256
|
+
}
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
const message = error.message;
|
|
1259
|
+
if (json) writeJson(streams.stderr, {
|
|
1260
|
+
ok: false,
|
|
1261
|
+
error: message
|
|
1262
|
+
});
|
|
1263
|
+
else streams.stderr.write(`Error: ${message}\n`);
|
|
1264
|
+
return 1;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
exports.runCli = __webpack_exports__.runCli;
|
|
1268
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
1269
|
+
"runCli"
|
|
1270
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
1271
|
+
Object.defineProperty(exports, '__esModule', {
|
|
1272
|
+
value: true
|
|
1273
|
+
});
|