caflip 0.3.1 → 0.4.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 +29 -7
- package/dist/cli.js +455 -106
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ bun run dev -- help
|
|
|
63
63
|
## Quick Start
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
# Show current account / managed accounts across both providers
|
|
66
|
+
# Show current active account / all managed accounts across both providers
|
|
67
67
|
caflip status
|
|
68
68
|
caflip list
|
|
69
69
|
|
|
@@ -110,7 +110,7 @@ After switching, restart the target CLI (Claude Code or Codex) to pick up new au
|
|
|
110
110
|
|---|---|
|
|
111
111
|
| `caflip` | Interactive provider picker (Claude/Codex) |
|
|
112
112
|
| `caflip list` | List managed accounts for Claude and Codex |
|
|
113
|
-
| `caflip status` | Show current account for Claude and Codex |
|
|
113
|
+
| `caflip status` | Show current active account for Claude and Codex |
|
|
114
114
|
| `caflip add [--alias name]` | Pick provider, then add current account |
|
|
115
115
|
| `caflip login [-- <args...>]` | Pick provider, then run provider login and register the resulting session |
|
|
116
116
|
| `caflip remove [email]` | Pick provider, then remove an account |
|
|
@@ -123,8 +123,8 @@ After switching, restart the target CLI (Claude Code or Codex) to pick up new au
|
|
|
123
123
|
| `caflip [provider] login [-- <args...>]` | Run provider login and register the resulting session |
|
|
124
124
|
| `caflip [provider] remove [email]` | Remove an account |
|
|
125
125
|
| `caflip [provider] next` | Rotate to next account |
|
|
126
|
-
| `caflip [provider] status` | Show current account |
|
|
127
|
-
| `caflip [provider] alias <name> [
|
|
126
|
+
| `caflip [provider] status` | Show current active account |
|
|
127
|
+
| `caflip [provider] alias <name> [account]` | Set alias for current or target account |
|
|
128
128
|
| `caflip help` | Show help |
|
|
129
129
|
|
|
130
130
|
### Alias Usage
|
|
@@ -133,13 +133,31 @@ After switching, restart the target CLI (Claude Code or Codex) to pick up new au
|
|
|
133
133
|
# Set alias for current active account
|
|
134
134
|
caflip claude alias work
|
|
135
135
|
|
|
136
|
-
# Set alias
|
|
137
|
-
caflip
|
|
136
|
+
# Set alias by list number
|
|
137
|
+
caflip codex list
|
|
138
|
+
# 1: me@example.com · team(org-ab12Cd)
|
|
139
|
+
# 2: me@example.com · team(org-xy98Qw)
|
|
140
|
+
caflip codex alias aibor 2
|
|
141
|
+
|
|
142
|
+
# Reuse an existing alias as the target
|
|
143
|
+
caflip claude alias primary work
|
|
138
144
|
|
|
139
|
-
#
|
|
145
|
+
# Email works only when it matches exactly one managed account
|
|
140
146
|
caflip codex alias work me@company.com
|
|
141
147
|
```
|
|
142
148
|
|
|
149
|
+
`<account>` accepts:
|
|
150
|
+
- the account number shown in `caflip [provider] list`
|
|
151
|
+
- an existing alias
|
|
152
|
+
- an email, only when that email matches exactly one managed account
|
|
153
|
+
|
|
154
|
+
If the same email exists in multiple workspaces or organizations, use the list number or an existing alias instead.
|
|
155
|
+
|
|
156
|
+
Codex display labels use provider metadata conservatively:
|
|
157
|
+
- workspace plans such as `team` or `business` show `email · plan(orgShortId)`
|
|
158
|
+
- `free` shows `email · free`
|
|
159
|
+
- alias is the primary human-readable name when you need your own team label
|
|
160
|
+
|
|
143
161
|
`add`, `remove`, and `login` can be used without a provider prefix. In that case, caflip asks you to choose Claude or Codex first, then continues the normal command flow.
|
|
144
162
|
|
|
145
163
|
`remove` target accepts email only. Omit it to choose from the interactive picker after selecting a provider.
|
|
@@ -153,6 +171,10 @@ caflip claude login -- --email lucien@aibor.io --sso
|
|
|
153
171
|
caflip codex login -- --device-auth
|
|
154
172
|
```
|
|
155
173
|
|
|
174
|
+
`status` shows the currently active account for the selected provider. It does not list every saved account.
|
|
175
|
+
|
|
176
|
+
Use `list` when you want to inspect all managed accounts for a provider.
|
|
177
|
+
|
|
156
178
|
## Shell Prompt Integration
|
|
157
179
|
|
|
158
180
|
Show the current account in your prompt:
|
package/dist/cli.js
CHANGED
|
@@ -186,8 +186,11 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
|
186
186
|
// src/config.ts
|
|
187
187
|
import { homedir } from "os";
|
|
188
188
|
import { join } from "path";
|
|
189
|
+
function getHomeDir() {
|
|
190
|
+
return process.env.HOME ?? homedir();
|
|
191
|
+
}
|
|
189
192
|
function getBackupDir(provider) {
|
|
190
|
-
return join(
|
|
193
|
+
return join(getHomeDir(), ".caflip-backup", provider);
|
|
191
194
|
}
|
|
192
195
|
function getSequenceFile(provider) {
|
|
193
196
|
return join(getBackupDir(provider), "sequence.json");
|
|
@@ -248,6 +251,43 @@ async function writeJsonAtomic(filePath, data) {
|
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
// src/accounts.ts
|
|
254
|
+
function getShortOrganizationId(organizationId) {
|
|
255
|
+
return organizationId.slice(0, 10);
|
|
256
|
+
}
|
|
257
|
+
function normalizeClaudeOrganizationName(email, organizationName) {
|
|
258
|
+
if (!organizationName) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (organizationName === `${email}'s Organization`) {
|
|
262
|
+
return "Personal";
|
|
263
|
+
}
|
|
264
|
+
return organizationName;
|
|
265
|
+
}
|
|
266
|
+
function getManagedAccountLabel(account) {
|
|
267
|
+
const provider = account.identity?.provider;
|
|
268
|
+
const organizationId = account.identity?.organizationId;
|
|
269
|
+
const organizationName = provider === "claude" ? normalizeClaudeOrganizationName(account.email, account.display?.organizationName) : account.display?.organizationName;
|
|
270
|
+
const planType = account.display?.planType;
|
|
271
|
+
if (provider === "codex") {
|
|
272
|
+
if (planType === "free") {
|
|
273
|
+
return `${account.email} · free`;
|
|
274
|
+
}
|
|
275
|
+
const orgShortId = organizationId ? getShortOrganizationId(organizationId) : null;
|
|
276
|
+
if (planType && orgShortId) {
|
|
277
|
+
return `${account.email} · ${planType}(${orgShortId})`;
|
|
278
|
+
}
|
|
279
|
+
if (orgShortId) {
|
|
280
|
+
return `${account.email} · ${orgShortId}`;
|
|
281
|
+
}
|
|
282
|
+
if (planType) {
|
|
283
|
+
return `${account.email} · ${planType}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (organizationName) {
|
|
287
|
+
return `${account.email} · ${organizationName}`;
|
|
288
|
+
}
|
|
289
|
+
return account.email;
|
|
290
|
+
}
|
|
251
291
|
async function initSequenceFile(path) {
|
|
252
292
|
if (existsSync2(path))
|
|
253
293
|
return;
|
|
@@ -260,7 +300,34 @@ async function initSequenceFile(path) {
|
|
|
260
300
|
await writeJsonAtomic(path, data);
|
|
261
301
|
}
|
|
262
302
|
async function loadSequence(path) {
|
|
263
|
-
|
|
303
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
304
|
+
const accounts = Object.fromEntries(Object.entries(raw.accounts).map(([num, account]) => {
|
|
305
|
+
const display = account.display ?? {
|
|
306
|
+
email: account.email,
|
|
307
|
+
accountName: null,
|
|
308
|
+
organizationName: null,
|
|
309
|
+
planType: null,
|
|
310
|
+
role: null,
|
|
311
|
+
label: account.email
|
|
312
|
+
};
|
|
313
|
+
display.label = getManagedAccountLabel({
|
|
314
|
+
email: account.email,
|
|
315
|
+
display,
|
|
316
|
+
identity: account.identity
|
|
317
|
+
});
|
|
318
|
+
return [
|
|
319
|
+
num,
|
|
320
|
+
{
|
|
321
|
+
...account,
|
|
322
|
+
display,
|
|
323
|
+
legacyUuid: account.legacyUuid ?? (account.identity ? undefined : account.uuid)
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
}));
|
|
327
|
+
return {
|
|
328
|
+
...raw,
|
|
329
|
+
accounts
|
|
330
|
+
};
|
|
264
331
|
}
|
|
265
332
|
function getNextAccountNumber(seq) {
|
|
266
333
|
const keys = Object.keys(seq.accounts).map(Number);
|
|
@@ -268,8 +335,11 @@ function getNextAccountNumber(seq) {
|
|
|
268
335
|
return 1;
|
|
269
336
|
return Math.max(...keys) + 1;
|
|
270
337
|
}
|
|
271
|
-
function accountExists(seq,
|
|
272
|
-
|
|
338
|
+
function accountExists(seq, identifier) {
|
|
339
|
+
if (typeof identifier === "string") {
|
|
340
|
+
return Object.values(seq.accounts).some((a) => a.email === identifier);
|
|
341
|
+
}
|
|
342
|
+
return resolveManagedAccount(seq, identifier) !== null;
|
|
273
343
|
}
|
|
274
344
|
function addAccountToSequence(seq, info) {
|
|
275
345
|
const num = getNextAccountNumber(seq);
|
|
@@ -282,6 +352,15 @@ function addAccountToSequence(seq, info) {
|
|
|
282
352
|
if (info.alias) {
|
|
283
353
|
account.alias = info.alias;
|
|
284
354
|
}
|
|
355
|
+
if (info.display) {
|
|
356
|
+
account.display = info.display;
|
|
357
|
+
}
|
|
358
|
+
if (info.identity) {
|
|
359
|
+
account.identity = info.identity;
|
|
360
|
+
}
|
|
361
|
+
if (info.providerMetadata) {
|
|
362
|
+
account.providerMetadata = info.providerMetadata;
|
|
363
|
+
}
|
|
285
364
|
return {
|
|
286
365
|
...seq,
|
|
287
366
|
accounts: { ...seq.accounts, [numStr]: account },
|
|
@@ -321,15 +400,35 @@ function getPostRemovalAction(original, updated, removedAccountNum) {
|
|
|
321
400
|
targetAccountNumber: String(updated.activeAccountNumber)
|
|
322
401
|
};
|
|
323
402
|
}
|
|
324
|
-
function
|
|
325
|
-
|
|
403
|
+
function resolveManagedAccountNumber(seq, currentAccount) {
|
|
404
|
+
const resolved = resolveManagedAccount(seq, currentAccount);
|
|
405
|
+
return resolved === null ? null : Number(resolved);
|
|
406
|
+
}
|
|
407
|
+
function resolveManagedAccount(seq, currentAccount) {
|
|
408
|
+
if (!currentAccount?.email || currentAccount.email === "none") {
|
|
326
409
|
return null;
|
|
327
410
|
}
|
|
328
|
-
|
|
329
|
-
|
|
411
|
+
if (currentAccount.uniqueKey) {
|
|
412
|
+
for (const [accountNum2, account2] of Object.entries(seq.accounts)) {
|
|
413
|
+
if (account2.identity?.uniqueKey === currentAccount.uniqueKey) {
|
|
414
|
+
return accountNum2;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const emailMatches = Object.entries(seq.accounts).filter(([, account2]) => {
|
|
419
|
+
if (account2.email !== currentAccount.email) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (!currentAccount.provider) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return !account2.identity || account2.identity.provider === currentAccount.provider;
|
|
426
|
+
});
|
|
427
|
+
if (emailMatches.length !== 1) {
|
|
330
428
|
return null;
|
|
331
429
|
}
|
|
332
|
-
|
|
430
|
+
const [accountNum, account] = emailMatches[0];
|
|
431
|
+
return account.identity ? null : accountNum;
|
|
333
432
|
}
|
|
334
433
|
function getNextInSequence(seq) {
|
|
335
434
|
const currentIndex = seq.sequence.indexOf(seq.activeAccountNumber);
|
|
@@ -347,27 +446,29 @@ function resolveAccountIdentifier(seq, identifier) {
|
|
|
347
446
|
}
|
|
348
447
|
return null;
|
|
349
448
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
449
|
+
const emailMatches = Object.entries(seq.accounts).filter(([, account]) => account.email === identifier);
|
|
450
|
+
if (emailMatches.length === 1) {
|
|
451
|
+
return emailMatches[0][0];
|
|
353
452
|
}
|
|
354
453
|
return null;
|
|
355
454
|
}
|
|
356
|
-
function
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
for (const [num, account] of Object.entries(seq.accounts)) {
|
|
362
|
-
if (account.email === options.identifier)
|
|
363
|
-
return num;
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
455
|
+
function resolveAccountTarget(seq, identifier) {
|
|
456
|
+
if (/^\d+$/.test(identifier)) {
|
|
457
|
+
const resolved = resolveAccountIdentifier(seq, identifier);
|
|
458
|
+
return resolved ? { status: "resolved", accountNum: resolved } : { status: "missing" };
|
|
366
459
|
}
|
|
367
|
-
|
|
368
|
-
|
|
460
|
+
const aliasMatch = findAccountByAlias(seq, identifier);
|
|
461
|
+
if (aliasMatch) {
|
|
462
|
+
return { status: "resolved", accountNum: aliasMatch };
|
|
463
|
+
}
|
|
464
|
+
const emailMatches = Object.entries(seq.accounts).filter(([, account]) => account.email === identifier).map(([accountNum]) => accountNum);
|
|
465
|
+
if (emailMatches.length === 1) {
|
|
466
|
+
return { status: "resolved", accountNum: emailMatches[0] };
|
|
369
467
|
}
|
|
370
|
-
|
|
468
|
+
if (emailMatches.length > 1) {
|
|
469
|
+
return { status: "ambiguous", matches: emailMatches };
|
|
470
|
+
}
|
|
471
|
+
return { status: "missing" };
|
|
371
472
|
}
|
|
372
473
|
function getDisplayAccountNumber(seq, accountNum) {
|
|
373
474
|
const idx = seq.sequence.indexOf(Number(accountNum));
|
|
@@ -510,8 +611,11 @@ function isProcessAlive(pid) {
|
|
|
510
611
|
import { homedir as homedir2 } from "os";
|
|
511
612
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
512
613
|
import { join as join4 } from "path";
|
|
614
|
+
function getHomeDir2() {
|
|
615
|
+
return process.env.HOME ?? homedir2();
|
|
616
|
+
}
|
|
513
617
|
function getBackupDir2(provider) {
|
|
514
|
-
return join4(
|
|
618
|
+
return join4(getHomeDir2(), ".caflip-backup", provider);
|
|
515
619
|
}
|
|
516
620
|
function getSequenceFile2(provider) {
|
|
517
621
|
return join4(getBackupDir2(provider), "sequence.json");
|
|
@@ -554,14 +658,14 @@ function detectPlatform() {
|
|
|
554
658
|
return "unknown";
|
|
555
659
|
}
|
|
556
660
|
}
|
|
557
|
-
function getClaudeConfigDir(env = process.env, home = homedir2()) {
|
|
661
|
+
function getClaudeConfigDir(env = process.env, home = env.HOME ?? homedir2()) {
|
|
558
662
|
const customDir = env.CLAUDE_CONFIG_DIR?.trim();
|
|
559
663
|
if (customDir) {
|
|
560
664
|
return customDir;
|
|
561
665
|
}
|
|
562
666
|
return join4(home, ".claude");
|
|
563
667
|
}
|
|
564
|
-
function getClaudeConfigPath(env = process.env, home = homedir2()) {
|
|
668
|
+
function getClaudeConfigPath(env = process.env, home = env.HOME ?? homedir2()) {
|
|
565
669
|
const customDir = env.CLAUDE_CONFIG_DIR?.trim();
|
|
566
670
|
if (customDir) {
|
|
567
671
|
return join4(customDir, ".claude.json");
|
|
@@ -617,7 +721,7 @@ function validateAlias(alias) {
|
|
|
617
721
|
// package.json
|
|
618
722
|
var package_default = {
|
|
619
723
|
name: "caflip",
|
|
620
|
-
version: "0.
|
|
724
|
+
version: "0.4.0",
|
|
621
725
|
type: "module",
|
|
622
726
|
bin: {
|
|
623
727
|
caflip: "bin/caflip"
|
|
@@ -2258,6 +2362,45 @@ var dist_default5 = createPrompt((config, done) => {
|
|
|
2258
2362
|
`).trimEnd();
|
|
2259
2363
|
return `${lines}${cursorHide}`;
|
|
2260
2364
|
});
|
|
2365
|
+
// src/accounts.ts
|
|
2366
|
+
function getShortOrganizationId2(organizationId) {
|
|
2367
|
+
return organizationId.slice(0, 10);
|
|
2368
|
+
}
|
|
2369
|
+
function normalizeClaudeOrganizationName2(email, organizationName) {
|
|
2370
|
+
if (!organizationName) {
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
if (organizationName === `${email}'s Organization`) {
|
|
2374
|
+
return "Personal";
|
|
2375
|
+
}
|
|
2376
|
+
return organizationName;
|
|
2377
|
+
}
|
|
2378
|
+
function getManagedAccountLabel2(account) {
|
|
2379
|
+
const provider = account.identity?.provider;
|
|
2380
|
+
const organizationId = account.identity?.organizationId;
|
|
2381
|
+
const organizationName = provider === "claude" ? normalizeClaudeOrganizationName2(account.email, account.display?.organizationName) : account.display?.organizationName;
|
|
2382
|
+
const planType = account.display?.planType;
|
|
2383
|
+
if (provider === "codex") {
|
|
2384
|
+
if (planType === "free") {
|
|
2385
|
+
return `${account.email} · free`;
|
|
2386
|
+
}
|
|
2387
|
+
const orgShortId = organizationId ? getShortOrganizationId2(organizationId) : null;
|
|
2388
|
+
if (planType && orgShortId) {
|
|
2389
|
+
return `${account.email} · ${planType}(${orgShortId})`;
|
|
2390
|
+
}
|
|
2391
|
+
if (orgShortId) {
|
|
2392
|
+
return `${account.email} · ${orgShortId}`;
|
|
2393
|
+
}
|
|
2394
|
+
if (planType) {
|
|
2395
|
+
return `${account.email} · ${planType}`;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (organizationName) {
|
|
2399
|
+
return `${account.email} · ${organizationName}`;
|
|
2400
|
+
}
|
|
2401
|
+
return account.email;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2261
2404
|
// src/interactive.ts
|
|
2262
2405
|
class PromptCancelledError extends Error {
|
|
2263
2406
|
constructor() {
|
|
@@ -2303,13 +2446,13 @@ async function wrapPromptCancellation(fn) {
|
|
|
2303
2446
|
cleanup();
|
|
2304
2447
|
}
|
|
2305
2448
|
}
|
|
2306
|
-
function formatAccount(num,
|
|
2307
|
-
let
|
|
2449
|
+
function formatAccount(num, label, alias, isActive) {
|
|
2450
|
+
let formatted = `${num}: ${label}`;
|
|
2308
2451
|
if (alias)
|
|
2309
|
-
|
|
2452
|
+
formatted += ` [${alias}]`;
|
|
2310
2453
|
if (isActive)
|
|
2311
|
-
|
|
2312
|
-
return
|
|
2454
|
+
formatted += " (active)";
|
|
2455
|
+
return formatted;
|
|
2313
2456
|
}
|
|
2314
2457
|
async function pickAccount(seq, message = "Switch to account:", promptSelect = dist_default5, extraChoices = []) {
|
|
2315
2458
|
const choices = [
|
|
@@ -2321,7 +2464,7 @@ async function pickAccount(seq, message = "Switch to account:", promptSelect = d
|
|
|
2321
2464
|
}
|
|
2322
2465
|
const isActive = num === seq.activeAccountNumber;
|
|
2323
2466
|
return {
|
|
2324
|
-
name: formatAccount(String(index + 1), account
|
|
2467
|
+
name: formatAccount(String(index + 1), getManagedAccountLabel2(account), account.alias, isActive),
|
|
2325
2468
|
value: numStr
|
|
2326
2469
|
};
|
|
2327
2470
|
}),
|
|
@@ -2482,14 +2625,14 @@ function activeSecretToolAttrs() {
|
|
|
2482
2625
|
function backupSecretToolAttrs(accountNum, email) {
|
|
2483
2626
|
return ["service", "ccflip", "account", accountNum, "email", email];
|
|
2484
2627
|
}
|
|
2485
|
-
function getClaudeCredentialsDir(env = process.env, home = homedir3()) {
|
|
2628
|
+
function getClaudeCredentialsDir(env = process.env, home = env.HOME ?? homedir3()) {
|
|
2486
2629
|
const customDir = env.CLAUDE_CONFIG_DIR?.trim();
|
|
2487
2630
|
if (customDir) {
|
|
2488
2631
|
return customDir;
|
|
2489
2632
|
}
|
|
2490
2633
|
return join5(home, ".claude");
|
|
2491
2634
|
}
|
|
2492
|
-
function getClaudeCredentialsPath(env = process.env, home = homedir3()) {
|
|
2635
|
+
function getClaudeCredentialsPath(env = process.env, home = env.HOME ?? homedir3()) {
|
|
2493
2636
|
return join5(getClaudeCredentialsDir(env, home), ".credentials.json");
|
|
2494
2637
|
}
|
|
2495
2638
|
async function secretToolLookup(attrs) {
|
|
@@ -2818,7 +2961,21 @@ function getClaudeCurrentAccount() {
|
|
|
2818
2961
|
return null;
|
|
2819
2962
|
}
|
|
2820
2963
|
const accountId = typeof content?.oauthAccount?.accountUuid === "string" ? content.oauthAccount.accountUuid : undefined;
|
|
2821
|
-
|
|
2964
|
+
const organizationId = typeof content?.oauthAccount?.organizationUuid === "string" ? content.oauthAccount.organizationUuid : undefined;
|
|
2965
|
+
const organizationName = typeof content?.oauthAccount?.organizationName === "string" ? content.oauthAccount.organizationName : undefined;
|
|
2966
|
+
const workspaceRole = typeof content?.oauthAccount?.workspaceRole === "string" ? content.oauthAccount.workspaceRole : undefined;
|
|
2967
|
+
const organizationRole = typeof content?.oauthAccount?.organizationRole === "string" ? content.oauthAccount.organizationRole : undefined;
|
|
2968
|
+
const accountName = typeof content?.oauthAccount?.displayName === "string" ? content.oauthAccount.displayName : undefined;
|
|
2969
|
+
return {
|
|
2970
|
+
email,
|
|
2971
|
+
accountId,
|
|
2972
|
+
organizationId,
|
|
2973
|
+
organizationName,
|
|
2974
|
+
role: workspaceRole ?? organizationRole,
|
|
2975
|
+
accountName,
|
|
2976
|
+
uniqueKey: accountId && organizationId ? `claude:${accountId}:${organizationId}` : undefined,
|
|
2977
|
+
identityStatus: accountId && organizationId ? "resolved" : "partial"
|
|
2978
|
+
};
|
|
2822
2979
|
}
|
|
2823
2980
|
function getClaudeCurrentAccountEmail() {
|
|
2824
2981
|
return getClaudeCurrentAccount()?.email ?? "none";
|
|
@@ -3012,35 +3169,50 @@ async function deleteCodexAccountAuthBackup(accountNum, email, credentialsDir) {
|
|
|
3012
3169
|
const backupPath = join6(credentialsDir, `.codex-auth-${accountNum}-${email}.json`);
|
|
3013
3170
|
rmSync4(backupPath, { force: true });
|
|
3014
3171
|
}
|
|
3015
|
-
function
|
|
3172
|
+
function resolveCodexCurrentAccount() {
|
|
3016
3173
|
const authPath = getCodexAuthPath();
|
|
3017
3174
|
if (!existsSync7(authPath)) {
|
|
3018
|
-
return null;
|
|
3175
|
+
return { account: null, ambiguousOrganization: false };
|
|
3019
3176
|
}
|
|
3020
3177
|
try {
|
|
3021
3178
|
const authObj = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
3022
3179
|
const idToken = authObj.tokens?.id_token;
|
|
3023
3180
|
if (!idToken) {
|
|
3024
|
-
return null;
|
|
3181
|
+
return { account: null, ambiguousOrganization: false };
|
|
3025
3182
|
}
|
|
3026
3183
|
const payload = decodeJwtPayload(idToken);
|
|
3027
3184
|
if (!payload) {
|
|
3028
|
-
return null;
|
|
3185
|
+
return { account: null, ambiguousOrganization: false };
|
|
3029
3186
|
}
|
|
3030
3187
|
const email = typeof payload.email === "string" ? payload.email : null;
|
|
3031
3188
|
if (!email) {
|
|
3032
|
-
return null;
|
|
3189
|
+
return { account: null, ambiguousOrganization: false };
|
|
3033
3190
|
}
|
|
3034
3191
|
const authPayload = payload["https://api.openai.com/auth"];
|
|
3035
3192
|
const accountId = authPayload?.chatgpt_account_id ?? authObj.tokens?.account_id;
|
|
3193
|
+
const organizations = Array.isArray(authPayload?.organizations) ? authPayload.organizations : [];
|
|
3194
|
+
const organization = organizations.find((candidate) => candidate.is_default === true) ?? (organizations.length === 1 ? organizations[0] : undefined);
|
|
3195
|
+
const ambiguousOrganization = organizations.length > 1 && !organization;
|
|
3036
3196
|
return {
|
|
3037
|
-
|
|
3038
|
-
|
|
3197
|
+
ambiguousOrganization,
|
|
3198
|
+
account: {
|
|
3199
|
+
email,
|
|
3200
|
+
accountId,
|
|
3201
|
+
organizationId: organization?.id,
|
|
3202
|
+
organizationName: organization?.title,
|
|
3203
|
+
planType: authPayload?.chatgpt_plan_type,
|
|
3204
|
+
role: organization?.role,
|
|
3205
|
+
uniqueKey: accountId && organization?.id ? `codex:${accountId}:${organization.id}` : undefined,
|
|
3206
|
+
identityStatus: ambiguousOrganization ? "ambiguous" : accountId && organization?.id ? "resolved" : "partial"
|
|
3207
|
+
}
|
|
3039
3208
|
};
|
|
3040
3209
|
} catch {
|
|
3041
|
-
return null;
|
|
3210
|
+
return { account: null, ambiguousOrganization: false };
|
|
3042
3211
|
}
|
|
3043
3212
|
}
|
|
3213
|
+
function getCodexCurrentAccount() {
|
|
3214
|
+
return resolveCodexCurrentAccount().account;
|
|
3215
|
+
}
|
|
3044
3216
|
function readCodexAuthFile() {
|
|
3045
3217
|
const authPath = getCodexAuthPath();
|
|
3046
3218
|
if (!existsSync7(authPath)) {
|
|
@@ -3085,17 +3257,28 @@ async function verifyCodexLogin(commandRunner = runCapturedCommand) {
|
|
|
3085
3257
|
}
|
|
3086
3258
|
};
|
|
3087
3259
|
}
|
|
3088
|
-
const currentAccount =
|
|
3260
|
+
const { account: currentAccount, ambiguousOrganization } = resolveCodexCurrentAccount();
|
|
3089
3261
|
if (!currentAccount?.email) {
|
|
3090
3262
|
return {
|
|
3091
3263
|
ok: false,
|
|
3092
3264
|
reason: "codex auth file did not resolve a current account email"
|
|
3093
3265
|
};
|
|
3094
3266
|
}
|
|
3267
|
+
if (ambiguousOrganization) {
|
|
3268
|
+
return {
|
|
3269
|
+
ok: false,
|
|
3270
|
+
reason: "codex login resolved an ambiguous workspace context: multiple organizations without a default workspace"
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3095
3273
|
return {
|
|
3096
3274
|
ok: true,
|
|
3097
3275
|
email: currentAccount.email,
|
|
3098
|
-
details:
|
|
3276
|
+
details: {
|
|
3277
|
+
accountId: currentAccount.accountId,
|
|
3278
|
+
organizationId: currentAccount.organizationId,
|
|
3279
|
+
organizationName: currentAccount.organizationName,
|
|
3280
|
+
planType: currentAccount.planType
|
|
3281
|
+
}
|
|
3099
3282
|
};
|
|
3100
3283
|
}
|
|
3101
3284
|
var codexLoginAdapter = {
|
|
@@ -3186,12 +3369,115 @@ function setupDirectories() {
|
|
|
3186
3369
|
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
3187
3370
|
}
|
|
3188
3371
|
}
|
|
3372
|
+
function getCurrentAccountIdentity() {
|
|
3373
|
+
return activeProvider.getCurrentAccount();
|
|
3374
|
+
}
|
|
3189
3375
|
function getCurrentAccount() {
|
|
3190
|
-
return
|
|
3376
|
+
return getCurrentAccountIdentity()?.email ?? "none";
|
|
3191
3377
|
}
|
|
3192
3378
|
function getProviderLabel() {
|
|
3193
3379
|
return activeProvider.name === "codex" ? "Codex" : "Claude Code";
|
|
3194
3380
|
}
|
|
3381
|
+
function hasMultipleProviderEmailMatches(seq, email, provider) {
|
|
3382
|
+
return Object.values(seq.accounts).filter((account) => {
|
|
3383
|
+
if (account.email !== email) {
|
|
3384
|
+
return false;
|
|
3385
|
+
}
|
|
3386
|
+
return !account.identity || account.identity.provider === provider;
|
|
3387
|
+
}).length > 1;
|
|
3388
|
+
}
|
|
3389
|
+
function resolveCurrentManagedAccountForSwitch(seq, currentIdentity, currentEmail) {
|
|
3390
|
+
if (currentIdentity) {
|
|
3391
|
+
const resolved = resolveManagedAccount(seq, currentIdentity);
|
|
3392
|
+
if (resolved) {
|
|
3393
|
+
return resolved;
|
|
3394
|
+
}
|
|
3395
|
+
const providerEmailMatchCount = Object.values(seq.accounts).filter((account) => {
|
|
3396
|
+
if (account.email !== currentIdentity.email) {
|
|
3397
|
+
return false;
|
|
3398
|
+
}
|
|
3399
|
+
return !account.identity || account.identity.provider === activeProvider.name;
|
|
3400
|
+
}).length;
|
|
3401
|
+
if (currentIdentity.identityStatus === "ambiguous") {
|
|
3402
|
+
throw new Error(`Cannot determine which managed account is currently active for ${currentIdentity.email}.`);
|
|
3403
|
+
}
|
|
3404
|
+
if (currentIdentity.email !== "none" && providerEmailMatchCount > 0) {
|
|
3405
|
+
throw new Error(`Cannot determine which managed account is currently active for ${currentIdentity.email}.`);
|
|
3406
|
+
}
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
if (currentEmail === "none") {
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
return resolveAccountIdentifier(seq, currentEmail);
|
|
3413
|
+
}
|
|
3414
|
+
function getAccountDisplayLabel(account) {
|
|
3415
|
+
return getManagedAccountLabel(account);
|
|
3416
|
+
}
|
|
3417
|
+
function getCurrentAccountDisplayLabel(currentAccount) {
|
|
3418
|
+
if (!currentAccount) {
|
|
3419
|
+
return "none";
|
|
3420
|
+
}
|
|
3421
|
+
return getManagedAccountLabel({
|
|
3422
|
+
email: currentAccount.email,
|
|
3423
|
+
display: {
|
|
3424
|
+
email: currentAccount.email,
|
|
3425
|
+
accountName: currentAccount.accountName ?? null,
|
|
3426
|
+
organizationName: currentAccount.organizationName ?? null,
|
|
3427
|
+
planType: currentAccount.planType ?? null,
|
|
3428
|
+
role: currentAccount.role ?? null,
|
|
3429
|
+
label: ""
|
|
3430
|
+
},
|
|
3431
|
+
identity: {
|
|
3432
|
+
provider: activeProvider.name,
|
|
3433
|
+
accountId: currentAccount.accountId ?? null,
|
|
3434
|
+
organizationId: currentAccount.organizationId ?? null,
|
|
3435
|
+
uniqueKey: ""
|
|
3436
|
+
}
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
function buildManagedAccountDetails(currentAccount) {
|
|
3440
|
+
if (!currentAccount) {
|
|
3441
|
+
return {
|
|
3442
|
+
email: "none",
|
|
3443
|
+
uuid: "",
|
|
3444
|
+
display: {
|
|
3445
|
+
email: "none",
|
|
3446
|
+
accountName: null,
|
|
3447
|
+
organizationName: null,
|
|
3448
|
+
planType: null,
|
|
3449
|
+
role: null,
|
|
3450
|
+
label: "none"
|
|
3451
|
+
},
|
|
3452
|
+
identity: undefined,
|
|
3453
|
+
providerMetadata: undefined
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
return {
|
|
3457
|
+
email: currentAccount.email,
|
|
3458
|
+
uuid: currentAccount.accountId ?? currentAccount.uniqueKey ?? "",
|
|
3459
|
+
display: {
|
|
3460
|
+
email: currentAccount.email,
|
|
3461
|
+
accountName: currentAccount.accountName ?? null,
|
|
3462
|
+
organizationName: currentAccount.organizationName ?? null,
|
|
3463
|
+
planType: currentAccount.planType ?? null,
|
|
3464
|
+
role: currentAccount.role ?? null,
|
|
3465
|
+
label: getCurrentAccountDisplayLabel(currentAccount)
|
|
3466
|
+
},
|
|
3467
|
+
identity: currentAccount.uniqueKey ? {
|
|
3468
|
+
provider: activeProvider.name,
|
|
3469
|
+
accountId: currentAccount.accountId ?? null,
|
|
3470
|
+
organizationId: currentAccount.organizationId ?? null,
|
|
3471
|
+
uniqueKey: currentAccount.uniqueKey
|
|
3472
|
+
} : undefined,
|
|
3473
|
+
providerMetadata: {
|
|
3474
|
+
organizationName: currentAccount.organizationName ?? null,
|
|
3475
|
+
planType: currentAccount.planType ?? null,
|
|
3476
|
+
role: currentAccount.role ?? null,
|
|
3477
|
+
accountName: currentAccount.accountName ?? null
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3195
3481
|
function showProviderRequiredError(command) {
|
|
3196
3482
|
console.error(`Error: ${command} requires provider prefix.`);
|
|
3197
3483
|
console.error(`Try: caflip claude ${command} or caflip codex ${command}`);
|
|
@@ -3268,8 +3554,7 @@ async function resolveCliContext(parsed, deps = { resolveProviderForCommand }) {
|
|
|
3268
3554
|
};
|
|
3269
3555
|
}
|
|
3270
3556
|
async function syncSequenceActiveAccount(seq) {
|
|
3271
|
-
const
|
|
3272
|
-
const resolvedActive = resolveManagedAccountNumberForEmail(seq, currentEmail);
|
|
3557
|
+
const resolvedActive = resolveManagedAccountNumber(seq, getCurrentAccountIdentity());
|
|
3273
3558
|
if (seq.activeAccountNumber !== resolvedActive) {
|
|
3274
3559
|
seq.activeAccountNumber = resolvedActive;
|
|
3275
3560
|
seq.lastUpdated = new Date().toISOString();
|
|
@@ -3289,22 +3574,26 @@ async function registerCurrentActiveAccount(options) {
|
|
|
3289
3574
|
if (options?.expectedEmail && currentEmail !== options.expectedEmail) {
|
|
3290
3575
|
throw new Error(`Active ${getProviderLabel()} account changed during login verification: expected ${options.expectedEmail}, got ${currentEmail}`);
|
|
3291
3576
|
}
|
|
3577
|
+
if (currentAccount?.identityStatus === "ambiguous") {
|
|
3578
|
+
throw new Error(`${getProviderLabel()} current account is in an ambiguous workspace context. Please pick a single workspace first, then retry.`);
|
|
3579
|
+
}
|
|
3292
3580
|
setupDirectories();
|
|
3293
3581
|
await initSequenceFile(activeSequenceFile);
|
|
3294
3582
|
const seq = await loadSequence(activeSequenceFile);
|
|
3295
3583
|
await syncSequenceActiveAccount(seq);
|
|
3584
|
+
const currentAccountNum = resolveManagedAccount(seq, currentAccount);
|
|
3585
|
+
const currentDetails = buildManagedAccountDetails(currentAccount);
|
|
3296
3586
|
if (options?.alias) {
|
|
3297
3587
|
const result = validateAlias(options.alias);
|
|
3298
3588
|
if (!result.valid) {
|
|
3299
3589
|
throw new Error(result.reason);
|
|
3300
3590
|
}
|
|
3301
3591
|
const existingAliasTarget = findAccountByAlias(seq, options.alias);
|
|
3302
|
-
const currentAccountNum = resolveAccountIdentifier(seq, currentEmail);
|
|
3303
3592
|
if (existingAliasTarget && existingAliasTarget !== currentAccountNum) {
|
|
3304
3593
|
throw new Error(`Alias "${options.alias}" is already in use`);
|
|
3305
3594
|
}
|
|
3306
3595
|
}
|
|
3307
|
-
const existingAccountNum =
|
|
3596
|
+
const existingAccountNum = currentAccountNum;
|
|
3308
3597
|
if (existingAccountNum) {
|
|
3309
3598
|
if (!options?.updateIfExists) {
|
|
3310
3599
|
console.log(`Account ${currentEmail} is already managed.`);
|
|
@@ -3333,7 +3622,11 @@ async function registerCurrentActiveAccount(options) {
|
|
|
3333
3622
|
...seq.accounts,
|
|
3334
3623
|
[existingAccountNum]: {
|
|
3335
3624
|
...seq.accounts[existingAccountNum],
|
|
3625
|
+
email: currentEmail,
|
|
3336
3626
|
uuid,
|
|
3627
|
+
identity: currentDetails.identity ?? seq.accounts[existingAccountNum].identity,
|
|
3628
|
+
display: currentDetails.display,
|
|
3629
|
+
providerMetadata: currentDetails.providerMetadata ?? seq.accounts[existingAccountNum].providerMetadata,
|
|
3337
3630
|
...options?.alias ? { alias: options.alias } : {}
|
|
3338
3631
|
}
|
|
3339
3632
|
}
|
|
@@ -3352,7 +3645,10 @@ async function registerCurrentActiveAccount(options) {
|
|
|
3352
3645
|
const updated = addAccountToSequence(seq, {
|
|
3353
3646
|
email: currentEmail,
|
|
3354
3647
|
uuid,
|
|
3355
|
-
alias: options?.alias
|
|
3648
|
+
alias: options?.alias,
|
|
3649
|
+
identity: currentDetails.identity,
|
|
3650
|
+
display: currentDetails.display,
|
|
3651
|
+
providerMetadata: currentDetails.providerMetadata
|
|
3356
3652
|
});
|
|
3357
3653
|
const accountNum = String(updated.activeAccountNumber);
|
|
3358
3654
|
await activeProvider.writeAccountAuth(accountNum, currentEmail, creds, activeCredentialsDir);
|
|
@@ -3380,17 +3676,27 @@ function getLoginPassthroughArgs(args) {
|
|
|
3380
3676
|
return args.slice(passthroughIdx + 1);
|
|
3381
3677
|
}
|
|
3382
3678
|
async function performSwitch(seq, targetAccount, options) {
|
|
3679
|
+
const targetProvider = seq.accounts[targetAccount].identity?.provider;
|
|
3680
|
+
if (targetProvider) {
|
|
3681
|
+
setActiveProvider(targetProvider);
|
|
3682
|
+
}
|
|
3683
|
+
const providerName = targetProvider ?? activeProvider.name;
|
|
3684
|
+
const sequenceFile = getSequenceFile(providerName);
|
|
3685
|
+
const credentialsDir = getCredentialsDir(providerName);
|
|
3686
|
+
const configsDir = getConfigsDir(providerName);
|
|
3383
3687
|
const targetEmail = seq.accounts[targetAccount].email;
|
|
3384
|
-
const
|
|
3385
|
-
const
|
|
3386
|
-
|
|
3688
|
+
const observedCurrentIdentity = getCurrentAccountIdentity();
|
|
3689
|
+
const currentIdentity = options?.currentEmail && observedCurrentIdentity && observedCurrentIdentity.email !== options.currentEmail ? null : observedCurrentIdentity;
|
|
3690
|
+
const currentEmail = options?.currentEmail ?? currentIdentity?.email ?? "none";
|
|
3691
|
+
const currentAccount = resolveCurrentManagedAccountForSwitch(seq, currentIdentity, currentEmail);
|
|
3692
|
+
if (currentAccount === targetAccount) {
|
|
3387
3693
|
const account = seq.accounts[targetAccount];
|
|
3388
3694
|
const aliasStr2 = account.alias ? ` [${account.alias}]` : "";
|
|
3389
3695
|
const displayLabel2 = getDisplayAccountLabel(seq, targetAccount);
|
|
3390
3696
|
if (seq.activeAccountNumber !== Number(targetAccount)) {
|
|
3391
3697
|
seq.activeAccountNumber = Number(targetAccount);
|
|
3392
3698
|
seq.lastUpdated = new Date().toISOString();
|
|
3393
|
-
await writeJsonAtomic2(
|
|
3699
|
+
await writeJsonAtomic2(sequenceFile, seq);
|
|
3394
3700
|
}
|
|
3395
3701
|
console.log(`Already using ${displayLabel2} (${account.email})${aliasStr2}`);
|
|
3396
3702
|
return;
|
|
@@ -3404,17 +3710,17 @@ async function performSwitch(seq, targetAccount, options) {
|
|
|
3404
3710
|
if (currentEmail !== "none" && currentAccount) {
|
|
3405
3711
|
const currentCreds = await activeProvider.readActiveAuth();
|
|
3406
3712
|
if (currentCreds) {
|
|
3407
|
-
await activeProvider.writeAccountAuth(currentAccount, currentEmail, currentCreds,
|
|
3713
|
+
await activeProvider.writeAccountAuth(currentAccount, currentEmail, currentCreds, credentialsDir);
|
|
3408
3714
|
}
|
|
3409
3715
|
if (activeProvider.usesAccountConfig) {
|
|
3410
3716
|
const currentConfig = await activeProvider.readActiveConfig();
|
|
3411
3717
|
if (currentConfig) {
|
|
3412
|
-
await activeProvider.writeAccountConfig(currentAccount, currentEmail, currentConfig,
|
|
3718
|
+
await activeProvider.writeAccountConfig(currentAccount, currentEmail, currentConfig, configsDir);
|
|
3413
3719
|
}
|
|
3414
3720
|
}
|
|
3415
3721
|
}
|
|
3416
|
-
const targetCreds = await activeProvider.readAccountAuth(targetAccount, targetEmail,
|
|
3417
|
-
const targetConfig = activeProvider.readAccountConfig(targetAccount, targetEmail,
|
|
3722
|
+
const targetCreds = await activeProvider.readAccountAuth(targetAccount, targetEmail, credentialsDir);
|
|
3723
|
+
const targetConfig = activeProvider.readAccountConfig(targetAccount, targetEmail, configsDir);
|
|
3418
3724
|
if (!targetCreds) {
|
|
3419
3725
|
throw new Error(`Missing backup data for ${getDisplayAccountLabel(seq, targetAccount)}`);
|
|
3420
3726
|
}
|
|
@@ -3427,10 +3733,23 @@ async function performSwitch(seq, targetAccount, options) {
|
|
|
3427
3733
|
}
|
|
3428
3734
|
seq.activeAccountNumber = Number(targetAccount);
|
|
3429
3735
|
seq.lastUpdated = new Date().toISOString();
|
|
3430
|
-
await writeJsonAtomic2(
|
|
3736
|
+
await writeJsonAtomic2(sequenceFile, seq);
|
|
3431
3737
|
const alias = seq.accounts[targetAccount].alias;
|
|
3432
3738
|
const aliasStr = alias ? ` [${alias}]` : "";
|
|
3433
3739
|
const displayLabel = getDisplayAccountLabel(seq, targetAccount);
|
|
3740
|
+
const refreshedAccount = activeProvider.getCurrentAccount();
|
|
3741
|
+
if (refreshedAccount) {
|
|
3742
|
+
const refreshedDetails = buildManagedAccountDetails(refreshedAccount);
|
|
3743
|
+
seq.accounts[targetAccount] = {
|
|
3744
|
+
...seq.accounts[targetAccount],
|
|
3745
|
+
email: refreshedDetails.email,
|
|
3746
|
+
uuid: refreshedDetails.uuid,
|
|
3747
|
+
display: refreshedDetails.display,
|
|
3748
|
+
identity: refreshedDetails.identity ?? seq.accounts[targetAccount].identity,
|
|
3749
|
+
providerMetadata: refreshedDetails.providerMetadata ?? seq.accounts[targetAccount].providerMetadata
|
|
3750
|
+
};
|
|
3751
|
+
await writeJsonAtomic2(sequenceFile, seq);
|
|
3752
|
+
}
|
|
3434
3753
|
console.log(`Switched to ${displayLabel} (${targetEmail})${aliasStr}`);
|
|
3435
3754
|
console.log(`
|
|
3436
3755
|
Please restart ${getProviderLabel()} to use the new authentication.
|
|
@@ -3454,15 +3773,16 @@ async function getManagedAccountLinesForActiveProvider() {
|
|
|
3454
3773
|
}
|
|
3455
3774
|
const seq = await loadSequence(activeSequenceFile);
|
|
3456
3775
|
await syncSequenceActiveAccount(seq);
|
|
3457
|
-
const
|
|
3776
|
+
const currentAccount = getCurrentAccountIdentity();
|
|
3777
|
+
const activeAccountNum = resolveManagedAccount(seq, currentAccount);
|
|
3458
3778
|
return seq.sequence.map((num, index) => {
|
|
3459
3779
|
const numStr = String(num);
|
|
3460
3780
|
const account = seq.accounts[numStr];
|
|
3461
3781
|
if (!account) {
|
|
3462
3782
|
throw new Error(`Corrupt sequence data: missing account entry for id ${numStr}`);
|
|
3463
3783
|
}
|
|
3464
|
-
const isActive =
|
|
3465
|
-
let line = ` ${index + 1}: ${account
|
|
3784
|
+
const isActive = activeAccountNum === numStr;
|
|
3785
|
+
let line = ` ${index + 1}: ${getAccountDisplayLabel(account)}`;
|
|
3466
3786
|
if (account.alias)
|
|
3467
3787
|
line += ` [${account.alias}]`;
|
|
3468
3788
|
if (isActive)
|
|
@@ -3566,6 +3886,8 @@ async function cmdStatus(options) {
|
|
|
3566
3886
|
console.log(JSON.stringify({
|
|
3567
3887
|
provider: activeProvider.name,
|
|
3568
3888
|
email: summary.email === "none" ? null : summary.email,
|
|
3889
|
+
label: summary.email === "none" ? null : summary.label,
|
|
3890
|
+
organizationName: summary.organizationName,
|
|
3569
3891
|
alias: summary.alias,
|
|
3570
3892
|
managed: summary.managed
|
|
3571
3893
|
}));
|
|
@@ -3573,29 +3895,37 @@ async function cmdStatus(options) {
|
|
|
3573
3895
|
}
|
|
3574
3896
|
if (summary.email === "none") {
|
|
3575
3897
|
console.log("none");
|
|
3898
|
+
} else if (summary.alias) {
|
|
3899
|
+
console.log(`${summary.label} [${summary.alias}]`);
|
|
3576
3900
|
} else {
|
|
3577
|
-
|
|
3578
|
-
console.log(`${summary.email} [${summary.alias}]`);
|
|
3579
|
-
return;
|
|
3580
|
-
}
|
|
3581
|
-
console.log(summary.email);
|
|
3901
|
+
console.log(summary.label);
|
|
3582
3902
|
}
|
|
3903
|
+
console.log(`managed accounts: ${summary.managedCount}`);
|
|
3583
3904
|
}
|
|
3584
3905
|
async function getStatusSummaryForActiveProvider() {
|
|
3585
|
-
const
|
|
3906
|
+
const currentAccount = getCurrentAccountIdentity();
|
|
3907
|
+
const email = currentAccount?.email ?? "none";
|
|
3586
3908
|
let alias = null;
|
|
3587
3909
|
let managed = false;
|
|
3910
|
+
let managedCount = 0;
|
|
3911
|
+
let label = getCurrentAccountDisplayLabel(currentAccount);
|
|
3912
|
+
let organizationName = currentAccount?.organizationName ?? null;
|
|
3588
3913
|
if (email !== "none" && existsSync9(activeSequenceFile)) {
|
|
3589
3914
|
const seq = await loadSequence(activeSequenceFile);
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3915
|
+
managedCount = Object.keys(seq.accounts).length;
|
|
3916
|
+
const matchedAccountNum = resolveManagedAccount(seq, currentAccount);
|
|
3917
|
+
if (matchedAccountNum) {
|
|
3918
|
+
const account = seq.accounts[matchedAccountNum];
|
|
3919
|
+
managed = true;
|
|
3920
|
+
alias = account.alias ?? null;
|
|
3921
|
+
label = getAccountDisplayLabel(account);
|
|
3922
|
+
organizationName = account.display?.organizationName ?? organizationName;
|
|
3923
|
+
}
|
|
3924
|
+
} else if (existsSync9(activeSequenceFile)) {
|
|
3925
|
+
const seq = await loadSequence(activeSequenceFile);
|
|
3926
|
+
managedCount = Object.keys(seq.accounts).length;
|
|
3597
3927
|
}
|
|
3598
|
-
return { email, alias, managed };
|
|
3928
|
+
return { email, alias, managed, managedCount, label, organizationName };
|
|
3599
3929
|
}
|
|
3600
3930
|
async function withActiveProvider(provider, fn) {
|
|
3601
3931
|
const previousProvider = activeProvider.name;
|
|
@@ -3645,13 +3975,12 @@ async function cmdStatusAllProviders() {
|
|
|
3645
3975
|
console.log(summary.heading);
|
|
3646
3976
|
if (summary.email === "none") {
|
|
3647
3977
|
console.log(" none");
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
console.log(` ${summary.
|
|
3652
|
-
continue;
|
|
3978
|
+
} else if (summary.alias) {
|
|
3979
|
+
console.log(` ${summary.label} [${summary.alias}]`);
|
|
3980
|
+
} else {
|
|
3981
|
+
console.log(` ${summary.label}`);
|
|
3653
3982
|
}
|
|
3654
|
-
console.log(` ${summary.
|
|
3983
|
+
console.log(` managed accounts: ${summary.managedCount}`);
|
|
3655
3984
|
}
|
|
3656
3985
|
}
|
|
3657
3986
|
async function cmdAlias(alias, identifier) {
|
|
@@ -3663,19 +3992,31 @@ async function cmdAlias(alias, identifier) {
|
|
|
3663
3992
|
throw new Error(result.reason);
|
|
3664
3993
|
}
|
|
3665
3994
|
const seq = await loadSequence(activeSequenceFile);
|
|
3666
|
-
if (identifier && /^\d+$/.test(identifier)) {
|
|
3667
|
-
throw new Error("Alias target must be an email, not a number");
|
|
3668
|
-
}
|
|
3669
3995
|
const currentEmail = getCurrentAccount();
|
|
3670
|
-
const
|
|
3671
|
-
|
|
3672
|
-
|
|
3996
|
+
const currentIdentity = getCurrentAccountIdentity();
|
|
3997
|
+
let accountNum = null;
|
|
3998
|
+
if (identifier) {
|
|
3999
|
+
const target = resolveAccountTarget(seq, identifier);
|
|
4000
|
+
if (target.status === "ambiguous") {
|
|
4001
|
+
throw new Error(`Multiple managed accounts match ${identifier}. Use account number or alias.`);
|
|
4002
|
+
}
|
|
4003
|
+
if (target.status === "missing") {
|
|
3673
4004
|
throw new Error(`Account not found: ${identifier}`);
|
|
3674
|
-
}
|
|
4005
|
+
}
|
|
4006
|
+
accountNum = target.accountNum;
|
|
4007
|
+
} else {
|
|
4008
|
+
accountNum = resolveManagedAccount(seq, currentIdentity);
|
|
4009
|
+
if (!accountNum && currentIdentity?.email && currentIdentity.email !== "none") {
|
|
4010
|
+
if (hasMultipleProviderEmailMatches(seq, currentIdentity.email, activeProvider.name)) {
|
|
4011
|
+
throw new Error(`Multiple managed accounts match ${currentIdentity.email}. Use account number or alias.`);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
if (!accountNum) {
|
|
4016
|
+
if (currentEmail === "none") {
|
|
3675
4017
|
throw new Error(`No active ${getProviderLabel()} account found. Please log in first.`);
|
|
3676
|
-
} else {
|
|
3677
|
-
throw new Error(`Current account is not managed: ${currentEmail}`);
|
|
3678
4018
|
}
|
|
4019
|
+
throw new Error(`Current account is not managed: ${currentEmail}`);
|
|
3679
4020
|
}
|
|
3680
4021
|
const updated = setAlias(seq, accountNum, alias);
|
|
3681
4022
|
await writeJsonAtomic2(activeSequenceFile, updated);
|
|
@@ -3683,7 +4024,9 @@ async function cmdAlias(alias, identifier) {
|
|
|
3683
4024
|
console.log(`Alias "${alias}" set for ${getDisplayAccountLabel(updated, accountNum)} (${account.email})`);
|
|
3684
4025
|
}
|
|
3685
4026
|
async function cmdInteractiveSwitch() {
|
|
3686
|
-
const
|
|
4027
|
+
const currentIdentity = getCurrentAccountIdentity();
|
|
4028
|
+
const currentEmail = currentIdentity?.email ?? "none";
|
|
4029
|
+
const currentLabel = getCurrentAccountDisplayLabel(currentIdentity);
|
|
3687
4030
|
const hasSequence = existsSync9(activeSequenceFile);
|
|
3688
4031
|
const seq = hasSequence ? await loadSequence(activeSequenceFile) : null;
|
|
3689
4032
|
if (seq) {
|
|
@@ -3692,7 +4035,7 @@ async function cmdInteractiveSwitch() {
|
|
|
3692
4035
|
if (!seq || seq.sequence.length === 0) {
|
|
3693
4036
|
const emptyStateChoices = [
|
|
3694
4037
|
{
|
|
3695
|
-
name: `+ Add current logged-in account${currentEmail === "none" ? "" : ` (${
|
|
4038
|
+
name: `+ Add current logged-in account${currentEmail === "none" ? "" : ` (${currentLabel})`}`,
|
|
3696
4039
|
value: ADD_CURRENT_ACCOUNT_CHOICE
|
|
3697
4040
|
},
|
|
3698
4041
|
{ name: "Back", value: "__back__" }
|
|
@@ -3704,8 +4047,8 @@ async function cmdInteractiveSwitch() {
|
|
|
3704
4047
|
await cmdAdd();
|
|
3705
4048
|
return;
|
|
3706
4049
|
}
|
|
3707
|
-
const shouldOfferAddCurrent = currentEmail !== "none" && !accountExists(seq, currentEmail);
|
|
3708
|
-
const extraChoices = shouldOfferAddCurrent ? [{ name: `+ Add current logged-in account (${
|
|
4050
|
+
const shouldOfferAddCurrent = currentEmail !== "none" && currentIdentity?.identityStatus !== "ambiguous" && !accountExists(seq, currentIdentity ?? currentEmail);
|
|
4051
|
+
const extraChoices = shouldOfferAddCurrent ? [{ name: `+ Add current logged-in account (${currentLabel})`, value: ADD_CURRENT_ACCOUNT_CHOICE }] : [];
|
|
3709
4052
|
const selected = await pickAccount(seq, `caflip v${package_default.version} \u2014 Switch ${getProviderLabel()} account:`, undefined, extraChoices);
|
|
3710
4053
|
if (selected === ADD_CURRENT_ACCOUNT_CHOICE) {
|
|
3711
4054
|
await cmdAdd();
|
|
@@ -3723,7 +4066,7 @@ Usage:
|
|
|
3723
4066
|
Commands:
|
|
3724
4067
|
(no args) Interactive provider picker
|
|
3725
4068
|
list List managed accounts for all providers
|
|
3726
|
-
status Show current account for all providers
|
|
4069
|
+
status Show current active account for all providers
|
|
3727
4070
|
add [--alias <name>] Pick provider, then add current account
|
|
3728
4071
|
login [-- <args...>] Pick provider, then run provider login
|
|
3729
4072
|
remove [<email>] Pick provider, then remove an account
|
|
@@ -3734,20 +4077,22 @@ Commands:
|
|
|
3734
4077
|
<provider> login [-- <args...>] Run provider login and register session
|
|
3735
4078
|
<provider> remove [<email>] Remove an account
|
|
3736
4079
|
<provider> next Rotate to next account
|
|
3737
|
-
<provider> status [--json] Show current account
|
|
3738
|
-
<provider> alias <name> [<
|
|
4080
|
+
<provider> status [--json] Show current active account
|
|
4081
|
+
<provider> alias <name> [<account>] Set alias for current or target account
|
|
3739
4082
|
help Show this help
|
|
3740
4083
|
|
|
3741
4084
|
Examples:
|
|
3742
4085
|
caflip Pick provider interactively
|
|
3743
4086
|
caflip list List managed accounts for Claude and Codex
|
|
3744
|
-
caflip status Show current account for Claude and Codex
|
|
4087
|
+
caflip status Show current active account for Claude and Codex
|
|
3745
4088
|
caflip add Pick provider, then add current account
|
|
3746
4089
|
caflip login Pick provider, then run provider login
|
|
3747
4090
|
caflip remove Pick provider, then remove an account interactively
|
|
3748
4091
|
caflip claude Pick Claude account interactively
|
|
3749
4092
|
caflip claude work Switch Claude account by alias
|
|
3750
4093
|
caflip claude add --alias personal Add current Claude account with alias
|
|
4094
|
+
caflip claude alias work Alias the current Claude account as "work"
|
|
4095
|
+
caflip claude alias work 2 Alias Claude Account-2 as "work"
|
|
3751
4096
|
caflip claude login Run Claude login and register session
|
|
3752
4097
|
caflip claude login -- --email me@example.com --sso
|
|
3753
4098
|
Pass provider-specific flags after --
|
|
@@ -3755,8 +4100,12 @@ Examples:
|
|
|
3755
4100
|
caflip codex list List managed Codex accounts
|
|
3756
4101
|
caflip codex login -- --device-auth Run Codex login and register session
|
|
3757
4102
|
caflip codex add --alias work Add current Codex account with alias
|
|
3758
|
-
caflip codex alias work
|
|
3759
|
-
Set Codex alias for target
|
|
4103
|
+
caflip codex alias work 2
|
|
4104
|
+
Set Codex alias for target account
|
|
4105
|
+
|
|
4106
|
+
Alias targets:
|
|
4107
|
+
<account> can be a list number, an existing alias, or an email when it matches exactly one managed account.
|
|
4108
|
+
Codex labels show workspace plans as email \xB7 plan(orgShortId); free shows email \xB7 free.`);
|
|
3760
4109
|
}
|
|
3761
4110
|
async function executeProviderCommand(command, args, provider, runWithLock) {
|
|
3762
4111
|
switch (command) {
|
|
@@ -3798,7 +4147,7 @@ async function executeProviderCommand(command, args, provider, runWithLock) {
|
|
|
3798
4147
|
break;
|
|
3799
4148
|
case "alias": {
|
|
3800
4149
|
if (!args[1]) {
|
|
3801
|
-
console.error(`Usage: caflip ${provider} alias <name> [<
|
|
4150
|
+
console.error(`Usage: caflip ${provider} alias <name> [<account>]`);
|
|
3802
4151
|
process.exit(1);
|
|
3803
4152
|
}
|
|
3804
4153
|
await runWithLock(async () => {
|