agent-switchboard 0.1.29 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +94 -46
  2. package/dist/commands/distribution.js +1 -1
  3. package/dist/commands/distribution.js.map +1 -1
  4. package/dist/commands/inventory.js +1 -1
  5. package/dist/commands/inventory.js.map +1 -1
  6. package/dist/commands/library.d.ts +1 -1
  7. package/dist/commands/library.js +3 -3
  8. package/dist/commands/library.js.map +1 -1
  9. package/dist/config/application-config.d.ts +14 -3
  10. package/dist/config/application-config.js +61 -6
  11. package/dist/config/application-config.js.map +1 -1
  12. package/dist/config/mcp-config.d.ts +7 -0
  13. package/dist/config/mcp-config.js +25 -0
  14. package/dist/config/mcp-config.js.map +1 -1
  15. package/dist/config/schemas.d.ts +828 -273
  16. package/dist/config/schemas.js +62 -18
  17. package/dist/config/schemas.js.map +1 -1
  18. package/dist/hooks/distribution.js +1 -1
  19. package/dist/hooks/distribution.js.map +1 -1
  20. package/dist/hooks/library.d.ts +1 -1
  21. package/dist/hooks/library.js +3 -3
  22. package/dist/hooks/library.js.map +1 -1
  23. package/dist/index.js +323 -67
  24. package/dist/index.js.map +1 -1
  25. package/dist/library/sources.d.ts +3 -2
  26. package/dist/library/sources.js +16 -17
  27. package/dist/library/sources.js.map +1 -1
  28. package/dist/library/state.d.ts +6 -6
  29. package/dist/library/state.js +15 -15
  30. package/dist/library/state.js.map +1 -1
  31. package/dist/marketplace/plugin-loader.d.ts +3 -0
  32. package/dist/marketplace/plugin-loader.js +53 -2
  33. package/dist/marketplace/plugin-loader.js.map +1 -1
  34. package/dist/marketplace/reader.d.ts +22 -0
  35. package/dist/marketplace/reader.js +122 -17
  36. package/dist/marketplace/reader.js.map +1 -1
  37. package/dist/marketplace/schemas.d.ts +48 -12
  38. package/dist/marketplace/schemas.js +4 -1
  39. package/dist/marketplace/schemas.js.map +1 -1
  40. package/dist/marketplace/source-loader.d.ts +6 -6
  41. package/dist/marketplace/source-loader.js +8 -8
  42. package/dist/marketplace/source-loader.js.map +1 -1
  43. package/dist/plugins/index.d.ts +57 -0
  44. package/dist/plugins/index.js +304 -0
  45. package/dist/plugins/index.js.map +1 -0
  46. package/dist/rules/composer.js +4 -4
  47. package/dist/rules/composer.js.map +1 -1
  48. package/dist/rules/inventory.js +1 -1
  49. package/dist/rules/inventory.js.map +1 -1
  50. package/dist/rules/library.d.ts +1 -1
  51. package/dist/rules/library.js +19 -3
  52. package/dist/rules/library.js.map +1 -1
  53. package/dist/rules/schema.d.ts +3 -3
  54. package/dist/rules/schema.js +1 -1
  55. package/dist/rules/schema.js.map +1 -1
  56. package/dist/rules/state.js +12 -12
  57. package/dist/rules/state.js.map +1 -1
  58. package/dist/skills/distribution.js +5 -5
  59. package/dist/skills/distribution.js.map +1 -1
  60. package/dist/skills/inventory.js +1 -1
  61. package/dist/skills/inventory.js.map +1 -1
  62. package/dist/skills/library.d.ts +1 -1
  63. package/dist/skills/library.js +3 -3
  64. package/dist/skills/library.js.map +1 -1
  65. package/dist/subagents/distribution.js +2 -2
  66. package/dist/subagents/distribution.js.map +1 -1
  67. package/dist/subagents/inventory.js +1 -1
  68. package/dist/subagents/inventory.js.map +1 -1
  69. package/dist/subagents/library.d.ts +1 -1
  70. package/dist/subagents/library.js +3 -3
  71. package/dist/subagents/library.js.map +1 -1
  72. package/dist/ui/library-selector.d.ts +1 -1
  73. package/dist/ui/library-selector.js +7 -7
  74. package/dist/ui/library-selector.js.map +1 -1
  75. package/dist/ui/plugin-ui.d.ts +3 -0
  76. package/dist/ui/plugin-ui.js +77 -0
  77. package/dist/ui/plugin-ui.js.map +1 -0
  78. package/dist/ui/rule-ui.d.ts +1 -1
  79. package/dist/ui/rule-ui.js +4 -4
  80. package/dist/ui/rule-ui.js.map +1 -1
  81. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,16 +14,18 @@ import { getAgentById } from './agents/registry.js';
14
14
  import { distributeCommands } from './commands/distribution.js';
15
15
  import { importCommandFromFile } from './commands/importer.js';
16
16
  import { buildCommandInventory } from './commands/inventory.js';
