agent-switchboard 0.1.27 → 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 +26 -1
- 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 -5
- 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 +7 -2
- package/dist/config/paths.js +12 -3
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schemas.d.ts +103 -22
- package/dist/config/schemas.js +22 -18
- 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 +608 -146
- 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 +3 -0
- package/dist/library/sources.js +8 -2
- package/dist/library/sources.js.map +1 -1
- 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/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 +2 -2
- package/dist/rules/distribution.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 -6
- 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 -19
- 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/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, getSourceCacheDir,
|
|
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
24
|
import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
|
|
23
|
-
import { loadMcpActiveState, saveMcpActiveState } from './library/state.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,11 +59,11 @@ 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')
|
|
65
69
|
.option('--no-update', 'Skip updating remote sources')
|
|
@@ -68,14 +72,12 @@ program
|
|
|
68
72
|
const scope = resolveScope(options);
|
|
69
73
|
const loadOptions = scopeToLoadOptions(scope);
|
|
70
74
|
const { config, layers } = loadSwitchboardConfigWithLayers(loadOptions);
|
|
71
|
-
console.log();
|
|
72
|
-
console.log(`${chalk.bgRed.white(' WARNING ')} ${chalk.red('`asb sync` overwrites target files without diff checks.')}`);
|
|
73
|
-
console.log(chalk.red('Proceeding with synchronization...'));
|
|
75
|
+
console.log(chalk.yellow('⚠ Sync overwrites agent config without diff.'));
|
|
74
76
|
console.log();
|
|
75
77
|
if (options.update !== false) {
|
|
76
78
|
const remoteResults = updateRemoteSources();
|
|
77
79
|
if (remoteResults.length > 0) {
|
|
78
|
-
console.log(chalk.blue('
|
|
80
|
+
console.log(chalk.blue('Sources:'));
|
|
79
81
|
for (const result of remoteResults) {
|
|
80
82
|
if (result.status === 'updated') {
|
|
81
83
|
console.log(` ${chalk.green('✓')} ${chalk.cyan(result.namespace)} ${chalk.dim(result.url)}`);
|
|
@@ -84,97 +86,175 @@ program
|
|
|
84
86
|
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(result.namespace)} ${chalk.yellow(result.error ?? 'update failed')}`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
console.log();
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
];
|
|
104
|
-
for (const entry of layerEntries) {
|
|
105
|
-
const marker = entry.exists ? chalk.green('✓') : chalk.gray('•');
|
|
106
|
-
const pathLabel = entry.exists ? chalk.dim(entry.path) : chalk.gray(entry.path);
|
|
107
|
-
console.log(` ${marker} ${entry.label}: ${pathLabel}`);
|
|
108
|
-
}
|
|
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}`);
|
|
109
104
|
console.log();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
181
|
}
|
|
119
|
-
|
|
120
|
-
|
|
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}`));
|
|
121
190
|
}
|
|
122
191
|
console.log();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await applyToAgents(scope);
|
|
126
|
-
const ruleDistribution = distributeRules(undefined, { force: true }, scope);
|
|
192
|
+
const mcpDistribution = await applyToAgents(scope, undefined, { useSpinner: false });
|
|
193
|
+
const ruleDistribution = distributeRules(undefined, undefined, scope);
|
|
127
194
|
const commandDistribution = distributeCommands(scope);
|
|
128
|
-
const
|
|
195
|
+
const agentDistribution = distributeSubagents(scope);
|
|
129
196
|
const skillDistribution = distributeSkills(scope, {
|
|
130
197
|
useAgentsDir: config.distribution.use_agents_dir,
|
|
131
198
|
});
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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);
|
|
167
251
|
console.log();
|
|
168
|
-
const hasErrors = ruleErrors.length > 0 ||
|
|
169
|
-
commandErrors.length > 0 ||
|
|
170
|
-
subagentErrors.length > 0 ||
|
|
171
|
-
skillErrors.length > 0;
|
|
172
252
|
if (hasErrors) {
|
|
173
|
-
console.log(chalk.red('✗
|
|
253
|
+
console.log(chalk.red('✗ Sync completed with errors.'));
|
|
174
254
|
process.exit(1);
|
|
175
255
|
}
|
|
176
256
|
else {
|
|
177
|
-
console.log(chalk.green('✓
|
|
257
|
+
console.log(chalk.green('✓ Sync complete.'));
|
|
178
258
|
}
|
|
179
259
|
}
|
|
180
260
|
catch (error) {
|
|
@@ -235,7 +315,7 @@ function defaultCommandSourceDir(platform) {
|
|
|
235
315
|
return getOpencodePath('command');
|
|
236
316
|
}
|
|
237
317
|
}
|
|
238
|
-
function
|
|
318
|
+
function defaultAgentSourceDir(platform) {
|
|
239
319
|
switch (platform) {
|
|
240
320
|
case 'claude-code':
|
|
241
321
|
return path.join(getClaudeDir(), 'agents');
|
|
@@ -243,6 +323,8 @@ function defaultSubagentSourceDir(platform) {
|
|
|
243
323
|
return path.join(getCursorDir(), 'agents');
|
|
244
324
|
case 'opencode':
|
|
245
325
|
return getOpencodePath('agent');
|
|
326
|
+
default:
|
|
327
|
+
throw new Error(`Unknown agent platform: ${String(platform)}`);
|
|
246
328
|
}
|
|
247
329
|
}
|
|
248
330
|
function defaultSkillSourceDir(platform) {
|
|
@@ -291,6 +373,141 @@ async function confirmOverwrite(filePath, force) {
|
|
|
291
373
|
return true;
|
|
292
374
|
return await confirm({ message: `File exists: ${filePath}. Overwrite?`, default: false });
|
|
293
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
|
+
}
|
|
294
511
|
program
|
|
295
512
|
.command('mcp')
|
|
296
513
|
.description('Interactive UI to enable/disable MCP servers')
|
|
@@ -668,13 +885,13 @@ commandRoot
|
|
|
668
885
|
process.exit(1);
|
|
669
886
|
}
|
|
670
887
|
});
|
|
671
|
-
//
|
|
672
|
-
const
|
|
673
|
-
.command('
|
|
674
|
-
.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')
|
|
675
892
|
.option('-p, --profile <name>', 'Profile configuration to use')
|
|
676
893
|
.option('--project <path>', 'Project directory containing .asb.toml');
|
|
677
|
-
|
|
894
|
+
agentRoot.action(async (options) => {
|
|
678
895
|
try {
|
|
679
896
|
const scope = resolveScope(options);
|
|
680
897
|
const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
|
|
@@ -682,18 +899,17 @@ subagentRoot.action(async (options) => {
|
|
|
682
899
|
if (!selection)
|
|
683
900
|
return;
|
|
684
901
|
console.log();
|
|
685
|
-
printActiveSelection('
|
|
902
|
+
printActiveSelection('agents', selection.active);
|
|
686
903
|
const out = distributeSubagents(scope);
|
|
687
904
|
if (out.results.length > 0) {
|
|
688
905
|
console.log();
|
|
689
906
|
printDistributionResults({
|
|
690
|
-
title: '
|
|
907
|
+
title: 'Agent distribution',
|
|
691
908
|
results: out.results,
|
|
692
909
|
getTargetLabel: (result) => result.platform,
|
|
693
910
|
getPath: (result) => result.filePath,
|
|
694
911
|
});
|
|
695
912
|
}
|
|
696
|
-
// Guidance: unsupported platforms for subagents
|
|
697
913
|
console.log();
|
|
698
914
|
console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
|
|
699
915
|
}
|
|
@@ -704,9 +920,9 @@ subagentRoot.action(async (options) => {
|
|
|
704
920
|
process.exit(1);
|
|
705
921
|
}
|
|
706
922
|
});
|
|
707
|
-
|
|
923
|
+
agentRoot
|
|
708
924
|
.command('load')
|
|
709
|
-
.description('Import existing platform files into the
|
|
925
|
+
.description('Import existing platform files into the agent library')
|
|
710
926
|
.argument('<platform>', 'claude-code | opencode | cursor')
|
|
711
927
|
.argument('[path]', 'Source file or directory (defaults by platform)')
|
|
712
928
|
.option('-r, --recursive', 'When [path] is a directory, import files recursively')
|
|
@@ -714,7 +930,7 @@ subagentRoot
|
|
|
714
930
|
.action(async (platform, srcPath, opts) => {
|
|
715
931
|
try {
|
|
716
932
|
const exts = ['.md', '.markdown'];
|
|
717
|
-
const source = srcPath && srcPath.trim().length > 0 ? srcPath :
|
|
933
|
+
const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultAgentSourceDir(platform);
|
|
718
934
|
if (!fs.existsSync(source)) {
|
|
719
935
|
console.error(chalk.red(`\n✗ Source not found: ${source}`));
|
|
720
936
|
process.exit(1);
|
|
@@ -734,7 +950,7 @@ subagentRoot
|
|
|
734
950
|
console.log(chalk.yellow('\n⚠ No files to import.'));
|
|
735
951
|
return;
|
|
736
952
|
}
|
|
737
|
-
const outDir =
|
|
953
|
+
const outDir = getAgentsDir();
|
|
738
954
|
let imported = 0;
|
|
739
955
|
for (const file of inputs) {
|
|
740
956
|
try {
|
|
@@ -751,7 +967,7 @@ subagentRoot
|
|
|
751
967
|
console.log(`${chalk.red('✗')} ${chalk.dim(file)} ${chalk.red(msg)}`);
|
|
752
968
|
}
|
|
753
969
|
}
|
|
754
|
-
console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into
|
|
970
|
+
console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into agent library.`);
|
|
755
971
|
}
|
|
756
972
|
catch (error) {
|
|
757
973
|
if (error instanceof Error) {
|
|
@@ -760,9 +976,9 @@ subagentRoot
|
|
|
760
976
|
process.exit(1);
|
|
761
977
|
}
|
|
762
978
|
});
|
|
763
|
-
|
|
979
|
+
agentRoot
|
|
764
980
|
.command('list')
|
|
765
|
-
.description('Display
|
|
981
|
+
.description('Display agent inventory and sync information')
|
|
766
982
|
.option('--json', 'Output inventory as JSON')
|
|
767
983
|
.option('-p, --profile <name>', 'Profile configuration to use')
|
|
768
984
|
.option('--project <path>', 'Project directory containing .asb.toml')
|
|
@@ -779,10 +995,10 @@ subagentRoot
|
|
|
779
995
|
return;
|
|
780
996
|
}
|
|
781
997
|
if (inventory.entries.length === 0) {
|
|
782
|
-
console.log(chalk.yellow('⚠ No
|
|
998
|
+
console.log(chalk.yellow('⚠ No agents found. Use `asb agent load <platform> [path]`.'));
|
|
783
999
|
}
|
|
784
1000
|
else {
|
|
785
|
-
console.log(chalk.blue('
|
|
1001
|
+
console.log(chalk.blue('Agents:'));
|
|
786
1002
|
const header = ['ID', 'Active', 'Title', 'Model', 'Tools', 'Extras'];
|
|
787
1003
|
const rows = inventory.entries.map((row) => {
|
|
788
1004
|
const activePlain = row.active ? 'yes' : 'no';
|
|
@@ -806,7 +1022,6 @@ subagentRoot
|
|
|
806
1022
|
}
|
|
807
1023
|
console.log();
|
|
808
1024
|
printAgentSyncStatus({ agentSync: inventory.state.agentSync });
|
|
809
|
-
// Guidance: unsupported platforms for subagents
|
|
810
1025
|
console.log();
|
|
811
1026
|
console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
|
|
812
1027
|
}
|
|
@@ -958,30 +1173,198 @@ skillRoot
|
|
|
958
1173
|
process.exit(1);
|
|
959
1174
|
}
|
|
960
1175
|
});
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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) {
|
|
967
1344
|
const mcpConfig = loadMcpConfig();
|
|
968
1345
|
const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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;
|
|
975
1356
|
}
|
|
976
1357
|
// Global MCP servers list (from UI selection or config)
|
|
977
1358
|
const globalMcpServers = enabledServerNames ?? loadMcpActiveState(scope);
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
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
|
+
};
|
|
982
1366
|
try {
|
|
983
|
-
|
|
984
|
-
const agentMcpConfig = resolveAgentSectionConfig('mcp', agentId, scope);
|
|
1367
|
+
const agentMcpConfig = resolveApplicationSectionConfig('mcp', agentId, scope);
|
|
985
1368
|
// If user selected servers via UI, use that as base; otherwise use per-agent resolved config
|
|
986
1369
|
const agentActiveServers = enabledServerNames
|
|
987
1370
|
? agentMcpConfig.active.filter((s) => globalMcpServers.includes(s))
|
|
@@ -991,23 +1374,57 @@ async function applyToAgents(scope, enabledServerNames) {
|
|
|
991
1374
|
const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
|
|
992
1375
|
const configToApply = { mcpServers: enabledServers };
|
|
993
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
|
+
};
|
|
994
1385
|
// Use project-level config when --project is specified
|
|
995
1386
|
if (scope?.project && agent.applyProjectConfig) {
|
|
996
|
-
agent.applyProjectConfig(scope.project, configToApply);
|
|
997
1387
|
const projectPath = agent.projectConfigPath?.(scope.project) ?? 'project config';
|
|
998
|
-
|
|
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
|
+
});
|
|
999
1399
|
}
|
|
1000
1400
|
else {
|
|
1401
|
+
const configPath = agent.configPath();
|
|
1402
|
+
const before = readFileSafe(configPath);
|
|
1001
1403
|
agent.applyConfig(configToApply);
|
|
1002
|
-
|
|
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
|
+
});
|
|
1003
1413
|
}
|
|
1004
1414
|
}
|
|
1005
1415
|
catch (error) {
|
|
1006
1416
|
if (error instanceof Error) {
|
|
1007
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
|
+
});
|
|
1008
1424
|
}
|
|
1009
1425
|
}
|
|
1010
1426
|
}
|
|
1427
|
+
return results;
|
|
1011
1428
|
}
|
|
1012
1429
|
/**
|
|
1013
1430
|
* Show summary of enabled/disabled servers and applied agents
|
|
@@ -1032,15 +1449,32 @@ function showSummary(selectedServers, scope) {
|
|
|
1032
1449
|
}
|
|
1033
1450
|
}
|
|
1034
1451
|
const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
|
|
1035
|
-
if (switchboardConfig.
|
|
1036
|
-
console.log(chalk.blue(`\nApplied to
|
|
1037
|
-
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) {
|
|
1038
1455
|
console.log(` ${chalk.dim('•')} ${agent}`);
|
|
1039
1456
|
}
|
|
1040
1457
|
}
|
|
1041
1458
|
console.log();
|
|
1042
1459
|
}
|
|
1043
|
-
|
|
1460
|
+
function printMarketplaceSummary(localPath) {
|
|
1461
|
+
try {
|
|
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}`));
|
|
1471
|
+
}
|
|
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
|
+
}
|
|
1044
1478
|
const sourceRoot = program
|
|
1045
1479
|
.command('source')
|
|
1046
1480
|
.description('Manage external library sources (local paths or git repos)');
|
|
@@ -1073,7 +1507,7 @@ sourceRoot
|
|
|
1073
1507
|
const validation = validateSourcePath(effectivePath);
|
|
1074
1508
|
if (!validation.valid) {
|
|
1075
1509
|
removeSource(name);
|
|
1076
|
-
console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/,
|
|
1510
|
+
console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
|
|
1077
1511
|
process.exit(1);
|
|
1078
1512
|
}
|
|
1079
1513
|
console.log(chalk.green(`\n✓ Added source "${name}" from ${parsed.url}`));
|
|
@@ -1081,22 +1515,32 @@ sourceRoot
|
|
|
1081
1515
|
console.log(chalk.dim(` Ref: ${parsed.ref}`));
|
|
1082
1516
|
if (parsed.subdir)
|
|
1083
1517
|
console.log(chalk.dim(` Subdir: ${parsed.subdir}`));
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
+
}
|
|
1087
1526
|
}
|
|
1088
1527
|
}
|
|
1089
1528
|
else {
|
|
1090
1529
|
const validation = validateSourcePath(location);
|
|
1091
1530
|
if (!validation.valid) {
|
|
1092
|
-
console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/,
|
|
1531
|
+
console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
|
|
1093
1532
|
process.exit(1);
|
|
1094
1533
|
}
|
|
1095
1534
|
addLocalSource(name, location);
|
|
1096
1535
|
console.log(chalk.green(`\n✓ Added source "${name}" at ${path.resolve(location)}`));
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
+
}
|
|
1100
1544
|
}
|
|
1101
1545
|
}
|
|
1102
1546
|
console.log();
|
|
@@ -1136,7 +1580,7 @@ sourceRoot
|
|
|
1136
1580
|
sourceRoot
|
|
1137
1581
|
.command('list')
|
|
1138
1582
|
.description('List all library sources')
|
|
1139
|
-
.option('--json', 'Output as JSON')
|
|
1583
|
+
.option('--json', 'Output inventory as JSON')
|
|
1140
1584
|
.action((options) => {
|
|
1141
1585
|
try {
|
|
1142
1586
|
const sources = getSources();
|
|
@@ -1145,7 +1589,7 @@ sourceRoot
|
|
|
1145
1589
|
return;
|
|
1146
1590
|
}
|
|
1147
1591
|
if (sources.length === 0) {
|
|
1148
|
-
console.log(chalk.yellow('
|
|
1592
|
+
console.log(chalk.yellow('⚠ No library sources configured.'));
|
|
1149
1593
|
console.log(chalk.dim(' Use `asb source add <location> [name]` to add one.'));
|
|
1150
1594
|
return;
|
|
1151
1595
|
}
|
|
@@ -1153,10 +1597,12 @@ sourceRoot
|
|
|
1153
1597
|
const header = ['Namespace', 'Type', 'Source', 'Status', 'Contains'];
|
|
1154
1598
|
const rows = sources.map((src) => {
|
|
1155
1599
|
const isRemote = !!src.remote;
|
|
1156
|
-
const typePlain = isRemote ? 'remote' : 'local';
|
|
1157
|
-
const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
|
|
1158
1600
|
const exists = fs.existsSync(src.path);
|
|
1159
|
-
const validation = exists
|
|
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;
|
|
1160
1606
|
let statusPlain;
|
|
1161
1607
|
if (isRemote) {
|
|
1162
1608
|
statusPlain = exists ? 'cached' : 'not cached';
|
|
@@ -1164,12 +1610,28 @@ sourceRoot
|
|
|
1164
1610
|
else {
|
|
1165
1611
|
statusPlain = exists ? 'ok' : 'missing';
|
|
1166
1612
|
}
|
|
1167
|
-
|
|
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
|
+
}
|
|
1168
1626
|
return [
|
|
1169
1627
|
{ plain: src.namespace, formatted: chalk.cyan(src.namespace) },
|
|
1170
1628
|
{
|
|
1171
1629
|
plain: typePlain,
|
|
1172
|
-
formatted:
|
|
1630
|
+
formatted: validation.isMarketplace
|
|
1631
|
+
? chalk.magenta(typePlain)
|
|
1632
|
+
: isRemote
|
|
1633
|
+
? chalk.blue(typePlain)
|
|
1634
|
+
: chalk.gray(typePlain),
|
|
1173
1635
|
},
|
|
1174
1636
|
{ plain: sourcePlain, formatted: chalk.dim(sourcePlain) },
|
|
1175
1637
|
{
|