Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,52 @@ codex auth doctor --json

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
---

## Soft Reset
## Reset Options

PowerShell:
- Delete a single saved account: `codex auth login` → pick account → **Delete Account**
- Delete saved accounts: `codex auth login` → Danger Zone → **Delete Saved Accounts**
- Reset local state: `codex auth login` → Danger Zone → **Reset Local State**

Exact effects:

| Action | Saved accounts | Flagged/problem accounts | Settings | Codex CLI sync state | Quota cache |
| --- | --- | --- | --- | --- | --- |
| Delete Account | Delete the selected saved account | Delete the matching flagged/problem entry for that refresh token | Keep | Keep | Keep |
| Delete Saved Accounts | Delete all saved accounts | Keep | Keep | Keep | Keep |
| Reset Local State | Delete all saved accounts | Delete all flagged/problem accounts | Keep | Keep | Clear |

To perform the same actions manually:

Delete saved accounts only:

```powershell
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\openai-codex-flagged-accounts.json" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\settings.json" -Force -ErrorAction SilentlyContinue
codex auth login
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json.wal" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json.bak*" -Force -ErrorAction SilentlyContinue
```

```bash
rm -f ~/.codex/multi-auth/openai-codex-accounts.json
rm -f ~/.codex/multi-auth/openai-codex-accounts.json.wal
rm -f ~/.codex/multi-auth/openai-codex-accounts.json.bak*
```

Bash:
Reset local state (also clears flagged/problem accounts and quota cache; preserves settings and Codex CLI sync state):

```powershell
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json.wal" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\openai-codex-accounts.json.bak*" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\openai-codex-flagged-accounts.json" -Force -ErrorAction SilentlyContinue
Remove-Item "$HOME\.codex\multi-auth\quota-cache.json" -Force -ErrorAction SilentlyContinue
```

