@rikcodes/teamclaude 1.1.20-rik.13 → 1.1.20-rik.14
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/package.json +1 -1
- package/src/account-manager.js +7 -0
- package/src/sync-accounts.js +5 -0
- package/src/tui.js +179 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rikcodes/teamclaude",
|
|
3
|
-
"version": "1.1.20-rik.
|
|
3
|
+
"version": "1.1.20-rik.14",
|
|
4
4
|
"description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/account-manager.js
CHANGED
|
@@ -214,6 +214,13 @@ function makeAccount(acct, index) {
|
|
|
214
214
|
hasClaudeMax: acct.hasClaudeMax ?? null,
|
|
215
215
|
hasClaudePro: acct.hasClaudePro ?? null,
|
|
216
216
|
priority: acct.priority || 0,
|
|
217
|
+
// Where this account sits in the list the TUI draws, arranged by the
|
|
218
|
+
// operator from the settings screen. Presentation only: nothing but
|
|
219
|
+
// _displayOrder in tui.js reads it, and it is emphatically NOT `priority`
|
|
220
|
+
// above — that one decides which account rotation spends next, and the two
|
|
221
|
+
// answer different questions about the same fleet. `null` means never
|
|
222
|
+
// placed, which sorts after every account that has been.
|
|
223
|
+
displayOrder: Number.isFinite(acct.displayOrder) ? acct.displayOrder : null,
|
|
217
224
|
disabled: acct.disabled || false,
|
|
218
225
|
maxUsage: acct.maxUsage ?? null,
|
|
219
226
|
upstream: acct.upstream || null,
|
package/src/sync-accounts.js
CHANGED
|
@@ -94,6 +94,11 @@ export async function syncAccountsFromDisk(diskConfig, memConfig, accountManager
|
|
|
94
94
|
}
|
|
95
95
|
if (diskAcct.name && mgr.name !== diskAcct.name) mgr.name = diskAcct.name;
|
|
96
96
|
if (diskAcct.priority != null && mgr.priority !== diskAcct.priority) mgr.priority = diskAcct.priority;
|
|
97
|
+
// A list position edited on disk applies on reload for the same reason a
|
|
98
|
+
// priority edit does, and `null` rather than `??` so deleting the field
|
|
99
|
+
// puts the account back among the unplaced instead of leaving it stuck at
|
|
100
|
+
// the position it last held. Display only — see makeAccount.
|
|
101
|
+
mgr.displayOrder = Number.isFinite(diskAcct.displayOrder) ? diskAcct.displayOrder : null;
|
|
97
102
|
// A cap edit applies live for the same reason priority does: it is an
|
|
98
103
|
// operator decision about a running fleet, and waiting for a restart to
|
|
99
104
|
// honour a budget defeats the budget.
|
package/src/tui.js
CHANGED
|
@@ -213,15 +213,27 @@ const BAR_MAX = 20;
|
|
|
213
213
|
// terminal lays the table out exactly as it did before the column could grow.
|
|
214
214
|
const NAME_MIN = 12;
|
|
215
215
|
|
|
216
|
-
// Clear space the version label needs on each side before it is drawn at all.
|
|
217
|
-
// Below that it reads as a collision with the title or the port block.
|
|
218
|
-
const HEAD_GAP = 2;
|
|
219
|
-
|
|
220
216
|
// Narrowest label still worth drawing: an ellipsis and three columns of build.
|
|
221
|
-
// Under that the
|
|
217
|
+
// Under that the footer goes back to naming no build at all, which at those
|
|
222
218
|
// widths is the honest answer.
|
|
223
219
|
const HEAD_LABEL_MIN = 4;
|
|
224
220
|
|
|
221
|
+
// Clear space the footer keeps between the last key hint and the build label.
|
|
222
|
+
// Any closer and the label reads as one more hint, which is the one thing it
|
|
223
|
+
// must not look like — there is no key that does it.
|
|
224
|
+
const FOOT_GAP = 2;
|
|
225
|
+
|
|
226
|
+
// Where an account sits in the list the operator arranged — the sort key behind
|
|
227
|
+
// _displayOrder, written by _doMoveAccount and by nothing else.
|
|
228
|
+
//
|
|
229
|
+
// An account with no `displayOrder` has never been placed: every account on a
|
|
230
|
+
// config that predates the field, and every account added since the last
|
|
231
|
+
// arrangement. Those sort after every account that has one, which is where a
|
|
232
|
+
// new account already appeared back when this list was raw array order — so
|
|
233
|
+
// the answer to "where does the one I just logged in with go" does not change
|
|
234
|
+
// with the feature, and there is nothing to migrate.
|
|
235
|
+
const listRank = (/** @type {any} */ a) => (Number.isFinite(a?.displayOrder) ? a.displayOrder : Infinity);
|
|
236
|
+
|
|
225
237
|
// Which pair of bars a row draws: the subscription buckets (Ses/Wk, plus the
|
|
226
238
|
// S7/F7 family bars) when any unified reading exists, else the metered Tok/Req
|
|
227
239
|
// pair an API-key account reports. The account row budget is drawn per
|
|
@@ -288,7 +300,7 @@ export function fitLine(s, w) {
|
|
|
288
300
|
/** The build label at `max` columns, or '' when nothing legible fits.
|
|
289
301
|
*
|
|
290
302
|
* Build metadata is spent before anything is cut. A checkout labels itself
|
|
291
|
-
* `<version>+<sha>`, and of the two it is the version the
|
|
303
|
+
* `<version>+<sha>`, and of the two it is the version the footer is read for;
|
|
292
304
|
* the sha only says which build of it. Shortening the sha instead would be
|
|
293
305
|
* worse than losing it — four hex digits name no commit, and are read as if
|
|
294
306
|
* they did.
|
|
@@ -299,7 +311,7 @@ export function fitLine(s, w) {
|
|
|
299
311
|
* Sliced by code unit against a display-width budget — a version or a sha is
|
|
300
312
|
* ASCII, and a label arriving over the wire is measured again by the caller
|
|
301
313
|
* before it is placed, so a wide glyph costs the label its slot rather than
|
|
302
|
-
* the
|
|
314
|
+
* the footer its width.
|
|
303
315
|
* @param {string} label
|
|
304
316
|
* @param {number} max */
|
|
305
317
|
export function fitHeadLabel(label, max) {
|
|
@@ -490,7 +502,7 @@ export class TUI {
|
|
|
490
502
|
this.mode = 'normal'; // normal | select | add | input | settings | pick
|
|
491
503
|
this.pick = null; // active list picker (routes editor accounts/bucket/color)
|
|
492
504
|
this.pickReturn = 'routes'; // mode to fall back to when the picker closes
|
|
493
|
-
this.selAction = null; // switch | remove | toggle
|
|
505
|
+
this.selAction = null; // switch | remove | toggle | reorder
|
|
494
506
|
this.selIdx = 0;
|
|
495
507
|
this.selRoute = null; // in switch mode: null = global default, else a getRoutes() entry to pin
|
|
496
508
|
this.selReturn = 'normal'; // mode to fall back to when select mode closes
|
|
@@ -900,6 +912,18 @@ export class TUI {
|
|
|
900
912
|
});
|
|
901
913
|
}
|
|
902
914
|
|
|
915
|
+
// Two rows is the least that can be arranged. Below that the row would open
|
|
916
|
+
// a screen on which no key does anything.
|
|
917
|
+
if (this._displayOrder().length > 1) {
|
|
918
|
+
fields.push({
|
|
919
|
+
id: 'orderAccounts',
|
|
920
|
+
label: 'Reorder accounts',
|
|
921
|
+
hint: 'Enter to arrange',
|
|
922
|
+
value: () => dim('—'),
|
|
923
|
+
enter: () => { this.mode = 'select'; this.selAction = 'reorder'; this.selIdx = this._displayOrder()[0] ?? 0; this.selReturn = 'settings'; },
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
903
927
|
fields.push({
|
|
904
928
|
id: 'upstreamProxy',
|
|
905
929
|
label: 'Upstream proxy',
|
|
@@ -1063,11 +1087,22 @@ export class TUI {
|
|
|
1063
1087
|
// can only ask for the default account — leaves these keys alone.
|
|
1064
1088
|
else if ((k === 'tab' || k === 'right') && this.selAction === 'switch' && !this.remote) this._cycleSelRoute(+1);
|
|
1065
1089
|
else if (k === 'left' && this.selAction === 'switch' && !this.remote) this._cycleSelRoute(-1);
|
|
1090
|
+
// ←→ in reorder mode move the ACCOUNT, not the cursor. ↑↓ already walk the
|
|
1091
|
+
// rows, so this is the pair left over, and ←→ is what "change the thing the
|
|
1092
|
+
// cursor is on" already means on the settings screen this is opened from.
|
|
1093
|
+
// The cursor does not move with the key: `selIdx` names the account being
|
|
1094
|
+
// dragged, and the row it marks travels with it.
|
|
1095
|
+
else if ((k === 'left' || k === 'h') && this.selAction === 'reorder') this._doMoveAccount(-1);
|
|
1096
|
+
else if ((k === 'right' || k === 'l') && this.selAction === 'reorder') this._doMoveAccount(+1);
|
|
1066
1097
|
else if (k === 'enter') {
|
|
1067
1098
|
if (this.selAction === 'switch') {
|
|
1068
1099
|
this._doSwitchSelection();
|
|
1069
1100
|
} else if (this.selAction === 'toggle') {
|
|
1070
1101
|
this._doToggleDisabled(this.selIdx);
|
|
1102
|
+
} else if (this.selAction === 'reorder') {
|
|
1103
|
+
// Every move has already been saved, so Enter only means "done" — and
|
|
1104
|
+
// it has to be caught here, ahead of the remove branch below, which is
|
|
1105
|
+
// what an unlisted action falls into.
|
|
1071
1106
|
} else {
|
|
1072
1107
|
this._doRemove(this.selIdx);
|
|
1073
1108
|
}
|
|
@@ -1481,6 +1516,50 @@ export class TUI {
|
|
|
1481
1516
|
this._addLog(`${next ? 'Disabled' : 'Enabled'} account "${acct.name}"`);
|
|
1482
1517
|
}
|
|
1483
1518
|
|
|
1519
|
+
/** Move the selected account `delta` rows through the list as drawn.
|
|
1520
|
+
*
|
|
1521
|
+
* The array is NOT permuted. `selIdx` is a manager index, and so are route
|
|
1522
|
+
* pins, session pins, `currentIndex`, `TC_ACCT` and the disable/switch CLI
|
|
1523
|
+
* paths — reordering `am.accounts` would silently repoint every one of them
|
|
1524
|
+
* at a different account. So the rows are sorted by a field instead and each
|
|
1525
|
+
* account keeps the slot it has held since startup.
|
|
1526
|
+
*
|
|
1527
|
+
* It is not `priority` either. That field is rotation preference and only
|
|
1528
|
+
* one account in a fleet usually carries a non-default value — a
|
|
1529
|
+
* deliberately deprioritised local backend, say. Deriving it from where a
|
|
1530
|
+
* row sits on screen would re-rank the fleet as a side effect of tidying the
|
|
1531
|
+
* display, which is a routing change nobody asked for.
|
|
1532
|
+
*
|
|
1533
|
+
* Every displayed account is renumbered from its new position rather than
|
|
1534
|
+
* only the two that moved: before the first move there are no numbers to
|
|
1535
|
+
* insert between, and a dense 0..n-1 is the form that reads in a hand-edited
|
|
1536
|
+
* config. Local backends are excluded throughout — _displayOrder filters
|
|
1537
|
+
* them out before this ever sees them, so they hold no position and their
|
|
1538
|
+
* array slots are simply skipped over.
|
|
1539
|
+
*
|
|
1540
|
+
* @param {number} delta rows to travel: -1 up the list, +1 down it
|
|
1541
|
+
*/
|
|
1542
|
+
async _doMoveAccount(delta) {
|
|
1543
|
+
const order = this._displayOrder();
|
|
1544
|
+
const from = order.indexOf(this.selIdx);
|
|
1545
|
+
const to = from + delta;
|
|
1546
|
+
if (from < 0 || to < 0 || to >= order.length) return; // already at the end it was pushed against
|
|
1547
|
+
order.splice(to, 0, ...order.splice(from, 1));
|
|
1548
|
+
order.forEach((/** @type {number} */ mgrIdx, /** @type {number} */ pos) => {
|
|
1549
|
+
this.am.accounts[mgrIdx].displayOrder = pos;
|
|
1550
|
+
// Onto this account's own entry: a manager index is not a config index
|
|
1551
|
+
// (account-pairing.js), and an account whose entry the config no longer
|
|
1552
|
+
// holds keeps its position on screen with nothing to persist.
|
|
1553
|
+
const cfgIdx = configIndexFor(this.config.accounts, this.am.accounts, mgrIdx);
|
|
1554
|
+
if (cfgIdx >= 0) this.config.accounts[cfgIdx].displayOrder = pos;
|
|
1555
|
+
});
|
|
1556
|
+
// Nothing is logged on success. The row visibly moves, which is the whole
|
|
1557
|
+
// feedback a drag needs, and a held arrow key would otherwise push the
|
|
1558
|
+
// activity pane out from under the list being arranged.
|
|
1559
|
+
try { await this.saveConfig(this.config); }
|
|
1560
|
+
catch (/** @type {any} */ e) { this._addLog(`Failed to save: ${e.message}`); }
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1484
1563
|
// ── rendering ──────────────────────────────────────
|
|
1485
1564
|
|
|
1486
1565
|
render({ force = false } = {}) {
|
|
@@ -1565,44 +1644,15 @@ export class TUI {
|
|
|
1565
1644
|
// mode): what is on screen is the last snapshot, not the current state.
|
|
1566
1645
|
const live = this.am.connected === false ? red('▼') : green('▲');
|
|
1567
1646
|
const right = `${sessStr}Port ${port} ${live} `;
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
// The marker is budgeted before the label is cut, so a shortened label and
|
|
1578
|
-
// its marker still fit the room they were measured against.
|
|
1579
|
-
const room = W - lw - rw;
|
|
1580
|
-
const markerW = upd ? 2 : 0;
|
|
1581
|
-
const text = label ? fitHeadLabel(label, room - 2 * HEAD_GAP - markerW) : '';
|
|
1582
|
-
const mid = text ? dim(text) + (upd ? ` ${green('▲')}` : '') : '';
|
|
1583
|
-
const mw = vw(mid);
|
|
1584
|
-
// Centred on the line, not in the gap between the two blocks, so the label
|
|
1585
|
-
// holds still as the session segment comes and goes.
|
|
1586
|
-
const start = Math.floor((W - mw) / 2);
|
|
1587
|
-
// Load-bearing, not cosmetic: both padding runs below would be negative
|
|
1588
|
-
// without it, and ' '.repeat(-1) throws. Satisfying it also means the mid
|
|
1589
|
-
// branch can never produce the over-wide line the other branch can, so the
|
|
1590
|
-
// two are not interchangeable.
|
|
1591
|
-
const midFits = mw > 0 && start - lw >= HEAD_GAP && (W - rw) - (start + mw) >= HEAD_GAP;
|
|
1592
|
-
// Line-centring is a position, not a fit. The two blocks are different
|
|
1593
|
-
// widths, so a label small enough for the gap can still be pushed inside
|
|
1594
|
-
// one of them by where the centre of the LINE falls — and that, not width,
|
|
1595
|
-
// is what used to drop the label every time the session segment grew,
|
|
1596
|
-
// leaving a header that silently stopped naming the build it exists to
|
|
1597
|
-
// name. Centre it in the GAP instead, which is exact by construction: the
|
|
1598
|
-
// two runs below sum to `room`. The label moves when sessions come and go,
|
|
1599
|
-
// which is the price; being able to read it is what that buys.
|
|
1600
|
-
const gapPad = mw > 0 && room - mw >= 2 * HEAD_GAP ? Math.floor((room - mw) / 2) : -1;
|
|
1601
|
-
lines.push(midFits
|
|
1602
|
-
? left + ' '.repeat(start - lw) + mid + ' '.repeat(W - rw - start - mw) + right
|
|
1603
|
-
: gapPad >= 0
|
|
1604
|
-
? left + ' '.repeat(gapPad) + mid + ' '.repeat(room - mw - gapPad) + right
|
|
1605
|
-
: left + ' '.repeat(Math.max(1, W - lw - rw)) + right);
|
|
1647
|
+
// Title, padding, port block — and nothing between them. The build this
|
|
1648
|
+
// checkout runs is named once, in the corner of the footer.
|
|
1649
|
+
//
|
|
1650
|
+
// That padding run is the whole of the line's arithmetic now, and it is
|
|
1651
|
+
// floored rather than trusted: a wide session segment on a narrow terminal
|
|
1652
|
+
// can leave the two blocks alone wider than the line, and ' '.repeat(-1)
|
|
1653
|
+
// throws. There the header overruns and fitLine takes its tail, as it has
|
|
1654
|
+
// since long before there was a label to lose.
|
|
1655
|
+
lines.push(left + ' '.repeat(Math.max(1, W - vw(left) - vw(right))) + right);
|
|
1606
1656
|
lines.push(' ' + dim('─'.repeat(W - 2)));
|
|
1607
1657
|
|
|
1608
1658
|
const footerH = 2;
|
|
@@ -1798,11 +1848,21 @@ export class TUI {
|
|
|
1798
1848
|
* Display only. `selIdx`, `currentIndex`, session pins and route entries all
|
|
1799
1849
|
* stay manager indices, so nothing about selection or routing moves with the
|
|
1800
1850
|
* rows — see _keySelect, which walks this order but still stores an index.
|
|
1851
|
+
*
|
|
1852
|
+
* Which is also why the operator's arrangement is a sort key and not a
|
|
1853
|
+
* permutation of `am.accounts`: see _doMoveAccount.
|
|
1801
1854
|
*/
|
|
1802
1855
|
_displayOrder() {
|
|
1803
1856
|
return this.am.accounts
|
|
1804
1857
|
.map((/** @type {any} */ _, /** @type {number} */ i) => i)
|
|
1805
|
-
.filter(i => !isLocalUpstream(this.am.accounts[i]))
|
|
1858
|
+
.filter(i => !isLocalUpstream(this.am.accounts[i]))
|
|
1859
|
+
.sort((/** @type {number} */ a, /** @type {number} */ b) => {
|
|
1860
|
+
const ra = listRank(this.am.accounts[a]);
|
|
1861
|
+
const rb = listRank(this.am.accounts[b]);
|
|
1862
|
+
// Infinity !== Infinity is false, so two unplaced accounts fall through
|
|
1863
|
+
// to the index rather than subtracting to NaN.
|
|
1864
|
+
return ra === rb ? a - b : ra - rb;
|
|
1865
|
+
});
|
|
1806
1866
|
}
|
|
1807
1867
|
|
|
1808
1868
|
/** Manager indices of the local backends, in config order. */
|
|
@@ -2052,9 +2112,10 @@ export class TUI {
|
|
|
2052
2112
|
lines.push(row(byId('blocklist')));
|
|
2053
2113
|
lines.push('');
|
|
2054
2114
|
// ── Accounts
|
|
2055
|
-
lines.push(bold(' Accounts') + dim(' — add (import / API key) or
|
|
2115
|
+
lines.push(bold(' Accounts') + dim(' — add (import / API key), remove, or set the order they list in'));
|
|
2056
2116
|
lines.push(row(byId('addAccount')));
|
|
2057
2117
|
if (byId('removeAccount')) lines.push(row(byId('removeAccount')));
|
|
2118
|
+
if (byId('orderAccounts')) lines.push(row(byId('orderAccounts')));
|
|
2058
2119
|
lines.push('');
|
|
2059
2120
|
// ── Network
|
|
2060
2121
|
// Drawn before the sx.org block, which returns early when sx is unavailable:
|
|
@@ -2377,12 +2438,26 @@ export class TUI {
|
|
|
2377
2438
|
}
|
|
2378
2439
|
}
|
|
2379
2440
|
|
|
2380
|
-
/**
|
|
2441
|
+
/** The footer line: the mode's key hints from the left, the build label set
|
|
2442
|
+
* against the right edge.
|
|
2443
|
+
* @param {number} [W] columns to compose against */
|
|
2381
2444
|
_renderFooter(W = process.stdout.columns || 80) {
|
|
2382
2445
|
// A restart drain outranks every mode: the keys this line would otherwise
|
|
2383
2446
|
// advertise are all refused while one runs (see _key), and how far the
|
|
2384
2447
|
// drain has got is the only thing on screen worth the row.
|
|
2448
|
+
//
|
|
2449
|
+
// It is also the one footer drawn without the build label or its update
|
|
2450
|
+
// marker. The line lives for at most the deadline, the build cannot change
|
|
2451
|
+
// under it, and when the restart is an update the label names the build
|
|
2452
|
+
// being replaced and the marker points at the thing already happening — so
|
|
2453
|
+
// those columns go to the escape hatch, which is what the line is read for.
|
|
2385
2454
|
if (this._restartDrain) return this._restartDrainFooter(this._restartDrain, W);
|
|
2455
|
+
return this._footerWithVersion(this._footerHints(), W);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/** What the keyboard does on the screen the operator is looking at. Composed
|
|
2459
|
+
* without regard to width: fitting it to the line is _renderFooter's job. */
|
|
2460
|
+
_footerHints() {
|
|
2386
2461
|
switch (this.mode) {
|
|
2387
2462
|
case 'normal':
|
|
2388
2463
|
return this.remote
|
|
@@ -2408,6 +2483,12 @@ export class TUI {
|
|
|
2408
2483
|
: 'default';
|
|
2409
2484
|
return ` ${dim('↑↓')} select ${dim('←→')} target: ${target} ${bold('Enter')} pin ${bold('Esc')} cancel`;
|
|
2410
2485
|
}
|
|
2486
|
+
// Both keys leave, because each move has already been saved: there is
|
|
2487
|
+
// no pending change for one to commit and the other to throw away, and
|
|
2488
|
+
// offering "cancel" would promise an undo this screen does not have.
|
|
2489
|
+
if (this.selAction === 'reorder') {
|
|
2490
|
+
return ` ${dim('↑↓')} select ${dim('←→')} move ${bold('Enter')}/${bold('Esc')} done`;
|
|
2491
|
+
}
|
|
2411
2492
|
const act = this.selAction === 'toggle' ? 'enable/disable' : 'remove';
|
|
2412
2493
|
return ` ${dim('↑↓')} select ${bold('Enter')} ${act} ${bold('Esc')} cancel`;
|
|
2413
2494
|
}
|
|
@@ -2420,6 +2501,55 @@ export class TUI {
|
|
|
2420
2501
|
}
|
|
2421
2502
|
}
|
|
2422
2503
|
|
|
2504
|
+
/**
|
|
2505
|
+
* `hints` with the build label — and, when one is waiting, an update marker —
|
|
2506
|
+
* set against the right edge of a W-column line. This is the only place the
|
|
2507
|
+
* display names either: the header carries its title and the port block and
|
|
2508
|
+
* nothing else.
|
|
2509
|
+
*
|
|
2510
|
+
* Composed to exactly W, never less. The paint loop pads every short line out
|
|
2511
|
+
* to the terminal width and truncates the tail of every long one (fitLine), so
|
|
2512
|
+
* a label merely appended is pushed out of the corner it was put in, and a
|
|
2513
|
+
* line built past W has that same corner eaten. Both failures are silent, and
|
|
2514
|
+
* both look like the label was never drawn at all.
|
|
2515
|
+
*
|
|
2516
|
+
* The hints are never cut to make room. They are the only thing on this line
|
|
2517
|
+
* that says what the keyboard does: a build the operator cannot read costs
|
|
2518
|
+
* them nothing, a key they cannot read costs them the screen. So the label is
|
|
2519
|
+
* fitted to whatever the hints leave over — fitHeadLabel spends the build sha
|
|
2520
|
+
* first and then the head of the version — and it is dropped whole when what
|
|
2521
|
+
* is left will not carry it.
|
|
2522
|
+
*
|
|
2523
|
+
* @param {string} hints
|
|
2524
|
+
* @param {number} W
|
|
2525
|
+
*/
|
|
2526
|
+
_footerWithVersion(hints, W) {
|
|
2527
|
+
// In attach mode the dashboard names the server's build, not this
|
|
2528
|
+
// process's, so the account manager's answer wins for both. It arrives
|
|
2529
|
+
// sanitized (applyStatus) and starts empty, which keeps the corner blank
|
|
2530
|
+
// until the first poll rather than briefly naming the local checkout as if
|
|
2531
|
+
// it were the server's. A local AccountManager has neither property.
|
|
2532
|
+
// `??` on purpose: '' is that deliberate blank, not a miss.
|
|
2533
|
+
const label = this.am.versionLabel ?? this.versionLabel;
|
|
2534
|
+
const upd = this.am.updateAvailable ?? this.updateAvailable;
|
|
2535
|
+
// The marker rides with the label and is never drawn without it. Alone in
|
|
2536
|
+
// the corner a bare glyph names nothing it could be an update TO, and reads
|
|
2537
|
+
// as one more key hint — the one thing this end of the line must not look
|
|
2538
|
+
// like. It would also be the wrong answer for an attached dashboard that
|
|
2539
|
+
// has not polled yet: no server label, but this process's update flag.
|
|
2540
|
+
if (!label) return hints;
|
|
2541
|
+
const hw = vw(hints);
|
|
2542
|
+
// Budgeted before the label is cut, so a shortened label and its marker
|
|
2543
|
+
// still fit the room they were measured against.
|
|
2544
|
+
const markerW = upd ? 2 : 0;
|
|
2545
|
+
// One column of margin at the edge — what the rule under the header and
|
|
2546
|
+
// the header's port block both leave.
|
|
2547
|
+
const text = fitHeadLabel(label, W - hw - FOOT_GAP - markerW - 1);
|
|
2548
|
+
if (!text) return hints;
|
|
2549
|
+
const marker = upd ? ` ${green('▲')}` : '';
|
|
2550
|
+
return `${hints}${' '.repeat(W - hw - vw(text) - markerW - 1)}${dim(text)}${marker} `;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2423
2553
|
/**
|
|
2424
2554
|
* The footer while this process drains to a restart: how long the wait has
|
|
2425
2555
|
* run against the bound it cannot exceed, what is still holding it, and the
|