@bjesuiter/codex-switcher 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/cdx.mjs +390 -231
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
# cdx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.openai.com/codex/cli/) and [opencode](https://opencode.ai/) auth between multiple openAI Plus and Pro accounts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Latest Changes
|
|
8
|
+
|
|
9
|
+
### 1.3.0
|
|
10
|
+
|
|
11
|
+
#### Features
|
|
12
|
+
|
|
13
|
+
- Rename `cdx refresh` command to `cdx relogin`
|
|
14
|
+
|
|
15
|
+
#### Fixes
|
|
16
|
+
|
|
17
|
+
- Fix `cdx relogin` selector flow exiting early after account selection (now continues into OAuth browser login)
|
|
18
|
+
|
|
19
|
+
#### Internal
|
|
20
|
+
|
|
21
|
+
- Modularize CLI command wiring by moving command handlers into per-command modules under `lib/commands/`, keeping `cdx.ts` as a thin composition entrypoint
|
|
22
|
+
- Update package dependencies and lockfile (`@clack/prompts`, `commander`, `tsdown`, `@types/bun`, `@types/node`)
|
|
23
|
+
|
|
24
|
+
see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
---
|
|
4
29
|
|
|
5
30
|
## Why codex-switcher?
|
|
6
31
|
|
|
@@ -9,7 +34,10 @@ So: switching between two $20 plans is the poor man's $100 plan for OpenAI. ^^
|
|
|
9
34
|
|
|
10
35
|
## Supported Configurations
|
|
11
36
|
|
|
12
|
-
- **OpenAI Plus & Pro subscription accounts**: Log in to multiple OpenAI accounts
|
|
37
|
+
- **OpenAI Plus & Pro subscription accounts**: Log in to multiple OpenAI OAuth accounts and switch the active credentials.
|
|
38
|
+
- **OpenCode auth target**: Writes active credentials to `~/.local/share/opencode/auth.json`.
|
|
39
|
+
- **Pi Agent auth target**: Writes active credentials to `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`).
|
|
40
|
+
- **Codex CLI auth target**: Writes active credentials to `~/.codex/auth.json` when `id_token` is available.
|
|
13
41
|
|
|
14
42
|
## Requirements
|
|
15
43
|
|
|
@@ -40,7 +68,10 @@ Opens your browser to authenticate with OpenAI. After successful login, your cre
|
|
|
40
68
|
cdx switch
|
|
41
69
|
```
|
|
42
70
|
|
|
43
|
-
Interactive picker to select an account. Writes credentials to
|
|
71
|
+
Interactive picker to select an account. Writes credentials to:
|
|
72
|
+
- `~/.local/share/opencode/auth.json` (OpenCode)
|
|
73
|
+
- `~/.pi/agent/auth.json` (Pi agent, or `$PI_CODING_AGENT_DIR/auth.json` when `PI_CODING_AGENT_DIR` is set)
|
|
74
|
+
- `~/.codex/auth.json` (Codex CLI; requires `id_token`)
|
|
44
75
|
|
|
45
76
|
```bash
|
|
46
77
|
cdx switch --next
|
|
@@ -86,14 +117,18 @@ Running `cdx` without arguments opens an interactive menu to:
|
|
|
86
117
|
|---------|-------------|
|
|
87
118
|
| `cdx` | Interactive mode |
|
|
88
119
|
| `cdx login` | Add a new OpenAI account via OAuth |
|
|
120
|
+
| `cdx relogin` | Re-authenticate an existing account via OAuth |
|
|
121
|
+
| `cdx relogin <account>` | Re-authenticate a specific account by ID or label |
|
|
89
122
|
| `cdx switch` | Switch account (interactive picker) |
|
|
90
123
|
| `cdx switch --next` | Cycle to next account |
|
|
91
124
|
| `cdx switch <id>` | Switch to specific account |
|
|
92
125
|
| `cdx label` | Label an account (interactive) |
|
|
93
126
|
| `cdx label <account> <label>` | Assign label directly |
|
|
94
|
-
| `cdx status` | Show account status, token expiry, and
|
|
127
|
+
| `cdx status` | Show account status, token expiry, usage, and auth file state |
|
|
95
128
|
| `cdx usage` | Show usage overview for all accounts |
|
|
96
129
|
| `cdx usage <account>` | Show detailed usage for a specific account |
|
|
130
|
+
| `cdx help [command]` | Show help for all commands or one command |
|
|
131
|
+
| `cdx version` | Show CLI version |
|
|
97
132
|
| `cdx --help` | Show help |
|
|
98
133
|
| `cdx --version` | Show version |
|
|
99
134
|
|
|
@@ -101,7 +136,10 @@ Running `cdx` without arguments opens an interactive menu to:
|
|
|
101
136
|
|
|
102
137
|
- OAuth credentials are stored securely in macOS Keychain
|
|
103
138
|
- Account list is stored in `~/.config/cdx/accounts.json`
|
|
104
|
-
- Active account credentials are written to
|
|
139
|
+
- Active account credentials are written to:
|
|
140
|
+
- `~/.local/share/opencode/auth.json`
|
|
141
|
+
- `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`)
|
|
142
|
+
- `~/.codex/auth.json` (when `id_token` exists)
|
|
105
143
|
|
|
106
144
|
## For Developers
|
|
107
145
|
|
package/cdx.mjs
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import os from "node:os";
|
|
7
|
-
import * as p from "@clack/prompts";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
11
|
import http from "node:http";
|
|
12
12
|
|
|
13
13
|
//#region package.json
|
|
14
|
-
var version = "1.
|
|
14
|
+
var version = "1.3.0";
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region lib/commands/errors.ts
|
|
18
|
+
const exitWithCommandError = (error) => {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
process.stderr.write(`${message}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
//#endregion
|
|
17
25
|
//#region lib/paths.ts
|
|
18
26
|
const defaultConfigDir = path.join(os.homedir(), ".config", "cdx");
|
|
19
|
-
const
|
|
27
|
+
const resolvePiAuthPath = () => {
|
|
28
|
+
const piAgentDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
29
|
+
if (piAgentDir) return path.join(piAgentDir, "auth.json");
|
|
30
|
+
return path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
31
|
+
};
|
|
32
|
+
const createDefaultPaths = () => ({
|
|
20
33
|
configDir: defaultConfigDir,
|
|
21
34
|
configPath: path.join(defaultConfigDir, "accounts.json"),
|
|
22
35
|
authPath: path.join(os.homedir(), ".local", "share", "opencode", "auth.json"),
|
|
23
|
-
codexAuthPath: path.join(os.homedir(), ".codex", "auth.json")
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
codexAuthPath: path.join(os.homedir(), ".codex", "auth.json"),
|
|
37
|
+
piAuthPath: resolvePiAuthPath()
|
|
38
|
+
});
|
|
39
|
+
let currentPaths = createDefaultPaths();
|
|
26
40
|
const getPaths = () => currentPaths;
|
|
27
41
|
const setPaths = (paths) => {
|
|
28
42
|
currentPaths = {
|
|
@@ -32,13 +46,14 @@ const setPaths = (paths) => {
|
|
|
32
46
|
if (paths.configDir && !paths.configPath) currentPaths.configPath = path.join(paths.configDir, "accounts.json");
|
|
33
47
|
};
|
|
34
48
|
const resetPaths = () => {
|
|
35
|
-
currentPaths =
|
|
49
|
+
currentPaths = createDefaultPaths();
|
|
36
50
|
};
|
|
37
51
|
const createTestPaths = (testDir) => ({
|
|
38
52
|
configDir: path.join(testDir, "config"),
|
|
39
53
|
configPath: path.join(testDir, "config", "accounts.json"),
|
|
40
54
|
authPath: path.join(testDir, "auth", "auth.json"),
|
|
41
|
-
codexAuthPath: path.join(testDir, "codex", "auth.json")
|
|
55
|
+
codexAuthPath: path.join(testDir, "codex", "auth.json"),
|
|
56
|
+
piAuthPath: path.join(testDir, "pi", "auth.json")
|
|
42
57
|
});
|
|
43
58
|
|
|
44
59
|
//#endregion
|
|
@@ -80,11 +95,26 @@ const writeCodexAuthFile = async (payload) => {
|
|
|
80
95
|
existing.last_refresh = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
96
|
await writeFile(codexAuthPath, JSON.stringify(existing, null, 2), "utf8");
|
|
82
97
|
};
|
|
98
|
+
const writePiAuthFile = async (payload) => {
|
|
99
|
+
const { piAuthPath } = getPaths();
|
|
100
|
+
await mkdir(path.dirname(piAuthPath), { recursive: true });
|
|
101
|
+
const existing = await readExistingJson(piAuthPath);
|
|
102
|
+
existing["openai-codex"] = {
|
|
103
|
+
type: "oauth",
|
|
104
|
+
access: payload.access,
|
|
105
|
+
refresh: payload.refresh,
|
|
106
|
+
expires: payload.expires,
|
|
107
|
+
accountId: payload.accountId
|
|
108
|
+
};
|
|
109
|
+
await writeFile(piAuthPath, JSON.stringify(existing, null, 2), "utf8");
|
|
110
|
+
};
|
|
83
111
|
const writeAllAuthFiles = async (payload) => {
|
|
84
112
|
await writeAuthFile(payload);
|
|
113
|
+
await writePiAuthFile(payload);
|
|
85
114
|
if (payload.idToken) {
|
|
86
115
|
await writeCodexAuthFile(payload);
|
|
87
116
|
return {
|
|
117
|
+
piWritten: true,
|
|
88
118
|
codexWritten: true,
|
|
89
119
|
codexMissingIdToken: false,
|
|
90
120
|
codexCleared: false
|
|
@@ -99,6 +129,7 @@ const writeAllAuthFiles = async (payload) => {
|
|
|
99
129
|
codexCleared = false;
|
|
100
130
|
}
|
|
101
131
|
return {
|
|
132
|
+
piWritten: true,
|
|
102
133
|
codexWritten: false,
|
|
103
134
|
codexMissingIdToken: true,
|
|
104
135
|
codexCleared
|
|
@@ -406,10 +437,16 @@ const startOAuthServer = (state) => {
|
|
|
406
437
|
//#endregion
|
|
407
438
|
//#region lib/oauth/login.ts
|
|
408
439
|
const openBrowser = (url) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
440
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
441
|
+
try {
|
|
442
|
+
spawn(cmd, [url], {
|
|
443
|
+
detached: true,
|
|
444
|
+
stdio: "ignore"
|
|
445
|
+
}).unref();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
448
|
+
p.log.warning(`Could not auto-open browser (${msg}).`);
|
|
449
|
+
}
|
|
413
450
|
};
|
|
414
451
|
const addAccountToConfig = async (accountId, label) => {
|
|
415
452
|
let config;
|
|
@@ -430,59 +467,74 @@ const addAccountToConfig = async (accountId, label) => {
|
|
|
430
467
|
};
|
|
431
468
|
await saveConfig(config);
|
|
432
469
|
};
|
|
433
|
-
const performRefresh = async (targetAccountId, label) => {
|
|
434
|
-
const
|
|
435
|
-
p.log.step(`Refreshing credentials for "${displayName}"...`);
|
|
436
|
-
let flow;
|
|
470
|
+
const performRefresh = async (targetAccountId, label, options = {}) => {
|
|
471
|
+
const keepAlive = setInterval(() => {}, 1e3);
|
|
437
472
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
spinner.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
const displayName = label ?? targetAccountId;
|
|
474
|
+
p.log.step(`Re-authenticating account "${displayName}"...`);
|
|
475
|
+
const useSpinner = options.useSpinner ?? true;
|
|
476
|
+
let flow;
|
|
477
|
+
try {
|
|
478
|
+
flow = await createAuthorizationFlow();
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
481
|
+
p.log.error(`Failed to create authorization flow: ${msg}`);
|
|
482
|
+
process.stderr.write(`Failed to create authorization flow: ${msg}\n`);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const server = await startOAuthServer(flow.state);
|
|
486
|
+
if (!server.ready) {
|
|
487
|
+
p.log.error("Failed to start local server on port 1455.");
|
|
488
|
+
p.log.info("Please ensure the port is not in use.");
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const spinner = useSpinner ? p.spinner() : null;
|
|
492
|
+
p.log.info("Opening browser for authentication...");
|
|
493
|
+
openBrowser(flow.url);
|
|
494
|
+
p.log.message(`If your browser did not open, paste this URL:\n${flow.url}`);
|
|
495
|
+
if (spinner) spinner.start("Waiting for authentication...");
|
|
496
|
+
const result = await server.waitForCode();
|
|
497
|
+
server.close();
|
|
498
|
+
if (!result) {
|
|
499
|
+
if (spinner) spinner.stop("Authentication timed out or failed.");
|
|
500
|
+
else p.log.warning("Authentication timed out or failed.");
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
if (spinner) spinner.message("Exchanging authorization code...");
|
|
504
|
+
else p.log.message("Exchanging authorization code...");
|
|
505
|
+
const tokenResult = await exchangeAuthorizationCode(result.code, flow.pkce.verifier);
|
|
506
|
+
if (tokenResult.type === "failed") {
|
|
507
|
+
if (spinner) spinner.stop("Failed to exchange authorization code.");
|
|
508
|
+
else p.log.error("Failed to exchange authorization code.");
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const newAccountId = extractAccountId(tokenResult.access);
|
|
512
|
+
if (!newAccountId) {
|
|
513
|
+
if (spinner) spinner.stop("Failed to extract account ID from token.");
|
|
514
|
+
else p.log.error("Failed to extract account ID from token.");
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
if (newAccountId !== targetAccountId) {
|
|
518
|
+
if (spinner) spinner.stop("Authentication completed for a different account.");
|
|
519
|
+
else p.log.error("Authentication completed for a different account.");
|
|
520
|
+
throw new Error(`Account mismatch: expected "${targetAccountId}" but got "${newAccountId}". Make sure you log in with the correct OpenAI account.`);
|
|
521
|
+
}
|
|
522
|
+
if (spinner) spinner.message("Updating credentials...");
|
|
523
|
+
else p.log.message("Updating credentials...");
|
|
524
|
+
saveKeychainPayload(newAccountId, {
|
|
525
|
+
refresh: tokenResult.refresh,
|
|
526
|
+
access: tokenResult.access,
|
|
527
|
+
expires: tokenResult.expires,
|
|
528
|
+
accountId: newAccountId,
|
|
529
|
+
...tokenResult.idToken ? { idToken: tokenResult.idToken } : {}
|
|
530
|
+
});
|
|
531
|
+
if (spinner) spinner.stop("Credentials refreshed!");
|
|
532
|
+
else p.log.success("Credentials refreshed!");
|
|
533
|
+
p.log.success(`Account "${displayName}" credentials updated in Keychain.`);
|
|
534
|
+
return { accountId: newAccountId };
|
|
535
|
+
} finally {
|
|
536
|
+
clearInterval(keepAlive);
|
|
474
537
|
}
|
|
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
538
|
};
|
|
487
539
|
const performLogin = async () => {
|
|
488
540
|
p.intro("cdx login - Add OpenAI account");
|
|
@@ -496,6 +548,7 @@ const performLogin = async () => {
|
|
|
496
548
|
const spinner = p.spinner();
|
|
497
549
|
p.log.info("Opening browser for authentication...");
|
|
498
550
|
openBrowser(flow.url);
|
|
551
|
+
p.log.message(`If your browser did not open, paste this URL:\n${flow.url}`);
|
|
499
552
|
spinner.start("Waiting for authentication...");
|
|
500
553
|
const result = await server.waitForCode();
|
|
501
554
|
server.close();
|
|
@@ -608,6 +661,25 @@ const readCodexAuthAccount = async () => {
|
|
|
608
661
|
};
|
|
609
662
|
}
|
|
610
663
|
};
|
|
664
|
+
const readPiAuthAccount = async () => {
|
|
665
|
+
const { piAuthPath } = getPaths();
|
|
666
|
+
if (!existsSync(piAuthPath)) return {
|
|
667
|
+
exists: false,
|
|
668
|
+
accountId: null
|
|
669
|
+
};
|
|
670
|
+
try {
|
|
671
|
+
const raw = await readFile(piAuthPath, "utf8");
|
|
672
|
+
return {
|
|
673
|
+
exists: true,
|
|
674
|
+
accountId: JSON.parse(raw)["openai-codex"]?.accountId ?? null
|
|
675
|
+
};
|
|
676
|
+
} catch {
|
|
677
|
+
return {
|
|
678
|
+
exists: true,
|
|
679
|
+
accountId: null
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
};
|
|
611
683
|
const getAccountStatus = (accountId, isCurrent, label) => {
|
|
612
684
|
const keychainExists = keychainPayloadExists(accountId);
|
|
613
685
|
let expiresAt = null;
|
|
@@ -636,11 +708,16 @@ const getStatus = async () => {
|
|
|
636
708
|
accounts.push(getAccountStatus(account.accountId, i === config.current, account.label));
|
|
637
709
|
}
|
|
638
710
|
}
|
|
639
|
-
const [opencodeAuth, codexAuth] = await Promise.all([
|
|
711
|
+
const [opencodeAuth, codexAuth, piAuth] = await Promise.all([
|
|
712
|
+
readOpenCodeAuthAccount(),
|
|
713
|
+
readCodexAuthAccount(),
|
|
714
|
+
readPiAuthAccount()
|
|
715
|
+
]);
|
|
640
716
|
return {
|
|
641
717
|
accounts,
|
|
642
718
|
opencodeAuth,
|
|
643
|
-
codexAuth
|
|
719
|
+
codexAuth,
|
|
720
|
+
piAuth
|
|
644
721
|
};
|
|
645
722
|
};
|
|
646
723
|
|
|
@@ -650,6 +727,14 @@ const getAccountDisplay = (accountId, isCurrent, label) => {
|
|
|
650
727
|
const name = label ? `${label} (${accountId})` : accountId;
|
|
651
728
|
return isCurrent ? `${name} (current)` : name;
|
|
652
729
|
};
|
|
730
|
+
const getRefreshExpiryState = (accountId) => {
|
|
731
|
+
if (!keychainPayloadExists(accountId)) return "unknown [no keychain]";
|
|
732
|
+
try {
|
|
733
|
+
return formatExpiry(loadKeychainPayload(accountId).expires);
|
|
734
|
+
} catch {
|
|
735
|
+
return "unknown";
|
|
736
|
+
}
|
|
737
|
+
};
|
|
653
738
|
const handleListAccounts = async () => {
|
|
654
739
|
if (!configExists()) {
|
|
655
740
|
p.log.warning("No accounts configured. Use 'Add account' to get started.");
|
|
@@ -709,31 +794,33 @@ const handleSwitchAccount = async () => {
|
|
|
709
794
|
await saveConfig(config);
|
|
710
795
|
const displayName = selectedAccount.label ?? selectedAccount.accountId;
|
|
711
796
|
const opencodeMark = "✓";
|
|
797
|
+
const piMark = result.piWritten ? "✓" : "✗";
|
|
712
798
|
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
713
799
|
p.log.success(`Switched to account ${displayName}`);
|
|
714
800
|
p.log.message(` OpenCode: ${opencodeMark}`);
|
|
801
|
+
p.log.message(` Pi Agent: ${piMark}`);
|
|
715
802
|
p.log.message(` Codex CLI: ${codexMark}`);
|
|
716
803
|
};
|
|
717
804
|
const handleAddAccount = async () => {
|
|
718
805
|
await performLogin();
|
|
719
806
|
};
|
|
720
|
-
const
|
|
807
|
+
const handleReloginAccount = async () => {
|
|
721
808
|
if (!configExists()) {
|
|
722
809
|
p.log.warning("No accounts configured. Use 'Add account' first.");
|
|
723
810
|
return;
|
|
724
811
|
}
|
|
725
812
|
const config = await loadConfig();
|
|
726
813
|
if (config.accounts.length === 0) {
|
|
727
|
-
p.log.warning("No accounts to
|
|
814
|
+
p.log.warning("No accounts to re-login.");
|
|
728
815
|
return;
|
|
729
816
|
}
|
|
730
817
|
const currentAccountId = config.accounts[config.current]?.accountId;
|
|
731
818
|
const options = config.accounts.map((account) => ({
|
|
732
819
|
value: account.accountId,
|
|
733
|
-
label: getAccountDisplay(account.accountId, account.accountId === currentAccountId, account.label)
|
|
820
|
+
label: `${getAccountDisplay(account.accountId, account.accountId === currentAccountId, account.label)} — ${getRefreshExpiryState(account.accountId)}`
|
|
734
821
|
}));
|
|
735
822
|
const selected = await p.select({
|
|
736
|
-
message: "Select account to
|
|
823
|
+
message: "Select account to re-login:",
|
|
737
824
|
options
|
|
738
825
|
});
|
|
739
826
|
if (p.isCancel(selected)) {
|
|
@@ -742,21 +829,26 @@ const handleRefreshAccount = async () => {
|
|
|
742
829
|
}
|
|
743
830
|
const accountId = selected;
|
|
744
831
|
const account = config.accounts.find((a) => a.accountId === accountId);
|
|
832
|
+
const expiryState = getRefreshExpiryState(accountId);
|
|
833
|
+
const displayName = account?.label ?? accountId;
|
|
834
|
+
p.log.info(`Current token status for ${displayName}: ${expiryState}`);
|
|
745
835
|
try {
|
|
746
|
-
const result = await performRefresh(accountId, account?.label);
|
|
747
|
-
if (!result) p.log.warning("
|
|
836
|
+
const result = await performRefresh(accountId, account?.label, { useSpinner: false });
|
|
837
|
+
if (!result) p.log.warning("Re-login was not completed.");
|
|
748
838
|
else {
|
|
749
839
|
const authResult = await writeActiveAuthFilesIfCurrent(result.accountId);
|
|
750
840
|
if (authResult) {
|
|
841
|
+
const piMark = authResult.piWritten ? "✓" : "✗";
|
|
751
842
|
const codexMark = authResult.codexWritten ? "✓" : authResult.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
752
843
|
p.log.message("Updated active auth files:");
|
|
753
844
|
p.log.message(" OpenCode: ✓");
|
|
845
|
+
p.log.message(` Pi Agent: ${piMark}`);
|
|
754
846
|
p.log.message(` Codex CLI: ${codexMark}`);
|
|
755
847
|
}
|
|
756
848
|
}
|
|
757
849
|
} catch (error) {
|
|
758
850
|
const msg = error instanceof Error ? error.message : String(error);
|
|
759
|
-
p.log.error(`
|
|
851
|
+
p.log.error(`Re-login failed: ${msg}`);
|
|
760
852
|
}
|
|
761
853
|
};
|
|
762
854
|
const handleRemoveAccount = async () => {
|
|
@@ -862,9 +954,11 @@ const handleStatus = async () => {
|
|
|
862
954
|
}
|
|
863
955
|
const ocStatus = status.opencodeAuth.exists ? `active: ${status.opencodeAuth.accountId ?? "unknown"}` : "not found";
|
|
864
956
|
const cxStatus = status.codexAuth.exists ? `active: ${status.codexAuth.accountId ?? "unknown"}` : "not found";
|
|
957
|
+
const piStatus = status.piAuth.exists ? `active: ${status.piAuth.accountId ?? "unknown"}` : "not found";
|
|
865
958
|
p.log.info(`Auth files:`);
|
|
866
959
|
p.log.message(` OpenCode: ${ocStatus}`);
|
|
867
960
|
p.log.message(` Codex CLI: ${cxStatus}`);
|
|
961
|
+
p.log.message(` Pi Agent: ${piStatus}`);
|
|
868
962
|
};
|
|
869
963
|
const runInteractiveMode = async () => {
|
|
870
964
|
p.intro("cdx - OpenAI Account Switcher");
|
|
@@ -893,8 +987,8 @@ const runInteractiveMode = async () => {
|
|
|
893
987
|
label: "Add account (OAuth login)"
|
|
894
988
|
},
|
|
895
989
|
{
|
|
896
|
-
value: "
|
|
897
|
-
label: "
|
|
990
|
+
value: "relogin",
|
|
991
|
+
label: "Re-login account"
|
|
898
992
|
},
|
|
899
993
|
{
|
|
900
994
|
value: "remove",
|
|
@@ -928,8 +1022,8 @@ const runInteractiveMode = async () => {
|
|
|
928
1022
|
case "add":
|
|
929
1023
|
await handleAddAccount();
|
|
930
1024
|
break;
|
|
931
|
-
case "
|
|
932
|
-
await
|
|
1025
|
+
case "relogin":
|
|
1026
|
+
await handleReloginAccount();
|
|
933
1027
|
break;
|
|
934
1028
|
case "remove":
|
|
935
1029
|
await handleRemoveAccount();
|
|
@@ -949,6 +1043,131 @@ const runInteractiveMode = async () => {
|
|
|
949
1043
|
p.outro("Goodbye!");
|
|
950
1044
|
};
|
|
951
1045
|
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region lib/commands/interactive.ts
|
|
1048
|
+
const registerDefaultInteractiveAction = (program) => {
|
|
1049
|
+
program.action(async () => {
|
|
1050
|
+
try {
|
|
1051
|
+
await runInteractiveMode();
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
exitWithCommandError(error);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
//#endregion
|
|
1059
|
+
//#region lib/commands/help.ts
|
|
1060
|
+
const registerHelpCommand = (program) => {
|
|
1061
|
+
program.command("help").description("Show available commands and usage information").argument("[command]", "Show help for a specific command").action((commandName) => {
|
|
1062
|
+
if (commandName) {
|
|
1063
|
+
const command = program.commands.find((entry) => entry.name() === commandName);
|
|
1064
|
+
if (command) {
|
|
1065
|
+
command.outputHelp();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
process.stderr.write(`Unknown command: ${commandName}\n`);
|
|
1069
|
+
program.outputHelp();
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
program.outputHelp();
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region lib/commands/label.ts
|
|
1078
|
+
const registerLabelCommand = (program) => {
|
|
1079
|
+
program.command("label").description("Add or change label for an account").argument("[account]", "Account ID or current label to relabel").argument("[new-label]", "New label to assign").action(async (account, newLabel) => {
|
|
1080
|
+
try {
|
|
1081
|
+
if (account && newLabel) {
|
|
1082
|
+
const config = await loadConfig();
|
|
1083
|
+
const target = config.accounts.find((entry) => entry.accountId === account || entry.label === account);
|
|
1084
|
+
if (!target) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1085
|
+
target.label = newLabel;
|
|
1086
|
+
await saveConfig(config);
|
|
1087
|
+
process.stdout.write(`Account ${target.accountId} labeled as "${newLabel}".\n`);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await handleLabelAccount();
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
exitWithCommandError(error);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
//#endregion
|
|
1098
|
+
//#region lib/commands/login.ts
|
|
1099
|
+
const registerLoginCommand = (program, deps = {}) => {
|
|
1100
|
+
const runLogin = deps.performLogin ?? performLogin;
|
|
1101
|
+
program.command("login").description("Add a new OpenAI account via OAuth").action(async () => {
|
|
1102
|
+
try {
|
|
1103
|
+
if (!await runLogin()) {
|
|
1104
|
+
process.stderr.write("Login failed.\n");
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
exitWithCommandError(error);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region lib/commands/output.ts
|
|
1115
|
+
const formatCodexMark = (result) => {
|
|
1116
|
+
if (result.codexWritten) return "✓";
|
|
1117
|
+
if (result.codexCleared) return "⚠ missing id_token (cleared)";
|
|
1118
|
+
return "⚠ missing id_token";
|
|
1119
|
+
};
|
|
1120
|
+
const writeSwitchSummary = (displayName, result) => {
|
|
1121
|
+
const piMark = result.piWritten ? "✓" : "✗";
|
|
1122
|
+
const codexMark = formatCodexMark(result);
|
|
1123
|
+
process.stdout.write(`Switched to account ${displayName}\n`);
|
|
1124
|
+
process.stdout.write(" OpenCode: ✓\n");
|
|
1125
|
+
process.stdout.write(` Pi Agent: ${piMark}\n`);
|
|
1126
|
+
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
1127
|
+
};
|
|
1128
|
+
const writeUpdatedAuthSummary = (result) => {
|
|
1129
|
+
const piMark = result.piWritten ? "✓" : "✗";
|
|
1130
|
+
const codexMark = formatCodexMark(result);
|
|
1131
|
+
process.stdout.write("Updated active auth files:\n");
|
|
1132
|
+
process.stdout.write(" OpenCode: ✓\n");
|
|
1133
|
+
process.stdout.write(` Pi Agent: ${piMark}\n`);
|
|
1134
|
+
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region lib/commands/refresh.ts
|
|
1139
|
+
const registerReloginCommand = (program) => {
|
|
1140
|
+
program.command("relogin").description("Re-authenticate an existing account with full OAuth login (no duplicate account)").argument("[account]", "Account ID or label to re-login").action(async (account) => {
|
|
1141
|
+
try {
|
|
1142
|
+
if (account) {
|
|
1143
|
+
const target = (await loadConfig()).accounts.find((entry) => entry.accountId === account || entry.label === account);
|
|
1144
|
+
if (!target) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1145
|
+
const displayName = target.label ?? target.accountId;
|
|
1146
|
+
let expiryState = "unknown";
|
|
1147
|
+
let keychainState = "";
|
|
1148
|
+
if (keychainPayloadExists(target.accountId)) try {
|
|
1149
|
+
expiryState = formatExpiry(loadKeychainPayload(target.accountId).expires);
|
|
1150
|
+
} catch {
|
|
1151
|
+
expiryState = "unknown";
|
|
1152
|
+
}
|
|
1153
|
+
else keychainState = " [no keychain]";
|
|
1154
|
+
process.stdout.write(`Current token status for ${displayName}: ${expiryState}${keychainState}\n`);
|
|
1155
|
+
const result = await performRefresh(target.accountId, target.label);
|
|
1156
|
+
if (!result) {
|
|
1157
|
+
process.stderr.write("Re-login failed.\n");
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
const authResult = await writeActiveAuthFilesIfCurrent(result.accountId);
|
|
1161
|
+
if (authResult) writeUpdatedAuthSummary(authResult);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
await handleReloginAccount();
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
exitWithCommandError(error);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
|
|
952
1171
|
//#endregion
|
|
953
1172
|
//#region lib/usage.ts
|
|
954
1173
|
const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -1106,107 +1325,9 @@ const formatUsageOverview = (entries) => {
|
|
|
1106
1325
|
};
|
|
1107
1326
|
|
|
1108
1327
|
//#endregion
|
|
1109
|
-
//#region
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
const nextIndex = (config.current + 1) % config.accounts.length;
|
|
1113
|
-
const nextAccount = config.accounts[nextIndex];
|
|
1114
|
-
if (!nextAccount?.accountId) throw new Error("Account entry missing accountId.");
|
|
1115
|
-
const payload = loadKeychainPayload(nextAccount.accountId);
|
|
1116
|
-
const result = await writeAllAuthFiles(payload);
|
|
1117
|
-
config.current = nextIndex;
|
|
1118
|
-
await saveConfig(config);
|
|
1119
|
-
const displayName = nextAccount.label ?? payload.accountId;
|
|
1120
|
-
const opencodeMark = "✓";
|
|
1121
|
-
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
1122
|
-
process.stdout.write(`Switched to account ${displayName}\n`);
|
|
1123
|
-
process.stdout.write(` OpenCode: ${opencodeMark}\n`);
|
|
1124
|
-
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
1125
|
-
};
|
|
1126
|
-
const switchToAccount = async (identifier) => {
|
|
1127
|
-
const config = await loadConfig();
|
|
1128
|
-
const index = config.accounts.findIndex((a) => a.accountId === identifier || a.label === identifier);
|
|
1129
|
-
if (index === -1) throw new Error(`Account "${identifier}" not found. Use 'cdx login' to add it.`);
|
|
1130
|
-
const account = config.accounts[index];
|
|
1131
|
-
const result = await writeAllAuthFiles(loadKeychainPayload(account.accountId));
|
|
1132
|
-
config.current = index;
|
|
1133
|
-
await saveConfig(config);
|
|
1134
|
-
const displayName = account.label ?? account.accountId;
|
|
1135
|
-
const opencodeMark = "✓";
|
|
1136
|
-
const codexMark = result.codexWritten ? "✓" : result.codexCleared ? "⚠ missing id_token (cleared)" : "⚠ missing id_token";
|
|
1137
|
-
process.stdout.write(`Switched to account ${displayName}\n`);
|
|
1138
|
-
process.stdout.write(` OpenCode: ${opencodeMark}\n`);
|
|
1139
|
-
process.stdout.write(` Codex CLI: ${codexMark}\n`);
|
|
1140
|
-
};
|
|
1141
|
-
const interactiveMode = runInteractiveMode;
|
|
1142
|
-
const createProgram = (deps = {}) => {
|
|
1143
|
-
const program = new Command();
|
|
1144
|
-
const runLogin = deps.performLogin ?? performLogin;
|
|
1145
|
-
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(version, "-v, --version");
|
|
1146
|
-
program.command("login").description("Add a new OpenAI account via OAuth").action(async () => {
|
|
1147
|
-
try {
|
|
1148
|
-
if (!await runLogin()) {
|
|
1149
|
-
process.stderr.write("Login failed.\n");
|
|
1150
|
-
process.exit(1);
|
|
1151
|
-
}
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1154
|
-
process.stderr.write(`${message}\n`);
|
|
1155
|
-
process.exit(1);
|
|
1156
|
-
}
|
|
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
|
-
});
|
|
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) => {
|
|
1183
|
-
try {
|
|
1184
|
-
if (options.next) await switchNext();
|
|
1185
|
-
else if (accountId) await switchToAccount(accountId);
|
|
1186
|
-
else await handleSwitchAccount();
|
|
1187
|
-
} catch (error) {
|
|
1188
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1189
|
-
process.stderr.write(`${message}\n`);
|
|
1190
|
-
process.exit(1);
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
program.command("label").description("Add or change label for an account").argument("[account]", "Account ID or current label to relabel").argument("[new-label]", "New label to assign").action(async (account, newLabel) => {
|
|
1194
|
-
try {
|
|
1195
|
-
if (account && newLabel) {
|
|
1196
|
-
const config = await loadConfig();
|
|
1197
|
-
const target = config.accounts.find((a) => a.accountId === account || a.label === account);
|
|
1198
|
-
if (!target) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1199
|
-
target.label = newLabel;
|
|
1200
|
-
await saveConfig(config);
|
|
1201
|
-
process.stdout.write(`Account ${target.accountId} labeled as "${newLabel}".\n`);
|
|
1202
|
-
} else await handleLabelAccount();
|
|
1203
|
-
} catch (error) {
|
|
1204
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1205
|
-
process.stderr.write(`${message}\n`);
|
|
1206
|
-
process.exit(1);
|
|
1207
|
-
}
|
|
1208
|
-
});
|
|
1209
|
-
program.command("status").description("Show account status, token expiry, and auth file state").action(async () => {
|
|
1328
|
+
//#region lib/commands/status.ts
|
|
1329
|
+
const registerStatusCommand = (program) => {
|
|
1330
|
+
program.command("status").description("Show account status, token expiry, usage, and auth file state").action(async () => {
|
|
1210
1331
|
try {
|
|
1211
1332
|
const status = await getStatus();
|
|
1212
1333
|
if (status.accounts.length === 0) {
|
|
@@ -1232,92 +1353,130 @@ const createProgram = (deps = {}) => {
|
|
|
1232
1353
|
}
|
|
1233
1354
|
if (i < status.accounts.length - 1) process.stdout.write("\n");
|
|
1234
1355
|
}
|
|
1235
|
-
const resolveLabel = (
|
|
1236
|
-
if (!
|
|
1237
|
-
return status.accounts.find((
|
|
1356
|
+
const resolveLabel = (accountId) => {
|
|
1357
|
+
if (!accountId) return "unknown";
|
|
1358
|
+
return status.accounts.find((account) => account.accountId === accountId)?.label ?? accountId;
|
|
1238
1359
|
};
|
|
1239
1360
|
process.stdout.write("\nAuth files:\n");
|
|
1240
1361
|
const ocStatus = status.opencodeAuth.exists ? `active: ${resolveLabel(status.opencodeAuth.accountId)}` : "not found";
|
|
1241
1362
|
process.stdout.write(` OpenCode: ${ocStatus}\n`);
|
|
1242
1363
|
const cxStatus = status.codexAuth.exists ? `active: ${resolveLabel(status.codexAuth.accountId)}` : "not found";
|
|
1243
1364
|
process.stdout.write(` Codex CLI: ${cxStatus}\n`);
|
|
1365
|
+
const piStatus = status.piAuth.exists ? `active: ${resolveLabel(status.piAuth.accountId)}` : "not found";
|
|
1366
|
+
process.stdout.write(` Pi Agent: ${piStatus}\n`);
|
|
1244
1367
|
process.stdout.write("\n");
|
|
1245
1368
|
} catch (error) {
|
|
1246
|
-
|
|
1247
|
-
process.stderr.write(`${message}\n`);
|
|
1248
|
-
process.exit(1);
|
|
1369
|
+
exitWithCommandError(error);
|
|
1249
1370
|
}
|
|
1250
1371
|
});
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region lib/commands/switch.ts
|
|
1376
|
+
const switchNext = async () => {
|
|
1377
|
+
const config = await loadConfig();
|
|
1378
|
+
const nextIndex = (config.current + 1) % config.accounts.length;
|
|
1379
|
+
const nextAccount = config.accounts[nextIndex];
|
|
1380
|
+
if (!nextAccount?.accountId) throw new Error("Account entry missing accountId.");
|
|
1381
|
+
const payload = loadKeychainPayload(nextAccount.accountId);
|
|
1382
|
+
const result = await writeAllAuthFiles(payload);
|
|
1383
|
+
config.current = nextIndex;
|
|
1384
|
+
await saveConfig(config);
|
|
1385
|
+
writeSwitchSummary(nextAccount.label ?? payload.accountId, result);
|
|
1386
|
+
};
|
|
1387
|
+
const switchToAccount = async (identifier) => {
|
|
1388
|
+
const config = await loadConfig();
|
|
1389
|
+
const index = config.accounts.findIndex((account) => account.accountId === identifier || account.label === identifier);
|
|
1390
|
+
if (index === -1) throw new Error(`Account "${identifier}" not found. Use 'cdx login' to add it.`);
|
|
1391
|
+
const account = config.accounts[index];
|
|
1392
|
+
const result = await writeAllAuthFiles(loadKeychainPayload(account.accountId));
|
|
1393
|
+
config.current = index;
|
|
1394
|
+
await saveConfig(config);
|
|
1395
|
+
writeSwitchSummary(account.label ?? account.accountId, result);
|
|
1396
|
+
};
|
|
1397
|
+
const registerSwitchCommand = (program) => {
|
|
1398
|
+
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) => {
|
|
1399
|
+
try {
|
|
1400
|
+
if (options.next) await switchNext();
|
|
1401
|
+
else if (accountId) await switchToAccount(accountId);
|
|
1402
|
+
else await handleSwitchAccount();
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
exitWithCommandError(error);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region lib/commands/usage.ts
|
|
1411
|
+
const registerUsageCommand = (program) => {
|
|
1251
1412
|
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
1413
|
try {
|
|
1253
1414
|
const config = await loadConfig();
|
|
1254
1415
|
if (account) {
|
|
1255
|
-
const found = config.accounts.find((
|
|
1416
|
+
const found = config.accounts.find((entry) => entry.accountId === account || entry.label === account);
|
|
1256
1417
|
if (!found) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1257
1418
|
const result = await fetchUsage(found.accountId);
|
|
1258
1419
|
if (!result.ok) throw new Error(result.error.message);
|
|
1259
1420
|
const displayName = found.label ? `${found.label} (${found.accountId})` : found.accountId;
|
|
1260
1421
|
process.stdout.write(`\n${displayName}\n${formatUsage(result.data)}\n\n`);
|
|
1261
|
-
|
|
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`);
|
|
1422
|
+
return;
|
|
1281
1423
|
}
|
|
1424
|
+
if (config.accounts.length === 0) throw new Error("No accounts configured. Use 'cdx login' to add one.");
|
|
1425
|
+
const results = await Promise.allSettled(config.accounts.map((entry) => fetchUsage(entry.accountId)));
|
|
1426
|
+
const entries = config.accounts.map((entry, index) => {
|
|
1427
|
+
const settled = results[index];
|
|
1428
|
+
const displayName = entry.label ? `${entry.label} (${entry.accountId})` : entry.accountId;
|
|
1429
|
+
const result = settled.status === "fulfilled" ? settled.value : {
|
|
1430
|
+
ok: false,
|
|
1431
|
+
error: {
|
|
1432
|
+
type: "network_error",
|
|
1433
|
+
message: settled.reason?.message ?? "Fetch failed"
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
return {
|
|
1437
|
+
displayName,
|
|
1438
|
+
isCurrent: index === config.current,
|
|
1439
|
+
result
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
process.stdout.write(`\n${formatUsageOverview(entries)}\n\n`);
|
|
1282
1443
|
} catch (error) {
|
|
1283
|
-
|
|
1284
|
-
process.stderr.write(`${message}\n`);
|
|
1285
|
-
process.exit(1);
|
|
1444
|
+
exitWithCommandError(error);
|
|
1286
1445
|
}
|
|
1287
1446
|
});
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
process.stderr.write(`Unknown command: ${commandName}\n`);
|
|
1294
|
-
program.outputHelp();
|
|
1295
|
-
process.exit(1);
|
|
1296
|
-
}
|
|
1297
|
-
} else program.outputHelp();
|
|
1298
|
-
});
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
//#endregion
|
|
1450
|
+
//#region lib/commands/version.ts
|
|
1451
|
+
const registerVersionCommand = (program, version) => {
|
|
1299
1452
|
program.command("version").description("Show CLI version").action(() => {
|
|
1300
1453
|
process.stdout.write(`${version}\n`);
|
|
1301
1454
|
});
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region cdx.ts
|
|
1459
|
+
const interactiveMode = runInteractiveMode;
|
|
1460
|
+
const createProgram = (deps = {}) => {
|
|
1461
|
+
const program = new Command();
|
|
1462
|
+
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(version, "-v, --version");
|
|
1463
|
+
registerLoginCommand(program, deps);
|
|
1464
|
+
registerReloginCommand(program);
|
|
1465
|
+
registerSwitchCommand(program);
|
|
1466
|
+
registerLabelCommand(program);
|
|
1467
|
+
registerStatusCommand(program);
|
|
1468
|
+
registerUsageCommand(program);
|
|
1469
|
+
registerHelpCommand(program);
|
|
1470
|
+
registerVersionCommand(program, version);
|
|
1471
|
+
registerDefaultInteractiveAction(program);
|
|
1311
1472
|
return program;
|
|
1312
1473
|
};
|
|
1313
1474
|
const main = async () => {
|
|
1314
1475
|
await createProgram().parseAsync(process.argv);
|
|
1315
1476
|
};
|
|
1316
1477
|
if (import.meta.main) main().catch((error) => {
|
|
1317
|
-
|
|
1318
|
-
process.stderr.write(`${message}\n`);
|
|
1319
|
-
process.exit(1);
|
|
1478
|
+
exitWithCommandError(error);
|
|
1320
1479
|
});
|
|
1321
1480
|
|
|
1322
1481
|
//#endregion
|
|
1323
|
-
export { createProgram, createTestPaths, getPaths, interactiveMode, loadConfig, resetPaths, runInteractiveMode, saveConfig, setPaths, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile };
|
|
1482
|
+
export { createProgram, createTestPaths, getPaths, interactiveMode, loadConfig, resetPaths, runInteractiveMode, saveConfig, setPaths, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bjesuiter/codex-switcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
|
|
6
6
|
"bin": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"author": "bjesuiter",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@clack/prompts": "^0.
|
|
23
|
+
"@clack/prompts": "^1.0.0",
|
|
24
24
|
"@openauthjs/openauth": "^0.4.3",
|
|
25
|
-
"commander": "^14.0.
|
|
25
|
+
"commander": "^14.0.3"
|
|
26
26
|
}
|
|
27
27
|
}
|