@phnx-labs/agents-cli 1.15.0 → 1.16.0

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 (87) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +546 -75
  7. package/dist/commands/commands.js +72 -22
  8. package/dist/commands/daemon.js +2 -2
  9. package/dist/commands/fork.js +2 -2
  10. package/dist/commands/hooks.js +71 -26
  11. package/dist/commands/mcp.js +81 -39
  12. package/dist/commands/plugins.js +48 -15
  13. package/dist/commands/prune.js +23 -1
  14. package/dist/commands/pull.js +3 -3
  15. package/dist/commands/repo.js +1 -1
  16. package/dist/commands/routines.js +2 -2
  17. package/dist/commands/secrets.js +37 -1
  18. package/dist/commands/sessions.js +62 -19
  19. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  20. package/dist/commands/{init.js → setup.js} +22 -21
  21. package/dist/commands/skills.js +60 -19
  22. package/dist/commands/subagents.js +41 -13
  23. package/dist/commands/utils.d.ts +16 -0
  24. package/dist/commands/utils.js +32 -0
  25. package/dist/commands/view.js +61 -16
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +17 -20
  28. package/dist/lib/agents.js +2 -2
  29. package/dist/lib/auto-pull-worker.js +2 -3
  30. package/dist/lib/auto-pull.js +2 -2
  31. package/dist/lib/browser/cdp.d.ts +7 -1
  32. package/dist/lib/browser/cdp.js +29 -1
  33. package/dist/lib/browser/chrome.js +5 -2
  34. package/dist/lib/browser/devices.d.ts +4 -0
  35. package/dist/lib/browser/devices.js +27 -0
  36. package/dist/lib/browser/drivers/local.js +9 -4
  37. package/dist/lib/browser/drivers/ssh.js +9 -2
  38. package/dist/lib/browser/ipc.js +144 -23
  39. package/dist/lib/browser/profiles.d.ts +5 -2
  40. package/dist/lib/browser/profiles.js +77 -37
  41. package/dist/lib/browser/service.d.ts +81 -13
  42. package/dist/lib/browser/service.js +738 -131
  43. package/dist/lib/browser/types.d.ts +81 -3
  44. package/dist/lib/browser/types.js +16 -0
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/commands.d.ts +1 -0
  48. package/dist/lib/commands.js +6 -2
  49. package/dist/lib/daemon.js +2 -3
  50. package/dist/lib/doctor-diff.js +4 -4
  51. package/dist/lib/events.js +2 -2
  52. package/dist/lib/hooks.d.ts +11 -7
  53. package/dist/lib/hooks.js +125 -49
  54. package/dist/lib/migrate.d.ts +1 -1
  55. package/dist/lib/migrate.js +1178 -21
  56. package/dist/lib/models.js +2 -2
  57. package/dist/lib/permissions.d.ts +8 -8
  58. package/dist/lib/permissions.js +8 -8
  59. package/dist/lib/plugins.d.ts +30 -1
  60. package/dist/lib/plugins.js +75 -3
  61. package/dist/lib/pty-server.js +9 -10
  62. package/dist/lib/resources/hooks.d.ts +5 -1
  63. package/dist/lib/resources/hooks.js +21 -4
  64. package/dist/lib/rotate.js +3 -4
  65. package/dist/lib/session/active.d.ts +3 -0
  66. package/dist/lib/session/active.js +92 -6
  67. package/dist/lib/session/cloud.js +2 -2
  68. package/dist/lib/session/db.js +8 -3
  69. package/dist/lib/session/discover.js +30 -15
  70. package/dist/lib/session/team-filter.js +2 -2
  71. package/dist/lib/shims.d.ts +2 -2
  72. package/dist/lib/shims.js +6 -6
  73. package/dist/lib/skills.js +6 -2
  74. package/dist/lib/state.d.ts +86 -14
  75. package/dist/lib/state.js +150 -23
  76. package/dist/lib/subagents.d.ts +28 -0
  77. package/dist/lib/subagents.js +98 -1
  78. package/dist/lib/sync-manifest.d.ts +1 -1
  79. package/dist/lib/sync-manifest.js +3 -3
  80. package/dist/lib/teams/persistence.js +15 -5
  81. package/dist/lib/teams/registry.js +2 -2
  82. package/dist/lib/types.d.ts +32 -3
  83. package/dist/lib/types.js +3 -3
  84. package/dist/lib/usage.js +2 -2
  85. package/dist/lib/versions.js +20 -21
  86. package/package.json +1 -1
  87. package/scripts/postinstall.js +1 -1
