@swarmify/agents-cli 1.4.0 → 1.5.1

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
@@ -2,7 +2,20 @@
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { fileURLToPath } from 'url';
5
8
  import { checkbox, confirm, select } from '@inquirer/prompts';
9
+ // Get version from package.json
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
12
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
13
+ const VERSION = packageJson.version;
14
+ function isPromptCancelled(err) {
15
+ return err instanceof Error && (err.name === 'ExitPromptError' ||
16
+ err.message.includes('force closed') ||
17
+ err.message.includes('User force closed'));
18
+ }
6
19
  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
20
  import { readManifest, writeManifest, createDefaultManifest, MANIFEST_FILENAME, } from './lib/manifest.js';
8
21
  import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
@@ -61,7 +74,35 @@ function getScopeLocalPath(scopeName) {
61
74
  program
62
75
  .name('agents')
63
76
  .description('Dotfiles manager for AI coding agents')
64
- .version('1.0.0');
77
+ .version(VERSION);
78
+ // Check for updates (non-blocking, shows hint if update available)
79
+ async function checkForUpdates() {
80
+ try {
81
+ const response = await fetch('https://registry.npmjs.org/@swarmify/agents-cli/latest', {
82
+ signal: AbortSignal.timeout(2000), // 2s timeout
83
+ });
84
+ if (!response.ok)
85
+ return;
86
+ const data = (await response.json());
87
+ const latestVersion = data.version;
88
+ if (latestVersion !== VERSION && compareVersions(latestVersion, VERSION) > 0) {
89
+ console.log(chalk.yellow(`\nUpdate available: ${VERSION} -> ${latestVersion}`));
90
+ console.log(chalk.gray(`Run: agents upgrade\n`));
91
+ }
92
+ }
93
+ catch {
94
+ // Silently ignore - don't block CLI usage
95
+ }
96
+ }
97
+ // Run update check after command completes (non-blocking)
98
+ program.hook('postAction', async () => {
99
+ // Don't check on upgrade command itself
100
+ const args = process.argv.slice(2);
101
+ if (args[0] === 'upgrade' || args[0] === '--version' || args[0] === '-V' || args[0] === '--help' || args[0] === '-h') {
102
+ return;
103
+ }
104
+ await checkForUpdates();
105
+ });
65
106
  // =============================================================================
66
107
  // STATUS COMMAND
67
108
  // =============================================================================
@@ -195,24 +236,65 @@ program
195
236
  }
196
237
  console.log();
197
238
  });
239
+ // =============================================================================
240
+ // PULL COMMAND
241
+ // =============================================================================
242
+ // Agent name aliases for flexible input
243
+ const AGENT_NAME_ALIASES = {
244
+ claude: 'claude',
245
+ 'claude-code': 'claude',
246
+ cc: 'claude',
247
+ codex: 'codex',
248
+ 'openai-codex': 'codex',
249
+ cx: 'codex',
250
+ gemini: 'gemini',
251
+ 'gemini-cli': 'gemini',
252
+ gx: 'gemini',
253
+ cursor: 'cursor',
254
+ 'cursor-agent': 'cursor',
255
+ cr: 'cursor',
256
+ opencode: 'opencode',
257
+ oc: 'opencode',
258
+ };
259
+ function resolveAgentName(input) {
260
+ return AGENT_NAME_ALIASES[input.toLowerCase()] || null;
261
+ }
262
+ function isAgentName(input) {
263
+ return resolveAgentName(input) !== null;
264
+ }
198
265
  program
199
- .command('pull [source]')
266
+ .command('pull [source] [agent]')
200
267
  .description('Pull and sync from remote .agents repo')
201
- .option('-y, --yes', 'Skip interactive prompts')
202
- .option('-f, --force', 'Overwrite existing resources without asking')
268
+ .option('-y, --yes', 'Auto-confirm and skip all conflicts')
269
+ .option('-f, --force', 'Auto-confirm and overwrite all conflicts')
203
270
  .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
