@phnx-labs/agents-cli 1.20.34 → 1.20.36

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.
Files changed (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -106,13 +106,12 @@ export function resourceUnit(file) {
106
106
  return { kind: 'other', unit: file };
107
107
  }
108
108
  /**
109
- * Render a set of changed files as a resource-level summary, e.g.
110
- * `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
111
- * three files all changed is "1 changed skill"), grouped by action then kind,
112
- * and colors each phrase green/yellow/red for new/changed/removed. Caps at
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 formatResourceDelta(entries, maxParts = 5) {
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 COLOR = {
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 (!n)
147
- continue;
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
- * Render one repo's row data: branch, resource-level sync (what a pull/push would
193
- * move), resource-level local edits, and the remote URL + commit. Used by
194
- * `agents repo list` and the hidden `agents repo status` alias.
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
- let sync;
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
- const incoming = formatResourceDelta(await diffResourceEntries(git, 'HEAD...@{upstream}'));
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
- const outgoing = formatResourceDelta(await diffResourceEntries(git, '@{upstream}...HEAD'));
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
- const changes = status.isClean() ? chalk.green('clean') : formatResourceDelta(localEntries);
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
- const url = origin?.refs?.fetch || '';
245
- const commit = (await git.log({ maxCount: 1 })).latest?.hash.slice(0, 8) || '';
246
- const remote = url
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. Prints an aligned table: repo, branch, resource-level sync (to pull /
260
- * to push), resource-level local changes, and remote URL + short commit.
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
- const tableRows = rows.filter((r) => r.cells);
274
- // Column widths grow to fit the widest visible content (resource summaries vary
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
- 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')}`);
283
- for (const r of rows) {
284
- const aliasCol = chalk.cyan(r.alias.padEnd(aliasW));
285
- if (r.cells) {
286
- const [branch, sync, changes, remote] = r.cells;
287
- console.log(` ${aliasCol} ${padVisible(branch, branchW)} ${padVisible(sync, syncW)} ${padVisible(changes, changesW)} ${remote}`);
288
- }
289
- else {
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
- .action(async (alias) => {
499
- await listRepos(alias);
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
- .action(async (alias) => {
747
- await listRepos(alias);
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
- /** Render resources as a plain-text table (used when output is piped). */
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 header = buildTableHeader(opts);
82
- console.log(header);
83
- console.log(chalk.gray(''.repeat(Math.min(process.stdout.columns || 100, 120))));
84
- for (const row of opts.rows) {
85
- const name = chalk.cyan(padRight(row.name, 22));
86
- const extra = opts.extraLabel
87
- ? padRight(row.extra ?? '-', 10)
88
- : '';
89
- const extra2 = opts.extra2Label
90
- ? padRight(row.extra2 ?? '-', 16)
91
- : '';
92
- const descRaw = row.description ? truncate(row.description, 40) : '-';
93
- const desc = padRight(chalk.gray(descRaw), 42);
94
- const sync = formatSyncSummary(row.targets, opts);
95
- console.log(`${name} ${extra}${extra2}${desc} ${sync}`);
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
- /** Build the column header line for the plain-text table. */
107
- function buildTableHeader(opts) {
108
- const name = chalk.bold(padRight('Name', 22));
109
- const extra = opts.extraLabel
110
- ? chalk.bold(padRight(opts.extraLabel, 10))
111
- : '';
112
- const extra2 = opts.extra2Label
113
- ? chalk.bold(padRight(opts.extra2Label, 16))
114
- : '';
115
- const desc = padRight(chalk.bold('Description'), 42);
116
- const sync = chalk.bold('Synced');
117
- return `${name} ${extra}${extra2}${desc} ${sync}`;
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) {