@swarmify/agents-cli 1.4.0 → 1.5.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.
package/dist/index.js CHANGED
@@ -3,6 +3,11 @@ import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  import { checkbox, confirm, select } from '@inquirer/prompts';
6
+ function isPromptCancelled(err) {
7
+ return err instanceof Error && (err.name === 'ExitPromptError' ||
8
+ err.message.includes('force closed') ||
9
+ err.message.includes('User force closed'));
10
+ }
6
11
  import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, SKILLS_CAPABLE_AGENTS, HOOKS_CAPABLE_AGENTS, getAllCliStates, isCliInstalled, getCliVersion, isMcpRegistered, registerMcp, unregisterMcp, listInstalledMcpsWithScope, promoteMcpToUser, } from './lib/agents.js';
7
12
  import { readManifest, writeManifest, createDefaultManifest, MANIFEST_FILENAME, } from './lib/manifest.js';
8
13
  import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
@@ -195,24 +200,65 @@ program
195
200
  }
196
201
  console.log();
197
202
  });
203
+ // =============================================================================
204
+ // PULL COMMAND
205
+ // =============================================================================
206
+ // Agent name aliases for flexible input
207
+ const AGENT_NAME_ALIASES = {
208
+ claude: 'claude',
209
+ 'claude-code': 'claude',
210
+ cc: 'claude',
211
+ codex: 'codex',
212
+ 'openai-codex': 'codex',
213
+ cx: 'codex',
214
+ gemini: 'gemini',
215
+ 'gemini-cli': 'gemini',
216
+ gx: 'gemini',
217
+ cursor: 'cursor',
218
+ 'cursor-agent': 'cursor',
219
+ cr: 'cursor',
220
+ opencode: 'opencode',
221
+ oc: 'opencode',
222
+ };
223
+ function resolveAgentName(input) {
224
+ return AGENT_NAME_ALIASES[input.toLowerCase()] || null;
225
+ }
226
+ function isAgentName(input) {
227
+ return resolveAgentName(input) !== null;
228
+ }
198
229
  program
199
- .command('pull [source]')
230
+ .command('pull [source] [agent]')
200
231
  .description('Pull and sync from remote .agents repo')
201
- .option('-y, --yes', 'Skip interactive prompts')
202
- .option('-f, --force', 'Overwrite existing resources without asking')
232
+ .option('-y, --yes', 'Auto-confirm and skip all conflicts')
233
+ .option('-f, --force', 'Auto-confirm and overwrite all conflicts')
203
234
  .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
204
235
  .option('--dry-run', 'Show what would change')
205
236
  .option('--skip-clis', 'Skip CLI version sync')
206
237
  .option('--skip-mcp', 'Skip MCP registration')
