maxpool 1.0.4 → 1.0.6
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 +88 -3
- package/package.json +1 -1
- package/src/index.js +41 -0
- package/src/tui.js +134 -60
package/README.md
CHANGED
|
@@ -52,6 +52,48 @@ claude /login # Log into an account in Claude Code
|
|
|
52
52
|
maxpool import # Import its credentials
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Recommended setup
|
|
56
|
+
|
|
57
|
+
The cleanest way to use maxpool day-to-day: **keep your normal `claude` login untouched, and add a separate alias that routes through the pool.** Then plain `claude` still uses your default single account, and `ccmax` (call it whatever you like) spreads work across all your accounts.
|
|
58
|
+
|
|
59
|
+
1. **Install and add your accounts** (see [Adding Accounts](#adding-accounts)):
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g maxpool
|
|
62
|
+
maxpool login # repeat for each account (browser) — or `maxpool import`
|
|
63
|
+
```
|
|
64
|
+
2. **Start the proxy** and leave it running (it shows a live dashboard):
|
|
65
|
+
```bash
|
|
66
|
+
maxpool
|
|
67
|
+
```
|
|
68
|
+
3. **Add an alias** to your `~/.zshrc` (or `~/.bashrc`):
|
|
69
|
+
```bash
|
|
70
|
+
# Run Claude Code through the maxpool proxy.
|
|
71
|
+
# Your plain `claude` stays on its own separate login.
|
|
72
|
+
ccmax() {
|
|
73
|
+
local url
|
|
74
|
+
url="$(maxpool env | sed -n 's/^export ANTHROPIC_BASE_URL=//p')"
|
|
75
|
+
( unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN
|
|
76
|
+
ANTHROPIC_BASE_URL="$url" \
|
|
77
|
+
ANTHROPIC_CUSTOM_HEADERS="x-maxpool-session: $(uuidgen)" \
|
|
78
|
+
claude "$@" )
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
Reload with `source ~/.zshrc` (or just open a new terminal).
|
|
82
|
+
4. **Use it:**
|
|
83
|
+
```bash
|
|
84
|
+
ccmax # Claude Code, load-balanced across all your accounts
|
|
85
|
+
claude # unchanged — still your normal single-account login
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Why this approach:
|
|
89
|
+
|
|
90
|
+
- **Your normal `claude` stays separate.** maxpool keeps its own account tokens in its config; your everyday Claude Code login (in the OS keychain) is never touched. Use `ccmax` when you want the pool, `claude` when you don't.
|
|
91
|
+
- **Session affinity.** The `x-maxpool-session` header pins each terminal to one account (with automatic failover), so a single task doesn't bounce between accounts mid-stream.
|
|
92
|
+
- **No key needed locally.** The proxy listens on `127.0.0.1` and accepts local clients without an API key, so the alias stays short.
|
|
93
|
+
- **Composes with your other aliases.** If you already use aliases for other providers (GLM, Kimi, …), `ccmax` slots right in alongside them.
|
|
94
|
+
|
|
95
|
+
> Prefer zero config? `maxpool run` launches Claude Code through the proxy for you — the alias above is the same idea, just composable with your own setup. Most people end up making an alias.
|
|
96
|
+
|
|
55
97
|
## Adding Accounts
|
|
56
98
|
|
|
57
99
|
### OAuth Login (recommended)
|
|
@@ -116,7 +158,18 @@ Falls back to plain log output when not a TTY (e.g. running as a service).
|
|
|
116
158
|
|
|
117
159
|
State-changing actions show what will happen and require `y` or `n`. In selection mode, use `j`/`k` or arrow keys to navigate, `Enter` to choose, and `Esc` to go back.
|
|
118
160
|
|
|
119
|
-
The Accounts menu
|
|
161
|
+
The Accounts menu (`a`) lets you add and manage accounts without leaving the TUI:
|
|
162
|
+
|
|
163
|
+
| Key | Action |
|
|
164
|
+
|-----|--------|
|
|
165
|
+
| `i` | Import the account you're currently logged into Claude Code as |
|
|
166
|
+
| `l` | Log in via browser — add *any* account, then name it |
|
|
167
|
+
| `k` | Add an Anthropic API key account |
|
|
168
|
+
| `n` | Rename the selected account |
|
|
169
|
+
| `t` | Enable or disable the selected account |
|
|
170
|
+
| `d` | Permanently delete an idle account |
|
|
171
|
+
|
|
172
|
+
Disabling keeps credentials in config but prevents new requests from using the account. Deletion is blocked while that account has active requests. The currently-active account is marked with a green `►`.
|
|
120
173
|
|
|
121
174
|
Routing modes:
|
|
122
175
|
|
|
@@ -174,6 +227,7 @@ maxpool accounts # List accounts with subscription tier and token statu
|
|
|
174
227
|
maxpool accounts -v # Also show token expiry times
|
|
175
228
|
maxpool status # Show live proxy status (requires running server)
|
|
176
229
|
maxpool remove <name> # Remove an account
|
|
230
|
+
maxpool rename <name|#> <new> # Rename an account (by name or list number)
|
|
177
231
|
maxpool api <path> # Call an API endpoint with account credentials
|
|
178
232
|
maxpool help # Show all commands
|
|
179
233
|
```
|
|
@@ -208,6 +262,8 @@ TEAMCLAUDE_CONFIG=./my-config.json maxpool server
|
|
|
208
262
|
"apiKey": "tc-auto-generated-key"
|
|
209
263
|
},
|
|
210
264
|
"upstream": "https://api.anthropic.com",
|
|
265
|
+
"updateCheck": true,
|
|
266
|
+
"autoUpdate": false,
|
|
211
267
|
"switchThreshold": 0.90,
|
|
212
268
|
"scheduler": {
|
|
213
269
|
"mode": "adaptive-least-loaded",
|
|
@@ -235,7 +291,7 @@ TEAMCLAUDE_CONFIG=./my-config.json maxpool server
|
|
|
235
291
|
"pollMs": 1000
|
|
236
292
|
},
|
|
237
293
|
"shutdown": {
|
|
238
|
-
"drainTimeoutMs":
|
|
294
|
+
"drainTimeoutMs": 15000
|
|
239
295
|
},
|
|
240
296
|
"accounts": [
|
|
241
297
|
{
|
|
@@ -256,7 +312,10 @@ TEAMCLAUDE_CONFIG=./my-config.json maxpool server
|
|
|
256
312
|
| `proxy.port` | Local port the proxy listens on |
|
|
257
313
|
| `proxy.apiKey` | API key clients use for status/admin requests |
|
|
258
314
|
| `upstream` | Upstream API base URL |
|
|
259
|
-
| `
|
|
315
|
+
| `updateCheck` | Check npm for a newer maxpool on startup and notify; defaults to `true` |
|
|
316
|
+
| `autoUpdate` | Install new versions automatically (applied on next restart, never interrupting sessions); defaults to `false` |
|
|
317
|
+
| `switchThreshold` | Quota utilization (0–1) at which an account is avoided (5h *and* weekly); default `0.90`. Raise toward `0.97` to use more before rotating |
|
|
318
|
+
| `scheduler.weeklySoftThreshold` / `weeklyReserveThreshold` / `weeklyCriticalThreshold` / `weeklyExhaustedThreshold` | Weekly (7d) quota tiers (0–1) controlling how aggressively an account is de-prioritised as its weekly usage climbs |
|
|
260
319
|
| `scheduler.safetyMaxActivePerAccount` | Emergency circuit breaker, not a normal capacity cap |
|
|
261
320
|
| `scheduler.safetyMaxGlobalActive` | Emergency global circuit breaker |
|
|
262
321
|
| `retry.maxAttemptsPerRequest` | Retry attempts before returning an error; `0` means one pass over accounts |
|
|
@@ -302,6 +361,32 @@ The weekly usage bar shows raw upstream utilization and reset timing. Reset-awar
|
|
|
302
361
|
17. When Restart is confirmed, new upstream admission pauses immediately. Existing upstream requests finish, queued requests cannot deadlock restart, and their sockets close during relaunch so Claude Code reconnects automatically
|
|
303
362
|
18. Client token refresh requests (`/v1/oauth/token`) are relayed to upstream untouched — the proxy and client manage their own token lifecycles independently
|
|
304
363
|
|
|
364
|
+
## FAQ
|
|
365
|
+
|
|
366
|
+
**Does this touch my normal Claude Code login?**
|
|
367
|
+
No. maxpool stores its own account tokens in its config file. Your everyday `claude` login lives in the OS keychain and is never modified. Run `ccmax` for the pool, `claude` for your normal login.
|
|
368
|
+
|
|
369
|
+
**How do I add another account?**
|
|
370
|
+
`maxpool login` (browser — adds any account), or in the TUI press `a` then `l`. To add the account you're currently logged into Claude Code with, use `maxpool import` (or `a` then `i`).
|
|
371
|
+
|
|
372
|
+
**Can I rename an account?**
|
|
373
|
+
Yes — `maxpool rename <name|number> <new-name>`, or in the TUI press `a` then `n`.
|
|
374
|
+
|
|
375
|
+
**An account stopped being used before it hit 100% — why?**
|
|
376
|
+
maxpool stops routing to an account at `switchThreshold` (default 90%) of its 5-hour or weekly window, leaving a safety margin so it's never hard rate-limited. Raise it in your config (toward 0.97) to use more before rotating.
|
|
377
|
+
|
|
378
|
+
**One account shows empty quota bars but it's serving requests — is it broken?**
|
|
379
|
+
No. The bars show how *full* an account is toward its limit, so an empty bar means lots of headroom (good). Actual activity is in the `15m`/`1h` request-count columns.
|
|
380
|
+
|
|
381
|
+
**Will updates apply automatically?**
|
|
382
|
+
Set `"autoUpdate": true` in your config and maxpool installs new versions itself (applied on the next restart; running sessions are never interrupted). Otherwise `npm i -g maxpool` updates it.
|
|
383
|
+
|
|
384
|
+
**Where do my tokens go?**
|
|
385
|
+
They're stored locally in your config (file mode `0600`) and sent only to Anthropic — or, in the optional `all` profile, to GLM/Kimi if you supply those. Nothing else leaves your machine. Zero third-party dependencies.
|
|
386
|
+
|
|
387
|
+
**How do I stop it?**
|
|
388
|
+
Press `q` in the TUI (or Ctrl-C). It drains briefly, then exits.
|
|
389
|
+
|
|
305
390
|
## Credits
|
|
306
391
|
|
|
307
392
|
maxpool is a fork of [KarpelesLab/teamclaude](https://github.com/KarpelesLab/teamclaude)
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -48,6 +48,10 @@ switch (command) {
|
|
|
48
48
|
await removeCommand();
|
|
49
49
|
process.exit(0);
|
|
50
50
|
break;
|
|
51
|
+
case 'rename':
|
|
52
|
+
await renameCommand();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
break;
|
|
51
55
|
case 'api':
|
|
52
56
|
await apiCommand();
|
|
53
57
|
process.exit(0);
|
|
@@ -798,6 +802,42 @@ async function removeCommand() {
|
|
|
798
802
|
console.log(`Removed account "${name}"`);
|
|
799
803
|
}
|
|
800
804
|
|
|
805
|
+
// Resolve an account by exact name, else by 1-based index. Returns -1 if none.
|
|
806
|
+
function resolveAccountIndex(accounts, target) {
|
|
807
|
+
let idx = accounts.findIndex(a => a.name === target);
|
|
808
|
+
if (idx < 0 && /^\d+$/.test(String(target))) idx = Number(target) - 1;
|
|
809
|
+
return idx >= 0 && idx < accounts.length ? idx : -1;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function renameCommand() {
|
|
813
|
+
const config = await loadOrCreateConfig();
|
|
814
|
+
const target = args[1];
|
|
815
|
+
const newName = args[2];
|
|
816
|
+
|
|
817
|
+
if (!target || !newName) {
|
|
818
|
+
console.error('Usage: maxpool rename <account-name|number> <new-name>');
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const idx = resolveAccountIndex(config.accounts, target);
|
|
823
|
+
if (idx < 0) {
|
|
824
|
+
console.error(`Account "${target}" not found`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
if (config.accounts.some((a, i) => i !== idx && a.name === newName)) {
|
|
828
|
+
console.error(`An account named "${newName}" already exists`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const old = config.accounts[idx].name;
|
|
833
|
+
config.accounts[idx].name = newName;
|
|
834
|
+
// Keep manual-preference routing pointing at the renamed account.
|
|
835
|
+
if (config.routing?.preferredAccount === old) config.routing.preferredAccount = newName;
|
|
836
|
+
await saveConfig(config);
|
|
837
|
+
console.log(`Renamed "${old}" → "${newName}"`);
|
|
838
|
+
console.log('Restart maxpool to apply this to a running proxy (or rename live from the TUI: a → n).');
|
|
839
|
+
}
|
|
840
|
+
|
|
801
841
|
// ── help ────────────────────────────────────────────────────
|
|
802
842
|
|
|
803
843
|
function showHelp() {
|
|
@@ -815,6 +855,7 @@ Commands:
|
|
|
815
855
|
status Show proxy & account status (live)
|
|
816
856
|
accounts List configured accounts
|
|
817
857
|
remove <name> Remove an account
|
|
858
|
+
rename <name|#> <new> Rename an account (by name or list number)
|
|
818
859
|
api <path> Call an API endpoint with account credentials
|
|
819
860
|
help Show this help
|
|
820
861
|
|
package/src/tui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { importCredentials, fetchProfile, loginOAuth } from './oauth.js';
|
|
2
3
|
|
|
3
4
|
// ── ANSI helpers ─────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -369,6 +370,14 @@ export class TUI {
|
|
|
369
370
|
() => this._doAddKey(value),
|
|
370
371
|
);
|
|
371
372
|
};
|
|
373
|
+
} else if (k === 'l') {
|
|
374
|
+
this._confirm(
|
|
375
|
+
'Log in via browser?',
|
|
376
|
+
'Opens a browser to add any Claude account; you name it afterward.',
|
|
377
|
+
() => this._doLogin(),
|
|
378
|
+
);
|
|
379
|
+
} else if (k === 'n' && this.am.accounts.length > 0) {
|
|
380
|
+
this._startSelection('rename');
|
|
372
381
|
} else if (k === 't' && this.am.accounts.length > 0) {
|
|
373
382
|
this._startSelection('toggle');
|
|
374
383
|
} else if (k === 'd' && this.am.accounts.length > 0) {
|
|
@@ -432,6 +441,14 @@ export class TUI {
|
|
|
432
441
|
'Permanently remove it from Maxpool config. Deletion is blocked while it has active requests.',
|
|
433
442
|
() => this._doDelete(this.selIdx),
|
|
434
443
|
);
|
|
444
|
+
} else if (this.selAction === 'rename') {
|
|
445
|
+
const targetIdx = this.selIdx;
|
|
446
|
+
const current = account.name;
|
|
447
|
+
this.mode = 'input';
|
|
448
|
+
this.inputPrompt = `New name for "${current}"`;
|
|
449
|
+
this.inputBuf = '';
|
|
450
|
+
this.inputSensitive = false;
|
|
451
|
+
this.inputCb = value => this._doRename(targetIdx, String(value || '').trim());
|
|
435
452
|
}
|
|
436
453
|
}
|
|
437
454
|
else if (k === 'esc' || k === 'q') { this.mode = 'normal'; }
|
|
@@ -519,74 +536,123 @@ export class TUI {
|
|
|
519
536
|
async _doImport() {
|
|
520
537
|
try {
|
|
521
538
|
this._addLog('Importing credentials...');
|
|
522
|
-
const creds = await importCredentials(
|
|
539
|
+
const creds = await importCredentials(); // file, then macOS Keychain fallback
|
|
523
540
|
const profile = await fetchProfile(creds.accessToken);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (!profileOk) {
|
|
541
|
+
if (!profile || profile.error) {
|
|
527
542
|
this._addLog(`Warning: could not fetch profile — ${profile?.error || 'no token'}`);
|
|
528
543
|
}
|
|
529
|
-
|
|
530
544
|
let name;
|
|
531
545
|
if (profile?.email) {
|
|
532
546
|
name = profile.email;
|
|
533
547
|
const tier = profile.hasClaudeMax ? 'Max' : profile.hasClaudePro ? 'Pro' : null;
|
|
534
548
|
if (tier) this._addLog(`Detected Claude ${tier}: ${name}`);
|
|
535
|
-
} else {
|
|
536
|
-
const n = this.config.accounts.filter(a => a.name.startsWith('account-')).length + 1;
|
|
537
|
-
name = `account-${n}`;
|
|
538
549
|
}
|
|
550
|
+
await this._upsertOAuthAccount({ creds, profile, name, source: 'import', verb: 'Imported' });
|
|
551
|
+
} catch (e) {
|
|
552
|
+
this._addLog(`Import failed: ${e.message}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
539
555
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
556
|
+
// Browser OAuth login: any Claude account, named afterward. Suspends the TUI
|
|
557
|
+
// around the interactive flow (browser + name prompt), then resumes.
|
|
558
|
+
async _doLogin() {
|
|
559
|
+
const wasRunning = this.running;
|
|
560
|
+
if (wasRunning) this.stop();
|
|
561
|
+
try {
|
|
562
|
+
process.stdout.write('\nOpening browser to log into Claude…\n');
|
|
563
|
+
const creds = await loginOAuth();
|
|
564
|
+
const profile = await fetchProfile(creds.accessToken);
|
|
565
|
+
const suggested = profile?.email
|
|
566
|
+
|| `account-${this.config.accounts.filter(a => a.name.startsWith('account-')).length + 1}`;
|
|
567
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
568
|
+
const answer = await new Promise(resolve => rl.question(`Name this account [${suggested}]: `, resolve));
|
|
569
|
+
rl.close();
|
|
570
|
+
const name = String(answer || '').trim() || suggested;
|
|
571
|
+
await this._upsertOAuthAccount({ creds, profile, name, source: 'login', verb: 'Added' });
|
|
572
|
+
process.stdout.write(`\nAdded account "${name}". Returning to maxpool…\n`);
|
|
573
|
+
} catch (e) {
|
|
574
|
+
process.stdout.write(`\nLogin failed: ${e.message}\n`);
|
|
575
|
+
} finally {
|
|
576
|
+
if (wasRunning) this.start();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
547
579
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
580
|
+
// Rename an account in config and in the running manager.
|
|
581
|
+
async _doRename(idx, newName) {
|
|
582
|
+
const account = this.am.accounts[idx];
|
|
583
|
+
if (!account) { this._addLog('Account no longer exists'); return; }
|
|
584
|
+
if (!newName) { this._addLog('Rename cancelled (empty name)'); return; }
|
|
585
|
+
if (this.am.accounts.some((a, i) => i !== idx && a.name === newName)) {
|
|
586
|
+
this._addLog(`An account named "${newName}" already exists`); return;
|
|
587
|
+
}
|
|
588
|
+
const cfgIdx = this._configAccountIndex(account);
|
|
589
|
+
if (cfgIdx < 0) { this._addLog(`Cannot rename "${account.name}" (not in config)`); return; }
|
|
590
|
+
const old = account.name;
|
|
591
|
+
const prev = this.config.accounts[cfgIdx].name;
|
|
592
|
+
this.config.accounts[cfgIdx].name = newName;
|
|
593
|
+
if (this.config.routing?.preferredAccount === old) this.config.routing.preferredAccount = newName;
|
|
594
|
+
try {
|
|
595
|
+
await this.saveConfig(this.config);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
this.config.accounts[cfgIdx].name = prev;
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
account.name = newName; // update the running account manager
|
|
601
|
+
this._addLog(`Renamed "${old}" → "${newName}"`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Upsert an OAuth account into config + the running manager. Dedupes by
|
|
605
|
+
// accountUuid, then name. Shared by import and browser login.
|
|
606
|
+
async _upsertOAuthAccount({ creds, profile, name, source, verb = 'Added' }) {
|
|
607
|
+
if (!name) {
|
|
608
|
+
name = profile?.email
|
|
609
|
+
|| `account-${this.config.accounts.filter(a => a.name.startsWith('account-')).length + 1}`;
|
|
610
|
+
}
|
|
611
|
+
const entry = {
|
|
612
|
+
name, type: 'oauth', source,
|
|
613
|
+
accountUuid: profile?.accountUuid || null,
|
|
614
|
+
accessToken: creds.accessToken,
|
|
615
|
+
refreshToken: creds.refreshToken,
|
|
616
|
+
expiresAt: creds.expiresAt,
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
let idx = entry.accountUuid
|
|
620
|
+
? this.config.accounts.findIndex(a => a.accountUuid === entry.accountUuid)
|
|
621
|
+
: -1;
|
|
622
|
+
if (idx < 0) idx = this.config.accounts.findIndex(a => a.name === name);
|
|
623
|
+
|
|
624
|
+
if (idx >= 0) {
|
|
625
|
+
const previous = this.config.accounts[idx];
|
|
626
|
+
entry.enabled = previous.enabled;
|
|
627
|
+
this.config.accounts[idx] = entry;
|
|
628
|
+
try {
|
|
629
|
+
await this.saveConfig(this.config);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this.config.accounts[idx] = previous;
|
|
632
|
+
throw error;
|
|
587
633
|
}
|
|
588
|
-
|
|
589
|
-
|
|
634
|
+
const amAcct = this.am.accounts.find(account =>
|
|
635
|
+
(entry.accountUuid && account.accountUuid === entry.accountUuid) || account.name === name
|
|
636
|
+
);
|
|
637
|
+
if (amAcct) {
|
|
638
|
+
amAcct.credential = creds.accessToken;
|
|
639
|
+
amAcct.refreshToken = creds.refreshToken;
|
|
640
|
+
amAcct.expiresAt = creds.expiresAt;
|
|
641
|
+
amAcct.accountUuid = entry.accountUuid;
|
|
642
|
+
amAcct.name = name;
|
|
643
|
+
if (amAcct.status === 'error') amAcct.status = 'active';
|
|
644
|
+
}
|
|
645
|
+
this._addLog(`Updated account "${name}"`);
|
|
646
|
+
} else {
|
|
647
|
+
this.config.accounts.push(entry);
|
|
648
|
+
try {
|
|
649
|
+
await this.saveConfig(this.config);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
this.config.accounts.pop();
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
this.am.addAccount(entry);
|
|
655
|
+
this._addLog(`${verb} account "${name}"`);
|
|
590
656
|
}
|
|
591
657
|
}
|
|
592
658
|
|
|
@@ -829,7 +895,13 @@ export class TUI {
|
|
|
829
895
|
|
|
830
896
|
_renderAcct(idx, bw, showBoth) {
|
|
831
897
|
const a = this.am.accounts[idx];
|
|
832
|
-
|
|
898
|
+
// Highlight the currently-active account. In manual mode that's the
|
|
899
|
+
// preferred account; in automatic mode it's the one most recently routed
|
|
900
|
+
// to (currentIndex). Previously only manual mode highlighted anything, so
|
|
901
|
+
// in automatic load-balancing no row was ever marked current.
|
|
902
|
+
const isCur = this.am.routingMode === 'preferred'
|
|
903
|
+
? a.name === this.am.preferredAccountName
|
|
904
|
+
: idx === this.am.currentIndex;
|
|
833
905
|
const isSel = this.mode === 'select' && idx === this.selIdx;
|
|
834
906
|
|
|
835
907
|
// Prefix: selection marker + current marker
|
|
@@ -936,7 +1008,7 @@ export class TUI {
|
|
|
936
1008
|
case 'normal':
|
|
937
1009
|
return ` ${bold('a')} Accounts ${bold('m')} Routing ${bold('s')} Sync ${bold('r')} Restart ${bold('q')} Stop`;
|
|
938
1010
|
case 'accounts':
|
|
939
|
-
return ` ${bold('i')} Import
|
|
1011
|
+
return ` ${bold('i')} Import ${bold('l')} Login (browser) ${bold('k')} API key ${bold('n')} Rename ${bold('t')} Enable/disable ${bold('d')} Delete ${bold('Esc')} Back`;
|
|
940
1012
|
case 'routing':
|
|
941
1013
|
return ` ${bold('a')} Automatic ${bold('p')} Manual preference ${bold('Esc')} Back`;
|
|
942
1014
|
case 'select': {
|
|
@@ -944,7 +1016,9 @@ export class TUI {
|
|
|
944
1016
|
? 'prefer'
|
|
945
1017
|
: this.selAction === 'toggle'
|
|
946
1018
|
? 'enable/disable'
|
|
947
|
-
: '
|
|
1019
|
+
: this.selAction === 'rename'
|
|
1020
|
+
? 'rename'
|
|
1021
|
+
: 'delete';
|
|
948
1022
|
return ` ${dim('↑↓')} select ${bold('Enter')} ${act} ${bold('Esc')} cancel`;
|
|
949
1023
|
}
|
|
950
1024
|
case 'input':
|