@@ -571,8 +571,8 @@ Examples:
571
571
  .action(async (options) => {
572
572
  if (options.follow) {
573
573
  const { exec: execCb } = await import('child_process');
574
- const { getAgentsDir } = await import('../lib/state.js');
575
- const logPath = path.join(getAgentsDir(), 'helpers/daemon/logs.jsonl');
574
+ const { getDaemonDir } = await import('../lib/state.js');
575
+ const logPath = path.join(getDaemonDir(), 'logs.jsonl');
576
576
  const child = execCb(`tail -f "${logPath}"`);
577
577
  child.stdout?.pipe(process.stdout);
578
578
  child.stderr?.pipe(process.stderr);
@@ -280,6 +280,10 @@ Examples:
280
280
  # Eval the bundle into your current shell
281
281
  eval "$(agents secrets export prod --plaintext)"
282
282
 
283
+ # Run a command with secrets injected
284
+ agents secrets exec prod -- ./deploy.sh
285
+ agents secrets exec hetzner.com -- crabbox list
286
+
283
287
  # Remove one key (purges the keychain item by default)
284
288
  agents secrets remove prod STRIPE_API_KEY
285
289
 
@@ -297,7 +301,7 @@ Examples:
297
301
  registerCommandGroups(cmd, [
298
302
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
299
303
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
300
- { title: 'Utilities', names: ['generate'] },
304
+ { title: 'Utilities', names: ['exec', 'generate'] },
301
305
  ]);
302
306
  cmd
303
307
  .command('list')
@@ -711,6 +715,38 @@ Examples:
711
715
  process.exit(1);
712
716
  }
713
717
  });
718
+ cmd
719
+ .command('exec <bundle> [command...]')
720
+ .description('Run a command with the bundle\'s secrets injected into the environment')
721
+ .allowUnknownOption()
722
+ .action(async (bundleName, commandParts) => {
723
+ try {
724
+ if (commandParts.length === 0) {
725
+ console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
726
+ process.exit(1);
727
+ }
728
+ const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
729
+ const bundle = readBundle(bundleName);
730
+ const secretEnv = resolveBundleEnv(bundle);
731
+ const { spawn } = await import('child_process');
732
+ const [cmd, ...args] = commandParts;
733
+ const proc = spawn(cmd, args, {
734
+ stdio: 'inherit',
735
+ env: { ...process.env, ...secretEnv },
736
+ });
737
+ proc.on('close', (code) => process.exit(code ?? 0));
738
+ proc.on('error', (err) => {
739
+ console.error(chalk.red(`Failed to run '${cmd}': ${err.message}`));
740
+ process.exit(1);
741
+ });
742
+ }
743
+ catch (err) {
744
+ if (isPromptCancelled(err))
745
+ return;
746
+ console.error(chalk.red(err.message));
747
+ process.exit(1);
748
+ }
749
+ });
714
750
  cmd
715
751
  .command('generate [length]')
716
752
  .description('Generate a random password')
@@ -160,6 +160,26 @@ function formatStartedAt(startedAtMs) {
160
160
  return '-';
161
161
  return formatRelativeTime(new Date(startedAtMs).toISOString());
162
162
  }
163
+ /** Build a display-friendly description for an active session (label or topic). */
164
+ function buildSessionDescription(s) {
165
+ if (s.context === 'cloud') {
166
+ return `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`;
167
+ }
168
+ if (s.context === 'teams') {
169
+ const parts = [s.teamName];
170
+ if (s.label)
171
+ parts.push(s.label);
172
+ else if (s.topic)
173
+ parts.push(s.topic);
174
+ return parts.filter(Boolean).join(' · ');
175
+ }
176
+ // Terminal or headless: prefer label, then topic
177
+ if (s.label)
178
+ return s.label;
179
+ if (s.topic)
180
+ return s.topic;
181
+ return '';
182
+ }
163
183
  /** Render the unified active-session view. */
