joyskills-cli 0.3.2 → 0.3.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joyskills-cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "JoySkills CLI v2.0 - Multi-agent skill management with JoyCode native support",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -14,6 +14,22 @@ import chalk from 'chalk';
14
14
  // Cache directory for cloned repos
15
15
  const CACHE_DIR = process.env.JOYSKILL_CACHE_DIR || path.join(os.homedir(), '.joyskill', 'cache');
16
16
 
17
+ /**
18
+ * Get git commit hash for a directory
19
+ */
20
+ async function getGitCommit(dirPath) {
21
+ try {
22
+ const git = simpleGit(dirPath);
23
+ const isRepo = await git.checkIsRepo();
24
+ if (!isRepo) return null;
25
+
26
+ const log = await git.log({ maxCount: 1 });
27
+ return log.latest?.hash || null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
17
33
  export function installCommand(program) {
18
34
  program
19
35
  .command('install [skill]')
@@ -252,6 +268,9 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
252
268
  const installInfo = fs.lstatSync(targetPath);
253
269
  const installMethod = installInfo.isSymbolicLink() ? 'symlink' : 'copy';
254
270
 
271
+ // Get git commit for the skill source
272
+ const commitHash = await getGitCommit(sourcePath);
273
+
255
274
  const lockfileManager = new LockfileManager(projectRoot);
256
275
  await lockfileManager.load();
257
276
  lockfileManager.updateSkill(skillName, {
@@ -259,6 +278,7 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
259
278
  source: sourceLabel,
260
279
  registry: registryManager.getRegistryInfo().registryId,
261
280
  installMethod,
281
+ commit: commitHash,
262
282
  installedAt: new Date().toISOString()
263
283
  });
264
284
  await lockfileManager.save();
@@ -279,11 +299,15 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
279
299
  const targetPath = path.join(targetDir, skillName);
280
300
  copyRecursive(skill.path, targetPath);
281
301
 
302
+ // Get git commit for the skill source
303
+ const commitHash = await getGitCommit(skill.path);
304
+
282
305
  const lockfileManager = new LockfileManager(projectRoot);
283
306
  await lockfileManager.load();
284
307
  lockfileManager.updateSkill(skillName, {
285
308
  version: skill.version || '1.0.0',
286
309
  source: sourceLabel,
310
+ commit: commitHash,
287
311
  installedAt: new Date().toISOString()
288
312
  });
289
313
  await lockfileManager.save();
@@ -119,6 +119,22 @@ async function getAllUpdatableSkills(projectRoot) {
119
119
  return updatable;
120
120
  }
121
121
 
122
+ /**
123
+ * 获取目录的 git commit hash
124
+ */
125
+ async function getDirCommit(dirPath) {
126
+ try {
127
+ const git = simpleGit(dirPath);
128
+ const isRepo = await git.checkIsRepo();
129
+ if (!isRepo) return null;
130
+
131
+ const log = await git.log({ maxCount: 1 });
132
+ return log.latest?.hash || null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
122
138
  /**
123
139
  * 更新 team:// 类型的 Skill
124
140
  */
@@ -126,7 +142,10 @@ async function updateTeamSkill(skill, projectRoot) {
126
142
  const lockfile = new LockfileManager(projectRoot);
127
143
  await lockfile.load();
128
144
 
129
- const updateInfo = await checkTeamSkillUpdate(skill, skill.source);
145
+ const lockData = lockfile.getSkill(skill.name);
146
+ const localCommit = lockData?.commit;
147
+
148
+ const updateInfo = await checkTeamSkillUpdate(skill, skill.source, localCommit);
130
149
 
131
150
  if (!updateInfo.hasUpdate) {
132
151
  throw new Error('No update available');
@@ -140,10 +159,14 @@ async function updateTeamSkill(skill, projectRoot) {
140
159
  // 复制新版本
141
160
  copyRecursive(updateInfo.registrySkillPath, skill.path);
142
161
 
162
+ // 获取新版本的 commit
163
+ const newCommit = await getDirCommit(updateInfo.registrySkillPath);
164
+
143
165
  // 更新 lockfile
144
166
  lockfile.updateSkill(skill.name, {
145
167
  version: updateInfo.latestVersion,
146
168
  source: skill.source,
169
+ commit: newCommit,
147
170
  updatedAt: new Date().toISOString(),
148
171
  });
149
172
  await lockfile.save();
@@ -187,18 +210,27 @@ async function getSpecifiedSkills(projectRoot, skillNames) {
187
210
  continue;
188
211
  }
189
212
 
213
+ let teamSource = null;
190
214
  const lockData = lockfile.getSkill(name);
191
215
 
192
216
  if (lockData?.source?.startsWith('team:')) {
217
+ teamSource = lockData.source;
218
+ } else {
219
+ // 尝试从所有 registry 查找
220
+ teamSource = await findSkillInRegistries(name);
221
+ }
222
+
223
+ if (teamSource) {
193
224
  // team:// 类型
194
- const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
225
+ const localCommit = lockData?.commit;
226
+ const updateInfo = await checkTeamSkillUpdate(skill, teamSource, localCommit);
195
227
  if (updateInfo.hasUpdate) {
196
228
  skills.push({
197
229
  name: skill.name,
198
230
  path: skill.path,
199
231
  currentVersion: skill.version,
200
232
  latestVersion: updateInfo.latestVersion,
201
- source: lockData.source,
233
+ source: teamSource,
202
234
  type: 'team',
203
235
  });
204
236
  }
@@ -230,7 +262,7 @@ async function getSpecifiedSkills(projectRoot, skillNames) {
230
262
  /**
231
263
  * 检查 team:// 类型的 Skill 是否有更新
232
264
  */
233
- async function checkTeamSkillUpdate(skill, source) {
265
+ async function checkTeamSkillUpdate(skill, source, localCommit = null) {
234
266
  const registryName = source.replace('team:', '');
235
267
  const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
236
268
  const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
@@ -246,7 +278,7 @@ async function checkTeamSkillUpdate(skill, source) {
246
278
  return { hasUpdate: false, error: `Registry ${registryName} not found` };
247
279
  }
248
280
 
249
- // 从 registry 查找最新版本
281
+ // 从 registry 查找 skill
250
282
  const { findSkills } = await import('./install.js');
251
283
  const registrySkills = await findSkills(reg.path);
252
284
  const registrySkill = registrySkills.find(s => s.name === skill.name);
@@ -255,16 +287,56 @@ async function checkTeamSkillUpdate(skill, source) {
255
287
  return { hasUpdate: false, error: 'Skill not found in registry' };
256
288
  }
257
289
 
258
- const hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
290
+ // 获取 registry skill commit
291
+ const remoteCommit = await getDirCommit(registrySkill.path);
292
+
293
+ // 使用 commit 对比,如果没有 commit 则 fallback 到 version 对比
294
+ let hasUpdate = false;
295
+ if (localCommit && remoteCommit) {
296
+ hasUpdate = localCommit !== remoteCommit;
297
+ } else {
298
+ hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
299
+ }
259
300
 
260
301
  return {
261
302
  hasUpdate,
262
303
  latestVersion: registrySkill.version,
304
+ localCommit,
305
+ remoteCommit,
263
306
  registryPath: reg.path,
264
307
  registrySkillPath: registrySkill.path,
265
308
  };
266
309
  }
267
310
 
311
+ /**
312
+ * 从所有 registry 中查找 skill
313
+ */
314
+ async function findSkillInRegistries(skillName) {
315
+ const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
316
+ const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
317
+
318
+ if (!fs.existsSync(configPath)) {
319
+ return null;
320
+ }
321
+
322
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
323
+ const registries = config.registries || {};
324
+
325
+ for (const [name, reg] of Object.entries(registries)) {
326
+ if (!reg.path || !fs.existsSync(reg.path)) continue;
327
+
328
+ const { findSkills } = await import('./install.js');
329
+ const skills = await findSkills(reg.path);
330
+ const found = skills.find(s => s.name === skillName);
331
+
332
+ if (found) {
333
+ return `team:${name}`;
334
+ }
335
+ }
336
+
337
+ return null;
338
+ }
339
+
268
340
  /**
269
341
  * 检查 Git 类型的 Skill 是否有更新
270
342
  */
@@ -311,7 +383,8 @@ async function getUpdatableTeamSkills(projectRoot) {
311
383
  for (const skill of allSkills) {
312
384
  const lockData = lockfile.getSkill(skill.name);
313
385
  if (lockData?.source?.startsWith('team:')) {
314
- const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
386
+ const localCommit = lockData?.commit;
387
+ const updateInfo = await checkTeamSkillUpdate(skill, lockData.source, localCommit);
315
388
  if (updateInfo.hasUpdate) {
316
389
  updatable.push({
317
390
  name: skill.name,
@@ -5,8 +5,10 @@
5
5
 
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import * as os from 'os';
8
9
  import simpleGit from 'simple-git';
9
10
  import { SkillLoader } from './skill-loader.js';
11
+ import { LockfileManager } from './lockfile.js';
10
12
 
11
13
  /**
12
14
  * 获取 Skill 的 Git 信息
@@ -77,7 +79,32 @@ export async function checkRemoteUpdate(skillPath, currentCommit) {
77
79
  /**
78
80
  * 检查单个 Skill
79
81
  */
80
- export async function checkSkill(skill) {
82
+ export async function checkSkill(skill, projectRoot) {
83
+ // 首先尝试从 lockfile 获取 source 和 commit
84
+ let teamSource = null;
85
+ let localCommit = null;
86
+
87
+ if (projectRoot) {
88
+ const lockfile = new LockfileManager(projectRoot);
89
+ await lockfile.load();
90
+ const lockData = lockfile.getSkill(skill.name);
91
+ if (lockData?.source?.startsWith('team:')) {
92
+ teamSource = lockData.source;
93
+ localCommit = lockData.commit;
94
+ }
95
+ }
96
+
97
+ // 如果没有 lockfile 记录,尝试从所有 registry 查找
98
+ if (!teamSource) {
99
+ teamSource = await findSkillInRegistries(skill.name);
100
+ }
101
+
102
+ // 如果是 team:// skill,对比 registry commit
103
+ if (teamSource) {
104
+ return await checkTeamSkill(skill, teamSource, localCommit);
105
+ }
106
+
107
+ // Git skill: 对比 commit
81
108
  const gitInfo = await getSkillGitInfo(skill.path);
82
109
 
83
110
  if (!gitInfo) {
@@ -103,6 +130,119 @@ export async function checkSkill(skill) {
103
130
  };
104
131
  }
105
132
 
133
+ /**
134
+ * 从所有 registry 中查找 skill
135
+ */
136
+ async function findSkillInRegistries(skillName) {
137
+ const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
138
+ const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
139
+
140
+ if (!fs.existsSync(configPath)) {
141
+ return null;
142
+ }
143
+
144
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
145
+ const registries = config.registries || {};
146
+
147
+ for (const [name, reg] of Object.entries(registries)) {
148
+ if (!reg.path || !fs.existsSync(reg.path)) continue;
149
+
150
+ const { findSkills } = await import('./commands/install.js');
151
+ const skills = await findSkills(reg.path);
152
+ const found = skills.find(s => s.name === skillName);
153
+
154
+ if (found) {
155
+ return `team:${name}`;
156
+ }
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * 获取目录的 git commit hash
164
+ */
165
+ async function getDirCommit(dirPath) {
166
+ try {
167
+ const git = simpleGit(dirPath);
168
+ const isRepo = await git.checkIsRepo();
169
+ if (!isRepo) return null;
170
+
171
+ const log = await git.log({ maxCount: 1 });
172
+ return log.latest?.hash || null;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 检查 team:// skill 更新
180
+ */
181
+ async function checkTeamSkill(skill, source, localCommit) {
182
+ const registryName = source.replace('team:', '');
183
+ const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
184
+ const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
185
+
186
+ if (!fs.existsSync(configPath)) {
187
+ return {
188
+ name: skill.name,
189
+ hasUpdate: false,
190
+ currentVersion: skill.version,
191
+ source: 'team',
192
+ error: 'No registry config',
193
+ };
194
+ }
195
+
196
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
197
+ const reg = config.registries?.[registryName];
198
+
199
+ if (!reg?.path || !fs.existsSync(reg.path)) {
200
+ return {
201
+ name: skill.name,
202
+ hasUpdate: false,
203
+ currentVersion: skill.version,
204
+ source: 'team',
205
+ error: `Registry ${registryName} not found`,
206
+ };
207
+ }
208
+
209
+ // 从 registry 查找 skill
210
+ const { findSkills } = await import('./commands/install.js');
211
+ const registrySkills = await findSkills(reg.path);
212
+ const registrySkill = registrySkills.find(s => s.name === skill.name);
213
+
214
+ if (!registrySkill) {
215
+ return {
216
+ name: skill.name,
217
+ hasUpdate: false,
218
+ currentVersion: skill.version,
219
+ source: 'team',
220
+ error: 'Skill not found in registry',
221
+ };
222
+ }
223
+
224
+ // 获取 registry 中 skill 的最新 commit
225
+ const remoteCommit = await getDirCommit(registrySkill.path);
226
+
227
+ // 如果没有 localCommit(旧版本安装的),使用 version 对比作为 fallback
228
+ let hasUpdate = false;
229
+ if (localCommit && remoteCommit) {
230
+ hasUpdate = localCommit !== remoteCommit;
231
+ } else {
232
+ hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
233
+ }
234
+
235
+ return {
236
+ name: skill.name,
237
+ hasUpdate,
238
+ currentVersion: skill.version,
239
+ localCommit: typeof localCommit === 'string' ? localCommit.slice(0, 7) : 'unknown',
240
+ remoteCommit: typeof remoteCommit === 'string' ? remoteCommit.slice(0, 7) : 'unknown',
241
+ commitsBehind: hasUpdate ? 1 : 0,
242
+ source: `team:${registryName}`,
243
+ };
244
+ }
245
+
106
246
  /**
107
247
  * 检查所有 Skills
108
248
  */
@@ -113,7 +253,7 @@ export async function checkAllSkills(projectRoot) {
113
253
  const results = [];
114
254
 
115
255
  for (const skill of skills) {
116
- const result = await checkSkill(skill);
256
+ const result = await checkSkill(skill, projectRoot);
117
257
  results.push(result);
118
258
  }
119
259