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.
@@ -1,263 +1,174 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
- import * as YAML from 'yaml';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- /**
12
- * joySkills sync 命令
13
- *
14
- * 功能:
15
- * 1. 扫描 skills/ 目录
16
- * 2. 生成/更新 AGENTS.md(与 openskills sync 格式一致)
17
- * 3. 更新 joySkills.lock(记录扫描到的所有 skill)
18
- * 4. 验证版本一致性
19
- *
20
- * 设计原则:
21
- * - 100% 兼容 OpenSkills 生成的 AGENTS.md
22
- * - 在 OpenSkills 基础上增强(版本锁定 + 一致性检查)
23
- */
24
-
25
- export async function syncCommand() {
26
- console.log('🔄 正在同步 skills AGENTS.md...\n');
27
-
28
- try {
29
- // 1. 扫描所有可能的 skills 目录(按优先级)
30
- const cwd = process.cwd();
31
- const homeDir = process.env.HOME || process.env.USERPROFILE;
32
-
33
- const searchDirs = [
34
- path.join(cwd, '.agent', 'skills'), // joySkills 项目级
35
- path.join(homeDir, '.agent', 'skills'), // joySkills 全局
36
- path.join(cwd, '.claude', 'skills'), // OpenSkills --project
37
- path.join(homeDir, '.claude', 'skills'), // OpenSkills --global
38
- ];
39
-
40
- const allSkills = [];
41
- for (const dir of searchDirs) {
42
- if (fs.existsSync(dir)) {
43
- const skills = scanSkills(dir);
44
- allSkills.push(...skills);
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(skillsDir, { withFileTypes: true });
93
-
96
+ const entries = fs.readdirSync(basePath, { withFileTypes: true });
97
+
94
98
  for (const entry of entries) {
95
- if (!entry.isDirectory()) continue;
96
-
97
- const skillPath = path.join(skillsDir, entry.name);
98
- const skillMdPath = path.join(skillPath, 'SKILL.md');
99
-
100
- if (!fs.existsSync(skillMdPath)) {
101
- console.log(`⚠️ 跳过 ${entry.name}(缺少 SKILL.md)`);
102
- continue;
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
- try {
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
- * 更新 joySkills.lock(joySkills 增强功能)
179
- */
180
- async function updateLockfile(skills) {
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
- // 读取现有 lockfile
191
- if (fs.existsSync(lockfilePath)) {
192
- const existingContent = fs.readFileSync(lockfilePath, 'utf-8');
193
- try {
194
- lockData = JSON.parse(existingContent);
195
- } catch (e) {
196
- console.log('⚠️ joySkills.lock 格式错误,将重新生成');
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
- lockData.skills[skill.id] = {
205
- version: skill.version || existingSkill.version || 'unknown',
206
- installedAt: existingSkill.installedAt || new Date().toISOString(),
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
- if (!hasWarning) {
248
- console.log('✅ 版本一致性检查通过');
249
- }
145
+
146
+ return result;
250
147
  }
251
148
 
252
- /**
253
- * 获取 joySkills 版本
254
- */
255
- function getVersion() {
256
- try {
257
- const packagePath = path.join(__dirname, '../../package.json');
258
- const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
259
- return pkg.version;
260
- } catch (e) {
261
- return 'unknown';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
  }