joyskills-cli 0.3.0 → 0.3.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/README.md +22 -238
- package/package.json +1 -1
- package/src/commands/install.js +1 -1
- package/src/commands/update.js +233 -27
- package/src/commands/upgrade.js +103 -13
package/README.md
CHANGED
|
@@ -1,256 +1,40 @@
|
|
|
1
|
-
# joySkills
|
|
1
|
+
# joySkills CLI
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 团队级 Skill 治理,像 npm 一样简单
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/joyskills-cli)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
## 一句话介绍
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
joySkills 帮你把 Skill 安装到正确目录、锁定版本、团队分发。100% 兼容 Claude Skills。
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- ❌ **版本混乱**:不同项目使用不同版本的 skill,难以统一升级
|
|
15
|
-
- ❌ **安全风险**:无法及时发现和处理已废弃或有风险的 skill 版本
|
|
16
|
-
- ❌ **缺乏治理**:谁在用什么 skill?如何审批新版本?
|
|
17
|
-
- ❌ **协作困难**:团队成员各自维护 skill,无法共享和复用
|
|
18
|
-
|
|
19
|
-
joySkills 提供了一套**完整的团队级 Skill 治理方案**,同时保持与现有生态的 100% 兼容。
|
|
20
|
-
|
|
21
|
-
## ✨ 核心特性
|
|
22
|
-
|
|
23
|
-
### 📦 统一 Skill 源(Registry)
|
|
24
|
-
- 团队/组织级 Skill 仓库
|
|
25
|
-
- 基于 Git 的版本管理
|
|
26
|
-
- 支持私有部署和权限控制
|
|
27
|
-
|
|
28
|
-
### 🔒 版本锁定(joySkills.lock)
|
|
29
|
-
- 项目级版本锁定,确保环境一致
|
|
30
|
-
- 自动记录安装历史
|
|
31
|
-
- 支持 semver 版本策略
|
|
32
|
-
|
|
33
|
-
### ✅ 发布审批流程
|
|
34
|
-
- `draft` → `pending_review` → `approved` → `deprecated` → `archived`
|
|
35
|
-
- 完整的状态机管理
|
|
36
|
-
- CI/CD 友好
|
|
37
|
-
|
|
38
|
-
### 🔐 权限与可见性
|
|
39
|
-
- `public` / `restricted` / `internal` 三级可见性
|
|
40
|
-
- 基于角色的访问控制(RBAC)
|
|
41
|
-
- 与 Git 权限无缝集成
|
|
42
|
-
|
|
43
|
-
### 📊 使用追踪与审计
|
|
44
|
-
- 实时监控 skill 使用情况
|
|
45
|
-
- 安全风险预警
|
|
46
|
-
- 自动生成升级建议
|
|
47
|
-
|
|
48
|
-
### 🤝 100% 兼容现有生态
|
|
49
|
-
- 兼容 Claude Code / Cursor / Windsurf / Aider
|
|
50
|
-
- 不改变 `skills/` 目录结构
|
|
51
|
-
- 不改变 `SKILL.md` 文件格式
|
|
52
|
-
|
|
53
|
-
## 🚀 快速开始
|
|
54
|
-
|
|
55
|
-
### 安装
|
|
56
|
-
|
|
57
|
-
joySkills 支持**全局安装**和**项目级安装**两种方式:
|
|
11
|
+
## 30 秒上手
|
|
58
12
|
|
|
59
13
|
```bash
|
|
60
|
-
#
|
|
14
|
+
# 安装
|
|
61
15
|
npm install -g joyskills-cli
|
|
62
|
-
joySkills --version
|
|
63
|
-
|
|
64
|
-
# 或:项目级安装
|
|
65
|
-
npm install joyskills-cli
|
|
66
|
-
npx joySkills --version
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 5 分钟上手
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
# 1. 进入你的项目目录
|
|
73
|
-
cd your-project
|
|
74
|
-
|
|
75
|
-
# 2. 安装一个 skill
|
|
76
|
-
joySkills install web-search
|
|
77
|
-
# 或使用 npx(如果是项目级安装)
|
|
78
|
-
npx joySkills install web-search
|
|
79
|
-
|
|
80
|
-
# 3. 查看已安装的 skills
|
|
81
|
-
joySkills list
|
|
82
16
|
|
|
83
|
-
#
|
|
84
|
-
joySkills
|
|
17
|
+
# 安装一个 Skill(默认到 .joycode/skills/)
|
|
18
|
+
joySkills install pdf
|
|
85
19
|
|
|
86
|
-
#
|
|
87
|
-
joySkills audit
|
|
20
|
+
# 完成!打开 JoyCode/Claude Code 即可使用
|
|
88
21
|
```
|
|
89
22
|
|
|
90
|
-
|
|
23
|
+
## 核心能力
|
|
91
24
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- `-c, --category <category>` - 按分类过滤
|
|
100
|
-
- `-s, --search <query>` - 搜索 skill 名称或描述
|
|
101
|
-
- `-i, --installed` - 只显示已安装的 skills
|
|
102
|
-
- `-l, --local` - 只显示本地 skills
|
|
103
|
-
- `-r, --registry <name>` - 显示指定 registry 的 skills
|
|
104
|
-
|
|
105
|
-
### `joySkills install <skill>`
|
|
106
|
-
安装一个 skill
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
# 安装最新版本
|
|
110
|
-
joySkills install web-search
|
|
111
|
-
|
|
112
|
-
# 安装指定版本
|
|
113
|
-
joySkills install web-search@1.0.0
|
|
114
|
-
|
|
115
|
-
# 从指定 registry 安装
|
|
116
|
-
joySkills install web-search --registry my-team
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### `joySkills remove <skill>`
|
|
120
|
-
删除已安装的 skill
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
joySkills remove web-search
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### `joySkills team <subcommand>`
|
|
127
|
-
管理团队 registry
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
# 添加团队 registry
|
|
131
|
-
joySkills team add my-team /path/to/registry
|
|
132
|
-
|
|
133
|
-
# 列出所有 registries
|
|
134
|
-
joySkills team list
|
|
135
|
-
|
|
136
|
-
# 删除 registry
|
|
137
|
-
joySkills team remove my-team
|
|
138
|
-
```
|
|
25
|
+
| 场景 | 命令 |
|
|
26
|
+
|------|------|
|
|
27
|
+
| 安装 Skill | `joySkills install <skill>` |
|
|
28
|
+
| 查看已安装 | `joySkills list` |
|
|
29
|
+
| 检查更新 | `joySkills check` |
|
|
30
|
+
| 一键更新 | `joySkills update` |
|
|
31
|
+
| 团队共享 | `joySkills team add <name> <git-url>` |
|
|
139
32
|
|
|
140
|
-
|
|
141
|
-
显示当前项目的 skills 状态
|
|
33
|
+
## 文档
|
|
142
34
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### `joySkills audit`
|
|
148
|
-
审计已安装 skills 的安全性和弃用状态
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
joySkills audit
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## 📖 核心概念
|
|
155
|
-
|
|
156
|
-
### Skill Registry(技能注册表)
|
|
157
|
-
|
|
158
|
-
Registry 是团队统一管理的 Skill 仓库,通常是一个 Git 仓库 + `registry.yaml` 索引文件:
|
|
159
|
-
|
|
160
|
-
```yaml
|
|
161
|
-
registryVersion: 1
|
|
162
|
-
registryId: team://your-team
|
|
163
|
-
skills:
|
|
164
|
-
- id: web-search
|
|
165
|
-
name: "Web 搜索"
|
|
166
|
-
visibility: public
|
|
167
|
-
versions:
|
|
168
|
-
- version: 1.2.0
|
|
169
|
-
state: approved
|
|
170
|
-
recommended: true
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### joySkills.lock(锁文件)
|
|
174
|
-
|
|
175
|
-
项目级锁文件,记录实际使用的 skill 版本:
|
|
176
|
-
|
|
177
|
-
```yaml
|
|
178
|
-
lockVersion: 1
|
|
179
|
-
projectId: "your-project"
|
|
180
|
-
skills:
|
|
181
|
-
web-search:
|
|
182
|
-
version: 1.2.0
|
|
183
|
-
registry: team://your-team
|
|
184
|
-
installedAt: 2026-02-02T10:00:00Z
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### 版本状态机
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
draft → pending_review → approved → deprecated → archived
|
|
191
|
-
↓ ↓ ↓ ↓ ↓
|
|
192
|
-
开发中 待审核 可用 不推荐 归档
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 🏗️ 项目结构
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
joySkills/
|
|
199
|
-
├── src/
|
|
200
|
-
│ ├── index.js # CLI 入口点
|
|
201
|
-
│ ├── types.js # 类型定义
|
|
202
|
-
│ ├── registry.js # Registry 管理
|
|
203
|
-
│ ├── lockfile.js # 锁文件操作
|
|
204
|
-
│ ├── local.js # 本地 skill 管理
|
|
205
|
-
│ └── commands/ # CLI 命令
|
|
206
|
-
│ ├── list.js # 列出 skills
|
|
207
|
-
│ ├── install.js # 安装 skill
|
|
208
|
-
│ ├── remove.js # 删除 skill
|
|
209
|
-
│ ├── team.js # Registry 管理
|
|
210
|
-
│ ├── status.js # 状态检查
|
|
211
|
-
│ └── audit.js # 安全审计
|
|
212
|
-
├── spec/ # 规范文档
|
|
213
|
-
│ ├── cli-spec.md # CLI 规范
|
|
214
|
-
│ ├── registry-spec.md # Registry 规范
|
|
215
|
-
│ └── lockfile-spec.md # 锁文件规范
|
|
216
|
-
├── test/ # 测试文件
|
|
217
|
-
├── .joyskill/ # 示例 registry
|
|
218
|
-
├── package.json
|
|
219
|
-
├── INSTALL.md
|
|
220
|
-
└── README.md
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## 🤝 贡献指南
|
|
224
|
-
|
|
225
|
-
### 开发环境搭建
|
|
226
|
-
|
|
227
|
-
```bash
|
|
228
|
-
# 克隆仓库
|
|
229
|
-
git clone https://coding.jd.com/rc-ai/joyskill.git
|
|
230
|
-
cd joyskill
|
|
231
|
-
|
|
232
|
-
# 安装依赖
|
|
233
|
-
npm install
|
|
234
|
-
|
|
235
|
-
# 本地开发链接
|
|
236
|
-
npm link
|
|
237
|
-
|
|
238
|
-
# 测试命令
|
|
239
|
-
joySkills --version
|
|
240
|
-
joySkills list
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### 运行测试
|
|
244
|
-
|
|
245
|
-
```bash
|
|
246
|
-
# 运行 registry 测试
|
|
247
|
-
node test/test-registry.js
|
|
248
|
-
|
|
249
|
-
# 测试 CLI 命令
|
|
250
|
-
joySkills list --search "test"
|
|
251
|
-
joySkills list --category "utility"
|
|
252
|
-
```
|
|
35
|
+
- [📖 完整文档](./docs/yuanshi.md) - 原理、使用场景、最佳实践
|
|
36
|
+
- [📋 详细说明](./docs/详细说明.md) - 所有命令和选项
|
|
253
37
|
|
|
254
|
-
##
|
|
38
|
+
## License
|
|
255
39
|
|
|
256
|
-
MIT
|
|
40
|
+
MIT
|
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -688,7 +688,7 @@ async function installFromLockfile(targetDir, projectRoot) {
|
|
|
688
688
|
console.log(chalk.green(`\n✅ All skills installed`));
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
-
async function findSkills(basePath) {
|
|
691
|
+
export async function findSkills(basePath) {
|
|
692
692
|
const skills = [];
|
|
693
693
|
|
|
694
694
|
async function scan(dir, relativePath = '') {
|
package/src/commands/update.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { getUpdatableSkills } from '../version-checker.js';
|
|
7
7
|
import { SkillLoader } from '../skill-loader.js';
|
|
8
|
+
import { LockfileManager } from '../lockfile.js';
|
|
8
9
|
import simpleGit from 'simple-git';
|
|
9
10
|
import chalk from 'chalk';
|
|
10
11
|
import { confirm } from '@inquirer/prompts';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
11
15
|
|
|
12
16
|
export function updateCommand(program) {
|
|
13
17
|
program
|
|
@@ -26,30 +30,11 @@ export function updateCommand(program) {
|
|
|
26
30
|
let skillsToUpdate = [];
|
|
27
31
|
|
|
28
32
|
if (options.all || skillNames.length === 0) {
|
|
29
|
-
// 更新所有有更新的 skills
|
|
30
|
-
|
|
31
|
-
skillsToUpdate = updatable;
|
|
33
|
+
// 更新所有有更新的 skills(Git + team://)
|
|
34
|
+
skillsToUpdate = await getAllUpdatableSkills(projectRoot);
|
|
32
35
|
} else {
|
|
33
36
|
// 更新指定的 skills
|
|
34
|
-
|
|
35
|
-
for (const name of skillNames) {
|
|
36
|
-
const skill = loader.getSkill(name);
|
|
37
|
-
if (skill) {
|
|
38
|
-
const git = simpleGit(skill.path);
|
|
39
|
-
const isRepo = await git.checkIsRepo();
|
|
40
|
-
if (isRepo) {
|
|
41
|
-
skillsToUpdate.push({
|
|
42
|
-
name: skill.name,
|
|
43
|
-
path: skill.path,
|
|
44
|
-
currentVersion: skill.version,
|
|
45
|
-
});
|
|
46
|
-
} else {
|
|
47
|
-
console.log(chalk.yellow(`⚠️ ${name} is not a git repository, skipping`));
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
console.log(chalk.red(`❌ Skill not found: ${name}`));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
37
|
+
skillsToUpdate = await getSpecifiedSkills(projectRoot, skillNames);
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
if (skillsToUpdate.length === 0) {
|
|
@@ -83,12 +68,16 @@ export function updateCommand(program) {
|
|
|
83
68
|
const results = [];
|
|
84
69
|
for (const skill of skillsToUpdate) {
|
|
85
70
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
if (skill.type === 'team') {
|
|
72
|
+
// team:// 类型:重新安装
|
|
73
|
+
await updateTeamSkill(skill, projectRoot);
|
|
74
|
+
} else {
|
|
75
|
+
// Git 类型:pull
|
|
76
|
+
const git = simpleGit(skill.path);
|
|
77
|
+
await git.pull('origin', 'main', ['--depth', '1']);
|
|
78
|
+
}
|
|
90
79
|
|
|
91
|
-
console.log(chalk.green(`✅ ${skill.name} updated
|
|
80
|
+
console.log(chalk.green(`✅ ${skill.name} updated to v${skill.latestVersion || 'latest'}`));
|
|
92
81
|
results.push({ name: skill.name, success: true });
|
|
93
82
|
} catch (e) {
|
|
94
83
|
console.log(chalk.red(`❌ ${skill.name} failed: ${e.message}`));
|
|
@@ -112,3 +101,220 @@ export function updateCommand(program) {
|
|
|
112
101
|
}
|
|
113
102
|
});
|
|
114
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取所有可更新的 Skills(Git + team://)
|
|
107
|
+
*/
|
|
108
|
+
async function getAllUpdatableSkills(projectRoot) {
|
|
109
|
+
const updatable = [];
|
|
110
|
+
|
|
111
|
+
// 1. 检查 Git 仓库类型的 Skills
|
|
112
|
+
const gitSkills = await getUpdatableSkills(projectRoot);
|
|
113
|
+
updatable.push(...gitSkills);
|
|
114
|
+
|
|
115
|
+
// 2. 检查 team:// 类型的 Skills
|
|
116
|
+
const teamSkills = await getUpdatableTeamSkills(projectRoot);
|
|
117
|
+
updatable.push(...teamSkills);
|
|
118
|
+
|
|
119
|
+
return updatable;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 更新 team:// 类型的 Skill
|
|
124
|
+
*/
|
|
125
|
+
async function updateTeamSkill(skill, projectRoot) {
|
|
126
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
127
|
+
await lockfile.load();
|
|
128
|
+
|
|
129
|
+
const updateInfo = await checkTeamSkillUpdate(skill, skill.source);
|
|
130
|
+
|
|
131
|
+
if (!updateInfo.hasUpdate) {
|
|
132
|
+
throw new Error('No update available');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 删除旧版本
|
|
136
|
+
if (fs.existsSync(skill.path)) {
|
|
137
|
+
fs.rmSync(skill.path, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 复制新版本
|
|
141
|
+
copyRecursive(updateInfo.registrySkillPath, skill.path);
|
|
142
|
+
|
|
143
|
+
// 更新 lockfile
|
|
144
|
+
lockfile.updateSkill(skill.name, {
|
|
145
|
+
version: updateInfo.latestVersion,
|
|
146
|
+
source: skill.source,
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
await lockfile.save();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 递归复制目录
|
|
154
|
+
*/
|
|
155
|
+
function copyRecursive(src, dest) {
|
|
156
|
+
if (!fs.existsSync(dest)) {
|
|
157
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const srcPath = path.join(src, entry.name);
|
|
164
|
+
const destPath = path.join(dest, entry.name);
|
|
165
|
+
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
copyRecursive(srcPath, destPath);
|
|
168
|
+
} else {
|
|
169
|
+
fs.copyFileSync(srcPath, destPath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取指定的 Skills
|
|
176
|
+
*/
|
|
177
|
+
async function getSpecifiedSkills(projectRoot, skillNames) {
|
|
178
|
+
const skills = [];
|
|
179
|
+
const loader = new SkillLoader(projectRoot);
|
|
180
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
181
|
+
await lockfile.load();
|
|
182
|
+
|
|
183
|
+
for (const name of skillNames) {
|
|
184
|
+
const skill = loader.getSkill(name);
|
|
185
|
+
if (!skill) {
|
|
186
|
+
console.log(chalk.red(`❌ Skill not found: ${name}`));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lockData = lockfile.getSkill(name);
|
|
191
|
+
|
|
192
|
+
if (lockData?.source?.startsWith('team:')) {
|
|
193
|
+
// team:// 类型
|
|
194
|
+
const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
|
|
195
|
+
if (updateInfo.hasUpdate) {
|
|
196
|
+
skills.push({
|
|
197
|
+
name: skill.name,
|
|
198
|
+
path: skill.path,
|
|
199
|
+
currentVersion: skill.version,
|
|
200
|
+
latestVersion: updateInfo.latestVersion,
|
|
201
|
+
source: lockData.source,
|
|
202
|
+
type: 'team',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Git 类型
|
|
207
|
+
const git = simpleGit(skill.path);
|
|
208
|
+
const isRepo = await git.checkIsRepo();
|
|
209
|
+
if (isRepo) {
|
|
210
|
+
const updateInfo = await checkGitSkillUpdate(skill);
|
|
211
|
+
if (updateInfo.hasUpdate) {
|
|
212
|
+
skills.push({
|
|
213
|
+
name: skill.name,
|
|
214
|
+
path: skill.path,
|
|
215
|
+
currentVersion: skill.version,
|
|
216
|
+
latestVersion: updateInfo.latestVersion,
|
|
217
|
+
source: lockData?.source || 'git',
|
|
218
|
+
type: 'git',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.yellow(`⚠️ ${name} is not a git repository, skipping`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return skills;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 检查 team:// 类型的 Skill 是否有更新
|
|
232
|
+
*/
|
|
233
|
+
async function checkTeamSkillUpdate(skill, source) {
|
|
234
|
+
const registryName = source.replace('team:', '');
|
|
235
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
236
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(configPath)) {
|
|
239
|
+
return { hasUpdate: false, error: 'No registry config' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
243
|
+
const reg = config.registries?.[registryName];
|
|
244
|
+
|
|
245
|
+
if (!reg?.path || !fs.existsSync(reg.path)) {
|
|
246
|
+
return { hasUpdate: false, error: `Registry ${registryName} not found` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 从 registry 查找最新版本
|
|
250
|
+
const { findSkills } = await import('./install.js');
|
|
251
|
+
const registrySkills = await findSkills(reg.path);
|
|
252
|
+
const registrySkill = registrySkills.find(s => s.name === skill.name);
|
|
253
|
+
|
|
254
|
+
if (!registrySkill) {
|
|
255
|
+
return { hasUpdate: false, error: 'Skill not found in registry' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
hasUpdate,
|
|
262
|
+
latestVersion: registrySkill.version,
|
|
263
|
+
registryPath: reg.path,
|
|
264
|
+
registrySkillPath: registrySkill.path,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 检查 Git 类型的 Skill 是否有更新
|
|
270
|
+
*/
|
|
271
|
+
async function checkGitSkillUpdate(skill) {
|
|
272
|
+
const git = simpleGit(skill.path);
|
|
273
|
+
const isRepo = await git.checkIsRepo();
|
|
274
|
+
|
|
275
|
+
if (!isRepo) {
|
|
276
|
+
return { hasUpdate: false, error: 'Not a git repository' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await git.fetch(['--depth', '1']);
|
|
280
|
+
const log = await git.log({ maxCount: 1 });
|
|
281
|
+
const latestCommit = log.latest;
|
|
282
|
+
|
|
283
|
+
// 简化:如果有 fetch 成功,认为可能有更新
|
|
284
|
+
return {
|
|
285
|
+
hasUpdate: true,
|
|
286
|
+
latestVersion: latestCommit?.hash?.slice(0, 7) || 'latest',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 获取所有可更新的 team:// Skills
|
|
292
|
+
*/
|
|
293
|
+
async function getUpdatableTeamSkills(projectRoot) {
|
|
294
|
+
const updatable = [];
|
|
295
|
+
const loader = new SkillLoader(projectRoot);
|
|
296
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
297
|
+
await lockfile.load();
|
|
298
|
+
|
|
299
|
+
const allSkills = loader.loadAllSkills();
|
|
300
|
+
|
|
301
|
+
for (const skill of allSkills) {
|
|
302
|
+
const lockData = lockfile.getSkill(skill.name);
|
|
303
|
+
if (lockData?.source?.startsWith('team:')) {
|
|
304
|
+
const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
|
|
305
|
+
if (updateInfo.hasUpdate) {
|
|
306
|
+
updatable.push({
|
|
307
|
+
name: skill.name,
|
|
308
|
+
path: skill.path,
|
|
309
|
+
currentVersion: skill.version,
|
|
310
|
+
latestVersion: updateInfo.latestVersion,
|
|
311
|
+
source: lockData.source,
|
|
312
|
+
type: 'team',
|
|
313
|
+
registrySkillPath: updateInfo.registrySkillPath,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return updatable;
|
|
320
|
+
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -23,9 +23,22 @@ export function upgradeCommand(program) {
|
|
|
23
23
|
const projectRoot = process.cwd();
|
|
24
24
|
const isGlobal = options.global;
|
|
25
25
|
// Scan both standard dirs; upgrade will check all
|
|
26
|
+
// v2.0: Support all agent directories, joycode first
|
|
26
27
|
const skillsDirs = isGlobal
|
|
27
|
-
? [
|
|
28
|
-
|
|
28
|
+
? [
|
|
29
|
+
path.join(os.homedir(), '.joycode', 'skills'),
|
|
30
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
31
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
32
|
+
path.join(os.homedir(), '.cursor', 'skills'),
|
|
33
|
+
path.join(os.homedir(), '.qoder', 'skills'),
|
|
34
|
+
]
|
|
35
|
+
: [
|
|
36
|
+
path.join(projectRoot, '.joycode', 'skills'),
|
|
37
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
38
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
39
|
+
path.join(projectRoot, '.cursor', 'skills'),
|
|
40
|
+
path.join(projectRoot, '.qoder', 'skills'),
|
|
41
|
+
];
|
|
29
42
|
// Use first existing dir as primary, but pass all dirs to findUpgradableSkills
|
|
30
43
|
const skillsDir = skillsDirs.find(d => fs.existsSync(d)) || skillsDirs[0];
|
|
31
44
|
|
|
@@ -143,10 +156,21 @@ async function findUpgradableSkills(lockfileManager, registryManager, skillsDir,
|
|
|
143
156
|
const upgradable = [];
|
|
144
157
|
const installedSkills = lockfileManager.lockData.skills || {};
|
|
145
158
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
// v2.0: Check all sibling dirs
|
|
160
|
+
const siblingDirs = [
|
|
161
|
+
skillsDir.replace('.joycode', '.claude'),
|
|
162
|
+
skillsDir.replace('.joycode', '.agent'),
|
|
163
|
+
skillsDir.replace('.joycode', '.cursor'),
|
|
164
|
+
skillsDir.replace('.joycode', '.qoder'),
|
|
165
|
+
skillsDir.replace('.claude', '.joycode'),
|
|
166
|
+
skillsDir.replace('.claude', '.agent'),
|
|
167
|
+
skillsDir.replace('.claude', '.cursor'),
|
|
168
|
+
skillsDir.replace('.claude', '.qoder'),
|
|
169
|
+
skillsDir.replace('.agent', '.joycode'),
|
|
170
|
+
skillsDir.replace('.agent', '.claude'),
|
|
171
|
+
skillsDir.replace('.agent', '.cursor'),
|
|
172
|
+
skillsDir.replace('.agent', '.qoder'),
|
|
173
|
+
].filter(d => d !== skillsDir);
|
|
150
174
|
|
|
151
175
|
const skillNames = specificSkill
|
|
152
176
|
? [specificSkill]
|
|
@@ -156,16 +180,41 @@ async function findUpgradableSkills(lockfileManager, registryManager, skillsDir,
|
|
|
156
180
|
const current = installedSkills[name];
|
|
157
181
|
if (!current) continue;
|
|
158
182
|
|
|
159
|
-
// Check existence in
|
|
160
|
-
const
|
|
183
|
+
// Check existence in all dirs
|
|
184
|
+
const allDirs = [skillsDir, ...siblingDirs];
|
|
185
|
+
const skillPath = allDirs.map(d => path.join(d, name)).find(p => fs.existsSync(p));
|
|
161
186
|
if (!skillPath) continue;
|
|
162
187
|
|
|
163
188
|
let latestVersion = null;
|
|
164
189
|
let hasBreakingChange = false;
|
|
165
190
|
let changelog = '';
|
|
166
191
|
|
|
167
|
-
// Check registry for updates
|
|
168
|
-
if (
|
|
192
|
+
// Check registry for updates (including team:// sources)
|
|
193
|
+
if (current.source?.startsWith('team:')) {
|
|
194
|
+
// Handle team:// protocol
|
|
195
|
+
const registryName = current.source.replace('team:', '');
|
|
196
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
197
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(configPath)) {
|
|
200
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
201
|
+
const reg = config.registries?.[registryName];
|
|
202
|
+
|
|
203
|
+
if (reg?.path && fs.existsSync(reg.path)) {
|
|
204
|
+
// Find skill in registry to get latest version
|
|
205
|
+
const { findSkills } = await import('../commands/install.js');
|
|
206
|
+
const skills = await findSkills(reg.path);
|
|
207
|
+
const skillInfo = skills.find(s => s.name === name);
|
|
208
|
+
|
|
209
|
+
if (skillInfo?.version && skillInfo.version !== current.version) {
|
|
210
|
+
latestVersion = skillInfo.version;
|
|
211
|
+
const currentMajor = parseInt(current.version.split('.')[0]);
|
|
212
|
+
const latestMajor = parseInt(latestVersion.split('.')[0]);
|
|
213
|
+
hasBreakingChange = latestMajor > currentMajor;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else if (registryManager && registryManager.hasSkill(name)) {
|
|
169
218
|
const recommended = registryManager.getRecommendedVersion(name);
|
|
170
219
|
if (recommended && recommended.version !== current.version) {
|
|
171
220
|
latestVersion = recommended.version;
|
|
@@ -289,9 +338,50 @@ async function upgradeFromGitHub(skill, skillsDir) {
|
|
|
289
338
|
}
|
|
290
339
|
|
|
291
340
|
async function upgradeFromRegistry(skill, skillsDir) {
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
341
|
+
// v2.0: Support team:// protocol upgrade
|
|
342
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
343
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(configPath)) {
|
|
346
|
+
throw new Error('No registry config found. Run: joySkills team add <name> <url>');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
350
|
+
const registries = config.registries || {};
|
|
351
|
+
|
|
352
|
+
// Find registry by source (team:registryName)
|
|
353
|
+
const registryName = skill.source.replace('team:', '');
|
|
354
|
+
const reg = registries[registryName];
|
|
355
|
+
|
|
356
|
+
if (!reg?.path || !fs.existsSync(reg.path)) {
|
|
357
|
+
throw new Error(`Registry "${registryName}" not found. Run: joySkills team list`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Find skill in registry
|
|
361
|
+
const { findSkills } = await import('../commands/install.js');
|
|
362
|
+
const skills = await findSkills(reg.path);
|
|
363
|
+
const skillInfo = skills.find(s => s.name === skill.name);
|
|
364
|
+
|
|
365
|
+
if (!skillInfo) {
|
|
366
|
+
throw new Error(`Skill "${skill.name}" not found in registry "${registryName}"`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check if version is newer
|
|
370
|
+
if (skillInfo.version && skillInfo.version <= skill.currentVersion) {
|
|
371
|
+
console.log(chalk.gray(` ${skill.name} is already up to date`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Remove old version
|
|
376
|
+
const targetPath = path.join(skillsDir, skill.name);
|
|
377
|
+
if (fs.existsSync(targetPath)) {
|
|
378
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Copy new version
|
|
382
|
+
copyRecursive(skillInfo.path, targetPath);
|
|
383
|
+
|
|
384
|
+
console.log(chalk.green(` ✓ Upgraded from registry ${registryName}`));
|
|
295
385
|
}
|
|
296
386
|
|
|
297
387
|
function copyRecursive(src, dest) {
|