agent-switchboard 0.3.6 → 0.3.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.
Files changed (46) hide show
  1. package/README.md +9 -9
  2. package/dist/config/application-config.d.ts +1 -0
  3. package/dist/config/application-config.js +57 -38
  4. package/dist/config/application-config.js.map +1 -1
  5. package/dist/config/layered-config.d.ts +1 -0
  6. package/dist/config/layered-config.js +11 -1
  7. package/dist/config/layered-config.js.map +1 -1
  8. package/dist/config/schemas.d.ts +56 -35
  9. package/dist/config/schemas.js +24 -9
  10. package/dist/config/schemas.js.map +1 -1
  11. package/dist/hooks/distribution.js +18 -3
  12. package/dist/hooks/distribution.js.map +1 -1
  13. package/dist/index.js +69 -437
  14. package/dist/index.js.map +1 -1
  15. package/dist/library/distribute-bundle.js +6 -9
  16. package/dist/library/distribute-bundle.js.map +1 -1
  17. package/dist/library/distribute.js +7 -10
  18. package/dist/library/distribute.js.map +1 -1
  19. package/dist/library/state.d.ts +3 -0
  20. package/dist/library/state.js +25 -5
  21. package/dist/library/state.js.map +1 -1
  22. package/dist/mcp/distribution.d.ts +13 -0
  23. package/dist/mcp/distribution.js +121 -0
  24. package/dist/mcp/distribution.js.map +1 -0
  25. package/dist/rules/composer.js +22 -18
  26. package/dist/rules/composer.js.map +1 -1
  27. package/dist/rules/distribution.d.ts +1 -1
  28. package/dist/rules/distribution.js +22 -5
  29. package/dist/rules/distribution.js.map +1 -1
  30. package/dist/rules/state.d.ts +3 -0
  31. package/dist/rules/state.js +27 -3
  32. package/dist/rules/state.js.map +1 -1
  33. package/dist/sync/command.d.ts +15 -0
  34. package/dist/sync/command.js +268 -0
  35. package/dist/sync/command.js.map +1 -0
  36. package/dist/targets/registry.d.ts +1 -1
  37. package/dist/targets/registry.js +1 -1
  38. package/dist/ui/library-selector.js +25 -2
  39. package/dist/ui/library-selector.js.map +1 -1
  40. package/dist/ui/rule-ui.d.ts +2 -2
  41. package/dist/ui/rule-ui.js +13 -4
  42. package/dist/ui/rule-ui.js.map +1 -1
  43. package/dist/ui/selection-state.d.ts +7 -0
  44. package/dist/ui/selection-state.js +13 -0
  45. package/dist/ui/selection-state.js.map +1 -0
  46. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,39 +13,42 @@ import ora from 'ora';
13
13
  import { distributeCommands } from './commands/distribution.js';
14
14
  import { importCommandFromFile } from './commands/importer.js';
15
15
  import { buildCommandInventory } from './commands/inventory.js';
16
- import { resolveEffectiveSectionConfig } from './config/application-config.js';
17
16
  import { updateConfigLayer } from './config/layered-config.js';
18
- import { loadMcpConfig, loadMcpConfigWithPlugins, stripLegacyEnabledFlagsFromMcpJson, } from './config/mcp-config.js';
17
+ import { loadMcpConfig, stripLegacyEnabledFlagsFromMcpJson } from './config/mcp-config.js';
19
18
  import { getAgentsDir, getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getHooksDir, getOpencodePath, getSkillsDir, getSourceCacheDir, } from './config/paths.js';
19
+ import { scopeToLayerOptions } from './config/scope.js';
20
20
  import { loadSwitchboardConfig, loadSwitchboardConfigWithLayers, } from './config/switchboard-config.js';
21
21
  import { distributeHooks } from './hooks/distribution.js';
22
22
  import { loadHookLibrary } from './hooks/library.js';
23
23
  import { copyDirRecursive, ensureLibraryDirectories, isDir, isFile, listFilesRecursively, writeFileSecure, } from './library/fs.js';
24
24
  import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
25
- import { loadLibraryStateSection, loadMcpEnabledState, resetAgentSyncCache, saveMcpEnabledState, } from './library/state.js';
25
+ import { loadLibraryStateSection, saveMcpEnabledState } from './library/state.js';
26
26
  import { readMarketplace } from './marketplace/reader.js';
27
+ import { distributeMcp } from './mcp/distribution.js';
27
28
  import { buildPluginIndex } from './plugins/index.js';
28
29
  import { composeActiveRules } from './rules/composer.js';
29
30
  import { distributeRules, listIndirectAgents, listPerFileAgents, listUnsupportedAgents, } from './rules/distribution.js';
30
31
  import { buildRuleInventory } from './rules/inventory.js';
31
32
  import { loadRuleLibrary } from './rules/library.js';
32
- import { loadRuleState, updateRuleState } from './rules/state.js';
33
+ import { loadRuleState, loadWritableRuleState, updateRuleState } from './rules/state.js';
33
34
  import { distributeSkills } from './skills/distribution.js';
34
35
  import { importSkill, listSkillsInDirectory } from './skills/importer.js';
35
36
  import { buildSkillInventory } from './skills/inventory.js';
36
37
  import { distributeSubagents } from './subagents/distribution.js';