17
- import { resolveApplicationSectionConfig } from './config/application-config.js';
18
- import { loadMcpConfig, stripLegacyEnabledFlagsFromMcpJson } from './config/mcp-config.js';
17
+ import { resolveEffectiveSectionConfig } from './config/application-config.js';
18
+ import { updateConfigLayer } from './config/layered-config.js';
19
+ import { loadMcpConfig, loadMcpConfigWithPlugins, stripLegacyEnabledFlagsFromMcpJson, } from './config/mcp-config.js';
19
20
  import { getAgentsDir, getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getHooksDir, getOpencodePath, getSkillsDir, getSourceCacheDir, } from './config/paths.js';
20
21
  import { loadSwitchboardConfig, loadSwitchboardConfigWithLayers, } from './config/switchboard-config.js';
21
22
  import { distributeHooks } from './hooks/distribution.js';
22
23
  import { loadHookLibrary } from './hooks/library.js';
23
24
  import { ensureLibraryDirectories, writeFileSecure } from './library/fs.js';
24
25
  import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
25
- import { loadLibraryStateSection, loadMcpActiveState, saveMcpActiveState, } from './library/state.js';
26
+ import { loadLibraryStateSection, loadMcpEnabledState, saveMcpEnabledState, } from './library/state.js';
26
27
  import { readMarketplace } from './marketplace/reader.js';
28
+ import { buildPluginIndex } from './plugins/index.js';
27
29
  import { RULE_SUPPORTED_AGENTS } from './rules/agents.js';
28
30
  import { composeActiveRules } from './rules/composer.js';
29
31
  import { distributeRules, listIndirectAgents, listPerFileAgents, listUnsupportedAgents, } from './rules/distribution.js';
@@ -39,6 +41,7 @@ import { buildSubagentInventory } from './subagents/inventory.js';
39
41
  import { showCommandSelector } from './ui/command-ui.js';
40
42
  import { showHookSelector } from './ui/hook-ui.js';
41
43
  import { showMcpServerUI } from './ui/mcp-ui.js';
44
+ import { printPluginInfo, printPluginList } from './ui/plugin-ui.js';
42
45
  import { showRuleSelector } from './ui/rule-ui.js';
43
46
  import { showSkillSelector } from './ui/skill-ui.js';
44
47
  import { showSubagentSelector } from './ui/subagent-ui.js';
@@ -103,7 +106,7 @@ program
103
106
  console.log(`${chalk.blue('Apps:')} ${appsLabel}`);
104
107
  console.log();
105
108
  const cursorSkillsDeduped = config.applications.active.includes('claude-code') &&
106
- resolveApplicationSectionConfig('skills', 'claude-code', scope).active.length > 0;
109
+ resolveEffectiveSectionConfig('skills', 'claude-code', scope).enabled.length > 0;
107
110
  console.log(chalk.blue('Inventory:'));
108
111
  {
109
112
  const sections = ['mcp', 'rules', 'commands', 'agents', 'skills', 'hooks'];
@@ -120,7 +123,7 @@ program
120
123
  };
121
124
  const termWidth = process.stdout.columns || 80;
122
125
  const maxSectionLen = Math.max(...sections.map((s) => s.length));
123
- const maxCountLen = Math.max(...sections.map((s) => `(${config[s].active.length})`.length));
126
+ const maxCountLen = Math.max(...sections.map((s) => `(${config[s].enabled.length})`.length));
124
127
  const prefixPlainLen = 2 + maxSectionLen + 1 + maxCountLen + 2;
125
128
  const fitPreview = (ids, maxWidth) => {
126
129
  if (ids.length === 0)
@@ -147,13 +150,13 @@ program
147
150
  return text;
148
151
  };
149
152
  for (const section of sections) {
150
- const globalActive = config[section].active;
153
+ const globalActive = config[section].enabled;
151
154
  const globalCount = globalActive.length;
152
155
  const supported = new Set(sectionPlatforms[section] ?? []);
153
156
  const applicableApps = config.applications.active.filter((id) => supported.has(id));
154
157
  const effectiveByApp = new Map();
155
158
  for (const appId of applicableApps) {
156
- effectiveByApp.set(appId, resolveApplicationSectionConfig(section, appId, scope).active);
159
+ effectiveByApp.set(appId, resolveEffectiveSectionConfig(section, appId, scope).enabled);
157
160
  }
158
161
  const perAppParts = applicableApps.map((appId) => {
159
162
  const eff = effectiveByApp.get(appId) ?? [];
@@ -179,6 +182,25 @@ program
179
182
  }
180
183
  }
181
184
  }
