agent-switchboard 0.1.26 → 0.1.28

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 (97) hide show
  1. package/README.md +37 -11
  2. package/dist/commands/distribution.js +2 -2
  3. package/dist/commands/distribution.js.map +1 -1
  4. package/dist/commands/library.d.ts +1 -1
  5. package/dist/commands/library.js +5 -6
  6. package/dist/commands/library.js.map +1 -1
  7. package/dist/config/application-config.d.ts +37 -0
  8. package/dist/config/application-config.js +83 -0
  9. package/dist/config/application-config.js.map +1 -0
  10. package/dist/config/layered-config.js +43 -2
  11. package/dist/config/layered-config.js.map +1 -1
  12. package/dist/config/paths.d.ts +13 -2
  13. package/dist/config/paths.js +21 -3
  14. package/dist/config/paths.js.map +1 -1
  15. package/dist/config/schemas.d.ts +408 -43
  16. package/dist/config/schemas.js +32 -22
  17. package/dist/config/schemas.js.map +1 -1
  18. package/dist/config/switchboard-config.d.ts +3 -6
  19. package/dist/config/switchboard-config.js +3 -6
  20. package/dist/config/switchboard-config.js.map +1 -1
  21. package/dist/hooks/distribution.d.ts +27 -0
  22. package/dist/hooks/distribution.js +235 -0
  23. package/dist/hooks/distribution.js.map +1 -0
  24. package/dist/hooks/library.d.ts +40 -0
  25. package/dist/hooks/library.js +143 -0
  26. package/dist/hooks/library.js.map +1 -0
  27. package/dist/hooks/schema.d.ts +542 -0
  28. package/dist/hooks/schema.js +49 -0
  29. package/dist/hooks/schema.js.map +1 -0
  30. package/dist/index.js +708 -168
  31. package/dist/index.js.map +1 -1
  32. package/dist/library/fs.d.ts +1 -3
  33. package/dist/library/fs.js +24 -7
  34. package/dist/library/fs.js.map +1 -1
  35. package/dist/library/schema.d.ts +1 -1
  36. package/dist/library/schema.js +1 -1
  37. package/dist/library/sources.d.ts +75 -0
  38. package/dist/library/sources.js +285 -0
  39. package/dist/library/sources.js.map +1 -0
  40. package/dist/library/state.d.ts +4 -5
  41. package/dist/library/state.js +15 -31
  42. package/dist/library/state.js.map +1 -1
  43. package/dist/marketplace/plugin-loader.d.ts +30 -0
  44. package/dist/marketplace/plugin-loader.js +178 -0
  45. package/dist/marketplace/plugin-loader.js.map +1 -0
  46. package/dist/marketplace/reader.d.ts +36 -0
  47. package/dist/marketplace/reader.js +90 -0
  48. package/dist/marketplace/reader.js.map +1 -0
  49. package/dist/marketplace/schemas.d.ts +467 -0
  50. package/dist/marketplace/schemas.js +57 -0
  51. package/dist/marketplace/schemas.js.map +1 -0
  52. package/dist/marketplace/source-loader.d.ts +32 -0
  53. package/dist/marketplace/source-loader.js +45 -0
  54. package/dist/marketplace/source-loader.js.map +1 -0
  55. package/dist/rules/agents.d.ts +4 -4
  56. package/dist/rules/agents.js +10 -4
  57. package/dist/rules/agents.js.map +1 -1
  58. package/dist/rules/composer.d.ts +2 -2
  59. package/dist/rules/composer.js +5 -5
  60. package/dist/rules/composer.js.map +1 -1
  61. package/dist/rules/distribution.js +48 -112
  62. package/dist/rules/distribution.js.map +1 -1
  63. package/dist/rules/library.d.ts +1 -1
  64. package/dist/rules/library.js +4 -5
  65. package/dist/rules/library.js.map +1 -1
  66. package/dist/skills/distribution.js +6 -6
  67. package/dist/skills/distribution.js.map +1 -1
  68. package/dist/skills/library.d.ts +1 -2
  69. package/dist/skills/library.js +6 -7
  70. package/dist/skills/library.js.map +1 -1
  71. package/dist/subagents/distribution.d.ts +1 -1
  72. package/dist/subagents/distribution.js +319 -20
  73. package/dist/subagents/distribution.js.map +1 -1
  74. package/dist/subagents/importer.d.ts +1 -1
  75. package/dist/subagents/importer.js +61 -1
  76. package/dist/subagents/importer.js.map +1 -1
  77. package/dist/subagents/inventory.js +3 -3
  78. package/dist/subagents/inventory.js.map +1 -1
  79. package/dist/subagents/library.d.ts +2 -2
  80. package/dist/subagents/library.js +14 -20
  81. package/dist/subagents/library.js.map +1 -1
  82. package/dist/ui/hook-ui.d.ts +8 -0
  83. package/dist/ui/hook-ui.js +17 -0
  84. package/dist/ui/hook-ui.js.map +1 -0
  85. package/dist/ui/library-selector.d.ts +1 -1
  86. package/dist/ui/subagent-ui.js +3 -3
  87. package/dist/ui/subagent-ui.js.map +1 -1
  88. package/dist/util/cli.d.ts +20 -0
  89. package/dist/util/cli.js +107 -14
  90. package/dist/util/cli.js.map +1 -1
  91. package/package.json +3 -2
  92. package/dist/config/agent-config.d.ts +0 -35
  93. package/dist/config/agent-config.js +0 -88
  94. package/dist/config/agent-config.js.map +0 -1
  95. package/dist/library/subscriptions.d.ts +0 -42
  96. package/dist/library/subscriptions.js +0 -116
  97. package/dist/library/subscriptions.js.map +0 -1
package/dist/index.js CHANGED
@@ -14,13 +14,16 @@ import { getAgentById } from './agents/registry.js';
14
14
  import { distributeCommands } from './commands/distribution.js';
15
15
  import { importCommandFromFile } from './commands/importer.js';
16
16
  import { buildCommandInventory } from './commands/inventory.js';
17
- import { resolveAgentSectionConfig } from './config/agent-config.js';
17
+ import { resolveApplicationSectionConfig } from './config/application-config.js';
18
18
  import { loadMcpConfig, stripLegacyEnabledFlagsFromMcpJson } from './config/mcp-config.js';
19
- import { getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getOpencodePath, getSkillsDir, getSubagentsDir, } from './config/paths.js';
19
+ import { getAgentsDir, getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getHooksDir, getOpencodePath, getSkillsDir, getSourceCacheDir, } from './config/paths.js';
20
20
  import { loadSwitchboardConfig, loadSwitchboardConfigWithLayers, } from './config/switchboard-config.js';
21
+ import { distributeHooks } from './hooks/distribution.js';
22
+ import { loadHookLibrary } from './hooks/library.js';
21
23
  import { ensureLibraryDirectories, writeFileSecure } from './library/fs.js';
22
- import { loadMcpActiveState, saveMcpActiveState } from './library/state.js';
23
- import { addSubscription, getSubscriptions, removeSubscription, validateSubscriptionPath, } from './library/subscriptions.js';
24
+ import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
25
+ import { loadLibraryStateSection, loadMcpActiveState, saveMcpActiveState, } from './library/state.js';
26
+ import { readMarketplace } from './marketplace/reader.js';
24
27
  import { RULE_SUPPORTED_AGENTS } from './rules/agents.js';