207
- .action(async (source, options) => {
238
+ .action(async (arg1, arg2, options) => {
239
+ // Parse source and agent filter from positional args
240
+ let targetSource;
241
+ let agentFilter;
242
+ if (arg1) {
243
+ if (isAgentName(arg1)) {
244
+ // agents pull claude
245
+ agentFilter = resolveAgentName(arg1);
246
+ }
247
+ else {
248
+ // agents pull gh:user/repo [agent]
249
+ targetSource = arg1;
250
+ if (arg2 && isAgentName(arg2)) {
251
+ agentFilter = resolveAgentName(arg2);
252
+ }
253
+ }
254
+ }
208
255
  const scopeName = options.scope;
209
256
  const meta = readState();
210
257
  const existingScope = meta.scopes[scopeName];
211
258
  // Try: 1) provided source, 2) existing scope source, 3) fall back to system scope
212
- let targetSource = source || existingScope?.source;
259
+ targetSource = targetSource || existingScope?.source;
213
260
  let effectiveScope = scopeName;
214
261
  if (!targetSource && scopeName === 'user') {
215
- // Fall back to system scope if user scope has no source
216
262
  const systemScope = meta.scopes['system'];
217
263
  if (systemScope?.source) {
218
264
  targetSource = systemScope.source;
@@ -221,13 +267,19 @@ program
221
267
  }
222
268
  }
223
269
  if (!targetSource) {
224
- console.log(chalk.red(`No source specified for scope '${scopeName}'.`));
225
- const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
226
- console.log(chalk.gray(` Usage: agents pull <source>${scopeHint}`));
227
- console.log(chalk.gray(' Example: agents pull gh:username/.agents'));
228
- process.exit(1);
270
+ if (scopeName === 'user' && Object.keys(meta.scopes).length === 0) {
271
+ console.log(chalk.gray(`First run detected. Initializing from ${DEFAULT_SYSTEM_REPO}...\n`));
272
+ targetSource = DEFAULT_SYSTEM_REPO;
273
+ effectiveScope = 'system';
274
+ }
275
+ else {
276
+ console.log(chalk.red(`No source specified for scope '${scopeName}'.`));
277
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
278
+ console.log(chalk.gray(` Usage: agents pull <source>${scopeHint}`));
279
+ console.log(chalk.gray(' Example: agents pull gh:username/.agents'));
280
+ process.exit(1);
281
+ }
229
282
  }
230
- // Prevent modification of readonly scopes (but allow syncing from them)
231
283
  const targetScopeConfig = meta.scopes[effectiveScope];
232
284
  const isReadonly = targetScopeConfig?.readonly || effectiveScope === 'system';
233
285
  const isUserScope = effectiveScope === 'user';
@@ -241,64 +293,18 @@ program
241
293
  console.log(chalk.yellow(`No ${MANIFEST_FILENAME} found in repository`));
242
294
  }
243
295
  // Discover all assets
244
- const commands = discoverCommands(localPath);
245
- const skills = discoverSkillsFromRepo(localPath);
296
+ const allCommands = discoverCommands(localPath);
297
+ const allSkills = discoverSkillsFromRepo(localPath);
246
298
  const discoveredHooks = discoverHooksFromRepo(localPath);
247
- const totalHooks = discoveredHooks.shared.length +
248
- Object.values(discoveredHooks.agentSpecific).reduce((sum, arr) => sum + arr.length, 0);
249
- console.log(chalk.bold(`\nDiscovered assets:\n`));
250
- if (commands.length > 0) {
251
- console.log(` Commands: ${commands.length}`);
252
- for (const command of commands.slice(0, 5)) {
253
- const src = command.isShared ? 'shared' : command.agentSpecific;
254
- console.log(` ${chalk.cyan(command.name.padEnd(18))} ${chalk.gray(src)}`);
255
- }
256
- if (commands.length > 5) {
257
- console.log(chalk.gray(` ... and ${commands.length - 5} more`));
258
- }
259
- }
260
- if (skills.length > 0) {
261
- console.log(` Skills: ${skills.length}`);
262
- for (const skill of skills.slice(0, 5)) {
263
- console.log(` ${chalk.cyan(skill.name.padEnd(18))} ${chalk.gray(skill.metadata.description || '')}`);
264
- }
265
- if (skills.length > 5) {
266
- console.log(chalk.gray(` ... and ${skills.length - 5} more`));
267
- }
268
- }
269
- if (totalHooks > 0) {
270
- console.log(` Hooks: ${totalHooks}`);
271
- for (const name of discoveredHooks.shared.slice(0, 3)) {
272
- console.log(` ${chalk.cyan(name.padEnd(18))} ${chalk.gray('shared')}`);
273
- }
274
- for (const [agentId, hooks] of Object.entries(discoveredHooks.agentSpecific)) {
275
- for (const name of hooks.slice(0, 2)) {
276
- console.log(` ${chalk.cyan(name.padEnd(18))} ${chalk.gray(agentId)}`);
277
- }
278
- }
279
- if (totalHooks > 5) {
280
- console.log(chalk.gray(` ... and more`));
281
- }
282
- }
283
- const mcpCount = manifest?.mcp ? Object.keys(manifest.mcp).length : 0;
284
- if (mcpCount > 0) {
285
- console.log(` MCP Servers: ${mcpCount}`);
286
- for (const name of Object.keys(manifest.mcp).slice(0, 5)) {
287
- console.log(` ${chalk.cyan(name)}`);
288
- }
289
- if (mcpCount > 5) {
290
- console.log(chalk.gray(` ... and ${mcpCount - 5} more`));
291
- }
292
- }
293
- if (options.dryRun) {
294
- console.log(chalk.yellow('\nDry run - no changes made'));
295
- return;
296
- }
299
+ // Determine which agents to sync
297
300
  let selectedAgents;
298
- let method;
299
- if (options.yes) {
301
+ if (agentFilter) {
302
+ // Single agent filter
303
+ selectedAgents = [agentFilter];
304
+ console.log(chalk.gray(`\nFiltering for ${AGENTS[agentFilter].name} only\n`));
305
+ }
306
+ else if (options.yes || options.force) {
300
307
  selectedAgents = (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']);
301
- method = manifest?.defaults?.method || 'symlink';
302
308
  }
303
309
  else {
304
310
  const installedAgents = ALL_AGENT_IDS.filter((id) => isCliInstalled(id) || id === 'cursor');
@@ -310,225 +316,323 @@ program
310
316
  checked: (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']).includes(id),
311
317
  })),
312
318
  });
313
- method = await select({
314
- message: 'Installation method:',
315
- choices: [
316
- { name: 'Symlink (updates automatically)', value: 'symlink' },
317
- { name: 'Copy (independent files)', value: 'copy' },
318
- ],
319
- default: manifest?.defaults?.method || 'symlink',
320
- });
321
319
  }
322
- // Detect conflicts
323
- const conflicts = [];
324
- // Check command conflicts
325
- for (const command of commands) {
326
- for (const agentId of selectedAgents) {
327
- if (!isCliInstalled(agentId) && agentId !== 'cursor')
328
- continue;
320
+ // Filter agents to only installed ones (plus cursor which doesn't need CLI)
321
+ selectedAgents = selectedAgents.filter((id) => isCliInstalled(id) || id === 'cursor');
322
+ if (selectedAgents.length === 0) {
323
+ console.log(chalk.yellow('\nNo agents selected or installed. Nothing to sync.'));
324
+ return;
325
+ }
326
+ // Build resource items with conflict detection
327
+ const newItems = [];
328
+ const existingItems = [];
329
+ // Process commands
330
+ for (const command of allCommands) {
331
+ const applicableAgents = selectedAgents.filter((agentId) => {
329
332
  const sourcePath = resolveCommandSource(localPath, command.name, agentId);
330
- if (sourcePath && commandExists(agentId, command.name)) {
331
- conflicts.push({ type: 'command', name: command.name, agent: agentId });
332
- }
333
+ return sourcePath !== null;
334
+ });
335
+ if (applicableAgents.length === 0)
336
+ continue;
337
+ const conflictingAgents = applicableAgents.filter((agentId) => commandExists(agentId, command.name));
338
+ const newAgents = applicableAgents.filter((agentId) => !commandExists(agentId, command.name));
339
+ if (conflictingAgents.length > 0) {
340
+ existingItems.push({ type: 'command', name: command.name, agents: conflictingAgents, isNew: false });
333
341
  }
334
- }
335
- // Check skill conflicts
336
- for (const skill of skills) {
337
- const skillAgents = SKILLS_CAPABLE_AGENTS.filter((id) => selectedAgents.includes(id) && (isCliInstalled(id) || id === 'cursor'));
338
- for (const agentId of skillAgents) {
339
- if (skillExists(agentId, skill.name)) {
340
- conflicts.push({ type: 'skill', name: skill.name, agent: agentId });
341
- }
342
+ if (newAgents.length > 0) {
343
+ newItems.push({ type: 'command', name: command.name, agents: newAgents, isNew: true });
342
344
  }
343
345
  }
344
- // Check hook conflicts
345
- const hookAgents = selectedAgents.filter((id) => HOOKS_CAPABLE_AGENTS.includes(id) && isCliInstalled(id));
346
- for (const hookName of discoveredHooks.shared) {
347
- for (const agentId of hookAgents) {
348
- if (hookExists(agentId, hookName)) {
349
- conflicts.push({ type: 'hook', name: hookName, agent: agentId });
350
- }
346
+ // Process skills
347
+ const skillAgents = SKILLS_CAPABLE_AGENTS.filter((id) => selectedAgents.includes(id));
348
+ for (const skill of allSkills) {
349
+ const conflictingAgents = skillAgents.filter((agentId) => skillExists(agentId, skill.name));
350
+ const newAgents = skillAgents.filter((agentId) => !skillExists(agentId, skill.name));
351
+ if (conflictingAgents.length > 0) {
352
+ existingItems.push({ type: 'skill', name: skill.name, agents: conflictingAgents, isNew: false });
351
353
  }
352
- }
353
- for (const [agentIdStr, hookNames] of Object.entries(discoveredHooks.agentSpecific)) {
354
- const agentId = agentIdStr;
355
- if (hookAgents.includes(agentId)) {
356
- for (const hookName of hookNames) {
357
- if (hookExists(agentId, hookName)) {
358
- conflicts.push({ type: 'hook', name: hookName, agent: agentId });
359
- }
360
- }
354
+ if (newAgents.length > 0) {
355
+ newItems.push({ type: 'skill', name: skill.name, agents: newAgents, isNew: true });
361
356
  }
362
357
  }
363
- // Check MCP conflicts
358
+ // Process hooks
359
+ const hookAgents = selectedAgents.filter((id) => HOOKS_CAPABLE_AGENTS.includes(id) && isCliInstalled(id));
360
+ const allHookNames = [
361
+ ...discoveredHooks.shared,
362
+ ...Object.entries(discoveredHooks.agentSpecific)
363
+ .filter(([agentId]) => hookAgents.includes(agentId))
364
+ .flatMap(([_, hooks]) => hooks),
365
+ ];
366
+ const uniqueHookNames = [...new Set(allHookNames)];
367
+ for (const hookName of uniqueHookNames) {
368
+ const conflictingAgents = hookAgents.filter((agentId) => hookExists(agentId, hookName));
369
+ const newAgents = hookAgents.filter((agentId) => !hookExists(agentId, hookName));
370
+ if (conflictingAgents.length > 0) {
371
+ existingItems.push({ type: 'hook', name: hookName, agents: conflictingAgents, isNew: false });
372
+ }
373
+ if (newAgents.length > 0) {
374
+ newItems.push({ type: 'hook', name: hookName, agents: newAgents, isNew: true });
375
+ }
376
+ }
377
+ // Process MCPs
364
378
  if (!options.skipMcp && manifest?.mcp) {
365
379
  for (const [name, config] of Object.entries(manifest.mcp)) {
366
380
  if (config.transport === 'http' || !config.command)
367
381
  continue;
368
- for (const agentId of config.agents) {
369
- if (!isCliInstalled(agentId))
370
- continue;
371
- if (isMcpRegistered(agentId, name)) {
372
- conflicts.push({ type: 'mcp', name, agent: agentId });
373
- }
382
+ const mcpAgents = config.agents.filter((agentId) => selectedAgents.includes(agentId) && isCliInstalled(agentId));
383
+ if (mcpAgents.length === 0)
384
+ continue;
385
+ const conflictingAgents = mcpAgents.filter((agentId) => isMcpRegistered(agentId, name));
386
+ const newAgents = mcpAgents.filter((agentId) => !isMcpRegistered(agentId, name));
387
+ if (conflictingAgents.length > 0) {
388
+ existingItems.push({ type: 'mcp', name, agents: conflictingAgents, isNew: false });
389
+ }
390
+ if (newAgents.length > 0) {
391
+ newItems.push({ type: 'mcp', name, agents: newAgents, isNew: true });
374
392
  }
375
393
  }
376
394
  }
377
- // Handle conflicts
378
- let conflictAction = 'overwrite';
379
- if (conflicts.length > 0 && !options.force) {
380
- // Deduplicate conflicts by name (show each resource once, not per-agent)
381
- const uniqueConflicts = new Map();
382
- for (const c of conflicts) {
383
- const key = `${c.type}:${c.name}`;
384
- if (!uniqueConflicts.has(key)) {
385
- uniqueConflicts.set(key, c);
395
+ // Display overview
396
+ console.log(chalk.bold('\nOverview\n'));
397
+ const formatAgentList = (agents) => agents.map((id) => AGENTS[id].name).join(', ');
398
+ if (newItems.length > 0) {
399
+ console.log(chalk.green(' NEW (will install):\n'));
400
+ const byType = { command: [], skill: [], hook: [], mcp: [] };
401
+ for (const item of newItems)
402
+ byType[item.type].push(item);
403
+ if (byType.command.length > 0) {
404
+ console.log(` Commands:`);
405
+ for (const item of byType.command) {
406
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
386
407
  }
387
408
  }
388
- console.log(chalk.yellow(`\nFound ${uniqueConflicts.size} existing resources that would be overwritten:\n`));
389
- const byType = {
390
- command: [],
391
- skill: [],
392
- hook: [],
393
- mcp: [],
394
- };
395
- for (const c of uniqueConflicts.values()) {
396
- byType[c.type].push(c.name);
409
+ if (byType.skill.length > 0) {
410
+ console.log(` Skills:`);
411
+ for (const item of byType.skill) {
412
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
413
+ }
397
414
  }
415
+ if (byType.hook.length > 0) {
416
+ console.log(` Hooks:`);
417
+ for (const item of byType.hook) {
418
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
419
+ }
420
+ }
421
+ if (byType.mcp.length > 0) {
422
+ console.log(` MCP Servers:`);
423
+ for (const item of byType.mcp) {
424
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
425
+ }
426
+ }
427
+ console.log();
428
+ }
429
+ if (existingItems.length > 0) {
430
+ console.log(chalk.yellow(' EXISTING (conflicts):\n'));
431
+ const byType = { command: [], skill: [], hook: [], mcp: [] };
432
+ for (const item of existingItems)
433
+ byType[item.type].push(item);
398
434
  if (byType.command.length > 0) {
399
- console.log(` Commands: ${byType.command.join(', ')}`);
435
+ console.log(` Commands:`);
436
+ for (const item of byType.command) {
437
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
438
+ }
400
439
  }
401
440
  if (byType.skill.length > 0) {
402
- console.log(` Skills: ${byType.skill.join(', ')}`);
441
+ console.log(` Skills:`);
442
+ for (const item of byType.skill) {
443
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
444
+ }
403
445
  }
404
446
  if (byType.hook.length > 0) {
405
- console.log(` Hooks: ${byType.hook.join(', ')}`);
447
+ console.log(` Hooks:`);
448
+ for (const item of byType.hook) {
449
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
450
+ }
406
451
  }
407
452
  if (byType.mcp.length > 0) {
408
- console.log(` MCP Servers: ${byType.mcp.join(', ')}`);
453
+ console.log(` MCP Servers:`);
454
+ for (const item of byType.mcp) {
455
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
456
+ }
409
457
  }
410
- if (!options.yes) {
411
- conflictAction = await select({
412
- message: 'How do you want to handle existing resources?',
458
+ console.log();
459
+ }
460
+ if (newItems.length === 0 && existingItems.length === 0) {
461
+ console.log(chalk.gray(' Nothing to sync.\n'));
462
+ return;
463
+ }
464
+ if (options.dryRun) {
465
+ console.log(chalk.yellow('Dry run - no changes made'));
466
+ return;
467
+ }
468
+ // Confirmation prompt
469
+ if (!options.yes && !options.force) {
470
+ const proceed = await confirm({
471
+ message: 'Proceed with installation?',
472
+ default: true,
473
+ });
474
+ if (!proceed) {
475
+ console.log(chalk.yellow('\nSync cancelled'));
476
+ return;
477
+ }
478
+ }
479
+ // Determine installation method
480
+ let method;
481
+ if (options.yes || options.force) {
482
+ method = manifest?.defaults?.method || 'symlink';
483
+ }
484
+ else {
485
+ method = await select({
486
+ message: 'Installation method:',
487
+ choices: [
488
+ { name: 'Symlink (updates automatically)', value: 'symlink' },
489
+ { name: 'Copy (independent files)', value: 'copy' },
490
+ ],
491
+ default: manifest?.defaults?.method || 'symlink',
492
+ });
493
+ }
494
+ // Per-resource conflict decisions
495
+ const decisions = new Map();
496
+ if (existingItems.length > 0 && !options.force && !options.yes) {
497
+ console.log(chalk.bold('\nResolve conflicts:\n'));
498
+ for (const item of existingItems) {
499
+ const typeLabel = item.type.charAt(0).toUpperCase() + item.type.slice(1);
500
+ const agentList = formatAgentList(item.agents);
501
+ const decision = await select({
502
+ message: `${typeLabel} '${item.name}' exists (${agentList})`,
413
503
  choices: [
414
- { name: 'Overwrite all', value: 'overwrite' },
415
- { name: 'Skip existing (only install new)', value: 'skip' },
416
- { name: 'Cancel', value: 'cancel' },
504
+ { name: 'Overwrite', value: 'overwrite' },
505
+ { name: 'Skip', value: 'skip' },
506
+ { name: 'Cancel all', value: 'cancel' },
417
507
  ],
418
508
  });
419
- if (conflictAction === 'cancel') {
509
+ if (decision === 'cancel') {
420
510
  console.log(chalk.yellow('\nSync cancelled'));
421
511
  return;
422
512
  }
513
+ decisions.set(`${item.type}:${item.name}`, decision);
514
+ }
515
+ }
516
+ else if (options.force) {
517
+ // Force mode: overwrite all
518
+ for (const item of existingItems) {
519
+ decisions.set(`${item.type}:${item.name}`, 'overwrite');
423
520
  }
424
521
  }
425
- const skipExisting = conflictAction === 'skip';
426
- const conflictNames = new Set(conflicts.map((c) => `${c.type}:${c.name}:${c.agent}`));
522
+ else if (options.yes) {
523
+ // Yes mode: skip all conflicts
524
+ for (const item of existingItems) {
525
+ decisions.set(`${item.type}:${item.name}`, 'skip');
526
+ }
527
+ }
528
+ // Install new items (no conflicts)
529
+ console.log();
530
+ let installed = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
531
+ let skipped = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
427
532
  // Install commands
428
- const installSpinner = ora('Installing commands...').start();
429
- let installed = 0;
430
- let skipped = 0;
431
- for (const command of commands) {
432
- for (const agentId of selectedAgents) {
433
- if (!isCliInstalled(agentId) && agentId !== 'cursor')
434
- continue;
435
- const sourcePath = resolveCommandSource(localPath, command.name, agentId);
436
- if (!sourcePath)
437
- continue;
438
- const conflictKey = `command:${command.name}:${agentId}`;
439
- if (skipExisting && conflictNames.has(conflictKey)) {
440
- skipped++;
441
- continue;
533
+ const cmdSpinner = ora('Installing commands...').start();
534
+ for (const item of [...newItems, ...existingItems].filter((i) => i.type === 'command')) {
535
+ const decision = item.isNew ? 'overwrite' : decisions.get(`command:${item.name}`);
536
+ if (decision === 'skip') {
537
+ skipped.commands++;
538
+ continue;
539
+ }
540
+ for (const agentId of item.agents) {
541
+ const sourcePath = resolveCommandSource(localPath, item.name, agentId);
542
+ if (sourcePath) {
543
+ installCommand(sourcePath, agentId, item.name, method);
544
+ installed.commands++;
442
545
  }
443
- installCommand(sourcePath, agentId, command.name, method);
444
- installed++;
445
546
  }
446
547
  }
447
- if (skipped > 0) {
448
- installSpinner.succeed(`Installed ${installed} commands (skipped ${skipped} existing)`);
548
+ if (skipped.commands > 0) {
549
+ cmdSpinner.succeed(`Installed ${installed.commands} commands (skipped ${skipped.commands})`);
550
+ }
551
+ else if (installed.commands > 0) {
552
+ cmdSpinner.succeed(`Installed ${installed.commands} commands`);
449
553
  }
450
554
  else {
451
- installSpinner.succeed(`Installed ${installed} command instances`);
555
+ cmdSpinner.info('No commands to install');
452
556
  }
453
557
  // Install skills
454
- if (skills.length > 0) {
558
+ const skillItems = [...newItems, ...existingItems].filter((i) => i.type === 'skill');
559
+ if (skillItems.length > 0) {
455
560
  const skillSpinner = ora('Installing skills...').start();
456
- let skillsInstalled = 0;
457
- let skillsSkipped = 0;
458
- for (const skill of skills) {
459
- const skillAgents = SKILLS_CAPABLE_AGENTS.filter((id) => selectedAgents.includes(id) && (isCliInstalled(id) || id === 'cursor'));
460
- // Check if should skip
461
- const shouldSkip = skipExisting && skillAgents.some((agentId) => conflictNames.has(`skill:${skill.name}:${agentId}`));
462
- if (shouldSkip) {
463
- skillsSkipped++;
561
+ for (const item of skillItems) {
562
+ const decision = item.isNew ? 'overwrite' : decisions.get(`skill:${item.name}`);
563
+ if (decision === 'skip') {
564
+ skipped.skills++;
464
565
  continue;
465
566
  }
466
- if (skillAgents.length > 0) {
467
- const result = installSkill(skill.path, skill.name, skillAgents);
567
+ const skill = allSkills.find((s) => s.name === item.name);
568
+ if (skill) {
569
+ const result = installSkill(skill.path, skill.name, item.agents);
468
570
  if (result.success)
469
- skillsInstalled++;
571
+ installed.skills++;
470
572
  }
471
573
  }
472
- if (skillsSkipped > 0) {
473
- skillSpinner.succeed(`Installed ${skillsInstalled} skills (skipped ${skillsSkipped} existing)`);
574
+ if (skipped.skills > 0) {
575
+ skillSpinner.succeed(`Installed ${installed.skills} skills (skipped ${skipped.skills})`);
576
+ }
577
+ else if (installed.skills > 0) {
578
+ skillSpinner.succeed(`Installed ${installed.skills} skills`);
474
579
  }
475
580
  else {
476
- skillSpinner.succeed(`Installed ${skillsInstalled} skills`);
581
+ skillSpinner.info('No skills to install');
477
582
  }
478
583
  }
479
584
  // Install hooks
480
- if (totalHooks > 0 && hookAgents.length > 0) {
585
+ const hookItems = [...newItems, ...existingItems].filter((i) => i.type === 'hook');
586
+ if (hookItems.length > 0 && hookAgents.length > 0) {
481
587
  const hookSpinner = ora('Installing hooks...').start();
482
- // Note: installHooks handles its own conflict detection internally
483
- // For now, pass the full list - in the future we could filter
484
588
  const result = await installHooks(localPath, hookAgents, { scope: 'user' });
485
589
  hookSpinner.succeed(`Installed ${result.installed.length} hooks`);
486
590
  }
487
591
  // Register MCP servers
488
- if (!options.skipMcp && manifest?.mcp) {
592
+ const mcpItems = [...newItems, ...existingItems].filter((i) => i.type === 'mcp');
593
+ if (mcpItems.length > 0 && manifest?.mcp) {
489
594
  const mcpSpinner = ora('Registering MCP servers...').start();
490
- let registered = 0;
491
- let mcpSkipped = 0;
492
- for (const [name, config] of Object.entries(manifest.mcp)) {
493
- // Skip HTTP transport MCPs for now (need different registration)
494
- if (config.transport === 'http' || !config.command)
595
+ for (const item of mcpItems) {
596
+ const decision = item.isNew ? 'overwrite' : decisions.get(`mcp:${item.name}`);
597
+ if (decision === 'skip') {
598
+ skipped.mcps++;
495
599
  continue;
496
- for (const agentId of config.agents) {
497
- if (!isCliInstalled(agentId))
498
- continue;
499
- const conflictKey = `mcp:${name}:${agentId}`;
500
- if (conflictNames.has(conflictKey)) {
501
- if (skipExisting) {
502
- mcpSkipped++;
503
- continue;
504
- }
505
- // If overwriting, unregister first then re-register
506
- unregisterMcp(agentId, name);
600
+ }
601
+ const config = manifest.mcp[item.name];
602
+ if (!config || !config.command)
603
+ continue;
604
+ for (const agentId of item.agents) {
605
+ if (!item.isNew) {
606
+ unregisterMcp(agentId, item.name);
507
607
  }
508
- const result = registerMcp(agentId, name, config.command, config.scope);
608
+ const result = registerMcp(agentId, item.name, config.command, config.scope);
509
609
  if (result.success)
510
- registered++;
610
+ installed.mcps++;
511
611
  }
512
612
  }
513
- if (mcpSkipped > 0) {
514
- mcpSpinner.succeed(`Registered ${registered} MCP servers (skipped ${mcpSkipped} existing)`);
613
+ if (skipped.mcps > 0) {
614
+ mcpSpinner.succeed(`Registered ${installed.mcps} MCP servers (skipped ${skipped.mcps})`);
615
+ }
616
+ else if (installed.mcps > 0) {
617
+ mcpSpinner.succeed(`Registered ${installed.mcps} MCP servers`);
515
618
  }
516
619
  else {
517
- mcpSpinner.succeed(`Registered ${registered} MCP servers`);
620
+ mcpSpinner.info('No MCP servers to register');
518
621
  }
519
622
  }
520
- // Sync CLI versions (user scope only, unless --skip-clis)
623
+ // Sync CLI versions (user scope only)
521
624
  if (isUserScope && !options.skipClis && manifest?.clis) {
522
625
  const cliSpinner = ora('Checking CLI versions...').start();
523
626
  const cliUpdates = [];
524
627
  for (const [agentIdStr, cliConfig] of Object.entries(manifest.clis)) {
525
628
  const agentId = agentIdStr;
629
+ if (agentFilter && agentId !== agentFilter)
630
+ continue;
526
631
  const agent = AGENTS[agentId];
527
632
  if (!agent || !cliConfig.package)
528
633
  continue;
529
634
  const currentVersion = getCliVersion(agentId);
530
635
  const targetVersion = cliConfig.version;
531
- // Skip if same version or if target is "latest" and CLI is installed
532
636
  if (currentVersion === targetVersion)
533
637
  continue;
534
638
  if (targetVersion === 'latest' && currentVersion)
@@ -546,7 +650,7 @@ program
546
650
  cliSpinner.succeed('CLI versions match');
547
651
  }
548
652
  }
549
- // Update scope config (only if not readonly)
653
+ // Update scope config
550
654
  if (!isReadonly) {
551
655
  const priority = getScopePriority(effectiveScope);
552
656
  setScope(effectiveScope, {
@@ -561,6 +665,10 @@ program
561
665
  console.log(chalk.green(`\nSync complete from ${effectiveScope} scope`));
562
666
  }
563
667
  catch (err) {
668
+ if (isPromptCancelled(err)) {
669
+ console.log(chalk.yellow('\nCancelled'));
670
+ process.exit(0);
671
+ }
564
672
  spinner.fail('Failed to sync');
565
673
  console.error(chalk.red(err.message));
566
674
  process.exit(1);
@@ -1968,7 +2076,7 @@ program
1968
2076
  const spinner = ora('Checking for updates...').start();
1969
2077
  try {
1970
2078
  // Get current version from package.json
1971
- const currentVersion = program.version();
2079
+ const currentVersion = program.version() || '0.0.0';
1972
2080
  // Fetch latest version from npm
1973
2081
  const response = await fetch('https://registry.npmjs.org/@swarmify/agents-cli/latest');
1974
2082
  if (!response.ok) {
@@ -1980,7 +2088,7 @@ program
1980
2088
  spinner.succeed(`Already on latest version (${currentVersion})`);
1981
2089
  return;
1982
2090
  }
1983
- spinner.text = `Upgrading from ${currentVersion} to ${latestVersion}...`;
2091
+ spinner.text = `Upgrading to ${latestVersion}...`;
1984
2092
  // Detect package manager
1985
2093
  const { execSync } = await import('child_process');
1986
2094
  let cmd;
@@ -2009,8 +2117,11 @@ program
2009
2117
  cmd = 'npm install -g @swarmify/agents-cli@latest';
2010
2118
  }
2011
2119
  }
2012
- execSync(cmd, { stdio: 'inherit' });
2120
+ // Run silently (suppress npm/bun output)
2121
+ execSync(cmd, { stdio: 'pipe' });
2013
2122
  spinner.succeed(`Upgraded to ${latestVersion}`);
2123
+ // Show what's new from changelog
2124
+ await showWhatsNew(currentVersion, latestVersion);
2014
2125
  }
2015
2126
  catch (err) {
2016
2127
  spinner.fail('Upgrade failed');
@@ -2019,5 +2130,67 @@ program
2019
2130
  process.exit(1);
2020
2131
  }
2021
2132
  });
2133
+ async function showWhatsNew(fromVersion, toVersion) {
2134
+ try {
2135
+ // Fetch changelog from npm package
2136
+ const response = await fetch(`https://unpkg.com/@swarmify/agents-cli@${toVersion}/CHANGELOG.md`);
2137
+ if (!response.ok)
2138
+ return;
2139
+ const changelog = await response.text();
2140
+ const lines = changelog.split('\n');
2141
+ // Parse changelog to find relevant sections
2142
+ const relevantChanges = [];
2143
+ let inRelevantSection = false;
2144
+ let currentVersion = '';
2145
+ for (const line of lines) {
2146
+ // Check for version header (## 1.5.0)
2147
+ const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
2148
+ if (versionMatch) {
2149
+ currentVersion = versionMatch[1];
2150
+ // Include versions newer than fromVersion
2151
+ const isNewer = currentVersion !== fromVersion &&
2152
+ compareVersions(currentVersion, fromVersion) > 0;
2153
+ inRelevantSection = isNewer;
2154
+ if (inRelevantSection) {
2155
+ relevantChanges.push('');
2156
+ relevantChanges.push(chalk.bold(`v${currentVersion}`));
2157
+ }
2158
+ continue;
2159
+ }
2160
+ if (inRelevantSection && line.trim()) {
2161
+ // Format the line
2162
+ if (line.startsWith('**') && line.endsWith('**')) {
2163
+ // Section header like **Pull command redesign**
2164
+ relevantChanges.push(chalk.cyan(line.replace(/\*\*/g, '')));
2165
+ }
2166
+ else if (line.startsWith('- ')) {
2167
+ // Bullet point
2168
+ relevantChanges.push(chalk.gray(` ${line}`));
2169
+ }
2170
+ }
2171
+ }
2172
+ if (relevantChanges.length > 0) {
2173
+ console.log(chalk.bold("\nWhat's new:\n"));
2174
+ for (const line of relevantChanges) {
2175
+ console.log(line);
2176
+ }
2177
+ console.log();
2178
+ }
2179
+ }
2180
+ catch {
2181
+ // Silently ignore changelog fetch errors
2182
+ }
2183
+ }
2184
+ function compareVersions(a, b) {
2185
+ const partsA = a.split('.').map(Number);
2186
+ const partsB = b.split('.').map(Number);
2187
+ for (let i = 0; i < 3; i++) {
2188
+ if (partsA[i] > partsB[i])
2189
+ return 1;
2190
+ if (partsA[i] < partsB[i])
2191
+ return -1;
2192
+ }
2193
+ return 0;
2194
+ }
2022
2195
  program.parse();
2023
2196
  //# sourceMappingURL=index.js.map