|
| 1 | +import { isAbsolute } from "node:path"; |
| 2 | +import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api"; |
| 3 | + |
| 4 | +const USAGE = `Usage: |
| 5 | + ocx account main doctor [--json] |
| 6 | + ocx account main list [--json] |
| 7 | + ocx account main register <label> [--json] |
| 8 | + ocx account main add <label> |
| 9 | + ocx account main switch <profile-id-or-label> --yes [--json] |
| 10 | + ocx account main recover [--rollback --yes] [--json] |
| 11 | +
|
| 12 | +Native main login profiles change the physical Codex App/CLI login in the effective CODEX_HOME. |
| 13 | +They are independent from the OpenCodex Pool selected by 'ocx account use openai'.`; |
| 14 | + |
| 15 | +interface PublicProfile { |
| 16 | + id: string; |
| 17 | + label: string; |
| 18 | + identityHint: string; |
| 19 | + state: "active" | "inactive"; |
| 20 | +} |
| 21 | + |
| 22 | +function flag(args: string[], name: string): boolean { |
| 23 | + const index = args.indexOf(name); |
| 24 | + if (index < 0) return false; |
| 25 | + args.splice(index, 1); |
| 26 | + return true; |
| 27 | +} |
| 28 | + |
| 29 | +function reject(args: string[]): number { |
| 30 | + if (args.length > 0) console.error(`Unexpected argument(s): ${args.join(", ")}`); |
| 31 | + console.error(USAGE); |
| 32 | + return 1; |
| 33 | +} |
| 34 | + |
| 35 | +function printProfiles(profiles: PublicProfile[]): void { |
| 36 | + if (profiles.length === 0) { console.log("No native main login profiles registered."); return; } |
| 37 | + const rows = profiles.map(profile => [profile.state === "active" ? "*" : " ", profile.label, profile.id, profile.identityHint]); |
| 38 | + const header = ["", "LABEL", "PROFILE ID", "IDENTITY"]; |
| 39 | + const widths = header.map((value, index) => Math.max(value.length, ...rows.map(row => row[index]!.length))); |
| 40 | + const line = (columns: string[]) => columns.map((value, index) => value.padEnd(widths[index]!)).join(" ").trimEnd(); |
| 41 | + console.log([line(header), ...rows.map(line)].join("\n")); |
| 42 | +} |
| 43 | + |
| 44 | +async function runOfficialCodexLogin(codexHome: string): Promise<number> { |
| 45 | + const child = Bun.spawn(["codex", "login"], { |
| 46 | + env: { ...process.env, CODEX_HOME: codexHome }, |
| 47 | + stdin: "inherit", |
| 48 | + stdout: "inherit", |
| 49 | + stderr: "inherit", |
| 50 | + }); |
| 51 | + return child.exited; |
| 52 | +} |
| 53 | + |
| 54 | +export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): Promise<number> { |
| 55 | + const sub = args.shift(); |
| 56 | + const wantsJson = flag(args, "--json"); |
| 57 | + const confirmed = flag(args, "--yes"); |
| 58 | + const rollback = flag(args, "--rollback"); |
| 59 | + const baseUrl = await resolveBaseUrl(deps); |
| 60 | + if (!baseUrl) return proxyUnreachable(); |
| 61 | + |
| 62 | + if (sub === "doctor" || sub === "list") { |
| 63 | + if (args.length > 0 || confirmed || rollback) return reject(args); |
| 64 | + const path = sub === "doctor" ? "/api/native-main-profiles/doctor" : "/api/native-main-profiles"; |
| 65 | + const result = await apiJson(deps, baseUrl, "GET", path); |
| 66 | + if (result.status === 0) return proxyUnreachable(); |
| 67 | + if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`); |
| 68 | + if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2)); |
| 69 | + else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []); |
| 70 | + return 0; |
| 71 | + } |
| 72 | + |
| 73 | + if (sub === "register") { |
| 74 | + const label = args.shift(); |
| 75 | + if (!label || args.length > 0 || confirmed || rollback) return reject(args); |
| 76 | + const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/register", { label }); |
| 77 | + if (result.status === 0) return proxyUnreachable(); |
| 78 | + if (result.status !== 200) return apiError(result.json, "failed to register the current native login"); |
| 79 | + if (wantsJson) console.log(JSON.stringify(result.json, null, 2)); |
| 80 | + else console.log(`Registered '${label}' for ${String(result.json.effectiveCodexHome ?? "the effective CODEX_HOME")}.`); |
| 81 | + return 0; |
| 82 | + } |
| 83 | + |
| 84 | + if (sub === "add") { |
| 85 | + const label = args.shift(); |
| 86 | + if (!label || args.length > 0 || wantsJson || confirmed || rollback) return reject(args); |
| 87 | + const stage = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage", {}); |
| 88 | + if (stage.status === 0) return proxyUnreachable(); |
| 89 | + if (stage.status !== 200) return apiError(stage.json, "failed to prepare native login staging"); |
| 90 | + const stageId = typeof stage.json.stageId === "string" ? stage.json.stageId : ""; |
| 91 | + const stagingHome = typeof stage.json.stagingCodexHome === "string" ? stage.json.stagingCodexHome : ""; |
| 92 | + if (!stageId || !isAbsolute(stagingHome)) { |
| 93 | + console.error("Error: the proxy returned an invalid staging session."); |
| 94 | + return 1; |
| 95 | + } |
| 96 | + let exitCode = 1; |
| 97 | + try { |
| 98 | + console.error(`Starting official Codex login in restricted staging home: ${stagingHome}`); |
| 99 | + exitCode = await (deps.runCodexLoginImpl ?? runOfficialCodexLogin)(stagingHome); |
| 100 | + if (exitCode !== 0) throw new Error("Official Codex login did not complete successfully."); |
| 101 | + const finish = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/finish", { stageId, label }); |
| 102 | + if (finish.status === 0) return proxyUnreachable(); |
| 103 | + if (finish.status !== 200) return apiError(finish.json, "failed to encrypt the staged native login"); |
| 104 | + console.log(`Added encrypted native profile '${label}'.`); |
| 105 | + return 0; |
| 106 | + } catch (error) { |
| 107 | + await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/cancel", { stageId }); |
| 108 | + console.error(`Error: ${error instanceof Error ? error.message : "Official Codex login failed."}`); |
| 109 | + return exitCode === 0 ? 1 : exitCode; |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + if (sub === "switch") { |
| 114 | + const target = args.shift(); |
| 115 | + if (!target || args.length > 0 || rollback || !confirmed) { |
| 116 | + if (!confirmed) console.error("Close Codex App/CLI, then pass --yes to confirm it is stopped."); |
| 117 | + return reject(args); |
| 118 | + } |
| 119 | + const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/switch", { target, confirmedStopped: true }); |
| 120 | + if (result.status === 0) return proxyUnreachable(); |
| 121 | + if (result.status !== 200) return apiError(result.json, "failed to switch the native login"); |
| 122 | + if (wantsJson) console.log(JSON.stringify(result.json, null, 2)); |
| 123 | + else { |
| 124 | + const profile = result.json.activeProfile as PublicProfile | undefined; |
| 125 | + console.log(`Native Codex login is now '${profile?.label ?? target}'. Restart Codex App/CLI before continuing.`); |
| 126 | + } |
| 127 | + return 0; |
| 128 | + } |
| 129 | + |
| 130 | + if (sub === "recover") { |
| 131 | + if (args.length > 0 || (rollback && !confirmed)) { |
| 132 | + if (rollback && !confirmed) console.error("--rollback changes the native login and requires --yes."); |
| 133 | + return reject(args); |
| 134 | + } |
| 135 | + const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/recover", { rollback }); |
| 136 | + if (result.status === 0) return proxyUnreachable(); |
| 137 | + if (result.status !== 200) return apiError(result.json, "failed to recover the native-profile transaction"); |
| 138 | + if (wantsJson) console.log(JSON.stringify(result.json, null, 2)); |
| 139 | + else console.log(result.json.recovered === true ? `Recovery completed: ${String(result.json.action ?? "converged")}.` : "No recovery journal is pending."); |
| 140 | + return 0; |
| 141 | + } |
| 142 | + |
| 143 | + return reject(args); |
| 144 | +} |
0 commit comments