eskill 1.0.21 → 1.0.25

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 +81 -6
  2. package/lib/installer.js +334 -34
  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.25');
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')
@@ -122,9 +160,9 @@ program
122
160
  }
123
161
 
124
162
  // 从 API 返回的数据结构中提取技能和分页信息
125
- const skills = result.data?.skills || result.data || result.skills || result.results || [];
126
- const pagination = result.data?.pagination || result.pagination || {};
127
- const total = pagination.total || result.total || skills.length;
163
+ let skills = result.data?.skills || result.data || result.skills || result.results || [];
164
+ let pagination = result.data?.pagination || result.pagination || {};
165
+ let total = pagination.total || result.total || skills.length;
128
166
 
129
167
  // 确保 skills 是数组
130
168
  if (!Array.isArray(skills)) {
@@ -133,6 +171,43 @@ program
133
171
  process.exit(1);
134
172
  }
135
173
 
174
+ // 智能搜索:如果没有结果且是 name@author 格式
175
+ if (skills.length === 0) {
176
+ const match = query.match(/^([^@]+)@([^@]+)$/);
177
+ if (match) {
178
+ const [, skillName, author] = match;
179
+ console.log(`\n💡 未找到 "${query}",尝试按技能名 "${skillName}" 和作者 "${author}" 搜索...\n`);
180
+
181
+ // 重新搜索技能名
182
+ const newResult = await searchSkills(skillName, {
183
+ page: 1,
184
+ limit: 100,
185
+ sortBy: options.sort,
186
+ ai: options.ai
187
+ });
188
+
189
+ const allSkills = newResult.data?.skills || newResult.data || newResult.skills || newResult.results || [];
190
+
191
+ // 筛选匹配作者的技能(不区分大小写)
192
+ skills = allSkills.filter(skill => {
193
+ const skillAuthor = (skill.author || skill.owner || '').toLowerCase();
194
+ return skillAuthor === author.toLowerCase();
195
+ });
196
+
197
+ total = skills.length;
198
+
199
+ if (skills.length > 0) {
200
+ console.log(`✓ 找到 ${total} 个匹配的技能\n`);
201
+ } else {
202
+ console.log(`❌ 仍然未找到匹配的技能\n`);
203
+ console.log('提示:');
204
+ console.log(` - 尝试只搜索技能名: eskill search ${skillName}`);
205
+ console.log(` - 尝试只搜索作者: eskill search ${author}\n`);
206
+ process.exit(0);
207
+ }
208
+ }
209
+ }
210
+
136
211
  console.log(formatSkillList(skills));
137
212
 
138
213
  // 显示分页信息和提示
