@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.
@@ -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
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Skillman Core Module
3
+ */
4
+
5
+ export { scanSkills } from './scanner.js';
6
+ export { installSkill } from './installer.js';
7
+ export { loadAgents, getAgent } from './config.js';
@@ -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
+ }