185
+ // Show enabled plugins summary
186
+ {
187
+ const pluginIndex = buildPluginIndex();
188
+ const enabledPluginRefs = Object.entries(config.plugins.enabled)
189
+ .filter(([, v]) => v === true)
190
+ .map(([k]) => k);
191
+ if (enabledPluginRefs.length > 0) {
192
+ const names = enabledPluginRefs
193
+ .map((pid) => {
194
+ const p = pluginIndex.get(pid);
195
+ return p ? pid : chalk.strikethrough(pid);
196
+ })
197
+ .join(', ');
198
+ console.log(` ${chalk.magenta('plugins')} ${chalk.gray(`(${enabledPluginRefs.length})`)} ${names}`);
199
+ }
200
+ else if (pluginIndex.plugins.length > 0) {
201
+ console.log(` ${chalk.magenta('plugins')} ${chalk.gray('(0)')} ${chalk.gray(`${pluginIndex.plugins.length} available`)}`);
202
+ }
203
+ }
182
204
  console.log();
183
205
  const notes = ['rules, skills also distribute to gemini'];
184
206
  if (cursorSkillsDeduped)
@@ -506,7 +528,7 @@ async function importHooksFromClaudeCode(opts) {
506
528
  fs.writeFileSync(path.join(bundleDir, 'hook.json'), `${JSON.stringify(hookJson, null, 2)}\n`, 'utf-8');
507
529
  const eventCount = Object.keys(rewrittenHooks).length;
508
530
  console.log(`\n${chalk.green('✓')} Imported ${eventCount} event(s) + ${scriptsCopied} script(s) → ${chalk.dim(bundleDir)}`);
509
- console.log(chalk.gray(' Activate with: asb hook (interactive) or add to [hooks].active in config.toml'));
531
+ console.log(chalk.gray(' Activate with: asb hook (interactive) or add to [hooks].enabled in config.toml'));
510
532
  }
511
533
  program
512
534
  .command('mcp')
@@ -519,21 +541,21 @@ program
519
541
  const { config, layers } = loadSwitchboardConfigWithLayers(scopeToLoadOptions(scope));
520
542
  const mcpConfig = loadMcpConfig();
521
543
  // Determine initial selection:
522
- // - If writing to project/profile scope, only use that layer's explicit [mcp].active (no fallback).
523
- // - If writing to user scope and [mcp].active is missing, fall back to legacy mcp.json enabled flags.
544
+ // - If writing to project/profile scope, only use that layer's explicit [mcp].enabled (no fallback).
545
+ // - If writing to user scope and [mcp].enabled is missing, fall back to legacy mcp.json enabled flags.
524
546
  let currentActive = [];
525
547
  if (scope?.project) {
526
- const projectActive = layers.project?.config?.mcp?.active;
527
- currentActive = Array.isArray(projectActive) ? [...projectActive] : [];
548
+ const projectEnabled = layers.project?.config?.mcp?.enabled;
549
+ currentActive = Array.isArray(projectEnabled) ? [...projectEnabled] : [];
528
550
  }
529
551
  else if (scope?.profile) {
530
- const profileActive = layers.profile?.config?.mcp?.active;
531
- currentActive = Array.isArray(profileActive) ? [...profileActive] : [];
552
+ const profileEnabled = layers.profile?.config?.mcp?.enabled;
553
+ currentActive = Array.isArray(profileEnabled) ? [...profileEnabled] : [];
532
554
  }
