@haolin-ai/skillman 1.0.6 → 1.0.7
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/package.json +2 -2
- package/src/cli.js +289 -50
- package/src/i18n.js +36 -0
- package/src/installer.js +21 -1
- package/src/scanner.js +11 -1
- package/src/version.js +215 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haolin-ai/skillman",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A CLI tool to install AI agent skills across multiple platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"skillman": "
|
|
7
|
+
"skillman": "bin/skillman.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/cli.js
CHANGED
|
@@ -13,10 +13,21 @@ import { loadAgents } from './config.js';
|
|
|
13
13
|
import { t } from './i18n.js';
|
|
14
14
|
import { loadHistory, addWorkspace, saveLastUsed } from './history.js';
|
|
15
15
|
import { downloadSkill, parseUrl, cleanupDownloads } from './downloader.js';
|
|
16
|
+
import { InstalledSkillRegistry, formatInstalledSkills, uninstallSkill, updateSkill } from './version.js';
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const VERSION = pkg.version || '1.0.0';
|
|
19
20
|
|
|
21
|
+
// ASCII Art Logo
|
|
22
|
+
const LOGO = `
|
|
23
|
+
███████╗██╗ ██╗██╗██╗ ██╗ ███╗ ███╗ █████╗ ███╗ ██╗
|
|
24
|
+
██╔════╝██║ ██╔╝██║██║ ██║ ████╗ ████║██╔══██╗████╗ ██║
|
|
25
|
+
███████╗█████╔╝ ██║██║ ██║ ██╔████╔██║███████║██╔██╗ ██║
|
|
26
|
+
╚════██║██╔═██╗ ██║██║ ██║ ██║╚██╔╝██║██╔══██║██║╚██╗██║
|
|
27
|
+
███████║██║ ██╗██║███████╗███████╗██║ ╚═╝ ██║██║ ██║██║ ╚████║
|
|
28
|
+
╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
29
|
+
`;
|
|
30
|
+
|
|
20
31
|
// ANSI colors
|
|
21
32
|
const c = {
|
|
22
33
|
reset: '\x1b[0m',
|
|
@@ -41,22 +52,51 @@ const log = {
|
|
|
41
52
|
function parseArgs(args) {
|
|
42
53
|
const result = {
|
|
43
54
|
command: null,
|
|
55
|
+
subcommand: null,
|
|
44
56
|
dryRun: false,
|
|
45
57
|
help: false,
|
|
46
58
|
version: false,
|
|
59
|
+
initVersion: '1.0.0',
|
|
60
|
+
initDescription: '',
|
|
61
|
+
initAuthor: '',
|
|
62
|
+
initDir: true,
|
|
47
63
|
positional: []
|
|
48
64
|
};
|
|
49
65
|
|
|
50
|
-
for (
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
51
68
|
if (arg === '--dry-run' || arg === '-n') {
|
|
52
69
|
result.dryRun = true;
|
|
53
70
|
} else if (arg === '--help' || arg === '-h') {
|
|
54
71
|
result.help = true;
|
|
55
72
|
} else if (arg === '--version' || arg === '-v') {
|
|
56
|
-
|
|
73
|
+
// Check if next arg is a value (for init command)
|
|
74
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
75
|
+
result.initVersion = args[i + 1];
|
|
76
|
+
i++;
|
|
77
|
+
} else {
|
|
78
|
+
result.version = true;
|
|
79
|
+
}
|
|
80
|
+
} else if (arg === '--description' || arg === '-d') {
|
|
81
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
82
|
+
result.initDescription = args[i + 1];
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
} else if (arg === '--author' || arg === '-a') {
|
|
86
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
87
|
+
result.initAuthor = args[i + 1];
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
} else if (arg === '--dir') {
|
|
91
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
92
|
+
result.initDir = args[i + 1].toLowerCase() !== 'false';
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
57
95
|
} else if (!arg.startsWith('-')) {
|
|
58
96
|
if (!result.command) {
|
|
59
97
|
result.command = arg;
|
|
98
|
+
} else if (!result.subcommand) {
|
|
99
|
+
result.subcommand = arg;
|
|
60
100
|
} else {
|
|
61
101
|
result.positional.push(arg);
|
|
62
102
|
}
|
|
@@ -72,8 +112,13 @@ function showHelp() {
|
|
|
72
112
|
|
|
73
113
|
${c.cyan}${t('help.usage')}:${c.reset}
|
|
74
114
|
skillman ${t('help.cmd.interactive')}
|
|
115
|
+
skillman init [name] ${t('help.cmd.init')}
|
|
75
116
|
skillman install <path> ${t('help.cmd.install')}
|
|
76
117
|
skillman i <path> ${t('help.cmd.install')}
|
|
118
|
+
skillman list ${t('help.cmd.list')}
|
|
119
|
+
skillman update <skill> ${t('help.cmd.update')}
|
|
120
|
+
skillman u <skill> ${t('help.cmd.update')}
|
|
121
|
+
skillman uninstall <skill> ${t('help.cmd.uninstall')}
|
|
77
122
|
skillman agents ${t('help.cmd.agents')}
|
|
78
123
|
|
|
79
124
|
${c.cyan}${t('help.options')}:${c.reset}
|
|
@@ -83,9 +128,15 @@ ${c.cyan}${t('help.options')}:${c.reset}
|
|
|
83
128
|
|
|
84
129
|
${c.cyan}${t('help.examples')}:${c.reset}
|
|
85
130
|
skillman # ${t('help.cmd.interactive')}
|
|
131
|
+
skillman init # Create skill with default name
|
|
132
|
+
skillman init my-skill # Create skill with custom name
|
|
133
|
+
skillman init -v 2.0.0 # Create skill with specific version
|
|
86
134
|
skillman --dry-run # ${t('help.opt.dry_run')}
|
|
87
135
|
skillman install ./my-skill # ${t('help.cmd.install')}
|
|
88
136
|
skillman i github.com/owner/repo # ${t('help.cmd.install')}
|
|
137
|
+
skillman list # ${t('help.cmd.list')}
|
|
138
|
+
skillman update my-skill # ${t('help.cmd.update')}
|
|
139
|
+
skillman uninstall my-skill # ${t('help.cmd.uninstall')}
|
|
89
140
|
skillman agents # ${t('help.cmd.agents')}
|
|
90
141
|
`);
|
|
91
142
|
}
|
|
@@ -109,11 +160,75 @@ async function listAgents() {
|
|
|
109
160
|
}
|
|
110
161
|
}
|
|
111
162
|
|
|
112
|
-
//
|
|
163
|
+
// Initialize a new skill template
|
|
164
|
+
export async function initSkill(skillName, options) {
|
|
165
|
+
const name = skillName || 'my-skill';
|
|
166
|
+
const version = options.initVersion || '1.0.0';
|
|
167
|
+
const description = options.initDescription || '';
|
|
168
|
+
const author = options.initAuthor || '';
|
|
169
|
+
const createDir = options.initDir !== false;
|
|
170
|
+
|
|
171
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
172
|
+
log.step(t('msg.init_skill') || 'Initializing skill template');
|
|
173
|
+
|
|
174
|
+
const targetDir = createDir ? path.join(process.cwd(), name) : process.cwd();
|
|
175
|
+
const skillFile = path.join(targetDir, 'SKILL.md');
|
|
176
|
+
|
|
177
|
+
// Check if already exists
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(skillFile);
|
|
180
|
+
log.error(t('error.skill_exists') || 'SKILL.md already exists');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
} catch {
|
|
183
|
+
// File doesn't exist, proceed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create directory if needed
|
|
187
|
+
if (createDir) {
|
|
188
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build metadata section
|
|
192
|
+
let metadataSection = `metadata:\n version: ${version}`;
|
|
193
|
+
if (author) {
|
|
194
|
+
metadataSection += `\n author: ${author}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Generate SKILL.md content
|
|
198
|
+
const skillContent = `---
|
|
199
|
+
name: ${name}
|
|
200
|
+
description: ${description}
|
|
201
|
+
${metadataSection}
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# ${name}
|
|
205
|
+
|
|
206
|
+
## Purpose
|
|
207
|
+
|
|
208
|
+
## Responsibilities
|
|
209
|
+
|
|
210
|
+
## Decision Rules
|
|
211
|
+
|
|
212
|
+
## Output Contract
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
await fs.writeFile(skillFile, skillContent);
|
|
216
|
+
|
|
217
|
+
log.success(`${t('msg.created') || 'Created'}: ${skillFile}`);
|
|
218
|
+
console.log(`\n${c.gray}${t('msg.init_hint') || 'Edit SKILL.md to customize your skill'}${c.reset}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Install from URL or local path with optional version
|
|
113
222
|
async function installFromUrl(url, dryRun) {
|
|
114
|
-
|
|
223
|
+
// Parse version from URL (e.g., skill@1.2.3 or github.com/owner/repo@1.2.3)
|
|
224
|
+
const versionMatch = url.match(/@([^@\/]+)$/);
|
|
225
|
+
const requestedVersion = versionMatch ? versionMatch[1] : null;
|
|
226
|
+
const cleanUrl = requestedVersion ? url.slice(0, -versionMatch[0].length) : url;
|
|
227
|
+
|
|
228
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
229
|
+
console.log(`${c.gray}${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
|
|
115
230
|
|
|
116
|
-
const parsed = parseUrl(
|
|
231
|
+
const parsed = parseUrl(cleanUrl);
|
|
117
232
|
const isRemote = parsed.type !== 'local';
|
|
118
233
|
|
|
119
234
|
// Step 1: Download/resolve path
|
|
@@ -142,35 +257,22 @@ async function installFromUrl(url, dryRun) {
|
|
|
142
257
|
|
|
143
258
|
log.success(t('msg.found_skills', { count: skills.length }));
|
|
144
259
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
|
-
selectedSkills = await checkbox({
|
|
159
|
-
message: t('step.select_skills') + ':',
|
|
160
|
-
choices: skillChoices,
|
|
161
|
-
pageSize: 10,
|
|
162
|
-
loop: false,
|
|
163
|
-
validate: (selected) => {
|
|
164
|
-
if (selected.length === 0) {
|
|
165
|
-
return t('error.no_selection') || 'Please select at least one skill';
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
log.success(`${t('msg.selected_count', { count: selectedSkills.length })}`);
|
|
260
|
+
// Check version if specified
|
|
261
|
+
if (requestedVersion) {
|
|
262
|
+
const matchingSkills = skills.filter(s => s.version === requestedVersion);
|
|
263
|
+
if (matchingSkills.length === 0) {
|
|
264
|
+
const availableVersions = [...new Set(skills.map(s => s.version).filter(Boolean))];
|
|
265
|
+
log.error(`Version ${requestedVersion} not found. Available versions: ${availableVersions.join(', ') || 'none'}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
skills.length = 0;
|
|
269
|
+
skills.push(...matchingSkills);
|
|
270
|
+
log.success(`Found ${skills.length} skill(s) matching version ${requestedVersion}`);
|
|
172
271
|
}
|
|
173
272
|
|
|
273
|
+
// Step 3: Select skills
|
|
274
|
+
const selectedSkills = await selectSkills(skills);
|
|
275
|
+
|
|
174
276
|
// Continue with rest of interactive flow
|
|
175
277
|
await continueInstallMultiple(selectedSkills, dryRun);
|
|
176
278
|
|
|
@@ -341,7 +443,12 @@ async function continueInstallMultiple(selectedSkills, dryRun) {
|
|
|
341
443
|
}
|
|
342
444
|
|
|
343
445
|
if (shouldInstall) {
|
|
344
|
-
await installSkill(skill.path, targetDir
|
|
446
|
+
await installSkill(skill.path, targetDir, {
|
|
447
|
+
name: skill.name,
|
|
448
|
+
version: skill.version,
|
|
449
|
+
agent: agent.name,
|
|
450
|
+
scope: scope
|
|
451
|
+
});
|
|
345
452
|
log.success(`${t('msg.target')}: ${targetDir}`);
|
|
346
453
|
installedCount++;
|
|
347
454
|
}
|
|
@@ -358,9 +465,133 @@ async function continueInstallMultiple(selectedSkills, dryRun) {
|
|
|
358
465
|
console.log();
|
|
359
466
|
}
|
|
360
467
|
|
|
468
|
+
// Helper: Select skills from list
|
|
469
|
+
async function selectSkills(skills, message = null) {
|
|
470
|
+
if (skills.length === 1) {
|
|
471
|
+
log.success(`${t('msg.selected')}: ${skills[0].name}`);
|
|
472
|
+
return [skills[0]];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const skillChoices = skills.map(s => {
|
|
476
|
+
const versionStr = s.version ? `@${s.version}` : '';
|
|
477
|
+
const descStr = s.description
|
|
478
|
+
? ` ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
|
|
479
|
+
: '';
|
|
480
|
+
return {
|
|
481
|
+
name: `${s.name}${versionStr}${descStr}`,
|
|
482
|
+
value: s
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const selectedSkills = await checkbox({
|
|
487
|
+
message: (message || t('step.select_skills')) + ':',
|
|
488
|
+
choices: skillChoices,
|
|
489
|
+
pageSize: 10,
|
|
490
|
+
loop: false,
|
|
491
|
+
validate: (selected) => {
|
|
492
|
+
if (selected.length === 0) {
|
|
493
|
+
return t('error.no_selection') || 'Please select at least one skill';
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
log.success(`${t('msg.selected_count', { count: selectedSkills.length })}`);
|
|
500
|
+
return selectedSkills;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// List installed skills
|
|
504
|
+
async function listSkills() {
|
|
505
|
+
const registry = new InstalledSkillRegistry();
|
|
506
|
+
const skills = await registry.load();
|
|
507
|
+
|
|
508
|
+
console.log(`\n${c.cyan}${t('msg.installed_skills')}:${c.reset}\n`);
|
|
509
|
+
|
|
510
|
+
const lines = formatInstalledSkills(skills, t);
|
|
511
|
+
for (const line of lines) {
|
|
512
|
+
console.log(line);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Uninstall a skill
|
|
517
|
+
async function uninstallCommand(skillName, dryRun) {
|
|
518
|
+
if (!skillName) {
|
|
519
|
+
log.error(t('error.no_skill_name') || 'Please specify a skill name');
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const registry = new InstalledSkillRegistry();
|
|
524
|
+
const skill = await registry.find(skillName);
|
|
525
|
+
|
|
526
|
+
if (!skill) {
|
|
527
|
+
log.error(t('error.skill_not_installed'));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (dryRun) {
|
|
532
|
+
log.dry(`${t('msg.uninstalling') || 'Would uninstall'}: ${skillName}`);
|
|
533
|
+
log.dry(` ${t('msg.target')}: ${skill.targetPath}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const confirmed = await confirm({
|
|
538
|
+
message: `${t('prompt.uninstall')} ${skillName}?`,
|
|
539
|
+
default: false
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
if (!confirmed) {
|
|
543
|
+
log.info(t('msg.cancelled') || 'Cancelled');
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const success = await uninstallSkill(skillName, registry);
|
|
548
|
+
|
|
549
|
+
if (success) {
|
|
550
|
+
log.success(`${t('msg.uninstalled')}: ${skillName}`);
|
|
551
|
+
} else {
|
|
552
|
+
log.error(t('error.unknown'));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Update a skill
|
|
558
|
+
async function updateCommand(skillName, dryRun) {
|
|
559
|
+
if (!skillName) {
|
|
560
|
+
log.error(t('error.no_skill_name') || 'Please specify a skill name');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const registry = new InstalledSkillRegistry();
|
|
565
|
+
const skill = await registry.find(skillName);
|
|
566
|
+
|
|
567
|
+
if (!skill) {
|
|
568
|
+
log.error(t('error.skill_not_installed'));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (dryRun) {
|
|
573
|
+
log.dry(`${t('msg.updating') || 'Would update'}: ${skillName}`);
|
|
574
|
+
log.dry(` ${t('msg.source')}: ${skill.sourcePath}`);
|
|
575
|
+
log.dry(` ${t('msg.target')}: ${skill.targetPath}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
log.step(`${t('msg.updating') || 'Updating'}: ${skillName}`);
|
|
580
|
+
|
|
581
|
+
const result = await updateSkill(skillName, registry);
|
|
582
|
+
|
|
583
|
+
if (result.success) {
|
|
584
|
+
log.success(`${t('msg.updated')}: ${skillName}`);
|
|
585
|
+
} else {
|
|
586
|
+
log.error(result.message);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
361
591
|
// Interactive install flow
|
|
362
592
|
async function interactiveInstall(dryRun) {
|
|
363
|
-
console.log(`${c.
|
|
593
|
+
console.log(`${c.cyan}${LOGO}${c.reset}`);
|
|
594
|
+
console.log(`${c.gray}${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
|
|
364
595
|
|
|
365
596
|
// Step 1: Scan skills
|
|
366
597
|
log.step(t('step.scan'));
|
|
@@ -373,24 +604,11 @@ async function interactiveInstall(dryRun) {
|
|
|
373
604
|
|
|
374
605
|
log.success(t('msg.found_skills', { count: skills.length }));
|
|
375
606
|
|
|
376
|
-
// Step 2: Select
|
|
377
|
-
const
|
|
378
|
-
name: s.description
|
|
379
|
-
? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
|
|
380
|
-
: s.name,
|
|
381
|
-
value: s
|
|
382
|
-
}));
|
|
383
|
-
|
|
384
|
-
const selectedSkill = await select({
|
|
385
|
-
message: t('step.select_skill') + ':',
|
|
386
|
-
choices: skillChoices,
|
|
387
|
-
pageSize: 10
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
log.success(`${t('msg.selected')}: ${selectedSkill.name}`);
|
|
607
|
+
// Step 2: Select skills
|
|
608
|
+
const selectedSkills = await selectSkills(skills);
|
|
391
609
|
|
|
392
610
|
// Continue with agent selection and installation
|
|
393
|
-
await
|
|
611
|
+
await continueInstallMultiple(selectedSkills, dryRun);
|
|
394
612
|
}
|
|
395
613
|
|
|
396
614
|
// Main CLI function
|
|
@@ -413,12 +631,33 @@ export async function cli() {
|
|
|
413
631
|
return;
|
|
414
632
|
}
|
|
415
633
|
|
|
634
|
+
if (options.command === 'init') {
|
|
635
|
+
const skillName = options.subcommand || options.positional[0];
|
|
636
|
+
await initSkill(skillName, options);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
416
640
|
if (options.command === 'install' || options.command === 'i') {
|
|
417
641
|
const url = options.positional[0] || process.cwd();
|
|
418
642
|
await installFromUrl(url, options.dryRun);
|
|
419
643
|
return;
|
|
420
644
|
}
|
|
421
645
|
|
|
646
|
+
if (options.command === 'list') {
|
|
647
|
+
await listSkills();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (options.command === 'uninstall') {
|
|
652
|
+
await uninstallCommand(options.subcommand, options.dryRun);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (options.command === 'update' || options.command === 'u') {
|
|
657
|
+
await updateCommand(options.subcommand, options.dryRun);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
422
661
|
// Default: interactive install
|
|
423
662
|
await interactiveInstall(options.dryRun);
|
|
424
663
|
}
|
package/src/i18n.js
CHANGED
|
@@ -64,12 +64,24 @@ const translations = {
|
|
|
64
64
|
'msg.installing': '正在安装',
|
|
65
65
|
'msg.skipped': '已跳过',
|
|
66
66
|
'msg.installed': '已安装',
|
|
67
|
+
'msg.no_installed_skills': '没有已安装的技能',
|
|
68
|
+
'msg.installed_skills': '已安装的技能',
|
|
69
|
+
'msg.skill_not_found': '未找到技能',
|
|
70
|
+
'msg.uninstalled': '已卸载',
|
|
71
|
+
'msg.updated': '已更新',
|
|
72
|
+
'msg.updating': '正在更新',
|
|
73
|
+
'msg.uninstalling': '正在卸载',
|
|
74
|
+
'msg.cancelled': '已取消',
|
|
75
|
+
'msg.init_skill': '初始化 Skill 模板',
|
|
76
|
+
'msg.created': '已创建',
|
|
77
|
+
'msg.init_hint': '编辑 SKILL.md 来自定义你的 skill',
|
|
67
78
|
|
|
68
79
|
// Prompts
|
|
69
80
|
'prompt.workspace_path': '输入 Workspace 路径',
|
|
70
81
|
'prompt.overwrite': '是否覆盖',
|
|
71
82
|
'prompt.select_workspace': '选择 Workspace 路径',
|
|
72
83
|
'prompt.new_path': '输入新路径...',
|
|
84
|
+
'prompt.uninstall': '确认卸载',
|
|
73
85
|
|
|
74
86
|
// Options
|
|
75
87
|
'option.global': '全局',
|
|
@@ -80,13 +92,19 @@ const translations = {
|
|
|
80
92
|
'error.empty_path': '路径不能为空',
|
|
81
93
|
'error.not_implemented': '该命令尚未实现,请使用交互模式',
|
|
82
94
|
'error.no_selection': '请至少选择一个 skill',
|
|
95
|
+
'error.skill_not_installed': '技能未安装',
|
|
96
|
+
'error.no_skill_name': '请指定技能名称',
|
|
83
97
|
|
|
84
98
|
// Help
|
|
85
99
|
'help.usage': '用法',
|
|
86
100
|
'help.options': '选项',
|
|
87
101
|
'help.examples': '示例',
|
|
88
102
|
'help.cmd.interactive': '交互式安装 (扫描当前目录)',
|
|
103
|
+
'help.cmd.init': '初始化新的 skill 模板',
|
|
89
104
|
'help.cmd.install': '从指定路径安装 skill',
|
|
105
|
+
'help.cmd.list': '列出已安装的技能',
|
|
106
|
+
'help.cmd.update': '更新技能',
|
|
107
|
+
'help.cmd.uninstall': '卸载技能',
|
|
90
108
|
'help.cmd.agents': '列出可用的 agents',
|
|
91
109
|
'help.opt.dry_run': '预览安装而不执行更改',
|
|
92
110
|
'help.opt.version': '显示版本号',
|
|
@@ -132,12 +150,24 @@ const translations = {
|
|
|
132
150
|
'msg.installing': 'Installing',
|
|
133
151
|
'msg.skipped': 'Skipped',
|
|
134
152
|
'msg.installed': 'Installed',
|
|
153
|
+
'msg.no_installed_skills': 'No skills installed',
|
|
154
|
+
'msg.installed_skills': 'Installed Skills',
|
|
155
|
+
'msg.skill_not_found': 'Skill not found',
|
|
156
|
+
'msg.uninstalled': 'Uninstalled',
|
|
157
|
+
'msg.updated': 'Updated',
|
|
158
|
+
'msg.updating': 'Updating',
|
|
159
|
+
'msg.uninstalling': 'Uninstalling',
|
|
160
|
+
'msg.cancelled': 'Cancelled',
|
|
161
|
+
'msg.init_skill': 'Initializing skill template',
|
|
162
|
+
'msg.created': 'Created',
|
|
163
|
+
'msg.init_hint': 'Edit SKILL.md to customize your skill',
|
|
135
164
|
|
|
136
165
|
// Prompts
|
|
137
166
|
'prompt.workspace_path': 'Enter workspace path',
|
|
138
167
|
'prompt.overwrite': 'Overwrite existing',
|
|
139
168
|
'prompt.select_workspace': 'Select workspace path',
|
|
140
169
|
'prompt.new_path': 'Enter new path...',
|
|
170
|
+
'prompt.uninstall': 'Confirm uninstall',
|
|
141
171
|
|
|
142
172
|
// Options
|
|
143
173
|
'option.global': 'Global',
|
|
@@ -148,13 +178,19 @@ const translations = {
|
|
|
148
178
|
'error.empty_path': 'Path cannot be empty',
|
|
149
179
|
'error.not_implemented': 'Command not implemented, use interactive mode',
|
|
150
180
|
'error.no_selection': 'Please select at least one skill',
|
|
181
|
+
'error.skill_not_installed': 'Skill is not installed',
|
|
182
|
+
'error.no_skill_name': 'Please specify a skill name',
|
|
151
183
|
|
|
152
184
|
// Help
|
|
153
185
|
'help.usage': 'Usage',
|
|
154
186
|
'help.options': 'Options',
|
|
155
187
|
'help.examples': 'Examples',
|
|
156
188
|
'help.cmd.interactive': 'Interactive install (scan current directory)',
|
|
189
|
+
'help.cmd.init': 'Initialize new skill template',
|
|
157
190
|
'help.cmd.install': 'Install skill from path',
|
|
191
|
+
'help.cmd.list': 'List installed skills',
|
|
192
|
+
'help.cmd.update': 'Update a skill',
|
|
193
|
+
'help.cmd.uninstall': 'Uninstall a skill',
|
|
158
194
|
'help.cmd.agents': 'List available agents',
|
|
159
195
|
'help.opt.dry_run': 'Preview installation without making changes',
|
|
160
196
|
'help.opt.version': 'Show version number',
|
package/src/installer.js
CHANGED
|
@@ -5,14 +5,34 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import { InstalledSkillRegistry } from './version.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Install skill by copying to target directory
|
|
11
12
|
* @param {string} srcPath - Source skill directory
|
|
12
13
|
* @param {string} targetDir - Target installation directory
|
|
14
|
+
* @param {Object} metadata - Installation metadata
|
|
15
|
+
* @param {string} metadata.name - Skill name
|
|
16
|
+
* @param {string} metadata.version - Skill version
|
|
17
|
+
* @param {string} metadata.agent - Target agent name
|
|
18
|
+
* @param {string} metadata.scope - Installation scope (global|workspace)
|
|
13
19
|
*/
|
|
14
|
-
export async function installSkill(srcPath, targetDir) {
|
|
20
|
+
export async function installSkill(srcPath, targetDir, metadata = {}) {
|
|
15
21
|
await copyDir(srcPath, targetDir);
|
|
22
|
+
|
|
23
|
+
// Record installation if metadata provided
|
|
24
|
+
if (metadata.name) {
|
|
25
|
+
const registry = new InstalledSkillRegistry();
|
|
26
|
+
await registry.add({
|
|
27
|
+
name: metadata.name,
|
|
28
|
+
version: metadata.version,
|
|
29
|
+
installedAt: new Date().toISOString(),
|
|
30
|
+
agent: metadata.agent,
|
|
31
|
+
scope: metadata.scope,
|
|
32
|
+
sourcePath: srcPath,
|
|
33
|
+
targetPath: targetDir
|
|
34
|
+
});
|
|
35
|
+
}
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
/**
|
package/src/scanner.js
CHANGED
|
@@ -32,12 +32,22 @@ async function scanSingleDir(dir) {
|
|
|
32
32
|
const content = await fs.readFile(skillFile, 'utf-8');
|
|
33
33
|
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
34
34
|
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
35
|
+
// Parse version from metadata block
|
|
36
|
+
const metadataMatch = content.match(/metadata:[\s\S]*?(?=\n\w|$)/);
|
|
37
|
+
let version;
|
|
38
|
+
if (metadataMatch) {
|
|
39
|
+
const versionInMeta = metadataMatch[0].match(/^\s+version:\s*(.+)$/m);
|
|
40
|
+
if (versionInMeta) {
|
|
41
|
+
version = versionInMeta[1].trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
if (nameMatch) {
|
|
37
46
|
skills.push({
|
|
38
47
|
name: nameMatch[1].trim(),
|
|
39
48
|
path: skillPath,
|
|
40
|
-
description: descMatch ? descMatch[1].trim() : ''
|
|
49
|
+
description: descMatch ? descMatch[1].trim() : '',
|
|
50
|
+
version: version
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
} catch {
|
package/src/version.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Management Module
|
|
3
|
+
* Handles tracking, listing, updating, and uninstalling skills
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'skillman');
|
|
11
|
+
const INSTALLED_FILE = path.join(CONFIG_DIR, 'installed.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the path to the installed skills registry file
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function getInstalledSkillsPath() {
|
|
18
|
+
return INSTALLED_FILE;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registry for tracking installed skills
|
|
23
|
+
*/
|
|
24
|
+
export class InstalledSkillRegistry {
|
|
25
|
+
constructor(registryPath = INSTALLED_FILE) {
|
|
26
|
+
this.registryPath = registryPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load all installed skills from registry
|
|
31
|
+
* @returns {Promise<Array>}
|
|
32
|
+
*/
|
|
33
|
+
async load() {
|
|
34
|
+
try {
|
|
35
|
+
const content = await fs.readFile(this.registryPath, 'utf-8');
|
|
36
|
+
const data = JSON.parse(content);
|
|
37
|
+
return data.skills || [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Save skills to registry
|
|
45
|
+
* @param {Array} skills
|
|
46
|
+
*/
|
|
47
|
+
async save(skills) {
|
|
48
|
+
await fs.mkdir(path.dirname(this.registryPath), { recursive: true });
|
|
49
|
+
const data = {
|
|
50
|
+
version: 1,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
skills
|
|
53
|
+
};
|
|
54
|
+
await fs.writeFile(this.registryPath, JSON.stringify(data, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a skill to the registry
|
|
59
|
+
* @param {Object} skill
|
|
60
|
+
*/
|
|
61
|
+
async add(skill) {
|
|
62
|
+
const skills = await this.load();
|
|
63
|
+
const existingIndex = skills.findIndex(s => s.name === skill.name);
|
|
64
|
+
|
|
65
|
+
if (existingIndex >= 0) {
|
|
66
|
+
skills[existingIndex] = skill;
|
|
67
|
+
} else {
|
|
68
|
+
skills.push(skill);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await this.save(skills);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Remove a skill from the registry
|
|
76
|
+
* @param {string} skillName
|
|
77
|
+
*/
|
|
78
|
+
async remove(skillName) {
|
|
79
|
+
const skills = await this.load();
|
|
80
|
+
const filtered = skills.filter(s => s.name !== skillName);
|
|
81
|
+
await this.save(filtered);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find a skill by name
|
|
86
|
+
* @param {string} skillName
|
|
87
|
+
* @returns {Promise<Object|undefined>}
|
|
88
|
+
*/
|
|
89
|
+
async find(skillName) {
|
|
90
|
+
const skills = await this.load();
|
|
91
|
+
return skills.find(s => s.name === skillName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format installed skills for display
|
|
97
|
+
* @param {Array} skills
|
|
98
|
+
* @param {Function} t - Translation function
|
|
99
|
+
* @returns {Array} Formatted lines
|
|
100
|
+
*/
|
|
101
|
+
export function formatInstalledSkills(skills, t = (key) => key) {
|
|
102
|
+
if (skills.length === 0) {
|
|
103
|
+
return [t('msg.no_installed_skills')];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lines = [];
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
// Group by agent
|
|
110
|
+
const byAgent = skills.reduce((acc, skill) => {
|
|
111
|
+
const agent = skill.agent || 'unknown';
|
|
112
|
+
if (!acc[agent]) acc[agent] = [];
|
|
113
|
+
acc[agent].push(skill);
|
|
114
|
+
return acc;
|
|
115
|
+
}, {});
|
|
116
|
+
|
|
117
|
+
for (const [agent, agentSkills] of Object.entries(byAgent)) {
|
|
118
|
+
lines.push(`${agent}:`);
|
|
119
|
+
for (const skill of agentSkills) {
|
|
120
|
+
const scope = skill.scope === 'global' ? 'G' : 'W';
|
|
121
|
+
const version = skill.version || 'unknown';
|
|
122
|
+
lines.push(` ${skill.name}@${version} [${scope}]`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Uninstall a skill
|
|
132
|
+
* @param {string} skillName
|
|
133
|
+
* @param {InstalledSkillRegistry} registry
|
|
134
|
+
* @returns {Promise<boolean>} - true if uninstalled, false if not found
|
|
135
|
+
*/
|
|
136
|
+
export async function uninstallSkill(skillName, registry = new InstalledSkillRegistry()) {
|
|
137
|
+
const skill = await registry.find(skillName);
|
|
138
|
+
|
|
139
|
+
if (!skill) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove from filesystem
|
|
144
|
+
if (skill.targetPath) {
|
|
145
|
+
try {
|
|
146
|
+
await fs.rm(skill.targetPath, { recursive: true, force: true });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Log but continue - we still want to remove from registry
|
|
149
|
+
console.warn(`Warning: Could not remove skill directory: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Remove from registry
|
|
154
|
+
await registry.remove(skillName);
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Update a skill by reinstalling from source
|
|
161
|
+
* @param {string} skillName
|
|
162
|
+
* @param {InstalledSkillRegistry} registry
|
|
163
|
+
* @returns {Promise<{success: boolean, message: string}>}
|
|
164
|
+
*/
|
|
165
|
+
export async function updateSkill(skillName, registry = new InstalledSkillRegistry()) {
|
|
166
|
+
const skill = await registry.find(skillName);
|
|
167
|
+
|
|
168
|
+
if (!skill) {
|
|
169
|
+
return { success: false, message: 'Skill not installed' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!skill.sourcePath) {
|
|
173
|
+
return { success: false, message: 'Cannot update: source path not recorded' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await fs.access(skill.sourcePath);
|
|
178
|
+
} catch {
|
|
179
|
+
return { success: false, message: 'Source no longer available' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Reinstall
|
|
183
|
+
await copyDir(skill.sourcePath, skill.targetPath);
|
|
184
|
+
|
|
185
|
+
// Update registry with new timestamp
|
|
186
|
+
await registry.add({
|
|
187
|
+
...skill,
|
|
188
|
+
updatedAt: new Date().toISOString()
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return { success: true, message: 'Updated successfully' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Copy directory recursively
|
|
196
|
+
* @param {string} src - Source directory
|
|
197
|
+
* @param {string} dest - Destination directory
|
|
198
|
+
*/
|
|
199
|
+
async function copyDir(src, dest) {
|
|
200
|
+
await fs.mkdir(dest, { recursive: true });
|
|
201
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
202
|
+
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const srcPath = path.join(src, entry.name);
|
|
205
|
+
const destPath = path.join(dest, entry.name);
|
|
206
|
+
|
|
207
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
208
|
+
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
await copyDir(srcPath, destPath);
|
|
211
|
+
} else {
|
|
212
|
+
await fs.copyFile(srcPath, destPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|