@phnx-labs/agents-cli 1.20.70 → 1.20.72
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 +85 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +36 -3
- package/dist/commands/check.js +3 -1
- package/dist/commands/commands.js +2 -0
- package/dist/commands/exec.js +12 -0
- package/dist/commands/fleet-capture.d.ts +14 -0
- package/dist/commands/fleet-capture.js +107 -0
- package/dist/commands/fork.js +13 -7
- package/dist/commands/hosts.js +11 -0
- package/dist/commands/logs.js +38 -9
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +24 -4
- package/dist/commands/sessions-tail.js +1 -2
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +14 -6
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.js +24 -6
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/usage.d.ts +13 -0
- package/dist/commands/usage.js +50 -28
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +5 -3
- package/dist/lib/commands.d.ts +10 -0
- package/dist/lib/commands.js +31 -7
- package/dist/lib/daemon.d.ts +9 -0
- package/dist/lib/daemon.js +43 -0
- package/dist/lib/devices/connect.d.ts +13 -0
- package/dist/lib/devices/connect.js +20 -0
- package/dist/lib/devices/fleet.d.ts +36 -3
- package/dist/lib/devices/fleet.js +42 -4
- package/dist/lib/devices/sync.d.ts +30 -0
- package/dist/lib/devices/sync.js +50 -0
- package/dist/lib/doctor-diff.js +38 -16
- package/dist/lib/fleet/apply.d.ts +4 -0
- package/dist/lib/fleet/apply.js +28 -5
- package/dist/lib/fleet/capture.d.ts +35 -0
- package/dist/lib/fleet/capture.js +51 -0
- package/dist/lib/fleet/manifest.d.ts +6 -0
- package/dist/lib/fleet/manifest.js +24 -1
- package/dist/lib/fleet/types.d.ts +24 -1
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +39 -1
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/menubar/install-menubar.d.ts +12 -0
- package/dist/lib/menubar/install-menubar.js +26 -13
- package/dist/lib/secrets/agent.d.ts +5 -0
- package/dist/lib/secrets/agent.js +35 -3
- package/dist/lib/secrets/bundles.js +28 -0
- package/dist/lib/secrets/index.d.ts +14 -0
- package/dist/lib/secrets/index.js +36 -5
- package/dist/lib/secrets/session-store.d.ts +90 -0
- package/dist/lib/secrets/session-store.js +221 -0
- package/dist/lib/session/render.d.ts +9 -1
- package/dist/lib/session/render.js +35 -4
- package/dist/lib/share/capture.d.ts +2 -0
- package/dist/lib/share/capture.js +12 -5
- package/dist/lib/shims.d.ts +27 -0
- package/dist/lib/shims.js +29 -3
- package/dist/lib/skills.d.ts +8 -1
- package/dist/lib/skills.js +11 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +5 -1
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/versions.d.ts +16 -0
- package/dist/lib/versions.js +40 -3
- package/package.json +1 -1
package/dist/commands/secrets.js
CHANGED
|
@@ -20,7 +20,8 @@ import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBund
|
|
|
20
20
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
21
21
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
22
22
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
23
|
-
import { secretsHoldMs, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
23
|
+
import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
24
|
+
import { saveSession, deleteSession, deleteAllSessions } from '../lib/secrets/session-store.js';
|
|
24
25
|
import { getCliVersionFresh } from '../lib/version.js';
|
|
25
26
|
import { readMeta } from '../lib/state.js';
|
|
26
27
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
@@ -249,6 +250,9 @@ export function buildRemoteUnlockArgs(names, opts) {
|
|
|
249
250
|
'unlock',
|
|
250
251
|
...(opts.all ? ['--all'] : names),
|
|
251
252
|
...(opts.ttl ? ['--ttl', opts.ttl] : []),
|
|
253
|
+
// Forward --durable so a remote unlock honors it too; without this the remote
|
|
254
|
+
// silently falls back to its own secrets.agent.durable default (off).
|
|
255
|
+
...(opts.durable ? ['--durable'] : []),
|
|
252
256
|
];
|
|
253
257
|
}
|
|
254
258
|
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
@@ -1840,6 +1844,7 @@ Examples:
|
|
|
1840
1844
|
.command('unlock [names...]')
|
|
1841
1845
|
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
1842
1846
|
.option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
|
|
1847
|
+
.option('--durable', 'Keep the unlock across sleep + reboot too (default: survives upgrade/restart but re-locks on sleep). Set secrets.agent.durable in agents.yaml to make this the default.')
|
|
1843
1848
|
.option('--all', 'Unlock every configured bundle')
|
|
1844
1849
|
.option('--host <target>', 'Unlock the bundle(s) on this remote machine over SSH instead of locally (file-backed bundles only — the remote\'s passphrase prompt surfaces on your terminal over a -tt session). Single-valued (NOT variadic) so it never swallows the bundle name: `unlock <name> --host <machine>`.')
|
|
1845
1850
|
.action(async (names, opts) => {
|
|
@@ -1880,8 +1885,12 @@ Examples:
|
|
|
1880
1885
|
return;
|
|
1881
1886
|
}
|
|
1882
1887
|
if (process.platform !== 'darwin') {
|
|
1883
|
-
|
|
1884
|
-
|
|
1888
|
+
// No broker + no biometry prompt off darwin: secrets already resolve
|
|
1889
|
+
// durably from the OS store (libsecret / Credential Manager) on every
|
|
1890
|
+
// read with no prompt, so an unlock is a friendly no-op — not an error.
|
|
1891
|
+
// Accept --durable as a documented no-op so the command is uniform.
|
|
1892
|
+
console.log(chalk.gray('Nothing to unlock: secrets already persist on this OS — reads never re-prompt.'));
|
|
1893
|
+
return;
|
|
1885
1894
|
}
|
|
1886
1895
|
let targets = opts.all ? listBundles().map((b) => b.name) : names;
|
|
1887
1896
|
if (!targets || targets.length === 0) {
|
|
@@ -1915,6 +1924,14 @@ Examples:
|
|
|
1915
1924
|
const { bundle, env } = readAndResolveBundleEnv(name, { noAgent: true, caller: 'unlock' });
|
|
1916
1925
|
if (await agentLoad(name, bundle, env, ttlMs)) {
|
|
1917
1926
|
loaded++;
|
|
1927
|
+
// Persist a durable session snapshot so the unlock survives a daemon
|
|
1928
|
+
// restart / upgrade (and sleep too, with --durable). session-store.ts.
|
|
1929
|
+
saveSession(name, {
|
|
1930
|
+
bundle,
|
|
1931
|
+
env,
|
|
1932
|
+
expiresAt: Date.now() + ttlMs,
|
|
1933
|
+
sleepPersist: opts.durable ?? secretsAgentDurable(),
|
|
1934
|
+
});
|
|
1918
1935
|
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(Date.now() + ttlMs)})`)}`);
|
|
1919
1936
|
}
|
|
1920
1937
|
else {
|
|
@@ -1941,12 +1958,15 @@ Examples:
|
|
|
1941
1958
|
return; // nothing to lock off darwin
|
|
1942
1959
|
if (names && names.length > 0 && !opts.all) {
|
|
1943
1960
|
let total = 0;
|
|
1944
|
-
for (const name of names)
|
|
1961
|
+
for (const name of names) {
|
|
1945
1962
|
total += await agentLock(name);
|
|
1963
|
+
deleteSession(name); // also drop the durable snapshot, or a restart re-warms it
|
|
1964
|
+
}
|
|
1946
1965
|
console.log(total > 0 ? chalk.green(`Locked ${total} bundle(s).`) : chalk.gray('Nothing to lock.'));
|
|
1947
1966
|
}
|
|
1948
1967
|
else {
|
|
1949
1968
|
const wiped = await agentLock();
|
|
1969
|
+
deleteAllSessions();
|
|
1950
1970
|
console.log(wiped > 0 ? chalk.green(`Locked ${wiped} bundle(s).`) : chalk.gray('Nothing to lock.'));
|
|
1951
1971
|
}
|
|
1952
1972
|
});
|
|
@@ -218,8 +218,7 @@ export function registerSessionsTailCommand(sessionsCmd) {
|
|
|
218
218
|
.command('tail [sessionId]')
|
|
219
219
|
.description('Stream JSONL events from a session file as they are written, one event per line. Long-running: Ctrl+C to stop. Claude and Codex only.')
|
|
220
220
|
.option('--latest', 'Tail the most recent tailable session (claude or codex)')
|
|
221
|
-
.option('--from-start', 'Emit the full file first, then follow (default: start at EOF)')
|
|
222
|
-
.option('--json', 'Raw JSONL passthrough (default)');
|
|
221
|
+
.option('--from-start', 'Emit the full file first, then follow (default: start at EOF)');
|
|
223
222
|
setHelpSections(tailCmd, {
|
|
224
223
|
examples: `
|
|
225
224
|
# Follow the most recent active Claude or Codex session
|
|
@@ -155,6 +155,12 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
155
155
|
* (`agents logs <id> --full`). Reuses the shared `renderSession` renderer.
|
|
156
156
|
*/
|
|
157
157
|
export declare function renderSessionLog(session: SessionMeta, mode?: ViewMode): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Emit one session exactly as `agents sessions <id> --json` does — the same
|
|
160
|
+
* redact-by-default `{ session, events }` shape — so `agents logs <id> --json`
|
|
161
|
+
* shares one machine-readable session contract instead of inventing another.
|
|
162
|
+
*/
|
|
163
|
+
export declare function renderSessionLogJson(session: SessionMeta): Promise<void>;
|
|
158
164
|
/** Column-visibility flags for the picker row, computed once over the whole pool. */
|
|
159
165
|
export interface PickerColumns {
|
|
160
166
|
/** Render the machine column (only when the pool spans more than one machine). */
|
|
@@ -998,7 +998,7 @@ async function sessionsAction(query, options) {
|
|
|
998
998
|
// When the user explicitly asks to render (via mode flag), resolve the
|
|
999
999
|
// query globally so sessions outside the default cwd/30d window are found.
|
|
1000
1000
|
if (wantsRender && searchQuery) {
|
|
1001
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts,
|
|
1001
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact });
|
|
1002
1002
|
return;
|
|
1003
1003
|
}
|
|
1004
1004
|
// Interactive picker loads a deep pool but shows only recent sessions
|
|
@@ -1068,7 +1068,7 @@ async function sessionsAction(query, options) {
|
|
|
1068
1068
|
return;
|
|
1069
1069
|
}
|
|
1070
1070
|
if (idMatches.length === 0 && looksLikeSessionId(searchQuery)) {
|
|
1071
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts,
|
|
1071
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact });
|
|
1072
1072
|
return;
|
|
1073
1073
|
}
|
|
1074
1074
|
}
|
|
@@ -1422,6 +1422,14 @@ function resolveViewMode(options, filters) {
|
|
|
1422
1422
|
export async function renderSessionLog(session, mode = 'summary') {
|
|
1423
1423
|
await renderSession(session, mode, {});
|
|
1424
1424
|
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Emit one session exactly as `agents sessions <id> --json` does — the same
|
|
1427
|
+
* redact-by-default `{ session, events }` shape — so `agents logs <id> --json`
|
|
1428
|
+
* shares one machine-readable session contract instead of inventing another.
|
|
1429
|
+
*/
|
|
1430
|
+
export async function renderSessionLogJson(session) {
|
|
1431
|
+
await renderSession(session, 'json', {});
|
|
1432
|
+
}
|
|
1425
1433
|
async function renderSession(session, mode, filters, options = {}) {
|
|
1426
1434
|
// OpenCode stores sessions in SQLite; filePath is "db_path#session_id"
|
|
1427
1435
|
const realPath = session.filePath.split('#')[0];
|
|
@@ -1475,7 +1483,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1475
1483
|
chalk.gray(` ${formatRelativeTime(session.timestamp)}`) +
|
|
1476
1484
|
(session.account ? chalk.gray(` (${session.account})`) : ''));
|
|
1477
1485
|
console.log(chalk.gray('─'.repeat(60)));
|
|
1478
|
-
process.stdout.write(renderMarkdown(renderConversationMarkdown(events, { redact: options.
|
|
1486
|
+
process.stdout.write(renderMarkdown(renderConversationMarkdown(events, { redact: options.redact !== false })));
|
|
1479
1487
|
return;
|
|
1480
1488
|
}
|
|
1481
1489
|
// json — normalized events plus the durable session signals from the state
|
|
@@ -1486,7 +1494,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1486
1494
|
// checklist reflects true session state regardless of any `--include` filter;
|
|
1487
1495
|
// it lets the Factory panel read the CLI's checklist instead of re-parsing.
|
|
1488
1496
|
const todos = inferSessionState(parsedEvents, { cwd: session.cwd }).todos;
|
|
1489
|
-
process.stdout.write(renderJson(events, todos ? { ...session, todos } : session));
|
|
1497
|
+
process.stdout.write(renderJson(events, todos ? { ...session, todos } : session, { redact: options.redact !== false }));
|
|
1490
1498
|
}
|
|
1491
1499
|
function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
1492
1500
|
const lbl = (label ?? '').trim();
|
|
@@ -2162,7 +2170,7 @@ async function renderOneSession(query, mode, scope) {
|
|
|
2162
2170
|
throw new Error('Session resolution failed');
|
|
2163
2171
|
}
|
|
2164
2172
|
spinner.stop();
|
|
2165
|
-
await renderSession(session, mode, scope.filter, {
|
|
2173
|
+
await renderSession(session, mode, scope.filter, { redact: scope.redact });
|
|
2166
2174
|
}
|
|
2167
2175
|
catch (err) {
|
|
2168
2176
|
if (isPromptCancelled(err))
|
|
@@ -2195,7 +2203,7 @@ export function registerSessionsCommands(program) {
|
|
|
2195
2203
|
.option('-n, --limit <n>', 'Maximum number of sessions to return', '50')
|
|
2196
2204
|
.option('--sort <field>', 'Sort the list by: recent (default), cost, or duration')
|
|
2197
2205
|
.option('--markdown', 'Render the session as markdown (user, assistant, thinking, tool calls)')
|
|
2198
|
-
.option('--no-redact', 'Disable default secret redaction in
|
|
2206
|
+
.option('--no-redact', 'Disable default secret redaction in rendered session output (--markdown and --json)')
|
|
2199
2207
|
.option('--json', 'Output JSON (session list when browsing, event array when rendering one session)')
|
|
2200
2208
|
.option('--include <roles>', 'Only include these roles (comma-separated): user, assistant, thinking, tools')
|
|
2201
2209
|
.option('--exclude <roles>', 'Exclude these roles (comma-separated): user, assistant, thinking, tools')
|
package/dist/commands/skills.js
CHANGED
|
@@ -45,6 +45,7 @@ When to use:
|
|
|
45
45
|
.command('list [agent]')
|
|
46
46
|
.description('Show which skills are installed and which agent versions they are synced to')
|
|
47
47
|
.option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
|
|
48
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
48
49
|
.action(async (agentArg, options) => {
|
|
49
50
|
const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
|
|
50
51
|
const agentInput = agentArg || options.agent;
|
|
@@ -74,6 +75,7 @@ When to use:
|
|
|
74
75
|
centralPath: getSkillsDir(),
|
|
75
76
|
filterAgent,
|
|
76
77
|
filterVersion,
|
|
78
|
+
json: options.json,
|
|
77
79
|
});
|
|
78
80
|
});
|
|
79
81
|
skillsCmd
|
package/dist/commands/ssh.js
CHANGED
|
@@ -18,6 +18,7 @@ import ora from 'ora';
|
|
|
18
18
|
import { getCliVersion } from '../lib/version.js';
|
|
19
19
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
|
+
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
21
22
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
22
23
|
import { addControlToken } from '../lib/serve/token.js';
|
|
23
24
|
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
@@ -27,7 +28,7 @@ import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-targe
|
|
|
27
28
|
import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
28
29
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
29
30
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
30
|
-
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
31
|
+
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
31
32
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
32
33
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
33
34
|
import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
@@ -244,10 +245,10 @@ async function probeRemoteHealth(target) {
|
|
|
244
245
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
245
246
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
246
247
|
const versionCmd = buildRemoteAgentsInvocation(['--version'], undefined, isWin ? 'windows' : undefined, env);
|
|
247
|
-
const versionRes = await sshExecAsync(target.
|
|
248
|
+
const versionRes = await sshExecAsync(target.dialTarget, versionCmd, { timeoutMs: 15000, multiplex: true });
|
|
248
249
|
const version = versionRes.code === 0 ? versionRes.stdout.trim().split(/\s+/)[0] || null : null;
|
|
249
250
|
const doctorCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
250
|
-
const doctorRes = await sshExecAsync(target.
|
|
251
|
+
const doctorRes = await sshExecAsync(target.dialTarget, doctorCmd, { timeoutMs: 30000, multiplex: true });
|
|
251
252
|
if (doctorRes.code !== 0) {
|
|
252
253
|
throw new Error(doctorRes.timedOut ? 'timed out' : (doctorRes.stderr.trim() || `exit ${doctorRes.code ?? 'unknown'}`));
|
|
253
254
|
}
|
|
@@ -280,6 +281,20 @@ async function runFleetStatus(opts) {
|
|
|
280
281
|
name: t.device.name,
|
|
281
282
|
platform: t.device.platform,
|
|
282
283
|
skip: t.skip,
|
|
284
|
+
dialTarget: fleetDialTarget(t.device),
|
|
285
|
+
// Fail fast: the stats probe already tried this box with the same
|
|
286
|
+
// (registry) address. If it came back unreachable, don't spend another
|
|
287
|
+
// 15s+30s on version+doctor — skip straight to an unreachable row so one
|
|
288
|
+
// genuinely-offline device can't stall the whole matrix.
|
|
289
|
+
//
|
|
290
|
+
// Only trust this on a FRESH probe (`--refresh`/`--live`). loadFleetStats
|
|
291
|
+
// is cache-first (daemon-warmed ~3min), so a box that came back online
|
|
292
|
+
// since the last warm would otherwise be skipped to "unreachable" without
|
|
293
|
+
// a live attempt — a false negative. On a default run we still do the live
|
|
294
|
+
// probe (fast now that it dials the registry address).
|
|
295
|
+
...(forceRefresh && statsMap.get(t.device.name)?.reachable === false && !t.skip
|
|
296
|
+
? { skip: 'unreachable' }
|
|
297
|
+
: {}),
|
|
283
298
|
}));
|
|
284
299
|
const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
|
|
285
300
|
for (const result of remote) {
|
|
@@ -333,7 +348,7 @@ async function probeRemoteAuth(target) {
|
|
|
333
348
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
334
349
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
335
350
|
const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
336
|
-
const res = await sshExecAsync(target.
|
|
351
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 60000, multiplex: true });
|
|
337
352
|
if (res.code !== 0) {
|
|
338
353
|
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
339
354
|
}
|
|
@@ -370,6 +385,7 @@ async function runFleetPing(opts) {
|
|
|
370
385
|
name: t.device.name,
|
|
371
386
|
platform: t.device.platform,
|
|
372
387
|
skip: t.skip,
|
|
388
|
+
dialTarget: fleetDialTarget(t.device),
|
|
373
389
|
}));
|
|
374
390
|
const probeable = remoteTargets.filter((t) => !t.skip).length;
|
|
375
391
|
const spinner = isInteractiveTerminal() && !opts.json
|
|
@@ -511,6 +527,8 @@ Typical workflow:
|
|
|
511
527
|
process.exit(1);
|
|
512
528
|
}
|
|
513
529
|
});
|
|
530
|
+
// `agents fleet capture` — snapshot live state into agents.yaml fleet:.
|
|
531
|
+
registerFleetCaptureCommand(devicesCmd);
|
|
514
532
|
devicesCmd
|
|
515
533
|
.command('register <name>')
|
|
516
534
|
.description('Register a discovered (pending) node by name — used by the menu-bar "NEW DEVICES → Register" action.')
|
|
@@ -781,7 +799,7 @@ Typical workflow:
|
|
|
781
799
|
return;
|
|
782
800
|
}
|
|
783
801
|
console.log(chalk.gray(`Running \`${cmd.join(' ')}\` on ${targets.filter((t) => !t.skip).length} online device(s)…`));
|
|
784
|
-
const results = runFleet(targets, cmd);
|
|
802
|
+
const results = runFleet(targets, cmd, { self: machineId() });
|
|
785
803
|
printFleetResults(results);
|
|
786
804
|
});
|
|
787
805
|
devicesCmd
|
|
@@ -800,7 +818,7 @@ Typical workflow:
|
|
|
800
818
|
return;
|
|
801
819
|
}
|
|
802
820
|
console.log(chalk.gray(`Running \`${cmd.join(' ')}\` on ${targets.filter((t) => !t.skip).length} online device(s)…`));
|
|
803
|
-
const results = runFleet(targets, cmd);
|
|
821
|
+
const results = runFleet(targets, cmd, { self: machineId() });
|
|
804
822
|
printFleetResults(results);
|
|
805
823
|
});
|
|
806
824
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
|
|
|
13
13
|
import { setHelpSections } from '../lib/help.js';
|
|
14
14
|
import { computeSyncStatus } from '../lib/sync-status.js';
|
|
15
15
|
import { promptDriftSync } from '../lib/drift-sync.js';
|
|
16
|
+
import { resolveSurface } from './utils.js';
|
|
16
17
|
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
17
18
|
function versionSummary(v) {
|
|
18
19
|
if (!v.everSynced)
|
|
@@ -46,9 +47,14 @@ export function registerStatusCommand(program) {
|
|
|
46
47
|
agents status --yes
|
|
47
48
|
`,
|
|
48
49
|
});
|
|
49
|
-
cmd.action(async (opts) => {
|
|
50
|
+
cmd.action(async (opts, command) => {
|
|
51
|
+
// Centralized surface read (the human/agent split in one place). Note we still
|
|
52
|
+
// pass the *raw* `opts.yes` to promptDriftSync below — it distinguishes an
|
|
53
|
+
// explicit `--yes` (act) from a non-TTY shell (report only), so `surface.assumeYes`
|
|
54
|
+
// (which conflates the two) would wrongly auto-reconcile in a plain pipe.
|
|
55
|
+
const surface = resolveSurface(command);
|
|
50
56
|
const cwd = opts.cwd ?? process.cwd();
|
|
51
|
-
if (
|
|
57
|
+
if (surface.json) {
|
|
52
58
|
const status = await computeSyncStatus({ cwd });
|
|
53
59
|
console.log(JSON.stringify(status, null, 2));
|
|
54
60
|
return;
|
|
@@ -53,7 +53,7 @@ When to use:
|
|
|
53
53
|
- Team sharing: distribute subagent definitions via GitHub repos
|
|
54
54
|
`);
|
|
55
55
|
// Shared list implementation, registered as `list` and hidden `view` alias.
|
|
56
|
-
const runList = async () => {
|
|
56
|
+
const runList = async (opts) => {
|
|
57
57
|
const rows = buildSubagentRows();
|
|
58
58
|
await showResourceList({
|
|
59
59
|
resourcePlural: 'subagents',
|
|
@@ -62,12 +62,14 @@ When to use:
|
|
|
62
62
|
rows,
|
|
63
63
|
emptyMessage: 'No subagents in ~/.agents/subagents/. Add one with: agents subagents add gh:user/repo',
|
|
64
64
|
centralPath: getSubagentsDir(),
|
|
65
|
+
json: opts?.json,
|
|
65
66
|
});
|
|
66
67
|
};
|
|
67
68
|
// agents subagents list
|
|
68
69
|
subagentsCmd
|
|
69
70
|
.command('list')
|
|
70
71
|
.description('Show subagents in a table with sync status across agent versions')
|
|
72
|
+
.option('--json', 'Emit machine-readable JSON instead of the table/picker')
|
|
71
73
|
.addHelpText('after', `
|
|
72
74
|
Examples:
|
|
73
75
|
# Interactive picker (TTY) or sync-status table (piped)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents uninstall` — completely remove agents-cli and restore the user's
|
|
3
|
+
* original agent configs. The reverse of `agents setup`.
|
|
4
|
+
*
|
|
5
|
+
* Thin command layer: the restore/teardown logic lives in `lib/uninstall.ts`
|
|
6
|
+
* (planUninstall / executeUninstall) so it can be tested against a real temp
|
|
7
|
+
* HOME without the CLI wrapper.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
/** Register `agents uninstall`. */
|
|
11
|
+
export declare function registerUninstallCommands(program: Command): void;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { planUninstall, executeUninstall } from '../lib/uninstall.js';
|
|
5
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
6
|
+
/** Render the read-only plan so the user sees exactly what will change. */
|
|
7
|
+
function printPlan(plan, purge) {
|
|
8
|
+
const restores = plan.configs.filter((c) => c.kind === 'restore-backup' || c.kind === 'restore-version-home');
|
|
9
|
+
const dangling = plan.configs.filter((c) => c.kind === 'remove-dangling');
|
|
10
|
+
const untouched = plan.configs.filter((c) => c.kind === 'leave-real' || c.kind === 'leave-foreign');
|
|
11
|
+
console.log(chalk.bold('\nagents uninstall — planned changes\n'));
|
|
12
|
+
if (restores.length > 0) {
|
|
13
|
+
console.log(chalk.green('Restore your original config (adopted by agents-cli):'));
|
|
14
|
+
for (const c of restores) {
|
|
15
|
+
const how = c.kind === 'restore-backup' ? 'from backup' : 'from version home';
|
|
16
|
+
console.log(chalk.gray(` ${c.realPath} ${chalk.dim(`(${how})`)}`));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (dangling.length > 0) {
|
|
20
|
+
console.log(chalk.yellow('Remove dangling symlink (no original found to restore):'));
|
|
21
|
+
for (const c of dangling)
|
|
22
|
+
console.log(chalk.gray(` ${c.realPath}`));
|
|
23
|
+
}
|
|
24
|
+
if (untouched.length > 0) {
|
|
25
|
+
console.log(chalk.cyan('Left untouched (real, un-adopted configs — never modified):'));
|
|
26
|
+
for (const c of untouched)
|
|
27
|
+
console.log(chalk.gray(` ${c.realPath}`));
|
|
28
|
+
}
|
|
29
|
+
if (plan.homeFiles.length > 0) {
|
|
30
|
+
console.log(chalk.green('Restore home files:'));
|
|
31
|
+
for (const hf of plan.homeFiles)
|
|
32
|
+
console.log(chalk.gray(` ${hf.realPath}`));
|
|
33
|
+
}
|
|
34
|
+
if (plan.launchers.length > 0) {
|
|
35
|
+
console.log(chalk.green('Release adopted launchers (restore native binaries on PATH):'));
|
|
36
|
+
console.log(chalk.gray(` ${plan.launchers.join(', ')}`));
|
|
37
|
+
}
|
|
38
|
+
if (plan.rcFiles.length > 0) {
|
|
39
|
+
console.log(chalk.green('Remove the shim directory from PATH in:'));
|
|
40
|
+
for (const rc of plan.rcFiles)
|
|
41
|
+
console.log(chalk.gray(` ${rc}`));
|
|
42
|
+
}
|
|
43
|
+
console.log(chalk.bold('\nData:'));
|
|
44
|
+
if (purge) {
|
|
45
|
+
console.log(chalk.red(` Permanently delete ${plan.agentsDir} (installed versions, session history, secrets metadata).`));
|
|
46
|
+
if (plan.legacySymlink)
|
|
47
|
+
console.log(chalk.red(` Permanently delete ${plan.legacySymlink}.`));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.gray(` Move ${plan.agentsDir} aside to ${plan.agentsDir}.removed-<timestamp> (recoverable).`));
|
|
51
|
+
if (plan.legacySymlink)
|
|
52
|
+
console.log(chalk.gray(` Remove the legacy symlink ${plan.legacySymlink}.`));
|
|
53
|
+
console.log(chalk.gray(' Use --purge to hard-delete instead.'));
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
/** Report what actually happened, then the final manual npm step. */
|
|
58
|
+
function printResult(result, cleanedPath) {
|
|
59
|
+
for (const r of result.restoredConfigs)
|
|
60
|
+
console.log(chalk.green(`Restored ${r.realPath}`));
|
|
61
|
+
for (const r of result.removedDanglingConfigs)
|
|
62
|
+
console.log(chalk.yellow(`Removed dangling symlink ${r.realPath}`));
|
|
63
|
+
for (const f of result.restoredHomeFiles)
|
|
64
|
+
console.log(chalk.green(`Restored ${f}`));
|
|
65
|
+
if (result.releasedLaunchers.length > 0)
|
|
66
|
+
console.log(chalk.green(`Released launchers: ${result.releasedLaunchers.join(', ')}`));
|
|
67
|
+
for (const rc of result.cleanedRcFiles)
|
|
68
|
+
console.log(chalk.green(`Cleaned shim PATH entry from ${rc}`));
|
|
69
|
+
if (result.agentsDir.disposition === 'moved') {
|
|
70
|
+
console.log(chalk.gray(`Moved ${result.agentsDir.path} to ${result.agentsDir.movedTo}`));
|
|
71
|
+
}
|
|
72
|
+
else if (result.agentsDir.disposition === 'purged') {
|
|
73
|
+
console.log(chalk.gray(`Deleted ${result.agentsDir.path}`));
|
|
74
|
+
}
|
|
75
|
+
for (const e of result.errors)
|
|
76
|
+
console.log(chalk.red(` ! ${e}`));
|
|
77
|
+
if (result.purgeDowngraded) {
|
|
78
|
+
console.log(chalk.yellow(`\n--purge was downgraded to move-aside because a restore step errored — ${result.agentsDir.path} was kept so nothing is lost. Resolve the errors above, then delete ${result.agentsDir.movedTo} manually.`));
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk.bold('\nFinish by removing the CLI package:'));
|
|
81
|
+
console.log(chalk.gray(' npm uninstall -g @phnx-labs/agents-cli # or: bun remove -g @phnx-labs/agents-cli'));
|
|
82
|
+
if (cleanedPath) {
|
|
83
|
+
console.log(chalk.gray('Open a new shell (or re-source your rc file) so the removed PATH entry takes effect.'));
|
|
84
|
+
}
|
|
85
|
+
if (result.agentsDir.disposition === 'moved') {
|
|
86
|
+
console.log(chalk.gray(`Your data is still at ${result.agentsDir.movedTo} — delete it once you are sure.`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Register `agents uninstall`. */
|
|
90
|
+
export function registerUninstallCommands(program) {
|
|
91
|
+
const cmd = program
|
|
92
|
+
.command('uninstall')
|
|
93
|
+
.description('Completely remove agents-cli and restore your original agent configs. Reverses `agents setup`.')
|
|
94
|
+
.option('--purge', 'Hard-delete ~/.agents (installed versions, sessions, secrets metadata) instead of moving it aside')
|
|
95
|
+
.option('--dry-run', 'Show exactly what would change without modifying anything')
|
|
96
|
+
.option('-y, --yes', 'Skip the confirmation prompt (required to run non-interactively)');
|
|
97
|
+
setHelpSections(cmd, {
|
|
98
|
+
examples: `
|
|
99
|
+
# Preview everything that would change, without touching anything
|
|
100
|
+
agents uninstall --dry-run
|
|
101
|
+
|
|
102
|
+
# Restore your configs and move ~/.agents aside (recoverable)
|
|
103
|
+
agents uninstall
|
|
104
|
+
|
|
105
|
+
# Same, but hard-delete ~/.agents (no recovery)
|
|
106
|
+
agents uninstall --purge
|
|
107
|
+
`,
|
|
108
|
+
notes: `
|
|
109
|
+
- Restores every ~/.<agent> that agents-cli adopted; a real config it never adopted is left untouched.
|
|
110
|
+
- Releases adopted launchers and strips the shim directory from your shell PATH.
|
|
111
|
+
- Without --purge, ~/.agents is moved to ~/.agents.removed-<timestamp> so nothing is lost.
|
|
112
|
+
- The CLI cannot delete its own running binary; it prints the final 'npm uninstall -g' step for you.
|
|
113
|
+
`,
|
|
114
|
+
});
|
|
115
|
+
cmd.action(async (options) => {
|
|
116
|
+
// We are tearing ~/.agents down. Silence the JSONL audit log for the rest
|
|
117
|
+
// of this process so a late emit() (its events path is memoized to the old
|
|
118
|
+
// location) can't re-create ~/.agents after we move it aside.
|
|
119
|
+
process.env.AGENTS_DISABLE_EVENT_LOG = '1';
|
|
120
|
+
const plan = planUninstall();
|
|
121
|
+
if (!plan.isInstalled) {
|
|
122
|
+
console.log(chalk.gray('agents-cli is not set up (no ~/.agents directory) — nothing to uninstall.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
printPlan(plan, !!options.purge);
|
|
126
|
+
if (options.dryRun) {
|
|
127
|
+
console.log(chalk.gray('Dry run — nothing was changed.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!options.yes) {
|
|
131
|
+
if (!isInteractiveTerminal()) {
|
|
132
|
+
console.log(chalk.red('Refusing to uninstall non-interactively. Re-run with --yes to confirm.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const ok = await confirm({
|
|
137
|
+
message: options.purge
|
|
138
|
+
? 'Permanently remove agents-cli and restore your original configs?'
|
|
139
|
+
: 'Remove agents-cli (data moved to a recoverable directory) and restore your original configs?',
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
if (!ok) {
|
|
143
|
+
console.log(chalk.gray('Cancelled.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (isPromptCancelled(err)) {
|
|
149
|
+
console.log(chalk.gray('Cancelled.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const result = executeUninstall(plan, { purge: options.purge, timestamp: Date.now() });
|
|
156
|
+
printResult(result, result.cleanedRcFiles.length > 0);
|
|
157
|
+
});
|
|
158
|
+
}
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -10,4 +10,17 @@
|
|
|
10
10
|
* don't publish per-account usage today)
|
|
11
11
|
*/
|
|
12
12
|
import type { Command } from 'commander';
|
|
13
|
+
import type { AgentId } from '../lib/types.js';
|
|
14
|
+
import { getUsageInfoForIdentity } from '../lib/usage.js';
|
|
15
|
+
/** One agent's usage snapshot — the unit the text and --json renderers share. */
|
|
16
|
+
export interface AgentUsageRecord {
|
|
17
|
+
agent: AgentId;
|
|
18
|
+
/** Plain agent name (no ANSI) — safe to emit in --json; the text table colorizes it. */
|
|
19
|
+
label: string;
|
|
20
|
+
status: 'unsupported' | 'no-version' | 'not-signed-in' | 'ok';
|
|
21
|
+
email?: string;
|
|
22
|
+
usage?: Awaited<ReturnType<typeof getUsageInfoForIdentity>>;
|
|
23
|
+
}
|
|
13
24
|
export declare function registerUsageCommand(program: Command): void;
|
|
25
|
+
/** Render one usage record as the human table section. */
|
|
26
|
+
export declare function formatAgentUsage(rec: AgentUsageRecord): string;
|
package/dist/commands/usage.js
CHANGED
|
@@ -13,13 +13,15 @@ const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
|
13
13
|
export function registerUsageCommand(program) {
|
|
14
14
|
addHostOption(program.command('usage [agent]'))
|
|
15
15
|
.description('Show rate-limit / quota usage per agent')
|
|
16
|
+
.option('--json', 'Emit machine-readable JSON (per-agent usage snapshot) instead of the table')
|
|
16
17
|
.addHelpText('after', `
|
|
17
18
|
Examples:
|
|
18
19
|
agents usage Show usage for all installed agents
|
|
19
20
|
agents usage claude Show usage for Claude only
|
|
20
21
|
agents usage codex Show usage for Codex only
|
|
22
|
+
agents usage --json Machine-readable snapshot for scripts
|
|
21
23
|
`)
|
|
22
|
-
.action(async (agentFilter) => {
|
|
24
|
+
.action(async (agentFilter, options) => {
|
|
23
25
|
let filter;
|
|
24
26
|
if (agentFilter) {
|
|
25
27
|
const resolved = resolveAgentName(agentFilter);
|
|
@@ -33,47 +35,67 @@ Examples:
|
|
|
33
35
|
? [filter]
|
|
34
36
|
: ALL_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
|
|
35
37
|
if (targets.length === 0) {
|
|
38
|
+
if (options.json) {
|
|
39
|
+
console.log('[]');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
console.log(chalk.gray('No agents installed. Run `agents add <agent>` first.'));
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
45
|
+
const records = await Promise.all(targets.map((agentId) => collectAgentUsage(agentId)));
|
|
46
|
+
if (options.json) {
|
|
47
|
+
console.log(JSON.stringify(records, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(records.map(formatAgentUsage).filter(Boolean).join('\n\n'));
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
/** Gather one agent's usage snapshot as structured data (shared by both renderers). */
|
|
54
|
+
async function collectAgentUsage(agentId) {
|
|
55
|
+
// Plain name — color is applied only at text-render time (formatAgentUsage), so
|
|
56
|
+
// `--json` never emits ANSI escapes in `label` (e.g. under FORCE_COLOR=1).
|
|
57
|
+
const label = AGENTS[agentId].name;
|
|
46
58
|
if (!USAGE_SUPPORTED.has(agentId)) {
|
|
47
|
-
return
|
|
48
|
-
`${heading}`,
|
|
49
|
-
` ${chalk.dim(`${cfg.name} CLI does not publish usage data.`)}`,
|
|
50
|
-
].join('\n');
|
|
59
|
+
return { agent: agentId, label, status: 'unsupported' };
|
|
51
60
|
}
|
|
52
61
|
const versions = listInstalledVersions(agentId);
|
|
53
62
|
const version = getGlobalDefault(agentId) || versions[0];
|
|
54
63
|
if (!version) {
|
|
55
|
-
return
|
|
64
|
+
return { agent: agentId, label, status: 'no-version' };
|
|
56
65
|
}
|
|
57
66
|
const home = getVersionHomePath(agentId, version);
|
|
58
67
|
const info = await getAccountInfo(agentId, home);
|
|
59
68
|
if (!info.usageKey && !info.accountKey) {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
const usage = await getUsageInfoForIdentity({
|
|
63
|
-
agentId,
|
|
64
|
-
home,
|
|
65
|
-
info,
|
|
66
|
-
cliVersion: null,
|
|
67
|
-
});
|
|
68
|
-
const lines = [heading];
|
|
69
|
-
if (info.email)
|
|
70
|
-
lines.push(` ${chalk.dim(info.email)}`);
|
|
71
|
-
const section = formatUsageSection(usage);
|
|
72
|
-
if (section.length === 0) {
|
|
73
|
-
lines.push(` ${chalk.dim('No usage data available right now.')}`);
|
|
69
|
+
return { agent: agentId, label, status: 'not-signed-in' };
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const usage = await getUsageInfoForIdentity({ agentId, home, info, cliVersion: null });
|
|
72
|
+
return { agent: agentId, label, status: 'ok', email: info.email ?? undefined, usage };
|
|
73
|
+
}
|
|
74
|
+
/** Render one usage record as the human table section. */
|
|
75
|
+
export function formatAgentUsage(rec) {
|
|
76
|
+
const cfg = AGENTS[rec.agent];
|
|
77
|
+
// Colorize the heading here (not in the record) so the plain `label` stays
|
|
78
|
+
// clean for --json while the text table keeps its per-agent color.
|
|
79
|
+
const heading = agentLabel(rec.agent);
|
|
80
|
+
switch (rec.status) {
|
|
81
|
+
case 'unsupported':
|
|
82
|
+
return [heading, ` ${chalk.dim(`${cfg.name} CLI does not publish usage data.`)}`].join('\n');
|
|
83
|
+
case 'no-version':
|
|
84
|
+
return [heading, ` ${chalk.dim('No version installed.')}`].join('\n');
|
|
85
|
+
case 'not-signed-in':
|
|
86
|
+
return [heading, ` ${chalk.dim('Not signed in.')}`].join('\n');
|
|
87
|
+
case 'ok': {
|
|
88
|
+
const lines = [heading];
|
|
89
|
+
if (rec.email)
|
|
90
|
+
lines.push(` ${chalk.dim(rec.email)}`);
|
|
91
|
+
const section = formatUsageSection(rec.usage);
|
|
92
|
+
if (section.length === 0) {
|
|
93
|
+
lines.push(` ${chalk.dim('No usage data available right now.')}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
lines.push(...section);
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
77
100
|
}
|
|
78
|
-
return lines.join('\n');
|
|
79
101
|
}
|