204
271
  .option('--dry-run', 'Show what would change')
205
272
  .option('--skip-clis', 'Skip CLI version sync')
206
273
  .option('--skip-mcp', 'Skip MCP registration')
207
- .action(async (source, options) => {
274
+ .action(async (arg1, arg2, options) => {
275
+ // Parse source and agent filter from positional args
276
+ let targetSource;
277
+ let agentFilter;
278
+ if (arg1) {
279
+ if (isAgentName(arg1)) {
280
+ // agents pull claude
281
+ agentFilter = resolveAgentName(arg1);
282
+ }
283
+ else {
284
+ // agents pull gh:user/repo [agent]
285
+ targetSource = arg1;
286
+ if (arg2 && isAgentName(arg2)) {
287
+ agentFilter = resolveAgentName(arg2);
288
+ }
289
+ }
290
+ }
208
291
  const scopeName = options.scope;
209
292
  const meta = readState();
210
293
  const existingScope = meta.scopes[scopeName];
211
294
  // Try: 1) provided source, 2) existing scope source, 3) fall back to system scope
212
- let targetSource = source || existingScope?.source;
295
+ targetSource = targetSource || existingScope?.source;
213
296
  let effectiveScope = scopeName;
214
297
  if (!targetSource && scopeName === 'user') {
215
- // Fall back to system scope if user scope has no source
216
298
  const systemScope = meta.scopes['system'];
217
299
  if (systemScope?.source) {
218
300
  targetSource = systemScope.source;
@@ -221,13 +303,19 @@ program
221
303
  }
222
304
  }
223
305
  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);
306
+ if (scopeName === 'user' && Object.keys(meta.scopes).length === 0) {
307
+ console.log(chalk.gray(`First run detected. Initializing from ${DEFAULT_SYSTEM_REPO}...\n`));
308
+ targetSource = DEFAULT_SYSTEM_REPO;
309
+ effectiveScope = 'system';
310
+ }
311
+ else {
312
+ console.log(chalk.red(`No source specified for scope '${scopeName}'.`));
313
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
314
+ console.log(chalk.gray(` Usage: agents pull <source>${scopeHint}`));
315
+ console.log(chalk.gray(' Example: agents pull gh:username/.agents'));
316
+ process.exit(1);
317
+ }
229
318
  }
230
- // Prevent modification of readonly scopes (but allow syncing from them)
231
319
  const targetScopeConfig = meta.scopes[effectiveScope];
232
320
  const isReadonly = targetScopeConfig?.readonly || effectiveScope === 'system';
233
321
  const isUserScope = effectiveScope === 'user';
@@ -241,64 +329,18 @@ program
241
329
  console.log(chalk.yellow(`No ${MANIFEST_FILENAME} found in repository`));
242
330
  }
243
331
  // Discover all assets
244
- const commands = discoverCommands(localPath);
245
- const skills = discoverSkillsFromRepo(localPath);
332
+ const allCommands = discoverCommands(localPath);
333
+ const allSkills = discoverSkillsFromRepo(localPath);
246
334
  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
- }
335
+ // Determine which agents to sync
297
336
  let selectedAgents;
