codex-multi-auth 2.3.0-beta.0 → 2.3.0-beta.2

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.
Files changed (470) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +4 -3
  3. package/config/schema/config.schema.json +268 -21
  4. package/dist/index.js +0 -1
  5. package/dist/lib/account-policy.js +0 -1
  6. package/dist/lib/accounts/rate-limits.js +0 -1
  7. package/dist/lib/accounts.d.ts +3 -8
  8. package/dist/lib/accounts.d.ts.map +1 -1
  9. package/dist/lib/accounts.js +2 -3
  10. package/dist/lib/audit.js +0 -1
  11. package/dist/lib/auth/auth.js +0 -1
  12. package/dist/lib/auth/browser.js +0 -1
  13. package/dist/lib/auth/device-auth.js +0 -1
  14. package/dist/lib/auth/index.js +0 -1
  15. package/dist/lib/auth/org-override.js +0 -1
  16. package/dist/lib/auth/server.js +0 -1
  17. package/dist/lib/auth/token-utils.js +0 -1
  18. package/dist/lib/auth-rate-limit.js +0 -1
  19. package/dist/lib/budget-guard.js +0 -1
  20. package/dist/lib/capability-policy.js +0 -1
  21. package/dist/lib/circuit-breaker.js +0 -1
  22. package/dist/lib/cli.js +1 -2
  23. package/dist/lib/codex-cli/index.js +0 -1
  24. package/dist/lib/codex-cli/observability.js +0 -1
  25. package/dist/lib/codex-cli/state.js +0 -1
  26. package/dist/lib/codex-cli/sync.d.ts +2 -2
  27. package/dist/lib/codex-cli/sync.d.ts.map +1 -1
  28. package/dist/lib/codex-cli/sync.js +1 -2
  29. package/dist/lib/codex-cli/writer.js +0 -1
  30. package/dist/lib/codex-manager/account-credentials.d.ts +13 -0
  31. package/dist/lib/codex-manager/account-credentials.d.ts.map +1 -0
  32. package/dist/lib/codex-manager/account-credentials.js +55 -0
  33. package/dist/lib/codex-manager/account-manager-commands.js +0 -1
  34. package/dist/lib/codex-manager/account-pool-write.d.ts +95 -0
  35. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -0
  36. package/dist/lib/codex-manager/account-pool-write.js +165 -0
  37. package/dist/lib/codex-manager/account-pool-write.js.map +1 -0
  38. package/dist/lib/codex-manager/backend-category-entry.js +0 -1
  39. package/dist/lib/codex-manager/backend-category-helpers.js +0 -1
  40. package/dist/lib/codex-manager/backend-category-prompt.js +0 -1
  41. package/dist/lib/codex-manager/backend-settings-controller.js +0 -1
  42. package/dist/lib/codex-manager/backend-settings-entry.js +0 -1
  43. package/dist/lib/codex-manager/backend-settings-helpers.js +0 -1
  44. package/dist/lib/codex-manager/backend-settings-prompt.js +0 -1
  45. package/dist/lib/codex-manager/backend-settings-schema.js +0 -1
  46. package/dist/lib/codex-manager/behavior-settings-panel.d.ts +1 -1
  47. package/dist/lib/codex-manager/behavior-settings-panel.d.ts.map +1 -1
  48. package/dist/lib/codex-manager/behavior-settings-panel.js +0 -1
  49. package/dist/lib/codex-manager/commands/account.js +0 -1
  50. package/dist/lib/codex-manager/commands/best.d.ts +8 -0
  51. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
  52. package/dist/lib/codex-manager/commands/best.js +64 -1
  53. package/dist/lib/codex-manager/commands/bridge.js +0 -1
  54. package/dist/lib/codex-manager/commands/budget.js +0 -1
  55. package/dist/lib/codex-manager/commands/check.js +0 -1
  56. package/dist/lib/codex-manager/commands/config-explain.js +0 -1
  57. package/dist/lib/codex-manager/commands/debug-bundle.js +0 -1
  58. package/dist/lib/codex-manager/commands/forecast.js +0 -1
  59. package/dist/lib/codex-manager/commands/init-config.js +0 -1
  60. package/dist/lib/codex-manager/commands/integrations.js +0 -1
  61. package/dist/lib/codex-manager/commands/models.js +0 -1
  62. package/dist/lib/codex-manager/commands/monitor.js +0 -1
  63. package/dist/lib/codex-manager/commands/report.js +0 -1
  64. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
  65. package/dist/lib/codex-manager/commands/rotation.js +1 -4
  66. package/dist/lib/codex-manager/commands/status.js +0 -1
  67. package/dist/lib/codex-manager/commands/switch.js +0 -1
  68. package/dist/lib/codex-manager/commands/uninstall.d.ts.map +1 -1
  69. package/dist/lib/codex-manager/commands/uninstall.js +2 -33
  70. package/dist/lib/codex-manager/commands/unpin.js +0 -1
  71. package/dist/lib/codex-manager/commands/usage.js +0 -1
  72. package/dist/lib/codex-manager/commands/verify.js +0 -1
  73. package/dist/lib/codex-manager/commands/why-selected.js +0 -1
  74. package/dist/lib/codex-manager/commands/workspace.js +0 -1
  75. package/dist/lib/codex-manager/dashboard-display-panel.d.ts +1 -1
  76. package/dist/lib/codex-manager/dashboard-display-panel.d.ts.map +1 -1
  77. package/dist/lib/codex-manager/dashboard-display-panel.js +0 -1
  78. package/dist/lib/codex-manager/dashboard-formatters.js +0 -1
  79. package/dist/lib/codex-manager/dashboard-settings-controller.js +0 -1
  80. package/dist/lib/codex-manager/dashboard-settings-data.js +0 -1
  81. package/dist/lib/codex-manager/dashboard-settings-entry.js +0 -1
  82. package/dist/lib/codex-manager/experimental-settings-entry.js +0 -1
  83. package/dist/lib/codex-manager/experimental-settings-prompt.js +0 -1
  84. package/dist/lib/codex-manager/experimental-settings-schema.js +1 -2
  85. package/dist/lib/codex-manager/experimental-sync-target-entry.js +0 -1
  86. package/dist/lib/codex-manager/experimental-sync-target.js +0 -1
  87. package/dist/lib/codex-manager/forecast-report-commands.js +0 -1
  88. package/dist/lib/codex-manager/forecast-report-shared.d.ts +2 -2
  89. package/dist/lib/codex-manager/forecast-report-shared.d.ts.map +1 -1
  90. package/dist/lib/codex-manager/forecast-report-shared.js +5 -21
  91. package/dist/lib/codex-manager/formatters/account-formatters.d.ts +11 -0
  92. package/dist/lib/codex-manager/formatters/account-formatters.d.ts.map +1 -0
  93. package/dist/lib/codex-manager/formatters/account-formatters.js +79 -0
  94. package/dist/lib/codex-manager/formatters/index.d.ts +5 -0
  95. package/dist/lib/codex-manager/formatters/index.d.ts.map +1 -0
  96. package/dist/lib/codex-manager/formatters/index.js +4 -0
  97. package/dist/lib/codex-manager/formatters/model-formatters.d.ts +12 -0
  98. package/dist/lib/codex-manager/formatters/model-formatters.d.ts.map +1 -0
  99. package/dist/lib/codex-manager/formatters/model-formatters.js +23 -0
  100. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts +11 -0
  101. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -0
  102. package/dist/lib/codex-manager/formatters/quota-formatters.js +104 -0
  103. package/dist/lib/codex-manager/formatters/text-style.d.ts +14 -0
  104. package/dist/lib/codex-manager/formatters/text-style.d.ts.map +1 -0
  105. package/dist/lib/codex-manager/formatters/text-style.js +111 -0
  106. package/dist/lib/codex-manager/health-check.d.ts +14 -0
  107. package/dist/lib/codex-manager/health-check.d.ts.map +1 -0
  108. package/dist/lib/codex-manager/health-check.js +278 -0
  109. package/dist/lib/codex-manager/help.js +0 -1
  110. package/dist/lib/codex-manager/login-action-panel.d.ts +4 -0
  111. package/dist/lib/codex-manager/login-action-panel.d.ts.map +1 -0
  112. package/dist/lib/codex-manager/login-action-panel.js +238 -0
  113. package/dist/lib/codex-manager/login-flow.d.ts +17 -0
  114. package/dist/lib/codex-manager/login-flow.d.ts.map +1 -0
  115. package/dist/lib/codex-manager/login-flow.js +387 -0
  116. package/dist/lib/codex-manager/login-menu-actions.d.ts +20 -0
  117. package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -0
  118. package/dist/lib/codex-manager/login-menu-actions.js +303 -0
  119. package/dist/lib/codex-manager/login-menu-data.d.ts +19 -0
  120. package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -0
  121. package/dist/lib/codex-manager/login-menu-data.js +378 -0
  122. package/dist/lib/codex-manager/login-oauth.d.ts +55 -0
  123. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -0
  124. package/dist/lib/codex-manager/login-oauth.js +345 -0
  125. package/dist/lib/codex-manager/manual-callback.d.ts +33 -0
  126. package/dist/lib/codex-manager/manual-callback.d.ts.map +1 -0
  127. package/dist/lib/codex-manager/manual-callback.js +30 -0
  128. package/dist/lib/codex-manager/manual-callback.js.map +1 -0
  129. package/dist/lib/codex-manager/persist-selected-account.d.ts +24 -0
  130. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -0
  131. package/dist/lib/codex-manager/persist-selected-account.js +107 -0
  132. package/dist/lib/codex-manager/quota-cache-helpers.d.ts +18 -0
  133. package/dist/lib/codex-manager/quota-cache-helpers.d.ts.map +1 -0
  134. package/dist/lib/codex-manager/quota-cache-helpers.js +112 -0
  135. package/dist/lib/codex-manager/rate-limit-markers.js +0 -1
  136. package/dist/lib/codex-manager/repair-commands.js +0 -1
  137. package/dist/lib/codex-manager/settings-hub/backend.js +1 -2
  138. package/dist/lib/codex-manager/settings-hub/dashboard.js +1 -2
  139. package/dist/lib/codex-manager/settings-hub/experimental.js +1 -2
  140. package/dist/lib/codex-manager/settings-hub/index.js +1 -2
  141. package/dist/lib/codex-manager/settings-hub/shared.js +0 -1
  142. package/dist/lib/codex-manager/settings-hub-entry.js +0 -1
  143. package/dist/lib/codex-manager/settings-hub-menu.js +0 -1
  144. package/dist/lib/codex-manager/settings-hub-prompt.js +0 -1
  145. package/dist/lib/codex-manager/settings-hub.js +0 -1
  146. package/dist/lib/codex-manager/settings-panels.js +0 -1
  147. package/dist/lib/codex-manager/settings-persist-utils.js +0 -1
  148. package/dist/lib/codex-manager/settings-preview.js +0 -1
  149. package/dist/lib/codex-manager/settings-write-queue.js +0 -1
  150. package/dist/lib/codex-manager/statusline-order.js +0 -1
  151. package/dist/lib/codex-manager/statusline-settings-panel.d.ts +1 -1
  152. package/dist/lib/codex-manager/statusline-settings-panel.d.ts.map +1 -1
  153. package/dist/lib/codex-manager/statusline-settings-panel.js +0 -1
  154. package/dist/lib/codex-manager/theme-settings-panel.d.ts +1 -1
  155. package/dist/lib/codex-manager/theme-settings-panel.d.ts.map +1 -1
  156. package/dist/lib/codex-manager/theme-settings-panel.js +0 -1
  157. package/dist/lib/codex-manager/unified-settings-controller.js +0 -1
  158. package/dist/lib/codex-manager/unified-settings-entry.js +0 -1
  159. package/dist/lib/codex-manager.d.ts +2 -4
  160. package/dist/lib/codex-manager.d.ts.map +1 -1
  161. package/dist/lib/codex-manager.js +219 -2578
  162. package/dist/lib/codex-manager.js.map +1 -1
  163. package/dist/lib/config-schema.d.ts +32 -0
  164. package/dist/lib/config-schema.d.ts.map +1 -0
  165. package/dist/lib/config-schema.js +96 -0
  166. package/dist/lib/config.d.ts.map +1 -1
  167. package/dist/lib/config.js +60 -104
  168. package/dist/lib/constants.js +0 -1
  169. package/dist/lib/context-overflow.js +0 -1
  170. package/dist/lib/dashboard-settings.js +0 -1
  171. package/dist/lib/entitlement-cache.js +0 -1
  172. package/dist/lib/env-parsing.js +0 -1
  173. package/dist/lib/errors.js +0 -1
  174. package/dist/lib/forecast.js +0 -1
  175. package/dist/lib/fs-retry.d.ts +46 -0
  176. package/dist/lib/fs-retry.d.ts.map +1 -1
  177. package/dist/lib/fs-retry.js +74 -6
  178. package/dist/lib/health.js +0 -1
  179. package/dist/lib/index.js +0 -1
  180. package/dist/lib/integration-generators.js +0 -1
  181. package/dist/lib/live-account-sync.js +0 -1
  182. package/dist/lib/local-bridge.js +0 -1
  183. package/dist/lib/local-client-tokens.js +0 -1
  184. package/dist/lib/logger.js +0 -1
  185. package/dist/lib/model-capability-matrix.js +0 -1
  186. package/dist/lib/named-backup-export.js +0 -1
  187. package/dist/lib/oc-chatgpt-import-adapter.js +0 -1
  188. package/dist/lib/oc-chatgpt-orchestrator.js +0 -1
  189. package/dist/lib/oc-chatgpt-target-detection.js +0 -1
  190. package/dist/lib/parallel-probe.js +0 -1
  191. package/dist/lib/policy/runtime-policy.js +0 -1
  192. package/dist/lib/preemptive-quota-scheduler.js +0 -1
  193. package/dist/lib/proactive-refresh.js +0 -1
  194. package/dist/lib/prompts/codex-host-bridge.js +0 -1
  195. package/dist/lib/prompts/codex.d.ts +3 -11
  196. package/dist/lib/prompts/codex.d.ts.map +1 -1
  197. package/dist/lib/prompts/codex.js +4 -12
  198. package/dist/lib/prompts/fetch-utils.js +0 -1
  199. package/dist/lib/prompts/host-codex-prompt.js +0 -1
  200. package/dist/lib/quota-cache.d.ts.map +1 -1
  201. package/dist/lib/quota-cache.js +15 -43
  202. package/dist/lib/quota-probe.js +0 -1
  203. package/dist/lib/quota-readiness.js +0 -1
  204. package/dist/lib/recovery/constants.js +0 -1
  205. package/dist/lib/recovery/index.js +0 -1
  206. package/dist/lib/recovery/storage.d.ts.map +1 -1
  207. package/dist/lib/recovery/storage.js +30 -51
  208. package/dist/lib/recovery/storage.js.map +1 -1
  209. package/dist/lib/recovery/types.js +0 -1
  210. package/dist/lib/recovery.js +0 -1
  211. package/dist/lib/refresh-guardian.js +0 -1
  212. package/dist/lib/refresh-lease.d.ts.map +1 -1
  213. package/dist/lib/refresh-lease.js +1 -4
  214. package/dist/lib/refresh-queue.js +0 -1
  215. package/dist/lib/request/error-classification.d.ts +75 -0
  216. package/dist/lib/request/error-classification.d.ts.map +1 -0
  217. package/dist/lib/request/error-classification.js +295 -0
  218. package/dist/lib/request/failover-config.js +0 -1
  219. package/dist/lib/request/failure-policy.js +0 -1
  220. package/dist/lib/request/fetch-helpers.d.ts +10 -114
  221. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  222. package/dist/lib/request/fetch-helpers.js +7 -589
  223. package/dist/lib/request/headers.d.ts +33 -0
  224. package/dist/lib/request/headers.d.ts.map +1 -0
  225. package/dist/lib/request/headers.js +64 -0
  226. package/dist/lib/request/helpers/input-utils.js +0 -1
  227. package/dist/lib/request/helpers/model-map.d.ts +10 -0
  228. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  229. package/dist/lib/request/helpers/model-map.js +11 -1
  230. package/dist/lib/request/helpers/tool-utils.js +0 -1
  231. package/dist/lib/request/index.js +0 -1
  232. package/dist/lib/request/rate-limit-backoff.js +0 -1
  233. package/dist/lib/request/rate-limit-decision.d.ts +28 -0
  234. package/dist/lib/request/rate-limit-decision.d.ts.map +1 -0
  235. package/dist/lib/request/rate-limit-decision.js +191 -0
  236. package/dist/lib/request/request-attempt-budget.js +0 -1
  237. package/dist/lib/request/request-init.js +0 -1
  238. package/dist/lib/request/request-resilience.js +0 -1
  239. package/dist/lib/request/request-transformer.js +0 -1
  240. package/dist/lib/request/response-compaction.js +0 -1
  241. package/dist/lib/request/response-handler.js +0 -1
  242. package/dist/lib/request/response-metadata.js +0 -1
  243. package/dist/lib/request/stream-failover-runtime.d.ts +8 -0
  244. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -0
  245. package/dist/lib/request/stream-failover-runtime.js +149 -0
  246. package/dist/lib/request/stream-failover.js +0 -1
  247. package/dist/lib/request/token-refresh.d.ts +30 -0
  248. package/dist/lib/request/token-refresh.d.ts.map +1 -0
  249. package/dist/lib/request/token-refresh.js +116 -0
  250. package/dist/lib/request/url-rewriting.d.ts +24 -0
  251. package/dist/lib/request/url-rewriting.d.ts.map +1 -0
  252. package/dist/lib/request/url-rewriting.js +153 -0
  253. package/dist/lib/request/wait-utils.js +0 -1
  254. package/dist/lib/rotation.d.ts +1 -1
  255. package/dist/lib/rotation.d.ts.map +1 -1
  256. package/dist/lib/rotation.js +0 -1
  257. package/dist/lib/routing-mutex.js +0 -1
  258. package/dist/lib/routing-profiles.js +0 -1
  259. package/dist/lib/runtime/account-check-helpers.js +0 -1
  260. package/dist/lib/runtime/account-check-types.js +0 -1
  261. package/dist/lib/runtime/account-check.js +0 -1
  262. package/dist/lib/runtime/account-health-check.js +0 -1
  263. package/dist/lib/runtime/account-manager-cache-entry.js +0 -1
  264. package/dist/lib/runtime/account-manager-cache.js +0 -1
  265. package/dist/lib/runtime/account-pool.js +0 -1
  266. package/dist/lib/runtime/account-scope.js +0 -1
  267. package/dist/lib/runtime/account-select-event.js +0 -1
  268. package/dist/lib/runtime/account-selection.js +0 -1
  269. package/dist/lib/runtime/account-state.js +0 -1
  270. package/dist/lib/runtime/account-status.js +0 -1
  271. package/dist/lib/runtime/account-storage-scope-entry.js +0 -1
  272. package/dist/lib/runtime/app-bind.js +0 -1
  273. package/dist/lib/runtime/auth-facade.js +0 -1
  274. package/dist/lib/runtime/browser-oauth-flow.js +0 -1
  275. package/dist/lib/runtime/capability-boost.js +0 -1
  276. package/dist/lib/runtime/config-toml.js +0 -1
  277. package/dist/lib/runtime/event-handler.js +0 -1
  278. package/dist/lib/runtime/first-run.d.ts +80 -0
  279. package/dist/lib/runtime/first-run.d.ts.map +1 -0
  280. package/dist/lib/runtime/first-run.js +327 -0
  281. package/dist/lib/runtime/flagged-verify-types.js +0 -1
  282. package/dist/lib/runtime/hydrate-emails.js +0 -1
  283. package/dist/lib/runtime/live-sync-entry.js +0 -1
  284. package/dist/lib/runtime/live-sync.js +0 -1
  285. package/dist/lib/runtime/loader-setup.js +0 -1
  286. package/dist/lib/runtime/login-menu-accounts.js +0 -1
  287. package/dist/lib/runtime/manual-oauth-flow.js +0 -1
  288. package/dist/lib/runtime/oauth-browser-flow.js +0 -1
  289. package/dist/lib/runtime/preemptive-quota.js +0 -1
  290. package/dist/lib/runtime/quota-headers.js +0 -1
  291. package/dist/lib/runtime/quota-probe.js +0 -1
  292. package/dist/lib/runtime/quota-settings.js +0 -1
  293. package/dist/lib/runtime/refresh-guardian-entry.js +0 -1
  294. package/dist/lib/runtime/refresh-guardian.js +0 -1
  295. package/dist/lib/runtime/request-init.js +0 -1
  296. package/dist/lib/runtime/rotation-account-selection.d.ts +38 -0
  297. package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -0
  298. package/dist/lib/runtime/rotation-account-selection.js +172 -0
  299. package/dist/lib/runtime/rotation-proxy-state.d.ts +54 -0
  300. package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -0
  301. package/dist/lib/runtime/rotation-proxy-state.js +56 -0
  302. package/dist/lib/runtime/rotation-server-types.d.ts +57 -0
  303. package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -0
  304. package/dist/lib/runtime/rotation-server-types.js +1 -0
  305. package/dist/lib/runtime/rotation-storage-meta.d.ts +40 -0
  306. package/dist/lib/runtime/rotation-storage-meta.d.ts.map +1 -0
  307. package/dist/lib/runtime/rotation-storage-meta.js +143 -0
  308. package/dist/lib/runtime/rotation-token-refresh.d.ts +25 -0
  309. package/dist/lib/runtime/rotation-token-refresh.d.ts.map +1 -0
  310. package/dist/lib/runtime/rotation-token-refresh.js +99 -0
  311. package/dist/lib/runtime/runtime-current-account.d.ts.map +1 -1
  312. package/dist/lib/runtime/runtime-current-account.js +1 -4
  313. package/dist/lib/runtime/runtime-observability.js +0 -1
  314. package/dist/lib/runtime/runtime-services.js +0 -1
  315. package/dist/lib/runtime/session-affinity-entry.js +0 -1
  316. package/dist/lib/runtime/session-affinity.js +0 -1
  317. package/dist/lib/runtime/session-recovery.js +0 -1
  318. package/dist/lib/runtime/status-marker.js +0 -1
  319. package/dist/lib/runtime/storage-scope.js +0 -1
  320. package/dist/lib/runtime/toast.js +0 -1
  321. package/dist/lib/runtime/ui-runtime-entry.js +0 -1
  322. package/dist/lib/runtime/ui-runtime.js +0 -1
  323. package/dist/lib/runtime/verify-flagged.js +0 -1
  324. package/dist/lib/runtime-constants.js +0 -1
  325. package/dist/lib/runtime-paths.js +0 -1
  326. package/dist/lib/runtime-rotation-proxy.d.ts +7 -124
  327. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  328. package/dist/lib/runtime-rotation-proxy.js +614 -1353
  329. package/dist/lib/schemas.d.ts +3 -3
  330. package/dist/lib/schemas.js +1 -2
  331. package/dist/lib/session-affinity.js +0 -1
  332. package/dist/lib/shutdown.js +0 -1
  333. package/dist/lib/storage/account-clear-entry.js +0 -1
  334. package/dist/lib/storage/account-clear.js +0 -1
  335. package/dist/lib/storage/account-match-utils.js +0 -1
  336. package/dist/lib/storage/account-persistence.d.ts +1 -1
  337. package/dist/lib/storage/account-persistence.d.ts.map +1 -1
  338. package/dist/lib/storage/account-persistence.js +0 -1
  339. package/dist/lib/storage/account-port.d.ts +1 -1
  340. package/dist/lib/storage/account-port.d.ts.map +1 -1
  341. package/dist/lib/storage/account-port.js +0 -1
  342. package/dist/lib/storage/account-save-entry.d.ts +1 -1
  343. package/dist/lib/storage/account-save-entry.d.ts.map +1 -1
  344. package/dist/lib/storage/account-save-entry.js +0 -1
  345. package/dist/lib/storage/account-save.d.ts +1 -1
  346. package/dist/lib/storage/account-save.d.ts.map +1 -1
  347. package/dist/lib/storage/account-save.js +0 -1
  348. package/dist/lib/storage/account-snapshot.js +0 -1
  349. package/dist/lib/storage/backup-metadata-builder.d.ts +1 -1
  350. package/dist/lib/storage/backup-metadata-builder.d.ts.map +1 -1
  351. package/dist/lib/storage/backup-metadata-builder.js +0 -1
  352. package/dist/lib/storage/backup-metadata.d.ts +13 -0
  353. package/dist/lib/storage/backup-metadata.d.ts.map +1 -1
  354. package/dist/lib/storage/backup-metadata.js +0 -1
  355. package/dist/lib/storage/backup-paths.js +0 -1
  356. package/dist/lib/storage/backup-restore.d.ts +1 -1
  357. package/dist/lib/storage/backup-restore.d.ts.map +1 -1
  358. package/dist/lib/storage/backup-restore.js +0 -1
  359. package/dist/lib/storage/cache-artifacts.js +0 -1
  360. package/dist/lib/storage/error-hints.js +0 -1
  361. package/dist/lib/storage/file-paths.js +0 -1
  362. package/dist/lib/storage/fixture-guards.d.ts +1 -1
  363. package/dist/lib/storage/fixture-guards.d.ts.map +1 -1
  364. package/dist/lib/storage/fixture-guards.js +0 -1
  365. package/dist/lib/storage/flagged-entry.js +0 -1
  366. package/dist/lib/storage/flagged-load-entry.d.ts +1 -1
  367. package/dist/lib/storage/flagged-load-entry.d.ts.map +1 -1
  368. package/dist/lib/storage/flagged-load-entry.js +0 -1
  369. package/dist/lib/storage/flagged-save-entry.d.ts +1 -1
  370. package/dist/lib/storage/flagged-save-entry.d.ts.map +1 -1
  371. package/dist/lib/storage/flagged-save-entry.js +0 -1
  372. package/dist/lib/storage/flagged-storage-file.d.ts +1 -1
  373. package/dist/lib/storage/flagged-storage-file.d.ts.map +1 -1
  374. package/dist/lib/storage/flagged-storage-file.js +0 -1
  375. package/dist/lib/storage/flagged-storage-io.d.ts +1 -1
  376. package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -1
  377. package/dist/lib/storage/flagged-storage-io.js +0 -1
  378. package/dist/lib/storage/flagged-storage.d.ts +1 -1
  379. package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
  380. package/dist/lib/storage/flagged-storage.js +15 -2
  381. package/dist/lib/storage/flagged-storage.js.map +1 -1
  382. package/dist/lib/storage/gitignore.js +0 -1
  383. package/dist/lib/storage/hash.js +0 -1
  384. package/dist/lib/storage/health.js +0 -1
  385. package/dist/lib/storage/identity.js +0 -1
  386. package/dist/lib/storage/import-export.d.ts +1 -1
  387. package/dist/lib/storage/import-export.d.ts.map +1 -1
  388. package/dist/lib/storage/import-export.js +0 -1
  389. package/dist/lib/storage/metadata-section.js +0 -1
  390. package/dist/lib/storage/migrations.d.ts +5 -48
  391. package/dist/lib/storage/migrations.d.ts.map +1 -1
  392. package/dist/lib/storage/migrations.js +5 -2
  393. package/dist/lib/storage/named-backup-entry.js +0 -1
  394. package/dist/lib/storage/named-backups-entry.d.ts +1 -1
  395. package/dist/lib/storage/named-backups-entry.d.ts.map +1 -1
  396. package/dist/lib/storage/named-backups-entry.js +0 -1
  397. package/dist/lib/storage/named-backups.js +0 -1
  398. package/dist/lib/storage/path-state.js +0 -1
  399. package/dist/lib/storage/paths.js +0 -1
  400. package/dist/lib/storage/project-migration.d.ts +1 -1
  401. package/dist/lib/storage/project-migration.d.ts.map +1 -1
  402. package/dist/lib/storage/project-migration.js +0 -1
  403. package/dist/lib/storage/public-types.d.ts +70 -0
  404. package/dist/lib/storage/public-types.d.ts.map +1 -0
  405. package/dist/lib/storage/public-types.js +6 -0
  406. package/dist/lib/storage/record-utils.js +0 -1
  407. package/dist/lib/storage/restore-assessment.d.ts +1 -1
  408. package/dist/lib/storage/restore-assessment.d.ts.map +1 -1
  409. package/dist/lib/storage/restore-assessment.js +0 -1
  410. package/dist/lib/storage/restore-backup-entry.d.ts +1 -1
  411. package/dist/lib/storage/restore-backup-entry.d.ts.map +1 -1
  412. package/dist/lib/storage/restore-backup-entry.js +0 -1
  413. package/dist/lib/storage/restore-metadata.d.ts +1 -1
  414. package/dist/lib/storage/restore-metadata.d.ts.map +1 -1
  415. package/dist/lib/storage/restore-metadata.js +0 -1
  416. package/dist/lib/storage/restore.d.ts +1 -1
  417. package/dist/lib/storage/restore.d.ts.map +1 -1
  418. package/dist/lib/storage/restore.js +0 -1
  419. package/dist/lib/storage/save-retry.d.ts +10 -0
  420. package/dist/lib/storage/save-retry.d.ts.map +1 -0
  421. package/dist/lib/storage/save-retry.js +26 -0
  422. package/dist/lib/storage/snapshot-inspectors.d.ts +1 -1
  423. package/dist/lib/storage/snapshot-inspectors.d.ts.map +1 -1
  424. package/dist/lib/storage/snapshot-inspectors.js +0 -1
  425. package/dist/lib/storage/storage-parser.d.ts +1 -1
  426. package/dist/lib/storage/storage-parser.d.ts.map +1 -1
  427. package/dist/lib/storage/storage-parser.js +0 -1
  428. package/dist/lib/storage/transactions.d.ts +1 -1
  429. package/dist/lib/storage/transactions.d.ts.map +1 -1
  430. package/dist/lib/storage/transactions.js +0 -1
  431. package/dist/lib/storage.d.ts +3 -24
  432. package/dist/lib/storage.d.ts.map +1 -1
  433. package/dist/lib/storage.js +17 -32
  434. package/dist/lib/table-formatter.js +0 -1
  435. package/dist/lib/tools/hashline-tools.js +0 -1
  436. package/dist/lib/types.js +0 -1
  437. package/dist/lib/ui/ansi.js +0 -1
  438. package/dist/lib/ui/auth-menu-builder.d.ts +89 -0
  439. package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -0
  440. package/dist/lib/ui/auth-menu-builder.js +350 -0
  441. package/dist/lib/ui/auth-menu.d.ts +2 -76
  442. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  443. package/dist/lib/ui/auth-menu.js +3 -349
  444. package/dist/lib/ui/confirm.js +0 -1
  445. package/dist/lib/ui/copy.d.ts +2 -0
  446. package/dist/lib/ui/copy.d.ts.map +1 -1
  447. package/dist/lib/ui/copy.js +2 -0
  448. package/dist/lib/ui/copy.js.map +1 -1
  449. package/dist/lib/ui/display-width.js +0 -1
  450. package/dist/lib/ui/format.js +0 -1
  451. package/dist/lib/ui/index.d.ts +10 -0
  452. package/dist/lib/ui/index.d.ts.map +1 -0
  453. package/dist/lib/ui/index.js +9 -0
  454. package/dist/lib/ui/runtime.js +0 -1
  455. package/dist/lib/ui/select.js +0 -1
  456. package/dist/lib/ui/theme.js +0 -1
  457. package/dist/lib/ui/ui-copy.d.ts +159 -0
  458. package/dist/lib/ui/ui-copy.d.ts.map +1 -0
  459. package/dist/lib/ui/ui-copy.js +163 -0
  460. package/dist/lib/unified-settings.js +0 -1
  461. package/dist/lib/update-notice.js +0 -1
  462. package/dist/lib/usage/index.js +0 -1
  463. package/dist/lib/usage/ledger.js +0 -1
  464. package/dist/lib/usage/pricing.js +0 -1
  465. package/dist/lib/usage/redaction.js +0 -1
  466. package/dist/lib/usage/types.js +0 -1
  467. package/dist/lib/utils.js +0 -1
  468. package/package.json +8 -8
  469. package/scripts/postinstall.js +34 -265
  470. package/scripts/preuninstall.js +2 -2
