joyskills-cli 0.3.3 → 0.3.5
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/check.js +6 -3
- package/src/commands/install.js +243 -100
- package/src/commands/sync.js +21 -5
- package/src/commands/update.js +76 -15
- package/src/skill-loader.js +20 -4
- package/src/version-checker.js +52 -12
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
-
import { checkAllSkills, getUpdatableSkills } from '../version-checker.js';
|
|
6
|
+
import { checkAllSkills, getUpdatableSkills, checkSkill } from '../version-checker.js';
|
|
7
|
+
import { SkillLoader } from '../skill-loader.js';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
|
|
9
10
|
export function checkCommand(program) {
|
|
@@ -11,6 +12,8 @@ export function checkCommand(program) {
|
|
|
11
12
|
.command('check')
|
|
12
13
|
.description('Check for available skill updates')
|
|
13
14
|
.option('-u, --updatable', 'Show only skills with updates')
|
|
15
|
+
.option('-g, --global', 'Check global (user-level) skills only')
|
|
16
|
+
.option('-p, --project', 'Check project-level skills only')
|
|
14
17
|
.action(async (options) => {
|
|
15
18
|
try {
|
|
16
19
|
const projectRoot = process.cwd();
|
|
@@ -19,8 +22,8 @@ export function checkCommand(program) {
|
|
|
19
22
|
console.log(chalk.gray('==============================\n'));
|
|
20
23
|
|
|
21
24
|
const results = options.updatable
|
|
22
|
-
? await getUpdatableSkills(projectRoot)
|
|
23
|
-
: await checkAllSkills(projectRoot);
|
|
25
|
+
? await getUpdatableSkills(projectRoot, options.global, options.project)
|
|
26
|
+
: await checkAllSkills(projectRoot, options.global, options.project);
|
|
24
27
|
|
|
25
28
|
if (results.length === 0) {
|
|
26
29
|
if (options.updatable) {
|
package/src/commands/install.js
CHANGED
|
@@ -8,12 +8,123 @@ import simpleGit from 'simple-git';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as os from 'os';
|
|
11
|
-
import { select, confirm } from '@inquirer/prompts';
|
|
11
|
+
import { select, confirm, input } from '@inquirer/prompts';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
|
|
14
|
+
const JOYSKILL_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
15
|
+
const REGISTRY_CACHE_DIR = path.join(JOYSKILL_DIR, 'registries');
|
|
16
|
+
|
|
14
17
|
// Cache directory for cloned repos
|
|
15
18
|
const CACHE_DIR = process.env.JOYSKILL_CACHE_DIR || path.join(os.homedir(), '.joyskill', 'cache');
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Check if a registry exists in global config
|
|
22
|
+
*/
|
|
23
|
+
function registryExists(registryName) {
|
|
24
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
25
|
+
if (!fs.existsSync(configPath)) return false;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
29
|
+
return config.registries && config.registries[registryName];
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add a registry to global config (used by auto-prompt)
|
|
37
|
+
*/
|
|
38
|
+
async function addRegistryInteractive(registryName, suggestedUrl = null) {
|
|
39
|
+
console.log(chalk.yellow(`\n⚠️ Registry '${registryName}' not found in local config.`));
|
|
40
|
+
console.log(chalk.gray(` This skill is from a team registry. Please add it first.`));
|
|
41
|
+
|
|
42
|
+
let gitUrl = suggestedUrl;
|
|
43
|
+
|
|
44
|
+
if (!gitUrl) {
|
|
45
|
+
gitUrl = await input({
|
|
46
|
+
message: `Enter the Git URL for registry '${registryName}':`,
|
|
47
|
+
validate: (value) => {
|
|
48
|
+
if (!value) return 'Git URL is required';
|
|
49
|
+
if (!value.includes('@') && !value.startsWith('http')) {
|
|
50
|
+
return 'Please enter a valid Git URL (SSH or HTTPS)';
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.blue(`\n📦 Adding team registry: ${registryName}`));
|
|
58
|
+
console.log(chalk.gray(` URL: ${gitUrl}`));
|
|
59
|
+
|
|
60
|
+
// Clone registry to cache
|
|
61
|
+
const registryCachePath = path.join(REGISTRY_CACHE_DIR, registryName);
|
|
62
|
+
|
|
63
|
+
if (fs.existsSync(registryCachePath)) {
|
|
64
|
+
fs.rmSync(registryCachePath, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.mkdirSync(registryCachePath, { recursive: true });
|
|
68
|
+
|
|
69
|
+
// Clone the registry
|
|
70
|
+
const git = simpleGit();
|
|
71
|
+
await git.clone(gitUrl, registryCachePath, ['--depth', '1']);
|
|
72
|
+
|
|
73
|
+
// Save to global config
|
|
74
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
75
|
+
let config = { registries: {} };
|
|
76
|
+
if (fs.existsSync(configPath)) {
|
|
77
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
config.registries[registryName] = {
|
|
81
|
+
url: gitUrl,
|
|
82
|
+
path: registryCachePath,
|
|
83
|
+
addedAt: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
fs.mkdirSync(JOYSKILL_DIR, { recursive: true });
|
|
87
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
88
|
+
|
|
89
|
+
console.log(chalk.green(` ✓ Added registry '${registryName}'`));
|
|
90
|
+
|
|
91
|
+
return registryCachePath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get registry path with auto-prompt for missing registries
|
|
96
|
+
*/
|
|
97
|
+
async function getRegistryPathWithPrompt(registryName, options = {}) {
|
|
98
|
+
if (!registryExists(registryName)) {
|
|
99
|
+
if (options.yes || process.env.CI) {
|
|
100
|
+
throw new Error(`Registry '${registryName}' not found. Run: joySkills team add ${registryName} <git-url>`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Auto-prompt to add registry
|
|
104
|
+
await addRegistryInteractive(registryName);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
108
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
109
|
+
return config.registries[registryName].path;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get git commit hash for a directory
|
|
114
|
+
*/
|
|
115
|
+
async function getGitCommit(dirPath) {
|
|
116
|
+
try {
|
|
117
|
+
const git = simpleGit(dirPath);
|
|
118
|
+
const isRepo = await git.checkIsRepo();
|
|
119
|
+
if (!isRepo) return null;
|
|
120
|
+
|
|
121
|
+
const log = await git.log({ maxCount: 1 });
|
|
122
|
+
return log.latest?.hash || null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
17
128
|
export function installCommand(program) {
|
|
18
129
|
program
|
|
19
130
|
.command('install [skill]')
|
|
@@ -131,21 +242,10 @@ async function resolveAndInstall(skillInput, targetDir, projectRoot, options) {
|
|
|
131
242
|
throw new Error(`Invalid team:// URL format: ${skillInput}. Expected: team://<registry>/<skill>`);
|
|
132
243
|
}
|
|
133
244
|
|
|
134
|
-
//
|
|
135
|
-
const
|
|
136
|
-
if (!fs.existsSync(configPath)) {
|
|
137
|
-
throw new Error(`No registries configured. Run: joySkills team add <name> <git-url>`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
141
|
-
const registries = config.registries || {};
|
|
142
|
-
const reg = registries[registryName];
|
|
245
|
+
// Get registry path with auto-prompt for missing registries
|
|
246
|
+
const registryPath = await getRegistryPathWithPrompt(registryName, options);
|
|
143
247
|
|
|
144
|
-
|
|
145
|
-
throw new Error(`Registry "${registryName}" not found. Run: joySkills team list`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const hit = await tryInstallFromRegistryDir(skillName, reg.path, targetDir, projectRoot, options, `team:${registryName}`);
|
|
248
|
+
const hit = await tryInstallFromRegistryDir(skillName, registryPath, targetDir, projectRoot, options, `team:${registryName}`);
|
|
149
249
|
if (!hit) {
|
|
150
250
|
throw new Error(`Skill "${skillName}" not found in registry "${registryName}"`);
|
|
151
251
|
}
|
|
@@ -202,16 +302,19 @@ async function resolveAndInstall(skillInput, targetDir, projectRoot, options) {
|
|
|
202
302
|
|
|
203
303
|
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
204
304
|
fs.writeFileSync(skillMdPath, generateSkillTemplate(skillInput));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
305
|
+
|
|
306
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
307
|
+
if (!options.global) {
|
|
308
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
309
|
+
await lockfileManager.load();
|
|
310
|
+
lockfileManager.updateSkill(skillInput, {
|
|
311
|
+
version: '1.0.0',
|
|
312
|
+
source: 'template',
|
|
313
|
+
path: skillPath,
|
|
314
|
+
installedAt: new Date().toISOString()
|
|
315
|
+
});
|
|
316
|
+
await lockfileManager.save();
|
|
317
|
+
}
|
|
215
318
|
console.log(chalk.green(`✅ Created template for ${skillInput} in ${targetDir}`));
|
|
216
319
|
}
|
|
217
320
|
|
|
@@ -252,16 +355,23 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
|
|
|
252
355
|
const installInfo = fs.lstatSync(targetPath);
|
|
253
356
|
const installMethod = installInfo.isSymbolicLink() ? 'symlink' : 'copy';
|
|
254
357
|
|
|
255
|
-
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
358
|
+
// Get git commit for the skill source
|
|
359
|
+
const commitHash = await getGitCommit(sourcePath);
|
|
360
|
+
|
|
361
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
362
|
+
if (!options.global) {
|
|
363
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
364
|
+
await lockfileManager.load();
|
|
365
|
+
lockfileManager.updateSkill(skillName, {
|
|
366
|
+
version,
|
|
367
|
+
source: sourceLabel,
|
|
368
|
+
registry: registryManager.getRegistryInfo().registryId,
|
|
369
|
+
installMethod,
|
|
370
|
+
commit: commitHash,
|
|
371
|
+
installedAt: new Date().toISOString()
|
|
372
|
+
});
|
|
373
|
+
await lockfileManager.save();
|
|
374
|
+
}
|
|
265
375
|
|
|
266
376
|
const methodLabel = installMethod === 'symlink' ? chalk.gray('(symlink)') : chalk.gray('(copy)');
|
|
267
377
|
console.log(chalk.green(`✅ Installed ${skillName} v${version} from ${sourceLabel}`) + ' ' + methodLabel);
|
|
@@ -279,14 +389,21 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
|
|
|
279
389
|
const targetPath = path.join(targetDir, skillName);
|
|
280
390
|
copyRecursive(skill.path, targetPath);
|
|
281
391
|
|
|
282
|
-
|
|
283
|
-
await
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
392
|
+
// Get git commit for the skill source
|
|
393
|
+
const commitHash = await getGitCommit(skill.path);
|
|
394
|
+
|
|
395
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
396
|
+
if (!options.global) {
|
|
397
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
398
|
+
await lockfileManager.load();
|
|
399
|
+
lockfileManager.updateSkill(skillName, {
|
|
400
|
+
version: skill.version || '1.0.0',
|
|
401
|
+
source: sourceLabel,
|
|
402
|
+
commit: commitHash,
|
|
403
|
+
installedAt: new Date().toISOString()
|
|
404
|
+
});
|
|
405
|
+
await lockfileManager.save();
|
|
406
|
+
}
|
|
290
407
|
|
|
291
408
|
console.log(chalk.green(`✅ Installed ${skillName} v${skill.version || '1.0.0'} from ${sourceLabel}`));
|
|
292
409
|
return true;
|
|
@@ -353,15 +470,18 @@ async function installFromLocalPath(localPath, targetDir, options) {
|
|
|
353
470
|
const versionMatch = content.match(/version:\s*(.+)/);
|
|
354
471
|
const version = versionMatch?.[1]?.trim() || '1.0.0';
|
|
355
472
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
473
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
474
|
+
if (!options.global) {
|
|
475
|
+
const lockfileManager = new LockfileManager(process.cwd());
|
|
476
|
+
await lockfileManager.load();
|
|
477
|
+
lockfileManager.updateSkill(skillName, {
|
|
478
|
+
version,
|
|
479
|
+
source: 'local',
|
|
480
|
+
path: absPath,
|
|
481
|
+
installedAt: new Date().toISOString()
|
|
482
|
+
});
|
|
483
|
+
await lockfileManager.save();
|
|
484
|
+
}
|
|
365
485
|
|
|
366
486
|
console.log(chalk.green(`✅ Installed ${skillName} v${version} from local path`));
|
|
367
487
|
} else {
|
|
@@ -389,20 +509,23 @@ async function installFromLocalPath(localPath, targetDir, options) {
|
|
|
389
509
|
});
|
|
390
510
|
}
|
|
391
511
|
|
|
392
|
-
|
|
393
|
-
|
|
512
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
513
|
+
const lockfileManager = options.global ? null : new LockfileManager(process.cwd());
|
|
514
|
+
if (lockfileManager) await lockfileManager.load();
|
|
394
515
|
|
|
395
516
|
for (const skill of selectedSkills) {
|
|
396
517
|
await installSkillFiles(skill, targetDir);
|
|
397
|
-
lockfileManager
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
518
|
+
if (lockfileManager) {
|
|
519
|
+
lockfileManager.updateSkill(skill.name, {
|
|
520
|
+
version: skill.version || '1.0.0',
|
|
521
|
+
source: 'local',
|
|
522
|
+
path: skill.path,
|
|
523
|
+
installedAt: new Date().toISOString()
|
|
524
|
+
});
|
|
525
|
+
}
|
|
403
526
|
}
|
|
404
527
|
|
|
405
|
-
await lockfileManager.save();
|
|
528
|
+
if (lockfileManager) await lockfileManager.save();
|
|
406
529
|
console.log(chalk.green(`\n✅ Installed ${selectedSkills.length} skill(s)`));
|
|
407
530
|
}
|
|
408
531
|
} else {
|
|
@@ -475,17 +598,20 @@ async function installFromGitUrl(gitUrl, targetDir, options, subPath = '') {
|
|
|
475
598
|
await installSkillFiles(skill, targetDir);
|
|
476
599
|
}
|
|
477
600
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
lockfileManager.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
601
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
602
|
+
if (!options.global) {
|
|
603
|
+
const lockfileManager = new LockfileManager(process.cwd());
|
|
604
|
+
await lockfileManager.load();
|
|
605
|
+
for (const skill of selectedSkills) {
|
|
606
|
+
lockfileManager.updateSkill(skill.name, {
|
|
607
|
+
version: skill.version || '1.0.0',
|
|
608
|
+
source: 'git',
|
|
609
|
+
repository: gitUrl,
|
|
610
|
+
installedAt: new Date().toISOString()
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
await lockfileManager.save();
|
|
487
614
|
}
|
|
488
|
-
await lockfileManager.save();
|
|
489
615
|
|
|
490
616
|
console.log(chalk.green(`\n✅ Successfully installed ${selectedSkills.length} skill(s)`));
|
|
491
617
|
}
|
|
@@ -566,18 +692,20 @@ async function installFromGitHub(owner, repo, skillPath, targetDir, options) {
|
|
|
566
692
|
await installSkillFiles(skill, targetDir);
|
|
567
693
|
}
|
|
568
694
|
|
|
569
|
-
// Update lockfile
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
695
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
696
|
+
if (!options.global) {
|
|
697
|
+
const lockfileManager = new LockfileManager(process.cwd());
|
|
698
|
+
await lockfileManager.load();
|
|
699
|
+
for (const skill of selectedSkills) {
|
|
700
|
+
lockfileManager.updateSkill(skill.name, {
|
|
701
|
+
version: skill.version || '1.0.0',
|
|
702
|
+
source: 'github',
|
|
703
|
+
repository: `${owner}/${repo}`,
|
|
704
|
+
installedAt: new Date().toISOString()
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
await lockfileManager.save();
|
|
579
708
|
}
|
|
580
|
-
await lockfileManager.save();
|
|
581
709
|
|
|
582
710
|
console.log(chalk.green(`\n✅ Successfully installed ${selectedSkills.length} skill(s)`));
|
|
583
711
|
}
|
|
@@ -623,16 +751,19 @@ async function installFromRegistry(skillName, targetDir, projectRoot, options) {
|
|
|
623
751
|
const sourcePath = path.join(registryPath, skill.location || skillName);
|
|
624
752
|
if (fs.existsSync(sourcePath)) {
|
|
625
753
|
copyRecursive(sourcePath, skillTargetPath);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
754
|
+
|
|
755
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
756
|
+
if (!options.global) {
|
|
757
|
+
await lockfileManager.load();
|
|
758
|
+
lockfileManager.updateSkill(skillName, {
|
|
759
|
+
version,
|
|
760
|
+
source: 'registry',
|
|
761
|
+
registry: registryManager.getRegistryInfo().registryId,
|
|
762
|
+
installedAt: new Date().toISOString()
|
|
763
|
+
});
|
|
764
|
+
await lockfileManager.save();
|
|
765
|
+
}
|
|
766
|
+
|
|
636
767
|
console.log(chalk.green(`✅ Installed ${skillName} v${version} from registry`));
|
|
637
768
|
return;
|
|
638
769
|
}
|
|
@@ -643,14 +774,17 @@ async function installFromRegistry(skillName, targetDir, projectRoot, options) {
|
|
|
643
774
|
console.log(chalk.yellow(`⚠️ Skill not found in registry, creating template...`));
|
|
644
775
|
const templateContent = generateSkillTemplate(skillName);
|
|
645
776
|
localManager.installSkill(skillName, templateContent);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
777
|
+
|
|
778
|
+
// Update lockfile only for project-level installation (npm-like behavior)
|
|
779
|
+
if (!options.global) {
|
|
780
|
+
await lockfileManager.load();
|
|
781
|
+
lockfileManager.updateSkill(skillName, {
|
|
782
|
+
version: '1.0.0',
|
|
783
|
+
source: 'template',
|
|
784
|
+
installedAt: new Date().toISOString()
|
|
785
|
+
});
|
|
786
|
+
await lockfileManager.save();
|
|
787
|
+
}
|
|
654
788
|
|
|
655
789
|
console.log(chalk.green(`✅ Created template for ${skillName}`));
|
|
656
790
|
}
|
|
@@ -679,6 +813,15 @@ async function installFromLockfile(targetDir, projectRoot) {
|
|
|
679
813
|
} else if (skillInfo.source === 'github' && skillInfo.repository) {
|
|
680
814
|
const [owner, repo] = skillInfo.repository.split('/');
|
|
681
815
|
await installFromGitHub(owner, repo, skillName, targetDir, {});
|
|
816
|
+
} else if (skillInfo.source?.startsWith('team:')) {
|
|
817
|
+
// Re-install from team registry with auto-prompt
|
|
818
|
+
const registryName = skillInfo.source.replace('team:', '');
|
|
819
|
+
const registryPath = await getRegistryPathWithPrompt(registryName, {});
|
|
820
|
+
const hit = await tryInstallFromRegistryDir(skillName, registryPath, targetDir, projectRoot, {}, skillInfo.source);
|
|
821
|
+
if (!hit) {
|
|
822
|
+
console.log(chalk.yellow(` ⚠️ Skill "${skillName}" not found in registry "${registryName}", trying resolve chain...`));
|
|
823
|
+
await resolveAndInstall(skillName, targetDir, projectRoot, {});
|
|
824
|
+
}
|
|
682
825
|
} else {
|
|
683
826
|
// Try full resolve chain
|
|
684
827
|
await resolveAndInstall(skillName, targetDir, projectRoot, {});
|
package/src/commands/sync.js
CHANGED
|
@@ -68,12 +68,28 @@ export function syncCommand(program) {
|
|
|
68
68
|
await lockfileManager.load();
|
|
69
69
|
for (const skill of allSkills) {
|
|
70
70
|
const existing = lockfileManager.getSkill(skill.name);
|
|
71
|
-
|
|
71
|
+
|
|
72
|
+
// Merge existing data with updates, preserving all original fields
|
|
73
|
+
const updatedEntry = {
|
|
74
|
+
...existing, // Keep all existing fields (registry, commit, etc.)
|
|
72
75
|
version: skill.version || existing?.version || '1.0.0',
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
path: skill.path, // Update path (may change if skill moved)
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Set source if not exists
|
|
80
|
+
if (!updatedEntry.source) {
|
|
81
|
+
updatedEntry.source = 'local';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Set installedAt if not exists
|
|
85
|
+
if (!updatedEntry.installedAt) {
|
|
86
|
+
updatedEntry.installedAt = new Date().toISOString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add syncedAt timestamp
|
|
90
|
+
updatedEntry.syncedAt = new Date().toISOString();
|
|
91
|
+
|
|
92
|
+
lockfileManager.updateSkill(skill.name, updatedEntry);
|
|
77
93
|
}
|
|
78
94
|
await lockfileManager.save();
|
|
79
95
|
console.log(chalk.green(` ✓ Updated joySkills.lock`));
|
package/src/commands/update.js
CHANGED
|
@@ -19,6 +19,8 @@ export function updateCommand(program) {
|
|
|
19
19
|
.description('Update installed skills to latest versions')
|
|
20
20
|
.option('-y, --yes', 'Skip confirmation prompts')
|
|
21
21
|
.option('-a, --all', 'Update all skills')
|
|
22
|
+
.option('-g, --global', 'Update global (user-level) skills only')
|
|
23
|
+
.option('-p, --project', 'Update project-level skills only')
|
|
22
24
|
.action(async (skillNames, options) => {
|
|
23
25
|
try {
|
|
24
26
|
const projectRoot = process.cwd();
|
|
@@ -31,10 +33,10 @@ export function updateCommand(program) {
|
|
|
31
33
|
|
|
32
34
|
if (options.all || skillNames.length === 0) {
|
|
33
35
|
// 更新所有有更新的 skills(Git + team://)
|
|
34
|
-
skillsToUpdate = await getAllUpdatableSkills(projectRoot);
|
|
36
|
+
skillsToUpdate = await getAllUpdatableSkills(projectRoot, options.global, options.project);
|
|
35
37
|
} else {
|
|
36
38
|
// 更新指定的 skills
|
|
37
|
-
skillsToUpdate = await getSpecifiedSkills(projectRoot, skillNames);
|
|
39
|
+
skillsToUpdate = await getSpecifiedSkills(projectRoot, skillNames, options.global, options.project);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
if (skillsToUpdate.length === 0) {
|
|
@@ -104,21 +106,39 @@ export function updateCommand(program) {
|
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
108
|
* 获取所有可更新的 Skills(Git + team://)
|
|
109
|
+
* @param {boolean} globalOnly - 只检查全局 skills
|
|
110
|
+
* @param {boolean} projectOnly - 只检查项目级 skills
|
|
107
111
|
*/
|
|
108
|
-
async function getAllUpdatableSkills(projectRoot) {
|
|
112
|
+
async function getAllUpdatableSkills(projectRoot, globalOnly = false, projectOnly = false) {
|
|
109
113
|
const updatable = [];
|
|
110
114
|
|
|
111
115
|
// 1. 检查 Git 仓库类型的 Skills
|
|
112
|
-
const gitSkills = await getUpdatableSkills(projectRoot);
|
|
116
|
+
const gitSkills = await getUpdatableSkills(projectRoot, globalOnly, projectOnly);
|
|
113
117
|
updatable.push(...gitSkills);
|
|
114
118
|
|
|
115
119
|
// 2. 检查 team:// 类型的 Skills
|
|
116
|
-
const teamSkills = await getUpdatableTeamSkills(projectRoot);
|
|
120
|
+
const teamSkills = await getUpdatableTeamSkills(projectRoot, globalOnly, projectOnly);
|
|
117
121
|
updatable.push(...teamSkills);
|
|
118
122
|
|
|
119
123
|
return updatable;
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 获取目录的 git commit hash
|
|
128
|
+
*/
|
|
129
|
+
async function getDirCommit(dirPath) {
|
|
130
|
+
try {
|
|
131
|
+
const git = simpleGit(dirPath);
|
|
132
|
+
const isRepo = await git.checkIsRepo();
|
|
133
|
+
if (!isRepo) return null;
|
|
134
|
+
|
|
135
|
+
const log = await git.log({ maxCount: 1 });
|
|
136
|
+
return log.latest?.hash || null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
/**
|
|
123
143
|
* 更新 team:// 类型的 Skill
|
|
124
144
|
*/
|
|
@@ -126,7 +146,10 @@ async function updateTeamSkill(skill, projectRoot) {
|
|
|
126
146
|
const lockfile = new LockfileManager(projectRoot);
|
|
127
147
|
await lockfile.load();
|
|
128
148
|
|
|
129
|
-
const
|
|
149
|
+
const lockData = lockfile.getSkill(skill.name);
|
|
150
|
+
const localCommit = lockData?.commit;
|
|
151
|
+
|
|
152
|
+
const updateInfo = await checkTeamSkillUpdate(skill, skill.source, localCommit);
|
|
130
153
|
|
|
131
154
|
if (!updateInfo.hasUpdate) {
|
|
132
155
|
throw new Error('No update available');
|
|
@@ -140,10 +163,14 @@ async function updateTeamSkill(skill, projectRoot) {
|
|
|
140
163
|
// 复制新版本
|
|
141
164
|
copyRecursive(updateInfo.registrySkillPath, skill.path);
|
|
142
165
|
|
|
166
|
+
// 获取新版本的 commit
|
|
167
|
+
const newCommit = await getDirCommit(updateInfo.registrySkillPath);
|
|
168
|
+
|
|
143
169
|
// 更新 lockfile
|
|
144
170
|
lockfile.updateSkill(skill.name, {
|
|
145
171
|
version: updateInfo.latestVersion,
|
|
146
172
|
source: skill.source,
|
|
173
|
+
commit: newCommit,
|
|
147
174
|
updatedAt: new Date().toISOString(),
|
|
148
175
|
});
|
|
149
176
|
await lockfile.save();
|
|
@@ -173,15 +200,27 @@ function copyRecursive(src, dest) {
|
|
|
173
200
|
|
|
174
201
|
/**
|
|
175
202
|
* 获取指定的 Skills
|
|
203
|
+
* @param {boolean} globalOnly - 只检查全局 skills
|
|
204
|
+
* @param {boolean} projectOnly - 只检查项目级 skills
|
|
176
205
|
*/
|
|
177
|
-
async function getSpecifiedSkills(projectRoot, skillNames) {
|
|
206
|
+
async function getSpecifiedSkills(projectRoot, skillNames, globalOnly = false, projectOnly = false) {
|
|
178
207
|
const skills = [];
|
|
179
208
|
const loader = new SkillLoader(projectRoot);
|
|
180
209
|
const lockfile = new LockfileManager(projectRoot);
|
|
181
210
|
await lockfile.load();
|
|
182
211
|
|
|
212
|
+
// 根据选项选择加载方式
|
|
213
|
+
let allSkills = [];
|
|
214
|
+
if (globalOnly) {
|
|
215
|
+
allSkills = loader.loadGlobalSkills();
|
|
216
|
+
} else if (projectOnly) {
|
|
217
|
+
allSkills = loader.loadProjectSkills();
|
|
218
|
+
} else {
|
|
219
|
+
allSkills = loader.loadAllSkills();
|
|
220
|
+
}
|
|
221
|
+
|
|
183
222
|
for (const name of skillNames) {
|
|
184
|
-
const skill =
|
|
223
|
+
const skill = allSkills.find(s => s.name === name);
|
|
185
224
|
if (!skill) {
|
|
186
225
|
console.log(chalk.red(`❌ Skill not found: ${name}`));
|
|
187
226
|
continue;
|
|
@@ -199,7 +238,8 @@ async function getSpecifiedSkills(projectRoot, skillNames) {
|
|
|
199
238
|
|
|
200
239
|
if (teamSource) {
|
|
201
240
|
// team:// 类型
|
|
202
|
-
const
|
|
241
|
+
const localCommit = lockData?.commit;
|
|
242
|
+
const updateInfo = await checkTeamSkillUpdate(skill, teamSource, localCommit);
|
|
203
243
|
if (updateInfo.hasUpdate) {
|
|
204
244
|
skills.push({
|
|
205
245
|
name: skill.name,
|
|
@@ -238,7 +278,7 @@ async function getSpecifiedSkills(projectRoot, skillNames) {
|
|
|
238
278
|
/**
|
|
239
279
|
* 检查 team:// 类型的 Skill 是否有更新
|
|
240
280
|
*/
|
|
241
|
-
async function checkTeamSkillUpdate(skill, source) {
|
|
281
|
+
async function checkTeamSkillUpdate(skill, source, localCommit = null) {
|
|
242
282
|
const registryName = source.replace('team:', '');
|
|
243
283
|
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
244
284
|
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
@@ -254,7 +294,7 @@ async function checkTeamSkillUpdate(skill, source) {
|
|
|
254
294
|
return { hasUpdate: false, error: `Registry ${registryName} not found` };
|
|
255
295
|
}
|
|
256
296
|
|
|
257
|
-
// 从 registry
|
|
297
|
+
// 从 registry 查找 skill
|
|
258
298
|
const { findSkills } = await import('./install.js');
|
|
259
299
|
const registrySkills = await findSkills(reg.path);
|
|
260
300
|
const registrySkill = registrySkills.find(s => s.name === skill.name);
|
|
@@ -263,11 +303,22 @@ async function checkTeamSkillUpdate(skill, source) {
|
|
|
263
303
|
return { hasUpdate: false, error: 'Skill not found in registry' };
|
|
264
304
|
}
|
|
265
305
|
|
|
266
|
-
|
|
306
|
+
// 获取 registry 中 skill 的 commit
|
|
307
|
+
const remoteCommit = await getDirCommit(registrySkill.path);
|
|
308
|
+
|
|
309
|
+
// 使用 commit 对比,如果没有 commit 则 fallback 到 version 对比
|
|
310
|
+
let hasUpdate = false;
|
|
311
|
+
if (localCommit && remoteCommit) {
|
|
312
|
+
hasUpdate = localCommit !== remoteCommit;
|
|
313
|
+
} else {
|
|
314
|
+
hasUpdate = registrySkill.version && registrySkill.version !== skill.version;
|
|
315
|
+
}
|
|
267
316
|
|
|
268
317
|
return {
|
|
269
318
|
hasUpdate,
|
|
270
319
|
latestVersion: registrySkill.version,
|
|
320
|
+
localCommit,
|
|
321
|
+
remoteCommit,
|
|
271
322
|
registryPath: reg.path,
|
|
272
323
|
registrySkillPath: registrySkill.path,
|
|
273
324
|
};
|
|
@@ -336,19 +387,29 @@ async function checkGitSkillUpdate(skill) {
|
|
|
336
387
|
|
|
337
388
|
/**
|
|
338
389
|
* 获取所有可更新的 team:// Skills
|
|
390
|
+
* @param {boolean} globalOnly - 只检查全局 skills
|
|
391
|
+
* @param {boolean} projectOnly - 只检查项目级 skills
|
|
339
392
|
*/
|
|
340
|
-
async function getUpdatableTeamSkills(projectRoot) {
|
|
393
|
+
async function getUpdatableTeamSkills(projectRoot, globalOnly = false, projectOnly = false) {
|
|
341
394
|
const updatable = [];
|
|
342
395
|
const loader = new SkillLoader(projectRoot);
|
|
343
396
|
const lockfile = new LockfileManager(projectRoot);
|
|
344
397
|
await lockfile.load();
|
|
345
398
|
|
|
346
|
-
|
|
399
|
+
let allSkills = [];
|
|
400
|
+
if (globalOnly) {
|
|
401
|
+
allSkills = loader.loadGlobalSkills();
|
|
402
|
+
} else if (projectOnly) {
|
|
403
|
+
allSkills = loader.loadProjectSkills();
|
|
404
|
+
} else {
|
|
405
|
+
allSkills = loader.loadAllSkills();
|
|
406
|
+
}
|
|
347
407
|
|
|
348
408
|
for (const skill of allSkills) {
|
|
349
409
|
const lockData = lockfile.getSkill(skill.name);
|
|
350
410
|
if (lockData?.source?.startsWith('team:')) {
|
|
351
|
-
const
|
|
411
|
+
const localCommit = lockData?.commit;
|
|
412
|
+
const updateInfo = await checkTeamSkillUpdate(skill, lockData.source, localCommit);
|
|
352
413
|
if (updateInfo.hasUpdate) {
|
|
353
414
|
updatable.push({
|
|
354
415
|
name: skill.name,
|
package/src/skill-loader.js
CHANGED
|
@@ -129,16 +129,32 @@ export class SkillLoader {
|
|
|
129
129
|
* 仅加载项目级 Skills
|
|
130
130
|
*/
|
|
131
131
|
loadProjectSkills(agentId = null) {
|
|
132
|
-
const
|
|
133
|
-
|
|
132
|
+
const paths = getAllSkillPaths(this.projectRoot, agentId);
|
|
133
|
+
const skills = [];
|
|
134
|
+
|
|
135
|
+
for (const { path: dirPath, scope, agent } of paths) {
|
|
136
|
+
if (scope !== 'project') continue;
|
|
137
|
+
const dirSkills = this.scanDirectory(dirPath, scope, agent);
|
|
138
|
+
skills.push(...dirSkills);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return skills;
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
/**
|
|
137
145
|
* 仅加载用户级 Skills
|
|
138
146
|
*/
|
|
139
147
|
loadGlobalSkills(agentId = null) {
|
|
140
|
-
const
|
|
141
|
-
|
|
148
|
+
const paths = getAllSkillPaths(this.projectRoot, agentId);
|
|
149
|
+
const skills = [];
|
|
150
|
+
|
|
151
|
+
for (const { path: dirPath, scope, agent } of paths) {
|
|
152
|
+
if (scope !== 'global') continue;
|
|
153
|
+
const dirSkills = this.scanDirectory(dirPath, scope, agent);
|
|
154
|
+
skills.push(...dirSkills);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return skills;
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
/**
|
package/src/version-checker.js
CHANGED
|
@@ -80,8 +80,9 @@ export async function checkRemoteUpdate(skillPath, currentCommit) {
|
|
|
80
80
|
* 检查单个 Skill
|
|
81
81
|
*/
|
|
82
82
|
export async function checkSkill(skill, projectRoot) {
|
|
83
|
-
// 首先尝试从 lockfile 获取 source
|
|
83
|
+
// 首先尝试从 lockfile 获取 source 和 commit
|
|
84
84
|
let teamSource = null;
|
|
85
|
+
let localCommit = null;
|
|
85
86
|
|
|
86
87
|
if (projectRoot) {
|
|
87
88
|
const lockfile = new LockfileManager(projectRoot);
|
|
@@ -89,6 +90,7 @@ export async function checkSkill(skill, projectRoot) {
|
|
|
89
90
|
const lockData = lockfile.getSkill(skill.name);
|
|
90
91
|
if (lockData?.source?.startsWith('team:')) {
|
|
91
92
|
teamSource = lockData.source;
|
|
93
|
+
localCommit = lockData.commit;
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -97,9 +99,9 @@ export async function checkSkill(skill, projectRoot) {
|
|
|
97
99
|
teamSource = await findSkillInRegistries(skill.name);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
// 如果是 team:// skill,对比 registry
|
|
102
|
+
// 如果是 team:// skill,对比 registry commit
|
|
101
103
|
if (teamSource) {
|
|
102
|
-
return await checkTeamSkill(skill, teamSource);
|
|
104
|
+
return await checkTeamSkill(skill, teamSource, localCommit);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
// Git skill: 对比 commit
|
|
@@ -157,10 +159,26 @@ async function findSkillInRegistries(skillName) {
|
|
|
157
159
|
return null;
|
|
158
160
|
}
|
|
159
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
|
+
|
|
160
178
|
/**
|
|
161
179
|
* 检查 team:// skill 更新
|
|
162
180
|
*/
|
|
163
|
-
async function checkTeamSkill(skill, source) {
|
|
181
|
+
async function checkTeamSkill(skill, source, localCommit) {
|
|
164
182
|
const registryName = source.replace('team:', '');
|
|
165
183
|
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
166
184
|
const configPath = path.join(JOYSKILL_CONFIG_DIR, 'config.json');
|
|
@@ -188,7 +206,7 @@ async function checkTeamSkill(skill, source) {
|
|
|
188
206
|
};
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
// 从 registry
|
|
209
|
+
// 从 registry 查找 skill
|
|
192
210
|
const { findSkills } = await import('./commands/install.js');
|
|
193
211
|
const registrySkills = await findSkills(reg.path);
|
|
194
212
|
const registrySkill = registrySkills.find(s => s.name === skill.name);
|
|
@@ -203,24 +221,44 @@ async function checkTeamSkill(skill, source) {
|
|
|
203
221
|
};
|
|
204
222
|
}
|
|
205
223
|
|
|
206
|
-
|
|
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
|
+
}
|
|
207
234
|
|
|
208
235
|
return {
|
|
209
236
|
name: skill.name,
|
|
210
237
|
hasUpdate,
|
|
211
238
|
currentVersion: skill.version,
|
|
212
|
-
|
|
213
|
-
|
|
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,
|
|
214
242
|
source: `team:${registryName}`,
|
|
215
243
|
};
|
|
216
244
|
}
|
|
217
245
|
|
|
218
246
|
/**
|
|
219
247
|
* 检查所有 Skills
|
|
248
|
+
* @param {boolean} globalOnly - 只检查全局 skills
|
|
249
|
+
* @param {boolean} projectOnly - 只检查项目级 skills
|
|
220
250
|
*/
|
|
221
|
-
export async function checkAllSkills(projectRoot) {
|
|
251
|
+
export async function checkAllSkills(projectRoot, globalOnly = false, projectOnly = false) {
|
|
222
252
|
const loader = new SkillLoader(projectRoot);
|
|
223
|
-
|
|
253
|
+
let skills = [];
|
|
254
|
+
|
|
255
|
+
if (globalOnly) {
|
|
256
|
+
skills = loader.loadGlobalSkills();
|
|
257
|
+
} else if (projectOnly) {
|
|
258
|
+
skills = loader.loadProjectSkills();
|
|
259
|
+
} else {
|
|
260
|
+
skills = loader.loadAllSkills();
|
|
261
|
+
}
|
|
224
262
|
|
|
225
263
|
const results = [];
|
|
226
264
|
|
|
@@ -234,8 +272,10 @@ export async function checkAllSkills(projectRoot) {
|
|
|
234
272
|
|
|
235
273
|
/**
|
|
236
274
|
* 获取可更新的 Skills
|
|
275
|
+
* @param {boolean} globalOnly - 只检查全局 skills
|
|
276
|
+
* @param {boolean} projectOnly - 只检查项目级 skills
|
|
237
277
|
*/
|
|
238
|
-
export async function getUpdatableSkills(projectRoot) {
|
|
239
|
-
const results = await checkAllSkills(projectRoot);
|
|
278
|
+
export async function getUpdatableSkills(projectRoot, globalOnly = false, projectOnly = false) {
|
|
279
|
+
const results = await checkAllSkills(projectRoot, globalOnly, projectOnly);
|
|
240
280
|
return results.filter(r => r.hasUpdate);
|
|
241
281
|
}
|