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 +1 -1
- package/src/commands/install.js +24 -0
- package/src/commands/update.js +80 -7
- package/src/version-checker.js +142 -2
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -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();
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,
|
package/src/version-checker.js
CHANGED
|
@@ -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
|
|