37
38
  import { importSubagentFromFile } from './subagents/importer.js';
38
39
  import { buildSubagentInventory } from './subagents/inventory.js';
40
+ import { runSyncCommand } from './sync/command.js';
39
41
  import { initTargets } from './targets/init.js';
40
- import { filterInstalled, getTargetById, getTargetsForSection, registerConfigTargets, } from './targets/registry.js';
42
+ import { getTargetsForSection } from './targets/registry.js';
41
43
  import { showCommandSelector } from './ui/command-ui.js';
42
44
  import { showHookSelector } from './ui/hook-ui.js';
43
45
  import { showMcpServerUI } from './ui/mcp-ui.js';
44
46
  import { printPluginInfo, printPluginList } from './ui/plugin-ui.js';
45
47
  import { showRuleSelector } from './ui/rule-ui.js';
48
+ import { shouldPersistSelection } from './ui/selection-state.js';
46
49
  import { showSkillSelector } from './ui/skill-ui.js';
47
50
  import { showSubagentSelector } from './ui/subagent-ui.js';
48
- import { printActiveSelection, printAgentSyncStatus, printCompactDistributions, printDistributionResults, printTable, shortenPath, } from './util/cli.js';
51
+ import { printActiveSelection, printAgentSyncStatus, printDistributionResults, printTable, } from './util/cli.js';
49
52
  const program = new Command();
50
53
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
51
54
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
@@ -58,7 +61,7 @@ Examples:
58
61
  $ asb mcp Enable/disable MCP servers interactively
59
62
  $ asb rule Select and order rule snippets
60
63
  $ asb sync Push all libraries to every active agent
61
- $ asb sync --project . Sync with project-level overrides
64
+ $ asb sync -P . Sync with project-level overrides
62
65
 
63
66
  Alias: agent-switchboard
64
67
  Config: ~/.agent-switchboard/config.toml`);
@@ -68,69 +71,21 @@ program
68
71
  .command('sync')
69
72
  .description('Synchronize active MCP servers, rules, commands, agents, and skills to application targets')
70
73
  .option('-p, --profile <name>', 'Profile configuration to use')
71
- .option('--project <path>', 'Project directory containing .asb.toml')
74
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
72
75
  .option('--no-update', 'Skip updating remote sources')
73
76
  .action(async (options) => {
74
77
  try {
75
78
  const scope = resolveScope(options);
76
- console.log(chalk.yellow('⚠ Sync overwrites agent config without diff.'));
79
+ const hasErrors = await runSyncCommand({
80
+ scope,
81
+ updateSources: options.update !== false,
82
+ });
77
83
  console.log();
78
- if (options.update !== false) {
79
- const remoteResults = updateRemoteSources();
80
- if (remoteResults.length > 0) {
81
- console.log(chalk.blue('Sources:'));
82
- for (const result of remoteResults) {
83
- if (result.status === 'updated') {
84
- console.log(` ${chalk.green('✓')} ${chalk.cyan(result.namespace)} ${chalk.dim(result.url)}`);
85
- }
86
- else {
87
- console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(result.namespace)} ${chalk.yellow(result.error ?? 'update failed')}`);
88
- }
89
- }
90
- }
91
- }
92
- if (scope?.project) {
93
- // Dual sync: global first, then project
94
- const { config: globalConfig, layers: globalLayers } = loadSwitchboardConfigWithLayers(scopeToLoadOptions(undefined));
95
- await initTargets(globalConfig);
96
- console.log(chalk.blue.bold('── Global ──'));
97
- const globalErrors = await runSyncPhase({
98
- scope: undefined,
99
- config: globalConfig,
100
- layers: globalLayers,
101
- });
102
- console.log();
103
- resetAgentSyncCache();
104
- console.log(chalk.blue.bold(`── Project: ${shortenPath(scope.project)} ──`));
105
- const { config: projectConfig, layers: projectLayers } = loadSwitchboardConfigWithLayers(scopeToLoadOptions(scope));
106
- // Register any project-level [targets] not seen during initTargets (which only ran once)
107
- const projectTargets = projectConfig.targets;
108
- if (projectTargets && Object.keys(projectTargets).length > 0) {
109
- registerConfigTargets(projectTargets);
110
- }
111
- const projectErrors = await runSyncPhase({
112
- scope,
113
- config: projectConfig,
114
- layers: projectLayers,
115
- });
116
- console.log();
117
- if (globalErrors || projectErrors) {
118
- console.log(chalk.red('✗ Sync completed with errors.'));
119
- process.exit(1);
120
- }
121
- console.log(chalk.green('✓ Sync complete.'));
122
- }
123
- else {
124
- const { config, layers } = loadSwitchboardConfigWithLayers(scopeToLoadOptions(scope));
125
- await initTargets(config);
126
- const hasErrors = await runSyncPhase({ scope, config, layers });
127
- console.log();
128
- if (hasErrors) {
129
- console.log(chalk.red('✗ Sync completed with errors.'));
130
- process.exit(1);
131
- }
132
- console.log(chalk.green('✓ Sync complete.'));
84
+ if (hasErrors) {
85
+ console.log(chalk.red('✗ Sync completed with errors.'));
86
+ process.exit(1);
133
87
  }
88
+ console.log(chalk.green('✓ Sync complete.'));
134
89
  }
