joyskills-cli 0.2.10 → 0.3.0

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,149 +1,91 @@
1
- import { RegistryManager } from '../registry.js';
2
- import { LockfileManager } from '../lockfile.js';
3
- import * as path from 'path';
4
- import * as fs from 'fs';
5
- import * as os from 'os';
1
+ import { SkillLoader } from '../skill-loader.js';
6
2
  import chalk from 'chalk';
7
3
 
8
- /** Scan all standard skill directories and return unique skill names */
9
- function scanAllSkillDirs(projectRoot) {
10
- const dirs = [
11
- path.join(projectRoot, '.agent', 'skills'),
12
- path.join(projectRoot, '.claude', 'skills'),
13
- path.join(os.homedir(), '.agent', 'skills'),
14
- path.join(os.homedir(), '.claude', 'skills'),
15
- path.join(projectRoot, 'skills'), // legacy
16
- ];
17
- const seen = new Set();
18
- const skills = [];
19
- for (const dir of dirs) {
20
- if (!fs.existsSync(dir)) continue;
21
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
22
- if (entry.isDirectory() && !seen.has(entry.name)) {
23
- seen.add(entry.name);
24
- const skillMd = path.join(dir, entry.name, 'SKILL.md');
25
- let description = '', version = '';
26
- if (fs.existsSync(skillMd)) {
27
- const content = fs.readFileSync(skillMd, 'utf-8');
28
- description = (content.match(/description:\s*(.+)/) || [])[1]?.trim() || '';
29
- version = (content.match(/version:\s*(.+)/) || [])[1]?.trim() || '';
30
- }
31
- skills.push({ name: entry.name, dir, description, version });
32
- }
33
- }
34
- }
35
- return skills;
36
- }
37
-
38
4
  export function listCommand(program) {
39
5
  program
40
6
  .command('list')
41
- .description('List available skills')
42
- .option('-a, --all-versions', 'Show all versions of each skill')
43
- .option('-c, --category <category>', 'Filter by category')
7
+ .alias('ls')
8
+ .description('List installed skills')
9
+ .option('-a, --agent <agent>', 'Filter by agent (joycode, claude-code, cursor, etc.)')
10
+ .option('-g, --global', 'Show only global (user-level) skills')
11
+ .option('-p, --project', 'Show only project-level skills')
44
12
  .option('-s, --search <query>', 'Search skills by name or description')
45
- .option('-i, --installed', 'Show only installed skills')
46
- .option('-l, --local', 'Show only local skills')
47
- .option('-r, --registry <name>', 'Show skills from specific registry')
13
+ .option('--stats', 'Show statistics')
48
14
  .action(async (options) => {
49
15
  const projectRoot = process.cwd();
50
- const lockfileManager = new LockfileManager(projectRoot);
51
-
52
- console.log(chalk.blue('Available Skills:'));
53
- console.log(chalk.gray('================='));
16
+ const loader = new SkillLoader(projectRoot);
54
17
 
55
18
  try {
56
- await lockfileManager.load();
57
- const installedSkills = lockfileManager.lockData.skills;
58
-
59
- // Scan all skill directories
60
- const localSkills = scanAllSkillDirs(projectRoot);
61
-
62
- if (!options.registry) {
63
- if (localSkills.length > 0) {
64
- console.log(chalk.blue('\nInstalled Skills:'));
65
- let filtered = localSkills;
66
- if (options.search) {
67
- const q = options.search.toLowerCase();
68
- filtered = filtered.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
69
- }
70
- if (filtered.length > 0) {
71
- filtered.forEach(skill => {
72
- const lock = installedSkills[skill.name];
73
- const ver = lock ? chalk.gray(`v${lock.version}`) : (skill.version ? chalk.gray(`v${skill.version}`) : '');
74
- const src = lock ? chalk.gray(`[${lock.source}]`) : '';
75
- console.log(` ✅ ${chalk.bold(skill.name)} ${ver} ${src}`);
76
- if (skill.description) console.log(` ${chalk.gray(skill.description)}`);
77
- });
78
- } else {
79
- console.log(' No matching skills found.');
80
- }
81
- } else {
82
- console.log(chalk.yellow('\n No skills installed.'));
83
- console.log(chalk.gray(' Run: joySkills install <skill>'));
84
- }
19
+ // 加载 skills
20
+ let skills = [];
21
+ if (options.global) {
22
+ skills = loader.loadGlobalSkills(options.agent);
23
+ } else if (options.project) {
24
+ skills = loader.loadProjectSkills(options.agent);
25
+ } else {
26
+ skills = loader.loadAllSkills(options.agent);
85
27
  }
86
28
 
87
- // Check for registry
88
- const registryPath = path.join(projectRoot, '.joyskill/registry');
89
- if (fs.existsSync(registryPath) && !options.local) {
90
- try {
91
- const registryManager = new RegistryManager(registryPath);
92
- await registryManager.load();
29
+ // 搜索过滤
30
+ if (options.search) {
31
+ const q = options.search.toLowerCase();
32
+ skills = skills.filter(s =>
33
+ s.name.toLowerCase().includes(q) ||
34
+ s.description.toLowerCase().includes(q)
35
+ );
36
+ }
93
37
 
94
- console.log('\nRegistry Skills:');
38
+ // 统计模式
39
+ if (options.stats) {
40
+ const stats = loader.getStats(options.agent);
41
+ console.log(chalk.blue('\nSkill Statistics:'));
42
+ console.log(chalk.gray('=================='));
43
+ console.log(` Total: ${stats.total}`);
44
+ console.log(` Project-level: ${stats.project}`);
45
+ console.log(` Global: ${stats.global}`);
46
+ console.log(` Agents: ${stats.agents.join(', ')}`);
47
+ return;
48
+ }
95
49
 
96
- let skills = registryManager.getAllSkills();
50
+ // 显示 skills
51
+ console.log(chalk.blue('\nInstalled Skills:'));
52
+ console.log(chalk.gray('=================='));
97
53
 
98
- // Apply filters
99
- if (options.category) {
100
- skills = registryManager.getSkillsByCategory(options.category);
101
- }
54
+ if (skills.length === 0) {
55
+ console.log(chalk.yellow(' No skills found.'));
56
+ console.log(chalk.gray(' Run: joySkills install <skill>'));
57
+ return;
58
+ }
102
59
 
103
- if (options.search) {
104
- skills = registryManager.searchSkills(options.search);
105
- }
60
+ // scope 分组显示
61
+ const projectSkills = skills.filter(s => s.scope === 'project');
62
+ const globalSkills = skills.filter(s => s.scope === 'global');
106
63
 
107
- if (options.installed) {
108
- skills = skills.filter(skill => installedSkills[skill.id]);
64
+ if (projectSkills.length > 0 && !options.global) {
65
+ console.log(chalk.cyan('\n 📁 Project-level:'));
66
+ projectSkills.forEach(skill => {
67
+ const scopeLabel = skill.agent === 'joycode' ? '' : chalk.gray(`[${skill.agent}]`);
68
+ console.log(` ✅ ${chalk.bold(skill.name)} ${chalk.gray(`v${skill.version}`)} ${scopeLabel}`);
69
+ if (skill.description) {
70
+ console.log(` ${chalk.gray(skill.description)}`);
109
71
  }
72
+ });
73
+ }
110
74
 
111
- if (skills.length > 0) {
112
- skills.forEach(skill => {
113
- const isInstalled = installedSkills[skill.id] ? '✅' : ' ';
114
- const installedVersion = installedSkills[skill.id]?.version;
115
-
116
- // Get recommended version
117
- const recommended = registryManager.getRecommendedVersion(skill.id);
118
- const recommendedVersion = recommended ? recommended.version : 'N/A';
119
-
120
- console.log(` ${isInstalled} ${skill.name || skill.id} (${skill.id})`);
121
- if (options.allVersions) {
122
- skill.versions.forEach(v => {
123
- const marker = v.recommended ? '⭐' : ' ';
124
- const state = v.state === 'approved' ? '✓' : v.state;
125
- console.log(` ${marker} v${v.version} [${state}]`);
126
- });
127
- } else {
128
- console.log(` Recommended: v${recommendedVersion}`);
129
- if (installedVersion) {
130
- console.log(` Installed: v${installedVersion}`);
131
- }
132
- }
133
- if (skill.description) {
134
- console.log(` ${skill.description}`);
135
- }
136
- console.log('');
137
- });
138
- } else {
139
- console.log(' No matching registry skills found.');
75
+ if (globalSkills.length > 0 && !options.project) {
76
+ console.log(chalk.cyan('\n 🏠 Global (user-level):'));
77
+ globalSkills.forEach(skill => {
78
+ const scopeLabel = skill.agent === 'joycode' ? '' : chalk.gray(`[${skill.agent}]`);
79
+ console.log(` ✅ ${chalk.bold(skill.name)} ${chalk.gray(`v${skill.version}`)} ${scopeLabel}`);
80
+ if (skill.description) {
81
+ console.log(` ${chalk.gray(skill.description)}`);
140
82
  }
141
- } catch (error) {
142
- console.error('Error loading registry:', error.message);
143
- }
83
+ });
144
84
  }
85
+
86
+ console.log(chalk.gray(`\n Total: ${skills.length} skill(s)`));
145
87
  } catch (error) {
146
- console.error('Error listing skills:', error);
88
+ console.error(chalk.red('Error listing skills:'), error.message);
147
89
  }
148
90
  });
149
91
  }
@@ -5,12 +5,21 @@ import * as os from 'os';
5
5
  import chalk from 'chalk';
6
6
 
7
7
  function findSkillOnDisk(skillName, projectRoot) {
8
+ // v2.0: Support all agent directories, joycode first
8
9
  const dirs = [
10
+ // Project-level
11
+ path.join(projectRoot, '.joycode', 'skills'),
9
12
  path.join(projectRoot, '.agent', 'skills'),
10
13
  path.join(projectRoot, '.claude', 'skills'),
14
+ path.join(projectRoot, '.cursor', 'skills'),
15
+ path.join(projectRoot, '.qoder', 'skills'),
16
+ path.join(projectRoot, 'skills'),
17
+ // Global
18
+ path.join(os.homedir(), '.joycode', 'skills'),
11
19
  path.join(os.homedir(), '.agent', 'skills'),
12
20
  path.join(os.homedir(), '.claude', 'skills'),
13
- path.join(projectRoot, 'skills'),
21
+ path.join(os.homedir(), '.cursor', 'skills'),
22
+ path.join(os.homedir(), '.qoder', 'skills'),
14
23
  ];
15
24
  for (const d of dirs) {
16
25
  const p = path.join(d, skillName);
@@ -20,12 +20,18 @@ export function syncCommand(program) {
20
20
 
21
21
  console.log(chalk.blue('🔍 Scanning skills...'));
22
22
 
23
- // Scan all skill directories (openskill priority order)
23
+ // Scan project-level skill directories only (v2.0)
24
+ // AGENTS.md should only reference project-level skills
24
25
  const skillDirs = [
26
+ // JoyCode (default, highest priority)
27
+ { path: path.join(projectRoot, '.joycode', 'skills'), label: 'joycode' },
28
+ // Universal (multi-agent compatible)
25
29
  { 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' },
30
+ // Claude Code
31
+ { path: path.join(projectRoot, '.claude', 'skills'), label: 'claude' },
32
+ // Other agents
33
+ { path: path.join(projectRoot, '.cursor', 'skills'), label: 'cursor' },
34
+ { path: path.join(projectRoot, '.qoder', 'skills'), label: 'qoder' },
29
35
  // legacy
30
36
  { path: path.join(projectRoot, 'skills'), label: 'legacy' },
31
37
  ];
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Update Command - 更新技能
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { getUpdatableSkills } from '../version-checker.js';
7
+ import { SkillLoader } from '../skill-loader.js';
8
+ import simpleGit from 'simple-git';
9
+ import chalk from 'chalk';
10
+ import { confirm } from '@inquirer/prompts';
11
+
12
+ export function updateCommand(program) {
13
+ program
14
+ .command('update [skills...]')
15
+ .description('Update installed skills to latest versions')
16
+ .option('-y, --yes', 'Skip confirmation prompts')
17
+ .option('-a, --all', 'Update all skills')
18
+ .action(async (skillNames, options) => {
19
+ try {
20
+ const projectRoot = process.cwd();
21
+
22
+ console.log(chalk.blue('Updating skills...'));
23
+ console.log(chalk.gray('==================\n'));
24
+
25
+ // 获取要更新的 skills
26
+ let skillsToUpdate = [];
27
+
28
+ if (options.all || skillNames.length === 0) {
29
+ // 更新所有有更新的 skills
30
+ const updatable = await getUpdatableSkills(projectRoot);
31
+ skillsToUpdate = updatable;
32
+ } else {
33
+ // 更新指定的 skills
34
+ const loader = new SkillLoader(projectRoot);
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
+ }
53
+ }
54
+
55
+ if (skillsToUpdate.length === 0) {
56
+ console.log(chalk.green('✅ No skills to update.'));
57
+ return;
58
+ }
59
+
60
+ // 显示将要更新的 skills
61
+ console.log(chalk.cyan('Skills to update:'));
62
+ for (const skill of skillsToUpdate) {
63
+ console.log(` 📦 ${skill.name} (v${skill.currentVersion})`);
64
+ }
65
+ console.log();
66
+
67
+ // 确认
68
+ if (!options.yes) {
69
+ const shouldUpdate = await confirm({
70
+ message: `Update ${skillsToUpdate.length} skill(s)?`,
71
+ default: true,
72
+ });
73
+
74
+ if (!shouldUpdate) {
75
+ console.log(chalk.gray('Update cancelled.'));
76
+ return;
77
+ }
78
+ }
79
+
80
+ // 执行更新
81
+ console.log(chalk.blue('\nUpdating...\n'));
82
+
83
+ const results = [];
84
+ for (const skill of skillsToUpdate) {
85
+ try {
86
+ const git = simpleGit(skill.path);
87
+
88
+ // 拉取最新代码
89
+ await git.pull('origin', 'main', ['--depth', '1']);
90
+
91
+ console.log(chalk.green(`✅ ${skill.name} updated successfully`));
92
+ results.push({ name: skill.name, success: true });
93
+ } catch (e) {
94
+ console.log(chalk.red(`❌ ${skill.name} failed: ${e.message}`));
95
+ results.push({ name: skill.name, success: false, error: e.message });
96
+ }
97
+ }
98
+
99
+ // 总结
100
+ const successCount = results.filter(r => r.success).length;
101
+ const failCount = results.length - successCount;
102
+
103
+ console.log(chalk.gray('\n=================='));
104
+ console.log(chalk.green(`✅ Updated: ${successCount}`));
105
+ if (failCount > 0) {
106
+ console.log(chalk.red(`❌ Failed: ${failCount}`));
107
+ }
108
+
109
+ } catch (error) {
110
+ console.error(chalk.red('❌ Failed to update:'), error.message);
111
+ process.exit(1);
112
+ }
113
+ });
114
+ }
package/src/index.js CHANGED
@@ -14,6 +14,8 @@ import { syncCommand } from './commands/sync.js';
14
14
  import { upgradeCommand } from './commands/upgrade.js';
15
15
  import { readCommand } from './commands/read.js';
16
16
  import { manageCommand } from './commands/manage.js';
17
+ import { checkCommand } from './commands/check.js';
18
+ import { updateCommand } from './commands/update.js';
17
19
 
18
20
  const program = new Command();
19
21
 
@@ -33,6 +35,8 @@ syncCommand(program);
33
35
  upgradeCommand(program);
34
36
  readCommand(program);
35
37
  manageCommand(program);
38
+ checkCommand(program);
39
+ updateCommand(program);
36
40
 
37
41
  // Parse arguments
38
42
  program.parse();
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Installer - Skill 安装器
3
+ * 支持 Symlink 和 Copy 两种模式
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ /**
11
+ * 安装方式枚举
12
+ */
13
+ export const InstallMethod = {
14
+ SYMLINK: 'symlink', // 符号链接(默认,单点更新)
15
+ COPY: 'copy', // 复制文件
16
+ };
17
+
18
+ /**
19
+ * 检查是否支持符号链接
20
+ */
21
+ export function supportsSymlink(targetDir) {
22
+ try {
23
+ // 尝试创建一个测试符号链接
24
+ const testLink = path.join(targetDir, '.symlink-test');
25
+ const testTarget = path.join(targetDir, '.symlink-target');
26
+
27
+ // 清理旧的测试文件
28
+ if (fs.existsSync(testLink)) fs.unlinkSync(testLink);
29
+ if (fs.existsSync(testTarget)) fs.unlinkSync(testTarget);
30
+
31
+ // 创建测试目标文件
32
+ fs.writeFileSync(testTarget, 'test');
33
+
34
+ // 尝试创建符号链接
35
+ fs.symlinkSync(testTarget, testLink);
36
+
37
+ // 清理
38
+ fs.unlinkSync(testLink);
39
+ fs.unlinkSync(testTarget);
40
+
41
+ return true;
42
+ } catch (e) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 复制目录(递归)
49
+ */
50
+ export function copyRecursive(src, dest) {
51
+ // 如果目标已存在,先删除
52
+ if (fs.existsSync(dest)) {
53
+ fs.rmSync(dest, { recursive: true, force: true });
54
+ }
55
+
56
+ const stat = fs.statSync(src);
57
+
58
+ if (stat.isDirectory()) {
59
+ fs.mkdirSync(dest, { recursive: true });
60
+ const entries = fs.readdirSync(src, { withFileTypes: true });
61
+
62
+ for (const entry of entries) {
63
+ const srcPath = path.join(src, entry.name);
64
+ const destPath = path.join(dest, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ copyRecursive(srcPath, destPath);
68
+ } else {
69
+ fs.copyFileSync(srcPath, destPath);
70
+ }
71
+ }
72
+ } else {
73
+ fs.copyFileSync(src, dest);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 创建符号链接(跨平台)
79
+ */
80
+ export function createSymlink(target, linkPath) {
81
+ // 确保目标存在
82
+ if (!fs.existsSync(target)) {
83
+ throw new Error(`Target does not exist: ${target}`);
84
+ }
85
+
86
+ // 如果链接已存在,先删除
87
+ if (fs.existsSync(linkPath)) {
88
+ const stat = fs.lstatSync(linkPath);
89
+ if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) {
90
+ fs.rmSync(linkPath, { recursive: true, force: true });
91
+ }
92
+ }
93
+
94
+ // 确保父目录存在
95
+ const parentDir = path.dirname(linkPath);
96
+ if (!fs.existsSync(parentDir)) {
97
+ fs.mkdirSync(parentDir, { recursive: true });
98
+ }
99
+
100
+ // 创建符号链接
101
+ const isDir = fs.statSync(target).isDirectory();
102
+ fs.symlinkSync(target, linkPath, isDir ? 'dir' : 'file');
103
+ }
104
+
105
+ /**
106
+ * 安装 Skill
107
+ */
108
+ export function installSkill(sourcePath, targetPath, method = InstallMethod.SYMLINK) {
109
+ // 确保源存在
110
+ if (!fs.existsSync(sourcePath)) {
111
+ throw new Error(`Source skill not found: ${sourcePath}`);
112
+ }
113
+
114
+ // 确保父目录存在
115
+ const parentDir = path.dirname(targetPath);
116
+ if (!fs.existsSync(parentDir)) {
117
+ fs.mkdirSync(parentDir, { recursive: true });
118
+ }
119
+
120
+ if (method === InstallMethod.SYMLINK) {
121
+ // 检查是否支持符号链接
122
+ if (!supportsSymlink(parentDir)) {
123
+ console.log('Symlink not supported, falling back to copy mode...');
124
+ copyRecursive(sourcePath, targetPath);
125
+ } else {
126
+ createSymlink(sourcePath, targetPath);
127
+ }
128
+ } else {
129
+ // Copy 模式
130
+ copyRecursive(sourcePath, targetPath);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 批量安装 Skills
136
+ */
137
+ export function installSkills(skills, targetDir, method = InstallMethod.SYMLINK) {
138
+ const results = [];
139
+
140
+ for (const skill of skills) {
141
+ try {
142
+ const targetPath = path.join(targetDir, skill.name);
143
+ installSkill(skill.path, targetPath, method);
144
+ results.push({
145
+ name: skill.name,
146
+ success: true,
147
+ method: fs.lstatSync(targetPath).isSymbolicLink() ? 'symlink' : 'copy',
148
+ });
149
+ } catch (e) {
150
+ results.push({
151
+ name: skill.name,
152
+ success: false,
153
+ error: e.message,
154
+ });
155
+ }
156
+ }
157
+
158
+ return results;
159
+ }
160
+
161
+ /**
162
+ * 移除 Skill
163
+ */
164
+ export function removeSkill(skillPath) {
165
+ if (!fs.existsSync(skillPath)) {
166
+ return false;
167
+ }
168
+
169
+ fs.rmSync(skillPath, { recursive: true, force: true });
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * 获取安装信息
175
+ */
176
+ export function getInstallInfo(skillPath) {
177
+ if (!fs.existsSync(skillPath)) {
178
+ return null;
179
+ }
180
+
181
+ const stat = fs.lstatSync(skillPath);
182
+
183
+ return {
184
+ exists: true,
185
+ isSymlink: stat.isSymbolicLink(),
186
+ target: stat.isSymbolicLink() ? fs.readlinkSync(skillPath) : null,
187
+ size: stat.size,
188
+ modified: stat.mtime,
189
+ };
190
+ }