298
- let method;
299
- if (options.yes) {
337
+ if (agentFilter) {
338
+ // Single agent filter
339
+ selectedAgents = [agentFilter];
340
+ console.log(chalk.gray(`\nFiltering for ${AGENTS[agentFilter].name} only\n`));
341
+ }
342
+ else if (options.yes || options.force) {
300
343
  selectedAgents = (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']);
301
- method = manifest?.defaults?.method || 'symlink';
302
344
  }
303
345
  else {
304
346
  const installedAgents = ALL_AGENT_IDS.filter((id) => isCliInstalled(id) || id === 'cursor');
@@ -310,225 +352,323 @@ program
310
352
  checked: (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']).includes(id),
311
353
  })),
312
354
  });
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
355
  }
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;
356
+ // Filter agents to only installed ones (plus cursor which doesn't need CLI)
357
+ selectedAgents = selectedAgents.filter((id) => isCliInstalled(id) || id === 'cursor');
358
+ if (selectedAgents.length === 0) {
359
+ console.log(chalk.yellow('\nNo agents selected or installed. Nothing to sync.'));
360
+ return;
361
+ }
362
+ // Build resource items with conflict detection
363
+ const newItems = [];
364
+ const existingItems = [];
365
+ // Process commands
366
+ for (const command of allCommands) {
367
+ const applicableAgents = selectedAgents.filter((agentId) => {
329
368
  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
- }
369
+ return sourcePath !== null;
370
+ });
371
+ if (applicableAgents.length === 0)
372
+ continue;
373
+ const conflictingAgents = applicableAgents.filter((agentId) => commandExists(agentId, command.name));
374
+ const newAgents = applicableAgents.filter((agentId) => !commandExists(agentId, command.name));
375
+ if (conflictingAgents.length > 0) {
376
+ existingItems.push({ type: 'command', name: command.name, agents: conflictingAgents, isNew: false });
333
377
  }
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
- }
378
+ if (newAgents.length > 0) {
379
+ newItems.push({ type: 'command', name: command.name, agents: newAgents, isNew: true });
342
380
  }
343
381
  }
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
- }
382
+ // Process skills
383
+ const skillAgents = SKILLS_CAPABLE_AGENTS.filter((id) => selectedAgents.includes(id));
384
+ for (const skill of allSkills) {
385
+ const conflictingAgents = skillAgents.filter((agentId) => skillExists(agentId, skill.name));
386
+ const newAgents = skillAgents.filter((agentId) => !skillExists(agentId, skill.name));
387
+ if (conflictingAgents.length > 0) {
388
+ existingItems.push({ type: 'skill', name: skill.name, agents: conflictingAgents, isNew: false });
351
389
  }
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
- }
390
+ if (newAgents.length > 0) {
391
+ newItems.push({ type: 'skill', name: skill.name, agents: newAgents, isNew: true });
361
392
  }
362
393
  }
363
- // Check MCP conflicts
394
+ // Process hooks
395
+ const hookAgents = selectedAgents.filter((id) => HOOKS_CAPABLE_AGENTS.includes(id) && isCliInstalled(id));
396
+ const allHookNames = [
397
+ ...discoveredHooks.shared,
398
+ ...Object.entries(discoveredHooks.agentSpecific)
399
+ .filter(([agentId]) => hookAgents.includes(agentId))
400
+ .flatMap(([_, hooks]) => hooks),
401
+ ];
402
+ const uniqueHookNames = [...new Set(allHookNames)];
403
+ for (const hookName of uniqueHookNames) {
404
+ const conflictingAgents = hookAgents.filter((agentId) => hookExists(agentId, hookName));
405
+ const newAgents = hookAgents.filter((agentId) => !hookExists(agentId, hookName));
406
+ if (conflictingAgents.length > 0) {
407
+ existingItems.push({ type: 'hook', name: hookName, agents: conflictingAgents, isNew: false });
408
+ }
409
+ if (newAgents.length > 0) {
410
+ newItems.push({ type: 'hook', name: hookName, agents: newAgents, isNew: true });
411
+ }
412
+ }
413
+ // Process MCPs
364
414
  if (!options.skipMcp && manifest?.mcp) {
365
415
  for (const [name, config] of Object.entries(manifest.mcp)) {
366
416
  if (config.transport === 'http' || !config.command)
367
417
  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
- }
418
+ const mcpAgents = config.agents.filter((agentId) => selectedAgents.includes(agentId) && isCliInstalled(agentId));
419
+ if (mcpAgents.length === 0)
420
+ continue;
421
+ const conflictingAgents = mcpAgents.filter((agentId) => isMcpRegistered(agentId, name));
422
+ const newAgents = mcpAgents.filter((agentId) => !isMcpRegistered(agentId, name));
423
+ if (conflictingAgents.length > 0) {
424
+ existingItems.push({ type: 'mcp', name, agents: conflictingAgents, isNew: false });
425
+ }
426
+ if (newAgents.length > 0) {
427
+ newItems.push({ type: 'mcp', name, agents: newAgents, isNew: true });
374
428
  }
375
429
  }
