oc-codex-multi-auth 6.18.0 → 6.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -79
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +22 -0
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +79 -0
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
- package/dist/lib/accounts/rate-limits.js +25 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -1
- package/dist/lib/accounts/rotation.d.ts +22 -23
- package/dist/lib/accounts/rotation.d.ts.map +1 -1
- package/dist/lib/accounts/rotation.js +62 -37
- package/dist/lib/accounts/rotation.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +31 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +56 -1
- package/dist/lib/accounts/stale-state.js.map +1 -1
- package/dist/lib/accounts/state.d.ts +6 -0
- package/dist/lib/accounts/state.d.ts.map +1 -1
- package/dist/lib/accounts/state.js +15 -1
- package/dist/lib/accounts/state.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts +3 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +35 -0
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auth/token-utils.d.ts +21 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -1
- package/dist/lib/auth/token-utils.js +30 -0
- package/dist/lib/auth/token-utils.js.map +1 -1
- package/dist/lib/codex-reset.d.ts +13 -0
- package/dist/lib/codex-reset.d.ts.map +1 -1
- package/dist/lib/codex-reset.js +20 -4
- package/dist/lib/codex-reset.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +55 -5
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +88 -21
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +4 -2
- package/dist/lib/parallel-probe.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +24 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +66 -20
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/schemas.d.ts +9 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +3 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +5 -0
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +21 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts.map +1 -1
- package/dist/lib/storage/normalize.js +106 -5
- package/dist/lib/storage/normalize.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +8 -66
- package/dist/lib/tools/codex-doctor.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +7 -1
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +8 -1
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +14 -1
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +6 -1
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/doctor-repair.d.ts +10 -0
- package/dist/lib/tools/doctor-repair.d.ts.map +1 -0
- package/dist/lib/tools/doctor-repair.js +86 -0
- package/dist/lib/tools/doctor-repair.js.map +1 -0
- package/dist/lib/tools/index.d.ts +3 -0
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +176 -46
- package/dist/lib/tui-status.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +226 -25
|
@@ -257,8 +257,28 @@ async function readStandaloneStorage(path) {
|
|
|
257
257
|
try {
|
|
258
258
|
const raw = await readFile(path, "utf-8");
|
|
259
259
|
const parsed = JSON.parse(raw);
|
|
260
|
+
// Shape validation, not just parse validation: a JSON array, scalar, or
|
|
261
|
+
// object without an `accounts` array is unreadable by the plugin runtime
|
|
262
|
+
// too (normalizeAccountStorage rejects it), so reporting it as a healthy
|
|
263
|
+
// empty pool (exit 0, "No accounts configured") hides the corruption
|
|
264
|
+
// from scripted callers that key on exit codes.
|
|
265
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
266
|
+
return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
|
|
267
|
+
}
|
|
268
|
+
// Forward-compat mirror of the runtime guard: a newer schema version
|
|
269
|
+
// must not be shown as readable accounts by this build.
|
|
270
|
+
const version = parsed.version;
|
|
271
|
+
if (typeof version === "number" && Number.isFinite(version) && version > 3) {
|
|
272
|
+
return {
|
|
273
|
+
storage: null,
|
|
274
|
+
error: `Unsupported account storage schema version ${version}; this build supports up to version 3.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (!Array.isArray(parsed.accounts)) {
|
|
278
|
+
return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
|
|
279
|
+
}
|
|
260
280
|
return {
|
|
261
|
-
storage:
|
|
281
|
+
storage: normalizeStandaloneStorage(parsed),
|
|
262
282
|
error: null,
|
|
263
283
|
};
|
|
264
284
|
} catch (error) {
|
|
@@ -307,6 +327,29 @@ function mergeStandaloneAccounts(target, source) {
|
|
|
307
327
|
return mergeStandaloneAccounts(source, target);
|
|
308
328
|
}
|
|
309
329
|
|
|
330
|
+
// Mirror of `isStaleGeneratedAccountLabel` / `dropStaleGeneratedLabel` in
|
|
331
|
+
// lib/auth/token-utils.ts and lib/storage/normalize.ts. The standalone CLI
|
|
332
|
+
// reads the pool through this normalizer and never through the compiled
|
|
333
|
+
// `normalizeAccountStorage`, so without the mirror `status`, `list`, `health`,
|
|
334
|
+
// `doctor` and `dashboard` keep printing the org-derived label the plugin
|
|
335
|
+
// itself now drops - next to the account id, which is the identity the label
|
|
336
|
+
// was misnaming. The marker must hold this account's own id suffix so a name
|
|
337
|
+
// set with `codex-label` survives.
|
|
338
|
+
const GENERATED_LABEL_PATTERN = /\s\[id:[^\]]*\]$/;
|
|
339
|
+
|
|
340
|
+
function dropStaleStandaloneLabel(account) {
|
|
341
|
+
const label = typeof account?.accountLabel === "string" ? account.accountLabel.trim() : "";
|
|
342
|
+
const accountId = typeof account?.accountId === "string" ? account.accountId.trim() : "";
|
|
343
|
+
if (!label || !accountId) return account;
|
|
344
|
+
const marker = label.match(GENERATED_LABEL_PATTERN)?.[0];
|
|
345
|
+
if (!marker) return account;
|
|
346
|
+
const suffix = accountId.length > 6 ? accountId.slice(-6) : accountId;
|
|
347
|
+
if (marker !== ` [id:${suffix}]`) return account;
|
|
348
|
+
const next = { ...account };
|
|
349
|
+
delete next.accountLabel;
|
|
350
|
+
return next;
|
|
351
|
+
}
|
|
352
|
+
|
|
310
353
|
function normalizeStandaloneStorage(storage) {
|
|
311
354
|
if (!Array.isArray(storage.accounts)) return storage;
|
|
312
355
|
const accounts = [...storage.accounts];
|
|
@@ -324,7 +367,9 @@ function normalizeStandaloneStorage(storage) {
|
|
|
324
367
|
if (sourceIndex === i) break;
|
|
325
368
|
}
|
|
326
369
|
}
|
|
327
|
-
const normalizedAccounts = accounts
|
|
370
|
+
const normalizedAccounts = accounts
|
|
371
|
+
.filter((_, index) => !removed.has(index))
|
|
372
|
+
.map(dropStaleStandaloneLabel);
|
|
328
373
|
return {
|
|
329
374
|
...storage,
|
|
330
375
|
accounts: normalizedAccounts,
|
|
@@ -332,9 +377,37 @@ function normalizeStandaloneStorage(storage) {
|
|
|
332
377
|
};
|
|
333
378
|
}
|
|
334
379
|
|
|
380
|
+
const MASKED_VALUE = "*****";
|
|
381
|
+
// The head/tail mask keeps eight characters, so it conceals nothing worth
|
|
382
|
+
// concealing below thirteen: `me@x.io` would print in full and `me@x.io12`
|
|
383
|
+
// all but its middle character. `doctor` output is what users paste into
|
|
384
|
+
// issues, so anything shorter is replaced outright instead. Input is trimmed
|
|
385
|
+
// first, or `" me@x.io "` clears the cutoff on padding alone and drops back
|
|
386
|
+
// into the partial mask.
|
|
387
|
+
const MASK_MIN_LENGTH = 13;
|
|
388
|
+
|
|
335
389
|
function maskValue(value, includeSensitive) {
|
|
336
|
-
if (includeSensitive || typeof value !== "string"
|
|
337
|
-
|
|
390
|
+
if (includeSensitive || typeof value !== "string") return value;
|
|
391
|
+
const trimmed = value.trim();
|
|
392
|
+
if (!trimmed) return trimmed;
|
|
393
|
+
if (trimmed.length < MASK_MIN_LENGTH) return MASKED_VALUE;
|
|
394
|
+
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Six characters when the id is shown in full, matching what the
|
|
398
|
+
// in-conversation surfaces print as `id:`. Four when it is head/tail masked,
|
|
399
|
+
// which is the tail `maskValue` already discloses as `accountId` in the same
|
|
400
|
+
// payload, so the printed identity never reveals more of an id than the field
|
|
401
|
+
// beside it. Nothing at all when the id was too short for that mask: the
|
|
402
|
+
// `accountId` next to it is then `*****`, and four raw characters of a short
|
|
403
|
+
// id can be the whole id.
|
|
404
|
+
function accountIdSuffix(accountId, includeSensitive) {
|
|
405
|
+
if (!accountId) return undefined;
|
|
406
|
+
if (includeSensitive) {
|
|
407
|
+
return accountId.length > 6 ? accountId.slice(-6) : accountId;
|
|
408
|
+
}
|
|
409
|
+
if (accountId.length < MASK_MIN_LENGTH) return undefined;
|
|
410
|
+
return accountId.slice(-4);
|
|
338
411
|
}
|
|
339
412
|
|
|
340
413
|
function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
|
|
@@ -345,21 +418,28 @@ function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
|
|
|
345
418
|
.filter(({ account }) => !normalizedTag ||
|
|
346
419
|
(Array.isArray(account?.accountTags) &&
|
|
347
420
|
account.accountTags.some((entry) => String(entry).toLowerCase() === normalizedTag)))
|
|
348
|
-
.map(({ account, index }) =>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
421
|
+
.map(({ account, index }) => {
|
|
422
|
+
const trimmedId =
|
|
423
|
+
typeof account?.accountId === "string" ? account.accountId.trim() : "";
|
|
424
|
+
const accountId = trimmedId || undefined;
|
|
425
|
+
return {
|
|
426
|
+
index,
|
|
427
|
+
label: account?.accountLabel ?? `Account ${index + 1}`,
|
|
428
|
+
email: maskValue(account?.email, includeSensitive),
|
|
429
|
+
accountId: maskValue(accountId, includeSensitive),
|
|
430
|
+
idSuffix: accountIdSuffix(accountId, includeSensitive),
|
|
431
|
+
accountIdSource: account?.accountIdSource,
|
|
432
|
+
enabled: account?.enabled !== false,
|
|
433
|
+
hasRefreshToken: typeof account?.refreshToken === "string" && account.refreshToken.length > 0,
|
|
434
|
+
hasAccessToken: typeof account?.accessToken === "string" && account.accessToken.length > 0,
|
|
435
|
+
expiresAt: account?.expiresAt,
|
|
436
|
+
expired: typeof account?.expiresAt === "number" ? account.expiresAt <= Date.now() : undefined,
|
|
437
|
+
tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
|
|
438
|
+
note: account?.accountNote,
|
|
439
|
+
rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
|
|
440
|
+
quotaExhaustedUntil: account?.quotaExhaustedUntil,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
363
443
|
}
|
|
364
444
|
|
|
365
445
|
function printStandaloneResult(command, payload, json) {
|
|
@@ -373,10 +453,16 @@ function printStandaloneResult(command, payload, json) {
|
|
|
373
453
|
console.log(`Accounts: ${payload.totalAccounts}`);
|
|
374
454
|
if (Array.isArray(payload.accounts)) {
|
|
375
455
|
for (const account of payload.accounts) {
|
|
376
|
-
|
|
456
|
+
const identity = [account.email, account.idSuffix ? `id:${account.idSuffix}` : undefined]
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.join(", ");
|
|
459
|
+
const name = identity ? `${account.label} (${identity})` : account.label;
|
|
460
|
+
console.log(`- [${account.index}] ${name} enabled=${account.enabled} refresh=${account.hasRefreshToken} access=${account.hasAccessToken}`);
|
|
377
461
|
}
|
|
378
462
|
}
|
|
379
463
|
if (payload.error) console.log(`Error: ${payload.error}`);
|
|
464
|
+
for (const fix of payload.appliedFixes ?? []) console.log(`Fixed: ${fix}`);
|
|
465
|
+
for (const error of payload.fixErrors ?? []) console.log(`Repair failed: ${error}`);
|
|
380
466
|
if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
|
|
381
467
|
}
|
|
382
468
|
|
|
@@ -446,9 +532,29 @@ export async function runWarmCommand(parsed, options = {}) {
|
|
|
446
532
|
// persisted to the SAME file the rest of the toolchain reads.
|
|
447
533
|
storageMod.setStoragePathDirect(storagePath);
|
|
448
534
|
|
|
449
|
-
|
|
535
|
+
let storage = null;
|
|
536
|
+
try {
|
|
537
|
+
storage = await storageMod.loadAccounts();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
// Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
|
|
540
|
+
// upgrade hint; surface them rather than crashing the CLI.
|
|
541
|
+
const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
|
|
542
|
+
const payload = { command: "warm", storagePath, error: `${formatErrorForLog(error)}${hint}` };
|
|
543
|
+
printWarmResult(payload, parsed.json);
|
|
544
|
+
return { exitCode: 1, action: "warm", storagePath };
|
|
545
|
+
}
|
|
450
546
|
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
451
547
|
if (accounts.length === 0) {
|
|
548
|
+
// `loadAccounts` swallows parse/IO errors and returns null. Probe the
|
|
549
|
+
// file so a corrupt storage fails like `status`/`doctor` do (exit 1)
|
|
550
|
+
// instead of reporting a healthy empty pool. ENOENT stays a silent
|
|
551
|
+
// empty pool: a missing file legitimately means no accounts yet.
|
|
552
|
+
const probe = await readStandaloneStorage(storagePath);
|
|
553
|
+
if (probe.error) {
|
|
554
|
+
const payload = { command: "warm", storagePath, error: probe.error };
|
|
555
|
+
printWarmResult(payload, parsed.json);
|
|
556
|
+
return { exitCode: 1, action: "warm", storagePath };
|
|
557
|
+
}
|
|
452
558
|
const payload = {
|
|
453
559
|
command: "warm",
|
|
454
560
|
storagePath,
|
|
@@ -552,9 +658,27 @@ export async function runLimitsCommand(parsed, options = {}) {
|
|
|
552
658
|
// persisted to the SAME file the rest of the toolchain reads.
|
|
553
659
|
storageMod.setStoragePathDirect(storagePath);
|
|
554
660
|
|
|
555
|
-
|
|
661
|
+
let storage = null;
|
|
662
|
+
try {
|
|
663
|
+
storage = await storageMod.loadAccounts();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
// Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
|
|
666
|
+
// upgrade hint; surface them rather than crashing the CLI.
|
|
667
|
+
const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
|
|
668
|
+
const payload = { command: "limits", storagePath, error: `${formatErrorForLog(error)}${hint}` };
|
|
669
|
+
printLimitsResult(payload, parsed.json);
|
|
670
|
+
return { exitCode: 1, action: "limits", storagePath };
|
|
671
|
+
}
|
|
556
672
|
const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
|
|
557
673
|
if (accounts.length === 0) {
|
|
674
|
+
// Same probe contract as `warm`: a corrupt file exits 1 like
|
|
675
|
+
// `status`/`doctor`; a missing file stays a silent empty pool.
|
|
676
|
+
const probe = await readStandaloneStorage(storagePath);
|
|
677
|
+
if (probe.error) {
|
|
678
|
+
const payload = { command: "limits", storagePath, error: probe.error };
|
|
679
|
+
printLimitsResult(payload, parsed.json);
|
|
680
|
+
return { exitCode: 1, action: "limits", storagePath };
|
|
681
|
+
}
|
|
558
682
|
const payload = {
|
|
559
683
|
command: "limits",
|
|
560
684
|
storagePath,
|
|
@@ -595,6 +719,7 @@ export async function runLimitsCommand(parsed, options = {}) {
|
|
|
595
719
|
label: account.accountLabel ?? `Account ${index + 1}`,
|
|
596
720
|
email: maskValue(account.email, parsed.includeSensitive),
|
|
597
721
|
rateLimitResetTimes: account.rateLimitResetTimes ?? {},
|
|
722
|
+
quotaExhaustedUntil: account.quotaExhaustedUntil,
|
|
598
723
|
};
|
|
599
724
|
try {
|
|
600
725
|
const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
|
|
@@ -624,6 +749,14 @@ export async function runLimitsCommand(parsed, options = {}) {
|
|
|
624
749
|
}
|
|
625
750
|
entry.planType = usage.planType;
|
|
626
751
|
entry.credits = usage.credits;
|
|
752
|
+
// Raw counts stay in `resetCredits` and the rendered line lives in
|
|
753
|
+
// its own field: embedding the English summary inside the counts
|
|
754
|
+
// object would make `--json` consumers parse presentation text to
|
|
755
|
+
// reach a number that is already beside it.
|
|
756
|
+
entry.resetCredits = usage.resetCredits;
|
|
757
|
+
entry.resetCreditsSummary = usage.resetCredits
|
|
758
|
+
? usageMod.formatResetCredits(usage.resetCredits)
|
|
759
|
+
: null;
|
|
627
760
|
entry.limits = usage.limits;
|
|
628
761
|
} catch (error) {
|
|
629
762
|
// `ensureCodexUsageAccessToken` can surface a raw OAuth refresh
|
|
@@ -675,6 +808,9 @@ function printLimitsResult(payload, json) {
|
|
|
675
808
|
}
|
|
676
809
|
if (account.planType) console.log(` Plan: ${account.planType}`);
|
|
677
810
|
if (account.credits) console.log(` Credits: ${account.credits}`);
|
|
811
|
+
if (account.resetCredits && account.resetCredits.available > 0) {
|
|
812
|
+
console.log(` Resets: ${account.resetCreditsSummary}`);
|
|
813
|
+
}
|
|
678
814
|
}
|
|
679
815
|
if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
|
|
680
816
|
}
|
|
@@ -697,7 +833,68 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
|
697
833
|
}
|
|
698
834
|
const { env = process.env } = options;
|
|
699
835
|
const storagePath = getStandaloneStoragePath(parsed, env);
|
|
700
|
-
const
|
|
836
|
+
const repairRequested = command === "doctor" && parsed.fix;
|
|
837
|
+
let storage = null;
|
|
838
|
+
let error = null;
|
|
839
|
+
if (parsed.configPath || !repairRequested) {
|
|
840
|
+
({ storage, error } = await readStandaloneStorage(storagePath));
|
|
841
|
+
}
|
|
842
|
+
const appliedFixes = [];
|
|
843
|
+
const fixErrors = [];
|
|
844
|
+
if (repairRequested && !error) {
|
|
845
|
+
const previousKeychain = process.env.CODEX_KEYCHAIN;
|
|
846
|
+
try {
|
|
847
|
+
const loadDoctorRuntime = options.loadDoctorRuntime ?? (() => loadDistModules(
|
|
848
|
+
["storage.js", "tools/doctor-repair.js", "shutdown.js"], "doctor",
|
|
849
|
+
));
|
|
850
|
+
const [storageMod, repairMod, shutdownMod] = await loadDoctorRuntime();
|
|
851
|
+
// A CLI file selection must not read or replace the global keychain pool.
|
|
852
|
+
if (parsed.configPath) process.env.CODEX_KEYCHAIN = "0";
|
|
853
|
+
storageMod.setStoragePathDirect(storagePath);
|
|
854
|
+
shutdownMod.setShutdownOwnsProcess(true);
|
|
855
|
+
try {
|
|
856
|
+
storage = await storageMod.loadAccounts();
|
|
857
|
+
} catch (loadError) {
|
|
858
|
+
// Typed storage errors (UNSUPPORTED_SCHEMA_VERSION, unknown V2)
|
|
859
|
+
// carry exact in-tree copy plus an upgrade/recovery hint, and the
|
|
860
|
+
// load never got far enough to attempt a repair. Surface them on
|
|
861
|
+
// the error channel (exit 1) instead of the generic catch below,
|
|
862
|
+
// which is reserved for unknown throws so upstream failure text
|
|
863
|
+
// never reaches output unredacted.
|
|
864
|
+
if (loadError && typeof loadError.code === "string") {
|
|
865
|
+
const hint = typeof loadError.hint === "string" ? ` ${loadError.hint}` : "";
|
|
866
|
+
error = `${formatErrorForLog(loadError)}${hint}`;
|
|
867
|
+
} else {
|
|
868
|
+
throw loadError;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (!storage && !error) {
|
|
872
|
+
// `loadAccounts` swallows JSON parse/IO errors and returns null. In
|
|
873
|
+
// default-path mode the pre-read above was skipped (keychain routing
|
|
874
|
+
// may own the pool), so probe the JSON file here: a corrupt file must
|
|
875
|
+
// surface as a parse error (exit 1) instead of "No accounts
|
|
876
|
+
// configured" (exit 0). ENOENT stays silent - a missing file with an
|
|
877
|
+
// empty keychain legitimately means no accounts yet. Skipped when a
|
|
878
|
+
// typed load error already set `error`; the probe would only
|
|
879
|
+
// overwrite the precise schema message with its own paraphrase.
|
|
880
|
+
const probe = await readStandaloneStorage(storagePath);
|
|
881
|
+
if (probe.error) error = probe.error;
|
|
882
|
+
}
|
|
883
|
+
if (!error) {
|
|
884
|
+
const repair = await repairMod.repairDoctorAccounts(storage?.accounts ?? []);
|
|
885
|
+
appliedFixes.push(...repair.appliedFixes);
|
|
886
|
+
fixErrors.push(...repair.fixErrors);
|
|
887
|
+
storage = (await storageMod.loadAccounts()) ?? storage;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
fixErrors.push("Doctor repair could not complete. Check the selected storage file and installed runtime.");
|
|
891
|
+
} finally {
|
|
892
|
+
if (parsed.configPath) {
|
|
893
|
+
if (previousKeychain === undefined) delete process.env.CODEX_KEYCHAIN;
|
|
894
|
+
else process.env.CODEX_KEYCHAIN = previousKeychain;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
701
898
|
const accounts = summarizeStandaloneAccounts(storage, parsed.includeSensitive, parsed.tag);
|
|
702
899
|
const totalAccounts = Array.isArray(storage?.accounts) ? storage.accounts.length : 0;
|
|
703
900
|
const payload = {
|
|
@@ -716,7 +913,11 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
|
716
913
|
} else if (command === "doctor") {
|
|
717
914
|
payload.message = error ? "Storage could not be parsed." : totalAccounts > 0 ? "Local diagnostics completed." : "No accounts configured.";
|
|
718
915
|
payload.deep = parsed.deep;
|
|
719
|
-
payload.fixApplied = parsed.fix ?
|
|
916
|
+
payload.fixApplied = parsed.fix ? appliedFixes.length > 0 : undefined;
|
|
917
|
+
if (parsed.fix) {
|
|
918
|
+
payload.appliedFixes = appliedFixes;
|
|
919
|
+
payload.fixErrors = fixErrors;
|
|
920
|
+
}
|
|
720
921
|
payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
|
|
721
922
|
} else if (command === "health") {
|
|
722
923
|
payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
|
|
@@ -725,7 +926,7 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
|
|
|
725
926
|
payload.message = totalAccounts > 0 ? "Account storage loaded." : "No accounts configured.";
|
|
726
927
|
}
|
|
727
928
|
printStandaloneResult(command, payload, parsed.json);
|
|
728
|
-
return { exitCode: error ? 1 : 0, action: command, storagePath };
|
|
929
|
+
return { exitCode: error || fixErrors.length > 0 ? 1 : 0, action: command, storagePath };
|
|
729
930
|
}
|
|
730
931
|
|
|
731
932
|
// Top-level keys inside `provider.openai` that the installer owns absolutely.
|