codex-multi-auth 1.1.11 → 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 -4
  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 +365 -33
  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" },
@@ -829,15 +852,64 @@ function applyTokenAccountIdentity(account, tokenAccountId) {
829
852
  account.accountIdSource = nextIdentity.accountIdSource;
830
853
  return true;
831
854
  }
832
- async function promptManualCallback(state) {
833
- if (!input.isTTY || !output.isTTY) {
855
+ async function promptManualCallback(state, options = {}) {
856
+ const useInteractivePrompt = input.isTTY && output.isTTY;
857
+ if (!useInteractivePrompt && !options.allowNonTty) {
834
858
  return null;
835
859
  }
836
860
  const rl = createInterface({ input, output });
837
861
  try {
838
- console.log("");
839
- console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
840
- 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
+ }
841
913
  if (answer.includes("\u001b")) {
842
914
  return null;
843
915
  }
@@ -857,7 +929,7 @@ async function promptManualCallback(state) {
857
929
  return parsed.code;
858
930
  }
859
931
  catch (error) {
860
- if (isAbortError(error)) {
932
+ if (isAbortError(error) || isReadlineClosedError(error)) {
861
933
  return null;
862
934
  }
863
935
  throw error;
@@ -866,20 +938,56 @@ async function promptManualCallback(state) {
866
938
  rl.close();
867
939
  }
868
940
  }
869
- 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) {
870
960
  if (!input.isTTY || !output.isTTY) {
871
961
  return "browser";
872
962
  }
873
963
  const ui = getUiRuntimeOptions();
874
964
  const items = [
965
+ { label: UI_COPY.oauth.signInHeading, value: "cancel", kind: "heading" },
875
966
  { label: UI_COPY.oauth.openBrowser, value: "browser", color: "green" },
876
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" },
877
981
  { label: UI_COPY.oauth.back, value: "cancel", color: "red" },
878
982
  ];
879
983
  const selected = await select(items, {
880
984
  message: UI_COPY.oauth.chooseModeTitle,
881
- subtitle: UI_COPY.oauth.chooseModeSubtitle,
882
- 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,
883
991
  clearScreen: true,
884
992
  theme: ui.theme,
885
993
  selectedEmphasis: "minimal",
@@ -892,11 +1000,81 @@ async function promptOAuthSignInMode() {
892
1000
  return "browser";
893
1001
  if (lower === "2")
894
1002
  return "manual";
1003
+ if (lower === "3" && backupOption)
1004
+ return "restore-backup";
895
1005
  return undefined;
896
1006
  },
897
1007
  });
898
1008
  return selected ?? "cancel";
899
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
+ }
900
1078
  async function waitForMenuReturn(options = {}) {
901
1079
  if (!input.isTTY || !output.isTTY) {
902
1080
  return;
@@ -1142,18 +1320,29 @@ async function runActionPanel(title, stage, action, settings) {
1142
1320
  throw failed;
1143
1321
  }
1144
1322
  }
1145
- async function runOAuthFlow(forceNewLogin) {
1323
+ async function runOAuthFlow(forceNewLogin, signInMode) {
1146
1324
  const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
1147
- const oauthServer = await startLocalOAuthServer({ state });
1148
1325
  let code = null;
1326
+ let oauthServer = null;
1149
1327
  try {
1150
- const signInMode = await promptOAuthSignInMode();
1151
- if (signInMode === "cancel") {
1152
- return {
1153
- type: "failed",
1154
- reason: "unknown",
1155
- message: UI_COPY.oauth.cancelled,
1156
- };
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
+ }
1157
1346
  }
1158
1347
  if (signInMode === "browser") {
1159
1348
  const opened = openBrowserUrl(url);
@@ -1172,18 +1361,23 @@ async function runOAuthFlow(forceNewLogin) {
1172
1361
  const copied = copyTextToClipboard(url);
1173
1362
  console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
1174
1363
  }
1175
- if (oauthServer.ready) {
1364
+ const waitingForCallback = signInMode === "browser" && oauthServer?.ready === true;
1365
+ if (waitingForCallback && oauthServer) {
1176
1366
  console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
1177
1367
  const callbackResult = await oauthServer.waitForCode(state);
1178
1368
  code = callbackResult?.code ?? null;
1179
1369
  }
1180
1370
  if (!code) {
1181
- console.log(stylePromptText(UI_COPY.oauth.callbackMissed, "warning"));
1182
- 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" });
1183
1377
  }
1184
1378
  }
1185
1379
  finally {
1186
- oauthServer.close();
1380
+ oauthServer?.close();
1187
1381
  }
1188
1382
  if (!code) {
1189
1383
  return {
@@ -3369,7 +3563,15 @@ async function handleManageAction(storage, menuResult) {
3369
3563
  const existing = storage.accounts[idx];
3370
3564
  if (!existing)
3371
3565
  return;
3372
- 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);
3373
3575
  if (tokenResult.type !== "success") {
3374
3576
  console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
3375
3577
  return;
@@ -3380,7 +3582,17 @@ async function handleManageAction(storage, menuResult) {
3380
3582
  console.log(`Refreshed account ${idx + 1}.`);
3381
3583
  }
3382
3584
  }
3383
- 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;
3384
3596
  setStoragePath(null);
3385
3597
  let pendingMenuQuotaRefresh = null;
3386
3598
  let menuQuotaRefreshStatus;
@@ -3485,10 +3697,113 @@ async function runAuthLogin() {
3485
3697
  }
3486
3698
  }
3487
3699
  const refreshedStorage = await loadAccounts();
3488
- const existingCount = refreshedStorage?.accounts.length ?? 0;
3700
+ let existingCount = refreshedStorage?.accounts.length ?? 0;
3489
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();
3490
3730
  while (true) {
3491
- 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);
3492
3807
  if (tokenResult.type !== "success") {
3493
3808
  if (isUserCancelledOAuth(tokenResult)) {
3494
3809
  if (existingCount > 0) {
@@ -3506,6 +3821,9 @@ async function runAuthLogin() {
3506
3821
  await syncSelectionToCodex(resolved);
3507
3822
  const latestStorage = await loadAccounts();
3508
3823
  const count = latestStorage?.accounts.length ?? 1;
3824
+ existingCount = count;
3825
+ namedBackups = [];
3826
+ onboardingBackupDiscoveryWarning = null;
3509
3827
  console.log(`Added account. Total: ${count}`);
3510
3828
  if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
3511
3829
  console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
@@ -3558,15 +3876,29 @@ async function runSwitch(args) {
3558
3876
  console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
3559
3877
  return 0;
3560
3878
  }
3561
- async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, }) {
3879
+ async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, preserveActiveIndexByFamily = false, }) {
3562
3880
  const account = storage.accounts[targetIndex];
3563
3881
  if (!account) {
3564
3882
  throw new Error(`Account ${parsed} not found.`);
3565
3883
  }
3884
+ const shouldPreserveActiveIndexByFamily = preserveActiveIndexByFamily &&
3885
+ !!storage.activeIndexByFamily &&
3886
+ targetIndex === storage.activeIndex;
3566
3887
  storage.activeIndex = targetIndex;
3567
3888
  storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
3568
- for (const family of MODEL_FAMILIES) {
3569
- 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
+ }
3570
3902
  }
3571
3903
  const wasDisabled = account.enabled === false;
3572
3904
  if (wasDisabled) {
@@ -3920,7 +4252,7 @@ export async function runCodexMultiAuthCli(rawArgs) {
3920
4252
  return 0;
3921
4253
  }
3922
4254
  if (command === "login") {
3923
- return runAuthLogin();
4255
+ return runAuthLogin(rest);
3924
4256
  }
3925
4257
  if (command === "list" || command === "status") {
3926
4258
  await showAccountStatus();