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 +140 -70
- package/package.json +1 -1
- package/src/commands/install.js +168 -64
- package/src/commands/read.js +127 -0
- package/src/commands/sync.js +267 -0
- package/src/index.js +13 -0
- package/src/local.js +9 -2
package/README.md
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
|
-
# joySkills -
|
|
1
|
+
# joySkills - OpenSkills 的增强替代品
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 100% 兼容 OpenSkills + 团队协作增强(版本锁定、团队 Registry、安全审计)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/joyskills-cli)
|
|
6
6
|
[](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
|
-
## 💡
|
|
10
|
+
## 💡 为什么选择 joySkills?
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**joySkills 是 OpenSkills 的超集**,提供:
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
|
|
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
|
-
### 📦
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
###
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
###
|
|
106
|
+
### 场景 2:团队协作(内部 skill)
|
|
70
107
|
|
|
71
108
|
```bash
|
|
72
|
-
# 1.
|
|
73
|
-
|
|
109
|
+
# 1. 安装
|
|
110
|
+
npm install -g joyskills-cli
|
|
74
111
|
|
|
75
|
-
# 2.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
81
|
-
joySkills
|
|
121
|
+
# 3. 安装团队 skill
|
|
122
|
+
joySkills install team://your-team/rc-onboarding
|
|
82
123
|
|
|
83
|
-
# 4.
|
|
84
|
-
joySkills
|
|
124
|
+
# 4. 生成 AGENTS.md + joySkills.lock
|
|
125
|
+
joySkills sync
|
|
85
126
|
|
|
86
|
-
# 5.
|
|
87
|
-
joySkills
|
|
127
|
+
# 5. 团队成员同步
|
|
128
|
+
git pull # 拉取 joySkills.lock
|
|
129
|
+
joySkills install # 自动安装锁定版本
|
|
88
130
|
```
|
|
89
131
|
|
|
90
|
-
|
|
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
|
-
###
|
|
95
|
-
|
|
148
|
+
### OpenSkills 兼容命令
|
|
149
|
+
|
|
150
|
+
#### `joySkills sync`
|
|
151
|
+
生成 AGENTS.md(100% 兼容 `openskills sync` + 增强)
|
|
96
152
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
153
|
+
```bash
|
|
154
|
+
joySkills sync
|
|
155
|
+
# ✅ 扫描 skills/ 目录
|
|
156
|
+
# ✅ 生成 AGENTS.md(OpenSkills 兼容格式)
|
|
157
|
+
# ✅ 生成 joySkills.lock(版本锁定)
|
|
158
|
+
# ✅ 版本一致性检查
|
|
159
|
+
```
|
|
104
160
|
|
|
105
|
-
|
|
106
|
-
|
|
161
|
+
#### `joySkills read <skill-name>`
|
|
162
|
+
读取 skill 内容(100% 兼容 `openskills read`)
|
|
107
163
|
|
|
108
164
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
165
|
+
joySkills read rc-onboarding
|
|
166
|
+
# ✅ 路径查找优先级与 openskills 一致
|
|
167
|
+
# ✅ 输出格式完全相同
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### joySkills 增强命令
|
|
111
171
|
|
|
112
|
-
|
|
113
|
-
|
|
172
|
+
#### `joySkills install <skill>`
|
|
173
|
+
安装 skill(公开 + 团队 + 本地)
|
|
114
174
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
202
|
+
joySkills remove rc-onboarding
|
|
124
203
|
```
|
|
125
204
|
|
|
126
|
-
|
|
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
|
-
|
|
214
|
+
#### `joySkills status`
|
|
141
215
|
显示当前项目的 skills 状态
|
|
142
216
|
|
|
143
217
|
```bash
|
|
144
218
|
joySkills status
|
|
145
219
|
```
|
|
146
220
|
|
|
147
|
-
|
|
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
package/src/commands/install.js
CHANGED
|
@@ -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 "
|
|
31
|
+
console.log(' Use "joySkills remove <skill>" first if you want to reinstall.');
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
169
|
+
// Install the skill
|
|
170
|
+
localManager.installSkill(skillName, skillMdContent);
|
|
100
171
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
180
|
+
await lockfileManager.save();
|
|
109
181
|
|
|
110
|
-
|
|
182
|
+
console.log(`✅ Successfully installed ${skillName} v${targetVersion}`);
|
|
111
183
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
/**
|