heflc-skill 1.0.0
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/README.md +56 -0
- package/bin/heflc-skill.js +46 -0
- package/package.json +19 -0
- package/src/commands/doctor.js +44 -0
- package/src/commands/install.js +93 -0
- package/src/commands/list.js +35 -0
- package/src/commands/uninstall.js +45 -0
- package/src/commands/update.js +81 -0
- package/src/config.js +18 -0
- package/src/editors.js +123 -0
- package/src/github.js +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# heflc-skill
|
|
2
|
+
|
|
3
|
+
> 一条命令安装 AI Skill 到所有编辑器。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g heflc-skill
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或不安装直接用:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx heflc-skill <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 使用
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 查看可用的 Skill
|
|
21
|
+
heflc-skill list
|
|
22
|
+
|
|
23
|
+
# 安装指定 Skill
|
|
24
|
+
heflc-skill install tech-branding
|
|
25
|
+
|
|
26
|
+
# 安装全部 Skill
|
|
27
|
+
heflc-skill install --all
|
|
28
|
+
|
|
29
|
+
# 更新已安装的 Skill
|
|
30
|
+
heflc-skill update
|
|
31
|
+
|
|
32
|
+
# 查看本机编辑器和已安装状态
|
|
33
|
+
heflc-skill doctor
|
|
34
|
+
|
|
35
|
+
# 卸载指定 Skill
|
|
36
|
+
heflc-skill uninstall tech-branding
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 支持的编辑器
|
|
40
|
+
|
|
41
|
+
自动检测本机安装的编辑器,将 Skill 注入全局配置:
|
|
42
|
+
|
|
43
|
+
| 编辑器 | Skill 路径 |
|
|
44
|
+
|--------|-----------|
|
|
45
|
+
| Claude Code | ~/.claude/skills/<name>/SKILL.md |
|
|
46
|
+
| Cursor | ~/.cursor/skills/<name>/SKILL.md |
|
|
47
|
+
| Trae | ~/.trae/skills/<name>/SKILL.md |
|
|
48
|
+
| Gemini | ~/.gemini/antigravity/skills/<name>/SKILL.md |
|
|
49
|
+
|
|
50
|
+
## 环境变量
|
|
51
|
+
|
|
52
|
+
| 变量 | 说明 | 默认值 |
|
|
53
|
+
|------|------|--------|
|
|
54
|
+
| GITHUB_TOKEN | GitHub API token(可选,提高速率限制) | 无 |
|
|
55
|
+
| HEFLC_SKILL_OWNER | 覆盖仓库拥有者 | {owner} |
|
|
56
|
+
| HEFLC_SKILL_REPO | 覆盖仓库名 | {repo} |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/heflc-skill.js
|
|
4
|
+
// CLI 入口,注册所有命令
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { listCommand } from '../src/commands/list.js';
|
|
8
|
+
import { installCommand } from '../src/commands/install.js';
|
|
9
|
+
import { uninstallCommand } from '../src/commands/uninstall.js';
|
|
10
|
+
import { updateCommand } from '../src/commands/update.js';
|
|
11
|
+
import { doctorCommand } from '../src/commands/doctor.js';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('heflc-skill')
|
|
17
|
+
.description('下载并安装 AI Skill 到本机编辑器')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('list')
|
|
22
|
+
.description('列出所有可用的 Skill')
|
|
23
|
+
.action(listCommand);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('install [name]')
|
|
27
|
+
.description('安装指定 Skill 到所有检测到的编辑器')
|
|
28
|
+
.option('-a, --all', '安装全部 Skill')
|
|
29
|
+
.action(installCommand);
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('uninstall <name>')
|
|
33
|
+
.description('从所有编辑器中卸载指定 Skill')
|
|
34
|
+
.action(uninstallCommand);
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('update [name]')
|
|
38
|
+
.description('更新已安装的 Skill(不指定名称则更新全部)')
|
|
39
|
+
.action(updateCommand);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('doctor')
|
|
43
|
+
.description('检查编辑器检测状态和已安装的 Skill')
|
|
44
|
+
.action(doctorCommand);
|
|
45
|
+
|
|
46
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heflc-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool to install AI Skills into editors (Claude Code, Cursor, Trae, Gemini)",
|
|
6
|
+
"bin": {
|
|
7
|
+
"heflc-skill": "./bin/heflc-skill.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["ai", "skill", "cli", "claude", "cursor", "trae"],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"commander": "^12.0.0",
|
|
17
|
+
"ora": "^8.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/commands/doctor.js
|
|
2
|
+
// 诊断本机编辑器和已安装的 skill 状态
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { detectEditors, listInstalledSkills } from '../editors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 执行 doctor 命令
|
|
9
|
+
*/
|
|
10
|
+
export async function doctorCommand() {
|
|
11
|
+
const editors = detectEditors();
|
|
12
|
+
|
|
13
|
+
// 打印编辑器检测结果
|
|
14
|
+
console.log('编辑器检测:\n');
|
|
15
|
+
for (const editor of editors) {
|
|
16
|
+
if (editor.detected) {
|
|
17
|
+
console.log(` ${chalk.green('✓')} ${editor.name} ${chalk.gray(`(${editor.detectFullPath})`)}`);
|
|
18
|
+
} else {
|
|
19
|
+
console.log(` ${chalk.gray('✗')} ${chalk.gray(editor.name)} ${chalk.gray('(未检测到)')}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 打印已安装的 skill
|
|
24
|
+
const activeEditors = editors.filter((e) => e.detected);
|
|
25
|
+
if (activeEditors.length === 0) {
|
|
26
|
+
console.log(chalk.yellow('\n未检测到任何支持的编辑器。'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('\n已安装的 Skill:\n');
|
|
31
|
+
for (const editor of activeEditors) {
|
|
32
|
+
const skills = listInstalledSkills(editor);
|
|
33
|
+
console.log(` ${chalk.bold(editor.name)}:`);
|
|
34
|
+
if (skills.length === 0) {
|
|
35
|
+
console.log(` ${chalk.gray('(无)')}`);
|
|
36
|
+
} else {
|
|
37
|
+
for (const skill of skills) {
|
|
38
|
+
console.log(` ${chalk.green('✓')} ${skill}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`\n使用 ${chalk.green('heflc-skill list')} 查看可安装的 Skill。`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/commands/install.js
|
|
2
|
+
// 安装 skill 到所有检测到的编辑器
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { listRemoteSkills, downloadSkill } from '../github.js';
|
|
7
|
+
import { detectEditors, writeSkillFile, getSkillFilePath } from '../editors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 打印编辑器检测结果
|
|
11
|
+
* @param {Array} editors - detectEditors() 返回值
|
|
12
|
+
*/
|
|
13
|
+
function printEditorStatus(editors) {
|
|
14
|
+
console.log('检测到以下编辑器:');
|
|
15
|
+
for (const editor of editors) {
|
|
16
|
+
if (editor.detected) {
|
|
17
|
+
console.log(` ${chalk.green('✓')} ${editor.name}`);
|
|
18
|
+
} else {
|
|
19
|
+
console.log(` ${chalk.gray('✗')} ${chalk.gray(editor.name)} ${chalk.gray('(未检测到)')}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
console.log();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 安装单个 skill 到所有检测到的编辑器
|
|
27
|
+
* @param {string} skillName - skill 名称
|
|
28
|
+
* @param {Array} activeEditors - 已检测到的编辑器列表
|
|
29
|
+
*/
|
|
30
|
+
async function installOne(skillName, activeEditors) {
|
|
31
|
+
const spinner = ora(`正在下载 ${skillName} ...`).start();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = await downloadSkill(skillName);
|
|
35
|
+
spinner.succeed(`已下载 ${chalk.cyan(skillName)}`);
|
|
36
|
+
|
|
37
|
+
for (const editor of activeEditors) {
|
|
38
|
+
try {
|
|
39
|
+
writeSkillFile(editor, skillName, content);
|
|
40
|
+
const filePath = getSkillFilePath(editor, skillName);
|
|
41
|
+
console.log(` ${chalk.green('✓')} ${filePath}`);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(` ${chalk.red('✗')} ${editor.name}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
spinner.fail(chalk.red(`${skillName}: ${error.message}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 执行 install 命令
|
|
53
|
+
* @param {string|undefined} name - skill 名称,undefined 时需要 --all
|
|
54
|
+
* @param {object} options - 命令选项
|
|
55
|
+
*/
|
|
56
|
+
export async function installCommand(name, options) {
|
|
57
|
+
// 检测编辑器
|
|
58
|
+
const editors = detectEditors();
|
|
59
|
+
printEditorStatus(editors);
|
|
60
|
+
|
|
61
|
+
const activeEditors = editors.filter((e) => e.detected);
|
|
62
|
+
if (activeEditors.length === 0) {
|
|
63
|
+
console.log(chalk.yellow('未检测到任何支持的编辑器,无法安装。'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 确定要安装的 skill 列表
|
|
68
|
+
let skillNames;
|
|
69
|
+
if (options.all) {
|
|
70
|
+
const spinner = ora('正在获取所有 Skill ...').start();
|
|
71
|
+
try {
|
|
72
|
+
skillNames = await listRemoteSkills();
|
|
73
|
+
spinner.succeed(`找到 ${skillNames.length} 个 Skill`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
spinner.fail(chalk.red(error.message));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
} else if (name) {
|
|
79
|
+
skillNames = [name];
|
|
80
|
+
} else {
|
|
81
|
+
console.log(chalk.red('请指定 skill 名称,或使用 --all 安装全部。'));
|
|
82
|
+
console.log(`用法: heflc-skill install <name>`);
|
|
83
|
+
console.log(` heflc-skill install --all`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 逐个安装
|
|
88
|
+
for (const skillName of skillNames) {
|
|
89
|
+
await installOne(skillName, activeEditors);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(chalk.green('\n安装完成!'));
|
|
93
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/commands/list.js
|
|
2
|
+
// 列出集中仓库中所有可用的 skill
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { listRemoteSkills } from '../github.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 执行 list 命令
|
|
10
|
+
*/
|
|
11
|
+
export async function listCommand() {
|
|
12
|
+
const spinner = ora('正在获取可用的 Skill 列表...').start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const skills = await listRemoteSkills();
|
|
16
|
+
|
|
17
|
+
if (skills.length === 0) {
|
|
18
|
+
spinner.warn('集中仓库中暂无可用的 Skill。');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
spinner.succeed(`找到 ${skills.length} 个可用的 Skill:\n`);
|
|
23
|
+
|
|
24
|
+
for (const name of skills) {
|
|
25
|
+
console.log(` ${chalk.cyan(name)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(
|
|
29
|
+
`\n使用 ${chalk.green('heflc-skill install <name>')} 安装指定 Skill。`
|
|
30
|
+
);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
spinner.fail(chalk.red(error.message));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/commands/uninstall.js
|
|
2
|
+
// 从所有编辑器中卸载指定 skill
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { detectEditors, removeSkillFile, getSkillFilePath } from '../editors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 执行 uninstall 命令
|
|
9
|
+
* @param {string} name - 要卸载的 skill 名称
|
|
10
|
+
*/
|
|
11
|
+
export async function uninstallCommand(name) {
|
|
12
|
+
if (!name) {
|
|
13
|
+
console.log(chalk.red('请指定要卸载的 skill 名称。'));
|
|
14
|
+
console.log(`用法: heflc-skill uninstall <name>`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const editors = detectEditors();
|
|
19
|
+
const activeEditors = editors.filter((e) => e.detected);
|
|
20
|
+
|
|
21
|
+
if (activeEditors.length === 0) {
|
|
22
|
+
console.log(chalk.yellow('未检测到任何编辑器。'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`正在卸载 ${chalk.cyan(name)} ...\n`);
|
|
27
|
+
|
|
28
|
+
let removedCount = 0;
|
|
29
|
+
for (const editor of activeEditors) {
|
|
30
|
+
const filePath = getSkillFilePath(editor, name);
|
|
31
|
+
const removed = removeSkillFile(editor, name);
|
|
32
|
+
if (removed) {
|
|
33
|
+
console.log(` ${chalk.green('✓')} 已删除 ${filePath}`);
|
|
34
|
+
removedCount++;
|
|
35
|
+
} else {
|
|
36
|
+
console.log(` ${chalk.gray('-')} ${editor.name}: 未安装该 skill`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (removedCount > 0) {
|
|
41
|
+
console.log(chalk.green(`\n卸载完成,已从 ${removedCount} 个编辑器中移除。`));
|
|
42
|
+
} else {
|
|
43
|
+
console.log(chalk.yellow('\n该 skill 未安装在任何编辑器中。'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/commands/update.js
|
|
2
|
+
// 更新已安装的 skill(重新从 GitHub 下载最新版覆盖)
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { detectEditors, listInstalledSkills } from '../editors.js';
|
|
7
|
+
import { downloadSkill } from '../github.js';
|
|
8
|
+
import { writeSkillFile, getSkillFilePath } from '../editors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 更新单个 skill 到所有检测到的编辑器
|
|
12
|
+
* @param {string} skillName - skill 名称
|
|
13
|
+
* @param {Array} activeEditors - 已检测到的编辑器列表
|
|
14
|
+
*/
|
|
15
|
+
async function updateOne(skillName, activeEditors) {
|
|
16
|
+
const spinner = ora(`正在更新 ${skillName} ...`).start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const content = await downloadSkill(skillName);
|
|
20
|
+
spinner.succeed(`已下载 ${chalk.cyan(skillName)}`);
|
|
21
|
+
|
|
22
|
+
for (const editor of activeEditors) {
|
|
23
|
+
try {
|
|
24
|
+
writeSkillFile(editor, skillName, content);
|
|
25
|
+
const filePath = getSkillFilePath(editor, skillName);
|
|
26
|
+
console.log(` ${chalk.green('✓')} ${filePath}`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.log(` ${chalk.red('✗')} ${editor.name}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
spinner.fail(chalk.red(`${skillName}: ${error.message}`));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行 update 命令
|
|
38
|
+
* @param {string|undefined} name - 指定更新的 skill 名称,不传则更新全部已安装的
|
|
39
|
+
*/
|
|
40
|
+
export async function updateCommand(name) {
|
|
41
|
+
const editors = detectEditors();
|
|
42
|
+
const activeEditors = editors.filter((e) => e.detected);
|
|
43
|
+
|
|
44
|
+
if (activeEditors.length === 0) {
|
|
45
|
+
console.log(chalk.yellow('未检测到任何编辑器。'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (name) {
|
|
50
|
+
// 更新指定 skill
|
|
51
|
+
console.log(`正在更新 ${chalk.cyan(name)} ...\n`);
|
|
52
|
+
await updateOne(name, activeEditors);
|
|
53
|
+
console.log(chalk.green('\n更新完成!'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 扫描所有编辑器,收集已安装的 skill(去重)
|
|
58
|
+
const installedSet = new Set();
|
|
59
|
+
for (const editor of activeEditors) {
|
|
60
|
+
const skills = listInstalledSkills(editor);
|
|
61
|
+
for (const skill of skills) {
|
|
62
|
+
installedSet.add(skill);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (installedSet.size === 0) {
|
|
67
|
+
console.log(chalk.yellow('未发现任何已安装的 Skill。'));
|
|
68
|
+
console.log(`使用 ${chalk.green('heflc-skill install <name>')} 安装。`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const installedNames = [...installedSet];
|
|
73
|
+
console.log(`发现 ${installedNames.length} 个已安装的 Skill:${chalk.cyan(installedNames.join(', '))}\n`);
|
|
74
|
+
|
|
75
|
+
// 逐个更新
|
|
76
|
+
for (const skillName of installedNames) {
|
|
77
|
+
await updateOne(skillName, activeEditors);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(chalk.green('\n全部更新完成!'));
|
|
81
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/config.js
|
|
2
|
+
// 集中仓库配置,支持环境变量覆盖
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
// GitHub 仓库拥有者(组织名或用户名)
|
|
6
|
+
repoOwner: process.env.HEFLC_SKILL_OWNER || 'rendaoxian',
|
|
7
|
+
|
|
8
|
+
// GitHub 仓库名
|
|
9
|
+
repoName: process.env.HEFLC_SKILL_REPO || 'heflc-skills',
|
|
10
|
+
|
|
11
|
+
// GitHub API token(可选,提高速率限制到 5000 次/小时)
|
|
12
|
+
githubToken: process.env.GITHUB_TOKEN || null,
|
|
13
|
+
|
|
14
|
+
// GitHub API 基础地址
|
|
15
|
+
apiBase: 'https://api.github.com',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default config;
|
package/src/editors.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/editors.js
|
|
2
|
+
// 编辑器检测与路径映射
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 支持的编辑器列表
|
|
10
|
+
* 新增编辑器只需往数组里加一条记录
|
|
11
|
+
*/
|
|
12
|
+
const EDITORS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'Claude Code',
|
|
15
|
+
detectPath: '.claude',
|
|
16
|
+
skillPath: '.claude/skills/{name}/SKILL.md',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Cursor',
|
|
20
|
+
detectPath: '.cursor',
|
|
21
|
+
skillPath: '.cursor/skills/{name}/SKILL.md',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'Trae',
|
|
25
|
+
detectPath: '.trae',
|
|
26
|
+
skillPath: '.trae/skills/{name}/SKILL.md',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Gemini',
|
|
30
|
+
detectPath: '.gemini',
|
|
31
|
+
skillPath: '.gemini/antigravity/skills/{name}/SKILL.md',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 获取用户 home 目录(跨平台)
|
|
37
|
+
* @returns {string} home 目录绝对路径
|
|
38
|
+
*/
|
|
39
|
+
function getHomeDir() {
|
|
40
|
+
return homedir();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 检测本机安装了哪些编辑器
|
|
45
|
+
* @returns {Array<{name: string, detected: boolean, detectFullPath: string, skillPathTemplate: string}>}
|
|
46
|
+
*/
|
|
47
|
+
export function detectEditors() {
|
|
48
|
+
const home = getHomeDir();
|
|
49
|
+
return EDITORS.map((editor) => {
|
|
50
|
+
const detectFullPath = join(home, editor.detectPath);
|
|
51
|
+
return {
|
|
52
|
+
name: editor.name,
|
|
53
|
+
detected: existsSync(detectFullPath),
|
|
54
|
+
detectFullPath,
|
|
55
|
+
skillPathTemplate: editor.skillPath,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取指定 skill 在某个编辑器中的完整文件路径
|
|
62
|
+
* @param {object} editor - detectEditors() 返回的编辑器对象
|
|
63
|
+
* @param {string} skillName - skill 名称
|
|
64
|
+
* @returns {string} 完整文件路径
|
|
65
|
+
*/
|
|
66
|
+
export function getSkillFilePath(editor, skillName) {
|
|
67
|
+
const home = getHomeDir();
|
|
68
|
+
const relativePath = editor.skillPathTemplate.replace('{name}', skillName);
|
|
69
|
+
return join(home, relativePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 将 skill 内容写入指定编辑器的 skills 目录
|
|
74
|
+
* @param {object} editor - 编辑器对象
|
|
75
|
+
* @param {string} skillName - skill 名称
|
|
76
|
+
* @param {string} content - SKILL.md 文件内容
|
|
77
|
+
*/
|
|
78
|
+
export function writeSkillFile(editor, skillName, content) {
|
|
79
|
+
const filePath = getSkillFilePath(editor, skillName);
|
|
80
|
+
const dir = dirname(filePath);
|
|
81
|
+
mkdirSync(dir, { recursive: true });
|
|
82
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 从指定编辑器中删除 skill
|
|
87
|
+
* @param {object} editor - 编辑器对象
|
|
88
|
+
* @param {string} skillName - skill 名称
|
|
89
|
+
* @returns {boolean} 是否成功删除
|
|
90
|
+
*/
|
|
91
|
+
export function removeSkillFile(editor, skillName) {
|
|
92
|
+
const filePath = getSkillFilePath(editor, skillName);
|
|
93
|
+
const dir = dirname(filePath);
|
|
94
|
+
if (existsSync(dir)) {
|
|
95
|
+
rmSync(dir, { recursive: true, force: true });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 扫描某个编辑器中已安装的 skill 列表
|
|
103
|
+
* @param {object} editor - 编辑器对象
|
|
104
|
+
* @returns {string[]} 已安装的 skill 名称数组
|
|
105
|
+
*/
|
|
106
|
+
export function listInstalledSkills(editor) {
|
|
107
|
+
const home = getHomeDir();
|
|
108
|
+
const parts = editor.skillPathTemplate.split('{name}');
|
|
109
|
+
const skillsDir = join(home, parts[0]);
|
|
110
|
+
|
|
111
|
+
if (!existsSync(skillsDir)) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
117
|
+
return entries
|
|
118
|
+
.filter((entry) => entry.isDirectory())
|
|
119
|
+
.map((entry) => entry.name);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/github.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/github.js
|
|
2
|
+
// GitHub REST API 封装
|
|
3
|
+
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 构造 GitHub API 请求头
|
|
8
|
+
* @returns {object} fetch headers
|
|
9
|
+
*/
|
|
10
|
+
function getHeaders() {
|
|
11
|
+
const headers = {
|
|
12
|
+
Accept: 'application/vnd.github.v3+json',
|
|
13
|
+
'User-Agent': 'heflc-skill-cli',
|
|
14
|
+
};
|
|
15
|
+
if (config.githubToken) {
|
|
16
|
+
headers.Authorization = `token ${config.githubToken}`;
|
|
17
|
+
}
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 统一错误处理
|
|
23
|
+
* @param {Response} response - fetch response
|
|
24
|
+
* @param {string} context - 错误上下文描述
|
|
25
|
+
*/
|
|
26
|
+
async function handleResponse(response, context) {
|
|
27
|
+
if (response.ok) {
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (response.status === 404) {
|
|
32
|
+
throw new Error(`未找到:${context}。请用 heflc-skill list 查看可用的 skill。`);
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 403) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'GitHub API 请求频率超限。请稍后再试,或设置环境变量 GITHUB_TOKEN 提高限额。'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`GitHub API 请求失败 (${response.status}):${context}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 列出集中仓库中所有可用的 skill(顶层目录)
|
|
44
|
+
* @returns {Promise<string[]>} skill 名称数组
|
|
45
|
+
*/
|
|
46
|
+
export async function listRemoteSkills() {
|
|
47
|
+
const url = `${config.apiBase}/repos/${config.repoOwner}/${config.repoName}/contents/`;
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await fetch(url, { headers: getHeaders(), signal: AbortSignal.timeout(15000) });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error('无法连接 GitHub API,请检查网络连接或配置代理。');
|
|
53
|
+
}
|
|
54
|
+
const data = await handleResponse(response, '集中仓库');
|
|
55
|
+
|
|
56
|
+
return data
|
|
57
|
+
.filter((item) => item.type === 'dir')
|
|
58
|
+
.map((item) => item.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 下载指定 skill 的 SKILL.md 文件内容
|
|
63
|
+
* @param {string} skillName - skill 名称(即仓库中的目录名)
|
|
64
|
+
* @returns {Promise<string>} SKILL.md 的文本内容
|
|
65
|
+
*/
|
|
66
|
+
export async function downloadSkill(skillName) {
|
|
67
|
+
const url = `${config.apiBase}/repos/${config.repoOwner}/${config.repoName}/contents/${skillName}/SKILL.md`;
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(url, { headers: getHeaders(), signal: AbortSignal.timeout(15000) });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new Error('无法连接 GitHub API,请检查网络连接或配置代理。');
|
|
73
|
+
}
|
|
74
|
+
const data = await handleResponse(response, `skill "${skillName}"`);
|
|
75
|
+
|
|
76
|
+
// GitHub API 返回 Base64 编码的文件内容
|
|
77
|
+
const content = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
78
|
+
return content;
|
|
79
|
+
}
|