codex-multi-auth 2.3.0-beta.1 → 2.3.0-beta.3

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