codex-multi-auth 1.1.10 → 1.2.0
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 +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -29
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +22 -2
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +84 -1
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/browser.d.ts +1 -0
- package/dist/lib/auth/browser.d.ts.map +1 -1
- package/dist/lib/auth/browser.js +13 -0
- package/dist/lib/auth/browser.js.map +1 -1
- package/dist/lib/auth/token-utils.d.ts +16 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -1
- package/dist/lib/auth/token-utils.js +13 -0
- package/dist/lib/auth/token-utils.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub.d.ts +21 -0
- package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
- package/dist/lib/codex-manager/settings-hub.js +48 -65
- package/dist/lib/codex-manager/settings-hub.js.map +1 -1
- package/dist/lib/codex-manager.d.ts +1 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +517 -78
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +18 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +157 -0
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/schemas.d.ts +7 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +7 -1
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +5 -2
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage.d.ts +10 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +191 -40
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/ui/copy.d.ts +24 -4
- package/dist/lib/ui/copy.d.ts.map +1 -1
- package/dist/lib/ui/copy.js +24 -4
- package/dist/lib/ui/copy.js.map +1 -1
- package/package.json +4 -3
|
@@ -4,25 +4,28 @@ import { promises as fs, existsSync } from "node:fs";
|
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./auth/auth.js";
|
|
6
6
|
import { startLocalOAuthServer } from "./auth/server.js";
|
|
7
|
-
import { copyTextToClipboard, openBrowserUrl } from "./auth/browser.js";
|
|
7
|
+
import { copyTextToClipboard, isBrowserLaunchSuppressed, openBrowserUrl } from "./auth/browser.js";
|
|
8
8
|
import { promptAddAnotherAccount, promptLoginMode } from "./cli.js";
|
|
9
9
|
import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./accounts.js";
|
|
10
10
|
import { ACCOUNT_LIMITS } from "./constants.js";
|
|
11
11
|
import { loadDashboardDisplaySettings, DEFAULT_DASHBOARD_DISPLAY_SETTINGS, } from "./dashboard-settings.js";
|
|
12
12
|
import { evaluateForecastAccounts, isHardRefreshFailure, recommendForecastAccount, summarizeForecast, } from "./forecast.js";
|
|
13
|
+
import { createLogger } from "./logger.js";
|
|
13
14
|
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
14
15
|
import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./quota-probe.js";
|
|
15
16
|
import { queuedRefresh } from "./refresh-queue.js";
|
|
16
17
|
import { loadQuotaCache, saveQuotaCache, } from "./quota-cache.js";
|
|
17
|
-
import { clearAccounts, findMatchingAccountIndex, getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, withAccountAndFlaggedStorageTransaction, withAccountStorageTransaction, } from "./storage.js";
|
|
18
|
+
import { clearAccounts, findMatchingAccountIndex, formatStorageErrorHint, getNamedBackups, getStoragePath, loadFlaggedAccounts, loadAccounts, StorageError, restoreAccountsFromBackup, saveFlaggedAccounts, saveAccounts, setStoragePath, withAccountAndFlaggedStorageTransaction, withAccountStorageTransaction, } from "./storage.js";
|
|
18
19
|
import { getCodexCliAuthPath, getCodexCliConfigPath, loadCodexCliState, } from "./codex-cli/state.js";
|
|
19
20
|
import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
|
|
20
21
|
import { ANSI } from "./ui/ansi.js";
|
|
21
22
|
import { UI_COPY } from "./ui/copy.js";
|
|
23
|
+
import { confirm } from "./ui/confirm.js";
|
|
22
24
|
import { paintUiText, quotaToneFromLeftPercent } from "./ui/format.js";
|
|
23
25
|
import { getUiRuntimeOptions } from "./ui/runtime.js";
|
|
24
26
|
import { select } from "./ui/select.js";
|
|
25
27
|
import { applyUiThemeFromDashboardSettings, configureUnifiedSettings, resolveMenuLayoutMode } from "./codex-manager/settings-hub.js";
|
|
28
|
+
const log = createLogger("codex-manager");
|
|
26
29
|
function stylePromptText(text, tone) {
|
|
27
30
|
if (!output.isTTY)
|
|
28
31
|
return text;
|
|
@@ -215,7 +218,7 @@ function printUsage() {
|
|
|
215
218
|
"Codex Multi-Auth CLI",
|
|
216
219
|
"",
|
|
217
220
|
"Usage:",
|
|
218
|
-
" codex auth login",
|
|
221
|
+
" codex auth login [--manual|--no-browser]",
|
|
219
222
|
" codex auth list",
|
|
220
223
|
" codex auth status",
|
|
221
224
|
" codex auth switch <index>",
|
|
@@ -233,6 +236,26 @@ function printUsage() {
|
|
|
233
236
|
" - Syncs active account into Codex CLI auth state",
|
|
234
237
|
].join("\n"));
|
|
235
238
|
}
|
|
239
|
+
function parseAuthLoginArgs(args) {
|
|
240
|
+
const options = {
|
|
241
|
+
manual: false,
|
|
242
|
+
};
|
|
243
|
+
for (const arg of args) {
|
|
244
|
+
if (arg === "--manual" || arg === "--no-browser") {
|
|
245
|
+
options.manual = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (arg === "--help" || arg === "-h") {
|
|
249
|
+
printUsage();
|
|
250
|
+
return { ok: false, message: "" };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
message: `Unknown login option: ${arg}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { ok: true, options };
|
|
258
|
+
}
|
|
236
259
|
const IMPLEMENTED_FEATURES = [
|
|
237
260
|
{ id: 1, name: "Multi-account OAuth login dashboard" },
|
|
238
261
|
{ id: 2, name: "Account add/update dedupe by token/id/email" },
|
|
@@ -344,6 +367,46 @@ function hasUniqueQuotaAccountId(accounts, account) {
|
|
|
344
367
|
}
|
|
345
368
|
return matchCount === 1;
|
|
346
369
|
}
|
|
370
|
+
function buildQuotaEmailFallbackState(accounts) {
|
|
371
|
+
const stateByEmail = new Map();
|
|
372
|
+
for (const account of accounts) {
|
|
373
|
+
const email = normalizeQuotaEmail(account.email);
|
|
374
|
+
if (!email)
|
|
375
|
+
continue;
|
|
376
|
+
const existing = stateByEmail.get(email);
|
|
377
|
+
if (existing) {
|
|
378
|
+
existing.matchingCount += 1;
|
|
379
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
380
|
+
if (accountId) {
|
|
381
|
+
existing.distinctAccountIds.add(accountId);
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const distinctAccountIds = new Set();
|
|
386
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
387
|
+
if (accountId) {
|
|
388
|
+
distinctAccountIds.add(accountId);
|
|
389
|
+
}
|
|
390
|
+
stateByEmail.set(email, {
|
|
391
|
+
matchingCount: 1,
|
|
392
|
+
distinctAccountIds,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return stateByEmail;
|
|
396
|
+
}
|
|
397
|
+
function hasSafeQuotaEmailFallback(emailFallbackState, account) {
|
|
398
|
+
const email = normalizeQuotaEmail(account.email);
|
|
399
|
+
if (!email)
|
|
400
|
+
return false;
|
|
401
|
+
const state = emailFallbackState.get(email);
|
|
402
|
+
if (!state)
|
|
403
|
+
return false;
|
|
404
|
+
// size > 1 only matters when multiple accounts share the same email but
|
|
405
|
+
// disagree on accountId; a single matching account already implies size <= 1.
|
|
406
|
+
if (state.distinctAccountIds.size > 1)
|
|
407
|
+
return false;
|
|
408
|
+
return state.matchingCount === 1;
|
|
409
|
+
}
|
|
347
410
|
function quotaCacheEntryToSnapshot(entry) {
|
|
348
411
|
return {
|
|
349
412
|
status: entry.status,
|
|
@@ -411,20 +474,22 @@ function formatAccountQuotaSummary(entry) {
|
|
|
411
474
|
}
|
|
412
475
|
return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
|
|
413
476
|
}
|
|
414
|
-
function getQuotaCacheEntryForAccount(cache, account, accounts) {
|
|
415
|
-
const email = normalizeQuotaEmail(account.email);
|
|
416
|
-
if (email && cache.byEmail[email]) {
|
|
417
|
-
return cache.byEmail[email] ?? null;
|
|
418
|
-
}
|
|
477
|
+
function getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
419
478
|
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
420
479
|
if (accountId &&
|
|
421
480
|
hasUniqueQuotaAccountId(accounts, account) &&
|
|
422
481
|
cache.byAccountId[accountId]) {
|
|
423
482
|
return cache.byAccountId[accountId] ?? null;
|
|
424
483
|
}
|
|
484
|
+
const email = normalizeQuotaEmail(account.email);
|
|
485
|
+
if (email &&
|
|
486
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account) &&
|
|
487
|
+
cache.byEmail[email]) {
|
|
488
|
+
return cache.byEmail[email] ?? null;
|
|
489
|
+
}
|
|
425
490
|
return null;
|
|
426
491
|
}
|
|
427
|
-
function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
|
|
492
|
+
function updateQuotaCacheForAccount(cache, account, snapshot, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
428
493
|
const nextEntry = {
|
|
429
494
|
updatedAt: Date.now(),
|
|
430
495
|
status: snapshot.status,
|
|
@@ -442,33 +507,67 @@ function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
|
|
|
442
507
|
},
|
|
443
508
|
};
|
|
444
509
|
let changed = false;
|
|
510
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
511
|
+
const hasUniqueAccountId = accountId !== null && hasUniqueQuotaAccountId(accounts, account);
|
|
512
|
+
if (hasUniqueAccountId) {
|
|
513
|
+
cache.byAccountId[accountId] = nextEntry;
|
|
514
|
+
changed = true;
|
|
515
|
+
}
|
|
445
516
|
const email = normalizeQuotaEmail(account.email);
|
|
446
|
-
if (email
|
|
517
|
+
if (email &&
|
|
518
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account) &&
|
|
519
|
+
!hasUniqueAccountId) {
|
|
447
520
|
cache.byEmail[email] = nextEntry;
|
|
448
521
|
changed = true;
|
|
449
|
-
return changed;
|
|
450
522
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
cache.byAccountId[accountId] = nextEntry;
|
|
523
|
+
else if (email && cache.byEmail[email]) {
|
|
524
|
+
delete cache.byEmail[email];
|
|
454
525
|
changed = true;
|
|
455
526
|
}
|
|
456
527
|
return changed;
|
|
457
528
|
}
|
|
529
|
+
function cloneQuotaCacheData(cache) {
|
|
530
|
+
// Shallow spreading is safe because quota cache entries are always replaced,
|
|
531
|
+
// never mutated in-place.
|
|
532
|
+
return {
|
|
533
|
+
byAccountId: { ...cache.byAccountId },
|
|
534
|
+
byEmail: { ...cache.byEmail },
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function pruneUnsafeQuotaEmailCacheEntry(cache, email, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
538
|
+
const normalizedEmail = normalizeQuotaEmail(email);
|
|
539
|
+
if (!normalizedEmail || !cache.byEmail[normalizedEmail]) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const hasSafeFallbackAccount = accounts.some((account) => normalizeQuotaEmail(account.email) === normalizedEmail &&
|
|
543
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account));
|
|
544
|
+
if (hasSafeFallbackAccount) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
delete cache.byEmail[normalizedEmail];
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
458
550
|
const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
|
|
459
551
|
const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
|
|
460
|
-
function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
|
|
552
|
+
function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
461
553
|
if (account.enabled === false)
|
|
462
554
|
return null;
|
|
463
555
|
if (!hasUsableAccessToken(account, now))
|
|
464
556
|
return null;
|
|
465
|
-
const existing = getQuotaCacheEntryForAccount(cache, account, accounts);
|
|
557
|
+
const existing = getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState);
|
|
466
558
|
if (existing &&
|
|
467
559
|
typeof existing.updatedAt === "number" &&
|
|
468
560
|
Number.isFinite(existing.updatedAt) &&
|
|
469
561
|
now - existing.updatedAt < maxAgeMs) {
|
|
470
562
|
return null;
|
|
471
563
|
}
|
|
564
|
+
// Menu auto-refresh is cache-backed, so only probe when the result can be
|
|
565
|
+
// written behind a safe lookup key for later reuse.
|
|
566
|
+
const canStore = (normalizeQuotaAccountId(account.accountId) !== null &&
|
|
567
|
+
hasUniqueQuotaAccountId(accounts, account)) ||
|
|
568
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account);
|
|
569
|
+
if (!canStore)
|
|
570
|
+
return null;
|
|
472
571
|
const accessToken = account.accessToken;
|
|
473
572
|
const accountId = accessToken
|
|
474
573
|
? (account.accountId ?? extractAccountId(accessToken))
|
|
@@ -477,10 +576,10 @@ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
|
|
|
477
576
|
return null;
|
|
478
577
|
return { accountId, accessToken };
|
|
479
578
|
}
|
|
480
|
-
function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
579
|
+
function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now(), emailFallbackState = buildQuotaEmailFallbackState(storage.accounts)) {
|
|
481
580
|
const targets = [];
|
|
482
581
|
for (const account of storage.accounts) {
|
|
483
|
-
const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts);
|
|
582
|
+
const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState);
|
|
484
583
|
if (!probeInput)
|
|
485
584
|
continue;
|
|
486
585
|
targets.push({
|
|
@@ -492,9 +591,10 @@ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now
|
|
|
492
591
|
return targets;
|
|
493
592
|
}
|
|
494
593
|
function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
594
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
495
595
|
let count = 0;
|
|
496
596
|
for (const account of storage.accounts) {
|
|
497
|
-
if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts)) {
|
|
597
|
+
if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState)) {
|
|
498
598
|
count += 1;
|
|
499
599
|
}
|
|
500
600
|
}
|
|
@@ -504,8 +604,10 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
|
|
|
504
604
|
if (storage.accounts.length === 0) {
|
|
505
605
|
return cache;
|
|
506
606
|
}
|
|
607
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
608
|
+
const nextCache = cloneQuotaCacheData(cache);
|
|
507
609
|
const now = Date.now();
|
|
508
|
-
const targets = collectMenuQuotaRefreshTargets(storage,
|
|
610
|
+
const targets = collectMenuQuotaRefreshTargets(storage, nextCache, maxAgeMs, now, emailFallbackState);
|
|
509
611
|
const total = targets.length;
|
|
510
612
|
let processed = 0;
|
|
511
613
|
onProgress?.(processed, total);
|
|
@@ -520,17 +622,16 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
|
|
|
520
622
|
model: MENU_QUOTA_REFRESH_MODEL,
|
|
521
623
|
});
|
|
522
624
|
changed =
|
|
523
|
-
updateQuotaCacheForAccount(
|
|
524
|
-
changed;
|
|
625
|
+
updateQuotaCacheForAccount(nextCache, target.account, snapshot, storage.accounts, emailFallbackState) || changed;
|
|
525
626
|
}
|
|
526
627
|
catch {
|
|
527
628
|
// Keep existing cached values if probing fails.
|
|
528
629
|
}
|
|
529
630
|
}
|
|
530
631
|
if (changed) {
|
|
531
|
-
await saveQuotaCache(
|
|
632
|
+
await saveQuotaCache(nextCache);
|
|
532
633
|
}
|
|
533
|
-
return
|
|
634
|
+
return nextCache;
|
|
534
635
|
}
|
|
535
636
|
const ACCESS_TOKEN_FRESH_WINDOW_MS = 5 * 60 * 1000;
|
|
536
637
|
function hasUsableAccessToken(account, now) {
|
|
@@ -643,9 +744,10 @@ function toExistingAccountInfo(storage, quotaCache, displaySettings) {
|
|
|
643
744
|
const now = Date.now();
|
|
644
745
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
645
746
|
const layoutMode = resolveMenuLayoutMode(displaySettings);
|
|
747
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
646
748
|
const baseAccounts = storage.accounts.map((account, index) => {
|
|
647
749
|
const entry = quotaCache
|
|
648
|
-
? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts)
|
|
750
|
+
? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts, emailFallbackState)
|
|
649
751
|
: null;
|
|
650
752
|
return {
|
|
651
753
|
index,
|
|
@@ -750,15 +852,64 @@ function applyTokenAccountIdentity(account, tokenAccountId) {
|
|
|
750
852
|
account.accountIdSource = nextIdentity.accountIdSource;
|
|
751
853
|
return true;
|
|
752
854
|
}
|
|
753
|
-
async function promptManualCallback(state) {
|
|
754
|
-
|
|
855
|
+
async function promptManualCallback(state, options = {}) {
|
|
856
|
+
const useInteractivePrompt = input.isTTY && output.isTTY;
|
|
857
|
+
if (!useInteractivePrompt && !options.allowNonTty) {
|
|
755
858
|
return null;
|
|
756
859
|
}
|
|
757
860
|
const rl = createInterface({ input, output });
|
|
758
861
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
862
|
+
if (useInteractivePrompt) {
|
|
863
|
+
console.log("");
|
|
864
|
+
console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
|
|
865
|
+
}
|
|
866
|
+
const answer = useInteractivePrompt
|
|
867
|
+
? await rl.question("◆ ")
|
|
868
|
+
: await new Promise((resolve, reject) => {
|
|
869
|
+
if (input.readableEnded || input.destroyed) {
|
|
870
|
+
resolve(null);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
let settled = false;
|
|
874
|
+
const handleInputClosed = () => {
|
|
875
|
+
if (settled)
|
|
876
|
+
return;
|
|
877
|
+
settled = true;
|
|
878
|
+
input.off("end", handleInputClosed);
|
|
879
|
+
input.off("close", handleInputClosed);
|
|
880
|
+
resolve(null);
|
|
881
|
+
};
|
|
882
|
+
const finish = (value) => {
|
|
883
|
+
if (settled)
|
|
884
|
+
return;
|
|
885
|
+
settled = true;
|
|
886
|
+
input.off("end", handleInputClosed);
|
|
887
|
+
input.off("close", handleInputClosed);
|
|
888
|
+
resolve(value);
|
|
889
|
+
};
|
|
890
|
+
const fail = (error) => {
|
|
891
|
+
if (settled)
|
|
892
|
+
return;
|
|
893
|
+
settled = true;
|
|
894
|
+
input.off("end", handleInputClosed);
|
|
895
|
+
input.off("close", handleInputClosed);
|
|
896
|
+
reject(error);
|
|
897
|
+
};
|
|
898
|
+
rl.question("")
|
|
899
|
+
.then((value) => finish(value))
|
|
900
|
+
.catch((error) => {
|
|
901
|
+
if (isAbortError(error) || isReadlineClosedError(error)) {
|
|
902
|
+
handleInputClosed();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
fail(error);
|
|
906
|
+
});
|
|
907
|
+
input.once("end", handleInputClosed);
|
|
908
|
+
input.once("close", handleInputClosed);
|
|
909
|
+
});
|
|
910
|
+
if (answer === null) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
762
913
|
if (answer.includes("\u001b")) {
|
|
763
914
|
return null;
|
|
764
915
|
}
|
|
@@ -778,7 +929,7 @@ async function promptManualCallback(state) {
|
|
|
778
929
|
return parsed.code;
|
|
779
930
|
}
|
|
780
931
|
catch (error) {
|
|
781
|
-
if (isAbortError(error)) {
|
|
932
|
+
if (isAbortError(error) || isReadlineClosedError(error)) {
|
|
782
933
|
return null;
|
|
783
934
|
}
|
|
784
935
|
throw error;
|
|
@@ -787,20 +938,56 @@ async function promptManualCallback(state) {
|
|
|
787
938
|
rl.close();
|
|
788
939
|
}
|
|
789
940
|
}
|
|
790
|
-
|
|
941
|
+
function isReadlineClosedError(error) {
|
|
942
|
+
if (!(error instanceof Error)) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
const errorCode = typeof error === "object" && error !== null && "code" in error
|
|
946
|
+
? String(error.code)
|
|
947
|
+
: "";
|
|
948
|
+
return errorCode === "ERR_USE_AFTER_CLOSE" || /readline was closed/i.test(error.message);
|
|
949
|
+
}
|
|
950
|
+
export function formatBackupSavedAt(mtimeMs) {
|
|
951
|
+
return new Date(mtimeMs).toLocaleString(undefined, {
|
|
952
|
+
month: "short",
|
|
953
|
+
day: "numeric",
|
|
954
|
+
year: "numeric",
|
|
955
|
+
hour: "numeric",
|
|
956
|
+
minute: "2-digit",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async function promptOAuthSignInMode(backupOption, backupDiscoveryWarning = null) {
|
|
791
960
|
if (!input.isTTY || !output.isTTY) {
|
|
792
961
|
return "browser";
|
|
793
962
|
}
|
|
794
963
|
const ui = getUiRuntimeOptions();
|
|
795
964
|
const items = [
|
|
965
|
+
{ label: UI_COPY.oauth.signInHeading, value: "cancel", kind: "heading" },
|
|
796
966
|
{ label: UI_COPY.oauth.openBrowser, value: "browser", color: "green" },
|
|
797
967
|
{ label: UI_COPY.oauth.manualMode, value: "manual", color: "yellow" },
|
|
968
|
+
...(backupOption
|
|
969
|
+
? [
|
|
970
|
+
{ separator: true, label: "", value: "cancel" },
|
|
971
|
+
{ label: UI_COPY.oauth.restoreHeading, value: "cancel", kind: "heading" },
|
|
972
|
+
{
|
|
973
|
+
label: UI_COPY.oauth.restoreSavedBackup,
|
|
974
|
+
value: "restore-backup",
|
|
975
|
+
hint: UI_COPY.oauth.loadLastBackupHint(backupOption.fileName, backupOption.accountCount, formatBackupSavedAt(backupOption.mtimeMs)),
|
|
976
|
+
color: "cyan",
|
|
977
|
+
},
|
|
978
|
+
]
|
|
979
|
+
: []),
|
|
980
|
+
{ separator: true, label: "", value: "cancel" },
|
|
798
981
|
{ label: UI_COPY.oauth.back, value: "cancel", color: "red" },
|
|
799
982
|
];
|
|
800
983
|
const selected = await select(items, {
|
|
801
984
|
message: UI_COPY.oauth.chooseModeTitle,
|
|
802
|
-
subtitle:
|
|
803
|
-
|
|
985
|
+
subtitle: backupDiscoveryWarning
|
|
986
|
+
? `${UI_COPY.oauth.chooseModeSubtitle} ${backupDiscoveryWarning}`
|
|
987
|
+
: UI_COPY.oauth.chooseModeSubtitle,
|
|
988
|
+
help: backupOption
|
|
989
|
+
? UI_COPY.oauth.chooseModeHelpWithBackup
|
|
990
|
+
: UI_COPY.oauth.chooseModeHelp,
|
|
804
991
|
clearScreen: true,
|
|
805
992
|
theme: ui.theme,
|
|
806
993
|
selectedEmphasis: "minimal",
|
|
@@ -813,11 +1000,81 @@ async function promptOAuthSignInMode() {
|
|
|
813
1000
|
return "browser";
|
|
814
1001
|
if (lower === "2")
|
|
815
1002
|
return "manual";
|
|
1003
|
+
if (lower === "3" && backupOption)
|
|
1004
|
+
return "restore-backup";
|
|
816
1005
|
return undefined;
|
|
817
1006
|
},
|
|
818
1007
|
});
|
|
819
1008
|
return selected ?? "cancel";
|
|
820
1009
|
}
|
|
1010
|
+
async function promptBackupRestoreMode(latestBackup) {
|
|
1011
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1012
|
+
return "latest";
|
|
1013
|
+
}
|
|
1014
|
+
const ui = getUiRuntimeOptions();
|
|
1015
|
+
const items = [
|
|
1016
|
+
{
|
|
1017
|
+
label: UI_COPY.oauth.loadLastBackup,
|
|
1018
|
+
value: "latest",
|
|
1019
|
+
hint: `${UI_COPY.oauth.restoreBackupLatestHint}\n${UI_COPY.oauth.manualBackupHint(latestBackup.accountCount, formatBackupSavedAt(latestBackup.mtimeMs))}`,
|
|
1020
|
+
color: "cyan",
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
label: UI_COPY.oauth.chooseBackupManually,
|
|
1024
|
+
value: "manual",
|
|
1025
|
+
color: "yellow",
|
|
1026
|
+
},
|
|
1027
|
+
{ label: UI_COPY.oauth.back, value: "back", color: "red" },
|
|
1028
|
+
];
|
|
1029
|
+
const selected = await select(items, {
|
|
1030
|
+
message: UI_COPY.oauth.restoreBackupTitle,
|
|
1031
|
+
subtitle: UI_COPY.oauth.restoreBackupSubtitle,
|
|
1032
|
+
help: UI_COPY.oauth.restoreBackupHelp,
|
|
1033
|
+
clearScreen: true,
|
|
1034
|
+
theme: ui.theme,
|
|
1035
|
+
selectedEmphasis: "minimal",
|
|
1036
|
+
allowEscape: false,
|
|
1037
|
+
onInput: (raw) => {
|
|
1038
|
+
const lower = raw.toLowerCase();
|
|
1039
|
+
if (lower === "q")
|
|
1040
|
+
return "back";
|
|
1041
|
+
if (lower === "1")
|
|
1042
|
+
return "latest";
|
|
1043
|
+
if (lower === "2")
|
|
1044
|
+
return "manual";
|
|
1045
|
+
return undefined;
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
return selected ?? "back";
|
|
1049
|
+
}
|
|
1050
|
+
async function promptManualBackupSelection(backups) {
|
|
1051
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1052
|
+
return backups[0] ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
const ui = getUiRuntimeOptions();
|
|
1055
|
+
const items = backups.map((backup) => ({
|
|
1056
|
+
label: backup.fileName,
|
|
1057
|
+
value: backup,
|
|
1058
|
+
hint: UI_COPY.oauth.manualBackupHint(backup.accountCount, formatBackupSavedAt(backup.mtimeMs)),
|
|
1059
|
+
color: "cyan",
|
|
1060
|
+
}));
|
|
1061
|
+
items.push({ label: UI_COPY.oauth.back, value: null, color: "red" });
|
|
1062
|
+
const selected = await select(items, {
|
|
1063
|
+
message: UI_COPY.oauth.manualBackupTitle,
|
|
1064
|
+
subtitle: UI_COPY.oauth.manualBackupSubtitle,
|
|
1065
|
+
help: UI_COPY.oauth.manualBackupHelp,
|
|
1066
|
+
clearScreen: true,
|
|
1067
|
+
theme: ui.theme,
|
|
1068
|
+
selectedEmphasis: "minimal",
|
|
1069
|
+
allowEscape: false,
|
|
1070
|
+
onInput: (raw) => {
|
|
1071
|
+
if (raw.toLowerCase() === "q")
|
|
1072
|
+
return null;
|
|
1073
|
+
return undefined;
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
return selected;
|
|
1077
|
+
}
|
|
821
1078
|
async function waitForMenuReturn(options = {}) {
|
|
822
1079
|
if (!input.isTTY || !output.isTTY) {
|
|
823
1080
|
return;
|
|
@@ -1063,18 +1320,29 @@ async function runActionPanel(title, stage, action, settings) {
|
|
|
1063
1320
|
throw failed;
|
|
1064
1321
|
}
|
|
1065
1322
|
}
|
|
1066
|
-
async function runOAuthFlow(forceNewLogin) {
|
|
1323
|
+
async function runOAuthFlow(forceNewLogin, signInMode) {
|
|
1067
1324
|
const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
|
|
1068
|
-
const oauthServer = await startLocalOAuthServer({ state });
|
|
1069
1325
|
let code = null;
|
|
1326
|
+
let oauthServer = null;
|
|
1070
1327
|
try {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1328
|
+
if (signInMode === "browser") {
|
|
1329
|
+
try {
|
|
1330
|
+
oauthServer = await startLocalOAuthServer({ state });
|
|
1331
|
+
}
|
|
1332
|
+
catch (serverError) {
|
|
1333
|
+
log.warn("Local OAuth callback server unavailable; falling back to manual callback entry.", serverError instanceof Error
|
|
1334
|
+
? {
|
|
1335
|
+
message: serverError.message,
|
|
1336
|
+
stack: serverError.stack,
|
|
1337
|
+
code: typeof serverError === "object" &&
|
|
1338
|
+
serverError !== null &&
|
|
1339
|
+
"code" in serverError
|
|
1340
|
+
? String(serverError.code)
|
|
1341
|
+
: undefined,
|
|
1342
|
+
}
|
|
1343
|
+
: { error: String(serverError) });
|
|
1344
|
+
oauthServer = null;
|
|
1345
|
+
}
|
|
1078
1346
|
}
|
|
1079
1347
|
if (signInMode === "browser") {
|
|
1080
1348
|
const opened = openBrowserUrl(url);
|
|
@@ -1093,18 +1361,23 @@ async function runOAuthFlow(forceNewLogin) {
|
|
|
1093
1361
|
const copied = copyTextToClipboard(url);
|
|
1094
1362
|
console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
|
|
1095
1363
|
}
|
|
1096
|
-
|
|
1364
|
+
const waitingForCallback = signInMode === "browser" && oauthServer?.ready === true;
|
|
1365
|
+
if (waitingForCallback && oauthServer) {
|
|
1097
1366
|
console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
|
|
1098
1367
|
const callbackResult = await oauthServer.waitForCode(state);
|
|
1099
1368
|
code = callbackResult?.code ?? null;
|
|
1100
1369
|
}
|
|
1101
1370
|
if (!code) {
|
|
1102
|
-
console.log(stylePromptText(
|
|
1103
|
-
|
|
1371
|
+
console.log(stylePromptText(waitingForCallback
|
|
1372
|
+
? UI_COPY.oauth.callbackMissed
|
|
1373
|
+
: signInMode === "manual"
|
|
1374
|
+
? UI_COPY.oauth.callbackBypassed
|
|
1375
|
+
: UI_COPY.oauth.callbackUnavailable, "warning"));
|
|
1376
|
+
code = await promptManualCallback(state, { allowNonTty: signInMode === "manual" });
|
|
1104
1377
|
}
|
|
1105
1378
|
}
|
|
1106
1379
|
finally {
|
|
1107
|
-
oauthServer
|
|
1380
|
+
oauthServer?.close();
|
|
1108
1381
|
}
|
|
1109
1382
|
if (!code) {
|
|
1110
1383
|
return {
|
|
@@ -1253,6 +1526,7 @@ async function runHealthCheck(options = {}) {
|
|
|
1253
1526
|
const probeModel = options.model?.trim() || "gpt-5-codex";
|
|
1254
1527
|
const display = options.display ?? DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
1255
1528
|
const quotaCache = liveProbe ? await loadQuotaCache() : null;
|
|
1529
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
1256
1530
|
let quotaCacheChanged = false;
|
|
1257
1531
|
setStoragePath(null);
|
|
1258
1532
|
const storage = await loadAccounts();
|
|
@@ -1260,6 +1534,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1260
1534
|
console.log("No accounts configured.");
|
|
1261
1535
|
return;
|
|
1262
1536
|
}
|
|
1537
|
+
let quotaEmailFallbackState = liveProbe && quotaCache
|
|
1538
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
1539
|
+
: null;
|
|
1263
1540
|
let changed = false;
|
|
1264
1541
|
let ok = 0;
|
|
1265
1542
|
let failed = 0;
|
|
@@ -1302,9 +1579,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1302
1579
|
accessToken: currentAccessToken,
|
|
1303
1580
|
model: probeModel,
|
|
1304
1581
|
});
|
|
1305
|
-
if (
|
|
1582
|
+
if (workingQuotaCache) {
|
|
1306
1583
|
quotaCacheChanged =
|
|
1307
|
-
updateQuotaCacheForAccount(
|
|
1584
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1308
1585
|
}
|
|
1309
1586
|
healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
1310
1587
|
}
|
|
@@ -1328,6 +1605,8 @@ async function runHealthCheck(options = {}) {
|
|
|
1328
1605
|
if (result.type === "success") {
|
|
1329
1606
|
const tokenAccountId = extractAccountId(result.access);
|
|
1330
1607
|
const nextEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
|
|
1608
|
+
const previousEmail = account.email;
|
|
1609
|
+
let accountIdentityChanged = false;
|
|
1331
1610
|
if (account.refreshToken !== result.refresh) {
|
|
1332
1611
|
account.refreshToken = result.refresh;
|
|
1333
1612
|
changed = true;
|
|
@@ -1343,14 +1622,21 @@ async function runHealthCheck(options = {}) {
|
|
|
1343
1622
|
if (nextEmail && nextEmail !== account.email) {
|
|
1344
1623
|
account.email = nextEmail;
|
|
1345
1624
|
changed = true;
|
|
1625
|
+
accountIdentityChanged = true;
|
|
1346
1626
|
}
|
|
1347
1627
|
if (applyTokenAccountIdentity(account, tokenAccountId)) {
|
|
1348
1628
|
changed = true;
|
|
1629
|
+
accountIdentityChanged = true;
|
|
1349
1630
|
}
|
|
1350
1631
|
if (account.enabled === false) {
|
|
1351
1632
|
account.enabled = true;
|
|
1352
1633
|
changed = true;
|
|
1353
1634
|
}
|
|
1635
|
+
if (accountIdentityChanged && liveProbe && workingQuotaCache) {
|
|
1636
|
+
quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
1637
|
+
quotaCacheChanged =
|
|
1638
|
+
pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
|
|
1639
|
+
}
|
|
1354
1640
|
account.lastUsed = Date.now();
|
|
1355
1641
|
if (i === activeIndex) {
|
|
1356
1642
|
activeAccountRefreshed = true;
|
|
@@ -1370,9 +1656,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1370
1656
|
accessToken: result.access,
|
|
1371
1657
|
model: probeModel,
|
|
1372
1658
|
});
|
|
1373
|
-
if (
|
|
1659
|
+
if (workingQuotaCache) {
|
|
1374
1660
|
quotaCacheChanged =
|
|
1375
|
-
updateQuotaCacheForAccount(
|
|
1661
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1376
1662
|
}
|
|
1377
1663
|
healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
1378
1664
|
}
|
|
@@ -1406,8 +1692,8 @@ async function runHealthCheck(options = {}) {
|
|
|
1406
1692
|
if (!display.showPerAccountRows) {
|
|
1407
1693
|
console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
|
|
1408
1694
|
}
|
|
1409
|
-
if (
|
|
1410
|
-
await saveQuotaCache(
|
|
1695
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
1696
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1411
1697
|
}
|
|
1412
1698
|
if (changed) {
|
|
1413
1699
|
await saveAccounts(storage);
|
|
@@ -1784,6 +2070,7 @@ async function runForecast(args) {
|
|
|
1784
2070
|
const options = parsedArgs.options;
|
|
1785
2071
|
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
1786
2072
|
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
2073
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
1787
2074
|
let quotaCacheChanged = false;
|
|
1788
2075
|
setStoragePath(null);
|
|
1789
2076
|
const storage = await loadAccounts();
|
|
@@ -1791,6 +2078,9 @@ async function runForecast(args) {
|
|
|
1791
2078
|
console.log("No accounts configured.");
|
|
1792
2079
|
return 0;
|
|
1793
2080
|
}
|
|
2081
|
+
const quotaEmailFallbackState = options.live && quotaCache
|
|
2082
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
2083
|
+
: null;
|
|
1794
2084
|
const now = Date.now();
|
|
1795
2085
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
1796
2086
|
const refreshFailures = new Map();
|
|
@@ -1827,11 +2117,11 @@ async function runForecast(args) {
|
|
|
1827
2117
|
model: options.model,
|
|
1828
2118
|
});
|
|
1829
2119
|
liveQuotaByIndex.set(i, liveQuota);
|
|
1830
|
-
if (
|
|
2120
|
+
if (workingQuotaCache) {
|
|
1831
2121
|
const account = storage.accounts[i];
|
|
1832
2122
|
if (account) {
|
|
1833
2123
|
quotaCacheChanged =
|
|
1834
|
-
updateQuotaCacheForAccount(
|
|
2124
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, liveQuota, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1835
2125
|
}
|
|
1836
2126
|
}
|
|
1837
2127
|
}
|
|
@@ -1852,8 +2142,8 @@ async function runForecast(args) {
|
|
|
1852
2142
|
const summary = summarizeForecast(forecastResults);
|
|
1853
2143
|
const recommendation = recommendForecastAccount(forecastResults);
|
|
1854
2144
|
if (options.json) {
|
|
1855
|
-
if (
|
|
1856
|
-
await saveQuotaCache(
|
|
2145
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2146
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1857
2147
|
}
|
|
1858
2148
|
console.log(JSON.stringify({
|
|
1859
2149
|
command: "forecast",
|
|
@@ -1923,8 +2213,8 @@ async function runForecast(args) {
|
|
|
1923
2213
|
console.log(` ${stylePromptText("-", "warning")} ${stylePromptText(error, "muted")}`);
|
|
1924
2214
|
}
|
|
1925
2215
|
}
|
|
1926
|
-
if (
|
|
1927
|
-
await saveQuotaCache(
|
|
2216
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2217
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1928
2218
|
}
|
|
1929
2219
|
return 0;
|
|
1930
2220
|
}
|
|
@@ -2410,6 +2700,7 @@ async function runFix(args) {
|
|
|
2410
2700
|
const options = parsedArgs.options;
|
|
2411
2701
|
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
2412
2702
|
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
2703
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
2413
2704
|
let quotaCacheChanged = false;
|
|
2414
2705
|
setStoragePath(null);
|
|
2415
2706
|
const storage = await loadAccounts();
|
|
@@ -2417,6 +2708,9 @@ async function runFix(args) {
|
|
|
2417
2708
|
console.log("No accounts configured.");
|
|
2418
2709
|
return 0;
|
|
2419
2710
|
}
|
|
2711
|
+
let quotaEmailFallbackState = options.live && quotaCache
|
|
2712
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
2713
|
+
: null;
|
|
2420
2714
|
const now = Date.now();
|
|
2421
2715
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2422
2716
|
let changed = false;
|
|
@@ -2450,9 +2744,9 @@ async function runFix(args) {
|
|
|
2450
2744
|
accessToken: currentAccessToken,
|
|
2451
2745
|
model: options.model,
|
|
2452
2746
|
});
|
|
2453
|
-
if (
|
|
2747
|
+
if (workingQuotaCache) {
|
|
2454
2748
|
quotaCacheChanged =
|
|
2455
|
-
updateQuotaCacheForAccount(
|
|
2749
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
2456
2750
|
}
|
|
2457
2751
|
reports.push({
|
|
2458
2752
|
index: i,
|
|
@@ -2490,7 +2784,9 @@ async function runFix(args) {
|
|
|
2490
2784
|
if (refreshResult.type === "success") {
|
|
2491
2785
|
const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
|
|
2492
2786
|
const nextAccountId = extractAccountId(refreshResult.access);
|
|
2787
|
+
const previousEmail = account.email;
|
|
2493
2788
|
let accountChanged = false;
|
|
2789
|
+
let accountIdentityChanged = false;
|
|
2494
2790
|
if (account.refreshToken !== refreshResult.refresh) {
|
|
2495
2791
|
account.refreshToken = refreshResult.refresh;
|
|
2496
2792
|
accountChanged = true;
|
|
@@ -2506,14 +2802,19 @@ async function runFix(args) {
|
|
|
2506
2802
|
if (nextEmail && nextEmail !== account.email) {
|
|
2507
2803
|
account.email = nextEmail;
|
|
2508
2804
|
accountChanged = true;
|
|
2805
|
+
accountIdentityChanged = true;
|
|
2509
2806
|
}
|
|
2510
|
-
if (
|
|
2511
|
-
account.accountId = nextAccountId;
|
|
2512
|
-
account.accountIdSource = "token";
|
|
2807
|
+
if (applyTokenAccountIdentity(account, nextAccountId)) {
|
|
2513
2808
|
accountChanged = true;
|
|
2809
|
+
accountIdentityChanged = true;
|
|
2514
2810
|
}
|
|
2515
2811
|
if (accountChanged)
|
|
2516
2812
|
changed = true;
|
|
2813
|
+
if (accountIdentityChanged && options.live && workingQuotaCache) {
|
|
2814
|
+
quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
2815
|
+
quotaCacheChanged =
|
|
2816
|
+
pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
|
|
2817
|
+
}
|
|
2517
2818
|
if (options.live) {
|
|
2518
2819
|
const probeAccountId = account.accountId ?? nextAccountId;
|
|
2519
2820
|
if (probeAccountId) {
|
|
@@ -2523,9 +2824,9 @@ async function runFix(args) {
|
|
|
2523
2824
|
accessToken: refreshResult.access,
|
|
2524
2825
|
model: options.model,
|
|
2525
2826
|
});
|
|
2526
|
-
if (
|
|
2827
|
+
if (workingQuotaCache) {
|
|
2527
2828
|
quotaCacheChanged =
|
|
2528
|
-
updateQuotaCacheForAccount(
|
|
2829
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
2529
2830
|
}
|
|
2530
2831
|
reports.push({
|
|
2531
2832
|
index: i,
|
|
@@ -2614,8 +2915,8 @@ async function runFix(args) {
|
|
|
2614
2915
|
await saveAccounts(storage);
|
|
2615
2916
|
}
|
|
2616
2917
|
if (options.json) {
|
|
2617
|
-
if (
|
|
2618
|
-
await saveQuotaCache(
|
|
2918
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2919
|
+
await saveQuotaCache(workingQuotaCache);
|
|
2619
2920
|
}
|
|
2620
2921
|
console.log(JSON.stringify({
|
|
2621
2922
|
command: "fix",
|
|
@@ -2681,8 +2982,8 @@ async function runFix(args) {
|
|
|
2681
2982
|
console.log(`${stylePromptText("Note:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
2682
2983
|
}
|
|
2683
2984
|
}
|
|
2684
|
-
if (
|
|
2685
|
-
await saveQuotaCache(
|
|
2985
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2986
|
+
await saveQuotaCache(workingQuotaCache);
|
|
2686
2987
|
}
|
|
2687
2988
|
if (changed && options.dryRun) {
|
|
2688
2989
|
console.log(`\n${stylePromptText("Preview only: no changes were saved.", "warning")}`);
|
|
@@ -3262,7 +3563,15 @@ async function handleManageAction(storage, menuResult) {
|
|
|
3262
3563
|
const existing = storage.accounts[idx];
|
|
3263
3564
|
if (!existing)
|
|
3264
3565
|
return;
|
|
3265
|
-
const
|
|
3566
|
+
const signInMode = await promptOAuthSignInMode(null);
|
|
3567
|
+
if (signInMode === "cancel") {
|
|
3568
|
+
console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
if (signInMode !== "browser" && signInMode !== "manual") {
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
const tokenResult = await runOAuthFlow(true, signInMode);
|
|
3266
3575
|
if (tokenResult.type !== "success") {
|
|
3267
3576
|
console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
|
|
3268
3577
|
return;
|
|
@@ -3273,7 +3582,17 @@ async function handleManageAction(storage, menuResult) {
|
|
|
3273
3582
|
console.log(`Refreshed account ${idx + 1}.`);
|
|
3274
3583
|
}
|
|
3275
3584
|
}
|
|
3276
|
-
async function runAuthLogin() {
|
|
3585
|
+
async function runAuthLogin(args) {
|
|
3586
|
+
const parsedArgs = parseAuthLoginArgs(args);
|
|
3587
|
+
if (!parsedArgs.ok) {
|
|
3588
|
+
if (parsedArgs.message) {
|
|
3589
|
+
console.error(parsedArgs.message);
|
|
3590
|
+
printUsage();
|
|
3591
|
+
return 1;
|
|
3592
|
+
}
|
|
3593
|
+
return 0;
|
|
3594
|
+
}
|
|
3595
|
+
const loginOptions = parsedArgs.options;
|
|
3277
3596
|
setStoragePath(null);
|
|
3278
3597
|
let pendingMenuQuotaRefresh = null;
|
|
3279
3598
|
let menuQuotaRefreshStatus;
|
|
@@ -3378,10 +3697,113 @@ async function runAuthLogin() {
|
|
|
3378
3697
|
}
|
|
3379
3698
|
}
|
|
3380
3699
|
const refreshedStorage = await loadAccounts();
|
|
3381
|
-
|
|
3700
|
+
let existingCount = refreshedStorage?.accounts.length ?? 0;
|
|
3382
3701
|
let forceNewLogin = existingCount > 0;
|
|
3702
|
+
let onboardingBackupDiscoveryWarning = null;
|
|
3703
|
+
const loadNamedBackupsForOnboarding = async () => {
|
|
3704
|
+
if (existingCount > 0) {
|
|
3705
|
+
onboardingBackupDiscoveryWarning = null;
|
|
3706
|
+
return [];
|
|
3707
|
+
}
|
|
3708
|
+
try {
|
|
3709
|
+
onboardingBackupDiscoveryWarning = null;
|
|
3710
|
+
return await getNamedBackups();
|
|
3711
|
+
}
|
|
3712
|
+
catch (error) {
|
|
3713
|
+
const code = error.code;
|
|
3714
|
+
log.debug("getNamedBackups failed, skipping restore option", {
|
|
3715
|
+
code,
|
|
3716
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3717
|
+
});
|
|
3718
|
+
if (code && code !== "ENOENT") {
|
|
3719
|
+
onboardingBackupDiscoveryWarning =
|
|
3720
|
+
"Named backup discovery failed. Continuing with browser or manual sign-in only.";
|
|
3721
|
+
console.warn(onboardingBackupDiscoveryWarning);
|
|
3722
|
+
}
|
|
3723
|
+
else {
|
|
3724
|
+
onboardingBackupDiscoveryWarning = null;
|
|
3725
|
+
}
|
|
3726
|
+
return [];
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
let namedBackups = await loadNamedBackupsForOnboarding();
|
|
3383
3730
|
while (true) {
|
|
3384
|
-
const
|
|
3731
|
+
const latestNamedBackup = namedBackups[0] ?? null;
|
|
3732
|
+
const preferManualMode = loginOptions.manual || isBrowserLaunchSuppressed();
|
|
3733
|
+
const signInMode = preferManualMode
|
|
3734
|
+
? "manual"
|
|
3735
|
+
: await promptOAuthSignInMode(latestNamedBackup, onboardingBackupDiscoveryWarning);
|
|
3736
|
+
if (signInMode === "cancel") {
|
|
3737
|
+
if (existingCount > 0) {
|
|
3738
|
+
console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
|
|
3739
|
+
continue loginFlow;
|
|
3740
|
+
}
|
|
3741
|
+
console.log("Cancelled.");
|
|
3742
|
+
return 0;
|
|
3743
|
+
}
|
|
3744
|
+
if (signInMode === "restore-backup") {
|
|
3745
|
+
const latestAvailableBackup = namedBackups[0] ?? null;
|
|
3746
|
+
if (!latestAvailableBackup) {
|
|
3747
|
+
namedBackups = await loadNamedBackupsForOnboarding();
|
|
3748
|
+
continue;
|
|
3749
|
+
}
|
|
3750
|
+
const restoreMode = await promptBackupRestoreMode(latestAvailableBackup);
|
|
3751
|
+
if (restoreMode === "back") {
|
|
3752
|
+
namedBackups = await loadNamedBackupsForOnboarding();
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
const selectedBackup = restoreMode === "manual"
|
|
3756
|
+
? await promptManualBackupSelection(namedBackups)
|
|
3757
|
+
: latestAvailableBackup;
|
|
3758
|
+
if (!selectedBackup) {
|
|
3759
|
+
namedBackups = await loadNamedBackupsForOnboarding();
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
const confirmed = await confirm(UI_COPY.oauth.restoreBackupConfirm(selectedBackup.fileName, selectedBackup.accountCount));
|
|
3763
|
+
if (!confirmed) {
|
|
3764
|
+
namedBackups = await loadNamedBackupsForOnboarding();
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
const displaySettings = await loadDashboardDisplaySettings();
|
|
3768
|
+
applyUiThemeFromDashboardSettings(displaySettings);
|
|
3769
|
+
try {
|
|
3770
|
+
await runActionPanel("Load Backup", `Loading ${selectedBackup.fileName}`, async () => {
|
|
3771
|
+
const restoredStorage = await restoreAccountsFromBackup(selectedBackup.path, { persist: false });
|
|
3772
|
+
const targetIndex = resolveActiveIndex(restoredStorage);
|
|
3773
|
+
const { synced } = await persistAndSyncSelectedAccount({
|
|
3774
|
+
storage: restoredStorage,
|
|
3775
|
+
targetIndex,
|
|
3776
|
+
parsed: targetIndex + 1,
|
|
3777
|
+
switchReason: "restore",
|
|
3778
|
+
preserveActiveIndexByFamily: true,
|
|
3779
|
+
});
|
|
3780
|
+
console.log(UI_COPY.oauth.restoreBackupLoaded(selectedBackup.fileName, restoredStorage.accounts.length));
|
|
3781
|
+
if (!synced) {
|
|
3782
|
+
console.warn(UI_COPY.oauth.restoreBackupSyncWarning);
|
|
3783
|
+
}
|
|
3784
|
+
}, displaySettings);
|
|
3785
|
+
}
|
|
3786
|
+
catch (error) {
|
|
3787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3788
|
+
if (error instanceof StorageError) {
|
|
3789
|
+
console.error(formatStorageErrorHint(error, selectedBackup.path));
|
|
3790
|
+
}
|
|
3791
|
+
else {
|
|
3792
|
+
console.error(`Backup restore failed: ${message}`);
|
|
3793
|
+
}
|
|
3794
|
+
const storageAfterRestoreAttempt = await loadAccounts().catch(() => null);
|
|
3795
|
+
if ((storageAfterRestoreAttempt?.accounts.length ?? 0) > 0) {
|
|
3796
|
+
continue loginFlow;
|
|
3797
|
+
}
|
|
3798
|
+
namedBackups = await loadNamedBackupsForOnboarding();
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
continue loginFlow;
|
|
3802
|
+
}
|
|
3803
|
+
if (signInMode !== "browser" && signInMode !== "manual") {
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3806
|
+
const tokenResult = await runOAuthFlow(forceNewLogin, signInMode);
|
|
3385
3807
|
if (tokenResult.type !== "success") {
|
|
3386
3808
|
if (isUserCancelledOAuth(tokenResult)) {
|
|
3387
3809
|
if (existingCount > 0) {
|
|
@@ -3399,6 +3821,9 @@ async function runAuthLogin() {
|
|
|
3399
3821
|
await syncSelectionToCodex(resolved);
|
|
3400
3822
|
const latestStorage = await loadAccounts();
|
|
3401
3823
|
const count = latestStorage?.accounts.length ?? 1;
|
|
3824
|
+
existingCount = count;
|
|
3825
|
+
namedBackups = [];
|
|
3826
|
+
onboardingBackupDiscoveryWarning = null;
|
|
3402
3827
|
console.log(`Added account. Total: ${count}`);
|
|
3403
3828
|
if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
3404
3829
|
console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
|
|
@@ -3451,15 +3876,29 @@ async function runSwitch(args) {
|
|
|
3451
3876
|
console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
|
|
3452
3877
|
return 0;
|
|
3453
3878
|
}
|
|
3454
|
-
async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, }) {
|
|
3879
|
+
async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, preserveActiveIndexByFamily = false, }) {
|
|
3455
3880
|
const account = storage.accounts[targetIndex];
|
|
3456
3881
|
if (!account) {
|
|
3457
3882
|
throw new Error(`Account ${parsed} not found.`);
|
|
3458
3883
|
}
|
|
3884
|
+
const shouldPreserveActiveIndexByFamily = preserveActiveIndexByFamily &&
|
|
3885
|
+
!!storage.activeIndexByFamily &&
|
|
3886
|
+
targetIndex === storage.activeIndex;
|
|
3459
3887
|
storage.activeIndex = targetIndex;
|
|
3460
3888
|
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
3461
|
-
|
|
3462
|
-
storage.
|
|
3889
|
+
if (shouldPreserveActiveIndexByFamily) {
|
|
3890
|
+
const maxIndex = Math.max(0, storage.accounts.length - 1);
|
|
3891
|
+
for (const family of MODEL_FAMILIES) {
|
|
3892
|
+
const raw = storage.activeIndexByFamily[family];
|
|
3893
|
+
const candidate = typeof raw === "number" && Number.isFinite(raw) ? raw : targetIndex;
|
|
3894
|
+
storage.activeIndexByFamily[family] = Math.max(0, Math.min(candidate, maxIndex));
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
else {
|
|
3898
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
3899
|
+
for (const family of MODEL_FAMILIES) {
|
|
3900
|
+
storage.activeIndexByFamily[family] = targetIndex;
|
|
3901
|
+
}
|
|
3463
3902
|
}
|
|
3464
3903
|
const wasDisabled = account.enabled === false;
|
|
3465
3904
|
if (wasDisabled) {
|
|
@@ -3813,7 +4252,7 @@ export async function runCodexMultiAuthCli(rawArgs) {
|
|
|
3813
4252
|
return 0;
|
|
3814
4253
|
}
|
|
3815
4254
|
if (command === "login") {
|
|
3816
|
-
return runAuthLogin();
|
|
4255
|
+
return runAuthLogin(rest);
|
|
3817
4256
|
}
|
|
3818
4257
|
if (command === "list" || command === "status") {
|
|
3819
4258
|
await showAccountStatus();
|