@modelprofile.com/authswitch 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/accounts.d.ts +63 -13
- package/dist_ts/accounts.js +199 -31
- package/dist_ts/classes.accountlist.d.ts +0 -24
- package/dist_ts/classes.accountlist.js +2 -213
- package/dist_ts/classes.claudecodeharness.d.ts +61 -3
- package/dist_ts/classes.claudecodeharness.js +130 -14
- package/dist_ts/classes.claudecodelocks.d.ts +35 -0
- package/dist_ts/classes.claudecodelocks.js +117 -0
- package/dist_ts/classes.claudestatus.d.ts +17 -4
- package/dist_ts/classes.claudestatus.js +89 -73
- package/dist_ts/classes.claudetokenrefresh.d.ts +32 -0
- package/dist_ts/classes.claudetokenrefresh.js +77 -0
- package/dist_ts/classes.cli.d.ts +29 -7
- package/dist_ts/classes.cli.js +172 -78
- package/dist_ts/classes.codexharness.d.ts +6 -2
- package/dist_ts/classes.codexharness.js +7 -3
- package/dist_ts/classes.codexstatus.d.ts +5 -4
- package/dist_ts/classes.codexstatus.js +36 -19
- package/dist_ts/classes.credentialstore.d.ts +28 -2
- package/dist_ts/classes.credentialstore.js +41 -12
- package/dist_ts/classes.fileharness.d.ts +39 -19
- package/dist_ts/classes.fileharness.js +58 -24
- package/dist_ts/classes.limits.d.ts +46 -7
- package/dist_ts/classes.limits.js +106 -32
- package/dist_ts/classes.listrenderer.d.ts +17 -0
- package/dist_ts/classes.listrenderer.js +313 -0
- package/dist_ts/classes.login.d.ts +1 -1
- package/dist_ts/classes.login.js +1 -1
- package/dist_ts/classes.opencodeharness.d.ts +6 -2
- package/dist_ts/classes.opencodeharness.js +9 -5
- package/dist_ts/classes.operations.js +3 -2
- package/dist_ts/classes.tui.js +10 -6
- package/dist_ts/classes.watch.d.ts +108 -0
- package/dist_ts/classes.watch.js +219 -0
- package/dist_ts/classes.watchlock.d.ts +33 -0
- package/dist_ts/classes.watchlock.js +118 -0
- package/dist_ts/claudehttp.d.ts +39 -0
- package/dist_ts/claudehttp.js +83 -0
- package/dist_ts/cliargs.d.ts +36 -0
- package/dist_ts/cliargs.js +60 -0
- package/dist_ts/consoletable.d.ts +21 -0
- package/dist_ts/consoletable.js +63 -0
- package/dist_ts/helpers.d.ts +7 -0
- package/dist_ts/helpers.js +16 -1
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/interfaces.harness.d.ts +76 -15
- package/dist_ts/interfaces.list.d.ts +7 -1
- package/dist_ts/plugins.d.ts +7 -0
- package/dist_ts/plugins.js +6 -1
- package/dist_ts/ratelimit.d.ts +8 -0
- package/dist_ts/ratelimit.js +13 -0
- package/dist_ts/watchpolicy.d.ts +44 -0
- package/dist_ts/watchpolicy.js +82 -0
- package/package.json +5 -3
- package/readme.md +360 -106
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/accounts.ts +202 -34
- package/ts/classes.accountlist.ts +2 -216
- package/ts/classes.claudecodeharness.ts +126 -13
- package/ts/classes.claudecodelocks.ts +132 -0
- package/ts/classes.claudestatus.ts +88 -54
- package/ts/classes.claudetokenrefresh.ts +85 -0
- package/ts/classes.cli.ts +163 -63
- package/ts/classes.codexharness.ts +7 -3
- package/ts/classes.codexstatus.ts +29 -18
- package/ts/classes.credentialstore.ts +53 -9
- package/ts/classes.fileharness.ts +69 -31
- package/ts/classes.limits.ts +126 -33
- package/ts/classes.listrenderer.ts +328 -0
- package/ts/classes.login.ts +1 -1
- package/ts/classes.opencodeharness.ts +9 -5
- package/ts/classes.operations.ts +2 -1
- package/ts/classes.tui.ts +9 -5
- package/ts/classes.watch.ts +263 -0
- package/ts/classes.watchlock.ts +100 -0
- package/ts/claudehttp.ts +92 -0
- package/ts/cliargs.ts +71 -0
- package/ts/consoletable.ts +62 -0
- package/ts/helpers.ts +14 -0
- package/ts/index.ts +3 -0
- package/ts/interfaces.harness.ts +78 -6
- package/ts/interfaces.list.ts +7 -1
- package/ts/plugins.ts +9 -0
- package/ts/ratelimit.ts +14 -0
- package/ts/watchpolicy.ts +121 -0
package/ts/interfaces.harness.ts
CHANGED
|
@@ -53,8 +53,9 @@ export interface IHarnessProcessControl {
|
|
|
53
53
|
* A live credential that no longer holds the account the last switch activated.
|
|
54
54
|
*
|
|
55
55
|
* Authswitch records the hash of what it wrote, in its own store. A different hash with the same
|
|
56
|
-
* account is that account's own token refresh; a different account
|
|
57
|
-
*
|
|
56
|
+
* account is that account's own token refresh; a different account means the login changed outside
|
|
57
|
+
* authswitch -- a new native login, or, for a harness that does not pick up swaps live, a running
|
|
58
|
+
* instance writing its in-memory login back.
|
|
58
59
|
*/
|
|
59
60
|
export interface IHarnessCredentialDrift {
|
|
60
61
|
slotId?: string;
|
|
@@ -97,6 +98,12 @@ export interface IHarnessAccountStatus {
|
|
|
97
98
|
problems: string[];
|
|
98
99
|
/** Optional structured data for shared presentation; never parse the display facts. */
|
|
99
100
|
summary?: IHarnessStatusSummary;
|
|
101
|
+
/**
|
|
102
|
+
* Set when the provider refused a lookup for too many requests (HTTP 429). It says nothing about the account's
|
|
103
|
+
* usage: callers back off, never read it as an exhausted or expired login. `retryAt` is when the provider asked to
|
|
104
|
+
* be tried again (ISO 8601 UTC), or null when it did not say.
|
|
105
|
+
*/
|
|
106
|
+
rateLimit?: { retryAt: string | null };
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
export interface IHarnessStatusFact {
|
|
@@ -127,11 +134,30 @@ export interface IHarnessUsageWindow {
|
|
|
127
134
|
headline?: true;
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The provider's own name for an account's plan, and where that name comes from.
|
|
139
|
+
*
|
|
140
|
+
* `live` was read from the provider in this lookup; `stored` is the last name the saved login
|
|
141
|
+
* carries, which is the only answer available while a provider cannot be reached.
|
|
142
|
+
*/
|
|
143
|
+
export interface IAccountType {
|
|
144
|
+
plan: string;
|
|
145
|
+
source: 'live' | 'stored';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Live billing information only. Never infer these fields from a plan or entitlement expiry. Dates are ISO 8601 UTC. */
|
|
149
|
+
export interface IHarnessBilling {
|
|
150
|
+
hasActiveSubscription?: boolean;
|
|
151
|
+
autoRenew?: boolean;
|
|
152
|
+
renewsAt?: string;
|
|
153
|
+
cancelsAt?: string;
|
|
154
|
+
expiresAt?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
130
157
|
export interface IHarnessStatusSummary {
|
|
131
158
|
/** A plan name does not imply an active billing status. */
|
|
132
|
-
subscription?:
|
|
133
|
-
|
|
134
|
-
billing?: { hasActiveSubscription?: boolean; autoRenew?: boolean; renewsAt?: string; cancelsAt?: string; expiresAt?: string };
|
|
159
|
+
subscription?: IAccountType;
|
|
160
|
+
billing?: IHarnessBilling;
|
|
135
161
|
/** Missing windows are unknown, not unused. Times are ISO 8601 UTC. */
|
|
136
162
|
usageWindows?: IHarnessUsageWindow[];
|
|
137
163
|
resets?: {
|
|
@@ -146,6 +172,27 @@ export interface IHarnessOutcome {
|
|
|
146
172
|
problems: string[];
|
|
147
173
|
}
|
|
148
174
|
|
|
175
|
+
/**
|
|
176
|
+
* A caller's status-reading session, such as one `authswitch watch`. Within it an adapter may reuse a lookup whose
|
|
177
|
+
* answer rarely changes (a Claude profile) instead of repeating it on every read; outside a session every lookup is live.
|
|
178
|
+
*/
|
|
179
|
+
export interface IHarnessStatusSession {
|
|
180
|
+
/**
|
|
181
|
+
* The value `lookupArg` resolved to under `keyArg` less than `maxAgeMsArg` ago, otherwise the lookup's fresh result,
|
|
182
|
+
* which is then remembered. A rejected lookup is never remembered. Keys belong to the adapter that uses them, which
|
|
183
|
+
* prefixes them with its own id, so a key always names values of one type.
|
|
184
|
+
*/
|
|
185
|
+
reuse<T>(keyArg: string, maxAgeMsArg: number, lookupArg: () => Promise<T>): Promise<T>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** One status lookup; see `IAuthHarness.readAccountStatus` for what it may and may not change. */
|
|
189
|
+
export interface IHarnessStatusOptions {
|
|
190
|
+
/** Cancels the lookup's requests; an adapter still bounds each request with its own timeout. */
|
|
191
|
+
signal?: AbortSignal;
|
|
192
|
+
/** The session the read belongs to, when the caller reads the same accounts repeatedly. */
|
|
193
|
+
session?: IHarnessStatusSession;
|
|
194
|
+
}
|
|
195
|
+
|
|
149
196
|
/** A single text-only inference request. Credentials remain inside the adapter. */
|
|
150
197
|
export interface IHarnessPreuseOptions {
|
|
151
198
|
prompt: string;
|
|
@@ -171,7 +218,32 @@ export interface IAuthHarness {
|
|
|
171
218
|
readState(): THarnessResult<IHarnessState>;
|
|
172
219
|
/** Running instances of this harness, when the adapter owns their lifecycle. */
|
|
173
220
|
readonly processes?: IHarnessProcessControl;
|
|
174
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Whether running instances pick up a credential swap by themselves, on their next request.
|
|
223
|
+
*
|
|
224
|
+
* Such a harness is never offered a stop before a switch, and its outcomes carry no restart advice;
|
|
225
|
+
* an explicit stop request is still honoured through `processes`. Absent means running instances keep
|
|
226
|
+
* the login they loaded until they are restarted.
|
|
227
|
+
*/
|
|
228
|
+
readonly liveSwap?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Set when this harness's provider never reports renewal or cancellation dates, as a lower-case clause
|
|
231
|
+
* that completes "<label>: ...". Views then explain the missing dates once instead of per account.
|
|
232
|
+
*/
|
|
233
|
+
readonly renewalUnavailableReason?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Whether `authswitch watch` may switch this harness's login by itself when the active account runs out: every
|
|
236
|
+
* saved account's usage can be read, and a switch needs no session of the user's stopped. A helper process the
|
|
237
|
+
* adapter manages itself may still be restarted by its own switch -- Codex' app-server is, because its
|
|
238
|
+
* credential file cannot be rewritten underneath it. Absent means the watch never switches this harness.
|
|
239
|
+
*/
|
|
240
|
+
readonly autoSwitch?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* The account's live status. It must never activate, switch or clear a login. It may renew the tokens of the
|
|
243
|
+
* saved copy it reads when the provider leaves it no other way to answer -- the Claude Code adapter does, for
|
|
244
|
+
* saved and inactive logins only, under its store's lock -- and must leave the active login's credential alone.
|
|
245
|
+
*/
|
|
246
|
+
readAccountStatus(accountIdArg: string, optionsArg?: IHarnessStatusOptions): Promise<IHarnessAccountStatus>;
|
|
175
247
|
readonly loginProviders?: IHarnessLoginProvider[];
|
|
176
248
|
beginLogin?(options: IHarnessLoginOptions): Promise<IHarnessLoginHandle>;
|
|
177
249
|
/** Optional quota-consuming capability; never changes the active login. */
|
package/ts/interfaces.list.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
|
|
1
|
+
import type { IAccountType, IHarnessAccount, IHarnessAccountStatus, IHarnessBilling, IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
|
|
2
2
|
|
|
3
3
|
/** Credential-free, versioned output of list --json. Missing status fields stay omitted. */
|
|
4
4
|
export interface IAccountList {
|
|
@@ -39,6 +39,10 @@ export interface IAccountLimitRow {
|
|
|
39
39
|
accountId: string | null;
|
|
40
40
|
slotId?: string;
|
|
41
41
|
isActive: boolean;
|
|
42
|
+
/** The adapter's plan name (`max`, `pro`), sanitised but not display-formatted; null when none is known or the row names no account. */
|
|
43
|
+
accountType: IAccountType | null;
|
|
44
|
+
/** The account's live billing fields as its provider reported them (ISO UTC dates); null when none were reported or the row names no account. */
|
|
45
|
+
billing: IHarnessBilling | null;
|
|
42
46
|
/** The provider's own window name, or null when no limit data is available. */
|
|
43
47
|
limitType: string | null;
|
|
44
48
|
scope: 'account' | 'feature' | null;
|
|
@@ -70,6 +74,8 @@ export interface IActiveAccountRow {
|
|
|
70
74
|
account: string | null;
|
|
71
75
|
accountId: string | null;
|
|
72
76
|
slotId?: string;
|
|
77
|
+
/** The adapter's plan name (`max`, `pro`), sanitised but not display-formatted; null when none is known or the row names no account. */
|
|
78
|
+
accountType: IAccountType | null;
|
|
73
79
|
/** When authswitch last saved this login; null when it was never stashed. */
|
|
74
80
|
savedAt: string | null;
|
|
75
81
|
/** Human elapsed time for the same `savedAt`, relative to `generatedAt`. */
|
package/ts/plugins.ts
CHANGED
|
@@ -13,6 +13,15 @@ const nativeRequire = createRequire(import.meta.url);
|
|
|
13
13
|
/** SQLite is needed only when accessing a Codex state database, not when hosting account APIs. */
|
|
14
14
|
export const loadSqlite = (): typeof import('node:sqlite') => nativeRequire('node:sqlite');
|
|
15
15
|
|
|
16
|
+
// third party modules
|
|
17
|
+
import type * as properLockfile from 'proper-lockfile';
|
|
18
|
+
export type TProperLockfile = typeof properLockfile;
|
|
19
|
+
/**
|
|
20
|
+
* Claude Code's own lock library, loaded on first use: it installs process exit hooks when it loads, which a
|
|
21
|
+
* process that never changes a Claude Code login (such as a host embedding the account service) does not need.
|
|
22
|
+
*/
|
|
23
|
+
export const loadProperLockfile = (): Promise<TProperLockfile> => import('proper-lockfile');
|
|
24
|
+
|
|
16
25
|
// @push.rocks modules
|
|
17
26
|
import * as smartconsole from '@push.rocks/smartconsole';
|
|
18
27
|
export { smartconsole };
|
package/ts/ratelimit.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a provider that answered HTTP 429 asked to be tried again, read from its `Retry-After` header (RFC 9110: a
|
|
3
|
+
* number of seconds, or an HTTP date) and returned as ISO 8601 UTC. An absent or unreadable header is null: the
|
|
4
|
+
* provider did not say, and the caller picks its own backoff.
|
|
5
|
+
*/
|
|
6
|
+
export const retryAtFrom = (headerArg: string | null, nowArg: number): string | null => {
|
|
7
|
+
const value = headerArg?.trim() ?? '';
|
|
8
|
+
const time = /^\d{1,9}$/.test(value) ? nowArg + Number(value) * 1000 : /^[A-Za-z]{3}, .+ GMT$/.test(value) ? Date.parse(value) : Number.NaN;
|
|
9
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** The latest retry time several refused requests asked for; null when none of them said. */
|
|
13
|
+
export const latestRetryAt = (retryAtsArg: readonly (string | null)[]): string | null =>
|
|
14
|
+
retryAtsArg.filter((value): value is string => value !== null).sort().at(-1) ?? null;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { compactUntil, usagePercentText } from './accounts.js';
|
|
2
|
+
import { plainText } from './formatting.js';
|
|
3
|
+
import type { IHarnessUsageWindow } from './interfaces.harness.js';
|
|
4
|
+
|
|
5
|
+
/** What a watch knows about one account of a harness when it decides. */
|
|
6
|
+
export interface IWatchReading {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
/** Only a saved account can be switched to. */
|
|
10
|
+
isSaved: boolean;
|
|
11
|
+
/** The provider's usage windows from a reading recent enough to act on; null while the usage is unknown. */
|
|
12
|
+
windows: readonly IHarnessUsageWindow[] | null;
|
|
13
|
+
/** Why the usage is unknown, for a decision that depends on it. */
|
|
14
|
+
unknownReason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IWatchPolicyInput {
|
|
18
|
+
activeId: string | null;
|
|
19
|
+
readings: readonly IWatchReading[];
|
|
20
|
+
/** The used percentage at which a window counts as used up. */
|
|
21
|
+
threshold: number;
|
|
22
|
+
/** A used-up account is traded for another used-up one only when that one is usable again more than this much sooner. */
|
|
23
|
+
intervalMs: number;
|
|
24
|
+
now: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `ok` says whether the active account is fine; a stay that is not ok still has nothing better to switch to. */
|
|
28
|
+
export type TWatchDecision =
|
|
29
|
+
| { action: 'stay'; ok: boolean; reason: string }
|
|
30
|
+
| { action: 'switch'; target: string; reason: string };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How far below the threshold a switch target must be on every limit, so that a switch never lands on an account that
|
|
34
|
+
* is about to reach it and the watch never flips back and forth.
|
|
35
|
+
*/
|
|
36
|
+
const WATCH_HYSTERESIS = 10;
|
|
37
|
+
|
|
38
|
+
const accountWindows = (readingArg: IWatchReading): IHarnessUsageWindow[] => (readingArg.windows ?? []).filter(window => window.scope !== 'feature');
|
|
39
|
+
const featureWindows = (readingArg: IWatchReading): IHarnessUsageWindow[] => (readingArg.windows ?? []).filter(window => window.scope === 'feature');
|
|
40
|
+
|
|
41
|
+
/** The fullest of the windows at or above the threshold (the longer one on a tie), which names why an account is limited. */
|
|
42
|
+
const fullest = (windowsArg: readonly IHarnessUsageWindow[], thresholdArg: number): IHarnessUsageWindow | undefined =>
|
|
43
|
+
windowsArg.filter(window => window.usedPercent >= thresholdArg)
|
|
44
|
+
.sort((left, right) => right.usedPercent - left.usedPercent || right.durationSeconds - left.durationSeconds)[0];
|
|
45
|
+
|
|
46
|
+
const limitText = (windowArg: IHarnessUsageWindow, thresholdArg: number): string =>
|
|
47
|
+
`${plainText(windowArg.label)} ${usagePercentText(windowArg.usedPercent)} ≥ ${thresholdArg}%`;
|
|
48
|
+
|
|
49
|
+
/** When a window resets; one without a known reset never does. */
|
|
50
|
+
const resetTime = (windowArg: IHarnessUsageWindow): number => {
|
|
51
|
+
const time = windowArg.resetAt === null ? Number.NaN : Date.parse(windowArg.resetAt);
|
|
52
|
+
return Number.isFinite(time) ? time : Number.POSITIVE_INFINITY;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** When every one of the account's used-up windows has reset. */
|
|
56
|
+
const usableAt = (readingArg: IWatchReading, thresholdArg: number): number =>
|
|
57
|
+
Math.max(...accountWindows(readingArg).filter(window => window.usedPercent >= thresholdArg).map(resetTime));
|
|
58
|
+
|
|
59
|
+
/** The reset of the account's longest window: capacity that expires first is used first. */
|
|
60
|
+
const longestWindowReset = (readingArg: IWatchReading): number => {
|
|
61
|
+
const windows = accountWindows(readingArg);
|
|
62
|
+
const longest = Math.max(...windows.map(window => window.durationSeconds));
|
|
63
|
+
return Math.min(...windows.filter(window => window.durationSeconds === longest).map(resetTime));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const peakUsage = (readingArg: IWatchReading): number => Math.max(...accountWindows(readingArg).map(window => window.usedPercent));
|
|
67
|
+
|
|
68
|
+
/** Numbers that are both unbounded compare as equal, so the next key decides. */
|
|
69
|
+
const ascending = (leftArg: number, rightArg: number): number => leftArg === rightArg ? 0 : leftArg < rightArg ? -1 : 1;
|
|
70
|
+
const byLabel = (leftArg: IWatchReading, rightArg: IWatchReading): number =>
|
|
71
|
+
leftArg.label.localeCompare(rightArg.label) || leftArg.id.localeCompare(rightArg.id);
|
|
72
|
+
const byPreference = (leftArg: IWatchReading, rightArg: IWatchReading): number =>
|
|
73
|
+
ascending(longestWindowReset(leftArg), longestWindowReset(rightArg)) || ascending(peakUsage(leftArg), peakUsage(rightArg)) || byLabel(leftArg, rightArg);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which account a harness should use, from one set of readings. Pure: the same input always decides the same.
|
|
77
|
+
*
|
|
78
|
+
* - The active account is used up when any of its account windows is at or above the threshold. The watch then
|
|
79
|
+
* switches to the saved account whose account windows are all at least `WATCH_HYSTERESIS` points below it,
|
|
80
|
+
* preferring the one whose longest window resets soonest, then the least used, then by label.
|
|
81
|
+
* - When no account qualifies and every other saved account with a known reading is used up too, it switches to the
|
|
82
|
+
* one that is usable again first, and only when that is more than one interval sooner than the active account.
|
|
83
|
+
* - An active account limited only on a feature window (one model) switches to an account that is below the margin on
|
|
84
|
+
* every window; without one it stays, because the active account still serves everything else.
|
|
85
|
+
* - A reading that is unknown, whatever the cause, never counts as used up and never makes an account a target.
|
|
86
|
+
*/
|
|
87
|
+
export const decideWatchAction = (inputArg: IWatchPolicyInput): TWatchDecision => {
|
|
88
|
+
const { threshold } = inputArg;
|
|
89
|
+
const stay = (okArg: boolean, reasonArg: string): TWatchDecision => ({ action: 'stay', ok: okArg, reason: reasonArg });
|
|
90
|
+
const active = inputArg.readings.find(reading => reading.id === inputArg.activeId);
|
|
91
|
+
if (!active) return stay(false, 'no active login');
|
|
92
|
+
if (active.windows === null) return stay(false, `usage unknown${active.unknownReason ? ` (${active.unknownReason})` : ''}`);
|
|
93
|
+
const margin = threshold - WATCH_HYSTERESIS;
|
|
94
|
+
const known = inputArg.readings.filter(reading => reading.id !== active.id && reading.isSaved && reading.windows !== null);
|
|
95
|
+
const below = (windowsArg: readonly IHarnessUsageWindow[]): boolean => windowsArg.every(window => window.usedPercent < margin);
|
|
96
|
+
const best = (candidatesArg: IWatchReading[]): IWatchReading | undefined => candidatesArg.sort(byPreference)[0];
|
|
97
|
+
|
|
98
|
+
const usedUp = fullest(accountWindows(active), threshold);
|
|
99
|
+
if (usedUp) {
|
|
100
|
+
const limit = limitText(usedUp, threshold);
|
|
101
|
+
const target = best(known.filter(reading => accountWindows(reading).length > 0 && below(accountWindows(reading))));
|
|
102
|
+
if (target) return { action: 'switch', target: target.id, reason: limit };
|
|
103
|
+
const exhausted = known.filter(reading => fullest(accountWindows(reading), threshold) !== undefined);
|
|
104
|
+
if (!exhausted.length || exhausted.length < known.length) return stay(false, `${limit}; no other saved account is known to be below ${margin}%`);
|
|
105
|
+
const soonest = exhausted.map(reading => ({ reading, at: usableAt(reading, threshold) }))
|
|
106
|
+
.sort((left, right) => ascending(left.at, right.at) || byLabel(left.reading, right.reading))[0];
|
|
107
|
+
if (soonest.at < usableAt(active, threshold) - inputArg.intervalMs) {
|
|
108
|
+
return { action: 'switch', target: soonest.reading.id, reason: `${limit}; every saved account is used up, and ${plainText(soonest.reading.label)} is usable again first (in ${compactUntil(new Date(soonest.at).toISOString(), inputArg.now)})` };
|
|
109
|
+
}
|
|
110
|
+
return stay(false, `${limit}; every saved account is used up, and none is usable again sooner`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const featureLimit = fullest(featureWindows(active), threshold);
|
|
114
|
+
if (featureLimit) {
|
|
115
|
+
const limit = limitText(featureLimit, threshold);
|
|
116
|
+
const target = best(known.filter(reading => accountWindows(reading).length > 0 && below(reading.windows!)));
|
|
117
|
+
if (target) return { action: 'switch', target: target.id, reason: limit };
|
|
118
|
+
return stay(false, `${limit}; no other saved account is known to be below ${margin}% on every limit`);
|
|
119
|
+
}
|
|
120
|
+
return stay(true, `every limit below ${threshold}%`);
|
|
121
|
+
};
|