@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
+
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
3
4
|
import * as fs from 'fs';
|
|
4
5
|
import * as os from 'os';
|
|
5
6
|
import * as path from 'path';
|
|
@@ -191,9 +192,15 @@ When to use:
|
|
|
191
192
|
return;
|
|
192
193
|
}
|
|
193
194
|
console.log(chalk.bold('Installed Permission Sets:\n'));
|
|
195
|
+
const permCols = terminalWidth();
|
|
194
196
|
for (const perm of sets) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
+
const prefix = ` ${chalk.cyan(perm.name)}`;
|
|
198
|
+
// Cap the description to the line so a prose-paragraph set (400+ chars) can't smear.
|
|
199
|
+
const budget = permCols - stringWidth(prefix) - 3;
|
|
200
|
+
const desc = perm.set.description && budget > 3
|
|
201
|
+
? ` - ${chalk.gray(truncateToWidth(perm.set.description, budget))}`
|
|
202
|
+
: '';
|
|
203
|
+
console.log(`${prefix}${desc}`);
|
|
197
204
|
console.log(` ${chalk.gray(`${perm.set.allow.length} allow, ${perm.set.deny?.length || 0} deny rules`)}`);
|
|
198
205
|
}
|
|
199
206
|
}
|
package/dist/commands/repo.d.ts
CHANGED
|
@@ -28,6 +28,36 @@ export declare function resourceUnit(file: string): {
|
|
|
28
28
|
kind: RepoResourceKind;
|
|
29
29
|
unit: string;
|
|
30
30
|
};
|
|
31
|
+
/** One resource kind + action, collapsed to a unit count (e.g. 2 new skills). */
|
|
32
|
+
export interface CountedResource {
|
|
33
|
+
action: ChangeAction;
|
|
34
|
+
kind: RepoResourceKind;
|
|
35
|
+
count: number;
|
|
36
|
+
}
|
|
37
|
+
/** Resource-level summary of a changed-file set. */
|
|
38
|
+
export interface ResourceDelta {
|
|
39
|
+
/** Counts in display order (grouped new -> changed -> removed, then RESOURCE_ORDER). */
|
|
40
|
+
counts: CountedResource[];
|
|
41
|
+
/** Total distinct resource units changed. */
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Collapse a changed-file set to distinct resource units, then count them by
|
|
46
|
+
* (action, kind). A skill whose three files all changed counts as one changed
|
|
47
|
+
* skill; a unit with mixed add+modify reads as a single change. This is the
|
|
48
|
+
* shared structured core behind every SYNC / CHANGES rendering.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resourceDelta(entries: {
|
|
51
|
+
action: ChangeAction;
|
|
52
|
+
file: string;
|
|
53
|
+
}[]): ResourceDelta;
|
|
54
|
+
/**
|
|
55
|
+
* Kind-only brief for the compact (wide-terminal) layout: `(24 skills, 9 commands, +20)`
|
|
56
|
+
* — the top `maxKinds` kinds by display order, with a `+N` remainder. Drops the
|
|
57
|
+
* new/changed verb (the arrow already carries direction) to stay short. Returns
|
|
58
|
+
* '' when there is nothing to summarize.
|
|
59
|
+
*/
|
|
60
|
+
export declare function deltaBrief(delta: ResourceDelta, maxKinds?: number): string;
|
|
31
61
|
/**
|
|
32
62
|
* Render a set of changed files as a resource-level summary, e.g.
|
|
33
63
|
* `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
|
|
@@ -39,6 +69,10 @@ export declare function formatResourceDelta(entries: {
|
|
|
39
69
|
action: ChangeAction;
|
|
40
70
|
file: string;
|
|
41
71
|
}[], maxParts?: number): string;
|
|
72
|
+
/** `owner/repo` from a git URL, for the compact card header. Falls back to the raw URL. */
|
|
73
|
+
export declare function repoSlug(url: string): string;
|
|
74
|
+
/** Pack colored phrases into `, `-joined lines no wider than `width`. */
|
|
75
|
+
export declare function wrapPhrases(parts: string[], width: number): string[];
|
|
42
76
|
/** Register the `agents repo` command tree. */
|
|
43
77
|
export declare function registerRepoCommands(program: Command): void;
|
|
44
78
|
export {};
|
package/dist/commands/repo.js
CHANGED
|
@@ -106,13 +106,12 @@ export function resourceUnit(file) {
|
|
|
106
106
|
return { kind: 'other', unit: file };
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `maxParts` phrases, appending `+N more` so a big diff stays scannable.
|
|
109
|
+
* Collapse a changed-file set to distinct resource units, then count them by
|
|
110
|
+
* (action, kind). A skill whose three files all changed counts as one changed
|
|
111
|
+
* skill; a unit with mixed add+modify reads as a single change. This is the
|
|
112
|
+
* shared structured core behind every SYNC / CHANGES rendering.
|
|
114
113
|
*/
|
|
115
|
-
export function
|
|
114
|
+
export function resourceDelta(entries) {
|
|
116
115
|
// Gather every action seen across a unit's files, then collapse to one action.
|
|
117
116
|
const units = new Map();
|
|
118
117
|
for (const { action, file } of entries) {
|
|
@@ -136,19 +135,28 @@ export function formatResourceDelta(entries, maxParts = 5) {
|
|
|
136
135
|
const key = `${action} ${kind}`;
|
|
137
136
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
138
137
|
}
|
|
139
|
-
const
|
|
140
|
-
new: chalk.green, changed: chalk.yellow, removed: chalk.red,
|
|
141
|
-
};
|
|
142
|
-
const parts = [];
|
|
138
|
+
const ordered = [];
|
|
143
139
|
for (const action of ['new', 'changed', 'removed']) {
|
|
144
140
|
for (const kind of RESOURCE_ORDER) {
|
|
145
141
|
const n = counts.get(`${action} ${kind}`);
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
const [singular, plural] = RESOURCE_LABELS[kind];
|
|
149
|
-
parts.push(COLOR[action](`${n} ${action} ${n === 1 ? singular : plural}`));
|
|
142
|
+
if (n)
|
|
143
|
+
ordered.push({ action, kind, count: n });
|
|
150
144
|
}
|
|
151
145
|
}
|
|
146
|
+
return { counts: ordered, total: units.size };
|
|
147
|
+
}
|
|
148
|
+
const ACTION_COLOR = {
|
|
149
|
+
new: chalk.green, changed: chalk.yellow, removed: chalk.red,
|
|
150
|
+
};
|
|
151
|
+
/** One colored phrase per counted resource, e.g. `2 new skills`. */
|
|
152
|
+
function deltaPhrases(delta) {
|
|
153
|
+
return delta.counts.map(({ action, kind, count }) => {
|
|
154
|
+
const [singular, plural] = RESOURCE_LABELS[kind];
|
|
155
|
+
return ACTION_COLOR[action](`${count} ${action} ${count === 1 ? singular : plural}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Join phrases with `, `, capping at `maxParts` and appending `+N more`. */
|
|
159
|
+
function joinPhrases(parts, maxParts) {
|
|
152
160
|
if (parts.length > maxParts) {
|
|
153
161
|
const shown = parts.slice(0, maxParts);
|
|
154
162
|
shown.push(chalk.gray(`+${parts.length - maxParts} more`));
|
|
@@ -156,6 +164,36 @@ export function formatResourceDelta(entries, maxParts = 5) {
|
|
|
156
164
|
}
|
|
157
165
|
return parts.join(', ');
|
|
158
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Kind-only brief for the compact (wide-terminal) layout: `(24 skills, 9 commands, +20)`
|
|
169
|
+
* — the top `maxKinds` kinds by display order, with a `+N` remainder. Drops the
|
|
170
|
+
* new/changed verb (the arrow already carries direction) to stay short. Returns
|
|
171
|
+
* '' when there is nothing to summarize.
|
|
172
|
+
*/
|
|
173
|
+
export function deltaBrief(delta, maxKinds = 2) {
|
|
174
|
+
if (delta.total === 0)
|
|
175
|
+
return '';
|
|
176
|
+
const shown = delta.counts.slice(0, maxKinds);
|
|
177
|
+
const shownUnits = shown.reduce((s, c) => s + c.count, 0);
|
|
178
|
+
const rest = delta.total - shownUnits;
|
|
179
|
+
const kinds = shown.map(({ kind, count }) => {
|
|
180
|
+
const [singular, plural] = RESOURCE_LABELS[kind];
|
|
181
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
182
|
+
});
|
|
183
|
+
if (rest > 0)
|
|
184
|
+
kinds.push(`+${rest}`);
|
|
185
|
+
return chalk.gray(`(${kinds.join(', ')})`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Render a set of changed files as a resource-level summary, e.g.
|
|
189
|
+
* `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
|
|
190
|
+
* three files all changed is "1 changed skill"), grouped by action then kind,
|
|
191
|
+
* and colors each phrase green/yellow/red for new/changed/removed. Caps at
|
|
192
|
+
* `maxParts` phrases, appending `+N more` so a big diff stays scannable.
|
|
193
|
+
*/
|
|
194
|
+
export function formatResourceDelta(entries, maxParts = 5) {
|
|
195
|
+
return joinPhrases(deltaPhrases(resourceDelta(entries)), maxParts);
|
|
196
|
+
}
|
|
159
197
|
/** Parse `git diff --name-status <range>` into resource-delta entries. */
|
|
160
198
|
async function diffResourceEntries(git, range) {
|
|
161
199
|
let raw;
|
|
@@ -189,9 +227,10 @@ function padVisible(s, width) {
|
|
|
189
227
|
return s + ' '.repeat(Math.max(0, width - visibleWidth(s)));
|
|
190
228
|
}
|
|
191
229
|
/**
|
|
192
|
-
*
|
|
193
|
-
* move), resource-level local edits, and
|
|
194
|
-
*
|
|
230
|
+
* Read one repo's status into structured data: branch, resource-level sync (what
|
|
231
|
+
* a pull/push would move), resource-level local edits, and remote URL + commit.
|
|
232
|
+
* Formatting is deferred to the layout renderers so the same data can drive both
|
|
233
|
+
* the wide table and the narrow cards.
|
|
195
234
|
*/
|
|
196
235
|
async function renderRepoRow(t) {
|
|
197
236
|
if (!fs.existsSync(t.dir)) {
|
|
@@ -205,29 +244,19 @@ async function renderRepoRow(t) {
|
|
|
205
244
|
const status = await git.status();
|
|
206
245
|
// Show the local branch name; the upstream remote is already implied by URL.
|
|
207
246
|
const branch = status.current || (status.tracking ? status.tracking.replace(/^origin\//, '') : '(detached)');
|
|
247
|
+
const row = { alias: t.alias, branch, tracking: !!status.tracking };
|
|
208
248
|
// SYNC: what a pull brings in / a push sends out, described by resource.
|
|
209
249
|
const ahead = status.ahead ?? 0;
|
|
210
250
|
const behind = status.behind ?? 0;
|
|
211
|
-
|
|
212
|
-
if (!status.tracking) {
|
|
213
|
-
sync = chalk.gray('no upstream');
|
|
214
|
-
}
|
|
215
|
-
else if (ahead === 0 && behind === 0) {
|
|
216
|
-
sync = chalk.green('up to date');
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
const pieces = [];
|
|
251
|
+
if (status.tracking) {
|
|
220
252
|
if (behind > 0) {
|
|
221
253
|
// Three-dot isolates upstream's side via the merge-base, so a diverged
|
|
222
254
|
// branch reports exactly what a pull adds (not the inverse of local commits).
|
|
223
|
-
|
|
224
|
-
pieces.push(`${incoming || chalk.yellow(`${behind} commit${behind > 1 ? 's' : ''}`)} ${chalk.gray('to pull')}`);
|
|
255
|
+
row.pull = { delta: resourceDelta(await diffResourceEntries(git, 'HEAD...@{upstream}')), commits: behind };
|
|
225
256
|
}
|
|
226
257
|
if (ahead > 0) {
|
|
227
|
-
|
|
228
|
-
pieces.push(`${outgoing || chalk.yellow(`${ahead} commit${ahead > 1 ? 's' : ''}`)} ${chalk.gray('to push')}`);
|
|
258
|
+
row.push = { delta: resourceDelta(await diffResourceEntries(git, '@{upstream}...HEAD')), commits: ahead };
|
|
229
259
|
}
|
|
230
|
-
sync = pieces.join(chalk.gray(' · '));
|
|
231
260
|
}
|
|
232
261
|
// CHANGES: uncommitted working-tree edits, described by resource.
|
|
233
262
|
const localEntries = [
|
|
@@ -238,28 +267,183 @@ async function renderRepoRow(t) {
|
|
|
238
267
|
...status.renamed.map((r) => ({ action: 'changed', file: r.to })),
|
|
239
268
|
...status.deleted.map((f) => ({ action: 'removed', file: f })),
|
|
240
269
|
];
|
|
241
|
-
|
|
270
|
+
row.clean = status.isClean();
|
|
271
|
+
if (!row.clean)
|
|
272
|
+
row.local = resourceDelta(localEntries);
|
|
242
273
|
const remotes = await git.getRemotes(true);
|
|
243
274
|
const origin = remotes.find((r) => r.name === 'origin');
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
? chalk.gray(`${url}${commit ? ` (${commit})` : ''}`)
|
|
248
|
-
: commit
|
|
249
|
-
? chalk.gray(`(${commit})`)
|
|
250
|
-
: '';
|
|
251
|
-
return { alias: t.alias, cells: [branch, sync, changes, remote] };
|
|
275
|
+
row.url = origin?.refs?.fetch || '';
|
|
276
|
+
row.commit = (await git.log({ maxCount: 1 })).latest?.hash.slice(0, 8) || '';
|
|
277
|
+
return row;
|
|
252
278
|
}
|
|
253
279
|
catch (err) {
|
|
254
280
|
return { alias: t.alias, raw: `${chalk.red('error')} ${err.message}` };
|
|
255
281
|
}
|
|
256
282
|
}
|
|
283
|
+
/** Terminal width at or above which `list` uses the wide table instead of cards. */
|
|
284
|
+
const WIDE_COLS = 120;
|
|
285
|
+
const commitsWord = (n) => chalk.yellow(`${n} commit${n > 1 ? 's' : ''}`);
|
|
286
|
+
/** `owner/repo` from a git URL, for the compact card header. Falls back to the raw URL. */
|
|
287
|
+
export function repoSlug(url) {
|
|
288
|
+
const m = url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
289
|
+
return m ? m[1] : url;
|
|
290
|
+
}
|
|
291
|
+
/** Full SYNC string: `24 new skills, ... to pull · ... to push` (verbose table). */
|
|
292
|
+
function syncFull(row) {
|
|
293
|
+
if (!row.tracking)
|
|
294
|
+
return chalk.gray('no upstream');
|
|
295
|
+
if (!row.pull && !row.push)
|
|
296
|
+
return chalk.green('up to date');
|
|
297
|
+
const pieces = [];
|
|
298
|
+
if (row.pull) {
|
|
299
|
+
const detail = joinPhrases(deltaPhrases(row.pull.delta), 5) || commitsWord(row.pull.commits);
|
|
300
|
+
pieces.push(`${detail} ${chalk.gray('to pull')}`);
|
|
301
|
+
}
|
|
302
|
+
if (row.push) {
|
|
303
|
+
const detail = joinPhrases(deltaPhrases(row.push.delta), 5) || commitsWord(row.push.commits);
|
|
304
|
+
pieces.push(`${detail} ${chalk.gray('to push')}`);
|
|
305
|
+
}
|
|
306
|
+
return pieces.join(chalk.gray(' · '));
|
|
307
|
+
}
|
|
308
|
+
/** Compact SYNC for the wide table: `↓53 (24 skills, 9 commands, +20) ↑13`. */
|
|
309
|
+
function syncCompact(row) {
|
|
310
|
+
if (!row.tracking)
|
|
311
|
+
return chalk.gray('no upstream');
|
|
312
|
+
if (!row.pull && !row.push)
|
|
313
|
+
return chalk.green('up to date');
|
|
314
|
+
const pieces = [];
|
|
315
|
+
if (row.pull) {
|
|
316
|
+
const n = row.pull.delta.total || row.pull.commits;
|
|
317
|
+
const brief = deltaBrief(row.pull.delta);
|
|
318
|
+
pieces.push(`${chalk.yellow(`↓${n}`)}${brief ? ` ${brief}` : ''}`);
|
|
319
|
+
}
|
|
320
|
+
if (row.push) {
|
|
321
|
+
const n = row.push.delta.total || row.push.commits;
|
|
322
|
+
pieces.push(chalk.magenta(`↑${n}`));
|
|
323
|
+
}
|
|
324
|
+
return pieces.join(' ');
|
|
325
|
+
}
|
|
326
|
+
/** Full CHANGES string for the verbose table. */
|
|
327
|
+
function changesFull(row) {
|
|
328
|
+
if (row.clean)
|
|
329
|
+
return chalk.green('clean');
|
|
330
|
+
return joinPhrases(deltaPhrases(row.local), 5);
|
|
331
|
+
}
|
|
332
|
+
/** Compact CHANGES for the wide table: `clean` or `~1 edit`. */
|
|
333
|
+
function changesCompact(row) {
|
|
334
|
+
if (row.clean)
|
|
335
|
+
return chalk.green('clean');
|
|
336
|
+
const n = row.local.total;
|
|
337
|
+
return chalk.yellow(`~${n} edit${n === 1 ? '' : 's'}`);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Print the aligned table. `compact` picks short (arrow-count) SYNC/CHANGES cells
|
|
341
|
+
* that keep the table bounded; verbose passes `compact: false` for full detail.
|
|
342
|
+
* Only the trailing REMOTE column is allowed to run long.
|
|
343
|
+
*/
|
|
344
|
+
function renderTable(rows, compact) {
|
|
345
|
+
const cells = new Map();
|
|
346
|
+
for (const r of rows) {
|
|
347
|
+
if (r.raw)
|
|
348
|
+
continue;
|
|
349
|
+
const remote = r.url
|
|
350
|
+
? chalk.gray(`${r.url}${r.commit ? ` (${r.commit})` : ''}`)
|
|
351
|
+
: r.commit ? chalk.gray(`(${r.commit})`) : '';
|
|
352
|
+
cells.set(r.alias, [
|
|
353
|
+
r.branch ?? '',
|
|
354
|
+
compact ? syncCompact(r) : syncFull(r),
|
|
355
|
+
compact ? changesCompact(r) : changesFull(r),
|
|
356
|
+
remote,
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
const headers = ['REPO', 'BRANCH', 'SYNC', 'CHANGES'];
|
|
360
|
+
const tableCells = [...cells.values()];
|
|
361
|
+
const aliasW = Math.max(headers[0].length, ...rows.map((r) => r.alias.length));
|
|
362
|
+
const branchW = Math.max(headers[1].length, 0, ...tableCells.map((c) => visibleWidth(c[0])));
|
|
363
|
+
const syncW = Math.max(headers[2].length, 0, ...tableCells.map((c) => visibleWidth(c[1])));
|
|
364
|
+
const changesW = Math.max(headers[3].length, 0, ...tableCells.map((c) => visibleWidth(c[2])));
|
|
365
|
+
console.log(` ${chalk.gray(headers[0].padEnd(aliasW))} ${chalk.gray(headers[1].padEnd(branchW))} ${chalk.gray(headers[2].padEnd(syncW))} ${chalk.gray(headers[3].padEnd(changesW))} ${chalk.gray('REMOTE')}`);
|
|
366
|
+
for (const r of rows) {
|
|
367
|
+
const aliasCol = chalk.cyan(r.alias.padEnd(aliasW));
|
|
368
|
+
const c = cells.get(r.alias);
|
|
369
|
+
if (c) {
|
|
370
|
+
console.log(` ${aliasCol} ${padVisible(c[0], branchW)} ${padVisible(c[1], syncW)} ${padVisible(c[2], changesW)} ${c[3]}`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.log(` ${aliasCol} ${r.raw}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Pack colored phrases into `, `-joined lines no wider than `width`. */
|
|
378
|
+
export function wrapPhrases(parts, width) {
|
|
379
|
+
const lines = [];
|
|
380
|
+
let cur = '';
|
|
381
|
+
for (const p of parts) {
|
|
382
|
+
const cand = cur ? `${cur}, ${p}` : p;
|
|
383
|
+
if (cur && visibleWidth(cand) > width) {
|
|
384
|
+
lines.push(cur);
|
|
385
|
+
cur = p;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
cur = cand;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (cur)
|
|
392
|
+
lines.push(cur);
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Print one block per repo, width-independent. The category list wraps under an
|
|
397
|
+
* indented `↓ pull` / `↑ push` / `local` label instead of running off the edge,
|
|
398
|
+
* so a narrow terminal stays readable.
|
|
399
|
+
*/
|
|
400
|
+
function renderCards(rows, cols) {
|
|
401
|
+
const LABEL = ' '; // indent for detail labels
|
|
402
|
+
const detailWidth = Math.max(20, cols - LABEL.length - 9); // 9 ≈ "↓ pull "
|
|
403
|
+
let first = true;
|
|
404
|
+
for (const r of rows) {
|
|
405
|
+
if (!first)
|
|
406
|
+
console.log('');
|
|
407
|
+
first = false;
|
|
408
|
+
if (r.raw) {
|
|
409
|
+
console.log(` ${chalk.cyan(r.alias)} ${r.raw}`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const head = [r.branch, r.commit || null, r.url ? repoSlug(r.url) : null].filter(Boolean).join(chalk.gray(' · '));
|
|
413
|
+
console.log(` ${chalk.green('●')} ${chalk.cyan(r.alias)} ${chalk.gray(head)}`);
|
|
414
|
+
const detail = (label, div, color) => {
|
|
415
|
+
const phrases = div.delta.total ? deltaPhrases(div.delta) : [commitsWord(div.commits)];
|
|
416
|
+
const lines = wrapPhrases(phrases, detailWidth);
|
|
417
|
+
lines.forEach((line, i) => {
|
|
418
|
+
const lab = i === 0 ? color(label.padEnd(9)) : ' '.repeat(9);
|
|
419
|
+
console.log(`${LABEL}${lab}${line}`);
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
if (r.tracking === false) {
|
|
423
|
+
console.log(`${LABEL}${chalk.gray('no upstream')}`);
|
|
424
|
+
}
|
|
425
|
+
else if (r.pull || r.push) {
|
|
426
|
+
if (r.pull)
|
|
427
|
+
detail('↓ pull', r.pull, chalk.yellow);
|
|
428
|
+
if (r.push)
|
|
429
|
+
detail('↑ push', r.push, chalk.magenta);
|
|
430
|
+
}
|
|
431
|
+
const local = r.clean ? chalk.green('clean') : wrapPhrases(deltaPhrases(r.local), detailWidth);
|
|
432
|
+
if (Array.isArray(local)) {
|
|
433
|
+
local.forEach((line, i) => console.log(`${LABEL}${chalk.gray((i === 0 ? 'local' : '').padEnd(9))}${line}`));
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.log(`${LABEL}${chalk.gray('local'.padEnd(9))}${local}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
257
440
|
/**
|
|
258
441
|
* Shared action body for `agents repo list` and the hidden `agents repo status`
|
|
259
|
-
* alias.
|
|
260
|
-
*
|
|
442
|
+
* alias. Responsive: a wide terminal gets an aligned table with compact SYNC /
|
|
443
|
+
* CHANGES cells; a narrow one gets one card per repo with wrapping detail;
|
|
444
|
+
* `--verbose` forces the full-detail table at any width.
|
|
261
445
|
*/
|
|
262
|
-
async function listRepos(alias) {
|
|
446
|
+
async function listRepos(alias, opts = {}) {
|
|
263
447
|
const targets = collectRepoTargets(alias);
|
|
264
448
|
if (!targets) {
|
|
265
449
|
process.exitCode = 1;
|
|
@@ -270,25 +454,17 @@ async function listRepos(alias) {
|
|
|
270
454
|
return;
|
|
271
455
|
}
|
|
272
456
|
const rows = await Promise.all(targets.map(renderRepoRow));
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// a lot in length), so the table stays aligned without truncating detail.
|
|
276
|
-
const headers = ['REPO', 'BRANCH', 'SYNC', 'CHANGES'];
|
|
277
|
-
const aliasW = Math.max(headers[0].length, ...rows.map((r) => r.alias.length));
|
|
278
|
-
const branchW = Math.max(headers[1].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[0])));
|
|
279
|
-
const syncW = Math.max(headers[2].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[1])));
|
|
280
|
-
const changesW = Math.max(headers[3].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[2])));
|
|
457
|
+
// TTY width when interactive; fall back to $COLUMNS (honored when piped) then 80.
|
|
458
|
+
const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
|
|
281
459
|
console.log('');
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
console.log(` ${aliasCol} ${r.raw}`);
|
|
291
|
-
}
|
|
460
|
+
if (opts.verbose) {
|
|
461
|
+
renderTable(rows, false);
|
|
462
|
+
}
|
|
463
|
+
else if (cols >= WIDE_COLS) {
|
|
464
|
+
renderTable(rows, true);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
renderCards(rows, cols);
|
|
292
468
|
}
|
|
293
469
|
const userDir = getUserAgentsDir();
|
|
294
470
|
if (!isGitRepo(userDir) && fs.existsSync(userDir)) {
|
|
@@ -495,8 +671,9 @@ export function registerRepoCommands(program) {
|
|
|
495
671
|
.command('list [alias]')
|
|
496
672
|
.alias('ls')
|
|
497
673
|
.description('Show all repos with resource-level sync (skills/commands/plugins to pull or push) and local changes.')
|
|
498
|
-
.
|
|
499
|
-
|
|
674
|
+
.option('-v, --verbose', 'Full per-resource detail in a table, regardless of terminal width')
|
|
675
|
+
.action(async (alias, options) => {
|
|
676
|
+
await listRepos(alias, options);
|
|
500
677
|
});
|
|
501
678
|
repoCmd
|
|
502
679
|
.command('view [name]')
|
|
@@ -743,8 +920,9 @@ export function registerRepoCommands(program) {
|
|
|
743
920
|
repoCmd
|
|
744
921
|
.command('status [alias]', { hidden: true })
|
|
745
922
|
.description('Alias of `list` (kept for muscle memory).')
|
|
746
|
-
.
|
|
747
|
-
|
|
923
|
+
.option('-v, --verbose', 'Full per-resource detail in a table, regardless of terminal width')
|
|
924
|
+
.action(async (alias, options) => {
|
|
925
|
+
await listRepos(alias, options);
|
|
748
926
|
});
|
|
749
927
|
}
|
|
750
928
|
/**
|
|
@@ -36,5 +36,25 @@ export interface ResourceViewOptions {
|
|
|
36
36
|
}
|
|
37
37
|
/** Display a resource list: interactive picker in TTY mode, plain table otherwise. */
|
|
38
38
|
export declare function showResourceList(opts: ResourceViewOptions): Promise<void>;
|
|
39
|
+
export interface ResourceLayout {
|
|
40
|
+
mode: 'table' | 'cards';
|
|
41
|
+
nameW: number;
|
|
42
|
+
extraW: number;
|
|
43
|
+
extra2W: number;
|
|
44
|
+
descW: number;
|
|
45
|
+
syncW: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Pure column arithmetic. Decides table vs. cards for the effective terminal
|
|
49
|
+
* width and sizes the flexible Description column from whatever is left after the
|
|
50
|
+
* fixed columns and a capped Sync column — so the table fits `cols` instead of
|
|
51
|
+
* the old fixed 22+10+16+42+∞ layout that overflowed every narrow terminal.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resourceLayout(cols: number, o: {
|
|
54
|
+
hasExtra: boolean;
|
|
55
|
+
hasExtra2: boolean;
|
|
56
|
+
nameW: number;
|
|
57
|
+
syncW: number;
|
|
58
|
+
}): ResourceLayout;
|
|
39
59
|
/** Build the sync targets section showing version pills grouped by agent. */
|
|
40
60
|
export declare function buildTargetsSection(targets: SyncTarget[]): string;
|
|
@@ -10,6 +10,7 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { agentLabel } from '../lib/agents.js';
|
|
11
11
|
import { itemPicker } from '../lib/picker.js';
|
|
12
12
|
import { isInteractiveTerminal, isPromptCancelled, printWithPager } from './utils.js';
|
|
13
|
+
import { terminalWidth, truncateToWidth, padToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
|
|
13
14
|
/** Display a resource list: interactive picker in TTY mode, plain table otherwise. */
|
|
14
15
|
export async function showResourceList(opts) {
|
|
15
16
|
if (opts.rows.length === 0) {
|
|
@@ -76,23 +77,49 @@ function formatPickerRow(row, opts) {
|
|
|
76
77
|
const sync = formatSyncSummary(row.targets, opts);
|
|
77
78
|
return `${name} ${extra}${extra2}${desc} ${sync}`;
|
|
78
79
|
}
|
|
79
|
-
/**
|
|
80
|
+
/** Max width for the Name column in the wide table. */
|
|
81
|
+
const NAME_CAP = 22;
|
|
82
|
+
/** Below this many columns of description budget, the list stacks into cards. */
|
|
83
|
+
const MIN_DESC_W = 24;
|
|
84
|
+
/**
|
|
85
|
+
* Pure column arithmetic. Decides table vs. cards for the effective terminal
|
|
86
|
+
* width and sizes the flexible Description column from whatever is left after the
|
|
87
|
+
* fixed columns and a capped Sync column — so the table fits `cols` instead of
|
|
88
|
+
* the old fixed 22+10+16+42+∞ layout that overflowed every narrow terminal.
|
|
89
|
+
*/
|
|
90
|
+
export function resourceLayout(cols, o) {
|
|
91
|
+
const extraW = o.hasExtra ? 10 : 0;
|
|
92
|
+
const extra2W = o.hasExtra2 ? 16 : 0;
|
|
93
|
+
// Cap Sync so a long "missing on …" tail can't starve the description.
|
|
94
|
+
const syncW = Math.min(o.syncW, Math.max(14, Math.floor(cols * 0.32)));
|
|
95
|
+
const fixed = o.nameW + 1 + (extraW ? extraW + 1 : 0) + (extra2W ? extra2W + 1 : 0) + 1 + syncW;
|
|
96
|
+
const descW = cols - fixed;
|
|
97
|
+
return {
|
|
98
|
+
mode: descW >= MIN_DESC_W ? 'table' : 'cards',
|
|
99
|
+
nameW: o.nameW,
|
|
100
|
+
extraW,
|
|
101
|
+
extra2W,
|
|
102
|
+
descW,
|
|
103
|
+
syncW,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Render resources (used when output is piped). Responsive: wide table or cards. */
|
|
80
107
|
function printResourceTable(opts) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const cols = terminalWidth();
|
|
109
|
+
const syncStrings = opts.rows.map((r) => formatSyncSummary(r.targets, opts));
|
|
110
|
+
const nameW = Math.min(NAME_CAP, Math.max('Name'.length, ...opts.rows.map((r) => stringWidth(r.name))));
|
|
111
|
+
const syncMax = Math.max('Synced'.length, ...syncStrings.map((s) => stringWidth(s)));
|
|
112
|
+
const layout = resourceLayout(cols, {
|
|
113
|
+
hasExtra: Boolean(opts.extraLabel),
|
|
114
|
+
hasExtra2: Boolean(opts.extra2Label),
|
|
115
|
+
nameW,
|
|
116
|
+
syncW: syncMax,
|
|
117
|
+
});
|
|
118
|
+
if (layout.mode === 'cards') {
|
|
119
|
+
renderResourceCards(opts, cols, syncStrings);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
renderResourceWideTable(opts, layout, syncStrings);
|
|
96
123
|
}
|
|
97
124
|
console.log();
|
|
98
125
|
const summary = [
|
|
@@ -103,18 +130,53 @@ function printResourceTable(opts) {
|
|
|
103
130
|
}
|
|
104
131
|
console.log(chalk.gray(summary.join(' · ')));
|
|
105
132
|
}
|
|
106
|
-
/**
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
?
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
/** Wide aligned table: Name · [Extra] · [Extra2] · Description (flex) · Synced. */
|
|
134
|
+
function renderResourceWideTable(opts, L, syncStrings) {
|
|
135
|
+
const cell = (text, width, color) => {
|
|
136
|
+
const clipped = truncateToWidth(text, width);
|
|
137
|
+
return padToWidth(color ? color(clipped) : clipped, width);
|
|
138
|
+
};
|
|
139
|
+
// Pass the colour as the third arg: cell() truncates the plain text first, then
|
|
140
|
+
// colours the result. Pre-colouring here would be stripped by truncateToWidth.
|
|
141
|
+
const headerParts = [cell('Name', L.nameW, chalk.bold)];
|
|
142
|
+
if (L.extraW)
|
|
143
|
+
headerParts.push(cell(opts.extraLabel ?? '', L.extraW, chalk.bold));
|
|
144
|
+
if (L.extra2W)
|
|
145
|
+
headerParts.push(cell(opts.extra2Label ?? '', L.extra2W, chalk.bold));
|
|
146
|
+
headerParts.push(cell('Description', L.descW, chalk.bold));
|
|
147
|
+
headerParts.push(chalk.bold('Synced'));
|
|
148
|
+
console.log(headerParts.join(' '));
|
|
149
|
+
const contentW = L.nameW + 1 + (L.extraW ? L.extraW + 1 : 0) + (L.extra2W ? L.extra2W + 1 : 0) + L.descW + 1 + L.syncW;
|
|
150
|
+
console.log(chalk.gray('─'.repeat(Math.min(contentW, terminalWidth()))));
|
|
151
|
+
opts.rows.forEach((row, i) => {
|
|
152
|
+
const parts = [cell(row.name, L.nameW, chalk.cyan)];
|
|
153
|
+
if (L.extraW)
|
|
154
|
+
parts.push(cell(row.extra ?? '-', L.extraW));
|
|
155
|
+
if (L.extra2W)
|
|
156
|
+
parts.push(cell(row.extra2 ?? '-', L.extra2W));
|
|
157
|
+
parts.push(cell(row.description ?? '-', L.descW, chalk.gray));
|
|
158
|
+
let sync = syncStrings[i] ?? '';
|
|
159
|
+
if (stringWidth(sync) > L.syncW)
|
|
160
|
+
sync = chalk.gray(truncateToWidth(sync, L.syncW));
|
|
161
|
+
parts.push(sync);
|
|
162
|
+
console.log(parts.join(' '));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Stacked cards for narrow terminals: name + meta on one line, description below. */
|
|
166
|
+
function renderResourceCards(opts, cols, syncStrings) {
|
|
167
|
+
opts.rows.forEach((row, i) => {
|
|
168
|
+
const meta = [row.extra, row.extra2, stripAnsi(syncStrings[i] ?? '')]
|
|
169
|
+
.filter((s) => Boolean(s && s.trim()))
|
|
170
|
+
.join(' · ');
|
|
171
|
+
const metaBudget = Math.max(8, cols - stringWidth(row.name) - 2);
|
|
172
|
+
const line1 = meta
|
|
173
|
+
? `${chalk.cyan(row.name)} ${chalk.gray(truncateToWidth(meta, metaBudget))}`
|
|
174
|
+
: chalk.cyan(row.name);
|
|
175
|
+
console.log(line1);
|
|
176
|
+
if (row.description) {
|
|
177
|
+
console.log(` ${chalk.gray(truncateToWidth(row.description, cols - 4))}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
118
180
|
}
|
|
119
181
|
/** Compact sync summary: "everywhere", "14 of 16 installs", or "not installed". */
|
|
120
182
|
function formatSyncSummary(targets, opts) {
|