135
90
  catch (error) {
136
91
  if (error instanceof Error) {
@@ -139,211 +94,6 @@ program
139
94
  process.exit(1);
140
95
  }
141
96
  });
142
- async function runSyncPhase({ scope, config, layers }) {
143
- // Config summary
144
- const activeLayers = [];
145
- if (layers.user.exists)
146
- activeLayers.push(shortenPath(layers.user.path));
147
- if (layers.profile?.exists)
148
- activeLayers.push(shortenPath(layers.profile.path));
149
- if (layers.project?.exists)
150
- activeLayers.push(shortenPath(layers.project.path));
151
- console.log(`${chalk.blue('Config:')} ${activeLayers.length > 0 ? chalk.dim(activeLayers.join(' + ')) : chalk.gray('no config files')}`);
152
- const assumeInstalledSet = new Set(config.applications.assume_installed);
153
- const appsLabel = config.applications.active.length > 0
154
- ? config.applications.active
155
- .map((id) => {
156
- const t = getTargetById(id);
157
- if (t?.isInstalled?.() === false) {
158
- if (assumeInstalledSet.has(id))
159
- return chalk.yellow(`${id} (assumed installed)`);
160
- return chalk.gray(`${id} (not installed)`);
161
- }
162
- return chalk.cyan(id);
163
- })
164
- .join(', ')
165
- : chalk.gray('none configured');
166
- console.log(`${chalk.blue('Apps:')} ${appsLabel}`);
167
- console.log();
168
- const cursorSkillsDeduped = config.applications.active.includes('claude-code') &&
169
- resolveEffectiveSectionConfig('skills', 'claude-code', scope).enabled.length > 0;
170
- console.log(chalk.blue('Inventory:'));
171
- {
172
- const sections = ['mcp', 'rules', 'commands', 'agents', 'skills', 'hooks'];
173
- const sectionPlatforms = {};
174
- for (const s of sections) {
175
- let ids = filterInstalled(getTargetsForSection(s), assumeInstalledSet).map((t) => t.id);
176
- if (s === 'skills' && cursorSkillsDeduped) {
177
- ids = ids.filter((id) => id !== 'cursor');
178
- }
179
- sectionPlatforms[s] = ids;
180
- }
181
- const termWidth = process.stdout.columns || 80;
182
- const maxSectionLen = Math.max(...sections.map((s) => s.length));
183
- const maxCountLen = Math.max(...sections.map((s) => `(${config[s].enabled.length})`.length));
184
- const prefixPlainLen = 2 + maxSectionLen + 1 + maxCountLen + 2;
185
- const fitPreview = (ids, maxWidth) => {
186
- if (ids.length === 0)
187
- return chalk.gray('none');
188
- const full = ids.join(', ');
189
- if (full.length <= maxWidth)
190
- return full;
191
- let text = '';
192
- let shown = 0;
193
- for (let i = 0; i < ids.length; i++) {
194
- const sep = shown > 0 ? ', ' : '';
195
- const candidate = text + sep + ids[i];
196
- const remaining = ids.length - (i + 1);
197
- if (remaining > 0) {
198
- const suffix = `, ... (+${remaining} more)`;
199
- if (candidate.length + suffix.length > maxWidth && shown > 0) {
200
- const left = ids.length - shown;
201
- return `${text}${chalk.gray(`, ... (+${left} more)`)}`;
202
- }
203
- }
204
- text = candidate;
205
- shown++;
206
- }
207
- return text;
208
- };
209
- for (const section of sections) {
210
- const globalActive = config[section].enabled;
211
- const globalCount = globalActive.length;
212
- const supported = new Set(sectionPlatforms[section] ?? []);
213
- const applicableApps = config.applications.active.filter((id) => supported.has(id));
214
- const effectiveByApp = new Map();
215
- for (const appId of applicableApps) {
216
- effectiveByApp.set(appId, resolveEffectiveSectionConfig(section, appId, scope).enabled);
217
- }
218
- const perAppParts = applicableApps.map((appId) => {
219
- const eff = effectiveByApp.get(appId) ?? [];
220
- const delta = eff.length - globalCount;
221
- const d = delta === 0 ? '' : delta > 0 ? `(+${delta})` : `(${delta})`;
222
- return `${appId}:${eff.length}${d}`;
223
- });
224
- const union = new Set();
225
- for (const [, ids] of effectiveByApp) {
226
- for (const id of ids)
227
- union.add(id);
228
- }
229
- const previewIds = globalActive.length > 0 ? [...globalActive] : [...union];
230
- const paddedSection = section.padEnd(maxSectionLen);
231
- const countStr = `(${globalCount})`.padStart(maxCountLen);
232
- const appsStr = perAppParts.join(' ');
233
- console.log(` ${chalk.cyan(paddedSection)} ${chalk.gray(countStr)} ${appsStr}`);
234
- if (previewIds.length > 0) {
235
- const indent = ' '.repeat(prefixPlainLen);
236
- const previewWidth = Math.max(20, termWidth - prefixPlainLen - 2);
237
- const preview = fitPreview(previewIds, previewWidth);
238
- console.log(`${indent}${chalk.gray('→')} ${preview}`);
239
- }
240
- }
241
- }
242
- // Show enabled plugins summary
243
- {
244
- const pluginIndex = buildPluginIndex();
245
- const enabledPluginRefs = config.plugins.enabled;
246
- if (enabledPluginRefs.length > 0) {
247
- const names = enabledPluginRefs
248
- .map((pid) => {
249
- const p = pluginIndex.get(pid);
250
- return p ? pid : chalk.strikethrough(pid);
251
- })
252
- .join(', ');
253
- console.log(` ${chalk.magenta('plugins')} ${chalk.gray(`(${enabledPluginRefs.length})`)} ${names}`);
254
- }
255
- else if (pluginIndex.plugins.length > 0) {
256
- console.log(` ${chalk.magenta('plugins')} ${chalk.gray('(0)')} ${chalk.gray(`${pluginIndex.plugins.length} available`)}`);
257
- }
258
- }
259
- console.log();
260
- const notes = [];
261
- if (cursorSkillsDeduped && config.applications.active.includes('cursor')) {
262
- notes.push('cursor reads skills via claude-code');
263
- }
264
- if (config.distribution.use_agents_dir) {
265
- const agentsMembers = ['codex', 'gemini', 'opencode'].filter((a) => config.applications.active.includes(a));
266
- if (agentsMembers.length > 0) {
267
- notes.push(`skills for ${agentsMembers.join(', ')} sync to shared .agents/skills`);
268
- }
269
- }
270
- for (let i = 0; i < notes.length; i++) {
271
- const prefix = i === 0 ? ' Note: ' : ' ';
272
- console.log(chalk.gray(`${prefix}${notes[i]}.`));
273
- }
274
- if (notes.length > 0)
275
- console.log();
276
- const activeAppIds = config.applications.active;
277
- const mcpDistribution = await applyToAgents(scope, undefined, {
278
- useSpinner: false,
279
- assumeInstalled: assumeInstalledSet,
280
- });
281
- const ruleDistribution = distributeRules(undefined, { activeAppIds, assumeInstalled: assumeInstalledSet }, scope);
282
- const commandDistribution = distributeCommands(scope, activeAppIds, assumeInstalledSet);
283
- const agentDistribution = distributeSubagents(scope, activeAppIds, assumeInstalledSet);
284
- const skillDistribution = distributeSkills(scope, {
285
- useAgentsDir: config.distribution.use_agents_dir,
286
- activeAppIds,
287
- assumeInstalled: assumeInstalledSet,
288
- });
289
- const hookDistribution = distributeHooks(scope, activeAppIds, assumeInstalledSet);
290
- const distSections = [
291
- {
292
- label: 'mcp',
293
- results: mcpDistribution,
294
- emptyMessage: 'no apps configured',
295
- getTargetLabel: (r) => r.application,
296
- getPath: (r) => r.filePath,
297
- },
298
- {
299
- label: 'rules',
300
- results: ruleDistribution.results,
301
- emptyMessage: 'none',
302
- getTargetLabel: (r) => r.agent,
303
- getPath: (r) => r.filePath,
304
- },
305
- {
306
- label: 'commands',
307
- results: commandDistribution.results,
308
- emptyMessage: 'none',
309
- getTargetLabel: (r) => r.platform,
310
- getPath: (r) => r.filePath,
311
- },
312
- {
313
- label: 'agents',
314
- results: agentDistribution.results,
315
- emptyMessage: 'none',
316
- getTargetLabel: (r) => r.platform,
317
- getPath: (r) => r.filePath,
318
- },
319
- {
320
- label: 'skills',
321
- results: skillDistribution.results,
322
- emptyMessage: 'none',
323
- getTargetLabel: (r) => {
324
- const sr = r;
325
- if (sr.platform === 'agents') {
326
- const members = ['codex', 'gemini', 'opencode'].filter((a) => activeAppIds.includes(a));
327
- return members.length > 0 ? members.join('+') : 'agents';
328
- }
329
- return sr.platform;
330
- },
331
- getPath: (r) => r.targetDir,
332
- },
333
- {
334
- label: 'hooks',
335
- results: hookDistribution.results,
336
- emptyMessage: 'none',
337
- getTargetLabel: (r) => r.platform,
338
- getPath: (r) => {
339
- const hr = r;
340
- return 'filePath' in hr ? hr.filePath : hr.targetDir;
341
- },
342
- },
343
- ];
344
- const { hasErrors } = printCompactDistributions(distSections);
345
- return hasErrors;
346
- }
347
97
  function resolveScope(input) {
348
98
  if (!input)
349
99
  return undefined;
@@ -357,14 +107,6 @@ function resolveScope(input) {
357
107
  project: project,
358
108
  };
359
109
  }
