agent-switchboard 0.3.3 → 0.3.5

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 (45) hide show
  1. package/README.md +8 -0
  2. package/dist/agents/codex.js +1 -1
  3. package/dist/agents/codex.js.map +1 -1
  4. package/dist/commands/distribution.d.ts +1 -1
  5. package/dist/commands/distribution.js +19 -10
  6. package/dist/commands/distribution.js.map +1 -1
  7. package/dist/config/application-config.js +5 -1
  8. package/dist/config/application-config.js.map +1 -1
  9. package/dist/config/mcp-config.d.ts +2 -1
  10. package/dist/config/mcp-config.js +3 -2
  11. package/dist/config/mcp-config.js.map +1 -1
  12. package/dist/config/schemas.d.ts +21 -0
  13. package/dist/config/schemas.js +2 -0
  14. package/dist/config/schemas.js.map +1 -1
  15. package/dist/hooks/distribution.d.ts +1 -1
  16. package/dist/hooks/distribution.js +7 -1
  17. package/dist/hooks/distribution.js.map +1 -1
  18. package/dist/index.js +262 -197
  19. package/dist/index.js.map +1 -1
  20. package/dist/library/state.d.ts +2 -0
  21. package/dist/library/state.js +6 -0
  22. package/dist/library/state.js.map +1 -1
  23. package/dist/rules/distribution.d.ts +1 -0
  24. package/dist/rules/distribution.js +5 -1
  25. package/dist/rules/distribution.js.map +1 -1
  26. package/dist/skills/distribution.d.ts +1 -0
  27. package/dist/skills/distribution.js +30 -16
  28. package/dist/skills/distribution.js.map +1 -1
  29. package/dist/subagents/distribution.d.ts +1 -1
  30. package/dist/subagents/distribution.js +34 -13
  31. package/dist/subagents/distribution.js.map +1 -1
  32. package/dist/targets/builtin/claude-code.js +2 -0
  33. package/dist/targets/builtin/claude-code.js.map +1 -1
  34. package/dist/targets/builtin/codex.js +3 -1
  35. package/dist/targets/builtin/codex.js.map +1 -1
  36. package/dist/targets/builtin/cursor.js +4 -2
  37. package/dist/targets/builtin/cursor.js.map +1 -1
  38. package/dist/targets/builtin/gemini.js +2 -0
  39. package/dist/targets/builtin/gemini.js.map +1 -1
  40. package/dist/targets/builtin/opencode.js +4 -2
  41. package/dist/targets/builtin/opencode.js.map +1 -1
  42. package/dist/targets/registry.d.ts +5 -2
  43. package/dist/targets/registry.js +6 -3
  44. package/dist/targets/registry.js.map +1 -1
  45. 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,195 +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 activeAppIds = config.applications.active;
218
- const mcpDistribution = await applyToAgents(scope, undefined, { useSpinner: false });
219
- const ruleDistribution = distributeRules(undefined, { activeAppIds }, scope);
220
- const commandDistribution = distributeCommands(scope, activeAppIds);
221
- const agentDistribution = distributeSubagents(scope, activeAppIds);
222
- const skillDistribution = distributeSkills(scope, {
223
- useAgentsDir: config.distribution.use_agents_dir,
224
- activeAppIds,
225
- });
226
- const hookDistribution = distributeHooks(scope, activeAppIds);
227
- const distSections = [
228
- {
229
- label: 'mcp',
230
- results: mcpDistribution,
231
- emptyMessage: 'no apps configured',
232
- getTargetLabel: (r) => r.application,
233
- getPath: (r) => r.filePath,
234
- },
235
- {
236
- label: 'rules',
237
- results: ruleDistribution.results,
238
- emptyMessage: 'none',
239
- getTargetLabel: (r) => r.agent,
240
- getPath: (r) => r.filePath,
241
- },
242
- {
243
- label: 'commands',
244
- results: commandDistribution.results,
245
- emptyMessage: 'none',
246
- getTargetLabel: (r) => r.platform,
247
- getPath: (r) => r.filePath,
248
- },
249
- {
250
- label: 'agents',
251
- results: agentDistribution.results,
252
- emptyMessage: 'none',
253
- getTargetLabel: (r) => r.platform,
254
- getPath: (r) => r.filePath,
255
- },
256
- {
257
- label: 'skills',
258
- results: skillDistribution.results,
259
- emptyMessage: 'none',
260
- getTargetLabel: (r) => {
261
- const sr = r;
262
- return sr.platform === 'agents' ? 'codex+gemini+opencode' : sr.platform;
263
- },
264
- getPath: (r) => r.targetDir,
265
- },
266
- {
267
- label: 'hooks',
268
- results: hookDistribution.results,
269
- emptyMessage: 'none',
270
- getTargetLabel: (r) => r.platform,
271
- getPath: (r) => {
272
- const hr = r;
273
- return 'filePath' in hr ? hr.filePath : hr.targetDir;
274
- },
275
- },
276
- ];
277
- const { hasErrors } = printCompactDistributions(distSections);
278
- console.log();
279
- if (hasErrors) {
280
- console.log(chalk.red('✗ Sync completed with errors.'));
281
- process.exit(1);
121
+ console.log(chalk.green('✓ Sync complete.'));
282
122
  }
