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.
- package/cli.js +40 -4
- package/lib/installer.js +236 -19
- 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
|
|
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
|
|
83
|
+
const sourceLabel = sourceLabels[skill.source] || sourceLabels['unknown'];
|
|
74
84
|
|
|
75
|
-
console.log(` • ${displayName}${
|
|
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}
|
|
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}
|
|
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,
|
|
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
|
-
|
|
429
|
-
|
|
488
|
+
try {
|
|
489
|
+
// 检测并处理 name@author 格式
|
|
490
|
+
let actualUrl = await handleSkillAtAuthorFormat(gitUrl, global);
|
|
430
491
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
584
|
-
const
|
|
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:
|
|
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
|
*/
|