@swarmify/agents-cli 1.5.6 → 1.5.8

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
@@ -21,9 +21,9 @@ import { readManifest, writeManifest, createDefaultManifest, MANIFEST_FILENAME,
21
21
  import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
22
22
  import { SCOPE_PRIORITIES, DEFAULT_SYSTEM_REPO } from './lib/types.js';
23
23
  import { cloneRepo, parseSource } from './lib/git.js';
24
- import { discoverCommands, resolveCommandSource, installCommand, uninstallCommand, listInstalledCommandsWithScope, promoteCommandToUser, commandExists, } from './lib/commands.js';
25
- import { discoverHooksFromRepo, installHooks, listInstalledHooksWithScope, promoteHookToUser, removeHook, hookExists, } from './lib/hooks.js';
26
- import { discoverSkillsFromRepo, installSkill, uninstallSkill, listInstalledSkillsWithScope, promoteSkillToUser, getSkillInfo, getSkillRules, skillExists, } from './lib/skills.js';
24
+ import { discoverCommands, resolveCommandSource, installCommand, uninstallCommand, listInstalledCommandsWithScope, promoteCommandToUser, commandExists, commandContentMatches, } from './lib/commands.js';
25
+ import { discoverHooksFromRepo, installHooks, listInstalledHooksWithScope, promoteHookToUser, removeHook, hookExists, hookContentMatches, getSourceHookEntry, } from './lib/hooks.js';
26
+ import { discoverSkillsFromRepo, installSkill, uninstallSkill, listInstalledSkillsWithScope, promoteSkillToUser, getSkillInfo, getSkillRules, skillExists, skillContentMatches, } from './lib/skills.js';
27
27
  import { DEFAULT_REGISTRIES } from './lib/types.js';
28
28
  import { search as searchRegistries, getRegistries, setRegistry, removeRegistry, resolvePackage, } from './lib/registry.js';
29
29
  const program = new Command();
@@ -95,10 +95,12 @@ async function checkForUpdates() {
95
95
  });