164
184
  async function renderActiveSessions(asJson) {
165
185
  const sessions = await getActiveSessions();
@@ -171,26 +191,49 @@ async function renderActiveSessions(asJson) {
171
191
  console.log(chalk.gray('No active agent sessions.'));
172
192
  return;
173
193
  }
194
+ // Group sessions by workspace (cwd), with cloud/undefined grouped separately
195
+ const byWorkspace = new Map();
174
196
  for (const s of sessions) {
175
- const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
176
- const ctxCol = contextColor(s.context)(padRight(truncate(s.context, 8), 9));
177
- const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
178
- const statusCol = statusColor(s.status)(padRight(truncate(s.status, 8), 9));
179
- const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
180
- const idCol = chalk.white(padRight(s.sessionId ? s.sessionId.slice(0, 8) : '-', 10));
181
- const detail = s.context === 'cloud'
182
- ? `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`
183
- : s.context === 'teams'
184
- ? `${s.teamName ?? ''}${s.label ? ` · ${s.label}` : ''}`
185
- : s.label ?? shortCwd(s.cwd);
186
- console.log(pidCol +
187
- kindCol +
188
- ctxCol +
189
- hostCol +
190
- statusCol +
191
- idCol +
192
- chalk.cyan(padRight(truncate(detail || '-', 30), 32)) +
193
- chalk.gray(formatStartedAt(s.startedAtMs)));
197
+ const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
198
+ const list = byWorkspace.get(key) || [];
199
+ list.push(s);
200
+ byWorkspace.set(key, list);
201
+ }
202
+ // Sort workspaces: most sessions first, then alphabetically
203
+ const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
204
+ const aCount = byWorkspace.get(a).length;
205
+ const bCount = byWorkspace.get(b).length;
206
+ if (aCount !== bCount)
207
+ return bCount - aCount;
208
+ return a.localeCompare(b);
209
+ });
210
+ let first = true;
211
+ for (const key of sortedKeys) {
212
+ const group = byWorkspace.get(key);
213
+ if (!first)
214
+ console.log();
215
+ first = false;
216
+ // Print workspace header
217
+ const header = key === '__cloud__'
218
+ ? chalk.magenta.bold('cloud')
219
+ : key === '__unknown__'
220
+ ? chalk.gray.bold('unknown')
221
+ : chalk.cyan.bold(shortCwd(key));
222
+ console.log(`${header} ${chalk.gray(`(${group.length})`)}`);
223
+ // Print each session in this workspace
224
+ for (const s of group) {
225
+ const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
226
+ const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
227
+ const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
228
+ const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
229
+ const desc = buildSessionDescription(s);
230
+ console.log(' ' +
231
+ pidCol +
232
+ kindCol +
233
+ hostCol +
234
+ statusCol +
235
+ chalk.white(truncate(desc || '-', 50)));
236
+ }
194
237
  }
195
238
  const runningCount = sessions.filter(s => s.status === 'running').length;
196
239
  const idleCount = sessions.filter(s => s.status === 'idle').length;
@@ -1,20 +1,21 @@
1
1
  /**
2
- * First-run initialization command.
2
+ * First-run setup command.
3
3
  *
4
- * Registers the `agents init` command which clones the system repo into
4
+ * Registers the `agents setup` command which clones the system repo into
5
5
  * ~/.agents-system/ and installs agent CLIs with resource syncing.
6
6
  */
7
7
  import type { Command } from 'commander';
8
8
  /** First-run setup. Clones ~/.agents-system/ from the system repo if needed. */
