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.
Files changed (44) hide show
  1. package/README.md +19 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +185 -29
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts +22 -2
  6. package/dist/lib/accounts.d.ts.map +1 -1
  7. package/dist/lib/accounts.js +84 -1
  8. package/dist/lib/accounts.js.map +1 -1
  9. package/dist/lib/auth/browser.d.ts +1 -0
  10. package/dist/lib/auth/browser.d.ts.map +1 -1
  11. package/dist/lib/auth/browser.js +13 -0
  12. package/dist/lib/auth/browser.js.map +1 -1
  13. package/dist/lib/auth/token-utils.d.ts +16 -0
  14. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  15. package/dist/lib/auth/token-utils.js +13 -0
  16. package/dist/lib/auth/token-utils.js.map +1 -1
  17. package/dist/lib/codex-manager/settings-hub.d.ts +21 -0
  18. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/settings-hub.js +48 -65
  20. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  21. package/dist/lib/codex-manager.d.ts +1 -0
  22. package/dist/lib/codex-manager.d.ts.map +1 -1
  23. package/dist/lib/codex-manager.js +517 -78
  24. package/dist/lib/codex-manager.js.map +1 -1
  25. package/dist/lib/request/fetch-helpers.d.ts +18 -0
  26. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  27. package/dist/lib/request/fetch-helpers.js +157 -0
  28. package/dist/lib/request/fetch-helpers.js.map +1 -1
  29. package/dist/lib/schemas.d.ts +7 -0
  30. package/dist/lib/schemas.d.ts.map +1 -1
  31. package/dist/lib/schemas.js +7 -1
  32. package/dist/lib/schemas.js.map +1 -1
  33. package/dist/lib/storage/migrations.d.ts +5 -2
  34. package/dist/lib/storage/migrations.d.ts.map +1 -1
  35. package/dist/lib/storage/migrations.js.map +1 -1
  36. package/dist/lib/storage.d.ts +10 -0
  37. package/dist/lib/storage.d.ts.map +1 -1
  38. package/dist/lib/storage.js +191 -40
  39. package/dist/lib/storage.js.map +1 -1
  40. package/dist/lib/ui/copy.d.ts +24 -4
  41. package/dist/lib/ui/copy.d.ts.map +1 -1
  42. package/dist/lib/ui/copy.js +24 -4
  43. package/dist/lib/ui/copy.js.map +1 -1
  44. 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
- const accountId = normalizeQuotaAccountId(account.accountId);
452
- if (accountId && hasUniqueQuotaAccountId(accounts, account)) {
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, cache, maxAgeMs, now);
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(cache, target.account, snapshot, storage.accounts) ||
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(cache);
632
+ await saveQuotaCache(nextCache);
532
633
  }
533
- return cache;
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
- if (!input.isTTY || !output.isTTY) {
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
- console.log("");
760
- console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
761
- const answer = await rl.question("◆ ");
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
- async function promptOAuthSignInMode() {
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: UI_COPY.oauth.chooseModeSubtitle,
803
- help: UI_COPY.oauth.chooseModeHelp,
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
- const signInMode = await promptOAuthSignInMode();
1072
- if (signInMode === "cancel") {
1073
- return {
1074
- type: "failed",
1075
- reason: "unknown",
1076
- message: UI_COPY.oauth.cancelled,
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
- if (oauthServer.ready) {
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(UI_COPY.oauth.callbackMissed, "warning"));
1103
- code = await promptManualCallback(state);
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.close();
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 (quotaCache) {
1582
+ if (workingQuotaCache) {
1306
1583
  quotaCacheChanged =
1307
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
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 (quotaCache) {
1659
+ if (workingQuotaCache) {
1374
1660
  quotaCacheChanged =
1375
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
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 (quotaCache && quotaCacheChanged) {
1410
- await saveQuotaCache(quotaCache);
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 (quotaCache) {
2120
+ if (workingQuotaCache) {
1831
2121
  const account = storage.accounts[i];
1832
2122
  if (account) {
1833
2123
  quotaCacheChanged =
1834
- updateQuotaCacheForAccount(quotaCache, account, liveQuota, storage.accounts) || quotaCacheChanged;
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 (quotaCache && quotaCacheChanged) {
1856
- await saveQuotaCache(quotaCache);
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 (quotaCache && quotaCacheChanged) {
1927
- await saveQuotaCache(quotaCache);
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 (quotaCache) {
2747
+ if (workingQuotaCache) {
2454
2748
  quotaCacheChanged =
2455
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
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 (!account.accountId && nextAccountId) {
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 (quotaCache) {
2827
+ if (workingQuotaCache) {
2527
2828
  quotaCacheChanged =
2528
- updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
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 (quotaCache && quotaCacheChanged) {
2618
- await saveQuotaCache(quotaCache);
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 (quotaCache && quotaCacheChanged) {
2685
- await saveQuotaCache(quotaCache);
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 tokenResult = await runOAuthFlow(true);
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
- const existingCount = refreshedStorage?.accounts.length ?? 0;
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 tokenResult = await runOAuthFlow(forceNewLogin);
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
- for (const family of MODEL_FAMILIES) {
3462
- storage.activeIndexByFamily[family] = targetIndex;
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();