@bjesuiter/codex-switcher 1.0.3 → 1.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/README.md +8 -0
- package/cdx.mjs +625 -16
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
CLI tool to switch between multiple OpenAI accounts for [OpenCode](https://opencode.ai).
|
|
4
4
|
|
|
5
|
+
## Why codex-switcher?
|
|
6
|
+
|
|
7
|
+
Anthropic has a $100/month plan, but OpenAI only offers $20 and $200 plans.
|
|
8
|
+
So: switching between two $20 plans is the poor man's $100 plan for OpenAI. ^^
|
|
9
|
+
|
|
5
10
|
## Supported Configurations
|
|
6
11
|
|
|
7
12
|
- **OpenAI Plus & Pro subscription accounts**: Log in to multiple OpenAI accounts via OAuth and switch the active auth credentials used by OpenCode.
|
|
@@ -86,6 +91,9 @@ Running `cdx` without arguments opens an interactive menu to:
|
|
|
86
91
|
| `cdx switch <id>` | Switch to specific account |
|
|
87
92
|
| `cdx label` | Label an account (interactive) |
|
|
88
93
|
| `cdx label <account> <label>` | Assign label directly |
|
|
94
|
+
| `cdx status` | Show account status, token expiry, and usage |
|
|
95
|
+
| `cdx usage` | Show usage overview for all accounts |
|
|
96
|
+
| `cdx usage <account>` | Show detailed usage for a specific account |
|
|
89
97
|
| `cdx --help` | Show help |
|
|
90
98
|
| `cdx --version` | Show version |
|
|
91
99
|
|
package/cdx.mjs
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import
|
|
4
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
7
|
import * as p from "@clack/prompts";
|
|
9
8
|
import { spawn } from "node:child_process";
|
|
10
9
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
11
10
|
import { randomBytes } from "node:crypto";
|
|
12
11
|
import http from "node:http";
|
|
13
12
|
|
|
13
|
+
//#region package.json
|
|
14
|
+
var version = "1.1.0";
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
14
17
|
//#region lib/paths.ts
|
|
15
18
|
const defaultConfigDir = path.join(os.homedir(), ".config", "cdx");
|
|
16
19
|
const defaultPaths = {
|
|
17
20
|
configDir: defaultConfigDir,
|
|
18
21
|
configPath: path.join(defaultConfigDir, "accounts.json"),
|
|
19
|
-
authPath: path.join(os.homedir(), ".local", "share", "opencode", "auth.json")
|
|
22
|
+
authPath: path.join(os.homedir(), ".local", "share", "opencode", "auth.json"),
|
|
23
|
+
codexAuthPath: path.join(os.homedir(), ".codex", "auth.json")
|
|
20
24
|
};
|
|
21
25
|
let currentPaths = { ...defaultPaths };
|
|
22
26
|
const getPaths = () => currentPaths;
|
|
@@ -33,22 +37,72 @@ const resetPaths = () => {
|
|
|
33
37
|
const createTestPaths = (testDir) => ({
|
|
34
38
|
configDir: path.join(testDir, "config"),
|
|
35
39
|
configPath: path.join(testDir, "config", "accounts.json"),
|
|
36
|
-
authPath: path.join(testDir, "auth", "auth.json")
|
|
40
|
+
authPath: path.join(testDir, "auth", "auth.json"),
|
|
41
|
+
codexAuthPath: path.join(testDir, "codex", "auth.json")
|
|
37
42
|
});
|
|
38
43
|
|
|
39
44
|
//#endregion
|
|
40
45
|
//#region lib/auth.ts
|
|
46
|
+
const readExistingJson = async (filePath) => {
|
|
47
|
+
if (!existsSync(filePath)) return {};
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(filePath, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
41
56
|
const writeAuthFile = async (payload) => {
|
|
42
57
|
const { authPath } = getPaths();
|
|
43
58
|
await mkdir(path.dirname(authPath), { recursive: true });
|
|
44
|
-
const
|
|
59
|
+
const existing = await readExistingJson(authPath);
|
|
60
|
+
existing.openai = {
|
|
45
61
|
type: "oauth",
|
|
46
62
|
refresh: payload.refresh,
|
|
47
63
|
access: payload.access,
|
|
48
64
|
expires: payload.expires,
|
|
49
65
|
accountId: payload.accountId
|
|
50
|
-
}
|
|
51
|
-
await writeFile(authPath, JSON.stringify(
|
|
66
|
+
};
|
|
67
|
+
await writeFile(authPath, JSON.stringify(existing, null, 2), "utf8");
|
|
68
|
+
};
|
|
69
|
+
const writeCodexAuthFile = async (payload) => {
|
|
70
|
+
const { codexAuthPath } = getPaths();
|
|
71
|
+
await mkdir(path.dirname(codexAuthPath), { recursive: true });
|
|
72
|
+
const existing = await readExistingJson(codexAuthPath);
|
|
73
|
+
existing.tokens = {
|
|
74
|
+
...typeof existing.tokens === "object" && existing.tokens !== null ? existing.tokens : {},
|
|
75
|
+
id_token: payload.idToken ?? null,
|
|
76
|
+
access_token: payload.access,
|
|
77
|
+
refresh_token: payload.refresh,
|
|
78
|
+
account_id: payload.accountId
|
|
79
|
+
};
|
|
80
|
+
existing.last_refresh = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
|
+
await writeFile(codexAuthPath, JSON.stringify(existing, null, 2), "utf8");
|
|
82
|
+
};
|
|
83
|
+
const writeAllAuthFiles = async (payload) => {
|
|
84
|
+
await writeAuthFile(payload);
|
|
85
|
+
if (payload.idToken) {
|
|
86
|
+
await writeCodexAuthFile(payload);
|
|
87
|
+
return {
|
|
88
|
+
codexWritten: true,
|
|
89
|
+
codexMissingIdToken: false,
|
|
90
|
+
codexCleared: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const { codexAuthPath } = getPaths();
|
|
94
|
+
let codexCleared = false;
|
|
95
|
+
if (existsSync(codexAuthPath)) try {
|
|
96
|
+
await rm(codexAuthPath);
|
|
97
|
+
codexCleared = true;
|
|
98
|
+
} catch {
|
|
99
|
+
codexCleared = false;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
codexWritten: false,
|
|
103
|
+
codexMissingIdToken: true,
|
|
104
|
+
codexCleared
|
|
105
|
+
};
|
|
52
106
|
};
|
|
53
107
|
|
|
54
108
|
//#endregion
|
|
@@ -207,9 +261,35 @@ const exchangeAuthorizationCode = async (code, verifier) => {
|
|
|
207
261
|
type: "success",
|
|
208
262
|
access: json.access_token,
|
|
209
263
|
refresh: json.refresh_token,
|
|
210
|
-
expires: Date.now() + json.expires_in * 1e3
|
|
264
|
+
expires: Date.now() + json.expires_in * 1e3,
|
|
265
|
+
idToken: json.id_token
|
|
211
266
|
};
|
|
212
267
|
};
|
|
268
|
+
const refreshAccessToken = async (refreshToken) => {
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(TOKEN_URL, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
273
|
+
body: new URLSearchParams({
|
|
274
|
+
grant_type: "refresh_token",
|
|
275
|
+
refresh_token: refreshToken,
|
|
276
|
+
client_id: CLIENT_ID
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
if (!response.ok) return { type: "failed" };
|
|
280
|
+
const json = await response.json();
|
|
281
|
+
if (!json?.access_token || !json?.refresh_token || typeof json?.expires_in !== "number") return { type: "failed" };
|
|
282
|
+
return {
|
|
283
|
+
type: "success",
|
|
284
|
+
access: json.access_token,
|
|
285
|
+
refresh: json.refresh_token,
|
|
286
|
+
expires: Date.now() + json.expires_in * 1e3,
|
|
287
|
+
idToken: json.id_token
|
|
288
|
+
};
|
|
289
|
+
} catch {
|
|
290
|
+
return { type: "failed" };
|
|
291
|
+
}
|
|
292
|
+
};
|
|
213
293
|
const decodeJWT = (token) => {
|
|
214
294
|
try {
|
|
215
295
|
const parts = token.split(".");
|
|
@@ -350,6 +430,60 @@ const addAccountToConfig = async (accountId, label) => {
|
|
|
350
430
|
};
|
|
351
431
|
await saveConfig(config);
|
|
352
432
|
};
|
|
433
|
+
const performRefresh = async (targetAccountId, label) => {
|
|
434
|
+
const displayName = label ?? targetAccountId;
|
|
435
|
+
p.log.step(`Refreshing credentials for "${displayName}"...`);
|
|
436
|
+
let flow;
|
|
437
|
+
try {
|
|
438
|
+
flow = await createAuthorizationFlow();
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
441
|
+
p.log.error(`Failed to create authorization flow: ${msg}`);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const server = await startOAuthServer(flow.state);
|
|
445
|
+
if (!server.ready) {
|
|
446
|
+
p.log.error("Failed to start local server on port 1455.");
|
|
447
|
+
p.log.info("Please ensure the port is not in use.");
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const spinner = p.spinner();
|
|
451
|
+
p.log.info("Opening browser for authentication...");
|
|
452
|
+
openBrowser(flow.url);
|
|
453
|
+
spinner.start("Waiting for authentication...");
|
|
454
|
+
const result = await server.waitForCode();
|
|
455
|
+
server.close();
|
|
456
|
+
if (!result) {
|
|
457
|
+
spinner.stop("Authentication timed out or failed.");
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
spinner.message("Exchanging authorization code...");
|
|
461
|
+
const tokenResult = await exchangeAuthorizationCode(result.code, flow.pkce.verifier);
|
|
462
|
+
if (tokenResult.type === "failed") {
|
|
463
|
+
spinner.stop("Failed to exchange authorization code.");
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
const newAccountId = extractAccountId(tokenResult.access);
|
|
467
|
+
if (!newAccountId) {
|
|
468
|
+
spinner.stop("Failed to extract account ID from token.");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
if (newAccountId !== targetAccountId) {
|
|
472
|
+
spinner.stop(`Account mismatch: expected "${targetAccountId}" but got "${newAccountId}". Make sure you log in with the correct OpenAI account.`);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
spinner.message("Updating credentials...");
|
|
476
|
+
saveKeychainPayload(newAccountId, {
|
|
477
|
+
refresh: tokenResult.refresh,
|
|
478
|
+
access: tokenResult.access,
|
|
479
|
+
expires: tokenResult.expires,
|
|
480
|
+
accountId: newAccountId,
|
|
481
|
+
...tokenResult.idToken ? { idToken: tokenResult.idToken } : {}
|
|
482
|
+
});
|
|
483
|
+
spinner.stop("Credentials refreshed!");
|
|
484
|
+
p.log.success(`Account "${displayName}" credentials updated in Keychain.`);
|
|
485
|
+
return { accountId: newAccountId };
|
|
486
|
+
};
|
|
353
487
|
const performLogin = async () => {
|
|
354
488
|
p.intro("cdx login - Add OpenAI account");
|
|
355
489
|
const flow = await createAuthorizationFlow();
|
|
@@ -385,7 +519,8 @@ const performLogin = async () => {
|
|
|
385
519
|
refresh: tokenResult.refresh,
|
|
386
520
|
access: tokenResult.access,
|
|
387
521
|
expires: tokenResult.expires,
|
|
388
|
-
accountId
|
|
522
|
+
accountId,
|
|
523
|
+
...tokenResult.idToken ? { idToken: tokenResult.idToken } : {}
|
|
389
524
|
});
|
|
390
525
|
spinner.stop("Login successful!");
|
|
391
526
|
const labelInput = await p.text({
|
|
@@ -400,6 +535,115 @@ const performLogin = async () => {
|
|
|
400
535
|
return { accountId };
|
|
401
536
|
};
|
|
402
537
|
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region lib/refresh.ts
|
|
540
|
+
const writeActiveAuthFilesIfCurrent = async (accountId) => {
|
|
541
|
+
if (!configExists()) return null;
|
|
542
|
+
const config = await loadConfig();
|
|
543
|
+
const current = config.accounts[config.current];
|
|
544
|
+
if (!current || current.accountId !== accountId) return null;
|
|
545
|
+
return writeAllAuthFiles(loadKeychainPayload(accountId));
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region lib/status.ts
|
|
550
|
+
const formatDuration = (ms) => {
|
|
551
|
+
const absMs = Math.abs(ms);
|
|
552
|
+
const seconds = Math.floor(absMs / 1e3);
|
|
553
|
+
const minutes = Math.floor(seconds / 60);
|
|
554
|
+
const hours = Math.floor(minutes / 60);
|
|
555
|
+
const days = Math.floor(hours / 24);
|
|
556
|
+
if (days > 0) {
|
|
557
|
+
const remainingHours = hours % 24;
|
|
558
|
+
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
559
|
+
}
|
|
560
|
+
if (hours > 0) {
|
|
561
|
+
const remainingMinutes = minutes % 60;
|
|
562
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
563
|
+
}
|
|
564
|
+
if (minutes > 0) return `${minutes}m`;
|
|
565
|
+
return `${seconds}s`;
|
|
566
|
+
};
|
|
567
|
+
const formatExpiry = (expiresAt) => {
|
|
568
|
+
if (expiresAt === null) return "unknown";
|
|
569
|
+
const remaining = expiresAt - Date.now();
|
|
570
|
+
if (remaining <= 0) return `EXPIRED ${formatDuration(remaining)} ago`;
|
|
571
|
+
return `expires in ${formatDuration(remaining)}`;
|
|
572
|
+
};
|
|
573
|
+
const readOpenCodeAuthAccount = async () => {
|
|
574
|
+
const { authPath } = getPaths();
|
|
575
|
+
if (!existsSync(authPath)) return {
|
|
576
|
+
exists: false,
|
|
577
|
+
accountId: null
|
|
578
|
+
};
|
|
579
|
+
try {
|
|
580
|
+
const raw = await readFile(authPath, "utf8");
|
|
581
|
+
return {
|
|
582
|
+
exists: true,
|
|
583
|
+
accountId: JSON.parse(raw).openai?.accountId ?? null
|
|
584
|
+
};
|
|
585
|
+
} catch {
|
|
586
|
+
return {
|
|
587
|
+
exists: true,
|
|
588
|
+
accountId: null
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const readCodexAuthAccount = async () => {
|
|
593
|
+
const { codexAuthPath } = getPaths();
|
|
594
|
+
if (!existsSync(codexAuthPath)) return {
|
|
595
|
+
exists: false,
|
|
596
|
+
accountId: null
|
|
597
|
+
};
|
|
598
|
+
try {
|
|
599
|
+
const raw = await readFile(codexAuthPath, "utf8");
|
|
600
|
+
return {
|
|
601
|
+
exists: true,
|
|
602
|
+
accountId: JSON.parse(raw).tokens?.account_id ?? null
|
|
603
|
+
};
|
|
604
|
+
} catch {
|
|
605
|
+
return {
|
|
606
|
+
exists: true,
|
|
607
|
+
accountId: null
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const getAccountStatus = (accountId, isCurrent, label) => {
|
|
612
|
+
const keychainExists = keychainPayloadExists(accountId);
|
|
613
|
+
let expiresAt = null;
|
|
614
|
+
let hasIdToken = false;
|
|
615
|
+
if (keychainExists) try {
|
|
616
|
+
const payload = loadKeychainPayload(accountId);
|
|
617
|
+
expiresAt = payload.expires;
|
|
618
|
+
hasIdToken = !!payload.idToken;
|
|
619
|
+
} catch {}
|
|
620
|
+
return {
|
|
621
|
+
accountId,
|
|
622
|
+
label,
|
|
623
|
+
isCurrent,
|
|
624
|
+
keychainExists,
|
|
625
|
+
hasIdToken,
|
|
626
|
+
expiresAt,
|
|
627
|
+
expiresIn: formatExpiry(expiresAt)
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
const getStatus = async () => {
|
|
631
|
+
const accounts = [];
|
|
632
|
+
if (configExists()) {
|
|
633
|
+
const config = await loadConfig();
|
|
634
|
+
for (let i = 0; i < config.accounts.length; i++) {
|
|
635
|
+
const account = config.accounts[i];
|
|
636
|
+
accounts.push(getAccountStatus(account.accountId, i === config.current, account.label));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const [opencodeAuth, codexAuth] = await Promise.all([readOpenCodeAuthAccount(), readCodexAuthAccount()]);
|
|
640
|
+
return {
|
|
641
|
+
accounts,
|
|
642
|
+
opencodeAuth,
|
|
643
|
+
codexAuth
|
|
644
|
+
};
|
|
645
|
+
};
|
|
646
|
+
|
|
403
647
|
//#endregion
|
|
404
648
|
//#region lib/interactive.ts
|
|
405
649
|
const getAccountDisplay = (accountId, isCurrent, label) => {
|
|
@@ -453,15 +697,68 @@ const handleSwitchAccount = async () => {
|
|
|
453
697
|
p.log.error("Invalid selection.");
|
|
454
698
|
return;
|
|
455
699
|
}
|
|
456
|
-
|
|
700
|
+
let payload;
|
|
701
|
+
try {
|
|
702
|
+
payload = loadKeychainPayload(selectedAccount.accountId);
|
|
703
|
+
} catch {
|
|
704
|
+
p.log.error(`Missing credentials for account ${selectedAccount.label ?? selectedAccount.accountId}. Re-login with 'cdx login'.`);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const result = await writeAllAuthFiles(payload);
|
|
457
708
|
config.current = selected;
|
|
458
709
|
await saveConfig(config);
|
|
459
710
|
const displayName = selectedAccount.label ?? selectedAccount.accountId;
|
|
711
|
+
const opencodeMark = "✓";
|
|
712
|
+
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
460
713
|
p.log.success(`Switched to account ${displayName}`);
|
|
714
|
+
p.log.message(` OpenCode: ${opencodeMark}`);
|
|
715
|
+
p.log.message(` Codex CLI: ${codexMark}`);
|
|
461
716
|
};
|
|
462
717
|
const handleAddAccount = async () => {
|
|
463
718
|
await performLogin();
|
|
464
719
|
};
|
|
720
|
+
const handleRefreshAccount = async () => {
|
|
721
|
+
if (!configExists()) {
|
|
722
|
+
p.log.warning("No accounts configured. Use 'Add account' first.");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const config = await loadConfig();
|
|
726
|
+
if (config.accounts.length === 0) {
|
|
727
|
+
p.log.warning("No accounts to refresh.");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const currentAccountId = config.accounts[config.current]?.accountId;
|
|
731
|
+
const options = config.accounts.map((account) => ({
|
|
732
|
+
value: account.accountId,
|
|
733
|
+
label: getAccountDisplay(account.accountId, account.accountId === currentAccountId, account.label)
|
|
734
|
+
}));
|
|
735
|
+
const selected = await p.select({
|
|
736
|
+
message: "Select account to refresh:",
|
|
737
|
+
options
|
|
738
|
+
});
|
|
739
|
+
if (p.isCancel(selected)) {
|
|
740
|
+
p.log.info("Cancelled.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const accountId = selected;
|
|
744
|
+
const account = config.accounts.find((a) => a.accountId === accountId);
|
|
745
|
+
try {
|
|
746
|
+
const result = await performRefresh(accountId, account?.label);
|
|
747
|
+
if (!result) p.log.warning("Refresh was not completed.");
|
|
748
|
+
else {
|
|
749
|
+
const authResult = await writeActiveAuthFilesIfCurrent(result.accountId);
|
|
750
|
+
if (authResult) {
|
|
751
|
+
const codexMark = authResult.codexWritten ? "✓" : authResult.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
752
|
+
p.log.message("Updated active auth files:");
|
|
753
|
+
p.log.message(" OpenCode: ✓");
|
|
754
|
+
p.log.message(` Codex CLI: ${codexMark}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
759
|
+
p.log.error(`Refresh failed: ${msg}`);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
465
762
|
const handleRemoveAccount = async () => {
|
|
466
763
|
if (!configExists()) {
|
|
467
764
|
p.log.warning("No accounts configured.");
|
|
@@ -549,6 +846,26 @@ const handleLabelAccount = async () => {
|
|
|
549
846
|
if (newLabel) p.log.success(`Account ${accountId} labeled as "${newLabel}".`);
|
|
550
847
|
else p.log.success(`Label removed from account ${accountId}.`);
|
|
551
848
|
};
|
|
849
|
+
const handleStatus = async () => {
|
|
850
|
+
const status = await getStatus();
|
|
851
|
+
if (status.accounts.length === 0) {
|
|
852
|
+
p.log.warning("No accounts configured. Use 'Add account' to get started.");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
p.log.info("Account status:");
|
|
856
|
+
for (const account of status.accounts) {
|
|
857
|
+
const marker = account.isCurrent ? "→ " : " ";
|
|
858
|
+
const name = account.label ? `${account.label} (${account.accountId})` : account.accountId;
|
|
859
|
+
const keychain = account.keychainExists ? "" : " [no keychain]";
|
|
860
|
+
const idToken = account.hasIdToken ? "" : " [no id_token]";
|
|
861
|
+
p.log.message(`${marker}${name} — ${account.expiresIn}${keychain}${idToken}`);
|
|
862
|
+
}
|
|
863
|
+
const ocStatus = status.opencodeAuth.exists ? `active: ${status.opencodeAuth.accountId ?? "unknown"}` : "not found";
|
|
864
|
+
const cxStatus = status.codexAuth.exists ? `active: ${status.codexAuth.accountId ?? "unknown"}` : "not found";
|
|
865
|
+
p.log.info(`Auth files:`);
|
|
866
|
+
p.log.message(` OpenCode: ${ocStatus}`);
|
|
867
|
+
p.log.message(` Codex CLI: ${cxStatus}`);
|
|
868
|
+
};
|
|
552
869
|
const runInteractiveMode = async () => {
|
|
553
870
|
p.intro("cdx - OpenAI Account Switcher");
|
|
554
871
|
let running = true;
|
|
@@ -575,6 +892,10 @@ const runInteractiveMode = async () => {
|
|
|
575
892
|
value: "add",
|
|
576
893
|
label: "Add account (OAuth login)"
|
|
577
894
|
},
|
|
895
|
+
{
|
|
896
|
+
value: "refresh",
|
|
897
|
+
label: "Refresh account (re-login)"
|
|
898
|
+
},
|
|
578
899
|
{
|
|
579
900
|
value: "remove",
|
|
580
901
|
label: "Remove account"
|
|
@@ -583,6 +904,10 @@ const runInteractiveMode = async () => {
|
|
|
583
904
|
value: "label",
|
|
584
905
|
label: "Label account"
|
|
585
906
|
},
|
|
907
|
+
{
|
|
908
|
+
value: "status",
|
|
909
|
+
label: "Account status & token expiry"
|
|
910
|
+
},
|
|
586
911
|
{
|
|
587
912
|
value: "exit",
|
|
588
913
|
label: "Exit"
|
|
@@ -603,12 +928,18 @@ const runInteractiveMode = async () => {
|
|
|
603
928
|
case "add":
|
|
604
929
|
await handleAddAccount();
|
|
605
930
|
break;
|
|
931
|
+
case "refresh":
|
|
932
|
+
await handleRefreshAccount();
|
|
933
|
+
break;
|
|
606
934
|
case "remove":
|
|
607
935
|
await handleRemoveAccount();
|
|
608
936
|
break;
|
|
609
937
|
case "label":
|
|
610
938
|
await handleLabelAccount();
|
|
611
939
|
break;
|
|
940
|
+
case "status":
|
|
941
|
+
await handleStatus();
|
|
942
|
+
break;
|
|
612
943
|
case "exit":
|
|
613
944
|
running = false;
|
|
614
945
|
break;
|
|
@@ -618,6 +949,162 @@ const runInteractiveMode = async () => {
|
|
|
618
949
|
p.outro("Goodbye!");
|
|
619
950
|
};
|
|
620
951
|
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region lib/usage.ts
|
|
954
|
+
const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
|
|
955
|
+
const USER_AGENT = "cdx-cli";
|
|
956
|
+
/**
|
|
957
|
+
* Hits the undocumented OpenAI usage endpoint (may change without notice).
|
|
958
|
+
*/
|
|
959
|
+
const fetchUsageRaw = async (accessToken, accountId) => {
|
|
960
|
+
const headers = {
|
|
961
|
+
Authorization: `Bearer ${accessToken}`,
|
|
962
|
+
"User-Agent": USER_AGENT,
|
|
963
|
+
Accept: "application/json"
|
|
964
|
+
};
|
|
965
|
+
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
|
966
|
+
return fetch(USAGE_ENDPOINT, { headers });
|
|
967
|
+
};
|
|
968
|
+
/**
|
|
969
|
+
* Fetches usage for an account. On 401, refreshes the token and retries once.
|
|
970
|
+
*/
|
|
971
|
+
const fetchUsage = async (accountId) => {
|
|
972
|
+
let payload;
|
|
973
|
+
try {
|
|
974
|
+
payload = loadKeychainPayload(accountId);
|
|
975
|
+
} catch (err) {
|
|
976
|
+
return {
|
|
977
|
+
ok: false,
|
|
978
|
+
error: {
|
|
979
|
+
type: "auth_failed",
|
|
980
|
+
message: err instanceof Error ? err.message : "Failed to load credentials"
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
let response = await fetchUsageRaw(payload.access, payload.accountId);
|
|
986
|
+
if (response.status === 401) {
|
|
987
|
+
const refreshResult = await refreshAccessToken(payload.refresh);
|
|
988
|
+
if (refreshResult.type === "failed") return {
|
|
989
|
+
ok: false,
|
|
990
|
+
error: {
|
|
991
|
+
type: "auth_failed",
|
|
992
|
+
message: "Token expired and refresh failed. Try 'cdx login' to re-authenticate."
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
const updatedPayload = {
|
|
996
|
+
...payload,
|
|
997
|
+
access: refreshResult.access,
|
|
998
|
+
refresh: refreshResult.refresh,
|
|
999
|
+
expires: refreshResult.expires,
|
|
1000
|
+
idToken: refreshResult.idToken ?? payload.idToken
|
|
1001
|
+
};
|
|
1002
|
+
saveKeychainPayload(accountId, updatedPayload);
|
|
1003
|
+
response = await fetchUsageRaw(updatedPayload.access, updatedPayload.accountId);
|
|
1004
|
+
if (!response.ok) return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
error: {
|
|
1007
|
+
type: "auth_failed",
|
|
1008
|
+
message: `Usage API returned ${response.status} after token refresh.`
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
} else if (!response.ok) return {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
error: {
|
|
1014
|
+
type: "unexpected",
|
|
1015
|
+
message: `Usage API returned ${response.status}: ${response.statusText}`
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
ok: true,
|
|
1020
|
+
data: await response.json()
|
|
1021
|
+
};
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
return {
|
|
1024
|
+
ok: false,
|
|
1025
|
+
error: {
|
|
1026
|
+
type: "network_error",
|
|
1027
|
+
message: err instanceof Error ? err.message : "Network request failed"
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
const formatWindowLabel = (seconds) => {
|
|
1033
|
+
const hours = seconds / 3600;
|
|
1034
|
+
if (hours >= 24) {
|
|
1035
|
+
const days = Math.round(hours / 24);
|
|
1036
|
+
return days === 7 ? "weekly" : `${days}d`;
|
|
1037
|
+
}
|
|
1038
|
+
return `${Math.round(hours)}h`;
|
|
1039
|
+
};
|
|
1040
|
+
const formatResetCountdown = (resetAtUnix) => {
|
|
1041
|
+
const diff = resetAtUnix * 1e3 - Date.now();
|
|
1042
|
+
if (diff <= 0) return "now";
|
|
1043
|
+
const minutes = Math.floor(diff / 6e4);
|
|
1044
|
+
const hours = Math.floor(minutes / 60);
|
|
1045
|
+
const remainingMinutes = minutes % 60;
|
|
1046
|
+
if (hours > 0) return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
1047
|
+
return `${minutes}m`;
|
|
1048
|
+
};
|
|
1049
|
+
const formatPercentageBar = (usedPercent) => {
|
|
1050
|
+
const width = 20;
|
|
1051
|
+
const filled = Math.round(usedPercent / 100 * width);
|
|
1052
|
+
const empty = width - filled;
|
|
1053
|
+
return `[${"█".repeat(filled)}${"░".repeat(empty)}] ${usedPercent}% used`;
|
|
1054
|
+
};
|
|
1055
|
+
const formatWindow = (label, w) => {
|
|
1056
|
+
return [
|
|
1057
|
+
`${label} (${formatWindowLabel(w.limit_window_seconds)} window):`,
|
|
1058
|
+
` ${formatPercentageBar(w.used_percent)}`,
|
|
1059
|
+
` Resets in: ${formatResetCountdown(w.reset_at)}`
|
|
1060
|
+
];
|
|
1061
|
+
};
|
|
1062
|
+
const formatUsage = (usage) => {
|
|
1063
|
+
const lines = [];
|
|
1064
|
+
const plan = usage.plan_type ?? "unknown";
|
|
1065
|
+
lines.push(`Plan: ${plan}`);
|
|
1066
|
+
lines.push("");
|
|
1067
|
+
if (usage.rate_limit?.primary_window) lines.push(...formatWindow("Primary", usage.rate_limit.primary_window));
|
|
1068
|
+
if (usage.rate_limit?.secondary_window) lines.push(...formatWindow("Secondary", usage.rate_limit.secondary_window));
|
|
1069
|
+
if (usage.credits) {
|
|
1070
|
+
lines.push("");
|
|
1071
|
+
if (usage.credits.unlimited) lines.push("Credits: unlimited");
|
|
1072
|
+
else if (usage.credits.has_credits && usage.credits.balance !== void 0) lines.push(`Credits: $${Number(usage.credits.balance).toFixed(2)}`);
|
|
1073
|
+
else if (!usage.credits.has_credits) lines.push("Credits: none");
|
|
1074
|
+
}
|
|
1075
|
+
return lines.join("\n");
|
|
1076
|
+
};
|
|
1077
|
+
const formatUsageBars = (usage, indent = " ") => {
|
|
1078
|
+
const windows = [];
|
|
1079
|
+
if (usage.rate_limit?.primary_window) windows.push({
|
|
1080
|
+
label: formatWindowLabel(usage.rate_limit.primary_window.limit_window_seconds),
|
|
1081
|
+
window: usage.rate_limit.primary_window
|
|
1082
|
+
});
|
|
1083
|
+
if (usage.rate_limit?.secondary_window) windows.push({
|
|
1084
|
+
label: formatWindowLabel(usage.rate_limit.secondary_window.limit_window_seconds),
|
|
1085
|
+
window: usage.rate_limit.secondary_window
|
|
1086
|
+
});
|
|
1087
|
+
const maxLabelLen = Math.max(...windows.map((w) => w.label.length), 0);
|
|
1088
|
+
return windows.map(({ label, window: w }) => {
|
|
1089
|
+
return `${indent}${label.padEnd(maxLabelLen)} ${formatPercentageBar(w.used_percent)} resets in ${formatResetCountdown(w.reset_at)}`;
|
|
1090
|
+
});
|
|
1091
|
+
};
|
|
1092
|
+
const formatUsageOverview = (entries) => {
|
|
1093
|
+
const lines = [];
|
|
1094
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1095
|
+
const entry = entries[i];
|
|
1096
|
+
const marker = entry.isCurrent ? "→ " : " ";
|
|
1097
|
+
if (entry.result.ok) {
|
|
1098
|
+
const usage = entry.result.data;
|
|
1099
|
+
const plan = usage.plan_type ?? "unknown";
|
|
1100
|
+
lines.push(`${marker}${entry.displayName} (${plan})`);
|
|
1101
|
+
lines.push(...formatUsageBars(usage));
|
|
1102
|
+
} else lines.push(`${marker}${entry.displayName}: [error] ${entry.result.error.message}`);
|
|
1103
|
+
if (i < entries.length - 1) lines.push("");
|
|
1104
|
+
}
|
|
1105
|
+
return lines.join("\n");
|
|
1106
|
+
};
|
|
1107
|
+
|
|
621
1108
|
//#endregion
|
|
622
1109
|
//#region cdx.ts
|
|
623
1110
|
const switchNext = async () => {
|
|
@@ -626,28 +1113,36 @@ const switchNext = async () => {
|
|
|
626
1113
|
const nextAccount = config.accounts[nextIndex];
|
|
627
1114
|
if (!nextAccount?.accountId) throw new Error("Account entry missing accountId.");
|
|
628
1115
|
const payload = loadKeychainPayload(nextAccount.accountId);
|
|
629
|
-
await
|
|
1116
|
+
const result = await writeAllAuthFiles(payload);
|
|
630
1117
|
config.current = nextIndex;
|
|
631
1118
|
await saveConfig(config);
|
|
632
1119
|
const displayName = nextAccount.label ?? payload.accountId;
|
|
1120
|
+
const opencodeMark = "✓";
|
|
1121
|
+
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
633
1122
|
process.stdout.write(`Switched to account ${displayName}\n`);
|
|
1123
|
+
process.stdout.write(` OpenCode: ${opencodeMark}\n`);
|
|
1124
|
+
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
634
1125
|
};
|
|
635
1126
|
const switchToAccount = async (identifier) => {
|
|
636
1127
|
const config = await loadConfig();
|
|
637
1128
|
const index = config.accounts.findIndex((a) => a.accountId === identifier || a.label === identifier);
|
|
638
1129
|
if (index === -1) throw new Error(`Account "${identifier}" not found. Use 'cdx login' to add it.`);
|
|
639
1130
|
const account = config.accounts[index];
|
|
640
|
-
await
|
|
1131
|
+
const result = await writeAllAuthFiles(loadKeychainPayload(account.accountId));
|
|
641
1132
|
config.current = index;
|
|
642
1133
|
await saveConfig(config);
|
|
643
1134
|
const displayName = account.label ?? account.accountId;
|
|
1135
|
+
const opencodeMark = "✓";
|
|
1136
|
+
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
644
1137
|
process.stdout.write(`Switched to account ${displayName}\n`);
|
|
1138
|
+
process.stdout.write(` OpenCode: ${opencodeMark}\n`);
|
|
1139
|
+
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
645
1140
|
};
|
|
646
1141
|
const interactiveMode = runInteractiveMode;
|
|
647
1142
|
const createProgram = (deps = {}) => {
|
|
648
1143
|
const program = new Command();
|
|
649
1144
|
const runLogin = deps.performLogin ?? performLogin;
|
|
650
|
-
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(
|
|
1145
|
+
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(version, "-v, --version");
|
|
651
1146
|
program.command("login").description("Add a new OpenAI account via OAuth").action(async () => {
|
|
652
1147
|
try {
|
|
653
1148
|
if (!await runLogin()) {
|
|
@@ -660,6 +1155,30 @@ const createProgram = (deps = {}) => {
|
|
|
660
1155
|
process.exit(1);
|
|
661
1156
|
}
|
|
662
1157
|
});
|
|
1158
|
+
program.command("refresh").description("Re-authenticate an existing account (update tokens without creating a duplicate)").argument("[account]", "Account ID or label to refresh").action(async (account) => {
|
|
1159
|
+
try {
|
|
1160
|
+
if (account) {
|
|
1161
|
+
const target = (await loadConfig()).accounts.find((a) => a.accountId === account || a.label === account);
|
|
1162
|
+
if (!target) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1163
|
+
const result = await performRefresh(target.accountId, target.label);
|
|
1164
|
+
if (!result) {
|
|
1165
|
+
process.stderr.write("Refresh failed.\n");
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
const authResult = await writeActiveAuthFilesIfCurrent(result.accountId);
|
|
1169
|
+
if (authResult) {
|
|
1170
|
+
const codexMark = authResult.codexWritten ? "✓" : authResult.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
1171
|
+
process.stdout.write("Updated active auth files:\n");
|
|
1172
|
+
process.stdout.write(" OpenCode: ✓\n");
|
|
1173
|
+
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
1174
|
+
}
|
|
1175
|
+
} else await handleRefreshAccount();
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1178
|
+
process.stderr.write(`${message}\n`);
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
663
1182
|
program.command("switch").description("Switch OpenAI account (interactive picker, by name, or --next)").argument("[account-id]", "Account ID to switch to directly").option("-n, --next", "Cycle to the next configured account").action(async (accountId, options) => {
|
|
664
1183
|
try {
|
|
665
1184
|
if (options.next) await switchNext();
|
|
@@ -687,8 +1206,98 @@ const createProgram = (deps = {}) => {
|
|
|
687
1206
|
process.exit(1);
|
|
688
1207
|
}
|
|
689
1208
|
});
|
|
1209
|
+
program.command("status").description("Show account status, token expiry, and auth file state").action(async () => {
|
|
1210
|
+
try {
|
|
1211
|
+
const status = await getStatus();
|
|
1212
|
+
if (status.accounts.length === 0) {
|
|
1213
|
+
process.stdout.write("No accounts configured. Use 'cdx login' to add one.\n");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
process.stdout.write("\n");
|
|
1217
|
+
for (let i = 0; i < status.accounts.length; i++) {
|
|
1218
|
+
const account = status.accounts[i];
|
|
1219
|
+
const marker = account.isCurrent ? "→ " : " ";
|
|
1220
|
+
const warnings = [];
|
|
1221
|
+
if (!account.keychainExists) warnings.push("[no keychain]");
|
|
1222
|
+
if (!account.hasIdToken) warnings.push("[no id_token]");
|
|
1223
|
+
const warnStr = warnings.length > 0 ? ` ${warnings.join(" ")}` : "";
|
|
1224
|
+
const displayName = account.label ?? account.accountId;
|
|
1225
|
+
process.stdout.write(`${marker}${displayName}${warnStr}\n`);
|
|
1226
|
+
if (account.label) process.stdout.write(` ${account.accountId}\n`);
|
|
1227
|
+
process.stdout.write(` ${account.expiresIn}\n`);
|
|
1228
|
+
const usageResult = await fetchUsage(account.accountId);
|
|
1229
|
+
if (usageResult.ok) {
|
|
1230
|
+
const bars = formatUsageBars(usageResult.data);
|
|
1231
|
+
for (const bar of bars) process.stdout.write(`${bar}\n`);
|
|
1232
|
+
}
|
|
1233
|
+
if (i < status.accounts.length - 1) process.stdout.write("\n");
|
|
1234
|
+
}
|
|
1235
|
+
const resolveLabel = (id) => {
|
|
1236
|
+
if (!id) return "unknown";
|
|
1237
|
+
return status.accounts.find((a) => a.accountId === id)?.label ?? id;
|
|
1238
|
+
};
|
|
1239
|
+
process.stdout.write("\nAuth files:\n");
|
|
1240
|
+
const ocStatus = status.opencodeAuth.exists ? `active: ${resolveLabel(status.opencodeAuth.accountId)}` : "not found";
|
|
1241
|
+
process.stdout.write(` OpenCode: ${ocStatus}\n`);
|
|
1242
|
+
const cxStatus = status.codexAuth.exists ? `active: ${resolveLabel(status.codexAuth.accountId)}` : "not found";
|
|
1243
|
+
process.stdout.write(` Codex CLI: ${cxStatus}\n`);
|
|
1244
|
+
process.stdout.write("\n");
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1247
|
+
process.stderr.write(`${message}\n`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
program.command("usage").description("Show OpenAI usage for all accounts (or detailed view for one)").argument("[account]", "Account ID or label (shows detailed single-account view)").action(async (account) => {
|
|
1252
|
+
try {
|
|
1253
|
+
const config = await loadConfig();
|
|
1254
|
+
if (account) {
|
|
1255
|
+
const found = config.accounts.find((a) => a.accountId === account || a.label === account);
|
|
1256
|
+
if (!found) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1257
|
+
const result = await fetchUsage(found.accountId);
|
|
1258
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
1259
|
+
const displayName = found.label ? `${found.label} (${found.accountId})` : found.accountId;
|
|
1260
|
+
process.stdout.write(`\n${displayName}\n${formatUsage(result.data)}\n\n`);
|
|
1261
|
+
} else {
|
|
1262
|
+
if (config.accounts.length === 0) throw new Error("No accounts configured. Use 'cdx login' to add one.");
|
|
1263
|
+
const results = await Promise.allSettled(config.accounts.map((a) => fetchUsage(a.accountId)));
|
|
1264
|
+
const entries = config.accounts.map((a, i) => {
|
|
1265
|
+
const settled = results[i];
|
|
1266
|
+
const displayName = a.label ? `${a.label} (${a.accountId})` : a.accountId;
|
|
1267
|
+
const result = settled.status === "fulfilled" ? settled.value : {
|
|
1268
|
+
ok: false,
|
|
1269
|
+
error: {
|
|
1270
|
+
type: "network_error",
|
|
1271
|
+
message: settled.reason?.message ?? "Fetch failed"
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
return {
|
|
1275
|
+
displayName,
|
|
1276
|
+
isCurrent: i === config.current,
|
|
1277
|
+
result
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
process.stdout.write(`\n${formatUsageOverview(entries)}\n\n`);
|
|
1281
|
+
}
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1284
|
+
process.stderr.write(`${message}\n`);
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
program.command("help").description("Show available commands and usage information").argument("[command]", "Show help for a specific command").action((commandName) => {
|
|
1289
|
+
if (commandName) {
|
|
1290
|
+
const cmd = program.commands.find((c) => c.name() === commandName);
|
|
1291
|
+
if (cmd) cmd.outputHelp();
|
|
1292
|
+
else {
|
|
1293
|
+
process.stderr.write(`Unknown command: ${commandName}\n`);
|
|
1294
|
+
program.outputHelp();
|
|
1295
|
+
process.exit(1);
|
|
1296
|
+
}
|
|
1297
|
+
} else program.outputHelp();
|
|
1298
|
+
});
|
|
690
1299
|
program.command("version").description("Show CLI version").action(() => {
|
|
691
|
-
process.stdout.write(`${
|
|
1300
|
+
process.stdout.write(`${version}\n`);
|
|
692
1301
|
});
|
|
693
1302
|
program.action(async () => {
|
|
694
1303
|
try {
|
|
@@ -711,4 +1320,4 @@ if (import.meta.main) main().catch((error) => {
|
|
|
711
1320
|
});
|
|
712
1321
|
|
|
713
1322
|
//#endregion
|
|
714
|
-
export { createProgram, createTestPaths, getPaths, interactiveMode, loadConfig, resetPaths, runInteractiveMode, saveConfig, setPaths, switchNext, switchToAccount, writeAuthFile };
|
|
1323
|
+
export { createProgram, createTestPaths, getPaths, interactiveMode, loadConfig, resetPaths, runInteractiveMode, saveConfig, setPaths, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bjesuiter/codex-switcher",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^0.11.0",
|
|
24
24
|
"@openauthjs/openauth": "^0.4.3",
|
|
25
|
-
"commander": "^14.0.2"
|
|
26
|
-
"project-version": "^2.0.0"
|
|
25
|
+
"commander": "^14.0.2"
|
|
27
26
|
}
|
|
28
27
|
}
|