joyskills-cli 0.2.6 → 0.2.7
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 +81 -195
- package/package.json +21 -12
- package/src/commands/install.js +616 -211
- package/src/commands/list.js +54 -25
- package/src/commands/manage.js +102 -0
- package/src/commands/read.js +61 -118
- package/src/commands/remove.js +28 -20
- package/src/commands/status.js +31 -41
- package/src/commands/sync.js +155 -244
- package/src/commands/team.js +267 -47
- package/src/commands/upgrade.js +311 -0
- package/src/index.js +10 -12
- package/src/local.js +2 -9
- package/LICENSE +0 -21
- package/spec/cli-spec.md +0 -167
- package/spec/lockfile-spec.md +0 -108
- package/spec/registry-spec.md +0 -117
package/src/commands/sync.js
CHANGED
|
@@ -1,263 +1,174 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { LocalManager } from '../local.js';
|
|
3
|
+
import { LockfileManager } from '../lockfile.js';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export function syncCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('sync')
|
|
12
|
+
.description('Scan skills and generate AGENTS.md')
|
|
13
|
+
.option('-o, --output <file>', 'Output file name', 'AGENTS.md')
|
|
14
|
+
.option('-y, --yes', 'Skip prompts')
|
|
15
|
+
.option('--no-lock', 'Skip updating joySkills.lock')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const projectRoot = process.cwd();
|
|
19
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
20
|
+
|
|
21
|
+
console.log(chalk.blue('🔍 Scanning skills...'));
|
|
22
|
+
|
|
23
|
+
// Scan all skill directories (openskill priority order)
|
|
24
|
+
const skillDirs = [
|
|
25
|
+
{ path: path.join(projectRoot, '.agent', 'skills'), label: 'universal' },
|
|
26
|
+
{ path: path.join(projectRoot, '.claude', 'skills'), label: 'project' },
|
|
27
|
+
{ path: path.join(os.homedir(), '.agent', 'skills'), label: 'global-universal' },
|
|
28
|
+
{ path: path.join(os.homedir(), '.claude', 'skills'), label: 'global' },
|
|
29
|
+
// legacy
|
|
30
|
+
{ path: path.join(projectRoot, 'skills'), label: 'legacy' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const allSkills = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
|
|
36
|
+
for (const dir of skillDirs) {
|
|
37
|
+
if (!fs.existsSync(dir.path)) continue;
|
|
38
|
+
const skills = scanSkillDirectory(dir.path);
|
|
39
|
+
for (const skill of skills) {
|
|
40
|
+
if (!seen.has(skill.name)) {
|
|
41
|
+
seen.add(skill.name);
|
|
42
|
+
allSkills.push(skill);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (allSkills.length === 0) {
|
|
48
|
+
console.log(chalk.yellow('⚠️ No skills found'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green(` Found ${allSkills.length} skill(s)`));
|
|
53
|
+
|
|
54
|
+
// Generate AGENTS.md with openskill-compatible XML format
|
|
55
|
+
const agentsMd = generateAgentsMd(allSkills);
|
|
56
|
+
const outputPath = path.join(projectRoot, options.output);
|
|
57
|
+
fs.writeFileSync(outputPath, agentsMd, 'utf-8');
|
|
58
|
+
console.log(chalk.green(` ✓ Generated ${options.output}`));
|
|
59
|
+
|
|
60
|
+
// Update lockfile
|
|
61
|
+
if (options.lock !== false) {
|
|
62
|
+
await lockfileManager.load();
|
|
63
|
+
for (const skill of allSkills) {
|
|
64
|
+
const existing = lockfileManager.getSkill(skill.name);
|
|
65
|
+
lockfileManager.updateSkill(skill.name, {
|
|
66
|
+
version: skill.version || existing?.version || '1.0.0',
|
|
67
|
+
source: existing?.source || 'local',
|
|
68
|
+
installedAt: existing?.installedAt || new Date().toISOString(),
|
|
69
|
+
path: skill.path
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
await lockfileManager.save();
|
|
73
|
+
console.log(chalk.green(` ✓ Updated joySkills.lock`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Print summary
|
|
77
|
+
console.log(chalk.blue('\n📋 Skills Summary:'));
|
|
78
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
79
|
+
for (const skill of allSkills) {
|
|
80
|
+
const status = skill.hasSkillMd ? chalk.green('✓') : chalk.red('✗');
|
|
81
|
+
console.log(` ${status} ${skill.name} ${chalk.gray(skill.version || '')}`);
|
|
82
|
+
if (skill.description) console.log(` ${chalk.gray(skill.description)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.green(`\n✅ Sync completed`));
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(chalk.red(`❌ Sync failed: ${error.message}`));
|
|
89
|
+
process.exit(1);
|
|
45
90
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 去重(按 id)
|
|
49
|
-
const uniqueSkills = [];
|
|
50
|
-
const seenIds = new Set();
|
|
51
|
-
for (const skill of allSkills) {
|
|
52
|
-
if (!seenIds.has(skill.id)) {
|
|
53
|
-
seenIds.add(skill.id);
|
|
54
|
-
uniqueSkills.push(skill);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
console.log(`📦 扫描到 ${uniqueSkills.length} 个 skills:\n`);
|
|
59
|
-
uniqueSkills.forEach(skill => {
|
|
60
|
-
console.log(` - ${skill.id} (${skill.version || 'unknown'})`);
|
|
61
91
|
});
|
|
62
|
-
console.log('');
|
|
63
|
-
|
|
64
|
-
// 2. 生成 AGENTS.md(OpenSkills 兼容格式)
|
|
65
|
-
const agentsMdContent = generateAgentsMd(uniqueSkills);
|
|
66
|
-
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
67
|
-
fs.writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
|
|
68
|
-
console.log(`✅ 已生成 AGENTS.md\n`);
|
|
69
|
-
|
|
70
|
-
// 3. 更新 joySkills.lock(joySkills 增强功能)
|
|
71
|
-
await updateLockfile(uniqueSkills);
|
|
72
|
-
|
|
73
|
-
// 4. 验证版本一致性(joySkills 增强功能)
|
|
74
|
-
await verifyConsistency(uniqueSkills);
|
|
75
|
-
|
|
76
|
-
console.log('✨ 同步完成!\n');
|
|
77
|
-
console.log('💡 提示:');
|
|
78
|
-
console.log(' - AGENTS.md 已生成,AI 编辑器会自动识别');
|
|
79
|
-
console.log(' - joySkills.lock 已更新,确保版本锁定\n');
|
|
80
|
-
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error('❌ 同步失败:', error.message);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
* 扫描 skills 目录
|
|
89
|
-
*/
|
|
90
|
-
function scanSkills(skillsDir) {
|
|
94
|
+
function scanSkillDirectory(basePath) {
|
|
91
95
|
const skills = [];
|
|
92
|
-
const entries = fs.readdirSync(
|
|
93
|
-
|
|
96
|
+
const entries = fs.readdirSync(basePath, { withFileTypes: true });
|
|
97
|
+
|
|
94
98
|
for (const entry of entries) {
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
100
|
+
const skillPath = path.join(basePath, entry.name);
|
|
101
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
102
|
+
|
|
103
|
+
let skillInfo = {
|
|
104
|
+
name: entry.name,
|
|
105
|
+
path: skillPath,
|
|
106
|
+
hasSkillMd: fs.existsSync(skillMdPath),
|
|
107
|
+
version: '1.0.0',
|
|
108
|
+
description: ''
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (skillInfo.hasSkillMd) {
|
|
112
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
113
|
+
const parsed = parseSkillMd(content);
|
|
114
|
+
skillInfo = { ...skillInfo, ...parsed };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
skills.push(skillInfo);
|
|
103
118
|
}
|
|
104
|
-
|
|
105
|
-
// 解析 SKILL.md
|
|
106
|
-
const skillContent = fs.readFileSync(skillMdPath, 'utf-8');
|
|
107
|
-
const metadata = parseSkillMetadata(skillContent);
|
|
108
|
-
|
|
109
|
-
skills.push({
|
|
110
|
-
id: entry.name,
|
|
111
|
-
path: skillPath,
|
|
112
|
-
...metadata
|
|
113
|
-
});
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
return skills;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 解析 SKILL.md 的 YAML frontmatter
|
|
121
|
-
*/
|
|
122
|
-
function parseSkillMetadata(content) {
|
|
123
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
124
|
-
if (!match) return {};
|
|
125
|
-
|
|
126
|
-
const yamlContent = match[1];
|
|
127
120
|
|
|
128
|
-
|
|
129
|
-
// 使用 yaml 包解析(支持完整 YAML 语法)
|
|
130
|
-
return YAML.parse(yamlContent) || {};
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.warn(`⚠️ YAML 解析失败: ${error.message}`);
|
|
133
|
-
return {};
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 生成 AGENTS.md(OpenSkills 兼容格式)
|
|
139
|
-
*/
|
|
140
|
-
function generateAgentsMd(skills) {
|
|
141
|
-
let content = `# AI Agent Skills\n\n`;
|
|
142
|
-
content += `> 此文件由 joySkills sync 自动生成\n`;
|
|
143
|
-
content += `> 兼容 OpenSkills 格式,所有 AI 编辑器均可识别\n\n`;
|
|
144
|
-
|
|
145
|
-
content += `## 已安装的 Skills (${skills.length})\n\n`;
|
|
146
|
-
|
|
147
|
-
for (const skill of skills) {
|
|
148
|
-
content += `### ${skill.name || skill.id}\n\n`;
|
|
149
|
-
content += `**ID**: \`${skill.id}\`\n`;
|
|
150
|
-
if (skill.version) {
|
|
151
|
-
content += `**Version**: ${skill.version}\n`;
|
|
152
|
-
}
|
|
153
|
-
if (skill.description) {
|
|
154
|
-
content += `**Description**: ${skill.description}\n`;
|
|
155
|
-
}
|
|
156
|
-
content += `**Path**: \`skills/${skill.id}\`\n\n`;
|
|
157
|
-
|
|
158
|
-
// 添加 XML 片段(供 AI 编辑器解析)
|
|
159
|
-
content += `\`\`\`xml\n`;
|
|
160
|
-
content += `<skill id="${skill.id}">\n`;
|
|
161
|
-
content += ` <name>${skill.name || skill.id}</name>\n`;
|
|
162
|
-
if (skill.version) {
|
|
163
|
-
content += ` <version>${skill.version}</version>\n`;
|
|
164
|
-
}
|
|
165
|
-
content += ` <path>skills/${skill.id}/SKILL.md</path>\n`;
|
|
166
|
-
content += `</skill>\n`;
|
|
167
|
-
content += `\`\`\`\n\n`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
content += `---\n\n`;
|
|
171
|
-
content += `生成时间: ${new Date().toISOString()}\n`;
|
|
172
|
-
content += `生成工具: joySkills ${getVersion()}\n`;
|
|
173
|
-
|
|
174
|
-
return content;
|
|
121
|
+
return skills;
|
|
175
122
|
}
|
|
176
123
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const lockfilePath = path.join(process.cwd(), 'joySkills.lock');
|
|
182
|
-
|
|
183
|
-
let lockData = {
|
|
184
|
-
lockVersion: 1,
|
|
185
|
-
projectId: path.basename(process.cwd()),
|
|
186
|
-
generatedAt: new Date().toISOString(),
|
|
187
|
-
skills: {}
|
|
124
|
+
function parseSkillMd(content) {
|
|
125
|
+
const result = {
|
|
126
|
+
version: '1.0.0',
|
|
127
|
+
description: ''
|
|
188
128
|
};
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
129
|
+
|
|
130
|
+
// Parse frontmatter
|
|
131
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
132
|
+
if (frontmatterMatch) {
|
|
133
|
+
const frontmatter = frontmatterMatch[1];
|
|
134
|
+
|
|
135
|
+
const versionMatch = frontmatter.match(/version:\s*(.+)/);
|
|
136
|
+
if (versionMatch) {
|
|
137
|
+
result.version = versionMatch[1].trim();
|
|
197
138
|
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 更新 skills 信息
|
|
201
|
-
for (const skill of skills) {
|
|
202
|
-
const existingSkill = lockData.skills[skill.id] || {};
|
|
203
139
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
lastSyncAt: new Date().toISOString()
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 写入 lockfile
|
|
212
|
-
fs.writeFileSync(
|
|
213
|
-
lockfilePath,
|
|
214
|
-
JSON.stringify(lockData, null, 2),
|
|
215
|
-
'utf-8'
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
console.log('✅ 已更新 joySkills.lock');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 验证版本一致性(joySkills 增强功能)
|
|
223
|
-
*/
|
|
224
|
-
async function verifyConsistency(skills) {
|
|
225
|
-
const lockfilePath = path.join(process.cwd(), 'joySkills.lock');
|
|
226
|
-
if (!fs.existsSync(lockfilePath)) return;
|
|
227
|
-
|
|
228
|
-
const lockData = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
|
|
229
|
-
let hasWarning = false;
|
|
230
|
-
|
|
231
|
-
for (const skill of skills) {
|
|
232
|
-
const lockedSkill = lockData.skills[skill.id];
|
|
233
|
-
if (!lockedSkill) continue;
|
|
234
|
-
|
|
235
|
-
if (skill.version && lockedSkill.version !== skill.version) {
|
|
236
|
-
if (!hasWarning) {
|
|
237
|
-
console.log('\n⚠️ 版本不一致警告:\n');
|
|
238
|
-
hasWarning = true;
|
|
239
|
-
}
|
|
240
|
-
console.log(` ${skill.id}:`);
|
|
241
|
-
console.log(` - Lock 文件: ${lockedSkill.version}`);
|
|
242
|
-
console.log(` - 实际版本: ${skill.version}`);
|
|
243
|
-
console.log(` 💡 建议: joySkills upgrade ${skill.id}\n`);
|
|
140
|
+
const descMatch = frontmatter.match(/description:\s*(.+)/);
|
|
141
|
+
if (descMatch) {
|
|
142
|
+
result.description = descMatch[1].trim();
|
|
244
143
|
}
|
|
245
144
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
console.log('✅ 版本一致性检查通过');
|
|
249
|
-
}
|
|
145
|
+
|
|
146
|
+
return result;
|
|
250
147
|
}
|
|
251
148
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
149
|
+
function generateAgentsMd(skills) {
|
|
150
|
+
// openskill-compatible <available_skills> XML block
|
|
151
|
+
const skillBlocks = skills.map(skill => {
|
|
152
|
+
const location = skill.hasSkillMd ? 'plugin' : 'inline';
|
|
153
|
+
const desc = skill.description
|
|
154
|
+
? skill.description.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
155
|
+
: `Skill: ${skill.name}`;
|
|
156
|
+
return [
|
|
157
|
+
`<skill>`,
|
|
158
|
+
`<name>${skill.name}</name>`,
|
|
159
|
+
`<description>${desc}</description>`,
|
|
160
|
+
`<location>${location}</location>`,
|
|
161
|
+
`</skill>`
|
|
162
|
+
].join('\n');
|
|
163
|
+
}).join('\n');
|
|
164
|
+
|
|
165
|
+
const lines = [
|
|
166
|
+
`<available_skills>`,
|
|
167
|
+
skillBlocks,
|
|
168
|
+
`</available_skills>`,
|
|
169
|
+
``,
|
|
170
|
+
`> Generated by joySkills on ${new Date().toISOString()}. Run \`joySkills sync\` to update.`,
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
return lines.join('\n');
|
|
263
174
|
}
|