agent-switchboard 0.3.2 → 0.3.4

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 (43) hide show
  1. package/README.md +8 -0
  2. package/dist/commands/distribution.d.ts +1 -1
  3. package/dist/commands/distribution.js +18 -7
  4. package/dist/commands/distribution.js.map +1 -1
  5. package/dist/config/application-config.js +5 -1
  6. package/dist/config/application-config.js.map +1 -1
  7. package/dist/config/mcp-config.d.ts +2 -1
  8. package/dist/config/mcp-config.js +3 -2
  9. package/dist/config/mcp-config.js.map +1 -1
  10. package/dist/config/schemas.d.ts +21 -0
  11. package/dist/config/schemas.js +2 -0
  12. package/dist/config/schemas.js.map +1 -1
  13. package/dist/hooks/distribution.d.ts +1 -1
  14. package/dist/hooks/distribution.js +10 -1
  15. package/dist/hooks/distribution.js.map +1 -1
  16. package/dist/index.js +263 -195
  17. package/dist/index.js.map +1 -1
  18. package/dist/library/state.d.ts +2 -0
  19. package/dist/library/state.js +6 -0
  20. package/dist/library/state.js.map +1 -1
  21. package/dist/rules/distribution.d.ts +2 -0
  22. package/dist/rules/distribution.js +7 -2
  23. package/dist/rules/distribution.js.map +1 -1
  24. package/dist/skills/distribution.d.ts +2 -0
  25. package/dist/skills/distribution.js +31 -6
  26. package/dist/skills/distribution.js.map +1 -1
  27. package/dist/subagents/distribution.d.ts +1 -1
  28. package/dist/subagents/distribution.js +33 -10
  29. package/dist/subagents/distribution.js.map +1 -1
  30. package/dist/targets/builtin/claude-code.js +2 -0
  31. package/dist/targets/builtin/claude-code.js.map +1 -1
  32. package/dist/targets/builtin/codex.js +2 -0
  33. package/dist/targets/builtin/codex.js.map +1 -1
  34. package/dist/targets/builtin/cursor.js +2 -0
  35. package/dist/targets/builtin/cursor.js.map +1 -1
  36. package/dist/targets/builtin/gemini.js +2 -0
  37. package/dist/targets/builtin/gemini.js.map +1 -1
  38. package/dist/targets/builtin/opencode.js +2 -0
  39. package/dist/targets/builtin/opencode.js.map +1 -1
  40. package/dist/targets/registry.d.ts +5 -2
  41. package/dist/targets/registry.js +6 -3
  42. package/dist/targets/registry.js.map +1 -1
  43. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ 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, saveMcpEnabledState, } from './library/state.js';
25
+ import { loadLibraryStateSection, loadMcpEnabledState, resetAgentSyncCache, saveMcpEnabledState, } from './library/state.js';
26
26
  import { readMarketplace } from './marketplace/reader.js';
27
27
  import { buildPluginIndex } from './plugins/index.js';
28
28
  import { composeActiveRules } from './rules/composer.js';
@@ -37,7 +37,7 @@ import { distributeSubagents } from './subagents/distribution.js';
37
37
  import { importSubagentFromFile } from './subagents/importer.js';
38
38
  import { buildSubagentInventory } from './subagents/inventory.js';
39
39
  import { initTargets } from './targets/init.js';
40
- import { filterInstalled, getTargetById, getTargetsForSection } from './targets/registry.js';
40
+ import { filterInstalled, getTargetById, getTargetsForSection, registerConfigTargets, } from './targets/registry.js';
41
41
  import { showCommandSelector } from './ui/command-ui.js';
42
42
  import { showHookSelector } from './ui/hook-ui.js';
43
43
  import { showMcpServerUI } from './ui/mcp-ui.js';
