Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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.

On a brand-new install (no `openai-codex-accounts.json` yet), `codex auth login` opens a first-run setup screen before OAuth. Use it to restore from backups, import an OpenCode pool, run doctor/check paths, open settings and Codex CLI sync, or skip straight to login. All actions reuse the existing backup browser, import, doctor, and settings flows.

Comment on lines +52 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

add upgrade-note and npm-script callout for this workflow change.

docs/getting-started.md:52 documents a new first-run flow, but this segment does not include an upgrade-note pointer or npm-script note. please add a short release/upgrade note reference (and explicitly state script changes or “no script changes”) so rollout docs stay complete.

as per coding guidelines "docs/**: keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/getting-started.md` around lines 52 - 53, Update the new-first-run
paragraph for "codex auth login" to include a short upgrade-note pointer and an
explicit npm-script callout: add a one-sentence upgrade note referencing the
release/upgrade docs and state whether any npm scripts changed (or explicitly
say "no script changes"), and append an npm-script example or note if a new
script was added; target the paragraph mentioning "first-run setup screen" /
"codex auth login" so readers see the upgrade guidance and script implications
immediately.

If you have named backups in `~/.codex/multi-auth/backups/` and no active accounts, the login flow can prompt you to restore before opening OAuth. Confirm to open `Restore From Backup`, review the recoverable backup list, and restore the entries you want. Skip the prompt to continue with a fresh login.

Verify the new account:
Expand Down
110 changes: 110 additions & 0 deletions lib/codex-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ import {
} from "./storage.js";
import type { AccountIdSource, TokenFailure, TokenResult } from "./types.js";
import { ANSI } from "./ui/ansi.js";
import {
type FirstRunWizardOptions,
showFirstRunWizard,
} from "./ui/auth-menu.js";
import { confirm } from "./ui/confirm.js";
import { UI_COPY } from "./ui/copy.js";
import { paintUiText, quotaToneFromLeftPercent } from "./ui/format.js";
Expand Down Expand Up @@ -4839,6 +4843,100 @@ async function runBackupBrowserManager(
}
}

async function buildFirstRunWizardOptions(): Promise<FirstRunWizardOptions> {
let namedBackupCount = 0;
let rotatingBackupCount = 0;
let hasOpencodeSource = false;

try {
const namedBackups = await listNamedBackups();
namedBackupCount = namedBackups.length;
} catch (error) {
console.warn("Failed to list named backups", error);
}
try {
const rotatingBackups = await listRotatingBackups();
rotatingBackupCount = rotatingBackups.length;
} catch (error) {
console.warn("Failed to list rotating backups", error);
}
try {
hasOpencodeSource = (await assessOpencodeAccountPool()) !== null;
} catch (error) {
console.warn("Failed to detect OpenCode import source", error);
Comment on lines +4862 to +4866
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOCTOU race: assessOpencodeAccountPool called twice per import attempt

buildFirstRunWizardOptions calls assessOpencodeAccountPool() to set hasOpencodeSource for the menu render. then inside the import-opencode case (line ~4893), it is called a second time to get the path for the actual import. on windows, antivirus or the opencode process itself may hold a lock on the source file between these two calls — the first succeeds (menu shows the option), the second fails or returns different data (e.g., path changed, pool invalidated). this is a windows-specific TOCTOU risk.

the assessment result from buildFirstRunWizardOptions should be passed down or the import-opencode case should reuse a cached value from the same buildFirstRunWizardOptions call rather than re-assessing independently.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager.ts
Line: 4862-4866

Comment:
**TOCTOU race: `assessOpencodeAccountPool` called twice per import attempt**

`buildFirstRunWizardOptions` calls `assessOpencodeAccountPool()` to set `hasOpencodeSource` for the menu render. then inside the `import-opencode` case (line ~4893), it is called a second time to get the path for the actual import. on windows, antivirus or the opencode process itself may hold a lock on the source file between these two calls — the first succeeds (menu shows the option), the second fails or returns different data (e.g., path changed, pool invalidated). this is a windows-specific TOCTOU risk.

the assessment result from `buildFirstRunWizardOptions` should be passed down or the `import-opencode` case should reuse a cached value from the same `buildFirstRunWizardOptions` call rather than re-assessing independently.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

}

return {
storagePath: getStoragePath(),
namedBackupCount,
rotatingBackupCount,
hasOpencodeSource,
};
}

