@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@haolin-ai/skillman",
3
- "version": "1.0.6",
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": "./bin/skillman.mjs"
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 (const arg of args) {
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
- result.version = true;
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
- // Install from URL or local path
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
- console.log(`${c.green}${t('app.name')}${c.reset} - ${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
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(url);
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
- // Step 3: Select skills (if multiple)
146
- let selectedSkills;
147
- if (skills.length === 1) {
148
- selectedSkills = [skills[0]];
149
- log.success(`${t('msg.selected')}: ${skills[0].name}`);
150
- } else {
151
- const skillChoices = skills.map(s => ({
152
- name: s.description
153
- ? `${s.name} ${c.gray}(${s.description.slice(0, 40)}${s.description.length > 40 ? '...' : ''})${c.reset}`
154
- : s.name,
155
- value: s
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.green}${t('app.name')}${c.reset} - ${t('app.description')}${dryRun ? c.yellow + ' [DRY-RUN]' + c.reset : ''}\n`);
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 skill
377
- const skillChoices = skills.map(s => ({
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 continueInstall(selectedSkill, dryRun);
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
+ }