joyskills-cli 0.2.10 → 0.3.1

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.
@@ -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
+ }