caflip 0.2.0 → 0.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 CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  A fast account switcher for coding agents like Claude Code and Codex.
4
4
 
5
- ![caflip interactive account picker](./docs/demo.png)
5
+ ![caflip provider + account interactive flow](./docs/demo.png)
6
+ Pick provider first, then switch account.
6
7
 
7
8
  caflip is built for one job: if you have multiple Claude or Codex accounts, switch between them quickly.
8
9
 
9
- Today, caflip focuses on Claude Code accounts. Your skills, settings, themes, `CLAUDE.md`, MCP servers, keybindings, and all other configuration stay exactly the same while switching accounts.
10
+ Today, caflip supports both Claude Code and Codex accounts. Your skills, settings, themes, `CLAUDE.md`, MCP servers, keybindings, and all other configuration stay exactly the same while switching accounts.
10
11
 
11
12
  Use case: you have personal/work Claude or Codex accounts and want to switch quickly without re-login flows every time.
12
13
 
@@ -16,7 +17,7 @@ Use case: you have personal/work Claude or Codex accounts and want to switch qui
16
17
  | Platform | Credential Storage |
17
18
  |---|---|
18
19
  | macOS | System Keychain |
19
- | Linux | `secret-tool` keyring (preferred), file-based fallback (owner-only access) |
20
+ | Linux | `CLAUDE_CONFIG_DIR/.credentials.json` when set, otherwise `~/.claude/.credentials.json`; `secret-tool` is kept as compatibility sync |
20
21
  | WSL | Same as Linux |
21
22
  | Windows | Not yet supported |
22
23
 
@@ -28,6 +29,12 @@ Use case: you have personal/work Claude or Codex accounts and want to switch qui
28
29
  curl -fsSL https://raw.githubusercontent.com/LucienLee/caflip/main/install.sh | bash
29
30
  ```
30
31
 
32
+ To uninstall the standalone binary installed by this script:
33
+
34
+ ```bash
35
+ curl -fsSL https://raw.githubusercontent.com/LucienLee/caflip/main/uninstall.sh | bash
36
+ ```
37
+
31
38
  ### Via npm (Node.js)
32
39
 
33
40
  ```bash
@@ -40,6 +47,13 @@ npm install -g caflip
40
47
  bun install -g caflip
41
48
  ```
42
49
 
50
+ For package-manager installs, uninstall with the same package manager:
51
+
52
+ ```bash
53
+ npm uninstall -g caflip
54
+ bun remove -g caflip
55
+ ```
56
+
43
57
  ### Local Development
44
58
 
45
59
  ```bash
@@ -49,6 +63,10 @@ bun run dev -- help
49
63
  ## Quick Start
50
64
 
51
65
  ```bash
66
+ # Show current account / managed accounts across both providers
67
+ caflip status
68
+ caflip list
69
+
52
70
  # Add your first Claude account (must already be logged in)
53
71
  caflip claude add --alias personal
54
72
 
@@ -69,6 +87,14 @@ caflip claude next
69
87
  caflip codex add --alias codex-work
70
88
  caflip codex list
71
89
  caflip codex next
90
+
91
+ # Run official provider login through caflip, then register the session
92
+ caflip claude login
93
+ caflip codex login
94
+
95
+ # Pass provider-specific flags after --
96
+ caflip claude login -- --email lucien@aibor.io --sso
97
+ caflip codex login -- --device-auth
72
98
  ```
73
99
 
74
100
  After switching, restart the target CLI (Claude Code or Codex) to pick up new authentication.
@@ -78,12 +104,15 @@ After switching, restart the target CLI (Claude Code or Codex) to pick up new au
78
104
  | Command | Description |
79
105
  |---|---|
80
106
  | `caflip` | Interactive provider picker (Claude/Codex) |
107
+ | `caflip list` | List managed accounts for Claude and Codex |
108
+ | `caflip status` | Show current account for Claude and Codex |
81
109
  | `caflip claude [command]` | Run command for Claude provider |
82
110
  | `caflip codex [command]` | Run command for Codex provider |
83
111
  | `caflip [provider]` | Interactive account picker for that provider |
84
112
  | `caflip [provider] <alias>` | Switch by alias for that provider |
85
113
  | `caflip [provider] list` | List managed accounts |
86
114
  | `caflip [provider] add [--alias name]` | Add current account |
115
+ | `caflip [provider] login [-- <args...>]` | Run provider login and register the resulting session |
87
116
  | `caflip [provider] remove [email]` | Remove an account |
88
117
  | `caflip [provider] next` | Rotate to next account |
89
118
  | `caflip [provider] status` | Show current account |
@@ -105,6 +134,14 @@ caflip codex alias work me@company.com
105
134
 
106
135
  `remove` target accepts email only. Omit it to choose from the interactive picker.
107
136
 
137
+ `login` can be used without arguments for the default login flow. Pass provider-specific flags after `--`:
138
+
139
+ ```bash
140
+ caflip claude login
141
+ caflip claude login -- --email lucien@aibor.io --sso
142
+ caflip codex login -- --device-auth
143
+ ```
144
+
108
145
  ## Shell Prompt Integration
109
146
 
110
147
  Show the current account in your prompt:
@@ -119,6 +156,10 @@ Account data lives in:
119
156
  - `~/.caflip-backup/claude/`
120
157
  - `~/.caflip-backup/codex/`
121
158
 
159
+ On Linux and WSL, caflip follows Claude's config root for active Claude credentials and config:
160
+ - if `CLAUDE_CONFIG_DIR` is set, caflip reads `"$CLAUDE_CONFIG_DIR/.credentials.json"` and `"$CLAUDE_CONFIG_DIR/.claude.json"`
161
+ - otherwise it falls back to `~/.claude/.credentials.json` and then Claude's standard config lookup
162
+
122
163
  ## Credits
123
164
 