9
- export declare function runInit(program: Command, options?: {
9
+ export declare function runSetup(program: Command, options?: {
10
10
  force?: boolean;
11
+ suppressFooter?: boolean;
11
12
  }): Promise<void>;
12
13
  /**
13
14
  * Ensure the system repo exists before running a command that needs it.
14
15
  * If ~/.agents-system/ is not a git repo AND we're in an interactive TTY,
15
- * prompt the user to run init now. In non-interactive mode, print a clear
16
+ * prompt the user to run setup now. In non-interactive mode, print a clear
16
17
  * error and exit.
17
18
  */
18
19
  export declare function ensureInitialized(program: Command): Promise<void>;
19
- /** Register the `agents init` command. */
20
- export declare function registerInitCommand(program: Command): void;
20
+ /** Register the `agents setup` command. */
21
+ export declare function registerSetupCommand(program: Command): void;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * First-run initialization command.
2
+ * First-run setup command.
3
3
  *
4
- * Registers the `agents init` command which clones the system repo into
4
+ * Registers the `agents setup` command which clones the system repo into
5
5
  * ~/.agents-system/ and installs agent CLIs with resource syncing.
6
6
  */
7
7
  import chalk from 'chalk';
@@ -52,13 +52,13 @@ async function importAgent(agentId, version) {
52
52
  }
53
53
  }
54
54
  /** First-run setup. Clones ~/.agents-system/ from the system repo if needed. */
55
- export async function runInit(program, options = {}) {
55
+ export async function runSetup(program, options = {}) {
56
56
  const agentsDir = getAgentsDir();
57
57
  const alreadyConfigured = isGitRepo(agentsDir);
58
58
  if (alreadyConfigured && !options.force) {
59
59
  console.log(chalk.gray('~/.agents-system/ is already set up.'));
60
60
  console.log(chalk.gray('\nTo sync updates: agents repo pull system'));
61
- console.log(chalk.gray('To re-initialize: agents init --force'));
61
+ console.log(chalk.gray('To re-run setup: agents setup --force'));
62
62
  return;
63
63
  }
64
64
  // Detect existing installations BEFORE cloning (they won't exist after if we import)
@@ -76,7 +76,7 @@ export async function runInit(program, options = {}) {
76
76
  const result = await pullRepo(agentsDir);
77
77
  if (!result.success) {
78
78
  spinner.fail(`Pull failed: ${result.error}`);
79
- console.log(chalk.gray('Fix the issue and re-run: agents init --force'));
79
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
80
80
  process.exit(1);
81
81
  }
82
82
  spinner.succeed(`Updated to ${result.commit}`);
@@ -95,7 +95,7 @@ export async function runInit(program, options = {}) {
95
95
  const result = await cloneIntoExisting(DEFAULT_SYSTEM_REPO, agentsDir);
96
96
  if (!result.success) {
97
97
  spinner.fail(`Clone failed: ${result.error}`);
98
- console.log(chalk.gray('Fix the issue and re-run: agents init --force'));
98
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
99
99
  process.exit(1);
100
100
  }
101
101
  spinner.succeed(`Cloned ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} (${result.commit})`);
@@ -156,6 +156,8 @@ export async function runInit(program, options = {}) {
156
156
  }
157
157
  }
158
158
  }
159
+ if (options.suppressFooter)
160
+ return;
159
161
  console.log(chalk.bold('\nSetup complete. Try:'));
160
162
  console.log(chalk.cyan(' agents view ') + chalk.gray(' # see what\'s installed'));
161
163
  console.log(chalk.cyan(' agents run <agent> "hello" ') + chalk.gray(' # run an agent'));
@@ -165,7 +167,7 @@ export async function runInit(program, options = {}) {
165
167
  /**
166
168
  * Ensure the system repo exists before running a command that needs it.
167
169
  * If ~/.agents-system/ is not a git repo AND we're in an interactive TTY,
168
- * prompt the user to run init now. In non-interactive mode, print a clear
170
+ * prompt the user to run setup now. In non-interactive mode, print a clear
169
171
  * error and exit.
170
172
  */
171
173
  export async function ensureInitialized(program) {
@@ -173,34 +175,33 @@ export async function ensureInitialized(program) {
173
175
  if (isGitRepo(agentsDir))
174
176
  return;
175
177
  if (!isInteractiveTerminal()) {
176
- console.error(chalk.red('agents-cli is not initialized. Run: agents init'));
178
+ console.error(chalk.red('agents-cli is not set up. Run: agents setup'));
177
179
  process.exit(1);
178
180
  }
179
181
  console.log(chalk.yellow('\nagents-cli has not been set up yet.'));
180
182
  const proceed = await confirm({
181
- message: 'Run `agents init` now?',
183
+ message: 'Run `agents setup` now?',
182
184
  default: true,
183
185
  }).catch(() => false);
184
186
  if (!proceed) {
185
- console.log(chalk.gray('Skipped. Run `agents init` when ready.'));
187
+ console.log(chalk.gray('Skipped. Run `agents setup` when ready.'));
186
188
  process.exit(0);
187
189
  }
188
- await runInit(program);
189
- process.exit(0);
190
+ await runSetup(program, { suppressFooter: true });
190
191
  }
191
- /** Register the `agents init` command. */
192
- export function registerInitCommand(program) {
192
+ /** Register the `agents setup` command. */
193
+ export function registerSetupCommand(program) {
193
194
  program
194
- .command('init')
195
+ .command('setup')
195
196
  .description('Set up agents-cli for the first time. Clones a config repo and installs agent CLIs.')
196
- .option('-f, --force', 'Reinitialize even if ~/.agents-system/ already exists (use with caution)')
197
+ .option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)')
197
198
  .addHelpText('after', `
198
199
  Examples:
199
200
  # First-time setup (clones the system repo into ~/.agents-system/)
200
- agents init
201
+ agents setup
201
202
 
202
- # Re-initialize after corruption
203
- agents init --force
203
+ # Re-run setup after corruption
204
+ agents setup --force
204
205
 
205
206
  When to use:
206
207
  - First time running agents-cli: this is your starting point
@@ -213,12 +214,12 @@ What it does:
213
214
  3. Syncs commands, skills, hooks, and MCP servers to each version
214
215
 
215
216
  Non-interactive alternative:
216
- Skip 'init' and run:
217
+ Skip 'setup' and run:
217
218
  agents pull
218
219
  `)
219
220
  .action(async (options) => {
220
221
  try {
221
- await runInit(program, options);
222
+ await runSetup(program, options);
222
223
  }
223
224
  catch (err) {
224
225
  if (isPromptCancelled(err)) {
@@ -4,12 +4,12 @@ import * as fs from 'fs';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
6
  import { select, checkbox } from '@inquirer/prompts';
7
- import { SKILLS_CAPABLE_AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, SKILLS_CAPABLE_AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
- import { discoverSkillsFromRepo, installSkillCentrally, uninstallSkill, listInstalledSkills, listInstalledSkillsWithScope, getSkillInfo, getSkillRules, getSkillsDir, countSkillFiles, tryParseSkillMetadata, diffVersionSkills, iterSkillsCapableVersions, } from '../lib/skills.js';
10
- import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, resolveAgentVersionTargets, } from '../lib/versions.js';
9
+ import { discoverSkillsFromRepo, installSkillCentrally, listInstalledSkills, listInstalledSkillsWithScope, getSkillInfo, getSkillRules, getSkillsDir, countSkillFiles, tryParseSkillMetadata, diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
10
+ import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
11
11
  import { recordVersionResources } from '../lib/state.js';
12
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, } from './utils.js';
12
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
13
13
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
14
14
  /** Register the `agents skills` command tree (list, add, remove, sync, prune, view). */
15
15
  export function registerSkillsCommands(program) {
@@ -294,15 +294,29 @@ Examples:
294
294
  agents skills remove
295
295
  `)
296
296
  .action(async (name) => {
297
+ const skillTargetMap = new Map();
298
+ for (const { agent, version } of iterSkillsCapableVersions()) {
299
+ const home = getVersionHomePath(agent, version);
300
+ const skills = listInstalledSkillsWithScope(agent, process.cwd(), { home });
301
+ for (const skill of skills) {
302
+ if (skill.scope !== 'user')
303
+ continue;
304
+ const existing = skillTargetMap.get(skill.name);
305
+ if (existing) {
306
+ existing.targets.push({ agent, version });
307
+ }
308
+ else {
309
+ skillTargetMap.set(skill.name, { name: skill.name, targets: [{ agent, version }] });
310
+ }
311
+ }
312
+ }
297
313
  let skillsToRemove;
298
314
  if (name) {
299
315
  skillsToRemove = [name];
300
316
  }
301
317
  else {
302
- // Interactive picker
303
- const installedSkills = listInstalledSkills();
304
- if (installedSkills.size === 0) {
305
- console.log(chalk.yellow('No skills installed.'));
318
+ if (skillTargetMap.size === 0) {
319
+ console.log(chalk.yellow('No skills installed in any version.'));
306
320
  return;
307
321
  }
308
322
  if (!isInteractiveTerminal()) {
@@ -311,12 +325,13 @@ Examples:
311
325
  ]);
312
326
  }
313
327
  try {
314
- const choices = Array.from(installedSkills.entries()).map(([skillName, skill]) => ({
315
- value: skillName,
316
- name: skill.metadata.description
317
- ? `${skillName} - ${skill.metadata.description}`
318
- : skillName,
319
- }));
328
+ const choices = Array.from(skillTargetMap.values()).map((skill) => {
329
+ const agents = [...new Set(skill.targets.map((t) => AGENTS[t.agent].name))];
330
+ return {
331
+ value: skill.name,
332
+ name: `${skill.name} (${agents.join(', ')})`,
333
+ };
334
+ });
320
335
  const selected = await checkbox({
321
336
  message: 'Select skills to remove',
322
337
  choices,
@@ -335,14 +350,40 @@ Examples:
335
350
  throw err;
336
351
  }
337
352
  }
353
+ let removed = 0;
338
354
  for (const skillName of skillsToRemove) {
339
- const result = uninstallSkill(skillName);
340
- if (result.success) {
341
- console.log(chalk.green(`Removed skill '${skillName}'`));
355
+ const skillInfo = skillTargetMap.get(skillName);
356
+ if (!skillInfo || skillInfo.targets.length === 0) {
357
+ console.log(chalk.yellow(` Skill '${skillName}' not found in any version.`));
358
+ continue;
342
359
  }
343
- else {
344
- console.log(chalk.red(result.error || `Failed to remove skill '${skillName}'`));
360
+ const removalTargets = skillInfo.targets.map((t) => ({
361
+ agent: t.agent,
362
+ version: t.version,
363
+ label: `${agentLabel(t.agent)}@${t.version}`,
364
+ }));
365
+ const selectedTargets = await promptRemovalTargets(skillName, removalTargets);
366
+ if (selectedTargets.length === 0) {
367
+ console.log(chalk.gray(` Skipped '${skillName}'.`));
368
+ continue;
345
369
  }
370
+ for (const target of selectedTargets) {
371
+ const result = removeSkillFromVersion(target.agent, target.version, skillName);
372
+ if (result.success) {
373
+ console.log(` ${chalk.red('-')} ${target.label}: ${skillName}`);
374
+ removed++;
375
+ }
376
+ else if (result.error) {
377
+ console.log(` ${chalk.yellow('!')} ${target.label}: ${result.error}`);
378
+ }
379
+ }
380
+ }
381
+ if (removed === 0) {
382
+ console.log(chalk.yellow('No skills removed.'));
383
+ }
384
+ else {
385
+ console.log(chalk.green(`\nRemoved ${removed} skill(s) from version homes.`));
386
+ console.log(chalk.gray('Central source unchanged. Skills will re-sync on next agent launch.'));
346
387
  }
347
388
  });
348
389
  // `skills sync` is gone — sync runs automatically when the agent launches.
@@ -12,10 +12,10 @@ import * as path from 'path';
12
12
  import { checkbox } from '@inquirer/prompts';
13
13
  import { AGENTS, agentLabel } from '../lib/agents.js';
14
14
  import { cloneRepo } from '../lib/git.js';
15
- import { discoverSubagentsFromRepo, installSubagentCentrally, removeSubagent, listInstalledSubagents, getInstalledSubagent, listSubagentsForAgent, SUBAGENT_CAPABLE_AGENTS, } from '../lib/subagents.js';
15
+ import { discoverSubagentsFromRepo, installSubagentCentrally, listInstalledSubagents, getInstalledSubagent, listSubagentsForAgent, SUBAGENT_CAPABLE_AGENTS, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
16
16
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
17
17
  import { getSubagentsDir } from '../lib/state.js';
18
- import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection, requireDestructiveArg, } from './utils.js';
18
+ import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection, requireDestructiveArg, promptRemovalTargets, } from './utils.js';
19
19
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
20
20
  /** Replace the home directory prefix with ~ for display. */
21
21
  function formatPath(p) {
@@ -240,20 +240,48 @@ Examples:
240
240
  console.log(chalk.red(`Subagent '${name}' not found`));
241
241
  process.exit(1);
242
242
  }
243
- const spinner = ora({ text: `Removing ${name}...`, isSilent: !process.stdout.isTTY }).start();
244
- const result = removeSubagent(name);
245
- if (!result.success) {
246
- spinner.fail(`Failed to remove: ${result.error}`);
247
- process.exit(1);
243
+ // Build list of targets that have this subagent synced
244
+ const availableTargets = [];
245
+ for (const { agent, version } of iterSubagentsCapableVersions()) {
246
+ const home = getVersionHomePath(agent, version);
247
+ const installed = listSubagentsForAgent(agent, home).some((s) => s.name === name);
248
+ if (installed) {
249
+ availableTargets.push({ agent, version });
250
+ }
251
+ }
252
+ if (availableTargets.length === 0) {
253
+ console.log(chalk.yellow(`Subagent '${name}' not synced to any version.`));
254
+ return;
255
+ }
256
+ // Show multi-select picker for targets
257
+ const removalTargets = availableTargets.map((t) => ({
258
+ agent: t.agent,
259
+ version: t.version,
260
+ label: `${agentLabel(t.agent)}@${t.version}`,
261
+ }));
262
+ const selectedTargets = await promptRemovalTargets(name, removalTargets);
263
+ if (selectedTargets.length === 0) {
264
+ console.log(chalk.gray('Cancelled.'));
265
+ return;
248
266
  }
249
- // Re-sync all installed versions to remove from agent homes
250
- for (const agentId of SUBAGENT_CAPABLE_AGENTS) {
251
- const versions = listInstalledVersions(agentId);
252
- for (const version of versions) {
253
- syncResourcesToVersion(agentId, version);
267
+ let removed = 0;
268
+ for (const target of selectedTargets) {
269
+ const result = removeSubagentFromVersion(target.agent, target.version, name);
270
+ if (result.success) {
271
+ console.log(` ${chalk.red('-')} ${target.label}: ${name}`);
272
+ removed++;
273
+ }
274
+ else if (result.error) {
275
+ console.log(` ${chalk.yellow('!')} ${target.label}: ${result.error}`);
254
276
  }
255
277
  }
256
- spinner.succeed(`Removed subagent '${name}'`);
278
+ if (removed === 0) {
279
+ console.log(chalk.yellow('No subagents removed.'));
280
+ }
281
+ else {
282
+ console.log(chalk.green(`\nRemoved ${removed} subagent(s) from version homes.`));
283
+ console.log(chalk.gray('Central source unchanged. Subagents will re-sync on next agent launch.'));
284
+ }
257
285
  });
258
286
  }
259
287
  /** Every (agent, version) that supports subagents and is installed. */
@@ -38,6 +38,22 @@ export declare function printWithPager(output: string, lineCount: number): void;
38
38
  * Parse a comma-separated CLI list, trimming whitespace and dropping empties.
39
39
  */
40
40
  export declare function parseCommaSeparatedList(value: string | undefined): string[];
41
+ /**
42
+ * A target for resource removal: agent + version.
43
+ */
44
+ export interface RemovalTarget {
45
+ agent: string;
46
+ version: string;
47
+ label: string;
48
+ }
49
+ /**
50
+ * Prompt user to select which agent/version targets to remove a resource from.
51
+ * If only one target, returns it without prompting. If multiple, shows checkbox.
52
+ * Returns empty array if user cancels or selects nothing.
53
+ */
54
+ export declare function promptRemovalTargets(resourceName: string, targets: RemovalTarget[], options?: {
55
+ skipPrompt?: boolean;
56
+ }): Promise<RemovalTarget[]>;
41
57
  /**
42
58
  * Format a path for display, using ~ for home directory
43
59
  */
@@ -89,6 +89,38 @@ export function parseCommaSeparatedList(value) {
89
89
  .map((item) => item.trim())
90
90
  .filter(Boolean);
91
91
  }
92
+ /**
93
+ * Prompt user to select which agent/version targets to remove a resource from.
94
+ * If only one target, returns it without prompting. If multiple, shows checkbox.
95
+ * Returns empty array if user cancels or selects nothing.
96
+ */
97
+ export async function promptRemovalTargets(resourceName, targets, options) {
98
+ if (targets.length === 0)
99
+ return [];
100
+ if (targets.length === 1 || options?.skipPrompt)
101
+ return targets;
102
+ if (!isInteractiveTerminal()) {
103
+ return targets;
104
+ }
105
+ const { checkbox } = await import('@inquirer/prompts');
106
+ try {
107
+ const selected = await checkbox({
108
+ message: `Select targets to remove '${resourceName}' from`,
109
+ choices: targets.map((t) => ({
110
+ value: t,
111
+ name: t.label,
112
+ checked: true,
113
+ })),
114
+ });
115
+ return selected;
116
+ }
117
+ catch (err) {
118
+ if (isPromptCancelled(err)) {
119
+ return [];
120
+ }
121
+ throw err;
122
+ }
123
+ }
92
124
  /**
93
125
  * Format a path for display, using ~ for home directory
94
126
  */