joyskills-cli 0.1.1 → 0.1.3

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 CHANGED
@@ -1,29 +1,54 @@
1
- # joySkills - 新一代团队 Skill 治理平台
1
+ # joySkills - OpenSkills 的增强替代品
2
2
 
3
- > 在协议层兼容 open skill / Claude Skills,在能力层扩展企业级治理能力
3
+ > 100% 兼容 OpenSkills + 团队协作增强(版本锁定、团队 Registry、安全审计)
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/joyskills-cli.svg)](https://www.npmjs.com/package/joyskills-cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  **🔗 Links:** [npm Package](https://www.npmjs.com/package/joyskills-cli) | [Git Repository](https://github.com/your-org/joyskills-cli)
9
9
 
10
- ## 💡 为什么需要 joySkills?
10
+ ## 💡 为什么选择 joySkills?
11
11
 
12
- AI 原生开发时代,**Skill**(技能)成为了比 function 更友好的能力抽象单元。然而当团队规模扩大,单纯的本地 skill 管理会遇到:
12
+ **joySkills OpenSkills 的超集**,提供:
13
13
 
14
- - **版本混乱**:不同项目使用不同版本的 skill,难以统一升级
15
- - **安全风险**:无法及时发现和处理已废弃或有风险的 skill 版本
16
- - **缺乏治理**:谁在用什么 skill?如何审批新版本?
17
- - **协作困难**:团队成员各自维护 skill,无法共享和复用
14
+ ### 🎯 OpenSkills 的全部能力
15
+ - 安装公开 skill(调用 `openskills install`)
16
+ - 生成 AGENTS.md(`joySkills sync` = `openskills sync` + 增强)
17
+ - 读取 skill 内容(`joySkills read` = `openskills read`)
18
+ - ✅ 100% 兼容 Claude Code / Cursor / Windsurf
18
19
 
19
- joySkills 提供了一套**完整的团队级 Skill 治理方案**,同时保持与现有生态的 100% 兼容。
20
+ ### 🚀 团队协作增强(OpenSkills 缺失的)
21
+ - ✅ **版本锁定**:joySkills.lock 确保团队版本统一
22
+ - ✅ **团队 Registry**:基于 Git 的内部 skill 仓库
23
+ - ✅ **安全审计**:自动检测废弃和有风险的 skill
24
+ - ✅ **状态管理**:draft → review → approved → deprecated
25
+ - ✅ **使用追踪**:了解哪些 skill 被使用、被谁使用
20
26
 
21
27
  ## ✨ 核心特性
22
28
 
23
- ### 📦 统一 Skill 源(Registry)
24
- - 团队/组织级 Skill 仓库
25
- - 基于 Git 的版本管理
26
- - 支持私有部署和权限控制
29
+ ### 📦 公开 + 内部 Skill 统一管理
30
+ ```bash
31
+ # 项目级安装(默认)
32
+ joySkills install team://your-team/rc-onboarding
33
+ # → 安装到 .agent/skills/rc-onboarding
34
+
35
+ # 全局安装
36
+ joySkills install -g anthropics/skills/pdf
37
+ # → 安装到 ~/.agent/skills/pdf
38
+
39
+ # 统一同步(扫描所有路径)
40
+ joySkills sync
41
+ # 扫描: .agent/skills, ~/.agent/skills, .claude/skills, ~/.claude/skills
42
+ # 生成: AGENTS.md + joySkills.lock
43
+ ```
44
+
45
+ **路径设计**:
46
+ | 安装方式 | joySkills | OpenSkills |
47
+ |---------|-----------|------------|
48
+ | 项目级 | `.agent/skills` | `.agent/skills` (--universal) |
49
+ | 全局 | `~/.agent/skills` | `~/.claude/skills` (--global) |
50
+
51
+ **优先级**:项目级 > 全局,不同项目可用不同版本
27
52
 
28
53
  ### 🔒 版本锁定(joySkills.lock)
29
54
  - 项目级版本锁定,确保环境一致
@@ -45,112 +70,157 @@ joySkills 提供了一套**完整的团队级 Skill 治理方案**,同时保
45
70
  - 安全风险预警
46
71
  - 自动生成升级建议
47
72
 
48
- ### 🤝 100% 兼容现有生态
49
- - 兼容 Claude Code / Cursor / Windsurf / Aider
50
- - 不改变 `skills/` 目录结构
51
- - 不改变 `SKILL.md` 文件格式
73
+ ### 📋 OpenSkills 兼容命令
74
+ ```bash
75
+ # sync 命令(扫描所有路径 + 生成 AGENTS.md)
76
+ joySkills sync
77
+ # 扫描路径:.agent/skills, ~/.agent/skills, .claude/skills, ~/.claude/skills
78
+ # ✅ 生成 AGENTS.md(与 openskills sync 格式相同)
79
+ # ✅ 额外生成 joySkills.lock(版本锁定)
80
+
81
+ # read 命令(按优先级读取 skill)
82
+ joySkills read rc-onboarding
83
+ # ✅ 路径查找优先级与 openskills read 一致
84
+ # ✅ 输出格式完全相同
85
+ ```
52
86
 
53
87
  ## 🚀 快速开始
54
88
 
55
- ### 安装
56
-
57
- joySkills 支持**全局安装**和**项目级安装**两种方式:
89
+ ### 场景 1:个人开发者(替代 OpenSkills)
58
90
 
59
91
  ```bash
60
- # 全局安装
92
+ # 1. 安装
61
93
  npm install -g joyskills-cli
62
- joySkills --version
63
94
 
64
- # 或:项目级安装
65
- npm install joyskills-cli
66
- npx joySkills --version
95
+ # 2. 安装公开 skill
96
+ joySkills install anthropics/skills/pdf
97
+ joySkills install @skillforge/code-review
98
+
99
+ # 3. 生成 AGENTS.md
100
+ joySkills sync
101
+
102
+ # 4. 查看已安装
103
+ joySkills list
67
104
  ```
68
105
 
69
- ### 5 分钟上手
106
+ ### 场景 2:团队协作(内部 skill)
70
107
 
71
108
  ```bash
72
- # 1. 进入你的项目目录
73
- cd your-project
109
+ # 1. 安装
110
+ npm install -g joyskills-cli
74
111
 
75
- # 2. 安装一个 skill
76
- joySkills install web-search
77
- # 或使用 npx(如果是项目级安装)
78
- npx joySkills install web-search
112
+ # 2. 配置团队 Registry
113
+ echo '{
114
+ "registries": [{
115
+ "id": "team://your-team",
116
+ "url": "https://your-git/team-skills.git",
117
+ "type": "git"
118
+ }]
119
+ }' > .joyskillrc
79
120
 
80
- # 3. 查看已安装的 skills
81
- joySkills list
121
+ # 3. 安装团队 skill
122
+ joySkills install team://your-team/rc-onboarding
82
123
 
83
- # 4. 检查状态
84
- joySkills status
124
+ # 4. 生成 AGENTS.md + joySkills.lock
125
+ joySkills sync
85
126
 
86
- # 5. 审计安全性
87
- joySkills audit
127
+ # 5. 团队成员同步
128
+ git pull # 拉取 joySkills.lock
129
+ joySkills install # 自动安装锁定版本
88
130
  ```
89
131
 
90
- 完成!你的 Claude Code / Cursor 等编辑器会自动识别 `skills/` 目录中的技能。
132
+ ### 场景 3:从 OpenSkills 迁移
133
+
134
+ ```bash
135
+ # 已经用 openskills 安装了很多 skills
136
+ openskills install skill-a
137
+ openskills install skill-b
138
+
139
+ # 切换到 joySkills(无需重装)
140
+ joySkills sync
141
+ # ✅ 自动识别已有 skills
142
+ # ✅ 生成 joySkills.lock
143
+ # ✅ 后续用 joySkills install 安装新 skill
144
+ ```
91
145
 
92
146
  ## 📋 完整命令参考
93
147
 
94
- ### `joySkills list`
95
- 列出可用的 skills
148
+ ### OpenSkills 兼容命令
149
+
150
+ #### `joySkills sync`
151
+ 生成 AGENTS.md(100% 兼容 `openskills sync` + 增强)
96
152
 
97
- **选项:**
98
- - `-a, --all-versions` - 显示所有版本
99
- - `-c, --category <category>` - 按分类过滤
100
- - `-s, --search <query>` - 搜索 skill 名称或描述
101
- - `-i, --installed` - 只显示已安装的 skills
102
- - `-l, --local` - 只显示本地 skills
103
- - `-r, --registry <name>` - 显示指定 registry 的 skills
153
+ ```bash
154
+ joySkills sync
155
+ # 扫描 skills/ 目录
156
+ # 生成 AGENTS.md(OpenSkills 兼容格式)
157
+ # 生成 joySkills.lock(版本锁定)
158
+ # 版本一致性检查
159
+ ```
104
160
 
105
- ### `joySkills install <skill>`
106
- 安装一个 skill
161
+ #### `joySkills read <skill-name>`
162
+ 读取 skill 内容(100% 兼容 `openskills read`)
107
163
 
108
164
  ```bash
109
- # 安装最新版本
110
- joySkills install web-search
165
+ joySkills read rc-onboarding
166
+ # 路径查找优先级与 openskills 一致
167
+ # ✅ 输出格式完全相同
168
+ ```
169
+
170
+ ### joySkills 增强命令
111
171
 
112
- # 安装指定版本
113
- joySkills install web-search@1.0.0
172
+ #### `joySkills install <skill>`
173
+ 安装 skill(公开 + 团队 + 本地)
114
174
 
115
- # 从指定 registry 安装
116
- joySkills install web-search --registry my-team
175
+ ```bash
176
+ # 公开 skill(自动调用 openskills)
177
+ joySkills install anthropics/skills/pdf
178
+
179
+ # 团队 skill
180
+ joySkills install team://your-team/rc-onboarding
181
+
182
+ # 本地 skill
183
+ joySkills install ./my-local-skill
184
+
185
+ # 指定版本
186
+ joySkills install team://your-team/rc-onboarding@1.2.0
117
187
  ```
118
188
 
119
- ### `joySkills remove <skill>`
189
+ #### `joySkills list`
190
+ 列出可用的 skills
191
+
192
+ ```bash
193
+ joySkills list
194
+ joySkills list --installed # 只显示已安装
195
+ joySkills list --search "web" # 搜索
196
+ ```
197
+
198
+ #### `joySkills remove <skill>`
120
199
  删除已安装的 skill
121
200
 
122
201
  ```bash
123
- joySkills remove web-search
202
+ joySkills remove rc-onboarding
124
203
  ```
125
204
 
126
- ### `joySkills team <subcommand>`
205
+ #### `joySkills team`
127
206
  管理团队 registry
128
207
 
129
208
  ```bash
130
- # 添加团队 registry
131
209
  joySkills team add my-team /path/to/registry
132
-
133
- # 列出所有 registries
134
210
  joySkills team list
135
-
136
- # 删除 registry
137
211
  joySkills team remove my-team
138
212
  ```
139
213
 
140
- ### `joySkills status`
214
+ #### `joySkills status`
141
215
  显示当前项目的 skills 状态
142
216
 
143
217
  ```bash
144
218
  joySkills status
145
219
  ```
146
220
 
147
- ### `joySkills audit`
221
+ #### `joySkills audit`
148
222
  审计已安装 skills 的安全性和弃用状态
149
223
 
150
- ```bash
151
- joySkills audit
152
- ```
153
-
154
224
  ## 📖 核心概念
155
225
 
156
226
  ### Skill Registry(技能注册表)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joyskills-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Team-level skill governance compatible with open skill / Claude Skills",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,6 +4,7 @@ import { LockfileManager } from '../lockfile.js';
4
4
  import { RegistryManager } from '../registry.js';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
+ import { execSync } from 'child_process';
7
8
 
8
9
  export function installCommand(program) {
9
10
  program
@@ -11,10 +12,11 @@ export function installCommand(program) {
11
12
  .description('Install a skill')
12
13
  .option('-v, --version <version>', 'Specify version to install')
13
14
  .option('-r, --registry <name>', 'Install from specific registry')
15
+ .option('-g, --global', 'Install globally (~/.agent/skills)')
14
16
  .option('--force', 'Force installation even if version is not recommended')
15
17
  .action(async (skillName, options) => {
16
18
  const projectRoot = process.cwd();
17
- const localManager = new LocalManager(projectRoot);
19
+ const localManager = new LocalManager(projectRoot, { global: options.global });
18
20
  const lockfileManager = new LockfileManager(projectRoot);
19
21
 
20
22
  console.log(`Installing skill: ${skillName}`);
@@ -26,57 +28,126 @@ export function installCommand(program) {
26
28
  // Check if skill already exists
27
29
  if (localManager.hasSkill(skillName)) {
28
30
  console.log(`⚠️ Skill ${skillName} already exists locally.`);
29
- console.log(' Use "joyskill remove <skill>" first if you want to reinstall.');
31
+ console.log(' Use "joySkills remove <skill>" first if you want to reinstall.');
30
32
  return;
31
33
  }
32
34
 
33
- // Check for registry
34
- let registryManager = null;
35
- const registryPath = path.join(projectRoot, '.joyskill/registry');
36
-
37
- if (fs.existsSync(registryPath)) {
38
- try {
39
- registryManager = new RegistryManager(registryPath);
40
- await registryManager.load();
41
- } catch (error) {
42
- console.error('Warning: Could not load registry:', error.message);
43
- }
35
+ // 判断 skill 类型
36
+ const skillType = detectSkillType(skillName);
37
+
38
+ if (skillType === 'public') {
39
+ // 公开 skill:调用 openskills install
40
+ await installPublicSkill(skillName, options, localManager, lockfileManager);
41
+ } else if (skillType === 'team') {
42
+ // 团队 skill:从团队 Registry 安装
43
+ await installTeamSkill(skillName, options, projectRoot, localManager, lockfileManager);
44
+ } else if (skillType === 'local') {
45
+ // 本地 skill:直接复制
46
+ await installLocalSkill(skillName, options, localManager, lockfileManager);
44
47
  }
45
48
 
46
- // Determine version to install
47
- let targetVersion = options.version || '1.0.0';
48
- let versionSource = 'default';
49
-
50
- if (registryManager && registryManager.hasSkill(skillName)) {
51
- if (!options.version) {
52
- // Get recommended version from registry
53
- const recommended = registryManager.getRecommendedVersion(skillName);
54
- if (recommended) {
55
- targetVersion = recommended.version;
56
- versionSource = 'registry';
57
- }
58
- }
59
-
60
- // Validate version
61
- const validation = registryManager.validateVersion(skillName, targetVersion);
62
- if (!validation.valid && !options.force) {
63
- console.error(`❌ Version validation failed: ${validation.error}`);
64
- return;
65
- }
66
-
67
- if (validation.warning && !options.force) {
68
- console.log(`⚠️ Warning: ${validation.warning}`);
69
- console.log(' Use --force to install anyway.');
70
- return;
71
- }
72
-
73
- console.log(`Installing ${skillName} v${targetVersion} from registry`);
74
- } else {
75
- console.log(`Installing ${skillName} v${targetVersion} (local/custom)`);
76
- }
49
+ } catch (error) {
50
+ console.error(`❌ Failed to install ${skillName}:`, error);
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * 检测 skill 类型
57
+ */
58
+ function detectSkillType(skillName) {
59
+ if (skillName.startsWith('team://')) {
60
+ return 'team';
61
+ } else if (skillName.startsWith('./') || skillName.startsWith('../')) {
62
+ return 'local';
63
+ } else {
64
+ // anthropics/skills/pdf, @skillforge/xxx
65
+ return 'public';
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 安装公开 skill(使用 openskills)
71
+ */
72
+ async function installPublicSkill(skillName, options, localManager, lockfileManager) {
73
+ console.log(`📦 Installing public skill: ${skillName}`);
74
+ console.log(`💡 Using openskills install...\n`);
75
+
76
+ try {
77
+ // 调用 openskills install
78
+ const cmd = `npx openskills install ${skillName}`;
79
+ execSync(cmd, { stdio: 'inherit' });
80
+
81
+ // 更新 lockfile
82
+ lockfileManager.updateSkill(skillName, {
83
+ version: options.version || 'latest',
84
+ source: 'public',
85
+ installedAt: new Date().toISOString()
86
+ });
77
87
 
78
- // Create sample SKILL.md content
79
- const skillMdContent = `---
88
+ await lockfileManager.save();
89
+
90
+ console.log(`\n✅ Successfully installed public skill: ${skillName}`);
91
+ console.log(`💡 Run 'joySkills sync' to update AGENTS.md`);
92
+
93
+ } catch (error) {
94
+ throw new Error(`openskills install failed: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 安装团队 skill(从团队 Registry)
100
+ */
101
+ async function installTeamSkill(skillName, options, projectRoot, localManager, lockfileManager) {
102
+ console.log(`🏢 Installing team skill: ${skillName}`);
103
+
104
+ // Check for registry
105
+ let registryManager = null;
106
+ const registryPath = path.join(projectRoot, '.joyskill/registry');
107
+
108
+ if (fs.existsSync(registryPath)) {
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
+ }
115
+ }
116
+
117
+ // Determine version to install
118
+ let targetVersion = options.version || '1.0.0';
119
+ let versionSource = 'default';
120
+
121
+ if (registryManager && registryManager.hasSkill(skillName)) {
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
+ }
137
+
138
+ if (validation.warning && !options.force) {
139
+ console.log(`⚠️ Warning: ${validation.warning}`);
140
+ console.log(' Use --force to install anyway.');
141
+ return;
142
+ }
143
+
144
+ console.log(`Installing ${skillName} v${targetVersion} from registry`);
145
+ } else {
146
+ console.log(`Installing ${skillName} v${targetVersion} (local/custom)`);
147
+ }
148
+
149
+ // Create sample SKILL.md content
150
+ const skillMdContent = `---
80
151
  name: ${skillName}
81
152
  description: Auto-generated skill for ${skillName}
82
153
  version: ${targetVersion}
@@ -84,7 +155,7 @@ version: ${targetVersion}
84
155
 
85
156
  # ${skillName}
86
157
 
87
- This skill was automatically generated by joyskill.
158
+ This skill was automatically generated by joySkills.
88
159
 
89
160
  ## Examples
90
161
  - Example usage 1
@@ -95,25 +166,58 @@ This skill was automatically generated by joyskill.
95
166
  - Guideline 2
96
167
  `;
97
168
 
98
- // Install the skill
99
- localManager.installSkill(skillName, skillMdContent);
169
+ // Install the skill
170
+ localManager.installSkill(skillName, skillMdContent);
100
171
 
101
- // Update lockfile
102
- lockfileManager.updateSkill(skillName, {
103
- version: targetVersion,
104
- registry: options.registry || 'local',
105
- installedAt: new Date().toISOString()
106
- });
172
+ // Update lockfile
173
+ lockfileManager.updateSkill(skillName, {
174
+ version: targetVersion,
175
+ source: 'team',
176
+ registry: options.registry || 'local',
177
+ installedAt: new Date().toISOString()
178
+ });
107
179
 
108
- await lockfileManager.save();
180
+ await lockfileManager.save();
109
181
 
110
- console.log(`✅ Successfully installed ${skillName} v${targetVersion}`);
182
+ console.log(`✅ Successfully installed ${skillName} v${targetVersion}`);
111
183
 
112
- if (versionSource === 'registry') {
113
- console.log(' Version selected from registry recommendations.');
114
- }
115
- } catch (error) {
116
- console.error(`❌ Failed to install ${skillName}:`, error);
117
- }
118
- });
184
+ if (versionSource === 'registry') {
185
+ console.log(' Version selected from registry recommendations.');
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 安装本地 skill(直接复制)
191
+ */
192
+ async function installLocalSkill(skillName, options, localManager, lockfileManager) {
193
+ console.log(`📁 Installing local skill: ${skillName}`);
194
+
195
+ const sourcePath = path.resolve(skillName);
196
+
197
+ if (!fs.existsSync(sourcePath)) {
198
+ throw new Error(`Local path not found: ${sourcePath}`);
199
+ }
200
+
201
+ const skillMdPath = path.join(sourcePath, 'SKILL.md');
202
+ if (!fs.existsSync(skillMdPath)) {
203
+ throw new Error(`SKILL.md not found in ${sourcePath}`);
204
+ }
205
+
206
+ const skillMdContent = fs.readFileSync(skillMdPath, 'utf-8');
207
+ const skillBaseName = path.basename(sourcePath);
208
+
209
+ // Install the skill
210
+ localManager.installSkill(skillBaseName, skillMdContent);
211
+
212
+ // Update lockfile
213
+ lockfileManager.updateSkill(skillBaseName, {
214
+ version: options.version || 'local',
215
+ source: 'local',
216
+ path: sourcePath,
217
+ installedAt: new Date().toISOString()
218
+ });
219
+
220
+ await lockfileManager.save();
221
+
222
+ console.log(`✅ Successfully installed local skill: ${skillBaseName}`);
119
223
  }
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * joySkills read 命令
8
+ *
9
+ * 功能:
10
+ * 读取指定 skill 的完整内容(YAML frontmatter + Markdown)
11
+ *
12
+ * 设计原则:
13
+ * - 100% 兼容 OpenSkills read 的输出格式
14
+ * - 路径查找优先级与 openskills sync 一致
15
+ * - 供 AI 编辑器解析使用
16
+ *
17
+ * 路径查找优先级:
18
+ * 1. 项目级 .agent/skills(--universal)
19
+ * 2. 全局 ~/.agent/skills
20
+ * 3. 项目级 .claude/skills(--project)
21
+ * 4. 全局 ~/.claude/skills(--global)
22
+ */
23
+
24
+ export async function readCommand(skillName) {
25
+ if (!skillName) {
26
+ console.error('❌ 错误:请指定要读取的 skill 名称');
27
+ console.log('\n用法: joySkills read <skill-name>\n');
28
+ console.log('示例: joySkills read rc-onboarding\n');
29
+ process.exit(1);
30
+ }
31
+
32
+ try {
33
+ // 查找 skill 路径
34
+ const skillPath = findSkillPath(skillName);
35
+
36
+ if (!skillPath) {
37
+ console.error(`❌ 找不到 skill: ${skillName}`);
38
+ console.log('\n💡 提示:');
39
+ console.log(` 1. 检查 skill 名称是否正确`);
40
+ console.log(` 2. 执行 joySkills list 查看已安装的 skills\n`);
41
+ process.exit(1);
42
+ }
43
+
44
+ // 读取 SKILL.md
45
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
46
+ if (!fs.existsSync(skillMdPath)) {
47
+ console.error(`❌ ${skillName} 缺少 SKILL.md 文件`);
48
+ process.exit(1);
49
+ }
50
+
51
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
52
+
53
+ // 输出到 stdout(供 AI 编辑器解析)
54
+ console.log(content);
55
+
56
+ } catch (error) {
57
+ console.error('❌ 读取失败:', error.message);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 查找 skill 路径(按优先级)
64
+ *
65
+ * 优先级与 OpenSkills sync 一致:
66
+ * 1. 项目 .agent > 全局 .agent
67
+ * 2. 项目 .claude > 全局 .claude
68
+ */
69
+ function findSkillPath(skillName) {
70
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
71
+ const cwd = process.cwd();
72
+
73
+ const searchPaths = [
74
+ // 1. 项目级 .agent/skills(joySkills 默认 / OpenSkills --universal)
75
+ path.join(cwd, '.agent', 'skills', skillName),
76
+
77
+ // 2. 全局 ~/.agent/skills(joySkills --global)
78
+ path.join(homeDir, '.agent', 'skills', skillName),
79
+
80
+ // 3. 项目级 .claude/skills(OpenSkills --project)
81
+ path.join(cwd, '.claude', 'skills', skillName),
82
+
83
+ // 4. 全局 ~/.claude/skills(OpenSkills --global)
84
+ path.join(homeDir, '.claude', 'skills', skillName),
85
+ ];
86
+
87
+ for (const searchPath of searchPaths) {
88
+ if (fs.existsSync(searchPath)) {
89
+ return searchPath;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * 列出所有可读取的 skills(辅助命令)
98
+ */
99
+ export function listReadableSkills() {
100
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
101
+ const cwd = process.cwd();
102
+
103
+ const searchDirs = [
104
+ path.join(cwd, '.agent', 'skills'),
105
+ path.join(homeDir, '.agent', 'skills'),
106
+ path.join(cwd, '.claude', 'skills'),
107
+ path.join(homeDir, '.claude', 'skills'),
108
+ ];
109
+
110
+ const skills = new Set();
111
+
112
+ for (const dir of searchDirs) {
113
+ if (!fs.existsSync(dir)) continue;
114
+
115
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
116
+ for (const entry of entries) {
117
+ if (!entry.isDirectory()) continue;
118
+
119
+ const skillMdPath = path.join(dir, entry.name, 'SKILL.md');
120
+ if (fs.existsSync(skillMdPath)) {
121
+ skills.add(entry.name);
122
+ }
123
+ }
124
+ }
125
+
126
+ return Array.from(skills).sort();
127
+ }
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ /**
11
+ * joySkills sync 命令
12
+ *
13
+ * 功能:
14
+ * 1. 扫描 skills/ 目录
15
+ * 2. 生成/更新 AGENTS.md(与 openskills sync 格式一致)
16
+ * 3. 更新 joySkills.lock(记录扫描到的所有 skill)
17
+ * 4. 验证版本一致性
18
+ *
19
+ * 设计原则:
20
+ * - 100% 兼容 OpenSkills 生成的 AGENTS.md
21
+ * - 在 OpenSkills 基础上增强(版本锁定 + 一致性检查)
22
+ */
23
+
24
+ export async function syncCommand() {
25
+ console.log('🔄 正在同步 skills 到 AGENTS.md...\n');
26
+
27
+ try {
28
+ // 1. 扫描所有可能的 skills 目录(按优先级)
29
+ const cwd = process.cwd();
30
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
31
+
32
+ const searchDirs = [
33
+ path.join(cwd, '.agent', 'skills'), // joySkills 项目级
34
+ path.join(homeDir, '.agent', 'skills'), // joySkills 全局
35
+ path.join(cwd, '.claude', 'skills'), // OpenSkills --project
36
+ path.join(homeDir, '.claude', 'skills'), // OpenSkills --global
37
+ ];
38
+
39
+ const allSkills = [];
40
+ for (const dir of searchDirs) {
41
+ if (fs.existsSync(dir)) {
42
+ const skills = scanSkills(dir);
43
+ allSkills.push(...skills);
44
+ }
45
+ }
46
+
47
+ // 去重(按 id)
48
+ const uniqueSkills = [];
49
+ const seenIds = new Set();
50
+ for (const skill of allSkills) {
51
+ if (!seenIds.has(skill.id)) {
52
+ seenIds.add(skill.id);
53
+ uniqueSkills.push(skill);
54
+ }
55
+ }
56
+
57
+ console.log(`📦 扫描到 ${uniqueSkills.length} 个 skills:\n`);
58
+ uniqueSkills.forEach(skill => {
59
+ console.log(` - ${skill.id} (${skill.version || 'unknown'})`);
60
+ });
61
+ console.log('');
62
+
63
+ // 2. 生成 AGENTS.md(OpenSkills 兼容格式)
64
+ const agentsMdContent = generateAgentsMd(uniqueSkills);
65
+ const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
66
+ fs.writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
67
+ console.log(`✅ 已生成 AGENTS.md\n`);
68
+
69
+ // 3. 更新 joySkills.lock(joySkills 增强功能)
70
+ await updateLockfile(uniqueSkills);
71
+
72
+ // 4. 验证版本一致性(joySkills 增强功能)
73
+ await verifyConsistency(uniqueSkills);
74
+
75
+ console.log('✨ 同步完成!\n');
76
+ console.log('💡 提示:');
77
+ console.log(' - AGENTS.md 已生成,AI 编辑器会自动识别');
78
+ console.log(' - joySkills.lock 已更新,确保版本锁定\n');
79
+
80
+ } catch (error) {
81
+ console.error('❌ 同步失败:', error.message);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 扫描 skills 目录
88
+ */
89
+ function scanSkills(skillsDir) {
90
+ const skills = [];
91
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
92
+
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory()) continue;
95
+
96
+ const skillPath = path.join(skillsDir, entry.name);
97
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
98
+
99
+ if (!fs.existsSync(skillMdPath)) {
100
+ console.log(`⚠️ 跳过 ${entry.name}(缺少 SKILL.md)`);
101
+ continue;
102
+ }
103
+
104
+ // 解析 SKILL.md
105
+ const skillContent = fs.readFileSync(skillMdPath, 'utf-8');
106
+ const metadata = parseSkillMetadata(skillContent);
107
+
108
+ skills.push({
109
+ id: entry.name,
110
+ path: skillPath,
111
+ ...metadata
112
+ });
113
+ }
114
+
115
+ return skills;
116
+ }
117
+
118
+ /**
119
+ * 解析 SKILL.md 的 YAML frontmatter
120
+ */
121
+ function parseSkillMetadata(content) {
122
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
123
+ if (!match) return {};
124
+
125
+ const yaml = match[1];
126
+ const metadata = {};
127
+
128
+ // 简单的 YAML 解析(关键字段)
129
+ const lines = yaml.split('\n');
130
+ for (const line of lines) {
131
+ const [key, ...valueParts] = line.split(':');
132
+ if (!key || !valueParts.length) continue;
133
+
134
+ const value = valueParts.join(':').trim().replace(/^["']|["']$/g, '');
135
+ metadata[key.trim()] = value;
136
+ }
137
+
138
+ return metadata;
139
+ }
140
+
141
+ /**
142
+ * 生成 AGENTS.md(OpenSkills 兼容格式)
143
+ */
144
+ function generateAgentsMd(skills) {
145
+ let content = `# AI Agent Skills\n\n`;
146
+ content += `> 此文件由 joySkills sync 自动生成\n`;
147
+ content += `> 兼容 OpenSkills 格式,所有 AI 编辑器均可识别\n\n`;
148
+
149
+ content += `## 已安装的 Skills (${skills.length})\n\n`;
150
+
151
+ for (const skill of skills) {
152
+ content += `### ${skill.name || skill.id}\n\n`;
153
+ content += `**ID**: \`${skill.id}\`\n`;
154
+ if (skill.version) {
155
+ content += `**Version**: ${skill.version}\n`;
156
+ }
157
+ if (skill.description) {
158
+ content += `**Description**: ${skill.description}\n`;
159
+ }
160
+ content += `**Path**: \`skills/${skill.id}\`\n\n`;
161
+
162
+ // 添加 XML 片段(供 AI 编辑器解析)
163
+ content += `\`\`\`xml\n`;
164
+ content += `<skill id="${skill.id}">\n`;
165
+ content += ` <name>${skill.name || skill.id}</name>\n`;
166
+ if (skill.version) {
167
+ content += ` <version>${skill.version}</version>\n`;
168
+ }
169
+ content += ` <path>skills/${skill.id}/SKILL.md</path>\n`;
170
+ content += `</skill>\n`;
171
+ content += `\`\`\`\n\n`;
172
+ }
173
+
174
+ content += `---\n\n`;
175
+ content += `生成时间: ${new Date().toISOString()}\n`;
176
+ content += `生成工具: joySkills ${getVersion()}\n`;
177
+
178
+ return content;
179
+ }
180
+
181
+ /**
182
+ * 更新 joySkills.lock(joySkills 增强功能)
183
+ */
184
+ async function updateLockfile(skills) {
185
+ const lockfilePath = path.join(process.cwd(), 'joySkills.lock');
186
+
187
+ let lockData = {
188
+ lockVersion: 1,
189
+ projectId: path.basename(process.cwd()),
190
+ generatedAt: new Date().toISOString(),
191
+ skills: {}
192
+ };
193
+
194
+ // 读取现有 lockfile
195
+ if (fs.existsSync(lockfilePath)) {
196
+ const existingContent = fs.readFileSync(lockfilePath, 'utf-8');
197
+ try {
198
+ lockData = JSON.parse(existingContent);
199
+ } catch (e) {
200
+ console.log('⚠️ joySkills.lock 格式错误,将重新生成');
201
+ }
202
+ }
203
+
204
+ // 更新 skills 信息
205
+ for (const skill of skills) {
206
+ const existingSkill = lockData.skills[skill.id] || {};
207
+
208
+ lockData.skills[skill.id] = {
209
+ version: skill.version || existingSkill.version || 'unknown',
210
+ installedAt: existingSkill.installedAt || new Date().toISOString(),
211
+ lastSyncAt: new Date().toISOString()
212
+ };
213
+ }
214
+
215
+ // 写入 lockfile
216
+ fs.writeFileSync(
217
+ lockfilePath,
218
+ JSON.stringify(lockData, null, 2),
219
+ 'utf-8'
220
+ );
221
+
222
+ console.log('✅ 已更新 joySkills.lock');
223
+ }
224
+
225
+ /**
226
+ * 验证版本一致性(joySkills 增强功能)
227
+ */
228
+ async function verifyConsistency(skills) {
229
+ const lockfilePath = path.join(process.cwd(), 'joySkills.lock');
230
+ if (!fs.existsSync(lockfilePath)) return;
231
+
232
+ const lockData = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
233
+ let hasWarning = false;
234
+
235
+ for (const skill of skills) {
236
+ const lockedSkill = lockData.skills[skill.id];
237
+ if (!lockedSkill) continue;
238
+
239
+ if (skill.version && lockedSkill.version !== skill.version) {
240
+ if (!hasWarning) {
241
+ console.log('\n⚠️ 版本不一致警告:\n');
242
+ hasWarning = true;
243
+ }
244
+ console.log(` ${skill.id}:`);
245
+ console.log(` - Lock 文件: ${lockedSkill.version}`);
246
+ console.log(` - 实际版本: ${skill.version}`);
247
+ console.log(` 💡 建议: joySkills upgrade ${skill.id}\n`);
248
+ }
249
+ }
250
+
251
+ if (!hasWarning) {
252
+ console.log('✅ 版本一致性检查通过');
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 获取 joySkills 版本
258
+ */
259
+ function getVersion() {
260
+ try {
261
+ const packagePath = path.join(__dirname, '../../package.json');
262
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
263
+ return pkg.version;
264
+ } catch (e) {
265
+ return 'unknown';
266
+ }
267
+ }
package/src/index.js CHANGED
@@ -7,6 +7,8 @@ import { removeCommand } from './commands/remove.js';
7
7
  import { teamCommand } from './commands/team.js';
8
8
  import { auditCommand } from './commands/audit.js';
9
9
  import { statusCommand } from './commands/status.js';
10
+ import { syncCommand } from './commands/sync.js';
11
+ import { readCommand } from './commands/read.js';
10
12
 
11
13
  const program = new Command();
12
14
 
@@ -23,5 +25,16 @@ teamCommand(program);
23
25
  auditCommand(program);
24
26
  statusCommand(program);
25
27
 
28
+ // OpenSkills compatible commands
29
+ program
30
+ .command('sync')
31
+ .description('Sync skills to AGENTS.md (OpenSkills compatible)')
32
+ .action(syncCommand);
33
+
34
+ program
35
+ .command('read <skill-name>')
36
+ .description('Read skill content (OpenSkills compatible)')
37
+ .action(readCommand);
38
+
26
39
  // Parse arguments
27
40
  program.parse();
package/src/local.js CHANGED
@@ -3,15 +3,22 @@ import * as path from 'path';
3
3
  import { LocalSkill } from './types.js';
4
4
 
5
5
  export class LocalManager {
6
- constructor(projectRoot) {
6
+ constructor(projectRoot, options = {}) {
7
7
  this.projectRoot = projectRoot;
8
+ this.global = options.global || false;
8
9
  }
9
10
 
10
11
  /**
11
12
  * Get local skills directory path
13
+ * 默认项目级:.agent/skills
14
+ * 全局:~/.agent/skills
12
15
  */
13
16
  getSkillsDir() {
14
- return path.join(this.projectRoot, 'skills');
17
+ if (this.global) {
18
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
19
+ return path.join(homeDir, '.agent', 'skills');
20
+ }
21
+ return path.join(this.projectRoot, '.agent', 'skills');
15
22
  }
16
23
 
17
24
  /**