124
165
  Inspired by [cc-account-switcher](https://github.com/ming86/cc-account-switcher).
package/dist/cli.js CHANGED
@@ -181,11 +181,10 @@ var require_lib = __commonJS((exports, module) => {
181
181
  });
182
182
 
183
183
  // src/index.ts
184
- import { existsSync as existsSync10, readFileSync as readFileSync10, mkdirSync as mkdirSync6 } from "fs";
184
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
185
185
 
186
186
  // src/config.ts
187
187
  import { homedir } from "os";
188
- import { existsSync, readFileSync } from "fs";
189
188
  import { join } from "path";
190
189
  function getBackupDir(provider) {
191
190
  return join(homedir(), ".caflip-backup", provider);
@@ -212,38 +211,26 @@ var RESERVED_COMMANDS = [
212
211
  "add",
213
212
  "remove",
214
213
  "next",
214
+ "login",
215
215
  "status",
216
216
  "alias",
217
217
  "claude",
218
218
  "codex",
219
219
  "help"
220
220
  ];
221
- function getClaudeConfigPath() {
222
- const primary = join(homedir(), ".claude", ".claude.json");
223
- const fallback = join(homedir(), ".claude.json");
224
- if (existsSync(primary)) {
225
- try {
226
- const content = JSON.parse(readFileSync(primary, "utf-8"));
227
- if (content.oauthAccount) {
228
- return primary;
229
- }
230
- } catch {}
231
- }
232
- return fallback;
233
- }
234
221
 
235
222
  // src/accounts.ts
236
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
223
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
237
224
 
238
225
  // src/files.ts
239
- import { existsSync as existsSync2, mkdirSync, rmSync, chmodSync, renameSync, writeFileSync, readFileSync as readFileSync2 } from "fs";
226
+ import { existsSync, mkdirSync, rmSync, chmodSync, renameSync, writeFileSync, readFileSync } from "fs";
240
227
  import { dirname, join as join2 } from "path";
241
228
  import { randomBytes } from "crypto";
242
229
  async function writeJsonAtomic(filePath, data) {
243
230
  const jsonStr = JSON.stringify(data, null, 2);
244
231
  JSON.parse(jsonStr);
245
232
  const dir = dirname(filePath);
246
- if (!existsSync2(dir)) {
233
+ if (!existsSync(dir)) {
247
234
  mkdirSync(dir, { recursive: true, mode: 448 });
248
235
  }
249
236
  chmodSync(dir, 448);
@@ -262,7 +249,7 @@ async function writeJsonAtomic(filePath, data) {
262
249
 
263
250
  // src/accounts.ts
264
251
  async function initSequenceFile(path) {
265
- if (existsSync3(path))
252
+ if (existsSync2(path))
266
253
  return;
267
254
  const data = {
268
255
  activeAccountNumber: null,
@@ -273,7 +260,7 @@ async function initSequenceFile(path) {
273
260
  await writeJsonAtomic(path, data);
274
261
  }
275
262
  async function loadSequence(path) {
276
- return JSON.parse(readFileSync3(path, "utf-8"));
263
+ return JSON.parse(readFileSync2(path, "utf-8"));
277
264
  }
278
265
  function getNextAccountNumber(seq) {
279
266
  const keys = Object.keys(seq.accounts).map(Number);
@@ -422,14 +409,14 @@ function findAccountByAlias(seq, alias) {
422
409
  }
423
410
 
424
411
  // src/files.ts
425
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2, renameSync as renameSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
412
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, rmSync as rmSync2, chmodSync as chmodSync2, renameSync as renameSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
426
413
  import { dirname as dirname2, join as join3 } from "path";
427
414
  import { randomBytes as randomBytes2 } from "crypto";
428
415
  async function writeJsonAtomic2(filePath, data) {
429
416
  const jsonStr = JSON.stringify(data, null, 2);
430
417
  JSON.parse(jsonStr);
431
418
  const dir = dirname2(filePath);
432
- if (!existsSync4(dir)) {
419
+ if (!existsSync3(dir)) {
433
420
  mkdirSync2(dir, { recursive: true, mode: 448 });
434
421
  }
435
422
  chmodSync2(dir, 448);
@@ -447,7 +434,7 @@ async function writeJsonAtomic2(filePath, data) {
447
434
  }
448
435
  function acquireLock(lockDir) {
449
436
  const parentDir = dirname2(lockDir);
450
- if (!existsSync4(parentDir)) {
437
+ if (!existsSync3(parentDir)) {
451
438
  mkdirSync2(parentDir, { recursive: true, mode: 448 });
452
439
  }
453
440
  try {
@@ -490,11 +477,11 @@ function writeLockOwner(lockDir) {
490
477
  }
491
478
  function isStaleLock(lockDir) {
492
479
  const ownerPath = getLockOwnerPath(lockDir);
493
- if (!existsSync4(ownerPath)) {
480
+ if (!existsSync3(ownerPath)) {
494
481
  return true;
495
482
  }
496
483
  try {
497
- const owner = JSON.parse(readFileSync4(ownerPath, "utf-8"));
484
+ const owner = JSON.parse(readFileSync3(ownerPath, "utf-8"));
498
485
  if (!owner.pid || !Number.isInteger(owner.pid) || owner.pid <= 0) {
499
486
  return true;
500
487
  }
@@ -521,7 +508,7 @@ function isProcessAlive(pid) {
521
508
 
522
509
  // src/config.ts
523
510
  import { homedir as homedir2 } from "os";
524
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
511
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
525
512
  import { join as join4 } from "path";
526
513
  function getBackupDir2(provider) {
527
514
  return join4(homedir2(), ".caflip-backup", provider);
@@ -548,6 +535,7 @@ var RESERVED_COMMANDS2 = [
548
535
  "add",
549
536
  "remove",
550
537
  "next",
538
+ "login",
551
539
  "status",
552
540
  "alias",
553
541
  "claude",
@@ -566,12 +554,23 @@ function detectPlatform() {
566
554
  return "unknown";
567
555
  }
568
556
  }
569
- function getClaudeConfigPath2() {
570
- const primary = join4(homedir2(), ".claude", ".claude.json");
571
- const fallback = join4(homedir2(), ".claude.json");
572
- if (existsSync5(primary)) {
557
+ function getClaudeConfigDir(env = process.env, home = homedir2()) {
558
+ const customDir = env.CLAUDE_CONFIG_DIR?.trim();
559
+ if (customDir) {
560
+ return customDir;
561
+ }
562
+ return join4(home, ".claude");
563
+ }
564
+ function getClaudeConfigPath(env = process.env, home = homedir2()) {
565
+ const customDir = env.CLAUDE_CONFIG_DIR?.trim();
566
+ if (customDir) {
567
+ return join4(customDir, ".claude.json");
568
+ }
569
+ const primary = join4(getClaudeConfigDir(env, home), ".claude.json");
570
+ const fallback = join4(home, ".claude.json");
571
+ if (existsSync4(primary)) {
573
572
  try {
574
- const content = JSON.parse(readFileSync5(primary, "utf-8"));
573
+ const content = JSON.parse(readFileSync4(primary, "utf-8"));
575
574
  if (content.oauthAccount) {
576
575
  return primary;
577
576
  }
@@ -618,7 +617,7 @@ function validateAlias(alias) {
618
617
  // package.json
619
618
  var package_default = {
620
619
  name: "caflip",
621
- version: "0.2.0",
620
+ version: "0.3.0",
622
621
  type: "module",
623
622
  bin: {
624
623
  caflip: "bin/caflip"
@@ -2368,6 +2367,25 @@ async function confirmAction(message, promptConfirm = dist_default4) {
2368
2367
  return wrapPromptCancellation(() => promptConfirm({ message, default: false }));
2369
2368
  }
2370
2369
 
2370
+ // src/login/runner.ts
2371
+ import { spawn } from "child_process";
2372
+ async function runLoginCommand(command) {
2373
+ return await new Promise((resolve, reject) => {
2374
+ const proc = spawn(command[0], command.slice(1), {
2375
+ stdio: "inherit"
2376
+ });
2377
+ proc.on("error", (error) => {
2378
+ reject(error);
2379
+ });
2380
+ proc.on("close", (code, signal) => {
2381
+ resolve({
2382
+ exitCode: code ?? 1,
2383
+ signal
2384
+ });
2385
+ });
2386
+ });
2387
+ }
2388
+
2371
2389
  // src/providers/types.ts
2372
2390
  var SUPPORTED_PROVIDERS = ["claude", "codex"];
2373
2391
  function isProviderName(value) {
@@ -2384,13 +2402,13 @@ function parseProviderArgs(args) {
2384
2402
  }
2385
2403
 
2386
2404
  // src/providers/claude.ts
2387
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2405
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
2388
2406
 
2389
2407
  // src/credentials.ts
2390
- import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync3, chmodSync as chmodSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2408
+ import { existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync3, chmodSync as chmodSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2391
2409
  import { join as join5 } from "path";
2392
2410
  import { homedir as homedir3 } from "os";
2393
- import { spawn, spawnSync } from "child_process";
2411
+ import { spawn as spawn2, spawnSync } from "child_process";
2394
2412
 
2395
2413
  // src/validation.ts
2396
2414
  var EMAIL_REGEX2 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
@@ -2416,7 +2434,7 @@ async function runCommand(cmd) {
2416
2434
  }
2417
2435
  async function runCommandWithInput(cmd, input) {
2418
2436
  return await new Promise((resolve, reject) => {
2419
- const proc = spawn(cmd[0], cmd.slice(1), {
2437
+ const proc = spawn2(cmd[0], cmd.slice(1), {
2420
2438
  stdio: ["pipe", "pipe", "pipe"]
2421
2439
  });
2422
2440
  let stdout = "";
@@ -2464,6 +2482,16 @@ function activeSecretToolAttrs() {
2464
2482
  function backupSecretToolAttrs(accountNum, email) {
2465
2483
  return ["service", "ccflip", "account", accountNum, "email", email];
2466
2484
  }
2485
+ function getClaudeCredentialsDir(env = process.env, home = homedir3()) {
2486
+ const customDir = env.CLAUDE_CONFIG_DIR?.trim();
2487
+ if (customDir) {
2488
+ return customDir;
2489
+ }
2490
+ return join5(home, ".claude");
2491
+ }
2492
+ function getClaudeCredentialsPath(env = process.env, home = homedir3()) {
2493
+ return join5(getClaudeCredentialsDir(env, home), ".credentials.json");
2494
+ }
2467
2495
  async function secretToolLookup(attrs) {
2468
2496
  const result = await runCommand(["secret-tool", "lookup", ...attrs]);
2469
2497
  if (result.exitCode === 0) {
@@ -2507,16 +2535,16 @@ async function readCredentials() {
2507
2535
  }
2508
2536
  case "linux":
2509
2537
  case "wsl": {
2538
+ const credPath = getClaudeCredentialsPath();
2539
+ if (existsSync5(credPath)) {
2540
+ return readFileSync5(credPath, "utf-8");
2541
+ }
2510
2542
  if (hasSecretTool()) {
2511
2543
  const keyringValue = await secretToolLookup(activeSecretToolAttrs());
2512
2544
  if (keyringValue) {
2513
2545
  return keyringValue;
2514
2546
  }
2515
2547
  }
2516
- const credPath = join5(homedir3(), ".claude", ".credentials.json");
2517
- if (existsSync6(credPath)) {
2518
- return readFileSync6(credPath, "utf-8");
2519
- }
2520
2548
  return "";
2521
2549
  }
2522
2550
  default:
@@ -2545,16 +2573,15 @@ async function writeCredentials(credentials) {
2545
2573
  }
2546
2574
  case "linux":
2547
2575
  case "wsl": {
2548
- if (hasSecretTool()) {
2549
- await secretToolStore(activeSecretToolAttrs(), credentials);
2550
- return;
2551
- }
2552
- const claudeDir = join5(homedir3(), ".claude");
2576
+ const claudeDir = getClaudeCredentialsDir();
2553
2577
  mkdirSync3(claudeDir, { recursive: true, mode: 448 });
2554
2578
  chmodSync3(claudeDir, 448);
2555
- const credPath = join5(claudeDir, ".credentials.json");
2579
+ const credPath = getClaudeCredentialsPath();
2556
2580
  writeFileSync3(credPath, credentials, { mode: 384 });
2557
2581
  chmodSync3(credPath, 384);
2582
+ if (hasSecretTool()) {
2583
+ await secretToolStore(activeSecretToolAttrs(), credentials);
2584
+ }
2558
2585
  break;
2559
2586
  }
2560
2587
  }
@@ -2576,11 +2603,11 @@ async function clearActiveCredentials() {
2576
2603
  }
2577
2604
  case "linux":
2578
2605
  case "wsl": {
2606
+ const credPath = getClaudeCredentialsPath();
2607
+ rmSync3(credPath, { force: true });
2579
2608
  if (hasSecretTool()) {
2580
2609
  await secretToolClear(activeSecretToolAttrs());
2581
2610
  }
2582
- const credPath = join5(homedir3(), ".claude", ".credentials.json");
2583
- rmSync3(credPath, { force: true });
2584
2611
  break;
2585
2612
  }
2586
2613
  }
@@ -2617,8 +2644,8 @@ async function readAccountCredentials(accountNum, email, credentialsDir = CREDEN
2617
2644
  }
2618
2645
  }
2619
2646
  const credFile = join5(credentialsDir, `.claude-credentials-${accountNum}-${email}.json`);
2620
- if (existsSync6(credFile)) {
2621
- return readFileSync6(credFile, "utf-8");
2647
+ if (existsSync5(credFile)) {
2648
+ return readFileSync5(credFile, "utf-8");
2622
2649
  }
2623
2650
  return "";
2624
2651
  }
@@ -2701,8 +2728,8 @@ function readAccountConfig(accountNum, email, configsDir) {
2701
2728
  throw new Error(`Unsafe email for filename: ${email}`);
2702
2729
  }
2703
2730
  const configFile = join5(configsDir, `.claude-config-${accountNum}-${email}.json`);
2704
- if (existsSync6(configFile)) {
2705
- return readFileSync6(configFile, "utf-8");
2731
+ if (existsSync5(configFile)) {
2732
+ return readFileSync5(configFile, "utf-8");
2706
2733
  }
2707
2734
  return "";
2708
2735
  }
@@ -2725,6 +2752,191 @@ function deleteAccountConfig(accountNum, email, configsDir) {
2725
2752
  rmSync3(configFile, { force: true });
2726
2753
  }
2727
2754
 
2755
+ // src/login/runner.ts
2756
+ import { spawn as spawn3 } from "child_process";
2757
+ var DEFAULT_CAPTURE_TIMEOUT_MS = 5000;
2758
+ function normalizeOutput(value) {
2759
+ return value.trim();
2760
+ }
2761
+ async function runCapturedCommand(command, options) {
2762
+ return await new Promise((resolve, reject) => {
2763
+ const proc = spawn3(command[0], command.slice(1), {
2764
+ stdio: ["ignore", "pipe", "pipe"]
2765
+ });
2766
+ let stdout = "";
2767
+ let stderr = "";
2768
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_CAPTURE_TIMEOUT_MS;
2769
+ const timer = setTimeout(() => {
2770
+ proc.kill("SIGTERM");
2771
+ }, timeoutMs);
2772
+ proc.stdout.on("data", (chunk) => {
2773
+ stdout += String(chunk);
2774
+ });
2775
+ proc.stderr.on("data", (chunk) => {
2776
+ stderr += String(chunk);
2777
+ });
2778
+ proc.on("error", (error) => {
2779
+ clearTimeout(timer);
2780
+ reject(error);
2781
+ });
2782
+ proc.on("close", (code, signal) => {
2783
+ clearTimeout(timer);
2784
+ if (signal === "SIGTERM") {
2785
+ resolve({
2786
+ exitCode: 124,
2787
+ stdout: normalizeOutput(stdout),
2788
+ stderr: normalizeOutput(stderr) || `command timed out after ${timeoutMs}ms`,
2789
+ signal
2790
+ });
2791
+ return;
2792
+ }
2793
+ resolve({
2794
+ exitCode: code ?? 1,
2795
+ stdout: normalizeOutput(stdout),
2796
+ stderr: normalizeOutput(stderr),
2797
+ signal
2798
+ });
2799
+ });
2800
+ });
2801
+ }
2802
+
2803
+ // src/providers/claude.ts
2804
+ function readClaudeConfigObject() {
2805
+ const configPath = getClaudeConfigPath();
2806
+ if (!existsSync6(configPath))
2807
+ return null;
2808
+ try {
2809
+ return JSON.parse(readFileSync6(configPath, "utf-8"));
2810
+ } catch {
2811
+ return null;
2812
+ }
2813
+ }
2814
+ function getClaudeCurrentAccount() {
2815
+ const content = readClaudeConfigObject();
2816
+ const email = content?.oauthAccount?.emailAddress;
2817
+ if (typeof email !== "string" || !email) {
2818
+ return null;
2819
+ }
2820
+ const accountId = typeof content?.oauthAccount?.accountUuid === "string" ? content.oauthAccount.accountUuid : undefined;
2821
+ return { email, accountId };
2822
+ }
2823
+ function getClaudeCurrentAccountEmail() {
2824
+ return getClaudeCurrentAccount()?.email ?? "none";
2825
+ }
2826
+ async function readClaudeActiveConfig() {
2827
+ const configPath = getClaudeConfigPath();
2828
+ if (!existsSync6(configPath)) {
2829
+ return "";
2830
+ }
2831
+ try {
2832
+ return readFileSync6(configPath, "utf-8");
2833
+ } catch {
2834
+ return "";
2835
+ }
2836
+ }
2837
+ async function writeClaudeActiveConfig(raw) {
2838
+ let targetConfig;
2839
+ try {
2840
+ targetConfig = JSON.parse(raw);
2841
+ } catch {
2842
+ throw new Error("Invalid Claude config backup");
2843
+ }
2844
+ const oauthAccount = targetConfig.oauthAccount;
2845
+ if (!oauthAccount) {
2846
+ throw new Error("Invalid oauthAccount in backup");
2847
+ }
2848
+ const configPath = getClaudeConfigPath();
2849
+ let currentConfigObj = {};
2850
+ if (existsSync6(configPath)) {
2851
+ try {
2852
+ currentConfigObj = JSON.parse(readFileSync6(configPath, "utf-8"));
2853
+ } catch {
2854
+ currentConfigObj = {};
2855
+ }
2856
+ }
2857
+ currentConfigObj.oauthAccount = oauthAccount;
2858
+ await writeJsonAtomic(configPath, currentConfigObj);
2859
+ }
2860
+ async function clearClaudeActiveConfig() {
2861
+ const configPath = getClaudeConfigPath();
2862
+ let configObj = {};
2863
+ if (existsSync6(configPath)) {
2864
+ try {
2865
+ configObj = JSON.parse(readFileSync6(configPath, "utf-8"));
2866
+ } catch {
2867
+ configObj = {};
2868
+ }
2869
+ }
2870
+ delete configObj.oauthAccount;
2871
+ await writeJsonAtomic(configPath, configObj);
2872
+ }
2873
+ async function verifyClaudeLogin(commandRunner = runCapturedCommand) {
2874
+ const result = await commandRunner(["claude", "auth", "status", "--json"]);
2875
+ if (result.exitCode !== 0) {
2876
+ return {
2877
+ ok: false,
2878
+ reason: result.stderr || "claude auth status failed"
2879
+ };
2880
+ }
2881
+ let payload;
2882
+ try {
2883
+ payload = JSON.parse(result.stdout);
2884
+ } catch {
2885
+ return {
2886
+ ok: false,
2887
+ reason: "claude auth status returned invalid JSON"
2888
+ };
2889
+ }
2890
+ if (payload.loggedIn !== true) {
2891
+ return {
2892
+ ok: false,
2893
+ reason: "claude auth status reported logged out",
2894
+ details: {
2895
+ loggedIn: payload.loggedIn ?? false
2896
+ }
2897
+ };
2898
+ }
2899
+ if (!payload.email) {
2900
+ return {
2901
+ ok: false,
2902
+ reason: "claude auth status did not include an email"
2903
+ };
2904
+ }
2905
+ return {
2906
+ ok: true,
2907
+ email: payload.email,
2908
+ details: {
2909
+ authMethod: payload.authMethod,
2910
+ orgId: payload.orgId,
2911
+ orgName: payload.orgName,
2912
+ subscriptionType: payload.subscriptionType
2913
+ }
2914
+ };
2915
+ }
2916
+ var claudeLoginAdapter = {
2917
+ buildCommand: (passthroughArgs) => ["claude", "auth", "login", ...passthroughArgs],
2918
+ verifyLogin: verifyClaudeLogin
2919
+ };
2920
+ var claudeProvider = {
2921
+ name: "claude",
2922
+ login: claudeLoginAdapter,
2923
+ usesAccountConfig: true,
2924
+ getCurrentAccount: getClaudeCurrentAccount,
2925
+ getCurrentAccountEmail: getClaudeCurrentAccountEmail,
2926
+ readActiveAuth: readCredentials,
2927
+ writeActiveAuth: writeCredentials,
2928
+ clearActiveAuth: clearActiveCredentials,
2929
+ readActiveConfig: readClaudeActiveConfig,
2930
+ writeActiveConfig: writeClaudeActiveConfig,
2931
+ clearActiveConfig: clearClaudeActiveConfig,
2932
+ readAccountAuth: readAccountCredentials,
2933
+ writeAccountAuth: writeAccountCredentials,
2934
+ deleteAccountAuth: deleteAccountCredentials,
2935
+ readAccountConfig,
2936
+ writeAccountConfig,
2937
+ deleteAccountConfig
2938
+ };
2939
+
2728
2940
  // src/providers/codex.ts
2729
2941
  import { chmodSync as chmodSync4, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
2730
2942
  import { homedir as homedir4 } from "os";
@@ -2758,7 +2970,11 @@ async function readCodexActiveAuth() {
2758
2970
  if (!existsSync7(authPath)) {
2759
2971
  return "";
2760
2972
  }
2761
- return readFileSync7(authPath, "utf-8");
2973
+ try {
2974
+ return readFileSync7(authPath, "utf-8");
2975
+ } catch {
2976
+ return "";
2977
+ }
2762
2978
  }
2763
2979
  async function writeCodexActiveAuth(raw) {
2764
2980
  const codexDir = join6(process.env.HOME ?? homedir4(), ".codex");
@@ -2777,7 +2993,11 @@ async function readCodexAccountAuthBackup(accountNum, email, credentialsDir) {
2777
2993
  if (!existsSync7(backupPath)) {
2778
2994
  return "";
2779
2995
  }
2780
- return readFileSync7(backupPath, "utf-8");
2996
+ try {
2997
+ return readFileSync7(backupPath, "utf-8");
2998
+ } catch {
2999
+ return "";
3000
+ }
2781
3001
  }
2782
3002
  async function writeCodexAccountAuthBackup(accountNum, email, raw, credentialsDir) {
2783
3003
  ensureBackupKeySafe(accountNum, email);
@@ -2821,45 +3041,79 @@ function getCodexCurrentAccount() {
2821
3041
  return null;
2822
3042
  }
2823
3043
  }
2824
-
2825
- // src/providers/claude.ts
2826
- function getClaudeCurrentAccountEmail() {
2827
- const configPath = getClaudeConfigPath2();
2828
- if (!existsSync8(configPath))
2829
- return "none";
3044
+ function readCodexAuthFile() {
3045
+ const authPath = getCodexAuthPath();
3046
+ if (!existsSync7(authPath)) {
3047
+ return null;
3048
+ }
2830
3049
  try {
2831
- const content = JSON.parse(readFileSync8(configPath, "utf-8"));
2832
- return content?.oauthAccount?.emailAddress ?? "none";
3050
+ return JSON.parse(readFileSync7(authPath, "utf-8"));
2833
3051
  } catch {
2834
- return "none";
3052
+ return null;
2835
3053
  }
2836
3054
  }
2837
- var claudeProvider = {
2838
- name: "claude",
2839
- getCurrentAccount: () => {
2840
- const email = getClaudeCurrentAccountEmail();
2841
- if (email === "none")
2842
- return null;
2843
- return { email };
2844
- },
2845
- getCurrentAccountEmail: getClaudeCurrentAccountEmail,
2846
- readActiveAuth: readCredentials,
2847
- writeActiveAuth: writeCredentials,
2848
- clearActiveAuth: clearActiveCredentials,
2849
- readAccountAuth: readAccountCredentials,
2850
- writeAccountAuth: writeAccountCredentials,
2851
- deleteAccountAuth: deleteAccountCredentials,
2852
- readAccountConfig,
2853
- writeAccountConfig,
2854
- deleteAccountConfig
3055
+ async function verifyCodexLogin(commandRunner = runCapturedCommand) {
3056
+ const result = await commandRunner(["codex", "login", "status"]);
3057
+ if (result.exitCode !== 0) {
3058
+ return {
3059
+ ok: false,
3060
+ reason: result.stderr || "codex login status failed"
3061
+ };
3062
+ }
3063
+ const authFile = readCodexAuthFile();
3064
+ if (!authFile) {
3065
+ return {
3066
+ ok: false,
3067
+ reason: "codex auth file was missing or unreadable after successful login status"
3068
+ };
3069
+ }
3070
+ if (authFile.OPENAI_API_KEY) {
3071
+ return {
3072
+ ok: false,
3073
+ reason: "caflip does not support Codex API key login sessions",
3074
+ details: {
3075
+ authMode: authFile.auth_mode ?? "apikey"
3076
+ }
3077
+ };
3078
+ }
3079
+ if (authFile.auth_mode && authFile.auth_mode !== "chatgpt") {
3080
+ return {
3081
+ ok: false,
3082
+ reason: `caflip does not support Codex ${authFile.auth_mode} login sessions`,
3083
+ details: {
3084
+ authMode: authFile.auth_mode
3085
+ }
3086
+ };
3087
+ }
3088
+ const currentAccount = getCodexCurrentAccount();
3089
+ if (!currentAccount?.email) {
3090
+ return {
3091
+ ok: false,
3092
+ reason: "codex auth file did not resolve a current account email"
3093
+ };
3094
+ }
3095
+ return {
3096
+ ok: true,
3097
+ email: currentAccount.email,
3098
+ details: currentAccount.accountId ? { accountId: currentAccount.accountId } : undefined
3099
+ };
3100
+ }
3101
+ var codexLoginAdapter = {
3102
+ buildCommand: (passthroughArgs) => ["codex", "login", ...passthroughArgs],
3103
+ verifyLogin: verifyCodexLogin
2855
3104
  };
2856
3105
  var codexProvider = {
2857
3106
  name: "codex",
3107
+ login: codexLoginAdapter,
3108
+ usesAccountConfig: false,
2858
3109
  getCurrentAccount: getCodexCurrentAccount,
2859
3110
  getCurrentAccountEmail: () => getCodexCurrentAccount()?.email ?? "none",
2860
3111
  readActiveAuth: readCodexActiveAuth,
2861
3112
  writeActiveAuth: writeCodexActiveAuth,
2862
3113
  clearActiveAuth: clearCodexActiveAuth,
3114
+ readActiveConfig: async () => "",
3115
+ writeActiveConfig: async () => {},
3116
+ clearActiveConfig: async () => {},
2863
3117
  readAccountAuth: readCodexAccountAuthBackup,
2864
3118
  writeAccountAuth: writeCodexAccountAuthBackup,
2865
3119
  deleteAccountAuth: deleteCodexAccountAuthBackup,
@@ -2878,7 +3132,7 @@ function getProvider(name) {
2878
3132
  }
2879
3133
 
2880
3134
  // src/meta.ts
2881
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
3135
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2882
3136
  import { mkdirSync as mkdirSync5 } from "fs";
2883
3137
  import { homedir as homedir5 } from "os";
2884
3138
  import { join as join7 } from "path";
@@ -2888,11 +3142,11 @@ function getMetaFilePath() {
2888
3142
  }
2889
3143
  function readCliMeta() {
2890
3144
  const metaFile = getMetaFilePath();
2891
- if (!existsSync9(metaFile)) {
3145
+ if (!existsSync8(metaFile)) {
2892
3146
  return { lastProvider: "claude" };
2893
3147
  }
2894
3148
  try {
2895
- const parsed = JSON.parse(readFileSync9(metaFile, "utf-8"));
3149
+ const parsed = JSON.parse(readFileSync8(metaFile, "utf-8"));
2896
3150
  if (parsed.lastProvider === "codex") {
2897
3151
  return { lastProvider: "codex" };
2898
3152
  }
@@ -2937,18 +3191,10 @@ function getCurrentAccount() {
2937
3191
  function getProviderLabel() {
2938
3192
  return activeProvider.name === "codex" ? "Codex" : "Claude Code";
2939
3193
  }
2940
- async function clearActiveOAuthAccount() {
2941
- const configPath = getClaudeConfigPath();
2942
- let configObj = {};
2943
- if (existsSync10(configPath)) {
2944
- try {
2945
- configObj = JSON.parse(readFileSync10(configPath, "utf-8"));
2946
- } catch {
2947
- configObj = {};
2948
- }
2949
- }
2950
- delete configObj.oauthAccount;
2951
- await writeJsonAtomic2(configPath, configObj);
3194
+ function showProviderRequiredError(command) {
3195
+ console.error(`Error: ${command} requires provider prefix.`);
3196
+ console.error(`Try: caflip claude ${command} or caflip codex ${command}`);
3197
+ process.exit(2);
2952
3198
  }
2953
3199
  async function syncSequenceActiveAccount(seq) {
2954
3200
  const currentEmail = getCurrentAccount();
@@ -2960,6 +3206,108 @@ async function syncSequenceActiveAccount(seq) {
2960
3206
  }
2961
3207
  return seq;
2962
3208
  }
3209
+ async function registerCurrentActiveAccount(options) {
3210
+ const currentAccount = activeProvider.getCurrentAccount();
3211
+ const currentEmail = currentAccount?.email ?? "none";
3212
+ if (currentEmail === "none") {
3213
+ throw new Error(`No active ${getProviderLabel()} account found. Please log in first.`);
3214
+ }
3215
+ if (!sanitizeEmailForFilename(currentEmail)) {
3216
+ throw new Error("Current account email is not safe for storage");
3217
+ }
3218
+ if (options?.expectedEmail && currentEmail !== options.expectedEmail) {
3219
+ throw new Error(`Active ${getProviderLabel()} account changed during login verification: expected ${options.expectedEmail}, got ${currentEmail}`);
3220
+ }
3221
+ setupDirectories();
3222
+ await initSequenceFile(activeSequenceFile);
3223
+ const seq = await loadSequence(activeSequenceFile);
3224
+ await syncSequenceActiveAccount(seq);
3225
+ if (options?.alias) {
3226
+ const result = validateAlias(options.alias);
3227
+ if (!result.valid) {
3228
+ throw new Error(result.reason);
3229
+ }
3230
+ const existingAliasTarget = findAccountByAlias(seq, options.alias);
3231
+ const currentAccountNum = resolveAccountIdentifier(seq, currentEmail);
3232
+ if (existingAliasTarget && existingAliasTarget !== currentAccountNum) {
3233
+ throw new Error(`Alias "${options.alias}" is already in use`);
3234
+ }
3235
+ }
3236
+ const existingAccountNum = resolveAccountIdentifier(seq, currentEmail);
3237
+ if (existingAccountNum) {
3238
+ if (!options?.updateIfExists) {
3239
+ console.log(`Account ${currentEmail} is already managed.`);
3240
+ return {
3241
+ action: "unchanged",
3242
+ accountNum: existingAccountNum,
3243
+ email: currentEmail
3244
+ };
3245
+ }
3246
+ }
3247
+ const creds = await activeProvider.readActiveAuth();
3248
+ if (!creds) {
3249
+ throw new Error("No credentials found for current account");
3250
+ }
3251
+ const config = await activeProvider.readActiveConfig();
3252
+ let uuid = currentAccount?.accountId ?? "";
3253
+ if (activeProvider.usesAccountConfig && !config) {
3254
+ throw new Error("No config found for current account");
3255
+ }
3256
+ if (existingAccountNum) {
3257
+ const updatedSeq = {
3258
+ ...seq,
3259
+ activeAccountNumber: Number(existingAccountNum),
3260
+ lastUpdated: new Date().toISOString(),
3261
+ accounts: {
3262
+ ...seq.accounts,
3263
+ [existingAccountNum]: {
3264
+ ...seq.accounts[existingAccountNum],
3265
+ uuid,
3266
+ ...options?.alias ? { alias: options.alias } : {}
3267
+ }
3268
+ }
3269
+ };
3270
+ await activeProvider.writeAccountAuth(existingAccountNum, currentEmail, creds, activeCredentialsDir);
3271
+ if (config) {
3272
+ await activeProvider.writeAccountConfig(existingAccountNum, currentEmail, config, activeConfigsDir);
3273
+ }
3274
+ await writeJsonAtomic2(activeSequenceFile, updatedSeq);
3275
+ return {
3276
+ action: "updated",
3277
+ accountNum: existingAccountNum,
3278
+ email: currentEmail
3279
+ };
3280
+ }
3281
+ const updated = addAccountToSequence(seq, {
3282
+ email: currentEmail,
3283
+ uuid,
3284
+ alias: options?.alias
3285
+ });
3286
+ const accountNum = String(updated.activeAccountNumber);
3287
+ await activeProvider.writeAccountAuth(accountNum, currentEmail, creds, activeCredentialsDir);
3288
+ if (config) {
3289
+ await activeProvider.writeAccountConfig(accountNum, currentEmail, config, activeConfigsDir);
3290
+ }
3291
+ await writeJsonAtomic2(activeSequenceFile, updated);
3292
+ return {
3293
+ action: "added",
3294
+ accountNum,
3295
+ email: currentEmail
3296
+ };
3297
+ }
3298
+ function getLoginPassthroughArgs(args) {
3299
+ const passthroughIdx = args.indexOf("--");
3300
+ if (passthroughIdx === -1) {
3301
+ if (args.length > 0) {
3302
+ throw new Error("Provider login arguments must be passed after --");
3303
+ }
3304
+ return [];
3305
+ }
3306
+ if (passthroughIdx !== 0) {
3307
+ throw new Error("Provider login arguments must be passed after --");
3308
+ }
3309
+ return args.slice(passthroughIdx + 1);
3310
+ }
2963
3311
  async function performSwitch(seq, targetAccount, options) {
2964
3312
  const targetEmail = seq.accounts[targetAccount].email;
2965
3313
  const currentEmail = options?.currentEmail ?? getCurrentAccount();
@@ -2987,9 +3335,8 @@ async function performSwitch(seq, targetAccount, options) {
2987
3335
  if (currentCreds) {
2988
3336
  await activeProvider.writeAccountAuth(currentAccount, currentEmail, currentCreds, activeCredentialsDir);
2989
3337
  }
2990
- if (activeProvider.name === "claude") {
2991
- const configPath = getClaudeConfigPath();
2992
- const currentConfig = existsSync10(configPath) ? readFileSync10(configPath, "utf-8") : "";
3338
+ if (activeProvider.usesAccountConfig) {
3339
+ const currentConfig = await activeProvider.readActiveConfig();
2993
3340
  if (currentConfig) {
2994
3341
  await activeProvider.writeAccountConfig(currentAccount, currentEmail, currentConfig, activeConfigsDir);
2995
3342
  }
@@ -3000,23 +3347,12 @@ async function performSwitch(seq, targetAccount, options) {
3000
3347
  if (!targetCreds) {
3001
3348
  throw new Error(`Missing backup data for ${getDisplayAccountLabel(seq, targetAccount)}`);
3002
3349
  }
3003
- if (activeProvider.name === "claude" && !targetConfig) {
3350
+ if (activeProvider.usesAccountConfig && !targetConfig) {
3004
3351
  throw new Error(`Missing backup data for ${getDisplayAccountLabel(seq, targetAccount)}`);
3005
3352
  }
3006
3353
  await activeProvider.writeActiveAuth(targetCreds);
3007
- if (activeProvider.name === "claude") {
3008
- const targetConfigObj = JSON.parse(targetConfig);
3009
- const oauthAccount = targetConfigObj.oauthAccount;
3010
- if (!oauthAccount) {
3011
- throw new Error("Invalid oauthAccount in backup");
3012
- }
3013
- const configPath = getClaudeConfigPath();
3014
- let currentConfigObj = {};
3015
- if (existsSync10(configPath)) {
3016
- currentConfigObj = JSON.parse(readFileSync10(configPath, "utf-8"));
3017
- }
3018
- currentConfigObj.oauthAccount = oauthAccount;
3019
- await writeJsonAtomic2(configPath, currentConfigObj);
3354
+ if (targetConfig) {
3355
+ await activeProvider.writeActiveConfig(targetConfig);
3020
3356
  }
3021
3357
  seq.activeAccountNumber = Number(targetAccount);
3022
3358
  seq.lastUpdated = new Date().toISOString();
@@ -3030,16 +3366,25 @@ Please restart ${getProviderLabel()} to use the new authentication.
3030
3366
  `);
3031
3367
  }
3032
3368
  async function cmdList() {
3033
- if (!existsSync10(activeSequenceFile)) {
3369
+ const lines = await getManagedAccountLinesForActiveProvider();
3370
+ if (!lines) {
3034
3371
  const providerCmd = activeProvider.name === "codex" ? "caflip codex add" : "caflip claude add";
3035
3372
  console.log(`No accounts managed yet. Run: ${providerCmd}`);
3036
3373
  return;
3037
3374
  }
3375
+ console.log("Accounts:");
3376
+ for (const line of lines) {
3377
+ console.log(line);
3378
+ }
3379
+ }
3380
+ async function getManagedAccountLinesForActiveProvider() {
3381
+ if (!existsSync9(activeSequenceFile)) {
3382
+ return null;
3383
+ }
3038
3384
  const seq = await loadSequence(activeSequenceFile);
3039
3385
  await syncSequenceActiveAccount(seq);
3040
3386
  const currentEmail = getCurrentAccount();
3041
- console.log("Accounts:");
3042
- seq.sequence.forEach((num, index) => {
3387
+ return seq.sequence.map((num, index) => {
3043
3388
  const numStr = String(num);
3044
3389
  const account = seq.accounts[numStr];
3045
3390
  if (!account) {
@@ -3051,64 +3396,44 @@ async function cmdList() {
3051
3396
  line += ` [${account.alias}]`;
3052
3397
  if (isActive)
3053
3398
  line += " (active)";
3054
- console.log(line);
3399
+ return line;
3055
3400
  });
3056
3401
  }
3057
3402
  async function cmdAdd(alias) {
3058
- setupDirectories();
3059
- await initSequenceFile(activeSequenceFile);
3060
- const currentAccount = activeProvider.getCurrentAccount();
3061
- const currentEmail = currentAccount?.email ?? "none";
3062
- if (currentEmail === "none") {
3063
- throw new Error(`No active ${getProviderLabel()} account found. Please log in first.`);
3064
- }
3065
- if (!sanitizeEmailForFilename(currentEmail)) {
3066
- throw new Error("Current account email is not safe for storage");
3067
- }
3068
- const seq = await loadSequence(activeSequenceFile);
3069
- await syncSequenceActiveAccount(seq);
3070
- if (accountExists(seq, currentEmail)) {
3071
- console.log(`Account ${currentEmail} is already managed.`);
3403
+ const result = await registerCurrentActiveAccount({ alias, updateIfExists: false });
3404
+ if (result.action === "unchanged") {
3072
3405
  return;
3073
3406
  }
3074
- if (alias) {
3075
- const result = validateAlias(alias);
3076
- if (!result.valid) {
3077
- throw new Error(result.reason);
3078
- }
3079
- if (findAccountByAlias(seq, alias)) {
3080
- throw new Error(`Alias "${alias}" is already in use`);
3081
- }
3082
- }
3083
- const creds = await activeProvider.readActiveAuth();
3084
- if (!creds) {
3085
- throw new Error("No credentials found for current account");
3086
- }
3087
- let config = "";
3088
- let uuid = currentAccount?.accountId ?? "";
3089
- if (activeProvider.name === "claude") {
3090
- const configPath = getClaudeConfigPath();
3091
- config = readFileSync10(configPath, "utf-8");
3092
- const configObj = JSON.parse(config);
3093
- uuid = configObj.oauthAccount?.accountUuid ?? "";
3094
- }
3095
- const updated = addAccountToSequence(seq, {
3096
- email: currentEmail,
3097
- uuid,
3098
- alias
3099
- });
3100
- const accountNum = String(updated.activeAccountNumber);
3101
- const displayLabel = getDisplayAccountLabel(updated, accountNum);
3102
- await activeProvider.writeAccountAuth(accountNum, currentEmail, creds, activeCredentialsDir);
3103
- if (config) {
3104
- await activeProvider.writeAccountConfig(accountNum, currentEmail, config, activeConfigsDir);
3105
- }
3106
- await writeJsonAtomic2(activeSequenceFile, updated);
3407
+ const seq = await loadSequence(activeSequenceFile);
3408
+ const displayLabel = getDisplayAccountLabel(seq, result.accountNum);
3107
3409
  const aliasStr = alias ? ` [${alias}]` : "";
3108
- console.log(`Added ${displayLabel}: ${currentEmail}${aliasStr}`);
3410
+ console.log(`Added ${displayLabel}: ${result.email}${aliasStr}`);
3411
+ }
3412
+ async function cmdLogin(args) {
3413
+ const passthroughArgs = getLoginPassthroughArgs(args);
3414
+ const loginCommand = activeProvider.login.buildCommand(passthroughArgs);
3415
+ const execution = await runLoginCommand(loginCommand);
3416
+ if (execution.exitCode !== 0) {
3417
+ throw new Error(`${getProviderLabel()} login failed`);
3418
+ }
3419
+ const verification = await activeProvider.login.verifyLogin();
3420
+ if (!verification.ok) {
3421
+ throw new Error(verification.reason);
3422
+ }
3423
+ const result = await registerCurrentActiveAccount({
3424
+ updateIfExists: true,
3425
+ expectedEmail: verification.email
3426
+ });
3427
+ const seq = await loadSequence(activeSequenceFile);
3428
+ const displayLabel = getDisplayAccountLabel(seq, result.accountNum);
3429
+ const verb = result.action === "added" ? "Added" : "Updated";
3430
+ console.log(`${verb} ${displayLabel}: ${result.email}`);
3431
+ }
3432
+ function validateLoginArgs(args) {
3433
+ getLoginPassthroughArgs(args);
3109
3434
  }
3110
3435
  async function cmdRemove(identifier) {
3111
- if (!existsSync10(activeSequenceFile)) {
3436
+ if (!existsSync9(activeSequenceFile)) {
3112
3437
  throw new Error("No accounts managed yet");
3113
3438
  }
3114
3439
  const seq = await loadSequence(activeSequenceFile);
@@ -3144,9 +3469,7 @@ async function cmdRemove(identifier) {
3144
3469
  await performSwitch(seq, action.targetAccountNumber);
3145
3470
  } else if (action.type === "logout") {
3146
3471
  await activeProvider.clearActiveAuth();
3147
- if (activeProvider.name === "claude") {
3148
- await clearActiveOAuthAccount();
3149
- }
3472
+ await activeProvider.clearActiveConfig();
3150
3473
  }
3151
3474
  await activeProvider.deleteAccountAuth(accountNum, account.email, activeCredentialsDir);
3152
3475
  activeProvider.deleteAccountConfig(accountNum, account.email, activeConfigsDir);
@@ -3154,7 +3477,7 @@ async function cmdRemove(identifier) {
3154
3477
  console.log(`${getDisplayAccountLabel(seq, accountNum)} (${account.email}) has been removed`);
3155
3478
  }
3156
3479
  async function cmdNext() {
3157
- if (!existsSync10(activeSequenceFile)) {
3480
+ if (!existsSync9(activeSequenceFile)) {
3158
3481
  throw new Error("No accounts managed yet");
3159
3482
  }
3160
3483
  const seq = await loadSequence(activeSequenceFile);
@@ -3166,11 +3489,32 @@ async function cmdNext() {
3166
3489
  await performSwitch(seq, String(nextNum));
3167
3490
  }
3168
3491
  async function cmdStatus(options) {
3492
+ const summary = await getStatusSummaryForActiveProvider();
3169
3493
  const jsonMode = options?.json ?? false;
3494
+ if (jsonMode) {
3495
+ console.log(JSON.stringify({
3496
+ provider: activeProvider.name,
3497
+ email: summary.email === "none" ? null : summary.email,
3498
+ alias: summary.alias,
3499
+ managed: summary.managed
3500
+ }));
3501
+ return;
3502
+ }
3503
+ if (summary.email === "none") {
3504
+ console.log("none");
3505
+ } else {
3506
+ if (summary.alias) {
3507
+ console.log(`${summary.email} [${summary.alias}]`);
3508
+ return;
3509
+ }
3510
+ console.log(summary.email);
3511
+ }
3512
+ }
3513
+ async function getStatusSummaryForActiveProvider() {
3170
3514
  const email = getCurrentAccount();
3171
3515
  let alias = null;
3172
3516
  let managed = false;
3173
- if (email !== "none" && existsSync10(activeSequenceFile)) {
3517
+ if (email !== "none" && existsSync9(activeSequenceFile)) {
3174
3518
  const seq = await loadSequence(activeSequenceFile);
3175
3519
  for (const account of Object.values(seq.accounts)) {
3176
3520
  if (account.email === email) {
@@ -3180,27 +3524,67 @@ async function cmdStatus(options) {
3180
3524
  }
3181
3525
  }
3182
3526
  }
3183
- if (jsonMode) {
3184
- console.log(JSON.stringify({
3185
- provider: activeProvider.name,
3186
- email: email === "none" ? null : email,
3187
- alias,
3188
- managed
3189
- }));
3190
- return;
3527
+ return { email, alias, managed };
3528
+ }
3529
+ async function withActiveProvider(provider, fn) {
3530
+ const previousProvider = activeProvider.name;
3531
+ setActiveProvider(provider);
3532
+ try {
3533
+ return await fn();
3534
+ } finally {
3535
+ setActiveProvider(previousProvider);
3191
3536
  }
3192
- if (email === "none") {
3193
- console.log("none");
3194
- } else {
3195
- if (alias) {
3196
- console.log(`${email} [${alias}]`);
3197
- return;
3537
+ }
3538
+ async function cmdListAllProviders() {
3539
+ for (const [index, provider] of SUPPORTED_PROVIDERS.entries()) {
3540
+ const output = await withActiveProvider(provider, async () => {
3541
+ const heading = getProviderLabel();
3542
+ const lines = await getManagedAccountLinesForActiveProvider();
3543
+ if (!lines) {
3544
+ return {
3545
+ heading,
3546
+ lines: [`No accounts managed yet. Run: caflip ${provider} add`]
3547
+ };
3548
+ }
3549
+ return {
3550
+ heading,
3551
+ lines: ["Accounts:", ...lines.map((line) => line.slice(2))]
3552
+ };
3553
+ });
3554
+ if (index > 0) {
3555
+ console.log("");
3198
3556
  }
3199
- console.log(email);
3557
+ console.log(output.heading);
3558
+ for (const line of output.lines) {
3559
+ console.log(` ${line}`);
3560
+ }
3561
+ }
3562
+ }
3563
+ async function cmdStatusAllProviders() {
3564
+ for (const [index, provider] of SUPPORTED_PROVIDERS.entries()) {
3565
+ const summary = await withActiveProvider(provider, async () => {
3566
+ return {
3567
+ heading: getProviderLabel(),
3568
+ ...await getStatusSummaryForActiveProvider()
3569
+ };
3570
+ });
3571
+ if (index > 0) {
3572
+ console.log("");
3573
+ }
3574
+ console.log(summary.heading);
3575
+ if (summary.email === "none") {
3576
+ console.log(" none");
3577
+ continue;
3578
+ }
3579
+ if (summary.alias) {
3580
+ console.log(` ${summary.email} [${summary.alias}]`);
3581
+ continue;
3582
+ }
3583
+ console.log(` ${summary.email}`);
3200
3584
  }
3201
3585
  }
3202
3586
  async function cmdAlias(alias, identifier) {
3203
- if (!existsSync10(activeSequenceFile)) {
3587
+ if (!existsSync9(activeSequenceFile)) {
3204
3588
  throw new Error("No accounts managed yet");
3205
3589
  }
3206
3590
  const result = validateAlias(alias);
@@ -3229,7 +3613,7 @@ async function cmdAlias(alias, identifier) {
3229
3613
  }
3230
3614
  async function cmdInteractiveSwitch() {
3231
3615
  const currentEmail = getCurrentAccount();
3232
- const hasSequence = existsSync10(activeSequenceFile);
3616
+ const hasSequence = existsSync9(activeSequenceFile);
3233
3617
  const seq = hasSequence ? await loadSequence(activeSequenceFile) : null;
3234
3618
  if (seq) {
3235
3619
  await syncSequenceActiveAccount(seq);
@@ -3267,10 +3651,13 @@ Usage:
3267
3651
 
3268
3652
  Commands:
3269
3653
  (no args) Interactive provider picker
3654
+ list List managed accounts for all providers
3655
+ status Show current account for all providers
3270
3656
  <provider> Interactive account picker for provider
3271
3657
  <provider> <alias> Switch by alias for provider
3272
3658
  <provider> list List all managed accounts
3273
3659
  <provider> add [--alias <name>] Add current account
3660
+ <provider> login [-- <args...>] Run provider login and register session
3274
3661
  <provider> remove [<email>] Remove an account
3275
3662
  <provider> next Rotate to next account
3276
3663
  <provider> status [--json] Show current account
@@ -3279,11 +3666,17 @@ Commands:
3279
3666
 
3280
3667
  Examples:
3281
3668
  caflip Pick provider interactively
3669
+ caflip list List managed accounts for Claude and Codex
3670
+ caflip status Show current account for Claude and Codex
3282
3671
  caflip claude Pick Claude account interactively
3283
3672
  caflip claude work Switch Claude account by alias
3284
3673
  caflip claude add --alias personal Add current Claude account with alias
3674
+ caflip claude login Run Claude login and register session
3675
+ caflip claude login -- --email me@example.com --sso
3676
+ Pass provider-specific flags after --
3285
3677
  caflip claude status --json Show Claude status as JSON
3286
3678
  caflip codex list List managed Codex accounts
3679
+ caflip codex login -- --device-auth Run Codex login and register session
3287
3680
  caflip codex add --alias work Add current Codex account with alias
3288
3681
  caflip codex alias work user@company.com
3289
3682
  Set Codex alias for target email`);
@@ -3312,11 +3705,10 @@ async function main() {
3312
3705
  return await runWithLock(fn);
3313
3706
  };
3314
3707
  const isHelpCommand = command === "help" || command === "--help" || command === "-h";
3315
- if (!parsed.isProviderQualified && command && !isHelpCommand) {
3708
+ const isProviderOptionalReadOnlyCommand = command === "list" || command === "status";
3709
+ if (!parsed.isProviderQualified && command && !isHelpCommand && !isProviderOptionalReadOnlyCommand) {
3316
3710
  if (RESERVED_COMMANDS.includes(command)) {
3317
- console.error("Error: Provider is required for non-interactive commands.");
3318
- console.error("Try: caflip claude list");
3319
- process.exit(2);
3711
+ showProviderRequiredError(command);
3320
3712
  } else {
3321
3713
  console.error("Error: Alias requires provider prefix.");
3322
3714
  console.error("Try: caflip claude <alias> or caflip codex <alias>");
@@ -3343,9 +3735,20 @@ async function main() {
3343
3735
  return;
3344
3736
  }
3345
3737
  if (!provider) {
3346
- console.error("Error: Provider is required for non-interactive commands.");
3347
- console.error("Try: caflip claude list");
3348
- process.exit(2);
3738
+ if (command === "list") {
3739
+ await cmdListAllProviders();
3740
+ return;
3741
+ }
3742
+ if (command === "status") {
3743
+ if (args.includes("--json")) {
3744
+ console.error("Error: Provider is required for status --json.");
3745
+ console.error("Try: caflip claude status --json");
3746
+ process.exit(2);
3747
+ }
3748
+ await cmdStatusAllProviders();
3749
+ return;
3750
+ }
3751
+ showProviderRequiredError(command);
3349
3752
  }
3350
3753
  setActiveProvider(provider);
3351
3754
  switch (command) {
@@ -3375,12 +3778,19 @@ async function main() {
3375
3778
  });
3376
3779
  break;
3377
3780
  }
3781
+ case "login": {
3782
+ validateLoginArgs(args.slice(1));
3783
+ await runWithLock(async () => {
3784
+ await cmdLogin(args.slice(1));
3785
+ });
3786
+ break;
3787
+ }
3378
3788
  case "status":
3379
3789
  await cmdStatus({ json: args.includes("--json") });
3380
3790
  break;
3381
3791
  case "alias": {
3382
3792
  if (!args[1]) {
3383
- console.error("Usage: caflip alias <name> [<email>]");
3793
+ console.error(`Usage: caflip ${provider} alias <name> [<email>]`);
3384
3794
  process.exit(1);
3385
3795
  }
3386
3796
  await runWithLock(async () => {
@@ -3389,7 +3799,7 @@ async function main() {
3389
3799
  break;
3390
3800
  }
3391
3801
  default: {
3392
- if (existsSync10(activeSequenceFile)) {
3802
+ if (existsSync9(activeSequenceFile)) {
3393
3803
  const seq = await loadSequence(activeSequenceFile);
3394
3804
  const accountNum = findAccountByAlias(seq, command);
3395
3805
  if (accountNum) {
package/install.sh CHANGED
@@ -3,7 +3,7 @@ set -e
3
3
 
4
4
  REPO="LucienLee/caflip"
5
5
  BINARY="caflip"
6
- INSTALL_DIR="/usr/local/bin"
6
+ INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
7
7
 
8
8
  OS=$(uname -s | tr '[:upper:]' '[:lower:]')
9
9
  ARCH=$(uname -m)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caflip",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "caflip": "bin/caflip"