@@ -73,9 +73,6 @@ program
73
73
  .action(async (options) => {
74
74
  try {
75
75
  const scope = resolveScope(options);
76
- const loadOptions = scopeToLoadOptions(scope);
77
- const { config, layers } = loadSwitchboardConfigWithLayers(loadOptions);
78
- await initTargets(config);
79
76
  console.log(chalk.yellow('⚠ Sync overwrites agent config without diff.'));
80
77
  console.log();
81
78
  if (options.update !== false) {
@@ -92,193 +89,46 @@ program
92
89
  }
93
90
  }
94
91
  }
95
- // Config summary: show active layers on one line
96
- const activeLayers = [];
97
- if (layers.user.exists)
98
- activeLayers.push(shortenPath(layers.user.path));
99
- if (layers.profile?.exists)
100
- activeLayers.push(shortenPath(layers.profile.path));
101
- if (layers.project?.exists)
102
- activeLayers.push(shortenPath(layers.project.path));
103
- console.log(`${chalk.blue('Config:')} ${activeLayers.length > 0 ? chalk.dim(activeLayers.join(' + ')) : chalk.gray('no config files')}`);
104
- const appsLabel = config.applications.active.length > 0
105
- ? config.applications.active
106
- .map((id) => {
107
- const t = getTargetById(id);
108
- if (t?.isInstalled?.() === false)
109
- return chalk.gray(`${id} (not installed)`);
110
- return chalk.cyan(id);
111
- })
112
- .join(', ')
113
- : chalk.gray('none configured');
114
- console.log(`${chalk.blue('Apps:')} ${appsLabel}`);
115
- console.log();
116
- const cursorSkillsDeduped = config.applications.active.includes('claude-code') &&
117
- resolveEffectiveSectionConfig('skills', 'claude-code', scope).enabled.length > 0;
118
- console.log(chalk.blue('Inventory:'));
119
- {
120
- const sections = ['mcp', 'rules', 'commands', 'agents', 'skills', 'hooks'];
121
- const sectionPlatforms = {};
122
- for (const s of sections) {
123
- let ids = filterInstalled(getTargetsForSection(s)).map((t) => t.id);
124
- if (s === 'skills' && cursorSkillsDeduped) {
125
- ids = ids.filter((id) => id !== 'cursor');
126
- }
127
- sectionPlatforms[s] = ids;
128
- }
129
- const termWidth = process.stdout.columns || 80;
130
- const maxSectionLen = Math.max(...sections.map((s) => s.length));
131
- const maxCountLen = Math.max(...sections.map((s) => `(${config[s].enabled.length})`.length));
132
- const prefixPlainLen = 2 + maxSectionLen + 1 + maxCountLen + 2;
133
- const fitPreview = (ids, maxWidth) => {
134
- if (ids.length === 0)
135
- return chalk.gray('none');
136
- const full = ids.join(', ');
137
- if (full.length <= maxWidth)
138
- return full;
139
- let text = '';
140
- let shown = 0;
141
- for (let i = 0; i < ids.length; i++) {
142
- const sep = shown > 0 ? ', ' : '';
143
- const candidate = text + sep + ids[i];
144
- const remaining = ids.length - (i + 1);
145
- if (remaining > 0) {
146
- const suffix = `, ... (+${remaining} more)`;
147
- if (candidate.length + suffix.length > maxWidth && shown > 0) {
148
- const left = ids.length - shown;
149
- return `${text}${chalk.gray(`, ... (+${left} more)`)}`;
150
- }
151
- }
152
- text = candidate;
153
- shown++;
154
- }
155
- return text;
156
- };
157
- for (const section of sections) {
158
- const globalActive = config[section].enabled;
159
- const globalCount = globalActive.length;
160
- const supported = new Set(sectionPlatforms[section] ?? []);
161
- const applicableApps = config.applications.active.filter((id) => supported.has(id));
162
- const effectiveByApp = new Map();
163
- for (const appId of applicableApps) {
164
- effectiveByApp.set(appId, resolveEffectiveSectionConfig(section, appId, scope).enabled);
165
- }
166
- const perAppParts = applicableApps.map((appId) => {
167
- const eff = effectiveByApp.get(appId) ?? [];
168
- const delta = eff.length - globalCount;
169
- const d = delta === 0 ? '' : delta > 0 ? `(+${delta})` : `(${delta})`;
170
- return `${appId}:${eff.length}${d}`;
171
- });
172
- const union = new Set();
173
- for (const [, ids] of effectiveByApp) {
174
- for (const id of ids)
175
- union.add(id);
176
- }
177
- const previewIds = globalActive.length > 0 ? [...globalActive] : [...union];
178
- const paddedSection = section.padEnd(maxSectionLen);
179
- const countStr = `(${globalCount})`.padStart(maxCountLen);
180
- const appsStr = perAppParts.join(' ');
181
- console.log(` ${chalk.cyan(paddedSection)} ${chalk.gray(countStr)} ${appsStr}`);
182
- if (previewIds.length > 0) {
183
- const indent = ' '.repeat(prefixPlainLen);
184
- const previewWidth = Math.max(20, termWidth - prefixPlainLen - 2);
185
- const preview = fitPreview(previewIds, previewWidth);
186
- console.log(`${indent}${chalk.gray('→')} ${preview}`);
187
- }
188
- }
189
- }
190
- // Show enabled plugins summary
191
- {
192
- const pluginIndex = buildPluginIndex();
193
- const enabledPluginRefs = config.plugins.enabled;
194
- if (enabledPluginRefs.length > 0) {
195
- const names = enabledPluginRefs
196
- .map((pid) => {
197
- const p = pluginIndex.get(pid);
198
- return p ? pid : chalk.strikethrough(pid);
199
- })
200
- .join(', ');
201
- console.log(` ${chalk.magenta('plugins')} ${chalk.gray(`(${enabledPluginRefs.length})`)} ${names}`);
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);
202
110
  }
203
- else if (pluginIndex.plugins.length > 0) {
204
- console.log(` ${chalk.magenta('plugins')} ${chalk.gray('(0)')} ${chalk.gray(`${pluginIndex.plugins.length} available`)}`);
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);
205
120
  }
