eskill 1.0.20 → 1.0.23

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.
Files changed (3) hide show
  1. package/cli.js +41 -3
  2. package/lib/installer.js +318 -29
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import ora from 'ora';
4
- import { installFromGitUrl, listSkills, removeSkill, cleanupAllSkills, cleanupAll } from './lib/installer.js';
4
+ import { installFromGitUrl, listSkills, removeSkill, cleanupAllSkills, cleanupAll, updateSkill, updateAllSkills } from './lib/installer.js';
5
5
  import { AGENTS, getDefaultAgent } from './lib/agent-config.js';
6
6
  import { bashCompletionScript, zshCompletionScript, listSkillsForCompletion } from './lib/completion.js';
7
7
  import { searchSkills, formatSkillList } from './lib/search.js';
@@ -13,7 +13,7 @@ const program = new Command();
13
13
  program
14
14
  .name('eskill')
15
15
  .description('Unified AI Agent Skills Management - Install skills from Git URLs')
16
- .version('1.0.11');
16
+ .version('1.0.23');
17
17
 
18
18
  // 安装命令
19
19
  program
@@ -59,7 +59,13 @@ program
59
59
  console.log(`\n已安装的技能:\n`);
60
60
  skills.forEach(skill => {
61
61
  const displayName = skill.author ? `${skill.name}@${skill.author}` : skill.name;
62
- console.log(` • ${displayName}`);
62
+ const versionInfo = skill.version || (skill.commitHash ? `#${skill.commitHash.substring(0, 7)}` : '');
63
+ const localMark = !skill.gitUrl ? ' [本地]' : '';
64
+
65
+ console.log(` • ${displayName}${localMark}`);
66
+ if (versionInfo) {
67
+ console.log(` 版本: ${versionInfo}`);
68
+ }
63
69
  });
64
70
  console.log(`\n总计: ${skills.length} 个技能\n`);
65
71
  } catch (error) {
@@ -85,6 +91,38 @@ program
85
91
  }
86
92
  });
87
93
 
94
+ // 更新命令
95
+ program
96
+ .command('update')
97
+ .description('更新技能')
98
+ .argument('[name]', '技能名称(不指定则更新所有)')
99
+ .option('-f, --force', '强制更新(即使版本相同)', false)
100
+ .action(async (name, options) => {
101
+ try {
102
+ if (name) {
103
+ // 更新单个技能
104
+ console.log(`\n检查技能: ${name}`);
105
+ const result = await updateSkill(name, options);
106
+
107
+ if (result.local) {
108
+ console.log('\n⚠️ 本地技能无法自动更新,请手动更新\n');
109
+ } else if (result.updated === false) {
110
+ console.log(`\n✓ 已是最新版本 (${result.currentVersion})\n`);
111
+ } else if (result.updated) {
112
+ console.log(`\n✓ 更新成功:`);
113
+ console.log(` 旧版本: ${result.currentVersion}`);
114
+ console.log(` 新版本: ${result.newVersion}\n`);
115
+ }
116
+ } else {
117
+ // 更新所有技能
118
+ await updateAllSkills(options);
119
+ }
120
+ } catch (error) {
121
+ console.error(`\n❌ 更新失败: ${error.message}\n`);
122
+ process.exit(1);
123
+ }
124
+ });
125
+
88
126
  // 搜索命令
89
127
  program
90
128
  .command('search')
package/lib/installer.js CHANGED
@@ -1,13 +1,52 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { existsSync, mkdirSync, symlinkSync, rmSync, readdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
3
- import { join, basename, dirname, homedir } from 'path';
4
- import { tmpdir } from 'os';
3
+ import { join, basename, dirname } from 'path';
4
+ import { tmpdir, homedir } from 'os';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { parseGitUrl } from './git-url-parser.js';
7
7
  import { AGENTS, expandHomePath } from './agent-config.js';
8
8
  import { searchSkills, formatSkillList } from './search.js';
9
9
  import readline from 'readline';
10
10
 
11
+ /**
12
+ * 获取 Git 仓库的当前 commit hash
13
+ */
14
+ function getGitCommitHash(repoPath) {
15
+ try {
16
+ return execSync('git rev-parse HEAD', { cwd: repoPath, encoding: 'utf-8' }).trim();
17
+ } catch (error) {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 获取 Git 仓库的当前版本(tag 或 branch)
24
+ */
25
+ function getGitVersion(repoPath) {
26
+ try {
27
+ // 尝试获取最新的 tag
28
+ const tag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', {
29
+ cwd: repoPath,
30
+ encoding: 'utf-8',
31
+ shell: '/bin/bash'
32
+ }).trim();
33
+
34
+ if (tag) {
35
+ return tag;
36
+ }
37
+
38
+ // 如果没有 tag,返回 branch
39
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
40
+ cwd: repoPath,
41
+ encoding: 'utf-8'
42
+ }).trim();
43
+
44
+ return branch;
45
+ } catch (error) {
46
+ return 'unknown';
47
+ }
48
+ }
49
+
11
50
  /**
12
51
  * 获取 eskill 包的安装目录
13
52
  * 技能安装在这里,卸载时会自动删除
@@ -79,29 +118,73 @@ function migrateOldSkills() {
79
118
  }
80
119
 
81
120
  /**
82
- * 保存技能元数据
121
+ * 读取所有技能元数据
83
122
  */
