@phnx-labs/agents-cli 1.20.71 → 1.20.73

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 (238) hide show
  1. package/CHANGELOG.md +325 -1
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/commands.js +2 -0
  11. package/dist/commands/doctor.js +8 -4
  12. package/dist/commands/events.d.ts +11 -10
  13. package/dist/commands/events.js +30 -21
  14. package/dist/commands/exec.d.ts +34 -0
  15. package/dist/commands/exec.js +337 -39
  16. package/dist/commands/feed.d.ts +13 -0
  17. package/dist/commands/feed.js +130 -29
  18. package/dist/commands/hq.d.ts +2 -0
  19. package/dist/commands/hq.js +58 -0
  20. package/dist/commands/lease.d.ts +19 -0
  21. package/dist/commands/lease.js +138 -11
  22. package/dist/commands/login.d.ts +2 -0
  23. package/dist/commands/login.js +123 -0
  24. package/dist/commands/logs.js +39 -10
  25. package/dist/commands/mcp.js +2 -0
  26. package/dist/commands/mine.d.ts +27 -0
  27. package/dist/commands/mine.js +207 -0
  28. package/dist/commands/monitors.js +79 -49
  29. package/dist/commands/plugins.js +1 -0
  30. package/dist/commands/profiles.js +158 -0
  31. package/dist/commands/repo.js +2 -60
  32. package/dist/commands/resource-view.d.ts +2 -0
  33. package/dist/commands/resource-view.js +8 -0
  34. package/dist/commands/resources.d.ts +5 -0
  35. package/dist/commands/resources.js +95 -0
  36. package/dist/commands/routines.d.ts +2 -0
  37. package/dist/commands/routines.js +139 -37
  38. package/dist/commands/secrets.js +234 -47
  39. package/dist/commands/send.d.ts +15 -0
  40. package/dist/commands/send.js +62 -0
  41. package/dist/commands/sessions-tail.d.ts +1 -0
  42. package/dist/commands/sessions-tail.js +20 -9
  43. package/dist/commands/sessions.d.ts +6 -0
  44. package/dist/commands/sessions.js +26 -3
  45. package/dist/commands/setup-mine.d.ts +18 -0
  46. package/dist/commands/setup-mine.js +106 -0
  47. package/dist/commands/setup-share.js +2 -2
  48. package/dist/commands/setup.js +3 -1
  49. package/dist/commands/share.d.ts +8 -1
  50. package/dist/commands/share.js +122 -37
  51. package/dist/commands/skills.js +2 -0
  52. package/dist/commands/ssh.d.ts +9 -0
  53. package/dist/commands/ssh.js +196 -26
  54. package/dist/commands/status.js +8 -2
  55. package/dist/commands/subagents.js +3 -1
  56. package/dist/commands/sync.js +1 -1
  57. package/dist/commands/teams.js +11 -2
  58. package/dist/commands/uninstall.d.ts +11 -0
  59. package/dist/commands/uninstall.js +158 -0
  60. package/dist/commands/utils.d.ts +29 -0
  61. package/dist/commands/utils.js +24 -0
  62. package/dist/commands/versions.js +120 -11
  63. package/dist/index.js +83 -12
  64. package/dist/lib/activity.d.ts +124 -0
  65. package/dist/lib/activity.js +542 -0
  66. package/dist/lib/agents.js +16 -5
  67. package/dist/lib/artifact-actions.d.ts +1 -1
  68. package/dist/lib/artifact-actions.js +1 -1
  69. package/dist/lib/ask-classifier.d.ts +2 -1
  70. package/dist/lib/ask-classifier.js +3 -3
  71. package/dist/lib/auth-health.d.ts +32 -4
  72. package/dist/lib/auth-health.js +40 -1
  73. package/dist/lib/auto-pull.js +8 -1
  74. package/dist/lib/brand.d.ts +40 -0
  75. package/dist/lib/brand.js +122 -0
  76. package/dist/lib/browser/drivers/ssh.js +4 -7
  77. package/dist/lib/browser/service.js +6 -8
  78. package/dist/lib/channels/providers/index.d.ts +2 -0
  79. package/dist/lib/channels/providers/index.js +20 -0
  80. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  81. package/dist/lib/channels/providers/mailbox.js +26 -0
  82. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  83. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  84. package/dist/lib/channels/providers/rush.d.ts +6 -0
  85. package/dist/lib/channels/providers/rush.js +55 -0
  86. package/dist/lib/channels/registry.d.ts +42 -0
  87. package/dist/lib/channels/registry.js +20 -0
  88. package/dist/lib/channels/resolve.d.ts +11 -0
  89. package/dist/lib/channels/resolve.js +12 -0
  90. package/dist/lib/cli-resources.d.ts +18 -0
  91. package/dist/lib/cli-resources.js +53 -4
  92. package/dist/lib/cloud/codex.js +6 -3
  93. package/dist/lib/cloud/factory.d.ts +1 -0
  94. package/dist/lib/cloud/factory.js +10 -4
  95. package/dist/lib/cloud/rush.d.ts +3 -1
  96. package/dist/lib/cloud/rush.js +6 -1
  97. package/dist/lib/cloud/types.d.ts +2 -0
  98. package/dist/lib/codex-home.d.ts +35 -0
  99. package/dist/lib/codex-home.js +136 -0
  100. package/dist/lib/crabbox/cli.d.ts +44 -0
  101. package/dist/lib/crabbox/cli.js +120 -20
  102. package/dist/lib/crabbox/lease.d.ts +52 -1
  103. package/dist/lib/crabbox/lease.js +117 -16
  104. package/dist/lib/crabbox/progress.d.ts +31 -0
  105. package/dist/lib/crabbox/progress.js +76 -0
  106. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  107. package/dist/lib/crabbox/runtimes.js +33 -8
  108. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  109. package/dist/lib/crabbox/setup-copy.js +127 -0
  110. package/dist/lib/daemon.d.ts +14 -5
  111. package/dist/lib/daemon.js +52 -9
  112. package/dist/lib/drive-sync.js +6 -3
  113. package/dist/lib/event-stream.d.ts +36 -0
  114. package/dist/lib/event-stream.js +68 -0
  115. package/dist/lib/events.d.ts +1 -1
  116. package/dist/lib/exec.d.ts +11 -0
  117. package/dist/lib/exec.js +23 -2
  118. package/dist/lib/feed-policy.d.ts +1 -1
  119. package/dist/lib/feed-policy.js +14 -9
  120. package/dist/lib/feed-ranking.d.ts +32 -0
  121. package/dist/lib/feed-ranking.js +224 -0
  122. package/dist/lib/feed.d.ts +35 -2
  123. package/dist/lib/feed.js +66 -3
  124. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  125. package/dist/lib/fleet/auth-sync.js +92 -0
  126. package/dist/lib/fleet/remote-login.d.ts +187 -0
  127. package/dist/lib/fleet/remote-login.js +556 -0
  128. package/dist/lib/format.d.ts +30 -3
  129. package/dist/lib/format.js +34 -5
  130. package/dist/lib/fs-atomic.d.ts +7 -2
  131. package/dist/lib/fs-atomic.js +8 -12
  132. package/dist/lib/git.d.ts +16 -0
  133. package/dist/lib/git.js +79 -2
  134. package/dist/lib/heal.js +11 -3
  135. package/dist/lib/hosts/logs.d.ts +12 -0
  136. package/dist/lib/hosts/logs.js +18 -0
  137. package/dist/lib/hosts/progress.d.ts +28 -10
  138. package/dist/lib/hosts/progress.js +79 -22
  139. package/dist/lib/hosts/remote-cmd.js +4 -0
  140. package/dist/lib/hq/floor.d.ts +87 -0
  141. package/dist/lib/hq/floor.js +226 -0
  142. package/dist/lib/menubar/install-menubar.js +14 -20
  143. package/dist/lib/notify.d.ts +1 -0
  144. package/dist/lib/notify.js +3 -3
  145. package/dist/lib/open-url.d.ts +2 -0
  146. package/dist/lib/open-url.js +19 -0
  147. package/dist/lib/openclaw-keychain.d.ts +56 -0
  148. package/dist/lib/openclaw-keychain.js +236 -0
  149. package/dist/lib/overdue.js +10 -0
  150. package/dist/lib/permissions.d.ts +44 -1
  151. package/dist/lib/permissions.js +284 -7
  152. package/dist/lib/project-launch.d.ts +6 -12
  153. package/dist/lib/project-launch.js +13 -228
  154. package/dist/lib/project-resources.d.ts +7 -0
  155. package/dist/lib/project-resources.js +291 -0
  156. package/dist/lib/pty-client.d.ts +27 -0
  157. package/dist/lib/pty-client.js +136 -10
  158. package/dist/lib/refresh.js +14 -10
  159. package/dist/lib/resource-profiles.d.ts +26 -0
  160. package/dist/lib/resource-profiles.js +157 -0
  161. package/dist/lib/resources/permissions.js +7 -1
  162. package/dist/lib/resources/types.d.ts +1 -1
  163. package/dist/lib/resources.d.ts +1 -1
  164. package/dist/lib/resources.js +32 -3
  165. package/dist/lib/routines.d.ts +11 -0
  166. package/dist/lib/routines.js +56 -15
  167. package/dist/lib/runner.d.ts +1 -0
  168. package/dist/lib/runner.js +66 -5
  169. package/dist/lib/secrets/bundles.d.ts +16 -3
  170. package/dist/lib/secrets/bundles.js +206 -37
  171. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  172. package/dist/lib/secrets/icloud-import.js +9 -6
  173. package/dist/lib/secrets/mcp.js +1 -1
  174. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  175. package/dist/lib/secrets/vault-age-helper.js +34 -0
  176. package/dist/lib/secrets/vault.d.ts +49 -0
  177. package/dist/lib/secrets/vault.js +397 -0
  178. package/dist/lib/self-heal/checks/path.js +3 -1
  179. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  180. package/dist/lib/self-heal/checks/shims.js +19 -11
  181. package/dist/lib/session/cloud.d.ts +2 -2
  182. package/dist/lib/session/cloud.js +2 -2
  183. package/dist/lib/session/db.d.ts +4 -0
  184. package/dist/lib/session/db.js +44 -7
  185. package/dist/lib/session/discover.d.ts +2 -0
  186. package/dist/lib/session/discover.js +114 -3
  187. package/dist/lib/session/stream-render.d.ts +3 -0
  188. package/dist/lib/session/stream-render.js +130 -0
  189. package/dist/lib/session/types.d.ts +6 -0
  190. package/dist/lib/share/analytics.d.ts +13 -0
  191. package/dist/lib/share/analytics.js +45 -0
  192. package/dist/lib/share/config.d.ts +19 -5
  193. package/dist/lib/share/config.js +44 -8
  194. package/dist/lib/share/provision.d.ts +58 -6
  195. package/dist/lib/share/provision.js +97 -22
  196. package/dist/lib/share/publish.d.ts +36 -13
  197. package/dist/lib/share/publish.js +55 -8
  198. package/dist/lib/share/worker-template.js +83 -17
  199. package/dist/lib/shims.d.ts +36 -0
  200. package/dist/lib/shims.js +120 -16
  201. package/dist/lib/ssh-exec.d.ts +14 -0
  202. package/dist/lib/ssh-exec.js +57 -0
  203. package/dist/lib/staleness/detectors/hooks.js +25 -1
  204. package/dist/lib/staleness/detectors/permissions.js +66 -0
  205. package/dist/lib/staleness/detectors/workflows.js +20 -0
  206. package/dist/lib/staleness/writers/hooks.js +58 -4
  207. package/dist/lib/staleness/writers/permissions.js +2 -2
  208. package/dist/lib/staleness/writers/skills.js +1 -1
  209. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  210. package/dist/lib/staleness/writers/sources.js +68 -1
  211. package/dist/lib/star-nudge.d.ts +45 -0
  212. package/dist/lib/star-nudge.js +91 -0
  213. package/dist/lib/startup/command-registry.d.ts +7 -1
  214. package/dist/lib/startup/command-registry.js +19 -3
  215. package/dist/lib/state.d.ts +2 -0
  216. package/dist/lib/state.js +3 -0
  217. package/dist/lib/subagents-registry.d.ts +2 -0
  218. package/dist/lib/subagents-registry.js +3 -0
  219. package/dist/lib/teams/parsers.js +214 -6
  220. package/dist/lib/teams/supervisor.d.ts +7 -0
  221. package/dist/lib/teams/supervisor.js +2 -1
  222. package/dist/lib/template.d.ts +1 -1
  223. package/dist/lib/template.js +1 -1
  224. package/dist/lib/triggers/webhook.js +36 -3
  225. package/dist/lib/types.d.ts +73 -0
  226. package/dist/lib/uninstall.d.ts +84 -0
  227. package/dist/lib/uninstall.js +347 -0
  228. package/dist/lib/usage.d.ts +13 -1
  229. package/dist/lib/usage.js +32 -14
  230. package/dist/lib/version.d.ts +40 -0
  231. package/dist/lib/version.js +94 -16
  232. package/dist/lib/versions.d.ts +27 -0
  233. package/dist/lib/versions.js +248 -79
  234. package/dist/lib/workflows.d.ts +2 -0
  235. package/dist/lib/workflows.js +88 -0
  236. package/package.json +2 -1
  237. package/dist/commands/daemon.d.ts +0 -10
  238. package/dist/commands/daemon.js +0 -121