206
- }
207
- console.log();
208
- const notes = ['rules, skills also distribute to gemini'];
209
- if (cursorSkillsDeduped)
210
- notes.push('cursor reads skills via claude-code');
211
- for (let i = 0; i < notes.length; i++) {
212
- const prefix = i === 0 ? ' Note: ' : ' ';
213
- const suffix = i === notes.length - 1 ? '.' : '.';
214
- console.log(chalk.gray(`${prefix}${notes[i]}${suffix}`));
215
- }
216
- console.log();
217
- const mcpDistribution = await applyToAgents(scope, undefined, { useSpinner: false });
218
- const ruleDistribution = distributeRules(undefined, undefined, scope);
219
- const commandDistribution = distributeCommands(scope);
220
- const agentDistribution = distributeSubagents(scope);
221
- const skillDistribution = distributeSkills(scope, {
222
- useAgentsDir: config.distribution.use_agents_dir,
223
- });
224
- const hookDistribution = distributeHooks(scope);
225
- const distSections = [
226
- {
227
- label: 'mcp',
228
- results: mcpDistribution,
229
- emptyMessage: 'no apps configured',
230
- getTargetLabel: (r) => r.application,
231
- getPath: (r) => r.filePath,
232
- },
233
- {
234
- label: 'rules',
235
- results: ruleDistribution.results,
236
- emptyMessage: 'none',
237
- getTargetLabel: (r) => r.agent,
238
- getPath: (r) => r.filePath,
239
- },
240
- {
241
- label: 'commands',
242
- results: commandDistribution.results,
243
- emptyMessage: 'none',
244
- getTargetLabel: (r) => r.platform,
245
- getPath: (r) => r.filePath,
246
- },
247
- {
248
- label: 'agents',
249
- results: agentDistribution.results,
250
- emptyMessage: 'none',
251
- getTargetLabel: (r) => r.platform,
252
- getPath: (r) => r.filePath,
253
- },
254
- {
255
- label: 'skills',
256
- results: skillDistribution.results,
257
- emptyMessage: 'none',
258
- getTargetLabel: (r) => {
259
- const sr = r;
260
- return sr.platform === 'agents' ? 'codex+gemini+opencode' : sr.platform;
261
- },
262
- getPath: (r) => r.targetDir,
263
- },
264
- {
265
- label: 'hooks',
266
- results: hookDistribution.results,
267
- emptyMessage: 'none',
268
- getTargetLabel: (r) => r.platform,
269
- getPath: (r) => {
270
- const hr = r;
271
- return 'filePath' in hr ? hr.filePath : hr.targetDir;
272
- },
273
- },
274
- ];
275
- const { hasErrors } = printCompactDistributions(distSections);
276
- console.log();
277
- if (hasErrors) {
278
- console.log(chalk.red('✗ Sync completed with errors.'));
279
- process.exit(1);
121
+ console.log(chalk.green('✓ Sync complete.'));
280
122
  }
281
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
+ }
282
132
  console.log(chalk.green('✓ Sync complete.'));
283
133
  }
284
134
  }
@@ -289,6 +139,211 @@ program
289
139
  process.exit(1);
290
140
  }
291
141
  });
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
+ }
292
347
  function resolveScope(input) {
293
348
  if (!input)
294
349
  return undefined;
@@ -668,7 +723,11 @@ ruleCommand.action(async (options) => {
668
723
  }
669
724
  }
670
725
  }