84
- function saveSkillMeta(skillPath, meta) {
85
- const metaPath = join(skillPath, '.eskill-meta.json');
86
- writeFileSync(metaPath, JSON.stringify(meta, null, 2));
87
- }
123
+ function getAllSkillsMeta() {
124
+ const skillDir = getSkillsStorageDir();
125
+ const metaPath = join(skillDir, '.eskill-meta.json');
88
126
 
89
- /**
90
- * 读取技能元数据
91
- */
92
- function getSkillMeta(skillPath) {
93
- const metaPath = join(skillPath, '.eskill-meta.json');
94
127
  if (!existsSync(metaPath)) {
95
- return null;
128
+ return [];
96
129
  }
130
+
97
131
  try {
98
132
  const content = readFileSync(metaPath, 'utf-8');
99
133
  return JSON.parse(content);
100
134
  } catch (error) {
101
- return null;
135
+ return [];
102
136
  }
103
137
  }
104
138
 
139
+ /**
140
+ * 保存所有技能元数据
141
+ */
142
+ function saveAllSkillsMeta(metaArray) {
143
+ const skillDir = getSkillsStorageDir();
144
+
145
+ // 确保目录存在
146
+ if (!existsSync(skillDir)) {
147
+ mkdirSync(skillDir, { recursive: true });
148
+ }
149
+
150
+ const metaPath = join(skillDir, '.eskill-meta.json');
151
+ writeFileSync(metaPath, JSON.stringify(metaArray, null, 2));
152
+ }
153
+
154
+ /**
155
+ * 保存单个技能元数据
156
+ */
157
+ function saveSkillMeta(skillName, meta) {
158
+ const allMeta = getAllSkillsMeta();
159
+
160
+ // 查找并更新或添加
161
+ const index = allMeta.findIndex(m => m.name === skillName);
162
+ if (index >= 0) {
163
+ allMeta[index] = { ...allMeta[index], ...meta };
164
+ } else {
165
+ allMeta.push(meta);
166
+ }
167
+
168
+ saveAllSkillsMeta(allMeta);
169
+ }
170
+
171
+ /**
172
+ * 读取单个技能元数据
173
+ */
174
+ function getSkillMeta(skillName) {
175
+ const allMeta = getAllSkillsMeta();
176
+ return allMeta.find(m => m.name === skillName) || null;
177
+ }
178
+
179
+ /**
180
+ * 删除技能元数据
181
+ */
182
+ function removeSkillMeta(skillName) {
183
+ const allMeta = getAllSkillsMeta();
184
+ const newMeta = allMeta.filter(m => m.name !== skillName);
185
+ saveAllSkillsMeta(newMeta);
186
+ }
187
+
105
188
  /**
106
189
  * 检测并处理 name@author 格式
107
190
  * 返回解析后的 GitHub URL,如果不是该格式则返回原 URL
@@ -275,15 +358,22 @@ export async function installFromGitUrl(gitUrl, options = {}) {
275
358
  console.log(` 存储位置: ~/.eskill/skills/`);
276
359
  console.log(` 说明: 技能永久保存,更新 eskill 不会丢失`);
277
360
 
278
- // 保存技能元数据(作者、Git URL 等)
279
- saveSkillMeta(targetPath, {
361
+ // 获取 git 版本信息
362
+ const commitHash = getGitCommitHash(sourcePath);
363
+ const version = getGitVersion(sourcePath);
364
+
365
+ // 保存技能元数据(作者、Git URL、版本等)
366
+ saveSkillMeta(skillName, {
280
367
  name: skillName,
281
368
  author: parsed.owner,
282
369
  gitUrl: gitUrl,
283
370
  platform: parsed.platform,
284
371
  repo: parsed.repo,
285
372
  branch: parsed.branch,
286
- installedAt: new Date().toISOString()
373
+ commitHash,
374
+ version,
375
+ installedAt: new Date().toISOString(),
376
+ updatedAt: new Date().toISOString()
287
377
  });
288
378
 
289
379
  return { success: true, path: targetPath };
@@ -310,18 +400,25 @@ export function listSkills(agent = 'claude') {
310
400
  }
311
401
 
312
402
  // 读取目录中的技能
313
- return readdirSync(skillDir, { withFileTypes: true })
314
- .filter(dirent => dirent.isDirectory())
315
- .map(dirent => {
316
- const skillPath = join(skillDir, dirent.name);
317
- const meta = getSkillMeta(skillPath);
318
- return {
319
- name: dirent.name,
320
- path: skillPath,
321
- author: meta?.author || null,
322
- installedAt: meta?.installedAt || null
323
- };
324
- });
403
+ const skillDirs = readdirSync(skillDir, { withFileTypes: true })
404
+ .filter(dirent => dirent.isDirectory());
405
+
406
+ // 读取所有元数据
407
+ const allMeta = getAllSkillsMeta();
408
+
409
+ return skillDirs.map(dirent => {
410
+ const skillPath = join(skillDir, dirent.name);
411
+ const meta = allMeta.find(m => m.name === dirent.name);
412
+ return {
413
+ name: dirent.name,
414
+ path: skillPath,
415
+ author: meta?.author || null,
416
+ version: meta?.version || null,
417
+ commitHash: meta?.commitHash || null,
418
+ installedAt: meta?.installedAt || null,
419
+ gitUrl: meta?.gitUrl || null
420
+ };
421
+ });
325
422
  }
326
423
 
327
424
  /**
@@ -336,6 +433,7 @@ export function removeSkill(skillName, agent = 'claude') {
336
433
  }
337
434
 
338
435
  rmSync(targetPath, { recursive: true, force: true });
436
+ removeSkillMeta(skillName);
339
437
  console.log(`✓ 已删除技能: ${skillName}`);
340
438
  }
341
439
 
@@ -385,3 +483,194 @@ export function cleanupAll() {
385
483
  export function getSkillsDir() {
386
484
  return getSkillsStorageDir();
387
485
  }
486
+
487
+ /**
488
+ * 更新单个技能
489
+ */
490
+ export async function updateSkill(skillName, options = {}) {
491
+ const { force = false } = options;
492
+ const skillDir = getSkillsStorageDir();
493
+ const targetPath = join(skillDir, skillName);
494
+
495
+ // 检查技能是否存在
496
+ if (!existsSync(targetPath)) {
497
+ throw new Error(`技能不存在: ${skillName}`);
498
+ }
499
+
500
+ // 读取元数据
501
+ const meta = getSkillMeta(skillName);
502
+
503
+ // 如果没有 gitUrl,说明是本地技能,无法更新
504
+ if (!meta || !meta.gitUrl) {
505
+ return { success: false, local: true, skill: skillName };
506
+ }
507
+
508
+ const currentVersion = meta.version || meta.commitHash?.substring(0, 7) || 'unknown';
509
+
510
+ // 重新安装技能(使用内部的 installFromGitUrl,但不打印)
511
+ try {
512
+ const parsed = parseGitUrl(meta.gitUrl);
513
+
514
+ // 确定技能名称
515
+ const skillNameFromUrl = parsed.path ? basename(parsed.path) : parsed.repo;
516
+
517
+ // 创建临时目录
518
+ const tempDir = join(tmpdir(), `eskill-update-${Date.now()}`);
519
+
520
+ try {
521
+ // 克隆仓库(静默模式)
522
+ const cloneUrl = parsed.cloneUrl;
523
+ execSync(
524
+ `git clone --depth 1 --branch ${parsed.branch} --single-branch "${cloneUrl}" "${tempDir}"`,
525
+ { stdio: 'inherit' }
526
+ );
527
+
528
+ // 确定源路径
529
+ const sourcePath = parsed.path ? join(tempDir, parsed.path) : tempDir;
530
+
531
+ // 验证源路径存在
532
+ if (!existsSync(sourcePath)) {
533
+ throw new Error(`路径不存在: ${sourcePath}`);
534
+ }
535
+
536
+ // 检查 SKILL.md 是否存在
537
+ const skillManifestPath = join(sourcePath, 'SKILL.md');
538
+ if (!existsSync(skillManifestPath)) {
539
+ throw new Error(`无效的技能包:未找到 SKILL.md 文件`);
540
+ }
541
+
542
+ // 获取新版本信息
543
+ const newCommitHash = getGitCommitHash(sourcePath);
544
+ const newVersion = getGitVersion(sourcePath);
545
+ const newVersionDisplay = newVersion || newCommitHash?.substring(0, 7) || 'unknown';
546
+
547
+ // 检查是否有更新
548
+ if (!force && newCommitHash === meta.commitHash) {
549
+ // 删除临时目录
550
+ rmSync(tempDir, { recursive: true, force: true });
551
+ return { success: true, skill: skillName, updated: false, currentVersion };
552
+ }
553
+
554
+ // 删除旧技能
555
+ rmSync(targetPath, { recursive: true, force: true });
556
+
557
+ // 复制新技能
558
+ if (process.platform === 'win32') {
559
+ execSync(`xcopy "${sourcePath}" "${targetPath}" /E /I /H /Y`, { stdio: 'inherit' });
560
+ } else {
561
+ execSync(`cp -r "${sourcePath}" "${targetPath}"`, { stdio: 'inherit' });
562
+ }
563
+
564
+ // 更新元数据
565
+ saveSkillMeta(skillName, {
566
+ ...meta,
567
+ commitHash: newCommitHash,
568
+ version: newVersion,
569
+ updatedAt: new Date().toISOString()
570
+ });
571
+
572
+ return {
573
+ success: true,
574
+ skill: skillName,
575
+ updated: true,
576
+ currentVersion,
577
+ newVersion: newVersionDisplay
578
+ };
579
+ } finally {
580
+ // 清理临时目录
581
+ if (existsSync(tempDir)) {
582
+ rmSync(tempDir, { recursive: true, force: true });
583
+ }
584
+ }
585
+ } catch (error) {
586
+ throw new Error(`更新失败: ${error.message}`);
587
+ }
588
+ }
589
+
590
+ /**
591
+ * 更新所有技能
592
+ */
593
+ export async function updateAllSkills(options = {}) {
594
+ const { force = false } = options;
595
+
596
+ // 自动迁移旧位置的技能
597
+ migrateOldSkills();
598
+
599
+ const skillDir = getSkillsStorageDir();
600
+
601
+ if (!existsSync(skillDir)) {
602
+ console.log('未安装任何技能');
603
+ return [];
604
+ }
605
+
606
+ // 读取所有技能
607
+ const skills = readdirSync(skillDir, { withFileTypes: true })
608
+ .filter(dirent => dirent.isDirectory())
609
+ .map(dirent => {
610
+ const skillPath = join(skillDir, dirent.name);
611
+ const meta = getSkillMeta(dirent.name);
612
+ return {
613
+ name: dirent.name,
614
+ path: skillPath,
615
+ meta
616
+ };
617
+ });
618
+
619
+ if (skills.length === 0) {
620
+ console.log('未安装任何技能');
621
+ return [];
622
+ }
623
+
624
+ console.log(`\n检查 ${skills.length} 个技能的更新...\n`);
625
+
626
+ const results = {
627
+ updated: [],
628
+ skipped: [],
629
+ failed: []
630
+ };
631
+
632
+ for (const skill of skills) {
633
+ try {
634
+ // 如果没有 gitUrl,跳过
635
+ if (!skill.meta || !skill.meta.gitUrl) {
636
+ console.log(`⊘ ${skill.name} - 本地技能`);
637
+ results.skipped.push({ name: skill.name, reason: 'local' });
638
+ continue;
639
+ }
640
+
641
+ // 更新技能
642
+ const result = await updateSkill(skill.name, { force });
643
+
644
+ if (result.success && result.updated) {
645
+ console.log(`✓ ${skill.name} - ${result.currentVersion} → ${result.newVersion}`);
646
+ results.updated.push({ name: skill.name, oldVersion: result.currentVersion, newVersion: result.newVersion });
647
+ } else if (result.success && !result.updated) {
648
+ console.log(`• ${skill.name} - 已是最新`);
649
+ results.skipped.push({ name: skill.name, reason: 'latest' });
650
+ } else if (result.local) {
651
+ results.skipped.push({ name: skill.name, reason: 'local' });
652
+ }
653
+ } catch (error) {
654
+ console.log(`✗ ${skill.name} - 失败: ${error.message}`);
655
+ results.failed.push({ name: skill.name, error: error.message });
656
+ }
657
+ }
658
+
659
+ // 显示总结
660
+ console.log(`\n${'─'.repeat(50)}`);
661
+ console.log(`更新完成:`);
662
+ console.log(` ✓ 成功: ${results.updated.length}`);
663
+ console.log(` ⊘ 跳过: ${results.skipped.length}`);
664
+ console.log(` ✗ 失败: ${results.failed.length}`);
665
+ console.log(`${'─'.repeat(50)}\n`);
666
+
667
+ if (results.failed.length > 0) {
668
+ console.log('失败的技能:');
669
+ results.failed.forEach(({ name, error }) => {
670
+ console.log(` - ${name}: ${error}`);
671
+ });
672
+ console.log('');
673
+ }
674
+
675
+ return results;
676
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eskill",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
4
4
  "description": "Unified AI Agent Skills Management - Install skills from Git URLs",
5
5
  "main": "index.js",
6
6
  "type": "module",