codex-multi-auth 0.1.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/LICENSE +1 -0
- package/README.md +162 -0
- package/assets/opencode-logo-ornate-dark.svg +18 -0
- package/assets/readme-hero.svg +31 -0
- package/config/README.md +87 -0
- package/config/minimal-opencode.json +13 -0
- package/config/opencode-legacy.json +571 -0
- package/config/opencode-modern.json +239 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3160 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
- package/dist/lib/accounts/rate-limits.js +63 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -0
- package/dist/lib/accounts.d.ts +95 -0
- package/dist/lib/accounts.d.ts.map +1 -0
- package/dist/lib/accounts.js +668 -0
- package/dist/lib/accounts.js.map +1 -0
- package/dist/lib/audit.d.ts +45 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +131 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/auth/auth.d.ts +56 -0
- package/dist/lib/auth/auth.d.ts.map +1 -0
- package/dist/lib/auth/auth.js +214 -0
- package/dist/lib/auth/auth.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +34 -0
- package/dist/lib/auth/browser.d.ts.map +1 -0
- package/dist/lib/auth/browser.js +185 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/server.d.ts +24 -0
- package/dist/lib/auth/server.d.ts.map +1 -0
- package/dist/lib/auth/server.js +116 -0
- package/dist/lib/auth/server.js.map +1 -0
- package/dist/lib/auth/token-utils.d.ts +59 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -0
- package/dist/lib/auth/token-utils.js +331 -0
- package/dist/lib/auth/token-utils.js.map +1 -0
- package/dist/lib/auth-rate-limit.d.ts +20 -0
- package/dist/lib/auth-rate-limit.d.ts.map +1 -0
- package/dist/lib/auth-rate-limit.js +91 -0
- package/dist/lib/auth-rate-limit.js.map +1 -0
- package/dist/lib/auto-update-checker.d.ts +10 -0
- package/dist/lib/auto-update-checker.d.ts.map +1 -0
- package/dist/lib/auto-update-checker.js +216 -0
- package/dist/lib/auto-update-checker.js.map +1 -0
- package/dist/lib/capability-policy.d.ts +18 -0
- package/dist/lib/capability-policy.d.ts.map +1 -0
- package/dist/lib/capability-policy.js +150 -0
- package/dist/lib/capability-policy.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts +34 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -0
- package/dist/lib/circuit-breaker.js +124 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/cli.d.ts +64 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +274 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/codex-cli/observability.d.ts +22 -0
- package/dist/lib/codex-cli/observability.d.ts.map +1 -0
- package/dist/lib/codex-cli/observability.js +36 -0
- package/dist/lib/codex-cli/observability.js.map +1 -0
- package/dist/lib/codex-cli/state.d.ts +86 -0
- package/dist/lib/codex-cli/state.d.ts.map +1 -0
- package/dist/lib/codex-cli/state.js +470 -0
- package/dist/lib/codex-cli/state.js.map +1 -0
- package/dist/lib/codex-cli/sync.d.ts +27 -0
- package/dist/lib/codex-cli/sync.d.ts.map +1 -0
- package/dist/lib/codex-cli/sync.js +325 -0
- package/dist/lib/codex-cli/sync.js.map +1 -0
- package/dist/lib/codex-cli/writer.d.ts +12 -0
- package/dist/lib/codex-cli/writer.d.ts.map +1 -0
- package/dist/lib/codex-cli/writer.js +388 -0
- package/dist/lib/codex-cli/writer.js.map +1 -0
- package/dist/lib/codex-manager.d.ts +2 -0
- package/dist/lib/codex-manager.d.ts.map +1 -0
- package/dist/lib/codex-manager.js +4841 -0
- package/dist/lib/codex-manager.js.map +1 -0
- package/dist/lib/config.d.ts +269 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +789 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +78 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +78 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/context-overflow.d.ts +27 -0
- package/dist/lib/context-overflow.d.ts.map +1 -0
- package/dist/lib/context-overflow.js +124 -0
- package/dist/lib/context-overflow.js.map +1 -0
- package/dist/lib/dashboard-settings.d.ts +90 -0
- package/dist/lib/dashboard-settings.d.ts.map +1 -0
- package/dist/lib/dashboard-settings.js +327 -0
- package/dist/lib/dashboard-settings.js.map +1 -0
- package/dist/lib/entitlement-cache.d.ts +41 -0
- package/dist/lib/entitlement-cache.d.ts.map +1 -0
- package/dist/lib/entitlement-cache.js +137 -0
- package/dist/lib/entitlement-cache.js.map +1 -0
- package/dist/lib/errors.d.ts +113 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +103 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/forecast.d.ts +42 -0
- package/dist/lib/forecast.d.ts.map +1 -0
- package/dist/lib/forecast.js +256 -0
- package/dist/lib/forecast.js.map +1 -0
- package/dist/lib/health.d.ts +33 -0
- package/dist/lib/health.d.ts.map +1 -0
- package/dist/lib/health.js +70 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/index.d.ts +32 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +32 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/live-account-sync.d.ts +39 -0
- package/dist/lib/live-account-sync.d.ts.map +1 -0
- package/dist/lib/live-account-sync.js +196 -0
- package/dist/lib/live-account-sync.js.map +1 -0
- package/dist/lib/logger.d.ts +40 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +364 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/oauth-success.html +338 -0
- package/dist/lib/parallel-probe.d.ts +28 -0
- package/dist/lib/parallel-probe.d.ts.map +1 -0
- package/dist/lib/parallel-probe.js +97 -0
- package/dist/lib/parallel-probe.js.map +1 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts +53 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -0
- package/dist/lib/preemptive-quota-scheduler.js +220 -0
- package/dist/lib/preemptive-quota-scheduler.js.map +1 -0
- package/dist/lib/proactive-refresh.d.ts +66 -0
- package/dist/lib/proactive-refresh.d.ts.map +1 -0
- package/dist/lib/proactive-refresh.js +143 -0
- package/dist/lib/proactive-refresh.js.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +19 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.js +169 -0
- package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +41 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -0
- package/dist/lib/prompts/codex.js +383 -0
- package/dist/lib/prompts/codex.js.map +1 -0
- package/dist/lib/prompts/opencode-codex.d.ts +25 -0
- package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
- package/dist/lib/prompts/opencode-codex.js +270 -0
- package/dist/lib/prompts/opencode-codex.js.map +1 -0
- package/dist/lib/quota-cache.d.ts +68 -0
- package/dist/lib/quota-cache.d.ts.map +1 -0
- package/dist/lib/quota-cache.js +224 -0
- package/dist/lib/quota-cache.js.map +1 -0
- package/dist/lib/quota-probe.d.ts +49 -0
- package/dist/lib/quota-probe.d.ts.map +1 -0
- package/dist/lib/quota-probe.js +368 -0
- package/dist/lib/quota-probe.js.map +1 -0
- package/dist/lib/recovery/constants.d.ts +12 -0
- package/dist/lib/recovery/constants.d.ts.map +1 -0
- package/dist/lib/recovery/constants.js +31 -0
- package/dist/lib/recovery/constants.js.map +1 -0
- package/dist/lib/recovery/index.d.ts +12 -0
- package/dist/lib/recovery/index.d.ts.map +1 -0
- package/dist/lib/recovery/index.js +12 -0
- package/dist/lib/recovery/index.js.map +1 -0
- package/dist/lib/recovery/storage.d.ts +24 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -0
- package/dist/lib/recovery/storage.js +362 -0
- package/dist/lib/recovery/storage.js.map +1 -0
- package/dist/lib/recovery/types.d.ts +116 -0
- package/dist/lib/recovery/types.d.ts.map +1 -0
- package/dist/lib/recovery/types.js +7 -0
- package/dist/lib/recovery/types.js.map +1 -0
- package/dist/lib/recovery.d.ts +31 -0
- package/dist/lib/recovery.d.ts.map +1 -0
- package/dist/lib/recovery.js +313 -0
- package/dist/lib/recovery.js.map +1 -0
- package/dist/lib/refresh-guardian.d.ts +31 -0
- package/dist/lib/refresh-guardian.d.ts.map +1 -0
- package/dist/lib/refresh-guardian.js +151 -0
- package/dist/lib/refresh-guardian.js.map +1 -0
- package/dist/lib/refresh-lease.d.ts +37 -0
- package/dist/lib/refresh-lease.d.ts.map +1 -0
- package/dist/lib/refresh-lease.js +335 -0
- package/dist/lib/refresh-lease.js.map +1 -0
- package/dist/lib/refresh-queue.d.ts +117 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +297 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- package/dist/lib/request/failure-policy.d.ts +42 -0
- package/dist/lib/request/failure-policy.d.ts.map +1 -0
- package/dist/lib/request/failure-policy.js +133 -0
- package/dist/lib/request/failure-policy.js.map +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +152 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
- package/dist/lib/request/fetch-helpers.js +704 -0
- package/dist/lib/request/fetch-helpers.js.map +1 -0
- package/dist/lib/request/helpers/input-utils.d.ts +7 -0
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/input-utils.js +214 -0
- package/dist/lib/request/helpers/input-utils.js.map +1 -0
- package/dist/lib/request/helpers/model-map.d.ts +28 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
- package/dist/lib/request/helpers/model-map.js +133 -0
- package/dist/lib/request/helpers/model-map.js.map +1 -0
- package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
- package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/tool-utils.js +117 -0
- package/dist/lib/request/helpers/tool-utils.js.map +1 -0
- package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
- package/dist/lib/request/rate-limit-backoff.js +83 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -0
- package/dist/lib/request/request-transformer.d.ts +107 -0
- package/dist/lib/request/request-transformer.d.ts.map +1 -0
- package/dist/lib/request/request-transformer.js +814 -0
- package/dist/lib/request/request-transformer.js.map +1 -0
- package/dist/lib/request/response-handler.d.ts +23 -0
- package/dist/lib/request/response-handler.d.ts.map +1 -0
- package/dist/lib/request/response-handler.js +155 -0
- package/dist/lib/request/response-handler.js.map +1 -0
- package/dist/lib/request/stream-failover.d.ts +21 -0
- package/dist/lib/request/stream-failover.d.ts.map +1 -0
- package/dist/lib/request/stream-failover.js +204 -0
- package/dist/lib/request/stream-failover.js.map +1 -0
- package/dist/lib/rotation.d.ts +146 -0
- package/dist/lib/rotation.d.ts.map +1 -0
- package/dist/lib/rotation.js +321 -0
- package/dist/lib/rotation.js.map +1 -0
- package/dist/lib/runtime-paths.d.ts +58 -0
- package/dist/lib/runtime-paths.d.ts.map +1 -0
- package/dist/lib/runtime-paths.js +164 -0
- package/dist/lib/runtime-paths.js.map +1 -0
- package/dist/lib/schemas.d.ts +435 -0
- package/dist/lib/schemas.d.ts.map +1 -0
- package/dist/lib/schemas.js +268 -0
- package/dist/lib/schemas.js.map +1 -0
- package/dist/lib/session-affinity.d.ts +23 -0
- package/dist/lib/session-affinity.d.ts.map +1 -0
- package/dist/lib/session-affinity.js +127 -0
- package/dist/lib/session-affinity.js.map +1 -0
- package/dist/lib/shutdown.d.ts +7 -0
- package/dist/lib/shutdown.d.ts.map +1 -0
- package/dist/lib/shutdown.js +43 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/storage/migrations.d.ts +59 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -0
- package/dist/lib/storage/migrations.js +41 -0
- package/dist/lib/storage/migrations.js.map +1 -0
- package/dist/lib/storage/paths.d.ts +51 -0
- package/dist/lib/storage/paths.d.ts.map +1 -0
- package/dist/lib/storage/paths.js +152 -0
- package/dist/lib/storage/paths.js.map +1 -0
- package/dist/lib/storage.d.ts +106 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +896 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/table-formatter.d.ts +32 -0
- package/dist/lib/table-formatter.d.ts.map +1 -0
- package/dist/lib/table-formatter.js +44 -0
- package/dist/lib/table-formatter.js.map +1 -0
- package/dist/lib/tools/hashline-tools.d.ts +51 -0
- package/dist/lib/tools/hashline-tools.d.ts.map +1 -0
- package/dist/lib/tools/hashline-tools.js +456 -0
- package/dist/lib/tools/hashline-tools.js.map +1 -0
- package/dist/lib/types.d.ts +130 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/ui/ansi.d.ts +40 -0
- package/dist/lib/ui/ansi.d.ts.map +1 -0
- package/dist/lib/ui/ansi.js +68 -0
- package/dist/lib/ui/ansi.js.map +1 -0
- package/dist/lib/ui/auth-menu.d.ts +76 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -0
- package/dist/lib/ui/auth-menu.js +590 -0
- package/dist/lib/ui/auth-menu.js.map +1 -0
- package/dist/lib/ui/confirm.d.ts +11 -0
- package/dist/lib/ui/confirm.d.ts.map +1 -0
- package/dist/lib/ui/confirm.js +29 -0
- package/dist/lib/ui/confirm.js.map +1 -0
- package/dist/lib/ui/copy.d.ts +123 -0
- package/dist/lib/ui/copy.d.ts.map +1 -0
- package/dist/lib/ui/copy.js +127 -0
- package/dist/lib/ui/copy.js.map +1 -0
- package/dist/lib/ui/format.d.ts +62 -0
- package/dist/lib/ui/format.d.ts.map +1 -0
- package/dist/lib/ui/format.js +205 -0
- package/dist/lib/ui/format.js.map +1 -0
- package/dist/lib/ui/runtime.d.ts +43 -0
- package/dist/lib/ui/runtime.d.ts.map +1 -0
- package/dist/lib/ui/runtime.js +69 -0
- package/dist/lib/ui/runtime.js.map +1 -0
- package/dist/lib/ui/select.d.ts +60 -0
- package/dist/lib/ui/select.d.ts.map +1 -0
- package/dist/lib/ui/select.js +467 -0
- package/dist/lib/ui/select.js.map +1 -0
- package/dist/lib/ui/theme.d.ts +56 -0
- package/dist/lib/ui/theme.d.ts.map +1 -0
- package/dist/lib/ui/theme.js +186 -0
- package/dist/lib/ui/theme.js.map +1 -0
- package/dist/lib/unified-settings.d.ts +71 -0
- package/dist/lib/unified-settings.d.ts.map +1 -0
- package/dist/lib/unified-settings.js +299 -0
- package/dist/lib/unified-settings.js.map +1 -0
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +54 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +115 -0
- package/scripts/audit-dev-allowlist.js +128 -0
- package/scripts/bench-format/hashline-v2.mjs +642 -0
- package/scripts/bench-format/models.mjs +105 -0
- package/scripts/bench-format/opencode.mjs +205 -0
- package/scripts/bench-format/render.mjs +496 -0
- package/scripts/bench-format/stats.mjs +54 -0
- package/scripts/bench-format/tasks.mjs +151 -0
- package/scripts/benchmark-edit-formats.mjs +1161 -0
- package/scripts/benchmark-render-dashboard.mjs +49 -0
- package/scripts/codex-multi-auth.js +6 -0
- package/scripts/codex-routing.js +34 -0
- package/scripts/codex.js +122 -0
- package/scripts/copy-oauth-success.js +37 -0
- package/scripts/install-opencode-codex-auth.js +193 -0
- package/scripts/test-all-models.sh +7 -0
- package/scripts/test-model-matrix.js +424 -0
- package/scripts/validate-model-map.sh +7 -0
|
@@ -0,0 +1,4841 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { promises as fs, existsSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, REDIRECT_URI, } from "./auth/auth.js";
|
|
6
|
+
import { startLocalOAuthServer } from "./auth/server.js";
|
|
7
|
+
import { copyTextToClipboard, openBrowserUrl } from "./auth/browser.js";
|
|
8
|
+
import { promptAddAnotherAccount, promptLoginMode } from "./cli.js";
|
|
9
|
+
import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, } from "./accounts.js";
|
|
10
|
+
import { ACCOUNT_LIMITS } from "./constants.js";
|
|
11
|
+
import { loadDashboardDisplaySettings, saveDashboardDisplaySettings, getDashboardSettingsPath, DEFAULT_DASHBOARD_DISPLAY_SETTINGS, } from "./dashboard-settings.js";
|
|
12
|
+
import { getDefaultPluginConfig, loadPluginConfig, savePluginConfig, } from "./config.js";
|
|
13
|
+
import { evaluateForecastAccounts, isHardRefreshFailure, recommendForecastAccount, summarizeForecast, } from "./forecast.js";
|
|
14
|
+
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
15
|
+
import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./quota-probe.js";
|
|
16
|
+
import { queuedRefresh } from "./refresh-queue.js";
|
|
17
|
+
import { loadQuotaCache, saveQuotaCache, } from "./quota-cache.js";
|
|
18
|
+
import { getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, } from "./storage.js";
|
|
19
|
+
import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
|
|
20
|
+
import { ANSI } from "./ui/ansi.js";
|
|
21
|
+
import { UI_COPY } from "./ui/copy.js";
|
|
22
|
+
import { paintUiText, quotaToneFromLeftPercent } from "./ui/format.js";
|
|
23
|
+
import { getUiRuntimeOptions, setUiRuntimeOptions } from "./ui/runtime.js";
|
|
24
|
+
import { select } from "./ui/select.js";
|
|
25
|
+
function stylePromptText(text, tone) {
|
|
26
|
+
if (!output.isTTY)
|
|
27
|
+
return text;
|
|
28
|
+
const ui = getUiRuntimeOptions();
|
|
29
|
+
if (ui.v2Enabled) {
|
|
30
|
+
if (tone === "muted") {
|
|
31
|
+
return `${ui.theme.colors.dim}${paintUiText(ui, text, "muted")}${ui.theme.colors.reset}`;
|
|
32
|
+
}
|
|
33
|
+
const mapped = tone === "accent" ? "primary" : tone;
|
|
34
|
+
return paintUiText(ui, text, mapped);
|
|
35
|
+
}
|
|
36
|
+
const legacyCode = tone === "accent"
|
|
37
|
+
? ANSI.green
|
|
38
|
+
: tone === "success"
|
|
39
|
+
? ANSI.green
|
|
40
|
+
: tone === "warning"
|
|
41
|
+
? ANSI.yellow
|
|
42
|
+
: tone === "danger"
|
|
43
|
+
? ANSI.red
|
|
44
|
+
: ANSI.dim;
|
|
45
|
+
return `${legacyCode}${text}${ANSI.reset}`;
|
|
46
|
+
}
|
|
47
|
+
function collapseWhitespace(value) {
|
|
48
|
+
return value.replace(/\s+/g, " ").trim();
|
|
49
|
+
}
|
|
50
|
+
function formatReasonLabel(reason) {
|
|
51
|
+
if (!reason)
|
|
52
|
+
return undefined;
|
|
53
|
+
const normalized = collapseWhitespace(reason.replace(/_/g, " "));
|
|
54
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
55
|
+
}
|
|
56
|
+
function extractErrorMessageFromPayload(payload) {
|
|
57
|
+
if (!payload || typeof payload !== "object")
|
|
58
|
+
return undefined;
|
|
59
|
+
const record = payload;
|
|
60
|
+
const directMessage = typeof record.message === "string"
|
|
61
|
+
? collapseWhitespace(record.message)
|
|
62
|
+
: "";
|
|
63
|
+
const directCode = typeof record.code === "string"
|
|
64
|
+
? collapseWhitespace(record.code)
|
|
65
|
+
: "";
|
|
66
|
+
if (directMessage) {
|
|
67
|
+
if (directCode && !directMessage.toLowerCase().includes(directCode.toLowerCase())) {
|
|
68
|
+
return `${directMessage} [${directCode}]`;
|
|
69
|
+
}
|
|
70
|
+
return directMessage;
|
|
71
|
+
}
|
|
72
|
+
const nested = record.error;
|
|
73
|
+
if (nested && typeof nested === "object") {
|
|
74
|
+
return extractErrorMessageFromPayload(nested);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function parseStructuredErrorMessage(raw) {
|
|
79
|
+
const trimmed = raw.trim();
|
|
80
|
+
if (!trimmed)
|
|
81
|
+
return undefined;
|
|
82
|
+
const candidates = new Set([trimmed]);
|
|
83
|
+
const firstBrace = trimmed.indexOf("{");
|
|
84
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
85
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
86
|
+
candidates.add(trimmed.slice(firstBrace, lastBrace + 1));
|
|
87
|
+
}
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(candidate);
|
|
91
|
+
const message = extractErrorMessageFromPayload(parsed);
|
|
92
|
+
if (message)
|
|
93
|
+
return message;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore non-JSON candidates
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function normalizeFailureDetail(message, reason) {
|
|
102
|
+
const reasonLabel = formatReasonLabel(reason);
|
|
103
|
+
const raw = message?.trim() || reasonLabel || "refresh failed";
|
|
104
|
+
const structured = parseStructuredErrorMessage(raw);
|
|
105
|
+
const normalized = collapseWhitespace(structured ?? raw);
|
|
106
|
+
const bounded = normalized.length > 260 ? `${normalized.slice(0, 257)}...` : normalized;
|
|
107
|
+
return bounded.length > 0 ? bounded : "refresh failed";
|
|
108
|
+
}
|
|
109
|
+
function joinStyledSegments(parts) {
|
|
110
|
+
if (parts.length === 0)
|
|
111
|
+
return "";
|
|
112
|
+
const separator = stylePromptText(" | ", "muted");
|
|
113
|
+
return parts.join(separator);
|
|
114
|
+
}
|
|
115
|
+
function formatResultSummary(segments) {
|
|
116
|
+
const rendered = segments.map((segment) => stylePromptText(segment.text, segment.tone));
|
|
117
|
+
return `${stylePromptText("Result:", "accent")} ${joinStyledSegments(rendered)}`;
|
|
118
|
+
}
|
|
119
|
+
function styleQuotaSummary(summary) {
|
|
120
|
+
const normalized = collapseWhitespace(summary);
|
|
121
|
+
if (!normalized)
|
|
122
|
+
return stylePromptText(summary, "muted");
|
|
123
|
+
const segments = normalized.split("|").map((segment) => segment.trim()).filter(Boolean);
|
|
124
|
+
if (segments.length === 0)
|
|
125
|
+
return stylePromptText(normalized, "muted");
|
|
126
|
+
const rendered = segments.map((segment) => {
|
|
127
|
+
if (/rate-limited/i.test(segment)) {
|
|
128
|
+
return stylePromptText(segment, "danger");
|
|
129
|
+
}
|
|
130
|
+
const match = segment.match(/^([0-9a-zA-Z]+)\s+(\d{1,3})%$/);
|
|
131
|
+
if (!match) {
|
|
132
|
+
return stylePromptText(segment, "muted");
|
|
133
|
+
}
|
|
134
|
+
const windowLabel = match[1] ?? "";
|
|
135
|
+
const leftPercent = Number.parseInt(match[2] ?? "", 10);
|
|
136
|
+
if (!Number.isFinite(leftPercent)) {
|
|
137
|
+
return stylePromptText(segment, "muted");
|
|
138
|
+
}
|
|
139
|
+
const tone = quotaToneFromLeftPercent(leftPercent);
|
|
140
|
+
return `${stylePromptText(windowLabel, "muted")} ${stylePromptText(`${leftPercent}%`, tone)}`;
|
|
141
|
+
});
|
|
142
|
+
return joinStyledSegments(rendered);
|
|
143
|
+
}
|
|
144
|
+
function styleAccountDetailText(detail, fallbackTone = "muted") {
|
|
145
|
+
const compact = collapseWhitespace(detail);
|
|
146
|
+
if (!compact)
|
|
147
|
+
return stylePromptText("", fallbackTone);
|
|
148
|
+
const quotaMatch = compact.match(/^(.*?)\(([^()]*\d{1,3}%[^()]*)\)(.*)$/);
|
|
149
|
+
if (quotaMatch) {
|
|
150
|
+
const prefix = (quotaMatch[1] ?? "").trim();
|
|
151
|
+
const quota = (quotaMatch[2] ?? "").trim();
|
|
152
|
+
const suffix = (quotaMatch[3] ?? "").trim();
|
|
153
|
+
const prefixTone = /failed|error/i.test(prefix)
|
|
154
|
+
? "danger"
|
|
155
|
+
: /ok|working|succeeded|valid/i.test(prefix)
|
|
156
|
+
? "success"
|
|
157
|
+
: fallbackTone;
|
|
158
|
+
const suffixTone = /re-login|stale|warning|retry|fallback/i.test(suffix)
|
|
159
|
+
? "warning"
|
|
160
|
+
: /failed|error/i.test(suffix)
|
|
161
|
+
? "danger"
|
|
162
|
+
: "muted";
|
|
163
|
+
const chunks = [];
|
|
164
|
+
if (prefix)
|
|
165
|
+
chunks.push(stylePromptText(prefix, prefixTone));
|
|
166
|
+
chunks.push(`(${styleQuotaSummary(quota)})`);
|
|
167
|
+
if (suffix)
|
|
168
|
+
chunks.push(stylePromptText(suffix, suffixTone));
|
|
169
|
+
return chunks.join(" ");
|
|
170
|
+
}
|
|
171
|
+
if (/rate-limited/i.test(compact))
|
|
172
|
+
return stylePromptText(compact, "danger");
|
|
173
|
+
if (/re-login|stale|warning|fallback/i.test(compact))
|
|
174
|
+
return stylePromptText(compact, "warning");
|
|
175
|
+
if (/failed|error/i.test(compact))
|
|
176
|
+
return stylePromptText(compact, "danger");
|
|
177
|
+
if (/ok|working|succeeded|valid/i.test(compact))
|
|
178
|
+
return stylePromptText(compact, "success");
|
|
179
|
+
return stylePromptText(compact, fallbackTone);
|
|
180
|
+
}
|
|
181
|
+
function riskTone(level) {
|
|
182
|
+
if (level === "low")
|
|
183
|
+
return "success";
|
|
184
|
+
if (level === "medium")
|
|
185
|
+
return "warning";
|
|
186
|
+
return "danger";
|
|
187
|
+
}
|
|
188
|
+
function availabilityTone(availability) {
|
|
189
|
+
if (availability === "ready")
|
|
190
|
+
return "success";
|
|
191
|
+
if (availability === "delayed")
|
|
192
|
+
return "warning";
|
|
193
|
+
return "danger";
|
|
194
|
+
}
|
|
195
|
+
const DASHBOARD_DISPLAY_OPTIONS = [
|
|
196
|
+
{
|
|
197
|
+
key: "menuShowStatusBadge",
|
|
198
|
+
label: "Show Status Badges",
|
|
199
|
+
description: "Show [ok], [active], and similar badges.",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "menuShowCurrentBadge",
|
|
203
|
+
label: "Show [current]",
|
|
204
|
+
description: "Mark the account active in Codex.",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: "menuShowLastUsed",
|
|
208
|
+
label: "Show Last Used",
|
|
209
|
+
description: "Show relative usage like 'today'.",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: "menuShowQuotaSummary",
|
|
213
|
+
label: "Show Limits (5h / 7d)",
|
|
214
|
+
description: "Show limit bars in each row.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: "menuShowQuotaCooldown",
|
|
218
|
+
label: "Show Limit Cooldowns",
|
|
219
|
+
description: "Show reset timers next to 5h/7d bars.",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "menuShowFetchStatus",
|
|
223
|
+
label: "Show Fetch Status",
|
|
224
|
+
description: "Show background limit refresh status in the menu subtitle.",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: "menuHighlightCurrentRow",
|
|
228
|
+
label: "Highlight Current Row",
|
|
229
|
+
description: "Use stronger color on the current row.",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: "menuSortEnabled",
|
|
233
|
+
label: "Enable Smart Sort",
|
|
234
|
+
description: "Sort accounts by readiness (view only).",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: "menuSortPinCurrent",
|
|
238
|
+
label: "Pin [current] when tied",
|
|
239
|
+
description: "Keep current at top only when it is equally ready.",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: "menuSortQuickSwitchVisibleRow",
|
|
243
|
+
label: "Quick Switch Uses Visible Rows",
|
|
244
|
+
description: "Number keys (1-9) follow what you see in the list.",
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
const DEFAULT_STATUSLINE_FIELDS = ["last-used", "limits", "status"];
|
|
248
|
+
const AUTO_RETURN_OPTIONS_MS = [1_000, 2_000, 4_000];
|
|
249
|
+
const MENU_QUOTA_TTL_OPTIONS_MS = [60_000, 5 * 60_000, 10 * 60_000];
|
|
250
|
+
const THEME_PRESET_OPTIONS = ["green", "blue"];
|
|
251
|
+
const ACCENT_COLOR_OPTIONS = ["green", "cyan", "blue", "yellow"];
|
|
252
|
+
const PREVIEW_ACCOUNT_EMAIL = "demo@example.com";
|
|
253
|
+
const PREVIEW_LAST_USED = "today";
|
|
254
|
+
const PREVIEW_STATUS = "active";
|
|
255
|
+
const PREVIEW_LIMITS = "5h ██████▒▒▒▒ 62% | 7d █████▒▒▒▒▒ 49%";
|
|
256
|
+
const PREVIEW_LIMIT_COOLDOWNS = "5h reset 1h 20m | 7d reset 2d 04h";
|
|
257
|
+
const BACKEND_TOGGLE_OPTIONS = [
|
|
258
|
+
{
|
|
259
|
+
key: "liveAccountSync",
|
|
260
|
+
label: "Enable Live Sync",
|
|
261
|
+
description: "Keep accounts synced when files change in another window.",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: "sessionAffinity",
|
|
265
|
+
label: "Enable Session Affinity",
|
|
266
|
+
description: "Try to keep each conversation on the same account.",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: "proactiveRefreshGuardian",
|
|
270
|
+
label: "Enable Token Refresh Guard",
|
|
271
|
+
description: "Refresh tokens early in the background.",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: "retryAllAccountsRateLimited",
|
|
275
|
+
label: "Retry When All Rate-Limited",
|
|
276
|
+
description: "If all accounts are limited, wait and try again.",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
key: "parallelProbing",
|
|
280
|
+
label: "Enable Parallel Probing",
|
|
281
|
+
description: "Check multiple accounts at the same time.",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
key: "storageBackupEnabled",
|
|
285
|
+
label: "Enable Storage Backups",
|
|
286
|
+
description: "Create a backup before account data changes.",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: "preemptiveQuotaEnabled",
|
|
290
|
+
label: "Enable Quota Deferral",
|
|
291
|
+
description: "Delay requests before limits are fully exhausted.",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: "fastSession",
|
|
295
|
+
label: "Enable Fast Session Mode",
|
|
296
|
+
description: "Use lighter request handling for faster responses.",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
key: "sessionRecovery",
|
|
300
|
+
label: "Enable Session Recovery",
|
|
301
|
+
description: "Restore recoverable sessions after restart.",
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
key: "autoResume",
|
|
305
|
+
label: "Enable Auto Resume",
|
|
306
|
+
description: "Automatically continue sessions when possible.",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: "perProjectAccounts",
|
|
310
|
+
label: "Enable Per-Project Accounts",
|
|
311
|
+
description: "Keep separate account lists for each project.",
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
const BACKEND_NUMBER_OPTIONS = [
|
|
315
|
+
{
|
|
316
|
+
key: "liveAccountSyncDebounceMs",
|
|
317
|
+
label: "Live Sync Debounce",
|
|
318
|
+
description: "Wait this long before applying sync file changes.",
|
|
319
|
+
min: 50,
|
|
320
|
+
max: 10_000,
|
|
321
|
+
step: 50,
|
|
322
|
+
unit: "ms",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
key: "liveAccountSyncPollMs",
|
|
326
|
+
label: "Live Sync Poll",
|
|
327
|
+
description: "How often to check files for account updates.",
|
|
328
|
+
min: 500,
|
|
329
|
+
max: 60_000,
|
|
330
|
+
step: 500,
|
|
331
|
+
unit: "ms",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
key: "sessionAffinityTtlMs",
|
|
335
|
+
label: "Session Affinity TTL",
|
|
336
|
+
description: "How long conversation-to-account mapping is kept.",
|
|
337
|
+
min: 1_000,
|
|
338
|
+
max: 24 * 60 * 60_000,
|
|
339
|
+
step: 60_000,
|
|
340
|
+
unit: "ms",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
key: "sessionAffinityMaxEntries",
|
|
344
|
+
label: "Session Affinity Max Entries",
|
|
345
|
+
description: "Maximum stored conversation mappings.",
|
|
346
|
+
min: 8,
|
|
347
|
+
max: 4_096,
|
|
348
|
+
step: 32,
|
|
349
|
+
unit: "count",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
key: "proactiveRefreshIntervalMs",
|
|
353
|
+
label: "Refresh Guard Interval",
|
|
354
|
+
description: "How often to scan for tokens near expiry.",
|
|
355
|
+
min: 5_000,
|
|
356
|
+
max: 10 * 60_000,
|
|
357
|
+
step: 5_000,
|
|
358
|
+
unit: "ms",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
key: "proactiveRefreshBufferMs",
|
|
362
|
+
label: "Refresh Guard Buffer",
|
|
363
|
+
description: "How early to refresh before expiry.",
|
|
364
|
+
min: 30_000,
|
|
365
|
+
max: 10 * 60_000,
|
|
366
|
+
step: 30_000,
|
|
367
|
+
unit: "ms",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
key: "parallelProbingMaxConcurrency",
|
|
371
|
+
label: "Parallel Probe Concurrency",
|
|
372
|
+
description: "Maximum checks running at once.",
|
|
373
|
+
min: 1,
|
|
374
|
+
max: 5,
|
|
375
|
+
step: 1,
|
|
376
|
+
unit: "count",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
key: "fastSessionMaxInputItems",
|
|
380
|
+
label: "Fast Session Max Inputs",
|
|
381
|
+
description: "Max number of input items kept in fast mode.",
|
|
382
|
+
min: 8,
|
|
383
|
+
max: 200,
|
|
384
|
+
step: 2,
|
|
385
|
+
unit: "count",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
key: "networkErrorCooldownMs",
|
|
389
|
+
label: "Network Error Cooldown",
|
|
390
|
+
description: "Wait time after network errors before retry.",
|
|
391
|
+
min: 0,
|
|
392
|
+
max: 120_000,
|
|
393
|
+
step: 500,
|
|
394
|
+
unit: "ms",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
key: "serverErrorCooldownMs",
|
|
398
|
+
label: "Server Error Cooldown",
|
|
399
|
+
description: "Wait time after server errors before retry.",
|
|
400
|
+
min: 0,
|
|
401
|
+
max: 120_000,
|
|
402
|
+
step: 500,
|
|
403
|
+
unit: "ms",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
key: "fetchTimeoutMs",
|
|
407
|
+
label: "Request Timeout",
|
|
408
|
+
description: "Max time to wait for a request.",
|
|
409
|
+
min: 1_000,
|
|
410
|
+
max: 10 * 60_000,
|
|
411
|
+
step: 5_000,
|
|
412
|
+
unit: "ms",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
key: "streamStallTimeoutMs",
|
|
416
|
+
label: "Stream Stall Timeout",
|
|
417
|
+
description: "Max wait before a stuck stream is retried.",
|
|
418
|
+
min: 1_000,
|
|
419
|
+
max: 10 * 60_000,
|
|
420
|
+
step: 5_000,
|
|
421
|
+
unit: "ms",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
key: "tokenRefreshSkewMs",
|
|
425
|
+
label: "Token Refresh Buffer",
|
|
426
|
+
description: "Refresh this long before token expiry.",
|
|
427
|
+
min: 0,
|
|
428
|
+
max: 10 * 60_000,
|
|
429
|
+
step: 10_000,
|
|
430
|
+
unit: "ms",
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
key: "preemptiveQuotaRemainingPercent5h",
|
|
434
|
+
label: "5h Remaining Threshold",
|
|
435
|
+
description: "Start delaying when 5h remaining reaches this percent.",
|
|
436
|
+
min: 0,
|
|
437
|
+
max: 100,
|
|
438
|
+
step: 1,
|
|
439
|
+
unit: "percent",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
key: "preemptiveQuotaRemainingPercent7d",
|
|
443
|
+
label: "7d Remaining Threshold",
|
|
444
|
+
description: "Start delaying when weekly remaining reaches this percent.",
|
|
445
|
+
min: 0,
|
|
446
|
+
max: 100,
|
|
447
|
+
step: 1,
|
|
448
|
+
unit: "percent",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
key: "preemptiveQuotaMaxDeferralMs",
|
|
452
|
+
label: "Max Preemptive Deferral",
|
|
453
|
+
description: "Maximum time allowed for quota-based delay.",
|
|
454
|
+
min: 1_000,
|
|
455
|
+
max: 24 * 60 * 60_000,
|
|
456
|
+
step: 60_000,
|
|
457
|
+
unit: "ms",
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
const BACKEND_DEFAULTS = getDefaultPluginConfig();
|
|
461
|
+
const BACKEND_TOGGLE_OPTION_BY_KEY = new Map(BACKEND_TOGGLE_OPTIONS.map((option) => [option.key, option]));
|
|
462
|
+
const BACKEND_NUMBER_OPTION_BY_KEY = new Map(BACKEND_NUMBER_OPTIONS.map((option) => [option.key, option]));
|
|
463
|
+
const BACKEND_CATEGORY_OPTIONS = [
|
|
464
|
+
{
|
|
465
|
+
key: "session-sync",
|
|
466
|
+
label: "Session & Sync",
|
|
467
|
+
description: "Sync and session behavior.",
|
|
468
|
+
toggleKeys: [
|
|
469
|
+
"liveAccountSync",
|
|
470
|
+
"sessionAffinity",
|
|
471
|
+
"perProjectAccounts",
|
|
472
|
+
"sessionRecovery",
|
|
473
|
+
"autoResume",
|
|
474
|
+
],
|
|
475
|
+
numberKeys: [
|
|
476
|
+
"liveAccountSyncDebounceMs",
|
|
477
|
+
"liveAccountSyncPollMs",
|
|
478
|
+
"sessionAffinityTtlMs",
|
|
479
|
+
"sessionAffinityMaxEntries",
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
key: "rotation-quota",
|
|
484
|
+
label: "Rotation & Quota",
|
|
485
|
+
description: "Quota and retry behavior.",
|
|
486
|
+
toggleKeys: ["preemptiveQuotaEnabled", "retryAllAccountsRateLimited"],
|
|
487
|
+
numberKeys: [
|
|
488
|
+
"preemptiveQuotaRemainingPercent5h",
|
|
489
|
+
"preemptiveQuotaRemainingPercent7d",
|
|
490
|
+
"preemptiveQuotaMaxDeferralMs",
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
key: "refresh-recovery",
|
|
495
|
+
label: "Refresh & Recovery",
|
|
496
|
+
description: "Token refresh and recovery safety.",
|
|
497
|
+
toggleKeys: ["proactiveRefreshGuardian", "storageBackupEnabled"],
|
|
498
|
+
numberKeys: [
|
|
499
|
+
"proactiveRefreshIntervalMs",
|
|
500
|
+
"proactiveRefreshBufferMs",
|
|
501
|
+
"tokenRefreshSkewMs",
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
key: "performance-timeouts",
|
|
506
|
+
label: "Performance & Timeouts",
|
|
507
|
+
description: "Speed, probing, and timeout controls.",
|
|
508
|
+
toggleKeys: ["fastSession", "parallelProbing"],
|
|
509
|
+
numberKeys: [
|
|
510
|
+
"fastSessionMaxInputItems",
|
|
511
|
+
"parallelProbingMaxConcurrency",
|
|
512
|
+
"fetchTimeoutMs",
|
|
513
|
+
"streamStallTimeoutMs",
|
|
514
|
+
"networkErrorCooldownMs",
|
|
515
|
+
"serverErrorCooldownMs",
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
];
|
|
519
|
+
function normalizeStatuslineFields(fields) {
|
|
520
|
+
const source = fields ?? DEFAULT_STATUSLINE_FIELDS;
|
|
521
|
+
const seen = new Set();
|
|
522
|
+
const normalized = [];
|
|
523
|
+
for (const field of source) {
|
|
524
|
+
if (seen.has(field))
|
|
525
|
+
continue;
|
|
526
|
+
seen.add(field);
|
|
527
|
+
normalized.push(field);
|
|
528
|
+
}
|
|
529
|
+
if (normalized.length === 0) {
|
|
530
|
+
return [...DEFAULT_STATUSLINE_FIELDS];
|
|
531
|
+
}
|
|
532
|
+
return normalized;
|
|
533
|
+
}
|
|
534
|
+
function highlightPreviewToken(text, ui) {
|
|
535
|
+
if (!output.isTTY)
|
|
536
|
+
return text;
|
|
537
|
+
if (ui.v2Enabled) {
|
|
538
|
+
return `${ui.theme.colors.accent}${ANSI.bold}${text}${ui.theme.colors.reset}`;
|
|
539
|
+
}
|
|
540
|
+
return `${ANSI.cyan}${ANSI.bold}${text}${ANSI.reset}`;
|
|
541
|
+
}
|
|
542
|
+
function isLastUsedPreviewFocus(focus) {
|
|
543
|
+
return focus === "menuShowLastUsed" || focus === "last-used";
|
|
544
|
+
}
|
|
545
|
+
function isLimitsPreviewFocus(focus) {
|
|
546
|
+
return focus === "menuShowQuotaSummary" || focus === "limits";
|
|
547
|
+
}
|
|
548
|
+
function isLimitsCooldownPreviewFocus(focus) {
|
|
549
|
+
return focus === "menuShowQuotaCooldown";
|
|
550
|
+
}
|
|
551
|
+
function isStatusPreviewFocus(focus) {
|
|
552
|
+
return focus === "menuShowStatusBadge" || focus === "status";
|
|
553
|
+
}
|
|
554
|
+
function isCurrentBadgePreviewFocus(focus) {
|
|
555
|
+
return focus === "menuShowCurrentBadge";
|
|
556
|
+
}
|
|
557
|
+
function isCurrentRowPreviewFocus(focus) {
|
|
558
|
+
return focus === "menuHighlightCurrentRow";
|
|
559
|
+
}
|
|
560
|
+
function isExpandedRowsPreviewFocus(focus) {
|
|
561
|
+
return focus === "menuShowDetailsForUnselectedRows" || focus === "menuLayoutMode";
|
|
562
|
+
}
|
|
563
|
+
function buildSummaryPreviewText(settings, ui, focus = null) {
|
|
564
|
+
const partsByField = new Map();
|
|
565
|
+
if (settings.menuShowLastUsed !== false) {
|
|
566
|
+
const part = `last used: ${PREVIEW_LAST_USED}`;
|
|
567
|
+
partsByField.set("last-used", isLastUsedPreviewFocus(focus) ? highlightPreviewToken(part, ui) : part);
|
|
568
|
+
}
|
|
569
|
+
if (settings.menuShowQuotaSummary !== false) {
|
|
570
|
+
const limitsText = settings.menuShowQuotaCooldown === false
|
|
571
|
+
? PREVIEW_LIMITS
|
|
572
|
+
: `${PREVIEW_LIMITS} | ${PREVIEW_LIMIT_COOLDOWNS}`;
|
|
573
|
+
const part = `limits: ${limitsText}`;
|
|
574
|
+
partsByField.set("limits", isLimitsPreviewFocus(focus) || isLimitsCooldownPreviewFocus(focus)
|
|
575
|
+
? highlightPreviewToken(part, ui)
|
|
576
|
+
: part);
|
|
577
|
+
}
|
|
578
|
+
if (settings.menuShowStatusBadge === false) {
|
|
579
|
+
const part = `status: ${PREVIEW_STATUS}`;
|
|
580
|
+
partsByField.set("status", isStatusPreviewFocus(focus) ? highlightPreviewToken(part, ui) : part);
|
|
581
|
+
}
|
|
582
|
+
const orderedParts = normalizeStatuslineFields(settings.menuStatuslineFields)
|
|
583
|
+
.map((field) => partsByField.get(field))
|
|
584
|
+
.filter((part) => typeof part === "string" && part.length > 0);
|
|
585
|
+
if (orderedParts.length > 0) {
|
|
586
|
+
return orderedParts.join(" | ");
|
|
587
|
+
}
|
|
588
|
+
const showsStatusField = normalizeStatuslineFields(settings.menuStatuslineFields).includes("status");
|
|
589
|
+
if (showsStatusField && settings.menuShowStatusBadge !== false) {
|
|
590
|
+
const note = "status text appears only when status badges are hidden";
|
|
591
|
+
return isStatusPreviewFocus(focus) ? highlightPreviewToken(note, ui) : note;
|
|
592
|
+
}
|
|
593
|
+
return "no summary text is visible with current account-list settings";
|
|
594
|
+
}
|
|
595
|
+
function buildAccountListPreview(settings, ui, focus = null) {
|
|
596
|
+
const badges = [];
|
|
597
|
+
if (settings.menuShowCurrentBadge !== false) {
|
|
598
|
+
const currentBadge = "[current]";
|
|
599
|
+
badges.push(isCurrentBadgePreviewFocus(focus) ? highlightPreviewToken(currentBadge, ui) : currentBadge);
|
|
600
|
+
}
|
|
601
|
+
if (settings.menuShowStatusBadge !== false) {
|
|
602
|
+
const statusBadge = "[active]";
|
|
603
|
+
badges.push(isStatusPreviewFocus(focus) ? highlightPreviewToken(statusBadge, ui) : statusBadge);
|
|
604
|
+
}
|
|
605
|
+
const badgeSuffix = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
606
|
+
const accountEmail = isCurrentRowPreviewFocus(focus)
|
|
607
|
+
? highlightPreviewToken(PREVIEW_ACCOUNT_EMAIL, ui)
|
|
608
|
+
: PREVIEW_ACCOUNT_EMAIL;
|
|
609
|
+
const rowDetailMode = resolveMenuLayoutMode(settings) === "expanded-rows"
|
|
610
|
+
? "details shown on all rows"
|
|
611
|
+
: "details shown on selected row only";
|
|
612
|
+
const detailModeText = isExpandedRowsPreviewFocus(focus)
|
|
613
|
+
? highlightPreviewToken(rowDetailMode, ui)
|
|
614
|
+
: rowDetailMode;
|
|
615
|
+
return {
|
|
616
|
+
label: `1. ${accountEmail}${badgeSuffix}`,
|
|
617
|
+
hint: `${buildSummaryPreviewText(settings, ui, focus)}\n${detailModeText}`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function cloneDashboardSettings(settings) {
|
|
621
|
+
const layoutMode = resolveMenuLayoutMode(settings);
|
|
622
|
+
return {
|
|
623
|
+
showPerAccountRows: settings.showPerAccountRows,
|
|
624
|
+
showQuotaDetails: settings.showQuotaDetails,
|
|
625
|
+
showForecastReasons: settings.showForecastReasons,
|
|
626
|
+
showRecommendations: settings.showRecommendations,
|
|
627
|
+
showLiveProbeNotes: settings.showLiveProbeNotes,
|
|
628
|
+
actionAutoReturnMs: settings.actionAutoReturnMs ?? 2_000,
|
|
629
|
+
actionPauseOnKey: settings.actionPauseOnKey ?? true,
|
|
630
|
+
menuAutoFetchLimits: settings.menuAutoFetchLimits ?? true,
|
|
631
|
+
menuSortEnabled: settings.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true),
|
|
632
|
+
menuSortMode: settings.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first"),
|
|
633
|
+
menuSortPinCurrent: settings.menuSortPinCurrent ??
|
|
634
|
+
(DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false),
|
|
635
|
+
menuSortQuickSwitchVisibleRow: settings.menuSortQuickSwitchVisibleRow ?? true,
|
|
636
|
+
uiThemePreset: settings.uiThemePreset ?? "green",
|
|
637
|
+
uiAccentColor: settings.uiAccentColor ?? "green",
|
|
638
|
+
menuShowStatusBadge: settings.menuShowStatusBadge ?? true,
|
|
639
|
+
menuShowCurrentBadge: settings.menuShowCurrentBadge ?? true,
|
|
640
|
+
menuShowLastUsed: settings.menuShowLastUsed ?? true,
|
|
641
|
+
menuShowQuotaSummary: settings.menuShowQuotaSummary ?? true,
|
|
642
|
+
menuShowQuotaCooldown: settings.menuShowQuotaCooldown ?? true,
|
|
643
|
+
menuShowFetchStatus: settings.menuShowFetchStatus ?? true,
|
|
644
|
+
menuShowDetailsForUnselectedRows: layoutMode === "expanded-rows",
|
|
645
|
+
menuLayoutMode: layoutMode,
|
|
646
|
+
menuQuotaTtlMs: settings.menuQuotaTtlMs ?? 5 * 60_000,
|
|
647
|
+
menuFocusStyle: settings.menuFocusStyle ?? "row-invert",
|
|
648
|
+
menuHighlightCurrentRow: settings.menuHighlightCurrentRow ?? true,
|
|
649
|
+
menuStatuslineFields: [...normalizeStatuslineFields(settings.menuStatuslineFields)],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function dashboardSettingsEqual(left, right) {
|
|
653
|
+
return (left.showPerAccountRows === right.showPerAccountRows &&
|
|
654
|
+
left.showQuotaDetails === right.showQuotaDetails &&
|
|
655
|
+
left.showForecastReasons === right.showForecastReasons &&
|
|
656
|
+
left.showRecommendations === right.showRecommendations &&
|
|
657
|
+
left.showLiveProbeNotes === right.showLiveProbeNotes &&
|
|
658
|
+
(left.actionAutoReturnMs ?? 2_000) === (right.actionAutoReturnMs ?? 2_000) &&
|
|
659
|
+
(left.actionPauseOnKey ?? true) === (right.actionPauseOnKey ?? true) &&
|
|
660
|
+
(left.menuAutoFetchLimits ?? true) === (right.menuAutoFetchLimits ?? true) &&
|
|
661
|
+
(left.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)) ===
|
|
662
|
+
(right.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)) &&
|
|
663
|
+
(left.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first")) ===
|
|
664
|
+
(right.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first")) &&
|
|
665
|
+
(left.menuSortPinCurrent ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false)) ===
|
|
666
|
+
(right.menuSortPinCurrent ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false)) &&
|
|
667
|
+
(left.menuSortQuickSwitchVisibleRow ?? true) ===
|
|
668
|
+
(right.menuSortQuickSwitchVisibleRow ?? true) &&
|
|
669
|
+
(left.uiThemePreset ?? "green") === (right.uiThemePreset ?? "green") &&
|
|
670
|
+
(left.uiAccentColor ?? "green") === (right.uiAccentColor ?? "green") &&
|
|
671
|
+
(left.menuShowStatusBadge ?? true) === (right.menuShowStatusBadge ?? true) &&
|
|
672
|
+
(left.menuShowCurrentBadge ?? true) === (right.menuShowCurrentBadge ?? true) &&
|
|
673
|
+
(left.menuShowLastUsed ?? true) === (right.menuShowLastUsed ?? true) &&
|
|
674
|
+
(left.menuShowQuotaSummary ?? true) === (right.menuShowQuotaSummary ?? true) &&
|
|
675
|
+
(left.menuShowQuotaCooldown ?? true) === (right.menuShowQuotaCooldown ?? true) &&
|
|
676
|
+
(left.menuShowFetchStatus ?? true) === (right.menuShowFetchStatus ?? true) &&
|
|
677
|
+
resolveMenuLayoutMode(left) === resolveMenuLayoutMode(right) &&
|
|
678
|
+
(left.menuQuotaTtlMs ?? 5 * 60_000) === (right.menuQuotaTtlMs ?? 5 * 60_000) &&
|
|
679
|
+
(left.menuFocusStyle ?? "row-invert") === (right.menuFocusStyle ?? "row-invert") &&
|
|
680
|
+
(left.menuHighlightCurrentRow ?? true) === (right.menuHighlightCurrentRow ?? true) &&
|
|
681
|
+
JSON.stringify(normalizeStatuslineFields(left.menuStatuslineFields)) ===
|
|
682
|
+
JSON.stringify(normalizeStatuslineFields(right.menuStatuslineFields)));
|
|
683
|
+
}
|
|
684
|
+
function cloneBackendPluginConfig(config) {
|
|
685
|
+
const fallbackChain = config.unsupportedCodexFallbackChain;
|
|
686
|
+
return {
|
|
687
|
+
...BACKEND_DEFAULTS,
|
|
688
|
+
...config,
|
|
689
|
+
unsupportedCodexFallbackChain: fallbackChain && typeof fallbackChain === "object"
|
|
690
|
+
? { ...fallbackChain }
|
|
691
|
+
: {},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function backendSettingsSnapshot(config) {
|
|
695
|
+
const snapshot = {};
|
|
696
|
+
for (const option of BACKEND_TOGGLE_OPTIONS) {
|
|
697
|
+
snapshot[option.key] = config[option.key] ?? BACKEND_DEFAULTS[option.key] ?? false;
|
|
698
|
+
}
|
|
699
|
+
for (const option of BACKEND_NUMBER_OPTIONS) {
|
|
700
|
+
snapshot[option.key] = config[option.key] ?? BACKEND_DEFAULTS[option.key] ?? option.min;
|
|
701
|
+
}
|
|
702
|
+
return snapshot;
|
|
703
|
+
}
|
|
704
|
+
function backendSettingsEqual(left, right) {
|
|
705
|
+
return JSON.stringify(backendSettingsSnapshot(left)) === JSON.stringify(backendSettingsSnapshot(right));
|
|
706
|
+
}
|
|
707
|
+
function formatBackendNumberValue(option, value) {
|
|
708
|
+
if (option.unit === "percent")
|
|
709
|
+
return `${Math.round(value)}%`;
|
|
710
|
+
if (option.unit === "count")
|
|
711
|
+
return `${Math.round(value)}`;
|
|
712
|
+
if (value >= 60_000 && value % 60_000 === 0) {
|
|
713
|
+
return `${Math.round(value / 60_000)}m`;
|
|
714
|
+
}
|
|
715
|
+
if (value >= 1_000 && value % 1_000 === 0) {
|
|
716
|
+
return `${Math.round(value / 1_000)}s`;
|
|
717
|
+
}
|
|
718
|
+
return `${Math.round(value)}ms`;
|
|
719
|
+
}
|
|
720
|
+
function clampBackendNumber(option, value) {
|
|
721
|
+
return Math.max(option.min, Math.min(option.max, Math.round(value)));
|
|
722
|
+
}
|
|
723
|
+
function buildBackendSettingsPreview(config, ui, focus = null) {
|
|
724
|
+
const liveSync = config.liveAccountSync ?? BACKEND_DEFAULTS.liveAccountSync ?? true;
|
|
725
|
+
const affinity = config.sessionAffinity ?? BACKEND_DEFAULTS.sessionAffinity ?? true;
|
|
726
|
+
const preemptive = config.preemptiveQuotaEnabled ?? BACKEND_DEFAULTS.preemptiveQuotaEnabled ?? true;
|
|
727
|
+
const threshold5h = config.preemptiveQuotaRemainingPercent5h ??
|
|
728
|
+
BACKEND_DEFAULTS.preemptiveQuotaRemainingPercent5h ??
|
|
729
|
+
5;
|
|
730
|
+
const threshold7d = config.preemptiveQuotaRemainingPercent7d ??
|
|
731
|
+
BACKEND_DEFAULTS.preemptiveQuotaRemainingPercent7d ??
|
|
732
|
+
5;
|
|
733
|
+
const fetchTimeout = config.fetchTimeoutMs ?? BACKEND_DEFAULTS.fetchTimeoutMs ?? 60_000;
|
|
734
|
+
const stallTimeout = config.streamStallTimeoutMs ?? BACKEND_DEFAULTS.streamStallTimeoutMs ?? 45_000;
|
|
735
|
+
const fetchTimeoutOption = BACKEND_NUMBER_OPTION_BY_KEY.get("fetchTimeoutMs");
|
|
736
|
+
const stallTimeoutOption = BACKEND_NUMBER_OPTION_BY_KEY.get("streamStallTimeoutMs");
|
|
737
|
+
const highlightIfFocused = (key, text) => {
|
|
738
|
+
if (focus !== key)
|
|
739
|
+
return text;
|
|
740
|
+
return highlightPreviewToken(text, ui);
|
|
741
|
+
};
|
|
742
|
+
const label = [
|
|
743
|
+
`live sync ${highlightIfFocused("liveAccountSync", liveSync ? "on" : "off")}`,
|
|
744
|
+
`affinity ${highlightIfFocused("sessionAffinity", affinity ? "on" : "off")}`,
|
|
745
|
+
`preemptive ${highlightIfFocused("preemptiveQuotaEnabled", preemptive ? "on" : "off")}`,
|
|
746
|
+
].join(" | ");
|
|
747
|
+
const hint = [
|
|
748
|
+
`thresholds 5h<=${highlightIfFocused("preemptiveQuotaRemainingPercent5h", `${threshold5h}%`)}`,
|
|
749
|
+
`7d<=${highlightIfFocused("preemptiveQuotaRemainingPercent7d", `${threshold7d}%`)}`,
|
|
750
|
+
`timeouts ${highlightIfFocused("fetchTimeoutMs", fetchTimeoutOption ? formatBackendNumberValue(fetchTimeoutOption, fetchTimeout) : `${fetchTimeout}ms`)}/${highlightIfFocused("streamStallTimeoutMs", stallTimeoutOption ? formatBackendNumberValue(stallTimeoutOption, stallTimeout) : `${stallTimeout}ms`)}`,
|
|
751
|
+
].join(" | ");
|
|
752
|
+
return { label, hint };
|
|
753
|
+
}
|
|
754
|
+
function buildBackendConfigPatch(config) {
|
|
755
|
+
const patch = {};
|
|
756
|
+
for (const option of BACKEND_TOGGLE_OPTIONS) {
|
|
757
|
+
const value = config[option.key];
|
|
758
|
+
if (typeof value === "boolean") {
|
|
759
|
+
patch[option.key] = value;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (const option of BACKEND_NUMBER_OPTIONS) {
|
|
763
|
+
const value = config[option.key];
|
|
764
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
765
|
+
patch[option.key] = clampBackendNumber(option, value);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return patch;
|
|
769
|
+
}
|
|
770
|
+
function applyUiThemeFromDashboardSettings(settings) {
|
|
771
|
+
const current = getUiRuntimeOptions();
|
|
772
|
+
setUiRuntimeOptions({
|
|
773
|
+
v2Enabled: current.v2Enabled,
|
|
774
|
+
colorProfile: current.colorProfile,
|
|
775
|
+
glyphMode: current.glyphMode,
|
|
776
|
+
palette: settings.uiThemePreset ?? "green",
|
|
777
|
+
accent: settings.uiAccentColor ?? "green",
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function formatDashboardSettingState(value) {
|
|
781
|
+
return value ? "[x]" : "[ ]";
|
|
782
|
+
}
|
|
783
|
+
function formatQuotaSnapshotForDashboard(snapshot, settings) {
|
|
784
|
+
if (!settings.showQuotaDetails)
|
|
785
|
+
return "live session OK";
|
|
786
|
+
return `live session OK (${formatCompactQuotaSnapshot(snapshot)})`;
|
|
787
|
+
}
|
|
788
|
+
function isAbortError(error) {
|
|
789
|
+
if (!(error instanceof Error))
|
|
790
|
+
return false;
|
|
791
|
+
const maybe = error;
|
|
792
|
+
return maybe.name === "AbortError" || maybe.code === "ABORT_ERR";
|
|
793
|
+
}
|
|
794
|
+
function isUserCancelledOAuth(result) {
|
|
795
|
+
if (result.type !== "failed")
|
|
796
|
+
return false;
|
|
797
|
+
const message = (result.message ?? "").toLowerCase();
|
|
798
|
+
return message.includes("cancelled");
|
|
799
|
+
}
|
|
800
|
+
function printUsage() {
|
|
801
|
+
console.log([
|
|
802
|
+
"Codex Multi-Auth CLI",
|
|
803
|
+
"",
|
|
804
|
+
"Usage:",
|
|
805
|
+
" codex-multi-auth auth login",
|
|
806
|
+
" codex-multi-auth auth list",
|
|
807
|
+
" codex-multi-auth auth status",
|
|
808
|
+
" codex-multi-auth auth switch <index>",
|
|
809
|
+
" codex-multi-auth auth check",
|
|
810
|
+
" codex-multi-auth auth features",
|
|
811
|
+
" codex-multi-auth auth verify-flagged [--dry-run] [--json] [--no-restore]",
|
|
812
|
+
" codex-multi-auth auth forecast [--live] [--json] [--model <model>]",
|
|
813
|
+
" codex-multi-auth auth report [--live] [--json] [--model <model>] [--out <path>]",
|
|
814
|
+
" codex-multi-auth auth fix [--dry-run] [--json]",
|
|
815
|
+
" codex-multi-auth auth doctor [--json] [--fix] [--dry-run]",
|
|
816
|
+
"",
|
|
817
|
+
"Notes:",
|
|
818
|
+
" - Uses ~/.codex/multi-auth/openai-codex-accounts.json",
|
|
819
|
+
" - Syncs active account into Codex CLI auth state",
|
|
820
|
+
].join("\n"));
|
|
821
|
+
}
|
|
822
|
+
const IMPLEMENTED_FEATURES = [
|
|
823
|
+
{ id: 1, name: "Multi-account OAuth login dashboard" },
|
|
824
|
+
{ id: 2, name: "Account add/update dedupe by token/id/email" },
|
|
825
|
+
{ id: 3, name: "Set current account command" },
|
|
826
|
+
{ id: 4, name: "Per-family active index handling" },
|
|
827
|
+
{ id: 5, name: "Quick health check command" },
|
|
828
|
+
{ id: 6, name: "Full refresh check command" },
|
|
829
|
+
{ id: 7, name: "Flagged account verification command" },
|
|
830
|
+
{ id: 8, name: "Flagged account restore flow" },
|
|
831
|
+
{ id: 9, name: "Best account forecast engine" },
|
|
832
|
+
{ id: 10, name: "Forecast live quota probing" },
|
|
833
|
+
{ id: 11, name: "Auto-fix command (safe mode)" },
|
|
834
|
+
{ id: 12, name: "Doctor diagnostics command" },
|
|
835
|
+
{ id: 13, name: "JSON outputs for machine automation" },
|
|
836
|
+
{ id: 14, name: "Report generation command" },
|
|
837
|
+
{ id: 15, name: "Storage v3 normalization and migration" },
|
|
838
|
+
{ id: 16, name: "Storage backup and recovery journal" },
|
|
839
|
+
{ id: 17, name: "Project-scoped and global storage paths" },
|
|
840
|
+
{ id: 18, name: "Quota cache storage" },
|
|
841
|
+
{ id: 19, name: "Live account sync watcher" },
|
|
842
|
+
{ id: 20, name: "Session affinity store" },
|
|
843
|
+
{ id: 21, name: "Refresh queue dedupe (in-process)" },
|
|
844
|
+
{ id: 22, name: "Refresh lease dedupe (cross-process)" },
|
|
845
|
+
{ id: 23, name: "Token rotation mapping in refresh queue" },
|
|
846
|
+
{ id: 24, name: "Refresh guardian (proactive refresh)" },
|
|
847
|
+
{ id: 25, name: "Preemptive quota scheduler" },
|
|
848
|
+
{ id: 26, name: "Entitlement cache for unsupported models" },
|
|
849
|
+
{ id: 27, name: "Capability policy scoring store" },
|
|
850
|
+
{ id: 28, name: "Failure policy evaluation module" },
|
|
851
|
+
{ id: 29, name: "Streaming failover pipeline" },
|
|
852
|
+
{ id: 30, name: "Rate-limit backoff and cooldown handling" },
|
|
853
|
+
{ id: 31, name: "OpenCode request transformer bridge" },
|
|
854
|
+
{ id: 32, name: "Prompt template sync with cache" },
|
|
855
|
+
{ id: 33, name: "Codex CLI active-account state sync" },
|
|
856
|
+
{ id: 34, name: "TUI quick-switch hotkeys (1-9)" },
|
|
857
|
+
{ id: 35, name: "TUI search and help toggles" },
|
|
858
|
+
{ id: 36, name: "TUI account detail hotkeys (S/R/E/D)" },
|
|
859
|
+
{ id: 37, name: "TUI settings hub (list/summary/behavior/theme)" },
|
|
860
|
+
{ id: 38, name: "Dashboard display customization" },
|
|
861
|
+
{ id: 39, name: "Unified color/theme runtime (v2 UI)" },
|
|
862
|
+
{ id: 40, name: "OAuth browser-first flow with manual callback fallback" },
|
|
863
|
+
];
|
|
864
|
+
function runFeaturesReport() {
|
|
865
|
+
console.log(`Implemented features (${IMPLEMENTED_FEATURES.length})`);
|
|
866
|
+
console.log("");
|
|
867
|
+
for (const feature of IMPLEMENTED_FEATURES) {
|
|
868
|
+
console.log(`${feature.id}. ${feature.name}`);
|
|
869
|
+
}
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
function resolveActiveIndex(storage, family = "codex") {
|
|
873
|
+
const total = storage.accounts.length;
|
|
874
|
+
if (total === 0)
|
|
875
|
+
return 0;
|
|
876
|
+
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
|
|
877
|
+
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
|
|
878
|
+
return Math.max(0, Math.min(raw, total - 1));
|
|
879
|
+
}
|
|
880
|
+
function getRateLimitResetTimeForFamily(account, now, family) {
|
|
881
|
+
const times = account.rateLimitResetTimes;
|
|
882
|
+
if (!times)
|
|
883
|
+
return null;
|
|
884
|
+
let minReset = null;
|
|
885
|
+
const prefix = `${family}:`;
|
|
886
|
+
for (const [key, value] of Object.entries(times)) {
|
|
887
|
+
if (typeof value !== "number")
|
|
888
|
+
continue;
|
|
889
|
+
if (value <= now)
|
|
890
|
+
continue;
|
|
891
|
+
if (key !== family && !key.startsWith(prefix))
|
|
892
|
+
continue;
|
|
893
|
+
if (minReset === null || value < minReset) {
|
|
894
|
+
minReset = value;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return minReset;
|
|
898
|
+
}
|
|
899
|
+
function formatRateLimitEntry(account, now, family = "codex") {
|
|
900
|
+
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
901
|
+
if (typeof resetAt !== "number")
|
|
902
|
+
return null;
|
|
903
|
+
const remaining = resetAt - now;
|
|
904
|
+
if (remaining <= 0)
|
|
905
|
+
return null;
|
|
906
|
+
return `resets in ${formatWaitTime(remaining)}`;
|
|
907
|
+
}
|
|
908
|
+
function normalizeQuotaEmail(email) {
|
|
909
|
+
const normalized = sanitizeEmail(email);
|
|
910
|
+
return normalized && normalized.length > 0 ? normalized : null;
|
|
911
|
+
}
|
|
912
|
+
function quotaCacheEntryToSnapshot(entry) {
|
|
913
|
+
return {
|
|
914
|
+
status: entry.status,
|
|
915
|
+
planType: entry.planType,
|
|
916
|
+
model: entry.model,
|
|
917
|
+
primary: {
|
|
918
|
+
usedPercent: entry.primary.usedPercent,
|
|
919
|
+
windowMinutes: entry.primary.windowMinutes,
|
|
920
|
+
resetAtMs: entry.primary.resetAtMs,
|
|
921
|
+
},
|
|
922
|
+
secondary: {
|
|
923
|
+
usedPercent: entry.secondary.usedPercent,
|
|
924
|
+
windowMinutes: entry.secondary.windowMinutes,
|
|
925
|
+
resetAtMs: entry.secondary.resetAtMs,
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function formatCompactQuotaWindowLabel(windowMinutes) {
|
|
930
|
+
if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
|
|
931
|
+
return "quota";
|
|
932
|
+
}
|
|
933
|
+
if (windowMinutes % 1440 === 0)
|
|
934
|
+
return `${windowMinutes / 1440}d`;
|
|
935
|
+
if (windowMinutes % 60 === 0)
|
|
936
|
+
return `${windowMinutes / 60}h`;
|
|
937
|
+
return `${windowMinutes}m`;
|
|
938
|
+
}
|
|
939
|
+
function formatCompactQuotaPart(windowMinutes, usedPercent) {
|
|
940
|
+
const label = formatCompactQuotaWindowLabel(windowMinutes);
|
|
941
|
+
if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
const left = quotaLeftPercentFromUsed(usedPercent);
|
|
945
|
+
return `${label} ${left}%`;
|
|
946
|
+
}
|
|
947
|
+
function quotaLeftPercentFromUsed(usedPercent) {
|
|
948
|
+
if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) {
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
return Math.max(0, Math.min(100, Math.round(100 - usedPercent)));
|
|
952
|
+
}
|
|
953
|
+
function formatCompactQuotaSnapshot(snapshot) {
|
|
954
|
+
const parts = [
|
|
955
|
+
formatCompactQuotaPart(snapshot.primary.windowMinutes, snapshot.primary.usedPercent),
|
|
956
|
+
formatCompactQuotaPart(snapshot.secondary.windowMinutes, snapshot.secondary.usedPercent),
|
|
957
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
958
|
+
if (snapshot.status === 429) {
|
|
959
|
+
parts.push("rate-limited");
|
|
960
|
+
}
|
|
961
|
+
if (parts.length > 0) {
|
|
962
|
+
return parts.join(" | ");
|
|
963
|
+
}
|
|
964
|
+
return formatQuotaSnapshotLine(snapshot);
|
|
965
|
+
}
|
|
966
|
+
function formatAccountQuotaSummary(entry) {
|
|
967
|
+
const parts = [
|
|
968
|
+
formatCompactQuotaPart(entry.primary.windowMinutes, entry.primary.usedPercent),
|
|
969
|
+
formatCompactQuotaPart(entry.secondary.windowMinutes, entry.secondary.usedPercent),
|
|
970
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
971
|
+
if (entry.status === 429) {
|
|
972
|
+
parts.push("rate-limited");
|
|
973
|
+
}
|
|
974
|
+
if (parts.length > 0) {
|
|
975
|
+
return parts.join(" | ");
|
|
976
|
+
}
|
|
977
|
+
return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
|
|
978
|
+
}
|
|
979
|
+
function getQuotaCacheEntryForAccount(cache, account) {
|
|
980
|
+
if (account.accountId && cache.byAccountId[account.accountId]) {
|
|
981
|
+
return cache.byAccountId[account.accountId] ?? null;
|
|
982
|
+
}
|
|
983
|
+
const email = normalizeQuotaEmail(account.email);
|
|
984
|
+
if (email && cache.byEmail[email]) {
|
|
985
|
+
return cache.byEmail[email] ?? null;
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
function updateQuotaCacheForAccount(cache, account, snapshot) {
|
|
990
|
+
const nextEntry = {
|
|
991
|
+
updatedAt: Date.now(),
|
|
992
|
+
status: snapshot.status,
|
|
993
|
+
model: snapshot.model,
|
|
994
|
+
planType: snapshot.planType,
|
|
995
|
+
primary: {
|
|
996
|
+
usedPercent: snapshot.primary.usedPercent,
|
|
997
|
+
windowMinutes: snapshot.primary.windowMinutes,
|
|
998
|
+
resetAtMs: snapshot.primary.resetAtMs,
|
|
999
|
+
},
|
|
1000
|
+
secondary: {
|
|
1001
|
+
usedPercent: snapshot.secondary.usedPercent,
|
|
1002
|
+
windowMinutes: snapshot.secondary.windowMinutes,
|
|
1003
|
+
resetAtMs: snapshot.secondary.resetAtMs,
|
|
1004
|
+
},
|
|
1005
|
+
};
|
|
1006
|
+
let changed = false;
|
|
1007
|
+
if (account.accountId) {
|
|
1008
|
+
cache.byAccountId[account.accountId] = nextEntry;
|
|
1009
|
+
changed = true;
|
|
1010
|
+
}
|
|
1011
|
+
const email = normalizeQuotaEmail(account.email);
|
|
1012
|
+
if (email) {
|
|
1013
|
+
cache.byEmail[email] = nextEntry;
|
|
1014
|
+
changed = true;
|
|
1015
|
+
}
|
|
1016
|
+
return changed;
|
|
1017
|
+
}
|
|
1018
|
+
const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
|
|
1019
|
+
const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
|
|
1020
|
+
function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now) {
|
|
1021
|
+
if (account.enabled === false)
|
|
1022
|
+
return null;
|
|
1023
|
+
if (!hasUsableAccessToken(account, now))
|
|
1024
|
+
return null;
|
|
1025
|
+
const existing = getQuotaCacheEntryForAccount(cache, account);
|
|
1026
|
+
if (existing &&
|
|
1027
|
+
typeof existing.updatedAt === "number" &&
|
|
1028
|
+
Number.isFinite(existing.updatedAt) &&
|
|
1029
|
+
now - existing.updatedAt < maxAgeMs) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
const accessToken = account.accessToken;
|
|
1033
|
+
const accountId = accessToken
|
|
1034
|
+
? (account.accountId ?? extractAccountId(accessToken))
|
|
1035
|
+
: account.accountId;
|
|
1036
|
+
if (!accountId || !accessToken)
|
|
1037
|
+
return null;
|
|
1038
|
+
return { accountId, accessToken };
|
|
1039
|
+
}
|
|
1040
|
+
function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
1041
|
+
const targets = [];
|
|
1042
|
+
for (const account of storage.accounts) {
|
|
1043
|
+
const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now);
|
|
1044
|
+
if (!probeInput)
|
|
1045
|
+
continue;
|
|
1046
|
+
targets.push({
|
|
1047
|
+
account,
|
|
1048
|
+
accountId: probeInput.accountId,
|
|
1049
|
+
accessToken: probeInput.accessToken,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return targets;
|
|
1053
|
+
}
|
|
1054
|
+
function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
1055
|
+
let count = 0;
|
|
1056
|
+
for (const account of storage.accounts) {
|
|
1057
|
+
if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now)) {
|
|
1058
|
+
count += 1;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return count;
|
|
1062
|
+
}
|
|
1063
|
+
async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
|
|
1064
|
+
if (storage.accounts.length === 0) {
|
|
1065
|
+
return cache;
|
|
1066
|
+
}
|
|
1067
|
+
const now = Date.now();
|
|
1068
|
+
const targets = collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now);
|
|
1069
|
+
const total = targets.length;
|
|
1070
|
+
let processed = 0;
|
|
1071
|
+
onProgress?.(processed, total);
|
|
1072
|
+
let changed = false;
|
|
1073
|
+
for (const target of targets) {
|
|
1074
|
+
processed += 1;
|
|
1075
|
+
onProgress?.(processed, total);
|
|
1076
|
+
try {
|
|
1077
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
1078
|
+
accountId: target.accountId,
|
|
1079
|
+
accessToken: target.accessToken,
|
|
1080
|
+
model: MENU_QUOTA_REFRESH_MODEL,
|
|
1081
|
+
});
|
|
1082
|
+
changed = updateQuotaCacheForAccount(cache, target.account, snapshot) || changed;
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
// Keep existing cached values if probing fails.
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (changed) {
|
|
1089
|
+
await saveQuotaCache(cache);
|
|
1090
|
+
}
|
|
1091
|
+
return cache;
|
|
1092
|
+
}
|
|
1093
|
+
const ACCESS_TOKEN_FRESH_WINDOW_MS = 5 * 60 * 1000;
|
|
1094
|
+
function hasUsableAccessToken(account, now) {
|
|
1095
|
+
if (!account.accessToken)
|
|
1096
|
+
return false;
|
|
1097
|
+
if (typeof account.expiresAt !== "number" || !Number.isFinite(account.expiresAt))
|
|
1098
|
+
return false;
|
|
1099
|
+
return account.expiresAt - now > ACCESS_TOKEN_FRESH_WINDOW_MS;
|
|
1100
|
+
}
|
|
1101
|
+
function hasLikelyInvalidRefreshToken(refreshToken) {
|
|
1102
|
+
if (!refreshToken)
|
|
1103
|
+
return true;
|
|
1104
|
+
const trimmed = refreshToken.trim();
|
|
1105
|
+
if (trimmed.length < 20)
|
|
1106
|
+
return true;
|
|
1107
|
+
return trimmed.startsWith("token-");
|
|
1108
|
+
}
|
|
1109
|
+
function mapAccountStatus(account, index, activeIndex, now) {
|
|
1110
|
+
if (account.enabled === false)
|
|
1111
|
+
return "disabled";
|
|
1112
|
+
if (typeof account.coolingDownUntil === "number" && account.coolingDownUntil > now) {
|
|
1113
|
+
return "cooldown";
|
|
1114
|
+
}
|
|
1115
|
+
const rateLimit = formatRateLimitEntry(account, now, "codex");
|
|
1116
|
+
if (rateLimit)
|
|
1117
|
+
return "rate-limited";
|
|
1118
|
+
if (index === activeIndex)
|
|
1119
|
+
return "active";
|
|
1120
|
+
return "ok";
|
|
1121
|
+
}
|
|
1122
|
+
function parseLeftPercentFromQuotaSummary(summary, windowLabel) {
|
|
1123
|
+
if (!summary)
|
|
1124
|
+
return -1;
|
|
1125
|
+
const match = summary.match(new RegExp(`(?:^|\\|)\\s*${windowLabel}\\s+(\\d{1,3})%`, "i"));
|
|
1126
|
+
const value = Number.parseInt(match?.[1] ?? "", 10);
|
|
1127
|
+
if (!Number.isFinite(value))
|
|
1128
|
+
return -1;
|
|
1129
|
+
return Math.max(0, Math.min(100, value));
|
|
1130
|
+
}
|
|
1131
|
+
function readQuotaLeftPercent(account, windowLabel) {
|
|
1132
|
+
const direct = windowLabel === "5h" ? account.quota5hLeftPercent : account.quota7dLeftPercent;
|
|
1133
|
+
if (typeof direct === "number" && Number.isFinite(direct)) {
|
|
1134
|
+
return Math.max(0, Math.min(100, Math.round(direct)));
|
|
1135
|
+
}
|
|
1136
|
+
return parseLeftPercentFromQuotaSummary(account.quotaSummary, windowLabel);
|
|
1137
|
+
}
|
|
1138
|
+
function accountStatusSortBucket(status) {
|
|
1139
|
+
switch (status) {
|
|
1140
|
+
case "active":
|
|
1141
|
+
case "ok":
|
|
1142
|
+
return 0;
|
|
1143
|
+
case "unknown":
|
|
1144
|
+
return 1;
|
|
1145
|
+
case "cooldown":
|
|
1146
|
+
case "rate-limited":
|
|
1147
|
+
return 2;
|
|
1148
|
+
case "disabled":
|
|
1149
|
+
case "error":
|
|
1150
|
+
case "flagged":
|
|
1151
|
+
return 3;
|
|
1152
|
+
default:
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
function compareReadyFirstAccounts(left, right) {
|
|
1157
|
+
const left5h = readQuotaLeftPercent(left, "5h");
|
|
1158
|
+
const right5h = readQuotaLeftPercent(right, "5h");
|
|
1159
|
+
if (left5h !== right5h)
|
|
1160
|
+
return right5h - left5h;
|
|
1161
|
+
const left7d = readQuotaLeftPercent(left, "7d");
|
|
1162
|
+
const right7d = readQuotaLeftPercent(right, "7d");
|
|
1163
|
+
if (left7d !== right7d)
|
|
1164
|
+
return right7d - left7d;
|
|
1165
|
+
const bucketDelta = accountStatusSortBucket(left.status) - accountStatusSortBucket(right.status);
|
|
1166
|
+
if (bucketDelta !== 0)
|
|
1167
|
+
return bucketDelta;
|
|
1168
|
+
const leftLastUsed = left.lastUsed ?? 0;
|
|
1169
|
+
const rightLastUsed = right.lastUsed ?? 0;
|
|
1170
|
+
if (leftLastUsed !== rightLastUsed)
|
|
1171
|
+
return rightLastUsed - leftLastUsed;
|
|
1172
|
+
const leftSource = left.sourceIndex ?? left.index;
|
|
1173
|
+
const rightSource = right.sourceIndex ?? right.index;
|
|
1174
|
+
return leftSource - rightSource;
|
|
1175
|
+
}
|
|
1176
|
+
function applyAccountMenuOrdering(accounts, displaySettings) {
|
|
1177
|
+
const sortEnabled = displaySettings.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true);
|
|
1178
|
+
const sortMode = displaySettings.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first");
|
|
1179
|
+
if (!sortEnabled || sortMode !== "ready-first") {
|
|
1180
|
+
return [...accounts];
|
|
1181
|
+
}
|
|
1182
|
+
const sorted = [...accounts].sort(compareReadyFirstAccounts);
|
|
1183
|
+
const pinCurrent = displaySettings.menuSortPinCurrent ??
|
|
1184
|
+
(DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false);
|
|
1185
|
+
if (pinCurrent) {
|
|
1186
|
+
const currentIndex = sorted.findIndex((account) => account.isCurrentAccount);
|
|
1187
|
+
if (currentIndex > 0) {
|
|
1188
|
+
const current = sorted.splice(currentIndex, 1)[0];
|
|
1189
|
+
const first = sorted[0];
|
|
1190
|
+
if (current && first && compareReadyFirstAccounts(current, first) <= 0) {
|
|
1191
|
+
sorted.unshift(current);
|
|
1192
|
+
}
|
|
1193
|
+
else if (current) {
|
|
1194
|
+
sorted.splice(currentIndex, 0, current);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return sorted;
|
|
1199
|
+
}
|
|
1200
|
+
function toExistingAccountInfo(storage, quotaCache, displaySettings) {
|
|
1201
|
+
const now = Date.now();
|
|
1202
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
1203
|
+
const layoutMode = resolveMenuLayoutMode(displaySettings);
|
|
1204
|
+
const baseAccounts = storage.accounts.map((account, index) => {
|
|
1205
|
+
const entry = quotaCache ? getQuotaCacheEntryForAccount(quotaCache, account) : null;
|
|
1206
|
+
return {
|
|
1207
|
+
index,
|
|
1208
|
+
sourceIndex: index,
|
|
1209
|
+
accountId: account.accountId,
|
|
1210
|
+
accountLabel: account.accountLabel,
|
|
1211
|
+
email: account.email,
|
|
1212
|
+
addedAt: account.addedAt,
|
|
1213
|
+
lastUsed: account.lastUsed,
|
|
1214
|
+
status: mapAccountStatus(account, index, activeIndex, now),
|
|
1215
|
+
quotaSummary: (displaySettings.menuShowQuotaSummary ?? true) && entry
|
|
1216
|
+
? formatAccountQuotaSummary(entry)
|
|
1217
|
+
: undefined,
|
|
1218
|
+
quota5hLeftPercent: quotaLeftPercentFromUsed(entry?.primary.usedPercent),
|
|
1219
|
+
quota5hResetAtMs: entry?.primary.resetAtMs,
|
|
1220
|
+
quota7dLeftPercent: quotaLeftPercentFromUsed(entry?.secondary.usedPercent),
|
|
1221
|
+
quota7dResetAtMs: entry?.secondary.resetAtMs,
|
|
1222
|
+
quotaRateLimited: entry?.status === 429,
|
|
1223
|
+
isCurrentAccount: index === activeIndex,
|
|
1224
|
+
enabled: account.enabled !== false,
|
|
1225
|
+
showStatusBadge: displaySettings.menuShowStatusBadge ?? true,
|
|
1226
|
+
showCurrentBadge: displaySettings.menuShowCurrentBadge ?? true,
|
|
1227
|
+
showLastUsed: displaySettings.menuShowLastUsed ?? true,
|
|
1228
|
+
showQuotaCooldown: displaySettings.menuShowQuotaCooldown ?? true,
|
|
1229
|
+
showHintsForUnselectedRows: layoutMode === "expanded-rows",
|
|
1230
|
+
highlightCurrentRow: displaySettings.menuHighlightCurrentRow ?? true,
|
|
1231
|
+
focusStyle: displaySettings.menuFocusStyle ?? "row-invert",
|
|
1232
|
+
statuslineFields: displaySettings.menuStatuslineFields ?? ["last-used", "limits", "status"],
|
|
1233
|
+
};
|
|
1234
|
+
});
|
|
1235
|
+
const orderedAccounts = applyAccountMenuOrdering(baseAccounts, displaySettings);
|
|
1236
|
+
const quickSwitchUsesVisibleRows = displaySettings.menuSortQuickSwitchVisibleRow ?? true;
|
|
1237
|
+
return orderedAccounts.map((account, displayIndex) => ({
|
|
1238
|
+
...account,
|
|
1239
|
+
index: displayIndex,
|
|
1240
|
+
quickSwitchNumber: quickSwitchUsesVisibleRows
|
|
1241
|
+
? displayIndex + 1
|
|
1242
|
+
: (account.sourceIndex ?? displayIndex) + 1,
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
function resolveAccountSelection(tokens) {
|
|
1246
|
+
const override = (process.env.CODEX_AUTH_ACCOUNT_ID ?? "").trim();
|
|
1247
|
+
if (override) {
|
|
1248
|
+
return {
|
|
1249
|
+
...tokens,
|
|
1250
|
+
accountIdOverride: override,
|
|
1251
|
+
accountIdSource: "manual",
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
|
|
1255
|
+
if (candidates.length === 0) {
|
|
1256
|
+
return tokens;
|
|
1257
|
+
}
|
|
1258
|
+
if (candidates.length === 1) {
|
|
1259
|
+
const [candidate] = candidates;
|
|
1260
|
+
if (candidate) {
|
|
1261
|
+
return {
|
|
1262
|
+
...tokens,
|
|
1263
|
+
accountIdOverride: candidate.accountId,
|
|
1264
|
+
accountIdSource: candidate.source,
|
|
1265
|
+
accountLabel: candidate.label,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const best = selectBestAccountCandidate(candidates);
|
|
1270
|
+
if (!best) {
|
|
1271
|
+
return tokens;
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
...tokens,
|
|
1275
|
+
accountIdOverride: best.accountId,
|
|
1276
|
+
accountIdSource: best.source ?? "token",
|
|
1277
|
+
accountLabel: best.label,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
async function promptManualCallback(state) {
|
|
1281
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const rl = createInterface({ input, output });
|
|
1285
|
+
try {
|
|
1286
|
+
console.log("");
|
|
1287
|
+
console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
|
|
1288
|
+
const answer = await rl.question("◆ ");
|
|
1289
|
+
if (answer.includes("\u001b")) {
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
const normalized = answer.trim().toLowerCase();
|
|
1293
|
+
if (normalized.length === 0 ||
|
|
1294
|
+
normalized === "q" ||
|
|
1295
|
+
normalized === "quit" ||
|
|
1296
|
+
normalized === "cancel" ||
|
|
1297
|
+
normalized === "back") {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const parsed = parseAuthorizationInput(answer);
|
|
1301
|
+
if (!parsed.code)
|
|
1302
|
+
return null;
|
|
1303
|
+
if (parsed.state && parsed.state !== state)
|
|
1304
|
+
return null;
|
|
1305
|
+
return parsed.code;
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
if (isAbortError(error)) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
finally {
|
|
1314
|
+
rl.close();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function promptOAuthSignInMode() {
|
|
1318
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1319
|
+
return "browser";
|
|
1320
|
+
}
|
|
1321
|
+
const ui = getUiRuntimeOptions();
|
|
1322
|
+
const items = [
|
|
1323
|
+
{ label: UI_COPY.oauth.openBrowser, value: "browser", color: "green" },
|
|
1324
|
+
{ label: UI_COPY.oauth.manualMode, value: "manual", color: "yellow" },
|
|
1325
|
+
{ label: UI_COPY.oauth.back, value: "cancel", color: "red" },
|
|
1326
|
+
];
|
|
1327
|
+
const selected = await select(items, {
|
|
1328
|
+
message: UI_COPY.oauth.chooseModeTitle,
|
|
1329
|
+
subtitle: UI_COPY.oauth.chooseModeSubtitle,
|
|
1330
|
+
help: UI_COPY.oauth.chooseModeHelp,
|
|
1331
|
+
clearScreen: true,
|
|
1332
|
+
theme: ui.theme,
|
|
1333
|
+
selectedEmphasis: "minimal",
|
|
1334
|
+
allowEscape: false,
|
|
1335
|
+
onInput: (raw) => {
|
|
1336
|
+
const lower = raw.toLowerCase();
|
|
1337
|
+
if (lower === "q")
|
|
1338
|
+
return "cancel";
|
|
1339
|
+
if (lower === "1")
|
|
1340
|
+
return "browser";
|
|
1341
|
+
if (lower === "2")
|
|
1342
|
+
return "manual";
|
|
1343
|
+
return undefined;
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
return selected ?? "cancel";
|
|
1347
|
+
}
|
|
1348
|
+
async function waitForMenuReturn(options = {}) {
|
|
1349
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const promptText = options.promptText ?? UI_COPY.returnFlow.continuePrompt;
|
|
1353
|
+
const autoReturnMs = options.autoReturnMs ?? 0;
|
|
1354
|
+
const pauseOnAnyKey = options.pauseOnAnyKey ?? true;
|
|
1355
|
+
try {
|
|
1356
|
+
let chunk;
|
|
1357
|
+
do {
|
|
1358
|
+
chunk = input.read();
|
|
1359
|
+
} while (chunk !== null);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// best effort buffer drain
|
|
1363
|
+
}
|
|
1364
|
+
const writeInlineStatus = (message) => {
|
|
1365
|
+
output.write(`\r${ANSI.clearLine}${stylePromptText(message, "muted")}`);
|
|
1366
|
+
};
|
|
1367
|
+
const clearInlineStatus = () => {
|
|
1368
|
+
output.write(`\r${ANSI.clearLine}`);
|
|
1369
|
+
};
|
|
1370
|
+
if (autoReturnMs > 0) {
|
|
1371
|
+
if (!pauseOnAnyKey) {
|
|
1372
|
+
await new Promise((resolve) => setTimeout(resolve, autoReturnMs));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const wasRaw = input.isRaw ?? false;
|
|
1376
|
+
const endAt = Date.now() + autoReturnMs;
|
|
1377
|
+
let lastShownSeconds = null;
|
|
1378
|
+
const renderCountdown = () => {
|
|
1379
|
+
const remainingMs = Math.max(0, endAt - Date.now());
|
|
1380
|
+
const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1000));
|
|
1381
|
+
if (lastShownSeconds === remainingSeconds)
|
|
1382
|
+
return;
|
|
1383
|
+
lastShownSeconds = remainingSeconds;
|
|
1384
|
+
writeInlineStatus(UI_COPY.returnFlow.autoReturn(remainingSeconds));
|
|
1385
|
+
};
|
|
1386
|
+
renderCountdown();
|
|
1387
|
+
const pinned = await new Promise((resolve) => {
|
|
1388
|
+
let done = false;
|
|
1389
|
+
const interval = setInterval(renderCountdown, 80);
|
|
1390
|
+
let timeout = setTimeout(() => {
|
|
1391
|
+
timeout = null;
|
|
1392
|
+
if (!done) {
|
|
1393
|
+
done = true;
|
|
1394
|
+
cleanup();
|
|
1395
|
+
resolve(false);
|
|
1396
|
+
}
|
|
1397
|
+
}, autoReturnMs);
|
|
1398
|
+
const onData = () => {
|
|
1399
|
+
if (done)
|
|
1400
|
+
return;
|
|
1401
|
+
done = true;
|
|
1402
|
+
cleanup();
|
|
1403
|
+
resolve(true);
|
|
1404
|
+
};
|
|
1405
|
+
const cleanup = () => {
|
|
1406
|
+
clearInterval(interval);
|
|
1407
|
+
if (timeout) {
|
|
1408
|
+
clearTimeout(timeout);
|
|
1409
|
+
timeout = null;
|
|
1410
|
+
}
|
|
1411
|
+
input.removeListener("data", onData);
|
|
1412
|
+
try {
|
|
1413
|
+
input.setRawMode(wasRaw);
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
// best effort restore
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
try {
|
|
1420
|
+
input.setRawMode(true);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
// if raw mode fails, keep countdown behavior
|
|
1424
|
+
}
|
|
1425
|
+
input.on("data", onData);
|
|
1426
|
+
input.resume();
|
|
1427
|
+
});
|
|
1428
|
+
clearInlineStatus();
|
|
1429
|
+
if (!pinned) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const paused = stylePromptText(UI_COPY.returnFlow.paused, "muted");
|
|
1433
|
+
writeInlineStatus(paused);
|
|
1434
|
+
await new Promise((resolve) => {
|
|
1435
|
+
const wasRaw = input.isRaw ?? false;
|
|
1436
|
+
const onData = () => {
|
|
1437
|
+
cleanup();
|
|
1438
|
+
resolve();
|
|
1439
|
+
};
|
|
1440
|
+
const cleanup = () => {
|
|
1441
|
+
input.removeListener("data", onData);
|
|
1442
|
+
try {
|
|
1443
|
+
input.setRawMode(wasRaw);
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
// best effort restore
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
try {
|
|
1450
|
+
input.setRawMode(true);
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
// best effort fallback
|
|
1454
|
+
}
|
|
1455
|
+
input.on("data", onData);
|
|
1456
|
+
input.resume();
|
|
1457
|
+
});
|
|
1458
|
+
clearInlineStatus();
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const rl = createInterface({ input, output });
|
|
1462
|
+
try {
|
|
1463
|
+
const question = promptText.length > 0 ? `${stylePromptText(promptText, "muted")} ` : "";
|
|
1464
|
+
output.write(`\r${ANSI.clearLine}`);
|
|
1465
|
+
await rl.question(question);
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
if (!isAbortError(error)) {
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
finally {
|
|
1473
|
+
rl.close();
|
|
1474
|
+
clearInlineStatus();
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function stringifyLogArgs(args) {
|
|
1478
|
+
return args
|
|
1479
|
+
.map((value) => {
|
|
1480
|
+
if (typeof value === "string")
|
|
1481
|
+
return value;
|
|
1482
|
+
try {
|
|
1483
|
+
return JSON.stringify(value);
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
return String(value);
|
|
1487
|
+
}
|
|
1488
|
+
})
|
|
1489
|
+
.join(" ");
|
|
1490
|
+
}
|
|
1491
|
+
async function runActionPanel(title, stage, action, settings) {
|
|
1492
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1493
|
+
await action();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const spinnerFrames = ["-", "\\", "|", "/"];
|
|
1497
|
+
let frame = 0;
|
|
1498
|
+
let running = true;
|
|
1499
|
+
let failed = null;
|
|
1500
|
+
const captured = [];
|
|
1501
|
+
const maxVisibleLines = Math.max(8, (output.rows ?? 24) - 8);
|
|
1502
|
+
const previousLog = console.log;
|
|
1503
|
+
const previousWarn = console.warn;
|
|
1504
|
+
const previousError = console.error;
|
|
1505
|
+
const capture = (prefix, args) => {
|
|
1506
|
+
const line = stringifyLogArgs(args).trim();
|
|
1507
|
+
if (!line)
|
|
1508
|
+
return;
|
|
1509
|
+
captured.push(prefix ? `${prefix}${line}` : line);
|
|
1510
|
+
if (captured.length > 400) {
|
|
1511
|
+
captured.splice(0, captured.length - 400);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
const render = () => {
|
|
1515
|
+
output.write(ANSI.clearScreen + ANSI.moveTo(1, 1));
|
|
1516
|
+
const spinner = running
|
|
1517
|
+
? `${spinnerFrames[frame % spinnerFrames.length] ?? "-"} `
|
|
1518
|
+
: failed
|
|
1519
|
+
? "x "
|
|
1520
|
+
: "+ ";
|
|
1521
|
+
const stageText = running
|
|
1522
|
+
? `${spinner}${stage}`
|
|
1523
|
+
: failed
|
|
1524
|
+
? UI_COPY.returnFlow.failed
|
|
1525
|
+
: UI_COPY.returnFlow.done;
|
|
1526
|
+
previousLog(stylePromptText(title, "accent"));
|
|
1527
|
+
previousLog(stylePromptText(stageText, failed ? "danger" : running ? "accent" : "success"));
|
|
1528
|
+
previousLog("");
|
|
1529
|
+
const lines = captured.slice(-maxVisibleLines);
|
|
1530
|
+
for (const line of lines) {
|
|
1531
|
+
previousLog(line);
|
|
1532
|
+
}
|
|
1533
|
+
const remainingLines = Math.max(0, maxVisibleLines - lines.length);
|
|
1534
|
+
for (let i = 0; i < remainingLines; i += 1) {
|
|
1535
|
+
previousLog("");
|
|
1536
|
+
}
|
|
1537
|
+
previousLog("");
|
|
1538
|
+
if (running)
|
|
1539
|
+
previousLog(stylePromptText(UI_COPY.returnFlow.working, "muted"));
|
|
1540
|
+
frame += 1;
|
|
1541
|
+
};
|
|
1542
|
+
console.log = (...args) => {
|
|
1543
|
+
capture("", args);
|
|
1544
|
+
};
|
|
1545
|
+
console.warn = (...args) => {
|
|
1546
|
+
capture("! ", args);
|
|
1547
|
+
};
|
|
1548
|
+
console.error = (...args) => {
|
|
1549
|
+
capture("x ", args);
|
|
1550
|
+
};
|
|
1551
|
+
output.write(ANSI.altScreenOn + ANSI.hide);
|
|
1552
|
+
let timer = null;
|
|
1553
|
+
try {
|
|
1554
|
+
render();
|
|
1555
|
+
timer = setInterval(() => {
|
|
1556
|
+
if (!running)
|
|
1557
|
+
return;
|
|
1558
|
+
render();
|
|
1559
|
+
}, 120);
|
|
1560
|
+
await action();
|
|
1561
|
+
}
|
|
1562
|
+
catch (error) {
|
|
1563
|
+
failed = error;
|
|
1564
|
+
capture("x ", [error instanceof Error ? error.message : String(error)]);
|
|
1565
|
+
}
|
|
1566
|
+
finally {
|
|
1567
|
+
running = false;
|
|
1568
|
+
if (timer) {
|
|
1569
|
+
clearInterval(timer);
|
|
1570
|
+
timer = null;
|
|
1571
|
+
}
|
|
1572
|
+
render();
|
|
1573
|
+
console.log = previousLog;
|
|
1574
|
+
console.warn = previousWarn;
|
|
1575
|
+
console.error = previousError;
|
|
1576
|
+
}
|
|
1577
|
+
if (failed) {
|
|
1578
|
+
await waitForMenuReturn({
|
|
1579
|
+
promptText: UI_COPY.returnFlow.actionFailedPrompt,
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
await waitForMenuReturn({
|
|
1584
|
+
autoReturnMs: settings?.actionAutoReturnMs ?? 2_000,
|
|
1585
|
+
pauseOnAnyKey: settings?.actionPauseOnKey ?? true,
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
output.write(ANSI.altScreenOff + ANSI.show + ANSI.clearScreen + ANSI.moveTo(1, 1));
|
|
1589
|
+
if (failed) {
|
|
1590
|
+
throw failed;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const STATUSLINE_FIELD_OPTIONS = [
|
|
1594
|
+
{
|
|
1595
|
+
key: "last-used",
|
|
1596
|
+
label: "Show Last Used",
|
|
1597
|
+
description: "Example: 'today' or '2d ago'.",
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
key: "limits",
|
|
1601
|
+
label: "Show Limits (5h / 7d)",
|
|
1602
|
+
description: "Uses cached limit data from checks.",
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
key: "status",
|
|
1606
|
+
label: "Show Status Text",
|
|
1607
|
+
description: "Visible when badges are hidden.",
|
|
1608
|
+
},
|
|
1609
|
+
];
|
|
1610
|
+
function formatMenuSortMode(mode) {
|
|
1611
|
+
return mode === "ready-first" ? "Ready-First" : "Manual";
|
|
1612
|
+
}
|
|
1613
|
+
function resolveMenuLayoutMode(settings) {
|
|
1614
|
+
if (settings.menuLayoutMode === "expanded-rows") {
|
|
1615
|
+
return "expanded-rows";
|
|
1616
|
+
}
|
|
1617
|
+
if (settings.menuLayoutMode === "compact-details") {
|
|
1618
|
+
return "compact-details";
|
|
1619
|
+
}
|
|
1620
|
+
return settings.menuShowDetailsForUnselectedRows === true ? "expanded-rows" : "compact-details";
|
|
1621
|
+
}
|
|
1622
|
+
function formatMenuLayoutMode(mode) {
|
|
1623
|
+
return mode === "expanded-rows" ? "Expanded Rows" : "Compact + Details Pane";
|
|
1624
|
+
}
|
|
1625
|
+
function formatMenuQuotaTtl(ttlMs) {
|
|
1626
|
+
if (ttlMs >= 60_000 && ttlMs % 60_000 === 0) {
|
|
1627
|
+
return `${Math.round(ttlMs / 60_000)}m`;
|
|
1628
|
+
}
|
|
1629
|
+
if (ttlMs >= 1_000 && ttlMs % 1_000 === 0) {
|
|
1630
|
+
return `${Math.round(ttlMs / 1_000)}s`;
|
|
1631
|
+
}
|
|
1632
|
+
return `${ttlMs}ms`;
|
|
1633
|
+
}
|
|
1634
|
+
async function promptDashboardDisplaySettings(initial) {
|
|
1635
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
const ui = getUiRuntimeOptions();
|
|
1639
|
+
let draft = cloneDashboardSettings(initial);
|
|
1640
|
+
let focusKey = DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? "menuShowStatusBadge";
|
|
1641
|
+
while (true) {
|
|
1642
|
+
const preview = buildAccountListPreview(draft, ui, focusKey);
|
|
1643
|
+
const optionItems = DASHBOARD_DISPLAY_OPTIONS.map((option, index) => {
|
|
1644
|
+
const enabled = draft[option.key] ?? true;
|
|
1645
|
+
const label = `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}`;
|
|
1646
|
+
const color = enabled ? "green" : "yellow";
|
|
1647
|
+
return {
|
|
1648
|
+
label,
|
|
1649
|
+
hint: option.description,
|
|
1650
|
+
value: { type: "toggle", key: option.key },
|
|
1651
|
+
color,
|
|
1652
|
+
};
|
|
1653
|
+
});
|
|
1654
|
+
const sortMode = draft.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first");
|
|
1655
|
+
const sortModeItem = {
|
|
1656
|
+
label: `Sort mode: ${formatMenuSortMode(sortMode)}`,
|
|
1657
|
+
hint: "Applies when smart sort is enabled.",
|
|
1658
|
+
value: { type: "cycle-sort-mode" },
|
|
1659
|
+
color: sortMode === "ready-first" ? "green" : "yellow",
|
|
1660
|
+
};
|
|
1661
|
+
const layoutMode = resolveMenuLayoutMode(draft);
|
|
1662
|
+
const layoutModeItem = {
|
|
1663
|
+
label: `Layout: ${formatMenuLayoutMode(layoutMode)}`,
|
|
1664
|
+
hint: "Compact shows one-line rows with a selected details pane.",
|
|
1665
|
+
value: { type: "cycle-layout-mode" },
|
|
1666
|
+
color: layoutMode === "compact-details" ? "green" : "yellow",
|
|
1667
|
+
};
|
|
1668
|
+
const items = [
|
|
1669
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1670
|
+
{
|
|
1671
|
+
label: preview.label,
|
|
1672
|
+
hint: preview.hint,
|
|
1673
|
+
value: { type: "cancel" },
|
|
1674
|
+
color: "green",
|
|
1675
|
+
disabled: true,
|
|
1676
|
+
hideUnavailableSuffix: true,
|
|
1677
|
+
},
|
|
1678
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1679
|
+
{ label: UI_COPY.settings.displayHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1680
|
+
...optionItems,
|
|
1681
|
+
sortModeItem,
|
|
1682
|
+
layoutModeItem,
|
|
1683
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1684
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1685
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1686
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1687
|
+
];
|
|
1688
|
+
const initialCursor = items.findIndex((item) => (item.value.type === "toggle" && item.value.key === focusKey) ||
|
|
1689
|
+
(item.value.type === "cycle-sort-mode" && focusKey === "menuSortMode") ||
|
|
1690
|
+
(item.value.type === "cycle-layout-mode" && focusKey === "menuLayoutMode"));
|
|
1691
|
+
const updateFocusedPreview = (cursor) => {
|
|
1692
|
+
const focusedItem = items[cursor];
|
|
1693
|
+
const focusedKey = focusedItem?.value.type === "toggle"
|
|
1694
|
+
? focusedItem.value.key
|
|
1695
|
+
: focusedItem?.value.type === "cycle-sort-mode"
|
|
1696
|
+
? "menuSortMode"
|
|
1697
|
+
: focusedItem?.value.type === "cycle-layout-mode"
|
|
1698
|
+
? "menuLayoutMode"
|
|
1699
|
+
: focusKey;
|
|
1700
|
+
const nextPreview = buildAccountListPreview(draft, ui, focusedKey);
|
|
1701
|
+
const previewItem = items[1];
|
|
1702
|
+
if (!previewItem)
|
|
1703
|
+
return;
|
|
1704
|
+
previewItem.label = nextPreview.label;
|
|
1705
|
+
previewItem.hint = nextPreview.hint;
|
|
1706
|
+
};
|
|
1707
|
+
const result = await select(items, {
|
|
1708
|
+
message: UI_COPY.settings.accountListTitle,
|
|
1709
|
+
subtitle: UI_COPY.settings.accountListSubtitle,
|
|
1710
|
+
help: UI_COPY.settings.accountListHelp,
|
|
1711
|
+
clearScreen: true,
|
|
1712
|
+
theme: ui.theme,
|
|
1713
|
+
selectedEmphasis: "minimal",
|
|
1714
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1715
|
+
onCursorChange: ({ cursor }) => {
|
|
1716
|
+
const focusedItem = items[cursor];
|
|
1717
|
+
if (focusedItem?.value.type === "toggle") {
|
|
1718
|
+
focusKey = focusedItem.value.key;
|
|
1719
|
+
}
|
|
1720
|
+
else if (focusedItem?.value.type === "cycle-sort-mode") {
|
|
1721
|
+
focusKey = "menuSortMode";
|
|
1722
|
+
}
|
|
1723
|
+
else if (focusedItem?.value.type === "cycle-layout-mode") {
|
|
1724
|
+
focusKey = "menuLayoutMode";
|
|
1725
|
+
}
|
|
1726
|
+
updateFocusedPreview(cursor);
|
|
1727
|
+
},
|
|
1728
|
+
onInput: (raw) => {
|
|
1729
|
+
const lower = raw.toLowerCase();
|
|
1730
|
+
if (lower === "q")
|
|
1731
|
+
return { type: "save" };
|
|
1732
|
+
if (lower === "s")
|
|
1733
|
+
return { type: "save" };
|
|
1734
|
+
if (lower === "r")
|
|
1735
|
+
return { type: "reset" };
|
|
1736
|
+
if (lower === "m")
|
|
1737
|
+
return { type: "cycle-sort-mode" };
|
|
1738
|
+
if (lower === "l")
|
|
1739
|
+
return { type: "cycle-layout-mode" };
|
|
1740
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1741
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= DASHBOARD_DISPLAY_OPTIONS.length) {
|
|
1742
|
+
const target = DASHBOARD_DISPLAY_OPTIONS[parsed - 1];
|
|
1743
|
+
if (target) {
|
|
1744
|
+
return { type: "toggle", key: target.key };
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (parsed === DASHBOARD_DISPLAY_OPTIONS.length + 1) {
|
|
1748
|
+
return { type: "cycle-sort-mode" };
|
|
1749
|
+
}
|
|
1750
|
+
if (parsed === DASHBOARD_DISPLAY_OPTIONS.length + 2) {
|
|
1751
|
+
return { type: "cycle-layout-mode" };
|
|
1752
|
+
}
|
|
1753
|
+
return undefined;
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
if (!result || result.type === "cancel") {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
if (result.type === "save") {
|
|
1760
|
+
return draft;
|
|
1761
|
+
}
|
|
1762
|
+
if (result.type === "reset") {
|
|
1763
|
+
draft = cloneDashboardSettings(DEFAULT_DASHBOARD_DISPLAY_SETTINGS);
|
|
1764
|
+
focusKey = DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? focusKey;
|
|
1765
|
+
await saveDashboardDisplaySettings(draft);
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (result.type === "cycle-sort-mode") {
|
|
1769
|
+
const currentMode = draft.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first");
|
|
1770
|
+
const nextMode = currentMode === "ready-first"
|
|
1771
|
+
? "manual"
|
|
1772
|
+
: "ready-first";
|
|
1773
|
+
draft = {
|
|
1774
|
+
...draft,
|
|
1775
|
+
menuSortMode: nextMode,
|
|
1776
|
+
menuSortEnabled: nextMode === "ready-first"
|
|
1777
|
+
? true
|
|
1778
|
+
: (draft.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)),
|
|
1779
|
+
};
|
|
1780
|
+
focusKey = "menuSortMode";
|
|
1781
|
+
await saveDashboardDisplaySettings(draft);
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (result.type === "cycle-layout-mode") {
|
|
1785
|
+
const currentLayout = resolveMenuLayoutMode(draft);
|
|
1786
|
+
const nextLayout = currentLayout === "compact-details" ? "expanded-rows" : "compact-details";
|
|
1787
|
+
draft = {
|
|
1788
|
+
...draft,
|
|
1789
|
+
menuLayoutMode: nextLayout,
|
|
1790
|
+
menuShowDetailsForUnselectedRows: nextLayout === "expanded-rows",
|
|
1791
|
+
};
|
|
1792
|
+
focusKey = "menuLayoutMode";
|
|
1793
|
+
await saveDashboardDisplaySettings(draft);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
focusKey = result.key;
|
|
1797
|
+
draft = {
|
|
1798
|
+
...draft,
|
|
1799
|
+
[result.key]: !draft[result.key],
|
|
1800
|
+
};
|
|
1801
|
+
await saveDashboardDisplaySettings(draft);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
async function configureDashboardDisplaySettings(currentSettings) {
|
|
1805
|
+
const current = currentSettings ?? await loadDashboardDisplaySettings();
|
|
1806
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1807
|
+
console.log("Settings require interactive mode.");
|
|
1808
|
+
console.log(`Settings file: ${getDashboardSettingsPath()}`);
|
|
1809
|
+
return current;
|
|
1810
|
+
}
|
|
1811
|
+
const selected = await promptDashboardDisplaySettings(current);
|
|
1812
|
+
if (!selected)
|
|
1813
|
+
return current;
|
|
1814
|
+
if (dashboardSettingsEqual(current, selected))
|
|
1815
|
+
return current;
|
|
1816
|
+
await saveDashboardDisplaySettings(selected);
|
|
1817
|
+
applyUiThemeFromDashboardSettings(selected);
|
|
1818
|
+
return selected;
|
|
1819
|
+
}
|
|
1820
|
+
function reorderField(fields, key, direction) {
|
|
1821
|
+
const index = fields.indexOf(key);
|
|
1822
|
+
if (index < 0)
|
|
1823
|
+
return fields;
|
|
1824
|
+
const target = index + direction;
|
|
1825
|
+
if (target < 0 || target >= fields.length)
|
|
1826
|
+
return fields;
|
|
1827
|
+
const next = [...fields];
|
|
1828
|
+
const current = next[index];
|
|
1829
|
+
const swap = next[target];
|
|
1830
|
+
if (!current || !swap)
|
|
1831
|
+
return fields;
|
|
1832
|
+
next[index] = swap;
|
|
1833
|
+
next[target] = current;
|
|
1834
|
+
return next;
|
|
1835
|
+
}
|
|
1836
|
+
async function promptStatuslineSettings(initial) {
|
|
1837
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
const ui = getUiRuntimeOptions();
|
|
1841
|
+
let draft = cloneDashboardSettings(initial);
|
|
1842
|
+
let focusKey = draft.menuStatuslineFields?.[0] ?? "last-used";
|
|
1843
|
+
while (true) {
|
|
1844
|
+
const preview = buildAccountListPreview(draft, ui, focusKey);
|
|
1845
|
+
const selectedSet = new Set(normalizeStatuslineFields(draft.menuStatuslineFields));
|
|
1846
|
+
const ordered = normalizeStatuslineFields(draft.menuStatuslineFields);
|
|
1847
|
+
const orderMap = new Map();
|
|
1848
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
1849
|
+
const key = ordered[index];
|
|
1850
|
+
if (key)
|
|
1851
|
+
orderMap.set(key, index + 1);
|
|
1852
|
+
}
|
|
1853
|
+
const optionItems = STATUSLINE_FIELD_OPTIONS.map((option, index) => {
|
|
1854
|
+
const enabled = selectedSet.has(option.key);
|
|
1855
|
+
const rank = orderMap.get(option.key);
|
|
1856
|
+
const label = `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}${rank ? ` (order ${rank})` : ""}`;
|
|
1857
|
+
return {
|
|
1858
|
+
label,
|
|
1859
|
+
hint: option.description,
|
|
1860
|
+
value: { type: "toggle", key: option.key },
|
|
1861
|
+
color: enabled ? "green" : "yellow",
|
|
1862
|
+
};
|
|
1863
|
+
});
|
|
1864
|
+
const items = [
|
|
1865
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1866
|
+
{
|
|
1867
|
+
label: preview.label,
|
|
1868
|
+
hint: preview.hint,
|
|
1869
|
+
value: { type: "cancel" },
|
|
1870
|
+
color: "green",
|
|
1871
|
+
disabled: true,
|
|
1872
|
+
hideUnavailableSuffix: true,
|
|
1873
|
+
},
|
|
1874
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1875
|
+
{ label: UI_COPY.settings.displayHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1876
|
+
...optionItems,
|
|
1877
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1878
|
+
{ label: UI_COPY.settings.moveUp, value: { type: "move-up", key: focusKey }, color: "green" },
|
|
1879
|
+
{ label: UI_COPY.settings.moveDown, value: { type: "move-down", key: focusKey }, color: "green" },
|
|
1880
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1881
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1882
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1883
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1884
|
+
];
|
|
1885
|
+
const initialCursor = items.findIndex((item) => item.value.type === "toggle" && item.value.key === focusKey);
|
|
1886
|
+
const updateFocusedPreview = (cursor) => {
|
|
1887
|
+
const focusedItem = items[cursor];
|
|
1888
|
+
const focusedKey = focusedItem?.value.type === "toggle" ? focusedItem.value.key : focusKey;
|
|
1889
|
+
const nextPreview = buildAccountListPreview(draft, ui, focusedKey);
|
|
1890
|
+
const previewItem = items[1];
|
|
1891
|
+
if (!previewItem)
|
|
1892
|
+
return;
|
|
1893
|
+
previewItem.label = nextPreview.label;
|
|
1894
|
+
previewItem.hint = nextPreview.hint;
|
|
1895
|
+
};
|
|
1896
|
+
const result = await select(items, {
|
|
1897
|
+
message: UI_COPY.settings.summaryTitle,
|
|
1898
|
+
subtitle: UI_COPY.settings.summarySubtitle,
|
|
1899
|
+
help: UI_COPY.settings.summaryHelp,
|
|
1900
|
+
clearScreen: true,
|
|
1901
|
+
theme: ui.theme,
|
|
1902
|
+
selectedEmphasis: "minimal",
|
|
1903
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1904
|
+
onCursorChange: ({ cursor }) => {
|
|
1905
|
+
const focusedItem = items[cursor];
|
|
1906
|
+
if (focusedItem?.value.type === "toggle") {
|
|
1907
|
+
focusKey = focusedItem.value.key;
|
|
1908
|
+
}
|
|
1909
|
+
updateFocusedPreview(cursor);
|
|
1910
|
+
},
|
|
1911
|
+
onInput: (raw) => {
|
|
1912
|
+
const lower = raw.toLowerCase();
|
|
1913
|
+
if (lower === "q")
|
|
1914
|
+
return { type: "save" };
|
|
1915
|
+
if (lower === "s")
|
|
1916
|
+
return { type: "save" };
|
|
1917
|
+
if (lower === "r")
|
|
1918
|
+
return { type: "reset" };
|
|
1919
|
+
if (lower === "[")
|
|
1920
|
+
return { type: "move-up", key: focusKey };
|
|
1921
|
+
if (lower === "]")
|
|
1922
|
+
return { type: "move-down", key: focusKey };
|
|
1923
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1924
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= STATUSLINE_FIELD_OPTIONS.length) {
|
|
1925
|
+
const target = STATUSLINE_FIELD_OPTIONS[parsed - 1];
|
|
1926
|
+
if (target) {
|
|
1927
|
+
return { type: "toggle", key: target.key };
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return undefined;
|
|
1931
|
+
},
|
|
1932
|
+
});
|
|
1933
|
+
if (!result || result.type === "cancel") {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
if (result.type === "save") {
|
|
1937
|
+
return draft;
|
|
1938
|
+
}
|
|
1939
|
+
if (result.type === "reset") {
|
|
1940
|
+
draft = cloneDashboardSettings(DEFAULT_DASHBOARD_DISPLAY_SETTINGS);
|
|
1941
|
+
focusKey = draft.menuStatuslineFields?.[0] ?? "last-used";
|
|
1942
|
+
await saveDashboardDisplaySettings(draft);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (result.type === "move-up") {
|
|
1946
|
+
draft = {
|
|
1947
|
+
...draft,
|
|
1948
|
+
menuStatuslineFields: reorderField(normalizeStatuslineFields(draft.menuStatuslineFields), result.key, -1),
|
|
1949
|
+
};
|
|
1950
|
+
focusKey = result.key;
|
|
1951
|
+
await saveDashboardDisplaySettings(draft);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
if (result.type === "move-down") {
|
|
1955
|
+
draft = {
|
|
1956
|
+
...draft,
|
|
1957
|
+
menuStatuslineFields: reorderField(normalizeStatuslineFields(draft.menuStatuslineFields), result.key, 1),
|
|
1958
|
+
};
|
|
1959
|
+
focusKey = result.key;
|
|
1960
|
+
await saveDashboardDisplaySettings(draft);
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
focusKey = result.key;
|
|
1964
|
+
const fields = normalizeStatuslineFields(draft.menuStatuslineFields);
|
|
1965
|
+
const isEnabled = fields.includes(result.key);
|
|
1966
|
+
if (isEnabled) {
|
|
1967
|
+
const next = fields.filter((field) => field !== result.key);
|
|
1968
|
+
draft = {
|
|
1969
|
+
...draft,
|
|
1970
|
+
menuStatuslineFields: next.length > 0 ? next : [result.key],
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
else {
|
|
1974
|
+
draft = {
|
|
1975
|
+
...draft,
|
|
1976
|
+
menuStatuslineFields: [...fields, result.key],
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
await saveDashboardDisplaySettings(draft);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function configureStatuslineSettings(currentSettings) {
|
|
1983
|
+
const current = currentSettings ?? await loadDashboardDisplaySettings();
|
|
1984
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1985
|
+
console.log("Settings require interactive mode.");
|
|
1986
|
+
console.log(`Settings file: ${getDashboardSettingsPath()}`);
|
|
1987
|
+
return current;
|
|
1988
|
+
}
|
|
1989
|
+
const selected = await promptStatuslineSettings(current);
|
|
1990
|
+
if (!selected)
|
|
1991
|
+
return current;
|
|
1992
|
+
if (dashboardSettingsEqual(current, selected))
|
|
1993
|
+
return current;
|
|
1994
|
+
await saveDashboardDisplaySettings(selected);
|
|
1995
|
+
applyUiThemeFromDashboardSettings(selected);
|
|
1996
|
+
return selected;
|
|
1997
|
+
}
|
|
1998
|
+
function formatDelayLabel(delayMs) {
|
|
1999
|
+
return delayMs <= 0 ? "Instant return" : `${Math.round(delayMs / 1000)}s auto-return`;
|
|
2000
|
+
}
|
|
2001
|
+
async function promptBehaviorSettings(initial) {
|
|
2002
|
+
if (!input.isTTY || !output.isTTY)
|
|
2003
|
+
return null;
|
|
2004
|
+
const ui = getUiRuntimeOptions();
|
|
2005
|
+
let draft = cloneDashboardSettings(initial);
|
|
2006
|
+
let focus = {
|
|
2007
|
+
type: "set-delay",
|
|
2008
|
+
delayMs: draft.actionAutoReturnMs ?? 2_000,
|
|
2009
|
+
};
|
|
2010
|
+
while (true) {
|
|
2011
|
+
const currentDelay = draft.actionAutoReturnMs ?? 2_000;
|
|
2012
|
+
const pauseOnKey = draft.actionPauseOnKey ?? true;
|
|
2013
|
+
const autoFetchLimits = draft.menuAutoFetchLimits ?? true;
|
|
2014
|
+
const fetchStatusVisible = draft.menuShowFetchStatus ?? true;
|
|
2015
|
+
const menuQuotaTtlMs = draft.menuQuotaTtlMs ?? 5 * 60_000;
|
|
2016
|
+
const delayItems = AUTO_RETURN_OPTIONS_MS.map((delayMs) => {
|
|
2017
|
+
const color = currentDelay === delayMs ? "green" : "yellow";
|
|
2018
|
+
return {
|
|
2019
|
+
label: `${currentDelay === delayMs ? "[x]" : "[ ]"} ${formatDelayLabel(delayMs)}`,
|
|
2020
|
+
hint: delayMs === 1_000
|
|
2021
|
+
? "Fastest loop for frequent actions."
|
|
2022
|
+
: delayMs === 2_000
|
|
2023
|
+
? "Balanced default for most users."
|
|
2024
|
+
: "More time to read action output.",
|
|
2025
|
+
value: { type: "set-delay", delayMs },
|
|
2026
|
+
color,
|
|
2027
|
+
};
|
|
2028
|
+
});
|
|
2029
|
+
const pauseColor = pauseOnKey ? "green" : "yellow";
|
|
2030
|
+
const items = [
|
|
2031
|
+
{ label: UI_COPY.settings.actionTiming, value: { type: "cancel" }, kind: "heading" },
|
|
2032
|
+
...delayItems,
|
|
2033
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2034
|
+
{
|
|
2035
|
+
label: `${pauseOnKey ? "[x]" : "[ ]"} Pause on key press`,
|
|
2036
|
+
hint: "Press any key to stop auto-return.",
|
|
2037
|
+
value: { type: "toggle-pause" },
|
|
2038
|
+
color: pauseColor,
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
label: `${autoFetchLimits ? "[x]" : "[ ]"} Auto-fetch limits on menu open (5m cache)`,
|
|
2042
|
+
hint: "Refreshes account limits automatically when opening the menu.",
|
|
2043
|
+
value: { type: "toggle-menu-limit-fetch" },
|
|
2044
|
+
color: autoFetchLimits ? "green" : "yellow",
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
label: `${fetchStatusVisible ? "[x]" : "[ ]"} Show limit refresh status`,
|
|
2048
|
+
hint: "Shows background fetch progress like [2/7] in menu subtitle.",
|
|
2049
|
+
value: { type: "toggle-menu-fetch-status" },
|
|
2050
|
+
color: fetchStatusVisible ? "green" : "yellow",
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
label: `Limit cache TTL: ${formatMenuQuotaTtl(menuQuotaTtlMs)}`,
|
|
2054
|
+
hint: "How fresh cached quota data must be before refresh runs.",
|
|
2055
|
+
value: { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs },
|
|
2056
|
+
color: "yellow",
|
|
2057
|
+
},
|
|
2058
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2059
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
2060
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
2061
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
2062
|
+
];
|
|
2063
|
+
const initialCursor = items.findIndex((item) => {
|
|
2064
|
+
const value = item.value;
|
|
2065
|
+
if (value.type !== focus.type)
|
|
2066
|
+
return false;
|
|
2067
|
+
if (value.type === "set-delay" && focus.type === "set-delay") {
|
|
2068
|
+
return value.delayMs === focus.delayMs;
|
|
2069
|
+
}
|
|
2070
|
+
return true;
|
|
2071
|
+
});
|
|
2072
|
+
const result = await select(items, {
|
|
2073
|
+
message: UI_COPY.settings.behaviorTitle,
|
|
2074
|
+
subtitle: UI_COPY.settings.behaviorSubtitle,
|
|
2075
|
+
help: UI_COPY.settings.behaviorHelp,
|
|
2076
|
+
clearScreen: true,
|
|
2077
|
+
theme: ui.theme,
|
|
2078
|
+
selectedEmphasis: "minimal",
|
|
2079
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
2080
|
+
onCursorChange: ({ cursor }) => {
|
|
2081
|
+
const item = items[cursor];
|
|
2082
|
+
if (item && !item.separator && item.kind !== "heading") {
|
|
2083
|
+
focus = item.value;
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
onInput: (raw) => {
|
|
2087
|
+
const lower = raw.toLowerCase();
|
|
2088
|
+
if (lower === "q")
|
|
2089
|
+
return { type: "save" };
|
|
2090
|
+
if (lower === "s")
|
|
2091
|
+
return { type: "save" };
|
|
2092
|
+
if (lower === "r")
|
|
2093
|
+
return { type: "reset" };
|
|
2094
|
+
if (lower === "p")
|
|
2095
|
+
return { type: "toggle-pause" };
|
|
2096
|
+
if (lower === "l")
|
|
2097
|
+
return { type: "toggle-menu-limit-fetch" };
|
|
2098
|
+
if (lower === "f")
|
|
2099
|
+
return { type: "toggle-menu-fetch-status" };
|
|
2100
|
+
if (lower === "t")
|
|
2101
|
+
return { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs };
|
|
2102
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2103
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= AUTO_RETURN_OPTIONS_MS.length) {
|
|
2104
|
+
const delayMs = AUTO_RETURN_OPTIONS_MS[parsed - 1];
|
|
2105
|
+
if (typeof delayMs === "number")
|
|
2106
|
+
return { type: "set-delay", delayMs };
|
|
2107
|
+
}
|
|
2108
|
+
return undefined;
|
|
2109
|
+
},
|
|
2110
|
+
});
|
|
2111
|
+
if (!result || result.type === "cancel")
|
|
2112
|
+
return null;
|
|
2113
|
+
if (result.type === "save")
|
|
2114
|
+
return draft;
|
|
2115
|
+
if (result.type === "reset") {
|
|
2116
|
+
draft = cloneDashboardSettings(DEFAULT_DASHBOARD_DISPLAY_SETTINGS);
|
|
2117
|
+
focus = { type: "set-delay", delayMs: draft.actionAutoReturnMs ?? 2_000 };
|
|
2118
|
+
await saveDashboardDisplaySettings(draft);
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
if (result.type === "toggle-pause") {
|
|
2122
|
+
draft = {
|
|
2123
|
+
...draft,
|
|
2124
|
+
actionPauseOnKey: !(draft.actionPauseOnKey ?? true),
|
|
2125
|
+
};
|
|
2126
|
+
focus = result;
|
|
2127
|
+
await saveDashboardDisplaySettings(draft);
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
if (result.type === "toggle-menu-limit-fetch") {
|
|
2131
|
+
draft = {
|
|
2132
|
+
...draft,
|
|
2133
|
+
menuAutoFetchLimits: !(draft.menuAutoFetchLimits ?? true),
|
|
2134
|
+
};
|
|
2135
|
+
focus = result;
|
|
2136
|
+
await saveDashboardDisplaySettings(draft);
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
if (result.type === "toggle-menu-fetch-status") {
|
|
2140
|
+
draft = {
|
|
2141
|
+
...draft,
|
|
2142
|
+
menuShowFetchStatus: !(draft.menuShowFetchStatus ?? true),
|
|
2143
|
+
};
|
|
2144
|
+
focus = result;
|
|
2145
|
+
await saveDashboardDisplaySettings(draft);
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
if (result.type === "set-menu-quota-ttl") {
|
|
2149
|
+
const currentIndex = MENU_QUOTA_TTL_OPTIONS_MS.findIndex((value) => value === menuQuotaTtlMs);
|
|
2150
|
+
const nextIndex = currentIndex < 0
|
|
2151
|
+
? 0
|
|
2152
|
+
: (currentIndex + 1) % MENU_QUOTA_TTL_OPTIONS_MS.length;
|
|
2153
|
+
const nextTtl = MENU_QUOTA_TTL_OPTIONS_MS[nextIndex] ?? MENU_QUOTA_TTL_OPTIONS_MS[0] ?? menuQuotaTtlMs;
|
|
2154
|
+
draft = {
|
|
2155
|
+
...draft,
|
|
2156
|
+
menuQuotaTtlMs: nextTtl,
|
|
2157
|
+
};
|
|
2158
|
+
focus = { type: "set-menu-quota-ttl", ttlMs: nextTtl };
|
|
2159
|
+
await saveDashboardDisplaySettings(draft);
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
draft = {
|
|
2163
|
+
...draft,
|
|
2164
|
+
actionAutoReturnMs: result.delayMs,
|
|
2165
|
+
};
|
|
2166
|
+
focus = result;
|
|
2167
|
+
await saveDashboardDisplaySettings(draft);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async function promptThemeSettings(initial) {
|
|
2171
|
+
if (!input.isTTY || !output.isTTY)
|
|
2172
|
+
return null;
|
|
2173
|
+
const ui = getUiRuntimeOptions();
|
|
2174
|
+
let draft = cloneDashboardSettings(initial);
|
|
2175
|
+
let focus = {
|
|
2176
|
+
type: "set-palette",
|
|
2177
|
+
palette: draft.uiThemePreset ?? "green",
|
|
2178
|
+
};
|
|
2179
|
+
while (true) {
|
|
2180
|
+
const palette = draft.uiThemePreset ?? "green";
|
|
2181
|
+
const accent = draft.uiAccentColor ?? "green";
|
|
2182
|
+
const paletteItems = THEME_PRESET_OPTIONS.map((candidate, index) => {
|
|
2183
|
+
const color = palette === candidate ? "green" : "yellow";
|
|
2184
|
+
return {
|
|
2185
|
+
label: `${palette === candidate ? "[x]" : "[ ]"} ${index + 1}. ${candidate === "green" ? "Green base" : "Blue base"}`,
|
|
2186
|
+
hint: candidate === "green" ? "High-contrast default." : "Codex-style blue look.",
|
|
2187
|
+
value: { type: "set-palette", palette: candidate },
|
|
2188
|
+
color,
|
|
2189
|
+
};
|
|
2190
|
+
});
|
|
2191
|
+
const accentItems = ACCENT_COLOR_OPTIONS.map((candidate) => {
|
|
2192
|
+
const color = accent === candidate ? "green" : "yellow";
|
|
2193
|
+
return {
|
|
2194
|
+
label: `${accent === candidate ? "[x]" : "[ ]"} ${candidate}`,
|
|
2195
|
+
value: { type: "set-accent", accent: candidate },
|
|
2196
|
+
color,
|
|
2197
|
+
};
|
|
2198
|
+
});
|
|
2199
|
+
const items = [
|
|
2200
|
+
{ label: UI_COPY.settings.baseTheme, value: { type: "cancel" }, kind: "heading" },
|
|
2201
|
+
...paletteItems,
|
|
2202
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2203
|
+
{ label: UI_COPY.settings.accentColor, value: { type: "cancel" }, kind: "heading" },
|
|
2204
|
+
...accentItems,
|
|
2205
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2206
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
2207
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
2208
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
2209
|
+
];
|
|
2210
|
+
const initialCursor = items.findIndex((item) => {
|
|
2211
|
+
const value = item.value;
|
|
2212
|
+
if (value.type !== focus.type)
|
|
2213
|
+
return false;
|
|
2214
|
+
if (value.type === "set-palette" && focus.type === "set-palette") {
|
|
2215
|
+
return value.palette === focus.palette;
|
|
2216
|
+
}
|
|
2217
|
+
if (value.type === "set-accent" && focus.type === "set-accent") {
|
|
2218
|
+
return value.accent === focus.accent;
|
|
2219
|
+
}
|
|
2220
|
+
return true;
|
|
2221
|
+
});
|
|
2222
|
+
const result = await select(items, {
|
|
2223
|
+
message: UI_COPY.settings.themeTitle,
|
|
2224
|
+
subtitle: UI_COPY.settings.themeSubtitle,
|
|
2225
|
+
help: UI_COPY.settings.themeHelp,
|
|
2226
|
+
clearScreen: true,
|
|
2227
|
+
theme: ui.theme,
|
|
2228
|
+
selectedEmphasis: "minimal",
|
|
2229
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
2230
|
+
onCursorChange: ({ cursor }) => {
|
|
2231
|
+
const item = items[cursor];
|
|
2232
|
+
if (item && !item.separator && item.kind !== "heading") {
|
|
2233
|
+
focus = item.value;
|
|
2234
|
+
}
|
|
2235
|
+
},
|
|
2236
|
+
onInput: (raw) => {
|
|
2237
|
+
const lower = raw.toLowerCase();
|
|
2238
|
+
if (lower === "q")
|
|
2239
|
+
return { type: "save" };
|
|
2240
|
+
if (lower === "s")
|
|
2241
|
+
return { type: "save" };
|
|
2242
|
+
if (lower === "r")
|
|
2243
|
+
return { type: "reset" };
|
|
2244
|
+
if (raw === "1")
|
|
2245
|
+
return { type: "set-palette", palette: "green" };
|
|
2246
|
+
if (raw === "2")
|
|
2247
|
+
return { type: "set-palette", palette: "blue" };
|
|
2248
|
+
return undefined;
|
|
2249
|
+
},
|
|
2250
|
+
});
|
|
2251
|
+
if (!result || result.type === "cancel")
|
|
2252
|
+
return null;
|
|
2253
|
+
if (result.type === "save")
|
|
2254
|
+
return draft;
|
|
2255
|
+
if (result.type === "reset") {
|
|
2256
|
+
draft = cloneDashboardSettings(DEFAULT_DASHBOARD_DISPLAY_SETTINGS);
|
|
2257
|
+
focus = { type: "set-palette", palette: draft.uiThemePreset ?? "green" };
|
|
2258
|
+
await saveDashboardDisplaySettings(draft);
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
if (result.type === "set-palette") {
|
|
2262
|
+
draft = { ...draft, uiThemePreset: result.palette };
|
|
2263
|
+
focus = result;
|
|
2264
|
+
applyUiThemeFromDashboardSettings(draft);
|
|
2265
|
+
await saveDashboardDisplaySettings(draft);
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
draft = { ...draft, uiAccentColor: result.accent };
|
|
2269
|
+
focus = result;
|
|
2270
|
+
applyUiThemeFromDashboardSettings(draft);
|
|
2271
|
+
await saveDashboardDisplaySettings(draft);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function resolveFocusedBackendNumberKey(focus, numberOptions = BACKEND_NUMBER_OPTIONS) {
|
|
2275
|
+
const numberKeys = new Set(numberOptions.map((option) => option.key));
|
|
2276
|
+
if (focus && numberKeys.has(focus)) {
|
|
2277
|
+
return focus;
|
|
2278
|
+
}
|
|
2279
|
+
return numberOptions[0]?.key ?? "fetchTimeoutMs";
|
|
2280
|
+
}
|
|
2281
|
+
function getBackendCategory(key) {
|
|
2282
|
+
return BACKEND_CATEGORY_OPTIONS.find((category) => category.key === key) ?? null;
|
|
2283
|
+
}
|
|
2284
|
+
function getBackendCategoryInitialFocus(category) {
|
|
2285
|
+
const firstToggle = category.toggleKeys[0];
|
|
2286
|
+
if (firstToggle)
|
|
2287
|
+
return firstToggle;
|
|
2288
|
+
return category.numberKeys[0] ?? null;
|
|
2289
|
+
}
|
|
2290
|
+
function applyBackendCategoryDefaults(draft, category) {
|
|
2291
|
+
const next = { ...draft };
|
|
2292
|
+
for (const key of category.toggleKeys) {
|
|
2293
|
+
next[key] = BACKEND_DEFAULTS[key] ?? false;
|
|
2294
|
+
}
|
|
2295
|
+
for (const key of category.numberKeys) {
|
|
2296
|
+
const option = BACKEND_NUMBER_OPTION_BY_KEY.get(key);
|
|
2297
|
+
const fallback = option?.min ?? 0;
|
|
2298
|
+
next[key] = BACKEND_DEFAULTS[key] ?? fallback;
|
|
2299
|
+
}
|
|
2300
|
+
return next;
|
|
2301
|
+
}
|
|
2302
|
+
async function promptBackendCategorySettings(initial, category, initialFocus) {
|
|
2303
|
+
const ui = getUiRuntimeOptions();
|
|
2304
|
+
let draft = cloneBackendPluginConfig(initial);
|
|
2305
|
+
let focusKey = initialFocus;
|
|
2306
|
+
if (!focusKey ||
|
|
2307
|
+
(!category.toggleKeys.includes(focusKey) &&
|
|
2308
|
+
!category.numberKeys.includes(focusKey))) {
|
|
2309
|
+
focusKey = getBackendCategoryInitialFocus(category);
|
|
2310
|
+
}
|
|
2311
|
+
const toggleOptions = category.toggleKeys
|
|
2312
|
+
.map((key) => BACKEND_TOGGLE_OPTION_BY_KEY.get(key))
|
|
2313
|
+
.filter((option) => !!option);
|
|
2314
|
+
const numberOptions = category.numberKeys
|
|
2315
|
+
.map((key) => BACKEND_NUMBER_OPTION_BY_KEY.get(key))
|
|
2316
|
+
.filter((option) => !!option);
|
|
2317
|
+
while (true) {
|
|
2318
|
+
const preview = buildBackendSettingsPreview(draft, ui, focusKey);
|
|
2319
|
+
const toggleItems = toggleOptions.map((option, index) => {
|
|
2320
|
+
const enabled = draft[option.key] ?? BACKEND_DEFAULTS[option.key] ?? false;
|
|
2321
|
+
return {
|
|
2322
|
+
label: `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}`,
|
|
2323
|
+
hint: option.description,
|
|
2324
|
+
value: { type: "toggle", key: option.key },
|
|
2325
|
+
color: enabled ? "green" : "yellow",
|
|
2326
|
+
};
|
|
2327
|
+
});
|
|
2328
|
+
const numberItems = numberOptions.map((option) => {
|
|
2329
|
+
const rawValue = draft[option.key] ?? BACKEND_DEFAULTS[option.key] ?? option.min;
|
|
2330
|
+
const numericValue = typeof rawValue === "number" && Number.isFinite(rawValue)
|
|
2331
|
+
? rawValue
|
|
2332
|
+
: option.min;
|
|
2333
|
+
const clampedValue = clampBackendNumber(option, numericValue);
|
|
2334
|
+
const valueLabel = formatBackendNumberValue(option, clampedValue);
|
|
2335
|
+
return {
|
|
2336
|
+
label: `${option.label}: ${valueLabel}`,
|
|
2337
|
+
hint: `${option.description} Step ${formatBackendNumberValue(option, option.step)}.`,
|
|
2338
|
+
value: { type: "bump", key: option.key, direction: 1 },
|
|
2339
|
+
color: "yellow",
|
|
2340
|
+
};
|
|
2341
|
+
});
|
|
2342
|
+
const focusedNumberKey = resolveFocusedBackendNumberKey(focusKey, numberOptions);
|
|
2343
|
+
const items = [
|
|
2344
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "back" }, kind: "heading" },
|
|
2345
|
+
{
|
|
2346
|
+
label: preview.label,
|
|
2347
|
+
hint: preview.hint,
|
|
2348
|
+
value: { type: "back" },
|
|
2349
|
+
disabled: true,
|
|
2350
|
+
color: "green",
|
|
2351
|
+
hideUnavailableSuffix: true,
|
|
2352
|
+
},
|
|
2353
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
2354
|
+
{ label: UI_COPY.settings.backendToggleHeading, value: { type: "back" }, kind: "heading" },
|
|
2355
|
+
...toggleItems,
|
|
2356
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
2357
|
+
{ label: UI_COPY.settings.backendNumberHeading, value: { type: "back" }, kind: "heading" },
|
|
2358
|
+
...numberItems,
|
|
2359
|
+
];
|
|
2360
|
+
if (numberOptions.length > 0) {
|
|
2361
|
+
items.push({ label: "", value: { type: "back" }, separator: true });
|
|
2362
|
+
items.push({
|
|
2363
|
+
label: UI_COPY.settings.backendDecrease,
|
|
2364
|
+
value: { type: "bump", key: focusedNumberKey, direction: -1 },
|
|
2365
|
+
color: "yellow",
|
|
2366
|
+
});
|
|
2367
|
+
items.push({
|
|
2368
|
+
label: UI_COPY.settings.backendIncrease,
|
|
2369
|
+
value: { type: "bump", key: focusedNumberKey, direction: 1 },
|
|
2370
|
+
color: "green",
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
items.push({ label: "", value: { type: "back" }, separator: true });
|
|
2374
|
+
items.push({ label: UI_COPY.settings.backendResetCategory, value: { type: "reset-category" }, color: "yellow" });
|
|
2375
|
+
items.push({ label: UI_COPY.settings.backendBackToCategories, value: { type: "back" }, color: "red" });
|
|
2376
|
+
const initialCursor = items.findIndex((item) => {
|
|
2377
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
2378
|
+
return false;
|
|
2379
|
+
if (item.value.type === "toggle" && focusKey === item.value.key)
|
|
2380
|
+
return true;
|
|
2381
|
+
if (item.value.type === "bump" && focusKey === item.value.key)
|
|
2382
|
+
return true;
|
|
2383
|
+
return false;
|
|
2384
|
+
});
|
|
2385
|
+
const result = await select(items, {
|
|
2386
|
+
message: `${UI_COPY.settings.backendCategoryTitle}: ${category.label}`,
|
|
2387
|
+
subtitle: category.description,
|
|
2388
|
+
help: UI_COPY.settings.backendCategoryHelp,
|
|
2389
|
+
clearScreen: true,
|
|
2390
|
+
theme: ui.theme,
|
|
2391
|
+
selectedEmphasis: "minimal",
|
|
2392
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
2393
|
+
onCursorChange: ({ cursor }) => {
|
|
2394
|
+
const focusedItem = items[cursor];
|
|
2395
|
+
if (focusedItem?.value.type === "toggle" || focusedItem?.value.type === "bump") {
|
|
2396
|
+
focusKey = focusedItem.value.key;
|
|
2397
|
+
}
|
|
2398
|
+
},
|
|
2399
|
+
onInput: (raw) => {
|
|
2400
|
+
const lower = raw.toLowerCase();
|
|
2401
|
+
if (lower === "q")
|
|
2402
|
+
return { type: "back" };
|
|
2403
|
+
if (lower === "r")
|
|
2404
|
+
return { type: "reset-category" };
|
|
2405
|
+
if (numberOptions.length > 0 && (lower === "+" || lower === "=" || lower === "]" || lower === "d")) {
|
|
2406
|
+
return { type: "bump", key: resolveFocusedBackendNumberKey(focusKey, numberOptions), direction: 1 };
|
|
2407
|
+
}
|
|
2408
|
+
if (numberOptions.length > 0 && (lower === "-" || lower === "[" || lower === "a")) {
|
|
2409
|
+
return { type: "bump", key: resolveFocusedBackendNumberKey(focusKey, numberOptions), direction: -1 };
|
|
2410
|
+
}
|
|
2411
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2412
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= toggleOptions.length) {
|
|
2413
|
+
const target = toggleOptions[parsed - 1];
|
|
2414
|
+
if (target)
|
|
2415
|
+
return { type: "toggle", key: target.key };
|
|
2416
|
+
}
|
|
2417
|
+
return undefined;
|
|
2418
|
+
},
|
|
2419
|
+
});
|
|
2420
|
+
if (!result || result.type === "back") {
|
|
2421
|
+
return { draft, focusKey };
|
|
2422
|
+
}
|
|
2423
|
+
if (result.type === "reset-category") {
|
|
2424
|
+
draft = applyBackendCategoryDefaults(draft, category);
|
|
2425
|
+
focusKey = getBackendCategoryInitialFocus(category);
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
if (result.type === "toggle") {
|
|
2429
|
+
const currentValue = draft[result.key] ?? BACKEND_DEFAULTS[result.key] ?? false;
|
|
2430
|
+
draft = { ...draft, [result.key]: !currentValue };
|
|
2431
|
+
focusKey = result.key;
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
const option = BACKEND_NUMBER_OPTION_BY_KEY.get(result.key);
|
|
2435
|
+
if (!option)
|
|
2436
|
+
continue;
|
|
2437
|
+
const currentValue = draft[result.key] ?? BACKEND_DEFAULTS[result.key] ?? option.min;
|
|
2438
|
+
const numericCurrent = typeof currentValue === "number" && Number.isFinite(currentValue)
|
|
2439
|
+
? currentValue
|
|
2440
|
+
: option.min;
|
|
2441
|
+
draft = {
|
|
2442
|
+
...draft,
|
|
2443
|
+
[result.key]: clampBackendNumber(option, numericCurrent + option.step * result.direction),
|
|
2444
|
+
};
|
|
2445
|
+
focusKey = result.key;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async function promptBackendSettings(initial) {
|
|
2449
|
+
if (!input.isTTY || !output.isTTY)
|
|
2450
|
+
return null;
|
|
2451
|
+
const ui = getUiRuntimeOptions();
|
|
2452
|
+
let draft = cloneBackendPluginConfig(initial);
|
|
2453
|
+
let activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? "session-sync";
|
|
2454
|
+
const focusByCategory = {};
|
|
2455
|
+
for (const category of BACKEND_CATEGORY_OPTIONS) {
|
|
2456
|
+
focusByCategory[category.key] = getBackendCategoryInitialFocus(category);
|
|
2457
|
+
}
|
|
2458
|
+
while (true) {
|
|
2459
|
+
const previewFocus = focusByCategory[activeCategory] ?? null;
|
|
2460
|
+
const preview = buildBackendSettingsPreview(draft, ui, previewFocus);
|
|
2461
|
+
const categoryItems = BACKEND_CATEGORY_OPTIONS.map((category, index) => {
|
|
2462
|
+
return {
|
|
2463
|
+
label: `${index + 1}. ${category.label}`,
|
|
2464
|
+
hint: category.description,
|
|
2465
|
+
value: { type: "open-category", key: category.key },
|
|
2466
|
+
color: "green",
|
|
2467
|
+
};
|
|
2468
|
+
});
|
|
2469
|
+
const items = [
|
|
2470
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
2471
|
+
{
|
|
2472
|
+
label: preview.label,
|
|
2473
|
+
hint: preview.hint,
|
|
2474
|
+
value: { type: "cancel" },
|
|
2475
|
+
disabled: true,
|
|
2476
|
+
color: "green",
|
|
2477
|
+
hideUnavailableSuffix: true,
|
|
2478
|
+
},
|
|
2479
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2480
|
+
{ label: UI_COPY.settings.backendCategoriesHeading, value: { type: "cancel" }, kind: "heading" },
|
|
2481
|
+
...categoryItems,
|
|
2482
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
2483
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
2484
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
2485
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
2486
|
+
];
|
|
2487
|
+
const initialCursor = items.findIndex((item) => {
|
|
2488
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
2489
|
+
return false;
|
|
2490
|
+
return item.value.type === "open-category" && item.value.key === activeCategory;
|
|
2491
|
+
});
|
|
2492
|
+
const result = await select(items, {
|
|
2493
|
+
message: UI_COPY.settings.backendTitle,
|
|
2494
|
+
subtitle: UI_COPY.settings.backendSubtitle,
|
|
2495
|
+
help: UI_COPY.settings.backendHelp,
|
|
2496
|
+
clearScreen: true,
|
|
2497
|
+
theme: ui.theme,
|
|
2498
|
+
selectedEmphasis: "minimal",
|
|
2499
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
2500
|
+
onCursorChange: ({ cursor }) => {
|
|
2501
|
+
const focusedItem = items[cursor];
|
|
2502
|
+
if (focusedItem?.value.type === "open-category") {
|
|
2503
|
+
activeCategory = focusedItem.value.key;
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2506
|
+
onInput: (raw) => {
|
|
2507
|
+
const lower = raw.toLowerCase();
|
|
2508
|
+
if (lower === "q")
|
|
2509
|
+
return { type: "save" };
|
|
2510
|
+
if (lower === "s")
|
|
2511
|
+
return { type: "save" };
|
|
2512
|
+
if (lower === "r")
|
|
2513
|
+
return { type: "reset" };
|
|
2514
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2515
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= BACKEND_CATEGORY_OPTIONS.length) {
|
|
2516
|
+
const target = BACKEND_CATEGORY_OPTIONS[parsed - 1];
|
|
2517
|
+
if (target)
|
|
2518
|
+
return { type: "open-category", key: target.key };
|
|
2519
|
+
}
|
|
2520
|
+
return undefined;
|
|
2521
|
+
},
|
|
2522
|
+
});
|
|
2523
|
+
if (!result || result.type === "cancel")
|
|
2524
|
+
return null;
|
|
2525
|
+
if (result.type === "save")
|
|
2526
|
+
return draft;
|
|
2527
|
+
if (result.type === "reset") {
|
|
2528
|
+
draft = cloneBackendPluginConfig(BACKEND_DEFAULTS);
|
|
2529
|
+
for (const category of BACKEND_CATEGORY_OPTIONS) {
|
|
2530
|
+
focusByCategory[category.key] = getBackendCategoryInitialFocus(category);
|
|
2531
|
+
}
|
|
2532
|
+
activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? activeCategory;
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
const category = getBackendCategory(result.key);
|
|
2536
|
+
if (!category)
|
|
2537
|
+
continue;
|
|
2538
|
+
activeCategory = category.key;
|
|
2539
|
+
const categoryResult = await promptBackendCategorySettings(draft, category, focusByCategory[category.key] ?? getBackendCategoryInitialFocus(category));
|
|
2540
|
+
draft = categoryResult.draft;
|
|
2541
|
+
focusByCategory[category.key] = categoryResult.focusKey;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
async function configureBackendSettings(currentConfig) {
|
|
2545
|
+
const current = cloneBackendPluginConfig(currentConfig ?? loadPluginConfig());
|
|
2546
|
+
if (!input.isTTY || !output.isTTY) {
|
|
2547
|
+
console.log("Settings require interactive mode.");
|
|
2548
|
+
return current;
|
|
2549
|
+
}
|
|
2550
|
+
const selected = await promptBackendSettings(current);
|
|
2551
|
+
if (!selected)
|
|
2552
|
+
return current;
|
|
2553
|
+
if (backendSettingsEqual(current, selected))
|
|
2554
|
+
return current;
|
|
2555
|
+
await savePluginConfig(buildBackendConfigPatch(selected));
|
|
2556
|
+
return selected;
|
|
2557
|
+
}
|
|
2558
|
+
async function promptSettingsHub(initialFocus = "account-list") {
|
|
2559
|
+
if (!input.isTTY || !output.isTTY)
|
|
2560
|
+
return null;
|
|
2561
|
+
const ui = getUiRuntimeOptions();
|
|
2562
|
+
const items = [
|
|
2563
|
+
{ label: UI_COPY.settings.sectionTitle, value: { type: "back" }, kind: "heading" },
|
|
2564
|
+
{ label: UI_COPY.settings.accountList, value: { type: "account-list" }, color: "green" },
|
|
2565
|
+
{ label: UI_COPY.settings.summaryFields, value: { type: "summary-fields" }, color: "green" },
|
|
2566
|
+
{ label: UI_COPY.settings.behavior, value: { type: "behavior" }, color: "green" },
|
|
2567
|
+
{ label: UI_COPY.settings.theme, value: { type: "theme" }, color: "green" },
|
|
2568
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
2569
|
+
{ label: UI_COPY.settings.advancedTitle, value: { type: "back" }, kind: "heading" },
|
|
2570
|
+
{ label: UI_COPY.settings.backend, value: { type: "backend" }, color: "green" },
|
|
2571
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
2572
|
+
{ label: UI_COPY.settings.exitTitle, value: { type: "back" }, kind: "heading" },
|
|
2573
|
+
{ label: UI_COPY.settings.back, value: { type: "back" }, color: "red" },
|
|
2574
|
+
];
|
|
2575
|
+
const initialCursor = items.findIndex((item) => {
|
|
2576
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
2577
|
+
return false;
|
|
2578
|
+
return item.value.type === initialFocus;
|
|
2579
|
+
});
|
|
2580
|
+
return select(items, {
|
|
2581
|
+
message: UI_COPY.settings.title,
|
|
2582
|
+
subtitle: UI_COPY.settings.subtitle,
|
|
2583
|
+
help: UI_COPY.settings.help,
|
|
2584
|
+
clearScreen: true,
|
|
2585
|
+
theme: ui.theme,
|
|
2586
|
+
selectedEmphasis: "minimal",
|
|
2587
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
2588
|
+
onInput: (raw) => {
|
|
2589
|
+
const lower = raw.toLowerCase();
|
|
2590
|
+
if (lower === "q")
|
|
2591
|
+
return { type: "back" };
|
|
2592
|
+
return undefined;
|
|
2593
|
+
},
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
async function configureUnifiedSettings(initialSettings) {
|
|
2597
|
+
let current = cloneDashboardSettings(initialSettings ?? await loadDashboardDisplaySettings());
|
|
2598
|
+
let backendConfig = cloneBackendPluginConfig(loadPluginConfig());
|
|
2599
|
+
applyUiThemeFromDashboardSettings(current);
|
|
2600
|
+
let hubFocus = "account-list";
|
|
2601
|
+
while (true) {
|
|
2602
|
+
const action = await promptSettingsHub(hubFocus);
|
|
2603
|
+
if (!action || action.type === "back") {
|
|
2604
|
+
return current;
|
|
2605
|
+
}
|
|
2606
|
+
hubFocus = action.type;
|
|
2607
|
+
if (action.type === "account-list") {
|
|
2608
|
+
current = await configureDashboardDisplaySettings(current);
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (action.type === "summary-fields") {
|
|
2612
|
+
current = await configureStatuslineSettings(current);
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
if (action.type === "behavior") {
|
|
2616
|
+
const selected = await promptBehaviorSettings(current);
|
|
2617
|
+
if (selected && !dashboardSettingsEqual(current, selected)) {
|
|
2618
|
+
current = selected;
|
|
2619
|
+
await saveDashboardDisplaySettings(current);
|
|
2620
|
+
}
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
if (action.type === "theme") {
|
|
2624
|
+
const selected = await promptThemeSettings(current);
|
|
2625
|
+
if (selected && !dashboardSettingsEqual(current, selected)) {
|
|
2626
|
+
current = selected;
|
|
2627
|
+
await saveDashboardDisplaySettings(current);
|
|
2628
|
+
applyUiThemeFromDashboardSettings(current);
|
|
2629
|
+
}
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
if (action.type === "backend") {
|
|
2633
|
+
backendConfig = await configureBackendSettings(backendConfig);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async function runOAuthFlow(forceNewLogin) {
|
|
2638
|
+
const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
|
|
2639
|
+
const oauthServer = await startLocalOAuthServer({ state });
|
|
2640
|
+
let code = null;
|
|
2641
|
+
try {
|
|
2642
|
+
const signInMode = await promptOAuthSignInMode();
|
|
2643
|
+
if (signInMode === "cancel") {
|
|
2644
|
+
return {
|
|
2645
|
+
type: "failed",
|
|
2646
|
+
reason: "unknown",
|
|
2647
|
+
message: UI_COPY.oauth.cancelled,
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
if (signInMode === "browser") {
|
|
2651
|
+
const opened = openBrowserUrl(url);
|
|
2652
|
+
if (opened) {
|
|
2653
|
+
console.log(stylePromptText(UI_COPY.oauth.browserOpened, "success"));
|
|
2654
|
+
}
|
|
2655
|
+
else {
|
|
2656
|
+
console.log(stylePromptText(UI_COPY.oauth.browserOpenFail, "warning"));
|
|
2657
|
+
console.log(`${stylePromptText(UI_COPY.oauth.goTo, "accent")} ${url}`);
|
|
2658
|
+
const copied = copyTextToClipboard(url);
|
|
2659
|
+
console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
else {
|
|
2663
|
+
console.log(`${stylePromptText(UI_COPY.oauth.goTo, "accent")} ${url}`);
|
|
2664
|
+
const copied = copyTextToClipboard(url);
|
|
2665
|
+
console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
|
|
2666
|
+
}
|
|
2667
|
+
if (oauthServer.ready) {
|
|
2668
|
+
console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
|
|
2669
|
+
const callbackResult = await oauthServer.waitForCode(state);
|
|
2670
|
+
code = callbackResult?.code ?? null;
|
|
2671
|
+
}
|
|
2672
|
+
if (!code) {
|
|
2673
|
+
console.log(stylePromptText(UI_COPY.oauth.callbackMissed, "warning"));
|
|
2674
|
+
code = await promptManualCallback(state);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
finally {
|
|
2678
|
+
oauthServer.close();
|
|
2679
|
+
}
|
|
2680
|
+
if (!code) {
|
|
2681
|
+
return {
|
|
2682
|
+
type: "failed",
|
|
2683
|
+
reason: "unknown",
|
|
2684
|
+
message: UI_COPY.oauth.cancelled,
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
return exchangeAuthorizationCode(code, pkce.verifier, REDIRECT_URI);
|
|
2688
|
+
}
|
|
2689
|
+
async function persistAccountPool(results, replaceAll) {
|
|
2690
|
+
if (results.length === 0)
|
|
2691
|
+
return;
|
|
2692
|
+
const loadedStorage = replaceAll
|
|
2693
|
+
? null
|
|
2694
|
+
: await loadAccounts();
|
|
2695
|
+
const now = Date.now();
|
|
2696
|
+
const accounts = loadedStorage?.accounts ? [...loadedStorage.accounts] : [];
|
|
2697
|
+
const indexByRefreshToken = new Map();
|
|
2698
|
+
const indexByAccountId = new Map();
|
|
2699
|
+
const indexByEmail = new Map();
|
|
2700
|
+
let selectedAccountIndex = null;
|
|
2701
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
2702
|
+
const account = accounts[i];
|
|
2703
|
+
if (!account)
|
|
2704
|
+
continue;
|
|
2705
|
+
if (account.refreshToken)
|
|
2706
|
+
indexByRefreshToken.set(account.refreshToken, i);
|
|
2707
|
+
if (account.accountId)
|
|
2708
|
+
indexByAccountId.set(account.accountId, i);
|
|
2709
|
+
if (account.email)
|
|
2710
|
+
indexByEmail.set(account.email, i);
|
|
2711
|
+
}
|
|
2712
|
+
for (const result of results) {
|
|
2713
|
+
const tokenAccountId = extractAccountId(result.access);
|
|
2714
|
+
const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
|
|
2715
|
+
const accountIdSource = accountId
|
|
2716
|
+
? (result.accountIdSource ?? (result.accountIdOverride ? "manual" : "token"))
|
|
2717
|
+
: undefined;
|
|
2718
|
+
const accountLabel = result.accountLabel;
|
|
2719
|
+
const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
|
|
2720
|
+
const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
|
|
2721
|
+
? indexByEmail.get(accountEmail)
|
|
2722
|
+
: undefined;
|
|
2723
|
+
const existingById = accountId && indexByAccountId.has(accountId)
|
|
2724
|
+
? indexByAccountId.get(accountId)
|
|
2725
|
+
: undefined;
|
|
2726
|
+
const existingByToken = indexByRefreshToken.get(result.refresh);
|
|
2727
|
+
const existingIndex = existingById ?? existingByEmail ?? existingByToken;
|
|
2728
|
+
if (existingIndex === undefined) {
|
|
2729
|
+
const newIndex = accounts.length;
|
|
2730
|
+
accounts.push({
|
|
2731
|
+
accountId,
|
|
2732
|
+
accountIdSource,
|
|
2733
|
+
accountLabel,
|
|
2734
|
+
email: accountEmail,
|
|
2735
|
+
refreshToken: result.refresh,
|
|
2736
|
+
accessToken: result.access,
|
|
2737
|
+
expiresAt: result.expires,
|
|
2738
|
+
enabled: true,
|
|
2739
|
+
addedAt: now,
|
|
2740
|
+
lastUsed: now,
|
|
2741
|
+
});
|
|
2742
|
+
indexByRefreshToken.set(result.refresh, newIndex);
|
|
2743
|
+
if (accountId)
|
|
2744
|
+
indexByAccountId.set(accountId, newIndex);
|
|
2745
|
+
if (accountEmail)
|
|
2746
|
+
indexByEmail.set(accountEmail, newIndex);
|
|
2747
|
+
selectedAccountIndex = newIndex;
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
const existing = accounts[existingIndex];
|
|
2751
|
+
if (!existing)
|
|
2752
|
+
continue;
|
|
2753
|
+
const oldToken = existing.refreshToken;
|
|
2754
|
+
const oldEmail = existing.email;
|
|
2755
|
+
const nextEmail = accountEmail ?? existing.email;
|
|
2756
|
+
const nextAccountId = accountId ?? existing.accountId;
|
|
2757
|
+
const nextAccountIdSource = accountId
|
|
2758
|
+
? (accountIdSource ?? existing.accountIdSource)
|
|
2759
|
+
: existing.accountIdSource;
|
|
2760
|
+
accounts[existingIndex] = {
|
|
2761
|
+
...existing,
|
|
2762
|
+
accountId: nextAccountId,
|
|
2763
|
+
accountIdSource: nextAccountIdSource,
|
|
2764
|
+
accountLabel: accountLabel ?? existing.accountLabel,
|
|
2765
|
+
email: nextEmail,
|
|
2766
|
+
refreshToken: result.refresh,
|
|
2767
|
+
accessToken: result.access,
|
|
2768
|
+
expiresAt: result.expires,
|
|
2769
|
+
enabled: true,
|
|
2770
|
+
lastUsed: now,
|
|
2771
|
+
};
|
|
2772
|
+
if (oldToken !== result.refresh) {
|
|
2773
|
+
indexByRefreshToken.delete(oldToken);
|
|
2774
|
+
indexByRefreshToken.set(result.refresh, existingIndex);
|
|
2775
|
+
}
|
|
2776
|
+
if (nextAccountId) {
|
|
2777
|
+
indexByAccountId.set(nextAccountId, existingIndex);
|
|
2778
|
+
}
|
|
2779
|
+
if (oldEmail && oldEmail !== nextEmail) {
|
|
2780
|
+
indexByEmail.delete(oldEmail);
|
|
2781
|
+
}
|
|
2782
|
+
if (nextEmail) {
|
|
2783
|
+
indexByEmail.set(nextEmail, existingIndex);
|
|
2784
|
+
}
|
|
2785
|
+
selectedAccountIndex = existingIndex;
|
|
2786
|
+
}
|
|
2787
|
+
const fallbackActiveIndex = accounts.length === 0
|
|
2788
|
+
? 0
|
|
2789
|
+
: Math.max(0, Math.min(loadedStorage?.activeIndex ?? 0, accounts.length - 1));
|
|
2790
|
+
const nextActiveIndex = accounts.length === 0
|
|
2791
|
+
? 0
|
|
2792
|
+
: selectedAccountIndex === null
|
|
2793
|
+
? fallbackActiveIndex
|
|
2794
|
+
: Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
|
|
2795
|
+
const activeIndexByFamily = {};
|
|
2796
|
+
for (const family of MODEL_FAMILIES) {
|
|
2797
|
+
activeIndexByFamily[family] = nextActiveIndex;
|
|
2798
|
+
}
|
|
2799
|
+
await saveAccounts({
|
|
2800
|
+
version: 3,
|
|
2801
|
+
accounts,
|
|
2802
|
+
activeIndex: nextActiveIndex,
|
|
2803
|
+
activeIndexByFamily,
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
async function syncSelectionToCodex(tokens) {
|
|
2807
|
+
const tokenAccountId = extractAccountId(tokens.access);
|
|
2808
|
+
const accountId = resolveRequestAccountId(tokens.accountIdOverride, tokens.accountIdSource, tokenAccountId);
|
|
2809
|
+
const email = sanitizeEmail(extractAccountEmail(tokens.access, tokens.idToken));
|
|
2810
|
+
await setCodexCliActiveSelection({
|
|
2811
|
+
accountId,
|
|
2812
|
+
email,
|
|
2813
|
+
accessToken: tokens.access,
|
|
2814
|
+
refreshToken: tokens.refresh,
|
|
2815
|
+
expiresAt: tokens.expires,
|
|
2816
|
+
idToken: tokens.idToken,
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
async function showAccountStatus() {
|
|
2820
|
+
setStoragePath(null);
|
|
2821
|
+
const storage = await loadAccounts();
|
|
2822
|
+
const path = getStoragePath();
|
|
2823
|
+
if (!storage || storage.accounts.length === 0) {
|
|
2824
|
+
console.log("No accounts configured.");
|
|
2825
|
+
console.log(`Storage: ${path}`);
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
const now = Date.now();
|
|
2829
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2830
|
+
console.log(`Accounts (${storage.accounts.length})`);
|
|
2831
|
+
console.log(`Storage: ${path}`);
|
|
2832
|
+
console.log("");
|
|
2833
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
2834
|
+
const account = storage.accounts[i];
|
|
2835
|
+
if (!account)
|
|
2836
|
+
continue;
|
|
2837
|
+
const label = formatAccountLabel(account, i);
|
|
2838
|
+
const markers = [];
|
|
2839
|
+
if (i === activeIndex)
|
|
2840
|
+
markers.push("current");
|
|
2841
|
+
if (account.enabled === false)
|
|
2842
|
+
markers.push("disabled");
|
|
2843
|
+
const rateLimit = formatRateLimitEntry(account, now, "codex");
|
|
2844
|
+
if (rateLimit)
|
|
2845
|
+
markers.push("rate-limited");
|
|
2846
|
+
const cooldown = formatCooldown(account, now);
|
|
2847
|
+
if (cooldown)
|
|
2848
|
+
markers.push(`cooldown:${cooldown}`);
|
|
2849
|
+
const markerLabel = markers.length > 0 ? ` [${markers.join(", ")}]` : "";
|
|
2850
|
+
const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
|
|
2851
|
+
? `used ${formatWaitTime(now - account.lastUsed)} ago`
|
|
2852
|
+
: "never used";
|
|
2853
|
+
console.log(`${i + 1}. ${label}${markerLabel} ${lastUsed}`);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
async function runHealthCheck(options = {}) {
|
|
2857
|
+
const forceRefresh = options.forceRefresh === true;
|
|
2858
|
+
const liveProbe = options.liveProbe === true;
|
|
2859
|
+
const probeModel = options.model?.trim() || "gpt-5-codex";
|
|
2860
|
+
const display = options.display ?? DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
2861
|
+
const quotaCache = liveProbe ? await loadQuotaCache() : null;
|
|
2862
|
+
let quotaCacheChanged = false;
|
|
2863
|
+
setStoragePath(null);
|
|
2864
|
+
const storage = await loadAccounts();
|
|
2865
|
+
if (!storage || storage.accounts.length === 0) {
|
|
2866
|
+
console.log("No accounts configured.");
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
let changed = false;
|
|
2870
|
+
let ok = 0;
|
|
2871
|
+
let failed = 0;
|
|
2872
|
+
let warnings = 0;
|
|
2873
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2874
|
+
let activeAccountRefreshed = false;
|
|
2875
|
+
const now = Date.now();
|
|
2876
|
+
console.log(stylePromptText(forceRefresh
|
|
2877
|
+
? `Checking ${storage.accounts.length} account(s) with full refresh test...`
|
|
2878
|
+
: `Checking ${storage.accounts.length} account(s) with quick check${liveProbe ? " + live check" : ""}...`, "accent"));
|
|
2879
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
2880
|
+
const account = storage.accounts[i];
|
|
2881
|
+
if (!account)
|
|
2882
|
+
continue;
|
|
2883
|
+
const label = formatAccountLabel(account, i);
|
|
2884
|
+
const labelText = stylePromptText(label, "accent");
|
|
2885
|
+
const sessionLikelyValid = hasUsableAccessToken(account, now);
|
|
2886
|
+
if (!forceRefresh && sessionLikelyValid) {
|
|
2887
|
+
if (account.enabled === false) {
|
|
2888
|
+
account.enabled = true;
|
|
2889
|
+
changed = true;
|
|
2890
|
+
}
|
|
2891
|
+
if (i === activeIndex) {
|
|
2892
|
+
activeAccountRefreshed = true;
|
|
2893
|
+
}
|
|
2894
|
+
let healthDetail = "signed in and working";
|
|
2895
|
+
if (liveProbe) {
|
|
2896
|
+
const currentAccessToken = account.accessToken;
|
|
2897
|
+
const probeAccountId = currentAccessToken
|
|
2898
|
+
? (account.accountId ?? extractAccountId(currentAccessToken))
|
|
2899
|
+
: undefined;
|
|
2900
|
+
if (!probeAccountId || !currentAccessToken) {
|
|
2901
|
+
warnings += 1;
|
|
2902
|
+
healthDetail = "signed in and working (live check skipped: missing account ID)";
|
|
2903
|
+
}
|
|
2904
|
+
else {
|
|
2905
|
+
try {
|
|
2906
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
2907
|
+
accountId: probeAccountId,
|
|
2908
|
+
accessToken: currentAccessToken,
|
|
2909
|
+
model: probeModel,
|
|
2910
|
+
});
|
|
2911
|
+
if (quotaCache) {
|
|
2912
|
+
quotaCacheChanged =
|
|
2913
|
+
updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
|
|
2914
|
+
}
|
|
2915
|
+
healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
2916
|
+
}
|
|
2917
|
+
catch (error) {
|
|
2918
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
2919
|
+
warnings += 1;
|
|
2920
|
+
healthDetail = `signed in and working (live check failed: ${message})`;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (hasLikelyInvalidRefreshToken(account.refreshToken)) {
|
|
2925
|
+
healthDetail += " (re-login suggested soon)";
|
|
2926
|
+
}
|
|
2927
|
+
ok += 1;
|
|
2928
|
+
if (display.showPerAccountRows) {
|
|
2929
|
+
console.log(` ${stylePromptText("✓", "success")} ${labelText} ${stylePromptText("|", "muted")} ${styleAccountDetailText(healthDetail)}`);
|
|
2930
|
+
}
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const result = await queuedRefresh(account.refreshToken);
|
|
2934
|
+
if (result.type === "success") {
|
|
2935
|
+
const tokenAccountId = extractAccountId(result.access);
|
|
2936
|
+
const nextEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
|
|
2937
|
+
if (account.refreshToken !== result.refresh) {
|
|
2938
|
+
account.refreshToken = result.refresh;
|
|
2939
|
+
changed = true;
|
|
2940
|
+
}
|
|
2941
|
+
if (account.accessToken !== result.access) {
|
|
2942
|
+
account.accessToken = result.access;
|
|
2943
|
+
changed = true;
|
|
2944
|
+
}
|
|
2945
|
+
if (account.expiresAt !== result.expires) {
|
|
2946
|
+
account.expiresAt = result.expires;
|
|
2947
|
+
changed = true;
|
|
2948
|
+
}
|
|
2949
|
+
if (nextEmail && nextEmail !== account.email) {
|
|
2950
|
+
account.email = nextEmail;
|
|
2951
|
+
changed = true;
|
|
2952
|
+
}
|
|
2953
|
+
if (tokenAccountId && tokenAccountId !== account.accountId) {
|
|
2954
|
+
account.accountId = tokenAccountId;
|
|
2955
|
+
account.accountIdSource = "token";
|
|
2956
|
+
changed = true;
|
|
2957
|
+
}
|
|
2958
|
+
if (account.enabled === false) {
|
|
2959
|
+
account.enabled = true;
|
|
2960
|
+
changed = true;
|
|
2961
|
+
}
|
|
2962
|
+
account.lastUsed = Date.now();
|
|
2963
|
+
if (i === activeIndex) {
|
|
2964
|
+
activeAccountRefreshed = true;
|
|
2965
|
+
}
|
|
2966
|
+
ok += 1;
|
|
2967
|
+
let healthyMessage = "working now";
|
|
2968
|
+
if (liveProbe) {
|
|
2969
|
+
const probeAccountId = account.accountId ?? tokenAccountId;
|
|
2970
|
+
if (!probeAccountId) {
|
|
2971
|
+
warnings += 1;
|
|
2972
|
+
healthyMessage = "working now (live check skipped: missing account ID)";
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
try {
|
|
2976
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
2977
|
+
accountId: probeAccountId,
|
|
2978
|
+
accessToken: result.access,
|
|
2979
|
+
model: probeModel,
|
|
2980
|
+
});
|
|
2981
|
+
if (quotaCache) {
|
|
2982
|
+
quotaCacheChanged =
|
|
2983
|
+
updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
|
|
2984
|
+
}
|
|
2985
|
+
healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
2986
|
+
}
|
|
2987
|
+
catch (error) {
|
|
2988
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
2989
|
+
warnings += 1;
|
|
2990
|
+
healthyMessage = `working now (live check failed: ${message})`;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
if (display.showPerAccountRows) {
|
|
2995
|
+
console.log(` ${stylePromptText("✓", "success")} ${labelText} ${stylePromptText("|", "muted")} ${styleAccountDetailText(healthyMessage)}`);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
else {
|
|
2999
|
+
const detail = normalizeFailureDetail(result.message, result.reason);
|
|
3000
|
+
if (sessionLikelyValid) {
|
|
3001
|
+
warnings += 1;
|
|
3002
|
+
if (display.showPerAccountRows) {
|
|
3003
|
+
console.log(` ${stylePromptText("!", "warning")} ${labelText} ${stylePromptText("|", "muted")} ${stylePromptText(`refresh failed (${detail}) but this account still works right now`, "warning")}`);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
else {
|
|
3007
|
+
failed += 1;
|
|
3008
|
+
if (display.showPerAccountRows) {
|
|
3009
|
+
console.log(` ${stylePromptText("✗", "danger")} ${labelText} ${stylePromptText("|", "muted")} ${stylePromptText(detail, "danger")}`);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (!display.showPerAccountRows) {
|
|
3015
|
+
console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
|
|
3016
|
+
}
|
|
3017
|
+
if (quotaCache && quotaCacheChanged) {
|
|
3018
|
+
await saveQuotaCache(quotaCache);
|
|
3019
|
+
}
|
|
3020
|
+
if (changed) {
|
|
3021
|
+
await saveAccounts(storage);
|
|
3022
|
+
}
|
|
3023
|
+
if (activeAccountRefreshed && activeIndex >= 0 && activeIndex < storage.accounts.length) {
|
|
3024
|
+
const activeAccount = storage.accounts[activeIndex];
|
|
3025
|
+
if (activeAccount) {
|
|
3026
|
+
await setCodexCliActiveSelection({
|
|
3027
|
+
accountId: activeAccount.accountId,
|
|
3028
|
+
email: activeAccount.email,
|
|
3029
|
+
accessToken: activeAccount.accessToken,
|
|
3030
|
+
refreshToken: activeAccount.refreshToken,
|
|
3031
|
+
expiresAt: activeAccount.expiresAt,
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
console.log("");
|
|
3036
|
+
console.log(formatResultSummary([
|
|
3037
|
+
{ text: `${ok} working`, tone: "success" },
|
|
3038
|
+
{ text: `${failed} need re-login`, tone: failed > 0 ? "danger" : "muted" },
|
|
3039
|
+
{ text: `${warnings} warning${warnings === 1 ? "" : "s"}`, tone: warnings > 0 ? "warning" : "muted" },
|
|
3040
|
+
]));
|
|
3041
|
+
}
|
|
3042
|
+
function printForecastUsage() {
|
|
3043
|
+
console.log([
|
|
3044
|
+
"Usage:",
|
|
3045
|
+
" codex auth forecast [--live] [--json] [--model <model>]",
|
|
3046
|
+
"",
|
|
3047
|
+
"Options:",
|
|
3048
|
+
" --live, -l Probe live quota headers via Codex backend",
|
|
3049
|
+
" --json, -j Print machine-readable JSON output",
|
|
3050
|
+
" --model, -m Probe model for live mode (default: gpt-5-codex)",
|
|
3051
|
+
].join("\n"));
|
|
3052
|
+
}
|
|
3053
|
+
function printFixUsage() {
|
|
3054
|
+
console.log([
|
|
3055
|
+
"Usage:",
|
|
3056
|
+
" codex auth fix [--dry-run] [--json] [--live] [--model <model>]",
|
|
3057
|
+
"",
|
|
3058
|
+
"Options:",
|
|
3059
|
+
" --dry-run, -n Preview changes without writing storage",
|
|
3060
|
+
" --json, -j Print machine-readable JSON output",
|
|
3061
|
+
" --live, -l Run live session probe before deciding health",
|
|
3062
|
+
" --model, -m Probe model for live mode (default: gpt-5-codex)",
|
|
3063
|
+
"",
|
|
3064
|
+
"Behavior:",
|
|
3065
|
+
" - Refreshes tokens for enabled accounts",
|
|
3066
|
+
" - Disables hard-failed accounts (never deletes)",
|
|
3067
|
+
" - Recommends a better current account when needed",
|
|
3068
|
+
].join("\n"));
|
|
3069
|
+
}
|
|
3070
|
+
function printVerifyFlaggedUsage() {
|
|
3071
|
+
console.log([
|
|
3072
|
+
"Usage:",
|
|
3073
|
+
" codex auth verify-flagged [--dry-run] [--json] [--no-restore]",
|
|
3074
|
+
"",
|
|
3075
|
+
"Options:",
|
|
3076
|
+
" --dry-run, -n Preview changes without writing storage",
|
|
3077
|
+
" --json, -j Print machine-readable JSON output",
|
|
3078
|
+
" --no-restore Check flagged accounts without restoring healthy ones",
|
|
3079
|
+
"",
|
|
3080
|
+
"Behavior:",
|
|
3081
|
+
" - Refresh-checks accounts from flagged storage",
|
|
3082
|
+
" - Restores healthy accounts back to active storage by default",
|
|
3083
|
+
].join("\n"));
|
|
3084
|
+
}
|
|
3085
|
+
function parseForecastArgs(args) {
|
|
3086
|
+
const options = {
|
|
3087
|
+
live: false,
|
|
3088
|
+
json: false,
|
|
3089
|
+
model: "gpt-5-codex",
|
|
3090
|
+
};
|
|
3091
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3092
|
+
const arg = args[i];
|
|
3093
|
+
if (!arg)
|
|
3094
|
+
continue;
|
|
3095
|
+
if (!arg)
|
|
3096
|
+
continue;
|
|
3097
|
+
if (arg === "--live" || arg === "-l") {
|
|
3098
|
+
options.live = true;
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
if (arg === "--json" || arg === "-j") {
|
|
3102
|
+
options.json = true;
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
if (arg === "--model" || arg === "-m") {
|
|
3106
|
+
const value = args[i + 1];
|
|
3107
|
+
if (!value) {
|
|
3108
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3109
|
+
}
|
|
3110
|
+
options.model = value;
|
|
3111
|
+
i += 1;
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
if (arg.startsWith("--model=")) {
|
|
3115
|
+
const value = arg.slice("--model=".length).trim();
|
|
3116
|
+
if (!value) {
|
|
3117
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3118
|
+
}
|
|
3119
|
+
options.model = value;
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
return { ok: false, message: `Unknown option: ${arg}` };
|
|
3123
|
+
}
|
|
3124
|
+
return { ok: true, options };
|
|
3125
|
+
}
|
|
3126
|
+
function parseFixArgs(args) {
|
|
3127
|
+
const options = {
|
|
3128
|
+
dryRun: false,
|
|
3129
|
+
json: false,
|
|
3130
|
+
live: false,
|
|
3131
|
+
model: "gpt-5-codex",
|
|
3132
|
+
};
|
|
3133
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3134
|
+
const argValue = args[i];
|
|
3135
|
+
if (typeof argValue !== "string")
|
|
3136
|
+
continue;
|
|
3137
|
+
if (argValue === "--dry-run" || argValue === "-n") {
|
|
3138
|
+
options.dryRun = true;
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
if (argValue === "--json" || argValue === "-j") {
|
|
3142
|
+
options.json = true;
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (argValue === "--live" || argValue === "-l") {
|
|
3146
|
+
options.live = true;
|
|
3147
|
+
continue;
|
|
3148
|
+
}
|
|
3149
|
+
if (argValue === "--model" || argValue === "-m") {
|
|
3150
|
+
const value = args[i + 1];
|
|
3151
|
+
if (!value) {
|
|
3152
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3153
|
+
}
|
|
3154
|
+
options.model = value;
|
|
3155
|
+
i += 1;
|
|
3156
|
+
continue;
|
|
3157
|
+
}
|
|
3158
|
+
if (argValue.startsWith("--model=")) {
|
|
3159
|
+
const value = argValue.slice("--model=".length).trim();
|
|
3160
|
+
if (!value) {
|
|
3161
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3162
|
+
}
|
|
3163
|
+
options.model = value;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
return { ok: false, message: `Unknown option: ${argValue}` };
|
|
3167
|
+
}
|
|
3168
|
+
return { ok: true, options };
|
|
3169
|
+
}
|
|
3170
|
+
function parseVerifyFlaggedArgs(args) {
|
|
3171
|
+
const options = {
|
|
3172
|
+
dryRun: false,
|
|
3173
|
+
json: false,
|
|
3174
|
+
restore: true,
|
|
3175
|
+
};
|
|
3176
|
+
for (const arg of args) {
|
|
3177
|
+
if (arg === "--dry-run" || arg === "-n") {
|
|
3178
|
+
options.dryRun = true;
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
if (arg === "--json" || arg === "-j") {
|
|
3182
|
+
options.json = true;
|
|
3183
|
+
continue;
|
|
3184
|
+
}
|
|
3185
|
+
if (arg === "--no-restore") {
|
|
3186
|
+
options.restore = false;
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
return { ok: false, message: `Unknown option: ${arg}` };
|
|
3190
|
+
}
|
|
3191
|
+
return { ok: true, options };
|
|
3192
|
+
}
|
|
3193
|
+
function printDoctorUsage() {
|
|
3194
|
+
console.log([
|
|
3195
|
+
"Usage:",
|
|
3196
|
+
" codex auth doctor [--json] [--fix] [--dry-run]",
|
|
3197
|
+
"",
|
|
3198
|
+
"Options:",
|
|
3199
|
+
" --json, -j Print machine-readable JSON diagnostics",
|
|
3200
|
+
" --fix Apply safe auto-fixes to storage",
|
|
3201
|
+
" --dry-run, -n Preview --fix changes without writing storage",
|
|
3202
|
+
"",
|
|
3203
|
+
"Behavior:",
|
|
3204
|
+
" - Validates account storage readability",
|
|
3205
|
+
" - Checks active index consistency and account duplication",
|
|
3206
|
+
" - Flags placeholder/demo accounts and disabled-all scenarios",
|
|
3207
|
+
].join("\n"));
|
|
3208
|
+
}
|
|
3209
|
+
function parseDoctorArgs(args) {
|
|
3210
|
+
const options = { json: false, fix: false, dryRun: false };
|
|
3211
|
+
for (const arg of args) {
|
|
3212
|
+
if (arg === "--json" || arg === "-j") {
|
|
3213
|
+
options.json = true;
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
if (arg === "--fix") {
|
|
3217
|
+
options.fix = true;
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (arg === "--dry-run" || arg === "-n") {
|
|
3221
|
+
options.dryRun = true;
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
return { ok: false, message: `Unknown option: ${arg}` };
|
|
3225
|
+
}
|
|
3226
|
+
if (options.dryRun && !options.fix) {
|
|
3227
|
+
return { ok: false, message: "--dry-run requires --fix" };
|
|
3228
|
+
}
|
|
3229
|
+
return { ok: true, options };
|
|
3230
|
+
}
|
|
3231
|
+
function printReportUsage() {
|
|
3232
|
+
console.log([
|
|
3233
|
+
"Usage:",
|
|
3234
|
+
" codex auth report [--live] [--json] [--model <model>] [--out <path>]",
|
|
3235
|
+
"",
|
|
3236
|
+
"Options:",
|
|
3237
|
+
" --live, -l Probe live quota headers via Codex backend",
|
|
3238
|
+
" --json, -j Print machine-readable JSON output",
|
|
3239
|
+
" --model, -m Probe model for live mode (default: gpt-5-codex)",
|
|
3240
|
+
" --out Write JSON report to a file path",
|
|
3241
|
+
].join("\n"));
|
|
3242
|
+
}
|
|
3243
|
+
function parseReportArgs(args) {
|
|
3244
|
+
const options = {
|
|
3245
|
+
live: false,
|
|
3246
|
+
json: false,
|
|
3247
|
+
model: "gpt-5-codex",
|
|
3248
|
+
};
|
|
3249
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3250
|
+
const arg = args[i];
|
|
3251
|
+
if (!arg)
|
|
3252
|
+
continue;
|
|
3253
|
+
if (arg === "--live" || arg === "-l") {
|
|
3254
|
+
options.live = true;
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
if (arg === "--json" || arg === "-j") {
|
|
3258
|
+
options.json = true;
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
if (arg === "--model" || arg === "-m") {
|
|
3262
|
+
const value = args[i + 1];
|
|
3263
|
+
if (!value) {
|
|
3264
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3265
|
+
}
|
|
3266
|
+
options.model = value;
|
|
3267
|
+
i += 1;
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
if (arg.startsWith("--model=")) {
|
|
3271
|
+
const value = arg.slice("--model=".length).trim();
|
|
3272
|
+
if (!value) {
|
|
3273
|
+
return { ok: false, message: "Missing value for --model" };
|
|
3274
|
+
}
|
|
3275
|
+
options.model = value;
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
if (arg === "--out") {
|
|
3279
|
+
const value = args[i + 1];
|
|
3280
|
+
if (!value) {
|
|
3281
|
+
return { ok: false, message: "Missing value for --out" };
|
|
3282
|
+
}
|
|
3283
|
+
options.outPath = value;
|
|
3284
|
+
i += 1;
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
if (arg.startsWith("--out=")) {
|
|
3288
|
+
const value = arg.slice("--out=".length).trim();
|
|
3289
|
+
if (!value) {
|
|
3290
|
+
return { ok: false, message: "Missing value for --out" };
|
|
3291
|
+
}
|
|
3292
|
+
options.outPath = value;
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
return { ok: false, message: `Unknown option: ${arg}` };
|
|
3296
|
+
}
|
|
3297
|
+
return { ok: true, options };
|
|
3298
|
+
}
|
|
3299
|
+
function serializeForecastResults(results, liveQuotaByIndex, refreshFailures) {
|
|
3300
|
+
return results.map((result) => {
|
|
3301
|
+
const liveQuota = liveQuotaByIndex.get(result.index);
|
|
3302
|
+
return {
|
|
3303
|
+
index: result.index,
|
|
3304
|
+
label: result.label,
|
|
3305
|
+
isCurrent: result.isCurrent,
|
|
3306
|
+
availability: result.availability,
|
|
3307
|
+
riskScore: result.riskScore,
|
|
3308
|
+
riskLevel: result.riskLevel,
|
|
3309
|
+
waitMs: result.waitMs,
|
|
3310
|
+
reasons: result.reasons,
|
|
3311
|
+
liveQuota: liveQuota
|
|
3312
|
+
? {
|
|
3313
|
+
status: liveQuota.status,
|
|
3314
|
+
planType: liveQuota.planType,
|
|
3315
|
+
activeLimit: liveQuota.activeLimit,
|
|
3316
|
+
model: liveQuota.model,
|
|
3317
|
+
summary: formatQuotaSnapshotLine(liveQuota),
|
|
3318
|
+
}
|
|
3319
|
+
: undefined,
|
|
3320
|
+
refreshFailure: refreshFailures.get(result.index),
|
|
3321
|
+
};
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
async function runForecast(args) {
|
|
3325
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3326
|
+
printForecastUsage();
|
|
3327
|
+
return 0;
|
|
3328
|
+
}
|
|
3329
|
+
const parsedArgs = parseForecastArgs(args);
|
|
3330
|
+
if (!parsedArgs.ok) {
|
|
3331
|
+
console.error(parsedArgs.message);
|
|
3332
|
+
printForecastUsage();
|
|
3333
|
+
return 1;
|
|
3334
|
+
}
|
|
3335
|
+
const options = parsedArgs.options;
|
|
3336
|
+
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
3337
|
+
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
3338
|
+
let quotaCacheChanged = false;
|
|
3339
|
+
setStoragePath(null);
|
|
3340
|
+
const storage = await loadAccounts();
|
|
3341
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3342
|
+
console.log("No accounts configured.");
|
|
3343
|
+
return 0;
|
|
3344
|
+
}
|
|
3345
|
+
const now = Date.now();
|
|
3346
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
3347
|
+
const refreshFailures = new Map();
|
|
3348
|
+
const liveQuotaByIndex = new Map();
|
|
3349
|
+
const probeErrors = [];
|
|
3350
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
3351
|
+
const account = storage.accounts[i];
|
|
3352
|
+
if (!account || !options.live)
|
|
3353
|
+
continue;
|
|
3354
|
+
if (account.enabled === false)
|
|
3355
|
+
continue;
|
|
3356
|
+
let probeAccessToken = account.accessToken;
|
|
3357
|
+
let probeAccountId = account.accountId ?? extractAccountId(account.accessToken);
|
|
3358
|
+
if (!hasUsableAccessToken(account, now)) {
|
|
3359
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
3360
|
+
if (refreshResult.type !== "success") {
|
|
3361
|
+
refreshFailures.set(i, {
|
|
3362
|
+
...refreshResult,
|
|
3363
|
+
message: normalizeFailureDetail(refreshResult.message, refreshResult.reason),
|
|
3364
|
+
});
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
probeAccessToken = refreshResult.access;
|
|
3368
|
+
probeAccountId = account.accountId ?? extractAccountId(refreshResult.access);
|
|
3369
|
+
}
|
|
3370
|
+
if (!probeAccessToken || !probeAccountId) {
|
|
3371
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: missing accountId for live probe`);
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
try {
|
|
3375
|
+
const liveQuota = await fetchCodexQuotaSnapshot({
|
|
3376
|
+
accountId: probeAccountId,
|
|
3377
|
+
accessToken: probeAccessToken,
|
|
3378
|
+
model: options.model,
|
|
3379
|
+
});
|
|
3380
|
+
liveQuotaByIndex.set(i, liveQuota);
|
|
3381
|
+
if (quotaCache) {
|
|
3382
|
+
const account = storage.accounts[i];
|
|
3383
|
+
if (account) {
|
|
3384
|
+
quotaCacheChanged =
|
|
3385
|
+
updateQuotaCacheForAccount(quotaCache, account, liveQuota) || quotaCacheChanged;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
catch (error) {
|
|
3390
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
3391
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: ${message}`);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
const forecastInputs = storage.accounts.map((account, index) => ({
|
|
3395
|
+
index,
|
|
3396
|
+
account,
|
|
3397
|
+
isCurrent: index === activeIndex,
|
|
3398
|
+
now,
|
|
3399
|
+
refreshFailure: refreshFailures.get(index),
|
|
3400
|
+
liveQuota: liveQuotaByIndex.get(index),
|
|
3401
|
+
}));
|
|
3402
|
+
const forecastResults = evaluateForecastAccounts(forecastInputs);
|
|
3403
|
+
const summary = summarizeForecast(forecastResults);
|
|
3404
|
+
const recommendation = recommendForecastAccount(forecastResults);
|
|
3405
|
+
if (options.json) {
|
|
3406
|
+
if (quotaCache && quotaCacheChanged) {
|
|
3407
|
+
await saveQuotaCache(quotaCache);
|
|
3408
|
+
}
|
|
3409
|
+
console.log(JSON.stringify({
|
|
3410
|
+
command: "forecast",
|
|
3411
|
+
model: options.model,
|
|
3412
|
+
liveProbe: options.live,
|
|
3413
|
+
summary,
|
|
3414
|
+
recommendation,
|
|
3415
|
+
probeErrors,
|
|
3416
|
+
accounts: serializeForecastResults(forecastResults, liveQuotaByIndex, refreshFailures),
|
|
3417
|
+
}, null, 2));
|
|
3418
|
+
return 0;
|
|
3419
|
+
}
|
|
3420
|
+
console.log(stylePromptText(`Best-account preview (${storage.accounts.length} account(s), model ${options.model}, live check ${options.live ? "on" : "off"})`, "accent"));
|
|
3421
|
+
console.log(formatResultSummary([
|
|
3422
|
+
{ text: `${summary.ready} ready now`, tone: "success" },
|
|
3423
|
+
{ text: `${summary.delayed} waiting`, tone: "warning" },
|
|
3424
|
+
{ text: `${summary.unavailable} unavailable`, tone: summary.unavailable > 0 ? "danger" : "muted" },
|
|
3425
|
+
{ text: `${summary.highRisk} high risk`, tone: summary.highRisk > 0 ? "danger" : "muted" },
|
|
3426
|
+
]));
|
|
3427
|
+
console.log("");
|
|
3428
|
+
for (const result of forecastResults) {
|
|
3429
|
+
if (!display.showPerAccountRows) {
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
const currentTag = result.isCurrent ? " [current]" : "";
|
|
3433
|
+
const waitLabel = result.waitMs > 0 ? stylePromptText(`wait ${formatWaitTime(result.waitMs)}`, "muted") : "";
|
|
3434
|
+
const indexLabel = stylePromptText(`${result.index + 1}.`, "accent");
|
|
3435
|
+
const accountLabel = stylePromptText(`${result.label}${currentTag}`, "accent");
|
|
3436
|
+
const riskLabel = stylePromptText(`${result.riskLevel} risk (${result.riskScore})`, riskTone(result.riskLevel));
|
|
3437
|
+
const availabilityLabel = stylePromptText(result.availability, availabilityTone(result.availability));
|
|
3438
|
+
const rowParts = [availabilityLabel, riskLabel];
|
|
3439
|
+
if (waitLabel)
|
|
3440
|
+
rowParts.push(waitLabel);
|
|
3441
|
+
console.log(`${indexLabel} ${accountLabel} ${stylePromptText("|", "muted")} ${joinStyledSegments(rowParts)}`);
|
|
3442
|
+
if (display.showForecastReasons && result.reasons.length > 0) {
|
|
3443
|
+
console.log(` ${stylePromptText(result.reasons.slice(0, 3).join("; "), "muted")}`);
|
|
3444
|
+
}
|
|
3445
|
+
const liveQuota = liveQuotaByIndex.get(result.index);
|
|
3446
|
+
if (display.showQuotaDetails && liveQuota) {
|
|
3447
|
+
console.log(` ${stylePromptText("quota:", "accent")} ${styleQuotaSummary(formatCompactQuotaSnapshot(liveQuota))}`);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
if (!display.showPerAccountRows) {
|
|
3451
|
+
console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
|
|
3452
|
+
}
|
|
3453
|
+
if (display.showRecommendations) {
|
|
3454
|
+
console.log("");
|
|
3455
|
+
if (recommendation.recommendedIndex !== null) {
|
|
3456
|
+
const index = recommendation.recommendedIndex;
|
|
3457
|
+
const account = forecastResults.find((result) => result.index === index);
|
|
3458
|
+
if (account) {
|
|
3459
|
+
console.log(`${stylePromptText("Best next account:", "accent")} ${stylePromptText(`${index + 1} (${account.label})`, "success")}`);
|
|
3460
|
+
console.log(`${stylePromptText("Why:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
3461
|
+
if (index !== activeIndex) {
|
|
3462
|
+
console.log(`${stylePromptText("Switch now with:", "accent")} codex auth switch ${index + 1}`);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
else {
|
|
3467
|
+
console.log(`${stylePromptText("Note:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
if (display.showLiveProbeNotes && probeErrors.length > 0) {
|
|
3471
|
+
console.log("");
|
|
3472
|
+
console.log(stylePromptText(`Live check notes (${probeErrors.length}):`, "warning"));
|
|
3473
|
+
for (const error of probeErrors) {
|
|
3474
|
+
console.log(` ${stylePromptText("-", "warning")} ${stylePromptText(error, "muted")}`);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (quotaCache && quotaCacheChanged) {
|
|
3478
|
+
await saveQuotaCache(quotaCache);
|
|
3479
|
+
}
|
|
3480
|
+
return 0;
|
|
3481
|
+
}
|
|
3482
|
+
async function runReport(args) {
|
|
3483
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3484
|
+
printReportUsage();
|
|
3485
|
+
return 0;
|
|
3486
|
+
}
|
|
3487
|
+
const parsedArgs = parseReportArgs(args);
|
|
3488
|
+
if (!parsedArgs.ok) {
|
|
3489
|
+
console.error(parsedArgs.message);
|
|
3490
|
+
printReportUsage();
|
|
3491
|
+
return 1;
|
|
3492
|
+
}
|
|
3493
|
+
const options = parsedArgs.options;
|
|
3494
|
+
setStoragePath(null);
|
|
3495
|
+
const storagePath = getStoragePath();
|
|
3496
|
+
const storage = await loadAccounts();
|
|
3497
|
+
const now = Date.now();
|
|
3498
|
+
const accountCount = storage?.accounts.length ?? 0;
|
|
3499
|
+
const activeIndex = storage ? resolveActiveIndex(storage, "codex") : 0;
|
|
3500
|
+
const refreshFailures = new Map();
|
|
3501
|
+
const liveQuotaByIndex = new Map();
|
|
3502
|
+
const probeErrors = [];
|
|
3503
|
+
if (storage && options.live) {
|
|
3504
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
3505
|
+
const account = storage.accounts[i];
|
|
3506
|
+
if (!account || account.enabled === false)
|
|
3507
|
+
continue;
|
|
3508
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
3509
|
+
if (refreshResult.type !== "success") {
|
|
3510
|
+
refreshFailures.set(i, {
|
|
3511
|
+
...refreshResult,
|
|
3512
|
+
message: normalizeFailureDetail(refreshResult.message, refreshResult.reason),
|
|
3513
|
+
});
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
const accountId = account.accountId ?? extractAccountId(refreshResult.access);
|
|
3517
|
+
if (!accountId) {
|
|
3518
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: missing accountId for live probe`);
|
|
3519
|
+
continue;
|
|
3520
|
+
}
|
|
3521
|
+
try {
|
|
3522
|
+
const liveQuota = await fetchCodexQuotaSnapshot({
|
|
3523
|
+
accountId,
|
|
3524
|
+
accessToken: refreshResult.access,
|
|
3525
|
+
model: options.model,
|
|
3526
|
+
});
|
|
3527
|
+
liveQuotaByIndex.set(i, liveQuota);
|
|
3528
|
+
}
|
|
3529
|
+
catch (error) {
|
|
3530
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
3531
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: ${message}`);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
const forecastResults = storage
|
|
3536
|
+
? evaluateForecastAccounts(storage.accounts.map((account, index) => ({
|
|
3537
|
+
index,
|
|
3538
|
+
account,
|
|
3539
|
+
isCurrent: index === activeIndex,
|
|
3540
|
+
now,
|
|
3541
|
+
refreshFailure: refreshFailures.get(index),
|
|
3542
|
+
liveQuota: liveQuotaByIndex.get(index),
|
|
3543
|
+
})))
|
|
3544
|
+
: [];
|
|
3545
|
+
const forecastSummary = summarizeForecast(forecastResults);
|
|
3546
|
+
const recommendation = recommendForecastAccount(forecastResults);
|
|
3547
|
+
const enabledCount = storage
|
|
3548
|
+
? storage.accounts.filter((account) => account.enabled !== false).length
|
|
3549
|
+
: 0;
|
|
3550
|
+
const disabledCount = Math.max(0, accountCount - enabledCount);
|
|
3551
|
+
const coolingCount = storage
|
|
3552
|
+
? storage.accounts.filter((account) => typeof account.coolingDownUntil === "number" && account.coolingDownUntil > now).length
|
|
3553
|
+
: 0;
|
|
3554
|
+
const rateLimitedCount = storage
|
|
3555
|
+
? storage.accounts.filter((account) => !!formatRateLimitEntry(account, now, "codex")).length
|
|
3556
|
+
: 0;
|
|
3557
|
+
const report = {
|
|
3558
|
+
command: "report",
|
|
3559
|
+
generatedAt: new Date(now).toISOString(),
|
|
3560
|
+
storagePath,
|
|
3561
|
+
model: options.model,
|
|
3562
|
+
liveProbe: options.live,
|
|
3563
|
+
accounts: {
|
|
3564
|
+
total: accountCount,
|
|
3565
|
+
enabled: enabledCount,
|
|
3566
|
+
disabled: disabledCount,
|
|
3567
|
+
coolingDown: coolingCount,
|
|
3568
|
+
rateLimited: rateLimitedCount,
|
|
3569
|
+
},
|
|
3570
|
+
activeIndex: accountCount > 0 ? activeIndex + 1 : null,
|
|
3571
|
+
forecast: {
|
|
3572
|
+
summary: forecastSummary,
|
|
3573
|
+
recommendation,
|
|
3574
|
+
probeErrors,
|
|
3575
|
+
accounts: serializeForecastResults(forecastResults, liveQuotaByIndex, refreshFailures),
|
|
3576
|
+
},
|
|
3577
|
+
};
|
|
3578
|
+
if (options.outPath) {
|
|
3579
|
+
const outputPath = resolve(process.cwd(), options.outPath);
|
|
3580
|
+
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
3581
|
+
await fs.writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
|
|
3582
|
+
}
|
|
3583
|
+
if (options.json) {
|
|
3584
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3585
|
+
return 0;
|
|
3586
|
+
}
|
|
3587
|
+
console.log(`Report generated at ${report.generatedAt}`);
|
|
3588
|
+
console.log(`Storage: ${report.storagePath}`);
|
|
3589
|
+
console.log(`Accounts: ${report.accounts.total} total (${report.accounts.enabled} enabled, ${report.accounts.disabled} disabled, ${report.accounts.coolingDown} cooling, ${report.accounts.rateLimited} rate-limited)`);
|
|
3590
|
+
if (report.activeIndex !== null) {
|
|
3591
|
+
console.log(`Active account: ${report.activeIndex}`);
|
|
3592
|
+
}
|
|
3593
|
+
console.log(`Forecast: ${report.forecast.summary.ready} ready, ${report.forecast.summary.delayed} delayed, ${report.forecast.summary.unavailable} unavailable`);
|
|
3594
|
+
if (report.forecast.recommendation.recommendedIndex !== null) {
|
|
3595
|
+
console.log(`Recommendation: account ${report.forecast.recommendation.recommendedIndex + 1} (${report.forecast.recommendation.reason})`);
|
|
3596
|
+
}
|
|
3597
|
+
else {
|
|
3598
|
+
console.log(`Recommendation: ${report.forecast.recommendation.reason}`);
|
|
3599
|
+
}
|
|
3600
|
+
if (options.outPath) {
|
|
3601
|
+
console.log(`Report written: ${resolve(process.cwd(), options.outPath)}`);
|
|
3602
|
+
}
|
|
3603
|
+
if (report.forecast.probeErrors.length > 0) {
|
|
3604
|
+
console.log(`Probe notes: ${report.forecast.probeErrors.length}`);
|
|
3605
|
+
}
|
|
3606
|
+
return 0;
|
|
3607
|
+
}
|
|
3608
|
+
function summarizeFixReports(reports) {
|
|
3609
|
+
let healthy = 0;
|
|
3610
|
+
let disabled = 0;
|
|
3611
|
+
let warnings = 0;
|
|
3612
|
+
let skipped = 0;
|
|
3613
|
+
for (const report of reports) {
|
|
3614
|
+
if (report.outcome === "healthy")
|
|
3615
|
+
healthy += 1;
|
|
3616
|
+
else if (report.outcome === "disabled-hard-failure")
|
|
3617
|
+
disabled += 1;
|
|
3618
|
+
else if (report.outcome === "warning-soft-failure")
|
|
3619
|
+
warnings += 1;
|
|
3620
|
+
else
|
|
3621
|
+
skipped += 1;
|
|
3622
|
+
}
|
|
3623
|
+
return { healthy, disabled, warnings, skipped };
|
|
3624
|
+
}
|
|
3625
|
+
function createEmptyAccountStorage() {
|
|
3626
|
+
const activeIndexByFamily = {};
|
|
3627
|
+
for (const family of MODEL_FAMILIES) {
|
|
3628
|
+
activeIndexByFamily[family] = 0;
|
|
3629
|
+
}
|
|
3630
|
+
return {
|
|
3631
|
+
version: 3,
|
|
3632
|
+
accounts: [],
|
|
3633
|
+
activeIndex: 0,
|
|
3634
|
+
activeIndexByFamily,
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
function findExistingAccountIndexForFlagged(storage, flagged, nextRefreshToken, nextAccountId, nextEmail) {
|
|
3638
|
+
const flaggedEmail = sanitizeEmail(flagged.email);
|
|
3639
|
+
const candidateAccountId = nextAccountId ?? flagged.accountId;
|
|
3640
|
+
const candidateEmail = sanitizeEmail(nextEmail) ?? flaggedEmail;
|
|
3641
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
3642
|
+
const account = storage.accounts[i];
|
|
3643
|
+
if (!account)
|
|
3644
|
+
continue;
|
|
3645
|
+
if (account.refreshToken === flagged.refreshToken || account.refreshToken === nextRefreshToken) {
|
|
3646
|
+
return i;
|
|
3647
|
+
}
|
|
3648
|
+
if (candidateAccountId &&
|
|
3649
|
+
typeof account.accountId === "string" &&
|
|
3650
|
+
account.accountId === candidateAccountId) {
|
|
3651
|
+
return i;
|
|
3652
|
+
}
|
|
3653
|
+
const existingEmail = sanitizeEmail(account.email);
|
|
3654
|
+
if (candidateEmail && existingEmail && existingEmail === candidateEmail) {
|
|
3655
|
+
return i;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
return -1;
|
|
3659
|
+
}
|
|
3660
|
+
function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
|
|
3661
|
+
const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken)) ?? flagged.email;
|
|
3662
|
+
const nextAccountId = extractAccountId(refreshResult.access) ?? flagged.accountId;
|
|
3663
|
+
const existingIndex = findExistingAccountIndexForFlagged(storage, flagged, refreshResult.refresh, nextAccountId, nextEmail);
|
|
3664
|
+
if (existingIndex >= 0) {
|
|
3665
|
+
const existing = storage.accounts[existingIndex];
|
|
3666
|
+
if (!existing) {
|
|
3667
|
+
return { restored: false, changed: false, message: "existing account entry is missing" };
|
|
3668
|
+
}
|
|
3669
|
+
let changed = false;
|
|
3670
|
+
if (existing.refreshToken !== refreshResult.refresh) {
|
|
3671
|
+
existing.refreshToken = refreshResult.refresh;
|
|
3672
|
+
changed = true;
|
|
3673
|
+
}
|
|
3674
|
+
if (existing.accessToken !== refreshResult.access) {
|
|
3675
|
+
existing.accessToken = refreshResult.access;
|
|
3676
|
+
changed = true;
|
|
3677
|
+
}
|
|
3678
|
+
if (existing.expiresAt !== refreshResult.expires) {
|
|
3679
|
+
existing.expiresAt = refreshResult.expires;
|
|
3680
|
+
changed = true;
|
|
3681
|
+
}
|
|
3682
|
+
if (nextEmail && nextEmail !== existing.email) {
|
|
3683
|
+
existing.email = nextEmail;
|
|
3684
|
+
changed = true;
|
|
3685
|
+
}
|
|
3686
|
+
if (nextAccountId && nextAccountId !== existing.accountId) {
|
|
3687
|
+
existing.accountId = nextAccountId;
|
|
3688
|
+
existing.accountIdSource = "token";
|
|
3689
|
+
changed = true;
|
|
3690
|
+
}
|
|
3691
|
+
if (existing.enabled === false) {
|
|
3692
|
+
existing.enabled = true;
|
|
3693
|
+
changed = true;
|
|
3694
|
+
}
|
|
3695
|
+
if (existing.accountLabel !== flagged.accountLabel && flagged.accountLabel) {
|
|
3696
|
+
existing.accountLabel = flagged.accountLabel;
|
|
3697
|
+
changed = true;
|
|
3698
|
+
}
|
|
3699
|
+
existing.lastUsed = now;
|
|
3700
|
+
return {
|
|
3701
|
+
restored: true,
|
|
3702
|
+
changed,
|
|
3703
|
+
message: `restored into existing account ${existingIndex + 1}`,
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
if (storage.accounts.length >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
3707
|
+
return {
|
|
3708
|
+
restored: false,
|
|
3709
|
+
changed: false,
|
|
3710
|
+
message: `cannot restore (max ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts reached)`,
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
storage.accounts.push({
|
|
3714
|
+
refreshToken: refreshResult.refresh,
|
|
3715
|
+
accessToken: refreshResult.access,
|
|
3716
|
+
expiresAt: refreshResult.expires,
|
|
3717
|
+
accountId: nextAccountId,
|
|
3718
|
+
accountIdSource: nextAccountId ? "token" : flagged.accountIdSource,
|
|
3719
|
+
accountLabel: flagged.accountLabel,
|
|
3720
|
+
email: nextEmail,
|
|
3721
|
+
addedAt: flagged.addedAt ?? now,
|
|
3722
|
+
lastUsed: now,
|
|
3723
|
+
enabled: true,
|
|
3724
|
+
});
|
|
3725
|
+
return {
|
|
3726
|
+
restored: true,
|
|
3727
|
+
changed: true,
|
|
3728
|
+
message: `restored as account ${storage.accounts.length}`,
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
async function runVerifyFlagged(args) {
|
|
3732
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3733
|
+
printVerifyFlaggedUsage();
|
|
3734
|
+
return 0;
|
|
3735
|
+
}
|
|
3736
|
+
const parsedArgs = parseVerifyFlaggedArgs(args);
|
|
3737
|
+
if (!parsedArgs.ok) {
|
|
3738
|
+
console.error(parsedArgs.message);
|
|
3739
|
+
printVerifyFlaggedUsage();
|
|
3740
|
+
return 1;
|
|
3741
|
+
}
|
|
3742
|
+
const options = parsedArgs.options;
|
|
3743
|
+
setStoragePath(null);
|
|
3744
|
+
const flaggedStorage = await loadFlaggedAccounts();
|
|
3745
|
+
if (flaggedStorage.accounts.length === 0) {
|
|
3746
|
+
if (options.json) {
|
|
3747
|
+
console.log(JSON.stringify({
|
|
3748
|
+
command: "verify-flagged",
|
|
3749
|
+
total: 0,
|
|
3750
|
+
restored: 0,
|
|
3751
|
+
healthyFlagged: 0,
|
|
3752
|
+
stillFlagged: 0,
|
|
3753
|
+
changed: false,
|
|
3754
|
+
dryRun: options.dryRun,
|
|
3755
|
+
restore: options.restore,
|
|
3756
|
+
reports: [],
|
|
3757
|
+
}, null, 2));
|
|
3758
|
+
return 0;
|
|
3759
|
+
}
|
|
3760
|
+
console.log("No flagged accounts to check.");
|
|
3761
|
+
return 0;
|
|
3762
|
+
}
|
|
3763
|
+
let storage = await loadAccounts();
|
|
3764
|
+
if (!storage) {
|
|
3765
|
+
storage = createEmptyAccountStorage();
|
|
3766
|
+
}
|
|
3767
|
+
let storageChanged = false;
|
|
3768
|
+
let flaggedChanged = false;
|
|
3769
|
+
const reports = [];
|
|
3770
|
+
const nextFlaggedAccounts = [];
|
|
3771
|
+
const now = Date.now();
|
|
3772
|
+
for (let i = 0; i < flaggedStorage.accounts.length; i += 1) {
|
|
3773
|
+
const flagged = flaggedStorage.accounts[i];
|
|
3774
|
+
if (!flagged)
|
|
3775
|
+
continue;
|
|
3776
|
+
const label = formatAccountLabel(flagged, i);
|
|
3777
|
+
const result = await queuedRefresh(flagged.refreshToken);
|
|
3778
|
+
if (result.type === "success") {
|
|
3779
|
+
if (!options.restore) {
|
|
3780
|
+
const nextFlagged = {
|
|
3781
|
+
...flagged,
|
|
3782
|
+
refreshToken: result.refresh,
|
|
3783
|
+
accessToken: result.access,
|
|
3784
|
+
expiresAt: result.expires,
|
|
3785
|
+
accountId: extractAccountId(result.access) ?? flagged.accountId,
|
|
3786
|
+
accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
|
|
3787
|
+
email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
|
|
3788
|
+
lastUsed: now,
|
|
3789
|
+
lastError: undefined,
|
|
3790
|
+
};
|
|
3791
|
+
nextFlaggedAccounts.push(nextFlagged);
|
|
3792
|
+
if (JSON.stringify(nextFlagged) !== JSON.stringify(flagged)) {
|
|
3793
|
+
flaggedChanged = true;
|
|
3794
|
+
}
|
|
3795
|
+
reports.push({
|
|
3796
|
+
index: i,
|
|
3797
|
+
label,
|
|
3798
|
+
outcome: "healthy-flagged",
|
|
3799
|
+
message: "session is healthy (left in flagged list due to --no-restore)",
|
|
3800
|
+
});
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
const upsertResult = upsertRecoveredFlaggedAccount(storage, flagged, result, now);
|
|
3804
|
+
if (upsertResult.restored) {
|
|
3805
|
+
storageChanged = storageChanged || upsertResult.changed;
|
|
3806
|
+
flaggedChanged = true;
|
|
3807
|
+
reports.push({
|
|
3808
|
+
index: i,
|
|
3809
|
+
label,
|
|
3810
|
+
outcome: "restored",
|
|
3811
|
+
message: upsertResult.message,
|
|
3812
|
+
});
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
const updatedFlagged = {
|
|
3816
|
+
...flagged,
|
|
3817
|
+
refreshToken: result.refresh,
|
|
3818
|
+
accessToken: result.access,
|
|
3819
|
+
expiresAt: result.expires,
|
|
3820
|
+
accountId: extractAccountId(result.access) ?? flagged.accountId,
|
|
3821
|
+
accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
|
|
3822
|
+
email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
|
|
3823
|
+
lastUsed: now,
|
|
3824
|
+
lastError: upsertResult.message,
|
|
3825
|
+
};
|
|
3826
|
+
nextFlaggedAccounts.push(updatedFlagged);
|
|
3827
|
+
if (JSON.stringify(updatedFlagged) !== JSON.stringify(flagged)) {
|
|
3828
|
+
flaggedChanged = true;
|
|
3829
|
+
}
|
|
3830
|
+
reports.push({
|
|
3831
|
+
index: i,
|
|
3832
|
+
label,
|
|
3833
|
+
outcome: "restore-skipped",
|
|
3834
|
+
message: upsertResult.message,
|
|
3835
|
+
});
|
|
3836
|
+
continue;
|
|
3837
|
+
}
|
|
3838
|
+
const detail = normalizeFailureDetail(result.message, result.reason);
|
|
3839
|
+
const failedFlagged = {
|
|
3840
|
+
...flagged,
|
|
3841
|
+
lastError: detail,
|
|
3842
|
+
};
|
|
3843
|
+
nextFlaggedAccounts.push(failedFlagged);
|
|
3844
|
+
if ((flagged.lastError ?? "") !== detail) {
|
|
3845
|
+
flaggedChanged = true;
|
|
3846
|
+
}
|
|
3847
|
+
reports.push({
|
|
3848
|
+
index: i,
|
|
3849
|
+
label,
|
|
3850
|
+
outcome: "still-flagged",
|
|
3851
|
+
message: detail,
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
const remainingFlagged = nextFlaggedAccounts.length;
|
|
3855
|
+
const restored = reports.filter((report) => report.outcome === "restored").length;
|
|
3856
|
+
const healthyFlagged = reports.filter((report) => report.outcome === "healthy-flagged").length;
|
|
3857
|
+
const stillFlagged = reports.filter((report) => report.outcome === "still-flagged").length;
|
|
3858
|
+
const changed = storageChanged || flaggedChanged;
|
|
3859
|
+
if (!options.dryRun) {
|
|
3860
|
+
if (storageChanged) {
|
|
3861
|
+
normalizeDoctorIndexes(storage);
|
|
3862
|
+
await saveAccounts(storage);
|
|
3863
|
+
}
|
|
3864
|
+
if (flaggedChanged) {
|
|
3865
|
+
await saveFlaggedAccounts({
|
|
3866
|
+
version: 1,
|
|
3867
|
+
accounts: nextFlaggedAccounts,
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (options.json) {
|
|
3872
|
+
console.log(JSON.stringify({
|
|
3873
|
+
command: "verify-flagged",
|
|
3874
|
+
total: flaggedStorage.accounts.length,
|
|
3875
|
+
restored,
|
|
3876
|
+
healthyFlagged,
|
|
3877
|
+
stillFlagged,
|
|
3878
|
+
remainingFlagged,
|
|
3879
|
+
changed,
|
|
3880
|
+
dryRun: options.dryRun,
|
|
3881
|
+
restore: options.restore,
|
|
3882
|
+
reports,
|
|
3883
|
+
}, null, 2));
|
|
3884
|
+
return 0;
|
|
3885
|
+
}
|
|
3886
|
+
console.log(stylePromptText(`Checking ${flaggedStorage.accounts.length} flagged account(s)...`, "accent"));
|
|
3887
|
+
for (const report of reports) {
|
|
3888
|
+
const tone = report.outcome === "restored"
|
|
3889
|
+
? "success"
|
|
3890
|
+
: report.outcome === "healthy-flagged"
|
|
3891
|
+
? "warning"
|
|
3892
|
+
: report.outcome === "restore-skipped"
|
|
3893
|
+
? "warning"
|
|
3894
|
+
: "danger";
|
|
3895
|
+
const marker = report.outcome === "restored"
|
|
3896
|
+
? "✓"
|
|
3897
|
+
: report.outcome === "healthy-flagged"
|
|
3898
|
+
? "!"
|
|
3899
|
+
: report.outcome === "restore-skipped"
|
|
3900
|
+
? "!"
|
|
3901
|
+
: "✗";
|
|
3902
|
+
console.log(`${stylePromptText(marker, tone)} ${stylePromptText(`${report.index + 1}. ${report.label}`, "accent")} ${stylePromptText("|", "muted")} ${styleAccountDetailText(report.message, tone)}`);
|
|
3903
|
+
}
|
|
3904
|
+
console.log("");
|
|
3905
|
+
console.log(formatResultSummary([
|
|
3906
|
+
{ text: `${restored} restored`, tone: restored > 0 ? "success" : "muted" },
|
|
3907
|
+
{ text: `${healthyFlagged} healthy (kept flagged)`, tone: healthyFlagged > 0 ? "warning" : "muted" },
|
|
3908
|
+
{ text: `${stillFlagged} still flagged`, tone: stillFlagged > 0 ? "danger" : "muted" },
|
|
3909
|
+
]));
|
|
3910
|
+
if (options.dryRun) {
|
|
3911
|
+
console.log(stylePromptText("Preview only: no changes were saved.", "warning"));
|
|
3912
|
+
}
|
|
3913
|
+
else if (!changed) {
|
|
3914
|
+
console.log(stylePromptText("No storage changes were needed.", "muted"));
|
|
3915
|
+
}
|
|
3916
|
+
return 0;
|
|
3917
|
+
}
|
|
3918
|
+
async function runFix(args) {
|
|
3919
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3920
|
+
printFixUsage();
|
|
3921
|
+
return 0;
|
|
3922
|
+
}
|
|
3923
|
+
const parsedArgs = parseFixArgs(args);
|
|
3924
|
+
if (!parsedArgs.ok) {
|
|
3925
|
+
console.error(parsedArgs.message);
|
|
3926
|
+
printFixUsage();
|
|
3927
|
+
return 1;
|
|
3928
|
+
}
|
|
3929
|
+
const options = parsedArgs.options;
|
|
3930
|
+
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
3931
|
+
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
3932
|
+
let quotaCacheChanged = false;
|
|
3933
|
+
setStoragePath(null);
|
|
3934
|
+
const storage = await loadAccounts();
|
|
3935
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3936
|
+
console.log("No accounts configured.");
|
|
3937
|
+
return 0;
|
|
3938
|
+
}
|
|
3939
|
+
const now = Date.now();
|
|
3940
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
3941
|
+
let changed = false;
|
|
3942
|
+
const reports = [];
|
|
3943
|
+
const refreshFailures = new Map();
|
|
3944
|
+
const hardDisabledIndexes = [];
|
|
3945
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
3946
|
+
const account = storage.accounts[i];
|
|
3947
|
+
if (!account)
|
|
3948
|
+
continue;
|
|
3949
|
+
const label = formatAccountLabel(account, i);
|
|
3950
|
+
if (account.enabled === false) {
|
|
3951
|
+
reports.push({
|
|
3952
|
+
index: i,
|
|
3953
|
+
label,
|
|
3954
|
+
outcome: "already-disabled",
|
|
3955
|
+
message: "already disabled",
|
|
3956
|
+
});
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
if (hasUsableAccessToken(account, now)) {
|
|
3960
|
+
if (options.live) {
|
|
3961
|
+
const currentAccessToken = account.accessToken;
|
|
3962
|
+
const probeAccountId = currentAccessToken
|
|
3963
|
+
? (account.accountId ?? extractAccountId(currentAccessToken))
|
|
3964
|
+
: undefined;
|
|
3965
|
+
if (probeAccountId && currentAccessToken) {
|
|
3966
|
+
try {
|
|
3967
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
3968
|
+
accountId: probeAccountId,
|
|
3969
|
+
accessToken: currentAccessToken,
|
|
3970
|
+
model: options.model,
|
|
3971
|
+
});
|
|
3972
|
+
if (quotaCache) {
|
|
3973
|
+
quotaCacheChanged =
|
|
3974
|
+
updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
|
|
3975
|
+
}
|
|
3976
|
+
reports.push({
|
|
3977
|
+
index: i,
|
|
3978
|
+
label,
|
|
3979
|
+
outcome: "healthy",
|
|
3980
|
+
message: display.showQuotaDetails
|
|
3981
|
+
? `live session OK (${formatCompactQuotaSnapshot(snapshot)})`
|
|
3982
|
+
: "live session OK",
|
|
3983
|
+
});
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
catch (error) {
|
|
3987
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
3988
|
+
reports.push({
|
|
3989
|
+
index: i,
|
|
3990
|
+
label,
|
|
3991
|
+
outcome: "warning-soft-failure",
|
|
3992
|
+
message: `live probe failed (${message}), trying refresh fallback`,
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
const refreshWarning = hasLikelyInvalidRefreshToken(account.refreshToken)
|
|
3998
|
+
? " (refresh token looks stale; re-login recommended)"
|
|
3999
|
+
: "";
|
|
4000
|
+
reports.push({
|
|
4001
|
+
index: i,
|
|
4002
|
+
label,
|
|
4003
|
+
outcome: "healthy",
|
|
4004
|
+
message: `access token still valid${refreshWarning}`,
|
|
4005
|
+
});
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
4009
|
+
if (refreshResult.type === "success") {
|
|
4010
|
+
const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
|
|
4011
|
+
const nextAccountId = extractAccountId(refreshResult.access);
|
|
4012
|
+
let accountChanged = false;
|
|
4013
|
+
if (account.refreshToken !== refreshResult.refresh) {
|
|
4014
|
+
account.refreshToken = refreshResult.refresh;
|
|
4015
|
+
accountChanged = true;
|
|
4016
|
+
}
|
|
4017
|
+
if (account.accessToken !== refreshResult.access) {
|
|
4018
|
+
account.accessToken = refreshResult.access;
|
|
4019
|
+
accountChanged = true;
|
|
4020
|
+
}
|
|
4021
|
+
if (account.expiresAt !== refreshResult.expires) {
|
|
4022
|
+
account.expiresAt = refreshResult.expires;
|
|
4023
|
+
accountChanged = true;
|
|
4024
|
+
}
|
|
4025
|
+
if (nextEmail && nextEmail !== account.email) {
|
|
4026
|
+
account.email = nextEmail;
|
|
4027
|
+
accountChanged = true;
|
|
4028
|
+
}
|
|
4029
|
+
if (!account.accountId && nextAccountId) {
|
|
4030
|
+
account.accountId = nextAccountId;
|
|
4031
|
+
account.accountIdSource = "token";
|
|
4032
|
+
accountChanged = true;
|
|
4033
|
+
}
|
|
4034
|
+
if (accountChanged)
|
|
4035
|
+
changed = true;
|
|
4036
|
+
if (options.live) {
|
|
4037
|
+
const probeAccountId = account.accountId ?? nextAccountId;
|
|
4038
|
+
if (probeAccountId) {
|
|
4039
|
+
try {
|
|
4040
|
+
const snapshot = await fetchCodexQuotaSnapshot({
|
|
4041
|
+
accountId: probeAccountId,
|
|
4042
|
+
accessToken: refreshResult.access,
|
|
4043
|
+
model: options.model,
|
|
4044
|
+
});
|
|
4045
|
+
if (quotaCache) {
|
|
4046
|
+
quotaCacheChanged =
|
|
4047
|
+
updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
|
|
4048
|
+
}
|
|
4049
|
+
reports.push({
|
|
4050
|
+
index: i,
|
|
4051
|
+
label,
|
|
4052
|
+
outcome: "healthy",
|
|
4053
|
+
message: display.showQuotaDetails
|
|
4054
|
+
? `refresh + live probe succeeded (${formatCompactQuotaSnapshot(snapshot)})`
|
|
4055
|
+
: "refresh + live probe succeeded",
|
|
4056
|
+
});
|
|
4057
|
+
continue;
|
|
4058
|
+
}
|
|
4059
|
+
catch (error) {
|
|
4060
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
4061
|
+
reports.push({
|
|
4062
|
+
index: i,
|
|
4063
|
+
label,
|
|
4064
|
+
outcome: "warning-soft-failure",
|
|
4065
|
+
message: `refresh succeeded but live probe failed: ${message}`,
|
|
4066
|
+
});
|
|
4067
|
+
continue;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
reports.push({
|
|
4072
|
+
index: i,
|
|
4073
|
+
label,
|
|
4074
|
+
outcome: "healthy",
|
|
4075
|
+
message: "refresh succeeded",
|
|
4076
|
+
});
|
|
4077
|
+
continue;
|
|
4078
|
+
}
|
|
4079
|
+
const detail = normalizeFailureDetail(refreshResult.message, refreshResult.reason);
|
|
4080
|
+
refreshFailures.set(i, {
|
|
4081
|
+
...refreshResult,
|
|
4082
|
+
message: detail,
|
|
4083
|
+
});
|
|
4084
|
+
if (isHardRefreshFailure(refreshResult)) {
|
|
4085
|
+
account.enabled = false;
|
|
4086
|
+
changed = true;
|
|
4087
|
+
hardDisabledIndexes.push(i);
|
|
4088
|
+
reports.push({
|
|
4089
|
+
index: i,
|
|
4090
|
+
label,
|
|
4091
|
+
outcome: "disabled-hard-failure",
|
|
4092
|
+
message: detail,
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
else {
|
|
4096
|
+
reports.push({
|
|
4097
|
+
index: i,
|
|
4098
|
+
label,
|
|
4099
|
+
outcome: "warning-soft-failure",
|
|
4100
|
+
message: detail,
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
if (hardDisabledIndexes.length > 0) {
|
|
4105
|
+
const enabledCount = storage.accounts.filter((account) => account.enabled !== false).length;
|
|
4106
|
+
if (enabledCount === 0) {
|
|
4107
|
+
const fallbackIndex = hardDisabledIndexes.includes(activeIndex) ? activeIndex : hardDisabledIndexes[0];
|
|
4108
|
+
const fallback = typeof fallbackIndex === "number"
|
|
4109
|
+
? storage.accounts[fallbackIndex]
|
|
4110
|
+
: undefined;
|
|
4111
|
+
if (fallback && fallback.enabled === false) {
|
|
4112
|
+
fallback.enabled = true;
|
|
4113
|
+
changed = true;
|
|
4114
|
+
const existingReport = reports.find((report) => report.index === fallbackIndex &&
|
|
4115
|
+
report.outcome === "disabled-hard-failure");
|
|
4116
|
+
if (existingReport) {
|
|
4117
|
+
existingReport.outcome = "warning-soft-failure";
|
|
4118
|
+
existingReport.message = `${existingReport.message} (kept enabled to avoid lockout; re-login required)`;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
const forecastResults = evaluateForecastAccounts(storage.accounts.map((account, index) => ({
|
|
4124
|
+
index,
|
|
4125
|
+
account,
|
|
4126
|
+
isCurrent: index === activeIndex,
|
|
4127
|
+
now,
|
|
4128
|
+
refreshFailure: refreshFailures.get(index),
|
|
4129
|
+
})));
|
|
4130
|
+
const recommendation = recommendForecastAccount(forecastResults);
|
|
4131
|
+
const reportSummary = summarizeFixReports(reports);
|
|
4132
|
+
if (changed && !options.dryRun) {
|
|
4133
|
+
await saveAccounts(storage);
|
|
4134
|
+
}
|
|
4135
|
+
if (options.json) {
|
|
4136
|
+
if (quotaCache && quotaCacheChanged) {
|
|
4137
|
+
await saveQuotaCache(quotaCache);
|
|
4138
|
+
}
|
|
4139
|
+
console.log(JSON.stringify({
|
|
4140
|
+
command: "fix",
|
|
4141
|
+
dryRun: options.dryRun,
|
|
4142
|
+
liveProbe: options.live,
|
|
4143
|
+
model: options.model,
|
|
4144
|
+
changed,
|
|
4145
|
+
summary: reportSummary,
|
|
4146
|
+
recommendation,
|
|
4147
|
+
recommendedSwitchCommand: recommendation.recommendedIndex !== null &&
|
|
4148
|
+
recommendation.recommendedIndex !== activeIndex
|
|
4149
|
+
? `codex auth switch ${recommendation.recommendedIndex + 1}`
|
|
4150
|
+
: null,
|
|
4151
|
+
reports,
|
|
4152
|
+
}, null, 2));
|
|
4153
|
+
return 0;
|
|
4154
|
+
}
|
|
4155
|
+
console.log(stylePromptText(`Auto-fix scan (${options.dryRun ? "preview" : "apply"})`, "accent"));
|
|
4156
|
+
console.log(formatResultSummary([
|
|
4157
|
+
{ text: `${reportSummary.healthy} working`, tone: "success" },
|
|
4158
|
+
{ text: `${reportSummary.disabled} disabled`, tone: reportSummary.disabled > 0 ? "danger" : "muted" },
|
|
4159
|
+
{
|
|
4160
|
+
text: `${reportSummary.warnings} warning${reportSummary.warnings === 1 ? "" : "s"}`,
|
|
4161
|
+
tone: reportSummary.warnings > 0 ? "warning" : "muted",
|
|
4162
|
+
},
|
|
4163
|
+
{ text: `${reportSummary.skipped} already disabled`, tone: "muted" },
|
|
4164
|
+
]));
|
|
4165
|
+
if (display.showPerAccountRows) {
|
|
4166
|
+
console.log("");
|
|
4167
|
+
for (const report of reports) {
|
|
4168
|
+
const prefix = report.outcome === "healthy"
|
|
4169
|
+
? "✓"
|
|
4170
|
+
: report.outcome === "disabled-hard-failure"
|
|
4171
|
+
? "✗"
|
|
4172
|
+
: report.outcome === "warning-soft-failure"
|
|
4173
|
+
? "!"
|
|
4174
|
+
: "-";
|
|
4175
|
+
const tone = report.outcome === "healthy"
|
|
4176
|
+
? "success"
|
|
4177
|
+
: report.outcome === "disabled-hard-failure"
|
|
4178
|
+
? "danger"
|
|
4179
|
+
: report.outcome === "warning-soft-failure"
|
|
4180
|
+
? "warning"
|
|
4181
|
+
: "muted";
|
|
4182
|
+
console.log(`${stylePromptText(prefix, tone)} ${stylePromptText(`${report.index + 1}. ${report.label}`, "accent")} ${stylePromptText("|", "muted")} ${styleAccountDetailText(report.message, tone === "success" ? "muted" : tone)}`);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
else {
|
|
4186
|
+
console.log("");
|
|
4187
|
+
console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
|
|
4188
|
+
}
|
|
4189
|
+
if (display.showRecommendations) {
|
|
4190
|
+
console.log("");
|
|
4191
|
+
if (recommendation.recommendedIndex !== null) {
|
|
4192
|
+
const target = recommendation.recommendedIndex + 1;
|
|
4193
|
+
console.log(`${stylePromptText("Best next account:", "accent")} ${stylePromptText(String(target), "success")}`);
|
|
4194
|
+
console.log(`${stylePromptText("Why:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
4195
|
+
if (recommendation.recommendedIndex !== activeIndex) {
|
|
4196
|
+
console.log(`${stylePromptText("Switch now with:", "accent")} codex auth switch ${target}`);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
else {
|
|
4200
|
+
console.log(`${stylePromptText("Note:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
if (quotaCache && quotaCacheChanged) {
|
|
4204
|
+
await saveQuotaCache(quotaCache);
|
|
4205
|
+
}
|
|
4206
|
+
if (changed && options.dryRun) {
|
|
4207
|
+
console.log(`\n${stylePromptText("Preview only: no changes were saved.", "warning")}`);
|
|
4208
|
+
}
|
|
4209
|
+
else if (changed) {
|
|
4210
|
+
console.log(`\n${stylePromptText("Saved updates.", "success")}`);
|
|
4211
|
+
}
|
|
4212
|
+
else {
|
|
4213
|
+
console.log(`\n${stylePromptText("No changes were needed.", "muted")}`);
|
|
4214
|
+
}
|
|
4215
|
+
return 0;
|
|
4216
|
+
}
|
|
4217
|
+
function hasPlaceholderEmail(value) {
|
|
4218
|
+
if (!value)
|
|
4219
|
+
return false;
|
|
4220
|
+
const email = value.trim().toLowerCase();
|
|
4221
|
+
if (!email)
|
|
4222
|
+
return false;
|
|
4223
|
+
return (email.endsWith("@example.com") ||
|
|
4224
|
+
email.includes("account1@example.com") ||
|
|
4225
|
+
email.includes("account2@example.com") ||
|
|
4226
|
+
email.includes("account3@example.com"));
|
|
4227
|
+
}
|
|
4228
|
+
function normalizeDoctorIndexes(storage) {
|
|
4229
|
+
const total = storage.accounts.length;
|
|
4230
|
+
const nextActive = total === 0 ? 0 : Math.max(0, Math.min(storage.activeIndex, total - 1));
|
|
4231
|
+
let changed = false;
|
|
4232
|
+
if (storage.activeIndex !== nextActive) {
|
|
4233
|
+
storage.activeIndex = nextActive;
|
|
4234
|
+
changed = true;
|
|
4235
|
+
}
|
|
4236
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
4237
|
+
for (const family of MODEL_FAMILIES) {
|
|
4238
|
+
const raw = storage.activeIndexByFamily[family];
|
|
4239
|
+
const fallback = storage.activeIndex;
|
|
4240
|
+
const candidate = typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
4241
|
+
const clamped = total === 0 ? 0 : Math.max(0, Math.min(candidate, total - 1));
|
|
4242
|
+
if (storage.activeIndexByFamily[family] !== clamped) {
|
|
4243
|
+
storage.activeIndexByFamily[family] = clamped;
|
|
4244
|
+
changed = true;
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
return changed;
|
|
4248
|
+
}
|
|
4249
|
+
function applyDoctorFixes(storage) {
|
|
4250
|
+
let changed = false;
|
|
4251
|
+
const actions = [];
|
|
4252
|
+
if (normalizeDoctorIndexes(storage)) {
|
|
4253
|
+
changed = true;
|
|
4254
|
+
actions.push({
|
|
4255
|
+
key: "active-index",
|
|
4256
|
+
message: "Normalized active account indexes",
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
const seenRefreshTokens = new Map();
|
|
4260
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
4261
|
+
const account = storage.accounts[i];
|
|
4262
|
+
if (!account)
|
|
4263
|
+
continue;
|
|
4264
|
+
const refreshToken = account.refreshToken.trim();
|
|
4265
|
+
const existingTokenIndex = seenRefreshTokens.get(refreshToken);
|
|
4266
|
+
if (typeof existingTokenIndex === "number") {
|
|
4267
|
+
if (account.enabled !== false) {
|
|
4268
|
+
account.enabled = false;
|
|
4269
|
+
changed = true;
|
|
4270
|
+
actions.push({
|
|
4271
|
+
key: "duplicate-refresh-token",
|
|
4272
|
+
message: `Disabled duplicate token entry on account ${i + 1} (kept account ${existingTokenIndex + 1})`,
|
|
4273
|
+
});
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
else {
|
|
4277
|
+
seenRefreshTokens.set(refreshToken, i);
|
|
4278
|
+
}
|
|
4279
|
+
const tokenEmail = sanitizeEmail(extractAccountEmail(account.accessToken));
|
|
4280
|
+
if (tokenEmail &&
|
|
4281
|
+
(!sanitizeEmail(account.email) || hasPlaceholderEmail(account.email))) {
|
|
4282
|
+
account.email = tokenEmail;
|
|
4283
|
+
changed = true;
|
|
4284
|
+
actions.push({
|
|
4285
|
+
key: "email-from-token",
|
|
4286
|
+
message: `Updated account ${i + 1} email from token claims`,
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
const tokenAccountId = extractAccountId(account.accessToken);
|
|
4290
|
+
if (!account.accountId && tokenAccountId) {
|
|
4291
|
+
account.accountId = tokenAccountId;
|
|
4292
|
+
account.accountIdSource = "token";
|
|
4293
|
+
changed = true;
|
|
4294
|
+
actions.push({
|
|
4295
|
+
key: "account-id-from-token",
|
|
4296
|
+
message: `Filled missing accountId for account ${i + 1}`,
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
const enabledCount = storage.accounts.filter((account) => account.enabled !== false).length;
|
|
4301
|
+
if (storage.accounts.length > 0 && enabledCount === 0) {
|
|
4302
|
+
const index = resolveActiveIndex(storage, "codex");
|
|
4303
|
+
const candidate = storage.accounts[index] ?? storage.accounts[0];
|
|
4304
|
+
if (candidate) {
|
|
4305
|
+
candidate.enabled = true;
|
|
4306
|
+
changed = true;
|
|
4307
|
+
actions.push({
|
|
4308
|
+
key: "enabled-accounts",
|
|
4309
|
+
message: `Re-enabled account ${index + 1} to avoid an all-disabled pool`,
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
if (normalizeDoctorIndexes(storage)) {
|
|
4314
|
+
changed = true;
|
|
4315
|
+
}
|
|
4316
|
+
return { changed, actions };
|
|
4317
|
+
}
|
|
4318
|
+
async function runDoctor(args) {
|
|
4319
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4320
|
+
printDoctorUsage();
|
|
4321
|
+
return 0;
|
|
4322
|
+
}
|
|
4323
|
+
const parsedArgs = parseDoctorArgs(args);
|
|
4324
|
+
if (!parsedArgs.ok) {
|
|
4325
|
+
console.error(parsedArgs.message);
|
|
4326
|
+
printDoctorUsage();
|
|
4327
|
+
return 1;
|
|
4328
|
+
}
|
|
4329
|
+
const options = parsedArgs.options;
|
|
4330
|
+
setStoragePath(null);
|
|
4331
|
+
const storagePath = getStoragePath();
|
|
4332
|
+
const checks = [];
|
|
4333
|
+
const addCheck = (check) => {
|
|
4334
|
+
checks.push(check);
|
|
4335
|
+
};
|
|
4336
|
+
addCheck({
|
|
4337
|
+
key: "storage-file",
|
|
4338
|
+
severity: existsSync(storagePath) ? "ok" : "warn",
|
|
4339
|
+
message: existsSync(storagePath)
|
|
4340
|
+
? "Account storage file found"
|
|
4341
|
+
: "Account storage file does not exist yet (first login pending)",
|
|
4342
|
+
details: storagePath,
|
|
4343
|
+
});
|
|
4344
|
+
if (existsSync(storagePath)) {
|
|
4345
|
+
try {
|
|
4346
|
+
const stat = await fs.stat(storagePath);
|
|
4347
|
+
addCheck({
|
|
4348
|
+
key: "storage-readable",
|
|
4349
|
+
severity: stat.size > 0 ? "ok" : "warn",
|
|
4350
|
+
message: stat.size > 0 ? "Storage file is readable" : "Storage file is empty",
|
|
4351
|
+
details: `${stat.size} bytes`,
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
catch (error) {
|
|
4355
|
+
addCheck({
|
|
4356
|
+
key: "storage-readable",
|
|
4357
|
+
severity: "error",
|
|
4358
|
+
message: "Unable to read storage file metadata",
|
|
4359
|
+
details: error instanceof Error ? error.message : String(error),
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
const storage = await loadAccounts();
|
|
4364
|
+
let fixChanged = false;
|
|
4365
|
+
let fixActions = [];
|
|
4366
|
+
if (options.fix && storage && storage.accounts.length > 0) {
|
|
4367
|
+
const fixed = applyDoctorFixes(storage);
|
|
4368
|
+
fixChanged = fixed.changed;
|
|
4369
|
+
fixActions = fixed.actions;
|
|
4370
|
+
if (fixChanged && !options.dryRun) {
|
|
4371
|
+
await saveAccounts(storage);
|
|
4372
|
+
}
|
|
4373
|
+
addCheck({
|
|
4374
|
+
key: "auto-fix",
|
|
4375
|
+
severity: fixChanged ? "warn" : "ok",
|
|
4376
|
+
message: fixChanged
|
|
4377
|
+
? options.dryRun
|
|
4378
|
+
? `Prepared ${fixActions.length} fix(es) (dry-run)`
|
|
4379
|
+
: `Applied ${fixActions.length} fix(es)`
|
|
4380
|
+
: "No safe auto-fixes needed",
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
if (!storage || storage.accounts.length === 0) {
|
|
4384
|
+
addCheck({
|
|
4385
|
+
key: "accounts",
|
|
4386
|
+
severity: "warn",
|
|
4387
|
+
message: "No accounts configured",
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
else {
|
|
4391
|
+
addCheck({
|
|
4392
|
+
key: "accounts",
|
|
4393
|
+
severity: "ok",
|
|
4394
|
+
message: `Loaded ${storage.accounts.length} account(s)`,
|
|
4395
|
+
});
|
|
4396
|
+
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
4397
|
+
const activeExists = activeIndex >= 0 && activeIndex < storage.accounts.length;
|
|
4398
|
+
addCheck({
|
|
4399
|
+
key: "active-index",
|
|
4400
|
+
severity: activeExists ? "ok" : "error",
|
|
4401
|
+
message: activeExists
|
|
4402
|
+
? `Active index is valid (${activeIndex + 1})`
|
|
4403
|
+
: "Active index is out of range",
|
|
4404
|
+
});
|
|
4405
|
+
const disabledCount = storage.accounts.filter((a) => a.enabled === false).length;
|
|
4406
|
+
addCheck({
|
|
4407
|
+
key: "enabled-accounts",
|
|
4408
|
+
severity: disabledCount >= storage.accounts.length ? "error" : "ok",
|
|
4409
|
+
message: disabledCount >= storage.accounts.length
|
|
4410
|
+
? "All accounts are disabled"
|
|
4411
|
+
: `${storage.accounts.length - disabledCount} enabled / ${disabledCount} disabled`,
|
|
4412
|
+
});
|
|
4413
|
+
const seenRefreshTokens = new Set();
|
|
4414
|
+
let duplicateTokenCount = 0;
|
|
4415
|
+
for (const account of storage.accounts) {
|
|
4416
|
+
const token = account.refreshToken.trim();
|
|
4417
|
+
if (seenRefreshTokens.has(token)) {
|
|
4418
|
+
duplicateTokenCount += 1;
|
|
4419
|
+
}
|
|
4420
|
+
else {
|
|
4421
|
+
seenRefreshTokens.add(token);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
addCheck({
|
|
4425
|
+
key: "duplicate-refresh-token",
|
|
4426
|
+
severity: duplicateTokenCount > 0 ? "warn" : "ok",
|
|
4427
|
+
message: duplicateTokenCount > 0
|
|
4428
|
+
? `Detected ${duplicateTokenCount} duplicate refresh token entr${duplicateTokenCount === 1 ? "y" : "ies"}`
|
|
4429
|
+
: "No duplicate refresh tokens detected",
|
|
4430
|
+
});
|
|
4431
|
+
const seenEmails = new Set();
|
|
4432
|
+
let duplicateEmailCount = 0;
|
|
4433
|
+
let placeholderEmailCount = 0;
|
|
4434
|
+
let likelyInvalidRefreshTokenCount = 0;
|
|
4435
|
+
for (const account of storage.accounts) {
|
|
4436
|
+
const email = sanitizeEmail(account.email);
|
|
4437
|
+
if (!email)
|
|
4438
|
+
continue;
|
|
4439
|
+
if (seenEmails.has(email))
|
|
4440
|
+
duplicateEmailCount += 1;
|
|
4441
|
+
seenEmails.add(email);
|
|
4442
|
+
if (hasPlaceholderEmail(email))
|
|
4443
|
+
placeholderEmailCount += 1;
|
|
4444
|
+
if (hasLikelyInvalidRefreshToken(account.refreshToken)) {
|
|
4445
|
+
likelyInvalidRefreshTokenCount += 1;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
addCheck({
|
|
4449
|
+
key: "duplicate-email",
|
|
4450
|
+
severity: duplicateEmailCount > 0 ? "warn" : "ok",
|
|
4451
|
+
message: duplicateEmailCount > 0
|
|
4452
|
+
? `Detected ${duplicateEmailCount} duplicate email entr${duplicateEmailCount === 1 ? "y" : "ies"}`
|
|
4453
|
+
: "No duplicate emails detected",
|
|
4454
|
+
});
|
|
4455
|
+
addCheck({
|
|
4456
|
+
key: "placeholder-email",
|
|
4457
|
+
severity: placeholderEmailCount > 0 ? "warn" : "ok",
|
|
4458
|
+
message: placeholderEmailCount > 0
|
|
4459
|
+
? `${placeholderEmailCount} account(s) appear to be placeholder/demo entries`
|
|
4460
|
+
: "No placeholder emails detected",
|
|
4461
|
+
});
|
|
4462
|
+
addCheck({
|
|
4463
|
+
key: "refresh-token-shape",
|
|
4464
|
+
severity: likelyInvalidRefreshTokenCount > 0 ? "warn" : "ok",
|
|
4465
|
+
message: likelyInvalidRefreshTokenCount > 0
|
|
4466
|
+
? `${likelyInvalidRefreshTokenCount} account(s) have likely invalid refresh token format`
|
|
4467
|
+
: "Refresh token format looks normal",
|
|
4468
|
+
});
|
|
4469
|
+
const now = Date.now();
|
|
4470
|
+
const forecastResults = evaluateForecastAccounts(storage.accounts.map((account, index) => ({
|
|
4471
|
+
index,
|
|
4472
|
+
account,
|
|
4473
|
+
isCurrent: index === activeIndex,
|
|
4474
|
+
now,
|
|
4475
|
+
})));
|
|
4476
|
+
const recommendation = recommendForecastAccount(forecastResults);
|
|
4477
|
+
if (recommendation.recommendedIndex !== null && recommendation.recommendedIndex !== activeIndex) {
|
|
4478
|
+
addCheck({
|
|
4479
|
+
key: "recommended-switch",
|
|
4480
|
+
severity: "warn",
|
|
4481
|
+
message: `A healthier account is available: switch to ${recommendation.recommendedIndex + 1}`,
|
|
4482
|
+
details: recommendation.reason,
|
|
4483
|
+
});
|
|
4484
|
+
}
|
|
4485
|
+
else {
|
|
4486
|
+
addCheck({
|
|
4487
|
+
key: "recommended-switch",
|
|
4488
|
+
severity: "ok",
|
|
4489
|
+
message: "Current account aligns with forecast recommendation",
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
const summary = checks.reduce((acc, check) => {
|
|
4494
|
+
acc[check.severity] += 1;
|
|
4495
|
+
return acc;
|
|
4496
|
+
}, { ok: 0, warn: 0, error: 0 });
|
|
4497
|
+
if (options.json) {
|
|
4498
|
+
console.log(JSON.stringify({
|
|
4499
|
+
command: "doctor",
|
|
4500
|
+
storagePath,
|
|
4501
|
+
summary,
|
|
4502
|
+
checks,
|
|
4503
|
+
fix: {
|
|
4504
|
+
enabled: options.fix,
|
|
4505
|
+
dryRun: options.dryRun,
|
|
4506
|
+
changed: fixChanged,
|
|
4507
|
+
actions: fixActions,
|
|
4508
|
+
},
|
|
4509
|
+
}, null, 2));
|
|
4510
|
+
return summary.error > 0 ? 1 : 0;
|
|
4511
|
+
}
|
|
4512
|
+
console.log("Doctor diagnostics");
|
|
4513
|
+
console.log(`Storage: ${storagePath}`);
|
|
4514
|
+
console.log(`Summary: ${summary.ok} ok, ${summary.warn} warnings, ${summary.error} errors`);
|
|
4515
|
+
console.log("");
|
|
4516
|
+
for (const check of checks) {
|
|
4517
|
+
const marker = check.severity === "ok" ? "✓" : check.severity === "warn" ? "!" : "✗";
|
|
4518
|
+
console.log(`${marker} ${check.key}: ${check.message}`);
|
|
4519
|
+
if (check.details) {
|
|
4520
|
+
console.log(` ${check.details}`);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
if (options.fix) {
|
|
4524
|
+
console.log("");
|
|
4525
|
+
if (fixActions.length > 0) {
|
|
4526
|
+
console.log(`Auto-fix actions (${options.dryRun ? "dry-run" : "applied"}):`);
|
|
4527
|
+
for (const action of fixActions) {
|
|
4528
|
+
console.log(` - ${action.message}`);
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
else {
|
|
4532
|
+
console.log("Auto-fix actions: none");
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
return summary.error > 0 ? 1 : 0;
|
|
4536
|
+
}
|
|
4537
|
+
async function clearAccountsAndReset() {
|
|
4538
|
+
await saveAccounts({
|
|
4539
|
+
version: 3,
|
|
4540
|
+
accounts: [],
|
|
4541
|
+
activeIndex: 0,
|
|
4542
|
+
activeIndexByFamily: {},
|
|
4543
|
+
});
|
|
4544
|
+
}
|
|
4545
|
+
async function handleManageAction(storage, menuResult) {
|
|
4546
|
+
if (typeof menuResult.switchAccountIndex === "number") {
|
|
4547
|
+
const index = menuResult.switchAccountIndex;
|
|
4548
|
+
await runSwitch([String(index + 1)]);
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
if (typeof menuResult.deleteAccountIndex === "number") {
|
|
4552
|
+
const idx = menuResult.deleteAccountIndex;
|
|
4553
|
+
if (idx >= 0 && idx < storage.accounts.length) {
|
|
4554
|
+
storage.accounts.splice(idx, 1);
|
|
4555
|
+
storage.activeIndex = 0;
|
|
4556
|
+
storage.activeIndexByFamily = {};
|
|
4557
|
+
for (const family of MODEL_FAMILIES) {
|
|
4558
|
+
storage.activeIndexByFamily[family] = 0;
|
|
4559
|
+
}
|
|
4560
|
+
await saveAccounts(storage);
|
|
4561
|
+
console.log(`Deleted account ${idx + 1}.`);
|
|
4562
|
+
}
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4565
|
+
if (typeof menuResult.toggleAccountIndex === "number") {
|
|
4566
|
+
const idx = menuResult.toggleAccountIndex;
|
|
4567
|
+
const account = storage.accounts[idx];
|
|
4568
|
+
if (account) {
|
|
4569
|
+
account.enabled = account.enabled === false;
|
|
4570
|
+
await saveAccounts(storage);
|
|
4571
|
+
console.log(`${account.enabled === false ? "Disabled" : "Enabled"} account ${idx + 1}.`);
|
|
4572
|
+
}
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
if (typeof menuResult.refreshAccountIndex === "number") {
|
|
4576
|
+
const idx = menuResult.refreshAccountIndex;
|
|
4577
|
+
const existing = storage.accounts[idx];
|
|
4578
|
+
if (!existing)
|
|
4579
|
+
return;
|
|
4580
|
+
const tokenResult = await runOAuthFlow(true);
|
|
4581
|
+
if (tokenResult.type !== "success") {
|
|
4582
|
+
console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
const resolved = resolveAccountSelection(tokenResult);
|
|
4586
|
+
await persistAccountPool([resolved], false);
|
|
4587
|
+
await syncSelectionToCodex(resolved);
|
|
4588
|
+
console.log(`Refreshed account ${idx + 1}.`);
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
async function runAuthLogin() {
|
|
4592
|
+
setStoragePath(null);
|
|
4593
|
+
let pendingMenuQuotaRefresh = null;
|
|
4594
|
+
let menuQuotaRefreshStatus;
|
|
4595
|
+
loginFlow: while (true) {
|
|
4596
|
+
let existingStorage = await loadAccounts();
|
|
4597
|
+
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
4598
|
+
while (true) {
|
|
4599
|
+
existingStorage = await loadAccounts();
|
|
4600
|
+
if (!existingStorage || existingStorage.accounts.length === 0) {
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
const currentStorage = existingStorage;
|
|
4604
|
+
const displaySettings = await loadDashboardDisplaySettings();
|
|
4605
|
+
applyUiThemeFromDashboardSettings(displaySettings);
|
|
4606
|
+
const quotaCache = await loadQuotaCache();
|
|
4607
|
+
const shouldAutoFetchLimits = displaySettings.menuAutoFetchLimits ?? true;
|
|
4608
|
+
const showFetchStatus = displaySettings.menuShowFetchStatus ?? true;
|
|
4609
|
+
const quotaTtlMs = displaySettings.menuQuotaTtlMs ?? DEFAULT_MENU_QUOTA_REFRESH_TTL_MS;
|
|
4610
|
+
if (shouldAutoFetchLimits && !pendingMenuQuotaRefresh) {
|
|
4611
|
+
const staleCount = countMenuQuotaRefreshTargets(currentStorage, quotaCache, quotaTtlMs);
|
|
4612
|
+
if (staleCount > 0) {
|
|
4613
|
+
if (showFetchStatus) {
|
|
4614
|
+
menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [0/${staleCount}]`;
|
|
4615
|
+
}
|
|
4616
|
+
pendingMenuQuotaRefresh = refreshQuotaCacheForMenu(currentStorage, quotaCache, quotaTtlMs, (current, total) => {
|
|
4617
|
+
if (!showFetchStatus)
|
|
4618
|
+
return;
|
|
4619
|
+
menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [${current}/${total}]`;
|
|
4620
|
+
})
|
|
4621
|
+
.then(() => undefined)
|
|
4622
|
+
.catch(() => undefined)
|
|
4623
|
+
.finally(() => {
|
|
4624
|
+
menuQuotaRefreshStatus = undefined;
|
|
4625
|
+
pendingMenuQuotaRefresh = null;
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
const flaggedStorage = await loadFlaggedAccounts();
|
|
4630
|
+
const menuResult = await promptLoginMode(toExistingAccountInfo(currentStorage, quotaCache, displaySettings), {
|
|
4631
|
+
flaggedCount: flaggedStorage.accounts.length,
|
|
4632
|
+
statusMessage: showFetchStatus ? () => menuQuotaRefreshStatus : undefined,
|
|
4633
|
+
});
|
|
4634
|
+
if (menuResult.mode === "cancel") {
|
|
4635
|
+
console.log("Cancelled.");
|
|
4636
|
+
return 0;
|
|
4637
|
+
}
|
|
4638
|
+
if (menuResult.mode === "check") {
|
|
4639
|
+
await runActionPanel("Quick Check", "Checking local session + live status", async () => {
|
|
4640
|
+
await runHealthCheck({ forceRefresh: false, liveProbe: true });
|
|
4641
|
+
}, displaySettings);
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
if (menuResult.mode === "deep-check") {
|
|
4645
|
+
await runActionPanel("Deep Check", "Refreshing and testing all accounts", async () => {
|
|
4646
|
+
await runHealthCheck({ forceRefresh: true, liveProbe: true });
|
|
4647
|
+
}, displaySettings);
|
|
4648
|
+
continue;
|
|
4649
|
+
}
|
|
4650
|
+
if (menuResult.mode === "forecast") {
|
|
4651
|
+
await runActionPanel("Best Account", "Comparing accounts", async () => {
|
|
4652
|
+
await runForecast(["--live"]);
|
|
4653
|
+
}, displaySettings);
|
|
4654
|
+
continue;
|
|
4655
|
+
}
|
|
4656
|
+
if (menuResult.mode === "fix") {
|
|
4657
|
+
await runActionPanel("Auto-Fix", "Checking and fixing common issues", async () => {
|
|
4658
|
+
await runFix(["--live"]);
|
|
4659
|
+
}, displaySettings);
|
|
4660
|
+
continue;
|
|
4661
|
+
}
|
|
4662
|
+
if (menuResult.mode === "settings") {
|
|
4663
|
+
await configureUnifiedSettings(displaySettings);
|
|
4664
|
+
continue;
|
|
4665
|
+
}
|
|
4666
|
+
if (menuResult.mode === "verify-flagged") {
|
|
4667
|
+
await runActionPanel("Problem Account Check", "Checking problem accounts", async () => {
|
|
4668
|
+
await runVerifyFlagged([]);
|
|
4669
|
+
}, displaySettings);
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
4672
|
+
if (menuResult.mode === "fresh" && menuResult.deleteAll) {
|
|
4673
|
+
await runActionPanel("Reset Accounts", "Deleting all saved accounts", async () => {
|
|
4674
|
+
await clearAccountsAndReset();
|
|
4675
|
+
console.log("Deleted all accounts.");
|
|
4676
|
+
}, displaySettings);
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
if (menuResult.mode === "manage") {
|
|
4680
|
+
const requiresInteractiveOAuth = typeof menuResult.refreshAccountIndex === "number";
|
|
4681
|
+
if (requiresInteractiveOAuth) {
|
|
4682
|
+
await handleManageAction(currentStorage, menuResult);
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
await runActionPanel("Applying Change", "Updating selected account", async () => {
|
|
4686
|
+
await handleManageAction(currentStorage, menuResult);
|
|
4687
|
+
}, displaySettings);
|
|
4688
|
+
continue;
|
|
4689
|
+
}
|
|
4690
|
+
if (menuResult.mode === "add") {
|
|
4691
|
+
break;
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
const refreshedStorage = await loadAccounts();
|
|
4696
|
+
const existingCount = refreshedStorage?.accounts.length ?? 0;
|
|
4697
|
+
let forceNewLogin = existingCount > 0;
|
|
4698
|
+
while (true) {
|
|
4699
|
+
const tokenResult = await runOAuthFlow(forceNewLogin);
|
|
4700
|
+
if (tokenResult.type !== "success") {
|
|
4701
|
+
if (isUserCancelledOAuth(tokenResult)) {
|
|
4702
|
+
if (existingCount > 0) {
|
|
4703
|
+
console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
|
|
4704
|
+
continue loginFlow;
|
|
4705
|
+
}
|
|
4706
|
+
console.log("Cancelled.");
|
|
4707
|
+
return 0;
|
|
4708
|
+
}
|
|
4709
|
+
console.error(`Login failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
|
|
4710
|
+
return 1;
|
|
4711
|
+
}
|
|
4712
|
+
const resolved = resolveAccountSelection(tokenResult);
|
|
4713
|
+
await persistAccountPool([resolved], false);
|
|
4714
|
+
await syncSelectionToCodex(resolved);
|
|
4715
|
+
const latestStorage = await loadAccounts();
|
|
4716
|
+
const count = latestStorage?.accounts.length ?? 1;
|
|
4717
|
+
console.log(`Added account. Total: ${count}`);
|
|
4718
|
+
if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
4719
|
+
console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
|
|
4720
|
+
break;
|
|
4721
|
+
}
|
|
4722
|
+
const addAnother = await promptAddAnotherAccount(count);
|
|
4723
|
+
if (!addAnother)
|
|
4724
|
+
break;
|
|
4725
|
+
forceNewLogin = true;
|
|
4726
|
+
}
|
|
4727
|
+
continue loginFlow;
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
async function runSwitch(args) {
|
|
4731
|
+
setStoragePath(null);
|
|
4732
|
+
const indexArg = args[0];
|
|
4733
|
+
if (!indexArg) {
|
|
4734
|
+
console.error("Missing index. Usage: codex-multi-auth auth switch <index>");
|
|
4735
|
+
return 1;
|
|
4736
|
+
}
|
|
4737
|
+
const parsed = Number.parseInt(indexArg, 10);
|
|
4738
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
4739
|
+
console.error(`Invalid index: ${indexArg}`);
|
|
4740
|
+
return 1;
|
|
4741
|
+
}
|
|
4742
|
+
const targetIndex = parsed - 1;
|
|
4743
|
+
const storage = await loadAccounts();
|
|
4744
|
+
if (!storage || storage.accounts.length === 0) {
|
|
4745
|
+
console.error("No accounts configured.");
|
|
4746
|
+
return 1;
|
|
4747
|
+
}
|
|
4748
|
+
if (targetIndex < 0 || targetIndex >= storage.accounts.length) {
|
|
4749
|
+
console.error(`Index out of range. Valid range: 1-${storage.accounts.length}`);
|
|
4750
|
+
return 1;
|
|
4751
|
+
}
|
|
4752
|
+
const account = storage.accounts[targetIndex];
|
|
4753
|
+
if (!account) {
|
|
4754
|
+
console.error(`Account ${parsed} not found.`);
|
|
4755
|
+
return 1;
|
|
4756
|
+
}
|
|
4757
|
+
storage.activeIndex = targetIndex;
|
|
4758
|
+
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
4759
|
+
for (const family of MODEL_FAMILIES) {
|
|
4760
|
+
storage.activeIndexByFamily[family] = targetIndex;
|
|
4761
|
+
}
|
|
4762
|
+
const wasDisabled = account.enabled === false;
|
|
4763
|
+
if (wasDisabled) {
|
|
4764
|
+
account.enabled = true;
|
|
4765
|
+
}
|
|
4766
|
+
account.lastUsed = Date.now();
|
|
4767
|
+
account.lastSwitchReason = "rotation";
|
|
4768
|
+
await saveAccounts(storage);
|
|
4769
|
+
const synced = await setCodexCliActiveSelection({
|
|
4770
|
+
accountId: account.accountId,
|
|
4771
|
+
email: account.email,
|
|
4772
|
+
accessToken: account.accessToken,
|
|
4773
|
+
refreshToken: account.refreshToken,
|
|
4774
|
+
expiresAt: account.expiresAt,
|
|
4775
|
+
});
|
|
4776
|
+
if (!synced) {
|
|
4777
|
+
console.error(`Switch failed: account ${parsed} was selected locally but Codex auth sync failed. Run \"Refresh login for this account\" and retry.`);
|
|
4778
|
+
return 1;
|
|
4779
|
+
}
|
|
4780
|
+
console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
|
|
4781
|
+
return 0;
|
|
4782
|
+
}
|
|
4783
|
+
export async function runCodexMultiAuthCli(rawArgs) {
|
|
4784
|
+
const startupDisplaySettings = await loadDashboardDisplaySettings();
|
|
4785
|
+
applyUiThemeFromDashboardSettings(startupDisplaySettings);
|
|
4786
|
+
const args = [...rawArgs];
|
|
4787
|
+
if (args.length === 0) {
|
|
4788
|
+
printUsage();
|
|
4789
|
+
return 0;
|
|
4790
|
+
}
|
|
4791
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
4792
|
+
printUsage();
|
|
4793
|
+
return 0;
|
|
4794
|
+
}
|
|
4795
|
+
const [root, sub, ...rest] = args;
|
|
4796
|
+
if (root !== "auth") {
|
|
4797
|
+
printUsage();
|
|
4798
|
+
return 1;
|
|
4799
|
+
}
|
|
4800
|
+
const command = sub ?? "login";
|
|
4801
|
+
if (command === "--help" || command === "-h") {
|
|
4802
|
+
printUsage();
|
|
4803
|
+
return 0;
|
|
4804
|
+
}
|
|
4805
|
+
if (command === "login") {
|
|
4806
|
+
return runAuthLogin();
|
|
4807
|
+
}
|
|
4808
|
+
if (command === "list" || command === "status") {
|
|
4809
|
+
await showAccountStatus();
|
|
4810
|
+
return 0;
|
|
4811
|
+
}
|
|
4812
|
+
if (command === "switch") {
|
|
4813
|
+
return runSwitch(rest);
|
|
4814
|
+
}
|
|
4815
|
+
if (command === "check") {
|
|
4816
|
+
await runHealthCheck({ liveProbe: true });
|
|
4817
|
+
return 0;
|
|
4818
|
+
}
|
|
4819
|
+
if (command === "features") {
|
|
4820
|
+
return runFeaturesReport();
|
|
4821
|
+
}
|
|
4822
|
+
if (command === "verify-flagged") {
|
|
4823
|
+
return runVerifyFlagged(rest);
|
|
4824
|
+
}
|
|
4825
|
+
if (command === "forecast") {
|
|
4826
|
+
return runForecast(rest);
|
|
4827
|
+
}
|
|
4828
|
+
if (command === "report") {
|
|
4829
|
+
return runReport(rest);
|
|
4830
|
+
}
|
|
4831
|
+
if (command === "fix") {
|
|
4832
|
+
return runFix(rest);
|
|
4833
|
+
}
|
|
4834
|
+
if (command === "doctor") {
|
|
4835
|
+
return runDoctor(rest);
|
|
4836
|
+
}
|
|
4837
|
+
console.error(`Unknown command: ${command}`);
|
|
4838
|
+
printUsage();
|
|
4839
|
+
return 1;
|
|
4840
|
+
}
|
|
4841
|
+
//# sourceMappingURL=codex-manager.js.map
|