joyskills-cli 0.2.9 → 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.
- package/package.json +10 -3
- package/src/agents.js +198 -0
- package/src/commands/check.js +69 -0
- package/src/commands/install.js +106 -22
- package/src/commands/list.js +66 -124
- package/src/commands/remove.js +12 -2
- package/src/commands/sync.js +10 -4
- package/src/commands/update.js +114 -0
- package/src/commands/upgrade.js +7 -4
- package/src/index.js +4 -0
- package/src/installer.js +190 -0
- package/src/skill-loader.js +207 -0
- package/src/version-checker.js +129 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillLoader - 统一加载 Skills
|
|
3
|
+
* 支持项目级和用户级目录,自动合并同名 Skill(项目级优先)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getAllSkillPaths, getAgentProjectPath, getAgentGlobalPath } from './agents.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Skill 对象
|
|
12
|
+
*/
|
|
13
|
+
export class Skill {
|
|
14
|
+
constructor(name, skillPath, scope, agent, metadata = {}) {
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.path = skillPath;
|
|
17
|
+
this.scope = scope; // 'project' | 'global'
|
|
18
|
+
this.agent = agent;
|
|
19
|
+
this.metadata = metadata;
|
|
20
|
+
this.version = metadata.version || '1.0.0';
|
|
21
|
+
this.description = metadata.description || '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SkillLoader 类
|
|
27
|
+
*/
|
|
28
|
+
export class SkillLoader {
|
|
29
|
+
constructor(projectRoot) {
|
|
30
|
+
this.projectRoot = projectRoot;
|
|
31
|
+
this.skillsCache = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 扫描单个目录中的所有 Skills
|
|
36
|
+
*/
|
|
37
|
+
scanDirectory(dirPath, scope, agent) {
|
|
38
|
+
const skills = [];
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(dirPath)) {
|
|
41
|
+
return skills;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const skillPath = path.join(dirPath, entry.name);
|
|
52
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 解析 SKILL.md
|
|
59
|
+
const metadata = this.parseSkillMd(skillMdPath);
|
|
60
|
+
|
|
61
|
+
skills.push(new Skill(
|
|
62
|
+
entry.name,
|
|
63
|
+
skillPath,
|
|
64
|
+
scope,
|
|
65
|
+
agent,
|
|
66
|
+
metadata
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return skills;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 解析 SKILL.md 的 frontmatter
|
|
75
|
+
*/
|
|
76
|
+
parseSkillMd(skillMdPath) {
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
79
|
+
const metadata = {};
|
|
80
|
+
|
|
81
|
+
// 解析 YAML frontmatter
|
|
82
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
83
|
+
if (frontmatterMatch) {
|
|
84
|
+
const frontmatter = frontmatterMatch[1];
|
|
85
|
+
|
|
86
|
+
// 简单解析 key: value
|
|
87
|
+
const lines = frontmatter.split('\n');
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
metadata[match[1]] = match[2].trim();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return metadata;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 加载所有 Skills(项目级 + 用户级)
|
|
104
|
+
* 同名 Skill,项目级优先覆盖用户级
|
|
105
|
+
*/
|
|
106
|
+
loadAllSkills(agentId = null) {
|
|
107
|
+
if (this.skillsCache) {
|
|
108
|
+
return this.skillsCache;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const skillPaths = getAllSkillPaths(this.projectRoot, agentId);
|
|
112
|
+
const skillMap = new Map();
|
|
113
|
+
|
|
114
|
+
// 按优先级顺序扫描(高优先级在后,覆盖低优先级)
|
|
115
|
+
for (const { path: dirPath, scope, agent } of skillPaths.reverse()) {
|
|
116
|
+
const skills = this.scanDirectory(dirPath, scope, agent);
|
|
117
|
+
|
|
118
|
+
for (const skill of skills) {
|
|
119
|
+
// 项目级覆盖用户级,同名 Skill 后加载的覆盖先加载的
|
|
120
|
+
skillMap.set(skill.name, skill);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.skillsCache = Array.from(skillMap.values());
|
|
125
|
+
return this.skillsCache;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 仅加载项目级 Skills
|
|
130
|
+
*/
|
|
131
|
+
loadProjectSkills(agentId = null) {
|
|
132
|
+
const allSkills = this.loadAllSkills(agentId);
|
|
133
|
+
return allSkills.filter(s => s.scope === 'project');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 仅加载用户级 Skills
|
|
138
|
+
*/
|
|
139
|
+
loadGlobalSkills(agentId = null) {
|
|
140
|
+
const allSkills = this.loadAllSkills(agentId);
|
|
141
|
+
return allSkills.filter(s => s.scope === 'global');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 获取指定 Skill
|
|
146
|
+
*/
|
|
147
|
+
getSkill(skillName, agentId = null) {
|
|
148
|
+
const skills = this.loadAllSkills(agentId);
|
|
149
|
+
return skills.find(s => s.name === skillName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 检查 Skill 是否存在
|
|
154
|
+
*/
|
|
155
|
+
hasSkill(skillName, agentId = null) {
|
|
156
|
+
return !!this.getSkill(skillName, agentId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 获取 Skill 路径
|
|
161
|
+
*/
|
|
162
|
+
getSkillPath(skillName, agentId = null) {
|
|
163
|
+
const skill = this.getSkill(skillName, agentId);
|
|
164
|
+
return skill ? skill.path : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 清除缓存
|
|
169
|
+
*/
|
|
170
|
+
clearCache() {
|
|
171
|
+
this.skillsCache = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取 Skill 统计信息
|
|
176
|
+
*/
|
|
177
|
+
getStats(agentId = null) {
|
|
178
|
+
const allSkills = this.loadAllSkills(agentId);
|
|
179
|
+
const projectSkills = allSkills.filter(s => s.scope === 'project');
|
|
180
|
+
const globalSkills = allSkills.filter(s => s.scope === 'global');
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
total: allSkills.length,
|
|
184
|
+
project: projectSkills.length,
|
|
185
|
+
global: globalSkills.length,
|
|
186
|
+
agents: [...new Set(allSkills.map(s => s.agent))],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 便捷的加载函数
|
|
193
|
+
*/
|
|
194
|
+
export function loadSkills(projectRoot, agentId = null) {
|
|
195
|
+
const loader = new SkillLoader(projectRoot);
|
|
196
|
+
return loader.loadAllSkills(agentId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function loadProjectSkills(projectRoot, agentId = null) {
|
|
200
|
+
const loader = new SkillLoader(projectRoot);
|
|
201
|
+
return loader.loadProjectSkills(agentId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function loadGlobalSkills(projectRoot, agentId = null) {
|
|
205
|
+
const loader = new SkillLoader(projectRoot);
|
|
206
|
+
return loader.loadGlobalSkills(agentId);
|
|
207
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VersionChecker - 技能版本检查
|
|
3
|
+
* 检查已安装技能是否有更新
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import simpleGit from 'simple-git';
|
|
9
|
+
import { SkillLoader } from './skill-loader.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取 Skill 的 Git 信息
|
|
13
|
+
*/
|
|
14
|
+
export async function getSkillGitInfo(skillPath) {
|
|
15
|
+
try {
|
|
16
|
+
const git = simpleGit(skillPath);
|
|
17
|
+
const isRepo = await git.checkIsRepo();
|
|
18
|
+
|
|
19
|
+
if (!isRepo) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const remotes = await git.getRemotes(true);
|
|
24
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
25
|
+
|
|
26
|
+
if (!origin) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const log = await git.log({ maxCount: 1 });
|
|
31
|
+
const latestCommit = log.latest;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
remote: origin.refs.fetch,
|
|
35
|
+
commit: latestCommit?.hash || null,
|
|
36
|
+
date: latestCommit?.date || null,
|
|
37
|
+
message: latestCommit?.message || null,
|
|
38
|
+
};
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 检查远程是否有更新
|
|
46
|
+
*/
|
|
47
|
+
export async function checkRemoteUpdate(skillPath, currentCommit) {
|
|
48
|
+
try {
|
|
49
|
+
const git = simpleGit(skillPath);
|
|
50
|
+
const isRepo = await git.checkIsRepo();
|
|
51
|
+
|
|
52
|
+
if (!isRepo) {
|
|
53
|
+
return { hasUpdate: false, error: 'Not a git repository' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 获取远程更新(不合并)
|
|
57
|
+
await git.fetch(['--depth', '1']);
|
|
58
|
+
|
|
59
|
+
const log = await git.log({
|
|
60
|
+
from: currentCommit,
|
|
61
|
+
to: 'HEAD',
|
|
62
|
+
maxCount: 10
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const commitsBehind = log.total;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
hasUpdate: commitsBehind > 0,
|
|
69
|
+
commitsBehind,
|
|
70
|
+
latestCommit: log.latest,
|
|
71
|
+
};
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return { hasUpdate: false, error: e.message };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 检查单个 Skill
|
|
79
|
+
*/
|
|
80
|
+
export async function checkSkill(skill) {
|
|
81
|
+
const gitInfo = await getSkillGitInfo(skill.path);
|
|
82
|
+
|
|
83
|
+
if (!gitInfo) {
|
|
84
|
+
return {
|
|
85
|
+
name: skill.name,
|
|
86
|
+
hasUpdate: false,
|
|
87
|
+
currentVersion: skill.version,
|
|
88
|
+
source: 'local',
|
|
89
|
+
error: 'Not a git repository',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const updateInfo = await checkRemoteUpdate(skill.path, gitInfo.commit);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
name: skill.name,
|
|
97
|
+
hasUpdate: updateInfo.hasUpdate,
|
|
98
|
+
currentVersion: skill.version,
|
|
99
|
+
commitsBehind: updateInfo.commitsBehind,
|
|
100
|
+
latestCommit: updateInfo.latestCommit,
|
|
101
|
+
remote: gitInfo.remote,
|
|
102
|
+
source: 'git',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 检查所有 Skills
|
|
108
|
+
*/
|
|
109
|
+
export async function checkAllSkills(projectRoot) {
|
|
110
|
+
const loader = new SkillLoader(projectRoot);
|
|
111
|
+
const skills = loader.loadAllSkills();
|
|
112
|
+
|
|
113
|
+
const results = [];
|
|
114
|
+
|
|
115
|
+
for (const skill of skills) {
|
|
116
|
+
const result = await checkSkill(skill);
|
|
117
|
+
results.push(result);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 获取可更新的 Skills
|
|
125
|
+
*/
|
|
126
|
+
export async function getUpdatableSkills(projectRoot) {
|
|
127
|
+
const results = await checkAllSkills(projectRoot);
|
|
128
|
+
return results.filter(r => r.hasUpdate);
|
|
129
|
+
}
|