@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
package/src/cli.ts
CHANGED
|
@@ -1,2071 +1,304 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* CLI
|
|
3
|
+
* CLI entrypoint — thin dispatcher that routes to command handlers.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* oaa [group] [command] [args] (short alias)
|
|
5
|
+
* Command handlers live in:
|
|
6
|
+
* ./cli/commands/auth.ts — auth, account commands (login, logout, list, switch, etc.)
|
|
7
|
+
* ./cli/commands/config.ts — config, usage, manage commands (strategy, stats, manage, etc.)
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
* auth Authentication: login, logout, reauth, refresh
|
|
12
|
-
* account Account management: list, switch, enable, disable, remove, reset
|
|
13
|
-
* usage Usage statistics: stats, reset-stats, status
|
|
14
|
-
* config Configuration: show, strategy
|
|
15
|
-
* manage Interactive account management menu
|
|
16
|
-
*
|
|
17
|
-
* Auth Commands:
|
|
18
|
-
* login Add a new account via browser OAuth flow
|
|
19
|
-
* logout <N> Revoke tokens and remove account N
|
|
20
|
-
* logout --all Revoke all tokens and clear all accounts
|
|
21
|
-
* reauth <N> Re-authenticate account N with fresh OAuth tokens
|
|
22
|
-
* refresh <N> Attempt token refresh (no browser needed)
|
|
23
|
-
*
|
|
24
|
-
* Account Commands:
|
|
25
|
-
* list Show all accounts with status (default)
|
|
26
|
-
* switch <N> Set account N as active
|
|
27
|
-
* enable <N> Enable a disabled account
|
|
28
|
-
* disable <N> Disable an account (skipped in rotation)
|
|
29
|
-
* remove <N> Remove an account permanently
|
|
30
|
-
* reset <N|all> Clear rate-limit / failure tracking
|
|
31
|
-
*
|
|
32
|
-
* Usage Commands:
|
|
33
|
-
* stats Show per-account usage statistics
|
|
34
|
-
* reset-stats [N|all] Reset usage statistics
|
|
35
|
-
* status Compact one-liner for scripts/prompts
|
|
36
|
-
*
|
|
37
|
-
* Config Commands:
|
|
38
|
-
* config Show current configuration and file paths
|
|
39
|
-
* strategy [name] Show or change account selection strategy
|
|
40
|
-
*
|
|
41
|
-
* Manage Commands:
|
|
42
|
-
* manage Interactive account management menu
|
|
43
|
-
* help Show this help message
|
|
44
|
-
*
|
|
45
|
-
* Group Help:
|
|
46
|
-
* auth help Show auth commands
|
|
47
|
-
* account help Show account commands
|
|
48
|
-
* usage help Show usage commands
|
|
49
|
-
* config help Show config commands
|
|
9
|
+
* This file owns: argv parsing, flag extraction, IO context routing, direct entry.
|
|
50
10
|
*/
|
|
51
11
|
|
|
52
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
53
|
-
import { exec } from "node:child_process";
|
|
54
13
|
import { pathToFileURL } from "node:url";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
14
|
+
import { c, setUseColor } from "./cli/formatting.js";
|
|
15
|
+
import {
|
|
16
|
+
cmdAuthGroupHelp,
|
|
17
|
+
cmdDisable,
|
|
18
|
+
cmdEnable,
|
|
19
|
+
cmdList,
|
|
20
|
+
cmdLogin,
|
|
21
|
+
cmdLogout,
|
|
22
|
+
cmdReauth,
|
|
23
|
+
cmdRefresh,
|
|
24
|
+
cmdRemove,
|
|
25
|
+
cmdReset,
|
|
26
|
+
cmdStats,
|
|
27
|
+
cmdStatus,
|
|
28
|
+
cmdSwitch,
|
|
29
|
+
} from "./cli/commands/auth.js";
|
|
30
|
+
import {
|
|
31
|
+
cmdHelp,
|
|
32
|
+
cmdManage,
|
|
33
|
+
cmdResetStats,
|
|
34
|
+
cmdStrategy,
|
|
35
|
+
dispatchConfigCommands,
|
|
36
|
+
dispatchManageCommands,
|
|
37
|
+
dispatchUsageCommands,
|
|
38
|
+
} from "./cli/commands/config.js";
|
|
39
|
+
|
|
40
|
+
// Re-export for backward compatibility (tests and external consumers import from cli.ts)
|
|
41
|
+
export { formatDuration, formatTimeAgo, renderBar, formatResetTime, renderUsageLines } from "./cli/formatting.js";
|
|
42
|
+
export {
|
|
43
|
+
ensureTokenAndFetchUsage,
|
|
44
|
+
fetchUsage,
|
|
45
|
+
refreshAccessToken,
|
|
46
|
+
cmdDisable,
|
|
47
|
+
cmdEnable,
|
|
48
|
+
cmdList,
|
|
49
|
+
cmdLogin,
|
|
50
|
+
cmdLogout,
|
|
51
|
+
cmdReauth,
|
|
52
|
+
cmdRefresh,
|
|
53
|
+
cmdRemove,
|
|
54
|
+
cmdReset,
|
|
55
|
+
cmdStats,
|
|
56
|
+
cmdStatus,
|
|
57
|
+
cmdSwitch,
|
|
58
|
+
} from "./cli/commands/auth.js";
|
|
59
|
+
export { cmdConfig, cmdHelp, cmdManage, cmdResetStats, cmdStrategy } from "./cli/commands/config.js";
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
-
//
|
|
62
|
+
// IO context — routes console.log/error through AsyncLocalStorage for testing
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const ansi = (code: string, text: string) => (USE_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
69
|
-
|
|
70
|
-
const c = {
|
|
71
|
-
bold: (t: string) => ansi("1", t),
|
|
72
|
-
dim: (t: string) => ansi("2", t),
|
|
73
|
-
green: (t: string) => ansi("32", t),
|
|
74
|
-
yellow: (t: string) => ansi("33", t),
|
|
75
|
-
cyan: (t: string) => ansi("36", t),
|
|
76
|
-
red: (t: string) => ansi("31", t),
|
|
77
|
-
gray: (t: string) => ansi("90", t),
|
|
65
|
+
type IoStore = {
|
|
66
|
+
log?: (...args: unknown[]) => void;
|
|
67
|
+
error?: (...args: unknown[]) => void;
|
|
78
68
|
};
|
|
69
|
+
const ioContext = new AsyncLocalStorage<IoStore>();
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Format milliseconds as a human-readable duration.
|
|
86
|
-
* @param {number} ms
|
|
87
|
-
* @returns {string}
|
|
88
|
-
*/
|
|
89
|
-
export function formatDuration(ms: number) {
|
|
90
|
-
if (ms <= 0) return "now";
|
|
91
|
-
const seconds = Math.floor(ms / 1000);
|
|
92
|
-
if (seconds < 60) return `${seconds}s`;
|
|
93
|
-
const minutes = Math.floor(seconds / 60);
|
|
94
|
-
const remainSec = seconds % 60;
|
|
95
|
-
if (minutes < 60) return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
|
|
96
|
-
const hours = Math.floor(minutes / 60);
|
|
97
|
-
const remainMin = minutes % 60;
|
|
98
|
-
if (hours < 24) return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
|
|
99
|
-
const days = Math.floor(hours / 24);
|
|
100
|
-
const remainHours = hours % 24;
|
|
101
|
-
return remainHours > 0 ? `${days}d ${remainHours}h` : `${days}d`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Format a timestamp as relative time ago.
|
|
106
|
-
* @param {number} timestamp
|
|
107
|
-
* @returns {string}
|
|
108
|
-
*/
|
|
109
|
-
export function formatTimeAgo(timestamp: number | null | undefined) {
|
|
110
|
-
if (!timestamp || timestamp === 0) return "never";
|
|
111
|
-
const ms = Date.now() - timestamp;
|
|
112
|
-
if (ms < 0) return "just now";
|
|
113
|
-
return `${formatDuration(ms)} ago`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Shorten a path by replacing home directory with ~.
|
|
118
|
-
* @param {string} p
|
|
119
|
-
* @returns {string}
|
|
120
|
-
*/
|
|
121
|
-
function shortPath(p: string) {
|
|
122
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
123
|
-
if (home && p.startsWith(home)) return "~" + p.slice(home.length);
|
|
124
|
-
return p;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Strip ANSI escape codes from a string to get its visible content.
|
|
129
|
-
* @param {string} str
|
|
130
|
-
* @returns {string}
|
|
131
|
-
*/
|
|
132
|
-
function stripAnsi(str: string) {
|
|
133
|
-
// eslint-disable-next-line no-control-regex -- ANSI escape sequences start with \x1b which is a control char
|
|
134
|
-
return str.replace(new RegExp("\x1b\\[[0-9;]*m", "g"), "");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Left-pad a string to a fixed visible width, accounting for ANSI escape codes.
|
|
139
|
-
* @param {string} str
|
|
140
|
-
* @param {number} width
|
|
141
|
-
* @returns {string}
|
|
142
|
-
*/
|
|
143
|
-
function pad(str: string, width: number) {
|
|
144
|
-
const diff = width - stripAnsi(str).length;
|
|
145
|
-
return diff > 0 ? str + " ".repeat(diff) : str;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Right-align a string to a fixed visible width, accounting for ANSI escape codes.
|
|
150
|
-
* @param {string} str
|
|
151
|
-
* @param {number} width
|
|
152
|
-
* @returns {string}
|
|
153
|
-
*/
|
|
154
|
-
function rpad(str: string, width: number) {
|
|
155
|
-
const diff = width - stripAnsi(str).length;
|
|
156
|
-
return diff > 0 ? " ".repeat(diff) + str : str;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Usage quota helpers
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Refresh an account's OAuth access token.
|
|
165
|
-
* Mutates the account object in-place and returns the new access token.
|
|
166
|
-
* @param {{ refreshToken: string, access?: string, expires?: number }} account
|
|
167
|
-
* @returns {Promise<string | null>}
|
|
168
|
-
*/
|
|
169
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mutable account shape shared across CLI/plugin; full typing deferred
|
|
170
|
-
export async function refreshAccessToken(account: Record<string, any>) {
|
|
171
|
-
try {
|
|
172
|
-
const resp = await fetch("https://platform.claude.com/v1/oauth/token", {
|
|
173
|
-
method: "POST",
|
|
174
|
-
headers: { "Content-Type": "application/json" },
|
|
175
|
-
body: JSON.stringify({
|
|
176
|
-
grant_type: "refresh_token",
|
|
177
|
-
refresh_token: account.refreshToken,
|
|
178
|
-
client_id: CLIENT_ID,
|
|
179
|
-
}),
|
|
180
|
-
signal: AbortSignal.timeout(5000),
|
|
181
|
-
});
|
|
182
|
-
if (!resp.ok) return null;
|
|
183
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OAuth token response shape is external API contract
|
|
184
|
-
const json: any = await resp.json();
|
|
185
|
-
account.access = json.access_token;
|
|
186
|
-
account.expires = Date.now() + json.expires_in * 1000;
|
|
187
|
-
if (json.refresh_token) account.refreshToken = json.refresh_token;
|
|
188
|
-
account.token_updated_at = Date.now();
|
|
189
|
-
return json.access_token;
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Fetch usage quotas from the Anthropic OAuth usage endpoint.
|
|
197
|
-
* @param {string} accessToken
|
|
198
|
-
* @returns {Promise<Record<string, any> | null>}
|
|
199
|
-
*/
|
|
200
|
-
export async function fetchUsage(accessToken: string) {
|
|
201
|
-
try {
|
|
202
|
-
const resp = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
203
|
-
headers: {
|
|
204
|
-
authorization: `Bearer ${accessToken}`,
|
|
205
|
-
"anthropic-beta": "oauth-2025-04-20",
|
|
206
|
-
accept: "application/json",
|
|
207
|
-
},
|
|
208
|
-
signal: AbortSignal.timeout(5000),
|
|
209
|
-
});
|
|
210
|
-
if (!resp.ok) return null;
|
|
211
|
-
return resp.json();
|
|
212
|
-
} catch {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Ensure an account has a valid access token and fetch its usage data.
|
|
219
|
-
* @param {{ refreshToken: string, access?: string, expires?: number, enabled: boolean }} account
|
|
220
|
-
* @returns {Promise<{ usage: Record<string, any> | null, tokenRefreshed: boolean }>}
|
|
221
|
-
*/
|
|
222
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mutable account shape shared across CLI/plugin; full typing deferred
|
|
223
|
-
export async function ensureTokenAndFetchUsage(account: Record<string, any>) {
|
|
224
|
-
if (!account.enabled) return { usage: null, tokenRefreshed: false };
|
|
225
|
-
|
|
226
|
-
let token = account.access;
|
|
227
|
-
let tokenRefreshed = false;
|
|
228
|
-
|
|
229
|
-
if (!token || !account.expires || account.expires < Date.now()) {
|
|
230
|
-
token = await refreshAccessToken(account);
|
|
231
|
-
tokenRefreshed = !!token;
|
|
232
|
-
if (!token) return { usage: null, tokenRefreshed: false };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const usage = await fetchUsage(token);
|
|
236
|
-
return { usage, tokenRefreshed };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Render a progress bar of a given width for a utilization percentage (0–100).
|
|
241
|
-
* @param {number} utilization - percentage (0 to 100)
|
|
242
|
-
* @param {number} [width=10] - bar character width
|
|
243
|
-
* @returns {string}
|
|
244
|
-
*/
|
|
245
|
-
export function renderBar(utilization: number, width = 10) {
|
|
246
|
-
const pct = Math.max(0, Math.min(100, utilization));
|
|
247
|
-
const filled = Math.round((pct / 100) * width);
|
|
248
|
-
const empty = width - filled;
|
|
249
|
-
|
|
250
|
-
let bar: string;
|
|
251
|
-
if (pct >= 90) {
|
|
252
|
-
bar = c.red("█".repeat(filled)) + c.dim("░".repeat(empty));
|
|
253
|
-
} else if (pct >= 70) {
|
|
254
|
-
bar = c.yellow("█".repeat(filled)) + c.dim("░".repeat(empty));
|
|
255
|
-
} else {
|
|
256
|
-
bar = c.green("█".repeat(filled)) + c.dim("░".repeat(empty));
|
|
257
|
-
}
|
|
258
|
-
return bar;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Format an ISO 8601 reset timestamp as a relative duration from now.
|
|
263
|
-
* @param {string} isoString
|
|
264
|
-
* @returns {string}
|
|
265
|
-
*/
|
|
266
|
-
export function formatResetTime(isoString: string) {
|
|
267
|
-
const resetMs = new Date(isoString).getTime();
|
|
268
|
-
const remaining = resetMs - Date.now();
|
|
269
|
-
if (remaining <= 0) return "now";
|
|
270
|
-
return formatDuration(remaining);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Known usage quota buckets and their display labels.
|
|
275
|
-
* Order determines display order.
|
|
276
|
-
*/
|
|
277
|
-
const QUOTA_BUCKETS = [
|
|
278
|
-
{ key: "five_hour", label: "5h" },
|
|
279
|
-
{ key: "seven_day", label: "7d" },
|
|
280
|
-
{ key: "seven_day_sonnet", label: "Sonnet 7d" },
|
|
281
|
-
{ key: "seven_day_opus", label: "Opus 7d" },
|
|
282
|
-
{ key: "seven_day_oauth_apps", label: "OAuth Apps 7d" },
|
|
283
|
-
{ key: "seven_day_cowork", label: "Cowork 7d" },
|
|
284
|
-
];
|
|
285
|
-
|
|
286
|
-
const USAGE_INDENT = " ";
|
|
287
|
-
const USAGE_LABEL_WIDTH = 13;
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Render usage quota lines for an account.
|
|
291
|
-
* Returns an array of pre-formatted strings (one per non-null bucket).
|
|
292
|
-
* @param {Record<string, any>} usage
|
|
293
|
-
* @returns {string[]}
|
|
294
|
-
*/
|
|
295
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- upstream Anthropic usage API response has unstable bucket shapes
|
|
296
|
-
export function renderUsageLines(usage: Record<string, any>) {
|
|
297
|
-
const lines = [];
|
|
298
|
-
for (const { key, label } of QUOTA_BUCKETS) {
|
|
299
|
-
const bucket = usage[key];
|
|
300
|
-
if (!bucket || bucket.utilization == null) continue;
|
|
301
|
-
|
|
302
|
-
const pct = bucket.utilization;
|
|
303
|
-
const bar = renderBar(pct);
|
|
304
|
-
const pctStr = pad(String(Math.round(pct)) + "%", 4);
|
|
305
|
-
const reset = bucket.resets_at ? c.dim(`resets in ${formatResetTime(bucket.resets_at)}`) : "";
|
|
306
|
-
|
|
307
|
-
lines.push(`${USAGE_INDENT}${pad(label, USAGE_LABEL_WIDTH)} ${bar} ${pctStr}${reset ? ` ${reset}` : ""}`);
|
|
308
|
-
}
|
|
309
|
-
return lines;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ---------------------------------------------------------------------------
|
|
313
|
-
// Browser opener
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Open a URL in the user's default browser.
|
|
318
|
-
* Best-effort: uses platform-specific command, silently fails on error.
|
|
319
|
-
* @param {string} url
|
|
320
|
-
*/
|
|
321
|
-
function openBrowser(url: string) {
|
|
322
|
-
if (process.platform === "win32") {
|
|
323
|
-
exec(`cmd /c start "" ${JSON.stringify(url)}`);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
328
|
-
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Run the OAuth PKCE login flow from the CLI.
|
|
333
|
-
* Opens browser, prompts for code, exchanges for tokens.
|
|
334
|
-
* @returns {Promise<{refresh: string, access: string, expires: number, email?: string} | null>}
|
|
335
|
-
*/
|
|
336
|
-
async function runOAuthFlow() {
|
|
337
|
-
const { url, verifier, state } = await authorize("max");
|
|
338
|
-
|
|
339
|
-
log.info("Opening browser for Anthropic OAuth login...");
|
|
340
|
-
log.info("If your browser didn't open, visit this URL:");
|
|
341
|
-
log.info(url);
|
|
342
|
-
|
|
343
|
-
openBrowser(url);
|
|
344
|
-
|
|
345
|
-
const code = await text({
|
|
346
|
-
message: "Paste the authorization code here:",
|
|
347
|
-
placeholder: "auth-code#state",
|
|
348
|
-
});
|
|
349
|
-
if (isCancel(code)) {
|
|
350
|
-
log.warn("Login cancelled.");
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
const trimmed = (code as string).trim();
|
|
354
|
-
if (!trimmed) {
|
|
355
|
-
log.error("Error: no authorization code provided.");
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Validate OAuth state to prevent CSRF
|
|
360
|
-
const parts = trimmed.split("#");
|
|
361
|
-
if (state && parts[1] && parts[1] !== state) {
|
|
362
|
-
log.error("Error: OAuth state mismatch — possible CSRF attack.");
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
71
|
+
const nativeConsoleLog = console.log.bind(console);
|
|
72
|
+
const nativeConsoleError = console.error.bind(console);
|
|
73
|
+
let consoleRouterUsers = 0;
|
|
365
74
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
75
|
+
function installConsoleRouter() {
|
|
76
|
+
if (consoleRouterUsers === 0) {
|
|
77
|
+
console.log = (...args) => {
|
|
78
|
+
const io = ioContext.getStore();
|
|
79
|
+
if (io?.log) return io.log(...args);
|
|
80
|
+
return nativeConsoleLog(...args);
|
|
81
|
+
};
|
|
82
|
+
console.error = (...args) => {
|
|
83
|
+
const io = ioContext.getStore();
|
|
84
|
+
if (io?.error) return io.error(...args);
|
|
85
|
+
return nativeConsoleError(...args);
|
|
86
|
+
};
|
|
374
87
|
}
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
s.stop("Token exchange successful.");
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
refresh: credentials.refresh,
|
|
382
|
-
access: credentials.access,
|
|
383
|
-
expires: credentials.expires,
|
|
384
|
-
email: credentials.email,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
// Auth commands (login, logout, reauth, refresh)
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Login: add a new account via browser OAuth flow.
|
|
394
|
-
* @returns {Promise<number>} exit code
|
|
395
|
-
*/
|
|
396
|
-
export async function cmdLogin() {
|
|
397
|
-
if (!process.stdin.isTTY) {
|
|
398
|
-
log.error("Error: 'login' requires an interactive terminal.");
|
|
399
|
-
return 1;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
intro("Login — Add a new account");
|
|
403
|
-
|
|
404
|
-
const stored = await loadAccounts();
|
|
405
|
-
|
|
406
|
-
const credentials = await runOAuthFlow();
|
|
407
|
-
if (!credentials) return 1;
|
|
408
|
-
|
|
409
|
-
// Load or create storage
|
|
410
|
-
const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
|
|
411
|
-
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
412
|
-
|
|
413
|
-
const existing =
|
|
414
|
-
findByIdentity(storage.accounts, identity) ||
|
|
415
|
-
storage.accounts.find((acc) => acc.refreshToken === credentials.refresh);
|
|
416
|
-
if (existing) {
|
|
417
|
-
const existingIdx = storage.accounts.indexOf(existing);
|
|
418
|
-
|
|
419
|
-
// Update existing account
|
|
420
|
-
existing.refreshToken = credentials.refresh;
|
|
421
|
-
existing.access = credentials.access;
|
|
422
|
-
existing.expires = credentials.expires;
|
|
423
|
-
existing.token_updated_at = Date.now();
|
|
424
|
-
if (credentials.email) existing.email = credentials.email;
|
|
425
|
-
existing.identity = identity;
|
|
426
|
-
existing.source = existing.source ?? "oauth";
|
|
427
|
-
existing.enabled = true;
|
|
428
|
-
await saveAccounts(storage);
|
|
429
|
-
|
|
430
|
-
const label = credentials.email || existing.email || `Account ${existingIdx + 1}`;
|
|
431
|
-
log.success(`Updated existing account #${existingIdx + 1} (${label}).`);
|
|
432
|
-
return 0;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (storage.accounts.length >= 10) {
|
|
436
|
-
log.error("Error: maximum of 10 accounts reached. Remove one first.");
|
|
437
|
-
return 1;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Add new account
|
|
441
|
-
const now = Date.now();
|
|
442
|
-
storage.accounts.push({
|
|
443
|
-
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
444
|
-
email: credentials.email,
|
|
445
|
-
identity,
|
|
446
|
-
refreshToken: credentials.refresh,
|
|
447
|
-
access: credentials.access,
|
|
448
|
-
expires: credentials.expires,
|
|
449
|
-
token_updated_at: now,
|
|
450
|
-
addedAt: now,
|
|
451
|
-
lastUsed: 0,
|
|
452
|
-
enabled: true,
|
|
453
|
-
rateLimitResetTimes: {},
|
|
454
|
-
consecutiveFailures: 0,
|
|
455
|
-
lastFailureTime: null,
|
|
456
|
-
stats: createDefaultStats(now),
|
|
457
|
-
source: "oauth",
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// If this is the first account, it's already active at index 0
|
|
461
|
-
await saveAccounts(storage);
|
|
462
|
-
|
|
463
|
-
const label = credentials.email || `Account ${storage.accounts.length}`;
|
|
464
|
-
log.success(`Added account #${storage.accounts.length} (${label}).`);
|
|
465
|
-
log.info(`${storage.accounts.length} account(s) total.`);
|
|
466
|
-
return 0;
|
|
88
|
+
consoleRouterUsers++;
|
|
467
89
|
}
|
|
468
90
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
* @param {boolean} [opts.all] Logout all accounts
|
|
475
|
-
* @returns {Promise<number>} exit code
|
|
476
|
-
*/
|
|
477
|
-
export async function cmdLogout(arg?: string, opts: { force?: boolean; all?: boolean } = {}) {
|
|
478
|
-
if (opts.all) {
|
|
479
|
-
return cmdLogoutAll(opts);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const n = parseInt(arg || "", 10);
|
|
483
|
-
if (isNaN(n) || n < 1) {
|
|
484
|
-
log.error("Error: provide a valid account number (e.g., 'logout 2') or --all.");
|
|
485
|
-
return 1;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const stored = await loadAccounts();
|
|
489
|
-
if (!stored || stored.accounts.length === 0) {
|
|
490
|
-
log.error("Error: no accounts configured.");
|
|
491
|
-
return 1;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const idx = n - 1;
|
|
495
|
-
if (idx >= stored.accounts.length) {
|
|
496
|
-
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
497
|
-
return 1;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
501
|
-
|
|
502
|
-
// Confirm unless --force
|
|
503
|
-
if (!opts.force) {
|
|
504
|
-
if (!process.stdin.isTTY) {
|
|
505
|
-
log.error("Error: use --force to logout in non-interactive mode.");
|
|
506
|
-
return 1;
|
|
507
|
-
}
|
|
508
|
-
const shouldLogout = await confirm({
|
|
509
|
-
message: `Logout account #${n} (${label})? This will revoke tokens and remove the account.`,
|
|
510
|
-
});
|
|
511
|
-
if (isCancel(shouldLogout) || !shouldLogout) {
|
|
512
|
-
log.info("Cancelled.");
|
|
513
|
-
return 0;
|
|
91
|
+
function uninstallConsoleRouter() {
|
|
92
|
+
consoleRouterUsers = Math.max(0, consoleRouterUsers - 1);
|
|
93
|
+
if (consoleRouterUsers === 0) {
|
|
94
|
+
console.log = nativeConsoleLog;
|
|
95
|
+
console.error = nativeConsoleError;
|
|
514
96
|
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Attempt token revocation (best-effort)
|
|
518
|
-
const revoked = await revoke(stored.accounts[idx].refreshToken);
|
|
519
|
-
if (revoked) {
|
|
520
|
-
log.info("Token revoked server-side.");
|
|
521
|
-
} else {
|
|
522
|
-
log.info("Token revocation skipped (server may not support it).");
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Remove the account
|
|
526
|
-
stored.accounts.splice(idx, 1);
|
|
527
|
-
|
|
528
|
-
// Adjust active index
|
|
529
|
-
if (stored.accounts.length === 0) {
|
|
530
|
-
stored.activeIndex = 0;
|
|
531
|
-
} else if (stored.activeIndex >= stored.accounts.length) {
|
|
532
|
-
stored.activeIndex = stored.accounts.length - 1;
|
|
533
|
-
} else if (stored.activeIndex > idx) {
|
|
534
|
-
stored.activeIndex--;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
await saveAccounts(stored);
|
|
538
|
-
log.success(`Logged out account #${n} (${label}).`);
|
|
539
|
-
|
|
540
|
-
if (stored.accounts.length > 0) {
|
|
541
|
-
log.info(`${stored.accounts.length} account(s) remaining.`);
|
|
542
|
-
} else {
|
|
543
|
-
log.info("No accounts remaining. Run 'login' to add one.");
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return 0;
|
|
547
97
|
}
|
|
548
98
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
async function cmdLogoutAll(opts: { force?: boolean } = {}) {
|
|
556
|
-
const stored = await loadAccounts();
|
|
557
|
-
if (!stored || stored.accounts.length === 0) {
|
|
558
|
-
log.info("No accounts to logout.");
|
|
559
|
-
return 0;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const count = stored.accounts.length;
|
|
563
|
-
|
|
564
|
-
// Confirm unless --force
|
|
565
|
-
if (!opts.force) {
|
|
566
|
-
if (!process.stdin.isTTY) {
|
|
567
|
-
log.error("Error: use --force to logout all in non-interactive mode.");
|
|
568
|
-
return 1;
|
|
569
|
-
}
|
|
570
|
-
const shouldLogoutAll = await confirm({
|
|
571
|
-
message: `Logout all ${count} account(s)? This will revoke tokens and remove all accounts.`,
|
|
572
|
-
});
|
|
573
|
-
if (isCancel(shouldLogoutAll) || !shouldLogoutAll) {
|
|
574
|
-
log.info("Cancelled.");
|
|
575
|
-
return 0;
|
|
99
|
+
async function runWithIoContext(io: IoStore, fn: () => Promise<number>) {
|
|
100
|
+
installConsoleRouter();
|
|
101
|
+
try {
|
|
102
|
+
return await ioContext.run(io, fn);
|
|
103
|
+
} finally {
|
|
104
|
+
uninstallConsoleRouter();
|
|
576
105
|
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Attempt token revocation for each account (best-effort, in parallel)
|
|
580
|
-
const results = await Promise.allSettled(stored.accounts.map((acc) => revoke(acc.refreshToken)));
|
|
581
|
-
const revokedCount = results.filter((r) => r.status === "fulfilled" && r.value === true).length;
|
|
582
|
-
|
|
583
|
-
if (revokedCount > 0) {
|
|
584
|
-
log.info(`Revoked ${revokedCount} of ${count} token(s) server-side.`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Write explicit empty state so running plugin instances reconcile immediately.
|
|
588
|
-
await saveAccounts({ version: 1, accounts: [], activeIndex: 0 });
|
|
589
|
-
log.success(`Logged out all ${count} account(s).`);
|
|
590
|
-
|
|
591
|
-
return 0;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Reauth: re-authenticate an existing account with fresh OAuth tokens.
|
|
596
|
-
* @param {string} arg - Account number
|
|
597
|
-
* @returns {Promise<number>} exit code
|
|
598
|
-
*/
|
|
599
|
-
export async function cmdReauth(arg: string) {
|
|
600
|
-
const n = parseInt(arg, 10);
|
|
601
|
-
if (isNaN(n) || n < 1) {
|
|
602
|
-
log.error("Error: provide a valid account number (e.g., 'reauth 1')");
|
|
603
|
-
return 1;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (!process.stdin.isTTY) {
|
|
607
|
-
log.error("Error: 'reauth' requires an interactive terminal.");
|
|
608
|
-
return 1;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const stored = await loadAccounts();
|
|
612
|
-
if (!stored || stored.accounts.length === 0) {
|
|
613
|
-
log.error("Error: no accounts configured.");
|
|
614
|
-
return 1;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const idx = n - 1;
|
|
618
|
-
if (idx >= stored.accounts.length) {
|
|
619
|
-
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
620
|
-
return 1;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const existing = stored.accounts[idx];
|
|
624
|
-
const wasDisabled = !existing.enabled;
|
|
625
|
-
const oldLabel = existing.email || `Account ${n}`;
|
|
626
|
-
log.info(`Re-authenticating account #${n} (${oldLabel})...`);
|
|
627
|
-
|
|
628
|
-
const credentials = await runOAuthFlow();
|
|
629
|
-
if (!credentials) return 1;
|
|
630
|
-
|
|
631
|
-
// Update the account at the target index with fresh tokens
|
|
632
|
-
existing.refreshToken = credentials.refresh;
|
|
633
|
-
existing.access = credentials.access;
|
|
634
|
-
existing.expires = credentials.expires;
|
|
635
|
-
if (credentials.email) existing.email = credentials.email;
|
|
636
|
-
|
|
637
|
-
// Re-enable and reset failure tracking
|
|
638
|
-
existing.enabled = true;
|
|
639
|
-
existing.consecutiveFailures = 0;
|
|
640
|
-
existing.lastFailureTime = null;
|
|
641
|
-
existing.rateLimitResetTimes = {};
|
|
642
|
-
|
|
643
|
-
await saveAccounts(stored);
|
|
644
|
-
|
|
645
|
-
const newLabel = credentials.email || `Account ${n}`;
|
|
646
|
-
log.success(`Re-authenticated account #${n} (${newLabel}).`);
|
|
647
|
-
if (wasDisabled) {
|
|
648
|
-
log.info("Account has been re-enabled.");
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return 0;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Refresh: attempt a token refresh for an account without browser interaction.
|
|
656
|
-
* @param {string} arg - Account number
|
|
657
|
-
* @returns {Promise<number>} exit code
|
|
658
|
-
*/
|
|
659
|
-
export async function cmdRefresh(arg: string) {
|
|
660
|
-
const n = parseInt(arg, 10);
|
|
661
|
-
if (isNaN(n) || n < 1) {
|
|
662
|
-
log.error("Error: provide a valid account number (e.g., 'refresh 1')");
|
|
663
|
-
return 1;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const stored = await loadAccounts();
|
|
667
|
-
if (!stored || stored.accounts.length === 0) {
|
|
668
|
-
log.error("Error: no accounts configured.");
|
|
669
|
-
return 1;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const idx = n - 1;
|
|
673
|
-
if (idx >= stored.accounts.length) {
|
|
674
|
-
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
675
|
-
return 1;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const account = stored.accounts[idx];
|
|
679
|
-
const label = account.email || `Account ${n}`;
|
|
680
|
-
|
|
681
|
-
const s = spinner();
|
|
682
|
-
s.start(`Refreshing token for account #${n} (${label})...`);
|
|
683
|
-
|
|
684
|
-
const token = await refreshAccessToken(account);
|
|
685
|
-
if (!token) {
|
|
686
|
-
s.stop(`Token refresh failed for account #${n}.`);
|
|
687
|
-
log.error("The refresh token may be invalid or expired.");
|
|
688
|
-
log.error(`Try: opencode-anthropic-auth reauth ${n}`);
|
|
689
|
-
return 1;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Re-enable if disabled and reset failure tracking
|
|
693
|
-
const wasDisabled = !account.enabled;
|
|
694
|
-
account.enabled = true;
|
|
695
|
-
account.consecutiveFailures = 0;
|
|
696
|
-
account.lastFailureTime = null;
|
|
697
|
-
account.rateLimitResetTimes = {};
|
|
698
|
-
|
|
699
|
-
await saveAccounts(stored);
|
|
700
|
-
|
|
701
|
-
const expiresIn = account.expires ? formatDuration(account.expires - Date.now()) : "unknown";
|
|
702
|
-
s.stop("Token refreshed.");
|
|
703
|
-
log.success(`Token refreshed for account #${n} (${label}).`);
|
|
704
|
-
log.info(`New token expires in ${expiresIn}.`);
|
|
705
|
-
if (wasDisabled) {
|
|
706
|
-
log.info("Account has been re-enabled.");
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
return 0;
|
|
710
106
|
}
|
|
711
107
|
|
|
712
108
|
// ---------------------------------------------------------------------------
|
|
713
|
-
//
|
|
109
|
+
// Group dispatchers — delegate to sub-module dispatchers where they exist,
|
|
110
|
+
// inline the thin auth/account routing that needs flag threading.
|
|
714
111
|
// ---------------------------------------------------------------------------
|
|
715
112
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (result.status === "fulfilled" && result.value.tokenRefreshed) {
|
|
742
|
-
anyRefreshed = true;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
if (anyRefreshed) {
|
|
746
|
-
// Best-effort persistence — if saveAccounts fails, the CLI continues to render the
|
|
747
|
-
// status view with the in-memory refreshed tokens. The next command run will retry persisting.
|
|
748
|
-
await saveAccounts(stored).catch((err) => {
|
|
749
|
-
console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", err);
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
log.message(c.bold("Anthropic Multi-Account Status"));
|
|
754
|
-
|
|
755
|
-
// Header
|
|
756
|
-
log.message(
|
|
757
|
-
" " +
|
|
758
|
-
pad(c.dim("#"), 5) +
|
|
759
|
-
pad(c.dim("Account"), 22) +
|
|
760
|
-
pad(c.dim("Status"), 14) +
|
|
761
|
-
pad(c.dim("Failures"), 11) +
|
|
762
|
-
c.dim("Rate Limit"),
|
|
763
|
-
);
|
|
764
|
-
log.message(c.dim(" " + "─".repeat(62)));
|
|
765
|
-
|
|
766
|
-
for (let i = 0; i < stored.accounts.length; i++) {
|
|
767
|
-
const acc = stored.accounts[i];
|
|
768
|
-
const isActive = i === stored.activeIndex;
|
|
769
|
-
const num = String(i + 1);
|
|
770
|
-
|
|
771
|
-
// Label
|
|
772
|
-
const label = acc.email || `Account ${i + 1}`;
|
|
773
|
-
|
|
774
|
-
// Status
|
|
775
|
-
let status: string;
|
|
776
|
-
if (!acc.enabled) {
|
|
777
|
-
status = c.gray("○ disabled");
|
|
778
|
-
} else if (isActive) {
|
|
779
|
-
status = c.green("● active");
|
|
780
|
-
} else {
|
|
781
|
-
status = c.cyan("● ready");
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Failures
|
|
785
|
-
let failures: string;
|
|
786
|
-
if (!acc.enabled) {
|
|
787
|
-
failures = c.dim("—");
|
|
788
|
-
} else if (acc.consecutiveFailures > 0) {
|
|
789
|
-
failures = c.yellow(String(acc.consecutiveFailures));
|
|
790
|
-
} else {
|
|
791
|
-
failures = c.dim("0");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Rate limit
|
|
795
|
-
let rateLimit: string;
|
|
796
|
-
if (!acc.enabled) {
|
|
797
|
-
rateLimit = c.dim("—");
|
|
798
|
-
} else {
|
|
799
|
-
const resetTimes = acc.rateLimitResetTimes || {};
|
|
800
|
-
const maxReset = Math.max(0, ...Object.values(resetTimes));
|
|
801
|
-
if (maxReset > now) {
|
|
802
|
-
rateLimit = c.yellow(`\u26A0 ${formatDuration(maxReset - now)}`);
|
|
803
|
-
} else {
|
|
804
|
-
rateLimit = c.dim("—");
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Render account header line
|
|
809
|
-
log.message(" " + pad(c.bold(num), 5) + pad(label, 22) + pad(status, 14) + pad(failures, 11) + rateLimit);
|
|
810
|
-
|
|
811
|
-
// Render usage quota lines for enabled accounts
|
|
812
|
-
if (acc.enabled) {
|
|
813
|
-
const result = usageResults[i];
|
|
814
|
-
const usage = result.status === "fulfilled" ? result.value.usage : null;
|
|
815
|
-
if (usage) {
|
|
816
|
-
const lines = renderUsageLines(usage);
|
|
817
|
-
for (const line of lines) {
|
|
818
|
-
log.message(line);
|
|
819
|
-
}
|
|
820
|
-
} else {
|
|
821
|
-
log.message(c.dim(`${USAGE_INDENT}quotas: unavailable`));
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (i < stored.accounts.length - 1) {
|
|
826
|
-
log.message("");
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
log.message("");
|
|
831
|
-
|
|
832
|
-
const enabled = stored.accounts.filter((a) => a.enabled).length;
|
|
833
|
-
const disabled = stored.accounts.length - enabled;
|
|
834
|
-
|
|
835
|
-
const parts = [
|
|
836
|
-
`Strategy: ${c.cyan(config.account_selection_strategy)}`,
|
|
837
|
-
`${c.bold(String(enabled))} of ${stored.accounts.length} enabled`,
|
|
838
|
-
];
|
|
839
|
-
if (disabled > 0) {
|
|
840
|
-
parts.push(`${c.yellow(String(disabled))} disabled`);
|
|
841
|
-
}
|
|
842
|
-
log.info(parts.join(c.dim(" | ")));
|
|
843
|
-
log.info(`Storage: ${shortPath(getStoragePath())}`);
|
|
844
|
-
|
|
845
|
-
return 0;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Show compact one-liner status.
|
|
850
|
-
* @returns {Promise<number>} exit code
|
|
851
|
-
*/
|
|
852
|
-
export async function cmdStatus() {
|
|
853
|
-
const stored = await loadAccounts();
|
|
854
|
-
if (!stored || stored.accounts.length === 0) {
|
|
855
|
-
console.log("anthropic: no accounts configured");
|
|
856
|
-
return 1;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const config = loadConfig();
|
|
860
|
-
const total = stored.accounts.length;
|
|
861
|
-
const enabled = stored.accounts.filter((a) => a.enabled).length;
|
|
862
|
-
const now = Date.now();
|
|
863
|
-
|
|
864
|
-
// Count rate-limited accounts
|
|
865
|
-
let rateLimited = 0;
|
|
866
|
-
for (const acc of stored.accounts) {
|
|
867
|
-
if (!acc.enabled) continue;
|
|
868
|
-
const resetTimes = acc.rateLimitResetTimes || {};
|
|
869
|
-
const maxReset = Math.max(0, ...Object.values(resetTimes));
|
|
870
|
-
if (maxReset > now) rateLimited++;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
let line = `anthropic: ${total} account${total !== 1 ? "s" : ""} (${enabled} active)`;
|
|
874
|
-
line += `, strategy: ${config.account_selection_strategy}`;
|
|
875
|
-
line += `, next: #${stored.activeIndex + 1}`;
|
|
876
|
-
if (rateLimited > 0) {
|
|
877
|
-
line += `, ${rateLimited} rate-limited`;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
console.log(line);
|
|
881
|
-
return 0;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Switch active account.
|
|
886
|
-
* @param {string} arg
|
|
887
|
-
* @returns {Promise<number>} exit code
|
|
888
|
-
*/
|
|
889
|
-
export async function cmdSwitch(arg?: string) {
|
|
890
|
-
const n = parseInt(arg || "", 10);
|
|
891
|
-
if (isNaN(n) || n < 1) {
|
|
892
|
-
log.error("Error: provide a valid account number (e.g., 'switch 2')");
|
|
893
|
-
return 1;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const stored = await loadAccounts();
|
|
897
|
-
if (!stored || stored.accounts.length === 0) {
|
|
898
|
-
log.error("Error: no accounts configured.");
|
|
899
|
-
return 1;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const idx = n - 1;
|
|
903
|
-
if (idx >= stored.accounts.length) {
|
|
904
|
-
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
905
|
-
return 1;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (!stored.accounts[idx].enabled) {
|
|
909
|
-
log.error(`Warning: account ${n} is disabled. Enable it first with 'enable ${n}'.`);
|
|
910
|
-
return 1;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
stored.activeIndex = idx;
|
|
914
|
-
await saveAccounts(stored);
|
|
915
|
-
|
|
916
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
917
|
-
log.success(`Switched active account to #${n} (${label}).`);
|
|
918
|
-
return 0;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Enable a disabled account.
|
|
923
|
-
* @param {string} arg
|
|
924
|
-
* @returns {Promise<number>} exit code
|
|
925
|
-
*/
|
|
926
|
-
export async function cmdEnable(arg?: string) {
|
|
927
|
-
const n = parseInt(arg || "", 10);
|
|
928
|
-
if (isNaN(n) || n < 1) {
|
|
929
|
-
log.error("Error: provide a valid account number (e.g., 'enable 3')");
|
|
930
|
-
return 1;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const stored = await loadAccounts();
|
|
934
|
-
if (!stored || stored.accounts.length === 0) {
|
|
935
|
-
log.error("Error: no accounts configured.");
|
|
936
|
-
return 1;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
const idx = n - 1;
|
|
940
|
-
if (idx >= stored.accounts.length) {
|
|
941
|
-
log.error(`Error: account ${n} does not exist.`);
|
|
942
|
-
return 1;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (stored.accounts[idx].enabled) {
|
|
946
|
-
log.info(`Account ${n} is already enabled.`);
|
|
947
|
-
return 0;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
stored.accounts[idx].enabled = true;
|
|
951
|
-
await saveAccounts(stored);
|
|
952
|
-
|
|
953
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
954
|
-
log.success(`Enabled account #${n} (${label}).`);
|
|
955
|
-
return 0;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Disable an account.
|
|
960
|
-
* @param {string} arg
|
|
961
|
-
* @returns {Promise<number>} exit code
|
|
962
|
-
*/
|
|
963
|
-
export async function cmdDisable(arg?: string) {
|
|
964
|
-
const n = parseInt(arg || "", 10);
|
|
965
|
-
if (isNaN(n) || n < 1) {
|
|
966
|
-
log.error("Error: provide a valid account number (e.g., 'disable 3')");
|
|
967
|
-
return 1;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const stored = await loadAccounts();
|
|
971
|
-
if (!stored || stored.accounts.length === 0) {
|
|
972
|
-
log.error("Error: no accounts configured.");
|
|
973
|
-
return 1;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const idx = n - 1;
|
|
977
|
-
if (idx >= stored.accounts.length) {
|
|
978
|
-
log.error(`Error: account ${n} does not exist.`);
|
|
979
|
-
return 1;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (!stored.accounts[idx].enabled) {
|
|
983
|
-
log.info(`Account ${n} is already disabled.`);
|
|
984
|
-
return 0;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// Don't allow disabling the last enabled account
|
|
988
|
-
const enabledCount = stored.accounts.filter((a) => a.enabled).length;
|
|
989
|
-
if (enabledCount <= 1) {
|
|
990
|
-
log.error("Error: cannot disable the last enabled account.");
|
|
991
|
-
return 1;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
stored.accounts[idx].enabled = false;
|
|
995
|
-
|
|
996
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
997
|
-
let switchedTo = null;
|
|
998
|
-
|
|
999
|
-
// If we disabled the active account, switch to the next enabled one
|
|
1000
|
-
// (adjust before saving to avoid a TOCTOU race with the running plugin)
|
|
1001
|
-
if (idx === stored.activeIndex) {
|
|
1002
|
-
const nextEnabled = stored.accounts.findIndex((a) => a.enabled);
|
|
1003
|
-
if (nextEnabled >= 0) {
|
|
1004
|
-
stored.activeIndex = nextEnabled;
|
|
1005
|
-
switchedTo = nextEnabled;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
await saveAccounts(stored);
|
|
1010
|
-
|
|
1011
|
-
log.warn(`Disabled account #${n} (${label}).`);
|
|
1012
|
-
if (switchedTo !== null) {
|
|
1013
|
-
const nextLabel = stored.accounts[switchedTo].email || `Account ${switchedTo + 1}`;
|
|
1014
|
-
log.info(`Active account switched to #${switchedTo + 1} (${nextLabel}).`);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
return 0;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Remove an account permanently.
|
|
1022
|
-
* @param {string} arg
|
|
1023
|
-
* @param {object} [opts]
|
|
1024
|
-
* @param {boolean} [opts.force] Skip confirmation prompt
|
|
1025
|
-
* @returns {Promise<number>} exit code
|
|
1026
|
-
*/
|
|
1027
|
-
export async function cmdRemove(arg?: string, opts: { force?: boolean } = {}) {
|
|
1028
|
-
const n = parseInt(arg || "", 10);
|
|
1029
|
-
if (isNaN(n) || n < 1) {
|
|
1030
|
-
log.error("Error: provide a valid account number (e.g., 'remove 2')");
|
|
1031
|
-
return 1;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
const stored = await loadAccounts();
|
|
1035
|
-
if (!stored || stored.accounts.length === 0) {
|
|
1036
|
-
log.error("Error: no accounts configured.");
|
|
1037
|
-
return 1;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
const idx = n - 1;
|
|
1041
|
-
if (idx >= stored.accounts.length) {
|
|
1042
|
-
log.error(`Error: account ${n} does not exist.`);
|
|
1043
|
-
return 1;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
1047
|
-
|
|
1048
|
-
// Confirm unless --force
|
|
1049
|
-
if (!opts.force) {
|
|
1050
|
-
if (!process.stdin.isTTY) {
|
|
1051
|
-
log.error("Error: use --force to remove accounts in non-interactive mode.");
|
|
1052
|
-
return 1;
|
|
1053
|
-
}
|
|
1054
|
-
const shouldRemove = await confirm({
|
|
1055
|
-
message: `Remove account #${n} (${label})? This cannot be undone.`,
|
|
1056
|
-
});
|
|
1057
|
-
if (isCancel(shouldRemove) || !shouldRemove) {
|
|
1058
|
-
log.info("Cancelled.");
|
|
1059
|
-
return 0;
|
|
113
|
+
async function dispatchAuth(args: string[], flags: { force: boolean; all: boolean }) {
|
|
114
|
+
const subcommand = args[0] || "help";
|
|
115
|
+
const arg = args[1];
|
|
116
|
+
|
|
117
|
+
switch (subcommand) {
|
|
118
|
+
case "login":
|
|
119
|
+
case "ln":
|
|
120
|
+
return cmdLogin();
|
|
121
|
+
case "logout":
|
|
122
|
+
case "lo":
|
|
123
|
+
return cmdLogout(arg, { force: flags.force, all: flags.all });
|
|
124
|
+
case "reauth":
|
|
125
|
+
case "ra":
|
|
126
|
+
return cmdReauth(arg);
|
|
127
|
+
case "refresh":
|
|
128
|
+
case "rf":
|
|
129
|
+
return cmdRefresh(arg);
|
|
130
|
+
case "help":
|
|
131
|
+
case "-h":
|
|
132
|
+
case "--help":
|
|
133
|
+
return cmdAuthGroupHelp("auth");
|
|
134
|
+
default:
|
|
135
|
+
console.error(c.red(`Unknown auth command: ${subcommand}`));
|
|
136
|
+
console.error(c.dim("Run 'opencode-anthropic-auth auth help' for usage."));
|
|
137
|
+
return 1;
|
|
1060
138
|
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
stored.accounts.splice(idx, 1);
|
|
1064
|
-
|
|
1065
|
-
// Adjust active index
|
|
1066
|
-
if (stored.accounts.length === 0) {
|
|
1067
|
-
stored.activeIndex = 0;
|
|
1068
|
-
} else if (stored.activeIndex >= stored.accounts.length) {
|
|
1069
|
-
stored.activeIndex = stored.accounts.length - 1;
|
|
1070
|
-
} else if (stored.activeIndex > idx) {
|
|
1071
|
-
stored.activeIndex--;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
await saveAccounts(stored);
|
|
1075
|
-
log.success(`Removed account #${n} (${label}).`);
|
|
1076
|
-
|
|
1077
|
-
if (stored.accounts.length > 0) {
|
|
1078
|
-
log.info(`${stored.accounts.length} account(s) remaining.`);
|
|
1079
|
-
} else {
|
|
1080
|
-
log.info("No accounts remaining. Run 'opencode auth login' to add one.");
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
return 0;
|
|
1084
139
|
}
|
|
1085
140
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const n = parseInt(arg, 10);
|
|
1117
|
-
if (isNaN(n) || n < 1) {
|
|
1118
|
-
log.error("Error: provide a valid account number or 'all'.");
|
|
1119
|
-
return 1;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
const idx = n - 1;
|
|
1123
|
-
if (idx >= stored.accounts.length) {
|
|
1124
|
-
log.error(`Error: account ${n} does not exist.`);
|
|
1125
|
-
return 1;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
stored.accounts[idx].rateLimitResetTimes = {};
|
|
1129
|
-
stored.accounts[idx].consecutiveFailures = 0;
|
|
1130
|
-
stored.accounts[idx].lastFailureTime = null;
|
|
1131
|
-
await saveAccounts(stored);
|
|
1132
|
-
|
|
1133
|
-
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
1134
|
-
log.success(`Reset tracking for account #${n} (${label}).`);
|
|
1135
|
-
return 0;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
/**
|
|
1139
|
-
* Show current configuration.
|
|
1140
|
-
* @returns {Promise<number>} exit code
|
|
1141
|
-
*/
|
|
1142
|
-
export async function cmdConfig() {
|
|
1143
|
-
const config = loadConfig();
|
|
1144
|
-
const stored = await loadAccounts();
|
|
1145
|
-
|
|
1146
|
-
log.info(c.bold("Anthropic Auth Configuration"));
|
|
1147
|
-
|
|
1148
|
-
const generalLines = [
|
|
1149
|
-
`Strategy: ${c.cyan(config.account_selection_strategy)}`,
|
|
1150
|
-
`Failure TTL: ${config.failure_ttl_seconds}s`,
|
|
1151
|
-
`Debug: ${config.debug ? c.yellow("on") : "off"}`,
|
|
1152
|
-
];
|
|
1153
|
-
note(generalLines.join("\n"), "General");
|
|
1154
|
-
|
|
1155
|
-
const healthLines = [
|
|
1156
|
-
`Initial: ${config.health_score.initial}`,
|
|
1157
|
-
`Success reward: +${config.health_score.success_reward}`,
|
|
1158
|
-
`Rate limit: ${config.health_score.rate_limit_penalty}`,
|
|
1159
|
-
`Failure: ${config.health_score.failure_penalty}`,
|
|
1160
|
-
`Recovery/hour: +${config.health_score.recovery_rate_per_hour}`,
|
|
1161
|
-
`Min usable: ${config.health_score.min_usable}`,
|
|
1162
|
-
];
|
|
1163
|
-
note(healthLines.join("\n"), "Health Score");
|
|
1164
|
-
|
|
1165
|
-
const bucketLines = [
|
|
1166
|
-
`Max tokens: ${config.token_bucket.max_tokens}`,
|
|
1167
|
-
`Regen/min: ${config.token_bucket.regeneration_rate_per_minute}`,
|
|
1168
|
-
`Initial: ${config.token_bucket.initial_tokens}`,
|
|
1169
|
-
];
|
|
1170
|
-
note(bucketLines.join("\n"), "Token Bucket");
|
|
1171
|
-
|
|
1172
|
-
const fileLines = [
|
|
1173
|
-
`Config: ${shortPath(getConfigPath())}`,
|
|
1174
|
-
`Accounts: ${shortPath(getStoragePath())}`,
|
|
1175
|
-
];
|
|
1176
|
-
if (stored) {
|
|
1177
|
-
const enabled = stored.accounts.filter((a) => a.enabled).length;
|
|
1178
|
-
fileLines.push(`Accounts total: ${stored.accounts.length} (${enabled} enabled)`);
|
|
1179
|
-
} else {
|
|
1180
|
-
fileLines.push(`Accounts total: none`);
|
|
1181
|
-
}
|
|
1182
|
-
note(fileLines.join("\n"), "Files");
|
|
1183
|
-
|
|
1184
|
-
const envOverrides: string[] = [];
|
|
1185
|
-
if (process.env.OPENCODE_ANTHROPIC_STRATEGY) {
|
|
1186
|
-
envOverrides.push(`OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY}`);
|
|
1187
|
-
}
|
|
1188
|
-
if (process.env.OPENCODE_ANTHROPIC_DEBUG) {
|
|
1189
|
-
envOverrides.push(`OPENCODE_ANTHROPIC_DEBUG=${process.env.OPENCODE_ANTHROPIC_DEBUG}`);
|
|
1190
|
-
}
|
|
1191
|
-
if (envOverrides.length > 0) {
|
|
1192
|
-
log.warn("Environment overrides:\n" + envOverrides.map((ov) => ` ${c.yellow(ov)}`).join("\n"));
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
return 0;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
/**
|
|
1199
|
-
* Show or change the account selection strategy.
|
|
1200
|
-
* @param {string} [arg] - New strategy name, or undefined to show current
|
|
1201
|
-
* @returns {Promise<number>} exit code
|
|
1202
|
-
*/
|
|
1203
|
-
export async function cmdStrategy(arg?: string) {
|
|
1204
|
-
const config = loadConfig();
|
|
1205
|
-
|
|
1206
|
-
if (!arg) {
|
|
1207
|
-
log.info(c.bold("Account Selection Strategy"));
|
|
1208
|
-
|
|
1209
|
-
const descriptions = {
|
|
1210
|
-
sticky: "Stay on one account until it fails or is rate-limited",
|
|
1211
|
-
"round-robin": "Rotate through accounts on every request",
|
|
1212
|
-
hybrid: "Prefer healthy accounts, rotate when degraded",
|
|
1213
|
-
};
|
|
1214
|
-
|
|
1215
|
-
const lines = VALID_STRATEGIES.map((s) => {
|
|
1216
|
-
const current = s === config.account_selection_strategy;
|
|
1217
|
-
const marker = current ? c.green("▸ ") : " ";
|
|
1218
|
-
const name = current ? c.bold(c.cyan(s)) : c.dim(s);
|
|
1219
|
-
const desc = current ? descriptions[s] : c.dim(descriptions[s]);
|
|
1220
|
-
return `${marker}${pad(name, 16)}${desc}`;
|
|
1221
|
-
});
|
|
1222
|
-
log.message(lines.join("\n"));
|
|
1223
|
-
|
|
1224
|
-
log.message(c.dim(`Change with: opencode-anthropic-auth strategy <${VALID_STRATEGIES.join("|")}>`));
|
|
1225
|
-
|
|
1226
|
-
if (process.env.OPENCODE_ANTHROPIC_STRATEGY) {
|
|
1227
|
-
log.warn(
|
|
1228
|
-
`OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY} overrides config file at runtime.`,
|
|
1229
|
-
);
|
|
141
|
+
async function dispatchAccount(args: string[], flags: { force: boolean }) {
|
|
142
|
+
const subcommand = args[0] || "list";
|
|
143
|
+
const arg = args[1];
|
|
144
|
+
|
|
145
|
+
switch (subcommand) {
|
|
146
|
+
case "list":
|
|
147
|
+
case "ls":
|
|
148
|
+
return cmdList();
|
|
149
|
+
case "switch":
|
|
150
|
+
case "sw":
|
|
151
|
+
return cmdSwitch(arg);
|
|
152
|
+
case "enable":
|
|
153
|
+
case "en":
|
|
154
|
+
return cmdEnable(arg);
|
|
155
|
+
case "disable":
|
|
156
|
+
case "dis":
|
|
157
|
+
return cmdDisable(arg);
|
|
158
|
+
case "remove":
|
|
159
|
+
case "rm":
|
|
160
|
+
return cmdRemove(arg, { force: flags.force });
|
|
161
|
+
case "reset":
|
|
162
|
+
return cmdReset(arg);
|
|
163
|
+
case "help":
|
|
164
|
+
case "-h":
|
|
165
|
+
case "--help":
|
|
166
|
+
return cmdAuthGroupHelp("account");
|
|
167
|
+
default:
|
|
168
|
+
console.error(c.red(`Unknown account command: ${subcommand}`));
|
|
169
|
+
console.error(c.dim("Run 'opencode-anthropic-auth account help' for usage."));
|
|
170
|
+
return 1;
|
|
1230
171
|
}
|
|
1231
|
-
|
|
1232
|
-
return 0;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const normalized = arg.toLowerCase().trim();
|
|
1236
|
-
|
|
1237
|
-
if (!VALID_STRATEGIES.includes(normalized as (typeof VALID_STRATEGIES)[number])) {
|
|
1238
|
-
log.error(`Invalid strategy '${arg}'. Valid strategies: ${VALID_STRATEGIES.join(", ")}`);
|
|
1239
|
-
return 1;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (normalized === config.account_selection_strategy && !process.env.OPENCODE_ANTHROPIC_STRATEGY) {
|
|
1243
|
-
log.message(c.dim(`Strategy is already '${normalized}'.`));
|
|
1244
|
-
return 0;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
saveConfig({ account_selection_strategy: normalized });
|
|
1248
|
-
log.success(`Strategy changed to '${normalized}'.`);
|
|
1249
|
-
|
|
1250
|
-
if (process.env.OPENCODE_ANTHROPIC_STRATEGY) {
|
|
1251
|
-
log.warn(`OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY} will override this at runtime.`);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
return 0;
|
|
1255
172
|
}
|
|
1256
173
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
* @returns {string}
|
|
1261
|
-
*/
|
|
1262
|
-
function fmtTokens(n: number) {
|
|
1263
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
1264
|
-
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
|
|
1265
|
-
return String(n);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/**
|
|
1269
|
-
* Show per-account usage statistics.
|
|
1270
|
-
* @returns {Promise<number>} exit code
|
|
1271
|
-
*/
|
|
1272
|
-
export async function cmdStats() {
|
|
1273
|
-
const stored = await loadAccounts();
|
|
1274
|
-
if (!stored || stored.accounts.length === 0) {
|
|
1275
|
-
log.warn("No accounts configured.");
|
|
1276
|
-
return 1;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Column widths: marker(1) + gap(1) + num(2) + gap(2) + name(20) + gap(2) + 5 numeric cols(10 each)
|
|
1280
|
-
const W = { num: 4, name: 22, val: 10 };
|
|
1281
|
-
|
|
1282
|
-
const RULE = c.dim(" " + "─".repeat(74));
|
|
1283
|
-
|
|
1284
|
-
log.message(c.bold("Anthropic Account Usage"));
|
|
1285
|
-
log.message(
|
|
1286
|
-
" " +
|
|
1287
|
-
pad(c.dim("#"), W.num) +
|
|
1288
|
-
pad(c.dim("Account"), W.name) +
|
|
1289
|
-
rpad(c.dim("Requests"), W.val) +
|
|
1290
|
-
rpad(c.dim("Input"), W.val) +
|
|
1291
|
-
rpad(c.dim("Output"), W.val) +
|
|
1292
|
-
rpad(c.dim("Cache R"), W.val) +
|
|
1293
|
-
rpad(c.dim("Cache W"), W.val),
|
|
1294
|
-
);
|
|
1295
|
-
log.message(RULE);
|
|
1296
|
-
|
|
1297
|
-
let totReq = 0,
|
|
1298
|
-
totIn = 0,
|
|
1299
|
-
totOut = 0,
|
|
1300
|
-
totCR = 0,
|
|
1301
|
-
totCW = 0;
|
|
1302
|
-
let oldestReset = Infinity;
|
|
1303
|
-
|
|
1304
|
-
for (let i = 0; i < stored.accounts.length; i++) {
|
|
1305
|
-
const acc = stored.accounts[i];
|
|
1306
|
-
const s = acc.stats || createDefaultStats();
|
|
1307
|
-
const isActive = i === stored.activeIndex;
|
|
1308
|
-
const marker = isActive ? c.green("●") : " ";
|
|
1309
|
-
const num = `${marker} ${i + 1}`;
|
|
1310
|
-
const name = acc.email || `Account ${i + 1}`;
|
|
1311
|
-
|
|
1312
|
-
log.message(
|
|
1313
|
-
" " +
|
|
1314
|
-
pad(num, W.num) +
|
|
1315
|
-
pad(name, W.name) +
|
|
1316
|
-
rpad(String(s.requests), W.val) +
|
|
1317
|
-
rpad(fmtTokens(s.inputTokens), W.val) +
|
|
1318
|
-
rpad(fmtTokens(s.outputTokens), W.val) +
|
|
1319
|
-
rpad(fmtTokens(s.cacheReadTokens), W.val) +
|
|
1320
|
-
rpad(fmtTokens(s.cacheWriteTokens), W.val),
|
|
1321
|
-
);
|
|
1322
|
-
|
|
1323
|
-
totReq += s.requests;
|
|
1324
|
-
totIn += s.inputTokens;
|
|
1325
|
-
totOut += s.outputTokens;
|
|
1326
|
-
totCR += s.cacheReadTokens;
|
|
1327
|
-
totCW += s.cacheWriteTokens;
|
|
1328
|
-
if (s.lastReset < oldestReset) oldestReset = s.lastReset;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (stored.accounts.length > 1) {
|
|
1332
|
-
log.message(RULE);
|
|
1333
|
-
log.message(
|
|
1334
|
-
c.bold(
|
|
1335
|
-
" " +
|
|
1336
|
-
pad("", W.num) +
|
|
1337
|
-
pad("Total", W.name) +
|
|
1338
|
-
rpad(String(totReq), W.val) +
|
|
1339
|
-
rpad(fmtTokens(totIn), W.val) +
|
|
1340
|
-
rpad(fmtTokens(totOut), W.val) +
|
|
1341
|
-
rpad(fmtTokens(totCR), W.val) +
|
|
1342
|
-
rpad(fmtTokens(totCW), W.val),
|
|
1343
|
-
),
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
if (oldestReset < Infinity) {
|
|
1348
|
-
log.message(c.dim(`Tracking since: ${new Date(oldestReset).toLocaleString()} (${formatTimeAgo(oldestReset)})`));
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
return 0;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
/**
|
|
1355
|
-
* Reset usage statistics for one or all accounts.
|
|
1356
|
-
* @param {string} [arg] - Account number or "all"
|
|
1357
|
-
* @returns {Promise<number>} exit code
|
|
1358
|
-
*/
|
|
1359
|
-
export async function cmdResetStats(arg?: string) {
|
|
1360
|
-
const stored = await loadAccounts();
|
|
1361
|
-
if (!stored || stored.accounts.length === 0) {
|
|
1362
|
-
log.warn("No accounts configured.");
|
|
1363
|
-
return 1;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const now = Date.now();
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Top-level dispatch — two-level (group → subcommand) with legacy flat aliases
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
1367
177
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
178
|
+
async function dispatch(argv: string[]) {
|
|
179
|
+
const args = argv.filter((a: string) => !a.startsWith("--"));
|
|
180
|
+
const flags = argv.filter((a: string) => a.startsWith("--"));
|
|
181
|
+
|
|
182
|
+
if (flags.includes("--no-color")) setUseColor(false);
|
|
183
|
+
if (flags.includes("--help")) return cmdHelp();
|
|
184
|
+
|
|
185
|
+
const command = args[0] || "list";
|
|
186
|
+
const remainingArgs = args.slice(1);
|
|
187
|
+
const force = flags.includes("--force");
|
|
188
|
+
const all = flags.includes("--all");
|
|
189
|
+
|
|
190
|
+
switch (command) {
|
|
191
|
+
// ── Group dispatchers ──────────────────────────────────────────────
|
|
192
|
+
case "auth":
|
|
193
|
+
return dispatchAuth(remainingArgs, { force, all });
|
|
194
|
+
case "account":
|
|
195
|
+
return dispatchAccount(remainingArgs, { force });
|
|
196
|
+
case "usage":
|
|
197
|
+
return dispatchUsageCommands(remainingArgs);
|
|
198
|
+
case "config":
|
|
199
|
+
return dispatchConfigCommands(remainingArgs);
|
|
200
|
+
case "manage":
|
|
201
|
+
return dispatchManageCommands(remainingArgs);
|
|
202
|
+
|
|
203
|
+
// ── Legacy flat aliases (backward compat) ──────────────────────────
|
|
204
|
+
// Auth
|
|
205
|
+
case "login":
|
|
206
|
+
case "ln":
|
|
207
|
+
return cmdLogin();
|
|
208
|
+
case "logout":
|
|
209
|
+
case "lo":
|
|
210
|
+
return cmdLogout(remainingArgs[0], { force, all });
|
|
211
|
+
case "reauth":
|
|
212
|
+
case "ra":
|
|
213
|
+
return cmdReauth(remainingArgs[0]);
|
|
214
|
+
case "refresh":
|
|
215
|
+
case "rf":
|
|
216
|
+
return cmdRefresh(remainingArgs[0]);
|
|
217
|
+
|
|
218
|
+
// Account
|
|
219
|
+
case "list":
|
|
220
|
+
case "ls":
|
|
221
|
+
return cmdList();
|
|
222
|
+
case "switch":
|
|
223
|
+
case "sw":
|
|
224
|
+
return cmdSwitch(remainingArgs[0]);
|
|
225
|
+
case "enable":
|
|
226
|
+
case "en":
|
|
227
|
+
return cmdEnable(remainingArgs[0]);
|
|
228
|
+
case "disable":
|
|
229
|
+
case "dis":
|
|
230
|
+
return cmdDisable(remainingArgs[0]);
|
|
231
|
+
case "remove":
|
|
232
|
+
case "rm":
|
|
233
|
+
return cmdRemove(remainingArgs[0], { force });
|
|
234
|
+
case "reset":
|
|
235
|
+
return cmdReset(remainingArgs[0]);
|
|
236
|
+
|
|
237
|
+
// Usage
|
|
238
|
+
case "stats":
|
|
239
|
+
return cmdStats();
|
|
240
|
+
case "reset-stats":
|
|
241
|
+
return cmdResetStats(remainingArgs[0]);
|
|
242
|
+
case "status":
|
|
243
|
+
case "st":
|
|
244
|
+
return cmdStatus();
|
|
245
|
+
|
|
246
|
+
// Config
|
|
247
|
+
case "strategy":
|
|
248
|
+
case "strat":
|
|
249
|
+
return cmdStrategy(remainingArgs[0]);
|
|
250
|
+
case "cfg":
|
|
251
|
+
return dispatchConfigCommands(["show"]);
|
|
252
|
+
|
|
253
|
+
// Manage
|
|
254
|
+
case "mg":
|
|
255
|
+
return cmdManage();
|
|
256
|
+
|
|
257
|
+
// Help
|
|
258
|
+
case "help":
|
|
259
|
+
case "-h":
|
|
260
|
+
case "--help":
|
|
261
|
+
return cmdHelp();
|
|
262
|
+
|
|
263
|
+
default:
|
|
264
|
+
console.error(c.red(`Unknown command: ${command}`));
|
|
265
|
+
console.error(c.dim("Run 'opencode-anthropic-auth help' for usage."));
|
|
266
|
+
return 1;
|
|
1371
267
|
}
|
|
1372
|
-
await saveAccounts(stored);
|
|
1373
|
-
log.success("Reset usage statistics for all accounts.");
|
|
1374
|
-
return 0;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const idx = parseInt(arg, 10) - 1;
|
|
1378
|
-
if (isNaN(idx) || idx < 0 || idx >= stored.accounts.length) {
|
|
1379
|
-
log.error(`Invalid account number. Use 1-${stored.accounts.length} or 'all'.`);
|
|
1380
|
-
return 1;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
stored.accounts[idx].stats = createDefaultStats(now);
|
|
1384
|
-
await saveAccounts(stored);
|
|
1385
|
-
const name = stored.accounts[idx].email || `Account ${idx + 1}`;
|
|
1386
|
-
log.success(`Reset usage statistics for ${name}.`);
|
|
1387
|
-
return 0;
|
|
1388
268
|
}
|
|
1389
269
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
* Operates on raw storage (not AccountManager) to avoid stale-state issues.
|
|
1394
|
-
* Each mutation is saved atomically before the next prompt.
|
|
1395
|
-
*
|
|
1396
|
-
* @returns {Promise<number>} exit code
|
|
1397
|
-
*/
|
|
1398
|
-
export async function cmdManage() {
|
|
1399
|
-
let stored = await loadAccounts();
|
|
1400
|
-
if (!stored || stored.accounts.length === 0) {
|
|
1401
|
-
console.log(c.yellow("No accounts configured."));
|
|
1402
|
-
console.log(c.dim("Run 'opencode auth login' and select 'Claude Pro/Max' to add accounts."));
|
|
1403
|
-
return 1;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
if (!process.stdin.isTTY) {
|
|
1407
|
-
console.error(c.red("Error: 'manage' requires an interactive terminal."));
|
|
1408
|
-
console.error(c.dim("Use 'enable', 'disable', 'remove', 'switch' for non-interactive use."));
|
|
1409
|
-
return 1;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
while (true) {
|
|
1413
|
-
// Re-read from disk each iteration to stay in sync
|
|
1414
|
-
stored = await loadAccounts();
|
|
1415
|
-
if (!stored || stored.accounts.length === 0) {
|
|
1416
|
-
console.log(c.dim("No accounts remaining."));
|
|
1417
|
-
break;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
const accounts = stored.accounts;
|
|
1421
|
-
const activeIndex = stored.activeIndex;
|
|
1422
|
-
const currentStrategy = loadConfig().account_selection_strategy;
|
|
1423
|
-
|
|
1424
|
-
console.log("");
|
|
1425
|
-
console.log(c.bold(`${accounts.length} account(s):`));
|
|
1426
|
-
for (let i = 0; i < accounts.length; i++) {
|
|
1427
|
-
const num = i + 1;
|
|
1428
|
-
const label = accounts[i].email || `Account ${num}`;
|
|
1429
|
-
const active = i === stored.activeIndex ? c.green(" (active)") : "";
|
|
1430
|
-
const disabled = !accounts[i].enabled ? c.yellow(" [disabled]") : "";
|
|
1431
|
-
console.log(` ${c.bold(String(num))}. ${label}${active}${disabled}`);
|
|
1432
|
-
}
|
|
1433
|
-
console.log("");
|
|
1434
|
-
console.log(c.dim(`Strategy: ${currentStrategy}`));
|
|
1435
|
-
|
|
1436
|
-
const action = await select<"switch" | "enable" | "disable" | "remove" | "reset" | "strategy" | "quit">({
|
|
1437
|
-
message: "Choose an action.",
|
|
1438
|
-
options: [
|
|
1439
|
-
{ value: "switch", label: "Switch", hint: "set the active account" },
|
|
1440
|
-
{ value: "enable", label: "Enable", hint: "re-enable a disabled account" },
|
|
1441
|
-
{ value: "disable", label: "Disable", hint: "skip an account in rotation" },
|
|
1442
|
-
{ value: "remove", label: "Remove", hint: "delete an account from storage" },
|
|
1443
|
-
{ value: "reset", label: "Reset", hint: "clear rate-limit and failure tracking" },
|
|
1444
|
-
{ value: "strategy", label: "Strategy", hint: currentStrategy },
|
|
1445
|
-
{ value: "quit", label: "Quit", hint: "exit manage" },
|
|
1446
|
-
],
|
|
1447
|
-
});
|
|
1448
|
-
if (isCancel(action) || action === "quit") break;
|
|
1449
|
-
|
|
1450
|
-
if (action === "strategy") {
|
|
1451
|
-
const strategy = await select<(typeof VALID_STRATEGIES)[number]>({
|
|
1452
|
-
message: "Choose an account selection strategy.",
|
|
1453
|
-
initialValue: currentStrategy,
|
|
1454
|
-
options: VALID_STRATEGIES.map((value) => ({
|
|
1455
|
-
value,
|
|
1456
|
-
label: value,
|
|
1457
|
-
hint: value === currentStrategy ? "current" : undefined,
|
|
1458
|
-
})),
|
|
1459
|
-
});
|
|
1460
|
-
if (isCancel(strategy)) break;
|
|
1461
|
-
saveConfig({ account_selection_strategy: strategy });
|
|
1462
|
-
console.log(c.green(`Strategy changed to '${strategy}'.`));
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
const target = await select<string>({
|
|
1467
|
-
message: "Choose an account.",
|
|
1468
|
-
initialValue: String(activeIndex),
|
|
1469
|
-
options: accounts.map((account, index) => {
|
|
1470
|
-
const num = index + 1;
|
|
1471
|
-
const label = account.email || `Account ${num}`;
|
|
1472
|
-
const statuses = [index === activeIndex ? "active" : null, !account.enabled ? "disabled" : null].filter(
|
|
1473
|
-
Boolean,
|
|
1474
|
-
);
|
|
1475
|
-
return {
|
|
1476
|
-
value: String(index),
|
|
1477
|
-
label: `#${num} ${label}`,
|
|
1478
|
-
hint: statuses.length > 0 ? statuses.join(", ") : undefined,
|
|
1479
|
-
};
|
|
1480
|
-
}),
|
|
1481
|
-
});
|
|
1482
|
-
if (isCancel(target)) break;
|
|
1483
|
-
|
|
1484
|
-
const idx = Number.parseInt(target, 10);
|
|
1485
|
-
const num = idx + 1;
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Public entry point
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
1486
273
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
console.log(c.yellow(`Account ${num} is disabled. Enable it first.`));
|
|
1491
|
-
break;
|
|
1492
|
-
}
|
|
1493
|
-
stored.activeIndex = idx;
|
|
1494
|
-
await saveAccounts(stored);
|
|
1495
|
-
const switchLabel = accounts[idx].email || `Account ${num}`;
|
|
1496
|
-
console.log(c.green(`Switched to #${num} (${switchLabel}).`));
|
|
1497
|
-
break;
|
|
1498
|
-
}
|
|
1499
|
-
case "enable": {
|
|
1500
|
-
if (accounts[idx].enabled) {
|
|
1501
|
-
console.log(c.dim(`Account ${num} is already enabled.`));
|
|
1502
|
-
break;
|
|
1503
|
-
}
|
|
1504
|
-
stored.accounts[idx].enabled = true;
|
|
1505
|
-
await saveAccounts(stored);
|
|
1506
|
-
console.log(c.green(`Enabled account #${num}.`));
|
|
1507
|
-
break;
|
|
1508
|
-
}
|
|
1509
|
-
case "disable": {
|
|
1510
|
-
if (!accounts[idx].enabled) {
|
|
1511
|
-
console.log(c.dim(`Account ${num} is already disabled.`));
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
const enabledCount = accounts.filter((account) => account.enabled).length;
|
|
1515
|
-
if (enabledCount <= 1) {
|
|
1516
|
-
console.log(c.red("Cannot disable the last enabled account."));
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
stored.accounts[idx].enabled = false;
|
|
1520
|
-
if (idx === stored.activeIndex) {
|
|
1521
|
-
const nextEnabled = accounts.findIndex((account, accountIndex) => accountIndex !== idx && account.enabled);
|
|
1522
|
-
if (nextEnabled >= 0) stored.activeIndex = nextEnabled;
|
|
1523
|
-
}
|
|
1524
|
-
await saveAccounts(stored);
|
|
1525
|
-
console.log(c.yellow(`Disabled account #${num}.`));
|
|
1526
|
-
break;
|
|
1527
|
-
}
|
|
1528
|
-
case "remove": {
|
|
1529
|
-
const removeLabel = accounts[idx].email || `Account ${num}`;
|
|
1530
|
-
const removeConfirm = await confirm({
|
|
1531
|
-
message: `Remove #${num} (${removeLabel})?`,
|
|
1532
|
-
});
|
|
1533
|
-
if (isCancel(removeConfirm)) break;
|
|
1534
|
-
if (removeConfirm) {
|
|
1535
|
-
stored.accounts.splice(idx, 1);
|
|
1536
|
-
if (stored.accounts.length === 0) {
|
|
1537
|
-
stored.activeIndex = 0;
|
|
1538
|
-
} else if (stored.activeIndex >= stored.accounts.length) {
|
|
1539
|
-
stored.activeIndex = stored.accounts.length - 1;
|
|
1540
|
-
} else if (stored.activeIndex > idx) {
|
|
1541
|
-
stored.activeIndex--;
|
|
1542
|
-
}
|
|
1543
|
-
await saveAccounts(stored);
|
|
1544
|
-
console.log(c.green(`Removed account #${num}.`));
|
|
1545
|
-
} else {
|
|
1546
|
-
console.log(c.dim("Cancelled."));
|
|
1547
|
-
}
|
|
1548
|
-
break;
|
|
1549
|
-
}
|
|
1550
|
-
case "reset": {
|
|
1551
|
-
stored.accounts[idx].rateLimitResetTimes = {};
|
|
1552
|
-
stored.accounts[idx].consecutiveFailures = 0;
|
|
1553
|
-
stored.accounts[idx].lastFailureTime = null;
|
|
1554
|
-
await saveAccounts(stored);
|
|
1555
|
-
console.log(c.green(`Reset tracking for account #${num}.`));
|
|
1556
|
-
break;
|
|
1557
|
-
}
|
|
274
|
+
export async function main(argv: string[], options: { io?: IoStore } = {}) {
|
|
275
|
+
if (options.io) {
|
|
276
|
+
return runWithIoContext(options.io, () => dispatch(argv));
|
|
1558
277
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return 0;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
/**
|
|
1565
|
-
* Show help text.
|
|
1566
|
-
*/
|
|
1567
|
-
export function cmdHelp() {
|
|
1568
|
-
const bin = "opencode-anthropic-auth";
|
|
1569
|
-
console.log(`
|
|
1570
|
-
${c.bold("Anthropic Multi-Account Auth CLI")}
|
|
1571
|
-
|
|
1572
|
-
${c.dim("Usage:")}
|
|
1573
|
-
${bin} [group] [command] [args]
|
|
1574
|
-
${bin} [command] [args] ${c.dim("(legacy format, still supported)")}
|
|
1575
|
-
oaa [group] [command] [args] ${c.dim("(short alias)")}
|
|
1576
|
-
|
|
1577
|
-
${c.dim("Command Groups:")}
|
|
1578
|
-
${pad(c.cyan("auth"), 22)}Authentication: login, logout, reauth, refresh
|
|
1579
|
-
${pad(c.cyan("account"), 22)}Account management: list, switch, enable, disable, remove, reset
|
|
1580
|
-
${pad(c.cyan("usage"), 22)}Usage statistics: stats, reset-stats, status
|
|
1581
|
-
${pad(c.cyan("config"), 22)}Configuration: show, strategy
|
|
1582
|
-
${pad(c.cyan("manage"), 22)}Interactive account management menu
|
|
1583
|
-
|
|
1584
|
-
${c.dim("Auth Commands:")}
|
|
1585
|
-
${pad(c.cyan("login"), 22)}Add a new account via browser OAuth (alias: ln)
|
|
1586
|
-
${pad(c.cyan("logout") + " <N>", 22)}Revoke tokens and remove account N (alias: lo)
|
|
1587
|
-
${pad(c.cyan("logout") + " --all", 22)}Revoke all tokens and clear all accounts
|
|
1588
|
-
${pad(c.cyan("reauth") + " <N>", 22)}Re-authenticate account N (alias: ra)
|
|
1589
|
-
${pad(c.cyan("refresh") + " <N>", 22)}Attempt token refresh (alias: rf)
|
|
1590
|
-
|
|
1591
|
-
${c.dim("Account Commands:")}
|
|
1592
|
-
${pad(c.cyan("list"), 22)}Show all accounts with status ${c.dim("(default, alias: ls)")}
|
|
1593
|
-
${pad(c.cyan("switch") + " <N>", 22)}Set account N as active (alias: sw)
|
|
1594
|
-
${pad(c.cyan("enable") + " <N>", 22)}Enable a disabled account (alias: en)
|
|
1595
|
-
${pad(c.cyan("disable") + " <N>", 22)}Disable an account (alias: dis)
|
|
1596
|
-
${pad(c.cyan("remove") + " <N>", 22)}Remove an account permanently (alias: rm)
|
|
1597
|
-
${pad(c.cyan("reset") + " <N|all>", 22)}Clear rate-limit / failure tracking
|
|
1598
|
-
|
|
1599
|
-
${c.dim("Usage Commands:")}
|
|
1600
|
-
${pad(c.cyan("stats"), 22)}Show per-account usage statistics
|
|
1601
|
-
${pad(c.cyan("reset-stats") + " [N|all]", 22)}Reset usage statistics
|
|
1602
|
-
${pad(c.cyan("status"), 22)}Compact one-liner for scripts/prompts (alias: st)
|
|
1603
|
-
|
|
1604
|
-
${c.dim("Config Commands:")}
|
|
1605
|
-
${pad(c.cyan("config"), 22)}Show configuration and file paths (alias: cfg)
|
|
1606
|
-
${pad(c.cyan("strategy") + " [name]", 22)}Show or change selection strategy (alias: strat)
|
|
1607
|
-
|
|
1608
|
-
${c.dim("Manage Commands:")}
|
|
1609
|
-
${pad(c.cyan("manage"), 22)}Interactive account management menu (alias: mg)
|
|
1610
|
-
${pad(c.cyan("help"), 22)}Show this help message
|
|
1611
|
-
|
|
1612
|
-
${c.dim("Group Help:")}
|
|
1613
|
-
${bin} auth help ${c.dim("# Show auth commands")}
|
|
1614
|
-
${bin} account help ${c.dim("# Show account commands")}
|
|
1615
|
-
${bin} usage help ${c.dim("# Show usage commands")}
|
|
1616
|
-
${bin} config help ${c.dim("# Show config commands")}
|
|
1617
|
-
|
|
1618
|
-
${c.dim("Options:")}
|
|
1619
|
-
--force Skip confirmation prompts
|
|
1620
|
-
--all Target all accounts (for logout)
|
|
1621
|
-
--no-color Disable colored output
|
|
1622
|
-
|
|
1623
|
-
${c.dim("Examples:")}
|
|
1624
|
-
${bin} login ${c.dim("# Add a new account via browser")}
|
|
1625
|
-
${bin} auth login ${c.dim("# Same as above (group format)")}
|
|
1626
|
-
oaa login ${c.dim("# Same as above (short alias)")}
|
|
1627
|
-
${bin} logout 2 ${c.dim("# Revoke tokens & remove account 2")}
|
|
1628
|
-
${bin} auth logout 2 ${c.dim("# Same as above (group format)")}
|
|
1629
|
-
${bin} list ${c.dim("# Show all accounts (default)")}
|
|
1630
|
-
${bin} account list ${c.dim("# Same as above (group format)")}
|
|
1631
|
-
${bin} switch 2 ${c.dim("# Make account 2 active")}
|
|
1632
|
-
${bin} account switch 2 ${c.dim("# Same as above (group format)")}
|
|
1633
|
-
${bin} stats ${c.dim("# Show token usage per account")}
|
|
1634
|
-
${bin} usage stats ${c.dim("# Same as above (group format)")}
|
|
1635
|
-
|
|
1636
|
-
${c.dim("Files:")}
|
|
1637
|
-
Config: ${shortPath(getConfigPath())}
|
|
1638
|
-
Accounts: ${shortPath(getStoragePath())}
|
|
1639
|
-
`);
|
|
1640
|
-
return 0;
|
|
278
|
+
return dispatch(argv);
|
|
1641
279
|
}
|
|
1642
280
|
|
|
1643
281
|
// ---------------------------------------------------------------------------
|
|
1644
|
-
//
|
|
282
|
+
// Direct execution detection
|
|
1645
283
|
// ---------------------------------------------------------------------------
|
|
1646
284
|
|
|
1647
|
-
/** @type {AsyncLocalStorage<{ log?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void }>} */
|
|
1648
|
-
type IoStore = {
|
|
1649
|
-
log?: (...args: unknown[]) => void;
|
|
1650
|
-
error?: (...args: unknown[]) => void;
|
|
1651
|
-
};
|
|
1652
|
-
const ioContext = new AsyncLocalStorage<IoStore>();
|
|
1653
|
-
|
|
1654
|
-
const nativeConsoleLog = console.log.bind(console);
|
|
1655
|
-
const nativeConsoleError = console.error.bind(console);
|
|
1656
|
-
let consoleRouterUsers = 0;
|
|
1657
|
-
|
|
1658
|
-
function installConsoleRouter() {
|
|
1659
|
-
if (consoleRouterUsers === 0) {
|
|
1660
|
-
console.log = (...args) => {
|
|
1661
|
-
const io = ioContext.getStore();
|
|
1662
|
-
if (io?.log) return io.log(...args);
|
|
1663
|
-
return nativeConsoleLog(...args);
|
|
1664
|
-
};
|
|
1665
|
-
console.error = (...args) => {
|
|
1666
|
-
const io = ioContext.getStore();
|
|
1667
|
-
if (io?.error) return io.error(...args);
|
|
1668
|
-
return nativeConsoleError(...args);
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
consoleRouterUsers++;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
function uninstallConsoleRouter() {
|
|
1675
|
-
consoleRouterUsers = Math.max(0, consoleRouterUsers - 1);
|
|
1676
|
-
if (consoleRouterUsers === 0) {
|
|
1677
|
-
console.log = nativeConsoleLog;
|
|
1678
|
-
console.error = nativeConsoleError;
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
/**
|
|
1683
|
-
* Run with async-local IO capture without persistent global side effects.
|
|
1684
|
-
* @param {{ log?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void }} io
|
|
1685
|
-
* @param {() => Promise<number>} fn
|
|
1686
|
-
*/
|
|
1687
|
-
async function runWithIoContext(io: IoStore, fn: () => Promise<number>) {
|
|
1688
|
-
installConsoleRouter();
|
|
1689
|
-
try {
|
|
1690
|
-
return await ioContext.run(io, fn);
|
|
1691
|
-
} finally {
|
|
1692
|
-
uninstallConsoleRouter();
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* Show help for a specific command group.
|
|
1698
|
-
* @param {string} group - The command group name
|
|
1699
|
-
* @returns {number} exit code
|
|
1700
|
-
*/
|
|
1701
|
-
function cmdGroupHelp(group: string) {
|
|
1702
|
-
const bin = "opencode-anthropic-auth";
|
|
1703
|
-
switch (group) {
|
|
1704
|
-
case "auth":
|
|
1705
|
-
console.log(`
|
|
1706
|
-
${c.bold("Auth Commands")}
|
|
1707
|
-
|
|
1708
|
-
${pad(c.cyan("login"), 20)}Add a new account via browser OAuth flow (alias: ln)
|
|
1709
|
-
${pad(c.cyan("logout") + " <N>", 20)}Revoke tokens and remove account N (alias: lo)
|
|
1710
|
-
${pad(c.cyan("logout") + " --all", 20)}Revoke all tokens and clear all accounts
|
|
1711
|
-
${pad(c.cyan("reauth") + " <N>", 20)}Re-authenticate account N with fresh tokens (alias: ra)
|
|
1712
|
-
${pad(c.cyan("refresh") + " <N>", 20)}Attempt token refresh without browser (alias: rf)
|
|
1713
|
-
|
|
1714
|
-
${c.dim("Examples:")}
|
|
1715
|
-
${bin} auth login
|
|
1716
|
-
${bin} auth logout 2
|
|
1717
|
-
${bin} auth reauth 1
|
|
1718
|
-
`);
|
|
1719
|
-
return 0;
|
|
1720
|
-
case "account":
|
|
1721
|
-
console.log(`
|
|
1722
|
-
${c.bold("Account Commands")}
|
|
1723
|
-
|
|
1724
|
-
${pad(c.cyan("list"), 20)}Show all accounts with status (alias: ls)
|
|
1725
|
-
${pad(c.cyan("switch") + " <N>", 20)}Set account N as active (alias: sw)
|
|
1726
|
-
${pad(c.cyan("enable") + " <N>", 20)}Enable a disabled account (alias: en)
|
|
1727
|
-
${pad(c.cyan("disable") + " <N>", 20)}Disable an account (alias: dis)
|
|
1728
|
-
${pad(c.cyan("remove") + " <N>", 20)}Remove an account permanently (alias: rm)
|
|
1729
|
-
${pad(c.cyan("reset"), 20)}Clear rate-limit / failure tracking
|
|
1730
|
-
|
|
1731
|
-
${c.dim("Examples:")}
|
|
1732
|
-
${bin} account list
|
|
1733
|
-
${bin} account switch 2
|
|
1734
|
-
${bin} account disable 3
|
|
1735
|
-
`);
|
|
1736
|
-
return 0;
|
|
1737
|
-
case "usage":
|
|
1738
|
-
console.log(`
|
|
1739
|
-
${c.bold("Usage Commands")}
|
|
1740
|
-
|
|
1741
|
-
${pad(c.cyan("stats"), 20)}Show per-account usage statistics
|
|
1742
|
-
${pad(c.cyan("reset-stats") + " [N|all]", 20)}Reset usage statistics
|
|
1743
|
-
${pad(c.cyan("status"), 20)}Compact one-liner for scripts/prompts (alias: st)
|
|
1744
|
-
|
|
1745
|
-
${c.dim("Examples:")}
|
|
1746
|
-
${bin} usage stats
|
|
1747
|
-
${bin} usage status
|
|
1748
|
-
`);
|
|
1749
|
-
return 0;
|
|
1750
|
-
case "config":
|
|
1751
|
-
console.log(`
|
|
1752
|
-
${c.bold("Config Commands")}
|
|
1753
|
-
|
|
1754
|
-
${pad(c.cyan("show"), 20)}Show current configuration and file paths (alias: cfg)
|
|
1755
|
-
${pad(c.cyan("strategy") + " [name]", 20)}Show or change selection strategy (alias: strat)
|
|
1756
|
-
|
|
1757
|
-
${c.dim("Examples:")}
|
|
1758
|
-
${bin} config show
|
|
1759
|
-
${bin} config strategy round-robin
|
|
1760
|
-
`);
|
|
1761
|
-
return 0;
|
|
1762
|
-
case "manage":
|
|
1763
|
-
console.log(`
|
|
1764
|
-
${c.bold("Manage Command")}
|
|
1765
|
-
|
|
1766
|
-
${pad(c.cyan("manage"), 20)}Interactive account management menu (alias: mg)
|
|
1767
|
-
|
|
1768
|
-
${c.dim("Examples:")}
|
|
1769
|
-
${bin} manage
|
|
1770
|
-
`);
|
|
1771
|
-
return 0;
|
|
1772
|
-
default:
|
|
1773
|
-
console.error(c.red(`Unknown command group: ${group}`));
|
|
1774
|
-
return 1;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
/**
|
|
1779
|
-
* Dispatch auth group commands.
|
|
1780
|
-
* @param {string[]} args - Arguments after the group name
|
|
1781
|
-
* @param {{ force: boolean, all: boolean }} flags - Parsed flags
|
|
1782
|
-
* @returns {Promise<number>} exit code
|
|
1783
|
-
*/
|
|
1784
|
-
async function dispatchAuth(args: string[], flags: { force: boolean; all: boolean }) {
|
|
1785
|
-
const subcommand = args[0] || "help";
|
|
1786
|
-
const arg = args[1];
|
|
1787
|
-
|
|
1788
|
-
switch (subcommand) {
|
|
1789
|
-
case "login":
|
|
1790
|
-
case "ln":
|
|
1791
|
-
return cmdLogin();
|
|
1792
|
-
case "logout":
|
|
1793
|
-
case "lo":
|
|
1794
|
-
return cmdLogout(arg, { force: flags.force, all: flags.all });
|
|
1795
|
-
case "reauth":
|
|
1796
|
-
case "ra":
|
|
1797
|
-
return cmdReauth(arg);
|
|
1798
|
-
case "refresh":
|
|
1799
|
-
case "rf":
|
|
1800
|
-
return cmdRefresh(arg);
|
|
1801
|
-
case "help":
|
|
1802
|
-
case "-h":
|
|
1803
|
-
case "--help":
|
|
1804
|
-
return cmdGroupHelp("auth");
|
|
1805
|
-
default:
|
|
1806
|
-
console.error(c.red(`Unknown auth command: ${subcommand}`));
|
|
1807
|
-
console.error(c.dim("Run 'opencode-anthropic-auth auth help' for usage."));
|
|
1808
|
-
return 1;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* Dispatch account group commands.
|
|
1814
|
-
* @param {string[]} args - Arguments after the group name
|
|
1815
|
-
* @param {{ force: boolean }} flags - Parsed flags
|
|
1816
|
-
* @returns {Promise<number>} exit code
|
|
1817
|
-
*/
|
|
1818
|
-
async function dispatchAccount(args: string[], flags: { force: boolean }) {
|
|
1819
|
-
const subcommand = args[0] || "list";
|
|
1820
|
-
const arg = args[1];
|
|
1821
|
-
|
|
1822
|
-
switch (subcommand) {
|
|
1823
|
-
case "list":
|
|
1824
|
-
case "ls":
|
|
1825
|
-
return cmdList();
|
|
1826
|
-
case "switch":
|
|
1827
|
-
case "sw":
|
|
1828
|
-
return cmdSwitch(arg);
|
|
1829
|
-
case "enable":
|
|
1830
|
-
case "en":
|
|
1831
|
-
return cmdEnable(arg);
|
|
1832
|
-
case "disable":
|
|
1833
|
-
case "dis":
|
|
1834
|
-
return cmdDisable(arg);
|
|
1835
|
-
case "remove":
|
|
1836
|
-
case "rm":
|
|
1837
|
-
return cmdRemove(arg, { force: flags.force });
|
|
1838
|
-
case "reset":
|
|
1839
|
-
return cmdReset(arg);
|
|
1840
|
-
case "help":
|
|
1841
|
-
case "-h":
|
|
1842
|
-
case "--help":
|
|
1843
|
-
return cmdGroupHelp("account");
|
|
1844
|
-
default:
|
|
1845
|
-
console.error(c.red(`Unknown account command: ${subcommand}`));
|
|
1846
|
-
console.error(c.dim("Run 'opencode-anthropic-auth account help' for usage."));
|
|
1847
|
-
return 1;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
/**
|
|
1852
|
-
* Dispatch usage group commands.
|
|
1853
|
-
* @param {string[]} args - Arguments after the group name
|
|
1854
|
-
* @returns {Promise<number>} exit code
|
|
1855
|
-
*/
|
|
1856
|
-
async function dispatchUsage(args: string[]) {
|
|
1857
|
-
const subcommand = args[0] || "stats";
|
|
1858
|
-
const arg = args[1];
|
|
1859
|
-
|
|
1860
|
-
switch (subcommand) {
|
|
1861
|
-
case "stats":
|
|
1862
|
-
return cmdStats();
|
|
1863
|
-
case "reset-stats":
|
|
1864
|
-
return cmdResetStats(arg);
|
|
1865
|
-
case "status":
|
|
1866
|
-
case "st":
|
|
1867
|
-
return cmdStatus();
|
|
1868
|
-
case "help":
|
|
1869
|
-
case "-h":
|
|
1870
|
-
case "--help":
|
|
1871
|
-
return cmdGroupHelp("usage");
|
|
1872
|
-
default:
|
|
1873
|
-
console.error(c.red(`Unknown usage command: ${subcommand}`));
|
|
1874
|
-
console.error(c.dim("Run 'opencode-anthropic-auth usage help' for usage."));
|
|
1875
|
-
return 1;
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
/**
|
|
1880
|
-
* Dispatch config group commands.
|
|
1881
|
-
* @param {string[]} args - Arguments after the group name
|
|
1882
|
-
* @returns {Promise<number>} exit code
|
|
1883
|
-
*/
|
|
1884
|
-
async function dispatchConfig(args: string[]) {
|
|
1885
|
-
const subcommand = args[0] || "show";
|
|
1886
|
-
const arg = args[1];
|
|
1887
|
-
|
|
1888
|
-
switch (subcommand) {
|
|
1889
|
-
case "show":
|
|
1890
|
-
case "cfg":
|
|
1891
|
-
return cmdConfig();
|
|
1892
|
-
case "strategy":
|
|
1893
|
-
case "strat":
|
|
1894
|
-
return cmdStrategy(arg);
|
|
1895
|
-
case "help":
|
|
1896
|
-
case "-h":
|
|
1897
|
-
case "--help":
|
|
1898
|
-
return cmdGroupHelp("config");
|
|
1899
|
-
default:
|
|
1900
|
-
console.error(c.red(`Unknown config command: ${subcommand}`));
|
|
1901
|
-
console.error(c.dim("Run 'opencode-anthropic-auth config help' for usage."));
|
|
1902
|
-
return 1;
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* Dispatch manage group commands.
|
|
1908
|
-
* @param {string[]} args - Arguments after the group name
|
|
1909
|
-
* @returns {Promise<number>} exit code
|
|
1910
|
-
*/
|
|
1911
|
-
async function dispatchManage(args: string[]) {
|
|
1912
|
-
const subcommand = args[0] || "help";
|
|
1913
|
-
|
|
1914
|
-
switch (subcommand) {
|
|
1915
|
-
case "manage":
|
|
1916
|
-
case "mg":
|
|
1917
|
-
return cmdManage();
|
|
1918
|
-
case "help":
|
|
1919
|
-
case "-h":
|
|
1920
|
-
case "--help":
|
|
1921
|
-
return cmdGroupHelp("manage");
|
|
1922
|
-
default:
|
|
1923
|
-
console.error(c.red(`Unknown manage command: ${subcommand}`));
|
|
1924
|
-
console.error(c.dim("Run 'opencode-anthropic-auth manage help' for usage."));
|
|
1925
|
-
return 1;
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
/**
|
|
1930
|
-
* Parse argv and route to the appropriate command.
|
|
1931
|
-
* Supports two-level dispatch: group → subcommand
|
|
1932
|
-
* Maintains backward compatibility with legacy flat commands.
|
|
1933
|
-
* @param {string[]} argv - process.argv.slice(2)
|
|
1934
|
-
* @returns {Promise<number>} exit code
|
|
1935
|
-
*/
|
|
1936
|
-
async function dispatch(argv: string[]) {
|
|
1937
|
-
const args = argv.filter((a: string) => !a.startsWith("--"));
|
|
1938
|
-
const flags = argv.filter((a: string) => a.startsWith("--"));
|
|
1939
|
-
|
|
1940
|
-
// Handle global flags
|
|
1941
|
-
if (flags.includes("--no-color")) USE_COLOR = false;
|
|
1942
|
-
if (flags.includes("--help")) return cmdHelp();
|
|
1943
|
-
|
|
1944
|
-
const command = args[0] || "list";
|
|
1945
|
-
const remainingArgs = args.slice(1);
|
|
1946
|
-
|
|
1947
|
-
const force = flags.includes("--force");
|
|
1948
|
-
const all = flags.includes("--all");
|
|
1949
|
-
|
|
1950
|
-
// Two-level dispatch: check if first arg is a command group
|
|
1951
|
-
switch (command) {
|
|
1952
|
-
// Group dispatchers
|
|
1953
|
-
case "auth":
|
|
1954
|
-
return dispatchAuth(remainingArgs, { force, all });
|
|
1955
|
-
case "account":
|
|
1956
|
-
return dispatchAccount(remainingArgs, { force });
|
|
1957
|
-
case "usage":
|
|
1958
|
-
return dispatchUsage(remainingArgs);
|
|
1959
|
-
case "config":
|
|
1960
|
-
return dispatchConfig(remainingArgs);
|
|
1961
|
-
case "manage":
|
|
1962
|
-
return dispatchManage(remainingArgs);
|
|
1963
|
-
|
|
1964
|
-
// Legacy backward compatibility: direct commands (map to groups)
|
|
1965
|
-
// Auth commands
|
|
1966
|
-
case "login":
|
|
1967
|
-
case "ln":
|
|
1968
|
-
return cmdLogin();
|
|
1969
|
-
case "logout":
|
|
1970
|
-
case "lo":
|
|
1971
|
-
return cmdLogout(remainingArgs[0], { force, all });
|
|
1972
|
-
case "reauth":
|
|
1973
|
-
case "ra":
|
|
1974
|
-
return cmdReauth(remainingArgs[0]);
|
|
1975
|
-
case "refresh":
|
|
1976
|
-
case "rf":
|
|
1977
|
-
return cmdRefresh(remainingArgs[0]);
|
|
1978
|
-
|
|
1979
|
-
// Account commands
|
|
1980
|
-
case "list":
|
|
1981
|
-
case "ls":
|
|
1982
|
-
return cmdList();
|
|
1983
|
-
case "switch":
|
|
1984
|
-
case "sw":
|
|
1985
|
-
return cmdSwitch(remainingArgs[0]);
|
|
1986
|
-
case "enable":
|
|
1987
|
-
case "en":
|
|
1988
|
-
return cmdEnable(remainingArgs[0]);
|
|
1989
|
-
case "disable":
|
|
1990
|
-
case "dis":
|
|
1991
|
-
return cmdDisable(remainingArgs[0]);
|
|
1992
|
-
case "remove":
|
|
1993
|
-
case "rm":
|
|
1994
|
-
return cmdRemove(remainingArgs[0], { force });
|
|
1995
|
-
case "reset":
|
|
1996
|
-
return cmdReset(remainingArgs[0]);
|
|
1997
|
-
|
|
1998
|
-
// Usage commands
|
|
1999
|
-
case "stats":
|
|
2000
|
-
return cmdStats();
|
|
2001
|
-
case "reset-stats":
|
|
2002
|
-
return cmdResetStats(remainingArgs[0]);
|
|
2003
|
-
case "status":
|
|
2004
|
-
case "st":
|
|
2005
|
-
return cmdStatus();
|
|
2006
|
-
|
|
2007
|
-
// Config commands (strategy only - config/cfg handled by group dispatcher)
|
|
2008
|
-
case "strategy":
|
|
2009
|
-
case "strat":
|
|
2010
|
-
return cmdStrategy(remainingArgs[0]);
|
|
2011
|
-
case "cfg":
|
|
2012
|
-
// cfg alias for config - handled by group dispatcher as default
|
|
2013
|
-
return dispatchConfig(["show"]);
|
|
2014
|
-
|
|
2015
|
-
// Manage commands
|
|
2016
|
-
case "mg":
|
|
2017
|
-
return cmdManage();
|
|
2018
|
-
|
|
2019
|
-
// Help
|
|
2020
|
-
case "help":
|
|
2021
|
-
case "-h":
|
|
2022
|
-
case "--help":
|
|
2023
|
-
return cmdHelp();
|
|
2024
|
-
|
|
2025
|
-
default:
|
|
2026
|
-
console.error(c.red(`Unknown command: ${command}`));
|
|
2027
|
-
console.error(c.dim("Run 'opencode-anthropic-auth help' for usage."));
|
|
2028
|
-
return 1;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
/**
|
|
2033
|
-
* Parse argv and route to the appropriate command.
|
|
2034
|
-
* @param {string[]} argv - process.argv.slice(2)
|
|
2035
|
-
* @param {{ io?: { log?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void } }} [options]
|
|
2036
|
-
* @returns {Promise<number>} exit code
|
|
2037
|
-
*/
|
|
2038
|
-
export async function main(
|
|
2039
|
-
argv: string[],
|
|
2040
|
-
options: {
|
|
2041
|
-
io?: IoStore;
|
|
2042
|
-
} = {},
|
|
2043
|
-
) {
|
|
2044
|
-
if (options.io) {
|
|
2045
|
-
return runWithIoContext(options.io, () => dispatch(argv));
|
|
2046
|
-
}
|
|
2047
|
-
return dispatch(argv);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// Run if executed directly (not imported)
|
|
2051
285
|
async function detectMain() {
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
}
|
|
286
|
+
if (!process.argv[1]) return false;
|
|
287
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) return true;
|
|
288
|
+
try {
|
|
289
|
+
const { realpath } = await import("node:fs/promises");
|
|
290
|
+
const resolved = await realpath(process.argv[1]);
|
|
291
|
+
return import.meta.url === pathToFileURL(resolved).href;
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
2062
295
|
}
|
|
2063
296
|
|
|
2064
297
|
if (await detectMain()) {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
298
|
+
main(process.argv.slice(2))
|
|
299
|
+
.then((code) => process.exit(code))
|
|
300
|
+
.catch((err) => {
|
|
301
|
+
console.error(c.red(`Fatal: ${err.message}`));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
});
|
|
2071
304
|
}
|