Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f9482b4
feat(auth): prompt for recovery on startup
ndycode Mar 12, 2026
d1a763a
Merge branch 'git-plan/02-backup-restore-manager' into git-plan/03-st…
ndycode Mar 12, 2026
d756d0a
fix(auth): harden startup recovery prompt flow
ndycode Mar 13, 2026
cedc3e9
Merge branch 'git-plan/02-backup-restore-manager' into git-plan/03-st…
ndycode Mar 13, 2026
4832de2
fix(auth): harden startup recovery flows
ndycode Mar 13, 2026
ef42d0d
fix(auth): avoid prereading injected backup restores
ndycode Mar 13, 2026
8eba086
fix(auth): keep startup recovery non-fatal
ndycode Mar 13, 2026
dd11f35
fix(storage): keep injected backup listing resilient
ndycode Mar 13, 2026
dcd1bac
fix(auth): address latest review follow-ups
ndycode Mar 13, 2026
6c1370b
fix(auth): reuse startup recovery assessments
ndycode Mar 13, 2026
a786c40
fix(auth): tighten startup recovery follow-ups
ndycode Mar 13, 2026
350187d
fix(auth): redact startup prompt failures
ndycode Mar 13, 2026
8b1843c
fix(auth): harden startup recovery prompt flow
ndycode Mar 13, 2026
e53a45b
fix(auth): reuse startup recovery state on retry
ndycode Mar 13, 2026
357ec0b
Merge branch 'git-plan/02-backup-restore-manager' into git-plan/03-st…
ndycode Mar 13, 2026
ac13c0a
Merge git-plan/02-backup-restore-manager into git-plan/03-startup-rec…
ndycode Mar 13, 2026
d155fc4
fix(auth): redact restore failures and cover stat retry path
ndycode Mar 13, 2026
8ddfe38
Merge remote-tracking branch 'origin/git-plan/02-backup-restore-manag…
ndycode Mar 14, 2026
e280be8
Merge remote-tracking branch 'origin/git-plan/02-backup-restore-manag…
ndycode Mar 14, 2026
5d278d0
test(auth): cover empty startup recovery menu path
ndycode Mar 14, 2026
8d7d09d
fix(auth): tighten startup recovery regressions
ndycode Mar 14, 2026
1bfed78
test(auth): cover zero-actionable startup recovery branches
ndycode Mar 14, 2026
cd85db2
Merge remote-tracking branch 'origin/git-plan/02-backup-restore-manag…
ndycode Mar 14, 2026
8380de6
fix(auth): harden startup restore manager
ndycode Mar 14, 2026
011aa0c
fix(auth): remove dead startup recovery branch
ndycode Mar 14, 2026
b37cb57
Merge remote-tracking branch 'origin/git-plan/02-backup-restore-manag…
ndycode Mar 14, 2026
142f4f1
fix(auth): address startup recovery review follow-ups
ndycode Mar 14, 2026
192e4e3
Merge branch 'origin/git-plan/02-backup-restore-manager' into git-pla…
ndycode Mar 14, 2026
ecfce8b
fix(auth): avoid false empty backup messaging
ndycode Mar 15, 2026
413d1b8
Merge branch 'origin/git-plan/02-backup-restore-manager' into pr77-fi…
ndycode Mar 15, 2026
c5e6cb9
Merge backup restore manager updates into startup recovery prompt
ndycode Mar 15, 2026
82e099d
Merge latest restore review gaps into startup recovery prompt
ndycode Mar 15, 2026
e1e6ff0
fix(auth): redact restore assessment failure output
ndycode Mar 15, 2026
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
2 changes: 2 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Expected flow:
4. Return to the terminal when the browser step completes.
5. Confirm the account appears in the saved account list.