@@ -45,6 +45,7 @@ When to use:
45
45
  .command('list [agent]')
46
46
  .description('Show which skills are installed and which agent versions they are synced to')
47
47
  .option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
48
+ .option('--json', 'Emit machine-readable JSON instead of the table/picker')
48
49
  .action(async (agentArg, options) => {
49
50
  const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
50
51
  const agentInput = agentArg || options.agent;
@@ -74,6 +75,7 @@ When to use:
74
75
  centralPath: getSkillsDir(),
75
76
  filterAgent,
76
77
  filterVersion,
78
+ json: options.json,
77
79
  });
78
80
  });
79
81
  skillsCmd
@@ -10,5 +10,14 @@
10
10
  * rsync / `agents sessions --host` resolve the same logical names.
11
11
  */
12
12
  import type { Command } from 'commander';
13
+ import { type CrabboxBox } from '../lib/crabbox/cli.js';
14
+ /**
15
+ * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
16
+ * from `crabboxList()` — these are ephemeral crabbox leases, NEVER written into
17
+ * the device registry. Returns [] when crabbox is unavailable / has no creds /
18
+ * reports no boxes, so the section is simply omitted. `nowSecs` is injected so
19
+ * the row formatting is deterministic in tests.
20
+ */
21
+ export declare function renderLeasedBoxesSection(boxes: CrabboxBox[], nowSecs: number): string[];
13
22
  /** Register both `agents ssh` and `agents devices`. */