25
28
  import { composeActiveRules } from './rules/composer.js';
26
29
  import { distributeRules, listIndirectAgents, listPerFileAgents, listUnsupportedAgents, } from './rules/distribution.js';
@@ -34,17 +37,18 @@ import { distributeSubagents } from './subagents/distribution.js';
34
37
  import { importSubagentFromFile } from './subagents/importer.js';
35
38
  import { buildSubagentInventory } from './subagents/inventory.js';
36
39
  import { showCommandSelector } from './ui/command-ui.js';
40
+ import { showHookSelector } from './ui/hook-ui.js';
37
41
  import { showMcpServerUI } from './ui/mcp-ui.js';
38
42
  import { showRuleSelector } from './ui/rule-ui.js';
39
43
  import { showSkillSelector } from './ui/skill-ui.js';
40
44
  import { showSubagentSelector } from './ui/subagent-ui.js';
41
- import { printActiveSelection, printAgentSyncStatus, printDistributionResults, printTable, } from './util/cli.js';
45
+ import { printActiveSelection, printAgentSyncStatus, printCompactDistributions, printDistributionResults, printTable, shortenPath, } from './util/cli.js';
42
46
  const program = new Command();
43
47
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
48
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
45
49
  program
46
50
  .name('asb')
47
- .description('Manage MCP servers, rules, commands, subagents, and skills across AI coding agents')
51
+ .description('Manage MCP servers, rules, commands, agents, skills, and hooks across AI coding agents')
48
52
  .version(packageJson.version)
49
53
  .addHelpText('after', `
50
54
  Examples:
@@ -55,110 +59,202 @@ Examples:
55
59
 
56
60
  Alias: agent-switchboard
57
61
  Config: ~/.agent-switchboard/config.toml`);
58
- // Initialize library directories for commands/subagents (secure permissions)
62
+ // Initialize library directories for commands/agents (secure permissions)
59
63
  ensureLibraryDirectories();
60
64
  program
61
65
  .command('sync')
62
- .description('Synchronize active MCP servers, rules, commands, subagents, and skills to agent targets')
66
+ .description('Synchronize active MCP servers, rules, commands, agents, and skills to application targets')
63
67
  .option('-p, --profile <name>', 'Profile configuration to use')
64
68
  .option('--project <path>', 'Project directory containing .asb.toml')
