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.
- package/README.md +37 -11
- package/dist/commands/distribution.js +2 -2
- package/dist/commands/distribution.js.map +1 -1
- package/dist/commands/library.d.ts +1 -1
- package/dist/commands/library.js +5 -6
- package/dist/commands/library.js.map +1 -1
- package/dist/config/application-config.d.ts +37 -0
- package/dist/config/application-config.js +83 -0
- package/dist/config/application-config.js.map +1 -0
- package/dist/config/layered-config.js +43 -2
- package/dist/config/layered-config.js.map +1 -1
- package/dist/config/paths.d.ts +13 -2
- package/dist/config/paths.js +21 -3
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schemas.d.ts +408 -43
- package/dist/config/schemas.js +32 -22
- package/dist/config/schemas.js.map +1 -1
- package/dist/config/switchboard-config.d.ts +3 -6
- package/dist/config/switchboard-config.js +3 -6
- package/dist/config/switchboard-config.js.map +1 -1
- package/dist/hooks/distribution.d.ts +27 -0
- package/dist/hooks/distribution.js +235 -0
- package/dist/hooks/distribution.js.map +1 -0
- package/dist/hooks/library.d.ts +40 -0
- package/dist/hooks/library.js +143 -0
- package/dist/hooks/library.js.map +1 -0
- package/dist/hooks/schema.d.ts +542 -0
- package/dist/hooks/schema.js +49 -0
- package/dist/hooks/schema.js.map +1 -0
- package/dist/index.js +708 -168
- package/dist/index.js.map +1 -1
- package/dist/library/fs.d.ts +1 -3
- package/dist/library/fs.js +24 -7
- package/dist/library/fs.js.map +1 -1
- package/dist/library/schema.d.ts +1 -1
- package/dist/library/schema.js +1 -1
- package/dist/library/sources.d.ts +75 -0
- package/dist/library/sources.js +285 -0
- package/dist/library/sources.js.map +1 -0
- package/dist/library/state.d.ts +4 -5
- package/dist/library/state.js +15 -31
- package/dist/library/state.js.map +1 -1
- package/dist/marketplace/plugin-loader.d.ts +30 -0
- package/dist/marketplace/plugin-loader.js +178 -0
- package/dist/marketplace/plugin-loader.js.map +1 -0
- package/dist/marketplace/reader.d.ts +36 -0
- package/dist/marketplace/reader.js +90 -0
- package/dist/marketplace/reader.js.map +1 -0
- package/dist/marketplace/schemas.d.ts +467 -0
- package/dist/marketplace/schemas.js +57 -0
- package/dist/marketplace/schemas.js.map +1 -0
- package/dist/marketplace/source-loader.d.ts +32 -0
- package/dist/marketplace/source-loader.js +45 -0
- package/dist/marketplace/source-loader.js.map +1 -0
- package/dist/rules/agents.d.ts +4 -4
- package/dist/rules/agents.js +10 -4
- package/dist/rules/agents.js.map +1 -1
- package/dist/rules/composer.d.ts +2 -2
- package/dist/rules/composer.js +5 -5
- package/dist/rules/composer.js.map +1 -1
- package/dist/rules/distribution.js +48 -112
- package/dist/rules/distribution.js.map +1 -1
- package/dist/rules/library.d.ts +1 -1
- package/dist/rules/library.js +4 -5
- package/dist/rules/library.js.map +1 -1
- package/dist/skills/distribution.js +6 -6
- package/dist/skills/distribution.js.map +1 -1
- package/dist/skills/library.d.ts +1 -2
- package/dist/skills/library.js +6 -7
- package/dist/skills/library.js.map +1 -1
- package/dist/subagents/distribution.d.ts +1 -1
- package/dist/subagents/distribution.js +319 -20
- package/dist/subagents/distribution.js.map +1 -1
- package/dist/subagents/importer.d.ts +1 -1
- package/dist/subagents/importer.js +61 -1
- package/dist/subagents/importer.js.map +1 -1
- package/dist/subagents/inventory.js +3 -3
- package/dist/subagents/inventory.js.map +1 -1
- package/dist/subagents/library.d.ts +2 -2
- package/dist/subagents/library.js +14 -20
- package/dist/subagents/library.js.map +1 -1
- package/dist/ui/hook-ui.d.ts +8 -0
- package/dist/ui/hook-ui.js +17 -0
- package/dist/ui/hook-ui.js.map +1 -0
- package/dist/ui/library-selector.d.ts +1 -1
- package/dist/ui/subagent-ui.js +3 -3
- package/dist/ui/subagent-ui.js.map +1 -1
- package/dist/util/cli.d.ts +20 -0
- package/dist/util/cli.js +107 -14
- package/dist/util/cli.js.map +1 -1
- package/package.json +3 -2
- package/dist/config/agent-config.d.ts +0 -35
- package/dist/config/agent-config.js +0 -88
- package/dist/config/agent-config.js.map +0 -1
- package/dist/library/subscriptions.d.ts +0 -42
- package/dist/library/subscriptions.js +0 -116
- 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 {
|
|
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,
|
|
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 {
|
|
23
|
-
import {
|
|
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,
|
|
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/
|
|
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,
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
console.log(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
195
|
+
const agentDistribution = distributeSubagents(scope);
|
|
113
196
|
const skillDistribution = distributeSkills(scope, {
|
|
114
197
|
useAgentsDir: config.distribution.use_agents_dir,
|
|
115
198
|
});
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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('✗
|
|
253
|
+
console.log(chalk.red('✗ Sync completed with errors.'));
|
|
158
254
|
process.exit(1);
|
|
159
255
|
}
|
|
160
256
|
else {
|
|
161
|
-
console.log(chalk.green('✓
|
|
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
|
|
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
|
-
//
|
|
656
|
-
const
|
|
657
|
-
.command('
|
|
658
|
-
.description('Select
|
|
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
|
-
|
|
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('
|
|
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: '
|
|
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
|
-
|
|
923
|
+
agentRoot
|
|
692
924
|
.command('load')
|
|
693
|
-
.description('Import existing platform files into the
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
-
|
|
979
|
+
agentRoot
|
|
748
980
|
.command('list')
|
|
749
|
-
.description('Display
|
|
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
|
|
998
|
+
console.log(chalk.yellow('⚠ No agents found. Use `asb agent load <platform> [path]`.'));
|
|
767
999
|
}
|
|
768
1000
|
else {
|
|
769
|
-
console.log(chalk.blue('
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1020
|
-
console.log(chalk.blue(`\nApplied to
|
|
1021
|
-
for (const agent of switchboardConfig.
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1059
|
-
.command('
|
|
1060
|
-
.description('Remove a library
|
|
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
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1075
|
-
.command('
|
|
1076
|
-
.description('List all library
|
|
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
|
|
1586
|
+
const sources = getSources();
|
|
1081
1587
|
if (options.json) {
|
|
1082
|
-
console.log(JSON.stringify(
|
|
1588
|
+
console.log(JSON.stringify(sources, null, 2));
|
|
1083
1589
|
return;
|
|
1084
1590
|
}
|
|
1085
|
-
if (
|
|
1086
|
-
console.log(chalk.yellow('
|
|
1087
|
-
console.log(chalk.dim(' Use `asb
|
|
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
|
|
1091
|
-
const header = ['Namespace', '
|
|
1092
|
-
const rows =
|
|
1093
|
-
const
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
|
|
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:
|
|
1099
|
-
{
|
|
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
|