14
23
  export declare function registerSshCommands(program: Command): void;
@@ -19,6 +19,7 @@ import { getCliVersion } from '../lib/version.js';
19
19
  import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
21
  import { registerFleetCaptureCommand } from './fleet-capture.js';
22
+ import { registerFleetApplyAlias } from './apply.js';
22
23
  import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
23
24
  import { addControlToken } from '../lib/serve/token.js';
24
25
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
@@ -40,7 +41,10 @@ import { checkAllClis } from '../lib/teams/agents.js';
40
41
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
41
42
  import { sshExecAsync } from '../lib/ssh-exec.js';
42
43
  import { ALL_AGENT_IDS } from '../lib/agents.js';
43
- import { formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
44
+ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
45
+ import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
46
+ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
47
+ import { runFleetLogin } from '../lib/fleet/remote-login.js';
44
48
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
45
49
  * command is running on so it stands out from the rest of the tailnet. */
46
50
  function deviceSummary(d, isSelf = false) {
@@ -130,6 +134,74 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
130
134
  }
131
135
  return lines;
132
136
  }
137
+ /**
138
+ * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
139
+ * from `crabboxList()` — these are ephemeral crabbox leases, NEVER written into
140
+ * the device registry. Returns [] when crabbox is unavailable / has no creds /
141
+ * reports no boxes, so the section is simply omitted. `nowSecs` is injected so
142
+ * the row formatting is deterministic in tests.
143
+ */
144
+ export function renderLeasedBoxesSection(boxes, nowSecs) {
145
+ if (boxes.length === 0)
146
+ return [];
147
+ const lines = [];
148
+ lines.push('');
149
+ lines.push(chalk.bold('Leased boxes') + chalk.gray(' (ephemeral · via crabbox)'));
150
+ lines.push(' ' +
151
+ chalk.gray('box'.padEnd(16)) +
152
+ chalk.gray('class'.padEnd(10)) +
153
+ chalk.gray('address'.padEnd(24)) +
154
+ chalk.gray('status'.padEnd(9)) +
155
+ chalk.gray('idle'.padEnd(12)) +
156
+ chalk.gray('expires'));
157
+ for (const b of boxes) {
158
+ const addr = boxAddress(b) ?? '—';
159
+ lines.push(' ' +
160
+ chalk.cyan(b.slug.padEnd(16)) +
161
+ (b.class ?? '?').padEnd(10) +
162
+ addr.padEnd(24) +
163
+ boxStatus(b).padEnd(9) +
164
+ chalk.gray(fmtIdleShort(b, nowSecs).padEnd(12)) +
165
+ chalk.gray(fmtExpiresShort(b, nowSecs)));
166
+ }
167
+ lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents lease stop <slug>`'));
168
+ return lines;
169
+ }
170
+ /** The leased-box rows for the devices list, or [] when crabbox can't be read. */
171
+ function loadLeasedBoxesSection() {
172
+ try {
173
+ const boxes = crabboxList({ secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
174
+ return renderLeasedBoxesSection(boxes, Math.floor(Date.now() / 1000));
175
+ }
176
+ catch {
177
+ return []; // crabbox not installed / no provider creds — omit the section
178
+ }
179
+ }
180
+ /**
181
+ * `agents ssh <slug>` targeting a leased crabbox box. crabbox provisions a
182
+ * per-lease identity key, so we ssh via crabbox's OWN emitted invocation
183
+ * (`crabboxSshArgv`) — a raw `ssh crabbox@ip` fails publickey. Returns false when
184
+ * `name` is not a known crabbox slug so the caller can fall through to the normal
185
+ * "Unknown device" error.
186
+ */
187
+ function trySshLeasedBox(name, cmd) {
188
+ let box;
189
+ try {
190
+ box = crabboxFind(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 5000 });
191
+ }
192
+ catch {
193
+ return false; // crabbox unavailable — not a leased-box target
194
+ }
195
+ if (!box)
196
+ return false;
197
+ const sshArgv = crabboxSshArgv(name, { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE, timeoutMs: 8000 });
198
+ if (!sshArgv) {
199
+ console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
200
+ process.exit(1);
201
+ }
202
+ const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...cmd], { stdio: 'inherit' });
203
+ process.exit(res.status ?? 1);
204
+ }
133
205
  /** Resolve a device or exit with a clear error. */
134
206
  async function mustGetDevice(name) {
135
207
  const d = await getDevice(name);
@@ -423,17 +495,28 @@ async function runFleetPing(opts) {
423
495
  if (opts.strict && anyBad)
424
496
  process.exitCode = 1;
425
497
  }
426
- /** Color a per-host×agent cell: green all-live, red any revoked/expired, yellow degraded. */
498
+ /** Resolve an {@link AuthCellColor} to a chalk painter. Single map for cells + labels. */
499
+ const CELL_PAINT = {
500
+ green: chalk.green,
501
+ yellow: chalk.yellow,
502
+ red: chalk.red,
503
+ gray: chalk.gray,
504
+ dim: chalk.dim,
505
+ };
506
+ /**
507
+ * Color a per-host×agent cell. The numerator counts accounts that are usable
508
+ * right now — live-verified PLUS signed-in-but-unverifiable (codex/grok) — over
509
+ * the total, so a logged-in codex fleet reads "1/1", not a scary "0/1". Color is
510
+ * the shared {@link authCellColor}: red only for revoked (re-login), yellow for
511
+ * soft/expired (self-refreshes), gray for present-but-unverifiable, green when
512
+ * all live.
513
+ */
427
514
  function authCell(summary, width) {
428
515
  if (summary.total === 0)
429
516
  return chalk.dim('·'.padEnd(width));
430
- const text = `${summary.live}/${summary.total}`;
431
- const padded = text.padEnd(width);
432
- if (summary.bad > 0)
433
- return chalk.red(padded);
434
- if (summary.warn > 0)
435
- return chalk.yellow(padded);
436
- return chalk.green(padded);
517
+ const ok = summary.live + summary.present;
518
+ const padded = `${ok}/${summary.total}`.padEnd(width);
519
+ return CELL_PAINT[authCellColor(summary)](padded);
437
520
  }
438
521
  /** Render the fleet auth matrix (device rows × agent columns) plus an optional per-account breakdown. */
439
522
  function renderAuthMatrix(results, opts) {
@@ -466,7 +549,7 @@ function renderAuthMatrix(results, opts) {
466
549
  lines.push(` ${r.host.padEnd(nameW)} ${cells.join(' ')}${note}`);
467
550
  }
468
551
  lines.push('');
469
- lines.push(chalk.gray(' cell = live/total accounts · green all live · red revoked (re-login) · yellow expired/unverified/limited'));
552
+ lines.push(chalk.gray(' cell = signed-in/total accounts · green live · gray signed-in (unverifiable: codex/grok) · yellow expired (self-refreshes) · red revoked (re-login)'));
470
553
  if (opts?.verbose) {
471
554
  lines.push('');
472
555
  lines.push(chalk.bold('Accounts'));
@@ -475,9 +558,7 @@ function renderAuthMatrix(results, opts) {
475
558
  continue;
476
559
  for (const row of r.rows.slice().sort((x, y) => (x.agent + x.version).localeCompare(y.agent + y.version))) {
477
560
  const v = row.health.verdict;
478
- const label = v === 'live' ? chalk.green(verdictLabel(v))
479
- : (v === 'revoked' || v === 'expired') ? chalk.red(verdictLabel(v))
480
- : chalk.yellow(verdictLabel(v));
561
+ const label = CELL_PAINT[verdictColor(v)](verdictLabel(v));
481
562
  const acctRaw = row.account ?? '—';
482
563
  const acct = row.account ? chalk.cyan(acctRaw.padEnd(28)) : chalk.dim(acctRaw.padEnd(28));
483
564
  const detail = row.health.detail ? chalk.dim(` ${row.health.detail}`) : '';
@@ -488,6 +569,46 @@ function renderAuthMatrix(results, opts) {
488
569
  }
489
570
  return lines;
490
571
  }
572
+ /** Render the final `fleet login` result summary in the house auth-matrix style. */
573
+ function renderLoginMatrix(results) {
574
+ const lines = [chalk.bold('Fleet login')];
575
+ if (results.length === 0) {
576
+ lines.push(chalk.gray(' Nothing pending — every requested account is already logged in.'));
577
+ return lines;
578
+ }
579
+ const nameW = Math.max(6, ...results.map((r) => `${r.agent}@${r.device}`.length));
580
+ for (const r of results) {
581
+ const who = `${r.agent}@${r.device}`.padEnd(nameW);
582
+ let badge;
583
+ switch (r.state) {
584
+ case 'authorized':
585
+ badge = chalk.green('authorized');
586
+ break;
587
+ case 'ready':
588
+ badge = chalk.cyan('code ready');
589
+ break;
590
+ case 'driving':
591
+ badge = chalk.yellow('driving');
592
+ break;
593
+ case 'skipped':
594
+ badge = chalk.gray('not remotable');
595
+ break;
596
+ case 'error':
597
+ badge = chalk.red('error');
598
+ break;
599
+ default:
600
+ badge = chalk.gray(r.state);
601
+ break;
602
+ }
603
+ const note = r.detail ? chalk.dim(` ${r.detail}`) : (r.reason && !r.remotable ? chalk.dim(` ${r.reason}`) : '');
604
+ lines.push(` ${who} ${badge}${note}`);
605
+ }
606
+ const auth = results.filter((r) => r.state === 'authorized').length;
607
+ const remotable = results.filter((r) => r.remotable).length;
608
+ lines.push('');
609
+ lines.push(chalk.gray(` ${auth}/${remotable} remotable logins authorized`));
610
+ return lines;
611
+ }
491
612
  /** Register the `agents devices` command tree (also aliased as `fleet`). */
492
613
  function registerDevicesCommands(program) {
493
614
  const devicesCmd = program
@@ -529,6 +650,9 @@ Typical workflow:
529
650
  });
530
651
  // `agents fleet capture` — snapshot live state into agents.yaml fleet:.
531
652
  registerFleetCaptureCommand(devicesCmd);
653
+ // `agents fleet apply` — same reconcile engine as the top-level `agents apply`,
654
+ // surfaced under the fleet tree for discoverability.
655
+ registerFleetApplyAlias(devicesCmd);
532
656
  devicesCmd
533
657
  .command('register <name>')
534
658
  .description('Register a discovered (pending) node by name — used by the menu-bar "NEW DEVICES → Register" action.')
@@ -576,16 +700,7 @@ Typical workflow:
576
700
  }
577
701
  console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
578
702
  });
579
- devicesCmd
580
- .command('list')
581
- .alias('ls')
582
- .description('List registered devices with platform, address, reachability, and live resource headroom.')
583
- .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
584
- .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
585
- .option('--refresh', 'force a live probe of every device, bypassing the cache')
586
- .option('--live', 'alias of --refresh (shorter to type)')
587
- .option('-f, --full', 'full mode: add per-device core count and free/total memory')
588
- .action(async (opts) => {
703
+ const runList = async (opts = {}) => {
589
704
  const reg = await loadDevices();
590
705
  const names = Object.keys(reg).sort();
591
706
  if (opts.json) {
@@ -629,7 +744,26 @@ Typical workflow:
629
744
  if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
630
745
  console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
631
746
  }
632
- });
747
+ // Ephemeral crabbox leases live alongside the registered fleet but are never
748
+ // written into the registry — surface them as their own live section. This is
749
+ // a live provider call, so honor --no-stats (the explicit "instant, no probes"
750
+ // opt-out) and bound it so a slow provider can't hang `agents devices`.
751
+ if (opts.stats !== false) {
752
+ for (const line of loadLeasedBoxesSection())
753
+ console.log(line);
754
+ }
755
+ };
756
+ devicesCmd.action(runList);
757
+ devicesCmd
758
+ .command('list')
759
+ .alias('ls')
760
+ .description('List registered devices with platform, address, reachability, and live resource headroom.')
761
+ .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
762
+ .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
763
+ .option('--refresh', 'force a live probe of every device, bypassing the cache')
764
+ .option('--live', 'alias of --refresh (shorter to type)')
765
+ .option('-f, --full', 'full mode: add per-device core count and free/total memory')
766
+ .action(runList);
633
767
  devicesCmd
634
768
  .command('status')
635
769
  .description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
@@ -648,8 +782,41 @@ Typical workflow:
648
782
  .option('--local', 'probe only this host (used internally for fan-out)')
649
783
  .option('--verbose', 'show a per-account breakdown, not just the per-host rollup')
650
784
  .option('--strict', 'exit non-zero when any account is revoked (expired is soft — it self-refreshes)')
785
+ .action(async (opts, cmd) => {
786
+ // The root program also defines a global `--verbose` (startup self-heal
787
+ // detail), and commander binds a shared long flag to the program, not the
788
+ // leaf — so `fleet ping --verbose` never set opts.verbose and the
789
+ // per-account breakdown was silently unreachable. Read the effective value
790
+ // from the merged globals so the flag works at either level.
791
+ const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
792
+ await runFleetPing({ ...opts, verbose });
793
+ });
794
+ devicesCmd
795
+ .command('login')
796
+ .description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
797
+ .option('--agents <csv>', 'only these agents (comma-separated); default: every agent with a device-code flow')
798
+ .option('--devices <csv>', 'only these devices (comma-separated); default: every online box')
799
+ .option('--all', 'target every device-code pair regardless of cached login state (cold cache / forced re-login)')
800
+ .option('--interactive', 'guided one-box-at-a-time wizard (codes requested just-in-time)')
801
+ .option('--json', 'output the final result matrix as JSON')
651
802
  .action(async (opts) => {
652
- await runFleetPing(opts);
803
+ const csv = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
804
+ const results = await runFleetLogin({
805
+ agents: csv(opts.agents),
806
+ devices: csv(opts.devices),
807
+ all: opts.all,
808
+ interactive: opts.interactive,
809
+ json: opts.json,
810
+ });
811
+ if (opts.json) {
812
+ console.log(JSON.stringify(results, null, 2));
813
+ }
814
+ else {
815
+ for (const line of renderLoginMatrix(results))
816
+ console.log(line);
817
+ }
818
+ if (results.some((r) => r.remotable && r.state !== 'authorized'))
819
+ process.exitCode = 1;
653
820
  });
654
821
  devicesCmd
655
822
  .command('show <name>')
@@ -849,6 +1016,9 @@ secrets bundle via an askpass shim — the password never touches argv.
849
1016
  // A bare unregistered alias still errors as "Unknown device".
850
1017
  const device = resolveDeviceTarget(name, await loadDevices());
851
1018
  if (!device) {
1019
+ // Not a registered device — it may be a leased crabbox box slug. ssh into
1020
+ // it directly (crabbox@<tailnet|ip>:2222) before giving up.
1021
+ trySshLeasedBox(name, cmd); // exits the process on a match
852
1022
  console.error(chalk.red(`Unknown device '${name}'. See 'agents devices list'.`));
853
1023
  process.exit(1);
854
1024
  }
@@ -907,7 +1077,7 @@ async function runAskpass() {
907
1077
  process.exit(1);
908
1078
  }
909
1079
  try {
910
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
1080
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
911
1081
  const value = env[key];
912
1082
  if (value === undefined) {
913
1083
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
13
13
  import { setHelpSections } from '../lib/help.js';
14
14
  import { computeSyncStatus } from '../lib/sync-status.js';
15
15
  import { promptDriftSync } from '../lib/drift-sync.js';
16
+ import { resolveSurface } from './utils.js';
16
17
  const agentName = (id) => AGENTS[id]?.name ?? id;
17
18
  function versionSummary(v) {
18
19
  if (!v.everSynced)
@@ -46,9 +47,14 @@ export function registerStatusCommand(program) {
46
47
  agents status --yes
47
48
  `,
48
49
  });
