sc-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 +61 -0
- package/dist/cli.js +106 -0
- package/dist/config.js +19 -0
- package/dist/git.js +29 -0
- package/dist/installer.js +56 -0
- package/dist/manager.js +69 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# skill-sync
|
|
2
|
+
|
|
3
|
+
团队技能同步工具 - 使用软链接管理 Claude Code 技能
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 🔄 从团队仓库同步技能到本地缓存
|
|
8
|
+
- 🎯 交互式勾选启用/禁用技能
|
|
9
|
+
- 🔗 使用软链接方式,不直接修改技能文件
|
|
10
|
+
- 📦 支持多平台 (Windows/macOS/Linux)
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm build
|
|
17
|
+
npm link
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 使用
|
|
21
|
+
|
|
22
|
+
### 1. 同步技能到缓存
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
skill-sync sync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
从团队仓库 `https://codeup.aliyun.com/sczlcq/skills.git` 拉取最新技能到 `~/.skill-sync/cache`
|
|
29
|
+
|
|
30
|
+
### 2. 交互式管理技能
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
skill-sync toggle
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
使用空格键勾选/取消勾选技能,回车确认。选中的技能会通过软链接启用到 `~/.claude/skills`
|
|
37
|
+
|
|
38
|
+
### 3. 查看技能状态
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
skill-sync list
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
显示所有缓存的技能及其启用状态
|
|
45
|
+
|
|
46
|
+
## 工作原理
|
|
47
|
+
|
|
48
|
+
1. **缓存区**: 技能存储在 `~/.skill-sync/cache`,通过 git 管理
|
|
49
|
+
2. **软链接**: 启用的技能通过软链接指向缓存区
|
|
50
|
+
3. **目标目录**: `~/.claude/skills` 只包含软链接,不存储实际文件
|
|
51
|
+
|
|
52
|
+
## 配置
|
|
53
|
+
|
|
54
|
+
配置文件位置: `~/.skill-sync/config.json`
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"repo": "https://codeup.aliyun.com/sczlcq/skills.git",
|
|
59
|
+
"lastSync": "2026-03-10T14:00:00.000Z"
|
|
60
|
+
}
|
|
61
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { GitRepo } from './git.js';
|
|
5
|
+
import { SkillManager } from './manager.js';
|
|
6
|
+
import { DEFAULT_CONFIG, getConfigPath } from './config.js';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name('skill-sync')
|
|
11
|
+
.description('团队技能同步工具')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
program
|
|
14
|
+
.command('sync')
|
|
15
|
+
.description('从团队仓库同步技能')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
try {
|
|
18
|
+
console.log('🔄 正在同步技能...');
|
|
19
|
+
const config = await loadConfig();
|
|
20
|
+
const git = new GitRepo();
|
|
21
|
+
await git.clone(config.repo);
|
|
22
|
+
console.log('✓ 已拉取最新技能到缓存');
|
|
23
|
+
await saveLastSync();
|
|
24
|
+
console.log('✅ 同步完成');
|
|
25
|
+
console.log('\n💡 运行 skill-sync toggle 来启用/禁用技能');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error('❌ 同步失败:', error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command('toggle')
|
|
34
|
+
.description('交互式启用/禁用技能')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
try {
|
|
37
|
+
const manager = new SkillManager();
|
|
38
|
+
const available = await manager.getAvailableSkills();
|
|
39
|
+
const enabled = await manager.getEnabledSkills();
|
|
40
|
+
if (available.length === 0) {
|
|
41
|
+
console.log('❌ 缓存中没有技能,请先运行 skill-sync sync');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const answers = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'checkbox',
|
|
47
|
+
name: 'skills',
|
|
48
|
+
message: '选择要启用的技能 (空格选择/取消):',
|
|
49
|
+
choices: available.map((skill) => ({
|
|
50
|
+
name: skill,
|
|
51
|
+
checked: enabled.includes(skill),
|
|
52
|
+
})),
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
const selected = answers.skills;
|
|
56
|
+
const toEnable = selected.filter((s) => !enabled.includes(s));
|
|
57
|
+
const toDisable = enabled.filter((s) => !selected.includes(s));
|
|
58
|
+
if (toEnable.length > 0) {
|
|
59
|
+
await manager.enableSkills(toEnable);
|
|
60
|
+
console.log(`✅ 已启用 ${toEnable.length} 个技能`);
|
|
61
|
+
}
|
|
62
|
+
if (toDisable.length > 0) {
|
|
63
|
+
await manager.disableSkills(toDisable);
|
|
64
|
+
console.log(`❌ 已禁用 ${toDisable.length} 个技能`);
|
|
65
|
+
}
|
|
66
|
+
if (toEnable.length === 0 && toDisable.length === 0) {
|
|
67
|
+
console.log('✨ 没有变化');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error('❌ 操作失败:', error);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
program
|
|
76
|
+
.command('list')
|
|
77
|
+
.description('列出技能状态')
|
|
78
|
+
.action(async () => {
|
|
79
|
+
const manager = new SkillManager();
|
|
80
|
+
const available = await manager.getAvailableSkills();
|
|
81
|
+
const enabled = await manager.getEnabledSkills();
|
|
82
|
+
if (available.length === 0) {
|
|
83
|
+
console.log('缓存中没有技能,请先运行 skill-sync sync');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log(`\n📦 缓存中的技能 (${available.length}):`);
|
|
87
|
+
available.forEach((skill) => {
|
|
88
|
+
const status = enabled.includes(skill) ? '✅' : '⬜';
|
|
89
|
+
console.log(` ${status} ${skill}`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
program.parse();
|
|
93
|
+
async function loadConfig() {
|
|
94
|
+
const configPath = getConfigPath();
|
|
95
|
+
if (await fs.pathExists(configPath)) {
|
|
96
|
+
return await fs.readJson(configPath);
|
|
97
|
+
}
|
|
98
|
+
return DEFAULT_CONFIG;
|
|
99
|
+
}
|
|
100
|
+
async function saveLastSync() {
|
|
101
|
+
const configPath = getConfigPath();
|
|
102
|
+
await fs.ensureDir(configPath.replace(/[^/\\]+$/, ''));
|
|
103
|
+
const config = await loadConfig();
|
|
104
|
+
config.lastSync = new Date().toISOString();
|
|
105
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
106
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const DEFAULT_REPO = 'https://codeup.aliyun.com/sczlcq/skills.git';
|
|
4
|
+
export const getSkillsDir = () => {
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
return path.join(home, '.claude', 'skills');
|
|
7
|
+
};
|
|
8
|
+
export const getCacheDir = () => {
|
|
9
|
+
const home = os.homedir();
|
|
10
|
+
return path.join(home, '.skill-sync', 'cache');
|
|
11
|
+
};
|
|
12
|
+
export const getConfigPath = () => {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
return path.join(home, '.skill-sync', 'config.json');
|
|
15
|
+
};
|
|
16
|
+
export const DEFAULT_CONFIG = {
|
|
17
|
+
repo: DEFAULT_REPO,
|
|
18
|
+
disabledSkills: [],
|
|
19
|
+
};
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { getCacheDir } from './config.js';
|
|
4
|
+
export class GitRepo {
|
|
5
|
+
git;
|
|
6
|
+
cacheDir;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.cacheDir = getCacheDir();
|
|
9
|
+
this.git = simpleGit();
|
|
10
|
+
}
|
|
11
|
+
async clone(repoUrl) {
|
|
12
|
+
await fs.ensureDir(this.cacheDir);
|
|
13
|
+
if (await fs.pathExists(this.cacheDir)) {
|
|
14
|
+
const files = await fs.readdir(this.cacheDir);
|
|
15
|
+
if (files.length > 0) {
|
|
16
|
+
await this.pull();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await this.git.clone(repoUrl, this.cacheDir);
|
|
21
|
+
}
|
|
22
|
+
async pull() {
|
|
23
|
+
const repoGit = simpleGit(this.cacheDir);
|
|
24
|
+
await repoGit.pull();
|
|
25
|
+
}
|
|
26
|
+
getCachePath() {
|
|
27
|
+
return this.cacheDir;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getSkillsDir } from './config.js';
|
|
4
|
+
export class SkillInstaller {
|
|
5
|
+
targetDir;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.targetDir = getSkillsDir();
|
|
8
|
+
}
|
|
9
|
+
async install(sourceDir) {
|
|
10
|
+
await fs.ensureDir(this.targetDir);
|
|
11
|
+
// 备份现有技能
|
|
12
|
+
const backupDir = `${this.targetDir}.backup.${Date.now()}`;
|
|
13
|
+
if (await fs.pathExists(this.targetDir)) {
|
|
14
|
+
await fs.copy(this.targetDir, backupDir);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
// 复制所有内容(目录和文件)
|
|
18
|
+
await fs.copy(sourceDir, this.targetDir, {
|
|
19
|
+
overwrite: true,
|
|
20
|
+
filter: (src) => {
|
|
21
|
+
// 跳过 .git 目录
|
|
22
|
+
if (src.includes('.git'))
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// 恢复备份
|
|
30
|
+
if (await fs.pathExists(backupDir)) {
|
|
31
|
+
await fs.remove(this.targetDir);
|
|
32
|
+
await fs.move(backupDir, this.targetDir);
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
// 清理备份
|
|
37
|
+
if (await fs.pathExists(backupDir)) {
|
|
38
|
+
await fs.remove(backupDir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async list() {
|
|
42
|
+
if (!(await fs.pathExists(this.targetDir))) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const items = await fs.readdir(this.targetDir);
|
|
46
|
+
const skills = [];
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
const itemPath = path.join(this.targetDir, item);
|
|
49
|
+
const stat = await fs.stat(itemPath);
|
|
50
|
+
if (stat.isDirectory() && !item.startsWith('.')) {
|
|
51
|
+
skills.push(item);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return skills;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getSkillsDir, getCacheDir } from './config.js';
|
|
4
|
+
export class SkillManager {
|
|
5
|
+
targetDir;
|
|
6
|
+
cacheDir;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.targetDir = getSkillsDir();
|
|
9
|
+
this.cacheDir = getCacheDir();
|
|
10
|
+
}
|
|
11
|
+
async getAvailableSkills() {
|
|
12
|
+
if (!(await fs.pathExists(this.cacheDir))) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const items = await fs.readdir(this.cacheDir);
|
|
16
|
+
const skills = [];
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
if (item.startsWith('.'))
|
|
19
|
+
continue;
|
|
20
|
+
const itemPath = path.join(this.cacheDir, item);
|
|
21
|
+
const stat = await fs.stat(itemPath);
|
|
22
|
+
if (stat.isDirectory()) {
|
|
23
|
+
skills.push(item);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return skills.sort();
|
|
27
|
+
}
|
|
28
|
+
async getEnabledSkills() {
|
|
29
|
+
if (!(await fs.pathExists(this.targetDir))) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const items = await fs.readdir(this.targetDir);
|
|
33
|
+
const enabled = [];
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (item.startsWith('.'))
|
|
36
|
+
continue;
|
|
37
|
+
const itemPath = path.join(this.targetDir, item);
|
|
38
|
+
try {
|
|
39
|
+
const stat = await fs.lstat(itemPath);
|
|
40
|
+
if (stat.isSymbolicLink() || stat.isDirectory()) {
|
|
41
|
+
enabled.push(item);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
// 忽略错误
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return enabled.sort();
|
|
49
|
+
}
|
|
50
|
+
async enableSkills(skillNames) {
|
|
51
|
+
await fs.ensureDir(this.targetDir);
|
|
52
|
+
for (const skill of skillNames) {
|
|
53
|
+
const sourcePath = path.join(this.cacheDir, skill);
|
|
54
|
+
const targetPath = path.join(this.targetDir, skill);
|
|
55
|
+
if (await fs.pathExists(targetPath)) {
|
|
56
|
+
await fs.remove(targetPath);
|
|
57
|
+
}
|
|
58
|
+
await fs.symlink(sourcePath, targetPath, 'junction');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async disableSkills(skillNames) {
|
|
62
|
+
for (const skill of skillNames) {
|
|
63
|
+
const targetPath = path.join(this.targetDir, skill);
|
|
64
|
+
if (await fs.pathExists(targetPath)) {
|
|
65
|
+
await fs.remove(targetPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sc-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "团队技能同步工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sc": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"start": "node dist/cli.js",
|
|
18
|
+
"prepublishOnly": "pnpm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"skills",
|
|
23
|
+
"sync",
|
|
24
|
+
"team",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"author": "red-k",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/yourusername/skill-sync.git"
|
|
32
|
+
},
|
|
33
|
+
"packageManager": "pnpm@10.30.2",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^12.0.0",
|
|
36
|
+
"fs-extra": "^11.2.0",
|
|
37
|
+
"simple-git": "^3.22.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/fs-extra": "^11.0.4",
|
|
41
|
+
"@types/inquirer": "^9.0.9",
|
|
42
|
+
"@types/node": "^20.11.0",
|
|
43
|
+
"inquirer": "^13.3.0",
|
|
44
|
+
"typescript": "^5.3.3"
|
|
45
|
+
}
|
|
46
|
+
}
|