Skip to content
Closed
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 @@ -82,6 +82,8 @@ Use the restore path when you already have named backup files and want to recove

The restore manager shows each backup name, account count, freshness, and whether the restore would exceed the account limit before it lets you apply anything.

If you already have an OpenCode account pool on the same machine, use the recovery import option from the login dashboard to preview and import those saved accounts before creating new OAuth sessions. The detector checks the standard OpenCode account file and honors `CODEX_OPENCODE_POOL_PATH` when you need to point at a specific file.

---

## Sync And Settings
Expand Down
4 changes: 4 additions & 0 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type LoginMode =
| "check"
| "deep-check"
| "verify-flagged"
| "import-opencode"
| "restore-backup"
| "cancel";

Expand Down Expand Up @@ -248,6 +249,9 @@ async function promptLoginModeFallback(
) {
return { mode: "restore-backup" };
}
if (normalized === "i" || normalized === "import-opencode") {
return { mode: "import-opencode" };
}
if (normalized === "q" || normalized === "quit")
return { mode: "cancel" };
console.log(UI_COPY.fallback.invalidModePrompt);
Expand Down
59 changes: 58 additions & 1 deletion lib/codex-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { promises as fs, existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { basename, dirname, resolve } from "node:path";
import {
createAuthorizationFlow,
exchangeAuthorizationCode,
Expand Down Expand Up @@ -65,9 +65,13 @@ import {
} from "./quota-cache.js";
import {
assessNamedBackupRestore,
assessOpencodeAccountPool,
type BackupRestoreAssessment,
formatRedactedFilesystemError,
getActionableNamedBackupRestores,
getRedactedFilesystemErrorLabel,
getNamedBackupsDirectoryPath,
importAccounts,
listNamedBackups,
listRotatingBackups,
NAMED_BACKUP_LIST_CONCURRENCY,
Expand Down Expand Up @@ -4448,6 +4452,59 @@ async function runAuthLogin(): Promise<number> {
}
continue;
}
if (menuResult.mode === "import-opencode") {
let assessment: BackupRestoreAssessment | null;
try {
assessment = await assessOpencodeAccountPool({
currentStorage,
});
} catch (error) {
const errorLabel = formatRedactedFilesystemError(error);
console.error(`Import assessment failed: ${errorLabel}`);
continue;
}
if (!assessment) {
console.log("No OpenCode account pool was detected.");
continue;
}
if (!assessment.backup.valid || !assessment.eligibleForRestore) {
const assessmentErrorLabel = formatRedactedFilesystemError(
assessment.error || "OpenCode account pool is not importable.",
);
console.log(assessmentErrorLabel);
continue;
}
if (assessment.wouldExceedLimit) {
console.log(
`Import would exceed the account limit (${assessment.currentAccountCount ?? "?"} current, ${assessment.mergedAccountCount ?? "?"} after import). Remove accounts first.`,
);
continue;
}
const backupLabel = basename(assessment.backup.path);
const confirmed = await confirm(
`Import OpenCode accounts from ${backupLabel}?`,
);
if (!confirmed) {
continue;
}
try {
await runActionPanel(
"Import OpenCode Accounts",
`Importing from ${backupLabel}`,
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,
);
} catch (error) {
const errorLabel = formatRedactedFilesystemError(error);
console.error(`Import failed: ${errorLabel}`);
}
continue;
}
if (menuResult.mode === "fresh" && menuResult.deleteAll) {
if (destructiveActionInFlight) {
console.log("Another destructive action is already running. Wait for it to finish.");
Expand Down
Loading