Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e4000df
feat: add codex multi-auth sync flow
ndycode Mar 10, 2026
6e28b6f
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
ddcdaec
fix: address sync flow review findings
ndycode Mar 10, 2026
36af180
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
9ef917a
fix: address sync review follow-ups
ndycode Mar 10, 2026
29a4198
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
34b56f3
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
55b4d8f
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
f7927eb
fix: clarify sync capacity guidance
ndycode Mar 10, 2026
ad7cc37
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
1d50b4d
Merge remote-tracking branch 'origin/split/pr75-sync-foundation' into…
ndycode Mar 10, 2026
11856b6
fix: harden sync prune recovery
ndycode Mar 15, 2026
84a3baa
fix: address final sync review comments
ndycode Mar 15, 2026
a6c91a0
fix: cover sync prune restore after sync failure
ndycode Mar 15, 2026
5403d19
fix: address remaining sync review findings
ndycode Mar 15, 2026
539bccc
fix: clean up sync prune backups
ndycode Mar 15, 2026
052c426
Fix remaining PR 77 sync review findings
ndycode Mar 15, 2026
64302b5
Harden remaining PR 77 review follow-ups
ndycode Mar 15, 2026
adfcb05
Fix backup pruning review follow-ups
ndycode Mar 15, 2026
b663643
Fix remaining Windows sync review issues
ndycode Mar 15, 2026
43de29a
Fix final sync review follow-ups
ndycode Mar 15, 2026
13f227e
Fix overlap cleanup rollback on persist failure
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
716 changes: 715 additions & 1 deletion index.ts

Large diffs are not rendered by default.

141 changes: 137 additions & 4 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AccountIdSource } from "./types.js";
import {
showAuthMenu,
showAccountDetails,
showSyncToolsMenu,
isTTY,
type AccountStatus,
} from "./ui/auth-menu.js";
Expand Down Expand Up @@ -46,6 +47,9 @@ export type LoginMode =
| "check"
| "deep-check"
| "verify-flagged"
| "experimental-toggle-sync"
| "experimental-sync-now"
| "experimental-cleanup-overlaps"
| "cancel";