283
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
+ }
284
132
  console.log(chalk.green('✓ Sync complete.'));
285
133
  }
286
134
  }
@@ -291,6 +139,211 @@ program
291
139
  process.exit(1);
292
140
  }
293
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
+ }
294
347
  function resolveScope(input) {
295
348
  if (!input)
296
349
  return undefined;
@@ -670,7 +723,11 @@ ruleCommand.action(async (options) => {
670
723
  }
671
724
  }
672
725
  }
673
- const distribution = distributeRules(composeActiveRules(scope), { force: !selectionChanged, activeAppIds: config.applications.active }, 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);
674
731
  if (distribution.results.length > 0) {
675
732
  console.log();
676
733
  printDistributionResults({
@@ -721,7 +778,7 @@ commandRoot.action(async (options) => {
721
778
  return;
722
779
  console.log();
723
780
  printActiveSelection('commands', selection.enabled);
724
- const out = distributeCommands(scope, config.applications.active);
781
+ const out = distributeCommands(scope, config.applications.active, new Set(config.applications.assume_installed));
725
782
  if (out.results.length > 0) {
726
783
  console.log();
727
784
  printDistributionResults({
@@ -871,7 +928,7 @@ agentRoot.action(async (options) => {
871
928
  return;
872
929
  console.log();
873
930
  printActiveSelection('agents', selection.enabled);
874
- const out = distributeSubagents(scope, config.applications.active);
931
+ const out = distributeSubagents(scope, config.applications.active, new Set(config.applications.assume_installed));
875
932
  if (out.results.length > 0) {
876
933
  console.log();
877
934
  printDistributionResults({
@@ -1022,13 +1079,20 @@ skillRoot.action(async (options) => {
1022
1079
  const out = distributeSkills(scope, {
1023
1080
  useAgentsDir: config.distribution.use_agents_dir,
1024
1081
  activeAppIds: config.applications.active,
1082
+ assumeInstalled: new Set(config.applications.assume_installed),
1025
1083
  });
1026
1084
  if (out.results.length > 0) {
1027
1085
  console.log();
1028
1086
  printDistributionResults({
1029
1087
  title: 'Skill distribution',
1030
1088
  results: out.results,
1031
- 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
+ },
1032
1096
  getPath: (result) => result.targetDir,
1033
1097
  });
1034
1098
  }
@@ -1161,7 +1225,7 @@ hookRoot.action(async (options) => {
1161
1225
  return;
1162
1226
  console.log();
1163
1227
  printActiveSelection('hooks', selection.enabled);
1164
- const out = distributeHooks(scope, config.applications.active);
1228
+ const out = distributeHooks(scope, config.applications.active, new Set(config.applications.assume_installed));
1165
1229
  if (out.results.length > 0) {
1166
1230
  console.log();
1167
1231
  printDistributionResults({
@@ -1314,10 +1378,11 @@ hookRoot
1314
1378
  }
1315
1379
  });
1316
1380
  async function applyToAgents(scope, enabledServerNames, options) {
1317
- const mcpConfig = loadMcpConfigWithPlugins();
1381
+ const mcpConfig = loadMcpConfigWithPlugins(scope);
1318
1382
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1319
1383
  await initTargets(switchboardConfig);
1320
1384
  const useSpinner = options?.useSpinner ?? true;
1385
+ const assumeInstalled = options?.assumeInstalled ?? new Set(switchboardConfig.applications.assume_installed);
1321
1386
  const results = [];
1322
1387
  if (switchboardConfig.applications.active.length === 0) {
1323
1388
  if (useSpinner) {
@@ -1348,7 +1413,7 @@ async function applyToAgents(scope, enabledServerNames, options) {
1348
1413
  const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
1349
1414
  const configToApply = { mcpServers: enabledServers };
1350
1415
  const target = getTargetById(agentId);
1351
- if (target?.isInstalled?.() === false) {
1416
+ if (!assumeInstalled.has(agentId) && target?.isInstalled?.() === false) {
1352
1417
  persist(chalk.gray('○'), `${chalk.cyan(agentId)} ${chalk.gray('(not installed, skipped)')}`);
1353
1418
  results.push({
1354
1419
  application: agentId,