@@ -1,17 +1,15 @@
1
- import { stdin as input, stdout as output } from "node:process";
2
- import { createInterface } from "node:readline/promises";
3
- import { extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./accounts.js";
4
- import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, redactOAuthUrlForLog, REDIRECT_URI, } from "./auth/auth.js";
5
- import { runDeviceAuthFlow } from "./auth/device-auth.js";
6
- import { resolveOrgOverride } from "./auth/org-override.js";
7
- import { copyTextToClipboard, isBrowserLaunchSuppressed, openBrowserUrl, } from "./auth/browser.js";
8
- import { startLocalOAuthServer } from "./auth/server.js";
9
- import { promptAddAnotherAccount, promptLoginMode, } from "./cli.js";
1
+ import { extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, } from "./accounts.js";
10
2
  import { loadCodexCliState } from "./codex-cli/state.js";
11
3
  import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
12
- import { runBestCommand, } from "./codex-manager/commands/best.js";
4
+ import { parseBestArgs, printBestUsage, runBestCommand, } from "./codex-manager/commands/best.js";
5
+ import { applyTokenAccountIdentity, hasLikelyInvalidRefreshToken, hasUsableAccessToken, resolveStoredAccountIdentity, } from "./codex-manager/account-credentials.js";
6
+ import { runHealthCheck } from "./codex-manager/health-check.js";
7
+ import { runAuthLogin } from "./codex-manager/login-flow.js";
8
+ import { persistAndSyncSelectedAccount } from "./codex-manager/persist-selected-account.js";
9
+ import { cloneQuotaCacheData, pruneUnsafeQuotaEmailCacheEntry, updateQuotaCacheForAccount, } from "./codex-manager/quota-cache-helpers.js";
13
10
  import { runAccountCommand } from "./codex-manager/commands/account.js";
14
11
  import { ACCOUNT_MANAGER_COMMANDS } from "./codex-manager/account-manager-commands.js";
12
+ import { ensureFirstRunSetup } from "./runtime/first-run.js";
15
13
  import { runBudgetCommand } from "./codex-manager/commands/budget.js";
16
14
  import { runBridgeCommand } from "./codex-manager/commands/bridge.js";
17
15
  import { runCheckCommand } from "./codex-manager/commands/check.js";
@@ -19,7 +17,6 @@ import { runIntegrationsCommand } from "./codex-manager/commands/integrations.js
19
17
  import { runModelsCommand } from "./codex-manager/commands/models.js";
20
18
  import { runMonitorCommand } from "./codex-manager/commands/monitor.js";
21
19
  import { runConfigExplainCommand } from "./codex-manager/commands/config-explain.js";
22
- import { saveAccountsWithRetry } from "./codex-manager/forecast-report-shared.js";
23
20
  import { runDebugBundleCommand } from "./codex-manager/commands/debug-bundle.js";
24
21
  import { parseWhySelectedArgs, printWhySelectedUsage, runWhySelectedCommand, } from "./codex-manager/commands/why-selected.js";
25
22
  import { parseVerifyArgs, printVerifyUsage, runVerifyCommand, } from "./codex-manager/commands/verify.js";
@@ -37,146 +34,24 @@ import { runSwitchCommand } from "./codex-manager/commands/switch.js";
37
34
  import { runUnpinCommand } from "./codex-manager/commands/unpin.js";
38
35
  import { runWorkspaceCommand } from "./codex-manager/commands/workspace.js";
39
36
  import { runUsageCommand } from "./codex-manager/commands/usage.js";
40
- import { parseAuthLoginArgs, printUsage, } from "./codex-manager/help.js";
41
- import { applyUiThemeFromDashboardSettings, configureUnifiedSettings, resolveMenuLayoutMode, } from "./codex-manager/settings-hub.js";
37
+ import { printUsage } from "./codex-manager/help.js";
38
+ import { availabilityTone, formatBackupSavedAt, formatCompactQuotaSnapshot, formatRateLimitEntry, formatResultSummary, normalizeFailureDetail, riskTone, styleAccountDetailText, stylePromptText, styleQuotaSummary, } from "./codex-manager/formatters/index.js";
39
+ import { applyUiThemeFromDashboardSettings } from "./codex-manager/settings-hub.js";
42
40
  import { getCodexRuntimeRotationProxy, getPluginConfigExplainReport, loadPluginConfig, savePluginConfig, } from "./config.js";
43
41
  import { bindCodexAppRuntimeRotation, getAppBindStatus, unbindCodexAppRuntimeRotation, } from "./runtime/app-bind.js";
44
- import { ACCOUNT_LIMITS } from "./constants.js";
45
42
  import { DEFAULT_DASHBOARD_DISPLAY_SETTINGS, loadDashboardDisplaySettings, } from "./dashboard-settings.js";
46
43
  import { evaluateForecastAccounts, recommendForecastAccount, summarizeForecast, } from "./forecast.js";
47
- import { createLogger } from "./logger.js";
48
- import { MODEL_FAMILIES } from "./prompts/codex.js";
49
- import { DEFAULT_MODEL, getModelCapabilities, getModelProfile, resolveNormalizedModel, } from "./request/helpers/model-map.js";
50
- import { formatRateLimitEntry as formatAccountRateLimitEntry, getRateLimitResetTimeForFamily, resolveActiveIndex, } from "./runtime/account-status.js";
51
- import { buildQuotaEmailFallbackState, hasSafeQuotaEmailFallback, hasUniqueQuotaAccountId, isQuotaCacheEntryExhausted, normalizeQuotaAccountId, normalizeQuotaEmail, quotaLeftPercentFromUsed, } from "./quota-readiness.js";
52
- import { loadQuotaCache, saveQuotaCache, } from "./quota-cache.js";
53
- import { isDisplayCurrentAccount, readAppRuntimeHelperAccountSignal, resolveAccountCurrentMarkers, resolveRuntimeCurrentAccount, } from "./runtime/runtime-current-account.js";
54
- import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, CODEX_UNAVAILABLE_PROBE_NOTE, } from "./quota-probe.js";
55
- import { isCodexUnavailableError } from "./errors.js";
44
+ import { resolveActiveIndex } from "./runtime/account-status.js";
45
+ import { buildQuotaEmailFallbackState } from "./quota-readiness.js";
46
+ import { loadQuotaCache, saveQuotaCache } from "./quota-cache.js";
47
+ import { readAppRuntimeHelperAccountSignal } from "./runtime/runtime-current-account.js";
48
+ import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./quota-probe.js";
56
49
  import { queuedRefresh } from "./refresh-queue.js";
