@haolin-ai/skillman 1.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/bin/skillman.mjs +13 -0
- package/package.json +34 -0
- package/src/agents.yaml +99 -0
- package/src/cli.js +306 -0
- package/src/config.js +82 -0
- package/src/history.js +63 -0
- package/src/i18n.js +160 -0
- package/src/index.js +7 -0
- package/src/installer.js +40 -0
- package/src/scanner.js +48 -0
package/bin/skillman.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skillman - AI Agent Skill Installer
|
|
4
|
+
*
|
|
5
|
+
* A CLI tool to install AI agent skills across multiple platforms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cli } from '../src/cli.js';
|
|
9
|
+
|
|
10
|
+
cli().catch(err => {
|
|
11
|
+
console.error(`\x1b[31m✗\x1b[0m ${err.message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haolin-ai/skillman",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A CLI tool to install AI agent skills across multiple platforms",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillman": "./bin/skillman.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cli",
|
|
18
|
+
"ai",
|
|
19
|
+
"agent",
|
|
20
|
+
"skills",
|
|
21
|
+
"installer"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "^7.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/agents.yaml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Agent Configuration
|
|
2
|
+
# Defines skill installation paths for each supported agent
|
|
3
|
+
|
|
4
|
+
agents:
|
|
5
|
+
claude-code:
|
|
6
|
+
name: claude-code
|
|
7
|
+
displayName: Claude Code
|
|
8
|
+
skillsDir: .claude/skills
|
|
9
|
+
globalSkillsDir: ~/.claude/skills
|
|
10
|
+
|
|
11
|
+
openclaw:
|
|
12
|
+
name: openclaw
|
|
13
|
+
displayName: OpenClaw
|
|
14
|
+
skillsDir: skills
|
|
15
|
+
globalSkillsDir: ~/.openclaw/skills
|
|
16
|
+
|
|
17
|
+
qoder:
|
|
18
|
+
name: qoder
|
|
19
|
+
displayName: Qoder
|
|
20
|
+
skillsDir: .qoder/skills
|
|
21
|
+
globalSkillsDir: ~/.qoder/skills
|
|
22
|
+
|
|
23
|
+
codex:
|
|
24
|
+
name: codex
|
|
25
|
+
displayName: Codex
|
|
26
|
+
skillsDir: .agents/skills
|
|
27
|
+
globalSkillsDir: ~/.codex/skills
|
|
28
|
+
|
|
29
|
+
opencode:
|
|
30
|
+
name: opencode
|
|
31
|
+
displayName: OpenCode
|
|
32
|
+
skillsDir: .agents/skills
|
|
33
|
+
globalSkillsDir: ~/.config/opencode/skills
|
|
34
|
+
|
|
35
|
+
cursor:
|
|
36
|
+
name: cursor
|
|
37
|
+
displayName: Cursor
|
|
38
|
+
skillsDir: .agents/skills
|
|
39
|
+
globalSkillsDir: ~/.cursor/skills
|
|
40
|
+
|
|
41
|
+
cline:
|
|
42
|
+
name: cline
|
|
43
|
+
displayName: Cline
|
|
44
|
+
skillsDir: .agents/skills
|
|
45
|
+
globalSkillsDir: ~/.agents/skills
|
|
46
|
+
|
|
47
|
+
goose:
|
|
48
|
+
name: goose
|
|
49
|
+
displayName: Goose
|
|
50
|
+
skillsDir: .goose/skills
|
|
51
|
+
globalSkillsDir: ~/.config/goose/skills
|
|
52
|
+
|
|
53
|
+
continue:
|
|
54
|
+
name: continue
|
|
55
|
+
displayName: Continue
|
|
56
|
+
skillsDir: .continue/skills
|
|
57
|
+
globalSkillsDir: ~/.continue/skills
|
|
58
|
+
|
|
59
|
+
augment:
|
|
60
|
+
name: augment
|
|
61
|
+
displayName: Augment
|
|
62
|
+
skillsDir: .augment/skills
|
|
63
|
+
globalSkillsDir: ~/.augment/skills
|
|
64
|
+
|
|
65
|
+
github-copilot:
|
|
66
|
+
name: github-copilot
|
|
67
|
+
displayName: GitHub Copilot
|
|
68
|
+
skillsDir: .agents/skills
|
|
69
|
+
globalSkillsDir: ~/.copilot/skills
|
|
70
|
+
|
|
71
|
+
gemini-cli:
|
|
72
|
+
name: gemini-cli
|
|
73
|
+
displayName: Gemini CLI
|
|
74
|
+
skillsDir: .agents/skills
|
|
75
|
+
globalSkillsDir: ~/.gemini/skills
|
|
76
|
+
|
|
77
|
+
pi:
|
|
78
|
+
name: pi
|
|
79
|
+
displayName: Pi
|
|
80
|
+
skillsDir: .pi/skills
|
|
81
|
+
globalSkillsDir: ~/.pi/agent/skills
|
|
82
|
+
|
|
83
|
+
replit:
|
|
84
|
+
name: replit
|
|
85
|
+
displayName: Replit
|
|
86
|
+
skillsDir: .agents/skills
|
|
87
|
+
globalSkillsDir: ~/.config/agents/skills
|
|
88
|
+
|
|
89
|
+
roo:
|
|
90
|
+
name: roo
|
|
91
|
+
displayName: Roo Code
|
|
92
|
+
skillsDir: .roo/skills
|
|
93
|
+
globalSkillsDir: ~/.roo/skills
|
|
94
|
+
|
|
95
|
+
trae:
|
|
96
|
+
name: trae
|
|
97
|
+
displayName: Trae
|
|
98
|
+
skillsDir: .trae/skills
|
|
99
|
+
globalSkillsDir: ~/.trae/skills
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Entry Point
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { select, confirm, input, Separator } from '@inquirer/prompts';
|
|
9
|
+
import { scanSkills } from './scanner.js';
|
|
10
|
+
import { installSkill } from './installer.js';
|
|
11
|
+
import { loadAgents } from './config.js';
|
|
12
|
+
import { t } from './i18n.js';
|
|
13
|
+
import { loadHistory, addWorkspace } from './history.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const VERSION = '1.0.0';
|
|
17
|
+
|
|
18
|
+
// ANSI colors
|
|
19
|
+
const c = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
green: '\x1b[32m',
|
|
22
|
+
yellow: '\x1b[33m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
gray: '\x1b[90m',
|
|
26
|
+
cyan: '\x1b[36m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const log = {
|
|
30
|
+
info: (msg) => console.log(`${c.blue}ℹ${c.reset} ${msg}`),
|
|
31
|
+
success: (msg) => console.log(`${c.green}✓${c.reset} ${msg}`),
|
|
32
|
+
warn: (msg) => console.log(`${c.yellow}⚠${c.reset} ${msg}`),
|
|
33
|
+
error: (msg) => console.log(`${c.red}✗${c.reset} ${msg}`),
|
|
34
|
+
step: (msg) => console.log(`\n${c.blue}▶${c.reset} ${msg}`),
|
|
35
|
+
dry: (msg) => console.log(`${c.yellow}[DRY-RUN]${c.reset} ${msg}`)
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Parse CLI arguments
|
|
39
|
+
function parseArgs(args) {
|
|
40
|
+
const result = {
|
|
41
|
+
command: null,
|
|
42
|
+
dryRun: false,
|
|
43
|
+
help: false,
|
|
44
|
+
version: false,
|
|
45
|
+
positional: []
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (const arg of args) {
|
|
49
|
+
if (arg === '--dry-run' || arg === '-n') {
|
|
50
|
+
result.dryRun = true;
|
|
51
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
52
|
+
result.help = true;
|
|
53
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
54
|
+
result.version = true;
|
|
55
|
+
} else if (!arg.startsWith('-')) {
|
|
56
|
+
if (!result.command) {
|
|
57
|
+
result.command = arg;
|
|
58
|
+
} else {
|
|
59
|
+
result.positional.push(arg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Show help
|
|
68
|
+
function showHelp() {
|
|
69
|
+
console.log(`${c.green}${t('app.name')}${c.reset} - ${t('app.description')}
|
|
70
|
+
|
|
71
|
+
${c.cyan}${t('help.usage')}:${c.reset}
|
|
72
|
+
skillman ${t('help.cmd.interactive')}
|
|
73
|
+
skillman install <path> ${t('help.cmd.install')}
|
|
74
|
+
skillman agents ${t('help.cmd.agents')}
|
|
75
|
+
|
|
76
|
+
${c.cyan}${t('help.options')}:${c.reset}
|
|
77
|
+
-n, --dry-run ${t('help.opt.dry_run')}
|
|
78
|
+
-v, --version ${t('help.opt.version')}
|
|
79
|
+
-h, --help ${t('help.opt.help')}
|
|
80
|
+
|
|
81
|
+
${c.cyan}${t('help.examples')}:${c.reset}
|
|
82
|
+
skillman # ${t('help.cmd.interactive')}
|
|
83
|
+
skillman --dry-run # ${t('help.opt.dry_run')}
|
|
84
|
+
skillman install ./my-skill # ${t('help.cmd.install')}
|
|
85
|
+
skillman agents # ${t('help.cmd.agents')}
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Show version
|
|
90
|
+
function showVersion() {
|
|
91
|
+
console.log(`${t('app.name').toLowerCase()} v${VERSION}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// List agents
|
|
95
|
+
async function listAgents() {
|
|
96
|
+
const agents = await loadAgents();
|
|
97
|
+
|
|
98
|
+
console.log(`\n${c.cyan}${t('msg.agent')}:${c.reset}\n`);
|
|
99
|
+
|
|
100
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
101
|
+
console.log(` ${c.green}${agent.displayName}${c.reset} (${name})`);
|
|
102
|
+
console.log(` ${t('option.global')}: ${c.gray}${agent.globalSkillsDir}${c.reset}`);
|
|
103
|
+
console.log(` ${t('option.workspace')}: ${c.gray}${agent.skillsDir}${c.reset}`);
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Interactive install flow
|
|
109
|
+
async function interactiveInstall(dryRun) {
|
|
110
|
+
console.log(`${c.green}${t('app.name')}${c.reset} - ${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
|
|
111
|
+
|
|
112
|
+
// Step 1: Scan skills
|
|
113
|
+
log.step(t('step.scan'));
|
|
114
|
+
|
|
115
|
+
const skills = await scanSkills(process.cwd());
|
|
116
|
+
if (skills.length === 0) {
|
|
117
|
+
log.error(t('msg.no_skills'));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
log.success(t('msg.found_skills', { count: skills.length }));
|
|
122
|
+
|
|
123
|
+
// Step 2: Select skill
|
|
124
|
+
const skillChoices = skills.map(s => ({
|
|
125
|
+
name: s.description
|
|
126
|
+
? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
|
|
127
|
+
: s.name,
|
|
128
|
+
value: s
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const selectedSkill = await select({
|
|
132
|
+
message: t('step.select_skill') + ':',
|
|
133
|
+
choices: skillChoices,
|
|
134
|
+
pageSize: 10
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
log.success(`${t('msg.selected')}: ${selectedSkill.name}`);
|
|
138
|
+
|
|
139
|
+
// Step 3: Select agent
|
|
140
|
+
const agents = await loadAgents();
|
|
141
|
+
const agentChoices = Object.values(agents).map(a => ({
|
|
142
|
+
name: a.displayName,
|
|
143
|
+
value: a
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const agent = await select({
|
|
147
|
+
message: t('step.select_agent') + ':',
|
|
148
|
+
choices: agentChoices,
|
|
149
|
+
pageSize: 10
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
log.success(`${t('msg.agent')}: ${agent.displayName}`);
|
|
153
|
+
|
|
154
|
+
// Step 4: Select scope
|
|
155
|
+
const scope = await select({
|
|
156
|
+
message: t('step.select_scope') + ':',
|
|
157
|
+
choices: [
|
|
158
|
+
{ name: `${t('option.global')} ${c.gray}(${agent.globalSkillsDir})${c.reset}`, value: 'global' },
|
|
159
|
+
{ name: `${t('option.workspace')} ${c.gray}(${t('option.custom_path')})${c.reset}`, value: 'workspace' }
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Step 5: If workspace scope, ask for workspace path
|
|
164
|
+
let workspacePath = agent.skillsDir;
|
|
165
|
+
if (scope === 'workspace') {
|
|
166
|
+
const history = await loadHistory(agent.name);
|
|
167
|
+
|
|
168
|
+
let customPath;
|
|
169
|
+
|
|
170
|
+
if (history.length > 0) {
|
|
171
|
+
// Show history choices with separator
|
|
172
|
+
const historyChoices = [
|
|
173
|
+
...history.map((h, idx) => ({
|
|
174
|
+
name: `${idx + 1}. ${h}`,
|
|
175
|
+
value: h
|
|
176
|
+
})),
|
|
177
|
+
new Separator(),
|
|
178
|
+
{ name: t('prompt.new_path'), value: '__NEW__' }
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const selected = await select({
|
|
182
|
+
message: t('prompt.select_workspace') + ':',
|
|
183
|
+
choices: historyChoices
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (selected === '__NEW__') {
|
|
187
|
+
customPath = await input({
|
|
188
|
+
message: t('prompt.workspace_path') + ':',
|
|
189
|
+
default: process.cwd(),
|
|
190
|
+
validate: (value) => {
|
|
191
|
+
if (!value.trim()) return t('error.empty_path');
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
customPath = selected;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// No history, ask for input
|
|
200
|
+
customPath = await input({
|
|
201
|
+
message: t('prompt.workspace_path') + ':',
|
|
202
|
+
default: process.cwd(),
|
|
203
|
+
validate: (value) => {
|
|
204
|
+
if (!value.trim()) return t('error.empty_path');
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Save to history (with agent name)
|
|
211
|
+
await addWorkspace(agent.name, customPath);
|
|
212
|
+
|
|
213
|
+
// Extract relative skills directory from agent config
|
|
214
|
+
const skillsRelDir = agent.skillsDir.includes(path.sep)
|
|
215
|
+
? agent.skillsDir.split(path.sep).slice(-2).join(path.sep)
|
|
216
|
+
: agent.skillsDir;
|
|
217
|
+
workspacePath = path.join(customPath.trim(), skillsRelDir);
|
|
218
|
+
log.info(`${t('msg.workspace_dir')}: ${workspacePath}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Step 6: Calculate target path
|
|
222
|
+
const targetDir = scope === 'global'
|
|
223
|
+
? path.join(agent.globalSkillsDir, selectedSkill.name)
|
|
224
|
+
: path.join(workspacePath, selectedSkill.name);
|
|
225
|
+
|
|
226
|
+
// Dry-run preview
|
|
227
|
+
if (dryRun) {
|
|
228
|
+
log.step(t('step.preview'));
|
|
229
|
+
log.dry(`${t('msg.source')}: ${selectedSkill.path}`);
|
|
230
|
+
log.dry(`${t('msg.target')}: ${targetDir}`);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await fs.access(targetDir);
|
|
234
|
+
log.dry(t('msg.exists'));
|
|
235
|
+
} catch {
|
|
236
|
+
log.dry(t('msg.not_exists'));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log.dry(t('msg.copy'));
|
|
240
|
+
|
|
241
|
+
console.log(`\n${c.yellow}📋 ${t('msg.preview_summary')}${c.reset}\n`);
|
|
242
|
+
console.log(` Skill: ${selectedSkill.name}`);
|
|
243
|
+
console.log(` ${t('msg.agent')}: ${agent.displayName}`);
|
|
244
|
+
console.log(` ${t('msg.scope')}: ${scope}`);
|
|
245
|
+
console.log(` ${t('msg.location')}: ${targetDir}`);
|
|
246
|
+
console.log(`\n${c.gray}${t('msg.dry_run_hint')}${c.reset}\n`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Step 6: Install
|
|
251
|
+
log.step(t('step.install'));
|
|
252
|
+
|
|
253
|
+
// Check if already exists
|
|
254
|
+
try {
|
|
255
|
+
await fs.access(targetDir);
|
|
256
|
+
log.warn(t('msg.skill_exists'));
|
|
257
|
+
const overwrite = await confirm({ message: t('prompt.overwrite') + '?', default: false });
|
|
258
|
+
if (!overwrite) {
|
|
259
|
+
log.info(t('msg.install_cancelled'));
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Directory doesn't exist, proceed
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Install
|
|
267
|
+
await installSkill(selectedSkill.path, targetDir);
|
|
268
|
+
log.success(`${t('msg.target')}: ${targetDir}`);
|
|
269
|
+
|
|
270
|
+
// Summary
|
|
271
|
+
console.log(`\n${c.green}✨ ${t('msg.install_complete')}${c.reset}\n`);
|
|
272
|
+
console.log(` Skill: ${selectedSkill.name}`);
|
|
273
|
+
console.log(` ${t('msg.agent')}: ${agent.displayName}`);
|
|
274
|
+
console.log(` ${t('msg.scope')}: ${scope}`);
|
|
275
|
+
console.log(` ${t('msg.location')}: ${targetDir}`);
|
|
276
|
+
console.log();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Main CLI function
|
|
280
|
+
export async function cli() {
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
const options = parseArgs(args);
|
|
283
|
+
|
|
284
|
+
if (options.help) {
|
|
285
|
+
showHelp();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (options.version) {
|
|
290
|
+
showVersion();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (options.command === 'agents') {
|
|
295
|
+
await listAgents();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (options.command === 'install') {
|
|
300
|
+
log.error(t('error.not_implemented'));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Default: interactive install
|
|
305
|
+
await interactiveInstall(options.dryRun);
|
|
306
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager
|
|
3
|
+
* Loads and manages agent configurations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const AGENTS_FILE = path.join(__dirname, 'agents.yaml');
|
|
13
|
+
|
|
14
|
+
let agentsCache = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load agents configuration from YAML file
|
|
18
|
+
* @returns {Promise<Record<string, {name: string, displayName: string, skillsDir: string, globalSkillsDir: string}>>}
|
|
19
|
+
*/
|
|
20
|
+
export async function loadAgents() {
|
|
21
|
+
if (agentsCache) return agentsCache;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(AGENTS_FILE, 'utf-8');
|
|
25
|
+
const agents = {};
|
|
26
|
+
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
let currentAgent = null;
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
34
|
+
|
|
35
|
+
// Detect agent entry (e.g., "claude-code:")
|
|
36
|
+
const agentMatch = line.match(/^(\s*)([\w-]+):\s*$/);
|
|
37
|
+
if (agentMatch && agentMatch[1].length === 2) {
|
|
38
|
+
currentAgent = agentMatch[2];
|
|
39
|
+
agents[currentAgent] = { name: currentAgent };
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse properties
|
|
44
|
+
if (currentAgent) {
|
|
45
|
+
const propMatch = line.match(/^\s+([\w]+):\s*(.+)$/);
|
|
46
|
+
if (propMatch) {
|
|
47
|
+
const [, key, value] = propMatch;
|
|
48
|
+
let parsedValue = value.trim();
|
|
49
|
+
|
|
50
|
+
// Remove quotes if present
|
|
51
|
+
if ((parsedValue.startsWith('"') && parsedValue.endsWith('"')) ||
|
|
52
|
+
(parsedValue.startsWith("'") && parsedValue.endsWith("'"))) {
|
|
53
|
+
parsedValue = parsedValue.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Expand ~ to home directory
|
|
57
|
+
if (parsedValue.startsWith('~/')) {
|
|
58
|
+
parsedValue = path.join(homedir(), parsedValue.slice(2));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
agents[currentAgent][key] = parsedValue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
agentsCache = agents;
|
|
67
|
+
return agents;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`Failed to load agents: ${err.message}`);
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a specific agent by name
|
|
76
|
+
* @param {string} name - Agent name
|
|
77
|
+
* @returns {Promise<object|null>}
|
|
78
|
+
*/
|
|
79
|
+
export async function getAgent(name) {
|
|
80
|
+
const agents = await loadAgents();
|
|
81
|
+
return agents[name] || null;
|
|
82
|
+
}
|
package/src/history.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace path history management
|
|
3
|
+
* Stores recently used workspace paths for quick selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
const HISTORY_FILE = path.join(os.homedir(), '.config', 'skillman', 'history.json');
|
|
11
|
+
const MAX_HISTORY_SIZE = 10;
|
|
12
|
+
|
|
13
|
+
async function ensureDir() {
|
|
14
|
+
const dir = path.dirname(HISTORY_FILE);
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(dir);
|
|
17
|
+
} catch {
|
|
18
|
+
await fs.mkdir(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadHistory(agentName) {
|
|
23
|
+
try {
|
|
24
|
+
await ensureDir();
|
|
25
|
+
const data = await fs.readFile(HISTORY_FILE, 'utf8');
|
|
26
|
+
const history = JSON.parse(data);
|
|
27
|
+
return history[agentName]?.workspaces || [];
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function saveHistory(agentName, workspaces) {
|
|
34
|
+
await ensureDir();
|
|
35
|
+
let data = {};
|
|
36
|
+
try {
|
|
37
|
+
const existing = await fs.readFile(HISTORY_FILE, 'utf8');
|
|
38
|
+
data = JSON.parse(existing);
|
|
39
|
+
} catch {
|
|
40
|
+
// File doesn't exist or is invalid
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
data[agentName] = {
|
|
44
|
+
workspaces: workspaces.slice(0, MAX_HISTORY_SIZE),
|
|
45
|
+
updatedAt: new Date().toISOString()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await fs.writeFile(HISTORY_FILE, JSON.stringify(data, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function addWorkspace(agentName, workspacePath) {
|
|
52
|
+
const normalized = path.resolve(workspacePath);
|
|
53
|
+
let workspaces = await loadHistory(agentName);
|
|
54
|
+
|
|
55
|
+
// Remove if exists (to move to front)
|
|
56
|
+
workspaces = workspaces.filter(w => path.resolve(w) !== normalized);
|
|
57
|
+
|
|
58
|
+
// Add to front
|
|
59
|
+
workspaces.unshift(normalized);
|
|
60
|
+
|
|
61
|
+
await saveHistory(agentName, workspaces);
|
|
62
|
+
return workspaces;
|
|
63
|
+
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internationalization (i18n) module
|
|
3
|
+
* Supports Chinese (zh) and English (en)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
// Detect system language
|
|
9
|
+
function detectLanguage() {
|
|
10
|
+
try {
|
|
11
|
+
const locale = process.env.LANG ||
|
|
12
|
+
process.env.LC_ALL ||
|
|
13
|
+
process.env.LC_MESSAGES ||
|
|
14
|
+
execSync('defaults read -g AppleLanguages 2>/dev/null || echo "en"', { encoding: 'utf8' }).trim();
|
|
15
|
+
|
|
16
|
+
if (locale.toLowerCase().includes('zh')) {
|
|
17
|
+
return 'zh';
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Fallback to English on error
|
|
21
|
+
}
|
|
22
|
+
return 'en';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lang = detectLanguage();
|
|
26
|
+
|
|
27
|
+
const translations = {
|
|
28
|
+
zh: {
|
|
29
|
+
// Common
|
|
30
|
+
'app.name': 'Skillman',
|
|
31
|
+
'app.description': 'AI Agent Skill 安装器',
|
|
32
|
+
'version': '版本',
|
|
33
|
+
|
|
34
|
+
// Steps
|
|
35
|
+
'step.scan': '扫描可安装的 Skills...',
|
|
36
|
+
'step.select_skill': '选择要安装的 Skill',
|
|
37
|
+
'step.select_agent': '选择目标 Agent',
|
|
38
|
+
'step.select_scope': '选择安装范围',
|
|
39
|
+
'step.install': '开始安装...',
|
|
40
|
+
'step.preview': '预览安装...',
|
|
41
|
+
|
|
42
|
+
// Messages
|
|
43
|
+
'msg.found_skills': '找到 {count} 个 skill',
|
|
44
|
+
'msg.no_skills': '当前目录未找到任何 skill (需要包含 SKILL.md 的文件夹)',
|
|
45
|
+
'msg.selected': '选择',
|
|
46
|
+
'msg.agent': 'Agent',
|
|
47
|
+
'msg.scope': '范围',
|
|
48
|
+
'msg.location': '位置',
|
|
49
|
+
'msg.source': '源目录',
|
|
50
|
+
'msg.target': '目标目录',
|
|
51
|
+
'msg.exists': '目标已存在,将覆盖',
|
|
52
|
+
'msg.not_exists': '目标不存在,将创建',
|
|
53
|
+
'msg.copy': '将复制 skill 文件夹到目标位置',
|
|
54
|
+
'msg.workspace_dir': 'Workspace skills 目录',
|
|
55
|
+
'msg.install_complete': '安装完成!',
|
|
56
|
+
'msg.preview_summary': '预览摘要',
|
|
57
|
+
'msg.dry_run_hint': '使用 --dry-run 预览,未执行任何操作',
|
|
58
|
+
'msg.skill_exists': '该 skill 已存在',
|
|
59
|
+
'msg.install_cancelled': '安装已取消',
|
|
60
|
+
|
|
61
|
+
// Prompts
|
|
62
|
+
'prompt.workspace_path': '输入 Workspace 路径',
|
|
63
|
+
'prompt.overwrite': '是否覆盖',
|
|
64
|
+
'prompt.select_workspace': '选择 Workspace 路径',
|
|
65
|
+
'prompt.new_path': '输入新路径...',
|
|
66
|
+
|
|
67
|
+
// Options
|
|
68
|
+
'option.global': '全局',
|
|
69
|
+
'option.workspace': '工作区',
|
|
70
|
+
'option.custom_path': '自定义路径',
|
|
71
|
+
|
|
72
|
+
// Errors
|
|
73
|
+
'error.empty_path': '路径不能为空',
|
|
74
|
+
'error.not_implemented': '该命令尚未实现,请使用交互模式',
|
|
75
|
+
|
|
76
|
+
// Help
|
|
77
|
+
'help.usage': '用法',
|
|
78
|
+
'help.options': '选项',
|
|
79
|
+
'help.examples': '示例',
|
|
80
|
+
'help.cmd.interactive': '交互式安装 (扫描当前目录)',
|
|
81
|
+
'help.cmd.install': '从指定路径安装 skill',
|
|
82
|
+
'help.cmd.agents': '列出可用的 agents',
|
|
83
|
+
'help.opt.dry_run': '预览安装而不执行更改',
|
|
84
|
+
'help.opt.version': '显示版本号',
|
|
85
|
+
'help.opt.help': '显示帮助信息',
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
en: {
|
|
89
|
+
// Common
|
|
90
|
+
'app.name': 'Skillman',
|
|
91
|
+
'app.description': 'AI Agent Skill Installer',
|
|
92
|
+
'version': 'Version',
|
|
93
|
+
|
|
94
|
+
// Steps
|
|
95
|
+
'step.scan': 'Scanning available skills...',
|
|
96
|
+
'step.select_skill': 'Select skill to install',
|
|
97
|
+
'step.select_agent': 'Select target agent',
|
|
98
|
+
'step.select_scope': 'Select installation scope',
|
|
99
|
+
'step.install': 'Starting installation...',
|
|
100
|
+
'step.preview': 'Previewing installation...',
|
|
101
|
+
|
|
102
|
+
// Messages
|
|
103
|
+
'msg.found_skills': 'Found {count} skill(s)',
|
|
104
|
+
'msg.no_skills': 'No skills found in current directory (folders with SKILL.md required)',
|
|
105
|
+
'msg.selected': 'Selected',
|
|
106
|
+
'msg.agent': 'Agent',
|
|
107
|
+
'msg.scope': 'Scope',
|
|
108
|
+
'msg.location': 'Location',
|
|
109
|
+
'msg.source': 'Source',
|
|
110
|
+
'msg.target': 'Target',
|
|
111
|
+
'msg.exists': 'Target exists, will overwrite',
|
|
112
|
+
'msg.not_exists': 'Target does not exist, will create',
|
|
113
|
+
'msg.copy': 'Will copy skill folder to target location',
|
|
114
|
+
'msg.workspace_dir': 'Workspace skills directory',
|
|
115
|
+
'msg.install_complete': 'Installation complete!',
|
|
116
|
+
'msg.preview_summary': 'Preview Summary',
|
|
117
|
+
'msg.dry_run_hint': 'Running with --dry-run, no changes made',
|
|
118
|
+
'msg.skill_exists': 'Skill already exists',
|
|
119
|
+
'msg.install_cancelled': 'Installation cancelled',
|
|
120
|
+
|
|
121
|
+
// Prompts
|
|
122
|
+
'prompt.workspace_path': 'Enter workspace path',
|
|
123
|
+
'prompt.overwrite': 'Overwrite existing',
|
|
124
|
+
'prompt.select_workspace': 'Select workspace path',
|
|
125
|
+
'prompt.new_path': 'Enter new path...',
|
|
126
|
+
|
|
127
|
+
// Options
|
|
128
|
+
'option.global': 'Global',
|
|
129
|
+
'option.workspace': 'Workspace',
|
|
130
|
+
'option.custom_path': 'Custom path',
|
|
131
|
+
|
|
132
|
+
// Errors
|
|
133
|
+
'error.empty_path': 'Path cannot be empty',
|
|
134
|
+
'error.not_implemented': 'Command not implemented, use interactive mode',
|
|
135
|
+
|
|
136
|
+
// Help
|
|
137
|
+
'help.usage': 'Usage',
|
|
138
|
+
'help.options': 'Options',
|
|
139
|
+
'help.examples': 'Examples',
|
|
140
|
+
'help.cmd.interactive': 'Interactive install (scan current directory)',
|
|
141
|
+
'help.cmd.install': 'Install skill from path',
|
|
142
|
+
'help.cmd.agents': 'List available agents',
|
|
143
|
+
'help.opt.dry_run': 'Preview installation without making changes',
|
|
144
|
+
'help.opt.version': 'Show version number',
|
|
145
|
+
'help.opt.help': 'Show this help message',
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export function t(key, vars = {}) {
|
|
150
|
+
const text = translations[lang][key] || translations['en'][key] || key;
|
|
151
|
+
return text.replace(/\{(\w+)\}/g, (match, varName) => vars[varName] ?? match);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getLanguage() {
|
|
155
|
+
return lang;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isChinese() {
|
|
159
|
+
return lang === 'zh';
|
|
160
|
+
}
|
package/src/index.js
ADDED
package/src/installer.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Installer
|
|
3
|
+
* Copies skill files to target location
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Install skill by copying to target directory
|
|
11
|
+
* @param {string} srcPath - Source skill directory
|
|
12
|
+
* @param {string} targetDir - Target installation directory
|
|
13
|
+
*/
|
|
14
|
+
export async function installSkill(srcPath, targetDir) {
|
|
15
|
+
await copyDir(srcPath, targetDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copy directory recursively
|
|
20
|
+
* @param {string} src - Source directory
|
|
21
|
+
* @param {string} dest - Destination directory
|
|
22
|
+
*/
|
|
23
|
+
async function copyDir(src, dest) {
|
|
24
|
+
await fs.mkdir(dest, { recursive: true });
|
|
25
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const srcPath = path.join(src, entry.name);
|
|
29
|
+
const destPath = path.join(dest, entry.name);
|
|
30
|
+
|
|
31
|
+
// Skip node_modules and hidden files
|
|
32
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
33
|
+
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
await copyDir(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
await fs.copyFile(srcPath, destPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Scanner
|
|
3
|
+
* Scans a directory for installable skills (folders with SKILL.md)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scan directory for skills
|
|
11
|
+
* @param {string} dir - Directory to scan
|
|
12
|
+
* @returns {Promise<Array<{name: string, path: string, description: string}>>}
|
|
13
|
+
*/
|
|
14
|
+
export async function scanSkills(dir) {
|
|
15
|
+
const skills = [];
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
19
|
+
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (!entry.isDirectory()) continue;
|
|
22
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
23
|
+
|
|
24
|
+
const skillPath = path.join(dir, entry.name);
|
|
25
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(skillFile, 'utf-8');
|
|
29
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
30
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
31
|
+
|
|
32
|
+
if (nameMatch) {
|
|
33
|
+
skills.push({
|
|
34
|
+
name: nameMatch[1].trim(),
|
|
35
|
+
path: skillPath,
|
|
36
|
+
description: descMatch ? descMatch[1].trim() : ''
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// No SKILL.md or parse error, skip
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Directory read error
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return skills;
|
|
48
|
+
}
|