533
555
  else {
534
- const userActive = layers.user.config?.mcp?.active;
535
- if (Array.isArray(userActive)) {
536
- currentActive = [...userActive];
556
+ const userEnabled = layers.user.config?.mcp?.enabled;
557
+ if (Array.isArray(userEnabled)) {
558
+ currentActive = [...userEnabled];
537
559
  }
538
560
  else {
539
561
  // Read from legacy mcp.json enabled flags
@@ -549,13 +571,13 @@ program
549
571
  });
550
572
  // Step 2: Save active state to config.toml (appropriate layer based on scope)
551
573
  const spinner = ora('Updating MCP configuration...').start();
552
- saveMcpActiveState(selectedServers, scope);
574
+ saveMcpEnabledState(selectedServers, scope);
553
575
  const layerName = scope?.project
554
576
  ? 'project .asb.toml'
555
577
  : scope?.profile
556
578
  ? `profile ${scope.profile}.toml`
557
579
  : 'config.toml';
558
- spinner.succeed(chalk.green(`✓ Updated ${layerName} [mcp].active`));
580
+ spinner.succeed(chalk.green(`✓ Updated ${layerName} [mcp].enabled`));
559
581
  // After user-level write, strip legacy enabled flags from mcp.json (definition-only file).
560
582
  if (!scope?.profile && !scope?.project) {
561
583
  const cleaned = stripLegacyEnabledFlagsFromMcpJson();
@@ -594,7 +616,7 @@ ruleCommand
594
616
  console.log(JSON.stringify({
595
617
  snippets: inventory.snippets,
596
618
  agentSync: inventory.state.agentSync,
597
- activeOrder: inventory.state.active,
619
+ activeOrder: inventory.state.enabled,
598
620
  }, null, 2));
599
621
  return;
600
622
  }
@@ -665,34 +687,34 @@ ruleCommand.action(async (options) => {
665
687
  const rules = loadRuleLibrary();
666
688
  const ruleMap = new Map(rules.map((rule) => [rule.id, rule]));
667
689
  const previousState = loadRuleState(scope);
668
- const desiredActive = selection.active;
690
+ const desiredEnabled = selection.enabled;
669
691
  const arraysEqual = (a, b) => {
670
692
  if (a.length !== b.length)
671
693
  return false;
672
694
  return a.every((value, index) => value === b[index]);
673
695
  };
674
- const selectionChanged = !arraysEqual(previousState.active, desiredActive);
696
+ const selectionChanged = !arraysEqual(previousState.enabled, desiredEnabled);
675
697
  const updatedState = updateRuleState((current) => {
676
698
  if (!selectionChanged) {
677
699
  return current;
678
700
  }
679
701
  return {
680
702
  ...current,
681
- active: desiredActive,
703
+ enabled: desiredEnabled,
682
704
  agentSync: {},
683
705
  };
684
706
  }, scope);
685
707
  if (!selectionChanged) {
686
- console.log(chalk.gray('\nNo changes to active rules. Refreshing agent files...'));
708
+ console.log(chalk.gray('\nNo changes to enabled rules. Refreshing agent files...'));
687
709
  }
688
710
  else {
689
711
  console.log();
690
- if (updatedState.active.length === 0) {
691
- console.log(chalk.yellow('⚠ Active rule set cleared. Agents will receive empty instructions on next sync.'));
712
+ if (updatedState.enabled.length === 0) {
713
+ console.log(chalk.yellow('⚠ Enabled rule set cleared. Agents will receive empty instructions on next sync.'));
692
714
  }
693
715
  else {
694
- console.log(chalk.green('✓ Updated active rule order:'));
695
- for (const [index, id] of updatedState.active.entries()) {
716
+ console.log(chalk.green('✓ Updated enabled rule order:'));
717
+ for (const [index, id] of updatedState.enabled.entries()) {
696
718
  const rule = ruleMap.get(id);
697
719
  const title = rule?.metadata.title?.trim();
698
720
  const name = title && title.length > 0 ? title : id;
@@ -750,7 +772,7 @@ commandRoot.action(async (options) => {
750
772
  if (!selection)
751
773
  return;
752
774
  console.log();
753
- printActiveSelection('commands', selection.active);
775
+ printActiveSelection('commands', selection.enabled);
754
776
  const out = distributeCommands(scope);
755
777
  if (out.results.length > 0) {
756
778
  console.log();
@@ -844,7 +866,7 @@ commandRoot
844
866
  console.log(JSON.stringify({
845
867
  entries: inventory.entries,
846
868
  agentSync: inventory.state.agentSync,
847
- active: inventory.state.active,
869
+ enabled: inventory.state.enabled,
848
870
  }, null, 2));
849
871
  return;
850
872
  }
@@ -899,7 +921,7 @@ agentRoot.action(async (options) => {
899
921
  if (!selection)
900
922
  return;
901
923
  console.log();
902
- printActiveSelection('agents', selection.active);
924
+ printActiveSelection('agents', selection.enabled);
903
925
  const out = distributeSubagents(scope);
904
926
  if (out.results.length > 0) {
905
927
  console.log();
@@ -990,7 +1012,7 @@ agentRoot
990
1012
  console.log(JSON.stringify({
991
1013
  entries: inventory.entries,
992
1014
  agentSync: inventory.state.agentSync,
993
- activeOrder: inventory.state.active,
1015
+ activeOrder: inventory.state.enabled,
994
1016
  }, null, 2));
995
1017
  return;
996
1018
  }
@@ -1046,7 +1068,7 @@ skillRoot.action(async (options) => {
1046
1068
  if (!selection)
1047
1069
  return;
1048
1070
  console.log();
1049
- printActiveSelection('skills', selection.active);
1071
+ printActiveSelection('skills', selection.enabled);
1050
1072
  const out = distributeSkills(scope, {
1051
1073
  useAgentsDir: config.distribution.use_agents_dir,
1052
1074
  });
@@ -1081,7 +1103,7 @@ skillRoot
1081
1103
  console.log(JSON.stringify({
1082
1104
  entries: inventory.entries,
1083
1105
  agentSync: inventory.state.agentSync,
1084
- active: inventory.state.active,
1106
+ enabled: inventory.state.enabled,
1085
1107
  }, null, 2));
1086
1108
  return;
1087
1109
  }
@@ -1187,7 +1209,7 @@ hookRoot.action(async (options) => {
1187
1209
  if (!selection)
1188
1210
  return;
1189
1211
  console.log();
1190
- printActiveSelection('hooks', selection.active);
1212
+ printActiveSelection('hooks', selection.enabled);
1191
1213
  const out = distributeHooks(scope);
1192
1214
  if (out.results.length > 0) {
1193
1215
  console.log();
@@ -1217,7 +1239,7 @@ hookRoot
1217
1239
  const scope = resolveScope(options);
1218
1240
  const entries = loadHookLibrary();
1219
1241
  const state = loadLibraryStateSection('hooks', scope);
1220
- const activeSet = new Set(state.active);
1242
+ const enabledSet = new Set(state.enabled);
1221
1243
  if (options.json) {
1222
1244
  console.log(JSON.stringify({
1223
1245
  entries: entries.map((e) => ({
@@ -1225,9 +1247,9 @@ hookRoot
1225
1247
  name: e.name,
1226
1248
  description: e.description,
1227
1249
  events: Object.keys(e.hooks),
1228
- active: activeSet.has(e.id),
1250
+ enabled: enabledSet.has(e.id),
1229
1251
  })),
1230
- active: state.active,
1252
+ enabled: state.enabled,
1231
1253
  }, null, 2));
1232
1254
  return;
1233
1255
  }
@@ -1238,7 +1260,7 @@ hookRoot
1238
1260
  console.log(chalk.blue('Hooks:'));
1239
1261
  const header = ['ID', 'Active', 'Name', 'Events', 'Description'];
1240
1262
  const rows = entries.map((e) => {
1241
- const active = activeSet.has(e.id);
1263
+ const active = enabledSet.has(e.id);
1242
1264
  const activePlain = active ? 'yes' : 'no';
1243
1265
  const events = Object.keys(e.hooks).join(', ');
1244
1266
  const desc = e.description ?? '—';
@@ -1341,7 +1363,7 @@ hookRoot
1341
1363
  }
1342
1364
  });
1343
1365
  async function applyToAgents(scope, enabledServerNames, options) {
1344
- const mcpConfig = loadMcpConfig();
1366
+ const mcpConfig = loadMcpConfigWithPlugins();
1345
1367
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1346
1368
  const useSpinner = options?.useSpinner ?? true;
1347
1369
  const results = [];
@@ -1355,7 +1377,7 @@ async function applyToAgents(scope, enabledServerNames, options) {
1355
1377
  return results;
1356
1378
  }
1357
1379
  // Global MCP servers list (from UI selection or config)
1358
- const globalMcpServers = enabledServerNames ?? loadMcpActiveState(scope);
1380
+ const globalMcpServers = enabledServerNames ?? loadMcpEnabledState(scope);
1359
1381
  for (const agentId of switchboardConfig.applications.active) {
1360
1382
  const spinner = useSpinner ? ora({ indent: 2 }).start(`Applying to ${agentId}...`) : null;
1361
1383
  const persist = (symbol, text) => {
@@ -1364,11 +1386,11 @@ async function applyToAgents(scope, enabledServerNames, options) {
1364
1386
  spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1365
1387
  };
1366
1388
  try {
1367
- const agentMcpConfig = resolveApplicationSectionConfig('mcp', agentId, scope);
1389
+ const agentMcpConfig = resolveEffectiveSectionConfig('mcp', agentId, scope);
1368
1390
  // If user selected servers via UI, use that as base; otherwise use per-agent resolved config
1369
1391
  const agentActiveServers = enabledServerNames
1370
- ? agentMcpConfig.active.filter((s) => globalMcpServers.includes(s))
1371
- : agentMcpConfig.active;
1392
+ ? agentMcpConfig.enabled.filter((s) => globalMcpServers.includes(s))
1393
+ : agentMcpConfig.enabled;
1372
1394
  // Filter to only enabled servers for this agent
1373
1395
  const activeSet = new Set(agentActiveServers);
1374
1396
  const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
@@ -1475,12 +1497,193 @@ function printMarketplaceSummary(localPath) {
1475
1497
  console.log(chalk.yellow(` ⚠ Failed to read marketplace: ${msg}`));
1476
1498
  }
1477
1499
  }
1478
- const sourceRoot = program
1479
- .command('source')
1480
- .description('Manage external library sources (local paths or git repos)');
1481
- sourceRoot
1500
+ // ── Plugin management ──────────────────────────────────────────────
1501
+ const pluginRoot = program
1502
+ .command('plugin')
1503
+ .description('Manage plugins: interactive selection, install/uninstall, marketplace sources')
1504
+ .option('-p, --profile <name>', 'Profile configuration to use')
1505
+ .option('--project <path>', 'Project directory containing .asb.toml');
1506
+ pluginRoot.action((_options) => {
1507
+ pluginRoot.outputHelp();
1508
+ });
1509
+ pluginRoot
1510
+ .command('list')
1511
+ .alias('ls')
1512
+ .description('List all discoverable plugins from configured sources')
1513
+ .option('-p, --profile <name>', 'Profile configuration to use')
1514
+ .option('--project <path>', 'Project directory containing .asb.toml')
1515
+ .option('--json', 'Output as JSON')
1516
+ .action((options) => {
1517
+ try {
1518
+ const index = buildPluginIndex();
1519
+ const scope = resolveScope(options);
1520
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1521
+ const enabledMap = config.plugins.enabled;
1522
+ if (options.json) {
1523
+ console.log(JSON.stringify(index.plugins.map((p) => {
1524
+ const ref = p.meta.sourceKind === 'marketplace' ? `${p.id}@${p.meta.sourceName}` : p.id;
1525
+ return {
1526
+ id: p.id,
1527
+ ref,
1528
+ enabled: enabledMap[ref] ?? null,
1529
+ ...p.meta,
1530
+ components: Object.fromEntries(Object.entries(p.components).map(([k, v]) => [k, v.length])),
1531
+ };
1532
+ }), null, 2));
1533
+ return;
1534
+ }
1535
+ printPluginList(index.plugins, enabledMap);
1536
+ }
1537
+ catch (error) {
1538
+ if (error instanceof Error) {
1539
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1540
+ }
1541
+ process.exit(1);
1542
+ }
1543
+ });
1544
+ pluginRoot
1545
+ .command('info <id>')
1546
+ .description('Show detailed information about a plugin')
1547
+ .action((id) => {
1548
+ try {
1549
+ const index = buildPluginIndex();
1550
+ const plugin = index.get(id);
1551
+ if (!plugin) {
1552
+ console.error(chalk.red(`✗ Plugin "${id}" not found.`));
1553
+ console.log(chalk.dim(' Available plugins:'));
1554
+ for (const p of index.plugins) {
1555
+ console.log(chalk.dim(` ${p.id}`));
1556
+ }
1557
+ process.exit(1);
1558
+ }
1559
+ printPluginInfo(plugin);
1560
+ }
1561
+ catch (error) {
1562
+ if (error instanceof Error) {
1563
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1564
+ }
1565
+ process.exit(1);
1566
+ }
1567
+ });
1568
+ function pluginEnableAction(id, options) {
1569
+ try {
1570
+ const scope = resolveScope(options);
1571
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1572
+ if (config.plugins.enabled[id] === true) {
1573
+ console.log(chalk.yellow(`⚠ Plugin "${id}" is already enabled.`));
1574
+ return;
1575
+ }
1576
+ const index = buildPluginIndex();
1577
+ if (!index.get(id)) {
1578
+ console.error(chalk.red(`✗ Plugin "${id}" not found.`));
1579
+ console.log(chalk.dim(' Run `asb plugin list` to see available plugins.'));
1580
+ process.exit(1);
1581
+ }
1582
+ updateConfigLayer((layer) => ({
1583
+ ...layer,
1584
+ plugins: {
1585
+ ...(layer.plugins ?? {}),
1586
+ enabled: { ...(layer.plugins?.enabled ?? {}), [id]: true },
1587
+ },
1588
+ }), scopeToLoadOptions(scope));
1589
+ console.log(chalk.green(`✓ Plugin "${id}" enabled.`));
1590
+ }
1591
+ catch (error) {
1592
+ if (error instanceof Error) {
1593
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1594
+ }
1595
+ process.exit(1);
1596
+ }
1597
+ }
1598
+ function pluginDisableAction(id, options) {
1599
+ try {
1600
+ const scope = resolveScope(options);
1601
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1602
+ if (config.plugins.enabled[id] === undefined) {
1603
+ console.log(chalk.yellow(`⚠ Plugin "${id}" is not installed.`));
1604
+ return;
1605
+ }
1606
+ if (config.plugins.enabled[id] === false) {
1607
+ console.log(chalk.yellow(`⚠ Plugin "${id}" is already disabled.`));
1608
+ return;
1609
+ }
1610
+ updateConfigLayer((layer) => ({
1611
+ ...layer,
1612
+ plugins: {
1613
+ ...(layer.plugins ?? {}),
1614
+ enabled: { ...(layer.plugins?.enabled ?? {}), [id]: false },
1615
+ },
1616
+ }), scopeToLoadOptions(scope));
1617
+ console.log(chalk.green(`✓ Plugin "${id}" disabled.`));
1618
+ }
1619
+ catch (error) {
1620
+ if (error instanceof Error) {
1621
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1622
+ }
1623
+ process.exit(1);
1624
+ }
1625
+ }
1626
+ function pluginUninstallAction(id, options) {
1627
+ try {
1628
+ const scope = resolveScope(options);
1629
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1630
+ if (config.plugins.enabled[id] === undefined) {
1631
+ console.log(chalk.yellow(`⚠ Plugin "${id}" is not installed.`));
1632
+ return;
1633
+ }
1634
+ updateConfigLayer((layer) => {
1635
+ const updated = { ...(layer.plugins?.enabled ?? {}) };
1636
+ delete updated[id];
1637
+ return {
1638
+ ...layer,
1639
+ plugins: { ...(layer.plugins ?? {}), enabled: updated },
1640
+ };
1641
+ }, scopeToLoadOptions(scope));
1642
+ console.log(chalk.green(`✓ Plugin "${id}" uninstalled.`));
1643
+ }
1644
+ catch (error) {
1645
+ if (error instanceof Error) {
1646
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1647
+ }
1648
+ process.exit(1);
1649
+ }
1650
+ }
1651
+ const pluginScopeOpts = [
1652
+ ['-p, --profile <name>', 'Profile configuration to use'],
1653
+ ['--project <path>', 'Project directory containing .asb.toml'],
1654
+ ];
1655
+ const enableCmd = pluginRoot
1656
+ .command('enable <id>')
1657
+ .description('Enable a plugin (set plugins.enabled to true)');
1658
+ for (const [flag, desc] of pluginScopeOpts)
1659
+ enableCmd.option(flag, desc);
1660
+ enableCmd.action(pluginEnableAction);
1661
+ const installCmd = pluginRoot
1662
+ .command('install <id>')
1663
+ .description('Enable a plugin (alias for enable)');
1664
+ for (const [flag, desc] of pluginScopeOpts)
1665
+ installCmd.option(flag, desc);
1666
+ installCmd.action(pluginEnableAction);
1667
+ const disableCmd = pluginRoot
1668
+ .command('disable <id>')
1669
+ .description('Disable a plugin (set plugins.enabled to false)');
1670
+ for (const [flag, desc] of pluginScopeOpts)
1671
+ disableCmd.option(flag, desc);
1672
+ disableCmd.action(pluginDisableAction);
1673
+ const uninstallCmd = pluginRoot
1674
+ .command('uninstall <id>')
1675
+ .description('Remove a plugin from plugins.enabled');
1676
+ for (const [flag, desc] of pluginScopeOpts)
1677
+ uninstallCmd.option(flag, desc);
1678
+ uninstallCmd.action(pluginUninstallAction);
1679
+ // ── Plugin marketplace (source management) ─────────────────────────
1680
+ const mktRoot = pluginRoot
1681
+ .command('marketplace')
1682
+ .alias('market')
1683
+ .description('Manage plugin sources (local paths, git repos, marketplaces)');
1684
+ mktRoot
1482
1685
  .command('add')
1483
- .description('Add a library source (local path or git URL)')
1686
+ .description('Add a marketplace or library source')
1484
1687
  .argument('<location>', 'Local path or git URL (e.g., https://github.com/org/repo)')
1485
1688
  .argument('[name]', 'Namespace (defaults to repo or directory name)')
1486
1689
  .action((location, nameArg) => {
@@ -1515,10 +1718,11 @@ sourceRoot
1515
1718
  console.log(chalk.dim(` Ref: ${parsed.ref}`));
1516
1719
  if (parsed.subdir)
1517
1720
  console.log(chalk.dim(` Subdir: ${parsed.subdir}`));
1518
- if (validation.isMarketplace) {
1721
+ if (validation.kind === 'marketplace') {
1519
1722
  printMarketplaceSummary(effectivePath);
1520
1723
  }
1521
1724
  else {
1725
+ console.log(chalk.dim(` Type: ${validation.kind}`));
1522
1726
  console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1523
1727
  if (validation.missing.length > 0) {
1524
1728
  console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
@@ -1533,7 +1737,7 @@ sourceRoot
1533
1737
  }
1534
1738
  addLocalSource(name, location);
1535
1739
  console.log(chalk.green(`\n✓ Added source "${name}" at ${path.resolve(location)}`));
1536
- if (validation.isMarketplace) {
1740
+ if (validation.kind === 'marketplace') {
1537
1741
  printMarketplaceSummary(path.resolve(location));
1538
1742
  }
1539
1743
  else {
@@ -1544,8 +1748,9 @@ sourceRoot
1544
1748
  }
1545
1749
  }
1546
1750
  console.log();
1547
- console.log(chalk.dim('Library entries will now use the namespace prefix, e.g., ') +
1548
- chalk.cyan(`${name}:my-rule`));
1751
+ console.log(chalk.dim('Plugins from this source are now discoverable. Run ') +
1752
+ chalk.cyan('asb plugin list') +
1753
+ chalk.dim(' to see them.'));
1549
1754
  }
1550
1755
  catch (error) {
1551
1756
  if (error instanceof Error) {
@@ -1554,10 +1759,11 @@ sourceRoot
1554
1759
  process.exit(1);
1555
1760
  }
1556
1761
  });
1557
- sourceRoot
1762
+ mktRoot
1558
1763
  .command('remove')
1559
- .description('Remove a library source by namespace')
1560
- .argument('<name>', 'Namespace to remove')
1764
+ .alias('rm')
1765
+ .description('Remove a marketplace source by name')
1766
+ .argument('<name>', 'Source namespace to remove')
1561
1767
  .action((name) => {
1562
1768
  try {
1563
1769
  const sources = getSources();
@@ -1577,9 +1783,43 @@ sourceRoot
1577
1783
  process.exit(1);
1578
1784
  }
1579
1785
  });
1580
- sourceRoot
1786
+ mktRoot
1787
+ .command('update')
1788
+ .description('Pull latest changes for all remote sources (or a specific one)')
1789
+ .argument('[name]', 'Specific source namespace to update')
1790
+ .action((name) => {
1791
+ try {
1792
+ const results = updateRemoteSources();
1793
+ const filtered = name ? results.filter((r) => r.namespace === name) : results;
1794
+ if (filtered.length === 0) {
1795
+ if (name) {
1796
+ console.log(chalk.yellow(`⚠ No remote source named "${name}" found.`));
1797
+ }
1798
+ else {
1799
+ console.log(chalk.yellow('⚠ No remote sources configured.'));
1800
+ }
1801
+ return;
1802
+ }
1803
+ for (const r of filtered) {
1804
+ if (r.status === 'updated') {
1805
+ console.log(chalk.green(` ✓ ${r.namespace}: updated from ${r.url}`));
1806
+ }
1807
+ else {
1808
+ console.log(chalk.red(` ✗ ${r.namespace}: ${r.error}`));
1809
+ }
1810
+ }
1811
+ }
1812
+ catch (error) {
1813
+ if (error instanceof Error) {
1814
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1815
+ }
1816
+ process.exit(1);
1817
+ }
1818
+ });
1819
+ mktRoot
1581
1820
  .command('list')
1582
- .description('List all library sources')
1821
+ .alias('ls')
1822
+ .description('List all configured marketplace sources')
1583
1823
  .option('--json', 'Output inventory as JSON')
1584
1824
  .action((options) => {
1585
1825
  try {
@@ -1589,19 +1829,23 @@ sourceRoot
1589
1829
  return;
1590
1830
  }
1591
1831
  if (sources.length === 0) {
1592
- console.log(chalk.yellow('⚠ No library sources configured.'));
1593
- console.log(chalk.dim(' Use `asb source add <location> [name]` to add one.'));
1832
+ console.log(chalk.yellow('⚠ No marketplace sources configured.'));
1833
+ console.log(chalk.dim(' Use `asb plugin marketplace add <location> [name]` to add one.'));
1594
1834
  return;
1595
1835
  }
1596
- console.log(chalk.blue('\nLibrary sources:'));
1836
+ console.log(chalk.blue('\nMarketplace sources:'));
1597
1837
  const header = ['Namespace', 'Type', 'Source', 'Status', 'Contains'];
1598
1838
  const rows = sources.map((src) => {
1599
1839
  const isRemote = !!src.remote;
1600
1840
  const exists = fs.existsSync(src.path);
1601
1841
  const validation = exists
1602
1842
  ? validateSourcePath(src.path)
1603
- : { found: [], missing: [], isMarketplace: false };
1604
- const typePlain = validation.isMarketplace ? 'marketplace' : isRemote ? 'remote' : 'local';
1843
+ : { found: [], missing: [], kind: 'plugin' };
1844
+ const typePlain = validation.kind === 'marketplace'
1845
+ ? 'marketplace'
1846
+ : isRemote
1847
+ ? 'plugin (remote)'
1848
+ : 'plugin';
1605
1849
  const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
1606
1850
  let statusPlain;
1607
1851
  if (isRemote) {
@@ -1611,7 +1855,7 @@ sourceRoot
1611
1855
  statusPlain = exists ? 'ok' : 'missing';
1612
1856
  }
1613
1857
  let containsPlain;
1614
- if (validation.isMarketplace && exists) {
1858
+ if (validation.kind === 'marketplace' && exists) {
1615
1859
  try {
1616
1860
  const mp = readMarketplace(src.path);
1617
1861
  containsPlain = `${mp.plugins.length} plugin(s)`;
@@ -1627,7 +1871,7 @@ sourceRoot
1627
1871
  { plain: src.namespace, formatted: chalk.cyan(src.namespace) },
1628
1872
  {
1629
1873
  plain: typePlain,
1630
- formatted: validation.isMarketplace
1874
+ formatted: validation.kind === 'marketplace'
1631
1875
  ? chalk.magenta(typePlain)
1632
1876
  : isRemote
1633
1877
  ? chalk.blue(typePlain)
@@ -1651,8 +1895,20 @@ sourceRoot
1651
1895
  process.exit(1);
1652
1896
  }
1653
1897
  });
1654
- sourceRoot.action(() => {
1655
- sourceRoot.commands.find((c) => c.name() === 'list')?.parse(process.argv);
1898
+ mktRoot.action(() => {
1899
+ mktRoot.commands.find((c) => c.name() === 'list')?.parse(process.argv);
1900
+ });
1901
+ // ── Removed: `asb source` ──────────────────────────────────────────
1902
+ program
1903
+ .command('source')
1904
+ .description('[removed] Use `asb plugin marketplace` instead')
1905
+ .allowUnknownOption(true)
1906
+ .action(() => {
1907
+ console.error(chalk.red('✗ `asb source` has been removed.\n' +
1908
+ ' Sources are now managed under `asb plugin marketplace`.\n' +
1909
+ ' Config has moved from [library.sources] to [plugins.sources].\n' +
1910
+ ' Run `asb plugin marketplace --help` for usage.'));
1911
+ process.exit(1);
1656
1912
  });
1657
1913
  program.parse(process.argv);
1658
1914
  //# sourceMappingURL=index.js.map