eskill 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +40 -4
  2. package/lib/installer.js +236 -19
  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, updateSkill, updateAllSkills } from './lib/installer.js';
4
+ import { installFromGitUrl, listSkills, removeSkill, linkSkill, 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.1.0')
16
+ .version('1.2.1')
17
17
  .option('-g, --global', '使用全局技能目录(~/.eskill/skills/),否则使用当前目录(./.claude/skills/)', false);
18
18
 
19
19
  // 安装命令
@@ -67,17 +67,34 @@ program
67
67
 
68
68
  const location = global ? '~/.eskill/skills/' : './.claude/skills/';
69
69
  console.log(`\n已安装的技能 (${location}):\n`);
70
+
71
+ // 来源类型显示映射
72
+ const sourceLabels = {
73
+ 'symlink': '🔗 软链',
74
+ 'copied-from-global': '📋 复制',
75
+ 'github': '📥 GitHub',
76
+ 'local': '👤 本地',
77
+ 'unknown': '❓ 未知'
78
+ };
79
+
70
80
  skills.forEach(skill => {
71
81
  const displayName = skill.author ? `${skill.name}@${skill.author}` : skill.name;
72
82
  const versionInfo = skill.version || (skill.commitHash ? `#${skill.commitHash.substring(0, 7)}` : '');
73
- const localMark = !skill.gitUrl ? ' [本地]' : '';
83
+ const sourceLabel = sourceLabels[skill.source] || sourceLabels['unknown'];
74
84
 
75
- console.log(` • ${displayName}${localMark}`);
85
+ console.log(` • ${displayName} [${sourceLabel}]`);
76
86
  if (versionInfo) {
77
87
  console.log(` 版本: ${versionInfo}`);
78
88
  }
79
89
  });
80
90
  console.log(`\n总计: ${skills.length} 个技能\n`);
91
+
92
+ // 显示来源说明
93
+ console.log('来源说明:');
94
+ console.log(' 🔗 软链 - 指向全局仓库的软链接');
95
+ console.log(' 📋 复制 - 从全局仓库复制到本地');
96
+ console.log(' 📥 GitHub - 从 GitHub 直接下载');
97
+ console.log(' 👤 本地 - 用户手动创建\n');
81
98
  } catch (error) {
82
99
  console.error(`错误: ${error.message}`);
83
100
  process.exit(1);
@@ -105,6 +122,25 @@ program
105
122
  }
106
123
  });
107
124
 
125
+ // 链接命令
126
+ program
127
+ .command('link')
128
+ .description('将全局技能软链接到本地')
129
+ .argument('<name>', '技能名称(不需要 @作者)')
130
+ .action(async (name) => {
131
+ try {
132
+ const result = await linkSkill(name);
133
+
134
+ // 如果用户取消链接,正常退出
135
+ if (result && result.cancelled) {
136
+ process.exit(0);
137
+ }
138
+ } catch (error) {
139
+ console.error(`\n❌ 链接失败: ${error.message}`);
140
+ process.exit(1);
141
+ }
142
+ });
143
+
108
144
  // 更新命令
109
145
  program
110
146
  .command('update')
package/lib/installer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'child_process';
2
- import { existsSync, mkdirSync, symlinkSync, rmSync, readdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, symlinkSync, rmSync, readdirSync, writeFileSync, readFileSync, copyFileSync, lstatSync } from 'fs';
3
3
  import { join, basename, dirname } from 'path';
4
4
  import { tmpdir, homedir } from 'os';
5
5
  import { fileURLToPath } from 'url';