```bash
rm -f ~/.codex/multi-auth/openai-codex-accounts.json
rm -f ~/.codex/multi-auth/openai-codex-accounts.json.wal
rm -f ~/.codex/multi-auth/openai-codex-accounts.json.bak*
rm -f ~/.codex/multi-auth/openai-codex-flagged-accounts.json
rm -f ~/.codex/multi-auth/settings.json
codex auth login
rm -f ~/.codex/multi-auth/quota-cache.json
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

---
Expand Down
60 changes: 43 additions & 17 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ import {
} from "./lib/logger.js";
import { checkAndNotify } from "./lib/auto-update-checker.js";
import { handleContextOverflow } from "./lib/context-overflow.js";
import {
DESTRUCTIVE_ACTION_COPY,
deleteAccountAtIndex,
deleteSavedAccounts,
resetLocalState,
} from "./lib/destructive-actions.js";
import {
AccountManager,
getAccountIdCandidates,
Expand All @@ -122,13 +128,11 @@ import {
loadAccounts,
saveAccounts,
withAccountStorageTransaction,
clearAccounts,
setStoragePath,
exportAccounts,
importAccounts,
loadFlaggedAccounts,
saveFlaggedAccounts,
clearFlaggedAccounts,
findMatchingAccountIndex,
StorageError,
formatStorageErrorHint,
Expand Down Expand Up @@ -3101,19 +3105,22 @@ while (attempted.size < Math.max(1, accountCount)) {

if (menuResult.mode === "manage") {
if (typeof menuResult.deleteAccountIndex === "number") {
const target = workingStorage.accounts[menuResult.deleteAccountIndex];
if (target) {
workingStorage.accounts.splice(menuResult.deleteAccountIndex, 1);
clampActiveIndices(workingStorage);
await saveAccounts(workingStorage);
await saveFlaggedAccounts({
version: 1,
accounts: flaggedStorage.accounts.filter(
(flagged) => flagged.refreshToken !== target.refreshToken,
),
});
const deleted = await deleteAccountAtIndex({
storage: workingStorage,
index: menuResult.deleteAccountIndex,
flaggedStorage,
});
Comment thread
ndycode marked this conversation as resolved.
if (deleted) {
invalidateAccountManagerCache();
console.log(`\nDeleted ${target.email ?? `Account ${menuResult.deleteAccountIndex + 1}`}.\n`);
const label = formatAccountLabel(
deleted.removedAccount,
menuResult.deleteAccountIndex,
);
const flaggedNote =
deleted.removedFlaggedCount > 0
? ` Removed ${deleted.removedFlaggedCount} matching problem account${deleted.removedFlaggedCount === 1 ? "" : "s"}.`
: "";
console.log(`\nDeleted ${label}.${flaggedNote}\n`);
}
continue;
}
Expand Down Expand Up @@ -3143,16 +3150,35 @@ while (attempted.size < Math.max(1, accountCount)) {
if (menuResult.mode === "fresh") {
startFresh = true;
if (menuResult.deleteAll) {
await clearAccounts();
await clearFlaggedAccounts();
const result = await deleteSavedAccounts();
invalidateAccountManagerCache();
console.log(
"\nCleared saved accounts from active storage. Recovery snapshots remain available. Starting fresh.\n",
`\n${
result.accountsCleared
? DESTRUCTIVE_ACTION_COPY.deleteSavedAccounts.completed
: "Delete saved accounts completed with warnings. Some saved account artifacts could not be removed; see logs."
}\n`,
);
}
break;
}

if (menuResult.mode === "reset") {
startFresh = true;
const result = await resetLocalState();
invalidateAccountManagerCache();
console.log(
`\n${
result.accountsCleared &&
result.flaggedCleared &&
result.quotaCacheCleared
? DESTRUCTIVE_ACTION_COPY.resetLocalState.completed
: "Reset local state completed with warnings. Some local artifacts could not be removed; see logs."
}\n`,
);
break;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

startFresh = false;
break;
}
Expand Down
115 changes: 93 additions & 22 deletions lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import { DESTRUCTIVE_ACTION_COPY } from "./destructive-actions.js";
import type { AccountIdSource } from "./types.js";
import {
showAuthMenu,
showAccountDetails,
isTTY,
type AccountStatus,
isTTY,
showAccountDetails,
showAuthMenu,
} from "./ui/auth-menu.js";
import { UI_COPY } from "./ui/copy.js";

Expand All @@ -19,20 +20,25 @@ export function isNonInteractiveMode(): boolean {
if (!input.isTTY || !output.isTTY) return true;
if (process.env.CODEX_TUI === "1") return true;
if (process.env.CODEX_DESKTOP === "1") return true;
if ((process.env.TERM_PROGRAM ?? "").trim().toLowerCase() === "codex") return true;
if ((process.env.TERM_PROGRAM ?? "").trim().toLowerCase() === "codex")
return true;
if (process.env.ELECTRON_RUN_AS_NODE === "1") return true;
return false;
}

export async function promptAddAnotherAccount(currentCount: number): Promise<boolean> {
export async function promptAddAnotherAccount(
currentCount: number,
): Promise<boolean> {
if (isNonInteractiveMode()) {
return false;
}

const rl = createInterface({ input, output });
try {
console.log(`\n${UI_COPY.fallback.addAnotherTip}\n`);
const answer = await rl.question(UI_COPY.fallback.addAnotherQuestion(currentCount));
const answer = await rl.question(
UI_COPY.fallback.addAnotherQuestion(currentCount),
);
const normalized = answer.trim().toLowerCase();
return normalized === "y" || normalized === "yes";
} finally {
Expand All @@ -46,6 +52,7 @@ export type LoginMode =
| "fix"
| "settings"
| "fresh"
| "reset"
| "manage"
| "check"
| "deep-check"
Expand Down Expand Up @@ -94,25 +101,34 @@ export interface LoginMenuResult {
deleteAll?: boolean;
}

function formatAccountLabel(account: ExistingAccountInfo, index: number): string {
function formatAccountLabel(
account: ExistingAccountInfo,
index: number,
): string {
const num = index + 1;
const label = account.accountLabel?.trim();
if (account.email?.trim()) {
return label ? `${num}. ${label} (${account.email})` : `${num}. ${account.email}`;
return label
? `${num}. ${label} (${account.email})`
: `${num}. ${account.email}`;
}
if (label) {
return `${num}. ${label}`;
}
if (account.accountId?.trim()) {
const suffix = account.accountId.length > 6 ? account.accountId.slice(-6) : account.accountId;
const suffix =
account.accountId.length > 6
? account.accountId.slice(-6)
: account.accountId;
return `${num}. ${suffix}`;
}
return `${num}. Account`;
}

function resolveAccountSourceIndex(account: ExistingAccountInfo): number {
const sourceIndex =
typeof account.sourceIndex === "number" && Number.isFinite(account.sourceIndex)
typeof account.sourceIndex === "number" &&
Number.isFinite(account.sourceIndex)
? Math.max(0, Math.floor(account.sourceIndex))
: undefined;
if (typeof sourceIndex === "number") return sourceIndex;
Expand All @@ -123,21 +139,40 @@ function resolveAccountSourceIndex(account: ExistingAccountInfo): number {
}

function warnUnresolvableAccountSelection(account: ExistingAccountInfo): void {
const label = account.email?.trim() || account.accountId?.trim() || `index ${account.index + 1}`;
const label =
account.email?.trim() ||
account.accountId?.trim() ||
`index ${account.index + 1}`;
console.log(`Unable to resolve saved account for action: ${label}`);
}

async function promptDeleteAllTypedConfirm(): Promise<boolean> {
const rl = createInterface({ input, output });
try {
const answer = await rl.question("Type DELETE to remove all saved accounts: ");
const answer = await rl.question(
DESTRUCTIVE_ACTION_COPY.deleteSavedAccounts.typedConfirm,
);
return answer.trim() === "DELETE";
} finally {
rl.close();
}
}

async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]): Promise<LoginMenuResult> {
async function promptResetTypedConfirm(): Promise<boolean> {
const rl = createInterface({ input, output });
try {
const answer = await rl.question(
DESTRUCTIVE_ACTION_COPY.resetLocalState.typedConfirm,
);
return answer.trim() === "RESET";
} finally {
rl.close();
}
}

async function promptLoginModeFallback(
existingAccounts: ExistingAccountInfo[],
): Promise<LoginMenuResult> {
const rl = createInterface({ input, output });
try {
if (existingAccounts.length > 0) {
Expand All @@ -152,17 +187,41 @@ async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]):
const answer = await rl.question(UI_COPY.fallback.selectModePrompt);
const normalized = answer.trim().toLowerCase();
if (normalized === "a" || normalized === "add") return { mode: "add" };
if (normalized === "b" || normalized === "p" || normalized === "forecast") {
if (
normalized === "b" ||
normalized === "p" ||
normalized === "forecast"
) {
return { mode: "forecast" };
}
if (normalized === "x" || normalized === "fix") return { mode: "fix" };
if (normalized === "s" || normalized === "settings" || normalized === "configure") {
if (
normalized === "s" ||
normalized === "settings" ||
normalized === "configure"
) {
return { mode: "settings" };
}
if (normalized === "f" || normalized === "fresh" || normalized === "clear") {
if (
normalized === "f" ||
normalized === "fresh" ||
normalized === "clear"
) {
if (!(await promptDeleteAllTypedConfirm())) {
console.log("\nDelete saved accounts cancelled.\n");
continue;
}
return { mode: "fresh", deleteAll: true };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if (normalized === "c" || normalized === "check") return { mode: "check" };
if (normalized === "r" || normalized === "reset") {
if (!(await promptResetTypedConfirm())) {
console.log("\nReset local state cancelled.\n");
continue;
}
return { mode: "reset" };
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (normalized === "c" || normalized === "check")
return { mode: "check" };
if (normalized === "d" || normalized === "deep") {
return { mode: "deep-check" };
}
Expand All @@ -174,7 +233,8 @@ async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]):
) {
return { mode: "verify-flagged" };
}
if (normalized === "q" || normalized === "quit") return { mode: "cancel" };
if (normalized === "q" || normalized === "quit")
return { mode: "cancel" };
console.log(UI_COPY.fallback.invalidModePrompt);
}
} finally {
Expand Down Expand Up @@ -215,6 +275,12 @@ export async function promptLoginMode(
continue;
}
return { mode: "fresh", deleteAll: true };
case "reset-all":
if (!(await promptResetTypedConfirm())) {
console.log("\nReset local state cancelled.\n");
continue;
}
return { mode: "reset" };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case "check":
return { mode: "check" };
case "deep-check":
Expand Down Expand Up @@ -306,7 +372,8 @@ export async function promptAccountSelection(
): Promise<AccountSelectionCandidate | null> {
if (candidates.length === 0) return null;
const defaultIndex =
typeof options.defaultIndex === "number" && Number.isFinite(options.defaultIndex)
typeof options.defaultIndex === "number" &&
Number.isFinite(options.defaultIndex)
? Math.max(0, Math.min(options.defaultIndex, candidates.length - 1))
: 0;

Expand All @@ -316,15 +383,19 @@ export async function promptAccountSelection(

const rl = createInterface({ input, output });
try {
console.log(`\n${options.title ?? "Multiple workspaces detected for this account:"}`);
console.log(
`\n${options.title ?? "Multiple workspaces detected for this account:"}`,
);
candidates.forEach((candidate, index) => {
const isDefault = candidate.isDefault ? " (default)" : "";
console.log(` ${index + 1}. ${candidate.label}${isDefault}`);
});
console.log("");

while (true) {
const answer = await rl.question(`Select workspace [${defaultIndex + 1}]: `);
const answer = await rl.question(
`Select workspace [${defaultIndex + 1}]: `,
);
const normalized = answer.trim().toLowerCase();
if (!normalized) {
return candidates[defaultIndex] ?? candidates[0] ?? null;
Expand Down
Loading