@phnx-labs/agents-cli 1.20.67 → 1.20.69
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/CHANGELOG.md +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
package/dist/commands/share.d.ts
CHANGED
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
export declare function registerShareCommands(program: Command): void;
|
|
3
|
+
/** Provision a fresh R2 bucket + Worker on the user's Cloudflare and persist the
|
|
4
|
+
* endpoint config + write token. Shared by `agents share setup` and the unified
|
|
5
|
+
* `agents setup share` wizard. */
|
|
6
|
+
export declare function runShareProvision(opts: {
|
|
7
|
+
bundle: string;
|
|
8
|
+
worker: string;
|
|
9
|
+
bucket: string;
|
|
10
|
+
account?: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
domain?: string;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
/** Join an existing share endpoint (no provisioning): prompt for the endpoint
|
|
15
|
+
* details + write token and persist them. Shared by `agents share join` and the
|
|
16
|
+
* unified `agents setup share` wizard. */
|
|
17
|
+
export declare function runShareJoin(baseUrl: string): Promise<void>;
|
package/dist/commands/share.js
CHANGED
|
@@ -52,7 +52,7 @@ export function registerShareCommands(program) {
|
|
|
52
52
|
.option('--domain <host>', 'also map a custom domain (e.g. share.agents-cli.sh) if the token owns the zone')
|
|
53
53
|
.action(async (opts) => {
|
|
54
54
|
try {
|
|
55
|
-
await
|
|
55
|
+
await runShareProvision(opts);
|
|
56
56
|
}
|
|
57
57
|
catch (e) {
|
|
58
58
|
console.error(chalk.red(e.message));
|
|
@@ -65,7 +65,7 @@ export function registerShareCommands(program) {
|
|
|
65
65
|
.argument('<baseUrl>', 'base URL of the endpoint, e.g. https://share.agents-cli.sh')
|
|
66
66
|
.action(async (baseUrl) => {
|
|
67
67
|
try {
|
|
68
|
-
await
|
|
68
|
+
await runShareJoin(baseUrl);
|
|
69
69
|
}
|
|
70
70
|
catch (e) {
|
|
71
71
|
console.error(chalk.red(e.message));
|
|
@@ -85,7 +85,10 @@ export function registerShareCommands(program) {
|
|
|
85
85
|
console.log(chalk.dim(`worker ${cfg.workerName} · bucket ${cfg.bucketName} · account ${cfg.accountId}`));
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
/** Provision a fresh R2 bucket + Worker on the user's Cloudflare and persist the
|
|
89
|
+
* endpoint config + write token. Shared by `agents share setup` and the unified
|
|
90
|
+
* `agents setup share` wizard. */
|
|
91
|
+
export async function runShareProvision(opts) {
|
|
89
92
|
const { default: ora } = await import('ora');
|
|
90
93
|
const { input } = await import('@inquirer/prompts');
|
|
91
94
|
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle, {
|
|
@@ -132,7 +135,10 @@ async function runSetup(opts) {
|
|
|
132
135
|
throw e;
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
|
-
|
|
138
|
+
/** Join an existing share endpoint (no provisioning): prompt for the endpoint
|
|
139
|
+
* details + write token and persist them. Shared by `agents share join` and the
|
|
140
|
+
* unified `agents setup share` wizard. */
|
|
141
|
+
export async function runShareJoin(baseUrl) {
|
|
136
142
|
const { password, input } = await import('@inquirer/prompts');
|
|
137
143
|
const clean = baseUrl.replace(/\/+$/, '');
|
|
138
144
|
const workerName = await input({ message: 'Worker name', default: DEFAULT_WORKER_NAME });
|
package/dist/commands/ssh.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as os from 'os';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import ora from 'ora';
|
|
18
|
+
import { getCliVersion } from '../lib/version.js';
|
|
18
19
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
19
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
20
21
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
@@ -28,8 +29,16 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
28
29
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
29
30
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
30
31
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
32
|
+
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
33
|
+
import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
34
|
+
import { fleetCapacity, fmtBytes, headroom, probeLocalStats, probeFleetStats, } from '../lib/devices/health.js';
|
|
35
|
+
import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
36
|
+
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
37
|
+
import { checkAllClis } from '../lib/teams/agents.js';
|
|
38
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
39
|
+
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
40
|
+
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
41
|
+
import { formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, summarizeVerdicts, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
33
42
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
34
43
|
* command is running on so it stands out from the rest of the tailnet. */
|
|
35
44
|
function deviceSummary(d, isSelf = false) {
|
|
@@ -219,6 +228,236 @@ function printFleetResults(results) {
|
|
|
219
228
|
if (failed > 0)
|
|
220
229
|
process.exitCode = 1;
|
|
221
230
|
}
|
|
231
|
+
function localHealthRow(self, stats) {
|
|
232
|
+
return {
|
|
233
|
+
name: self,
|
|
234
|
+
platform: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
235
|
+
version: getCliVersion(),
|
|
236
|
+
stats,
|
|
237
|
+
clis: checkAllClis(),
|
|
238
|
+
sync: checkSyncStatus(process.cwd()),
|
|
239
|
+
orphans: countOrphans(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function probeRemoteHealth(target) {
|
|
243
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
244
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
245
|
+
const versionCmd = buildRemoteAgentsInvocation(['--version'], undefined, isWin ? 'windows' : undefined, env);
|
|
246
|
+
const versionRes = await sshExecAsync(target.name, versionCmd, { timeoutMs: 15000, multiplex: true });
|
|
247
|
+
const version = versionRes.code === 0 ? versionRes.stdout.trim().split(/\s+/)[0] || null : null;
|
|
248
|
+
const doctorCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
249
|
+
const doctorRes = await sshExecAsync(target.name, doctorCmd, { timeoutMs: 30000, multiplex: true });
|
|
250
|
+
if (doctorRes.code !== 0) {
|
|
251
|
+
throw new Error(doctorRes.timedOut ? 'timed out' : (doctorRes.stderr.trim() || `exit ${doctorRes.code ?? 'unknown'}`));
|
|
252
|
+
}
|
|
253
|
+
const parsed = JSON.parse(doctorRes.stdout);
|
|
254
|
+
return {
|
|
255
|
+
version,
|
|
256
|
+
clis: parsed.clis ?? {},
|
|
257
|
+
sync: parsed.sync ?? [],
|
|
258
|
+
orphans: parsed.orphans ?? [],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function runFleetStatus(opts) {
|
|
262
|
+
const reg = await loadDevices();
|
|
263
|
+
const self = machineId();
|
|
264
|
+
const planned = planFleetTargets(reg);
|
|
265
|
+
const probeable = planned.filter((t) => !t.skip).map((t) => t.device);
|
|
266
|
+
const statsMap = opts.stats === false
|
|
267
|
+
? new Map()
|
|
268
|
+
: await probeFleetStats(probeable, { selfName: self });
|
|
269
|
+
if (opts.stats !== false && !statsMap.has(self)) {
|
|
270
|
+
statsMap.set(self, await probeLocalStats(self));
|
|
271
|
+
}
|
|
272
|
+
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
273
|
+
const remoteTargets = remoteFleetTargets(planned, self)
|
|
274
|
+
.map((t) => ({
|
|
275
|
+
name: t.device.name,
|
|
276
|
+
platform: t.device.platform,
|
|
277
|
+
skip: t.skip,
|
|
278
|
+
}));
|
|
279
|
+
const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
|
|
280
|
+
for (const result of remote) {
|
|
281
|
+
const profile = reg[result.name];
|
|
282
|
+
if (result.status === 'ok' && result.value) {
|
|
283
|
+
rows.push({
|
|
284
|
+
name: result.name,
|
|
285
|
+
platform: profile?.platform,
|
|
286
|
+
stats: statsMap.get(result.name),
|
|
287
|
+
...result.value,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
rows.push({
|
|
292
|
+
name: result.name,
|
|
293
|
+
platform: profile?.platform,
|
|
294
|
+
stats: statsMap.get(result.name),
|
|
295
|
+
skipped: result.reason ? String(result.reason) : undefined,
|
|
296
|
+
error: result.error,
|
|
297
|
+
clis: {},
|
|
298
|
+
sync: [],
|
|
299
|
+
orphans: [],
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const report = buildFleetHealthReport(rows);
|
|
304
|
+
if (opts.json) {
|
|
305
|
+
console.log(JSON.stringify(report, null, 2));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
for (const line of renderFleetWarnings(report))
|
|
309
|
+
console.log(line);
|
|
310
|
+
console.log();
|
|
311
|
+
for (const line of renderFleetMatrix(report))
|
|
312
|
+
console.log(line);
|
|
313
|
+
}
|
|
314
|
+
if (opts.strict && report.hasWarnings)
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
}
|
|
317
|
+
/** SSH into a host and run its local auth probe, returning its rows. */
|
|
318
|
+
async function probeRemoteAuth(target) {
|
|
319
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
320
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
321
|
+
const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
322
|
+
const res = await sshExecAsync(target.name, cmd, { timeoutMs: 60000, multiplex: true });
|
|
323
|
+
if (res.code !== 0) {
|
|
324
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
325
|
+
}
|
|
326
|
+
const parsed = JSON.parse(res.stdout);
|
|
327
|
+
return parsed.rows ?? [];
|
|
328
|
+
}
|
|
329
|
+
async function runFleetPing(opts) {
|
|
330
|
+
const self = machineId();
|
|
331
|
+
const cliVersion = getCliVersion();
|
|
332
|
+
// --local: probe just this host. Used both directly and as the fan-out worker.
|
|
333
|
+
if (opts.local) {
|
|
334
|
+
const rows = await probeLocalFleetAuth({ cliVersion });
|
|
335
|
+
writeFleetAuthRows(self, rows);
|
|
336
|
+
if (opts.json) {
|
|
337
|
+
console.log(JSON.stringify({ host: self, rows }));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
for (const line of renderAuthMatrix([{ host: self, rows }], { verbose: opts.verbose }))
|
|
341
|
+
console.log(line);
|
|
342
|
+
}
|
|
343
|
+
if (opts.strict && rows.some((r) => isDeadVerdict(r.health.verdict))) {
|
|
344
|
+
process.exitCode = 1;
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Origin: probe locally, then fan out to the rest of the fleet in parallel.
|
|
349
|
+
const reg = await loadDevices();
|
|
350
|
+
const planned = planFleetTargets(reg);
|
|
351
|
+
const results = [];
|
|
352
|
+
const localRows = await probeLocalFleetAuth({ cliVersion });
|
|
353
|
+
writeFleetAuthRows(self, localRows);
|
|
354
|
+
results.push({ host: self, rows: localRows });
|
|
355
|
+
const remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
|
|
356
|
+
name: t.device.name,
|
|
357
|
+
platform: t.device.platform,
|
|
358
|
+
skip: t.skip,
|
|
359
|
+
}));
|
|
360
|
+
const probeable = remoteTargets.filter((t) => !t.skip).length;
|
|
361
|
+
const spinner = isInteractiveTerminal() && !opts.json
|
|
362
|
+
? ora(`Pinging ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
|
|
363
|
+
: undefined;
|
|
364
|
+
let remote;
|
|
365
|
+
try {
|
|
366
|
+
remote = await fanOutDevices(remoteTargets, probeRemoteAuth);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
spinner?.stop();
|
|
370
|
+
}
|
|
371
|
+
for (const r of remote) {
|
|
372
|
+
if (r.status === 'ok' && r.value) {
|
|
373
|
+
results.push({ host: r.name, rows: r.value });
|
|
374
|
+
writeFleetAuthRows(r.name, r.value);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
results.push({
|
|
378
|
+
host: r.name,
|
|
379
|
+
rows: [],
|
|
380
|
+
error: r.error,
|
|
381
|
+
skipped: r.reason ? String(r.reason) : undefined,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (opts.json) {
|
|
386
|
+
console.log(JSON.stringify(results, null, 2));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
for (const line of renderAuthMatrix(results, { verbose: opts.verbose }))
|
|
390
|
+
console.log(line);
|
|
391
|
+
}
|
|
392
|
+
const anyBad = results.some((r) => r.rows.some((row) => isDeadVerdict(row.health.verdict)));
|
|
393
|
+
if (opts.strict && anyBad)
|
|
394
|
+
process.exitCode = 1;
|
|
395
|
+
}
|
|
396
|
+
/** Color a per-host×agent cell: green all-live, red any revoked/expired, yellow degraded. */
|
|
397
|
+
function authCell(summary, width) {
|
|
398
|
+
if (summary.total === 0)
|
|
399
|
+
return chalk.dim('·'.padEnd(width));
|
|
400
|
+
const text = `${summary.live}/${summary.total}`;
|
|
401
|
+
const padded = text.padEnd(width);
|
|
402
|
+
if (summary.bad > 0)
|
|
403
|
+
return chalk.red(padded);
|
|
404
|
+
if (summary.warn > 0)
|
|
405
|
+
return chalk.yellow(padded);
|
|
406
|
+
return chalk.green(padded);
|
|
407
|
+
}
|
|
408
|
+
/** Render the fleet auth matrix (device rows × agent columns) plus an optional per-account breakdown. */
|
|
409
|
+
function renderAuthMatrix(results, opts) {
|
|
410
|
+
// Only show agent columns that appear somewhere in the results.
|
|
411
|
+
const present = new Set();
|
|
412
|
+
for (const r of results)
|
|
413
|
+
for (const row of r.rows)
|
|
414
|
+
present.add(row.agent);
|
|
415
|
+
const agents = ALL_AGENT_IDS.filter((a) => present.has(a));
|
|
416
|
+
const cellW = 6; // 6 disambiguates opencode/openclaw (both 'openc' at 5)
|
|
417
|
+
const nameW = Math.max(6, ...results.map((r) => r.host.length));
|
|
418
|
+
const lines = [chalk.bold('Fleet auth')];
|
|
419
|
+
const header = ` ${'Device'.padEnd(nameW)} ${agents.map((a) => a.slice(0, cellW).padEnd(cellW)).join(' ')}`;
|
|
420
|
+
lines.push(chalk.gray(header));
|
|
421
|
+
for (const r of results) {
|
|
422
|
+
const cells = agents.map((a) => {
|
|
423
|
+
const verdicts = r.rows.filter((row) => row.agent === a).map((row) => row.health.verdict);
|
|
424
|
+
return authCell(summarizeVerdicts(verdicts), cellW);
|
|
425
|
+
});
|
|
426
|
+
let note = '';
|
|
427
|
+
if (r.skipped)
|
|
428
|
+
note = chalk.dim(` ${r.skipped}`);
|
|
429
|
+
else if (r.error)
|
|
430
|
+
note = chalk.red(` ${r.error}`);
|
|
431
|
+
else {
|
|
432
|
+
const dead = r.rows.filter((row) => isDeadVerdict(row.health.verdict)).length;
|
|
433
|
+
if (dead > 0)
|
|
434
|
+
note = chalk.red(` ${dead} revoked — re-login`);
|
|
435
|
+
}
|
|
436
|
+
lines.push(` ${r.host.padEnd(nameW)} ${cells.join(' ')}${note}`);
|
|
437
|
+
}
|
|
438
|
+
lines.push('');
|
|
439
|
+
lines.push(chalk.gray(' cell = live/total accounts · green all live · red revoked (re-login) · yellow expired/unverified/limited'));
|
|
440
|
+
if (opts?.verbose) {
|
|
441
|
+
lines.push('');
|
|
442
|
+
lines.push(chalk.bold('Accounts'));
|
|
443
|
+
for (const r of results) {
|
|
444
|
+
if (r.rows.length === 0)
|
|
445
|
+
continue;
|
|
446
|
+
for (const row of r.rows.slice().sort((x, y) => (x.agent + x.version).localeCompare(y.agent + y.version))) {
|
|
447
|
+
const v = row.health.verdict;
|
|
448
|
+
const label = v === 'live' ? chalk.green(verdictLabel(v))
|
|
449
|
+
: (v === 'revoked' || v === 'expired') ? chalk.red(verdictLabel(v))
|
|
450
|
+
: chalk.yellow(verdictLabel(v));
|
|
451
|
+
const acctRaw = row.account ?? '—';
|
|
452
|
+
const acct = row.account ? chalk.cyan(acctRaw.padEnd(28)) : chalk.dim(acctRaw.padEnd(28));
|
|
453
|
+
const detail = row.health.detail ? chalk.dim(` ${row.health.detail}`) : '';
|
|
454
|
+
const age = chalk.dim(` · ${formatCheckedAge(row.health.checkedAt)}`);
|
|
455
|
+
lines.push(` ${r.host.padEnd(nameW)} ${`${row.agent}@${row.version}`.padEnd(22)} ${acct} ${label}${detail}${age}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
222
461
|
/** Register the `agents devices` command tree (also aliased as `fleet`). */
|
|
223
462
|
function registerDevicesCommands(program) {
|
|
224
463
|
const devicesCmd = program
|
|
@@ -346,6 +585,25 @@ Typical workflow:
|
|
|
346
585
|
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
347
586
|
console.log(line);
|
|
348
587
|
});
|
|
588
|
+
devicesCmd
|
|
589
|
+
.command('status')
|
|
590
|
+
.description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
|
|
591
|
+
.option('--json', 'output machine-readable JSON')
|
|
592
|
+
.option('--strict', 'exit non-zero when any device has drift or is unreachable')
|
|
593
|
+
.option('--no-stats', 'skip the live resource probe')
|
|
594
|
+
.action(async (opts) => {
|
|
595
|
+
await runFleetStatus(opts);
|
|
596
|
+
});
|
|
597
|
+
devicesCmd
|
|
598
|
+
.command('ping')
|
|
599
|
+
.description('Live auth health: complete a real request for every agent account across the fleet (unlike the cached "signed in" flag). Writes the shared auth-health cache read by `agents view` and `fleet status`.')
|
|
600
|
+
.option('--json', 'output machine-readable JSON')
|
|
601
|
+
.option('--local', 'probe only this host (used internally for fan-out)')
|
|
602
|
+
.option('--verbose', 'show a per-account breakdown, not just the per-host rollup')
|
|
603
|
+
.option('--strict', 'exit non-zero when any account is revoked (expired is soft — it self-refreshes)')
|
|
604
|
+
.action(async (opts) => {
|
|
605
|
+
await runFleetPing(opts);
|
|
606
|
+
});
|
|
349
607
|
devicesCmd
|
|
350
608
|
.command('show <name>')
|
|
351
609
|
.description('Show the full profile for one device.')
|
|
@@ -567,6 +825,13 @@ secrets bundle via an askpass shim — the password never touches argv.
|
|
|
567
825
|
const addr = hostNameFor(device);
|
|
568
826
|
const pinned = addr ? isHostPinned(addr) : false;
|
|
569
827
|
const { args, env } = buildSshInvocation(device, cmd, shim, { pinned });
|
|
828
|
+
// Interactive login: make the local terminal's terminfo (e.g.
|
|
829
|
+
// xterm-ghostty) available on the remote so backspace/colors/clear work.
|
|
830
|
+
// Best-effort + cached per host — never blocks the login (see terminfo.ts).
|
|
831
|
+
if (cmd.length === 0 && shouldSyncTerminfo({ term: process.env.TERM, shell: device.shell, interactive: process.stdout.isTTY ?? false })) {
|
|
832
|
+
const { args: tinfoArgs, env: tinfoEnv } = buildSshInvocation(device, ['tic', '-x', '-'], shim, { pinned });
|
|
833
|
+
syncTerminfoToDevice({ device, host: terminfoHostKey(device, addr), term: process.env.TERM, sshArgs: tinfoArgs, sshEnv: tinfoEnv });
|
|
834
|
+
}
|
|
570
835
|
const res = spawnSync('ssh', args, {
|
|
571
836
|
stdio: 'inherit',
|
|
572
837
|
env: { ...process.env, ...env },
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -32,5 +32,16 @@ export declare function decideTeamMessageRoute(status: AgentStatus, hasMessage:
|
|
|
32
32
|
* the teammate itself so we don't need the original --cloud CLI args.
|
|
33
33
|
*/
|
|
34
34
|
export declare function wireCloudDispatcher(mgr: AgentManager): void;
|
|
35
|
+
/**
|
|
36
|
+
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
37
|
+
* `--host` option family (option.ts) and is meaningful for commands that *route*
|
|
38
|
+
* to a host, but `teams add` special-cases `--host`/`--device` as PLACEMENT, so
|
|
39
|
+
* the flag is never read — a teammate's directory is the team's repo plus its
|
|
40
|
+
* `--worktree`, not a path passed here. Silently ignoring it misleads you into
|
|
41
|
+
* thinking it set the teammate's repo path (the exact wrong model that turns one
|
|
42
|
+
* team into a teardown-and-rebuild). Reject with guidance instead. Exported for
|
|
43
|
+
* the unit test that pins the message.
|
|
44
|
+
*/
|
|
45
|
+
export declare function remoteCwdOnAddError(team: string): string;
|
|
35
46
|
/** Register the `agents teams` command tree (list, create, add, status, start, remove, disband, logs, doctor). */
|
|
36
47
|
export declare function registerTeamsCommands(program: Command): void;
|
package/dist/commands/teams.js
CHANGED
|
@@ -809,6 +809,23 @@ async function pickTeamOr(mgr, command) {
|
|
|
809
809
|
throw err;
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
814
|
+
* `--host` option family (option.ts) and is meaningful for commands that *route*
|
|
815
|
+
* to a host, but `teams add` special-cases `--host`/`--device` as PLACEMENT, so
|
|
816
|
+
* the flag is never read — a teammate's directory is the team's repo plus its
|
|
817
|
+
* `--worktree`, not a path passed here. Silently ignoring it misleads you into
|
|
818
|
+
* thinking it set the teammate's repo path (the exact wrong model that turns one
|
|
819
|
+
* team into a teardown-and-rebuild). Reject with guidance instead. Exported for
|
|
820
|
+
* the unit test that pins the message.
|
|
821
|
+
*/
|
|
822
|
+
export function remoteCwdOnAddError(team) {
|
|
823
|
+
return (`--remote-cwd has no effect on 'teams add' — it does not set a teammate's repo or directory.\n` +
|
|
824
|
+
` A teammate works in the team's repo plus its own --worktree:\n` +
|
|
825
|
+
` • Set the repo once, on the team: agents teams create ${team} --repo <url|path>\n` +
|
|
826
|
+
` • Place the teammate on a machine: agents teams add ${team} <agent> "<task>" --device <host> --worktree <name>\n` +
|
|
827
|
+
` (Remote worktrees fork from the host's freshly-fetched origin/<default> automatically.)`);
|
|
828
|
+
}
|
|
812
829
|
/** Register the `agents teams` command tree (list, create, add, status, start, remove, disband, logs, doctor). */
|
|
813
830
|
export function registerTeamsCommands(program) {
|
|
814
831
|
const teams = program
|
|
@@ -853,6 +870,20 @@ export function registerTeamsCommands(program) {
|
|
|
853
870
|
'<profile>' a profile from 'agents view' — runs through 'agents
|
|
854
871
|
run <profile>' with the profile's host harness
|
|
855
872
|
|
|
873
|
+
Placement & repos (the part people get wrong):
|
|
874
|
+
--remote-cwd does NOT place a teammate or set its repo — it is ignored on
|
|
875
|
+
'teams add' (and rejected, so you find out immediately). A teammate's
|
|
876
|
+
directory is the team's repo plus its --worktree:
|
|
877
|
+
--device <host> run THIS teammate on <host>
|
|
878
|
+
create --repo <r> ONE repo for the whole team (defaults to this
|
|
879
|
+
checkout's origin). Work spanning repos → one team
|
|
880
|
+
per repo — don't build a cross-repo team then rebuild.
|
|
881
|
+
With --enable-worktrees each teammate gets its own worktree + branch:
|
|
882
|
+
local teammate forks from your CURRENT local HEAD — no fetch, so
|
|
883
|
+
pull/sync the checkout first or it forks stale
|
|
884
|
+
remote (--device) forks from the freshly-fetched origin/<default> on
|
|
885
|
+
the host (no manual sync needed)
|
|
886
|
+
|
|
856
887
|
Short aliases:
|
|
857
888
|
teams c = create teams a = add teams s = status
|
|
858
889
|
teams rm = remove teams d = disband teams ls = list
|
|
@@ -978,7 +1009,7 @@ export function registerTeamsCommands(program) {
|
|
|
978
1009
|
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
979
1010
|
.option('--devices <list>', 'Pool of machines this team may run teammates on (comma-separated). Enables distributed auto-scheduling.')
|
|
980
1011
|
.option('--hosts <list>', 'Alias for --devices.')
|
|
981
|
-
.option('--repo <urlOrPath>', 'How each device gets the code
|
|
1012
|
+
.option('--repo <urlOrPath>', 'How each remote (--device) teammate gets the code — ONE git URL/path for the whole team (existing checkout reused, else cloned). A team is single-repo; for work across repos, make one team per repo. Defaults to this checkout origin.')
|
|
982
1013
|
.option('--json', 'Output machine-readable JSON')
|
|
983
1014
|
.action(async (team, opts) => {
|
|
984
1015
|
try {
|
|
@@ -1073,6 +1104,13 @@ export function registerTeamsCommands(program) {
|
|
|
1073
1104
|
.option('--force', "Skip the advisory 'may not be signed in' / 'account throttled' warnings")
|
|
1074
1105
|
.option('--json', 'Output machine-readable JSON')
|
|
1075
1106
|
.action(async (team, teammate, task, opts) => {
|
|
1107
|
+
// `--remote-cwd` rides the shared --host option family but is never read by
|
|
1108
|
+
// `teams add` (placement, not routing). Fail loud with guidance rather than
|
|
1109
|
+
// silently ignoring it — see remoteCwdOnAddError. `!== undefined` so even an
|
|
1110
|
+
// explicit empty value (`--remote-cwd ""`) is rejected, not silently dropped.
|
|
1111
|
+
if (opts.remoteCwd !== undefined) {
|
|
1112
|
+
die(remoteCwdOnAddError(team));
|
|
1113
|
+
}
|
|
1076
1114
|
if (!VALID_MODES.includes(opts.mode)) {
|
|
1077
1115
|
die(`Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
|
|
1078
1116
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -7,23 +7,12 @@
|
|
|
7
7
|
* rules, hooks, and promptcuts synced to that version.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
+
import { accountDisplayLabel } from '../lib/agents.js';
|
|
10
11
|
import type { AccountInfo } from '../lib/agents.js';
|
|
11
12
|
import type { AgentId } from '../lib/types.js';
|
|
12
13
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
* signed-in agent whose credential carries no email but does carry an opaque
|
|
16
|
-
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
17
|
-
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
18
|
-
* have neither (Antigravity), and empty when signed out.
|
|
19
|
-
*
|
|
20
|
-
* When the account carries a Claude organizationType, the org badge is appended
|
|
21
|
-
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
22
|
-
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
23
|
-
* suffix is plain text inside the label so the existing column-width pass
|
|
24
|
-
* measures and pads it for free.
|
|
25
|
-
*/
|
|
26
|
-
export declare function accountColumnLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
14
|
+
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
15
|
+
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
27
16
|
type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
|
|
28
17
|
/** Per-section filter flags. When any are true, only those sections render. */
|
|
29
18
|
export interface ViewSectionFilter {
|
|
@@ -133,6 +122,7 @@ export declare function viewAction(agentArg?: string, options?: {
|
|
|
133
122
|
dryRun?: boolean;
|
|
134
123
|
resources?: string | boolean;
|
|
135
124
|
detailed?: boolean;
|
|
125
|
+
refresh?: boolean;
|
|
136
126
|
} & ViewSectionFilter): Promise<void>;
|
|
137
127
|
/** Register the `agents view` command. */
|
|
138
128
|
export declare function registerViewCommand(program: Command): void;
|
package/dist/commands/view.js
CHANGED
|
@@ -4,8 +4,11 @@ import { visibleWidth, termLink } from '../lib/format.js';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS,
|
|
8
|
-
import {
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
|
+
import { loginHint } from '../lib/signin-badge.js';
|
|
9
|
+
import { machineId } from '../lib/machine-id.js';
|
|
10
|
+
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
|
+
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
12
|
import { readManifest } from '../lib/manifest.js';
|
|
10
13
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
11
14
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
@@ -25,33 +28,8 @@ import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
|
25
28
|
import { confirm } from '@inquirer/prompts';
|
|
26
29
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
27
30
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const SIGNED_IN_LABEL = 'signed in';
|
|
31
|
-
/**
|
|
32
|
-
* Text for the account (email) column. Prefers the real email; otherwise, for a
|
|
33
|
-
* signed-in agent whose credential carries no email but does carry an opaque
|
|
34
|
-
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
35
|
-
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
36
|
-
* have neither (Antigravity), and empty when signed out.
|
|
37
|
-
*
|
|
38
|
-
* When the account carries a Claude organizationType, the org badge is appended
|
|
39
|
-
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
40
|
-
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
41
|
-
* suffix is plain text inside the label so the existing column-width pass
|
|
42
|
-
* measures and pads it for free.
|
|
43
|
-
*/
|
|
44
|
-
export function accountColumnLabel(info) {
|
|
45
|
-
if (!info)
|
|
46
|
-
return '';
|
|
47
|
-
if (info.email) {
|
|
48
|
-
const badge = accountOrgBadge(info);
|
|
49
|
-
return badge ? `${info.email} (${badge})` : info.email;
|
|
50
|
-
}
|
|
51
|
-
if (info.signedIn)
|
|
52
|
-
return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
31
|
+
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
32
|
+
export const accountColumnLabel = accountDisplayLabel;
|
|
55
33
|
/**
|
|
56
34
|
* Group profile summaries by their host harness, optionally filtered to a
|
|
57
35
|
* single agent. Profile YAMLs that fail validation are silently skipped by
|
|
@@ -244,7 +222,23 @@ function renderHostClisSection(cwd) {
|
|
|
244
222
|
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
245
223
|
}
|
|
246
224
|
}
|
|
247
|
-
|
|
225
|
+
/**
|
|
226
|
+
* A compact live-auth chip for a version row, read from the fleet auth-health
|
|
227
|
+
* cache (written by `agents fleet ping` / the daemon). Empty when the install
|
|
228
|
+
* hasn't been probed — this is the local "signed in" flag's ground-truth
|
|
229
|
+
* companion: ● live, ○ revoked (re-login), ◐ unverified/limited, plus its age.
|
|
230
|
+
*/
|
|
231
|
+
function liveAuthChip(cache, host, agentId, version) {
|
|
232
|
+
const h = cache[authCacheKey(host, agentId, version)];
|
|
233
|
+
if (!h)
|
|
234
|
+
return '';
|
|
235
|
+
const glyph = h.verdict === 'live' ? chalk.green('●')
|
|
236
|
+
: h.verdict === 'revoked' ? chalk.red('○')
|
|
237
|
+
: h.verdict === 'expired' ? chalk.yellow('○')
|
|
238
|
+
: chalk.yellow('◐');
|
|
239
|
+
return `${glyph} ${chalk.gray(formatCheckedAge(h.checkedAt))}`;
|
|
240
|
+
}
|
|
241
|
+
async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
248
242
|
const spinnerText = filterAgentId
|
|
249
243
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
250
244
|
: 'Checking installed agents...';
|
|
@@ -275,6 +269,9 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
275
269
|
}
|
|
276
270
|
// Shim healing is silent — users don't need to know about internal repairs
|
|
277
271
|
console.log(chalk.bold('Installed Agent CLIs\n'));
|
|
272
|
+
const selfHost = machineId();
|
|
273
|
+
// Read the auth-health cache once (not per version row — see the batching note above).
|
|
274
|
+
const authCache = readAuthHealthCache();
|
|
278
275
|
// Pre-fetch account info for all versions in parallel
|
|
279
276
|
const infoFetches = [];
|
|
280
277
|
const globalInfoFetches = [];
|
|
@@ -326,7 +323,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
326
323
|
cliVersion,
|
|
327
324
|
info,
|
|
328
325
|
})),
|
|
329
|
-
]);
|
|
326
|
+
], { forceRefresh: viewOpts?.forceRefresh });
|
|
330
327
|
const mergeCanonical = (info) => {
|
|
331
328
|
const key = getUsageLookupKey(info);
|
|
332
329
|
if (!key)
|
|
@@ -417,7 +414,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
417
414
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
418
415
|
const usageKey = getUsageLookupKey(info);
|
|
419
416
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
420
|
-
const
|
|
417
|
+
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
|
|
418
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
421
419
|
maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
|
|
422
420
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
423
421
|
maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
|
|
@@ -457,7 +455,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
457
455
|
const parts = [` ${label}`];
|
|
458
456
|
const hasEmail = !!vInfo?.email;
|
|
459
457
|
const signedIn = !!vInfo?.signedIn;
|
|
460
|
-
const
|
|
458
|
+
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
459
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
461
460
|
const hasUsage = usageStr.length > 0;
|
|
462
461
|
// Only show lastActive for versions with an actual logged-in account.
|
|
463
462
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -471,7 +470,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
471
470
|
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
472
471
|
if (!hasEmail && !hasUsage && !signedIn) {
|
|
473
472
|
// Installed but never signed in
|
|
474
|
-
parts.push(chalk.gray('(
|
|
473
|
+
parts.push(chalk.gray('(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
475
474
|
}
|
|
476
475
|
else {
|
|
477
476
|
if (hasEmail || hasUsage || hasActive || signedIn) {
|
|
@@ -497,6 +496,9 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
497
496
|
if (runDefaultBits.length > 0) {
|
|
498
497
|
parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
|
|
499
498
|
}
|
|
499
|
+
const authChip = liveAuthChip(authCache, selfHost, agentId, version);
|
|
500
|
+
if (authChip)
|
|
501
|
+
parts.push(authChip);
|
|
500
502
|
console.log(parts.join(' '));
|
|
501
503
|
if (showPaths) {
|
|
502
504
|
const versionDir = getVersionDir(agentId, version);
|
|
@@ -1438,7 +1440,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1438
1440
|
return;
|
|
1439
1441
|
}
|
|
1440
1442
|
// No argument: show all installed versions
|
|
1441
|
-
await showInstalledVersions();
|
|
1443
|
+
await showInstalledVersions(undefined, { forceRefresh: options?.refresh === true });
|
|
1442
1444
|
return;
|
|
1443
1445
|
}
|
|
1444
1446
|
// Parse agent@version syntax
|
|
@@ -1499,7 +1501,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1499
1501
|
}
|
|
1500
1502
|
else {
|
|
1501
1503
|
// Just agent name: show versions for that agent
|
|
1502
|
-
await showInstalledVersions(agentId);
|
|
1504
|
+
await showInstalledVersions(agentId, { forceRefresh: options?.refresh === true });
|
|
1503
1505
|
}
|
|
1504
1506
|
}
|
|
1505
1507
|
/** Register the `agents view` command. */
|
|
@@ -1509,6 +1511,7 @@ export function registerViewCommand(program) {
|
|
|
1509
1511
|
.option('--json', 'Emit machine-readable JSON (version list, usage, signed-in status).')
|
|
1510
1512
|
.option('--resources [sections]', 'In --json mode, include each version\'s resources: "all" (default) or a comma list (skills,plugins,mcp,commands,workflows,memory,hooks). Implies --json.')
|
|
1511
1513
|
.option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
|
|
1514
|
+
.option('-r, --refresh', 'Force a live usage refresh, bypassing the cache (slower). Repopulates the S:/W: limit bars for every account whose token is reachable.')
|
|
1512
1515
|
.option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
|
|
1513
1516
|
.option('--dry-run', 'With --prune, show duplicate versions without deleting')
|
|
1514
1517
|
.option('-y, --yes', 'Skip the prune confirmation prompt.')
|