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.
- package/README.md +22 -238
- package/package.json +10 -3
- package/src/agents.js +198 -0
- package/src/commands/check.js +69 -0
- package/src/commands/install.js +91 -23
- package/src/commands/list.js +66 -124
- package/src/commands/remove.js +10 -1
- package/src/commands/sync.js +10 -4
- package/src/commands/update.js +320 -0
- package/src/commands/upgrade.js +103 -13
- 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,320 @@
|
|
|
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 { LockfileManager } from '../lockfile.js';
|
|
9
|
+
import simpleGit from 'simple-git';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { confirm } from '@inquirer/prompts';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
|
|
16
|
+
export function updateCommand(program) {
|
|
17
|
+
program
|
|
18
|
+
.command('update [skills...]')
|
|
19
|
+
.description('Update installed skills to latest versions')
|
|
20
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
21
|
+
.option('-a, --all', 'Update all skills')
|
|
22
|
+
.action(async (skillNames, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const projectRoot = process.cwd();
|
|
25
|
+
|
|
26
|
+
console.log(chalk.blue('Updating skills...'));
|
|
27
|
+
console.log(chalk.gray('==================\n'));
|
|
28
|
+
|
|
29
|
+
// 获取要更新的 skills
|
|
30
|
+
let skillsToUpdate = [];
|
|
31
|
+
|
|
32
|
+
if (options.all || skillNames.length === 0) {
|
|
33
|
+
// 更新所有有更新的 skills(Git + team://)
|
|
34
|
+
skillsToUpdate = await getAllUpdatableSkills(projectRoot);
|
|
35
|
+
} else {
|
|
36
|
+
// 更新指定的 skills
|
|
37
|
+
skillsToUpdate = await getSpecifiedSkills(projectRoot, skillNames);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (skillsToUpdate.length === 0) {
|
|
41
|
+
console.log(chalk.green('✅ No skills to update.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 显示将要更新的 skills
|
|
46
|
+
console.log(chalk.cyan('Skills to update:'));
|
|
47
|
+
for (const skill of skillsToUpdate) {
|
|
48
|
+
console.log(` 📦 ${skill.name} (v${skill.currentVersion})`);
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
|
|
52
|
+
// 确认
|
|
53
|
+
if (!options.yes) {
|
|
54
|
+
const shouldUpdate = await confirm({
|
|
55
|
+
message: `Update ${skillsToUpdate.length} skill(s)?`,
|
|
56
|
+
default: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!shouldUpdate) {
|
|
60
|
+
console.log(chalk.gray('Update cancelled.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 执行更新
|
|
66
|
+
console.log(chalk.blue('\nUpdating...\n'));
|
|
67
|
+
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const skill of skillsToUpdate) {
|
|
70
|
+
try {
|
|
71
|
+
if (skill.type === 'team') {
|
|
72
|
+
// team:// 类型:重新安装
|
|
73
|
+
await updateTeamSkill(skill, projectRoot);
|
|
74
|
+
} else {
|
|
75
|
+
// Git 类型:pull
|
|
76
|
+
const git = simpleGit(skill.path);
|
|
77
|
+
await git.pull('origin', 'main', ['--depth', '1']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(chalk.green(`✅ ${skill.name} updated to v${skill.latestVersion || 'latest'}`));
|
|
81
|
+
results.push({ name: skill.name, success: true });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.log(chalk.red(`❌ ${skill.name} failed: ${e.message}`));
|
|
84
|
+
results.push({ name: skill.name, success: false, error: e.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 总结
|
|
89
|
+
const successCount = results.filter(r => r.success).length;
|
|
90
|
+
const failCount = results.length - successCount;
|
|
91
|
+
|
|
92
|
+
console.log(chalk.gray('\n=================='));
|
|
93
|
+
console.log(chalk.green(`✅ Updated: ${successCount}`));
|
|
94
|
+
if (failCount > 0) {
|
|
95
|
+
console.log(chalk.red(`❌ Failed: ${failCount}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(chalk.red('❌ Failed to update:'), error.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取所有可更新的 Skills(Git + team://)
|
|
107
|
+
*/
|
|
108
|
+
async function getAllUpdatableSkills(projectRoot) {
|
|
109
|
+
const updatable = [];
|
|
110
|
+
|
|
111
|
+
// 1. 检查 Git 仓库类型的 Skills
|
|
112
|
+
const gitSkills = await getUpdatableSkills(projectRoot);
|
|
113
|
+
updatable.push(...gitSkills);
|
|
114
|
+
|
|
115
|
+
// 2. 检查 team:// 类型的 Skills
|
|
116
|
+
const teamSkills = await getUpdatableTeamSkills(projectRoot);
|
|
117
|
+
updatable.push(...teamSkills);
|
|
118
|
+
|
|
119
|
+
return updatable;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 更新 team:// 类型的 Skill
|
|
124
|
+
*/
|
|
125
|
+
async function updateTeamSkill(skill, projectRoot) {
|
|
126
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
127
|
+
await lockfile.load();
|
|
128
|
+
|
|
129
|
+
const updateInfo = await checkTeamSkillUpdate(skill, skill.source);
|
|
130
|
+
|
|
131
|
+
if (!updateInfo.hasUpdate) {
|
|
132
|
+
throw new Error('No update available');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 删除旧版本
|
|
136
|
+
if (fs.existsSync(skill.path)) {
|
|
137
|
+
fs.rmSync(skill.path, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 复制新版本
|
|
141
|
+
copyRecursive(updateInfo.registrySkillPath, skill.path);
|
|
142
|
+
|
|
143
|
+
// 更新 lockfile
|
|
144
|
+
lockfile.updateSkill(skill.name, {
|
|
145
|
+
version: updateInfo.latestVersion,
|
|
146
|
+
source: skill.source,
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
await lockfile.save();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 递归复制目录
|
|
154
|
+
*/
|
|
155
|
+
function copyRecursive(src, dest) {
|
|
156
|
+
if (!fs.existsSync(dest)) {
|
|
157
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const srcPath = path.join(src, entry.name);
|
|
164
|
+
const destPath = path.join(dest, entry.name);
|
|
165
|
+
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
copyRecursive(srcPath, destPath);
|
|
168
|
+
} else {
|
|
169
|
+
fs.copyFileSync(srcPath, destPath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取指定的 Skills
|
|
176
|
+
*/
|
|
177
|
+
async function getSpecifiedSkills(projectRoot, skillNames) {
|
|
178
|
+
const skills = [];
|
|
179
|
+
const loader = new SkillLoader(projectRoot);
|
|
180
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
181
|
+
await lockfile.load();
|
|
182
|
+
|
|
183
|
+
for (const name of skillNames) {
|
|
184
|
+
const skill = loader.getSkill(name);
|
|
185
|
+
if (!skill) {
|
|
186
|
+
console.log(chalk.red(`❌ Skill not found: ${name}`));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lockData = lockfile.getSkill(name);
|
|
191
|
+
|
|
192
|
+
if (lockData?.source?.startsWith('team:')) {
|
|
193
|
+
// team:// 类型
|
|
194
|
+
const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
|
|
195
|
+
if (updateInfo.hasUpdate) {
|
|
196
|
+
skills.push({
|
|
197
|
+
name: skill.name,
|
|
198
|
+
path: skill.path,
|
|
199
|
+
currentVersion: skill.version,
|
|
200
|
+
latestVersion: updateInfo.latestVersion,
|
|
201
|
+
source: lockData.source,
|
|
202
|
+
type: 'team',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Git 类型
|
|
207
|
+
const git = simpleGit(skill.path);
|
|
208
|
+
const isRepo = await git.checkIsRepo();
|
|
209
|
+
if (isRepo) {
|
|
210
|
+
const updateInfo = await checkGitSkillUpdate(skill);
|
|
211
|
+
if (updateInfo.hasUpdate) {
|
|
212
|
+
skills.push({
|
|
213
|
+
name: skill.name,
|
|
214
|
+
path: skill.path,
|
|
215
|
+
currentVersion: skill.version,
|
|
216
|
+
latestVersion: updateInfo.latestVersion,
|
|
217
|
+
source: lockData?.source || 'git',
|
|
218
|
+
type: 'git',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.yellow(`⚠️ ${name} is not a git repository, skipping`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return skills;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 检查 team:// 类型的 Skill 是否有更新
|
|
232
|
+
*/
|
|
233
|
+
async function checkTeamSkillUpdate(skill, source) {
|
|
234
|
+
const registryName = source.replace('team:', '');
|
|
235
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
236
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(configPath)) {
|
|
239
|
+
return { hasUpdate: false, error: 'No registry config' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
243
|
+
const reg = config.registries?.[registryName];
|
|
244
|
+
|
|
245
|
+
if (!reg?.path || !fs.existsSync(reg.path)) {
|
|
246
|
+
return { hasUpdate: false, error: `Registry ${registryName} not found` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 从 registry 查找最新版本
|
|
250
|
+
const { findSkills } = await import('./install.js');
|
|
251
|
+
const registrySkills = await findSkills(reg.path);
|
|
252
|
+
const registrySkill = registrySkills.find(s => s.name === skill.name);
|
|
253
|
+
|
|
254
|
+
if (!registrySkill) {
|
|
255
|
+
return { hasUpdate: false, error: 'Skill not found in registry' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
hasUpdate,
|
|
262
|
+
latestVersion: registrySkill.version,
|
|
263
|
+
registryPath: reg.path,
|
|
264
|
+
registrySkillPath: registrySkill.path,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 检查 Git 类型的 Skill 是否有更新
|
|
270
|
+
*/
|
|
271
|
+
async function checkGitSkillUpdate(skill) {
|
|
272
|
+
const git = simpleGit(skill.path);
|
|
273
|
+
const isRepo = await git.checkIsRepo();
|
|
274
|
+
|
|
275
|
+
if (!isRepo) {
|
|
276
|
+
return { hasUpdate: false, error: 'Not a git repository' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await git.fetch(['--depth', '1']);
|
|
280
|
+
const log = await git.log({ maxCount: 1 });
|
|
281
|
+
const latestCommit = log.latest;
|
|
282
|
+
|
|
283
|
+
// 简化:如果有 fetch 成功,认为可能有更新
|
|
284
|
+
return {
|
|
285
|
+
hasUpdate: true,
|
|
286
|
+
latestVersion: latestCommit?.hash?.slice(0, 7) || 'latest',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 获取所有可更新的 team:// Skills
|
|
292
|
+
*/
|
|
293
|
+
async function getUpdatableTeamSkills(projectRoot) {
|
|
294
|
+
const updatable = [];
|
|
295
|
+
const loader = new SkillLoader(projectRoot);
|
|
296
|
+
const lockfile = new LockfileManager(projectRoot);
|
|
297
|
+
await lockfile.load();
|
|
298
|
+
|
|
299
|
+
const allSkills = loader.loadAllSkills();
|
|
300
|
+
|
|
301
|
+
for (const skill of allSkills) {
|
|
302
|
+
const lockData = lockfile.getSkill(skill.name);
|
|
303
|
+
if (lockData?.source?.startsWith('team:')) {
|
|
304
|
+
const updateInfo = await checkTeamSkillUpdate(skill, lockData.source);
|
|
305
|
+
if (updateInfo.hasUpdate) {
|
|
306
|
+
updatable.push({
|
|
307
|
+
name: skill.name,
|
|
308
|
+
path: skill.path,
|
|
309
|
+
currentVersion: skill.version,
|
|
310
|
+
latestVersion: updateInfo.latestVersion,
|
|
311
|
+
source: lockData.source,
|
|
312
|
+
type: 'team',
|
|
313
|
+
registrySkillPath: updateInfo.registrySkillPath,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return updatable;
|
|
320
|
+
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -23,9 +23,22 @@ export function upgradeCommand(program) {
|
|
|
23
23
|
const projectRoot = process.cwd();
|
|
24
24
|
const isGlobal = options.global;
|
|
25
25
|
// Scan both standard dirs; upgrade will check all
|
|
26
|
+
// v2.0: Support all agent directories, joycode first
|
|
26
27
|
const skillsDirs = isGlobal
|
|
27
|
-
? [
|
|
28
|
-
|
|
28
|
+
? [
|
|
29
|
+
path.join(os.homedir(), '.joycode', 'skills'),
|
|
30
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
31
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
32
|
+
path.join(os.homedir(), '.cursor', 'skills'),
|
|
33
|
+
path.join(os.homedir(), '.qoder', 'skills'),
|
|
34
|
+
]
|
|
35
|
+
: [
|
|
36
|
+
path.join(projectRoot, '.joycode', 'skills'),
|
|
37
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
38
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
39
|
+
path.join(projectRoot, '.cursor', 'skills'),
|
|
40
|
+
path.join(projectRoot, '.qoder', 'skills'),
|
|
41
|
+
];
|
|
29
42
|
// Use first existing dir as primary, but pass all dirs to findUpgradableSkills
|
|
30
43
|
const skillsDir = skillsDirs.find(d => fs.existsSync(d)) || skillsDirs[0];
|
|
31
44
|
|
|
@@ -143,10 +156,21 @@ async function findUpgradableSkills(lockfileManager, registryManager, skillsDir,
|
|
|
143
156
|
const upgradable = [];
|
|
144
157
|
const installedSkills = lockfileManager.lockData.skills || {};
|
|
145
158
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
// v2.0: Check all sibling dirs
|
|
160
|
+
const siblingDirs = [
|
|
161
|
+
skillsDir.replace('.joycode', '.claude'),
|
|
162
|
+
skillsDir.replace('.joycode', '.agent'),
|
|
163
|
+
skillsDir.replace('.joycode', '.cursor'),
|
|
164
|
+
skillsDir.replace('.joycode', '.qoder'),
|
|
165
|
+
skillsDir.replace('.claude', '.joycode'),
|
|
166
|
+
skillsDir.replace('.claude', '.agent'),
|
|
167
|
+
skillsDir.replace('.claude', '.cursor'),
|
|
168
|
+
skillsDir.replace('.claude', '.qoder'),
|
|
169
|
+
skillsDir.replace('.agent', '.joycode'),
|
|
170
|
+
skillsDir.replace('.agent', '.claude'),
|
|
171
|
+
skillsDir.replace('.agent', '.cursor'),
|
|
172
|
+
skillsDir.replace('.agent', '.qoder'),
|
|
173
|
+
].filter(d => d !== skillsDir);
|
|
150
174
|
|
|
151
175
|
const skillNames = specificSkill
|
|
152
176
|
? [specificSkill]
|
|
@@ -156,16 +180,41 @@ async function findUpgradableSkills(lockfileManager, registryManager, skillsDir,
|
|
|
156
180
|
const current = installedSkills[name];
|
|
157
181
|
if (!current) continue;
|
|
158
182
|
|
|
159
|
-
// Check existence in
|
|
160
|
-
const
|
|
183
|
+
// Check existence in all dirs
|
|
184
|
+
const allDirs = [skillsDir, ...siblingDirs];
|
|
185
|
+
const skillPath = allDirs.map(d => path.join(d, name)).find(p => fs.existsSync(p));
|
|
161
186
|
if (!skillPath) continue;
|
|
162
187
|
|
|
163
188
|
let latestVersion = null;
|
|
164
189
|
let hasBreakingChange = false;
|
|
165
190
|
let changelog = '';
|
|
166
191
|
|
|
167
|
-
// Check registry for updates
|
|
168
|
-
if (
|
|
192
|
+
// Check registry for updates (including team:// sources)
|
|
193
|
+
if (current.source?.startsWith('team:')) {
|
|
194
|
+
// Handle team:// protocol
|
|
195
|
+
const registryName = current.source.replace('team:', '');
|
|
196
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
197
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(configPath)) {
|
|
200
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
201
|
+
const reg = config.registries?.[registryName];
|
|
202
|
+
|
|
203
|
+
if (reg?.path && fs.existsSync(reg.path)) {
|
|
204
|
+
// Find skill in registry to get latest version
|
|
205
|
+
const { findSkills } = await import('../commands/install.js');
|
|
206
|
+
const skills = await findSkills(reg.path);
|
|
207
|
+
const skillInfo = skills.find(s => s.name === name);
|
|
208
|
+
|
|
209
|
+
if (skillInfo?.version && skillInfo.version !== current.version) {
|
|
210
|
+
latestVersion = skillInfo.version;
|
|
211
|
+
const currentMajor = parseInt(current.version.split('.')[0]);
|
|
212
|
+
const latestMajor = parseInt(latestVersion.split('.')[0]);
|
|
213
|
+
hasBreakingChange = latestMajor > currentMajor;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else if (registryManager && registryManager.hasSkill(name)) {
|
|
169
218
|
const recommended = registryManager.getRecommendedVersion(name);
|
|
170
219
|
if (recommended && recommended.version !== current.version) {
|
|
171
220
|
latestVersion = recommended.version;
|
|
@@ -289,9 +338,50 @@ async function upgradeFromGitHub(skill, skillsDir) {
|
|
|
289
338
|
}
|
|
290
339
|
|
|
291
340
|
async function upgradeFromRegistry(skill, skillsDir) {
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
341
|
+
// v2.0: Support team:// protocol upgrade
|
|
342
|
+
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
343
|
+
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(configPath)) {
|
|
346
|
+
throw new Error('No registry config found. Run: joySkills team add <name> <url>');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
350
|
+
const registries = config.registries || {};
|
|
351
|
+
|
|
352
|
+
// Find registry by source (team:registryName)
|
|
353
|
+
const registryName = skill.source.replace('team:', '');
|
|
354
|
+
const reg = registries[registryName];
|
|
355
|
+
|
|
356
|
+
if (!reg?.path || !fs.existsSync(reg.path)) {
|
|
357
|
+
throw new Error(`Registry "${registryName}" not found. Run: joySkills team list`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Find skill in registry
|
|
361
|
+
const { findSkills } = await import('../commands/install.js');
|
|
362
|
+
const skills = await findSkills(reg.path);
|
|
363
|
+
const skillInfo = skills.find(s => s.name === skill.name);
|
|
364
|
+
|
|
365
|
+
if (!skillInfo) {
|
|
366
|
+
throw new Error(`Skill "${skill.name}" not found in registry "${registryName}"`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check if version is newer
|
|
370
|
+
if (skillInfo.version && skillInfo.version <= skill.currentVersion) {
|
|
371
|
+
console.log(chalk.gray(` ${skill.name} is already up to date`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Remove old version
|
|
376
|
+
const targetPath = path.join(skillsDir, skill.name);
|
|
377
|
+
if (fs.existsSync(targetPath)) {
|
|
378
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Copy new version
|
|
382
|
+
copyRecursive(skillInfo.path, targetPath);
|
|
383
|
+
|
|
384
|
+
console.log(chalk.green(` ✓ Upgraded from registry ${registryName}`));
|
|
295
385
|
}
|
|
296
386
|
|
|
297
387
|
function copyRecursive(src, dest) {
|
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();
|
package/src/installer.js
ADDED
|
@@ -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
|
+
}
|