@modelprofile.com/authswitch 2.2.0 → 2.3.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 +3 -0
- package/dist_ts/accounts.js +37 -1
- package/dist_ts/classes.accountlist.d.ts +4 -0
- package/dist_ts/classes.accountlist.js +18 -12
- package/dist_ts/classes.cli.d.ts +5 -0
- package/dist_ts/classes.cli.js +31 -8
- package/dist_ts/classes.limits.d.ts +24 -0
- package/dist_ts/classes.limits.js +178 -0
- package/dist_ts/interfaces.list.d.ts +51 -0
- package/package.json +2 -2
- package/readme.md +80 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/accounts.ts +29 -0
- package/ts/classes.accountlist.ts +22 -10
- package/ts/classes.cli.ts +28 -7
- package/ts/classes.limits.ts +187 -0
- package/ts/interfaces.list.ts +55 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { compactSince, compactUntil, orderedUsageWindows } from './accounts.js';
|
|
3
|
+
import { consoleHeading, consoleTable } from './classes.accountlist.js';
|
|
4
|
+
import type { IAccountList, IAccountLimitRow, IAccountLimits, IActiveAccountRow, IActiveAccounts, IHarnessAccountList } from './interfaces.list.js';
|
|
5
|
+
import { dim, plainText } from './formatting.js';
|
|
6
|
+
|
|
7
|
+
type TListedAccount = IHarnessAccountList['accounts'][number];
|
|
8
|
+
|
|
9
|
+
const UNAVAILABLE = 'n/a';
|
|
10
|
+
const NONE = 'none';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Provider naming stays data-driven.
|
|
14
|
+
*
|
|
15
|
+
* A harness that owns exactly one credential slot is its own provider; one that owns several (an
|
|
16
|
+
* OpenCode installation with an OpenAI and an Anthropic login) has to say which slot a row is about.
|
|
17
|
+
*/
|
|
18
|
+
const providerName = (harnessArg: IHarnessAccountList, accountArg?: TListedAccount): string => {
|
|
19
|
+
const slots = new Set(harnessArg.accounts.map(account => account.slotId).filter((slot): slot is string => slot !== undefined));
|
|
20
|
+
return accountArg?.slotId !== undefined && slots.size > 1
|
|
21
|
+
? `${plainText(harnessArg.label)} / ${plainText(accountArg.slotId)}`
|
|
22
|
+
: plainText(harnessArg.label);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why an account contributes no numbers.
|
|
27
|
+
*
|
|
28
|
+
* The provider's own explanation is preferred over ours, and a failed lookup is never presented as a
|
|
29
|
+
* missing capability: the two have different fixes.
|
|
30
|
+
*/
|
|
31
|
+
const missingLimitReason = (accountArg: TListedAccount): string => {
|
|
32
|
+
if (accountArg.status.problems.length) return accountArg.status.problems.map(plainText).join(' ');
|
|
33
|
+
const availability = accountArg.status.facts.filter(fact => fact.section === 'Availability').map(fact => plainText(fact.value));
|
|
34
|
+
if (accountArg.status.summary?.usageWindows === undefined) {
|
|
35
|
+
return availability[0] ?? 'This login does not expose a usage or quota API.';
|
|
36
|
+
}
|
|
37
|
+
return 'The provider reported no usage window for this account.';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const accountKeys = (harnessArg: IHarnessAccountList, accountArg: TListedAccount) => ({
|
|
41
|
+
harnessId: harnessArg.id,
|
|
42
|
+
provider: providerName(harnessArg, accountArg),
|
|
43
|
+
account: plainText(accountArg.label),
|
|
44
|
+
accountId: accountArg.id,
|
|
45
|
+
...(accountArg.slotId === undefined ? {} : { slotId: plainText(accountArg.slotId) }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const harnessProblem = (harnessArg: IHarnessAccountList): string | null =>
|
|
49
|
+
harnessArg.problems.length ? harnessArg.problems.map(plainText).join(' ') : null;
|
|
50
|
+
|
|
51
|
+
/** Provider then account, keeping each account's windows in the order the adapter ranked them. */
|
|
52
|
+
const byProviderThenAccount = <TRow extends { provider: string; account: string | null }>(rowsArg: TRow[]): TRow[] =>
|
|
53
|
+
[...rowsArg].sort((left, right) => left.provider.localeCompare(right.provider)
|
|
54
|
+
|| (left.account ?? '').localeCompare(right.account ?? ''));
|
|
55
|
+
|
|
56
|
+
const noteFor = (rowArg: { provider: string; account: string | null; unavailableReason: string | null }): string | null =>
|
|
57
|
+
rowArg.unavailableReason === null ? null
|
|
58
|
+
: `${rowArg.provider}${rowArg.account === null ? '' : ` · ${rowArg.account}`}: ${rowArg.unavailableReason}`;
|
|
59
|
+
|
|
60
|
+
const collectNotes = (rowsArg: { provider: string; account: string | null; unavailableReason: string | null }[]): string[] =>
|
|
61
|
+
[...new Set(rowsArg.map(noteFor).filter((note): note is string => note !== null))];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One row per account and limit type, projected from the account list.
|
|
65
|
+
*
|
|
66
|
+
* Nothing here talks to a provider: the list is already the read-only, per-account-isolated lookup,
|
|
67
|
+
* so a single failing account costs its own rows a number and nothing else.
|
|
68
|
+
*/
|
|
69
|
+
export const accountLimits = (listArg: IAccountList): IAccountLimits => {
|
|
70
|
+
const now = Date.parse(listArg.generatedAt);
|
|
71
|
+
const rows: IAccountLimitRow[] = [];
|
|
72
|
+
for (const harness of listArg.harnesses) {
|
|
73
|
+
const problem = harnessProblem(harness);
|
|
74
|
+
const empty = {
|
|
75
|
+
limitType: null, scope: null, windowSeconds: null, usedPercent: null, resetAt: null, resetsIn: UNAVAILABLE,
|
|
76
|
+
} as const;
|
|
77
|
+
if (problem !== null) {
|
|
78
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, ...empty, unavailableReason: problem });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!harness.accounts.length) {
|
|
82
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, ...empty,
|
|
83
|
+
unavailableReason: harness.saveUnavailableReason === null ? 'No accounts are known for this harness.' : plainText(harness.saveUnavailableReason) });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const account of harness.accounts) {
|
|
87
|
+
const windows = orderedUsageWindows(account.status.summary?.usageWindows);
|
|
88
|
+
if (!windows.length) {
|
|
89
|
+
rows.push({ ...accountKeys(harness, account), isActive: account.isActive, ...empty, unavailableReason: missingLimitReason(account) });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const window of windows) {
|
|
93
|
+
rows.push({
|
|
94
|
+
...accountKeys(harness, account), isActive: account.isActive,
|
|
95
|
+
limitType: plainText(window.label), scope: window.scope ?? 'account', windowSeconds: window.durationSeconds,
|
|
96
|
+
usedPercent: window.usedPercent, resetAt: window.resetAt, resetsIn: compactUntil(window.resetAt, now, UNAVAILABLE),
|
|
97
|
+
unavailableReason: null,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const limits = byProviderThenAccount(rows);
|
|
103
|
+
return { schemaVersion: 1, generatedAt: listArg.generatedAt, complete: listArg.complete, limits, notes: collectNotes(limits) };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One row per active credential slot.
|
|
108
|
+
*
|
|
109
|
+
* A harness with no active login still gets a row: "nothing is active here" is an answer, and
|
|
110
|
+
* silently dropping the provider would read as "this provider does not exist".
|
|
111
|
+
*/
|
|
112
|
+
export const activeAccounts = (listArg: IAccountList): IActiveAccounts => {
|
|
113
|
+
const now = Date.parse(listArg.generatedAt);
|
|
114
|
+
const rows: IActiveAccountRow[] = [];
|
|
115
|
+
for (const harness of listArg.harnesses) {
|
|
116
|
+
const problem = harnessProblem(harness);
|
|
117
|
+
if (problem !== null) {
|
|
118
|
+
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, savedAt: null, savedAgo: UNAVAILABLE, source: 'none', unavailableReason: problem });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const active = harness.accounts.filter(account => account.isActive);
|
|
122
|
+
if (!active.length) {
|
|
123
|
+
const stashed = harness.accounts.filter(account => account.isStashed);
|
|
124
|
+
rows.push({
|
|
125
|
+
harnessId: harness.id, provider: providerName(harness), account: null, accountId: null,
|
|
126
|
+
savedAt: null, savedAgo: UNAVAILABLE, source: stashed.length ? 'stash only' : 'none',
|
|
127
|
+
unavailableReason: harness.saveUnavailableReason !== null ? plainText(harness.saveUnavailableReason)
|
|
128
|
+
: stashed.length ? `No login is active; ${stashed.length} saved account(s) can be activated with authswitch ${plainText(harness.id)} use.`
|
|
129
|
+
: `No login is active and none is saved. ${plainText(harness.loginHint)}`,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const account of active) {
|
|
134
|
+
rows.push({
|
|
135
|
+
...accountKeys(harness, account),
|
|
136
|
+
savedAt: account.savedAt, savedAgo: compactSince(account.savedAt, now, UNAVAILABLE),
|
|
137
|
+
source: 'credential file',
|
|
138
|
+
unavailableReason: account.isStashed ? null : 'This active login is not saved yet; authswitch cannot restore it after a switch.',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const active = byProviderThenAccount(rows);
|
|
143
|
+
return { schemaVersion: 1, generatedAt: listArg.generatedAt, complete: listArg.complete, active, notes: collectNotes(active) };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const percent = (rowArg: IAccountLimitRow): string => rowArg.usedPercent === null ? UNAVAILABLE : `${rowArg.usedPercent}%`;
|
|
147
|
+
|
|
148
|
+
/** Condensed presentation for `authswitch limits` and `authswitch active`. */
|
|
149
|
+
export class CondensedRenderer {
|
|
150
|
+
constructor(private readonly out: plugins.smartconsole.SmartConsole) {}
|
|
151
|
+
|
|
152
|
+
private notes(notesArg: string[], prefixArg: string): void {
|
|
153
|
+
if (!notesArg.length) return;
|
|
154
|
+
process.stdout.write('\n');
|
|
155
|
+
for (const note of notesArg) process.stdout.write(`${dim(`${prefixArg}${note}`)}\n`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async renderLimits(limitsArg: IAccountLimits): Promise<void> {
|
|
159
|
+
consoleHeading('Account limits');
|
|
160
|
+
if (!limitsArg.limits.length) { process.stdout.write(`${dim('No harnesses registered.')}\n`); return; }
|
|
161
|
+
await consoleTable(this.out, limitsArg.limits, [
|
|
162
|
+
{ key: 'provider', title: 'Provider', value: row => row.provider },
|
|
163
|
+
{ key: 'account', title: 'Account', value: row => row.account ?? UNAVAILABLE, style: row => ({ bold: row.isActive }) },
|
|
164
|
+
{ key: 'limit', title: 'Limit type', value: row => row.limitType ?? UNAVAILABLE },
|
|
165
|
+
{ key: 'used', title: 'Used %', value: percent, align: 'right',
|
|
166
|
+
style: row => ({ foreground: row.usedPercent === null ? 'orange' : row.usedPercent >= 100 ? 'red' : undefined }) },
|
|
167
|
+
{ key: 'reset', title: 'Resets in', value: row => row.resetsIn, align: 'right' },
|
|
168
|
+
]);
|
|
169
|
+
this.notes(limitsArg.notes, 'n/a — ');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async renderActive(activeArg: IActiveAccounts): Promise<void> {
|
|
173
|
+
consoleHeading('Active accounts');
|
|
174
|
+
if (!activeArg.active.length) { process.stdout.write(`${dim('No harnesses registered.')}\n`); return; }
|
|
175
|
+
await consoleTable(this.out, activeArg.active, [
|
|
176
|
+
{ key: 'provider', title: 'Provider', value: row => row.provider },
|
|
177
|
+
{ key: 'account', title: 'Account', value: row => row.account ?? NONE,
|
|
178
|
+
style: row => ({ bold: row.account !== null, foreground: row.account === null ? 'orange' : undefined }) },
|
|
179
|
+
// The absolute timestamp stays in --json; one screen is worth more here than one more column.
|
|
180
|
+
{ key: 'saved', title: 'Saved', value: row => row.savedAt === null ? (row.account === null ? UNAVAILABLE : 'not saved') : `${row.savedAgo} ago`, align: 'right',
|
|
181
|
+
style: row => ({ foreground: row.savedAt === null && row.account !== null ? 'orange' : undefined }) },
|
|
182
|
+
{ key: 'source', title: 'Source', value: row => row.source,
|
|
183
|
+
style: row => ({ foreground: row.source === 'credential file' ? 'green' : 'orange' }) },
|
|
184
|
+
]);
|
|
185
|
+
this.notes(activeArg.notes, 'note — ');
|
|
186
|
+
}
|
|
187
|
+
}
|
package/ts/interfaces.list.ts
CHANGED
|
@@ -17,3 +17,58 @@ export interface IHarnessAccountList {
|
|
|
17
17
|
/** A state lookup failure is distinct from an empty account list. */
|
|
18
18
|
problems: string[];
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** Credential-free, versioned output of limits --json. One entry per account and limit type. */
|
|
22
|
+
export interface IAccountLimits {
|
|
23
|
+
schemaVersion: 1;
|
|
24
|
+
generatedAt: string;
|
|
25
|
+
complete: boolean;
|
|
26
|
+
limits: IAccountLimitRow[];
|
|
27
|
+
/** Why a row has no numbers. Never a substitute for a value. */
|
|
28
|
+
notes: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IAccountLimitRow {
|
|
32
|
+
harnessId: string;
|
|
33
|
+
/** Harness label, qualified by the credential slot when the harness owns several. */
|
|
34
|
+
provider: string;
|
|
35
|
+
/** The account's public label, which is its email wherever the provider reports one. */
|
|
36
|
+
account: string | null;
|
|
37
|
+
accountId: string | null;
|
|
38
|
+
slotId?: string;
|
|
39
|
+
isActive: boolean;
|
|
40
|
+
/** The provider's own window name, or null when no limit data is available. */
|
|
41
|
+
limitType: string | null;
|
|
42
|
+
scope: 'account' | 'feature' | null;
|
|
43
|
+
windowSeconds: number | null;
|
|
44
|
+
/** Never inferred: null means the provider reported no percentage, not zero usage. */
|
|
45
|
+
usedPercent: number | null;
|
|
46
|
+
resetAt: string | null;
|
|
47
|
+
/** Human countdown for the same `resetAt`, relative to `generatedAt`. */
|
|
48
|
+
resetsIn: string;
|
|
49
|
+
unavailableReason: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Credential-free, versioned output of active --json. One entry per active credential slot. */
|
|
53
|
+
export interface IActiveAccounts {
|
|
54
|
+
schemaVersion: 1;
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
complete: boolean;
|
|
57
|
+
active: IActiveAccountRow[];
|
|
58
|
+
notes: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IActiveAccountRow {
|
|
62
|
+
harnessId: string;
|
|
63
|
+
provider: string;
|
|
64
|
+
account: string | null;
|
|
65
|
+
accountId: string | null;
|
|
66
|
+
slotId?: string;
|
|
67
|
+
/** When authswitch last saved this login; null when it was never stashed. */
|
|
68
|
+
savedAt: string | null;
|
|
69
|
+
/** Human elapsed time for the same `savedAt`, relative to `generatedAt`. */
|
|
70
|
+
savedAgo: string;
|
|
71
|
+
/** Where the shown account comes from. Whether it is also saved is the `savedAt` field's answer. */
|
|
72
|
+
source: 'credential file' | 'stash only' | 'none';
|
|
73
|
+
unavailableReason: string | null;
|
|
74
|
+
}
|