@modelprofile.com/authswitch 3.0.0 → 3.2.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 +1 -1
- package/dist_ts/accounts.d.ts +13 -5
- package/dist_ts/accounts.js +20 -5
- package/dist_ts/classes.accountlist.d.ts +9 -3
- package/dist_ts/classes.accountlist.js +66 -32
- package/dist_ts/classes.claudestatus.js +94 -18
- package/dist_ts/classes.limits.js +36 -10
- package/dist_ts/classes.tui.js +4 -3
- package/dist_ts/interfaces.harness.d.ts +18 -8
- package/dist_ts/interfaces.list.d.ts +5 -1
- package/package.json +3 -3
- package/readme.md +107 -33
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/accounts.ts +22 -8
- package/ts/classes.accountlist.ts +69 -33
- package/ts/classes.claudestatus.ts +102 -14
- package/ts/classes.limits.ts +43 -11
- package/ts/classes.tui.ts +5 -4
- package/ts/interfaces.harness.ts +20 -8
- package/ts/interfaces.list.ts +5 -1
package/readme.md
CHANGED
|
@@ -188,7 +188,21 @@ the local file login, so launch-time flags, managed policies and credentials sup
|
|
|
188
188
|
by embedding applications remain under Claude Code's control.
|
|
189
189
|
|
|
190
190
|
Claude's direct OAuth profile and usage lookups report the plan, weekly and five-hour
|
|
191
|
-
usage, model-
|
|
191
|
+
usage, per-model and per-surface windows, and available extra-usage budget information.
|
|
192
|
+
The usage response's `limits[]` rows are the authoritative window list: which
|
|
193
|
+
meters apply, their scope, the service's `severity` (`normal`, `warning`,
|
|
194
|
+
`critical`) and its headline pick are Claude's, so a new per-model weekly limit
|
|
195
|
+
such as **Fable weekly** appears without an authswitch release. `list --json` keeps
|
|
196
|
+
the rows in the service's order; the tables and `limits --json` order them as
|
|
197
|
+
described under **Condensed views**. Rows are classified on their
|
|
198
|
+
kind and reset group exactly as sent. Labels combine the service's model or surface
|
|
199
|
+
name with the window period, and such a scoped row is treated as a feature limit.
|
|
200
|
+
Rows in a reset group authswitch cannot represent (anything but the five-hour
|
|
201
|
+
session and the weekly window) are not shown; one **Unsupported limits**
|
|
202
|
+
availability fact names them. A malformed row, or a `limits` value that is not a
|
|
203
|
+
list, makes the whole usage lookup unavailable rather than partially guessed.
|
|
204
|
+
Responses without `limits[]` rows fall back to the older keyed weekly, five-hour
|
|
205
|
+
and model windows.
|
|
192
206
|
These endpoints require the `user:profile` scope and may reject expired or restricted
|
|
193
207
|
logins. A null reset timestamp is shown as **Not scheduled**. Billing renewal and
|
|
194
208
|
cancellation dates and earned reset credits are unavailable from these endpoints;
|
|
@@ -240,7 +254,8 @@ an uncertain request. `--json` remains a list-only option.
|
|
|
240
254
|
### Management dashboard
|
|
241
255
|
|
|
242
256
|
`authswitch --tui` opens a resizable account table with subscription, usage and
|
|
243
|
-
reset availability, usage bars
|
|
257
|
+
reset availability, usage bars for the lead window and the next general window,
|
|
258
|
+
scrollable account details and an activity log.
|
|
244
259
|
With multiple registered adapters, a harness selector switches the view; account
|
|
245
260
|
selection and mutations always belong to the displayed harness. Status loads
|
|
246
261
|
incrementally through each account's read-only API, without activating it.
|
|
@@ -322,17 +337,25 @@ output to Codex. Each section includes saved accounts and an identifiable active
|
|
|
322
337
|
login even if it has never been saved. `*` marks the active account, and the saved
|
|
323
338
|
marker checks the credential itself rather than just the existence of metadata.
|
|
324
339
|
|
|
325
|
-
The overview compares login state, plan provenance,
|
|
326
|
-
|
|
340
|
+
The overview compares login state, plan provenance, lead-window usage (the
|
|
341
|
+
provider's headline window, otherwise weekly), earned resets and that window's
|
|
342
|
+
reset countdown. Countdowns use days, hours and minutes, such as
|
|
327
343
|
`7d 5h 6min`;
|
|
328
344
|
`<1min` means less than a minute remains and `due` means the reported deadline has
|
|
329
345
|
passed, without claiming the service has refreshed the quota. All countdowns share
|
|
330
346
|
the same snapshot time.
|
|
331
|
-
Usage and countdown refer to the same
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
Usage and countdown refer to the same window. When the provider picks a headline
|
|
348
|
+
window (Claude does, for example its Fable weekly limit), that window leads.
|
|
349
|
+
Otherwise the
|
|
350
|
+
general account window leads, preferring weekly; if no weekly window is reported,
|
|
351
|
+
the longest general window is shown with its actual duration. Other exhausted
|
|
352
|
+
general windows retain their own reset warning. A general window is named by its
|
|
353
|
+
period (`Weekly`, `5h`) unless another shown general window has the same period; a
|
|
354
|
+
feature limit, or a general window sharing its period, is named by its label
|
|
355
|
+
(`Fable weekly: 100% used`, `Claude weekly (weekly_oauth) exhausted`). Feature
|
|
356
|
+
limits such as Spark and code review have a separate table and determine the
|
|
357
|
+
summary only when the provider picks one as its headline. The TUI uses the same
|
|
358
|
+
selection and names.
|
|
336
359
|
Window durations come from the provider, not from the plan name or
|
|
337
360
|
primary/secondary position; a Pro account with only a general weekly window gets
|
|
338
361
|
no invented 5-hour limit.
|
|
@@ -340,34 +363,50 @@ no invented 5-hour limit.
|
|
|
340
363
|
Email addresses identify Codex accounts throughout the overview, usage/reset
|
|
341
364
|
schedules, earned reset expiry details, grouped credits and activity metrics,
|
|
342
365
|
saved-login notes and availability/actions tables. Other harnesses use their
|
|
343
|
-
account labels.
|
|
344
|
-
|
|
366
|
+
account labels. When accounts of one harness share a label, every table numbers
|
|
367
|
+
the later ones (`alice@example.com (2)`), so each account keeps its own column in
|
|
368
|
+
the metric comparisons and reads the same everywhere. Provider facts already
|
|
369
|
+
represented by structured fields are not repeated. Narrow terminals use compact
|
|
370
|
+
account sections. Missing
|
|
345
371
|
data stays Unavailable or Not reported; known zeroes remain zero. Stored plans are
|
|
346
372
|
marked unverified. Renewal/cancellation dates are shown only when an adapter can
|
|
347
373
|
provide live billing data. Codex uses the desktop app's account-check endpoint for
|
|
348
374
|
automatic renewal, explicit renewal/cancellation dates and subscription expiry.
|
|
349
375
|
|
|
376
|
+
Tables that list an account on several rows (the usage/reset schedule,
|
|
377
|
+
feature-specific limits, earned reset details, and availability/actions) draw a
|
|
378
|
+
divider between one account and the next. The divider follows the account id, not
|
|
379
|
+
the email, so two accounts that share an address stay separate blocks, and the
|
|
380
|
+
harness-level availability row is a block of its own. The overview and the metric
|
|
381
|
+
comparisons show each account once and have no dividers. Each `list` section covers
|
|
382
|
+
one harness, so no colour line is drawn. Narrow terminals separate the same blocks
|
|
383
|
+
with a rule line.
|
|
384
|
+
|
|
350
385
|
### Condensed views
|
|
351
386
|
|
|
352
387
|
`authswitch limits` answers "how much is left, and when does it come back" in one
|
|
353
388
|
table. It is one row per account **and** limit type, sorted by provider and then by
|
|
354
|
-
account email
|
|
389
|
+
account email with each harness and each account kept together, and a two-unit countdown (`2h 13m`, `3d 4h`, `45m`, `<1m`, `due`):
|
|
355
390
|
|
|
356
391
|
```
|
|
357
392
|
Account limits
|
|
358
393
|
┌──────────────────────┬─────────────────────────┬───────────────────────┬────────┬───────────────┐
|
|
359
394
|
│ Provider │ Account │ Limit type │ Used % │ Resets in │
|
|
360
395
|
├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
│
|
|
370
|
-
|
|
396
|
+
┃ Claude Code │ phil@example.com │ Claude weekly │ 75% │ 4d 3h │
|
|
397
|
+
┃ Claude Code │ phil@example.com │ Claude five-hour │ 12% │ 1h 2m │
|
|
398
|
+
┃ Claude Code │ phil@example.com │ Fable weekly │ 88% │ 2d 5h │
|
|
399
|
+
├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
|
|
400
|
+
┃ Codex │ alice@example.com │ Codex secondary │ 12% │ 3d 4h │
|
|
401
|
+
┃ Codex │ alice@example.com │ Codex primary │ 37% │ 2h 13m │
|
|
402
|
+
┃ Codex │ alice@example.com │ Code review secondary │ 100% │ 3h 59m │
|
|
403
|
+
├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
|
|
404
|
+
┃ Codex │ bob@example.com │ n/a │ n/a │ n/a │
|
|
405
|
+
├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
|
|
406
|
+
┃ OpenCode / anthropic │ anthropic API key 20e75 │ n/a │ n/a │ n/a │
|
|
407
|
+
┃ │ 1c3b707 │ │ │ │
|
|
408
|
+
├──────────────────────┼─────────────────────────┼───────────────────────┼────────┼───────────────┤
|
|
409
|
+
┃ OpenCode / openai │ alice@example.com │ Codex primary │ 0% │ not scheduled │
|
|
371
410
|
└──────────────────────┴─────────────────────────┴───────────────────────┴────────┴───────────────┘
|
|
372
411
|
|
|
373
412
|
n/a — Codex · bob@example.com: Subscription and limits: Login expired or was rejected.
|
|
@@ -382,9 +421,31 @@ account never blanks another's numbers. `not scheduled` means the provider repor
|
|
|
382
421
|
usage without a reset deadline; it is not the same as `n/a`.
|
|
383
422
|
|
|
384
423
|
Limit types are the provider's own window names, ordered general-before-feature and
|
|
385
|
-
weekly-first within an account.
|
|
424
|
+
weekly-first within an account. Claude's per-model and per-surface limits carry the
|
|
425
|
+
service's name (`Fable weekly`); an unscoped Claude meter other than the five-hour
|
|
426
|
+
session and the weekly total keeps its kind in parentheses
|
|
427
|
+
(`Claude weekly (weekly_oauth)`). The provider column names the harness, qualified by
|
|
386
428
|
the credential slot when a harness owns several (`OpenCode / openai`).
|
|
387
429
|
|
|
430
|
+
**Used %** is coloured by the provider's own reading when it reports one: orange for
|
|
431
|
+
`warning`, red for `critical`, uncoloured for `normal`. Without a reading, `n/a` is
|
|
432
|
+
orange and an exhausted window (100% or more) is red. Colour is omitted when
|
|
433
|
+
`NO_COLOR` is set, output is not a terminal, or the terminal is narrower than 40
|
|
434
|
+
columns.
|
|
435
|
+
|
|
436
|
+
Rows are grouped in two independent ways. A divider separates one account from the
|
|
437
|
+
next, so an account's limit types read as one block; a provider row without an
|
|
438
|
+
account is a block of its own. A line along the left edge (`┃`) marks the harness:
|
|
439
|
+
all its rows share one colour, including every credential slot of an OpenCode
|
|
440
|
+
installation, and the next harness takes the next colour (cyan, orange, green, pink,
|
|
441
|
+
blue, then again from the start). Without colour the `┃` edge still shows where the
|
|
442
|
+
data rows are. Groups follow harness and account ids, never labels: two accounts
|
|
443
|
+
that share an email are separate blocks, ordered by account id, and two harnesses
|
|
444
|
+
that share a label are ordered by harness id. A harness is placed by its first
|
|
445
|
+
provider name, so no other provider sorts between its slots. Below 40 columns the
|
|
446
|
+
table becomes labelled lines; a rule line separates the accounts there, and no
|
|
447
|
+
colour line is drawn.
|
|
448
|
+
|
|
388
449
|
`authswitch active` answers "what am I logged in as" for every harness at once:
|
|
389
450
|
|
|
390
451
|
```
|
|
@@ -392,11 +453,11 @@ Active accounts
|
|
|
392
453
|
┌──────────────────────┬────────────────────────────────┬────────────┬─────────────────┐
|
|
393
454
|
│ Provider │ Account │ Saved │ Source │
|
|
394
455
|
├──────────────────────┼────────────────────────────────┼────────────┼─────────────────┤
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
456
|
+
┃ Claude Code │ phil@example.com │ 45m ago │ credential file │
|
|
457
|
+
┃ Codex │ alice@example.com │ 2h 13m ago │ credential file │
|
|
458
|
+
┃ Flex │ none │ n/a │ none │
|
|
459
|
+
┃ OpenCode / anthropic │ anthropic API key 20e751c3b707 │ 30d ago │ credential file │
|
|
460
|
+
┃ OpenCode / openai │ alice@example.com │ not saved │ credential file │
|
|
400
461
|
└──────────────────────┴────────────────────────────────┴────────────┴─────────────────┘
|
|
401
462
|
|
|
402
463
|
note — OpenCode / openai · alice@example.com: This active login is not saved yet; authswitch cannot restore it after a switch.
|
|
@@ -407,6 +468,8 @@ own live credential, `stash only` means nothing is active and only saved copies
|
|
|
407
468
|
exist, `none` means neither. **Saved** is how long ago `authswitch` last saved that
|
|
408
469
|
login, or `not saved` — which is also the warning that a switch could not bring it
|
|
409
470
|
back. A harness with no active login keeps its row and explains itself in a note.
|
|
471
|
+
Each harness has its own colour line, ordered and coloured as in `limits`; the
|
|
472
|
+
table has no dividers, since every row is a different credential slot.
|
|
410
473
|
|
|
411
474
|
**Source** also answers whether the file still holds what the last switch wrote.
|
|
412
475
|
When authswitch switches a login it records that account and the hash of the
|
|
@@ -440,7 +503,9 @@ arguments to these commands exit with 2.
|
|
|
440
503
|
`limits --json` emits `IAccountLimits` and `active --json` emits `IActiveAccounts`:
|
|
441
504
|
the same rows as the tables, plus the machine-readable fields the tables condense —
|
|
442
505
|
`accountId`, `slotId`, `scope`, `windowSeconds`, the ISO `resetAt` beside the human
|
|
443
|
-
`resetsIn`, the
|
|
506
|
+
`resetsIn`, the provider's `severity` (`normal`, `warning`, `critical`, or null when
|
|
507
|
+
it gave none) and `headline` (true for the window the provider picks for a
|
|
508
|
+
single-value summary), the ISO `savedAt` beside the human `savedAgo`, the per-row
|
|
444
509
|
`unavailableReason` that the footnotes summarise, and `drift`, which carries the
|
|
445
510
|
expected and current account ids, their labels, whether the current one is saved,
|
|
446
511
|
the ISO `switchedAt` of the switch that was undone, and the same sentence as `reason`. Both carry `schemaVersion: 1`, the
|
|
@@ -456,7 +521,9 @@ The exported `IAccountList` contract contains:
|
|
|
456
521
|
empty list).
|
|
457
522
|
- Each account's opaque `id`, `label`, `isActive`, verified `isStashed`, `savedAt`,
|
|
458
523
|
`details`, and `status` containing all labelled `facts`, `problems`, and optional
|
|
459
|
-
typed `summary` fields. Missing fields remain omitted, not replaced by zero
|
|
524
|
+
typed `summary` fields. Missing fields remain omitted, not replaced by zero;
|
|
525
|
+
`summary.usageWindows[]` carries `severity` and `headline` only when the provider
|
|
526
|
+
reported them.
|
|
460
527
|
|
|
461
528
|
JSON retains full fact strings and absolute ISO reset/expiry timestamps. It exports
|
|
462
529
|
the credential-free adapter contract, never credentials, raw HTTP responses or
|
|
@@ -561,11 +628,18 @@ explicit zeroes remain zero. A successful reset-detail lookup supplies the summa
|
|
|
561
628
|
count; if it fails, the limits response can still supply availability.
|
|
562
629
|
|
|
563
630
|
Each usage window may set `scope: 'account' | 'feature'` (default `account`).
|
|
564
|
-
Adapters mark model-specific or feature-specific quotas as `feature
|
|
565
|
-
|
|
566
|
-
|
|
631
|
+
Adapters mark model-specific or feature-specific quotas as `feature`; those quotas
|
|
632
|
+
appear in the feature tables, JSON and details and lead the account summary only
|
|
633
|
+
when the provider marks one as its headline (see below). Weekly windows are identified by `durationSeconds: 604800`;
|
|
567
634
|
neither window labels nor plan names are parsed to infer limits.
|
|
568
635
|
|
|
636
|
+
A window may also carry the provider's own reading: `severity?: TUsageSeverity`
|
|
637
|
+
(`'normal' | 'warning' | 'critical'`) and `headline?: true` for the window the
|
|
638
|
+
provider picks for a single-value summary. Adapters set them only when the provider
|
|
639
|
+
reports them, never from local thresholds. A headline window leads the account
|
|
640
|
+
summary whatever its scope; the first one in the adapter's order wins if several are
|
|
641
|
+
marked.
|
|
642
|
+
|
|
569
643
|
Optional `summary.billing` contains provider-reported `hasActiveSubscription`,
|
|
570
644
|
`autoRenew`, `renewsAt`, `cancelsAt`, and `expiresAt`, independent of the plan name.
|
|
571
645
|
Dates are ISO UTC and omitted
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/accounts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, IHarnessState,
|
|
1
|
+
import type { IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, IHarnessState, IHarnessUsageWindow } from './interfaces.harness.js';
|
|
2
2
|
import { plainText } from './formatting.js';
|
|
3
3
|
|
|
4
4
|
export interface IAccountRow {
|
|
@@ -72,20 +72,34 @@ export const accountPlan = (rowArg: IAccountRow): string => {
|
|
|
72
72
|
const subscription = rowArg.status?.summary?.subscription;
|
|
73
73
|
return subscription ? `${plainText(subscription.plan)}${subscription.source === 'stored' ? ' (stored; unverified)' : ' (live)'}` : rowArg.status ? 'Unavailable' : 'Loading';
|
|
74
74
|
};
|
|
75
|
-
type TUsageWindow = NonNullable<IHarnessStatusSummary['usageWindows']>[number];
|
|
76
75
|
type TAccountStatusRow = Pick<IAccountRow, 'status'>;
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* How a single-value summary names a window among the windows it shows. An account window is named by
|
|
78
|
+
* its period while it is the only shown account window of that period; a feature window, or an account
|
|
79
|
+
* window sharing its period with another, is named by its own label, so no meter reads as another's.
|
|
80
|
+
*/
|
|
81
|
+
export const usageWindowName = (windowArg: IHarnessUsageWindow, shownArg: readonly IHarnessUsageWindow[]): string =>
|
|
82
|
+
windowArg.scope === 'feature' || shownArg.some(other => other !== windowArg && other.scope !== 'feature' && other.durationSeconds === windowArg.durationSeconds)
|
|
83
|
+
? plainText(windowArg.label)
|
|
84
|
+
: windowArg.durationSeconds === 604800 ? 'Weekly' : duration(windowArg.durationSeconds);
|
|
78
85
|
/** Order by scope and actual duration: a provider's primary slot need not be five hours. */
|
|
79
|
-
export const orderedUsageWindows = (windowsArg: readonly
|
|
86
|
+
export const orderedUsageWindows = (windowsArg: readonly IHarnessUsageWindow[] = []): IHarnessUsageWindow[] => [...windowsArg].sort((left, right) =>
|
|
80
87
|
Number(left.scope === 'feature') - Number(right.scope === 'feature') ||
|
|
81
88
|
Number(left.durationSeconds !== 604800) - Number(right.durationSeconds !== 604800) || right.durationSeconds - left.durationSeconds);
|
|
82
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The windows a single-value summary shows, lead window first: the provider's headline pick, whatever
|
|
91
|
+
* its scope, then the account windows in display order. Other feature quotas never lead a summary.
|
|
92
|
+
*/
|
|
93
|
+
export const accountUsageWindows = (rowArg: TAccountStatusRow): IHarnessUsageWindow[] | undefined => {
|
|
83
94
|
const windows = rowArg.status?.summary?.usageWindows;
|
|
84
|
-
|
|
95
|
+
if (windows === undefined) return undefined;
|
|
96
|
+
const headline = windows.find(window => window.headline === true);
|
|
97
|
+
const accountWindows = orderedUsageWindows(windows.filter(window => window !== headline && window.scope !== 'feature'));
|
|
98
|
+
return headline === undefined ? accountWindows : [headline, ...accountWindows];
|
|
85
99
|
};
|
|
86
100
|
export const accountUsage = (rowArg: TAccountStatusRow): string => {
|
|
87
101
|
const windows = accountUsageWindows(rowArg);
|
|
88
|
-
return windows?.length ? `${
|
|
102
|
+
return windows?.length ? `${usageWindowName(windows[0], windows)}: ${windows[0].usedPercent}% used` : windows ? 'None reported' : rowArg.status ? 'Unavailable' : 'Loading';
|
|
89
103
|
};
|
|
90
104
|
export const accountResets = (rowArg: IAccountRow): string => rowArg.status?.summary?.resets === undefined ? rowArg.status ? 'Unavailable' : 'Loading' : String(rowArg.status.summary.resets.available);
|
|
91
105
|
export const accountNextReset = (rowArg: TAccountStatusRow, nowArg = Date.now()): string => {
|
|
@@ -96,7 +110,7 @@ export const accountQuotaSummary = (rowArg: TAccountStatusRow, nowArg: number):
|
|
|
96
110
|
const windows = accountUsageWindows(rowArg);
|
|
97
111
|
return [accountUsage(rowArg), ...(windows?.length ? [
|
|
98
112
|
`Reset in: ${accountNextReset(rowArg, nowArg)}`,
|
|
99
|
-
...windows.slice(1).filter(window => window.usedPercent >= 100).map(window => `${
|
|
113
|
+
...windows.slice(1).filter(window => window.usedPercent >= 100).map(window => `${usageWindowName(window, windows)} exhausted\nResets in: ${until(window.resetAt, nowArg)}`),
|
|
100
114
|
] : [])].join('\n');
|
|
101
115
|
};
|
|
102
116
|
export const accountState = (rowArg: IAccountRow): string => `${rowArg.account.isActive ? '* active, ' : ''}${rowArg.account.isStashed ? 'saved' : 'not saved'}`;
|
|
@@ -5,6 +5,7 @@ import type { IAccountList, IHarnessAccountList } from './interfaces.list.js';
|
|
|
5
5
|
import { bold, dim, orange, plainText } from './formatting.js';
|
|
6
6
|
|
|
7
7
|
type TListedAccount = IHarnessAccountList['accounts'][number];
|
|
8
|
+
type TNamedAccount = TListedAccount & { name: string };
|
|
8
9
|
interface IDisplayFact { account: number; label: string; value: string; }
|
|
9
10
|
|
|
10
11
|
/** Allowlist the public data contract: never serialize a harness or its credential-bearing internals. */
|
|
@@ -16,7 +17,7 @@ const publicStatus = (statusArg: IHarnessAccountStatus): IHarnessAccountStatus =
|
|
|
16
17
|
...(summary === undefined ? {} : { summary: {
|
|
17
18
|
...(summary.subscription === undefined ? {} : { subscription: { plan: summary.subscription.plan, source: summary.subscription.source } }),
|
|
18
19
|
...(summary.billing === undefined ? {} : { billing: { hasActiveSubscription: summary.billing.hasActiveSubscription, autoRenew: summary.billing.autoRenew, renewsAt: summary.billing.renewsAt, cancelsAt: summary.billing.cancelsAt, expiresAt: summary.billing.expiresAt } }),
|
|
19
|
-
...(summary.usageWindows === undefined ? {} : { usageWindows: summary.usageWindows.map(({ label, scope, durationSeconds, usedPercent, resetAt }) => ({ label, scope, durationSeconds, usedPercent, resetAt })) }),
|
|
20
|
+
...(summary.usageWindows === undefined ? {} : { usageWindows: summary.usageWindows.map(({ label, scope, durationSeconds, usedPercent, resetAt, severity, headline }) => ({ label, scope, durationSeconds, usedPercent, resetAt, severity, headline })) }),
|
|
20
21
|
...(summary.resets === undefined ? {} : { resets: {
|
|
21
22
|
available: summary.resets.available,
|
|
22
23
|
...(summary.resets.details === undefined ? {} : { details: summary.resets.details.map(({ status, kind, expiresAt }) => ({ status, kind, expiresAt })) }),
|
|
@@ -53,20 +54,57 @@ export const readAccountList = async (harnessesArg: IAuthHarness[]): Promise<IAc
|
|
|
53
54
|
export const consoleWidth = (): number => process.stdout.columns ?? 100;
|
|
54
55
|
export const consoleHeading = (textArg: string): void => { process.stdout.write(`\n${bold(textArg)}\n`); };
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* One table implementation for every command, including the label-per-line fallback on narrow terminals.
|
|
59
|
+
*
|
|
60
|
+
* `groupsArg` is smartconsole's optional visual grouping: a divider between runs of consecutive rows with
|
|
61
|
+
* different keys, and a colour line along the left edge per run. Keys are identities, never display labels,
|
|
62
|
+
* and the caller orders the rows so that every group is one run. Below 40 columns the fallback separates
|
|
63
|
+
* divider runs with one dim rule line; it has no left edge, so it draws no colour line.
|
|
64
|
+
*/
|
|
57
65
|
export const consoleTable = async <TRow>(
|
|
58
66
|
outArg: plugins.smartconsole.SmartConsole,
|
|
59
67
|
rowsArg: TRow[],
|
|
60
68
|
columnsArg: plugins.smartconsole.IBackendTableColumn<TRow>[],
|
|
69
|
+
groupsArg?: NoInfer<plugins.smartconsole.IBackendTableGroups<TRow>>,
|
|
61
70
|
): Promise<void> => {
|
|
62
71
|
if (!rowsArg.length) return;
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const width = consoleWidth();
|
|
73
|
+
if (width < 40) {
|
|
74
|
+
// smartconsole's run semantics: null and undefined are the same key.
|
|
75
|
+
const dividerKeys = rowsArg.map(row => groupsArg?.divider?.(row) ?? null);
|
|
76
|
+
const rule = ` ${dim('─'.repeat(Math.max(1, width - 4)))}\n\n`;
|
|
77
|
+
rowsArg.forEach((row, index) => {
|
|
78
|
+
if (index > 0 && dividerKeys[index] !== dividerKeys[index - 1]) process.stdout.write(rule);
|
|
65
79
|
process.stdout.write(columnsArg.map(column => ` ${column.title}: ${String(column.value(row) ?? 'Unavailable').replace(/\n/g, '\n ')}`).join('\n') + '\n\n');
|
|
66
|
-
}
|
|
80
|
+
});
|
|
67
81
|
return;
|
|
68
82
|
}
|
|
69
|
-
await outArg.table(rowsArg, {
|
|
83
|
+
await outArg.table(rowsArg, {
|
|
84
|
+
columns: columnsArg, overflow: 'wrap', groups: groupsArg,
|
|
85
|
+
theme: { header: { bold: true, foreground: 'cyan' }, border: { dim: true } },
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** List tables that repeat one account across rows divide it from the next account by its id. */
|
|
90
|
+
const accountDividers: plugins.smartconsole.IBackendTableGroups<{ accountId: string | null }> = { divider: row => row.accountId };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Names every account of one harness by its label, numbering each later account that repeats a label
|
|
94
|
+
* (`alice@example.com (2)`), so all tables name an account alike and the metric columns stay distinct.
|
|
95
|
+
*/
|
|
96
|
+
const nameAccounts = (accountsArg: TListedAccount[]): TNamedAccount[] => {
|
|
97
|
+
const labels = accountsArg.map(account => plainText(account.label));
|
|
98
|
+
const taken = new Set(labels);
|
|
99
|
+
return accountsArg.map((account, index) => {
|
|
100
|
+
const label = labels[index];
|
|
101
|
+
let name = label;
|
|
102
|
+
if (labels.indexOf(label) < index) {
|
|
103
|
+
for (let ordinal = 2; taken.has(name); ordinal++) name = `${label} (${ordinal})`;
|
|
104
|
+
taken.add(name);
|
|
105
|
+
}
|
|
106
|
+
return { ...account, name };
|
|
107
|
+
});
|
|
70
108
|
};
|
|
71
109
|
|
|
72
110
|
/** Shared, provider-independent list presentation. Adapters supply grouping metadata. */
|
|
@@ -75,10 +113,6 @@ export class AccountListRenderer {
|
|
|
75
113
|
private get width(): number { return consoleWidth(); }
|
|
76
114
|
private heading(textArg: string): void { consoleHeading(textArg); }
|
|
77
115
|
|
|
78
|
-
private async table<TRow>(rowsArg: TRow[], columnsArg: plugins.smartconsole.IBackendTableColumn<TRow>[]): Promise<void> {
|
|
79
|
-
await consoleTable(this.out, rowsArg, columnsArg);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
116
|
public async render(listArg: IAccountList): Promise<void> {
|
|
83
117
|
const now = Date.parse(listArg.generatedAt);
|
|
84
118
|
if (!listArg.harnesses.length) process.stdout.write('No harnesses registered.\n');
|
|
@@ -92,55 +126,57 @@ export class AccountListRenderer {
|
|
|
92
126
|
process.stdout.write(`${dim(plainText(harness.saveUnavailableReason ?? 'No known accounts. ' + harness.loginHint))}\n`);
|
|
93
127
|
continue;
|
|
94
128
|
}
|
|
95
|
-
|
|
96
|
-
|
|
129
|
+
const accounts = nameAccounts(harness.accounts);
|
|
130
|
+
await this.overview(accounts, now);
|
|
131
|
+
const schedules = accounts.flatMap(account => orderedUsageWindows(account.status.summary?.usageWindows).map(window => ({ accountId: account.id, account: account.name, window })));
|
|
97
132
|
for (const feature of [false, true]) {
|
|
98
133
|
const scopedSchedules = schedules.filter(row => (row.window.scope === 'feature') === feature);
|
|
99
134
|
if (!scopedSchedules.length) continue;
|
|
100
135
|
this.heading(feature ? 'Feature-specific limits' : 'Usage & reset schedule');
|
|
101
|
-
await this.
|
|
136
|
+
await consoleTable(this.out, scopedSchedules, [
|
|
102
137
|
{ key: 'account', title: 'Account', value: row => row.account },
|
|
103
138
|
{ key: 'window', title: 'Window', value: row => `${plainText(row.window.label)} (${duration(row.window.durationSeconds)})` },
|
|
104
139
|
{ key: 'usage', title: 'Usage', value: row => `${row.window.usedPercent}% used\n${Math.max(0, 100 - row.window.usedPercent)}% left` },
|
|
105
140
|
{ key: 'reset', title: 'Reset in', value: row => until(row.window.resetAt, now) },
|
|
106
|
-
]);
|
|
141
|
+
], accountDividers);
|
|
107
142
|
}
|
|
108
|
-
const resets =
|
|
143
|
+
const resets = accounts.flatMap(account => (account.status.summary?.resets?.details ?? []).map(reset => ({ accountId: account.id, account: account.name, reset })));
|
|
109
144
|
if (resets.length) {
|
|
110
145
|
this.heading('Earned reset details');
|
|
111
|
-
await this.
|
|
146
|
+
await consoleTable(this.out, resets, [
|
|
112
147
|
{ key: 'account', title: 'Account', value: row => row.account },
|
|
113
148
|
{ key: 'kind', title: 'Type', value: row => plainText(row.reset.kind) },
|
|
114
149
|
{ key: 'status', title: 'Status', value: row => plainText(row.reset.status) },
|
|
115
150
|
{ key: 'expiry', title: 'Expires (UTC)', value: row => row.reset.expiresAt === null ? 'Not reported' : plainText(row.reset.expiresAt).replace('T', '\n').replace('.000Z', '').replace(/Z$/, '') },
|
|
116
|
-
]);
|
|
151
|
+
], accountDividers);
|
|
117
152
|
}
|
|
118
153
|
const groups = new Map<string, IDisplayFact[]>();
|
|
119
154
|
const add = (section: string, fact: IDisplayFact) => { const group = groups.get(section) ?? []; group.push(fact); groups.set(section, group); };
|
|
120
|
-
|
|
155
|
+
accounts.forEach((account, index) => {
|
|
121
156
|
for (const fact of account.status.facts) {
|
|
122
157
|
if (!this.covered(fact, account.status)) add(plainText(fact.section ?? 'Additional information'), { account: index, label: plainText(fact.label), value: plainText(fact.value) });
|
|
123
158
|
}
|
|
124
159
|
});
|
|
125
|
-
for (const [section, facts] of groups) await this.facts(section, facts,
|
|
126
|
-
const saved =
|
|
160
|
+
for (const [section, facts] of groups) await this.facts(section, facts, accounts);
|
|
161
|
+
const saved = accounts.flatMap((account, index) => [
|
|
127
162
|
...(account.savedAt ? [{ account: index, label: 'Saved (UTC)', value: plainText(account.savedAt) }] : []),
|
|
128
163
|
...account.details.map((detail, detailIndex) => ({ account: index, label: `Note ${detailIndex + 1}`, value: plainText(detail) })),
|
|
129
164
|
]);
|
|
130
|
-
await this.facts('Saved logins', saved,
|
|
131
|
-
const issues
|
|
132
|
-
|
|
165
|
+
await this.facts('Saved logins', saved, accounts);
|
|
166
|
+
const issues: { accountId: string | null; account: string; issue: string }[] = accounts.flatMap(account =>
|
|
167
|
+
account.status.problems.map(problem => ({ accountId: account.id, account: account.name, issue: plainText(problem) })));
|
|
168
|
+
if (harness.saveUnavailableReason) issues.push({ accountId: null, account: 'Harness', issue: plainText(harness.saveUnavailableReason) });
|
|
133
169
|
if (issues.length) {
|
|
134
170
|
this.heading('Availability & actions');
|
|
135
|
-
await this.
|
|
171
|
+
await consoleTable(this.out, issues, [
|
|
136
172
|
{ key: 'account', title: 'Account', value: row => row.account },
|
|
137
173
|
{ key: 'issue', title: 'Unavailable / action needed', value: row => row.issue },
|
|
138
|
-
]);
|
|
174
|
+
], accountDividers);
|
|
139
175
|
}
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
private async overview(accountsArg:
|
|
179
|
+
private async overview(accountsArg: TNamedAccount[], nowArg: number): Promise<void> {
|
|
144
180
|
const rows = accountsArg;
|
|
145
181
|
const state = (account: TListedAccount) => `${account.isActive ? '* active\n' : ''}${account.isStashed ? 'saved' : 'not saved'}${account.status.problems.length ? '\n! partial status' : ''}`;
|
|
146
182
|
const plan = (account: TListedAccount) => account.status.summary?.subscription ? `${plainText(account.status.summary.subscription.plan)} (${account.status.summary.subscription.source})${account.status.summary.subscription.source === 'stored' ? '\nunverified' : ''}` : 'Unavailable';
|
|
@@ -159,7 +195,7 @@ export class AccountListRenderer {
|
|
|
159
195
|
const resets = (account: TListedAccount) => account.status.summary?.resets === undefined ? 'Unavailable' : String(account.status.summary.resets.available);
|
|
160
196
|
const usage = (account: TListedAccount) => accountQuotaSummary(account, nowArg);
|
|
161
197
|
const columns: plugins.smartconsole.IBackendTableColumn<typeof rows[number]>[] = [
|
|
162
|
-
{ key: 'account', title: 'Account', value: row =>
|
|
198
|
+
{ key: 'account', title: 'Account', value: row => row.name, style: row => ({ bold: row.isActive }) },
|
|
163
199
|
];
|
|
164
200
|
if (this.width >= 100) columns.push(
|
|
165
201
|
{ key: 'state', title: 'Login', value: state, style: row => ({ foreground: row.isStashed ? 'green' : 'orange' }) },
|
|
@@ -174,7 +210,7 @@ export class AccountListRenderer {
|
|
|
174
210
|
{ key: 'usage', title: 'Usage / resets', value: row => `${usage(row)}\nEarned resets: ${resets(row)}` },
|
|
175
211
|
);
|
|
176
212
|
else columns.push({ key: 'status', title: 'Status', value: row => `${state(row)}\nPlan: ${plan(row)}\nBilling: ${billing(row)}\nUsage: ${usage(row)}\nEarned resets: ${resets(row)}` });
|
|
177
|
-
await this.
|
|
213
|
+
await consoleTable(this.out, rows, columns);
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
private covered(factArg: IHarnessStatusFact, statusArg: IHarnessAccountStatus): boolean {
|
|
@@ -191,10 +227,10 @@ export class AccountListRenderer {
|
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
/** Compare like metrics across accounts, keeping duplicate labels as separate rows. */
|
|
194
|
-
private async facts(titleArg: string, factsArg: IDisplayFact[], accountsArg:
|
|
230
|
+
private async facts(titleArg: string, factsArg: IDisplayFact[], accountsArg: TNamedAccount[]): Promise<void> {
|
|
195
231
|
if (!factsArg.length) return;
|
|
196
232
|
this.heading(titleArg);
|
|
197
|
-
const valueBudget = Math.max(10, Math.min(40, Math.max(...factsArg.map(fact => Math.max(fact.value.length,
|
|
233
|
+
const valueBudget = Math.max(10, Math.min(40, Math.max(...factsArg.map(fact => Math.max(fact.value.length, accountsArg[fact.account].name.length))) + 3));
|
|
198
234
|
const perPage = Math.max(1, Math.min(6, Math.floor((this.width - 30) / valueBudget)));
|
|
199
235
|
const accountIndices = accountsArg.map((_, index) => index).filter(index => factsArg.some(fact => fact.account === index));
|
|
200
236
|
for (let start = 0; start < accountIndices.length; start += perPage) {
|
|
@@ -212,12 +248,12 @@ export class AccountListRenderer {
|
|
|
212
248
|
}
|
|
213
249
|
if (this.width < 40) {
|
|
214
250
|
for (const index of indices) {
|
|
215
|
-
process.stdout.write(` Account: ${
|
|
251
|
+
process.stdout.write(` Account: ${accountsArg[index].name}\n`);
|
|
216
252
|
for (const metric of metrics.values()) if (metric.values.has(index)) process.stdout.write(` - ${metric.label}: ${metric.values.get(index)}\n`);
|
|
217
253
|
}
|
|
218
|
-
} else await this.
|
|
254
|
+
} else await consoleTable(this.out, [...metrics.values()], [
|
|
219
255
|
{ key: 'metric', title: 'Metric', value: row => row.label },
|
|
220
|
-
...indices.map(index => ({ key: String(index), title:
|
|
256
|
+
...indices.map(index => ({ key: String(index), title: accountsArg[index].name, value: (row: { values: Map<number, string> }) => row.values.get(index) ?? 'Not reported' })),
|
|
221
257
|
]);
|
|
222
258
|
}
|
|
223
259
|
}
|