@joehom/awm-cli 0.0.1
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 +666 -0
- package/bin/awm.js +2 -0
- package/package.json +25 -0
- package/skills/awm-cli/SKILL.md +189 -0
- package/src/adapters/jsonAdapter.js +54 -0
- package/src/adapters/skillApplier.js +35 -0
- package/src/adapters/tomlAdapter.js +49 -0
- package/src/commands/doctor.js +100 -0
- package/src/commands/init.js +34 -0
- package/src/commands/mcp.js +253 -0
- package/src/commands/pull.js +168 -0
- package/src/commands/setup.js +31 -0
- package/src/commands/skill.js +187 -0
- package/src/commands/status.js +17 -0
- package/src/commands/tool.js +45 -0
- package/src/defaults/mcps/fetch.json +6 -0
- package/src/defaults/mcps/filesystem.json +6 -0
- package/src/defaults/mcps/github.json +9 -0
- package/src/defaults/mcps/memory.json +6 -0
- package/src/defaults/skills/awm-cli/SKILL.md +189 -0
- package/src/defaults/tools/claude-code.json +27 -0
- package/src/defaults/tools/codex.json +27 -0
- package/src/defaults/tools/copilot-cli.json +18 -0
- package/src/defaults/tools/cursor.json +27 -0
- package/src/defaults/tools/gemini-cli.json +27 -0
- package/src/defaults/tools/github-copilot.json +23 -0
- package/src/defaults/tools/windsurf.json +23 -0
- package/src/index.js +35 -0
- package/src/registry/mcpRegistry.js +68 -0
- package/src/registry/paths.js +21 -0
- package/src/registry/skillRegistry.js +61 -0
- package/src/registry/toolRegistry.js +43 -0
- package/src/seed.js +131 -0
- package/src/tools/claude-code.json +27 -0
- package/src/tools/codex.json +27 -0
- package/src/tools/copilot-cli.json +15 -0
- package/src/tools/cursor.json +27 -0
- package/src/tools/gemini-cli.json +27 -0
- package/src/tools/github-copilot.json +23 -0
- package/src/tools/windsurf.json +23 -0
- package/src/utils/fileUtils.js +76 -0
- package/src/utils/logger.js +17 -0
- package/src/utils/pathResolver.js +40 -0
- package/src/utils/validator.js +68 -0
- package/src/workspace/applyWorkspace.js +81 -0
- package/src/workspace/workspaceConfig.js +34 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
import { listMcps, getMcp, saveMcp, removeMcp } from '../registry/mcpRegistry.js';
|
|
7
|
+
import { requireWorkspace, writeWorkspace } from '../workspace/workspaceConfig.js';
|
|
8
|
+
import { applyAll } from '../workspace/applyWorkspace.js';
|
|
9
|
+
import { resolvePath } from '../utils/pathResolver.js';
|
|
10
|
+
import { readJson } from '../utils/fileUtils.js';
|
|
11
|
+
|
|
12
|
+
// Tool config files to scan for 'mcp import'
|
|
13
|
+
const SCAN_TARGETS = [
|
|
14
|
+
'.mcp.json',
|
|
15
|
+
'.cursor/mcp.json',
|
|
16
|
+
'.gemini/settings.json',
|
|
17
|
+
'.vscode/mcp.json',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const GLOBAL_SCAN_TARGETS = [
|
|
21
|
+
'~/.copilot/mcp-config.json',
|
|
22
|
+
'~/.codeium/windsurf/mcp_config.json',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Returns false if the user cancelled (ESC / Ctrl+C), rethrows other errors. */
|
|
26
|
+
async function prompt(questions) {
|
|
27
|
+
try {
|
|
28
|
+
return await inquirer.prompt(questions);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.name === 'ExitPromptError') {
|
|
31
|
+
log.info('Cancelled.');
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract MCP entries from a parsed JSON config object.
|
|
40
|
+
* Tries common root keys: mcpServers, servers
|
|
41
|
+
*/
|
|
42
|
+
function extractMcpsFromJson(data) {
|
|
43
|
+
const block = data?.mcpServers ?? data?.servers ?? {};
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const [id, def] of Object.entries(block)) {
|
|
46
|
+
if (!def.command) continue;
|
|
47
|
+
results.push({
|
|
48
|
+
id,
|
|
49
|
+
transport: 'stdio',
|
|
50
|
+
command: def.command,
|
|
51
|
+
...(def.args ? { args: def.args } : {}),
|
|
52
|
+
...(def.env ? { env: def.env } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function makeMcpCommand() {
|
|
59
|
+
const mcp = new Command('mcp').description('Manage MCP server definitions');
|
|
60
|
+
|
|
61
|
+
// register — save to global registry
|
|
62
|
+
mcp
|
|
63
|
+
.command('register [id]')
|
|
64
|
+
.description('Interactively register an MCP server in the global registry')
|
|
65
|
+
.action(async (idArg) => {
|
|
66
|
+
const questions = [];
|
|
67
|
+
if (!idArg) {
|
|
68
|
+
questions.push({ type: 'input', name: 'id', message: 'MCP ID (unique slug):', validate: v => !!v || 'Required' });
|
|
69
|
+
}
|
|
70
|
+
questions.push(
|
|
71
|
+
{ type: 'list', name: 'transport', message: 'Transport:', choices: ['stdio', 'sse', 'http'] },
|
|
72
|
+
{ type: 'input', name: 'command', message: 'Command:', validate: v => !!v || 'Required' },
|
|
73
|
+
{ type: 'input', name: 'args', message: 'Args (space-separated, optional):' },
|
|
74
|
+
{ type: 'input', name: 'env', message: 'Env vars (KEY=VALUE pairs, space-separated, optional):' },
|
|
75
|
+
);
|
|
76
|
+
const answers = await inquirer.prompt(questions);
|
|
77
|
+
if (idArg) answers.id = idArg;
|
|
78
|
+
|
|
79
|
+
const mcpDef = {
|
|
80
|
+
id: answers.id,
|
|
81
|
+
transport: answers.transport,
|
|
82
|
+
command: answers.command,
|
|
83
|
+
};
|
|
84
|
+
if (answers.args?.trim()) {
|
|
85
|
+
mcpDef.args = answers.args.trim().split(/\s+/);
|
|
86
|
+
}
|
|
87
|
+
if (answers.env?.trim()) {
|
|
88
|
+
mcpDef.env = {};
|
|
89
|
+
for (const pair of answers.env.trim().split(/\s+/)) {
|
|
90
|
+
const idx = pair.indexOf('=');
|
|
91
|
+
if (idx > 0) mcpDef.env[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
saveMcp(mcpDef);
|
|
96
|
+
log.success(`MCP "${mcpDef.id}" registered in global registry`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// unregister — remove from global registry
|
|
100
|
+
mcp
|
|
101
|
+
.command('unregister <id>')
|
|
102
|
+
.description('Remove an MCP from the global registry')
|
|
103
|
+
.action((id) => {
|
|
104
|
+
removeMcp(id);
|
|
105
|
+
log.success(`MCP "${id}" removed from registry`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// import — scan current dir tool configs and register MCPs found
|
|
109
|
+
mcp
|
|
110
|
+
.command('import')
|
|
111
|
+
.description('Scan cwd tool config files and import MCPs into the global registry')
|
|
112
|
+
.action(() => {
|
|
113
|
+
const allTargets = [
|
|
114
|
+
...SCAN_TARGETS.map(f => path.join(process.cwd(), f)),
|
|
115
|
+
...GLOBAL_SCAN_TARGETS.map(f => resolvePath(f)),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
let imported = 0;
|
|
119
|
+
let skipped = 0;
|
|
120
|
+
|
|
121
|
+
for (const filePath of allTargets) {
|
|
122
|
+
if (!fs.existsSync(filePath)) continue;
|
|
123
|
+
const data = readJson(filePath);
|
|
124
|
+
if (!data) continue;
|
|
125
|
+
|
|
126
|
+
const entries = extractMcpsFromJson(data);
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (getMcp(entry.id)) {
|
|
129
|
+
log.info(` Skipping "${entry.id}" (already in registry)`);
|
|
130
|
+
skipped++;
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
saveMcp(entry);
|
|
134
|
+
log.success(` Registered "${entry.id}" from ${path.basename(filePath)}`);
|
|
135
|
+
imported++;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
log.warn(` Could not register "${entry.id}": ${e.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (imported === 0 && skipped === 0) {
|
|
144
|
+
log.info('No MCP entries found in tool config files.');
|
|
145
|
+
} else {
|
|
146
|
+
log.info(`Import complete: ${imported} registered, ${skipped} skipped.`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// add — select from registry, add to .awm.json, apply
|
|
151
|
+
mcp
|
|
152
|
+
.command('add')
|
|
153
|
+
.description('Add MCPs from the registry to this workspace and apply to all tools')
|
|
154
|
+
.option('--dry-run', 'Preview what would be written without making changes')
|
|
155
|
+
.action(async (opts) => {
|
|
156
|
+
const ws = requireWorkspace();
|
|
157
|
+
const allIds = listMcps();
|
|
158
|
+
|
|
159
|
+
if (allIds.length === 0) {
|
|
160
|
+
log.info('No MCPs in registry. Run "awm mcp register" to add one.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const available = allIds.filter(id => !ws.mcps.includes(id));
|
|
165
|
+
if (available.length === 0) {
|
|
166
|
+
log.info('All registry MCPs are already in this workspace.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const answer = await prompt([{
|
|
171
|
+
type: 'checkbox',
|
|
172
|
+
name: 'chosen',
|
|
173
|
+
message: 'Select MCPs to add to this workspace:',
|
|
174
|
+
choices: available,
|
|
175
|
+
validate: v => v.length > 0 || 'Select at least one MCP',
|
|
176
|
+
}]);
|
|
177
|
+
if (!answer) return;
|
|
178
|
+
|
|
179
|
+
const updated = { ...ws, mcps: [...ws.mcps, ...answer.chosen] };
|
|
180
|
+
writeWorkspace(updated);
|
|
181
|
+
await applyAll(updated, opts.dryRun);
|
|
182
|
+
if (!opts.dryRun) {
|
|
183
|
+
log.success(`Added MCPs: ${answer.chosen.join(', ')}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// delete — select from workspace list, remove from .awm.json, re-apply
|
|
188
|
+
mcp
|
|
189
|
+
.command('delete')
|
|
190
|
+
.description('Select MCPs to remove from this workspace and re-apply all tools')
|
|
191
|
+
.option('--dry-run', 'Preview what would be written without making changes')
|
|
192
|
+
.action(async (opts) => {
|
|
193
|
+
const ws = requireWorkspace();
|
|
194
|
+
if (!ws.mcps.length) {
|
|
195
|
+
log.info('No MCPs in this workspace to remove.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const answer = await prompt([{
|
|
200
|
+
type: 'checkbox',
|
|
201
|
+
name: 'chosen',
|
|
202
|
+
message: 'Select MCPs to remove from this workspace:',
|
|
203
|
+
choices: ws.mcps,
|
|
204
|
+
validate: v => v.length > 0 || 'Select at least one MCP',
|
|
205
|
+
}]);
|
|
206
|
+
if (!answer) return;
|
|
207
|
+
|
|
208
|
+
const updated = { ...ws, mcps: ws.mcps.filter(m => !answer.chosen.includes(m)) };
|
|
209
|
+
writeWorkspace(updated);
|
|
210
|
+
await applyAll(updated, opts.dryRun);
|
|
211
|
+
if (!opts.dryRun) {
|
|
212
|
+
log.success(`Removed MCPs: ${answer.chosen.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// list — project workspace or global registry
|
|
217
|
+
mcp
|
|
218
|
+
.command('list')
|
|
219
|
+
.description('List MCPs in this workspace; use -g to list the global registry')
|
|
220
|
+
.option('-g, --global', 'List all MCPs in the global registry instead')
|
|
221
|
+
.action((opts) => {
|
|
222
|
+
if (opts.global) {
|
|
223
|
+
const ids = listMcps();
|
|
224
|
+
log.info('Global registry MCPs:');
|
|
225
|
+
if (ids.length === 0) {
|
|
226
|
+
log.info(' (none)');
|
|
227
|
+
} else {
|
|
228
|
+
ids.forEach(id => log.info(` ${id}`));
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ws = requireWorkspace();
|
|
234
|
+
log.info('Workspace MCPs:');
|
|
235
|
+
if (ws.mcps?.length) {
|
|
236
|
+
ws.mcps.forEach(id => log.info(` ${id}`));
|
|
237
|
+
} else {
|
|
238
|
+
log.info(' (none)');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// show — unchanged
|
|
243
|
+
mcp
|
|
244
|
+
.command('show <id>')
|
|
245
|
+
.description('Show details of a registered MCP')
|
|
246
|
+
.action((id) => {
|
|
247
|
+
const def = getMcp(id);
|
|
248
|
+
if (!def) throw new Error(`MCP "${id}" not found`);
|
|
249
|
+
log.info(JSON.stringify(def, null, 2));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return mcp;
|
|
253
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import TOML from '@iarna/toml';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
import { resolvePath } from '../utils/pathResolver.js';
|
|
7
|
+
import { readJson } from '../utils/fileUtils.js';
|
|
8
|
+
import { listTools, loadTool } from '../registry/toolRegistry.js';
|
|
9
|
+
import { getMcp, saveMcp } from '../registry/mcpRegistry.js';
|
|
10
|
+
import { listSkills, addSkill } from '../registry/skillRegistry.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract MCPs from a tool's global config file.
|
|
14
|
+
* @param {object} tool
|
|
15
|
+
* @returns {{id: string, transport: string, command: string, args?: string[], env?: object}[]}
|
|
16
|
+
*/
|
|
17
|
+
function extractMcpsFromTool(tool) {
|
|
18
|
+
if (!tool.mcp?.global) return [];
|
|
19
|
+
|
|
20
|
+
const resolvedPath = resolvePath(tool.mcp.global.targetFile);
|
|
21
|
+
if (!fs.existsSync(resolvedPath)) return [];
|
|
22
|
+
|
|
23
|
+
let data;
|
|
24
|
+
if (tool.mcp.format === 'toml') {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
27
|
+
data = TOML.parse(raw);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
log.warn(`Could not parse TOML at ${resolvedPath}: ${e.message}`);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
data = readJson(resolvedPath);
|
|
34
|
+
if (!data) return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries = data[tool.mcp.global.rootObject] ?? {};
|
|
38
|
+
const results = [];
|
|
39
|
+
|
|
40
|
+
for (const [id, def] of Object.entries(entries)) {
|
|
41
|
+
if (!def.command) continue;
|
|
42
|
+
const entry = { id, transport: 'stdio', command: def.command };
|
|
43
|
+
if (def.args) entry.args = def.args;
|
|
44
|
+
if (def.env) entry.env = def.env;
|
|
45
|
+
results.push(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract skills from a tool's global skills folder.
|
|
53
|
+
* @param {object} tool
|
|
54
|
+
* @returns {{name: string, srcPath: string}[]}
|
|
55
|
+
*/
|
|
56
|
+
function extractSkillsFromTool(tool) {
|
|
57
|
+
if (!tool.skills?.global) return [];
|
|
58
|
+
|
|
59
|
+
const folderPath = resolvePath(tool.skills.global.targetFolder);
|
|
60
|
+
if (!fs.existsSync(folderPath)) return [];
|
|
61
|
+
|
|
62
|
+
const entries = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
63
|
+
const results = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry.isDirectory()) continue;
|
|
67
|
+
const skillMd = path.join(folderPath, entry.name, 'SKILL.md');
|
|
68
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
69
|
+
results.push({ name: entry.name, srcPath: path.join(folderPath, entry.name) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function makePullCommand() {
|
|
76
|
+
const cmd = new Command('pull');
|
|
77
|
+
|
|
78
|
+
cmd
|
|
79
|
+
.description('Pull skills and MCPs from global tool config files into the AWM registry')
|
|
80
|
+
.argument('[type]', 'What to pull: "skills" or "mcps" (default: both)')
|
|
81
|
+
.option('--tool <id>', 'Scope to a specific tool ID')
|
|
82
|
+
.option('--force', 'Overwrite existing registry entries')
|
|
83
|
+
.option('--dry-run', 'Show what would be imported without making changes')
|
|
84
|
+
.action(async (type, opts) => {
|
|
85
|
+
// Phase A — Resolve tool list
|
|
86
|
+
const toolIds = opts.tool ? [opts.tool] : listTools();
|
|
87
|
+
|
|
88
|
+
if (opts.tool) {
|
|
89
|
+
const check = loadTool(opts.tool);
|
|
90
|
+
if (!check) {
|
|
91
|
+
log.error(`Unknown tool: "${opts.tool}"`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Phase B — Determine what to pull
|
|
97
|
+
const pullMcps = !type || type === 'mcps';
|
|
98
|
+
const pullSkills = !type || type === 'skills';
|
|
99
|
+
|
|
100
|
+
if (!pullMcps && !pullSkills) {
|
|
101
|
+
log.error(`Unknown type: "${type}". Use "skills" or "mcps".`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let mcpPulled = 0, mcpSkipped = 0;
|
|
106
|
+
let skillPulled = 0, skillSkipped = 0;
|
|
107
|
+
|
|
108
|
+
// Phase C — Per-tool loop
|
|
109
|
+
for (const id of toolIds) {
|
|
110
|
+
const tool = loadTool(id);
|
|
111
|
+
if (!tool) continue;
|
|
112
|
+
|
|
113
|
+
if (pullMcps) {
|
|
114
|
+
for (const entry of extractMcpsFromTool(tool)) {
|
|
115
|
+
const exists = getMcp(entry.id) !== null;
|
|
116
|
+
if (exists && !opts.force) {
|
|
117
|
+
log.info(` [mcp] Skipping "${entry.id}" (already registered)`);
|
|
118
|
+
mcpSkipped++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (opts.dryRun) {
|
|
122
|
+
log.dryRun(`Would register MCP "${entry.id}" from ${id}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
saveMcp(entry);
|
|
127
|
+
log.success(` [mcp] Registered "${entry.id}" from ${id}`);
|
|
128
|
+
mcpPulled++;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
log.warn(` [mcp] Could not register "${entry.id}": ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pullSkills) {
|
|
136
|
+
for (const entry of extractSkillsFromTool(tool)) {
|
|
137
|
+
const exists = listSkills().includes(entry.name);
|
|
138
|
+
if (exists && !opts.force) {
|
|
139
|
+
log.info(` [skill] Skipping "${entry.name}" (already registered)`);
|
|
140
|
+
skillSkipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (opts.dryRun) {
|
|
144
|
+
log.dryRun(`Would register skill "${entry.name}" from ${id}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await addSkill(entry.name, entry.srcPath);
|
|
149
|
+
log.success(` [skill] Registered "${entry.name}" from ${id}`);
|
|
150
|
+
skillPulled++;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
log.warn(` [skill] Could not register "${entry.name}": ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Phase D — Summary
|
|
159
|
+
if (!opts.dryRun) {
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (pullMcps) parts.push(`MCPs: ${mcpPulled} pulled, ${mcpSkipped} skipped`);
|
|
162
|
+
if (pullSkills) parts.push(`Skills: ${skillPulled} pulled, ${skillSkipped} skipped`);
|
|
163
|
+
log.info(parts.join(' | '));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return cmd;
|
|
168
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../utils/logger.js';
|
|
3
|
+
import { runSeed } from '../seed.js';
|
|
4
|
+
|
|
5
|
+
export function makeSetupCommand() {
|
|
6
|
+
return new Command('setup')
|
|
7
|
+
.description('Seed the registry with default MCPs and skills')
|
|
8
|
+
.option('--force', 'Overwrite existing defaults with fresh copies')
|
|
9
|
+
.action((opts) => {
|
|
10
|
+
const force = !!opts.force;
|
|
11
|
+
const results = runSeed(force);
|
|
12
|
+
|
|
13
|
+
if (results.tools.length > 0) {
|
|
14
|
+
log.success(`Seeded tools: ${results.tools.join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
if (results.mcps.length > 0) {
|
|
17
|
+
log.success(`Seeded MCPs: ${results.mcps.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
if (results.skills.length > 0) {
|
|
20
|
+
log.success(`Seeded skills: ${results.skills.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
if (results.skipped.length > 0) {
|
|
23
|
+
log.info(`Skipped (already present): ${results.skipped.join(', ')}`);
|
|
24
|
+
log.info('Use --force to overwrite existing defaults.');
|
|
25
|
+
}
|
|
26
|
+
const seeded = results.tools.length + results.mcps.length + results.skills.length;
|
|
27
|
+
if (seeded === 0 && results.skipped.length === 0) {
|
|
28
|
+
log.info('Nothing to seed.');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
import { listSkills, addSkill, showSkill, removeSkill } from '../registry/skillRegistry.js';
|
|
7
|
+
import { requireWorkspace, writeWorkspace } from '../workspace/workspaceConfig.js';
|
|
8
|
+
import { applyAll } from '../workspace/applyWorkspace.js';
|
|
9
|
+
|
|
10
|
+
/** Returns null if the user cancelled (ESC / Ctrl+C), rethrows other errors. */
|
|
11
|
+
async function prompt(questions) {
|
|
12
|
+
try {
|
|
13
|
+
return await inquirer.prompt(questions);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.name === 'ExitPromptError') {
|
|
16
|
+
log.info('Cancelled.');
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Directories to scan for 'skill import'
|
|
24
|
+
const SKILL_SCAN_DIRS = [
|
|
25
|
+
'.claude/skills',
|
|
26
|
+
'.agents/skills',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function makeSkillCommand() {
|
|
30
|
+
const skill = new Command('skill').description('Manage skills');
|
|
31
|
+
|
|
32
|
+
// register — copy into global registry
|
|
33
|
+
skill
|
|
34
|
+
.command('register <name>')
|
|
35
|
+
.description('Register a skill from a directory or .md file into the global registry')
|
|
36
|
+
.requiredOption('--from <path>', 'Source directory or .md file')
|
|
37
|
+
.action(async (name, opts) => {
|
|
38
|
+
const src = path.resolve(opts.from);
|
|
39
|
+
await addSkill(name, src);
|
|
40
|
+
log.success(`Skill "${name}" registered in global registry`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// unregister — remove from global registry
|
|
44
|
+
skill
|
|
45
|
+
.command('unregister <name>')
|
|
46
|
+
.description('Remove a skill from the global registry')
|
|
47
|
+
.action((name) => {
|
|
48
|
+
removeSkill(name);
|
|
49
|
+
log.success(`Skill "${name}" removed from registry`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// import — scan cwd skill dirs and register found skills
|
|
53
|
+
skill
|
|
54
|
+
.command('import')
|
|
55
|
+
.description('Scan cwd skill directories and import skills into the global registry')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
let imported = 0;
|
|
58
|
+
let skipped = 0;
|
|
59
|
+
|
|
60
|
+
for (const relDir of SKILL_SCAN_DIRS) {
|
|
61
|
+
const scanDir = path.join(process.cwd(), relDir);
|
|
62
|
+
if (!fs.existsSync(scanDir)) continue;
|
|
63
|
+
|
|
64
|
+
for (const entry of fs.readdirSync(scanDir)) {
|
|
65
|
+
const skillPath = path.join(scanDir, entry);
|
|
66
|
+
if (!fs.statSync(skillPath).isDirectory()) continue;
|
|
67
|
+
const skillMd = path.join(skillPath, 'SKILL.md');
|
|
68
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
69
|
+
|
|
70
|
+
if (listSkills().includes(entry)) {
|
|
71
|
+
log.info(` Skipping "${entry}" (already in registry)`);
|
|
72
|
+
skipped++;
|
|
73
|
+
} else {
|
|
74
|
+
await addSkill(entry, skillPath);
|
|
75
|
+
log.success(` Registered "${entry}" from ${relDir}`);
|
|
76
|
+
imported++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (imported === 0 && skipped === 0) {
|
|
82
|
+
log.info('No skill directories found to import.');
|
|
83
|
+
} else {
|
|
84
|
+
log.info(`Import complete: ${imported} registered, ${skipped} skipped.`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// add — select from registry, add to .awm.json, apply
|
|
89
|
+
skill
|
|
90
|
+
.command('add')
|
|
91
|
+
.description('Add skills from the registry to this workspace and apply to all tools')
|
|
92
|
+
.option('--dry-run', 'Preview what would be written without making changes')
|
|
93
|
+
.action(async (opts) => {
|
|
94
|
+
const ws = requireWorkspace();
|
|
95
|
+
const allNames = listSkills();
|
|
96
|
+
|
|
97
|
+
if (allNames.length === 0) {
|
|
98
|
+
log.info('No skills in registry. Run "awm skill register --from <path>" to add one.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const available = allNames.filter(n => !ws.skills.includes(n));
|
|
103
|
+
if (available.length === 0) {
|
|
104
|
+
log.info('All registry skills are already in this workspace.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const answer = await prompt([{
|
|
109
|
+
type: 'checkbox',
|
|
110
|
+
name: 'chosen',
|
|
111
|
+
message: 'Select skills to add to this workspace:',
|
|
112
|
+
choices: available,
|
|
113
|
+
validate: v => v.length > 0 || 'Select at least one skill',
|
|
114
|
+
}]);
|
|
115
|
+
if (!answer) return;
|
|
116
|
+
|
|
117
|
+
const updated = { ...ws, skills: [...ws.skills, ...answer.chosen] };
|
|
118
|
+
writeWorkspace(updated);
|
|
119
|
+
await applyAll(updated, opts.dryRun);
|
|
120
|
+
if (!opts.dryRun) {
|
|
121
|
+
log.success(`Added skills: ${answer.chosen.join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// delete — select from workspace list, remove from .awm.json (no file deletion)
|
|
126
|
+
skill
|
|
127
|
+
.command('delete')
|
|
128
|
+
.description('Select skills to remove from this workspace (does not delete files from tool dirs)')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
const ws = requireWorkspace();
|
|
131
|
+
if (!ws.skills.length) {
|
|
132
|
+
log.info('No skills in this workspace to remove.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const answer = await prompt([{
|
|
137
|
+
type: 'checkbox',
|
|
138
|
+
name: 'chosen',
|
|
139
|
+
message: 'Select skills to remove from this workspace:',
|
|
140
|
+
choices: ws.skills,
|
|
141
|
+
validate: v => v.length > 0 || 'Select at least one skill',
|
|
142
|
+
}]);
|
|
143
|
+
if (!answer) return;
|
|
144
|
+
|
|
145
|
+
const updated = { ...ws, skills: ws.skills.filter(s => !answer.chosen.includes(s)) };
|
|
146
|
+
writeWorkspace(updated);
|
|
147
|
+
log.success(`Removed skills: ${answer.chosen.join(', ')}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// list — project workspace or global registry
|
|
151
|
+
skill
|
|
152
|
+
.command('list')
|
|
153
|
+
.description('List skills in this workspace; use -g to list the global registry')
|
|
154
|
+
.option('-g, --global', 'List all skills in the global registry instead')
|
|
155
|
+
.action((opts) => {
|
|
156
|
+
if (opts.global) {
|
|
157
|
+
const names = listSkills();
|
|
158
|
+
log.info('Global registry skills:');
|
|
159
|
+
if (names.length === 0) {
|
|
160
|
+
log.info(' (none)');
|
|
161
|
+
} else {
|
|
162
|
+
names.forEach(n => log.info(` ${n}`));
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const ws = requireWorkspace();
|
|
168
|
+
log.info('Workspace skills:');
|
|
169
|
+
if (ws.skills?.length) {
|
|
170
|
+
ws.skills.forEach(n => log.info(` ${n}`));
|
|
171
|
+
} else {
|
|
172
|
+
log.info(' (none)');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// show — unchanged
|
|
177
|
+
skill
|
|
178
|
+
.command('show <name>')
|
|
179
|
+
.description('Show SKILL.md for a registered skill')
|
|
180
|
+
.action((name) => {
|
|
181
|
+
const content = showSkill(name);
|
|
182
|
+
if (content === null) throw new Error(`Skill "${name}" not found or missing SKILL.md`);
|
|
183
|
+
log.info(content);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return skill;
|
|
187
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../utils/logger.js';
|
|
3
|
+
import { requireWorkspace } from '../workspace/workspaceConfig.js';
|
|
4
|
+
|
|
5
|
+
export function makeStatusCommand() {
|
|
6
|
+
return new Command('status')
|
|
7
|
+
.description('Show current workspace state (.awm.json)')
|
|
8
|
+
.action(() => {
|
|
9
|
+
const ws = requireWorkspace();
|
|
10
|
+
|
|
11
|
+
log.info('Workspace status:');
|
|
12
|
+
log.info(` Tools: ${ws.tools?.length ? ws.tools.join(', ') : '(none)'}`);
|
|
13
|
+
log.info(` MCPs: ${ws.mcps?.length ? ws.mcps.join(', ') : '(none)'}`);
|
|
14
|
+
log.info(` Skills: ${ws.skills?.length ? ws.skills.join(', ') : '(none)'}`);
|
|
15
|
+
log.info(` lastSync: ${ws.lastSync ?? '(never)'}`);
|
|
16
|
+
});
|
|
17
|
+
}
|