671
- const distribution = distributeRules(composeActiveRules(scope), { force: !selectionChanged }, scope);
726
+ const distribution = distributeRules(composeActiveRules(scope), {
727
+ force: !selectionChanged,
728
+ activeAppIds: config.applications.active,
729
+ assumeInstalled: new Set(config.applications.assume_installed),
730
+ }, scope);
672
731
  if (distribution.results.length > 0) {
673
732
  console.log();
674
733
  printDistributionResults({
@@ -719,7 +778,7 @@ commandRoot.action(async (options) => {
719
778
  return;
720
779
  console.log();
721
780
  printActiveSelection('commands', selection.enabled);
722
- const out = distributeCommands(scope);
781
+ const out = distributeCommands(scope, config.applications.active, new Set(config.applications.assume_installed));
723
782
  if (out.results.length > 0) {
724
783
  console.log();
725
784
  printDistributionResults({
@@ -869,7 +928,7 @@ agentRoot.action(async (options) => {
869
928
  return;
870
929
  console.log();
871
930
  printActiveSelection('agents', selection.enabled);
872
- const out = distributeSubagents(scope);
931
+ const out = distributeSubagents(scope, config.applications.active, new Set(config.applications.assume_installed));
873
932
  if (out.results.length > 0) {
874
933
  console.log();
875
934
  printDistributionResults({
@@ -1019,13 +1078,21 @@ skillRoot.action(async (options) => {
1019
1078
  printActiveSelection('skills', selection.enabled);
1020
1079
  const out = distributeSkills(scope, {
1021
1080
  useAgentsDir: config.distribution.use_agents_dir,
1081
+ activeAppIds: config.applications.active,
1082
+ assumeInstalled: new Set(config.applications.assume_installed),
1022
1083
  });
1023
1084
  if (out.results.length > 0) {
1024
1085
  console.log();
1025
1086
  printDistributionResults({
1026
1087
  title: 'Skill distribution',
1027
1088
  results: out.results,
1028
- getTargetLabel: (result) => result.platform === 'agents' ? 'codex+gemini+opencode' : result.platform,
1089
+ getTargetLabel: (result) => {
1090
+ if (result.platform === 'agents') {
1091
+ const members = ['codex', 'gemini', 'opencode'].filter((a) => config.applications.active.includes(a));
1092
+ return members.length > 0 ? members.join('+') : 'agents';
1093
+ }
1094
+ return result.platform;
1095
+ },
1029
1096
  getPath: (result) => result.targetDir,
1030
1097
  });
1031
1098
  }
@@ -1158,7 +1225,7 @@ hookRoot.action(async (options) => {
1158
1225
  return;
1159
1226
  console.log();
1160
1227
  printActiveSelection('hooks', selection.enabled);
1161
- const out = distributeHooks(scope);
1228
+ const out = distributeHooks(scope, config.applications.active, new Set(config.applications.assume_installed));
1162
1229
  if (out.results.length > 0) {
1163
1230
  console.log();
1164
1231
  printDistributionResults({
@@ -1311,10 +1378,11 @@ hookRoot
1311
1378
  }
1312
1379
  });
1313
1380
  async function applyToAgents(scope, enabledServerNames, options) {
1314
- const mcpConfig = loadMcpConfigWithPlugins();
1381
+ const mcpConfig = loadMcpConfigWithPlugins(scope);
1315
1382
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1316
1383
  await initTargets(switchboardConfig);
1317
1384
  const useSpinner = options?.useSpinner ?? true;
1385
+ const assumeInstalled = options?.assumeInstalled ?? new Set(switchboardConfig.applications.assume_installed);
1318
1386
  const results = [];
1319
1387
  if (switchboardConfig.applications.active.length === 0) {
1320
1388
  if (useSpinner) {
@@ -1345,7 +1413,7 @@ async function applyToAgents(scope, enabledServerNames, options) {
1345
1413
  const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
1346
1414
  const configToApply = { mcpServers: enabledServers };
1347
1415
  const target = getTargetById(agentId);
1348
- if (target?.isInstalled?.() === false) {
1416
+ if (!assumeInstalled.has(agentId) && target?.isInstalled?.() === false) {
1349
1417
  persist(chalk.gray('○'), `${chalk.cyan(agentId)} ${chalk.gray('(not installed, skipped)')}`);
1350
1418
  results.push({
1351
1419
  application: agentId,