export interface ExistingAccountInfo {
Expand All @@ -62,6 +66,7 @@ export interface ExistingAccountInfo {

export interface LoginMenuOptions {
flaggedCount?: number;
syncFromCodexMultiAuthEnabled?: boolean;
}

export interface LoginMenuResult {
Expand Down Expand Up @@ -101,7 +106,117 @@ async function promptDeleteAllTypedConfirm(): Promise<boolean> {
}
}

async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]): Promise<LoginMenuResult> {
async function promptSyncToolsFallback(
rl: ReturnType<typeof createInterface>,
syncEnabled: boolean,
): Promise<LoginMenuResult | null> {
while (true) {
const syncState = syncEnabled ? "enabled" : "disabled";
const answer = await rl.question(
`Sync tools: (t)oggle [${syncState}], (i)mport now, (o)verlap cleanup, (b)ack [t/i/o/b]: `,
);
const normalized = answer.trim().toLowerCase();
if (normalized === "t" || normalized === "toggle") return { mode: "experimental-toggle-sync" };
if (normalized === "i" || normalized === "import") return { mode: "experimental-sync-now" };
if (normalized === "o" || normalized === "overlap") return { mode: "experimental-cleanup-overlaps" };
if (normalized === "b" || normalized === "back") return null;
console.log("Please enter one of: t, i, o, b.");
}
}

export interface SyncPruneCandidate {
index: number;
email?: string;
accountLabel?: string;
isCurrentAccount?: boolean;
reason?: string;
}

function formatPruneCandidate(candidate: SyncPruneCandidate): string {
const label = formatAccountLabel(
{
index: candidate.index,
email: candidate.email,
accountLabel: candidate.accountLabel,
isCurrentAccount: candidate.isCurrentAccount,
},
candidate.index,
);
const details: string[] = [];
if (candidate.isCurrentAccount) details.push("current");
if (candidate.reason) details.push(candidate.reason);
return details.length > 0 ? `${label} | ${details.join(" | ")}` : label;
}

export async function promptCodexMultiAuthSyncPrune(
neededCount: number,
candidates: SyncPruneCandidate[],
): Promise<number[] | null> {
if (isNonInteractiveMode()) {
return null;
}

const suggested = candidates
.filter((candidate) => candidate.isCurrentAccount !== true)
.slice(0, neededCount)
.map((candidate) => candidate.index);

const rl = createInterface({ input, output });
try {
console.log("");
console.log(`Sync needs ${neededCount} free slot(s).`);
console.log("Suggested removals:");
for (const candidate of candidates) {
console.log(` ${formatPruneCandidate(candidate)}`);
}
console.log("");
console.log(
suggested.length >= neededCount
? "Press Enter to remove the suggested accounts, or enter comma-separated numbers."
: "Enter comma-separated account numbers to remove, or Q to cancel.",
);

while (true) {
const answer = await rl.question(`Remove at least ${neededCount} account(s): `);
const normalized = answer.trim();
if (!normalized) {
if (suggested.length >= neededCount) {
return suggested;
}
console.log("No default suggestion is available. Enter one or more account numbers.");
continue;
}

if (normalized.toLowerCase() === "q" || normalized.toLowerCase() === "quit") {
return null;
}

const tokens = normalized.split(",").map((value) => value.trim());
if (tokens.length === 0 || tokens.some((value) => !/^\d+$/.test(value))) {
console.log("Enter comma-separated account numbers (for example: 1,2).");
continue;
}
const allowedIndexes = new Set(candidates.map((candidate) => candidate.index));
const unique = Array.from(new Set(tokens.map((value) => Number.parseInt(value, 10) - 1)));
if (unique.some((index) => !allowedIndexes.has(index))) {
console.log("Enter only account numbers shown above.");
continue;
}
if (unique.length < neededCount) {
console.log(`Select at least ${neededCount} unique account number(s).`);
continue;
}
return unique;
}
} finally {
rl.close();
}
}

async function promptLoginModeFallback(
existingAccounts: ExistingAccountInfo[],
options: LoginMenuOptions,
): Promise<LoginMenuResult> {
const rl = createInterface({ input, output });
try {
if (existingAccounts.length > 0) {
Expand All @@ -113,15 +228,25 @@ async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]):
}

while (true) {
const answer = await rl.question("(a)dd, (f)resh, (c)heck, (d)eep, (v)erify flagged, or (q)uit? [a/f/c/d/v/q]: ");
const answer = await rl.question(
"(a)dd, (f)resh, (c)heck, (d)eep, (v)erify flagged, (s)ync tools, or (q)uit? [a/f/c/d/v/s/q]: ",
);
Comment on lines +231 to +233
const normalized = answer.trim().toLowerCase();
if (normalized === "a" || normalized === "add") return { mode: "add" };
if (normalized === "f" || normalized === "fresh") return { mode: "fresh", deleteAll: true };
if (normalized === "c" || normalized === "check") return { mode: "check" };
if (normalized === "d" || normalized === "deep") return { mode: "deep-check" };
if (normalized === "v" || normalized === "verify") return { mode: "verify-flagged" };
if (normalized === "s" || normalized === "sync") {
const syncAction = await promptSyncToolsFallback(
rl,
options.syncFromCodexMultiAuthEnabled === true,
);
if (syncAction) return syncAction;
continue;
}
if (normalized === "q" || normalized === "quit") return { mode: "cancel" };
console.log("Please enter one of: a, f, c, d, v, q.");
console.log("Please enter one of: a, f, c, d, v, s, q.");
}
} finally {
rl.close();
Expand All @@ -137,12 +262,13 @@ export async function promptLoginMode(
}

if (!isTTY()) {
return promptLoginModeFallback(existingAccounts);
return promptLoginModeFallback(existingAccounts, options);
}

while (true) {
const action = await showAuthMenu(existingAccounts, {
flaggedCount: options.flaggedCount ?? 0,
syncFromCodexMultiAuthEnabled: options.syncFromCodexMultiAuthEnabled === true,
});

switch (action.type) {
Expand All @@ -160,6 +286,13 @@ export async function promptLoginMode(
return { mode: "deep-check" };
case "verify-flagged":
return { mode: "verify-flagged" };
case "sync-tools": {
const syncAction = await showSyncToolsMenu(options.syncFromCodexMultiAuthEnabled === true);
if (syncAction === "toggle-sync") return { mode: "experimental-toggle-sync" };
if (syncAction === "sync-now") return { mode: "experimental-sync-now" };
if (syncAction === "cleanup-overlaps") return { mode: "experimental-cleanup-overlaps" };
continue;
}
case "select-account": {
const accountAction = await showAccountDetails(action.account);
if (accountAction === "delete") {
Expand Down
Loading