opencode-copilot-account-switcher 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js ADDED
@@ -0,0 +1,707 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { isTTY } from "./ui/ansi";
4
+ import { showAccountActions, showMenu } from "./ui/menu";
5
+ import { authPath, readAuth, readStore, writeStore } from "./store";
6
+ function now() {
7
+ return Date.now();
8
+ }
9
+ function getGitHubToken(entry) {
10
+ // Prefer access token if it looks like a GitHub access token
11
+ if (entry.access && (entry.access.startsWith("ghu_") || entry.access.startsWith("gho_") || entry.access.startsWith("ghp_") || entry.access.startsWith("github_pat_"))) {
12
+ return entry.access;
13
+ }
14
+ // Fallback to refresh if it looks like a GitHub access token
15
+ if (entry.refresh && (entry.refresh.startsWith("ghu_") || entry.refresh.startsWith("gho_") || entry.refresh.startsWith("ghp_") || entry.refresh.startsWith("github_pat_"))) {
16
+ return entry.refresh;
17
+ }
18
+ // If refresh is a refresh token (ghr_), and access is not, it's safer to try access
19
+ if (entry.refresh?.startsWith("ghr_") && entry.access && !entry.access.startsWith("ghr_")) {
20
+ return entry.access;
21
+ }
22
+ // Fallback to refresh then access
23
+ return entry.refresh || entry.access;
24
+ }
25
+ const CLIENT_ID = "Ov23li8tweQw6odWQebz";
26
+ const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
27
+ function normalizeDomain(url) {
28
+ return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
29
+ }
30
+ function getUrls(domain) {
31
+ return {
32
+ DEVICE_CODE_URL: `https://${domain}/login/device/code`,
33
+ ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
34
+ };
35
+ }
36
+ async function sleep(ms) {
37
+ await new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+ function toInfo(name, entry, index, active) {
40
+ const status = entry.expires && entry.expires > 0 && entry.expires < now() ? "expired" : "active";
41
+ const labelName = name.startsWith("github.com:") ? name.slice("github.com:".length) : name;
42
+ const hasUser = entry.user ? labelName.includes(entry.user) : false;
43
+ const hasEmail = entry.email ? labelName.includes(entry.email) : false;
44
+ const suffix = entry.user
45
+ ? hasUser
46
+ ? ""
47
+ : ` (${entry.user})`
48
+ : entry.email
49
+ ? ` (${entry.email})`
50
+ : "";
51
+ const label = `${labelName}${suffix}`;
52
+ return {
53
+ name: label,
54
+ index,
55
+ addedAt: entry.addedAt,
56
+ lastUsed: entry.lastUsed,
57
+ status,
58
+ isCurrent: active === name,
59
+ };
60
+ }
61
+ async function promptText(message) {
62
+ const rl = createInterface({ input, output });
63
+ try {
64
+ const answer = await rl.question(message);
65
+ return answer.trim();
66
+ }
67
+ finally {
68
+ rl.close();
69
+ }
70
+ }
71
+ async function promptAccountName(existing) {
72
+ while (true) {
73
+ const name = await promptText("Account name: ");
74
+ if (!name)
75
+ continue;
76
+ if (!existing.includes(name))
77
+ return name;
78
+ console.log(`Name already exists: ${name}`);
79
+ }
80
+ }
81
+ async function promptAccountEntry(existing) {
82
+ const name = await promptAccountName(existing);
83
+ const refresh = await promptText("OAuth refresh/access token: ");
84
+ const access = await promptText("Copilot access token (optional, press Enter to skip): ");
85
+ const expiresRaw = await promptText("Access token expires (unix ms, optional): ");
86
+ const enterpriseUrl = await promptText("Enterprise URL (optional): ");
87
+ const expires = Number(expiresRaw);
88
+ const entry = {
89
+ name,
90
+ refresh,
91
+ access: access || refresh,
92
+ expires: Number.isFinite(expires) ? expires : 0,
93
+ enterpriseUrl: enterpriseUrl || undefined,
94
+ addedAt: now(),
95
+ source: "manual",
96
+ };
97
+ return { name, entry };
98
+ }
99
+ async function loginOauth(deployment, enterpriseUrl) {
100
+ const domain = deployment === "enterprise" ? normalizeDomain(enterpriseUrl ?? "") : "github.com";
101
+ const urls = getUrls(domain);
102
+ const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
103
+ method: "POST",
104
+ headers: {
105
+ Accept: "application/json",
106
+ "Content-Type": "application/json",
107
+ },
108
+ body: JSON.stringify({
109
+ client_id: CLIENT_ID,
110
+ scope: "read:user user:email",
111
+ }),
112
+ });
113
+ if (!deviceResponse.ok)
114
+ throw new Error("Failed to initiate device authorization");
115
+ const deviceData = (await deviceResponse.json());
116
+ console.log(`Go to: ${deviceData.verification_uri}`);
117
+ console.log(`Enter code: ${deviceData.user_code}`);
118
+ while (true) {
119
+ const response = await fetch(urls.ACCESS_TOKEN_URL, {
120
+ method: "POST",
121
+ headers: {
122
+ Accept: "application/json",
123
+ "Content-Type": "application/json",
124
+ },
125
+ body: JSON.stringify({
126
+ client_id: CLIENT_ID,
127
+ device_code: deviceData.device_code,
128
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
129
+ }),
130
+ });
131
+ if (!response.ok)
132
+ throw new Error("Failed to poll token");
133
+ const data = (await response.json());
134
+ if (data.access_token) {
135
+ const entry = {
136
+ name: deployment === "enterprise" ? `enterprise:${domain}` : "github.com",
137
+ refresh: data.access_token,
138
+ access: data.access_token,
139
+ expires: 0,
140
+ enterpriseUrl: deployment === "enterprise" ? domain : undefined,
141
+ addedAt: now(),
142
+ source: "auth",
143
+ };
144
+ const user = await fetchUser(entry);
145
+ if (user?.login)
146
+ entry.user = user.login;
147
+ if (user?.email)
148
+ entry.email = user.email;
149
+ if (user?.orgs?.length)
150
+ entry.orgs = user.orgs;
151
+ return entry;
152
+ }
153
+ if (data.error === "authorization_pending") {
154
+ await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS);
155
+ continue;
156
+ }
157
+ if (data.error === "slow_down") {
158
+ const serverInterval = data.interval;
159
+ const next = (serverInterval && serverInterval > 0 ? serverInterval : deviceData.interval + 5) * 1000;
160
+ await sleep(next + OAUTH_POLLING_SAFETY_MARGIN_MS);
161
+ continue;
162
+ }
163
+ throw new Error("Authorization failed");
164
+ }
165
+ }
166
+ async function promptFilePath(message, defaultValue) {
167
+ const rl = createInterface({ input, output });
168
+ try {
169
+ const answer = await rl.question(`${message} (${defaultValue}): `);
170
+ return answer.trim() || defaultValue;
171
+ }
172
+ finally {
173
+ rl.close();
174
+ }
175
+ }
176
+ function buildSnapshot(raw) {
177
+ if (!raw)
178
+ return undefined;
179
+ const entitlement = raw.entitlement;
180
+ const remaining = raw.remaining;
181
+ const used = raw.used ??
182
+ (entitlement !== undefined && remaining !== undefined ? entitlement - remaining : undefined);
183
+ const percentRemaining = raw.percent_remaining;
184
+ return {
185
+ entitlement,
186
+ remaining,
187
+ used,
188
+ unlimited: raw.unlimited,
189
+ percentRemaining,
190
+ };
191
+ }
192
+ function buildName(entry, login) {
193
+ const user = login ?? entry.user;
194
+ if (!user)
195
+ return entry.name;
196
+ if (!entry.enterpriseUrl)
197
+ return user;
198
+ const host = normalizeDomain(entry.enterpriseUrl);
199
+ return `${host}:${user}`;
200
+ }
201
+ function score(entry) {
202
+ return (entry.user ? 2 : 0) + (entry.email ? 2 : 0) + (entry.orgs?.length ? 1 : 0);
203
+ }
204
+ function key(entry) {
205
+ if (entry.refresh)
206
+ return `refresh:${entry.refresh}`;
207
+ return undefined;
208
+ }
209
+ function dedupe(store) {
210
+ const seen = new Map();
211
+ for (const [name, entry] of Object.entries(store.accounts)) {
212
+ const k = key(entry);
213
+ if (!k)
214
+ continue;
215
+ const current = seen.get(k);
216
+ if (!current) {
217
+ seen.set(k, name);
218
+ continue;
219
+ }
220
+ const currentEntry = store.accounts[current];
221
+ if (score(entry) > score(currentEntry)) {
222
+ delete store.accounts[current];
223
+ seen.set(k, name);
224
+ if (store.active === current)
225
+ store.active = name;
226
+ continue;
227
+ }
228
+ delete store.accounts[name];
229
+ if (store.active === name)
230
+ store.active = current;
231
+ }
232
+ }
233
+ function mergeAuth(store, imported) {
234
+ dedupe(store);
235
+ const byRefresh = new Map();
236
+ for (const [name, entry] of Object.entries(store.accounts)) {
237
+ if (entry.refresh)
238
+ byRefresh.set(entry.refresh, name);
239
+ }
240
+ for (const [key, entry] of imported) {
241
+ const match = byRefresh.get(entry.refresh);
242
+ if (match) {
243
+ store.accounts[match] = {
244
+ ...store.accounts[match],
245
+ ...entry,
246
+ name: store.accounts[match].name,
247
+ source: "auth",
248
+ providerId: key,
249
+ };
250
+ if (!store.active)
251
+ store.active = match;
252
+ continue;
253
+ }
254
+ const name = entry.name || `auth:${key}`;
255
+ store.accounts[name] = {
256
+ ...entry,
257
+ name,
258
+ source: "auth",
259
+ providerId: key,
260
+ };
261
+ if (!store.active)
262
+ store.active = name;
263
+ }
264
+ }
265
+ function renameAccounts(store, items) {
266
+ const counts = new Map();
267
+ const renamed = items.map((item) => {
268
+ const count = (counts.get(item.base) ?? 0) + 1;
269
+ counts.set(item.base, count);
270
+ const name = count === 1 ? item.base : `${item.base}#${count}`;
271
+ return { ...item, name, entry: { ...item.entry, name } };
272
+ });
273
+ store.accounts = renamed.reduce((acc, item) => {
274
+ acc[item.name] = item.entry;
275
+ return acc;
276
+ }, {});
277
+ const active = renamed.find((item) => item.oldName === store.active);
278
+ if (active)
279
+ store.active = active.name;
280
+ }
281
+ async function refreshIdentity(store) {
282
+ const items = await Promise.all(Object.entries(store.accounts).map(async ([name, entry]) => {
283
+ const user = await fetchUser(entry);
284
+ const base = buildName(entry, user?.login ?? entry.user);
285
+ return {
286
+ oldName: name,
287
+ base,
288
+ entry: {
289
+ ...entry,
290
+ user: user?.login ?? entry.user,
291
+ email: user?.email ?? entry.email,
292
+ orgs: user?.orgs ?? entry.orgs,
293
+ name: base,
294
+ },
295
+ };
296
+ }));
297
+ renameAccounts(store, items);
298
+ }
299
+ async function fetchQuota(entry) {
300
+ try {
301
+ const headers = {
302
+ Accept: "application/json",
303
+ Authorization: `token ${getGitHubToken(entry)}`,
304
+ "User-Agent": "GitHubCopilotChat/0.26.7",
305
+ "Editor-Version": "vscode/1.96.2",
306
+ "Copilot-Integration-Id": "vscode-chat",
307
+ "X-Github-Api-Version": "2025-04-01",
308
+ };
309
+ const base = entry.enterpriseUrl ? `https://api.${normalizeDomain(entry.enterpriseUrl)}` : "https://api.github.com";
310
+ const quotaRes = await fetch(`${base}/copilot_internal/user`, { headers });
311
+ if (!quotaRes.ok) {
312
+ return { error: `quota ${quotaRes.status}` };
313
+ }
314
+ const quotaData = (await quotaRes.json());
315
+ return {
316
+ sku: quotaData.access_type_sku,
317
+ plan: quotaData.copilot_plan,
318
+ reset: quotaData.quota_reset_date,
319
+ updatedAt: now(),
320
+ snapshots: {
321
+ premium: buildSnapshot(quotaData.quota_snapshots?.premium_interactions),
322
+ chat: buildSnapshot(quotaData.quota_snapshots?.chat),
323
+ completions: buildSnapshot(quotaData.quota_snapshots?.completions),
324
+ },
325
+ };
326
+ }
327
+ catch (error) {
328
+ return { error: error instanceof Error ? error.message : String(error) };
329
+ }
330
+ }
331
+ async function fetchModels(entry) {
332
+ try {
333
+ const headers = {
334
+ Accept: "application/json",
335
+ Authorization: `Bearer ${entry.access}`,
336
+ "User-Agent": "GitHubCopilotChat/0.26.7",
337
+ "Editor-Version": "vscode/1.96.2",
338
+ "Editor-Plugin-Version": "copilot/1.159.0",
339
+ "Copilot-Integration-Id": "vscode-chat",
340
+ "X-Github-Api-Version": "2025-04-01",
341
+ };
342
+ const modelsUrl = entry.enterpriseUrl
343
+ ? `https://copilot-api.${normalizeDomain(entry.enterpriseUrl)}/models`
344
+ : "https://api.githubcopilot.com/models";
345
+ const modelRes = await fetch(modelsUrl, { headers });
346
+ if (!modelRes.ok) {
347
+ const base = entry.enterpriseUrl ? `https://api.${normalizeDomain(entry.enterpriseUrl)}` : "https://api.github.com";
348
+ const tokenRes = await fetch(`${base}/copilot_internal/v2/token`, {
349
+ headers: {
350
+ Accept: "application/json",
351
+ Authorization: `token ${getGitHubToken(entry)}`,
352
+ "User-Agent": "GitHubCopilotChat/0.26.7",
353
+ "Editor-Version": "vscode/1.96.2",
354
+ "Editor-Plugin-Version": "copilot/1.159.0",
355
+ "X-Github-Api-Version": "2025-04-01",
356
+ },
357
+ });
358
+ if (!tokenRes.ok)
359
+ return { available: [], disabled: [], error: `token ${tokenRes.status}` };
360
+ const tokenData = (await tokenRes.json());
361
+ if (!tokenData.token)
362
+ return { available: [], disabled: [], error: "token missing" };
363
+ // Update entry with new session token
364
+ entry.access = tokenData.token;
365
+ if (tokenData.expires_at)
366
+ entry.expires = tokenData.expires_at * 1000;
367
+ const fallbackRes = await fetch(modelsUrl, {
368
+ headers: {
369
+ Accept: "application/json",
370
+ Authorization: `Bearer ${tokenData.token}`,
371
+ "User-Agent": "GitHubCopilotChat/0.26.7",
372
+ "Editor-Version": "vscode/1.96.2",
373
+ "Editor-Plugin-Version": "copilot/1.159.0",
374
+ "Copilot-Integration-Id": "vscode-chat",
375
+ "X-Github-Api-Version": "2025-04-01",
376
+ },
377
+ });
378
+ if (!fallbackRes.ok)
379
+ return { available: [], disabled: [], error: `models ${fallbackRes.status}` };
380
+ return parseModels((await fallbackRes.json()));
381
+ }
382
+ return parseModels((await modelRes.json()));
383
+ }
384
+ catch (error) {
385
+ return { available: [], disabled: [], error: error instanceof Error ? error.message : String(error) };
386
+ }
387
+ }
388
+ function parseModels(modelData) {
389
+ const available = [];
390
+ const disabled = [];
391
+ for (const item of modelData.data ?? []) {
392
+ if (!item.id)
393
+ continue;
394
+ const enabled = item.model_picker_enabled === true && item.policy?.state !== "disabled";
395
+ if (enabled)
396
+ available.push(item.id);
397
+ else
398
+ disabled.push(item.id);
399
+ }
400
+ return { available, disabled, updatedAt: now() };
401
+ }
402
+ async function fetchUser(entry) {
403
+ try {
404
+ const base = entry.enterpriseUrl ? `https://api.${normalizeDomain(entry.enterpriseUrl)}` : "https://api.github.com";
405
+ const headers = {
406
+ Accept: "application/json",
407
+ Authorization: `token ${getGitHubToken(entry)}`,
408
+ "User-Agent": "GitHubCopilotChat/0.26.7",
409
+ };
410
+ const userRes = await fetch(`${base}/user`, { headers });
411
+ if (!userRes.ok)
412
+ return undefined;
413
+ const user = (await userRes.json());
414
+ let email = user.email;
415
+ if (!email) {
416
+ const emailRes = await fetch(`${base}/user/emails`, { headers });
417
+ if (emailRes.ok) {
418
+ const items = (await emailRes.json());
419
+ const primary = items.find((item) => item.primary && item.verified);
420
+ email = primary?.email ?? items[0]?.email;
421
+ }
422
+ }
423
+ const orgRes = await fetch(`${base}/user/orgs`, { headers });
424
+ const orgs = orgRes.ok ? (await orgRes.json()).map((o) => o.login).filter(Boolean) : undefined;
425
+ return { login: user.login, email, orgs };
426
+ }
427
+ catch {
428
+ return undefined;
429
+ }
430
+ }
431
+ async function switchAccount(client, entry) {
432
+ const payload = {
433
+ type: "oauth",
434
+ refresh: entry.refresh,
435
+ access: entry.access,
436
+ expires: entry.expires,
437
+ ...(entry.enterpriseUrl ? { enterpriseUrl: entry.enterpriseUrl } : {}),
438
+ };
439
+ await client.auth.set({
440
+ path: { id: entry.enterpriseUrl ? "github-copilot-enterprise" : "github-copilot" },
441
+ body: payload,
442
+ });
443
+ }
444
+ export const CopilotAccountSwitcher = async ({ client }) => {
445
+ async function runMenu() {
446
+ const store = await readStore();
447
+ const auth = await readAuth().catch(() => ({}));
448
+ const imported = Object.entries(auth).filter(([key]) => key === "github-copilot" || key === "github-copilot-enterprise");
449
+ if (imported.length > 0) {
450
+ mergeAuth(store, imported);
451
+ const preferred = imported.find(([key]) => key === "github-copilot") ?? imported[0];
452
+ if (!store.active)
453
+ store.active = preferred?.[1].name;
454
+ }
455
+ if (!Object.entries(store.accounts).length) {
456
+ const { name, entry } = await promptAccountEntry([]);
457
+ store.accounts[name] = entry;
458
+ store.active = name;
459
+ await writeStore(store);
460
+ await switchAccount(client, entry);
461
+ // fallthrough to menu
462
+ }
463
+ if (!Object.values(store.accounts).some((entry) => entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))) {
464
+ await refreshIdentity(store);
465
+ dedupe(store);
466
+ await writeStore(store);
467
+ }
468
+ if (!isTTY()) {
469
+ console.log("Interactive menu requires a TTY terminal");
470
+ return;
471
+ }
472
+ let nextRefresh = 0;
473
+ while (true) {
474
+ if (store.autoRefresh === true && now() >= nextRefresh) {
475
+ const updated = await Promise.all(Object.entries(store.accounts).map(async ([name, entry]) => ({
476
+ name,
477
+ entry: {
478
+ ...entry,
479
+ quota: await fetchQuota(entry),
480
+ },
481
+ })));
482
+ for (const item of updated) {
483
+ store.accounts[item.name] = item.entry;
484
+ }
485
+ store.lastQuotaRefresh = now();
486
+ await writeStore(store);
487
+ nextRefresh = now() + (store.refreshMinutes ?? 15) * 60_000;
488
+ }
489
+ const entries = Object.entries(store.accounts);
490
+ const refreshed = await Promise.all(entries.map(async ([name, entry]) => {
491
+ if (entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))
492
+ return { name, entry };
493
+ const user = await fetchUser(entry);
494
+ return {
495
+ name,
496
+ entry: {
497
+ ...entry,
498
+ user: user?.login ?? entry.user,
499
+ email: user?.email ?? entry.email,
500
+ orgs: user?.orgs ?? entry.orgs,
501
+ },
502
+ };
503
+ }));
504
+ for (const item of refreshed) {
505
+ store.accounts[item.name] = item.entry;
506
+ }
507
+ const accounts = entries.map(([name, entry], index) => ({
508
+ ...toInfo(name, entry, index, store.active),
509
+ source: entry.source,
510
+ orgs: entry.orgs,
511
+ plan: entry.quota?.plan,
512
+ sku: entry.quota?.sku,
513
+ reset: entry.quota?.reset,
514
+ models: entry.models
515
+ ? {
516
+ enabled: entry.models.available.length,
517
+ disabled: entry.models.disabled.length,
518
+ }
519
+ : undefined,
520
+ modelsError: entry.models?.error,
521
+ modelList: entry.models
522
+ ? {
523
+ available: entry.models.available,
524
+ disabled: entry.models.disabled,
525
+ }
526
+ : undefined,
527
+ quota: entry.quota?.snapshots
528
+ ? {
529
+ premium: entry.quota.snapshots.premium,
530
+ chat: entry.quota.snapshots.chat,
531
+ completions: entry.quota.snapshots.completions,
532
+ }
533
+ : undefined,
534
+ }));
535
+ const refresh = { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 };
536
+ const action = await showMenu(accounts, refresh, store.lastQuotaRefresh);
537
+ if (action.type === "cancel") {
538
+ const active = store.active ? store.accounts[store.active] : undefined;
539
+ return active;
540
+ }
541
+ if (action.type === "add") {
542
+ const mode = await promptText("Add via device login? (y/n): ");
543
+ const useDevice = mode.toLowerCase() === "y" || mode.toLowerCase() === "yes";
544
+ if (useDevice) {
545
+ const dep = await promptText("Enterprise? (y/n): ");
546
+ const isEnterprise = dep.toLowerCase() === "y" || dep.toLowerCase() === "yes";
547
+ const domain = isEnterprise ? await promptText("Enterprise URL or domain: ") : undefined;
548
+ const entry = await loginOauth(isEnterprise ? "enterprise" : "github.com", domain);
549
+ const user = await fetchUser(entry);
550
+ if (user?.login)
551
+ entry.user = user.login;
552
+ if (user?.email)
553
+ entry.email = user.email;
554
+ if (user?.orgs?.length)
555
+ entry.orgs = user.orgs;
556
+ entry.name = buildName(entry, user?.login);
557
+ store.accounts[entry.name] = entry;
558
+ store.active = store.active ?? entry.name;
559
+ await writeStore(store);
560
+ if (store.active === entry.name)
561
+ await switchAccount(client, entry);
562
+ continue;
563
+ }
564
+ const manual = await promptAccountEntry(Object.keys(store.accounts));
565
+ const user = await fetchUser(manual.entry);
566
+ if (user?.login)
567
+ manual.entry.user = user.login;
568
+ if (user?.email)
569
+ manual.entry.email = user.email;
570
+ if (user?.orgs?.length)
571
+ manual.entry.orgs = user.orgs;
572
+ manual.entry.name = buildName(manual.entry, user?.login);
573
+ store.accounts[manual.entry.name] = manual.entry;
574
+ store.active = store.active ?? manual.entry.name;
575
+ await writeStore(store);
576
+ if (store.active === manual.entry.name)
577
+ await switchAccount(client, manual.entry);
578
+ continue;
579
+ }
580
+ if (action.type === "import") {
581
+ const file = await promptFilePath("auth.json path", authPath());
582
+ const auth = await readAuth(file).catch(() => ({}));
583
+ const imported = Object.entries(auth).filter(([key]) => key === "github-copilot" || key === "github-copilot-enterprise");
584
+ for (const [key, entry] of imported) {
585
+ const user = await fetchUser(entry);
586
+ if (user?.login)
587
+ entry.user = user.login;
588
+ if (user?.email)
589
+ entry.email = user.email;
590
+ if (user?.orgs?.length)
591
+ entry.orgs = user.orgs;
592
+ entry.name = buildName(entry, user?.login);
593
+ }
594
+ mergeAuth(store, imported);
595
+ await writeStore(store);
596
+ continue;
597
+ }
598
+ if (action.type === "refresh-identity") {
599
+ await refreshIdentity(store);
600
+ dedupe(store);
601
+ await writeStore(store);
602
+ continue;
603
+ }
604
+ if (action.type === "toggle-refresh") {
605
+ store.autoRefresh = !store.autoRefresh;
606
+ store.refreshMinutes = store.refreshMinutes ?? 15;
607
+ await writeStore(store);
608
+ continue;
609
+ }
610
+ if (action.type === "set-interval") {
611
+ const value = await promptText("Refresh interval (minutes): ");
612
+ const minutes = Math.max(1, Math.min(180, Number(value)));
613
+ if (Number.isFinite(minutes))
614
+ store.refreshMinutes = minutes;
615
+ await writeStore(store);
616
+ continue;
617
+ }
618
+ if (action.type === "quota") {
619
+ const updated = await Promise.all(entries.map(async ([name, entry]) => ({
620
+ name,
621
+ entry: {
622
+ ...entry,
623
+ quota: await fetchQuota(entry),
624
+ },
625
+ })));
626
+ for (const item of updated) {
627
+ store.accounts[item.name] = item.entry;
628
+ }
629
+ store.lastQuotaRefresh = now();
630
+ await writeStore(store);
631
+ continue;
632
+ }
633
+ if (action.type === "check-models") {
634
+ const updated = await Promise.all(entries.map(async ([name, entry]) => ({
635
+ name,
636
+ entry: {
637
+ ...entry,
638
+ models: await fetchModels(entry),
639
+ },
640
+ })));
641
+ for (const item of updated) {
642
+ store.accounts[item.name] = item.entry;
643
+ }
644
+ await writeStore(store);
645
+ continue;
646
+ }
647
+ if (action.type === "remove-all") {
648
+ store.accounts = {};
649
+ store.active = undefined;
650
+ await writeStore(store);
651
+ continue;
652
+ }
653
+ if (action.type === "switch") {
654
+ const selected = entries[action.account.index];
655
+ if (!selected)
656
+ continue;
657
+ const [name, entry] = selected;
658
+ const decision = await showAccountActions(action.account);
659
+ if (decision === "back")
660
+ continue;
661
+ if (decision === "remove") {
662
+ delete store.accounts[name];
663
+ if (store.active === name)
664
+ store.active = undefined;
665
+ await writeStore(store);
666
+ continue;
667
+ }
668
+ await switchAccount(client, entry);
669
+ store.active = name;
670
+ store.accounts[name].lastUsed = now();
671
+ await writeStore(store);
672
+ continue;
673
+ }
674
+ }
675
+ }
676
+ return {
677
+ auth: {
678
+ provider: "github-copilot",
679
+ methods: [
680
+ {
681
+ type: "oauth",
682
+ label: "Manage GitHub Copilot accounts",
683
+ async authorize() {
684
+ const entry = await runMenu();
685
+ return {
686
+ url: "",
687
+ instructions: "",
688
+ method: "auto",
689
+ async callback() {
690
+ if (!entry)
691
+ return { type: "failed" };
692
+ return {
693
+ type: "success",
694
+ provider: entry.enterpriseUrl ? "github-copilot-enterprise" : "github-copilot",
695
+ refresh: entry.refresh,
696
+ access: entry.access,
697
+ expires: entry.expires,
698
+ ...(entry.enterpriseUrl ? { enterpriseUrl: entry.enterpriseUrl } : {}),
699
+ };
700
+ },
701
+ };
702
+ },
703
+ },
704
+ ],
705
+ },
706
+ };
707
+ };