360
- function scopeToLoadOptions(scope) {
361
- return scope
362
- ? {
363
- profile: scope.profile ?? undefined,
364
- projectPath: scope.project ?? undefined,
365
- }
366
- : undefined;
367
- }
368
110
  function defaultCommandSourceDir(platform) {
369
111
  switch (platform) {
370
112
  case 'claude-code':
@@ -533,11 +275,11 @@ program
533
275
  .command('mcp')
534
276
  .description('Interactive UI to enable/disable MCP servers')
535
277
  .option('-p, --profile <name>', 'Profile configuration to use')
536
- .option('--project <path>', 'Project directory containing .asb.toml')
278
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
537
279
  .action(async (options) => {
538
280
  try {
539
281
  const scope = resolveScope(options);
540
- const { config, layers } = loadSwitchboardConfigWithLayers(scopeToLoadOptions(scope));
282
+ const { config, layers } = loadSwitchboardConfigWithLayers(scopeToLayerOptions(scope));
541
283
  const mcpConfig = loadMcpConfig();
542
284
  // Determine initial selection:
543
285
  // - If writing to project/profile scope, only use that layer's explicit [mcp].enabled (no fallback).
@@ -585,7 +327,7 @@ program
585
327
  }
586
328
  }
587
329
  // Step 3: Apply to registered agents
588
- await applyToAgents(scope, selectedServers);
330
+ await distributeMcp(scope, selectedServers);
589
331
  // Step 4: Show summary
590
332
  showSummary(selectedServers, scope);
591
333
  }
@@ -600,13 +342,13 @@ const ruleCommand = program
600
342
  .command('rule')
601
343
  .description('Select and order rule snippets interactively, then sync to agents')
602
344
  .option('-p, --profile <name>', 'Profile configuration to use')
603
- .option('--project <path>', 'Project directory containing .asb.toml');
345
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
604
346
  ruleCommand
605
347
  .command('list')
606
348
  .description('Display rule snippets and sync information')
607
349
  .option('--json', 'Output inventory as JSON')
608
350
  .option('-p, --profile <name>', 'Profile configuration to use')
609
- .option('--project <path>', 'Project directory containing .asb.toml')
351
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
610
352
  .action((options) => {
611
353
  try {
612
354
  const scope = resolveScope(options);
@@ -678,7 +420,7 @@ ruleCommand
678
420
  ruleCommand.action(async (options) => {
679
421
  try {
680
422
  const scope = resolveScope(options);
681
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
423
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
682
424
  await initTargets(config);
683
425
  const selection = await showRuleSelector({ scope, pageSize: config.ui.page_size });
684
426
  if (!selection) {
@@ -686,24 +428,22 @@ ruleCommand.action(async (options) => {
686
428
  }
687
429
  const rules = loadRuleLibrary();
688
430
  const ruleMap = new Map(rules.map((rule) => [rule.id, rule]));
689
- const previousState = loadRuleState(scope);
431
+ const previousState = loadWritableRuleState(scope);
432
+ const effectiveState = loadRuleState(scope);
690
433
  const desiredEnabled = selection.enabled;
691
- const arraysEqual = (a, b) => {
692
- if (a.length !== b.length)
693
- return false;
694
- return a.every((value, index) => value === b[index]);
695
- };
696
- const selectionChanged = !arraysEqual(previousState.enabled, desiredEnabled);
697
- const updatedState = updateRuleState((current) => {
698
- if (!selectionChanged) {
699
- return current;
700
- }
701
- return {
434
+ const selectionChanged = selection.explicitEmpty ||
435
+ shouldPersistSelection({
436
+ currentEnabled: previousState.enabled,
437
+ effectiveEnabled: effectiveState.enabled,
438
+ selectedEnabled: desiredEnabled,
439
+ });
440
+ const updatedState = selectionChanged
441
+ ? updateRuleState((current) => ({
702
442
  ...current,
703
443
  enabled: desiredEnabled,
704
444
  agentSync: {},
705
- };
706
- }, scope);
445
+ }), scope)
446
+ : previousState;
707
447
  if (!selectionChanged) {
708
448
  console.log(chalk.gray('\nNo changes to enabled rules. Refreshing agent files...'));
709
449
  }
@@ -725,7 +465,7 @@ ruleCommand.action(async (options) => {
725
465
  }
726
466
  const distribution = distributeRules(composeActiveRules(scope), {
727
467
  force: !selectionChanged,
728
- activeAppIds: config.applications.active,
468
+ activeAppIds: config.applications.enabled,
729
469
  assumeInstalled: new Set(config.applications.assume_installed),
730
470
  }, scope);
731
471
  if (distribution.results.length > 0) {
@@ -767,18 +507,18 @@ const commandRoot = program
767
507
  .command('command')
768
508
  .description('Select slash commands interactively and distribute to agents')
769
509
  .option('-p, --profile <name>', 'Profile configuration to use')
770
- .option('--project <path>', 'Project directory containing .asb.toml');
510
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
771
511
  commandRoot.action(async (options) => {
772
512
  try {
773
513
  const scope = resolveScope(options);
774
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
514
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
775
515
  await initTargets(config);
776
516
  const selection = await showCommandSelector({ scope, pageSize: config.ui.page_size });
777
517
  if (!selection)
778
518
  return;
779
519
  console.log();
780
520
  printActiveSelection('commands', selection.enabled);
781
- const out = distributeCommands(scope, config.applications.active, new Set(config.applications.assume_installed));
521
+ const out = distributeCommands(scope, config.applications.enabled, new Set(config.applications.assume_installed));
782
522
  if (out.results.length > 0) {
783
523
  console.log();
784
524
  printDistributionResults({
@@ -862,7 +602,7 @@ commandRoot
862
602
  .description('Display command inventory and sync information')
863
603
  .option('--json', 'Output inventory as JSON')
864
604
  .option('-p, --profile <name>', 'Profile configuration to use')
865
- .option('--project <path>', 'Project directory containing .asb.toml')
605
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
866
606
  .action((options) => {
867
607
  try {
868
608
  const scope = resolveScope(options);
@@ -917,18 +657,18 @@ const agentRoot = program
917
657
  .command('agent')
918
658
  .description('Select agent definitions interactively and distribute to applications')
919
659
  .option('-p, --profile <name>', 'Profile configuration to use')
920
- .option('--project <path>', 'Project directory containing .asb.toml');
660
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
921
661
  agentRoot.action(async (options) => {
922
662
  try {
923
663
  const scope = resolveScope(options);
924
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
664
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
925
665
  await initTargets(config);
926
666
  const selection = await showSubagentSelector({ scope, pageSize: config.ui.page_size });
927
667
  if (!selection)
928
668
  return;
929
669
  console.log();
930
670
  printActiveSelection('agents', selection.enabled);
931
- const out = distributeSubagents(scope, config.applications.active, new Set(config.applications.assume_installed));
671
+ const out = distributeSubagents(scope, config.applications.enabled, new Set(config.applications.assume_installed));
932
672
  if (out.results.length > 0) {
933
673
  console.log();
934
674
  printDistributionResults({
@@ -1009,7 +749,7 @@ agentRoot
1009
749
  .description('Display agent inventory and sync information')
1010
750
  .option('--json', 'Output inventory as JSON')
1011
751
  .option('-p, --profile <name>', 'Profile configuration to use')
1012
- .option('--project <path>', 'Project directory containing .asb.toml')
752
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
1013
753
  .action((options) => {
1014
754
  try {
1015
755
  const scope = resolveScope(options);
@@ -1065,11 +805,11 @@ const skillRoot = program
1065
805
  .command('skill')
1066
806
  .description('Select skill bundles interactively and distribute to agents')
1067
807
  .option('-p, --profile <name>', 'Profile configuration to use')
1068
- .option('--project <path>', 'Project directory containing .asb.toml');
808
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
1069
809
  skillRoot.action(async (options) => {
1070
810
  try {
1071
811
  const scope = resolveScope(options);
1072
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
812
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1073
813
  await initTargets(config);
1074
814
  const selection = await showSkillSelector({ scope, pageSize: config.ui.page_size });
1075
815
  if (!selection)
@@ -1078,7 +818,7 @@ skillRoot.action(async (options) => {
1078
818
  printActiveSelection('skills', selection.enabled);
1079
819
  const out = distributeSkills(scope, {
1080
820
  useAgentsDir: config.distribution.use_agents_dir,
1081
- activeAppIds: config.applications.active,
821
+ activeAppIds: config.applications.enabled,
1082
822
  assumeInstalled: new Set(config.applications.assume_installed),
1083
823
  });
1084
824
  if (out.results.length > 0) {
@@ -1088,7 +828,7 @@ skillRoot.action(async (options) => {
1088
828
  results: out.results,
1089
829
  getTargetLabel: (result) => {
1090
830
  if (result.platform === 'agents') {
1091
- const members = ['codex', 'gemini', 'opencode'].filter((a) => config.applications.active.includes(a));
831
+ const members = ['codex', 'gemini', 'opencode'].filter((a) => config.applications.enabled.includes(a));
1092
832
  return members.length > 0 ? members.join('+') : 'agents';
1093
833
  }
1094
834
  return result.platform;
@@ -1109,7 +849,7 @@ skillRoot
1109
849
  .description('Display skill inventory and sync information')
1110
850
  .option('--json', 'Output inventory as JSON')
1111
851
  .option('-p, --profile <name>', 'Profile configuration to use')
1112
- .option('--project <path>', 'Project directory containing .asb.toml')
852
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
1113
853
  .action((options) => {
1114
854
  try {
1115
855
  const scope = resolveScope(options);
@@ -1215,17 +955,17 @@ const hookRoot = program
1215
955
  .command('hook')
1216
956
  .description('Select hooks interactively and distribute to Claude Code')
1217
957
  .option('-p, --profile <name>', 'Profile configuration to use')
1218
- .option('--project <path>', 'Project directory containing .asb.toml');
958
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
1219
959
  hookRoot.action(async (options) => {
1220
960
  try {
1221
961
  const scope = resolveScope(options);
1222
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
962
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1223
963
  const selection = await showHookSelector({ scope, pageSize: config.ui.page_size });
1224
964
  if (!selection)
1225
965
  return;
1226
966
  console.log();
1227
967
  printActiveSelection('hooks', selection.enabled);
1228
- const out = distributeHooks(scope, config.applications.active, new Set(config.applications.assume_installed));
968
+ const out = distributeHooks(scope, config.applications.enabled, new Set(config.applications.assume_installed));
1229
969
  if (out.results.length > 0) {
1230
970
  console.log();
1231
971
  printDistributionResults({
@@ -1248,7 +988,7 @@ hookRoot
1248
988
  .description('Display hook library entries')
1249
989
  .option('--json', 'Output inventory as JSON')
1250
990
  .option('-p, --profile <name>', 'Profile configuration to use')
1251
- .option('--project <path>', 'Project directory containing .asb.toml')
991
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
1252
992
  .action((options) => {
1253
993
  try {
1254
994
  const scope = resolveScope(options);
@@ -1377,114 +1117,6 @@ hookRoot
1377
1117
  process.exit(1);
1378
1118
  }
1379
1119
  });
1380
- async function applyToAgents(scope, enabledServerNames, options) {
1381
- const mcpConfig = loadMcpConfigWithPlugins(scope);
1382
- const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1383
- await initTargets(switchboardConfig);
1384
- const useSpinner = options?.useSpinner ?? true;
1385
- const assumeInstalled = options?.assumeInstalled ?? new Set(switchboardConfig.applications.assume_installed);
1386
- const results = [];
1387
- if (switchboardConfig.applications.active.length === 0) {
1388
- if (useSpinner) {
1389
- console.log(chalk.yellow('\n⚠ No applications found in the active configuration stack.'));
1390
- console.log();
1391
- console.log('Add applications under the relevant TOML layer (user, profile, or project).');
1392
- console.log(chalk.dim(' Example: [applications]\n active = ["claude-code", "cursor"]'));
1393
- }
1394
- return results;
1395
- }
1396
- // Global MCP servers list (from UI selection or config)
1397
- const globalMcpServers = enabledServerNames ?? loadMcpEnabledState(scope);
1398
- for (const agentId of switchboardConfig.applications.active) {
1399
- const spinner = useSpinner ? ora({ indent: 2 }).start(`Applying to ${agentId}...`) : null;
1400
- const persist = (symbol, text) => {
1401
- if (!spinner)
1402
- return;
1403
- spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1404
- };
1405
- try {
1406
- const agentMcpConfig = resolveEffectiveSectionConfig('mcp', agentId, scope);
1407
- // If user selected servers via UI, use that as base; otherwise use per-agent resolved config
1408
- const agentActiveServers = enabledServerNames
1409
- ? agentMcpConfig.enabled.filter((s) => globalMcpServers.includes(s))
1410
- : agentMcpConfig.enabled;
1411
- // Filter to only enabled servers for this agent
1412
- const activeSet = new Set(agentActiveServers);
1413
- const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
1414
- const configToApply = { mcpServers: enabledServers };
1415
- const target = getTargetById(agentId);
1416
- if (!assumeInstalled.has(agentId) && target?.isInstalled?.() === false) {
1417
- persist(chalk.gray('○'), `${chalk.cyan(agentId)} ${chalk.gray('(not installed, skipped)')}`);
1418
- results.push({
1419
- application: agentId,
1420
- filePath: '(not installed)',
1421
- status: 'skipped',
1422
- reason: 'not installed',
1423
- });
1424
- continue;
1425
- }
1426
- if (!target?.mcp) {
1427
- persist(chalk.yellow('⚠'), `${chalk.cyan(agentId)} - no MCP handler (skipped)`);
1428
- results.push({
1429
- application: agentId,
1430
- filePath: '(unknown)',
1431
- status: 'skipped',
1432
- reason: 'no MCP handler',
1433
- });
1434
- continue;
1435
- }
1436
- const mcpHandler = target.mcp;
1437
- const readFileSafe = (p) => {
1438
- try {
1439
- return fs.readFileSync(p, 'utf-8');
1440
- }
1441
- catch {
1442
- return null;
1443
- }
1444
- };
1445
- if (scope?.project && mcpHandler.applyProjectConfig) {
1446
- const projectPath = mcpHandler.projectConfigPath?.(scope.project) ?? 'project config';
1447
- const before = readFileSafe(projectPath);
1448
- mcpHandler.applyProjectConfig(scope.project, configToApply);
1449
- const after = readFileSafe(projectPath);
1450
- const changed = before !== after;
1451
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(projectPath))}`);
1452
- results.push({
1453
- application: agentId,
1454
- filePath: projectPath,
1455
- status: changed ? 'written' : 'skipped',
1456
- reason: changed ? 'applied' : 'up-to-date',
1457
- });
1458
- }
1459
- else {
1460
- const configPath = mcpHandler.configPath();
1461
- const before = readFileSafe(configPath);
1462
- mcpHandler.applyConfig(configToApply);
1463
- const after = readFileSafe(configPath);
1464
- const changed = before !== after;
1465
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(configPath))}`);
1466
- results.push({
1467
- application: agentId,
1468
- filePath: configPath,
1469
- status: changed ? 'written' : 'skipped',
1470
- reason: changed ? 'applied' : 'up-to-date',
1471
- });
1472
- }
1473
- }
1474
- catch (error) {
1475
- if (error instanceof Error) {
1476
- persist(chalk.yellow('⚠'), `${chalk.cyan(agentId)} - ${error.message} (skipped)`);
1477
- results.push({
1478
- application: agentId,
1479
- filePath: '(unknown)',
1480
- status: 'error',
1481
- error: `${error.message} (skipped)`,
1482
- });
1483
- }
1484
- }
1485
- }
1486
- return results;
1487
- }
1488
1120
  /**
1489
1121
  * Show summary of enabled/disabled servers and applied agents
1490
1122
  */
@@ -1507,10 +1139,10 @@ function showSummary(selectedServers, scope) {
1507
1139
  console.log(` ${chalk.gray('✗')} ${server}`);
1508
1140
  }
1509
1141
  }
1510
- const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1511
- if (switchboardConfig.applications.active.length > 0) {
1512
- console.log(chalk.blue(`\nApplied to applications (${switchboardConfig.applications.active.length}):`));
1513
- for (const agent of switchboardConfig.applications.active) {
1142
+ const switchboardConfig = loadSwitchboardConfig(scopeToLayerOptions(scope));
1143
+ if (switchboardConfig.applications.enabled.length > 0) {
1144
+ console.log(chalk.blue(`\nApplied to applications (${switchboardConfig.applications.enabled.length}):`));
1145
+ for (const agent of switchboardConfig.applications.enabled) {
1514
1146
  console.log(` ${chalk.dim('•')} ${agent}`);
1515
1147
  }
1516
1148
  }
@@ -1539,7 +1171,7 @@ const pluginRoot = program
1539
1171
  .command('plugin')
1540
1172
  .description('Manage plugins: interactive selection, install/uninstall, marketplace sources')
1541
1173
  .option('-p, --profile <name>', 'Profile configuration to use')
1542
- .option('--project <path>', 'Project directory containing .asb.toml');
1174
+ .option('-P, --project <path>', 'Project directory containing .asb.toml');
1543
1175
  pluginRoot.action((_options) => {
1544
1176
  pluginRoot.outputHelp();
1545
1177
  });
@@ -1548,13 +1180,13 @@ pluginRoot
1548
1180
  .alias('ls')
1549
1181
  .description('List all discoverable plugins from configured sources')
1550
1182
  .option('-p, --profile <name>', 'Profile configuration to use')
1551
- .option('--project <path>', 'Project directory containing .asb.toml')
1183
+ .option('-P, --project <path>', 'Project directory containing .asb.toml')
1552
1184
  .option('--json', 'Output as JSON')
1553
1185
  .action((options) => {
1554
1186
  try {
1555
1187
  const index = buildPluginIndex();
1556
1188
  const scope = resolveScope(options);
1557
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1189
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1558
1190
  const enabledList = config.plugins.enabled;
1559
1191
  const enabledSet = new Set(enabledList);
1560
1192
  if (options.json) {
@@ -1606,7 +1238,7 @@ pluginRoot
1606
1238
  function pluginEnableAction(id, options) {
1607
1239
  try {
1608
1240
  const scope = resolveScope(options);
1609
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1241
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1610
1242
  if (config.plugins.enabled.includes(id)) {
1611
1243
  console.log(chalk.yellow(`⚠ Plugin "${id}" is already enabled.`));
1612
1244
  return;
@@ -1623,7 +1255,7 @@ function pluginEnableAction(id, options) {
1623
1255
  ...(layer.plugins ?? {}),
1624
1256
  enabled: [...(layer.plugins?.enabled ?? []), id],
1625
1257
  },
1626
- }), scopeToLoadOptions(scope));
1258
+ }), scopeToLayerOptions(scope));
1627
1259
  console.log(chalk.green(`✓ Plugin "${id}" enabled.`));
1628
1260
  }
1629
1261
  catch (error) {
@@ -1636,7 +1268,7 @@ function pluginEnableAction(id, options) {
1636
1268
  function pluginDisableAction(id, options) {
1637
1269
  try {
1638
1270
  const scope = resolveScope(options);
1639
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1271
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1640
1272
  if (!config.plugins.enabled.includes(id)) {
1641
1273
  console.log(chalk.yellow(`⚠ Plugin "${id}" is not enabled.`));
1642
1274
  return;
@@ -1647,7 +1279,7 @@ function pluginDisableAction(id, options) {
1647
1279
  ...(layer.plugins ?? {}),
1648
1280
  enabled: (layer.plugins?.enabled ?? []).filter((x) => x !== id),
1649
1281
  },
1650
- }), scopeToLoadOptions(scope));
1282
+ }), scopeToLayerOptions(scope));
1651
1283
  console.log(chalk.green(`✓ Plugin "${id}" disabled.`));
1652
1284
  }
1653
1285
  catch (error) {
@@ -1660,7 +1292,7 @@ function pluginDisableAction(id, options) {
1660
1292
  function pluginUninstallAction(id, options) {
1661
1293
  try {
1662
1294
  const scope = resolveScope(options);
1663
- const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1295
+ const config = loadSwitchboardConfig(scopeToLayerOptions(scope));
1664
1296
  if (!config.plugins.enabled.includes(id)) {
1665
1297
  console.log(chalk.yellow(`⚠ Plugin "${id}" is not enabled.`));
1666
1298
  return;
@@ -1671,7 +1303,7 @@ function pluginUninstallAction(id, options) {
1671
1303
  ...(layer.plugins ?? {}),
1672
1304
  enabled: (layer.plugins?.enabled ?? []).filter((x) => x !== id),
1673
1305
  },
1674
- }), scopeToLoadOptions(scope));
1306
+ }), scopeToLayerOptions(scope));
1675
1307
  console.log(chalk.green(`✓ Plugin "${id}" uninstalled.`));
1676
1308
  }
1677
1309
  catch (error) {
@@ -1683,7 +1315,7 @@ function pluginUninstallAction(id, options) {
1683
1315
  }
1684
1316
  const pluginScopeOpts = [
1685
1317
  ['-p, --profile <name>', 'Profile configuration to use'],
1686
- ['--project <path>', 'Project directory containing .asb.toml'],
1318
+ ['-P, --project <path>', 'Project directory containing .asb.toml'],
1687
1319
  ];
1688
1320
  const enableCmd = pluginRoot.command('enable <id>').description('Add a plugin to the enabled list');
1689
1321
  for (const [flag, desc] of pluginScopeOpts)