@@ -4,16 +4,18 @@ import {
44 buildForecastExplanation ,
55 type ForecastAccountResult ,
66} from "../../forecast.js" ;
7+ import {
8+ applyRefreshedAccountPatch ,
9+ persistRefreshedAccountPatch ,
10+ serializeForecastResults ,
11+ type AccountIdentityMatch ,
12+ type RefreshedAccountPatch ,
13+ } from "../forecast-report-shared.js" ;
714import type { QuotaCacheData } from "../../quota-cache.js" ;
815import type { CodexQuotaSnapshot } from "../../quota-probe.js" ;
916import { resolveNormalizedModel } from "../../request/helpers/model-map.js" ;
10- import {
11- findMatchingAccountIndex ,
12- type AccountMetadataV3 ,
13- type AccountStorageV3 ,
14- } from "../../storage.js" ;
17+ import { type AccountMetadataV3 , type AccountStorageV3 } from "../../storage.js" ;
1518import type { TokenFailure , TokenResult } from "../../types.js" ;
16- import { sleep } from "../../utils.js" ;
1719
1820interface ForecastCliOptions {
1921 live : boolean ;
@@ -32,8 +34,6 @@ type QuotaEmailFallbackState = ReadonlyMap<
3234 { matchingCount : number ; distinctAccountIds : Set < string > }
3335> ;
3436
35- const RETRYABLE_STORAGE_WRITE_CODES = new Set ( [ "EBUSY" , "EPERM" ] ) ;
36-
3737export interface ForecastCommandDeps {
3838 setStoragePath : ( path : string | null ) => void ;
3939 loadAccounts : ( ) => Promise < AccountStorageV3 | null > ;
@@ -112,82 +112,6 @@ export interface ForecastCommandDeps {
112112 getNow ?: ( ) => number ;
113113}
114114
115- function isRetryableStorageWriteError ( error : unknown ) : boolean {
116- const code = ( error as NodeJS . ErrnoException | undefined ) ?. code ;
117- return typeof code === "string" && RETRYABLE_STORAGE_WRITE_CODES . has ( code ) ;
118- }
119-
120- async function saveAccountsWithRetry (
121- storage : AccountStorageV3 ,
122- saveAccounts : ForecastCommandDeps [ "saveAccounts" ] ,
123- ) : Promise < void > {
124- for ( let attempt = 0 ; ; attempt += 1 ) {
125- try {
126- await saveAccounts ( storage ) ;
127- return ;
128- } catch ( error ) {
129- if ( ! isRetryableStorageWriteError ( error ) || attempt >= 3 ) {
130- throw error ;
131- }
132- await sleep ( 10 * 2 ** attempt ) ;
133- }
134- }
135- }
136-
137- type AccountIdentityMatch = Pick <
138- AccountMetadataV3 ,
139- "accountId" | "email" | "refreshToken"
140- > ;
141- type RefreshedAccountPatch = Pick <
142- AccountMetadataV3 ,
143- "refreshToken" | "accessToken" | "expiresAt"
144- > & {
145- email ?: AccountMetadataV3 [ "email" ] ;
146- accountId ?: AccountMetadataV3 [ "accountId" ] ;
147- accountIdSource ?: AccountMetadataV3 [ "accountIdSource" ] ;
148- } ;
149-
150- function applyRefreshedAccountPatch (
151- account : AccountMetadataV3 ,
152- patch : RefreshedAccountPatch ,
153- ) : void {
154- account . refreshToken = patch . refreshToken ;
155- account . accessToken = patch . accessToken ;
156- account . expiresAt = patch . expiresAt ;
157- if ( patch . email ) account . email = patch . email ;
158- if ( patch . accountId ) {
159- account . accountId = patch . accountId ;
160- account . accountIdSource = patch . accountIdSource ;
161- }
162- }
163-
164- async function persistRefreshedAccountPatch (
165- storage : AccountStorageV3 ,
166- accountMatch : AccountIdentityMatch ,
167- patch : RefreshedAccountPatch ,
168- loadAccounts : ForecastCommandDeps [ "loadAccounts" ] ,
169- saveAccounts : ForecastCommandDeps [ "saveAccounts" ] ,
170- ) : Promise < void > {
171- const latestStorage = ( await loadAccounts ( ) ) ?? storage ;
172- const nextStorage = structuredClone ( latestStorage ) ;
173- const targetIndex =
174- findMatchingAccountIndex ( nextStorage . accounts , accountMatch , {
175- allowUniqueAccountIdFallbackWithoutEmail : true ,
176- } ) ??
177- findMatchingAccountIndex ( nextStorage . accounts , patch , {
178- allowUniqueAccountIdFallbackWithoutEmail : true ,
179- } ) ;
180- if ( targetIndex === undefined ) {
181- throw new Error ( "Unable to resolve refreshed account for persistence" ) ;
182- }
183- const targetAccount = nextStorage . accounts [ targetIndex ] ;
184- if ( ! targetAccount ) {
185- throw new Error ( "Unable to resolve refreshed account for persistence" ) ;
186- }
187- applyRefreshedAccountPatch ( targetAccount , patch ) ;
188- await saveAccountsWithRetry ( nextStorage , saveAccounts ) ;
189- }
190-
191115function joinStyledSegments (
192116 parts : string [ ] ,
193117 styleText : ( text : string , tone : PromptTone ) => string ,
@@ -255,54 +179,6 @@ function parseForecastArgs(
255179 return { ok : true , options } ;
256180}
257181
258- function serializeForecastResults (
259- results : ForecastAccountResult [ ] ,
260- liveQuotaByIndex : Map < number , CodexQuotaSnapshot > ,
261- refreshFailures : Map < number , TokenFailure > ,
262- formatQuotaSnapshotLine : ( snapshot : CodexQuotaSnapshot ) => string ,
263- ) : Array < {
264- index : number ;
265- label : string ;
266- isCurrent : boolean ;
267- availability : ForecastAccountResult [ "availability" ] ;
268- riskScore : number ;
269- riskLevel : ForecastAccountResult [ "riskLevel" ] ;
270- waitMs : number ;
271- reasons : string [ ] ;
272- liveQuota ?: {
273- status : number ;
274- planType ?: string ;
275- activeLimit ?: number ;
276- model : string ;
277- summary : string ;
278- } ;
279- refreshFailure ?: TokenFailure ;
280- } > {
281- return results . map ( ( result ) => {
282- const liveQuota = liveQuotaByIndex . get ( result . index ) ;
283- return {
284- index : result . index ,
285- label : result . label ,
286- isCurrent : result . isCurrent ,
287- availability : result . availability ,
288- riskScore : result . riskScore ,
289- riskLevel : result . riskLevel ,
290- waitMs : result . waitMs ,
291- reasons : result . reasons ,
292- liveQuota : liveQuota
293- ? {
294- status : liveQuota . status ,
295- planType : liveQuota . planType ,
296- activeLimit : liveQuota . activeLimit ,
297- model : liveQuota . model ,
298- summary : formatQuotaSnapshotLine ( liveQuota ) ,
299- }
300- : undefined ,
301- refreshFailure : refreshFailures . get ( result . index ) ,
302- } ;
303- } ) ;
304- }
305-
306182export async function runForecastCommand (
307183 args : string [ ] ,
308184 deps : ForecastCommandDeps & {
0 commit comments