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