96
96
  if (answer === 'now') {
97
97
  // Run upgrade
98
- const { execSync } = await import('child_process');
98
+ const { exec } = await import('child_process');
99
+ const { promisify } = await import('util');
100
+ const execAsync = promisify(exec);
99
101
  const spinner = ora('Upgrading...').start();
100
102
  try {
101
- execSync('npm install -g @swarmify/agents-cli@latest', { stdio: 'pipe' });
103
+ await execAsync('npm install -g @swarmify/agents-cli@latest');
102
104
  spinner.succeed(`Upgraded to ${latestVersion}`);
103
105
  await showWhatsNew(VERSION, latestVersion);
104
106
  }
@@ -133,14 +135,11 @@ program.hook('preAction', async () => {
133
135
  program
134
136
  .command('status [agent]')
135
137
  .description('Show sync status, CLI versions, installed commands and MCP servers')
136
- .action((agentFilter) => {
137
- const state = readState();
138
- const cliStates = getAllCliStates();
139
- const cwd = process.cwd();
138
+ .action(async (agentFilter) => {
139
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
140
140
  // Resolve agent filter to AgentId
141
141
  let filterAgentId;
142
142
  if (agentFilter) {
143
- // Map common names to AgentId
144
143
  const agentMap = {
145
144
  claude: 'claude',
146
145
  'claude-code': 'claude',
@@ -151,11 +150,14 @@ program
151
150
  };
152
151
  filterAgentId = agentMap[agentFilter.toLowerCase()];
153
152
  if (!filterAgentId) {
153
+ spinner.stop();
154
154
  console.log(chalk.red(`Unknown agent: ${agentFilter}`));
155
155
  console.log(chalk.gray(`Valid agents: claude, codex, gemini, cursor, opencode`));
156
156
  process.exit(1);
157
157
  }
158
158
  }
159
+ const cwd = process.cwd();
160
+ const cliStates = await getAllCliStates();
159
161
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
160
162
  const skillAgentsToShow = filterAgentId
161
163
  ? SKILLS_CAPABLE_AGENTS.filter((id) => id === filterAgentId)
@@ -163,6 +165,22 @@ program
163
165
  const mcpAgentsToShow = filterAgentId
164
166
  ? MCP_CAPABLE_AGENTS.filter((id) => id === filterAgentId)
165
167
  : MCP_CAPABLE_AGENTS;
168
+ // Collect all data while spinner is active
169
+ const commandsData = agentsToShow.map((agentId) => ({
170
+ agent: AGENTS[agentId],
171
+ commands: listInstalledCommandsWithScope(agentId, cwd),
172
+ }));
173
+ const skillsData = skillAgentsToShow.map((agentId) => ({
174
+ agent: AGENTS[agentId],
175
+ skills: listInstalledSkillsWithScope(agentId, cwd),
176
+ }));
177
+ const installedMcpAgents = mcpAgentsToShow.filter((agentId) => cliStates[agentId]?.installed);
178
+ const mcpsData = installedMcpAgents.map((agentId) => ({
179
+ agent: AGENTS[agentId],
180
+ mcps: listInstalledMcpsWithScope(agentId, cwd),
181
+ }));
182
+ const scopes = filterAgentId ? [] : getScopesByPriority();
183
+ spinner.stop();
166
184
  // Helper to format MCP with version
167
185
  const formatMcp = (m, color) => {
168
186
  return m.version ? color(`${m.name}@${m.version}`) : color(m.name);
@@ -177,9 +195,7 @@ program
177
195
  console.log(` ${agent.name.padEnd(14)} ${status}`);
178
196
  }
179
197
  console.log(chalk.bold('\nInstalled Commands\n'));
180
- for (const agentId of agentsToShow) {
181
- const agent = AGENTS[agentId];
182
- const commands = listInstalledCommandsWithScope(agentId, cwd);
198
+ for (const { agent, commands } of commandsData) {
183
199
  const userCommands = commands.filter((c) => c.scope === 'user');
184
200
  const projectCommands = commands.filter((c) => c.scope === 'project');
185
201
  if (commands.length === 0) {
@@ -195,11 +211,9 @@ program
195
211
  }
196
212
  }
197
213
  }
198
- if (skillAgentsToShow.length > 0) {
214
+ if (skillsData.length > 0) {
199
215
  console.log(chalk.bold('\nInstalled Skills\n'));
200
- for (const agentId of skillAgentsToShow) {
201
- const agent = AGENTS[agentId];
202
- const skills = listInstalledSkillsWithScope(agentId, cwd);
216
+ for (const { agent, skills } of skillsData) {
203
217
  const userSkills = skills.filter((s) => s.scope === 'user');
204
218
  const projectSkills = skills.filter((s) => s.scope === 'project');
205
219
  if (skills.length === 0) {
@@ -216,13 +230,9 @@ program
216
230
  }
217
231
  }
218
232
  }
219
- if (mcpAgentsToShow.length > 0) {
233
+ if (mcpsData.length > 0) {
220
234
  console.log(chalk.bold('\nInstalled MCP Servers\n'));
221
- for (const agentId of mcpAgentsToShow) {
222
- const agent = AGENTS[agentId];
223
- if (!isCliInstalled(agentId))
224
- continue;
225
- const mcps = listInstalledMcpsWithScope(agentId, cwd);
235
+ for (const { agent, mcps } of mcpsData) {
226
236
  const userMcps = mcps.filter((m) => m.scope === 'user');
227
237
  const projectMcps = mcps.filter((m) => m.scope === 'project');
228
238
  if (mcps.length === 0) {
@@ -241,7 +251,6 @@ program
241
251
  }
242
252
  // Only show scopes when not filtering by agent
243
253
  if (!filterAgentId) {
244
- const scopes = getScopesByPriority();
245
254
  if (scopes.length > 0) {
246
255
  console.log(chalk.bold('\nConfigured Scopes\n'));
247
256
  for (const { name, config } of scopes) {
@@ -258,7 +267,6 @@ program
258
267
  console.log(chalk.gray(' Run: agents repo add <source>'));
259
268
  }
260
269
  }
261
- console.log();
262
270
  });
263
271
  // =============================================================================
264
272
  // PULL COMMAND
@@ -357,6 +365,7 @@ program
357
365
  const allSkills = discoverSkillsFromRepo(localPath);
358
366
  const discoveredHooks = discoverHooksFromRepo(localPath);
359
367
  // Determine which agents to sync
368
+ const cliStates = await getAllCliStates();
360
369
  let selectedAgents;
361
370
  if (agentFilter) {
362
371
  // Single agent filter
@@ -367,7 +376,7 @@ program
367
376
  selectedAgents = (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']);
368
377
  }
369
378
  else {
370
- const installedAgents = ALL_AGENT_IDS.filter((id) => isCliInstalled(id) || id === 'cursor');
379
+ const installedAgents = ALL_AGENT_IDS.filter((id) => cliStates[id]?.installed || id === 'cursor');
371
380
  selectedAgents = await checkbox({
372
381
  message: 'Select agents to sync:',
373
382
  choices: installedAgents.map((id) => ({
@@ -378,7 +387,7 @@ program
378
387
  });
379
388
  }
380
389
  // Filter agents to only installed ones (plus cursor which doesn't need CLI)
381
- selectedAgents = selectedAgents.filter((id) => isCliInstalled(id) || id === 'cursor');
390
+ selectedAgents = selectedAgents.filter((id) => cliStates[id]?.installed || id === 'cursor');
382
391
  if (selectedAgents.length === 0) {
383
392
  console.log(chalk.yellow('\nNo agents selected or installed. Nothing to sync.'));
384
393
  return;
@@ -386,6 +395,7 @@ program
386
395
  // Build resource items with conflict detection
387
396
  const newItems = [];
388
397
  const existingItems = [];
398
+ const upToDateItems = [];
389
399
  // Process commands
390
400
  for (const command of allCommands) {
391
401
  const applicableAgents = selectedAgents.filter((agentId) => {
@@ -394,29 +404,55 @@ program
394
404
  });
395
405
  if (applicableAgents.length === 0)
396
406
  continue;
397
- const conflictingAgents = applicableAgents.filter((agentId) => commandExists(agentId, command.name));
398
407
  const newAgents = applicableAgents.filter((agentId) => !commandExists(agentId, command.name));
399
- if (conflictingAgents.length > 0) {
400
- existingItems.push({ type: 'command', name: command.name, agents: conflictingAgents, isNew: false });
401
- }
408
+ const upToDateAgents = applicableAgents.filter((agentId) => {
409
+ if (!commandExists(agentId, command.name))
410
+ return false;
411
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
412
+ return sourcePath && commandContentMatches(agentId, command.name, sourcePath);
413
+ });
414
+ const conflictingAgents = applicableAgents.filter((agentId) => {
415
+ if (!commandExists(agentId, command.name))
416
+ return false;
417
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
418
+ return sourcePath && !commandContentMatches(agentId, command.name, sourcePath);
419
+ });
402
420
  if (newAgents.length > 0) {
403
421
  newItems.push({ type: 'command', name: command.name, agents: newAgents, isNew: true });
404
422
  }
423
+ if (upToDateAgents.length > 0) {
424
+ upToDateItems.push({ type: 'command', name: command.name, agents: upToDateAgents, isNew: false });
425
+ }
426
+ if (conflictingAgents.length > 0) {
427
+ existingItems.push({ type: 'command', name: command.name, agents: conflictingAgents, isNew: false });
428
+ }
405
429
  }
406
430
  // Process skills
407
431
  const skillAgents = SKILLS_CAPABLE_AGENTS.filter((id) => selectedAgents.includes(id));
408
432
  for (const skill of allSkills) {
409
- const conflictingAgents = skillAgents.filter((agentId) => skillExists(agentId, skill.name));
410
433
  const newAgents = skillAgents.filter((agentId) => !skillExists(agentId, skill.name));
411
- if (conflictingAgents.length > 0) {
412
- existingItems.push({ type: 'skill', name: skill.name, agents: conflictingAgents, isNew: false });
413
- }
434
+ const upToDateAgents = skillAgents.filter((agentId) => {
435
+ if (!skillExists(agentId, skill.name))
436
+ return false;
437
+ return skillContentMatches(agentId, skill.name, skill.path);
438
+ });
439
+ const conflictingAgents = skillAgents.filter((agentId) => {
440
+ if (!skillExists(agentId, skill.name))
441
+ return false;
442
+ return !skillContentMatches(agentId, skill.name, skill.path);
443
+ });
414
444
  if (newAgents.length > 0) {
415
445
  newItems.push({ type: 'skill', name: skill.name, agents: newAgents, isNew: true });
416
446
  }
447
+ if (upToDateAgents.length > 0) {
448
+ upToDateItems.push({ type: 'skill', name: skill.name, agents: upToDateAgents, isNew: false });
449
+ }
450
+ if (conflictingAgents.length > 0) {
451
+ existingItems.push({ type: 'skill', name: skill.name, agents: conflictingAgents, isNew: false });
452
+ }
417
453
  }
418
454
  // Process hooks
419
- const hookAgents = selectedAgents.filter((id) => HOOKS_CAPABLE_AGENTS.includes(id) && isCliInstalled(id));
455
+ const hookAgents = selectedAgents.filter((id) => HOOKS_CAPABLE_AGENTS.includes(id) && cliStates[id]?.installed);
420
456
  const allHookNames = [
421
457
  ...discoveredHooks.shared,
422
458
  ...Object.entries(discoveredHooks.agentSpecific)
@@ -425,25 +461,43 @@ program
425
461
  ];
426
462
  const uniqueHookNames = [...new Set(allHookNames)];
427
463
  for (const hookName of uniqueHookNames) {
428
- const conflictingAgents = hookAgents.filter((agentId) => hookExists(agentId, hookName));
429
464
  const newAgents = hookAgents.filter((agentId) => !hookExists(agentId, hookName));
430
- if (conflictingAgents.length > 0) {
431
- existingItems.push({ type: 'hook', name: hookName, agents: conflictingAgents, isNew: false });
432
- }
465
+ const upToDateAgents = hookAgents.filter((agentId) => {
466
+ if (!hookExists(agentId, hookName))
467
+ return false;
468
+ const sourceEntry = getSourceHookEntry(localPath, agentId, hookName);
469
+ return sourceEntry && hookContentMatches(agentId, hookName, sourceEntry);
470
+ });
471
+ const conflictingAgents = hookAgents.filter((agentId) => {
472
+ if (!hookExists(agentId, hookName))
473
+ return false;
474
+ const sourceEntry = getSourceHookEntry(localPath, agentId, hookName);
475
+ return !sourceEntry || !hookContentMatches(agentId, hookName, sourceEntry);
476
+ });
433
477
  if (newAgents.length > 0) {
434
478
  newItems.push({ type: 'hook', name: hookName, agents: newAgents, isNew: true });
435
479
  }
480
+ if (upToDateAgents.length > 0) {
481
+ upToDateItems.push({ type: 'hook', name: hookName, agents: upToDateAgents, isNew: false });
482
+ }
483
+ if (conflictingAgents.length > 0) {
484
+ existingItems.push({ type: 'hook', name: hookName, agents: conflictingAgents, isNew: false });
485
+ }
436
486
  }
437
- // Process MCPs
487
+ // Process MCPs (no content comparison - just existence check)
438
488
  if (!options.skipMcp && manifest?.mcp) {
439
489
  for (const [name, config] of Object.entries(manifest.mcp)) {
440
490
  if (config.transport === 'http' || !config.command)
441
491
  continue;
442
- const mcpAgents = config.agents.filter((agentId) => selectedAgents.includes(agentId) && isCliInstalled(agentId));
492
+ const mcpAgents = config.agents.filter((agentId) => selectedAgents.includes(agentId) && cliStates[agentId]?.installed);
443
493
  if (mcpAgents.length === 0)
444
494
  continue;
445
- const conflictingAgents = mcpAgents.filter((agentId) => isMcpRegistered(agentId, name));
446
- const newAgents = mcpAgents.filter((agentId) => !isMcpRegistered(agentId, name));
495
+ const registrationChecks = await Promise.all(mcpAgents.map(async (agentId) => ({
496
+ agentId,
497
+ isRegistered: await isMcpRegistered(agentId, name),
498
+ })));
499
+ const conflictingAgents = registrationChecks.filter((r) => r.isRegistered).map((r) => r.agentId);
500
+ const newAgents = registrationChecks.filter((r) => !r.isRegistered).map((r) => r.agentId);
447
501
  if (conflictingAgents.length > 0) {
448
502
  existingItems.push({ type: 'mcp', name, agents: conflictingAgents, isNew: false });
449
503
  }
@@ -486,6 +540,31 @@ program
486
540
  }
487
541
  console.log();
488
542
  }
543
+ if (upToDateItems.length > 0) {
544
+ console.log(chalk.gray(' UP TO DATE (no changes):\n'));
545
+ const byType = { command: [], skill: [], hook: [], mcp: [] };
546
+ for (const item of upToDateItems)
547
+ byType[item.type].push(item);
548
+ if (byType.command.length > 0) {
549
+ console.log(` Commands:`);
550
+ for (const item of byType.command) {
551
+ console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
552
+ }
553
+ }
554
+ if (byType.skill.length > 0) {
555
+ console.log(` Skills:`);
556
+ for (const item of byType.skill) {
557
+ console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
558
+ }
559
+ }
560
+ if (byType.hook.length > 0) {
561
+ console.log(` Hooks:`);
562
+ for (const item of byType.hook) {
563
+ console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
564
+ }
565
+ }
566
+ console.log();
567
+ }
489
568
  if (existingItems.length > 0) {
490
569
  console.log(chalk.yellow(' EXISTING (conflicts):\n'));
491
570
  const byType = { command: [], skill: [], hook: [], mcp: [] };
@@ -518,7 +597,7 @@ program
518
597
  console.log();
519
598
  }
520
599
  if (newItems.length === 0 && existingItems.length === 0) {
521
- console.log(chalk.gray(' Nothing to sync.\n'));
600
+ console.log(chalk.gray(' Already up to date.\n'));
522
601
  return;
523
602
  }
524
603
  if (options.dryRun) {
@@ -663,9 +742,9 @@ program
663
742
  continue;
664
743
  for (const agentId of item.agents) {
665
744
  if (!item.isNew) {
666
- unregisterMcp(agentId, item.name);
745
+ await unregisterMcp(agentId, item.name);
667
746
  }
668
- const result = registerMcp(agentId, item.name, config.command, config.scope);
747
+ const result = await registerMcp(agentId, item.name, config.command, config.scope);
669
748
  if (result.success)
670
749
  installed.mcps++;
671
750
  }
@@ -691,7 +770,7 @@ program
691
770
  const agent = AGENTS[agentId];
692
771
  if (!agent || !cliConfig.package)
693
772
  continue;
694
- const currentVersion = getCliVersion(agentId);
773
+ const currentVersion = await getCliVersion(agentId);
695
774
  const targetVersion = cliConfig.version;
696
775
  if (currentVersion === targetVersion)
697
776
  continue;
@@ -758,7 +837,7 @@ program
758
837
  const localPath = getRepoLocalPath(scope.source);
759
838
  const manifest = readManifest(localPath) || createDefaultManifest();
760
839
  console.log(chalk.bold('\nExporting local configuration...\n'));
761
- const cliStates = getAllCliStates();
840
+ const cliStates = await getAllCliStates();
762
841
  let exported = 0;
763
842
  for (const agentId of ALL_AGENT_IDS) {
764
843
  const agent = AGENTS[agentId];
@@ -813,18 +892,20 @@ commandsCmd
813
892
  .description('List installed commands')
814
893
  .option('-a, --agent <agent>', 'Filter by agent')
815
894
  .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
816
- .action((options) => {
817
- console.log(chalk.bold('Installed Commands\n'));
895
+ .action(async (options) => {
896
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
818
897
  const cwd = process.cwd();
819
898
  const agents = options.agent
820
899
  ? [options.agent]
821
900
  : ALL_AGENT_IDS;
822
- for (const agentId of agents) {
823
- const agent = AGENTS[agentId];
824
- let commands = listInstalledCommandsWithScope(agentId, cwd);
825
- if (options.scope !== 'all') {
826
- commands = commands.filter((c) => c.scope === options.scope);
827
- }
901
+ // Collect all data while spinner is active
902
+ const agentCommands = agents.map((agentId) => ({
903
+ agent: AGENTS[agentId],
904
+ commands: listInstalledCommandsWithScope(agentId, cwd).filter((c) => options.scope === 'all' || c.scope === options.scope),
905
+ }));
906
+ spinner.stop();
907
+ console.log(chalk.bold('Installed Commands\n'));
908
+ for (const { agent, commands } of agentCommands) {
828
909
  if (commands.length === 0) {
829
910
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
830
911
  }
@@ -863,10 +944,11 @@ commandsCmd
863
944
  const agents = options.agents
864
945
  ? options.agents.split(',')
865
946
  : ['claude', 'codex', 'gemini'];
947
+ const cliStates = await getAllCliStates();
866
948
  for (const command of commands) {
867
949
  console.log(`\n ${chalk.cyan(command.name)}: ${command.description}`);
868
950
  for (const agentId of agents) {
869
- if (!isCliInstalled(agentId) && agentId !== 'cursor')
951
+ if (!cliStates[agentId]?.installed && agentId !== 'cursor')
870
952
  continue;
871
953
  const sourcePath = resolveCommandSource(localPath, command.name, agentId);
872
954
  if (sourcePath) {
@@ -909,14 +991,15 @@ commandsCmd
909
991
  .command('push <name>')
910
992
  .description('Save project-scoped command to user scope')
911
993
  .option('-a, --agents <list>', 'Comma-separated agents to push for')
912
- .action((name, options) => {
994
+ .action(async (name, options) => {
913
995
  const cwd = process.cwd();
914
996
  const agents = options.agents
915
997
  ? options.agents.split(',')
916
998
  : ALL_AGENT_IDS;
999
+ const cliStates = await getAllCliStates();
917
1000
  let pushed = 0;
918
1001
  for (const agentId of agents) {
919
- if (!isCliInstalled(agentId) && agentId !== 'cursor')
1002
+ if (!cliStates[agentId]?.installed && agentId !== 'cursor')
920
1003
  continue;
921
1004
  const result = promoteCommandToUser(agentId, name, cwd);
922
1005
  if (result.success) {
@@ -940,23 +1023,27 @@ hooksCmd
940
1023
  .description('List installed hooks')
941
1024
  .option('-a, --agent <agent>', 'Filter by agent')
942
1025
  .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
943
- .action((options) => {
944
- console.log(chalk.bold('Installed Hooks\n'));
1026
+ .action(async (options) => {
1027
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
945
1028
  const cwd = process.cwd();
946
1029
  const agents = options.agent
947
1030
  ? [options.agent]
948
1031
  : Array.from(HOOKS_CAPABLE_AGENTS);
949
- for (const agentId of agents) {
950
- const agent = AGENTS[agentId];
951
- if (!agent.supportsHooks) {
1032
+ // Collect all data while spinner is active
1033
+ const agentHooks = agents.map((agentId) => ({
1034
+ agent: AGENTS[agentId],
1035
+ hooks: AGENTS[agentId].supportsHooks
1036
+ ? listInstalledHooksWithScope(agentId, cwd).filter((h) => options.scope === 'all' || h.scope === options.scope)
1037
+ : null,
1038
+ }));
1039
+ spinner.stop();
1040
+ console.log(chalk.bold('Installed Hooks\n'));
1041
+ for (const { agent, hooks } of agentHooks) {
1042
+ if (hooks === null) {
952
1043
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('hooks not supported')}`);
953
1044
  console.log();
954
1045
  continue;
955
1046
  }
956
- let hooks = listInstalledHooksWithScope(agentId, cwd);
957
- if (options.scope !== 'all') {
958
- hooks = hooks.filter((h) => h.scope === options.scope);
959
- }
960
1047
  if (hooks.length === 0) {
961
1048
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
962
1049
  }
@@ -1104,23 +1191,27 @@ skillsCmd
1104
1191
  .description('List installed Agent Skills')
1105
1192
  .option('-a, --agent <agent>', 'Filter by agent')
1106
1193
  .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
1107
- .action((options) => {
1108
- console.log(chalk.bold('Installed Agent Skills\n'));
1194
+ .action(async (options) => {
1195
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
1109
1196
  const cwd = process.cwd();
1110
1197
  const agents = options.agent
1111
1198
  ? [options.agent]
1112
1199
  : SKILLS_CAPABLE_AGENTS;
1113
- for (const agentId of agents) {
1114
- const agent = AGENTS[agentId];
1115
- if (!agent.capabilities.skills) {
1200
+ // Collect all data while spinner is active
1201
+ const agentSkills = agents.map((agentId) => ({
1202
+ agent: AGENTS[agentId],
1203
+ skills: AGENTS[agentId].capabilities.skills
1204
+ ? listInstalledSkillsWithScope(agentId, cwd).filter((s) => options.scope === 'all' || s.scope === options.scope)
1205
+ : null,
1206
+ }));
1207
+ spinner.stop();
1208
+ console.log(chalk.bold('Installed Agent Skills\n'));
1209
+ for (const { agent, skills } of agentSkills) {
1210
+ if (skills === null) {
1116
1211
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('skills not supported')}`);
1117
1212
  console.log();
1118
1213
  continue;
1119
1214
  }
1120
- let skills = listInstalledSkillsWithScope(agentId, cwd);
1121
- if (options.scope !== 'all') {
1122
- skills = skills.filter((s) => s.scope === options.scope);
1123
- }
1124
1215
  if (skills.length === 0) {
1125
1216
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
1126
1217
  }
@@ -1168,11 +1259,12 @@ skillsCmd
1168
1259
  console.log(` ${chalk.gray(`${skill.ruleCount} rules`)}`);
1169
1260
  }
1170
1261
  }
1262
+ const cliStates = await getAllCliStates();
1171
1263
  const agents = options.agents
1172
1264
  ? options.agents.split(',')
1173
1265
  : await checkbox({
1174
1266
  message: 'Select agents to install skills to:',
1175
- choices: SKILLS_CAPABLE_AGENTS.filter((id) => isCliInstalled(id) || id === 'cursor').map((id) => ({
1267
+ choices: SKILLS_CAPABLE_AGENTS.filter((id) => cliStates[id]?.installed || id === 'cursor').map((id) => ({
1176
1268
  name: AGENTS[id].name,
1177
1269
  value: id,
1178
1270
  checked: ['claude', 'codex', 'gemini'].includes(id),
@@ -1345,26 +1437,38 @@ mcpCmd
1345
1437
  .description('List MCP servers and registration status')
1346
1438
  .option('-a, --agent <agent>', 'Filter by agent')
1347
1439
  .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
1348
- .action((options) => {
1349
- console.log(chalk.bold('MCP Servers\n'));
1440
+ .action(async (options) => {
1441
+ const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
1350
1442
  const cwd = process.cwd();
1351
1443
  const agents = options.agent
1352
1444
  ? [options.agent]
1353
1445
  : MCP_CAPABLE_AGENTS;
1354
- for (const agentId of agents) {
1446
+ // Collect all data while spinner is active
1447
+ const cliStates = await getAllCliStates();
1448
+ const agentMcps = agents.map((agentId) => {
1355
1449
  const agent = AGENTS[agentId];
1356
1450
  if (!agent.capabilities.mcp) {
1357
- console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('mcp not supported')}`);
1358
- console.log();
1359
- continue;
1451
+ return { agent, mcps: null };
1452
+ }
1453
+ if (!cliStates[agentId]?.installed) {
1454
+ return { agent, mcps: null, notInstalled: true };
1360
1455
  }
1361
- if (!isCliInstalled(agentId)) {
1456
+ return {
1457
+ agent,
1458
+ mcps: listInstalledMcpsWithScope(agentId, cwd).filter((m) => options.scope === 'all' || m.scope === options.scope),
1459
+ };
1460
+ });
1461
+ spinner.stop();
1462
+ console.log(chalk.bold('MCP Servers\n'));
1463
+ for (const { agent, mcps, notInstalled } of agentMcps) {
1464
+ if (mcps === null && notInstalled) {
1362
1465
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('CLI not installed')}`);
1363
1466
  continue;
1364
1467
  }
1365
- let mcps = listInstalledMcpsWithScope(agentId, cwd);
1366
- if (options.scope !== 'all') {
1367
- mcps = mcps.filter((m) => m.scope === options.scope);
1468
+ if (mcps === null) {
1469
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('mcp not supported')}`);
1470
+ console.log();
1471
+ continue;
1368
1472
  }
1369
1473
  if (mcps.length === 0) {
1370
1474
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
@@ -1447,15 +1551,16 @@ mcpCmd
1447
1551
  .command('remove <name>')
1448
1552
  .description('Remove MCP server from agents')
1449
1553
  .option('-a, --agents <list>', 'Comma-separated agents')
1450
- .action((name, options) => {
1554
+ .action(async (name, options) => {
1451
1555
  const agents = options.agents
1452
1556
  ? options.agents.split(',')
1453
1557
  : MCP_CAPABLE_AGENTS;
1558
+ const cliStates = await getAllCliStates();
1454
1559
  let removed = 0;
1455
1560
  for (const agentId of agents) {
1456
- if (!isCliInstalled(agentId))
1561
+ if (!cliStates[agentId]?.installed)
1457
1562
  continue;
1458
- const result = unregisterMcp(agentId, name);
1563
+ const result = await unregisterMcp(agentId, name);
1459
1564
  if (result.success) {
1460
1565
  console.log(` ${chalk.red('-')} ${AGENTS[agentId].name}`);
1461
1566
  removed++;
@@ -1481,6 +1586,7 @@ mcpCmd
1481
1586
  console.log(chalk.yellow('No MCP servers in manifest'));
1482
1587
  return;
1483
1588
  }
1589
+ const cliStates = await getAllCliStates();
1484
1590
  for (const [mcpName, config] of Object.entries(manifest.mcp)) {
1485
1591
  // Skip HTTP transport MCPs for now (need different registration)
1486
1592
  if (config.transport === 'http' || !config.command) {
@@ -1489,9 +1595,9 @@ mcpCmd
1489
1595
  }
1490
1596
  console.log(`\n ${chalk.cyan(mcpName)}:`);
1491
1597
  for (const agentId of config.agents) {
1492
- if (!isCliInstalled(agentId))
1598
+ if (!cliStates[agentId]?.installed)
1493
1599
  continue;
1494
- const result = registerMcp(agentId, mcpName, config.command, config.scope);
1600
+ const result = await registerMcp(agentId, mcpName, config.command, config.scope);
1495
1601
  if (result.success) {
1496
1602
  console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1497
1603
  }
@@ -1508,16 +1614,17 @@ mcpCmd
1508
1614
  .command('push <name>')
1509
1615
  .description('Save project-scoped MCP to user scope')
1510
1616
  .option('-a, --agents <list>', 'Comma-separated agents to push for')
1511
- .action((name, options) => {
1617
+ .action(async (name, options) => {
1512
1618
  const cwd = process.cwd();
1513
1619
  const agents = options.agents
1514
1620
  ? options.agents.split(',')
1515
1621
  : MCP_CAPABLE_AGENTS;
1622
+ const cliStates = await getAllCliStates();
1516
1623
  let pushed = 0;
1517
1624
  for (const agentId of agents) {
1518
- if (!isCliInstalled(agentId))
1625
+ if (!cliStates[agentId]?.installed)
1519
1626
  continue;
1520
- const result = promoteMcpToUser(agentId, name, cwd);
1627
+ const result = await promoteMcpToUser(agentId, name, cwd);
1521
1628
  if (result.success) {
1522
1629
  console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1523
1630
  pushed++;
@@ -1544,7 +1651,7 @@ cliCmd
1544
1651
  .description('List installed agent CLIs')
1545
1652
  .action(async () => {
1546
1653
  const spinner = ora('Checking installed CLIs...').start();
1547
- const states = getAllCliStates();
1654
+ const states = await getAllCliStates();
1548
1655
  spinner.stop();
1549
1656
  console.log(chalk.bold('Agent CLIs\n'));
1550
1657
  for (const agentId of ALL_AGENT_IDS) {
@@ -1560,7 +1667,6 @@ cliCmd
1560
1667
  console.log(` ${agent.name.padEnd(14)} ${chalk.gray('not installed')}`);
1561
1668
  }
1562
1669
  }
1563
- console.log();
1564
1670
  });
1565
1671
  cliCmd
1566
1672
  .command('add <agent>')
@@ -1583,10 +1689,12 @@ cliCmd
1583
1689
  console.log(chalk.yellow(`${agentConfig.name} has no npm package. Install manually.`));
1584
1690
  }
1585
1691
  else {
1586
- const { execSync } = await import('child_process');
1692
+ const { exec } = await import('child_process');
1693
+ const { promisify } = await import('util');
1694
+ const execAsync = promisify(exec);
1587
1695
  const spinner = ora(`Installing ${agentConfig.name}@${version}...`).start();
1588
1696
  try {
1589
- execSync(`npm install -g ${pkg}@${version}`, { stdio: 'pipe' });
1697
+ await execAsync(`npm install -g ${pkg}@${version}`);
1590
1698
  spinner.succeed(`Installed ${agentConfig.name}@${version}`);
1591
1699
  }
1592
1700
  catch (err) {
@@ -1626,14 +1734,16 @@ cliCmd
1626
1734
  if (!pkg) {
1627
1735
  console.log(chalk.yellow(`${agentConfig.name} has no npm package.`));
1628
1736
  }
1629
- else if (!isCliInstalled(agentId)) {
1737
+ else if (!(await isCliInstalled(agentId))) {
1630
1738
  console.log(chalk.gray(`${agentConfig.name} is not installed`));
1631
1739
  }
1632
1740
  else {
1633
- const { execSync } = await import('child_process');
1741
+ const { exec } = await import('child_process');
1742
+ const { promisify } = await import('util');
1743
+ const execAsync = promisify(exec);
1634
1744
  const spinner = ora(`Uninstalling ${agentConfig.name}...`).start();
1635
1745
  try {
1636
- execSync(`npm uninstall -g ${pkg}`, { stdio: 'pipe' });
1746
+ await execAsync(`npm uninstall -g ${pkg}`);
1637
1747
  spinner.succeed(`Uninstalled ${agentConfig.name}`);
1638
1748
  }
1639
1749
  catch (err) {
@@ -1669,7 +1779,9 @@ cliCmd
1669
1779
  console.log(chalk.yellow('No CLIs to upgrade. Add CLIs to manifest or use --latest'));
1670
1780
  return;
1671
1781
  }
1672
- const { execSync } = await import('child_process');
1782
+ const { exec } = await import('child_process');
1783
+ const { promisify } = await import('util');
1784
+ const execAsync = promisify(exec);
1673
1785
  for (const agentId of agentsToUpgrade) {
1674
1786
  const agentConfig = AGENTS[agentId];
1675
1787
  if (!agentConfig) {
@@ -1681,7 +1793,7 @@ cliCmd
1681
1793
  const pkg = cliConfig?.package || agentConfig.npmPackage;
1682
1794
  const spinner = ora(`Upgrading ${agentConfig.name} to ${version}...`).start();
1683
1795
  try {
1684
- execSync(`npm install -g ${pkg}@${version}`, { stdio: 'pipe' });
1796
+ await execAsync(`npm install -g ${pkg}@${version}`);
1685
1797
  spinner.succeed(`${agentConfig.name} upgraded to ${version}`);
1686
1798
  }
1687
1799
  catch (err) {
@@ -2096,18 +2208,19 @@ program
2096
2208
  else {
2097
2209
  command = pkg.name || pkg.registry_name;
2098
2210
  }
2211
+ const cliStates = await getAllCliStates();
2099
2212
  const agents = options.agents
2100
2213
  ? options.agents.split(',')
2101
- : MCP_CAPABLE_AGENTS.filter((id) => isCliInstalled(id));
2214
+ : MCP_CAPABLE_AGENTS.filter((id) => cliStates[id]?.installed);
2102
2215
  if (agents.length === 0) {
2103
2216
  console.log(chalk.yellow('\nNo MCP-capable agents installed.'));
2104
2217
  process.exit(1);
2105
2218
  }
2106
2219
  console.log(chalk.bold('\nInstalling to agents...'));
2107
2220
  for (const agentId of agents) {
2108
- if (!isCliInstalled(agentId))
2221
+ if (!cliStates[agentId]?.installed)
2109
2222
  continue;
2110
- const result = registerMcp(agentId, entry.name, command, 'user');
2223
+ const result = await registerMcp(agentId, entry.name, command, 'user');
2111
2224
  if (result.success) {
2112
2225
  console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
2113
2226
  }
@@ -2142,13 +2255,14 @@ program
2142
2255
  const agents = options.agents
2143
2256
  ? options.agents.split(',')
2144
2257
  : ['claude', 'codex', 'gemini'];
2258
+ const gitCliStates = await getAllCliStates();
2145
2259
  // Install commands
2146
2260
  if (hasCommands) {
2147
2261
  console.log(chalk.bold('\nInstalling commands...'));
2148
2262
  let installed = 0;
2149
2263
  for (const command of commands) {
2150
2264
  for (const agentId of agents) {
2151
- if (!isCliInstalled(agentId) && agentId !== 'cursor')
2265
+ if (!gitCliStates[agentId]?.installed && agentId !== 'cursor')
2152
2266
  continue;
2153
2267
  const sourcePath = resolveCommandSource(localPath, command.name, agentId);
2154
2268
  if (sourcePath) {
@@ -2210,7 +2324,9 @@ program
2210
2324
  }
2211
2325
  spinner.text = `Upgrading to ${latestVersion}...`;
2212
2326
  // Detect package manager
2213
- const { execSync } = await import('child_process');
2327
+ const { execSync, exec } = await import('child_process');
2328
+ const { promisify } = await import('util');
2329
+ const execAsync = promisify(exec);
2214
2330
  let cmd;
2215
2331
  // Check if installed globally via npm, bun, or other
2216
2332
  try {
@@ -2237,8 +2353,8 @@ program
2237
2353
  cmd = 'npm install -g @swarmify/agents-cli@latest';
2238
2354
  }
2239
2355
  }
2240
- // Run silently (suppress npm/bun output)
2241
- execSync(cmd, { stdio: 'pipe' });
2356
+ // Run silently (suppress npm/bun output) - use async to allow spinner to animate
2357
+ await execAsync(cmd);
2242
2358
  spinner.succeed(`Upgraded to ${latestVersion}`);
2243
2359
  // Show what's new from changelog
2244
2360
  await showWhatsNew(currentVersion, latestVersion);