@phnx-labs/agents-cli 1.20.34 → 1.20.35
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 +22 -0
- package/README.md +5 -1
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +59 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/secrets.js +40 -16
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +58 -31
- package/dist/lib/agents.js +30 -9
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +76 -16
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.js +22 -0
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +11 -3
- package/dist/lib/shims.js +54 -30
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents events` — read the structured audit/event log.
|
|
3
|
+
*
|
|
4
|
+
* The event log (`~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl`) records every
|
|
5
|
+
* `agents <module> <cmd>` invocation plus richer typed events (secrets access,
|
|
6
|
+
* version installs, ...), each stamped with who ran it and from where (OS user,
|
|
7
|
+
* local vs SSH, remote client IP). This command reads it back — the audit trail
|
|
8
|
+
* for "who accessed a secret / created a team / started an agent, and from which
|
|
9
|
+
* host?".
|
|
10
|
+
*
|
|
11
|
+
* Filter by `--module` (top-level group, e.g. teams), `--command` (path prefix,
|
|
12
|
+
* e.g. "teams create"), `--event` (typed event), `--agent`, and `--since`.
|
|
13
|
+
* `--follow` tails today's log live.
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import { query, LOGS_PATH } from '../lib/events.js';
|
|
18
|
+
/** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. */
|
|
19
|
+
function parseSince(s) {
|
|
20
|
+
const m = s.match(/^(\d+)([smhdw])$/);
|
|
21
|
+
if (m) {
|
|
22
|
+
const n = parseInt(m[1], 10);
|
|
23
|
+
const unitMs = {
|
|
24
|
+
s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
|
|
25
|
+
};
|
|
26
|
+
return new Date(Date.now() - n * unitMs[m[2]]);
|
|
27
|
+
}
|
|
28
|
+
const ms = Date.parse(s);
|
|
29
|
+
if (isNaN(ms))
|
|
30
|
+
throw new Error(`Invalid --since value: ${s} (use e.g. 2h, 7d, or an ISO date)`);
|
|
31
|
+
return new Date(ms);
|
|
32
|
+
}
|
|
33
|
+
/** How the run reached this host — 'local' or 'ssh 203.0.113.7'. */
|
|
34
|
+
function originLabel(r) {
|
|
35
|
+
if (r.transport === 'ssh') {
|
|
36
|
+
return chalk.yellow(`ssh${r.sshClientIp ? ' ' + r.sshClientIp : ''}`);
|
|
37
|
+
}
|
|
38
|
+
return chalk.gray('local');
|
|
39
|
+
}
|
|
40
|
+
/** The most useful one-line detail for a record, by event family. */
|
|
41
|
+
function detailFor(r) {
|
|
42
|
+
if (r.command)
|
|
43
|
+
return r.command;
|
|
44
|
+
const bits = [];
|
|
45
|
+
if (typeof r.team === 'string')
|
|
46
|
+
bits.push(`team=${r.team}`);
|
|
47
|
+
if (typeof r.bundle === 'string')
|
|
48
|
+
bits.push(`bundle=${r.bundle}`);
|
|
49
|
+
if (typeof r.skill === 'string')
|
|
50
|
+
bits.push(`skill=${r.skill}`);
|
|
51
|
+
if (typeof r.version === 'string')
|
|
52
|
+
bits.push(`v=${r.version}`);
|
|
53
|
+
if (typeof r.profile === 'string')
|
|
54
|
+
bits.push(`profile=${r.profile}`);
|
|
55
|
+
if (typeof r.error === 'string')
|
|
56
|
+
bits.push(chalk.red(r.error));
|
|
57
|
+
return bits.join(' ');
|
|
58
|
+
}
|
|
59
|
+
function renderRow(r) {
|
|
60
|
+
const time = chalk.gray(r.ts.slice(0, 19).replace('T', ' '));
|
|
61
|
+
const user = `${r.osUser ?? '?'}@${r.hostname}`;
|
|
62
|
+
const ev = r.event.startsWith('error') ? chalk.red(r.event) : chalk.cyan(r.event);
|
|
63
|
+
const agent = r.agent ? chalk.gray(` ${r.agent}`) : '';
|
|
64
|
+
return `${time} ${originLabel(r).padEnd(24)} ${user.padEnd(22)} ${ev.padEnd(26)}${agent} ${detailFor(r)}`;
|
|
65
|
+
}
|
|
66
|
+
export function registerEventsCommand(program) {
|
|
67
|
+
program
|
|
68
|
+
.command('events')
|
|
69
|
+
.description('Read the structured audit/event log (who ran what, from where)')
|
|
70
|
+
.option('--module <name>', 'Only events from this command group (e.g. teams, secrets)')
|
|
71
|
+
.option('--command <path>', 'Only this command path — prefix match (e.g. "teams create")')
|
|
72
|
+
.option('--event <type>', 'Only this typed event (repeatable, e.g. secrets.get)', collect, [])
|
|
73
|
+
.option('--agent <name>', 'Only events tagged with this agent')
|
|
74
|
+
.option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
|
|
75
|
+
.option('--limit <n>', 'Max records to show (default 50)', '50')
|
|
76
|
+
.option('--json', 'Output raw records as JSON')
|
|
77
|
+
.option('-f, --follow', "Tail today's log live")
|
|
78
|
+
.addHelpText('after', `
|
|
79
|
+
Examples:
|
|
80
|
+
agents events Recent activity across everything
|
|
81
|
+
agents events --module teams Team lifecycle (create / add / disband)
|
|
82
|
+
agents events --module secrets Every secret accessed or revealed
|
|
83
|
+
agents events --command "teams create" Just team creations
|
|
84
|
+
agents events --event secrets.get --since 7d --json
|
|
85
|
+
agents events -f Live tail`)
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
if (options.follow) {
|
|
88
|
+
await followLog();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const limit = Math.max(1, parseInt(options.limit ?? '50', 10) || 50);
|
|
92
|
+
let startDate;
|
|
93
|
+
try {
|
|
94
|
+
startDate = options.since ? parseSince(options.since) : undefined;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error(chalk.red(err.message));
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
const records = query({
|
|
101
|
+
startDate,
|
|
102
|
+
eventTypes: options.event && options.event.length ? options.event : undefined,
|
|
103
|
+
agent: options.agent,
|
|
104
|
+
command: options.command,
|
|
105
|
+
module: options.module,
|
|
106
|
+
limit,
|
|
107
|
+
});
|
|
108
|
+
if (options.json) {
|
|
109
|
+
console.log(JSON.stringify(records, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (records.length === 0) {
|
|
113
|
+
console.log(chalk.gray('No matching events.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// query() returns newest-first; print oldest-first so a tail reads naturally.
|
|
117
|
+
for (const r of records.slice().reverse())
|
|
118
|
+
console.log(renderRow(r));
|
|
119
|
+
console.log(chalk.gray(`\n${records.length} event(s). Log: ${LOGS_PATH}`));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** commander repeatable-option collector. */
|
|
123
|
+
function collect(value, previous) {
|
|
124
|
+
return previous.concat([value]);
|
|
125
|
+
}
|
|
126
|
+
/** Tail today's event file, printing new lines as they land. */
|
|
127
|
+
async function followLog() {
|
|
128
|
+
const today = new Date();
|
|
129
|
+
const file = `${LOGS_PATH}/events-${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}.jsonl`;
|
|
130
|
+
let offset = 0;
|
|
131
|
+
try {
|
|
132
|
+
offset = fs.statSync(file).size;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// File may not exist yet — start at 0 and pick it up on first write.
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.gray(`Tailing ${file} — Ctrl-C to stop`));
|
|
138
|
+
const drain = () => {
|
|
139
|
+
let size = 0;
|
|
140
|
+
try {
|
|
141
|
+
size = fs.statSync(file).size;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (size <= offset) {
|
|
147
|
+
if (size < offset)
|
|
148
|
+
offset = 0; // rotated/truncated
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const fd = fs.openSync(file, 'r');
|
|
152
|
+
try {
|
|
153
|
+
const buf = Buffer.alloc(size - offset);
|
|
154
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
155
|
+
offset = size;
|
|
156
|
+
for (const line of buf.toString('utf-8').split('\n').filter(Boolean)) {
|
|
157
|
+
try {
|
|
158
|
+
console.log(renderRow(JSON.parse(line)));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Skip malformed lines.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
fs.closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
// Poll — simpler and more portable than fs.watch across platforms/editors.
|
|
170
|
+
await new Promise(() => {
|
|
171
|
+
setInterval(drain, 500);
|
|
172
|
+
});
|
|
173
|
+
}
|
package/dist/commands/exec.js
CHANGED
|
@@ -294,7 +294,12 @@ export function registerRunCommand(program) {
|
|
|
294
294
|
console.log(chalk.green(`Dispatched to ${host.name}.`) + chalk.gray(' Track: agents hosts ps · Follow: agents hosts logs <id> -f'));
|
|
295
295
|
process.exit(0);
|
|
296
296
|
}
|
|
297
|
-
|
|
297
|
+
// -1 = the follow window closed but the run continues on the host (the
|
|
298
|
+
// reattach hint is already printed). That's a detach, not a failure —
|
|
299
|
+
// exit 0. Any real remote code passes through.
|
|
300
|
+
if (exitCode === -1)
|
|
301
|
+
process.exit(0);
|
|
302
|
+
process.exit(exitCode ?? 1);
|
|
298
303
|
}
|
|
299
304
|
catch (err) {
|
|
300
305
|
console.error(chalk.red(err.message));
|
package/dist/commands/hosts.js
CHANGED
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
* hosts. Dispatch itself is `agents run --host <name>` (see commands/exec.ts).
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
+
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
10
11
|
import { checkbox, confirm } from '@inquirer/prompts';
|
|
11
12
|
import { assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
12
13
|
import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
|
|
13
14
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
14
15
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
15
16
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
16
|
-
import { listTasks
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
17
|
+
import { listTasks } from '../lib/hosts/tasks.js';
|
|
18
|
+
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
19
|
+
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
19
20
|
/** Parse `user@host` or `host` into its pieces. */
|
|
20
21
|
function parseTarget(target) {
|
|
21
22
|
const at = target.indexOf('@');
|
|
@@ -23,9 +24,12 @@ function parseTarget(target) {
|
|
|
23
24
|
return { address: target };
|
|
24
25
|
return { user: target.slice(0, at), address: target.slice(at + 1) };
|
|
25
26
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Bootstrap/verify agents-cli on a freshly-enrolled host (best-effort, prompts).
|
|
29
|
+
* Callers that just probed the host pass the result through to avoid a second,
|
|
30
|
+
* redundant `uname` round-trip; otherwise we probe here.
|
|
31
|
+
*/
|
|
32
|
+
async function maybeBootstrap(target, hostName, probe = probeHost(target)) {
|
|
29
33
|
if (!probe.reachable) {
|
|
30
34
|
console.log(chalk.yellow(` Not reachable over SSH yet — skipping bootstrap. Fix key auth, then: agents hosts check ${hostName}`));
|
|
31
35
|
return;
|
|
@@ -75,7 +79,7 @@ async function doAdd(name, target, opts) {
|
|
|
75
79
|
await registerHost({ name: c, provider: 'local', source, ...(source === 'inline' ? { address: c } : {}), os: probe.os, caps: opts.cap });
|
|
76
80
|
console.log(chalk.green(`Enrolled ${c}`) + chalk.gray(` (${source}${probe.os ? `, ${probe.os}` : ''})`));
|
|
77
81
|
if (opts.enroll !== false)
|
|
78
|
-
await maybeBootstrap(c, c);
|
|
82
|
+
await maybeBootstrap(c, c, probe);
|
|
79
83
|
}
|
|
80
84
|
return;
|
|
81
85
|
}
|
|
@@ -107,7 +111,7 @@ async function doAdd(name, target, opts) {
|
|
|
107
111
|
await registerHost(spec);
|
|
108
112
|
console.log(chalk.green(`Enrolled ${name}`) + chalk.gray(` (${spec.source}${spec.os ? `, ${spec.os}` : ''}${spec.caps?.length ? `, caps: ${spec.caps.join(',')}` : ''})`));
|
|
109
113
|
if (opts.enroll !== false)
|
|
110
|
-
await maybeBootstrap(sshTarget, name);
|
|
114
|
+
await maybeBootstrap(sshTarget, name, probe);
|
|
111
115
|
}
|
|
112
116
|
async function doList(json) {
|
|
113
117
|
const hosts = await listAllHosts();
|
|
@@ -122,8 +126,11 @@ async function doList(json) {
|
|
|
122
126
|
console.log(chalk.bold('NAME').padEnd(20) + chalk.bold('SOURCE').padEnd(13) + chalk.bold('TARGET').padEnd(28) + chalk.bold('CAPS'));
|
|
123
127
|
for (const h of hosts) {
|
|
124
128
|
const tgt = h.source === 'ssh-config' ? chalk.gray('(ssh-config)') : `${h.user ? h.user + '@' : ''}${h.address ?? ''}`;
|
|
129
|
+
// Cap TARGET at its column so a long user@host can't shove CAPS out of alignment.
|
|
130
|
+
const tgtW = stringWidth(tgt);
|
|
131
|
+
const tgtCol = tgtW > 28 ? truncateToWidth(tgt, 28) : tgt + ' '.repeat(28 - tgtW);
|
|
125
132
|
const mark = h.enrolled ? '' : chalk.gray(' ·available');
|
|
126
|
-
console.log(h.name.padEnd(20) + h.source.padEnd(13) +
|
|
133
|
+
console.log(h.name.padEnd(20) + h.source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
async function doCheck(name) {
|
|
@@ -155,7 +162,10 @@ async function doRemove(name) {
|
|
|
155
162
|
console.log(chalk.green(`Removed ${name}`));
|
|
156
163
|
}
|
|
157
164
|
async function doPs(json) {
|
|
158
|
-
|
|
165
|
+
// Heal any 'running' record whose local follower died (dropped connection,
|
|
166
|
+
// laptop sleep) against the remote `.exit` before listing, so `ps` never shows
|
|
167
|
+
// a finished run stuck at 'running'.
|
|
168
|
+
const tasks = reconcileRunningTasks(listTasks());
|
|
159
169
|
if (json) {
|
|
160
170
|
console.log(JSON.stringify(tasks, null, 2));
|
|
161
171
|
return;
|
|
@@ -164,30 +174,24 @@ async function doPs(json) {
|
|
|
164
174
|
console.log(chalk.gray('No host tasks yet. Dispatch one: agents run <agent> "<task>" --host <name>'));
|
|
165
175
|
return;
|
|
166
176
|
}
|
|
177
|
+
const cols = terminalWidth();
|
|
167
178
|
console.log(chalk.bold('ID').padEnd(11) + chalk.bold('HOST').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
|
|
168
179
|
for (const t of tasks) {
|
|
169
180
|
const status = t.status === 'completed' ? chalk.green(t.status) : t.status === 'failed' ? chalk.red(t.status) : chalk.yellow(t.status);
|
|
170
|
-
|
|
181
|
+
// Prompt fills the remaining width instead of a fixed 50-char byte slice (98-char rows).
|
|
182
|
+
const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 10 + 11)));
|
|
183
|
+
console.log(t.id.padEnd(11) + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
186
|
async function doLogs(id, follow) {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
187
|
+
const res = await showHostTaskLog(id, follow);
|
|
188
|
+
if (!res.found) {
|
|
176
189
|
console.log(chalk.red(`Unknown task "${id}".`));
|
|
177
190
|
process.exitCode = 1;
|
|
178
191
|
return;
|
|
179
192
|
}
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
process.exitCode = code === -1 ? 1 : code;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
process.stdout.write(fs.readFileSync(localLogPath(id), 'utf-8'));
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
console.log(chalk.gray('(no local log captured for this task)'));
|
|
190
|
-
}
|
|
193
|
+
if (res.exitCode !== undefined)
|
|
194
|
+
process.exitCode = res.exitCode;
|
|
191
195
|
}
|
|
192
196
|
/** Register the `agents hosts` command tree. */
|
|
193
197
|
export function registerHostsCommand(program) {
|
|
@@ -18,6 +18,8 @@ import { type PluginResourceGroup } from '../lib/plugins.js';
|
|
|
18
18
|
/** Resource kinds the inspect command can drill into. */
|
|
19
19
|
declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
|
|
20
20
|
type DrillableKind = typeof DRILLABLE_KINDS[number];
|
|
21
|
+
/** Wrap a separator-joined value list under `prefix`, preserving a hanging indent. */
|
|
22
|
+
export declare function wrapJoined(prefix: string, items: string[], sep: string, width: number): string[];
|
|
21
23
|
interface ResourceItem {
|
|
22
24
|
name: string;
|
|
23
25
|
source: string;
|
package/dist/commands/inspect.js
CHANGED
|
@@ -31,6 +31,7 @@ import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../
|
|
|
31
31
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
32
32
|
import { countSessionsInScope } from '../lib/session/discover.js';
|
|
33
33
|
import { damerauLevenshtein } from '../lib/fuzzy.js';
|
|
34
|
+
import { terminalWidth, truncateToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
|
|
34
35
|
/** Resource kinds the inspect command can drill into. */
|
|
35
36
|
const DRILLABLE_KINDS = [
|
|
36
37
|
'commands',
|
|
@@ -68,6 +69,44 @@ const SINGULAR_DRILL_ALIASES = {
|
|
|
68
69
|
const CAPABILITY_NAMES = [
|
|
69
70
|
'hooks', 'mcp', 'skills', 'commands', 'subagents', 'plugins', 'workflows', 'rules', 'allowlist',
|
|
70
71
|
];
|
|
72
|
+
/** Wrap a separator-joined value list under `prefix`, preserving a hanging indent. */
|
|
73
|
+
export function wrapJoined(prefix, items, sep, width) {
|
|
74
|
+
if (items.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
const continuation = ' '.repeat(stringWidth(prefix));
|
|
77
|
+
const fitItem = (item, linePrefix) => {
|
|
78
|
+
const room = Math.max(1, width - stringWidth(linePrefix));
|
|
79
|
+
return stringWidth(item) > room ? truncateToWidth(item, room) : item;
|
|
80
|
+
};
|
|
81
|
+
const lines = [];
|
|
82
|
+
let linePrefix = prefix;
|
|
83
|
+
let line = prefix;
|
|
84
|
+
let hasItem = false;
|
|
85
|
+
for (const raw of items) {
|
|
86
|
+
const item = fitItem(raw, linePrefix);
|
|
87
|
+
const candidate = hasItem ? `${line}${sep}${item}` : `${line}${item}`;
|
|
88
|
+
if (hasItem && stringWidth(candidate) > width) {
|
|
89
|
+
lines.push(line);
|
|
90
|
+
linePrefix = continuation;
|
|
91
|
+
line = continuation + fitItem(raw, linePrefix);
|
|
92
|
+
hasItem = true;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
line = candidate;
|
|
96
|
+
hasItem = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (hasItem)
|
|
100
|
+
lines.push(line);
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
function printWrappedJoined(prefix, items, sep) {
|
|
104
|
+
for (const line of wrapJoined(prefix, items, sep, terminalWidth()))
|
|
105
|
+
console.log(line);
|
|
106
|
+
}
|
|
107
|
+
function truncateValueForPrefix(prefix, value) {
|
|
108
|
+
return truncateToWidth(stripAnsi(value), Math.max(1, terminalWidth() - stringWidth(prefix)));
|
|
109
|
+
}
|
|
71
110
|
// ─── Command registration ────────────────────────────────────────────────────
|
|
72
111
|
export function registerInspectCommand(program) {
|
|
73
112
|
const cmd = addHostOption(program.command('inspect <target>'))
|
|
@@ -425,7 +464,10 @@ function renderRepoSummary(repo, options) {
|
|
|
425
464
|
console.log(` ${'git'.padEnd(10)} ${git.branch}${dirty}${url}`);
|
|
426
465
|
if (git.lastCommit) {
|
|
427
466
|
const rel = git.lastCommit.relative ? ` ${chalk.gray(`(${git.lastCommit.relative})`)}` : '';
|
|
428
|
-
|
|
467
|
+
const prefix = ` ${''.padEnd(10)} ${chalk.gray('last'.padEnd(8))} `;
|
|
468
|
+
const sha = chalk.cyan(git.lastCommit.sha);
|
|
469
|
+
const subjectWidth = Math.max(8, terminalWidth() - stringWidth(prefix) - stringWidth(sha) - 2 - stringWidth(rel));
|
|
470
|
+
console.log(`${prefix}${sha} ${truncateToWidth(git.lastCommit.subject, subjectWidth)}${rel}`);
|
|
429
471
|
}
|
|
430
472
|
if (git.ahead !== null && git.behind !== null && (git.ahead > 0 || git.behind > 0)) {
|
|
431
473
|
sub('sync', `ahead ${git.ahead} ${chalk.gray('·')} behind ${git.behind}`);
|
|
@@ -440,10 +482,12 @@ function renderRepoSummary(repo, options) {
|
|
|
440
482
|
console.log(` ${'manifests'.padEnd(10)} ${manifests.join(', ')}`);
|
|
441
483
|
if (manifest) {
|
|
442
484
|
if (manifest.versions.length > 0) {
|
|
443
|
-
|
|
485
|
+
const prefix = ` ${''.padEnd(10)} ${chalk.gray('versions'.padEnd(8))} `;
|
|
486
|
+
printWrappedJoined(prefix, manifest.versions.map(v => `${v.agent} ${chalk.cyan(v.version)}`), chalk.gray(' · '));
|
|
444
487
|
}
|
|
445
488
|
if (manifest.strategies.length > 0) {
|
|
446
|
-
|
|
489
|
+
const prefix = ` ${''.padEnd(10)} ${chalk.gray('run'.padEnd(8))} `;
|
|
490
|
+
printWrappedJoined(prefix, manifest.strategies.map(s => `${s.agent}:${s.strategy}`), chalk.gray(' · '));
|
|
447
491
|
}
|
|
448
492
|
}
|
|
449
493
|
}
|
|
@@ -551,8 +595,10 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
551
595
|
['alias', aliasPath],
|
|
552
596
|
['strategy', strategy],
|
|
553
597
|
];
|
|
554
|
-
for (const [k, v] of rows)
|
|
555
|
-
|
|
598
|
+
for (const [k, v] of rows) {
|
|
599
|
+
const prefix = ` ${k.padEnd(10)} `;
|
|
600
|
+
console.log(prefix + truncateValueForPrefix(prefix, v));
|
|
601
|
+
}
|
|
556
602
|
console.log('\n' + chalk.bold('Capabilities'));
|
|
557
603
|
for (const cap of CAPABILITY_NAMES) {
|
|
558
604
|
const res = capabilities[cap];
|
|
@@ -603,7 +649,8 @@ function renderItemList(header, jsonHead, kind, items, options) {
|
|
|
603
649
|
const tag = chalk.gray(`[${item.source}]`.padEnd(10));
|
|
604
650
|
console.log(` ${tag} ${termLink(chalk.cyan(item.name), item.linkTarget)}`);
|
|
605
651
|
if (item.description) {
|
|
606
|
-
|
|
652
|
+
const prefix = ' ';
|
|
653
|
+
console.log(prefix + chalk.gray(truncateToWidth(item.description, Math.max(1, terminalWidth() - stringWidth(prefix)))));
|
|
607
654
|
}
|
|
608
655
|
if (item.groups)
|
|
609
656
|
printGroupRows(item.groups);
|
|
@@ -1171,7 +1218,7 @@ function readFirstProseLine(p) {
|
|
|
1171
1218
|
}
|
|
1172
1219
|
// ─── OSC-8 + path helpers ────────────────────────────────────────────────────
|
|
1173
1220
|
function termLink(text, filePath) {
|
|
1174
|
-
if (!filePath)
|
|
1221
|
+
if (!filePath || !process.stdout.isTTY)
|
|
1175
1222
|
return text;
|
|
1176
1223
|
const url = `file://${filePath}`;
|
|
1177
1224
|
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents logs` — unified, discoverable run-log viewer.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a run across two substrates and shows (or `-f` follows) its log:
|
|
5
|
+
* - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
|
|
6
|
+
* - sessions (the local index) → transcript, tailed via the sessions tailer
|
|
7
|
+
*
|
|
8
|
+
* `[id]`/`--session` load directly (host task tried first, then session). With no
|
|
9
|
+
* id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
|
|
10
|
+
* shown, several open the fuzzy picker (or, non-TTY, print the list).
|
|
11
|
+
*
|
|
12
|
+
* Additive: `agents hosts logs` and `agents sessions tail` are unchanged and share
|
|
13
|
+
* the same underlying helpers (showHostTaskLog / streamSessionTail).
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from 'commander';
|
|
16
|
+
/** Register the top-level `agents logs` command. */
|
|
17
|
+
export declare function registerLogsCommand(program: Command): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents logs` — unified, discoverable run-log viewer.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a run across two substrates and shows (or `-f` follows) its log:
|
|
5
|
+
* - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
|
|
6
|
+
* - sessions (the local index) → transcript, tailed via the sessions tailer
|
|
7
|
+
*
|
|
8
|
+
* `[id]`/`--session` load directly (host task tried first, then session). With no
|
|
9
|
+
* id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
|
|
10
|
+
* shown, several open the fuzzy picker (or, non-TTY, print the list).
|
|
11
|
+
*
|
|
12
|
+
* Additive: `agents hosts logs` and `agents sessions tail` are unchanged and share
|
|
13
|
+
* the same underlying helpers (showHostTaskLog / streamSessionTail).
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
|
|
17
|
+
import { parseAgentFilter, renderSessionLog } from './sessions.js';
|
|
18
|
+
import { streamSessionTail, isTailable } from './sessions-tail.js';
|
|
19
|
+
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
20
|
+
import { listTasks } from '../lib/hosts/tasks.js';
|
|
21
|
+
import { itemPicker } from '../lib/picker.js';
|
|
22
|
+
/** Compact one-line label used by both the picker and the non-TTY list. */
|
|
23
|
+
function candidateLabel(c) {
|
|
24
|
+
if (c.kind === 'task') {
|
|
25
|
+
const t = c.task;
|
|
26
|
+
const status = t.status === 'completed' ? chalk.green(t.status)
|
|
27
|
+
: t.status === 'failed' ? chalk.red(t.status)
|
|
28
|
+
: chalk.yellow(t.status);
|
|
29
|
+
return `${chalk.gray('task')} ${t.id.slice(0, 8).padEnd(9)} ${t.host.padEnd(14)} ${t.agent.padEnd(8)} ${status} ${t.prompt.slice(0, 40)}`;
|
|
30
|
+
}
|
|
31
|
+
const s = c.session;
|
|
32
|
+
const ver = s.version ? chalk.gray(`@${s.version}`) : '';
|
|
33
|
+
const title = s.label || s.topic || '';
|
|
34
|
+
return `${chalk.gray('sess')} ${s.shortId.padEnd(9)} ${(s.agent + ver).padEnd(14)} ${chalk.gray(s.timestamp.slice(0, 16))} ${title.slice(0, 40)}`;
|
|
35
|
+
}
|
|
36
|
+
/** Show a resolved session — follow (tail) or render its transcript. */
|
|
37
|
+
async function showSession(session, follow) {
|
|
38
|
+
if (follow) {
|
|
39
|
+
if (!isTailable(session.agent)) {
|
|
40
|
+
console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
await streamSessionTail(session, {});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await renderSessionLog(session);
|
|
47
|
+
}
|
|
48
|
+
async function showCandidate(c, follow) {
|
|
49
|
+
if (c.kind === 'task') {
|
|
50
|
+
const res = await showHostTaskLog(c.task.id, follow);
|
|
51
|
+
if (res.exitCode !== undefined)
|
|
52
|
+
process.exitCode = res.exitCode;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await showSession(c.session, follow);
|
|
56
|
+
}
|
|
57
|
+
/** Resolve an explicit id/--session: host task first, then a session. */
|
|
58
|
+
async function showById(id, follow) {
|
|
59
|
+
const hostRes = await showHostTaskLog(id, follow);
|
|
60
|
+
if (hostRes.found) {
|
|
61
|
+
if (hostRes.exitCode !== undefined)
|
|
62
|
+
process.exitCode = hostRes.exitCode;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const sessions = await discoverSessions({ all: true, limit: 5000 });
|
|
66
|
+
const matches = resolveSessionById(sessions, id);
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
console.error(chalk.red(`No run or session found matching "${id}".`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
await showSession(matches[0], follow);
|
|
72
|
+
}
|
|
73
|
+
async function runLogs(id, opts) {
|
|
74
|
+
const follow = !!opts.follow;
|
|
75
|
+
const directId = opts.session ?? id;
|
|
76
|
+
if (directId) {
|
|
77
|
+
await showById(directId, follow);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const { agent, version } = parseAgentFilter(opts.agent);
|
|
81
|
+
const wantVersion = opts.version ?? version;
|
|
82
|
+
// Host tasks carry no session-index metadata; sessions carry no host tag.
|
|
83
|
+
// So --host scopes to dispatched tasks, and --version to sessions.
|
|
84
|
+
const candidates = [];
|
|
85
|
+
let tasks = listTasks();
|
|
86
|
+
if (opts.host)
|
|
87
|
+
tasks = tasks.filter((t) => t.host === opts.host);
|
|
88
|
+
if (agent)
|
|
89
|
+
tasks = tasks.filter((t) => t.agent === agent);
|
|
90
|
+
for (const t of tasks)
|
|
91
|
+
candidates.push({ kind: 'task', task: t });
|
|
92
|
+
if (!opts.host) {
|
|
93
|
+
const sessions = await discoverSessions({ agent, version: wantVersion, limit: 50 });
|
|
94
|
+
for (const s of sessions)
|
|
95
|
+
candidates.push({ kind: 'session', session: s });
|
|
96
|
+
}
|
|
97
|
+
if (candidates.length === 0) {
|
|
98
|
+
console.error(chalk.yellow('No matching runs. Dispatch one: agents run <agent> "<task>" [--host <name>]'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (candidates.length === 1) {
|
|
102
|
+
await showCandidate(candidates[0], follow);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Multiple sessions matched → picker if interactive, else a list to pick from.
|
|
106
|
+
if (!process.stdin.isTTY) {
|
|
107
|
+
console.error(chalk.yellow(`${candidates.length} runs match. Pass an id or --session <id>:`));
|
|
108
|
+
for (const c of candidates.slice(0, 30))
|
|
109
|
+
console.error(' ' + candidateLabel(c));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const picked = await itemPicker({
|
|
113
|
+
message: 'Select a run to view its log:',
|
|
114
|
+
items: candidates,
|
|
115
|
+
filter: (query) => {
|
|
116
|
+
const q = query.trim().toLowerCase();
|
|
117
|
+
if (!q)
|
|
118
|
+
return candidates;
|
|
119
|
+
return candidates.filter((c) => candidateLabel(c).toLowerCase().includes(q));
|
|
120
|
+
},
|
|
121
|
+
labelFor: (c) => candidateLabel(c),
|
|
122
|
+
shortIdFor: (c) => (c.kind === 'task' ? c.task.id : c.session.shortId),
|
|
123
|
+
});
|
|
124
|
+
if (!picked)
|
|
125
|
+
return;
|
|
126
|
+
await showCandidate(picked.item, follow);
|
|
127
|
+
}
|
|
128
|
+
/** Register the top-level `agents logs` command. */
|
|
129
|
+
export function registerLogsCommand(program) {
|
|
130
|
+
program
|
|
131
|
+
.command('logs [id]')
|
|
132
|
+
.description('Show a run’s log — a host-dispatch task or a session. -f to follow a live one.')
|
|
133
|
+
.option('--host <name>', 'Scope to runs dispatched to a host')
|
|
134
|
+
.option('-a, --agent <agent>', 'Filter by agent (e.g. claude, codex@0.116.0)')
|
|
135
|
+
.option('--version <version>', 'Filter by agent version')
|
|
136
|
+
.option('--session <id>', 'Select a session/run by id (same as the positional id)')
|
|
137
|
+
.option('-f, --follow', 'Follow live output')
|
|
138
|
+
.action((id, opts) => runLogs(id, opts));
|
|
139
|
+
}
|
|
@@ -8,3 +8,6 @@
|
|
|
8
8
|
import type { Command } from 'commander';
|
|
9
9
|
/** Register the hidden `agents models` command. */
|
|
10
10
|
export declare function registerModelsCommand(program: Command): void;
|
|
11
|
+
export declare function formatModelSourceLine(kind: string, sourcePath: string, width?: number): string;
|
|
12
|
+
export declare function formatModelAliasLines(parts: string[], width?: number): string[];
|
|
13
|
+
export declare function formatModelSummaryLine(star: string, id: string, displayName?: string, alias?: string, width?: number): string;
|
package/dist/commands/models.js
CHANGED
|
@@ -11,6 +11,8 @@ import { homeDir } from '../lib/platform/index.js';
|
|
|
11
11
|
import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
|
|
12
12
|
import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
|
|
13
13
|
import { getModelCatalog, locateModelSource } from '../lib/models.js';
|
|
14
|
+
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
15
|
+
import { wrapJoined } from './inspect.js';
|
|
14
16
|
const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi'];
|
|
15
17
|
/**
|
|
16
18
|
* Agents that don't necessarily install under ~/.agents/versions (cursor ships
|
|
@@ -131,21 +133,19 @@ function printCatalog(agent, version, isDefault, options) {
|
|
|
131
133
|
console.log(chalk.yellow(` No models extracted from ${src.kind} at ${src.path}.`));
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
|
-
console.log(chalk.gray(
|
|
136
|
+
console.log(chalk.gray(formatModelSourceLine(src.kind, shortPath(src.path))));
|
|
135
137
|
if (Object.keys(catalog.aliases).length > 0) {
|
|
136
138
|
const parts = Object.entries(catalog.aliases).map(([alias, id]) => `${chalk.cyan(alias)}=${id}`);
|
|
137
|
-
|
|
139
|
+
for (const line of formatModelAliasLines(parts))
|
|
140
|
+
console.log(line);
|
|
138
141
|
}
|
|
139
142
|
console.log();
|
|
140
143
|
for (const model of catalog.models) {
|
|
141
144
|
const star = model.isDefault ? chalk.green('*') : ' ';
|
|
142
|
-
|
|
143
|
-
? chalk.gray(` (${model.displayName})`)
|
|
144
|
-
: '';
|
|
145
|
-
const aliasTag = model.alias ? chalk.cyan(` [${model.alias}]`) : '';
|
|
146
|
-
console.log(` ${star} ${chalk.bold(model.id)}${display}${aliasTag}`);
|
|
145
|
+
console.log(formatModelSummaryLine(star, model.id, model.displayName, model.alias));
|
|
147
146
|
if (model.description) {
|
|
148
|
-
|
|
147
|
+
const descPrefix = ' ';
|
|
148
|
+
console.log(chalk.gray(descPrefix + truncateToWidth(model.description, Math.max(1, terminalWidth() - stringWidth(descPrefix)))));
|
|
149
149
|
}
|
|
150
150
|
if (options.cloud && model.perCloud) {
|
|
151
151
|
const c = model.perCloud;
|
|
@@ -175,3 +175,22 @@ function printCatalog(agent, version, isDefault, options) {
|
|
|
175
175
|
function shortPath(p) {
|
|
176
176
|
return p.replace(homeDir(), '~');
|
|
177
177
|
}
|
|
178
|
+
export function formatModelSourceLine(kind, sourcePath, width = terminalWidth()) {
|
|
179
|
+
const prefix = ` source: ${kind} (`;
|
|
180
|
+
const suffix = ')';
|
|
181
|
+
const room = Math.max(1, width - stringWidth(prefix) - stringWidth(suffix));
|
|
182
|
+
return prefix + truncateToWidth(sourcePath, room) + suffix;
|
|
183
|
+
}
|
|
184
|
+
export function formatModelAliasLines(parts, width = terminalWidth()) {
|
|
185
|
+
return wrapJoined(chalk.gray(' aliases: '), parts, ', ', width);
|
|
186
|
+
}
|
|
187
|
+
export function formatModelSummaryLine(star, id, displayName, alias, width = terminalWidth()) {
|
|
188
|
+
const display = displayName && displayName !== id ? ` (${displayName})` : '';
|
|
189
|
+
const aliasTag = alias ? ` [${alias}]` : '';
|
|
190
|
+
const prefix = ` ${star} `;
|
|
191
|
+
const plain = `${id}${display}${aliasTag}`;
|
|
192
|
+
if (stringWidth(plain) > Math.max(1, width - stringWidth(prefix))) {
|
|
193
|
+
return prefix + truncateToWidth(plain, Math.max(1, width - stringWidth(prefix)));
|
|
194
|
+
}
|
|
195
|
+
return prefix + chalk.bold(id) + chalk.gray(display) + chalk.cyan(aliasTag);
|
|
196
|
+
}
|