@@ -250,9 +250,25 @@ async function handleSkillAtAuthorFormat(input, global = false) {
250
250
  const globalSkillPath = join(globalSkillsDir, skillName);
251
251
 
252
252
  if (existsSync(globalSkillPath)) {
253
- // 全局仓库中找到,直接复制
253
+ // 全局仓库中找到,检查本地是否已存在
254
254
  console.log(`✓ 在全局仓库中找到技能: ${skillName}`);
255
- console.log(` 位置: ~/.eskill/skills/${skillName}\n`);
255
+ console.log(` 位置: ~/.eskill/skills/${skillName}`);
256
+
257
+ const localSkillsDir = getSkillsDir(false);
258
+ const localSkillPath = join(localSkillsDir, skillName);
259
+
260
+ if (existsSync(localSkillPath)) {
261
+ console.log(`\n⚠️ 本地已存在同名技能: ${skillName}`);
262
+ console.log(` 位置: ${localSkillPath}\n`);
263
+
264
+ const overwrite = await confirmAction('是否覆盖本地已存在的技能?');
265
+ if (!overwrite) {
266
+ console.log('\n已取消安装\n');
267
+ throw new Error('用户取消安装');
268
+ }
269
+ }
270
+
271
+ console.log();
256
272
 
257
273
  // 返回特殊标记,表示需要从全局复制
258
274
  return `GLOBAL:${skillName}`;
@@ -284,9 +300,25 @@ async function handleSkillAtAuthorFormat(input, global = false) {
284
300
  const globalSkillPath = join(globalSkillsDir, skillName);
285
301
 
286
302
  if (existsSync(globalSkillPath)) {
287
- // 全局仓库中找到,直接复制
303
+ // 全局仓库中找到,检查本地是否已存在
288
304
  console.log(`✓ 在全局仓库中找到技能: ${skillName}`);
289
- console.log(` 位置: ~/.eskill/skills/${skillName}\n`);
305
+ console.log(` 位置: ~/.eskill/skills/${skillName}`);
306
+
307
+ const localSkillsDir = getSkillsDir(false);
308
+ const localSkillPath = join(localSkillsDir, skillName);
309
+
310
+ if (existsSync(localSkillPath)) {
311
+ console.log(`\n⚠️ 本地已存在同名技能: ${skillName}`);
312
+ console.log(` 位置: ${localSkillPath}\n`);
313
+
314
+ const overwrite = await confirmAction('是否覆盖本地已存在的技能?');
315
+ if (!overwrite) {
316
+ console.log('\n已取消安装\n');
317
+ throw new Error('用户取消安装');
318
+ }
319
+ }
320
+
321
+ console.log();
290
322
 
291
323
  // 返回特殊标记,表示需要从全局复制
292
324
  return `GLOBAL:${skillName}`;
@@ -366,6 +398,21 @@ async function copyFromGlobal(skillName, options = {}) {
366
398
  const sourcePath = join(globalSkillsDir, skillName);
367
399
  const targetPath = join(localSkillsDir, skillName);
368
400
 
401
+ // 检查本地技能目录是否存在
402
+ if (!existsSync(localSkillsDir)) {
403
+ console.log(`\n📁 本地技能目录不存在`);
404
+ console.log(` 需要创建: ${localSkillsDir}\n`);
405
+
406
+ const createDir = await confirmAction('是否创建本地技能目录?');
407
+ if (!createDir) {
408
+ console.log('\n已取消安装\n');
409
+ return { success: false, cancelled: true };
410
+ }
411
+
412
+ mkdirSync(localSkillsDir, { recursive: true });
413
+ console.log(`✓ 已创建目录: ${localSkillsDir}\n`);
414
+ }
415
+
369
416
  // 检查本地是否已存在同名技能
370
417
  if (existsSync(targetPath)) {
371
418
  if (!force) {
@@ -389,10 +436,23 @@ async function copyFromGlobal(skillName, options = {}) {
389
436
 
390
437
  execSync(`cp -r "${sourcePath}" "${targetPath}"`, { stdio: 'inherit' });
391
438
 
392
- // 复制元数据
439
+ // 复制元数据,并标记来源
393
440
  const globalMeta = getSkillMeta(skillName, true);
394
441
  if (globalMeta) {
395
- saveSkillMeta(skillName, globalMeta, false);
442
+ saveSkillMeta(skillName, {
443
+ ...globalMeta,
444
+ source: 'copied-from-global',
445
+ installedAt: new Date().toISOString(),
446
+ updatedAt: new Date().toISOString()
447
+ }, false);
448
+ } else {
449
+ // 如果全局没有元数据,创建基础元数据
450
+ saveSkillMeta(skillName, {
451
+ name: skillName,
452
+ source: 'copied-from-global',
453
+ installedAt: new Date().toISOString(),
454
+ updatedAt: new Date().toISOString()
455
+ }, false);
396
456
  }
397
457
 
398
458
  console.log(`\n✓ 技能已从全局仓库复制到本地`);
@@ -425,13 +485,21 @@ function confirmAction(message) {
425
485
  export async function installFromGitUrl(gitUrl, options = {}) {
426
486
  const { agent = 'claude', link = false, force = false, global = false } = options;
427
487
 
428
- // 检测并处理 name@author 格式
429
- let actualUrl = await handleSkillAtAuthorFormat(gitUrl, global);
488
+ try {
489
+ // 检测并处理 name@author 格式
490
+ let actualUrl = await handleSkillAtAuthorFormat(gitUrl, global);
430
491
 
431
- // 如果是从全局复制
432
- if (actualUrl.startsWith('GLOBAL:')) {
433
- const skillName = actualUrl.replace('GLOBAL:', '');
434
- return await copyFromGlobal(skillName, options);
492
+ // 如果是从全局复制
493
+ if (actualUrl.startsWith('GLOBAL:')) {
494
+ const skillName = actualUrl.replace('GLOBAL:', '');
495
+ return await copyFromGlobal(skillName, options);
496
+ }
497
+ } catch (error) {
498
+ // 用户取消安装
499
+ if (error.message === '用户取消安装') {
500
+ return { success: false, cancelled: true };
501
+ }
502
+ throw error;
435
503
  }
436
504
 
437
505
  // 解析 Git URL
@@ -464,9 +532,19 @@ export async function installFromGitUrl(gitUrl, options = {}) {
464
532
  console.log(`路径: ${parsed.path}`);
465
533
  }
466
534
 
467
- // 确保技能目录存在
535
+ // 检查技能目录是否存在
468
536
  if (!existsSync(skillsDir)) {
537
+ console.log(`\n📁 技能目录不存在`);
538
+ console.log(` 需要创建: ${skillsDir}\n`);
539
+
540
+ const createDir = await confirmAction('是否创建技能目录?');
541
+ if (!createDir) {
542
+ console.log('\n已取消安装\n');
543
+ return { success: false, cancelled: true };
544
+ }
545
+
469
546
  mkdirSync(skillsDir, { recursive: true });
547
+ console.log(`✓ 已创建目录: ${skillsDir}\n`);
470
548
  }
471
549
 
472
550
  // 检查是否已存在同名技能
@@ -545,6 +623,7 @@ export async function installFromGitUrl(gitUrl, options = {}) {
545
623
  branch: parsed.branch,
546
624
  commitHash,
547
625
  version,
626
+ source: global ? 'github' : 'github',
548
627
  installedAt: new Date().toISOString(),
549
628
  updatedAt: new Date().toISOString()
550
629
  }, global);
@@ -574,22 +653,71 @@ export function listSkills(agent = 'claude', global = false) {
574
653
 
575
654
  // 读取目录中的技能
576
655
  const skillDirs = readdirSync(skillDir, { withFileTypes: true })
577
- .filter(dirent => dirent.isDirectory());
656
+ .filter(dirent => dirent.isDirectory() || dirent.isSymbolicLink());
578
657
 
579
658
  // 读取所有元数据
580
659
  const allMeta = getAllSkillsMeta(global);
581
660
 
661
+ // 获取全局技能目录(用于检测是否从全局复制)
662
+ const globalSkillsDir = getSkillsDir(true);
663
+ const globalSkillsExists = existsSync(globalSkillsDir);
664
+ const globalSkillNames = globalSkillsExists
665
+ ? readdirSync(globalSkillsDir, { withFileTypes: true })
666
+ .filter(dirent => dirent.isDirectory())
667
+ .map(dirent => dirent.name)
668
+ : [];
669
+
582
670
  return skillDirs.map(dirent => {
583
- const skillPath = join(skillDir, dirent.name);
584
- const meta = allMeta.find(m => m.name === dirent.name);
671
+ const skillName = dirent.name;
672
+ const skillPath = join(skillDir, skillName);
673
+ const meta = allMeta.find(m => m.name === skillName);
674
+
675
+ // 优先使用元数据中的 source 字段
676
+ let source = meta?.source || null;
677
+ let isSymlink = false;
678
+
679
+ // 如果元数据中没有 source,则进行检测
680
+ if (!source) {
681
+ try {
682
+ const stat = lstatSync(skillPath);
683
+ isSymlink = stat.isSymbolicLink();
684
+
685
+ if (isSymlink) {
686
+ source = 'symlink';
687
+ } else if (meta?.gitUrl) {
688
+ // 有 gitUrl,判断是否从全局复制
689
+ if (!global && globalSkillNames.includes(skillName)) {
690
+ source = 'copied-from-global';
691
+ } else {
692
+ source = 'github';
693
+ }
694
+ } else {
695
+ // 没有 gitUrl,是用户自己创建的
696
+ source = 'local';
697
+ }
698
+ } catch (error) {
699
+ // 检测失败,默认为 local
700
+ source = 'local';
701
+ }
702
+ } else {
703
+ // 从元数据中读取是否为符号链接
704
+ try {
705
+ isSymlink = lstatSync(skillPath).isSymbolicLink();
706
+ } catch (error) {
707
+ isSymlink = false;
708
+ }
709
+ }
710
+
585
711
  return {
586
- name: dirent.name,
712
+ name: skillName,
587
713
  path: skillPath,
588
714
  author: meta?.author || null,
589
715
  version: meta?.version || null,
590
716
  commitHash: meta?.commitHash || null,
591
717
  installedAt: meta?.installedAt || null,
592
- gitUrl: meta?.gitUrl || null
718
+ gitUrl: meta?.gitUrl || null,
719
+ source: source,
720
+ isSymlink: isSymlink
593
721
  };
594
722
  });
595
723
  }
@@ -610,6 +738,95 @@ export function removeSkill(skillName, agent = 'claude', global = false) {
610
738
  console.log(`✓ 已删除技能: ${skillName}`);
611
739
  }
612
740
 
741
+ /**
742
+ * 链接全局技能到本地
743
+ */
744
+ export async function linkSkill(skillName) {
745
+ const globalSkillsDir = getSkillsDir(true);
746
+ const localSkillsDir = getSkillsDir(false);
747
+
748
+ const globalSkillPath = join(globalSkillsDir, skillName);
749
+ const localSkillPath = join(localSkillsDir, skillName);
750
+
751
+ // 检查全局是否存在该技能
752
+ if (!existsSync(globalSkillPath)) {
753
+ console.log(`\n❌ 全局仓库中不存在技能: ${skillName}`);
754
+ console.log(` 位置: ~/.eskill/skills/${skillName}\n`);
755
+ console.log(`💡 提示:`);
756
+ console.log(` - 使用 "eskill install -g ${skillName}" 先安装到全局仓库`);
757
+ console.log(` - 或使用 "eskill install" 直接安装到本地\n`);
758
+ throw new Error(`全局仓库中不存在技能: ${skillName}`);
759
+ }
760
+
761
+ // 检查本地技能目录是否存在
762
+ if (!existsSync(localSkillsDir)) {
763
+ console.log(`\n📁 本地技能目录不存在`);
764
+ console.log(` 需要创建: ${localSkillsDir}\n`);
765
+
766
+ const createDir = await confirmAction('是否创建本地技能目录?');
767
+ if (!createDir) {
768
+ console.log('\n已取消链接\n');
769
+ return { success: false, cancelled: true };
770
+ }
771
+
772
+ mkdirSync(localSkillsDir, { recursive: true });
773
+ console.log(`✓ 已创建目录: ${localSkillsDir}\n`);
774
+ }
775
+
776
+ // 检查本地是否已存在同名技能
777
+ if (existsSync(localSkillPath)) {
778
+ console.log(`\n⚠️ 本地已存在同名技能: ${skillName}`);
779
+ console.log(` 位置: ${localSkillPath}\n`);
780
+
781
+ const overwrite = await confirmAction('是否删除本地技能并创建软链接?');
782
+ if (!overwrite) {
783
+ console.log('\n已取消链接\n');
784
+ return { success: false, cancelled: true };
785
+ }
786
+
787
+ console.log(`删除本地已存在的技能: ${localSkillPath}`);
788
+ rmSync(localSkillPath, { recursive: true, force: true });
789
+ }
790
+
791
+ // 创建软链接
792
+ console.log(`\n创建软链接:`);
793
+ console.log(` 源: ${globalSkillPath}`);
794
+ console.log(` 目标: ${localSkillPath}\n`);
795
+
796
+ try {
797
+ symlinkSync(globalSkillPath, localSkillPath, 'dir');
798
+
799
+ console.log(`✓ 软链接创建成功`);
800
+ console.log(` 技能: ${skillName}`);
801
+ console.log(` 本地路径: ${localSkillPath}`);
802
+ console.log(` 全局路径: ${globalSkillPath}`);
803
+ console.log(` 说明: 本地技能指向全局仓库,全局更新会自动同步\n`);
804
+
805
+ // 保存元数据,标记来源为软链接
806
+ const globalMeta = getSkillMeta(skillName, true);
807
+ if (globalMeta) {
808
+ saveSkillMeta(skillName, {
809
+ ...globalMeta,
810
+ source: 'symlink',
811
+ installedAt: new Date().toISOString(),
812
+ updatedAt: new Date().toISOString()
813
+ }, false);
814
+ } else {
815
+ // 如果全局没有元数据,创建基础元数据
816
+ saveSkillMeta(skillName, {
817
+ name: skillName,
818
+ source: 'symlink',
819
+ installedAt: new Date().toISOString(),
820
+ updatedAt: new Date().toISOString()
821
+ }, false);
822
+ }
823
+
824
+ return { success: true, path: localSkillPath };
825
+ } catch (error) {
826
+ throw new Error(`创建软链接失败: ${error.message}`);
827
+ }
828
+ }
829
+
613
830
  /**
614
831
  * 清理所有技能(用于卸载)
615
832
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eskill",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Unified AI Agent Skills Management - Install skills from Git URLs",
5
5
  "main": "index.js",
6
6
  "type": "module",