57
- import { clearAccounts, findMatchingAccountIndex, formatStorageErrorHint, inspectStorageHealth, getLastAccountsSaveTimestamp, getNamedBackups, getStoragePath, loadAccounts, loadFlaggedAccounts, readAffinityGenerationFromDisk, restoreAccountsFromBackup, StorageError, saveAccounts, setStoragePath, withAccountStorageTransaction, } from "./storage.js";
58
- import { ANSI } from "./ui/ansi.js";
59
- import { confirm } from "./ui/confirm.js";
60
- import { UI_COPY } from "./ui/copy.js";
61
- import { paintUiText, quotaToneFromLeftPercent } from "./ui/format.js";
62
- import { getUiRuntimeOptions } from "./ui/runtime.js";
63
- import { select } from "./ui/select.js";
64
- const log = createLogger("codex-manager");
65
- function stylePromptText(text, tone) {
66
- if (!output.isTTY)
67
- return text;
68
- const ui = getUiRuntimeOptions();
69
- if (ui.v2Enabled) {
70
- if (tone === "muted") {
71
- return `${ui.theme.colors.dim}${paintUiText(ui, text, "muted")}${ui.theme.colors.reset}`;
72
- }
73
- const mapped = tone === "accent" ? "primary" : tone;
74
- return paintUiText(ui, text, mapped);
75
- }
76
- const legacyCode = tone === "accent"
77
- ? ANSI.green
78
- : tone === "success"
79
- ? ANSI.green
80
- : tone === "warning"
81
- ? ANSI.yellow
82
- : tone === "danger"
83
- ? ANSI.red
84
- : ANSI.dim;
85
- return `${legacyCode}${text}${ANSI.reset}`;
86
- }
87
- function inspectRequestedModel(requestedModel) {
88
- const normalized = resolveNormalizedModel(requestedModel);
89
- const profile = getModelProfile(normalized);
90
- return {
91
- requested: requestedModel,
92
- normalized,
93
- remapped: requestedModel !== normalized,
94
- promptFamily: profile.promptFamily,
95
- capabilities: getModelCapabilities(normalized),
96
- };
97
- }
98
- function formatModelInspection(model) {
99
- const route = model.remapped
100
- ? `${model.requested} -> ${model.normalized}`
101
- : model.normalized;
102
- return [
103
- route,
104
- `prompt family ${model.promptFamily}`,
105
- `tool search ${model.capabilities.toolSearch ? "yes" : "no"}`,
106
- `computer use ${model.capabilities.computerUse ? "yes" : "no"}`,
107
- ].join(" | ");
108
- }
109
- function collapseWhitespace(value) {
110
- return value.replace(/\s+/g, " ").trim();
111
- }
112
- function formatReasonLabel(reason) {
113
- if (!reason)
114
- return undefined;
115
- const normalized = collapseWhitespace(reason.replace(/_/g, " "));
116
- return normalized.length > 0 ? normalized : undefined;
117
- }
118
- function extractErrorMessageFromPayload(payload) {
119
- if (!payload || typeof payload !== "object")
120
- return undefined;
121
- const record = payload;
122
- const directMessage = typeof record.message === "string"
123
- ? collapseWhitespace(record.message)
124
- : "";
125
- const directCode = typeof record.code === "string" ? collapseWhitespace(record.code) : "";
126
- if (directMessage) {
127
- if (directCode &&
128
- !directMessage.toLowerCase().includes(directCode.toLowerCase())) {
129
- return `${directMessage} [${directCode}]`;
130
- }
131
- return directMessage;
132
- }
133
- const nested = record.error;
134
- if (nested && typeof nested === "object") {
135
- return extractErrorMessageFromPayload(nested);
136
- }
137
- return undefined;
138
- }
139
- function parseStructuredErrorMessage(raw) {
140
- const trimmed = raw.trim();
141
- if (!trimmed)
142
- return undefined;
143
- const candidates = new Set([trimmed]);
144
- const firstBrace = trimmed.indexOf("{");
145
- const lastBrace = trimmed.lastIndexOf("}");
146
- if (firstBrace >= 0 && lastBrace > firstBrace) {
147
- candidates.add(trimmed.slice(firstBrace, lastBrace + 1));
148
- }
149
- for (const candidate of candidates) {
150
- try {
151
- const parsed = JSON.parse(candidate);
152
- const message = extractErrorMessageFromPayload(parsed);
153
- if (message)
154
- return message;
155
- }
156
- catch {
157
- // ignore non-JSON candidates
158
- }
159
- }
160
- return undefined;
161
- }
162
- function normalizeFailureDetail(message, reason) {
163
- const reasonLabel = formatReasonLabel(reason);
164
- const raw = message?.trim() || reasonLabel || "refresh failed";
165
- const structured = parseStructuredErrorMessage(raw);
166
- const normalized = collapseWhitespace(structured ?? raw);
167
- const bounded = normalized.length > 260 ? `${normalized.slice(0, 257)}...` : normalized;
168
- return bounded.length > 0 ? bounded : "refresh failed";
169
- }
170
- function joinStyledSegments(parts) {
171
- if (parts.length === 0)
172
- return "";
173
- const separator = stylePromptText(" | ", "muted");
174
- return parts.join(separator);
175
- }
176
- function formatResultSummary(segments) {
177
- const rendered = segments.map((segment) => stylePromptText(segment.text, segment.tone));
178
- return `${stylePromptText("Result:", "accent")} ${joinStyledSegments(rendered)}`;
179
- }
50
+ import { clearAccounts, findMatchingAccountIndex, inspectStorageHealth, getLastAccountsSaveTimestamp, getStoragePath, loadAccounts, loadFlaggedAccounts, saveAccounts, setStoragePath, withAccountStorageTransaction, } from "./storage.js";
51
+ // Formatter implementations moved to lib/codex-manager/formatters/ (audit
52
+ // roadmap §4.1.1 phase 1). Re-exported here so existing consumers importing
53
+ // from lib/codex-manager.js keep working unchanged.
54
+ export { formatBackupSavedAt, styleAccountDetailText };
180
55
  function createRepairCommandDeps() {
181
56
  return {
182
57
  stylePromptText,
@@ -195,112 +70,6 @@ function createRepairCommandDeps() {
195
70
  applyTokenAccountIdentity,
196
71
  };
197
72
  }
198
- function isOAuthCancellation(result) {
199
- const message = (result.message ?? result.reason ?? "").trim().toLowerCase();
200
- return message.includes("cancelled") || message.includes("canceled");
201
- }
202
- function styleQuotaSummary(summary) {
203
- const normalized = collapseWhitespace(summary);
204
- if (!normalized)
205
- return stylePromptText(summary, "muted");
206
- const segments = normalized
207
- .split("|")
208
- .map((segment) => segment.trim())
209
- .filter(Boolean);
210
- if (segments.length === 0)
211
- return stylePromptText(normalized, "muted");
212
- const rendered = segments.map((segment) => {
213
- if (/rate-limited/i.test(segment)) {
214
- return stylePromptText(segment, "danger");
215
- }
216
- const match = segment.match(/^([0-9a-zA-Z]+)\s+(\d{1,3})%$/);
217
- if (!match) {
218
- return stylePromptText(segment, "muted");
219
- }
220
- const windowLabel = match[1] ?? "";
221
- const leftPercent = Math.max(0, Math.min(100, Number.parseInt(match[2] ?? "", 10)));
222
- if (!Number.isFinite(leftPercent)) {
223
- return stylePromptText(segment, "muted");
224
- }
225
- const tone = quotaToneFromLeftPercent(leftPercent);
226
- return `${stylePromptText(windowLabel, "muted")} ${stylePromptText(`${leftPercent}%`, tone)}`;
227
- });
228
- return joinStyledSegments(rendered);
229
- }
230
- // Exported for unit tests: tone precedence (danger before warning) is
231
- // security/UX-relevant — a failure whose text contains "unavailable" must
232
- // render red, not be downgraded to yellow. See test/codex-manager-detail-tone.test.ts.
233
- export function styleAccountDetailText(detail, fallbackTone = "muted") {
234
- const compact = collapseWhitespace(detail);
235
- if (!compact)
236
- return stylePromptText("", fallbackTone);
237
- const quotaMatch = compact.match(/^(.*?)\(([^()]*\d{1,3}%[^()]*)\)(.*)$/);
238
- if (quotaMatch) {
239
- const prefix = (quotaMatch[1] ?? "").trim();
240
- const quota = (quotaMatch[2] ?? "").trim();
241
- const suffix = (quotaMatch[3] ?? "").trim();
242
- // danger wins across the WHOLE detail: a failure keyword anywhere — even
243
- // trapped inside the (…%) quota segment, e.g.
244
- // "signed in and working (live check failed: … 0%)" — must keep the prefix
245
- // red, never let a "working"/"ok" prefix render green over a real failure.
246
- const detailHasFailure = /failed|error|rate-limited/i.test(compact);
247
- const prefixTone = detailHasFailure
248
- ? "danger"
249
- : /\b(ok|working|succeeded|valid)\b/i.test(prefix)
250
- ? "success"
251
- : fallbackTone;
252
- const suffixTone =
253
- // danger wins first: a real failure whose text happens to contain
254
- // "unavailable"/"not available" (e.g. a 5xx "service not available")
255
- // must render red, not be downgraded to a yellow warning.
256
- /failed|error/i.test(suffix)
257
- ? "danger"
258
- : /re-login|stale|warning|retry|fallback|unavailable|not available/i.test(suffix)
259
- ? "warning"
260
- : "muted";
261
- const chunks = [];
262
- if (prefix)
263
- chunks.push(stylePromptText(prefix, prefixTone));
264
- chunks.push(`(${styleQuotaSummary(quota)})`);
265
- if (suffix)
266
- chunks.push(stylePromptText(suffix, suffixTone));
267
- return chunks.join(" ");
268
- }
269
- if (/rate-limited/i.test(compact))
270
- return stylePromptText(compact, "danger");
271
- if (/failed|error/i.test(compact))
272
- return stylePromptText(compact, "danger");
273
- if (/re-login|stale|warning|fallback|unavailable|not available/i.test(compact))
274
- return stylePromptText(compact, "warning");
275
- if (/\b(ok|working|succeeded|valid)\b/i.test(compact))
276
- return stylePromptText(compact, "success");
277
- return stylePromptText(compact, fallbackTone);
278
- }
279
- function riskTone(level) {
280
- if (level === "low")
281
- return "success";
282
- if (level === "medium")
283
- return "warning";
284
- return "danger";
285
- }
286
- function availabilityTone(availability) {
287
- if (availability === "ready")
288
- return "success";
289
- if (availability === "delayed")
290
- return "warning";
291
- return "danger";
292
- }
293
- function formatQuotaSnapshotForDashboard(snapshot, settings) {
294
- if (!settings.showQuotaDetails)
295
- return "live session OK";
296
- return `live session OK (${formatCompactQuotaSnapshot(snapshot)})`;
297
- }
298
- function isAbortError(error) {
299
- if (!(error instanceof Error))
300
- return false;
301
- const maybe = error;
302
- return maybe.name === "AbortError" || maybe.code === "ABORT_ERR";
303
- }
304
73
  const IMPLEMENTED_FEATURES = [
305
74
  { id: 1, name: "Multi-account OAuth login dashboard" },
306
75
  { id: 2, name: "Account add/update dedupe by token/id/email" },
@@ -312,1599 +81,38 @@ const IMPLEMENTED_FEATURES = [
312
81
  { id: 8, name: "Flagged account restore flow" },
313
82
  { id: 9, name: "Best account forecast engine" },
314
83
  { id: 10, name: "Forecast live quota probing" },
315
- { id: 11, name: "Auto-fix command (safe mode)" },
316
- { id: 12, name: "Doctor diagnostics command" },
317
- { id: 13, name: "JSON outputs for machine automation" },
318
- { id: 14, name: "Report generation command" },
319
- { id: 15, name: "Storage v3 normalization and migration" },
320
- { id: 16, name: "Storage backup and recovery journal" },
321
- { id: 17, name: "Project-scoped and global storage paths" },
322
- { id: 18, name: "Quota cache storage" },
323
- { id: 19, name: "Live account sync watcher" },
324
- { id: 20, name: "Session affinity store" },
325
- { id: 21, name: "Refresh queue dedupe (in-process)" },
326
- { id: 22, name: "Refresh lease dedupe (cross-process)" },
327
- { id: 23, name: "Token rotation mapping in refresh queue" },
328
- { id: 24, name: "Refresh guardian (proactive refresh)" },
329
- { id: 25, name: "Preemptive quota scheduler" },
330
- { id: 26, name: "Entitlement cache for unsupported models" },
331
- { id: 27, name: "Capability policy scoring store" },
332
- { id: 28, name: "Failure policy evaluation module" },
333
- { id: 29, name: "Streaming failover pipeline" },
334
- { id: 30, name: "Rate-limit backoff and cooldown handling" },
335
- { id: 31, name: "Host request transformer bridge" },
336
- { id: 32, name: "Prompt template sync with cache" },
337
- { id: 33, name: "Codex CLI active-account state sync" },
338
- { id: 34, name: "TUI quick-switch hotkeys (1-9)" },
339
- { id: 35, name: "TUI search and help toggles" },
340
- { id: 36, name: "TUI account detail hotkeys (S/R/E/D)" },
341
- { id: 37, name: "TUI settings hub (list/summary/behavior/theme)" },
342
- { id: 38, name: "Dashboard display customization" },
343
- { id: 39, name: "Unified color/theme runtime (v2 UI)" },
344
- { id: 40, name: "OAuth browser-first flow with manual callback fallback" },
345
- { id: 41, name: "Auto-switch to best account command" },
346
- ];
347
- function formatRateLimitEntry(account, now, family = "codex") {
348
- return formatAccountRateLimitEntry(account, now, formatWaitTime, family);
349
- }
350
- function quotaCacheEntryToSnapshot(entry) {
351
- return {
352
- status: entry.status,
353
- planType: entry.planType,
354
- model: entry.model,
355
- primary: {
356
- usedPercent: entry.primary.usedPercent,
357
- windowMinutes: entry.primary.windowMinutes,
358
- resetAtMs: entry.primary.resetAtMs,
359
- },
360
- secondary: {
361
- usedPercent: entry.secondary.usedPercent,
362
- windowMinutes: entry.secondary.windowMinutes,
363
- resetAtMs: entry.secondary.resetAtMs,
364
- },
365
- };
366
- }
367
- function formatCompactQuotaWindowLabel(windowMinutes) {
368
- if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
369
- return "quota";
370
- }
371
- if (windowMinutes % 1440 === 0)
372
- return `${windowMinutes / 1440}d`;
373
- if (windowMinutes % 60 === 0)
374
- return `${windowMinutes / 60}h`;
375
- return `${windowMinutes}m`;
376
- }
377
- function formatCompactQuotaPart(windowMinutes, usedPercent) {
378
- const label = formatCompactQuotaWindowLabel(windowMinutes);
379
- if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) {
380
- return null;
381
- }
382
- const left = quotaLeftPercentFromUsed(usedPercent);
383
- return `${label} ${left}%`;
384
- }
385
- function formatCompactQuotaSnapshot(snapshot, now = Date.now()) {
386
- const parts = [
387
- formatCompactQuotaPart(snapshot.primary.windowMinutes, snapshot.primary.usedPercent),
388
- formatCompactQuotaPart(snapshot.secondary.windowMinutes, snapshot.secondary.usedPercent),
389
- ].filter((value) => typeof value === "string" && value.length > 0);
390
- if (snapshot.status === 429) {
391
- parts.push("rate-limited");
392
- }
393
- if (isQuotaCacheEntryExhausted(snapshot, now)) {
394
- parts.push("quota-exhausted");
395
- }
396
- if (parts.length > 0) {
397
- return parts.join(" | ");
398
- }
399
- return formatQuotaSnapshotLine(snapshot);
400
- }
401
- function formatAccountQuotaSummary(entry, now = Date.now()) {
402
- const parts = [
403
- formatCompactQuotaPart(entry.primary.windowMinutes, entry.primary.usedPercent),
404
- formatCompactQuotaPart(entry.secondary.windowMinutes, entry.secondary.usedPercent),
405
- ].filter((value) => typeof value === "string" && value.length > 0);
406
- if (entry.status === 429) {
407
- parts.push("rate-limited");
408
- }
409
- if (isQuotaCacheEntryExhausted(entry, now)) {
410
- parts.push("quota-exhausted");
411
- }
412
- if (parts.length > 0) {
413
- return parts.join(" | ");
414
- }
415
- return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
416
- }
417
- function getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
418
- const accountId = normalizeQuotaAccountId(account.accountId);
419
- if (accountId &&
420
- hasUniqueQuotaAccountId(accounts, account) &&
421
- cache.byAccountId[accountId]) {
422
- return cache.byAccountId[accountId] ?? null;
423
- }
424
- const email = normalizeQuotaEmail(account.email);
425
- if (email &&
426
- hasSafeQuotaEmailFallback(emailFallbackState, account) &&
427
- cache.byEmail[email]) {
428
- return cache.byEmail[email] ?? null;
429
- }
430
- return null;
431
- }
432
- function getPersistedQuotaViewForAccount(cache, account, accounts, now, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
433
- const cachedEntry = cache
434
- ? getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState)
435
- : null;
436
- const persistedResetAt = getRateLimitResetTimeForFamily(account, now, "codex");
437
- if (typeof persistedResetAt !== "number") {
438
- return cachedEntry;
439
- }
440
- const cachedPrimaryResetAt = cachedEntry?.primary.resetAtMs ?? 0;
441
- const cachedSecondaryResetAt = cachedEntry?.secondary.resetAtMs ?? 0;
442
- if (cachedEntry?.status === 429 &&
443
- Math.max(cachedPrimaryResetAt, cachedSecondaryResetAt) >= persistedResetAt) {
444
- return cachedEntry;
445
- }
446
- return {
447
- updatedAt: cachedEntry?.updatedAt ?? now,
448
- status: 429,
449
- model: cachedEntry?.model ?? DEFAULT_MODEL,
450
- planType: cachedEntry?.planType,
451
- primary: {
452
- ...cachedEntry?.primary,
453
- resetAtMs: Math.max(cachedPrimaryResetAt, persistedResetAt),
454
- },
455
- secondary: cachedEntry?.secondary ?? {},
456
- };
457
- }
458
- function updateQuotaCacheForAccount(cache, account, snapshot, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
459
- const nextEntry = {
460
- updatedAt: Date.now(),
461
- status: snapshot.status,
462
- model: snapshot.model,
463
- planType: snapshot.planType,
464
- primary: {
465
- usedPercent: snapshot.primary.usedPercent,
466
- windowMinutes: snapshot.primary.windowMinutes,
467
- resetAtMs: snapshot.primary.resetAtMs,
468
- },
469
- secondary: {
470
- usedPercent: snapshot.secondary.usedPercent,
471
- windowMinutes: snapshot.secondary.windowMinutes,
472
- resetAtMs: snapshot.secondary.resetAtMs,
473
- },
474
- };
475
- let changed = false;
476
- const accountId = normalizeQuotaAccountId(account.accountId);
477
- const hasUniqueAccountId = accountId !== null && hasUniqueQuotaAccountId(accounts, account);
478
- if (hasUniqueAccountId) {
479
- cache.byAccountId[accountId] = nextEntry;
480
- changed = true;
481
- }
482
- const email = normalizeQuotaEmail(account.email);
483
- if (email &&
484
- hasSafeQuotaEmailFallback(emailFallbackState, account) &&
485
- !hasUniqueAccountId) {
486
- cache.byEmail[email] = nextEntry;
487
- changed = true;
488
- }
489
- else if (email && cache.byEmail[email]) {
490
- delete cache.byEmail[email];
491
- changed = true;
492
- }
493
- return changed;
494
- }
495
- function cloneQuotaCacheData(cache) {
496
- // Shallow spreading is safe because quota cache entries are always replaced,
497
- // never mutated in-place.
498
- return {
499
- byAccountId: { ...cache.byAccountId },
500
- byEmail: { ...cache.byEmail },
501
- };
502
- }
503
- function pruneUnsafeQuotaEmailCacheEntry(cache, email, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
504
- const normalizedEmail = normalizeQuotaEmail(email);
505
- if (!normalizedEmail || !cache.byEmail[normalizedEmail]) {
506
- return false;
507
- }
508
- const hasSafeFallbackAccount = accounts.some((account) => normalizeQuotaEmail(account.email) === normalizedEmail &&
509
- hasSafeQuotaEmailFallback(emailFallbackState, account));
510
- if (hasSafeFallbackAccount) {
511
- return false;
512
- }
513
- delete cache.byEmail[normalizedEmail];
514
- return true;
515
- }
516
- const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
517
- const DEFAULT_LIVE_PROBE_MODEL = DEFAULT_MODEL;
518
- const MENU_QUOTA_REFRESH_MODEL = DEFAULT_LIVE_PROBE_MODEL;
519
- function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
520
- if (account.enabled === false)
521
- return null;
522
- if (!hasUsableAccessToken(account, now))
523
- return null;
524
- const existing = getPersistedQuotaViewForAccount(cache, account, accounts, now, emailFallbackState);
525
- if (existing &&
526
- typeof existing.updatedAt === "number" &&
527
- Number.isFinite(existing.updatedAt) &&
528
- now - existing.updatedAt < maxAgeMs) {
529
- return null;
530
- }
531
- // Menu auto-refresh is cache-backed, so only probe when the result can be
532
- // written behind a safe lookup key for later reuse.
533
- const canStore = (normalizeQuotaAccountId(account.accountId) !== null &&
534
- hasUniqueQuotaAccountId(accounts, account)) ||
535
- hasSafeQuotaEmailFallback(emailFallbackState, account);
536
- if (!canStore)
537
- return null;
538
- const accessToken = account.accessToken;
539
- const accountId = accessToken
540
- ? (account.accountId ?? extractAccountId(accessToken))
541
- : account.accountId;
542
- if (!accountId || !accessToken)
543
- return null;
544
- return { accountId, accessToken };
545
- }
546
- function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now(), emailFallbackState = buildQuotaEmailFallbackState(storage.accounts)) {
547
- const targets = [];
548
- for (const account of storage.accounts) {
549
- const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState);
550
- if (!probeInput)
551
- continue;
552
- targets.push({
553
- account,
554
- accountId: probeInput.accountId,
555
- accessToken: probeInput.accessToken,
556
- });
557
- }
558
- return targets;
559
- }
560
- function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
561
- const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
562
- let count = 0;
563
- for (const account of storage.accounts) {
564
- if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState)) {
565
- count += 1;
566
- }
567
- }
568
- return count;
569
- }
570
- async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
571
- if (storage.accounts.length === 0) {
572
- return cache;
573
- }
574
- const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
575
- const nextCache = cloneQuotaCacheData(cache);
576
- const now = Date.now();
577
- const targets = collectMenuQuotaRefreshTargets(storage, nextCache, maxAgeMs, now, emailFallbackState);
578
- const total = targets.length;
579
- let processed = 0;
580
- onProgress?.(processed, total);
581
- let changed = false;
582
- for (const target of targets) {
583
- processed += 1;
584
- onProgress?.(processed, total);
585
- try {
586
- const snapshot = await fetchCodexQuotaSnapshot({
587
- accountId: target.accountId,
588
- accessToken: target.accessToken,
589
- model: MENU_QUOTA_REFRESH_MODEL,
590
- });
591
- changed =
592
- updateQuotaCacheForAccount(nextCache, target.account, snapshot, storage.accounts, emailFallbackState) || changed;
593
- }
594
- catch {
595
- // Keep existing cached values if probing fails.
596
- }
597
- }
598
- if (changed) {
599
- await saveQuotaCache(nextCache);
600
- }
601
- return nextCache;
602
- }
603
- const ACCESS_TOKEN_FRESH_WINDOW_MS = 5 * 60 * 1000;
604
- function hasUsableAccessToken(account, now) {
605
- if (!account.accessToken)
606
- return false;
607
- if (typeof account.expiresAt !== "number" ||
608
- !Number.isFinite(account.expiresAt))
609
- return false;
610
- return account.expiresAt - now > ACCESS_TOKEN_FRESH_WINDOW_MS;
611
- }
612
- function hasLikelyInvalidRefreshToken(refreshToken) {
613
- if (!refreshToken)
614
- return true;
615
- const trimmed = refreshToken.trim();
616
- if (trimmed.length < 20)
617
- return true;
618
- return trimmed.startsWith("token-");
619
- }
620
- function mapAccountStatus(account, isCurrentAccount, now, persistedQuotaEntry) {
621
- if (account.enabled === false)
622
- return "disabled";
623
- if (typeof account.coolingDownUntil === "number" &&
624
- account.coolingDownUntil > now) {
625
- return "cooldown";
626
- }
627
- if (persistedQuotaEntry && isQuotaCacheEntryExhausted(persistedQuotaEntry, now)) {
628
- return "quota-exhausted";
629
- }
630
- if (persistedQuotaEntry?.status === 429)
631
- return "rate-limited";
632
- const rateLimit = formatRateLimitEntry(account, now, "codex");
633
- if (rateLimit)
634
- return "rate-limited";
635
- if (isCurrentAccount)
636
- return "active";
637
- return "ok";
638
- }
639
- function parseLeftPercentFromQuotaSummary(summary, windowLabel) {
640
- if (!summary)
641
- return -1;
642
- const match = summary.match(new RegExp(`(?:^|\\|)\\s*${windowLabel}\\s+(\\d{1,3})%`, "i"));
643
- const value = Number.parseInt(match?.[1] ?? "", 10);
644
- if (!Number.isFinite(value))
645
- return -1;
646
- return Math.max(0, Math.min(100, value));
647
- }
648
- function readQuotaLeftPercent(account, windowLabel) {
649
- const direct = windowLabel === "5h"
650
- ? account.quota5hLeftPercent
651
- : account.quota7dLeftPercent;
652
- if (typeof direct === "number" && Number.isFinite(direct)) {
653
- return Math.max(0, Math.min(100, Math.round(direct)));
654
- }
655
- return parseLeftPercentFromQuotaSummary(account.quotaSummary, windowLabel);
656
- }
657
- function readQuotaFloorPercent(account) {
658
- return Math.min(readQuotaLeftPercent(account, "5h"), readQuotaLeftPercent(account, "7d"));
659
- }
660
- function accountStatusSortBucket(status) {
661
- switch (status) {
662
- case "active":
663
- case "ok":
664
- return 0;
665
- case "unknown":
666
- return 1;
667
- case "quota-exhausted":
668
- case "cooldown":
669
- case "rate-limited":
670
- return 2;
671
- case "disabled":
672
- case "error":
673
- case "flagged":
674
- return 3;
675
- default:
676
- return 1;
677
- }
678
- }
679
- function accountReadinessSortBucket(account) {
680
- const statusBucket = accountStatusSortBucket(account.status);
681
- if (statusBucket >= 2)
682
- return statusBucket;
683
- return account.quotaRateLimited || account.quotaExhausted ? 2 : statusBucket;
684
- }
685
- function compareReadyFirstAccounts(left, right) {
686
- const bucketDelta = accountReadinessSortBucket(left) - accountReadinessSortBucket(right);
687
- if (bucketDelta !== 0)
688
- return bucketDelta;
689
- const leftFloor = readQuotaFloorPercent(left);
690
- const rightFloor = readQuotaFloorPercent(right);
691
- if (leftFloor !== rightFloor)
692
- return rightFloor - leftFloor;
693
- const left5h = readQuotaLeftPercent(left, "5h");
694
- const right5h = readQuotaLeftPercent(right, "5h");
695
- if (left5h !== right5h)
696
- return right5h - left5h;
697
- const left7d = readQuotaLeftPercent(left, "7d");
698
- const right7d = readQuotaLeftPercent(right, "7d");
699
- if (left7d !== right7d)
700
- return right7d - left7d;
701
- const leftLastUsed = left.lastUsed ?? 0;
702
- const rightLastUsed = right.lastUsed ?? 0;
703
- if (leftLastUsed !== rightLastUsed)
704
- return rightLastUsed - leftLastUsed;
705
- const leftSource = left.sourceIndex ?? left.index;
706
- const rightSource = right.sourceIndex ?? right.index;
707
- return leftSource - rightSource;
708
- }
709
- function applyAccountMenuOrdering(accounts, displaySettings) {
710
- const sortEnabled = displaySettings.menuSortEnabled ??
711
- DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ??
712
- true;
713
- const sortMode = displaySettings.menuSortMode ??
714
- DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ??
715
- "ready-first";
716
- if (!sortEnabled || sortMode !== "ready-first") {
717
- return [...accounts];
718
- }
719
- const sorted = [...accounts].sort(compareReadyFirstAccounts);
720
- const pinCurrent = displaySettings.menuSortPinCurrent ??
721
- DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ??
722
- false;
723
- if (pinCurrent) {
724
- const currentIndex = sorted.findIndex((account) => account.isCurrentAccount);
725
- if (currentIndex > 0) {
726
- const current = sorted.splice(currentIndex, 1)[0];
727
- const first = sorted[0];
728
- if (current && first && compareReadyFirstAccounts(current, first) <= 0) {
729
- sorted.unshift(current);
730
- }
731
- else if (current) {
732
- sorted.splice(currentIndex, 0, current);
733
- }
734
- }
735
- }
736
- return sorted;
737
- }
738
- function toExistingAccountInfo(storage, quotaCache, displaySettings, runtimeCurrent = null) {
739
- const now = Date.now();
740
- const activeIndex = resolveActiveIndex(storage, "codex");
741
- const layoutMode = resolveMenuLayoutMode(displaySettings);
742
- const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
743
- const baseAccounts = storage.accounts.map((account, index) => {
744
- const entry = getPersistedQuotaViewForAccount(quotaCache, account, storage.accounts, now, emailFallbackState);
745
- const currentMarkers = resolveAccountCurrentMarkers(index, activeIndex, runtimeCurrent);
746
- const isCurrentAccount = isDisplayCurrentAccount(index, activeIndex, runtimeCurrent);
747
- const quotaExhausted = entry ? isQuotaCacheEntryExhausted(entry, now) : false;
748
- return {
749
- index,
750
- sourceIndex: index,
751
- accountId: account.accountId,
752
- accountLabel: account.accountLabel,
753
- email: account.email,
754
- addedAt: account.addedAt,
755
- lastUsed: account.lastUsed,
756
- status: mapAccountStatus(account, isCurrentAccount, now, entry),
757
- quotaSummary: (displaySettings.menuShowQuotaSummary ?? true) && entry
758
- ? formatAccountQuotaSummary(entry, now)
759
- : undefined,
760
- quota5hLeftPercent: quotaLeftPercentFromUsed(entry?.primary.usedPercent),
761
- quota5hResetAtMs: entry?.primary.resetAtMs,
762
- quota7dLeftPercent: quotaLeftPercentFromUsed(entry?.secondary.usedPercent),
763
- quota7dResetAtMs: entry?.secondary.resetAtMs,
764
- quotaRateLimited: entry?.status === 429,
765
- quotaExhausted,
766
- isCurrentAccount,
767
- isDefaultAccount: index === activeIndex,
768
- isRuntimeCurrentAccount: runtimeCurrent?.index === index,
769
- currentMarkers,
770
- enabled: account.enabled !== false,
771
- showStatusBadge: displaySettings.menuShowStatusBadge ?? true,
772
- showCurrentBadge: displaySettings.menuShowCurrentBadge ?? true,
773
- showLastUsed: displaySettings.menuShowLastUsed ?? true,
774
- showQuotaCooldown: displaySettings.menuShowQuotaCooldown ?? true,
775
- showHintsForUnselectedRows: layoutMode === "expanded-rows",
776
- highlightCurrentRow: displaySettings.menuHighlightCurrentRow ?? true,
777
- focusStyle: displaySettings.menuFocusStyle ?? "row-invert",
778
- statuslineFields: displaySettings.menuStatuslineFields ?? [
779
- "last-used",
780
- "limits",
781
- "status",
782
- ],
783
- };
784
- });
785
- const orderedAccounts = applyAccountMenuOrdering(baseAccounts, displaySettings);
786
- const quickSwitchUsesVisibleRows = displaySettings.menuSortQuickSwitchVisibleRow ?? true;
787
- return orderedAccounts.map((account, displayIndex) => ({
788
- ...account,
789
- index: displayIndex,
790
- quickSwitchNumber: quickSwitchUsesVisibleRows
791
- ? displayIndex + 1
792
- : (account.sourceIndex ?? displayIndex) + 1,
793
- }));
794
- }
795
- async function loadRuntimeCurrentSelectionForStorage(storage, now = Date.now()) {
796
- const [runtimeSnapshot, appBindStatus] = await Promise.all([
797
- loadPersistedRuntimeObservabilitySnapshot().catch(() => null),
798
- getAppBindStatus()
799
- .then((status) => (status.running ? status.router : null))
800
- .catch(() => null),
801
- ]);
802
- return resolveRuntimeCurrentAccount(storage, {
803
- runtimeSnapshot,
804
- appBindStatus,
805
- appHelperStatus: readAppRuntimeHelperAccountSignal(),
806
- }, { now });
807
- }
808
- function activeAccountMatchesCodexCliState(account, state) {
809
- if (!state)
810
- return true;
811
- const accountId = account.accountId?.trim();
812
- const activeAccountId = state.activeAccountId?.trim();
813
- if (accountId && activeAccountId) {
814
- return accountId === activeAccountId;
815
- }
816
- const email = sanitizeEmail(account.email);
817
- const activeEmail = sanitizeEmail(state.activeEmail);
818
- if (email && activeEmail) {
819
- return email === activeEmail;
820
- }
821
- return false;
822
- }
823
- async function syncCodexCliActiveSelectionIfDrifted(storage) {
824
- const activeIndex = resolveActiveIndex(storage, "codex");
825
- if (activeIndex < 0 || activeIndex >= storage.accounts.length) {
826
- return false;
827
- }
828
- const account = storage.accounts[activeIndex];
829
- if (!account) {
830
- return false;
831
- }
832
- try {
833
- const cliState = await loadCodexCliState({ forceRefresh: true });
834
- if (!cliState || activeAccountMatchesCodexCliState(account, cliState)) {
835
- return false;
836
- }
837
- return setCodexCliActiveSelection({
838
- accountId: account.accountId,
839
- email: account.email,
840
- accessToken: account.accessToken,
841
- refreshToken: account.refreshToken,
842
- expiresAt: account.expiresAt,
843
- });
844
- }
845
- catch {
846
- return false;
847
- }
848
- }
849
- /**
850
- * Resolve the account-id selection for freshly-minted tokens.
851
- *
852
- * The org-override precedence (explicit `login --org` wins over the ambient
853
- * CODEX_AUTH_ACCOUNT_ID env, for this call only) lives in the internal
854
- * lib/auth/org-override.ts module so it can be unit-tested without exporting this
855
- * CLI-internal function. Threading the org as a parameter avoids mutating
856
- * process.env for the duration of a login, which raced on concurrent re-entry.
857
- */
858
- function resolveAccountSelection(tokens, orgOverride) {
859
- const override = resolveOrgOverride(orgOverride);
860
- if (override) {
861
- return {
862
- ...tokens,
863
- accountIdOverride: override,
864
- accountIdSource: "manual",
865
- };
866
- }
867
- const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
868
- if (candidates.length === 0) {
869
- return tokens;
870
- }
871
- if (candidates.length === 1) {
872
- const [candidate] = candidates;
873
- if (candidate) {
874
- return {
875
- ...tokens,
876
- accountIdOverride: candidate.accountId,
877
- accountIdSource: candidate.source,
878
- accountLabel: candidate.label,
879
- };
880
- }
881
- }
882
- const best = selectBestAccountCandidate(candidates);
883
- if (!best) {
884
- return tokens;
885
- }
886
- return {
887
- ...tokens,
888
- accountIdOverride: best.accountId,
889
- accountIdSource: best.source ?? "token",
890
- accountLabel: best.label,
891
- };
892
- }
893
- function resolveStoredAccountIdentity(storedAccountId, storedAccountIdSource, tokenAccountId) {
894
- const accountId = resolveRequestAccountId(storedAccountId, storedAccountIdSource, tokenAccountId);
895
- if (!accountId) {
896
- return {};
897
- }
898
- if (!shouldUpdateAccountIdFromToken(storedAccountIdSource, storedAccountId)) {
899
- return {
900
- accountId,
901
- accountIdSource: storedAccountIdSource,
902
- };
903
- }
904
- return {
905
- accountId,
906
- accountIdSource: accountId === tokenAccountId ? "token" : storedAccountIdSource,
907
- };
908
- }
909
- function applyTokenAccountIdentity(account, tokenAccountId) {
910
- const nextIdentity = resolveStoredAccountIdentity(account.accountId, account.accountIdSource, tokenAccountId);
911
- if (!nextIdentity.accountId) {
912
- return false;
913
- }
914
- if (nextIdentity.accountId === account.accountId &&
915
- nextIdentity.accountIdSource === account.accountIdSource) {
916
- return false;
917
- }
918
- account.accountId = nextIdentity.accountId;
919
- account.accountIdSource = nextIdentity.accountIdSource;
920
- return true;
921
- }
922
- async function promptManualCallback(state, options = {}) {
923
- const useInteractivePrompt = input.isTTY && output.isTTY;
924
- if (!useInteractivePrompt && !options.allowNonTty) {
925
- return null;
926
- }
927
- const rl = createInterface({ input, output });
928
- try {
929
- if (useInteractivePrompt) {
930
- console.log("");
931
- console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
932
- }
933
- const answer = useInteractivePrompt
934
- ? await rl.question("◆ ")
935
- : await new Promise((resolve, reject) => {
936
- if (input.readableEnded || input.destroyed) {
937
- resolve(null);
938
- return;
939
- }
940
- let settled = false;
941
- const handleInputClosed = () => {
942
- if (settled)
943
- return;
944
- settled = true;
945
- input.off("end", handleInputClosed);
946
- input.off("close", handleInputClosed);
947
- resolve(null);
948
- };
949
- const finish = (value) => {
950
- if (settled)
951
- return;
952
- settled = true;
953
- input.off("end", handleInputClosed);
954
- input.off("close", handleInputClosed);
955
- resolve(value);
956
- };
957
- const fail = (error) => {
958
- if (settled)
959
- return;
960
- settled = true;
961
- input.off("end", handleInputClosed);
962
- input.off("close", handleInputClosed);
963
- reject(error);
964
- };
965
- rl.question("")
966
- .then((value) => finish(value))
967
- .catch((error) => {
968
- if (isAbortError(error) || isReadlineClosedError(error)) {
969
- handleInputClosed();
970
- return;
971
- }
972
- fail(error);
973
- });
974
- input.once("end", handleInputClosed);
975
- input.once("close", handleInputClosed);
976
- });
977
- if (answer === null) {
978
- return null;
979
- }
980
- if (answer.includes("\u001b")) {
981
- return null;
982
- }
983
- const normalized = answer.trim().toLowerCase();
984
- if (normalized.length === 0 ||
985
- normalized === "q" ||
986
- normalized === "quit" ||
987
- normalized === "cancel" ||
988
- normalized === "back") {
989
- return null;
990
- }
991
- const parsed = parseAuthorizationInput(answer);
992
- if (!parsed.code || !parsed.state)
993
- return null;
994
- if (parsed.state !== state)
995
- return null;
996
- return parsed.code;
997
- }
998
- catch (error) {
999
- if (isAbortError(error) || isReadlineClosedError(error)) {
1000
- return null;
1001
- }
1002
- throw error;
1003
- }
1004
- finally {
1005
- rl.close();
1006
- }
1007
- }
1008
- function isReadlineClosedError(error) {
1009
- if (!(error instanceof Error)) {
1010
- return false;
1011
- }
1012
- const errorCode = typeof error === "object" && error !== null && "code" in error
1013
- ? String(error.code)
1014
- : "";
1015
- return (errorCode === "ERR_USE_AFTER_CLOSE" ||
1016
- /readline was closed/i.test(error.message));
1017
- }
1018
- export function formatBackupSavedAt(mtimeMs) {
1019
- return new Date(mtimeMs).toLocaleString(undefined, {
1020
- month: "short",
1021
- day: "numeric",
1022
- year: "numeric",
1023
- hour: "numeric",
1024
- minute: "2-digit",
1025
- });
1026
- }
1027
- async function promptOAuthSignInMode(backupOption, backupDiscoveryWarning = null) {
1028
- if (!input.isTTY || !output.isTTY) {
1029
- return "browser";
1030
- }
1031
- const ui = getUiRuntimeOptions();
1032
- const items = [
1033
- {
1034
- label: UI_COPY.oauth.signInHeading,
1035
- value: "cancel",
1036
- kind: "heading",
1037
- },
1038
- { label: UI_COPY.oauth.openBrowser, value: "browser", color: "green" },
1039
- { label: UI_COPY.oauth.manualMode, value: "manual", color: "yellow" },
1040
- ...(backupOption
1041
- ? [
1042
- { separator: true, label: "", value: "cancel" },
1043
- {
1044
- label: UI_COPY.oauth.restoreHeading,
1045
- value: "cancel",
1046
- kind: "heading",
1047
- },
1048
- {
1049
- label: UI_COPY.oauth.restoreSavedBackup,
1050
- value: "restore-backup",
1051
- hint: UI_COPY.oauth.loadLastBackupHint(backupOption.fileName, backupOption.accountCount, formatBackupSavedAt(backupOption.mtimeMs)),
1052
- color: "cyan",
1053
- },
1054
- ]
1055
- : []),
1056
- { separator: true, label: "", value: "cancel" },
1057
- { label: UI_COPY.oauth.back, value: "cancel", color: "red" },
1058
- ];
1059
- const selected = await select(items, {
1060
- message: UI_COPY.oauth.chooseModeTitle,
1061
- subtitle: backupDiscoveryWarning
1062
- ? `${UI_COPY.oauth.chooseModeSubtitle} ${backupDiscoveryWarning}`
1063
- : UI_COPY.oauth.chooseModeSubtitle,
1064
- help: backupOption
1065
- ? UI_COPY.oauth.chooseModeHelpWithBackup
1066
- : UI_COPY.oauth.chooseModeHelp,
1067
- clearScreen: true,
1068
- theme: ui.theme,
1069
- selectedEmphasis: "minimal",
1070
- allowEscape: false,
1071
- onInput: (raw) => {
1072
- const lower = raw.toLowerCase();
1073
- if (lower === "q")
1074
- return "cancel";
1075
- if (lower === "1")
1076
- return "browser";
1077
- if (lower === "2")
1078
- return "manual";
1079
- if (lower === "3" && backupOption)
1080
- return "restore-backup";
1081
- return undefined;
1082
- },
1083
- });
1084
- return selected ?? "cancel";
1085
- }
1086
- async function promptBackupRestoreMode(latestBackup) {
1087
- if (!input.isTTY || !output.isTTY) {
1088
- return "latest";
1089
- }
1090
- const ui = getUiRuntimeOptions();
1091
- const items = [
1092
- {
1093
- label: UI_COPY.oauth.loadLastBackup,
1094
- value: "latest",
1095
- hint: `${UI_COPY.oauth.restoreBackupLatestHint}\n${UI_COPY.oauth.manualBackupHint(latestBackup.accountCount, formatBackupSavedAt(latestBackup.mtimeMs))}`,
1096
- color: "cyan",
1097
- },
1098
- {
1099
- label: UI_COPY.oauth.chooseBackupManually,
1100
- value: "manual",
1101
- color: "yellow",
1102
- },
1103
- { label: UI_COPY.oauth.back, value: "back", color: "red" },
1104
- ];
1105
- const selected = await select(items, {
1106
- message: UI_COPY.oauth.restoreBackupTitle,
1107
- subtitle: UI_COPY.oauth.restoreBackupSubtitle,
1108
- help: UI_COPY.oauth.restoreBackupHelp,
1109
- clearScreen: true,
1110
- theme: ui.theme,
1111
- selectedEmphasis: "minimal",
1112
- allowEscape: false,
1113
- onInput: (raw) => {
1114
- const lower = raw.toLowerCase();
1115
- if (lower === "q")
1116
- return "back";
1117
- if (lower === "1")
1118
- return "latest";
1119
- if (lower === "2")
1120
- return "manual";
1121
- return undefined;
1122
- },
1123
- });
1124
- return selected ?? "back";
1125
- }
1126
- async function promptManualBackupSelection(backups) {
1127
- if (!input.isTTY || !output.isTTY) {
1128
- return backups[0] ?? null;
1129
- }
1130
- const ui = getUiRuntimeOptions();
1131
- const items = backups.map((backup) => ({
1132
- label: backup.fileName,
1133
- value: backup,
1134
- hint: UI_COPY.oauth.manualBackupHint(backup.accountCount, formatBackupSavedAt(backup.mtimeMs)),
1135
- color: "cyan",
1136
- }));
1137
- items.push({ label: UI_COPY.oauth.back, value: null, color: "red" });
1138
- const selected = await select(items, {
1139
- message: UI_COPY.oauth.manualBackupTitle,
1140
- subtitle: UI_COPY.oauth.manualBackupSubtitle,
1141
- help: UI_COPY.oauth.manualBackupHelp,
1142
- clearScreen: true,
1143
- theme: ui.theme,
1144
- selectedEmphasis: "minimal",
1145
- allowEscape: false,
1146
- onInput: (raw) => {
1147
- if (raw.toLowerCase() === "q")
1148
- return null;
1149
- return undefined;
1150
- },
1151
- });
1152
- return selected;
1153
- }
1154
- async function waitForMenuReturn(options = {}) {
1155
- if (!input.isTTY || !output.isTTY) {
1156
- return;
1157
- }
1158
- const promptText = options.promptText ?? UI_COPY.returnFlow.continuePrompt;
1159
- const autoReturnMs = options.autoReturnMs ?? 0;
1160
- const pauseOnAnyKey = options.pauseOnAnyKey ?? true;
1161
- try {
1162
- let chunk;
1163
- do {
1164
- chunk = input.read();
1165
- } while (chunk !== null);
1166
- }
1167
- catch {
1168
- // best effort buffer drain
1169
- }
1170
- const writeInlineStatus = (message) => {
1171
- output.write(`\r${ANSI.clearLine}${stylePromptText(message, "muted")}`);
1172
- };
1173
- const clearInlineStatus = () => {
1174
- output.write(`\r${ANSI.clearLine}`);
1175
- };
1176
- if (autoReturnMs > 0) {
1177
- if (!pauseOnAnyKey) {
1178
- await new Promise((resolve) => setTimeout(resolve, autoReturnMs));
1179
- return;
1180
- }
1181
- const wasRaw = input.isRaw ?? false;
1182
- const endAt = Date.now() + autoReturnMs;
1183
- let lastShownSeconds = null;
1184
- const renderCountdown = () => {
1185
- const remainingMs = Math.max(0, endAt - Date.now());
1186
- const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1000));
1187
- if (lastShownSeconds === remainingSeconds)
1188
- return;
1189
- lastShownSeconds = remainingSeconds;
1190
- writeInlineStatus(UI_COPY.returnFlow.autoReturn(remainingSeconds));
1191
- };
1192
- renderCountdown();
1193
- const pinned = await new Promise((resolve) => {
1194
- let done = false;
1195
- const interval = setInterval(renderCountdown, 80);
1196
- let timeout = setTimeout(() => {
1197
- timeout = null;
1198
- if (!done) {
1199
- done = true;
1200
- cleanup();
1201
- resolve(false);
1202
- }
1203
- }, autoReturnMs);
1204
- const onData = () => {
1205
- if (done)
1206
- return;
1207
- done = true;
1208
- cleanup();
1209
- resolve(true);
1210
- };
1211
- const cleanup = () => {
1212
- clearInterval(interval);
1213
- if (timeout) {
1214
- clearTimeout(timeout);
1215
- timeout = null;
1216
- }
1217
- input.removeListener("data", onData);
1218
- try {
1219
- input.setRawMode(wasRaw);
1220
- }
1221
- catch {
1222
- // best effort restore
1223
- }
1224
- };
1225
- try {
1226
- input.setRawMode(true);
1227
- }
1228
- catch {
1229
- // if raw mode fails, keep countdown behavior
1230
- }
1231
- input.on("data", onData);
1232
- input.resume();
1233
- });
1234
- clearInlineStatus();
1235
- if (!pinned) {
1236
- return;
1237
- }
1238
- const paused = stylePromptText(UI_COPY.returnFlow.paused, "muted");
1239
- writeInlineStatus(paused);
1240
- await new Promise((resolve) => {
1241
- const wasRaw = input.isRaw ?? false;
1242
- const onData = () => {
1243
- cleanup();
1244
- resolve();
1245
- };
1246
- const cleanup = () => {
1247
- input.removeListener("data", onData);
1248
- try {
1249
- input.setRawMode(wasRaw);
1250
- }
1251
- catch {
1252
- // best effort restore
1253
- }
1254
- };
1255
- try {
1256
- input.setRawMode(true);
1257
- }
1258
- catch {
1259
- // best effort fallback
1260
- }
1261
- input.on("data", onData);
1262
- input.resume();
1263
- });
1264
- clearInlineStatus();
1265
- return;
1266
- }
1267
- const rl = createInterface({ input, output });
1268
- try {
1269
- const question = promptText.length > 0 ? `${stylePromptText(promptText, "muted")} ` : "";
1270
- output.write(`\r${ANSI.clearLine}`);
1271
- await rl.question(question);
1272
- }
1273
- catch (error) {
1274
- if (!isAbortError(error)) {
1275
- throw error;
1276
- }
1277
- }
1278
- finally {
1279
- rl.close();
1280
- clearInlineStatus();
1281
- }
1282
- }
1283
- function stringifyLogArgs(args) {
1284
- return args
1285
- .map((value) => {
1286
- if (typeof value === "string")
1287
- return value;
1288
- try {
1289
- return JSON.stringify(value);
1290
- }
1291
- catch {
1292
- return String(value);
1293
- }
1294
- })
1295
- .join(" ");
1296
- }
1297
- async function runActionPanel(title, stage, action, settings) {
1298
- if (!input.isTTY || !output.isTTY) {
1299
- await action();
1300
- return;
1301
- }
1302
- const spinnerFrames = ["-", "\\", "|", "/"];
1303
- let frame = 0;
1304
- let running = true;
1305
- let failed = null;
1306
- const captured = [];
1307
- const maxVisibleLines = Math.max(8, (output.rows ?? 24) - 8);
1308
- const previousLog = console.log;
1309
- const previousWarn = console.warn;
1310
- const previousError = console.error;
1311
- const capture = (prefix, args) => {
1312
- const line = stringifyLogArgs(args).trim();
1313
- if (!line)
1314
- return;
1315
- captured.push(prefix ? `${prefix}${line}` : line);
1316
- if (captured.length > 400) {
1317
- captured.splice(0, captured.length - 400);
1318
- }
1319
- };
1320
- const render = () => {
1321
- output.write(ANSI.clearScreen + ANSI.moveTo(1, 1));
1322
- const spinner = running
1323
- ? `${spinnerFrames[frame % spinnerFrames.length] ?? "-"} `
1324
- : failed
1325
- ? "x "
1326
- : "+ ";
1327
- const stageText = running
1328
- ? `${spinner}${stage}`
1329
- : failed
1330
- ? UI_COPY.returnFlow.failed
1331
- : UI_COPY.returnFlow.done;
1332
- previousLog(stylePromptText(title, "accent"));
1333
- previousLog(stylePromptText(stageText, failed ? "danger" : running ? "accent" : "success"));
1334
- previousLog("");
1335
- const lines = captured.slice(-maxVisibleLines);
1336
- for (const line of lines) {
1337
- previousLog(line);
1338
- }
1339
- const remainingLines = Math.max(0, maxVisibleLines - lines.length);
1340
- for (let i = 0; i < remainingLines; i += 1) {
1341
- previousLog("");
1342
- }
1343
- previousLog("");
1344
- if (running)
1345
- previousLog(stylePromptText(UI_COPY.returnFlow.working, "muted"));
1346
- frame += 1;
1347
- };
1348
- console.log = (...args) => {
1349
- capture("", args);
1350
- };
1351
- console.warn = (...args) => {
1352
- capture("! ", args);
1353
- };
1354
- console.error = (...args) => {
1355
- capture("x ", args);
1356
- };
1357
- output.write(ANSI.altScreenOn + ANSI.hide);
1358
- let timer = null;
1359
- try {
1360
- render();
1361
- timer = setInterval(() => {
1362
- if (!running)
1363
- return;
1364
- render();
1365
- }, 120);
1366
- await action();
1367
- }
1368
- catch (error) {
1369
- failed = error;
1370
- capture("x ", [error instanceof Error ? error.message : String(error)]);
1371
- }
1372
- finally {
1373
- running = false;
1374
- if (timer) {
1375
- clearInterval(timer);
1376
- timer = null;
1377
- }
1378
- render();
1379
- console.log = previousLog;
1380
- console.warn = previousWarn;
1381
- console.error = previousError;
1382
- }
1383
- if (failed) {
1384
- await waitForMenuReturn({
1385
- promptText: UI_COPY.returnFlow.actionFailedPrompt,
1386
- });
1387
- }
1388
- else {
1389
- await waitForMenuReturn({
1390
- autoReturnMs: settings?.actionAutoReturnMs ?? 2_000,
1391
- pauseOnAnyKey: settings?.actionPauseOnKey ?? true,
1392
- });
1393
- }
1394
- output.write(ANSI.altScreenOff + ANSI.show + ANSI.clearScreen + ANSI.moveTo(1, 1));
1395
- if (failed) {
1396
- throw failed;
1397
- }
1398
- }
1399
- async function runOAuthFlow(forceNewLogin, signInMode) {
1400
- const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
1401
- let code = null;
1402
- let oauthServer = null;
1403
- try {
1404
- if (signInMode === "browser") {
1405
- try {
1406
- oauthServer = await startLocalOAuthServer({ state });
1407
- }
1408
- catch (serverError) {
1409
- log.warn("Local OAuth callback server unavailable; falling back to manual callback entry.", serverError instanceof Error
1410
- ? {
1411
- message: serverError.message,
1412
- stack: serverError.stack,
1413
- code: typeof serverError === "object" &&
1414
- serverError !== null &&
1415
- "code" in serverError
1416
- ? String(serverError.code)
1417
- : undefined,
1418
- }
1419
- : { error: String(serverError) });
1420
- oauthServer = null;
1421
- }
1422
- }
1423
- // Display the OAuth URL with sensitive query parameters (state,
1424
- // code, code_challenge, code_verifier) redacted so they do not leak
1425
- // into shell history, screen captures, CI transcripts, or clipboard
1426
- // managers. The full URL is still handed to the browser opener and
1427
- // the clipboard so sign-in continues to work end-to-end.
1428
- const displayUrl = redactOAuthUrlForLog(url);
1429
- if (signInMode === "browser") {
1430
- const opened = openBrowserUrl(url);
1431
- if (opened) {
1432
- console.log(stylePromptText(UI_COPY.oauth.browserOpened, "success"));
1433
- }
1434
- else {
1435
- console.log(stylePromptText(UI_COPY.oauth.browserOpenFail, "warning"));
1436
- console.log(`${stylePromptText(UI_COPY.oauth.goTo, "accent")} ${displayUrl}`);
1437
- const copied = copyTextToClipboard(url);
1438
- console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
1439
- }
1440
- }
1441
- else {
1442
- console.log(`${stylePromptText(UI_COPY.oauth.goTo, "accent")} ${displayUrl}`);
1443
- const copied = copyTextToClipboard(url);
1444
- console.log(stylePromptText(copied ? UI_COPY.oauth.copyOk : UI_COPY.oauth.copyFail, copied ? "success" : "warning"));
1445
- }
1446
- const waitingForCallback = signInMode === "browser" && oauthServer?.ready === true;
1447
- if (waitingForCallback && oauthServer) {
1448
- console.log(stylePromptText(UI_COPY.oauth.waitingCallback, "muted"));
1449
- const callbackResult = await oauthServer.waitForCode(state);
1450
- code = callbackResult?.code ?? null;
1451
- }
1452
- if (!code) {
1453
- console.log(stylePromptText(waitingForCallback
1454
- ? UI_COPY.oauth.callbackMissed
1455
- : signInMode === "manual"
1456
- ? UI_COPY.oauth.callbackBypassed
1457
- : UI_COPY.oauth.callbackUnavailable, "warning"));
1458
- code = await promptManualCallback(state, {
1459
- allowNonTty: signInMode === "manual",
1460
- });
1461
- }
1462
- }
1463
- finally {
1464
- oauthServer?.close();
1465
- }
1466
- if (!code) {
1467
- return {
1468
- type: "failed",
1469
- reason: "unknown",
1470
- message: UI_COPY.oauth.cancelled,
1471
- };
1472
- }
1473
- return exchangeAuthorizationCode(code, pkce.verifier, REDIRECT_URI);
1474
- }
1475
- async function runSignInFlow(forceNewLogin, signInMode, options = {}) {
1476
- if (signInMode === "device") {
1477
- // OpenAI owns the device-code account picker; there is no force-new-login
1478
- // equivalent to pass through for this mode.
1479
- // TODO: Thread a manager-level AbortSignal when login cancellation exists.
1480
- return runDeviceAuthFlow({
1481
- log: console.log,
1482
- timeoutMs: options.timeoutMs,
1483
- // CLI invocations rely on top-level await in scripts/codex-multi-auth.js;
1484
- // without keepAlive the polling timers unref and Node exits before the
1485
- // user can complete the browser step (issue #477).
1486
- keepAlive: true,
1487
- });
1488
- }
1489
- return runOAuthFlow(forceNewLogin, signInMode);
1490
- }
1491
- async function persistAccountPool(results, replaceAll) {
1492
- if (results.length === 0)
1493
- return;
1494
- await withAccountStorageTransaction(async (loadedStorage, persist) => {
1495
- const stored = replaceAll ? null : loadedStorage;
1496
- const now = Date.now();
1497
- const accounts = stored?.accounts ? [...stored.accounts] : [];
1498
- let selectedAccountIndex = null;
1499
- for (const result of results) {
1500
- const tokenAccountId = extractAccountId(result.access);
1501
- const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1502
- const accountIdSource = accountId
1503
- ? (result.accountIdSource ??
1504
- (result.accountIdOverride ? "manual" : "token"))
1505
- : undefined;
1506
- const accountLabel = result.accountLabel;
1507
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1508
- const existingIndex = findMatchingAccountIndex(accounts, {
1509
- accountId,
1510
- email: accountEmail,
1511
- refreshToken: result.refresh,
1512
- }, {
1513
- allowUniqueAccountIdFallbackWithoutEmail: true,
1514
- });
1515
- if (existingIndex === undefined) {
1516
- const newIndex = accounts.length;
1517
- accounts.push({
1518
- accountId,
1519
- accountIdSource,
1520
- accountLabel,
1521
- email: accountEmail,
1522
- refreshToken: result.refresh,
1523
- accessToken: result.access,
1524
- expiresAt: result.expires,
1525
- enabled: true,
1526
- addedAt: now,
1527
- lastUsed: now,
1528
- });
1529
- selectedAccountIndex = newIndex;
1530
- continue;
1531
- }
1532
- const existing = accounts[existingIndex];
1533
- if (!existing)
1534
- continue;
1535
- const nextEmail = accountEmail ?? existing.email;
1536
- const nextAccountId = accountId ?? existing.accountId;
1537
- const nextAccountIdSource = accountId
1538
- ? (accountIdSource ?? existing.accountIdSource)
1539
- : existing.accountIdSource;
1540
- accounts[existingIndex] = {
1541
- ...existing,
1542
- accountId: nextAccountId,
1543
- accountIdSource: nextAccountIdSource,
1544
- accountLabel: accountLabel ?? existing.accountLabel,
1545
- email: nextEmail,
1546
- refreshToken: result.refresh,
1547
- accessToken: result.access,
1548
- expiresAt: result.expires,
1549
- enabled: true,
1550
- lastUsed: now,
1551
- };
1552
- selectedAccountIndex = existingIndex;
1553
- }
1554
- const fallbackActiveIndex = accounts.length === 0
1555
- ? 0
1556
- : Math.max(0, Math.min(stored?.activeIndex ?? 0, accounts.length - 1));
1557
- const nextActiveIndex = accounts.length === 0
1558
- ? 0
1559
- : selectedAccountIndex === null
1560
- ? fallbackActiveIndex
1561
- : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1562
- const activeIndexByFamily = {};
1563
- for (const family of MODEL_FAMILIES) {
1564
- activeIndexByFamily[family] = nextActiveIndex;
1565
- }
1566
- await persist({
1567
- version: 3,
1568
- accounts,
1569
- activeIndex: nextActiveIndex,
1570
- activeIndexByFamily,
1571
- });
1572
- });
1573
- }
1574
- async function syncSelectionToCodex(tokens) {
1575
- const tokenAccountId = extractAccountId(tokens.access);
1576
- const accountId = resolveRequestAccountId(tokens.accountIdOverride, tokens.accountIdSource, tokenAccountId);
1577
- const email = sanitizeEmail(extractAccountEmail(tokens.access, tokens.idToken));
1578
- await setCodexCliActiveSelection({
1579
- accountId,
1580
- email,
1581
- accessToken: tokens.access,
1582
- refreshToken: tokens.refresh,
1583
- expiresAt: tokens.expires,
1584
- idToken: tokens.idToken,
1585
- });
1586
- }
1587
- async function runHealthCheck(options = {}) {
1588
- const forceRefresh = options.forceRefresh === true;
1589
- const liveProbe = options.liveProbe === true;
1590
- const probeModel = options.model?.trim() || DEFAULT_LIVE_PROBE_MODEL;
1591
- const modelInspection = inspectRequestedModel(probeModel);
1592
- const display = options.display ?? DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
1593
- const quotaCache = liveProbe ? await loadQuotaCache() : null;
1594
- const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
1595
- let quotaCacheChanged = false;
1596
- setStoragePath(null);
1597
- const storage = await loadAccounts();
1598
- if (!storage || storage.accounts.length === 0) {
1599
- console.log("No accounts configured.");
1600
- return;
1601
- }
1602
- let quotaEmailFallbackState = liveProbe && quotaCache
1603
- ? buildQuotaEmailFallbackState(storage.accounts)
1604
- : null;
1605
- let changed = false;
1606
- let ok = 0;
1607
- let failed = 0;
1608
- let warnings = 0;
1609
- let codexAvailable = 0;
1610
- let signedInOnly = 0;
1611
- const activeIndex = resolveActiveIndex(storage, "codex");
1612
- let activeAccountRefreshed = false;
1613
- const now = Date.now();
1614
- console.log(stylePromptText(forceRefresh
1615
- ? `Checking ${storage.accounts.length} account(s) with full refresh test...`
1616
- : `Checking ${storage.accounts.length} account(s) with quick check${liveProbe ? " + live check" : ""}...`, "accent"));
1617
- if (liveProbe) {
1618
- console.log(stylePromptText(`Model probe: ${formatModelInspection(modelInspection)}`, "muted"));
1619
- }
1620
- for (let i = 0; i < storage.accounts.length; i += 1) {
1621
- const account = storage.accounts[i];
1622
- if (!account)
1623
- continue;
1624
- const label = formatAccountLabel(account, i);
1625
- const labelText = stylePromptText(label, "accent");
1626
- const sessionLikelyValid = hasUsableAccessToken(account, now);
1627
- if (!forceRefresh && sessionLikelyValid) {
1628
- if (account.enabled === false) {
1629
- account.enabled = true;
1630
- changed = true;
1631
- }
1632
- if (i === activeIndex) {
1633
- activeAccountRefreshed = true;
1634
- }
1635
- let healthDetail = "signed in and working";
1636
- let healthTone = "success";
1637
- if (liveProbe) {
1638
- const currentAccessToken = account.accessToken;
1639
- const probeAccountId = currentAccessToken
1640
- ? (account.accountId ?? extractAccountId(currentAccessToken))
1641
- : undefined;
1642
- if (!probeAccountId || !currentAccessToken) {
1643
- warnings += 1;
1644
- signedInOnly += 1;
1645
- healthTone = "warning";
1646
- healthDetail =
1647
- "signed in (live check skipped: missing account ID)";
1648
- }
1649
- else {
1650
- try {
1651
- const snapshot = await fetchCodexQuotaSnapshot({
1652
- accountId: probeAccountId,
1653
- accessToken: currentAccessToken,
1654
- model: modelInspection.normalized,
1655
- });
1656
- if (workingQuotaCache) {
1657
- quotaCacheChanged =
1658
- updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
1659
- }
1660
- healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
1661
- codexAvailable += 1;
1662
- }
1663
- catch (error) {
1664
- warnings += 1;
1665
- signedInOnly += 1;
1666
- healthTone = "warning";
1667
- if (isCodexUnavailableError(error)) {
1668
- healthDetail =
1669
- `signed in; ${CODEX_UNAVAILABLE_PROBE_NOTE}`;
1670
- }
1671
- else {
1672
- const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
1673
- healthDetail = `signed in (live check failed: ${message})`;
1674
- }
1675
- }
1676
- }
1677
- }
1678
- if (hasLikelyInvalidRefreshToken(account.refreshToken)) {
1679
- healthDetail += " (re-login suggested soon)";
1680
- }
1681
- ok += 1;
1682
- if (display.showPerAccountRows) {
1683
- const healthMarker = healthTone === "success" ? "✓" : "!";
1684
- console.log(` ${stylePromptText(healthMarker, healthTone)} ${labelText} ${stylePromptText("|", "muted")} ${styleAccountDetailText(healthDetail, healthTone)}`);
1685
- }
1686
- continue;
1687
- }
1688
- const result = await queuedRefresh(account.refreshToken);
1689
- if (result.type === "success") {
1690
- const tokenAccountId = extractAccountId(result.access);
1691
- const nextEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1692
- const previousEmail = account.email;
1693
- let accountIdentityChanged = false;
1694
- if (account.refreshToken !== result.refresh) {
1695
- account.refreshToken = result.refresh;
1696
- changed = true;
1697
- }
1698
- if (account.accessToken !== result.access) {
1699
- account.accessToken = result.access;
1700
- changed = true;
1701
- }
1702
- if (account.expiresAt !== result.expires) {
1703
- account.expiresAt = result.expires;
1704
- changed = true;
1705
- }
1706
- if (nextEmail && nextEmail !== account.email) {
1707
- account.email = nextEmail;
1708
- changed = true;
1709
- accountIdentityChanged = true;
1710
- }
1711
- if (applyTokenAccountIdentity(account, tokenAccountId)) {
1712
- changed = true;
1713
- accountIdentityChanged = true;
1714
- }
1715
- if (account.enabled === false) {
1716
- account.enabled = true;
1717
- changed = true;
1718
- }
1719
- if (accountIdentityChanged && liveProbe && workingQuotaCache) {
1720
- quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
1721
- quotaCacheChanged =
1722
- pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
1723
- }
1724
- account.lastUsed = Date.now();
1725
- if (i === activeIndex) {
1726
- activeAccountRefreshed = true;
1727
- }
1728
- ok += 1;
1729
- let healthyMessage = "working now";
1730
- let healthyTone = "success";
1731
- if (liveProbe) {
1732
- const probeAccountId = account.accountId ?? tokenAccountId;
1733
- if (!probeAccountId) {
1734
- warnings += 1;
1735
- signedInOnly += 1;
1736
- healthyTone = "warning";
1737
- healthyMessage =
1738
- "signed in (live check skipped: missing account ID)";
1739
- }
1740
- else {
1741
- try {
1742
- const snapshot = await fetchCodexQuotaSnapshot({
1743
- accountId: probeAccountId,
1744
- accessToken: result.access,
1745
- model: modelInspection.normalized,
1746
- });
1747
- if (workingQuotaCache) {
1748
- quotaCacheChanged =
1749
- updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
1750
- }
1751
- healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
1752
- codexAvailable += 1;
1753
- }
1754
- catch (error) {
1755
- warnings += 1;
1756
- signedInOnly += 1;
1757
- healthyTone = "warning";
1758
- if (isCodexUnavailableError(error)) {
1759
- healthyMessage =
1760
- `signed in; ${CODEX_UNAVAILABLE_PROBE_NOTE}`;
1761
- }
1762
- else {
1763
- const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
1764
- healthyMessage = `signed in (live check failed: ${message})`;
1765
- }
1766
- }
1767
- }
1768
- }
1769
- if (display.showPerAccountRows) {
1770
- const healthyMarker = healthyTone === "success" ? "✓" : "!";
1771
- console.log(` ${stylePromptText(healthyMarker, healthyTone)} ${labelText} ${stylePromptText("|", "muted")} ${styleAccountDetailText(healthyMessage, healthyTone)}`);
1772
- }
1773
- }
1774
- else {
1775
- const detail = normalizeFailureDetail(result.message, result.reason);
1776
- if (sessionLikelyValid) {
1777
- warnings += 1;
1778
- if (liveProbe) {
1779
- signedInOnly += 1;
1780
- }
1781
- if (display.showPerAccountRows) {
1782
- console.log(` ${stylePromptText("!", "warning")} ${labelText} ${stylePromptText("|", "muted")} ${stylePromptText(`refresh failed (${detail}) but this account still works right now`, "warning")}`);
1783
- }
1784
- }
1785
- else {
1786
- failed += 1;
1787
- if (display.showPerAccountRows) {
1788
- console.log(` ${stylePromptText("✗", "danger")} ${labelText} ${stylePromptText("|", "muted")} ${stylePromptText(detail, "danger")}`);
1789
- }
1790
- }
1791
- }
1792
- }
1793
- if (!display.showPerAccountRows) {
1794
- console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
1795
- }
1796
- if (workingQuotaCache && quotaCacheChanged) {
1797
- try {
1798
- await saveQuotaCache(workingQuotaCache);
1799
- }
1800
- catch (error) {
1801
- // Quota cache is a derived artifact; a transient Windows EBUSY/EPERM
1802
- // here must not abort the health check before account fixes commit.
1803
- console.warn(`Quota cache save failed: ${error instanceof Error ? error.message : String(error)}`);
1804
- }
1805
- }
1806
- if (changed) {
1807
- await saveAccountsWithRetry(storage, saveAccounts);
1808
- }
1809
- if (activeAccountRefreshed &&
1810
- activeIndex >= 0 &&
1811
- activeIndex < storage.accounts.length) {
1812
- const activeAccount = storage.accounts[activeIndex];
1813
- if (activeAccount) {
1814
- await setCodexCliActiveSelection({
1815
- accountId: activeAccount.accountId,
1816
- email: activeAccount.email,
1817
- accessToken: activeAccount.accessToken,
1818
- refreshToken: activeAccount.refreshToken,
1819
- expiresAt: activeAccount.expiresAt,
1820
- });
1821
- }
1822
- }
1823
- console.log("");
1824
- console.log(formatResultSummary(liveProbe
1825
- ? [
1826
- {
1827
- text: `${codexAvailable} Codex available`,
1828
- tone: codexAvailable > 0 ? "success" : "muted",
1829
- },
1830
- {
1831
- text: `${signedInOnly} signed in only`,
1832
- tone: signedInOnly > 0 ? "warning" : "muted",
1833
- },
1834
- {
1835
- text: `${failed} need re-login`,
1836
- tone: failed > 0 ? "danger" : "muted",
1837
- },
1838
- ]
1839
- : [
1840
- { text: `${ok} working`, tone: "success" },
1841
- {
1842
- text: `${failed} need re-login`,
1843
- tone: failed > 0 ? "danger" : "muted",
1844
- },
1845
- {
1846
- text: `${warnings} warning${warnings === 1 ? "" : "s"}`,
1847
- tone: warnings > 0 ? "warning" : "muted",
1848
- },
1849
- ]));
1850
- }
1851
- function printBestUsage() {
1852
- console.log([
1853
- "Usage:",
1854
- " codex-multi-auth best [--live] [--json] [--model <model>]",
1855
- "",
1856
- "Options:",
1857
- " --live, -l Probe live quota headers via Codex backend before switching",
1858
- " --json, -j Print machine-readable JSON output",
1859
- ` --model, -m Probe model for live mode (default: ${DEFAULT_LIVE_PROBE_MODEL})`,
1860
- "",
1861
- "Behavior:",
1862
- " - Chooses the healthiest account using forecast scoring",
1863
- " - Switches to the recommended account when it is not already active",
1864
- ].join("\n"));
1865
- }
1866
- function parseBestArgs(args) {
1867
- const options = {
1868
- live: false,
1869
- json: false,
1870
- model: DEFAULT_LIVE_PROBE_MODEL,
1871
- modelProvided: false,
1872
- };
1873
- for (let i = 0; i < args.length; i += 1) {
1874
- const arg = args[i];
1875
- if (!arg)
1876
- continue;
1877
- if (arg === "--live" || arg === "-l") {
1878
- options.live = true;
1879
- continue;
1880
- }
1881
- if (arg === "--json" || arg === "-j") {
1882
- options.json = true;
1883
- continue;
1884
- }
1885
- if (arg === "--model" || arg === "-m") {
1886
- const value = args[i + 1]?.trim();
1887
- if (!value || value.startsWith("-")) {
1888
- return { ok: false, message: "Missing value for --model" };
1889
- }
1890
- options.model = value;
1891
- options.modelProvided = true;
1892
- i += 1;
1893
- continue;
1894
- }
1895
- if (arg.startsWith("--model=")) {
1896
- const value = arg.slice("--model=".length).trim();
1897
- if (!value || value.startsWith("-")) {
1898
- return { ok: false, message: "Missing value for --model" };
1899
- }
1900
- options.model = value;
1901
- options.modelProvided = true;
1902
- continue;
1903
- }
1904
- return { ok: false, message: `Unknown option: ${arg}` };
1905
- }
1906
- return { ok: true, options };
1907
- }
84
+ { id: 11, name: "Auto-fix command (safe mode)" },
85
+ { id: 12, name: "Doctor diagnostics command" },
86
+ { id: 13, name: "JSON outputs for machine automation" },
87
+ { id: 14, name: "Report generation command" },
88
+ { id: 15, name: "Storage v3 normalization and migration" },
89
+ { id: 16, name: "Storage backup and recovery journal" },
90
+ { id: 17, name: "Project-scoped and global storage paths" },
91
+ { id: 18, name: "Quota cache storage" },
92
+ { id: 19, name: "Live account sync watcher" },
93
+ { id: 20, name: "Session affinity store" },
94
+ { id: 21, name: "Refresh queue dedupe (in-process)" },
95
+ { id: 22, name: "Refresh lease dedupe (cross-process)" },
96
+ { id: 23, name: "Token rotation mapping in refresh queue" },
97
+ { id: 24, name: "Refresh guardian (proactive refresh)" },
98
+ { id: 25, name: "Preemptive quota scheduler" },
99
+ { id: 26, name: "Entitlement cache for unsupported models" },
100
+ { id: 27, name: "Capability policy scoring store" },
101
+ { id: 28, name: "Failure policy evaluation module" },
102
+ { id: 29, name: "Streaming failover pipeline" },
103
+ { id: 30, name: "Rate-limit backoff and cooldown handling" },
104
+ { id: 31, name: "Host request transformer bridge" },
105
+ { id: 32, name: "Prompt template sync with cache" },
106
+ { id: 33, name: "Codex CLI active-account state sync" },
107
+ { id: 34, name: "TUI quick-switch hotkeys (1-9)" },
108
+ { id: 35, name: "TUI search and help toggles" },
109
+ { id: 36, name: "TUI account detail hotkeys (S/R/E/D)" },
110
+ { id: 37, name: "TUI settings hub (list/summary/behavior/theme)" },
111
+ { id: 38, name: "Dashboard display customization" },
112
+ { id: 39, name: "Unified color/theme runtime (v2 UI)" },
113
+ { id: 40, name: "OAuth browser-first flow with manual callback fallback" },
114
+ { id: 41, name: "Auto-switch to best account command" },
115
+ ];
1908
116
  async function runForecast(args) {
1909
117
  return runForecastCommand(args, {
1910
118
  setStoragePath,
@@ -1938,593 +146,6 @@ async function runForecast(args) {
1938
146
  loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
1939
147
  });
1940
148
  }
1941
- async function clearAccountsAndReset() {
1942
- await clearAccounts();
1943
- }
1944
- function adjustManageActionSelectionIndex(currentIndex, removedIndex, remainingCount) {
1945
- if (remainingCount <= 0) {
1946
- return 0;
1947
- }
1948
- if (typeof currentIndex !== "number" || currentIndex < 0) {
1949
- return 0;
1950
- }
1951
- if (currentIndex < removedIndex) {
1952
- return Math.min(currentIndex, remainingCount - 1);
1953
- }
1954
- if (currentIndex > removedIndex) {
1955
- return currentIndex - 1;
1956
- }
1957
- return Math.min(removedIndex, remainingCount - 1);
1958
- }
1959
- function resetManageActionSelection(storage, removedIndex) {
1960
- const remainingCount = storage.accounts.length;
1961
- if (remainingCount <= 0) {
1962
- storage.activeIndex = 0;
1963
- storage.activeIndexByFamily = {};
1964
- for (const family of MODEL_FAMILIES) {
1965
- storage.activeIndexByFamily[family] = 0;
1966
- }
1967
- return;
1968
- }
1969
- const previousActiveIndex = storage.activeIndex;
1970
- const previousByFamily = { ...storage.activeIndexByFamily };
1971
- storage.activeIndex = adjustManageActionSelectionIndex(previousActiveIndex, removedIndex, remainingCount);
1972
- storage.activeIndexByFamily = {};
1973
- for (const family of MODEL_FAMILIES) {
1974
- storage.activeIndexByFamily[family] = adjustManageActionSelectionIndex(previousByFamily[family] ?? previousActiveIndex, removedIndex, remainingCount);
1975
- }
1976
- }
1977
- function replaceManageActionStorage(target, source) {
1978
- target.version = source.version;
1979
- target.accounts = structuredClone(source.accounts);
1980
- target.activeIndex = source.activeIndex;
1981
- target.activeIndexByFamily = {
1982
- ...source.activeIndexByFamily,
1983
- };
1984
- }
1985
- function resolveManageActionAccountIndex(storage, fallbackIndex, account) {
1986
- if (account) {
1987
- const matchedIndex = findMatchingAccountIndex(storage.accounts, {
1988
- accountId: account.accountId,
1989
- email: account.email,
1990
- refreshToken: account.refreshToken,
1991
- }, {
1992
- allowUniqueAccountIdFallbackWithoutEmail: true,
1993
- });
1994
- if (typeof matchedIndex === "number" && matchedIndex >= 0) {
1995
- return matchedIndex;
1996
- }
1997
- return null;
1998
- }
1999
- return fallbackIndex >= 0 && fallbackIndex < storage.accounts.length
2000
- ? fallbackIndex
2001
- : null;
2002
- }
2003
- function matchesManageActionAccount(account, candidate) {
2004
- if (!account || !candidate) {
2005
- return false;
2006
- }
2007
- if (account.accountId || candidate.accountId) {
2008
- return account.accountId === candidate.accountId;
2009
- }
2010
- return (account.refreshToken === candidate.refreshToken &&
2011
- sanitizeEmail(account.email) === sanitizeEmail(candidate.email));
2012
- }
2013
- async function handleManageAction(storage, menuResult) {
2014
- if (typeof menuResult.switchAccountIndex === "number") {
2015
- const index = menuResult.switchAccountIndex;
2016
- await runSwitchCommand([String(index + 1)], {
2017
- setStoragePath,
2018
- loadAccounts,
2019
- persistAndSyncSelectedAccount,
2020
- });
2021
- return;
2022
- }
2023
- if (typeof menuResult.deleteAccountIndex === "number") {
2024
- const idx = menuResult.deleteAccountIndex;
2025
- const selectedAccount = storage.accounts[idx];
2026
- let deleted = false;
2027
- if (selectedAccount) {
2028
- await withAccountStorageTransaction(async (loadedStorage, persist) => {
2029
- const nextStorage = loadedStorage
2030
- ? structuredClone(loadedStorage)
2031
- : structuredClone(storage);
2032
- const nextIndex = resolveManageActionAccountIndex(nextStorage, idx, selectedAccount);
2033
- if (nextIndex === null) {
2034
- return;
2035
- }
2036
- const nextAccount = nextStorage.accounts[nextIndex];
2037
- if (!matchesManageActionAccount(selectedAccount, nextAccount)) {
2038
- return;
2039
- }
2040
- nextStorage.accounts.splice(nextIndex, 1);
2041
- resetManageActionSelection(nextStorage, nextIndex);
2042
- await persist(nextStorage);
2043
- replaceManageActionStorage(storage, nextStorage);
2044
- deleted = true;
2045
- });
2046
- }
2047
- if (deleted) {
2048
- console.log(`Deleted account ${idx + 1}.`);
2049
- }
2050
- return;
2051
- }
2052
- if (typeof menuResult.toggleAccountIndex === "number") {
2053
- const idx = menuResult.toggleAccountIndex;
2054
- const selectedAccount = storage.accounts[idx];
2055
- let nextEnabledState = null;
2056
- if (selectedAccount) {
2057
- await withAccountStorageTransaction(async (loadedStorage, persist) => {
2058
- const nextStorage = loadedStorage
2059
- ? structuredClone(loadedStorage)
2060
- : structuredClone(storage);
2061
- const nextIndex = resolveManageActionAccountIndex(nextStorage, idx, selectedAccount);
2062
- if (nextIndex === null) {
2063
- return;
2064
- }
2065
- const nextAccount = nextStorage.accounts[nextIndex];
2066
- if (!nextAccount ||
2067
- !matchesManageActionAccount(selectedAccount, nextAccount)) {
2068
- return;
2069
- }
2070
- nextAccount.enabled = nextAccount.enabled === false;
2071
- await persist(nextStorage);
2072
- replaceManageActionStorage(storage, nextStorage);
2073
- nextEnabledState = nextAccount.enabled !== false;
2074
- });
2075
- }
2076
- if (nextEnabledState !== null) {
2077
- console.log(`${nextEnabledState ? "Enabled" : "Disabled"} account ${idx + 1}.`);
2078
- }
2079
- return;
2080
- }
2081
- if (typeof menuResult.refreshAccountIndex === "number") {
2082
- const idx = menuResult.refreshAccountIndex;
2083
- const existing = storage.accounts[idx];
2084
- if (!existing)
2085
- return;
2086
- const signInMode = await promptOAuthSignInMode(null);
2087
- if (signInMode === "cancel") {
2088
- console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
2089
- return;
2090
- }
2091
- if (signInMode !== "browser" && signInMode !== "manual") {
2092
- return;
2093
- }
2094
- const tokenResult = await runOAuthFlow(true, signInMode);
2095
- if (tokenResult.type !== "success") {
2096
- console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
2097
- return;
2098
- }
2099
- const resolved = resolveAccountSelection(tokenResult);
2100
- await persistAccountPool([resolved], false);
2101
- await syncSelectionToCodex(resolved);
2102
- console.log(`Refreshed account ${idx + 1}.`);
2103
- }
2104
- }
2105
- async function runAuthLogin(args) {
2106
- const parsedArgs = parseAuthLoginArgs(args);
2107
- if (!parsedArgs.ok) {
2108
- if (parsedArgs.reason === "error") {
2109
- console.error(parsedArgs.message);
2110
- printUsage();
2111
- return 1;
2112
- }
2113
- return 0;
2114
- }
2115
- const loginOptions = parsedArgs.options;
2116
- // `--org <id>` binds this login to a specific workspace/org so the same
2117
- // email's personal vs business/team workspace can be registered on demand
2118
- // (issue #491). The org is threaded explicitly into resolveAccountSelection
2119
- // (no process.env mutation), so concurrent re-entry (menu re-entry, a reused
2120
- // test worker) can never bind a login to a stale org via a shared global.
2121
- if (loginOptions.org) {
2122
- console.log(`Binding this login to workspace org id: ${loginOptions.org}`);
2123
- }
2124
- return runAuthLoginFlow(loginOptions);
2125
- }
2126
- async function runAuthLoginFlow(loginOptions) {
2127
- setStoragePath(null);
2128
- let pendingMenuQuotaRefresh = null;
2129
- let menuQuotaRefreshStatus;
2130
- let skipNextMenuQuotaAutoRefresh = false;
2131
- let menuQuotaRefreshGeneration = 0;
2132
- const clearMenuQuotaAutoRefreshSkip = () => {
2133
- skipNextMenuQuotaAutoRefresh = false;
2134
- menuQuotaRefreshGeneration += 1;
2135
- };
2136
- // When the user explicitly picks a sign-in transport on the command line
2137
- // (--device-auth, --manual, --no-browser), they want to add a new account
2138
- // directly. Skipping the dashboard menu keeps `login --device-auth`
2139
- // usable from scripts and matches the documented behavior of the help text.
2140
- const explicitSignInMode = loginOptions.deviceAuth || loginOptions.manual;
2141
- loginFlow: while (true) {
2142
- let existingStorage = await loadAccounts();
2143
- if (!explicitSignInMode &&
2144
- existingStorage &&
2145
- existingStorage.accounts.length > 0) {
2146
- while (true) {
2147
- existingStorage = await loadAccounts();
2148
- if (!existingStorage || existingStorage.accounts.length === 0) {
2149
- break;
2150
- }
2151
- const currentStorage = existingStorage;
2152
- const displaySettings = await loadDashboardDisplaySettings();
2153
- applyUiThemeFromDashboardSettings(displaySettings);
2154
- const quotaCache = await loadQuotaCache();
2155
- const shouldAutoFetchLimits = displaySettings.menuAutoFetchLimits ?? true;
2156
- const showFetchStatus = displaySettings.menuShowFetchStatus ?? true;
2157
- const quotaTtlMs = displaySettings.menuQuotaTtlMs ?? DEFAULT_MENU_QUOTA_REFRESH_TTL_MS;
2158
- const shouldSkipAutoFetchThisPass = !pendingMenuQuotaRefresh && skipNextMenuQuotaAutoRefresh;
2159
- if (shouldSkipAutoFetchThisPass) {
2160
- skipNextMenuQuotaAutoRefresh = false;
2161
- }
2162
- if (shouldAutoFetchLimits &&
2163
- !pendingMenuQuotaRefresh &&
2164
- !shouldSkipAutoFetchThisPass) {
2165
- const staleCount = countMenuQuotaRefreshTargets(currentStorage, quotaCache, quotaTtlMs);
2166
- if (staleCount > 0) {
2167
- if (showFetchStatus) {
2168
- menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [0/${staleCount}]`;
2169
- }
2170
- const refreshGeneration = menuQuotaRefreshGeneration;
2171
- pendingMenuQuotaRefresh = refreshQuotaCacheForMenu(currentStorage, quotaCache, quotaTtlMs, (current, total) => {
2172
- if (!showFetchStatus)
2173
- return;
2174
- menuQuotaRefreshStatus = `${UI_COPY.mainMenu.loadingLimits} [${current}/${total}]`;
2175
- })
2176
- .then(() => {
2177
- if (refreshGeneration === menuQuotaRefreshGeneration) {
2178
- skipNextMenuQuotaAutoRefresh = true;
2179
- }
2180
- return undefined;
2181
- })
2182
- .catch(() => undefined)
2183
- .finally(() => {
2184
- menuQuotaRefreshStatus = undefined;
2185
- pendingMenuQuotaRefresh = null;
2186
- });
2187
- }
2188
- }
2189
- const flaggedStorage = await loadFlaggedAccounts();
2190
- await syncCodexCliActiveSelectionIfDrifted(currentStorage);
2191
- const runtimeCurrent = await loadRuntimeCurrentSelectionForStorage(currentStorage);
2192
- const menuResult = await promptLoginMode(toExistingAccountInfo(currentStorage, quotaCache, displaySettings, runtimeCurrent), {
2193
- flaggedCount: flaggedStorage.accounts.length,
2194
- statusMessage: showFetchStatus
2195
- ? () => menuQuotaRefreshStatus
2196
- : undefined,
2197
- });
2198
- if (menuResult.mode === "cancel") {
2199
- console.log("Cancelled.");
2200
- return 0;
2201
- }
2202
- if (menuResult.mode === "check") {
2203
- clearMenuQuotaAutoRefreshSkip();
2204
- await runActionPanel("Quick Check", "Checking local session + live status", async () => {
2205
- await runHealthCheck({ forceRefresh: false, liveProbe: true });
2206
- }, displaySettings);
2207
- continue;
2208
- }
2209
- if (menuResult.mode === "deep-check") {
2210
- clearMenuQuotaAutoRefreshSkip();
2211
- await runActionPanel("Deep Check", "Refreshing and testing all accounts", async () => {
2212
- await runHealthCheck({ forceRefresh: true, liveProbe: true });
2213
- }, displaySettings);
2214
- continue;
2215
- }
2216
- if (menuResult.mode === "forecast") {
2217
- clearMenuQuotaAutoRefreshSkip();
2218
- await runActionPanel("Best Account", "Comparing accounts", async () => {
2219
- await runForecast(["--live"]);
2220
- }, displaySettings);
2221
- continue;
2222
- }
2223
- if (menuResult.mode === "fix") {
2224
- clearMenuQuotaAutoRefreshSkip();
2225
- await runActionPanel("Auto-Fix", "Checking and fixing common issues", async () => {
2226
- await runRepairFix(["--live"], createRepairCommandDeps());
2227
- }, displaySettings);
2228
- continue;
2229
- }
2230
- if (menuResult.mode === "settings") {
2231
- clearMenuQuotaAutoRefreshSkip();
2232
- await configureUnifiedSettings(displaySettings);
2233
- continue;
2234
- }
2235
- if (menuResult.mode === "verify-flagged") {
2236
- clearMenuQuotaAutoRefreshSkip();
2237
- await runActionPanel("Problem Account Check", "Checking problem accounts", async () => {
2238
- await runRepairVerifyFlagged([], createRepairCommandDeps());
2239
- }, displaySettings);
2240
- continue;
2241
- }
2242
- if (menuResult.mode === "fresh" && menuResult.deleteAll) {
2243
- clearMenuQuotaAutoRefreshSkip();
2244
- await runActionPanel("Reset Accounts", "Deleting all saved accounts", async () => {
2245
- await clearAccountsAndReset();
2246
- console.log("Cleared saved accounts from active storage. Recovery snapshots remain available.");
2247
- }, displaySettings);
2248
- continue;
2249
- }
2250
- if (menuResult.mode === "manage") {
2251
- clearMenuQuotaAutoRefreshSkip();
2252
- const requiresInteractiveOAuth = typeof menuResult.refreshAccountIndex === "number";
2253
- if (requiresInteractiveOAuth) {
2254
- await handleManageAction(currentStorage, menuResult);
2255
- continue;
2256
- }
2257
- await runActionPanel("Applying Change", "Updating selected account", async () => {
2258
- await handleManageAction(currentStorage, menuResult);
2259
- }, displaySettings);
2260
- continue;
2261
- }
2262
- if (menuResult.mode === "add") {
2263
- break;
2264
- }
2265
- }
2266
- }
2267
- const refreshedStorage = await loadAccounts();
2268
- let existingCount = refreshedStorage?.accounts.length ?? 0;
2269
- let forceNewLogin = existingCount > 0;
2270
- let onboardingBackupDiscoveryWarning = null;
2271
- const loadNamedBackupsForOnboarding = async () => {
2272
- if (existingCount > 0) {
2273
- onboardingBackupDiscoveryWarning = null;
2274
- return [];
2275
- }
2276
- try {
2277
- onboardingBackupDiscoveryWarning = null;
2278
- return await getNamedBackups();
2279
- }
2280
- catch (error) {
2281
- const code = error.code;
2282
- log.debug("getNamedBackups failed, skipping restore option", {
2283
- code,
2284
- error: error instanceof Error ? error.message : String(error),
2285
- });
2286
- if (code && code !== "ENOENT") {
2287
- onboardingBackupDiscoveryWarning =
2288
- "Named backup discovery failed. Continuing with browser or manual sign-in only.";
2289
- console.warn(onboardingBackupDiscoveryWarning);
2290
- }
2291
- else {
2292
- onboardingBackupDiscoveryWarning = null;
2293
- }
2294
- return [];
2295
- }
2296
- };
2297
- let namedBackups = await loadNamedBackupsForOnboarding();
2298
- while (true) {
2299
- const latestNamedBackup = namedBackups[0] ?? null;
2300
- const preferManualMode = loginOptions.manual || isBrowserLaunchSuppressed();
2301
- const signInMode = loginOptions.deviceAuth
2302
- ? "device"
2303
- : preferManualMode
2304
- ? "manual"
2305
- : await promptOAuthSignInMode(latestNamedBackup, onboardingBackupDiscoveryWarning);
2306
- if (signInMode === "cancel") {
2307
- if (existingCount > 0) {
2308
- console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
2309
- continue loginFlow;
2310
- }
2311
- console.log("Cancelled.");
2312
- return 0;
2313
- }
2314
- if (signInMode === "restore-backup") {
2315
- const latestAvailableBackup = namedBackups[0] ?? null;
2316
- if (!latestAvailableBackup) {
2317
- namedBackups = await loadNamedBackupsForOnboarding();
2318
- continue;
2319
- }
2320
- const restoreMode = await promptBackupRestoreMode(latestAvailableBackup);
2321
- if (restoreMode === "back") {
2322
- namedBackups = await loadNamedBackupsForOnboarding();
2323
- continue;
2324
- }
2325
- const selectedBackup = restoreMode === "manual"
2326
- ? await promptManualBackupSelection(namedBackups)
2327
- : latestAvailableBackup;
2328
- if (!selectedBackup) {
2329
- namedBackups = await loadNamedBackupsForOnboarding();
2330
- continue;
2331
- }
2332
- const confirmed = await confirm(UI_COPY.oauth.restoreBackupConfirm(selectedBackup.fileName, selectedBackup.accountCount));
2333
- if (!confirmed) {
2334
- namedBackups = await loadNamedBackupsForOnboarding();
2335
- continue;
2336
- }
2337
- const displaySettings = await loadDashboardDisplaySettings();
2338
- applyUiThemeFromDashboardSettings(displaySettings);
2339
- try {
2340
- await runActionPanel("Load Backup", `Loading ${selectedBackup.fileName}`, async () => {
2341
- const restoredStorage = await restoreAccountsFromBackup(selectedBackup.path, { persist: false });
2342
- const targetIndex = resolveActiveIndex(restoredStorage);
2343
- const { synced } = await persistAndSyncSelectedAccount({
2344
- storage: restoredStorage,
2345
- targetIndex,
2346
- parsed: targetIndex + 1,
2347
- switchReason: "restore",
2348
- preserveActiveIndexByFamily: true,
2349
- });
2350
- console.log(UI_COPY.oauth.restoreBackupLoaded(selectedBackup.fileName, restoredStorage.accounts.length));
2351
- if (!synced) {
2352
- console.warn(UI_COPY.oauth.restoreBackupSyncWarning);
2353
- }
2354
- }, displaySettings);
2355
- }
2356
- catch (error) {
2357
- const message = error instanceof Error ? error.message : String(error);
2358
- if (error instanceof StorageError) {
2359
- console.error(formatStorageErrorHint(error, selectedBackup.path));
2360
- }
2361
- else {
2362
- console.error(`Backup restore failed: ${message}`);
2363
- }
2364
- const storageAfterRestoreAttempt = await loadAccounts().catch(() => null);
2365
- if ((storageAfterRestoreAttempt?.accounts.length ?? 0) > 0) {
2366
- continue loginFlow;
2367
- }
2368
- namedBackups = await loadNamedBackupsForOnboarding();
2369
- continue;
2370
- }
2371
- continue loginFlow;
2372
- }
2373
- if (signInMode !== "browser" &&
2374
- signInMode !== "manual" &&
2375
- signInMode !== "device") {
2376
- continue;
2377
- }
2378
- const tokenResult = await runSignInFlow(forceNewLogin, signInMode);
2379
- if (tokenResult.type !== "success") {
2380
- if (isOAuthCancellation(tokenResult)) {
2381
- // In explicit-mode invocations the dashboard was bypassed,
2382
- // so falling back to it on cancel would re-enter the same
2383
- // transport flow and trap the user. Exit cleanly instead.
2384
- if (explicitSignInMode) {
2385
- console.log("Cancelled.");
2386
- return 0;
2387
- }
2388
- if (existingCount > 0) {
2389
- console.log(stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"));
2390
- continue loginFlow;
2391
- }
2392
- console.log("Cancelled.");
2393
- return 0;
2394
- }
2395
- console.error(`Login failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
2396
- return 1;
2397
- }
2398
- const resolved = resolveAccountSelection(tokenResult, loginOptions.org);
2399
- await persistAccountPool([resolved], false);
2400
- await syncSelectionToCodex(resolved);
2401
- const latestStorage = await loadAccounts();
2402
- const count = latestStorage?.accounts.length ?? 1;
2403
- existingCount = count;
2404
- namedBackups = [];
2405
- onboardingBackupDiscoveryWarning = null;
2406
- console.log(`Added account. Total: ${count}`);
2407
- console.log("Next steps:");
2408
- console.log(" codex-multi-auth status Check that the wrapper is active.");
2409
- console.log(" codex-multi-auth check Confirm your saved accounts look healthy.");
2410
- console.log(" codex-multi-auth list Review saved accounts before switching.");
2411
- if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
2412
- console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
2413
- // Same reasoning as the addAnother=false branch below: in
2414
- // explicit mode the dashboard is bypassed, so falling out of
2415
- // the inner loop would re-enter loginFlow and silently start
2416
- // another sign-in session despite the cap.
2417
- if (explicitSignInMode) {
2418
- return 0;
2419
- }
2420
- break;
2421
- }
2422
- const addAnother = await promptAddAnotherAccount(count);
2423
- if (!addAnother) {
2424
- // With an explicit transport flag the dashboard was bypassed,
2425
- // so falling back to it after declining would loop into a fresh
2426
- // sign-in instead of exiting. Return directly in that case.
2427
- if (explicitSignInMode) {
2428
- return 0;
2429
- }
2430
- break;
2431
- }
2432
- forceNewLogin = true;
2433
- }
2434
- }
2435
- }
2436
- async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, preserveActiveIndexByFamily = false, setPin = false, clearPin = false, bumpAffinityGeneration = false, }) {
2437
- const account = storage.accounts[targetIndex];
2438
- if (!account) {
2439
- throw new Error(`Account ${parsed} not found.`);
2440
- }
2441
- const shouldPreserveActiveIndexByFamily = preserveActiveIndexByFamily &&
2442
- !!storage.activeIndexByFamily &&
2443
- targetIndex === storage.activeIndex;
2444
- storage.activeIndex = targetIndex;
2445
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
2446
- if (shouldPreserveActiveIndexByFamily) {
2447
- const maxIndex = Math.max(0, storage.accounts.length - 1);
2448
- for (const family of MODEL_FAMILIES) {
2449
- const raw = storage.activeIndexByFamily[family];
2450
- const candidate = typeof raw === "number" && Number.isFinite(raw) ? raw : targetIndex;
2451
- storage.activeIndexByFamily[family] = Math.max(0, Math.min(candidate, maxIndex));
2452
- }
2453
- }
2454
- else {
2455
- storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
2456
- for (const family of MODEL_FAMILIES) {
2457
- storage.activeIndexByFamily[family] = targetIndex;
2458
- }
2459
- }
2460
- const wasDisabled = account.enabled === false;
2461
- if (wasDisabled) {
2462
- account.enabled = true;
2463
- }
2464
- const switchNow = Date.now();
2465
- let syncAccessToken = account.accessToken;
2466
- let syncRefreshToken = account.refreshToken;
2467
- let syncExpiresAt = account.expiresAt;
2468
- let syncIdToken = initialSyncIdToken;
2469
- if (!hasUsableAccessToken(account, switchNow)) {
2470
- const refreshResult = await queuedRefresh(account.refreshToken);
2471
- if (refreshResult.type === "success") {
2472
- const tokenAccountId = extractAccountId(refreshResult.access);
2473
- const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
2474
- if (account.refreshToken !== refreshResult.refresh) {
2475
- account.refreshToken = refreshResult.refresh;
2476
- }
2477
- if (account.accessToken !== refreshResult.access) {
2478
- account.accessToken = refreshResult.access;
2479
- }
2480
- if (account.expiresAt !== refreshResult.expires) {
2481
- account.expiresAt = refreshResult.expires;
2482
- }
2483
- if (nextEmail && nextEmail !== account.email) {
2484
- account.email = nextEmail;
2485
- }
2486
- applyTokenAccountIdentity(account, tokenAccountId);
2487
- syncAccessToken = refreshResult.access;
2488
- syncRefreshToken = refreshResult.refresh;
2489
- syncExpiresAt = refreshResult.expires;
2490
- syncIdToken = refreshResult.idToken;
2491
- }
2492
- else {
2493
- console.warn(`Switch validation refresh failed for account ${parsed}: ${normalizeFailureDetail(refreshResult.message, refreshResult.reason)}.`);
2494
- }
2495
- }
2496
- account.lastUsed = switchNow;
2497
- account.lastSwitchReason = switchReason;
2498
- if (setPin) {
2499
- storage.pinnedAccountIndex = targetIndex;
2500
- }
2501
- else if (clearPin) {
2502
- delete storage.pinnedAccountIndex;
2503
- }
2504
- if (bumpAffinityGeneration) {
2505
- // Re-read the on-disk generation right before saving so concurrent CLI
2506
- // processes don't lose increments via lost-update on the load+mutate
2507
- // pair. Math.max keeps the counter monotonically increasing — extra
2508
- // bumps are harmless (just an additional affinity invalidation), but a
2509
- // missed bump can let the proxy cling to the wrong account. The save
2510
- // itself is serialized by withStorageLock; this only narrows the
2511
- // lost-update window. See issue #474.
2512
- const diskGeneration = readAffinityGenerationFromDisk(getStoragePath());
2513
- const inMemoryGeneration = storage.affinityGeneration ?? 0;
2514
- storage.affinityGeneration =
2515
- Math.max(inMemoryGeneration, diskGeneration) + 1;
2516
- }
2517
- await saveAccountsWithRetry(storage, saveAccounts);
2518
- const synced = await setCodexCliActiveSelection({
2519
- accountId: account.accountId,
2520
- email: account.email,
2521
- accessToken: syncAccessToken,
2522
- refreshToken: syncRefreshToken,
2523
- expiresAt: syncExpiresAt,
2524
- ...(syncIdToken ? { idToken: syncIdToken } : {}),
2525
- });
2526
- return { synced, wasDisabled };
2527
- }
2528
149
  async function runBest(args) {
2529
150
  return runBestCommand(args, {
2530
151
  setStoragePath,
@@ -2674,117 +295,103 @@ function buildSelectAccountTraced() {
2674
295
  });
2675
296
  };
2676
297
  }
2677
- export async function runCodexMultiAuthCli(rawArgs) {
2678
- const startupDisplaySettings = await loadDashboardDisplaySettings();
2679
- applyUiThemeFromDashboardSettings(startupDisplaySettings);
2680
- const args = rawArgs[0] && rawArgs[0] !== "auth" && ACCOUNT_MANAGER_COMMANDS.has(rawArgs[0])
2681
- ? ["auth", ...rawArgs]
2682
- : [...rawArgs];
2683
- if (args.length === 0) {
2684
- printUsage();
2685
- return 0;
2686
- }
2687
- if (args[0] === "--help" || args[0] === "-h") {
2688
- printUsage();
2689
- return 0;
2690
- }
2691
- const [root, sub, ...rest] = args;
2692
- if (root !== "auth") {
2693
- printUsage();
2694
- return 1;
2695
- }
2696
- const command = sub ?? "login";
2697
- if (command === "--help" || command === "-h") {
2698
- printUsage();
2699
- return 0;
2700
- }
2701
- if (command === "login") {
2702
- return runAuthLogin(rest);
2703
- }
2704
- if (command === "list" || command === "status") {
2705
- return runStatusCommand({
2706
- setStoragePath,
2707
- getStoragePath,
2708
- loadAccounts,
2709
- inspectStorageHealth,
2710
- resolveActiveIndex,
2711
- formatRateLimitEntry,
2712
- loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
2713
- loadAppBindStatus: async () => getAppBindStatus()
2714
- .then((status) => (status.running ? status.router : null))
2715
- .catch(() => null),
2716
- loadAppHelperStatus: readAppRuntimeHelperAccountSignal,
2717
- loadQuotaCache,
2718
- json: rest.includes("--json") || rest.includes("-j"),
2719
- });
2720
- }
2721
- if (command === "switch") {
2722
- return runSwitchCommand(rest, {
298
+ /**
299
+ * Shared handler for `list` and `status` (aliases of the same view).
300
+ */
301
+ const runListOrStatusCommand = (rest) => runStatusCommand({
302
+ setStoragePath,
303
+ getStoragePath,
304
+ loadAccounts,
305
+ inspectStorageHealth,
306
+ resolveActiveIndex,
307
+ formatRateLimitEntry,
308
+ loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
309
+ loadAppBindStatus: async () => getAppBindStatus()
310
+ .then((status) => (status.running ? status.router : null))
311
+ .catch(() => null),
312
+ loadAppHelperStatus: readAppRuntimeHelperAccountSignal,
313
+ loadQuotaCache,
314
+ json: rest.includes("--json") || rest.includes("-j"),
315
+ });
316
+ /**
317
+ * Command registry for the `auth` dispatcher (audit roadmap §4.1.1 phase 2).
318
+ *
319
+ * Replaces the former `if (command === …)` chain in runCodexMultiAuthCli with
320
+ * a lookup map. Aliases (`status` → `list` view) point at the same handler.
321
+ * Per-invocation dependency factories (createRepairCommandDeps,
322
+ * buildSelectAccountTraced) are still invoked inside each handler so they are
323
+ * constructed at dispatch time, not at module load — identical to the old
324
+ * chain. Keys are exact-match and unique, so lookup order cannot change
325
+ * semantics relative to the sequential chain it replaces.
326
+ */
327
+ const CLI_COMMAND_HANDLERS = new Map([
328
+ ["login", (rest) => runAuthLogin(rest, { runForecast, createRepairCommandDeps })],
329
+ ["list", runListOrStatusCommand],
330
+ ["status", runListOrStatusCommand],
331
+ [
332
+ "switch",
333
+ (rest) => runSwitchCommand(rest, {
2723
334
  setStoragePath,
2724
335
  loadAccounts,
2725
336
  persistAndSyncSelectedAccount,
2726
- });
2727
- }
2728
- if (command === "unpin") {
2729
- return runUnpinCommand({
337
+ }),
338
+ ],
339
+ [
340
+ "unpin",
341
+ () => runUnpinCommand({
2730
342
  setStoragePath,
2731
343
  loadAccounts,
2732
344
  saveAccounts,
2733
345
  getStoragePath,
2734
- });
2735
- }
2736
- if (command === "workspace") {
2737
- return runWorkspaceCommand(rest, {
346
+ }),
347
+ ],
348
+ [
349
+ "workspace",
350
+ (rest) => runWorkspaceCommand(rest, {
2738
351
  setStoragePath,
2739
352
  loadAccounts,
2740
353
  saveAccounts,
2741
- });
2742
- }
2743
- if (command === "check") {
2744
- return runCheckCommand({ runHealthCheck });
2745
- }
2746
- if (command === "features") {
2747
- return runFeaturesCommand({ implementedFeatures: IMPLEMENTED_FEATURES });
2748
- }
2749
- if (command === "verify-flagged") {
2750
- return runRepairVerifyFlagged(rest, createRepairCommandDeps());
2751
- }
2752
- if (command === "forecast") {
2753
- return runForecast(rest);
2754
- }
2755
- if (command === "best") {
2756
- return runBest(rest);
2757
- }
2758
- if (command === "account") {
2759
- return runAccountCommand(rest, {
354
+ }),
355
+ ],
356
+ ["check", () => runCheckCommand({ runHealthCheck })],
357
+ [
358
+ "features",
359
+ () => runFeaturesCommand({ implementedFeatures: IMPLEMENTED_FEATURES }),
360
+ ],
361
+ [
362
+ "verify-flagged",
363
+ (rest) => runRepairVerifyFlagged(rest, createRepairCommandDeps()),
364
+ ],
365
+ ["forecast", (rest) => runForecast(rest)],
366
+ ["best", (rest) => runBest(rest)],
367
+ [
368
+ "account",
369
+ (rest) => runAccountCommand(rest, {
2760
370
  setStoragePath,
2761
371
  loadAccounts,
2762
- });
2763
- }
2764
- if (command === "budget") {
2765
- return runBudgetCommand(rest);
2766
- }
2767
- if (command === "bridge") {
2768
- return runBridgeCommand(rest);
2769
- }
2770
- if (command === "integrations") {
2771
- return runIntegrationsCommand(rest);
2772
- }
2773
- if (command === "models") {
2774
- return runModelsCommand(rest, {
372
+ }),
373
+ ],
374
+ ["budget", (rest) => runBudgetCommand(rest)],
375
+ ["bridge", (rest) => runBridgeCommand(rest)],
376
+ ["integrations", (rest) => runIntegrationsCommand(rest)],
377
+ [
378
+ "models",
379
+ (rest) => runModelsCommand(rest, {
2775
380
  setStoragePath,
2776
381
  loadAccounts,
2777
382
  loadQuotaCache,
2778
- });
2779
- }
2780
- if (command === "monitor") {
2781
- return runMonitorCommand(rest, {
383
+ }),
384
+ ],
385
+ [
386
+ "monitor",
387
+ (rest) => runMonitorCommand(rest, {
2782
388
  setStoragePath,
2783
389
  loadAccounts,
2784
- });
2785
- }
2786
- if (command === "report") {
2787
- return runReportCommand(rest, {
390
+ }),
391
+ ],
392
+ [
393
+ "report",
394
+ (rest) => runReportCommand(rest, {
2788
395
  setStoragePath,
2789
396
  getStoragePath,
2790
397
  loadAccounts,
@@ -2798,13 +405,12 @@ export async function runCodexMultiAuthCli(rawArgs) {
2798
405
  normalizeFailureDetail,
2799
406
  loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
2800
407
  loadQuotaCache,
2801
- });
2802
- }
2803
- if (command === "usage") {
2804
- return runUsageCommand(rest);
2805
- }
2806
- if (command === "rotation") {
2807
- return runRotationCommand(rest, {
408
+ }),
409
+ ],
410
+ ["usage", (rest) => runUsageCommand(rest)],
411
+ [
412
+ "rotation",
413
+ (rest) => runRotationCommand(rest, {
2808
414
  loadPluginConfig,
2809
415
  savePluginConfig,
2810
416
  getCodexRuntimeRotationProxy,
@@ -2818,10 +424,11 @@ export async function runCodexMultiAuthCli(rawArgs) {
2818
424
  getCodexAppBindStatus: getAppBindStatus,
2819
425
  loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
2820
426
  loadQuotaCache,
2821
- });
2822
- }
2823
- if (command === "why-selected") {
2824
- return runWhySelectedCommand(rest, {
427
+ }),
428
+ ],
429
+ [
430
+ "why-selected",
431
+ (rest) => runWhySelectedCommand(rest, {
2825
432
  parseWhySelectedArgs,
2826
433
  printWhySelectedUsage,
2827
434
  setStoragePath,
@@ -2841,10 +448,11 @@ export async function runCodexMultiAuthCli(rawArgs) {
2841
448
  return { generatedAt };
2842
449
  },
2843
450
  sanitizeEmail,
2844
- });
2845
- }
2846
- if (command === "verify") {
2847
- return runVerifyCommand(rest, {
451
+ }),
452
+ ],
453
+ [
454
+ "verify",
455
+ (rest) => runVerifyCommand(rest, {
2848
456
  parseVerifyArgs,
2849
457
  printVerifyUsage,
2850
458
  runVerifyFlagged: async (flaggedArgs) => runRepairVerifyFlagged(flaggedArgs, createRepairCommandDeps()),
@@ -2858,50 +466,83 @@ export async function runCodexMultiAuthCli(rawArgs) {
2858
466
  getProjectGlobalConfigDir,
2859
467
  resolvePath: resolveStoragePath,
2860
468
  },
2861
- });
2862
- }
2863
- if (command === "fix") {
2864
- return runRepairFix(rest, createRepairCommandDeps());
2865
- }
2866
- if (command === "doctor") {
2867
- return runRepairDoctor(rest, createRepairCommandDeps());
469
+ }),
470
+ ],
471
+ ["fix", (rest) => runRepairFix(rest, createRepairCommandDeps())],
472
+ ["doctor", (rest) => runRepairDoctor(rest, createRepairCommandDeps())],
473
+ ["uninstall", (rest) => runUninstallCommand(rest, { clearAccounts })],
474
+ [
475
+ "config",
476
+ (rest) => {
477
+ const [subcommand, ...configArgs] = rest;
478
+ if (subcommand === "explain") {
479
+ return runConfigExplainCommand(configArgs, {
480
+ getReport: getPluginConfigExplainReport,
481
+ });
482
+ }
483
+ if (subcommand === "template") {
484
+ return runInitConfigCommand(configArgs);
485
+ }
486
+ console.error(`Unknown config command: ${subcommand ?? "(missing)"}`);
487
+ return 1;
488
+ },
489
+ ],
490
+ ["init-config", (rest) => runInitConfigCommand(rest)],
491
+ [
492
+ "debug",
493
+ (rest) => {
494
+ const [subcommand, ...debugArgs] = rest;
495
+ if (subcommand === "bundle") {
496
+ return runDebugBundleCommand(debugArgs, {
497
+ getConfigReport: getPluginConfigExplainReport,
498
+ getStoragePath,
499
+ loadAccounts,
500
+ loadFlaggedAccounts,
501
+ loadCodexCliState,
502
+ getLastAccountsSaveTimestamp,
503
+ });
504
+ }
505
+ console.error(`Unknown debug command: ${subcommand ?? "(missing)"}`);
506
+ return 1;
507
+ },
508
+ ],
509
+ ]);
510
+ export async function runCodexMultiAuthCli(rawArgs) {
511
+ // Lazy install setup (audit roadmap §4.5.4): app detection, Codex app bind,
512
+ // and launcher routing moved out of npm postinstall to the first CLI run.
513
+ // ensureFirstRunSetup never throws; the catch is belt-and-braces so no
514
+ // command can ever fail because of first-run housekeeping.
515
+ await ensureFirstRunSetup({
516
+ notify: (message) => console.error(`codex-multi-auth: ${message}`),
517
+ }).catch(() => undefined);
518
+ const startupDisplaySettings = await loadDashboardDisplaySettings();
519
+ applyUiThemeFromDashboardSettings(startupDisplaySettings);
520
+ const args = rawArgs[0] && rawArgs[0] !== "auth" && ACCOUNT_MANAGER_COMMANDS.has(rawArgs[0])
521
+ ? ["auth", ...rawArgs]
522
+ : [...rawArgs];
523
+ if (args.length === 0) {
524
+ printUsage();
525
+ return 0;
2868
526
  }
2869
- if (command === "uninstall") {
2870
- return runUninstallCommand(rest, { clearAccounts });
527
+ if (args[0] === "--help" || args[0] === "-h") {
528
+ printUsage();
529
+ return 0;
2871
530
  }
2872
- if (command === "config") {
2873
- const [subcommand, ...configArgs] = rest;
2874
- if (subcommand === "explain") {
2875
- return runConfigExplainCommand(configArgs, {
2876
- getReport: getPluginConfigExplainReport,
2877
- });
2878
- }
2879
- if (subcommand === "template") {
2880
- return runInitConfigCommand(configArgs);
2881
- }
2882
- console.error(`Unknown config command: ${subcommand ?? "(missing)"}`);
531
+ const [root, sub, ...rest] = args;
532
+ if (root !== "auth") {
533
+ printUsage();
2883
534
  return 1;
2884
535
  }
2885
- if (command === "init-config") {
2886
- return runInitConfigCommand(rest);
536
+ const command = sub ?? "login";
537
+ if (command === "--help" || command === "-h") {
538
+ printUsage();
539
+ return 0;
2887
540
  }
2888
- if (command === "debug") {
2889
- const [subcommand, ...debugArgs] = rest;
2890
- if (subcommand === "bundle") {
2891
- return runDebugBundleCommand(debugArgs, {
2892
- getConfigReport: getPluginConfigExplainReport,
2893
- getStoragePath,
2894
- loadAccounts,
2895
- loadFlaggedAccounts,
2896
- loadCodexCliState,
2897
- getLastAccountsSaveTimestamp,
2898
- });
2899
- }
2900
- console.error(`Unknown debug command: ${subcommand ?? "(missing)"}`);
2901
- return 1;
541
+ const handler = CLI_COMMAND_HANDLERS.get(command);
542
+ if (handler) {
543
+ return handler(rest);
2902
544
  }
2903
545
  console.error(`Unknown command: ${command}`);
2904
546
  printUsage();
2905
547
  return 1;
2906
548
  }
2907
- //# sourceMappingURL=codex-manager.js.map