49
- cmd.action(async (opts) => {
50
+ cmd.action(async (opts, command) => {
51
+ // Centralized surface read (the human/agent split in one place). Note we still
52
+ // pass the *raw* `opts.yes` to promptDriftSync below — it distinguishes an
53
+ // explicit `--yes` (act) from a non-TTY shell (report only), so `surface.assumeYes`
54
+ // (which conflates the two) would wrongly auto-reconcile in a plain pipe.
55
+ const surface = resolveSurface(command);
50
56
  const cwd = opts.cwd ?? process.cwd();
51
- if (opts.json) {
57
+ if (surface.json) {
52
58
  const status = await computeSyncStatus({ cwd });
53
59
  console.log(JSON.stringify(status, null, 2));
54
60
  return;
@@ -53,7 +53,7 @@ When to use:
53
53
  - Team sharing: distribute subagent definitions via GitHub repos
54
54
  `);
55
55
  // Shared list implementation, registered as `list` and hidden `view` alias.
56
- const runList = async () => {
56
+ const runList = async (opts) => {
57
57
  const rows = buildSubagentRows();
58
58
  await showResourceList({
59
59
  resourcePlural: 'subagents',
@@ -62,12 +62,14 @@ When to use:
62
62
  rows,
63
63
  emptyMessage: 'No subagents in ~/.agents/subagents/. Add one with: agents subagents add gh:user/repo',
64
64
  centralPath: getSubagentsDir(),
65
+ json: opts?.json,
65
66
  });
66
67
  };
67
68
  // agents subagents list
68
69
  subagentsCmd
69
70
  .command('list')
70
71
  .description('Show subagents in a table with sync status across agent versions')
72
+ .option('--json', 'Emit machine-readable JSON instead of the table/picker')
71
73
  .addHelpText('after', `
72
74
  Examples:
73
75
  # Interactive picker (TTY) or sync-status table (piped)
@@ -544,7 +544,7 @@ function runLaunchMode(agent, version, cwd, quiet) {
544
544
  if (result.rulesCompiled)
545
545
  bits.push('rules');
546
546
  if (result.workspaceLinks > 0)
547
- bits.push(`${result.workspaceLinks} workspace link(s)`);
547
+ bits.push(`${result.workspaceLinks} project resource(s)`);
548
548
  const mpCount = Object.keys(result.marketplaces).length;
549
549
  if (mpCount > 0) {
550
550
  const pluginCount = Object.values(result.marketplaces).reduce((acc, names) => acc + names.length, 0);
@@ -7,6 +7,8 @@ import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, ge
7
7
  import { mailboxDir, enqueue } from '../lib/mailbox.js';
8
8
  import { resolveProvider } from '../lib/cloud/registry.js';
9
9
  import { emit } from '../lib/events.js';
10
+ import { maybeShowStarNudge } from '../lib/star-nudge.js';
11
+ import { shareRuntimeEnv } from '../lib/share/config.js';
10
12
  import { runSupervisor } from '../lib/teams/supervisor.js';
11
13
  import { debug } from '../lib/teams/debug.js';
12
14
  import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
@@ -49,8 +51,6 @@ const VALID_AGENTS = Object.keys(AGENT_NAMES);
49
51
  const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
50
52
  const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
51
53
  const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
52
- // Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
53
- // parseable output by default.
54
54
  function statusColor(status) {
55
55
  switch (status) {
56
56
  case 'pending': return chalk.blue;
@@ -1352,6 +1352,7 @@ export function registerTeamsCommands(program) {
1352
1352
  repo: opts.repo,
1353
1353
  branch: opts.branch,
1354
1354
  model: a.model ?? undefined,
1355
+ env: shareRuntimeEnv({ agentOnly: true }),
1355
1356
  };
1356
1357
  const cloudTask = await prov.dispatch(dispatchOpts);
1357
1358
  return { cloudSessionId: cloudTask.id };
@@ -1369,6 +1370,7 @@ export function registerTeamsCommands(program) {
1369
1370
  repo: opts.repo,
1370
1371
  branch: opts.branch,
1371
1372
  model: opts.model,
1373
+ env: shareRuntimeEnv({ agentOnly: true }),
1372
1374
  };
1373
1375
  try {
1374
1376
  const cloudTask = await prov.dispatch(dispatchOpts);
@@ -1594,6 +1596,13 @@ export function registerTeamsCommands(program) {
1594
1596
  emit('teams.complete', { module: 'teams', team, stoppedBy: result.stoppedBy, waves: result.waves, durationMs: result.elapsed_ms });
1595
1597
  if (result.stoppedBy === 'drained') {
1596
1598
  console.log(chalk.green(`Factory drained in ${elapsed}s (${result.waves} waves).`));
1599
+ // First-successful-team star nudge (one-time, non-nagging). "drained"
1600
+ // only means nothing is pending/running — teammates may have failed — so
1601
+ // gate on a clean drain (failed === 0). A team where every teammate
1602
+ // failed is not the success this nudge celebrates.
1603
+ if ((result.failed ?? 0) === 0) {
1604
+ maybeShowStarNudge({ quiet: opts.json });
1605
+ }
1597
1606
  }
1598
1607
  else if (result.stoppedBy === 'max-waves') {
1599
1608
  console.error(chalk.yellow(`Hit --max-waves=${maxWaves}; stopping. Re-run to continue.`));
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `agents uninstall` — completely remove agents-cli and restore the user's
3
+ * original agent configs. The reverse of `agents setup`.
4
+ *
5
+ * Thin command layer: the restore/teardown logic lives in `lib/uninstall.ts`
6
+ * (planUninstall / executeUninstall) so it can be tested against a real temp
7
+ * HOME without the CLI wrapper.
8
+ */
9
+ import type { Command } from 'commander';
10
+ /** Register `agents uninstall`. */
11
+ export declare function registerUninstallCommands(program: Command): void;
@@ -0,0 +1,158 @@
1
+ import chalk from 'chalk';
2
+ import { confirm } from '@inquirer/prompts';
3
+ import { setHelpSections } from '../lib/help.js';
4
+ import { planUninstall, executeUninstall } from '../lib/uninstall.js';
5
+ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
6
+ /** Render the read-only plan so the user sees exactly what will change. */
7
+ function printPlan(plan, purge) {
8
+ const restores = plan.configs.filter((c) => c.kind === 'restore-backup' || c.kind === 'restore-version-home');
9
+ const dangling = plan.configs.filter((c) => c.kind === 'remove-dangling');
10
+ const untouched = plan.configs.filter((c) => c.kind === 'leave-real' || c.kind === 'leave-foreign');
11
+ console.log(chalk.bold('\nagents uninstall — planned changes\n'));
12
+ if (restores.length > 0) {
13
+ console.log(chalk.green('Restore your original config (adopted by agents-cli):'));
14
+ for (const c of restores) {
15
+ const how = c.kind === 'restore-backup' ? 'from backup' : 'from version home';
16
+ console.log(chalk.gray(` ${c.realPath} ${chalk.dim(`(${how})`)}`));
17
+ }
18
+ }
19
+ if (dangling.length > 0) {
20
+ console.log(chalk.yellow('Remove dangling symlink (no original found to restore):'));
21
+ for (const c of dangling)
22
+ console.log(chalk.gray(` ${c.realPath}`));
23
+ }
24
+ if (untouched.length > 0) {
25
+ console.log(chalk.cyan('Left untouched (real, un-adopted configs — never modified):'));
26
+ for (const c of untouched)
27
+ console.log(chalk.gray(` ${c.realPath}`));
28
+ }
29
+ if (plan.homeFiles.length > 0) {
30
+ console.log(chalk.green('Restore home files:'));
31
+ for (const hf of plan.homeFiles)
32
+ console.log(chalk.gray(` ${hf.realPath}`));
33
+ }
34
+ if (plan.launchers.length > 0) {
35
+ console.log(chalk.green('Release adopted launchers (restore native binaries on PATH):'));
36
+ console.log(chalk.gray(` ${plan.launchers.join(', ')}`));
37
+ }
38
+ if (plan.rcFiles.length > 0) {
39
+ console.log(chalk.green('Remove the shim directory from PATH in:'));
40
+ for (const rc of plan.rcFiles)
41
+ console.log(chalk.gray(` ${rc}`));
42
+ }
43
+ console.log(chalk.bold('\nData:'));
44
+ if (purge) {
45
+ console.log(chalk.red(` Permanently delete ${plan.agentsDir} (installed versions, session history, secrets metadata).`));
46
+ if (plan.legacySymlink)
47
+ console.log(chalk.red(` Permanently delete ${plan.legacySymlink}.`));
48
+ }
49
+ else {
50
+ console.log(chalk.gray(` Move ${plan.agentsDir} aside to ${plan.agentsDir}.removed-<timestamp> (recoverable).`));
51
+ if (plan.legacySymlink)
52
+ console.log(chalk.gray(` Remove the legacy symlink ${plan.legacySymlink}.`));
53
+ console.log(chalk.gray(' Use --purge to hard-delete instead.'));
54
+ }
55
+ console.log();
56
+ }
57
+ /** Report what actually happened, then the final manual npm step. */
58
+ function printResult(result, cleanedPath) {
59
+ for (const r of result.restoredConfigs)
60
+ console.log(chalk.green(`Restored ${r.realPath}`));
61
+ for (const r of result.removedDanglingConfigs)
62
+ console.log(chalk.yellow(`Removed dangling symlink ${r.realPath}`));
63
+ for (const f of result.restoredHomeFiles)
64
+ console.log(chalk.green(`Restored ${f}`));
65
+ if (result.releasedLaunchers.length > 0)
66
+ console.log(chalk.green(`Released launchers: ${result.releasedLaunchers.join(', ')}`));
67
+ for (const rc of result.cleanedRcFiles)
68
+ console.log(chalk.green(`Cleaned shim PATH entry from ${rc}`));
69
+ if (result.agentsDir.disposition === 'moved') {
70
+ console.log(chalk.gray(`Moved ${result.agentsDir.path} to ${result.agentsDir.movedTo}`));
71
+ }
72
+ else if (result.agentsDir.disposition === 'purged') {
73
+ console.log(chalk.gray(`Deleted ${result.agentsDir.path}`));
74
+ }
75
+ for (const e of result.errors)
76
+ console.log(chalk.red(` ! ${e}`));
77
+ if (result.purgeDowngraded) {
78
+ console.log(chalk.yellow(`\n--purge was downgraded to move-aside because a restore step errored — ${result.agentsDir.path} was kept so nothing is lost. Resolve the errors above, then delete ${result.agentsDir.movedTo} manually.`));
79
+ }
80
+ console.log(chalk.bold('\nFinish by removing the CLI package:'));
81
+ console.log(chalk.gray(' npm uninstall -g @phnx-labs/agents-cli # or: bun remove -g @phnx-labs/agents-cli'));
82
+ if (cleanedPath) {
83
+ console.log(chalk.gray('Open a new shell (or re-source your rc file) so the removed PATH entry takes effect.'));
84
+ }
85
+ if (result.agentsDir.disposition === 'moved') {
86
+ console.log(chalk.gray(`Your data is still at ${result.agentsDir.movedTo} — delete it once you are sure.`));
87
+ }
88
+ }
89
+ /** Register `agents uninstall`. */
90
+ export function registerUninstallCommands(program) {
91
+ const cmd = program
92
+ .command('uninstall')
93
+ .description('Completely remove agents-cli and restore your original agent configs. Reverses `agents setup`.')
94
+ .option('--purge', 'Hard-delete ~/.agents (installed versions, sessions, secrets metadata) instead of moving it aside')
95
+ .option('--dry-run', 'Show exactly what would change without modifying anything')
96
+ .option('-y, --yes', 'Skip the confirmation prompt (required to run non-interactively)');
97
+ setHelpSections(cmd, {
98
+ examples: `
99
+ # Preview everything that would change, without touching anything
100
+ agents uninstall --dry-run
101
+
102
+ # Restore your configs and move ~/.agents aside (recoverable)
103
+ agents uninstall
104
+
105
+ # Same, but hard-delete ~/.agents (no recovery)
106
+ agents uninstall --purge
107
+ `,
108
+ notes: `
109
+ - Restores every ~/.<agent> that agents-cli adopted; a real config it never adopted is left untouched.
110
+ - Releases adopted launchers and strips the shim directory from your shell PATH.
111
+ - Without --purge, ~/.agents is moved to ~/.agents.removed-<timestamp> so nothing is lost.
112
+ - The CLI cannot delete its own running binary; it prints the final 'npm uninstall -g' step for you.
113
+ `,
114
+ });
115
+ cmd.action(async (options) => {
116
+ // We are tearing ~/.agents down. Silence the JSONL audit log for the rest
117
+ // of this process so a late emit() (its events path is memoized to the old
118
+ // location) can't re-create ~/.agents after we move it aside.
119
+ process.env.AGENTS_DISABLE_EVENT_LOG = '1';
120
+ const plan = planUninstall();
121
+ if (!plan.isInstalled) {
122
+ console.log(chalk.gray('agents-cli is not set up (no ~/.agents directory) — nothing to uninstall.'));
123
+ return;
124
+ }
125
+ printPlan(plan, !!options.purge);
126
+ if (options.dryRun) {
127
+ console.log(chalk.gray('Dry run — nothing was changed.'));
128
+ return;
129
+ }
130
+ if (!options.yes) {
131
+ if (!isInteractiveTerminal()) {
132
+ console.log(chalk.red('Refusing to uninstall non-interactively. Re-run with --yes to confirm.'));
133
+ return;
134
+ }
135
+ try {
136
+ const ok = await confirm({
137
+ message: options.purge
138
+ ? 'Permanently remove agents-cli and restore your original configs?'
139
+ : 'Remove agents-cli (data moved to a recoverable directory) and restore your original configs?',
140
+ default: false,
141
+ });
142
+ if (!ok) {
143
+ console.log(chalk.gray('Cancelled.'));
144
+ return;
145
+ }
146
+ }
147
+ catch (err) {
148
+ if (isPromptCancelled(err)) {
149
+ console.log(chalk.gray('Cancelled.'));
150
+ return;
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+ const result = executeUninstall(plan, { purge: options.purge, timestamp: Date.now() });
156
+ printResult(result, result.cleanedRcFiles.length > 0);
157
+ });
158
+ }