package/lib/installer.js CHANGED
@@ -8,6 +8,45 @@ 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
@@ -117,7 +200,7 @@ async function handleSkillAtAuthorFormat(input) {
117
200
 
118
201
  const [, skillName, author] = match;
119
202
  console.log(`\n检测到技能名称格式: ${skillName}@${author}`);
120
- console.log(`正在搜索 "${skillName}"...\n`);
203
+ console.log(`正在搜索 "${skillName}" 和作者 "${author}"...\n`);
121
204
 
122
205
  try {
123
206
  // 搜索技能
@@ -129,17 +212,28 @@ async function handleSkillAtAuthorFormat(input) {
129
212
 
130
213
  const skills = result.data?.skills || result.data || result.skills || result.results || [];
131
214
 
132
- // 筛选出匹配作者的技能
215
+ // 筛选出匹配作者的技能(不区分大小写)
133
216
  const matchedSkills = skills.filter(skill => {
134
- const skillAuthor = skill.author || skill.owner || '';
135
- return skillAuthor.toLowerCase() === author.toLowerCase();
217
+ const skillAuthor = (skill.author || skill.owner || '').toLowerCase();
218
+ return skillAuthor === author.toLowerCase();
136
219
  });
137
220
 
138
221
  if (matchedSkills.length === 0) {
139
222
  // 没有找到匹配的作者
140
223
  console.log(`❌ 未找到作者 "${author}" 的技能 "${skillName}"\n`);
141
- console.log('搜索结果如下:\n');
142
- console.log(formatSkillList(skills));
224
+
225
+ // 显示找到的同名技能(如果有)
226
+ if (skills.length > 0) {
227
+ console.log(`找到 ${skills.length} 个名为 "${skillName}" 的技能,但作者不匹配:\n`);
228
+ console.log(formatSkillList(skills));
229
+ }
230
+
231
+ // 给出建议
232
+ console.log(`💡 建议:`);
233
+ console.log(` - 尝试搜索其他作者: eskill search ${skillName}`);
234
+ console.log(` - 尝试搜索该作者的其他技能: eskill search ${author}`);
235
+ console.log(` - 或直接使用 GitHub URL 安装\n`);
236
+
143
237
  throw new Error(`未找到匹配的技能`);
144
238
  }
145
239
 
@@ -151,7 +245,7 @@ async function handleSkillAtAuthorFormat(input) {
151
245
  throw new Error('技能信息中没有 GitHub URL');
152
246
  }
153
247
 
154
- console.log(`\n✓ 找到匹配的技能:`);
248
+ console.log(`✓ 找到匹配的技能:`);
155
249
  console.log(` 名称: ${skillName}@${author}`);
156
250
  console.log(` Stars: ⭐ ${matchedSkill.stars || 'N/A'}`);
157
251
  console.log(` GitHub: ${githubUrl}\n`);
@@ -275,15 +369,22 @@ export async function installFromGitUrl(gitUrl, options = {}) {
275
369
  console.log(` 存储位置: ~/.eskill/skills/`);
276
370
  console.log(` 说明: 技能永久保存,更新 eskill 不会丢失`);
277
371
 
278
- // 保存技能元数据(作者、Git URL 等)
279
- saveSkillMeta(targetPath, {
372
+ // 获取 git 版本信息
373
+ const commitHash = getGitCommitHash(sourcePath);
374
+ const version = getGitVersion(sourcePath);
375
+
376
+ // 保存技能元数据(作者、Git URL、版本等)
377
+ saveSkillMeta(skillName, {
280
378
  name: skillName,
281
379
  author: parsed.owner,
282
380
  gitUrl: gitUrl,
283
381
  platform: parsed.platform,
284
382
  repo: parsed.repo,
285
383
  branch: parsed.branch,
286
- installedAt: new Date().toISOString()
384
+ commitHash,
385
+ version,
386
+ installedAt: new Date().toISOString(),
387
+ updatedAt: new Date().toISOString()
287
388
  });
288
389
 
289
390
  return { success: true, path: targetPath };
@@ -310,18 +411,25 @@ export function listSkills(agent = 'claude') {
310
411
  }
311
412
 
312
413
  // 读取目录中的技能
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
- });
414
+ const skillDirs = readdirSync(skillDir, { withFileTypes: true })
415
+ .filter(dirent => dirent.isDirectory());
416
+
417
+ // 读取所有元数据
418
+ const allMeta = getAllSkillsMeta();
419
+
420
+ return skillDirs.map(dirent => {
421
+ const skillPath = join(skillDir, dirent.name);
422
+ const meta = allMeta.find(m => m.name === dirent.name);
423
+ return {
424
+ name: dirent.name,
425
+ path: skillPath,
426
+ author: meta?.author || null,
427
+ version: meta?.version || null,
428
+ commitHash: meta?.commitHash || null,
429
+ installedAt: meta?.installedAt || null,
430
+ gitUrl: meta?.gitUrl || null
431
+ };
432
+ });
325
433
  }
326
434
 
327
435
  /**
@@ -336,6 +444,7 @@ export function removeSkill(skillName, agent = 'claude') {
336
444
  }
337
445
 
338
446
  rmSync(targetPath, { recursive: true, force: true });
447
+ removeSkillMeta(skillName);
339
448
  console.log(`✓ 已删除技能: ${skillName}`);
340
449
  }
341
450
 
@@ -385,3 +494,194 @@ export function cleanupAll() {
385
494
  export function getSkillsDir() {
386
495
  return getSkillsStorageDir();
387
496
  }
497
+
498
+ /**
499
+ * 更新单个技能
500
+ */
501
+ export async function updateSkill(skillName, options = {}) {
502
+ const { force = false } = options;
503
+ const skillDir = getSkillsStorageDir();
504
+ const targetPath = join(skillDir, skillName);
505
+
506
+ // 检查技能是否存在
507
+ if (!existsSync(targetPath)) {
508
+ throw new Error(`技能不存在: ${skillName}`);
509
+ }
510
+
511
+ // 读取元数据
512
+ const meta = getSkillMeta(skillName);
513
+
514
+ // 如果没有 gitUrl,说明是本地技能,无法更新
515
+ if (!meta || !meta.gitUrl) {
516
+ return { success: false, local: true, skill: skillName };
517
+ }
518
+
519
+ const currentVersion = meta.version || meta.commitHash?.substring(0, 7) || 'unknown';
520
+
521
+ // 重新安装技能(使用内部的 installFromGitUrl,但不打印)
522
+ try {
523
+ const parsed = parseGitUrl(meta.gitUrl);
524
+
525
+ // 确定技能名称
526
+ const skillNameFromUrl = parsed.path ? basename(parsed.path) : parsed.repo;
527
+
528
+ // 创建临时目录
529
+ const tempDir = join(tmpdir(), `eskill-update-${Date.now()}`);
530
+
531
+ try {
532
+ // 克隆仓库(静默模式)
533
+ const cloneUrl = parsed.cloneUrl;
534
+ execSync(
535
+ `git clone --depth 1 --branch ${parsed.branch} --single-branch "${cloneUrl}" "${tempDir}"`,
536
+ { stdio: 'inherit' }
537
+ );
538
+
539
+ // 确定源路径
540
+ const sourcePath = parsed.path ? join(tempDir, parsed.path) : tempDir;
541
+
542
+ // 验证源路径存在
543
+ if (!existsSync(sourcePath)) {
544
+ throw new Error(`路径不存在: ${sourcePath}`);
545
+ }
546
+
547
+ // 检查 SKILL.md 是否存在
548
+ const skillManifestPath = join(sourcePath, 'SKILL.md');
549
+ if (!existsSync(skillManifestPath)) {
550
+ throw new Error(`无效的技能包:未找到 SKILL.md 文件`);
551
+ }
552
+
553
+ // 获取新版本信息
554
+ const newCommitHash = getGitCommitHash(sourcePath);
555
+ const newVersion = getGitVersion(sourcePath);
556
+ const newVersionDisplay = newVersion || newCommitHash?.substring(0, 7) || 'unknown';
557
+
558
+ // 检查是否有更新
559
+ if (!force && newCommitHash === meta.commitHash) {
560
+ // 删除临时目录
561
+ rmSync(tempDir, { recursive: true, force: true });
562
+ return { success: true, skill: skillName, updated: false, currentVersion };
563
+ }
564
+
565
+ // 删除旧技能
566
+ rmSync(targetPath, { recursive: true, force: true });
567
+
568
+ // 复制新技能
569
+ if (process.platform === 'win32') {
570
+ execSync(`xcopy "${sourcePath}" "${targetPath}" /E /I /H /Y`, { stdio: 'inherit' });
571
+ } else {
572
+ execSync(`cp -r "${sourcePath}" "${targetPath}"`, { stdio: 'inherit' });
573
+ }
574
+
575
+ // 更新元数据
576
+ saveSkillMeta(skillName, {
577
+ ...meta,
578
+ commitHash: newCommitHash,
579
+ version: newVersion,
580
+ updatedAt: new Date().toISOString()
581
+ });
582
+
583
+ return {
584
+ success: true,
585
+ skill: skillName,
586
+ updated: true,
587
+ currentVersion,
588
+ newVersion: newVersionDisplay
589
+ };
590
+ } finally {
591
+ // 清理临时目录
592
+ if (existsSync(tempDir)) {
593
+ rmSync(tempDir, { recursive: true, force: true });
594
+ }
595
+ }
596
+ } catch (error) {
597
+ throw new Error(`更新失败: ${error.message}`);
598
+ }
599
+ }
600
+
601
+ /**
602
+ * 更新所有技能
603
+ */
604
+ export async function updateAllSkills(options = {}) {
605
+ const { force = false } = options;
606
+
607
+ // 自动迁移旧位置的技能
608
+ migrateOldSkills();
609
+
610
+ const skillDir = getSkillsStorageDir();
611
+
612
+ if (!existsSync(skillDir)) {
613
+ console.log('未安装任何技能');
614
+ return [];
615
+ }
616
+
617
+ // 读取所有技能
618
+ const skills = readdirSync(skillDir, { withFileTypes: true })
619
+ .filter(dirent => dirent.isDirectory())
620
+ .map(dirent => {
621
+ const skillPath = join(skillDir, dirent.name);
622
+ const meta = getSkillMeta(dirent.name);
623
+ return {
624
+ name: dirent.name,
625
+ path: skillPath,
626
+ meta
627
+ };
628
+ });
629
+
630
+ if (skills.length === 0) {
631
+ console.log('未安装任何技能');
632
+ return [];
633
+ }
634
+
635
+ console.log(`\n检查 ${skills.length} 个技能的更新...\n`);
636
+
637
+ const results = {
638
+ updated: [],
639
+ skipped: [],
640
+ failed: []
641
+ };
642
+
643
+ for (const skill of skills) {
644
+ try {
645
+ // 如果没有 gitUrl,跳过
646
+ if (!skill.meta || !skill.meta.gitUrl) {
647
+ console.log(`⊘ ${skill.name} - 本地技能`);
648
+ results.skipped.push({ name: skill.name, reason: 'local' });
649
+ continue;
650
+ }
651
+
652
+ // 更新技能
653
+ const result = await updateSkill(skill.name, { force });
654
+
655
+ if (result.success && result.updated) {
656
+ console.log(`✓ ${skill.name} - ${result.currentVersion} → ${result.newVersion}`);
657
+ results.updated.push({ name: skill.name, oldVersion: result.currentVersion, newVersion: result.newVersion });
658
+ } else if (result.success && !result.updated) {
659
+ console.log(`• ${skill.name} - 已是最新`);
660
+ results.skipped.push({ name: skill.name, reason: 'latest' });
661
+ } else if (result.local) {
662
+ results.skipped.push({ name: skill.name, reason: 'local' });
663
+ }
664
+ } catch (error) {
665
+ console.log(`✗ ${skill.name} - 失败: ${error.message}`);
666
+ results.failed.push({ name: skill.name, error: error.message });
667
+ }
668
+ }
669
+
670
+ // 显示总结
671
+ console.log(`\n${'─'.repeat(50)}`);
672
+ console.log(`更新完成:`);
673
+ console.log(` ✓ 成功: ${results.updated.length}`);
674
+ console.log(` ⊘ 跳过: ${results.skipped.length}`);
675
+ console.log(` ✗ 失败: ${results.failed.length}`);
676
+ console.log(`${'─'.repeat(50)}\n`);
677
+
678
+ if (results.failed.length > 0) {
679
+ console.log('失败的技能:');
680
+ results.failed.forEach(({ name, error }) => {
681
+ console.log(` - ${name}: ${error}`);
682
+ });
683
+ console.log('');
684
+ }
685
+
686
+ return results;
687
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eskill",
3
- "version": "1.0.21",
3
+ "version": "1.0.25",
4
4
  "description": "Unified AI Agent Skills Management - Install skills from Git URLs",
5
5
  "main": "index.js",
6
6
  "type": "module",