If you have named backups in your `backups/` directory and no active accounts, the login flow will prompt you to restore before opening OAuth. Confirm to launch the existing restore manager; skip to proceed with a fresh login.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Verify the new account:

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ If the account pool is still not usable:
codex auth login
```

If `codex auth login` starts with no saved accounts and named backups are present, you will be prompted to restore before OAuth. This prompt only appears in interactive terminals and is skipped after intentional reset flows.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

---

## Verify Install And Routing
Expand Down
6 changes: 5 additions & 1 deletion lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export function isNonInteractiveMode(): boolean {
return false;
}

export function isInteractiveLoginMenuAvailable(): boolean {
return !isNonInteractiveMode() && isTTY();
}

export async function promptAddAnotherAccount(
currentCount: number,
): Promise<boolean> {
Expand Down Expand Up @@ -243,7 +247,7 @@ export async function promptLoginMode(
return { mode: "add" };
}

if (!isTTY()) {
if (!isInteractiveLoginMenuAvailable()) {
return promptLoginModeFallback(existingAccounts);
}

Expand Down
37 changes: 37 additions & 0 deletions lib/codex-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { copyTextToClipboard, openBrowserUrl } from "./auth/browser.js";
import { startLocalOAuthServer } from "./auth/server.js";
import {
type ExistingAccountInfo,
isInteractiveLoginMenuAvailable,
promptAddAnotherAccount,
promptLoginMode,
} from "./cli.js";
Expand Down Expand Up @@ -76,6 +77,7 @@ import {
assessNamedBackupRestore,
type FlaggedAccountMetadataV1,
type FlaggedAccountStorageV1,
getActionableNamedBackupRestores,
getNamedBackupsDirectoryPath,
getStoragePath,
listNamedBackups,
Expand Down Expand Up @@ -4176,6 +4178,8 @@ async function runBackupRestoreManager(

async function runAuthLogin(): Promise<number> {
setStoragePath(null);
let suppressRecoveryPrompt = false;
let recoveryPromptAttempted = false;
let pendingMenuQuotaRefresh: Promise<void> | null = null;
let menuQuotaRefreshStatus: string | undefined;
loginFlow: while (true) {
Expand Down Expand Up @@ -4302,6 +4306,7 @@ async function runAuthLogin(): Promise<number> {
continue;
}
if (menuResult.mode === "fresh" && menuResult.deleteAll) {
suppressRecoveryPrompt = true;
await runActionPanel(
DESTRUCTIVE_ACTION_COPY.deleteSavedAccounts.label,
DESTRUCTIVE_ACTION_COPY.deleteSavedAccounts.stage,
Expand All @@ -4316,6 +4321,7 @@ async function runAuthLogin(): Promise<number> {
continue;
}
if (menuResult.mode === "reset") {
suppressRecoveryPrompt = true;
await runActionPanel(
DESTRUCTIVE_ACTION_COPY.resetLocalState.label,
DESTRUCTIVE_ACTION_COPY.resetLocalState.stage,
Expand Down Expand Up @@ -4360,6 +4366,37 @@ async function runAuthLogin(): Promise<number> {

const refreshedStorage = await loadAccounts();
const existingCount = refreshedStorage?.accounts.length ?? 0;
const canPromptForRecovery =
!suppressRecoveryPrompt &&
!recoveryPromptAttempted &&
existingCount === 0 &&
isInteractiveLoginMenuAvailable();
if (canPromptForRecovery) {
recoveryPromptAttempted = true;
const recoveryState = await getActionableNamedBackupRestores({
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
currentStorage: refreshedStorage,
});
if (recoveryState.assessments.length > 0) {
const displaySettings = await loadDashboardDisplaySettings();
applyUiThemeFromDashboardSettings(displaySettings);
const backupDir = getNamedBackupsDirectoryPath();
const sample = recoveryState.assessments[0];
const backupLabel =
sample?.backup.name ??
`${recoveryState.assessments.length} backup${
recoveryState.assessments.length === 1 ? "" : "s"
}`;
const restoreNow = await confirm(
`Found ${recoveryState.assessments.length} recoverable backup${
recoveryState.assessments.length === 1 ? "" : "s"
} (${backupLabel}) in ${backupDir}. Restore now?`,
);
if (restoreNow) {
await runBackupRestoreManager(displaySettings);
continue;
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
let forceNewLogin = existingCount > 0;
while (true) {
const tokenResult = await runOAuthFlow(forceNewLogin);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down
37 changes: 37 additions & 0 deletions lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,11 @@ export interface BackupRestoreAssessment {
error?: string;
}

export interface ActionableNamedBackupRecoveries {
assessments: Awaited<ReturnType<typeof assessNamedBackupRestore>>[];
totalBackups: number;
}

export function getLastAccountsSaveTimestamp(): number {
return lastAccountsSaveTimestamp;
}
Expand Down Expand Up @@ -1312,6 +1317,38 @@ export function getNamedBackupsDirectoryPath(): string {
return getNamedBackupsDirectory();
}

export async function getActionableNamedBackupRestores(
options: {
currentStorage?: AccountStorageV3 | null;
backups?: NamedBackupMetadata[];
assess?: typeof assessNamedBackupRestore;
} = {},
): Promise<ActionableNamedBackupRecoveries> {
const backups = options.backups ?? (await listNamedBackups());
if (backups.length === 0) {
return { assessments: [], totalBackups: 0 };
}

const currentStorage =
options.currentStorage === undefined
? await loadAccounts()
: options.currentStorage;
const assess = options.assess ?? assessNamedBackupRestore;
const assessments = await Promise.all(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
backups.map((backup) => assess(backup.name, { currentStorage })),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const actionable = assessments.filter(
(assessment) =>
assessment.valid &&
!assessment.wouldExceedLimit &&
assessment.imported !== null &&
assessment.imported > 0,
);

return { assessments: actionable, totalBackups: backups.length };
}

export async function createNamedBackup(
name: string,
options: { force?: boolean } = {},
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Expand Down
Loading