376
430
  }
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);
431
+ // Display overview
432
+ console.log(chalk.bold('\nOverview\n'));
433
+ const formatAgentList = (agents) => agents.map((id) => AGENTS[id].name).join(', ');
434
+ if (newItems.length > 0) {
435
+ console.log(chalk.green(' NEW (will install):\n'));
436
+ const byType = { command: [], skill: [], hook: [], mcp: [] };
437
+ for (const item of newItems)
438
+ byType[item.type].push(item);
439
+ if (byType.command.length > 0) {
440
+ console.log(` Commands:`);
441
+ for (const item of byType.command) {
442
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
386
443
  }
387
444
  }
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);
445
+ if (byType.skill.length > 0) {
446
+ console.log(` Skills:`);
447
+ for (const item of byType.skill) {
448
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
449
+ }
397
450
  }
451
+ if (byType.hook.length > 0) {
452
+ console.log(` Hooks:`);
453
+ for (const item of byType.hook) {
454
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
455
+ }
456
+ }
457
+ if (byType.mcp.length > 0) {
458
+ console.log(` MCP Servers:`);
459
+ for (const item of byType.mcp) {
460
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
461
+ }
462
+ }
463
+ console.log();
464
+ }
465
+ if (existingItems.length > 0) {
466
+ console.log(chalk.yellow(' EXISTING (conflicts):\n'));
467
+ const byType = { command: [], skill: [], hook: [], mcp: [] };
468
+ for (const item of existingItems)
469
+ byType[item.type].push(item);
398
470
  if (byType.command.length > 0) {
399
- console.log(` Commands: ${byType.command.join(', ')}`);
471
+ console.log(` Commands:`);
472
+ for (const item of byType.command) {
473
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
474
+ }
400
475
  }
401
476
  if (byType.skill.length > 0) {
402
- console.log(` Skills: ${byType.skill.join(', ')}`);
477
+ console.log(` Skills:`);
478
+ for (const item of byType.skill) {
479
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
480
+ }
403
481
  }
404
482
  if (byType.hook.length > 0) {
405
- console.log(` Hooks: ${byType.hook.join(', ')}`);
483
+ console.log(` Hooks:`);
484
+ for (const item of byType.hook) {
485
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
486
+ }
406
487
  }
407
488
  if (byType.mcp.length > 0) {
408
- console.log(` MCP Servers: ${byType.mcp.join(', ')}`);
489
+ console.log(` MCP Servers:`);
490
+ for (const item of byType.mcp) {
491
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
492
+ }
409
493
  }
410
- if (!options.yes) {
411
- conflictAction = await select({
412
- message: 'How do you want to handle existing resources?',
494
+ console.log();
495
+ }
496
+ if (newItems.length === 0 && existingItems.length === 0) {
497
+ console.log(chalk.gray(' Nothing to sync.\n'));
498
+ return;
499
+ }
500
+ if (options.dryRun) {
501
+ console.log(chalk.yellow('Dry run - no changes made'));
502
+ return;
503
+ }
504
+ // Confirmation prompt
505
+ if (!options.yes && !options.force) {
506
+ const proceed = await confirm({
507
+ message: 'Proceed with installation?',
508
+ default: true,
509
+ });
510
+ if (!proceed) {
511
+ console.log(chalk.yellow('\nSync cancelled'));
512
+ return;
513
+ }
514
+ }
515
+ // Determine installation method
516
+ let method;
517
+ if (options.yes || options.force) {
518
+ method = manifest?.defaults?.method || 'symlink';
519
+ }
520
+ else {
521
+ method = await select({
522
+ message: 'Installation method:',
523
+ choices: [
524
+ { name: 'Symlink (updates automatically)', value: 'symlink' },
525
+ { name: 'Copy (independent files)', value: 'copy' },
526
+ ],
527
+ default: manifest?.defaults?.method || 'symlink',
528
+ });
529
+ }
530
+ // Per-resource conflict decisions
531
+ const decisions = new Map();
532
+ if (existingItems.length > 0 && !options.force && !options.yes) {
533
+ console.log(chalk.bold('\nResolve conflicts:\n'));
534
+ for (const item of existingItems) {
535
+ const typeLabel = item.type.charAt(0).toUpperCase() + item.type.slice(1);
536
+ const agentList = formatAgentList(item.agents);
537
+ const decision = await select({
538
+ message: `${typeLabel} '${item.name}' exists (${agentList})`,
413
539
  choices: [
414
- { name: 'Overwrite all', value: 'overwrite' },
415
- { name: 'Skip existing (only install new)', value: 'skip' },
416
- { name: 'Cancel', value: 'cancel' },
540
+ { name: 'Overwrite', value: 'overwrite' },
541
+ { name: 'Skip', value: 'skip' },
542
+ { name: 'Cancel all', value: 'cancel' },
417
543
  ],
418
544
  });
419
- if (conflictAction === 'cancel') {
545
+ if (decision === 'cancel') {
420
546
  console.log(chalk.yellow('\nSync cancelled'));
421
547
  return;
422
548
  }
549
+ decisions.set(`${item.type}:${item.name}`, decision);
423
550
  }
424
551
  }
425
- const skipExisting = conflictAction === 'skip';
426
- const conflictNames = new Set(conflicts.map((c) => `${c.type}:${c.name}:${c.agent}`));
552
+ else if (options.force) {
553
+ // Force mode: overwrite all
554
+ for (const item of existingItems) {
555
+ decisions.set(`${item.type}:${item.name}`, 'overwrite');
556
+ }
557
+ }
558
+ else if (options.yes) {
559
+ // Yes mode: skip all conflicts
560
+ for (const item of existingItems) {
561
+ decisions.set(`${item.type}:${item.name}`, 'skip');
562
+ }
563
+ }
564
+ // Install new items (no conflicts)
565
+ console.log();
566
+ let installed = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
567
+ let skipped = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
427
568
  // 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;
569
+ const cmdSpinner = ora('Installing commands...').start();
570
+ for (const item of [...newItems, ...existingItems].filter((i) => i.type === 'command')) {
571
+ const decision = item.isNew ? 'overwrite' : decisions.get(`command:${item.name}`);
572
+ if (decision === 'skip') {
573
+ skipped.commands++;
574
+ continue;
575
+ }
576
+ for (const agentId of item.agents) {
577
+ const sourcePath = resolveCommandSource(localPath, item.name, agentId);
578
+ if (sourcePath) {
579
+ installCommand(sourcePath, agentId, item.name, method);
580
+ installed.commands++;
442
581
  }
443
- installCommand(sourcePath, agentId, command.name, method);
444
- installed++;
445
582
  }
446
583
  }
447
- if (skipped > 0) {
448
- installSpinner.succeed(`Installed ${installed} commands (skipped ${skipped} existing)`);
584
+ if (skipped.commands > 0) {
585
+ cmdSpinner.succeed(`Installed ${installed.commands} commands (skipped ${skipped.commands})`);
586
+ }
587
+ else if (installed.commands > 0) {
588
+ cmdSpinner.succeed(`Installed ${installed.commands} commands`);
449
589
  }
450
590
  else {
451
- installSpinner.succeed(`Installed ${installed} command instances`);
591
+ cmdSpinner.info('No commands to install');
452
592
  }
453
593
  // Install skills
454
- if (skills.length > 0) {
594
+ const skillItems = [...newItems, ...existingItems].filter((i) => i.type === 'skill');
595
+ if (skillItems.length > 0) {
455
596
  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++;
597
+ for (const item of skillItems) {
598
+ const decision = item.isNew ? 'overwrite' : decisions.get(`skill:${item.name}`);
599
+ if (decision === 'skip') {
600
+ skipped.skills++;
464
601
  continue;
465
602
  }
466
- if (skillAgents.length > 0) {
467
- const result = installSkill(skill.path, skill.name, skillAgents);
603
+ const skill = allSkills.find((s) => s.name === item.name);
604
+ if (skill) {
605
+ const result = installSkill(skill.path, skill.name, item.agents);
468
606
  if (result.success)
469
- skillsInstalled++;
607
+ installed.skills++;
470
608
  }
471
609
  }
472
- if (skillsSkipped > 0) {
473
- skillSpinner.succeed(`Installed ${skillsInstalled} skills (skipped ${skillsSkipped} existing)`);
610
+ if (skipped.skills > 0) {
611
+ skillSpinner.succeed(`Installed ${installed.skills} skills (skipped ${skipped.skills})`);
612
+ }
613
+ else if (installed.skills > 0) {
614
+ skillSpinner.succeed(`Installed ${installed.skills} skills`);
474
615
  }
475
616
  else {
476
- skillSpinner.succeed(`Installed ${skillsInstalled} skills`);
617
+ skillSpinner.info('No skills to install');
477
618
  }
478
619
  }
479
620
  // Install hooks
480
- if (totalHooks > 0 && hookAgents.length > 0) {
621
+ const hookItems = [...newItems, ...existingItems].filter((i) => i.type === 'hook');
622
+ if (hookItems.length > 0 && hookAgents.length > 0) {
481
623
  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
624
  const result = await installHooks(localPath, hookAgents, { scope: 'user' });
485
625
  hookSpinner.succeed(`Installed ${result.installed.length} hooks`);
486
626
  }
487
627
  // Register MCP servers
488
- if (!options.skipMcp && manifest?.mcp) {
628
+ const mcpItems = [...newItems, ...existingItems].filter((i) => i.type === 'mcp');
629
+ if (mcpItems.length > 0 && manifest?.mcp) {
489
630
  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)
631
+ for (const item of mcpItems) {
632
+ const decision = item.isNew ? 'overwrite' : decisions.get(`mcp:${item.name}`);
633
+ if (decision === 'skip') {
634
+ skipped.mcps++;
495
635
  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);
636
+ }
637
+ const config = manifest.mcp[item.name];
638
+ if (!config || !config.command)
639
+ continue;
640
+ for (const agentId of item.agents) {
641
+ if (!item.isNew) {
642
+ unregisterMcp(agentId, item.name);
507
643
  }
508
- const result = registerMcp(agentId, name, config.command, config.scope);
644
+ const result = registerMcp(agentId, item.name, config.command, config.scope);
509
645
  if (result.success)
510
- registered++;
646
+ installed.mcps++;
511
647
  }
512
648
  }
513
- if (mcpSkipped > 0) {
514
- mcpSpinner.succeed(`Registered ${registered} MCP servers (skipped ${mcpSkipped} existing)`);
649
+ if (skipped.mcps > 0) {
650
+ mcpSpinner.succeed(`Registered ${installed.mcps} MCP servers (skipped ${skipped.mcps})`);
651
+ }
652
+ else if (installed.mcps > 0) {
653
+ mcpSpinner.succeed(`Registered ${installed.mcps} MCP servers`);
515
654
  }
516
655
  else {
517
- mcpSpinner.succeed(`Registered ${registered} MCP servers`);
656
+ mcpSpinner.info('No MCP servers to register');
518
657
  }
519
658
  }
520
- // Sync CLI versions (user scope only, unless --skip-clis)
659
+ // Sync CLI versions (user scope only)
521
660
  if (isUserScope && !options.skipClis && manifest?.clis) {
522
661
  const cliSpinner = ora('Checking CLI versions...').start();
523
662
  const cliUpdates = [];
524
663
  for (const [agentIdStr, cliConfig] of Object.entries(manifest.clis)) {
525
664
  const agentId = agentIdStr;
665
+ if (agentFilter && agentId !== agentFilter)
666
+ continue;
526
667
  const agent = AGENTS[agentId];
527
668
  if (!agent || !cliConfig.package)
528
669
  continue;
529
670
  const currentVersion = getCliVersion(agentId);
530
671
  const targetVersion = cliConfig.version;
531
- // Skip if same version or if target is "latest" and CLI is installed
532
672
  if (currentVersion === targetVersion)
533
673
  continue;
534
674
  if (targetVersion === 'latest' && currentVersion)
@@ -546,7 +686,7 @@ program
546
686
  cliSpinner.succeed('CLI versions match');
547
687
  }
548
688
  }
549
- // Update scope config (only if not readonly)
689
+ // Update scope config
550
690
  if (!isReadonly) {
551
691
  const priority = getScopePriority(effectiveScope);
552
692
  setScope(effectiveScope, {
@@ -561,6 +701,10 @@ program
561
701
  console.log(chalk.green(`\nSync complete from ${effectiveScope} scope`));
562
702
  }
563
703
  catch (err) {
704
+ if (isPromptCancelled(err)) {
705
+ console.log(chalk.yellow('\nCancelled'));
706
+ process.exit(0);
707
+ }
564
708
  spinner.fail('Failed to sync');
565
709
  console.error(chalk.red(err.message));
566
710
  process.exit(1);
@@ -1076,9 +1220,48 @@ skillsCmd
1076
1220
  }
1077
1221
  });
1078
1222
  skillsCmd
1079
- .command('info <name>')
1080
- .description('Show detailed info about an installed skill')
1081
- .action((name) => {
1223
+ .command('view [name]')
1224
+ .alias('info')
1225
+ .description('View detailed info about an installed skill')
1226
+ .action(async (name) => {
1227
+ // If no name provided, show interactive select
1228
+ if (!name) {
1229
+ const cwd = process.cwd();
1230
+ const allSkills = [];
1231
+ const seenNames = new Set();
1232
+ for (const agentId of SKILLS_CAPABLE_AGENTS) {
1233
+ const skills = listInstalledSkillsWithScope(agentId, cwd);
1234
+ for (const skill of skills) {
1235
+ if (!seenNames.has(skill.name)) {
1236
+ seenNames.add(skill.name);
1237
+ allSkills.push({
1238
+ name: skill.name,
1239
+ description: skill.metadata.description || '',
1240
+ });
1241
+ }
1242
+ }
1243
+ }
1244
+ if (allSkills.length === 0) {
1245
+ console.log(chalk.yellow('No skills installed'));
1246
+ return;
1247
+ }
1248
+ try {
1249
+ name = await select({
1250
+ message: 'Select a skill to view',
1251
+ choices: allSkills.map((s) => ({
1252
+ value: s.name,
1253
+ name: s.description ? `${s.name} - ${s.description}` : s.name,
1254
+ })),
1255
+ });
1256
+ }
1257
+ catch (err) {
1258
+ if (isPromptCancelled(err)) {
1259
+ console.log(chalk.gray('Cancelled'));
1260
+ return;
1261
+ }
1262
+ throw err;
1263
+ }
1264
+ }
1082
1265
  const skill = getSkillInfo(name);
1083
1266
  if (!skill) {
1084
1267
  console.log(chalk.yellow(`Skill '${name}' not found`));
@@ -1968,7 +2151,7 @@ program
1968
2151
  const spinner = ora('Checking for updates...').start();
1969
2152
  try {
1970
2153
  // Get current version from package.json
1971
- const currentVersion = program.version();
2154
+ const currentVersion = program.version() || '0.0.0';
1972
2155
  // Fetch latest version from npm
1973
2156
  const response = await fetch('https://registry.npmjs.org/@swarmify/agents-cli/latest');
1974
2157
  if (!response.ok) {
@@ -1980,7 +2163,7 @@ program
1980
2163
  spinner.succeed(`Already on latest version (${currentVersion})`);
1981
2164
  return;
1982
2165
  }
1983
- spinner.text = `Upgrading from ${currentVersion} to ${latestVersion}...`;
2166
+ spinner.text = `Upgrading to ${latestVersion}...`;
1984
2167
  // Detect package manager
1985
2168
  const { execSync } = await import('child_process');
1986
2169
  let cmd;
@@ -2009,8 +2192,11 @@ program
2009
2192
  cmd = 'npm install -g @swarmify/agents-cli@latest';
2010
2193
  }
2011
2194
  }
2012
- execSync(cmd, { stdio: 'inherit' });
2195
+ // Run silently (suppress npm/bun output)
2196
+ execSync(cmd, { stdio: 'pipe' });
2013
2197
  spinner.succeed(`Upgraded to ${latestVersion}`);
2198
+ // Show what's new from changelog
2199
+ await showWhatsNew(currentVersion, latestVersion);
2014
2200
  }
2015
2201
  catch (err) {
2016
2202
  spinner.fail('Upgrade failed');
@@ -2019,5 +2205,67 @@ program
2019
2205
  process.exit(1);
2020
2206
  }
2021
2207
  });
2208
+ async function showWhatsNew(fromVersion, toVersion) {
2209
+ try {
2210
+ // Fetch changelog from npm package
2211
+ const response = await fetch(`https://unpkg.com/@swarmify/agents-cli@${toVersion}/CHANGELOG.md`);
2212
+ if (!response.ok)
2213
+ return;
2214
+ const changelog = await response.text();
2215
+ const lines = changelog.split('\n');
2216
+ // Parse changelog to find relevant sections
2217
+ const relevantChanges = [];
2218
+ let inRelevantSection = false;
2219
+ let currentVersion = '';
2220
+ for (const line of lines) {
2221
+ // Check for version header (## 1.5.0)
2222
+ const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
2223
+ if (versionMatch) {
2224
+ currentVersion = versionMatch[1];
2225
+ // Include versions newer than fromVersion
2226
+ const isNewer = currentVersion !== fromVersion &&
2227
+ compareVersions(currentVersion, fromVersion) > 0;
2228
+ inRelevantSection = isNewer;
2229
+ if (inRelevantSection) {
2230
+ relevantChanges.push('');
2231
+ relevantChanges.push(chalk.bold(`v${currentVersion}`));
2232
+ }
2233
+ continue;
2234
+ }
2235
+ if (inRelevantSection && line.trim()) {
2236
+ // Format the line
2237
+ if (line.startsWith('**') && line.endsWith('**')) {
2238
+ // Section header like **Pull command redesign**
2239
+ relevantChanges.push(chalk.cyan(line.replace(/\*\*/g, '')));
2240
+ }
2241
+ else if (line.startsWith('- ')) {
2242
+ // Bullet point
2243
+ relevantChanges.push(chalk.gray(` ${line}`));
2244
+ }
2245
+ }
2246
+ }
2247
+ if (relevantChanges.length > 0) {
2248
+ console.log(chalk.bold("\nWhat's new:\n"));
2249
+ for (const line of relevantChanges) {
2250
+ console.log(line);
2251
+ }
2252
+ console.log();
2253
+ }
2254
+ }
2255
+ catch {
2256
+ // Silently ignore changelog fetch errors
2257
+ }
2258
+ }
2259
+ function compareVersions(a, b) {
2260
+ const partsA = a.split('.').map(Number);
2261
+ const partsB = b.split('.').map(Number);
2262
+ for (let i = 0; i < 3; i++) {
2263
+ if (partsA[i] > partsB[i])
2264
+ return 1;
2265
+ if (partsA[i] < partsB[i])
2266
+ return -1;
2267
+ }
2268
+ return 0;
2269
+ }
2022
2270
  program.parse();
2023
2271
  //# sourceMappingURL=index.js.map