teamclaude-cloud 1.5.0 → 1.5.2
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/index.js +79 -5
- package/src/tui.js +21 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -309,6 +309,41 @@ async function serverCommand() {
|
|
|
309
309
|
if (disabled) set.add(uuid); else set.delete(uuid);
|
|
310
310
|
cloud.localDisabled = [...set];
|
|
311
311
|
}),
|
|
312
|
+
// `p` in the dashboard = `teamclaude cloud push`, so the admin can register the
|
|
313
|
+
// server's live account tokens to the cloud without dropping to a shell. Only
|
|
314
|
+
// wired when a cloud config exists (else the footer hides the key). Uses the
|
|
315
|
+
// running AccountManager's LIVE tokens (freshest) and re-reads the cloud session
|
|
316
|
+
// from disk so a `cloud login` done after the server started is picked up.
|
|
317
|
+
cloudPush: config.cloud ? (async () => {
|
|
318
|
+
// Use the running server's LIVE tokens (freshest) for each account.
|
|
319
|
+
const accounts = (config.accounts || []).filter(a => a.type !== 'apikey').map(a => {
|
|
320
|
+
const am = (a.accountUuid && accountManager.accounts.find(x => x.accountUuid === a.accountUuid))
|
|
321
|
+
|| accountManager.accounts.find(x => x.name === a.name);
|
|
322
|
+
return am ? { ...a, accessToken: am.credential, refreshToken: am.refreshToken, expiresAt: am.expiresAt } : a;
|
|
323
|
+
});
|
|
324
|
+
if (!accounts.length) throw new Error('No OAuth accounts to push');
|
|
325
|
+
// Prefer the AUTH KEY (push to your own tenant, no expiring owner login).
|
|
326
|
+
const fresh = (await loadConfig()) || config;
|
|
327
|
+
const key = fresh.cloud?.pullKey || config.cloud?.pullKey;
|
|
328
|
+
if (key) {
|
|
329
|
+
const r = await cloudPushViaKey(resolveCloud(fresh), key, accounts);
|
|
330
|
+
const na = r.notAllowed ? `, ${r.notAllowed} not in the key's group` : '';
|
|
331
|
+
const sk = r.skipped ? `, ${r.skipped} skipped` : '';
|
|
332
|
+
return { summary: `${r.updated} updated, ${r.unchanged} unchanged${na}${sk} (via auth key)` };
|
|
333
|
+
}
|
|
334
|
+
// No key — owner login (also creates new accounts) + usage snapshot.
|
|
335
|
+
const cloud = await ensureCloudSession(fresh);
|
|
336
|
+
if (!cloud?.session?.access_token) throw new Error('No auth key and not logged in — pull once with a key, or run `teamclaude cloud login`');
|
|
337
|
+
const usageByUuid = new Map();
|
|
338
|
+
const quotaCache = await readQuotaCache();
|
|
339
|
+
for (const entry of quotaCache?.accounts || []) {
|
|
340
|
+
if (!entry?.accountUuid) continue;
|
|
341
|
+
const usage = buildUsageFromQuota(entry);
|
|
342
|
+
if (usage) usageByUuid.set(entry.accountUuid, usage);
|
|
343
|
+
}
|
|
344
|
+
const r = await cloudPush(cloud, accounts, usageByUuid);
|
|
345
|
+
return { summary: `${r.pushed} new, ${r.updated} updated, ${r.skipped} skipped${usageByUuid.size ? `, usage ${r.usage ?? 0}` : ''}` };
|
|
346
|
+
}) : undefined,
|
|
312
347
|
});
|
|
313
348
|
hooks = {
|
|
314
349
|
onRequestStart: (id, info) => tui.onRequestStart(id, info),
|
|
@@ -1554,6 +1589,17 @@ async function ensureCloudSession(config) {
|
|
|
1554
1589
|
// atomicConfigUpdate's documented cross-process limitation).
|
|
1555
1590
|
await _cloudSessionRefresh;
|
|
1556
1591
|
}
|
|
1592
|
+
// If the session is (still) expired and cannot be refreshed — a Google/SSO token
|
|
1593
|
+
// login (`cloud login --token`) stores NO refresh token, so its ~1h token simply
|
|
1594
|
+
// lapses — fail with an actionable message instead of letting the caller hit a
|
|
1595
|
+
// bare 401 unauthorized.
|
|
1596
|
+
const curExpMs = (Number(cloud.session.expires_at) || 0) * 1000;
|
|
1597
|
+
if (curExpMs && Date.now() >= curExpMs) {
|
|
1598
|
+
const hint = cloud.session.refresh_token
|
|
1599
|
+
? 'Run `teamclaude cloud login` to sign in again.'
|
|
1600
|
+
: 'Re-copy the CLI login token from the dashboard (the "관리자 로그인" box in the Auth Keys section) and run `teamclaude cloud login --token`.';
|
|
1601
|
+
throw new Error(`Cloud session expired. ${hint}`);
|
|
1602
|
+
}
|
|
1557
1603
|
return cloud;
|
|
1558
1604
|
}
|
|
1559
1605
|
|
|
@@ -1681,14 +1727,42 @@ async function cloudLoginCommand(config) {
|
|
|
1681
1727
|
console.log(`Logged in to cloud as ${email}.`);
|
|
1682
1728
|
}
|
|
1683
1729
|
|
|
1730
|
+
// Push account tokens to the cloud using an AUTH KEY — no owner login needed. Reuses
|
|
1731
|
+
// the key-authenticated, group-scoped, freshest-wins /sync/report-token path, so the
|
|
1732
|
+
// tokens land in the KEY'S OWNER tenant (your personal key → your own account). This
|
|
1733
|
+
// UPDATES accounts already registered + in the key's group; an account the key can't
|
|
1734
|
+
// pull is reported as "not in the key's group" (create it via an owner login once).
|
|
1735
|
+
async function cloudPushViaKey(cloud, key, accounts) {
|
|
1736
|
+
const r = { updated: 0, unchanged: 0, notAllowed: 0, skipped: 0 };
|
|
1737
|
+
for (const a of accounts) {
|
|
1738
|
+
if (!a.accountUuid || !a.accessToken || !a.refreshToken) { r.skipped++; continue; }
|
|
1739
|
+
try {
|
|
1740
|
+
const res = await cloudReportToken(cloud, key, a.accountUuid, { accessToken: a.accessToken, refreshToken: a.refreshToken, expiresAt: a.expiresAt });
|
|
1741
|
+
if (res.action === 'updated') r.updated++; else r.unchanged++;
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
if (/\(40[34]\)/.test(e.message)) r.notAllowed++; else r.skipped++; // 403 not in group / 404 unknown account
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return r;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1684
1749
|
async function cloudPushCommand(config) {
|
|
1685
|
-
const cloud = await ensureCloudSession(config);
|
|
1686
|
-
if (!cloud?.session?.access_token) { console.error('Not logged in. Run `teamclaude cloud login`.'); process.exit(1); }
|
|
1687
1750
|
const accounts = (config.accounts || []).filter(a => a.type !== 'apikey');
|
|
1688
1751
|
if (!accounts.length) { console.error('No OAuth accounts to push.'); process.exit(1); }
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
|
|
1752
|
+
// Prefer the AUTH KEY: pushing to your own tenant is the key's job — no separate,
|
|
1753
|
+
// short-lived owner login required (the key is the credential, same as pull).
|
|
1754
|
+
const key = config.cloud?.pullKey || process.env.TEAMCLAUDE_AUTH_KEY;
|
|
1755
|
+
if (key) {
|
|
1756
|
+
const r = await cloudPushViaKey(resolveCloud(config), key, accounts);
|
|
1757
|
+
const na = r.notAllowed ? `, ${r.notAllowed} not in the key's group` : '';
|
|
1758
|
+
const sk = r.skipped ? `, ${r.skipped} skipped (no/expired token)` : '';
|
|
1759
|
+
console.log(`Pushed tokens via auth key: ${r.updated} updated, ${r.unchanged} unchanged${na}${sk}.`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
// No auth key — fall back to an owner login (which also CREATES new accounts) and
|
|
1763
|
+
// rides the quota snapshot so the dashboard can show utilization.
|
|
1764
|
+
const cloud = await ensureCloudSession(config);
|
|
1765
|
+
if (!cloud?.session?.access_token) { console.error('No auth key found and not logged in. Pull once with `teamclaude cloud pull --key <key>`, or `teamclaude cloud login`.'); process.exit(1); }
|
|
1692
1766
|
const usageByUuid = new Map();
|
|
1693
1767
|
const quotaCache = await readQuotaCache();
|
|
1694
1768
|
for (const entry of quotaCache?.accounts || []) {
|
package/src/tui.js
CHANGED
|
@@ -114,7 +114,7 @@ function timestamp() {
|
|
|
114
114
|
// ── TUI class ────────────────────────────────────────────────
|
|
115
115
|
|
|
116
116
|
export class TUI {
|
|
117
|
-
constructor({ accountManager, config, saveConfig, syncAccounts, refreshQuota, onQuit, setLocalDisabled }) {
|
|
117
|
+
constructor({ accountManager, config, saveConfig, syncAccounts, refreshQuota, onQuit, setLocalDisabled, cloudPush }) {
|
|
118
118
|
this.am = accountManager;
|
|
119
119
|
this.config = config;
|
|
120
120
|
this.saveConfig = saveConfig;
|
|
@@ -122,6 +122,7 @@ export class TUI {
|
|
|
122
122
|
this.refreshQuota = refreshQuota; // optional: forced fleet quota re-measure (R)
|
|
123
123
|
this.onQuit = onQuit;
|
|
124
124
|
this.setLocalDisabled = setLocalDisabled; // optional: persist offline local-disable overlay (by uuid) to disk (fresh-read merge)
|
|
125
|
+
this.cloudPush = cloudPush; // optional: push account tokens to the cloud (`p`) — returns {pushed,updated,skipped,usage}
|
|
125
126
|
|
|
126
127
|
this.log = []; // completed activity entries
|
|
127
128
|
this.active = new Map(); // in-flight requests
|
|
@@ -263,6 +264,7 @@ export class TUI {
|
|
|
263
264
|
if (k === 'q') { this.stop(); this.onQuit?.(); return; }
|
|
264
265
|
if (k === 'a') { this.mode = 'add'; return; }
|
|
265
266
|
if (k === 'R') { this._doSync(); return; }
|
|
267
|
+
if (k === 'p') { this._doPush(); return; }
|
|
266
268
|
|
|
267
269
|
if (this.am.accounts.length === 0) return;
|
|
268
270
|
|
|
@@ -390,6 +392,19 @@ export class TUI {
|
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
|
|
395
|
+
// `p` — push this machine's account tokens (+ usage snapshot) to the cloud, the
|
|
396
|
+
// same as `teamclaude push` / `teamclaude cloud push`, without leaving the dashboard.
|
|
397
|
+
async _doPush() {
|
|
398
|
+
if (!this.cloudPush) { this._addLog('Cloud push unavailable (no cloud config)'); return; }
|
|
399
|
+
try {
|
|
400
|
+
this._addLog('Pushing account tokens to the cloud...');
|
|
401
|
+
const r = await this.cloudPush();
|
|
402
|
+
this._addLog(`Cloud push: ${r?.summary ?? 'done'}`);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
this._addLog(`Cloud push failed: ${e.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
393
408
|
async _doImport() {
|
|
394
409
|
try {
|
|
395
410
|
this._addLog('Importing credentials...');
|
|
@@ -835,8 +850,11 @@ export class TUI {
|
|
|
835
850
|
|
|
836
851
|
_renderFooter() {
|
|
837
852
|
switch (this.mode) {
|
|
838
|
-
case 'normal':
|
|
839
|
-
|
|
853
|
+
case 'normal': {
|
|
854
|
+
// `p`ush is only shown when a cloud config is present (the callback is wired).
|
|
855
|
+
const push = this.cloudPush ? ` ${bold('p')}ush` : '';
|
|
856
|
+
return ` ${dim('↑↓')} select ${bold('s')}witch ${bold('e')}nable/disable ${bold('o')}rder ${bold('d')}elete ${bold('a')}dd${push} ${bold('R')}eload ${bold('q')}uit`;
|
|
857
|
+
}
|
|
840
858
|
case 'select':
|
|
841
859
|
return ` ${dim('↑↓')} select ${bold('Enter')} delete ${bold('Esc')} cancel`;
|
|
842
860
|
case 'order':
|