joyskills-cli 0.1.3 → 0.2.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 +76 -32
- package/package.json +1 -1
- package/src/commands/install.js +85 -67
- package/src/commands/sync.js +9 -13
package/README.md
CHANGED
|
@@ -19,29 +19,29 @@
|
|
|
19
19
|
|
|
20
20
|
### 🚀 团队协作增强(OpenSkills 缺失的)
|
|
21
21
|
- ✅ **版本锁定**:joySkills.lock 确保团队版本统一
|
|
22
|
-
- ✅ **团队 Registry
|
|
22
|
+
- ✅ **团队 Registry**:从 Git 仓库安装内部 skill
|
|
23
23
|
- ✅ **安全审计**:自动检测废弃和有风险的 skill
|
|
24
24
|
- ✅ **状态管理**:draft → review → approved → deprecated
|
|
25
|
-
- ✅ **使用追踪**:了解哪些 skill 被使用、被谁使用
|
|
26
25
|
|
|
27
26
|
## ✨ 核心特性
|
|
28
27
|
|
|
29
28
|
### 📦 公开 + 内部 Skill 统一管理
|
|
30
29
|
```bash
|
|
31
|
-
#
|
|
32
|
-
joySkills install
|
|
33
|
-
# → 安装到 .agent/skills/rc-onboarding
|
|
30
|
+
# 安装公开 skill
|
|
31
|
+
joySkills install anthropics/skills/pdf
|
|
34
32
|
|
|
35
|
-
#
|
|
36
|
-
joySkills install -
|
|
37
|
-
# → 安装到 ~/.agent/skills/pdf
|
|
33
|
+
# 安装内部 skill(需配置 Registry)
|
|
34
|
+
joySkills install team://your-team/rc-onboarding
|
|
38
35
|
|
|
39
|
-
#
|
|
40
|
-
joySkills sync
|
|
41
|
-
# 扫描: .agent/skills, ~/.agent/skills, .claude/skills, ~/.claude/skills
|
|
42
|
-
# 生成: AGENTS.md + joySkills.lock
|
|
36
|
+
# 统一同步
|
|
37
|
+
joySkills sync # 生成 AGENTS.md + joySkills.lock
|
|
43
38
|
```
|
|
44
39
|
|
|
40
|
+
**核心价值**:
|
|
41
|
+
- ✅ 公开 + 内部 skill 统一管理,一个命令搞定
|
|
42
|
+
- ✅ 版本锁定(joySkills.lock),团队环境一致
|
|
43
|
+
- ✅ 100% 兼容 OpenSkills,随时可回退
|
|
44
|
+
|
|
45
45
|
**路径设计**:
|
|
46
46
|
| 安装方式 | joySkills | OpenSkills |
|
|
47
47
|
|---------|-----------|------------|
|
|
@@ -105,30 +105,54 @@ joySkills list
|
|
|
105
105
|
|
|
106
106
|
### 场景 2:团队协作(内部 skill)
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
**痛点**:
|
|
109
|
+
- 😓 内部 skill 群里发 zip,手动下载 + 解压
|
|
110
|
+
- 😓 新人 onboarding,问“哪些 skill 必须装”
|
|
111
|
+
- 😓 skill 更新后,通知所有人重新下载
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
**解决方案**:
|
|
114
|
+
```bash
|
|
115
|
+
# 1. Tech Lead 搭建团队 Registry(Git 仓库)
|
|
116
|
+
mkdir team-skills && cd team-skills
|
|
117
|
+
mkdir rc-onboarding rc-commit-lint
|
|
118
|
+
# 将 SKILL.md 放入对应目录
|
|
119
|
+
git init && git add . && git commit -m "init"
|
|
120
|
+
git remote add origin https://git.yourcompany.com/team-skills.git
|
|
121
|
+
git push
|
|
122
|
+
|
|
123
|
+
# 2. 团队成员配置(一次)
|
|
124
|
+
cat > .joyskillrc << 'EOF'
|
|
125
|
+
{
|
|
114
126
|
"registries": [{
|
|
115
127
|
"id": "team://your-team",
|
|
116
|
-
"url": "https://
|
|
128
|
+
"url": "https://git.yourcompany.com/team-skills.git",
|
|
117
129
|
"type": "git"
|
|
118
130
|
}]
|
|
119
|
-
}
|
|
131
|
+
}
|
|
132
|
+
EOF
|
|
120
133
|
|
|
121
|
-
# 3.
|
|
134
|
+
# 3. 安装 skill(像 npm 一样)
|
|
122
135
|
joySkills install team://your-team/rc-onboarding
|
|
123
|
-
|
|
124
|
-
# 4. 生成 AGENTS.md + joySkills.lock
|
|
136
|
+
joySkills install team://your-team/rc-commit-lint
|
|
125
137
|
joySkills sync
|
|
126
138
|
|
|
127
|
-
#
|
|
128
|
-
git
|
|
129
|
-
|
|
139
|
+
# 4. 团队同步(自动化)
|
|
140
|
+
git add AGENTS.md joySkills.lock .joyskillrc
|
|
141
|
+
git commit -m "setup team skills"
|
|
142
|
+
git push
|
|
143
|
+
|
|
144
|
+
# 5. 新成员加入(1 分钟)
|
|
145
|
+
git clone <project-repo>
|
|
146
|
+
joySkills install # 自动安装所有锁定版本
|
|
130
147
|
```
|
|
131
148
|
|
|
149
|
+
**效果对比**:
|
|
150
|
+
| 场景 | 之前 | 现在 |
|
|
151
|
+
|------|------|------|
|
|
152
|
+
| 新人 onboarding | 群里找 8 个 zip,手动装(30分钟) | `joySkills install`(1分钟) |
|
|
153
|
+
| skill 更新 | 群发通知 + 手动更新(15分钟) | `git pull && joySkills install`(1分钟) |
|
|
154
|
+
| 版本不一致 | 频繁出现,难排查 | joySkills.lock 锁定,不会出现 |
|
|
155
|
+
|
|
132
156
|
### 场景 3:从 OpenSkills 迁移
|
|
133
157
|
|
|
134
158
|
```bash
|
|
@@ -152,7 +176,7 @@ joySkills sync
|
|
|
152
176
|
|
|
153
177
|
```bash
|
|
154
178
|
joySkills sync
|
|
155
|
-
# ✅ 扫描 skills/
|
|
179
|
+
# ✅ 扫描 .agent/skills, ~/.agent/skills, .claude/skills, ~/.claude/skills
|
|
156
180
|
# ✅ 生成 AGENTS.md(OpenSkills 兼容格式)
|
|
157
181
|
# ✅ 生成 joySkills.lock(版本锁定)
|
|
158
182
|
# ✅ 版本一致性检查
|
|
@@ -223,23 +247,43 @@ joySkills status
|
|
|
223
247
|
|
|
224
248
|
## 📖 核心概念
|
|
225
249
|
|
|
226
|
-
### Skill Registry
|
|
250
|
+
### Skill Registry(团队 skill 仓库)
|
|
227
251
|
|
|
228
|
-
Registry
|
|
252
|
+
Registry 是团队统一管理 skill 的 Git 仓库,像 npm 私有源一样。
|
|
229
253
|
|
|
254
|
+
**目录结构**:
|
|
255
|
+
```
|
|
256
|
+
team-skills/ # Git 仓库
|
|
257
|
+
├── registry.yaml # 版本配置(可选)
|
|
258
|
+
├── rc-onboarding/
|
|
259
|
+
│ ├── SKILL.md # 必须
|
|
260
|
+
│ ├── scripts/ # 可选:脚本
|
|
261
|
+
│ └── templates/ # 可选:模板
|
|
262
|
+
└── rc-commit-lint/
|
|
263
|
+
└── SKILL.md
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**registry.yaml 示例**(可选,用于版本管理):
|
|
230
267
|
```yaml
|
|
231
268
|
registryVersion: 1
|
|
232
269
|
registryId: team://your-team
|
|
233
270
|
skills:
|
|
234
|
-
- id:
|
|
235
|
-
name: "
|
|
236
|
-
visibility: public
|
|
271
|
+
- id: rc-onboarding
|
|
272
|
+
name: "搜推团队 Onboarding"
|
|
273
|
+
visibility: internal # internal / public / restricted
|
|
237
274
|
versions:
|
|
238
275
|
- version: 1.2.0
|
|
239
|
-
state: approved
|
|
276
|
+
state: approved # draft / pending_review / approved / deprecated
|
|
240
277
|
recommended: true
|
|
278
|
+
- version: 1.1.0
|
|
279
|
+
state: deprecated
|
|
241
280
|
```
|
|
242
281
|
|
|
282
|
+
**使用场景**:
|
|
283
|
+
- ✅ 内部业务 skill(如 rc-onboarding、rc-commit-lint)
|
|
284
|
+
- ✅ 公司级通用 skill(如 code-review、test-helper)
|
|
285
|
+
- ✅ 项目级定制 skill(如 project-specific-tools)
|
|
286
|
+
|
|
243
287
|
### joySkills.lock(锁文件)
|
|
244
288
|
|
|
245
289
|
项目级锁文件,记录实际使用的 skill 版本:
|
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -74,6 +74,13 @@ async function installPublicSkill(skillName, options, localManager, lockfileMana
|
|
|
74
74
|
console.log(`💡 Using openskills install...\n`);
|
|
75
75
|
|
|
76
76
|
try {
|
|
77
|
+
// 检查 openskills 是否可用
|
|
78
|
+
try {
|
|
79
|
+
execSync('npx openskills --version', { stdio: 'pipe' });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error('openskills not found. Please install: npm install -g openskills');
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// 调用 openskills install
|
|
78
85
|
const cmd = `npx openskills install ${skillName}`;
|
|
79
86
|
execSync(cmd, { stdio: 'inherit' });
|
|
@@ -101,88 +108,99 @@ async function installPublicSkill(skillName, options, localManager, lockfileMana
|
|
|
101
108
|
async function installTeamSkill(skillName, options, projectRoot, localManager, lockfileManager) {
|
|
102
109
|
console.log(`🏢 Installing team skill: ${skillName}`);
|
|
103
110
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
registryManager = new RegistryManager(registryPath);
|
|
111
|
-
await registryManager.load();
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.error('Warning: Could not load registry:', error.message);
|
|
114
|
-
}
|
|
111
|
+
// 解析 team:// URL
|
|
112
|
+
const teamUrl = skillName.replace(/^team:\/\//, '');
|
|
113
|
+
const parts = teamUrl.split('/');
|
|
114
|
+
if (parts.length !== 2) {
|
|
115
|
+
throw new Error('Invalid team skill format. Expected: team://registry-name/skill-name');
|
|
115
116
|
}
|
|
117
|
+
const [registryName, skillId] = parts;
|
|
116
118
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!options.version) {
|
|
123
|
-
// Get recommended version from registry
|
|
124
|
-
const recommended = registryManager.getRecommendedVersion(skillName);
|
|
125
|
-
if (recommended) {
|
|
126
|
-
targetVersion = recommended.version;
|
|
127
|
-
versionSource = 'registry';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Validate version
|
|
132
|
-
const validation = registryManager.validateVersion(skillName, targetVersion);
|
|
133
|
-
if (!validation.valid && !options.force) {
|
|
134
|
-
console.error(`❌ Version validation failed: ${validation.error}`);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
119
|
+
// 读取配置文件
|
|
120
|
+
const configPath = path.join(projectRoot, '.joyskillrc');
|
|
121
|
+
if (!fs.existsSync(configPath)) {
|
|
122
|
+
throw new Error('.joyskillrc not found. Please configure team registry first.');
|
|
123
|
+
}
|
|
137
124
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
125
|
+
let config;
|
|
126
|
+
try {
|
|
127
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`Failed to parse .joyskillrc: ${error.message}`);
|
|
130
|
+
}
|
|
143
131
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
// 查找 registry 配置
|
|
133
|
+
const registry = config.registries?.find(r => r.id === `team://${registryName}`);
|
|
134
|
+
if (!registry) {
|
|
135
|
+
throw new Error(`Registry "team://${registryName}" not found in .joyskillrc`);
|
|
147
136
|
}
|
|
148
137
|
|
|
149
|
-
|
|
150
|
-
const skillMdContent = `---
|
|
151
|
-
name: ${skillName}
|
|
152
|
-
description: Auto-generated skill for ${skillName}
|
|
153
|
-
version: ${targetVersion}
|
|
154
|
-
---
|
|
138
|
+
console.log(`📦 Fetching from ${registry.url}...`);
|
|
155
139
|
|
|
156
|
-
|
|
140
|
+
// 临时目录
|
|
141
|
+
const tmpDir = path.join(projectRoot, '.joyskill', 'tmp', `${Date.now()}`);
|
|
142
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
157
143
|
|
|
158
|
-
|
|
144
|
+
try {
|
|
145
|
+
// Git clone
|
|
146
|
+
console.log('🔄 Cloning registry...');
|
|
147
|
+
execSync(`git clone --depth 1 ${registry.url} ${tmpDir}`, {
|
|
148
|
+
stdio: 'pipe',
|
|
149
|
+
encoding: 'utf-8'
|
|
150
|
+
});
|
|
159
151
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
// 检查 skill 是否存在
|
|
153
|
+
const skillPath = path.join(tmpDir, skillId);
|
|
154
|
+
if (!fs.existsSync(skillPath)) {
|
|
155
|
+
throw new Error(`Skill "${skillId}" not found in registry`);
|
|
156
|
+
}
|
|
163
157
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
158
|
+
// 检查 SKILL.md
|
|
159
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
160
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
161
|
+
throw new Error(`SKILL.md not found for "${skillId}"`);
|
|
162
|
+
}
|
|
168
163
|
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
// 读取 SKILL.md 内容
|
|
165
|
+
const skillMdContent = fs.readFileSync(skillMdPath, 'utf-8');
|
|
166
|
+
|
|
167
|
+
// 安装 skill
|
|
168
|
+
localManager.installSkill(skillId, skillMdContent);
|
|
169
|
+
|
|
170
|
+
// 复制其他文件(如果有)
|
|
171
|
+
const skillFiles = fs.readdirSync(skillPath);
|
|
172
|
+
const targetDir = path.join(localManager.getSkillsDir(), skillId);
|
|
173
|
+
|
|
174
|
+
for (const file of skillFiles) {
|
|
175
|
+
if (file === 'SKILL.md') continue;
|
|
176
|
+
const srcFile = path.join(skillPath, file);
|
|
177
|
+
const destFile = path.join(targetDir, file);
|
|
178
|
+
|
|
179
|
+
if (fs.statSync(srcFile).isDirectory()) {
|
|
180
|
+
fs.cpSync(srcFile, destFile, { recursive: true });
|
|
181
|
+
} else {
|
|
182
|
+
fs.copyFileSync(srcFile, destFile);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
171
185
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
// 更新 lockfile
|
|
187
|
+
lockfileManager.updateSkill(skillId, {
|
|
188
|
+
version: options.version || 'latest',
|
|
189
|
+
source: 'team',
|
|
190
|
+
registry: `team://${registryName}`,
|
|
191
|
+
installedAt: new Date().toISOString()
|
|
192
|
+
});
|
|
179
193
|
|
|
180
|
-
|
|
194
|
+
await lockfileManager.save();
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
console.log(`✅ Successfully installed ${skillId}`);
|
|
197
|
+
console.log(`💡 Run 'joySkills sync' to update AGENTS.md`);
|
|
183
198
|
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
} finally {
|
|
200
|
+
// 清理临时目录
|
|
201
|
+
if (fs.existsSync(tmpDir)) {
|
|
202
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
203
|
+
}
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
|
package/src/commands/sync.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import * as YAML from 'yaml';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -122,20 +123,15 @@ function parseSkillMetadata(content) {
|
|
|
122
123
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
123
124
|
if (!match) return {};
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const value = valueParts.join(':').trim().replace(/^["']|["']$/g, '');
|
|
135
|
-
metadata[key.trim()] = value;
|
|
126
|
+
const yamlContent = match[1];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// 使用 yaml 包解析(支持完整 YAML 语法)
|
|
130
|
+
return YAML.parse(yamlContent) || {};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn(`⚠️ YAML 解析失败: ${error.message}`);
|
|
133
|
+
return {};
|
|
136
134
|
}
|
|
137
|
-
|
|
138
|
-
return metadata;
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
/**
|