async function runFirstRunWizard(
displaySettings: DashboardDisplaySettings,
): Promise<"continue" | "cancelled"> {
while (true) {
const action = await showFirstRunWizard(await buildFirstRunWizardOptions());
switch (action.type) {
case "cancel":
console.log("Cancelled.");
return "cancelled";
case "login":
case "skip":
return "continue";
case "restore":
await runBackupBrowserManager(displaySettings);
break;
case "import-opencode": {
const assessment = await assessOpencodeAccountPool();
if (!assessment) {
console.log("No OpenCode account pool was detected.");
break;
}
if (!assessment.valid || assessment.wouldExceedLimit) {
console.log(
assessment.error ?? "OpenCode account pool is not importable.",
);
break;
}
const confirmed = await confirm(
`Import OpenCode accounts from ${assessment.backup.path}?`,
);
if (!confirmed) {
break;
}
await runActionPanel(
"Import OpenCode Accounts",
`Importing from ${assessment.backup.path}`,
async () => {
const imported = await importAccounts(assessment.backup.path);
console.log(
`Imported ${imported.imported} account${imported.imported === 1 ? "" : "s"}. Skipped ${imported.skipped}. Total accounts: ${imported.total}.`,
);
},
displaySettings,
);
break;
}
Comment on lines +4892 to +4922
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

missing try-catch around assessOpencodeAccountPool in import-opencode action.

assessOpencodeAccountPool() at line 4893 can throw (see lib/storage.ts:2071-2095 - it calls loadBackupCandidate and assessBackupRestoreFromPath). compare with lib/codex-manager.ts:4863-4867 where the same call is properly wrapped in buildFirstRunWizardOptions. if the opencode pool file is corrupted or locked, the wizard crashes instead of showing a friendly error.

proposed fix
 			case "import-opencode": {
+				let assessment: Awaited<ReturnType<typeof assessOpencodeAccountPool>> = null;
+				try {
+					assessment = await assessOpencodeAccountPool();
+				} catch (error) {
+					console.log(
+						`Failed to assess OpenCode pool: ${error instanceof Error ? error.message : String(error)}`,
+					);
+					break;
+				}
-				const assessment = await assessOpencodeAccountPool();
 				if (!assessment) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` around lines 4892 - 4922, Wrap the call to
assessOpencodeAccountPool() inside the "import-opencode" case in a try-catch
(the call currently at the start of that case) so that any errors thrown by
assessOpencodeAccountPool (which calls
loadBackupCandidate/assessBackupRestoreFromPath) are caught; on error, log a
friendly message via console.error or processLogger.error including the
error.message and then break out of the case instead of letting the wizard
crash; keep the rest of the import flow (confirm, runActionPanel,
importAccounts) unchanged when assessOpencodeAccountPool succeeds.

case "settings":
await configureUnifiedSettings(displaySettings);
break;
case "doctor":
await runActionPanel(
"Doctor",
"Checking storage and sync paths",
async () => {
await runDoctor(["--json"]);
},
displaySettings,
);
break;
Comment on lines +4927 to +4935
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--json flag renders raw JSON in interactive action panel

runDoctor(["--json"]) emits a machine-readable JSON blob via console.log. runActionPanel captures those log calls and surfaces them line-by-line in the live panel — so a first-run user will see a wall of unformatted JSON instead of the human-readable "Doctor diagnostics / ✓ / !" output produced when the flag is omitted.

Suggested change
await runActionPanel(
"Doctor",
"Checking storage and sync paths",
async () => {
await runDoctor(["--json"]);
},
displaySettings,
);
break;
case "doctor":
await runActionPanel(
"Doctor",
"Checking storage and sync paths",
async () => {
await runDoctor([]);
},
displaySettings,
);
break;
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager.ts
Line: 4927-4935

Comment:
**`--json` flag renders raw JSON in interactive action panel**

`runDoctor(["--json"])` emits a machine-readable JSON blob via `console.log`. `runActionPanel` captures those log calls and surfaces them line-by-line in the live panel — so a first-run user will see a wall of unformatted JSON instead of the human-readable "Doctor diagnostics / ✓ / !" output produced when the flag is omitted.

```suggestion
		case "doctor":
			await runActionPanel(
				"Doctor",
				"Checking storage and sync paths",
				async () => {
					await runDoctor([]);
				},
				displaySettings,
			);
			break;
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

}
}
}
Comment on lines +4877 to +4938
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# check for test coverage of first-run wizard
rg -n "firstRunWizard|buildFirstRunWizardOptions|runFirstRunWizard" --type=ts -g '*test*'

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Find test files and understand repository structure
fd -e 'test.ts' -e 'test.js' -e 'spec.ts' -e 'spec.js' | head -20

Repository: ndycode/codex-multi-auth

Length of output: 608


🏁 Script executed:

# Also check for vitest config and test directories
find . -maxdepth 3 -type d -name '__tests__' -o -name 'tests' -o -name 'test' 2>/dev/null | head -20

Repository: ndycode/codex-multi-auth

Length of output: 74


🏁 Script executed:

# Search for files that import or reference the functions from codex-manager.ts
rg "runFirstRunWizard|buildFirstRunWizardOptions" --type=ts | head -30

Repository: ndycode/codex-multi-auth

Length of output: 410


🏁 Script executed:

# Search for wizard-related tests
rg "wizard" --type=ts -i test/ | head -20

Repository: ndycode/codex-multi-auth

Length of output: 1272


🏁 Script executed:

# Search for firstRun in test files
rg "firstRun" --type=ts test/ | head -20

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Check if there's a dedicated manager test file
fd -e 'test.ts' | xargs grep -l "codex-manager\|CodexManager" 2>/dev/null

Repository: ndycode/codex-multi-auth

Length of output: 230


🏁 Script executed:

# Check size and read the test file for first-run wizard tests
wc -l test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 103


🏁 Script executed:

# Extract and show the wizard-related tests
rg -A 15 "it.*first-run wizard|it.*imports OpenCode" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 3441


🏁 Script executed:

# Look for buildFirstRunWizardOptions tests
rg -B 2 -A 10 "buildFirstRunWizardOptions" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Look for assessOpencodeAccountPool tests
rg "assessOpencodeAccountPool" --type=ts test/

Repository: ndycode/codex-multi-auth

Length of output: 987


🏁 Script executed:

# Search for error handling in import-opencode tests
rg -B 5 -A 20 "import-opencode.*invalid|import-opencode.*error|wouldExceedLimit|assessment.error" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 5522


🏁 Script executed:

# Check what the actual test covers for import-opencode by looking at more context
rg -A 40 "it.*imports OpenCode accounts from the first-run wizard" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 1250


🏁 Script executed:

# Look for error/invalid test cases in first-run wizard context
rg -B 3 -A 30 "it.*first-run wizard.*invalid|it.*first-run wizard.*error|it.*import.*opencode.*error|it.*import.*opencode.*invalid" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Check what happens with cancel action in first-run wizard
rg -B 3 -A 15 'type.*cancel|"cancel"' test/codex-manager-cli.test.ts | head -50

Repository: ndycode/codex-multi-auth

Length of output: 1736


🏁 Script executed:

# Search for any test that might use wouldExceedLimit or assessment.error with first-run wizard
rg -B 5 -A 15 "wouldExceedLimit.*true|assessment.error" test/codex-manager-cli.test.ts | grep -A 15 -B 5 "first-run\|import-opencode"

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Search for skip action in first-run wizard tests
rg '"skip"' test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Look for tests that specifically test buildFirstRunWizardOptions function
rg -B 2 -A 20 "describe.*buildFirstRunWizardOptions|it.*buildFirstRunWizardOptions" test/

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Check test/storage.test.ts for assessOpencodeAccountPool error tests
rg -B 2 -A 15 "assessOpencodeAccountPool" test/storage.test.ts | head -60

Repository: ndycode/codex-multi-auth

Length of output: 1496


🏁 Script executed:

# Check what error cases are tested in storage.test.ts for assessOpencodeAccountPool
rg -B 5 -A 15 "refuses malformed|wouldExceedLimit|assessOpencodeAccountPool" test/storage.test.ts | head -80

Repository: ndycode/codex-multi-auth

Length of output: 2556


🏁 Script executed:

# Check if there's a test for buildFirstRunWizardOptions error handling
rg -n "buildFirstRunWizardOptions\|assessOpencodeAccountPool\|listNamedBackups\|listRotatingBackups" lib/codex-manager.ts | head -20

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Get the buildFirstRunWizardOptions function implementation
rg -B 2 -A 30 "async function buildFirstRunWizardOptions" lib/codex-manager.ts

Repository: ndycode/codex-multi-auth

Length of output: 881


🏁 Script executed:

# Look at the full structure of runFirstRunWizard and buildFirstRunWizardOptions to see error handling
sed -n '4800,4950p' lib/codex-manager.ts | head -100

Repository: ndycode/codex-multi-auth

Length of output: 2821


🏁 Script executed:

# Check if there are tests for buildFirstRunWizardOptions error paths that I might have missed
rg -n "buildFirstRunWizardOptions" test/codex-manager-cli.test.ts

Repository: ndycode/codex-multi-auth

Length of output: 50


🏁 Script executed:

# Look at the full test file to see all test cases for first-run wizard
rg -n "it\(" test/codex-manager-cli.test.ts | grep -i "first\|wizard\|import"

Repository: ndycode/codex-multi-auth

Length of output: 601


🏁 Script executed:

# Final verification: list all test cases to confirm skip and restore are not tested
rg -A 1 "it\(\"" test/codex-manager-cli.test.ts | grep -A 1 "skip\|restore" | head -20

Repository: ndycode/codex-multi-auth

Length of output: 583


add tests for first-run wizard error paths and missing action branches.

the code handles multiple error scenarios (buildFirstRunWizardOptions try-catch, import-opencode validity checks) but test coverage is incomplete. currently tested: cancel/login/settings/doctor actions with happy-path import-opencode. missing:

  • buildFirstRunWizardOptions when listNamedBackups/listRotatingBackups/assessOpencodeAccountPool throw
  • skip action in first-run wizard (test/codex-manager-cli.test.ts shows skip scenarios in other contexts, not in wizard)
  • restore action in first-run wizard (only backup browser restore tested, not wizard action)
  • import-opencode with assessment.valid === false, assessment.wouldExceedLimit === true, or assessment === null

violates lib/** guidelines requiring affected test citations. verify these paths are covered before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager.ts` around lines 4877 - 4938, Add tests exercising the
missing first-run wizard branches and error paths referenced by
runFirstRunWizard: simulate errors thrown by buildFirstRunWizardOptions (and
underlying listNamedBackups/listRotatingBackups) and assert the wizard handles
those exceptions; add a test for the "skip" action returning "continue"; add a
test for the "restore" action invoking runBackupBrowserManager; and add tests
for import-opencode when assessOpencodeAccountPool returns null, returns {
valid: false } and returns { wouldExceedLimit: true } to assert the correct
console/log behavior and no import occurs; implement these tests in
test/codex-manager-cli.test.ts by stubbing/mocking buildFirstRunWizardOptions,
assessOpencodeAccountPool, runBackupBrowserManager, and importAccounts so you
can drive each branch of runFirstRunWizard deterministically.


async function runAuthLogin(): Promise<number> {
setStoragePath(null);
let suppressRecoveryPrompt = false;
Expand Down Expand Up @@ -5095,6 +5193,18 @@ async function runAuthLogin(): Promise<number> {
}
}
}
if (existingCount === 0 && isInteractiveLoginMenuAvailable()) {
const displaySettings = await loadDashboardDisplaySettings();
applyUiThemeFromDashboardSettings(displaySettings);
const firstRunResult = await runFirstRunWizard(displaySettings);
if (firstRunResult === "cancelled") {
return 0;
}
const refreshedAfterWizard = await loadAccounts();
if ((refreshedAfterWizard?.accounts.length ?? 0) > 0) {
continue;
}
}
let forceNewLogin = existingCount > 0;
while (true) {
const tokenResult = await runOAuthFlow(forceNewLogin);
Expand Down
59 changes: 59 additions & 0 deletions lib/ui/auth-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ export type AuthMenuAction =
| { type: "delete-all" }
| { type: "cancel" };

export interface FirstRunWizardOptions {
storagePath: string;
namedBackupCount: number;
rotatingBackupCount: number;
hasOpencodeSource: boolean;
}

export type FirstRunWizardAction =
| { type: "login" }
| { type: "restore" }
| { type: "import-opencode" }
| { type: "settings" }
| { type: "doctor" }
| { type: "skip" }
| { type: "cancel" };

export type AccountAction =
| "back"
| "delete"
Expand Down Expand Up @@ -935,4 +951,47 @@ export async function showAccountDetails(
}
}

export async function showFirstRunWizard(
options: FirstRunWizardOptions,
): Promise<FirstRunWizardAction> {
const ui = getUiRuntimeOptions();
const items: MenuItem<FirstRunWizardAction>[] = [
{
label: UI_COPY.firstRun.restore,
hint: UI_COPY.firstRun.backupSummary(
options.namedBackupCount,
options.rotatingBackupCount,
),
value: { type: "restore" },
color: "yellow",
},
...(options.hasOpencodeSource
? [
{
label: UI_COPY.firstRun.importOpencode,
value: { type: "import-opencode" as const },
color: "yellow" as const,
},
]
: []),
{ label: UI_COPY.firstRun.login, value: { type: "login" }, color: "green" },
{ label: UI_COPY.firstRun.settings, value: { type: "settings" } },
{ label: UI_COPY.firstRun.doctor, value: { type: "doctor" } },
{ label: UI_COPY.firstRun.skip, value: { type: "skip" } },
{ label: UI_COPY.firstRun.cancel, value: { type: "cancel" }, color: "red" },
];

const result = await select(items, {
message: UI_COPY.firstRun.title,
subtitle: UI_COPY.firstRun.subtitle(options.storagePath),
help: UI_COPY.firstRun.help,
clearScreen: true,
selectedEmphasis: "minimal",
focusStyle: ui.v2Enabled ? "chip" : "row-invert",
theme: ui.theme,
});

return result ?? { type: "cancel" };
}

export { isTTY };
21 changes: 21 additions & 0 deletions lib/ui/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ export const UI_COPY = {
"(a) add, (c) check, (b) best, fi(x), (s) settings, (d) deep, (g) problem, (f) fresh, (r) reset, (q) back [a/c/b/x/s/d/g/f/r/q]: ",
invalidModePrompt: "Use one of: a, c, b, x, s, d, g, f, r, q.",
},
firstRun: {
title: "First-Run Setup",
subtitle: (storagePath: string) =>
`No saved accounts detected. Storage will be created at ${storagePath}.`,
backupSummary: (named: number, rotating: number) => {
const total = named + rotating;
if (total === 0) return "No backups detected yet";
if (named > 0 && rotating > 0)
return `Named backups: ${named}, rotating backups: ${rotating}`;
if (named > 0) return `Named backups: ${named}`;
return `Rotating backups: ${rotating}`;
},
restore: "Open Backup Browser",
importOpencode: "Import OpenCode Accounts",
login: "Add or Log In",
settings: "Open Settings & Sync",
doctor: "Run Doctor / Check Paths",
skip: "Skip setup and continue",
cancel: "Exit",
help: "↑↓ Move | Enter Select | Q Exit",
},
} as const;

/**
Expand Down
Loading