69
+ .option('--no-update', 'Skip updating remote sources')
65
70
  .action(async (options) => {
66
71
  try {
67
72
  const scope = resolveScope(options);
68
73
  const loadOptions = scopeToLoadOptions(scope);
69
74
  const { config, layers } = loadSwitchboardConfigWithLayers(loadOptions);
75
+ console.log(chalk.yellow('⚠ Sync overwrites agent config without diff.'));
70
76
  console.log();
71
- console.log(`${chalk.bgRed.white(' WARNING ')} ${chalk.red('`asb sync` overwrites target files without diff checks.')}`);
72
- console.log(chalk.red('Proceeding with synchronization...'));
73
- console.log();
74
- console.log(chalk.blue('Configuration layers:'));
75
- const layerEntries = [
76
- { label: 'User', exists: layers.user.exists, path: layers.user.path },
77
- {
78
- label: 'Profile',
79
- exists: layers.profile?.exists === true,
80
- path: layers.profile?.path ?? '(none)',
81
- },
82
- {
83
- label: 'Project',
84
- exists: layers.project?.exists === true,
85
- path: layers.project?.path ?? '(none)',
86
- },
87
- ];
88
- for (const entry of layerEntries) {
89
- const marker = entry.exists ? chalk.green('✓') : chalk.gray('•');
90
- const pathLabel = entry.exists ? chalk.dim(entry.path) : chalk.gray(entry.path);
91
- console.log(` ${marker} ${entry.label}: ${pathLabel}`);
77
+ if (options.update !== false) {
78
+ const remoteResults = updateRemoteSources();
79
+ if (remoteResults.length > 0) {
80
+ console.log(chalk.blue('Sources:'));
81
+ for (const result of remoteResults) {
82
+ if (result.status === 'updated') {
83
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(result.namespace)} ${chalk.dim(result.url)}`);
84
+ }
85
+ else {
86
+ console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(result.namespace)} ${chalk.yellow(result.error ?? 'update failed')}`);
87
+ }
88
+ }
89
+ }
92
90
  }
91
+ // Config summary: show active layers on one line
92
+ const activeLayers = [];
93
+ if (layers.user.exists)
94
+ activeLayers.push(shortenPath(layers.user.path));
95
+ if (layers.profile?.exists)
96
+ activeLayers.push(shortenPath(layers.profile.path));
97
+ if (layers.project?.exists)
98
+ activeLayers.push(shortenPath(layers.project.path));
99
+ console.log(`${chalk.blue('Config:')} ${activeLayers.length > 0 ? chalk.dim(activeLayers.join(' + ')) : chalk.gray('no config files')}`);
100
+ const appsLabel = config.applications.active.length > 0
101
+ ? chalk.cyan(config.applications.active.join(', '))
102
+ : chalk.gray('none configured');
103
+ console.log(`${chalk.blue('Apps:')} ${appsLabel}`);
93
104
  console.log();
94
- console.log(chalk.blue('Active selections:'));
95
- console.log(` MCP servers: ${chalk.cyan(String(config.mcp.active.length))}`);
96
- console.log(` Rules: ${chalk.cyan(String(config.rules.active.length))}`);
97
- console.log(` Commands: ${chalk.cyan(String(config.commands.active.length))}`);
98
- console.log(` Subagents: ${chalk.cyan(String(config.subagents.active.length))}`);
99
- console.log(` Skills: ${chalk.cyan(String(config.skills.active.length))}`);
100
- if (config.agents.active.length > 0) {
101
- console.log(` Agents: ${chalk.cyan(config.agents.active.join(', '))}`);
105
+ const cursorSkillsDeduped = config.applications.active.includes('claude-code') &&
106
+ resolveApplicationSectionConfig('skills', 'claude-code', scope).active.length > 0;
107
+ console.log(chalk.blue('Inventory:'));
108
+ {
109
+ const sections = ['mcp', 'rules', 'commands', 'agents', 'skills', 'hooks'];
110
+ // Keep in sync with platform lists in each distribution module
111
+ const sectionPlatforms = {
112
+ mcp: ['claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'opencode'],
113
+ rules: ['claude-code', 'codex', 'cursor', 'gemini', 'opencode'],
114
+ commands: ['claude-code', 'codex', 'cursor', 'gemini', 'opencode'],
115
+ agents: ['claude-code', 'opencode', 'cursor'],
116
+ skills: cursorSkillsDeduped
117
+ ? ['claude-code', 'codex', 'gemini', 'opencode']
118
+ : ['claude-code', 'codex', 'gemini', 'opencode', 'cursor'],
119
+ hooks: ['claude-code'],
120
+ };
121
+ const termWidth = process.stdout.columns || 80;
122
+ const maxSectionLen = Math.max(...sections.map((s) => s.length));
123
+ const maxCountLen = Math.max(...sections.map((s) => `(${config[s].active.length})`.length));
124
+ const prefixPlainLen = 2 + maxSectionLen + 1 + maxCountLen + 2;
125
+ const fitPreview = (ids, maxWidth) => {
126
+ if (ids.length === 0)
127
+ return chalk.gray('none');
128
+ const full = ids.join(', ');
129
+ if (full.length <= maxWidth)
130
+ return full;
131
+ let text = '';
132
+ let shown = 0;
133
+ for (let i = 0; i < ids.length; i++) {
134
+ const sep = shown > 0 ? ', ' : '';
135
+ const candidate = text + sep + ids[i];
136
+ const remaining = ids.length - (i + 1);
137
+ if (remaining > 0) {
138
+ const suffix = `, ... (+${remaining} more)`;
139
+ if (candidate.length + suffix.length > maxWidth && shown > 0) {
140
+ const left = ids.length - shown;
141
+ return `${text}${chalk.gray(`, ... (+${left} more)`)}`;
142
+ }
143
+ }
144
+ text = candidate;
145
+ shown++;
146
+ }
147
+ return text;
148
+ };
149
+ for (const section of sections) {
150
+ const globalActive = config[section].active;
151
+ const globalCount = globalActive.length;
152
+ const supported = new Set(sectionPlatforms[section] ?? []);
153
+ const applicableApps = config.applications.active.filter((id) => supported.has(id));
154
+ const effectiveByApp = new Map();
155
+ for (const appId of applicableApps) {
156
+ effectiveByApp.set(appId, resolveApplicationSectionConfig(section, appId, scope).active);
157
+ }
158
+ const perAppParts = applicableApps.map((appId) => {
159
+ const eff = effectiveByApp.get(appId) ?? [];
160
+ const delta = eff.length - globalCount;
161
+ const d = delta === 0 ? '' : delta > 0 ? `(+${delta})` : `(${delta})`;
162
+ return `${appId}:${eff.length}${d}`;
163
+ });
164
+ const union = new Set();
165
+ for (const [, ids] of effectiveByApp) {
166
+ for (const id of ids)
167
+ union.add(id);
168
+ }
169
+ const previewIds = globalActive.length > 0 ? [...globalActive] : [...union];
170
+ const paddedSection = section.padEnd(maxSectionLen);
171
+ const countStr = `(${globalCount})`.padStart(maxCountLen);
172
+ const appsStr = perAppParts.join(' ');
173
+ console.log(` ${chalk.cyan(paddedSection)} ${chalk.gray(countStr)} ${appsStr}`);
174
+ if (previewIds.length > 0) {
175
+ const indent = ' '.repeat(prefixPlainLen);
176
+ const previewWidth = Math.max(20, termWidth - prefixPlainLen - 2);
177
+ const preview = fitPreview(previewIds, previewWidth);
178
+ console.log(`${indent}${chalk.gray('→')} ${preview}`);
179
+ }
180
+ }
102
181
  }
103
- else {
104
- console.log(` Agents: ${chalk.gray('none configured')}`);
182
+ console.log();
183
+ const notes = ['rules, skills also distribute to gemini'];
184
+ if (cursorSkillsDeduped)
185
+ notes.push('cursor reads skills via claude-code');
186
+ for (let i = 0; i < notes.length; i++) {
187
+ const prefix = i === 0 ? ' Note: ' : ' ';
188
+ const suffix = i === notes.length - 1 ? '.' : '.';
189
+ console.log(chalk.gray(`${prefix}${notes[i]}${suffix}`));
105
190
  }
106
191
  console.log();
107
- // Sync MCP servers to agents
108
- console.log(chalk.blue('MCP server distribution:'));
109
- await applyToAgents(scope);
110
- const ruleDistribution = distributeRules(undefined, { force: true }, scope);
192
+ const mcpDistribution = await applyToAgents(scope, undefined, { useSpinner: false });
193
+ const ruleDistribution = distributeRules(undefined, undefined, scope);
111
194
  const commandDistribution = distributeCommands(scope);
112
- const subagentDistribution = distributeSubagents(scope);
195
+ const agentDistribution = distributeSubagents(scope);
113
196
  const skillDistribution = distributeSkills(scope, {
114
197
  useAgentsDir: config.distribution.use_agents_dir,
115
198
  });
116
- const ruleErrors = ruleDistribution.results.filter((result) => result.status === 'error');
117
- const commandErrors = commandDistribution.results.filter((result) => result.status === 'error');
118
- const subagentErrors = subagentDistribution.results.filter((result) => result.status === 'error');
119
- const skillErrors = skillDistribution.results.filter((result) => result.status === 'error');
120
- printDistributionResults({
121
- title: 'Rule distribution',
122
- results: ruleDistribution.results,
123
- emptyMessage: 'no supported agents configured',
124
- getTargetLabel: (result) => result.agent,
125
- getPath: (result) => result.filePath,
126
- });
127
- console.log();
128
- printDistributionResults({
129
- title: 'Command distribution',
130
- results: commandDistribution.results,
131
- emptyMessage: 'no active commands',
132
- getTargetLabel: (result) => result.platform,
133
- getPath: (result) => result.filePath,
134
- });
135
- console.log();
136
- printDistributionResults({
137
- title: 'Subagent distribution',
138
- results: subagentDistribution.results,
139
- emptyMessage: 'no active subagents',
140
- getTargetLabel: (result) => result.platform,
141
- getPath: (result) => result.filePath,
142
- });
143
- console.log();
144
- printDistributionResults({
145
- title: 'Skill distribution',
146
- results: skillDistribution.results,
147
- emptyMessage: 'no active skills',
148
- getTargetLabel: (result) => result.platform === 'agents' ? 'codex+gemini+opencode' : result.platform,
149
- getPath: (result) => result.targetDir,
150
- });
199
+ const hookDistribution = distributeHooks(scope);
200
+ const distSections = [
201
+ {
202
+ label: 'mcp',
203
+ results: mcpDistribution,
204
+ emptyMessage: 'no apps configured',
205
+ getTargetLabel: (r) => r.application,
206
+ getPath: (r) => r.filePath,
207
+ },
208
+ {
209
+ label: 'rules',
210
+ results: ruleDistribution.results,
211
+ emptyMessage: 'none',
212
+ getTargetLabel: (r) => r.agent,
213
+ getPath: (r) => r.filePath,
214
+ },
215
+ {
216
+ label: 'commands',
217
+ results: commandDistribution.results,
218
+ emptyMessage: 'none',
219
+ getTargetLabel: (r) => r.platform,
220
+ getPath: (r) => r.filePath,
221
+ },
222
+ {
223
+ label: 'agents',
224
+ results: agentDistribution.results,
225
+ emptyMessage: 'none',
226
+ getTargetLabel: (r) => r.platform,
227
+ getPath: (r) => r.filePath,
228
+ },
229
+ {
230
+ label: 'skills',
231
+ results: skillDistribution.results,
232
+ emptyMessage: 'none',
233
+ getTargetLabel: (r) => {
234
+ const sr = r;
235
+ return sr.platform === 'agents' ? 'codex+gemini+opencode' : sr.platform;
236
+ },
237
+ getPath: (r) => r.targetDir,
238
+ },
239
+ {
240
+ label: 'hooks',
241
+ results: hookDistribution.results,
242
+ emptyMessage: 'none',
243
+ getTargetLabel: (r) => r.platform,
244
+ getPath: (r) => {
245
+ const hr = r;
246
+ return 'filePath' in hr ? hr.filePath : hr.targetDir;
247
+ },
248
+ },
249
+ ];
250
+ const { hasErrors } = printCompactDistributions(distSections);
151
251
  console.log();
152
- const hasErrors = ruleErrors.length > 0 ||
153
- commandErrors.length > 0 ||
154
- subagentErrors.length > 0 ||
155
- skillErrors.length > 0;
156
252
  if (hasErrors) {
157
- console.log(chalk.red('✗ Synchronization completed with errors.'));
253
+ console.log(chalk.red('✗ Sync completed with errors.'));
158
254
  process.exit(1);
159
255
  }
160
256
  else {
161
- console.log(chalk.green('✓ Synchronization complete.'));
257
+ console.log(chalk.green('✓ Sync complete.'));
162
258
  }
163
259
  }
164
260
  catch (error) {
@@ -219,7 +315,7 @@ function defaultCommandSourceDir(platform) {
219
315
  return getOpencodePath('command');
220
316
  }
221
317
  }
222
- function defaultSubagentSourceDir(platform) {
318
+ function defaultAgentSourceDir(platform) {
223
319
  switch (platform) {
224
320
  case 'claude-code':
225
321
  return path.join(getClaudeDir(), 'agents');
@@ -227,6 +323,8 @@ function defaultSubagentSourceDir(platform) {
227
323
  return path.join(getCursorDir(), 'agents');
228
324
  case 'opencode':
229
325
  return getOpencodePath('agent');
326
+ default:
327
+ throw new Error(`Unknown agent platform: ${String(platform)}`);
230
328
  }
231
329
  }
232
330
  function defaultSkillSourceDir(platform) {
@@ -275,6 +373,141 @@ async function confirmOverwrite(filePath, force) {
275
373
  return true;
276
374
  return await confirm({ message: `File exists: ${filePath}. Overwrite?`, default: false });
277
375
  }
376
+ function copyDirRecursive(src, dest) {
377
+ fs.mkdirSync(dest, { recursive: true });
378
+ const entries = fs.readdirSync(src, { withFileTypes: true });
379
+ for (const entry of entries) {
380
+ const srcPath = path.join(src, entry.name);
381
+ const destPath = path.join(dest, entry.name);
382
+ if (entry.isDirectory()) {
383
+ copyDirRecursive(srcPath, destPath);
384
+ }
385
+ else {
386
+ fs.copyFileSync(srcPath, destPath);
387
+ // Preserve executable permissions
388
+ try {
389
+ const mode = fs.statSync(srcPath).mode;
390
+ if (mode & 0o111)
391
+ fs.chmodSync(destPath, mode & 0o777);
392
+ }
393
+ catch {
394
+ // Ignore
395
+ }
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * Extract hooks from Claude Code's ~/.claude/settings.json and import as a
401
+ * bundle into ~/.asb/hooks/. Copies referenced script files if they exist
402
+ * alongside the hooks and rewrites commands to use ${HOOK_DIR}.
403
+ */
404
+ async function importHooksFromClaudeCode(opts) {
405
+ const settingsPath = path.join(getClaudeDir(), 'settings.json');
406
+ if (!isFile(settingsPath)) {
407
+ console.error(chalk.red(`\n✗ Claude Code settings not found: ${settingsPath}`));
408
+ process.exit(1);
409
+ }
410
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
411
+ const hooks = settings.hooks;
412
+ if (!hooks || Object.keys(hooks).length === 0) {
413
+ console.log(chalk.yellow('\n⚠ No hooks found in Claude Code settings.'));
414
+ return;
415
+ }
416
+ // Filter out ASB-managed hooks (avoid re-importing our own output)
417
+ const userHooks = {};
418
+ for (const [event, groups] of Object.entries(hooks)) {
419
+ const userGroups = groups.filter((g) => g._asb_source === undefined);
420
+ if (userGroups.length > 0)
421
+ userHooks[event] = userGroups;
422
+ }
423
+ if (Object.keys(userHooks).length === 0) {
424
+ console.log(chalk.yellow('\n⚠ No user-defined hooks found (all are ASB-managed).'));
425
+ return;
426
+ }
427
+ // Collect script paths referenced in commands
428
+ const referencedScripts = new Set();
429
+ for (const groups of Object.values(userHooks)) {
430
+ for (const group of groups) {
431
+ const handlers = group.hooks;
432
+ if (!handlers)
433
+ continue;
434
+ for (const handler of handlers) {
435
+ if (typeof handler.command !== 'string')
436
+ continue;
437
+ // Extract file paths from commands like "node ~/.claude/hooks/script.mjs"
438
+ const match = handler.command.match(/(?:^|\s)(~\/\.claude\/hooks\/\S+|(?:\/\S*\/)?\.claude\/hooks\/\S+)/);
439
+ if (match) {
440
+ const scriptPath = match[1].replace(/^~/, process.env.HOME ?? '');
441
+ if (fs.existsSync(scriptPath)) {
442
+ referencedScripts.add(scriptPath);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ const slug = 'claude-code-hooks';
449
+ const bundleDir = path.join(getHooksDir(), slug);
450
+ if (fs.existsSync(bundleDir)) {
451
+ if (!(await confirmOverwrite(bundleDir, opts.force)))
452
+ return;
453
+ fs.rmSync(bundleDir, { recursive: true });
454
+ }
455
+ fs.mkdirSync(bundleDir, { recursive: true });
456
+ // Copy referenced scripts and rewrite commands to use ${HOOK_DIR}
457
+ const { HOOK_DIR_PLACEHOLDER } = await import('./hooks/schema.js');
458
+ const rewrittenHooks = {};
459
+ for (const [event, groups] of Object.entries(userHooks)) {
460
+ rewrittenHooks[event] = groups.map((group) => {
461
+ const handlers = group.hooks;
462
+ if (!handlers)
463
+ return group;
464
+ return {
465
+ ...group,
466
+ hooks: handlers.map((handler) => {
467
+ if (typeof handler.command !== 'string')
468
+ return handler;
469
+ let cmd = handler.command;
470
+ for (const scriptPath of referencedScripts) {
471
+ const scriptName = path.basename(scriptPath);
472
+ if (cmd.includes(scriptPath) ||
473
+ cmd.includes(scriptPath.replace(process.env.HOME ?? '', '~'))) {
474
+ cmd = cmd.replace(scriptPath, `${HOOK_DIR_PLACEHOLDER}/${scriptName}`);
475
+ cmd = cmd.replace(scriptPath.replace(process.env.HOME ?? '', '~'), `${HOOK_DIR_PLACEHOLDER}/${scriptName}`);
476
+ }
477
+ }
478
+ return { ...handler, command: cmd };
479
+ }),
480
+ };
481
+ });
482
+ }
483
+ // Copy script files into the bundle
484
+ let scriptsCopied = 0;
485
+ for (const scriptPath of referencedScripts) {
486
+ const scriptName = path.basename(scriptPath);
487
+ const destPath = path.join(bundleDir, scriptName);
488
+ fs.copyFileSync(scriptPath, destPath);
489
+ try {
490
+ const mode = fs.statSync(scriptPath).mode;
491
+ if (mode & 0o111)
492
+ fs.chmodSync(destPath, mode & 0o777);
493
+ }
494
+ catch {
495
+ // Ignore
496
+ }
497
+ scriptsCopied++;
498
+ console.log(` ${chalk.green('✓')} ${chalk.dim(scriptName)}`);
499
+ }
500
+ // Write hook.json
501
+ const hookJson = {
502
+ name: 'claude-code-hooks',
503
+ description: 'Hooks imported from Claude Code settings.json',
504
+ hooks: rewrittenHooks,
505
+ };
506
+ fs.writeFileSync(path.join(bundleDir, 'hook.json'), `${JSON.stringify(hookJson, null, 2)}\n`, 'utf-8');
507
+ const eventCount = Object.keys(rewrittenHooks).length;
508
+ console.log(`\n${chalk.green('✓')} Imported ${eventCount} event(s) + ${scriptsCopied} script(s) → ${chalk.dim(bundleDir)}`);
509
+ console.log(chalk.gray(' Activate with: asb hook (interactive) or add to [hooks].active in config.toml'));
510
+ }
278
511
  program
279
512
  .command('mcp')
280
513
  .description('Interactive UI to enable/disable MCP servers')
@@ -652,13 +885,13 @@ commandRoot
652
885
  process.exit(1);
653
886
  }
654
887
  });
655
- // Subagents library: scaffold, load, list, and interactive distribute
656
- const subagentRoot = program
657
- .command('subagent')
658
- .description('Select subagent definitions interactively and distribute to agents')
888
+ // Agents library: load, list, and interactive distribute
889
+ const agentRoot = program
890
+ .command('agent')
891
+ .description('Select agent definitions interactively and distribute to applications')
659
892
  .option('-p, --profile <name>', 'Profile configuration to use')
660
893
  .option('--project <path>', 'Project directory containing .asb.toml');
661
- subagentRoot.action(async (options) => {
894
+ agentRoot.action(async (options) => {
662
895
  try {
663
896
  const scope = resolveScope(options);
664
897
  const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
@@ -666,18 +899,17 @@ subagentRoot.action(async (options) => {
666
899
  if (!selection)
667
900
  return;
668
901
  console.log();
669
- printActiveSelection('subagents', selection.active);
902
+ printActiveSelection('agents', selection.active);
670
903
  const out = distributeSubagents(scope);
671
904
  if (out.results.length > 0) {
672
905
  console.log();
673
906
  printDistributionResults({
674
- title: 'Subagent distribution',
907
+ title: 'Agent distribution',
675
908
  results: out.results,
676
909
  getTargetLabel: (result) => result.platform,
677
910
  getPath: (result) => result.filePath,
678
911
  });
679
912
  }
680
- // Guidance: unsupported platforms for subagents
681
913
  console.log();
682
914
  console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
683
915
  }
@@ -688,9 +920,9 @@ subagentRoot.action(async (options) => {
688
920
  process.exit(1);
689
921
  }
690
922
  });
691
- subagentRoot
923
+ agentRoot
692
924
  .command('load')
693
- .description('Import existing platform files into the subagent library')
925
+ .description('Import existing platform files into the agent library')
694
926
  .argument('<platform>', 'claude-code | opencode | cursor')
695
927
  .argument('[path]', 'Source file or directory (defaults by platform)')
696
928
  .option('-r, --recursive', 'When [path] is a directory, import files recursively')
@@ -698,7 +930,7 @@ subagentRoot
698
930
  .action(async (platform, srcPath, opts) => {
699
931
  try {
700
932
  const exts = ['.md', '.markdown'];
701
- const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultSubagentSourceDir(platform);
933
+ const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultAgentSourceDir(platform);
702
934
  if (!fs.existsSync(source)) {
703
935
  console.error(chalk.red(`\n✗ Source not found: ${source}`));
704
936
  process.exit(1);
@@ -718,7 +950,7 @@ subagentRoot
718
950
  console.log(chalk.yellow('\n⚠ No files to import.'));
719
951
  return;
720
952
  }
721
- const outDir = getSubagentsDir();
953
+ const outDir = getAgentsDir();
722
954
  let imported = 0;
723
955
  for (const file of inputs) {
724
956
  try {
@@ -735,7 +967,7 @@ subagentRoot
735
967
  console.log(`${chalk.red('✗')} ${chalk.dim(file)} ${chalk.red(msg)}`);
736
968
  }
737
969
  }
738
- console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into subagent library.`);
970
+ console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into agent library.`);
739
971
  }
740
972
  catch (error) {
741
973
  if (error instanceof Error) {
@@ -744,9 +976,9 @@ subagentRoot
744
976
  process.exit(1);
745
977
  }
746
978
  });
747
- subagentRoot
979
+ agentRoot
748
980
  .command('list')
749
- .description('Display subagent inventory and sync information')
981
+ .description('Display agent inventory and sync information')
750
982
  .option('--json', 'Output inventory as JSON')
751
983
  .option('-p, --profile <name>', 'Profile configuration to use')
752
984
  .option('--project <path>', 'Project directory containing .asb.toml')
@@ -763,10 +995,10 @@ subagentRoot
763
995
  return;
764
996
  }
765
997
  if (inventory.entries.length === 0) {
766
- console.log(chalk.yellow('⚠ No subagents found. Use `asb subagent load <platform> [path]`.'));
998
+ console.log(chalk.yellow('⚠ No agents found. Use `asb agent load <platform> [path]`.'));
767
999
  }
768
1000
  else {
769
- console.log(chalk.blue('Subagents:'));
1001
+ console.log(chalk.blue('Agents:'));
770
1002
  const header = ['ID', 'Active', 'Title', 'Model', 'Tools', 'Extras'];
771
1003
  const rows = inventory.entries.map((row) => {
772
1004
  const activePlain = row.active ? 'yes' : 'no';
@@ -790,7 +1022,6 @@ subagentRoot
790
1022
  }
791
1023
  console.log();
792
1024
  printAgentSyncStatus({ agentSync: inventory.state.agentSync });
793
- // Guidance: unsupported platforms for subagents
794
1025
  console.log();
795
1026
  console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
796
1027
  }
@@ -942,30 +1173,198 @@ skillRoot
942
1173
  process.exit(1);
943
1174
  }
944
1175
  });
945
- /**
946
- * Apply enabled MCP servers to all registered agents
947
- * @param scope - Configuration scope (profile/project)
948
- * @param enabledServerNames - List of enabled server names
949
- */
950
- async function applyToAgents(scope, enabledServerNames) {
1176
+ // Hooks library: manage and distribute hooks to Claude Code
1177
+ const hookRoot = program
1178
+ .command('hook')
1179
+ .description('Select hooks interactively and distribute to Claude Code')
1180
+ .option('-p, --profile <name>', 'Profile configuration to use')
1181
+ .option('--project <path>', 'Project directory containing .asb.toml');
1182
+ hookRoot.action(async (options) => {
1183
+ try {
1184
+ const scope = resolveScope(options);
1185
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1186
+ const selection = await showHookSelector({ scope, pageSize: config.ui.page_size });
1187
+ if (!selection)
1188
+ return;
1189
+ console.log();
1190
+ printActiveSelection('hooks', selection.active);
1191
+ const out = distributeHooks(scope);
1192
+ if (out.results.length > 0) {
1193
+ console.log();
1194
+ printDistributionResults({
1195
+ title: 'Hook distribution',
1196
+ results: out.results,
1197
+ getTargetLabel: (result) => result.platform,
1198
+ getPath: (result) => 'filePath' in result ? result.filePath : result.targetDir,
1199
+ });
1200
+ }
1201
+ }
1202
+ catch (error) {
1203
+ if (error instanceof Error) {
1204
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1205
+ }
1206
+ process.exit(1);
1207
+ }
1208
+ });
1209
+ hookRoot
1210
+ .command('list')
1211
+ .description('Display hook library entries')
1212
+ .option('--json', 'Output inventory as JSON')
1213
+ .option('-p, --profile <name>', 'Profile configuration to use')
1214
+ .option('--project <path>', 'Project directory containing .asb.toml')
1215
+ .action((options) => {
1216
+ try {
1217
+ const scope = resolveScope(options);
1218
+ const entries = loadHookLibrary();
1219
+ const state = loadLibraryStateSection('hooks', scope);
1220
+ const activeSet = new Set(state.active);
1221
+ if (options.json) {
1222
+ console.log(JSON.stringify({
1223
+ entries: entries.map((e) => ({
1224
+ id: e.id,
1225
+ name: e.name,
1226
+ description: e.description,
1227
+ events: Object.keys(e.hooks),
1228
+ active: activeSet.has(e.id),
1229
+ })),
1230
+ active: state.active,
1231
+ }, null, 2));
1232
+ return;
1233
+ }
1234
+ if (entries.length === 0) {
1235
+ console.log(chalk.yellow(`⚠ No hooks found. Add JSON hook files to ${getHooksDir()}`));
1236
+ }
1237
+ else {
1238
+ console.log(chalk.blue('Hooks:'));
1239
+ const header = ['ID', 'Active', 'Name', 'Events', 'Description'];
1240
+ const rows = entries.map((e) => {
1241
+ const active = activeSet.has(e.id);
1242
+ const activePlain = active ? 'yes' : 'no';
1243
+ const events = Object.keys(e.hooks).join(', ');
1244
+ const desc = e.description ?? '—';
1245
+ const descTrunc = desc.length > 40 ? `${desc.substring(0, 37)}...` : desc;
1246
+ return [
1247
+ { plain: e.id, formatted: e.id },
1248
+ {
1249
+ plain: activePlain,
1250
+ formatted: active ? chalk.green(activePlain) : chalk.gray(activePlain),
1251
+ },
1252
+ { plain: e.name ?? e.id, formatted: e.name ?? e.id },
1253
+ { plain: events, formatted: events },
1254
+ { plain: descTrunc, formatted: descTrunc },
1255
+ ];
1256
+ });
1257
+ printTable(header, rows);
1258
+ }
1259
+ }
1260
+ catch (error) {
1261
+ if (error instanceof Error) {
1262
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1263
+ }
1264
+ process.exit(1);
1265
+ }
1266
+ });
1267
+ hookRoot
1268
+ .command('load')
1269
+ .description('Import hooks into the library. Accepts:\n' +
1270
+ ' - A JSON file (single-file hook)\n' +
1271
+ ' - A directory with hook.json (bundle hook)\n' +
1272
+ ' - "claude-code" to extract from ~/.claude/settings.json')
1273
+ .argument('<source>', 'JSON file, directory, or "claude-code"')
1274
+ .option('-f, --force', 'Overwrite existing library entries without confirmation')
1275
+ .option('-n, --name <name>', 'Override the hook ID (default: basename of source)')
1276
+ .action(async (source, opts) => {
1277
+ try {
1278
+ if (source === 'claude-code') {
1279
+ await importHooksFromClaudeCode(opts);
1280
+ return;
1281
+ }
1282
+ const resolved = path.resolve(source);
1283
+ if (isFile(resolved)) {
1284
+ // Single-file hook import
1285
+ const content = fs.readFileSync(resolved, 'utf-8');
1286
+ try {
1287
+ const { hookFileSchema } = await import('./hooks/schema.js');
1288
+ hookFileSchema.parse(JSON.parse(content));
1289
+ }
1290
+ catch (error) {
1291
+ const msg = error instanceof Error ? error.message : String(error);
1292
+ console.error(chalk.red(`\n✗ Invalid hook file: ${msg}`));
1293
+ process.exit(1);
1294
+ }
1295
+ const slug = opts.name ?? path.basename(resolved, path.extname(resolved));
1296
+ const target = path.join(getHooksDir(), `${slug}.json`);
1297
+ if (!(await confirmOverwrite(target, opts.force)))
1298
+ return;
1299
+ writeFileSecure(target, content);
1300
+ console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(target)}`);
1301
+ }
1302
+ else if (isDir(resolved)) {
1303
+ // Bundle hook import (directory with hook.json + scripts)
1304
+ const hookJsonPath = path.join(resolved, 'hook.json');
1305
+ if (!isFile(hookJsonPath)) {
1306
+ console.error(chalk.red('\n✗ Directory does not contain hook.json'));
1307
+ process.exit(1);
1308
+ }
1309
+ // Validate hook.json
1310
+ try {
1311
+ const { hookFileSchema } = await import('./hooks/schema.js');
1312
+ hookFileSchema.parse(JSON.parse(fs.readFileSync(hookJsonPath, 'utf-8')));
1313
+ }
1314
+ catch (error) {
1315
+ const msg = error instanceof Error ? error.message : String(error);
1316
+ console.error(chalk.red(`\n✗ Invalid hook.json: ${msg}`));
1317
+ process.exit(1);
1318
+ }
1319
+ const slug = opts.name ?? path.basename(resolved);
1320
+ const targetDir = path.join(getHooksDir(), slug);
1321
+ if (fs.existsSync(targetDir)) {
1322
+ if (!(await confirmOverwrite(targetDir, opts.force)))
1323
+ return;
1324
+ fs.rmSync(targetDir, { recursive: true });
1325
+ }
1326
+ // Copy entire directory
1327
+ copyDirRecursive(resolved, targetDir);
1328
+ const fileCount = listFilesRecursively(targetDir, []).length;
1329
+ console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(targetDir)} (${fileCount} files)`);
1330
+ }
1331
+ else {
1332
+ console.error(chalk.red(`\n✗ Source not found: ${resolved}`));
1333
+ process.exit(1);
1334
+ }
1335
+ }
1336
+ catch (error) {
1337
+ if (error instanceof Error) {
1338
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1339
+ }
1340
+ process.exit(1);
1341
+ }
1342
+ });
1343
+ async function applyToAgents(scope, enabledServerNames, options) {
951
1344
  const mcpConfig = loadMcpConfig();
952
1345
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
953
- if (switchboardConfig.agents.active.length === 0) {
954
- console.log(chalk.yellow('\n⚠ No agents found in the active configuration stack.'));
955
- console.log();
956
- console.log('Add agents under the relevant TOML layer (user, profile, or project).');
957
- console.log(chalk.dim(' Example: [agents]\n active = ["claude-code", "cursor"]'));
958
- return;
1346
+ const useSpinner = options?.useSpinner ?? true;
1347
+ const results = [];
1348
+ if (switchboardConfig.applications.active.length === 0) {
1349
+ if (useSpinner) {
1350
+ console.log(chalk.yellow('\n⚠ No applications found in the active configuration stack.'));
1351
+ console.log();
1352
+ console.log('Add applications under the relevant TOML layer (user, profile, or project).');
1353
+ console.log(chalk.dim(' Example: [applications]\n active = ["claude-code", "cursor"]'));
1354
+ }
1355
+ return results;
959
1356
  }
960
1357
  // Global MCP servers list (from UI selection or config)
961
1358
  const globalMcpServers = enabledServerNames ?? loadMcpActiveState(scope);
962
- // Apply to each registered agent with per-agent MCP overrides
963
- for (const agentId of switchboardConfig.agents.active) {
964
- const spinner = ora({ indent: 2 }).start(`Applying to ${agentId}...`);
965
- const persist = (symbol, text) => spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1359
+ for (const agentId of switchboardConfig.applications.active) {
1360
+ const spinner = useSpinner ? ora({ indent: 2 }).start(`Applying to ${agentId}...`) : null;
1361
+ const persist = (symbol, text) => {
1362
+ if (!spinner)
1363
+ return;
1364
+ spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1365
+ };
966
1366
  try {
967
- // Get per-agent MCP config (applies add/remove overrides)
968
- const agentMcpConfig = resolveAgentSectionConfig('mcp', agentId, scope);
1367
+ const agentMcpConfig = resolveApplicationSectionConfig('mcp', agentId, scope);
969
1368
  // If user selected servers via UI, use that as base; otherwise use per-agent resolved config
970
1369
  const agentActiveServers = enabledServerNames
971
1370
  ? agentMcpConfig.active.filter((s) => globalMcpServers.includes(s))
@@ -975,23 +1374,57 @@ async function applyToAgents(scope, enabledServerNames) {
975
1374
  const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
976
1375
  const configToApply = { mcpServers: enabledServers };
977
1376
  const agent = getAgentById(agentId);
1377
+ const readFileSafe = (p) => {
1378
+ try {
1379
+ return fs.readFileSync(p, 'utf-8');
1380
+ }
1381
+ catch {
1382
+ return null;
1383
+ }
1384
+ };
978
1385
  // Use project-level config when --project is specified
979
1386
  if (scope?.project && agent.applyProjectConfig) {
980
- agent.applyProjectConfig(scope.project, configToApply);
981
1387
  const projectPath = agent.projectConfigPath?.(scope.project) ?? 'project config';
982
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(projectPath)}`);
1388
+ const before = readFileSafe(projectPath);
1389
+ agent.applyProjectConfig(scope.project, configToApply);
1390
+ const after = readFileSafe(projectPath);
1391
+ const changed = before !== after;
1392
+ persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(projectPath))}`);
1393
+ results.push({
1394
+ application: agentId,
1395
+ filePath: projectPath,
1396
+ status: changed ? 'written' : 'skipped',
1397
+ reason: changed ? 'applied' : 'up-to-date',
1398
+ });
983
1399
  }
984
1400
  else {
1401
+ const configPath = agent.configPath();
1402
+ const before = readFileSafe(configPath);
985
1403
  agent.applyConfig(configToApply);
986
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(agent.configPath())}`);
1404
+ const after = readFileSafe(configPath);
1405
+ const changed = before !== after;
1406
+ persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(configPath))}`);
1407
+ results.push({
1408
+ application: agentId,
1409
+ filePath: configPath,
1410
+ status: changed ? 'written' : 'skipped',
1411
+ reason: changed ? 'applied' : 'up-to-date',
1412
+ });
987
1413
  }
988
1414
  }
989
1415
  catch (error) {
990
1416
  if (error instanceof Error) {
991
1417
  persist(chalk.yellow('⚠'), `${chalk.cyan(agentId)} - ${error.message} (skipped)`);
1418
+ results.push({
1419
+ application: agentId,
1420
+ filePath: '(unknown)',
1421
+ status: 'error',
1422
+ error: `${error.message} (skipped)`,
1423
+ });
992
1424
  }
993
1425
  }
994
1426
  }
1427
+ return results;
995
1428
  }
996
1429
  /**
997
1430
  * Show summary of enabled/disabled servers and applied agents
@@ -1016,33 +1449,99 @@ function showSummary(selectedServers, scope) {
1016
1449
  }
1017
1450
  }
1018
1451
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1019
- if (switchboardConfig.agents.active.length > 0) {
1020
- console.log(chalk.blue(`\nApplied to agents (${switchboardConfig.agents.active.length}):`));
1021
- for (const agent of switchboardConfig.agents.active) {
1452
+ if (switchboardConfig.applications.active.length > 0) {
1453
+ console.log(chalk.blue(`\nApplied to applications (${switchboardConfig.applications.active.length}):`));
1454
+ for (const agent of switchboardConfig.applications.active) {
1022
1455
  console.log(` ${chalk.dim('•')} ${agent}`);
1023
1456
  }
1024
1457
  }
1025
1458
  console.log();
1026
1459
  }
1027
- // Library subscription commands
1028
- program
1029
- .command('subscribe')
1030
- .description('Add a library subscription with a namespace')
1031
- .argument('<name>', 'Namespace for this subscription (e.g., "team", "project")')
1032
- .argument('<path>', 'Path to the library directory')
1033
- .action((name, libraryPath) => {
1460
+ function printMarketplaceSummary(localPath) {
1034
1461
  try {
1035
- // Validate the path has library structure
1036
- const validation = validateSubscriptionPath(libraryPath);
1037
- if (!validation.valid) {
1038
- console.error(chalk.red(`\n✗ Path does not contain any library folders (rules/, commands/, subagents/, skills/).`));
1039
- process.exit(1);
1462
+ const mp = readMarketplace(localPath);
1463
+ console.log(chalk.dim(` Type: marketplace (${mp.name})`));
1464
+ console.log(chalk.dim(` Plugins: ${mp.plugins.length}`));
1465
+ for (const plugin of mp.plugins) {
1466
+ const desc = plugin.description ? ` - ${plugin.description}` : '';
1467
+ console.log(chalk.dim(` ${plugin.name}${desc}`));
1468
+ }
1469
+ for (const w of mp.warnings) {
1470
+ console.log(chalk.yellow(` ⚠ ${w}`));
1040
1471
  }
1041
- addSubscription(name, libraryPath);
1042
- console.log(chalk.green(`\n✓ Subscribed to "${name}" at ${path.resolve(libraryPath)}`));
1043
- console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1044
- if (validation.missing.length > 0) {
1045
- console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1472
+ }
1473
+ catch (error) {
1474
+ const msg = error instanceof Error ? error.message : String(error);
1475
+ console.log(chalk.yellow(` ⚠ Failed to read marketplace: ${msg}`));
1476
+ }
1477
+ }
1478
+ const sourceRoot = program
1479
+ .command('source')
1480
+ .description('Manage external library sources (local paths or git repos)');
1481
+ sourceRoot
1482
+ .command('add')
1483
+ .description('Add a library source (local path or git URL)')
1484
+ .argument('<location>', 'Local path or git URL (e.g., https://github.com/org/repo)')
1485
+ .argument('[name]', 'Namespace (defaults to repo or directory name)')
1486
+ .action((location, nameArg) => {
1487
+ try {
1488
+ const name = nameArg ?? inferSourceName(location);
1489
+ if (isGitUrl(location)) {
1490
+ const parsed = parseGitUrl(location);
1491
+ const spinner = ora(`Cloning ${parsed.url}...`).start();
1492
+ try {
1493
+ addRemoteSource(name, {
1494
+ url: parsed.url,
1495
+ ref: parsed.ref,
1496
+ subdir: parsed.subdir,
1497
+ });
1498
+ spinner.succeed(chalk.green(`✓ Cloned ${parsed.url}`));
1499
+ }
1500
+ catch (err) {
1501
+ spinner.fail(chalk.red('Failed to clone'));
1502
+ throw err;
1503
+ }
1504
+ let effectivePath = getSourceCacheDir(name);
1505
+ if (parsed.subdir)
1506
+ effectivePath = path.join(effectivePath, parsed.subdir);
1507
+ const validation = validateSourcePath(effectivePath);
1508
+ if (!validation.valid) {
1509
+ removeSource(name);
1510
+ console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
1511
+ process.exit(1);
1512
+ }
1513
+ console.log(chalk.green(`\n✓ Added source "${name}" from ${parsed.url}`));
1514
+ if (parsed.ref)
1515
+ console.log(chalk.dim(` Ref: ${parsed.ref}`));
1516
+ if (parsed.subdir)
1517
+ console.log(chalk.dim(` Subdir: ${parsed.subdir}`));
1518
+ if (validation.isMarketplace) {
1519
+ printMarketplaceSummary(effectivePath);
1520
+ }
1521
+ else {
1522
+ console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1523
+ if (validation.missing.length > 0) {
1524
+ console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1525
+ }
1526
+ }
1527
+ }
1528
+ else {
1529
+ const validation = validateSourcePath(location);
1530
+ if (!validation.valid) {
1531
+ console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
1532
+ process.exit(1);
1533
+ }
1534
+ addLocalSource(name, location);
1535
+ console.log(chalk.green(`\n✓ Added source "${name}" at ${path.resolve(location)}`));
1536
+ if (validation.isMarketplace) {
1537
+ printMarketplaceSummary(path.resolve(location));
1538
+ }
1539
+ else {
1540
+ console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1541
+ if (validation.missing.length > 0) {
1542
+ console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1543
+ }
1544
+ }
1046
1545
  }
1047
1546
  console.log();
1048
1547
  console.log(chalk.dim('Library entries will now use the namespace prefix, e.g., ') +
@@ -1055,14 +1554,21 @@ program
1055
1554
  process.exit(1);
1056
1555
  }
1057
1556
  });
1058
- program
1059
- .command('unsubscribe')
1060
- .description('Remove a library subscription by namespace')
1557
+ sourceRoot
1558
+ .command('remove')
1559
+ .description('Remove a library source by namespace')
1061
1560
  .argument('<name>', 'Namespace to remove')
1062
1561
  .action((name) => {
1063
1562
  try {
1064
- removeSubscription(name);
1065
- console.log(chalk.green(`\n✓ Unsubscribed from "${name}"`));
1563
+ const sources = getSources();
1564
+ const source = sources.find((s) => s.namespace === name);
1565
+ removeSource(name);
1566
+ if (source?.remote) {
1567
+ console.log(chalk.green(`\n✓ Removed source "${name}" and cleaned up cache`));
1568
+ }
1569
+ else {
1570
+ console.log(chalk.green(`\n✓ Removed source "${name}"`));
1571
+ }
1066
1572
  }
1067
1573
  catch (error) {
1068
1574
  if (error instanceof Error) {
@@ -1071,32 +1577,63 @@ program
1071
1577
  process.exit(1);
1072
1578
  }
1073
1579
  });
1074
- program
1075
- .command('subscriptions')
1076
- .description('List all library subscriptions')
1077
- .option('--json', 'Output as JSON')
1580
+ sourceRoot
1581
+ .command('list')
1582
+ .description('List all library sources')
1583
+ .option('--json', 'Output inventory as JSON')
1078
1584
  .action((options) => {
1079
1585
  try {
1080
- const subscriptions = getSubscriptions();
1586
+ const sources = getSources();
1081
1587
  if (options.json) {
1082
- console.log(JSON.stringify(subscriptions, null, 2));
1588
+ console.log(JSON.stringify(sources, null, 2));
1083
1589
  return;
1084
1590
  }
1085
- if (subscriptions.length === 0) {
1086
- console.log(chalk.yellow('\n⚠ No library subscriptions configured.'));
1087
- console.log(chalk.dim(' Use `asb subscribe <name> <path>` to add one.'));
1591
+ if (sources.length === 0) {
1592
+ console.log(chalk.yellow('⚠ No library sources configured.'));
1593
+ console.log(chalk.dim(' Use `asb source add <location> [name]` to add one.'));
1088
1594
  return;
1089
1595
  }
1090
- console.log(chalk.blue('\nLibrary subscriptions:'));
1091
- const header = ['Namespace', 'Path', 'Status', 'Contains'];
1092
- const rows = subscriptions.map((sub) => {
1093
- const exists = fs.existsSync(sub.path);
1094
- const validation = exists ? validateSubscriptionPath(sub.path) : { found: [], missing: [] };
1095
- const statusPlain = exists ? 'ok' : 'missing';
1096
- const containsPlain = validation.found.length > 0 ? validation.found.join(', ') : '-';
1596
+ console.log(chalk.blue('\nLibrary sources:'));
1597
+ const header = ['Namespace', 'Type', 'Source', 'Status', 'Contains'];
1598
+ const rows = sources.map((src) => {
1599
+ const isRemote = !!src.remote;
1600
+ const exists = fs.existsSync(src.path);
1601
+ const validation = exists
1602
+ ? validateSourcePath(src.path)
1603
+ : { found: [], missing: [], isMarketplace: false };
1604
+ const typePlain = validation.isMarketplace ? 'marketplace' : isRemote ? 'remote' : 'local';
1605
+ const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
1606
+ let statusPlain;
1607
+ if (isRemote) {
1608
+ statusPlain = exists ? 'cached' : 'not cached';
1609
+ }
1610
+ else {
1611
+ statusPlain = exists ? 'ok' : 'missing';
1612
+ }
1613
+ let containsPlain;
1614
+ if (validation.isMarketplace && exists) {
1615
+ try {
1616
+ const mp = readMarketplace(src.path);
1617
+ containsPlain = `${mp.plugins.length} plugin(s)`;
1618
+ }
1619
+ catch {
1620
+ containsPlain = 'marketplace (error)';
1621
+ }
1622
+ }
1623
+ else {
1624
+ containsPlain = validation.found.length > 0 ? validation.found.join(', ') : '-';
1625
+ }
1097
1626
  return [
1098
- { plain: sub.namespace, formatted: chalk.cyan(sub.namespace) },
1099
- { plain: sub.path, formatted: chalk.dim(sub.path) },
1627
+ { plain: src.namespace, formatted: chalk.cyan(src.namespace) },
1628
+ {
1629
+ plain: typePlain,
1630
+ formatted: validation.isMarketplace
1631
+ ? chalk.magenta(typePlain)
1632
+ : isRemote
1633
+ ? chalk.blue(typePlain)
1634
+ : chalk.gray(typePlain),
1635
+ },
1636
+ { plain: sourcePlain, formatted: chalk.dim(sourcePlain) },
1100
1637
  {
1101
1638
  plain: statusPlain,
1102
1639
  formatted: exists ? chalk.green(statusPlain) : chalk.red(statusPlain),
@@ -1114,5 +1651,8 @@ program
1114
1651
  process.exit(1);
1115
1652
  }
1116
1653
  });
1654
+ sourceRoot.action(() => {
1655
+ sourceRoot.commands.find((c) => c.name() === 'list')?.parse(process.argv);
1656
+ });
1117
1657
  program.parse(process.argv);
1118
1658
  //# sourceMappingURL=index.js.map