joyskills-cli 0.2.5 → 0.2.7
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 +81 -195
- package/package.json +21 -12
- package/src/commands/install.js +616 -211
- package/src/commands/list.js +54 -25
- package/src/commands/manage.js +102 -0
- package/src/commands/read.js +61 -118
- package/src/commands/remove.js +28 -20
- package/src/commands/status.js +31 -41
- package/src/commands/sync.js +155 -244
- package/src/commands/team.js +267 -47
- package/src/commands/upgrade.js +311 -0
- package/src/index.js +10 -12
- package/src/local.js +2 -9
- package/LICENSE +0 -21
- package/spec/cli-spec.md +0 -167
- package/spec/lockfile-spec.md +0 -108
- package/spec/registry-spec.md +0 -117
package/src/commands/team.js
CHANGED
|
@@ -1,92 +1,312 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { LockfileManager } from '../lockfile.js';
|
|
3
|
+
import { RegistryManager } from '../registry.js';
|
|
4
|
+
import simpleGit from 'simple-git';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import { input, confirm } from '@inquirer/prompts';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
const JOYSKILL_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
12
|
+
const REGISTRY_CACHE_DIR = path.join(JOYSKILL_DIR, 'registries');
|
|
3
13
|
|
|
4
14
|
export function teamCommand(program) {
|
|
5
15
|
const teamProgram = program
|
|
6
16
|
.command('team')
|
|
7
17
|
.description('Team registry management');
|
|
8
18
|
|
|
19
|
+
// team add - Add a team registry
|
|
9
20
|
teamProgram
|
|
10
|
-
.command('add <name> <
|
|
11
|
-
.description('Add a team registry')
|
|
12
|
-
.action(async (name,
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
.command('add <name> <git-url>')
|
|
22
|
+
.description('Add a team registry from Git URL')
|
|
23
|
+
.action(async (name, gitUrl) => {
|
|
24
|
+
try {
|
|
25
|
+
console.log(chalk.blue(`📦 Adding team registry: ${name}`));
|
|
26
|
+
console.log(chalk.gray(` URL: ${gitUrl}`));
|
|
27
|
+
|
|
28
|
+
// Clone registry to cache
|
|
29
|
+
const registryCachePath = path.join(REGISTRY_CACHE_DIR, name);
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(registryCachePath)) {
|
|
32
|
+
const overwrite = await confirm({
|
|
33
|
+
message: `Registry "${name}" already exists. Overwrite?`,
|
|
34
|
+
default: false
|
|
35
|
+
});
|
|
36
|
+
if (!overwrite) {
|
|
37
|
+
console.log(chalk.gray(' Cancelled'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
fs.rmSync(registryCachePath, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.mkdirSync(registryCachePath, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Clone the registry
|
|
46
|
+
const git = simpleGit();
|
|
47
|
+
await git.clone(gitUrl, registryCachePath, ['--depth', '1']);
|
|
48
|
+
|
|
49
|
+
// Validate registry structure (flexible: registry.yaml is optional)
|
|
50
|
+
const registryYamlPath = path.join(registryCachePath, 'registry.yaml');
|
|
51
|
+
const hasRegistryYaml = fs.existsSync(registryYamlPath);
|
|
15
52
|
|
|
16
|
-
|
|
53
|
+
// Count skill directories (dirs with SKILL.md)
|
|
54
|
+
const entries = fs.readdirSync(registryCachePath, { withFileTypes: true });
|
|
55
|
+
const skillDirs = entries.filter(e => e.isDirectory() && fs.existsSync(path.join(registryCachePath, e.name, 'SKILL.md')));
|
|
17
56
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
if (!hasRegistryYaml && skillDirs.length === 0) {
|
|
58
|
+
throw new Error('Invalid registry: no registry.yaml and no skill directories found');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Save to global config
|
|
62
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
63
|
+
let config = { registries: {} };
|
|
64
|
+
if (fs.existsSync(configPath)) {
|
|
65
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let registryId = name;
|
|
69
|
+
let skillCount = skillDirs.length;
|
|
70
|
+
|
|
71
|
+
if (hasRegistryYaml) {
|
|
72
|
+
const registryManager = new RegistryManager(registryCachePath);
|
|
73
|
+
await registryManager.load();
|
|
74
|
+
const info = registryManager.getRegistryInfo();
|
|
75
|
+
registryId = info.registryId;
|
|
76
|
+
skillCount = registryManager.getAllSkills().length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
config.registries[name] = {
|
|
80
|
+
url: gitUrl,
|
|
81
|
+
path: registryCachePath,
|
|
82
|
+
registryId,
|
|
83
|
+
addedAt: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
fs.mkdirSync(JOYSKILL_DIR, { recursive: true });
|
|
87
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
88
|
+
|
|
89
|
+
console.log(chalk.green(`✅ Successfully added registry: ${name}`));
|
|
90
|
+
console.log(chalk.gray(` Registry ID: ${registryId}`));
|
|
91
|
+
console.log(chalk.gray(` Skills: ${skillCount}${hasRegistryYaml ? '' : ' (no registry.yaml, direct scan)'}`));
|
|
92
|
+
|
|
23
93
|
} catch (error) {
|
|
24
|
-
console.error(`❌ Failed to add registry
|
|
94
|
+
console.error(chalk.red(`❌ Failed to add registry: ${error.message}`));
|
|
95
|
+
process.exit(1);
|
|
25
96
|
}
|
|
26
97
|
});
|
|
27
98
|
|
|
99
|
+
// team remove - Remove a team registry
|
|
28
100
|
teamProgram
|
|
29
101
|
.command('remove <name>')
|
|
30
102
|
.description('Remove a team registry')
|
|
31
103
|
.action(async (name) => {
|
|
32
|
-
const projectRoot = process.cwd();
|
|
33
|
-
const lockfileManager = new LockfileManager(projectRoot);
|
|
34
|
-
|
|
35
|
-
console.log(`Removing team registry: ${name}`);
|
|
36
|
-
|
|
37
104
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
105
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
106
|
+
if (!fs.existsSync(configPath)) {
|
|
107
|
+
console.log(chalk.yellow('⚠️ No registries configured'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
112
|
+
|
|
113
|
+
if (!config.registries[name]) {
|
|
114
|
+
console.log(chalk.yellow(`⚠️ Registry "${name}" not found`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove cache
|
|
119
|
+
const registryPath = config.registries[name].path;
|
|
120
|
+
if (fs.existsSync(registryPath)) {
|
|
121
|
+
fs.rmSync(registryPath, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Remove from config
|
|
125
|
+
delete config.registries[name];
|
|
126
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
127
|
+
|
|
128
|
+
console.log(chalk.green(`✅ Successfully removed registry: ${name}`));
|
|
129
|
+
|
|
42
130
|
} catch (error) {
|
|
43
|
-
console.error(`❌ Failed to remove registry
|
|
131
|
+
console.error(chalk.red(`❌ Failed to remove registry: ${error.message}`));
|
|
132
|
+
process.exit(1);
|
|
44
133
|
}
|
|
45
134
|
});
|
|
46
135
|
|
|
136
|
+
// team list - List team registries
|
|
47
137
|
teamProgram
|
|
48
138
|
.command('list')
|
|
49
139
|
.description('List team registries')
|
|
50
140
|
.action(async () => {
|
|
51
|
-
const projectRoot = process.cwd();
|
|
52
|
-
const lockfileManager = new LockfileManager(projectRoot);
|
|
53
|
-
|
|
54
|
-
console.log('Team Registries:');
|
|
55
|
-
console.log('================');
|
|
56
|
-
|
|
57
141
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (registryNames.length === 0) {
|
|
64
|
-
console.log('No team registries configured.');
|
|
142
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(configPath)) {
|
|
145
|
+
console.log(chalk.yellow('⚠️ No registries configured'));
|
|
146
|
+
console.log(chalk.gray(' Run: joySkills team add <name> <git-url>'));
|
|
65
147
|
return;
|
|
66
148
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
149
|
+
|
|
150
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
151
|
+
const registries = config.registries || {};
|
|
152
|
+
const names = Object.keys(registries);
|
|
153
|
+
|
|
154
|
+
if (names.length === 0) {
|
|
155
|
+
console.log(chalk.yellow('⚠️ No registries configured'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.blue('\n📦 Team Registries:'));
|
|
160
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
161
|
+
|
|
162
|
+
for (const name of names) {
|
|
163
|
+
const reg = registries[name];
|
|
164
|
+
const exists = fs.existsSync(reg.path);
|
|
165
|
+
const status = exists ? chalk.green('✓') : chalk.red('✗');
|
|
166
|
+
|
|
167
|
+
console.log(` ${status} ${chalk.bold(name)}`);
|
|
168
|
+
console.log(` URL: ${chalk.gray(reg.url)}`);
|
|
169
|
+
console.log(` ID: ${chalk.gray(reg.registryId)}`);
|
|
170
|
+
|
|
171
|
+
if (exists) {
|
|
172
|
+
try {
|
|
173
|
+
// Only load RegistryManager if registry.yaml exists
|
|
174
|
+
const hasYaml = fs.existsSync(path.join(reg.path, 'registry.yaml'));
|
|
175
|
+
if (hasYaml) {
|
|
176
|
+
const registryManager = new RegistryManager(reg.path);
|
|
177
|
+
await registryManager.load();
|
|
178
|
+
console.log(` Skills: ${chalk.blue(registryManager.getAllSkills().length)}`);
|
|
179
|
+
} else {
|
|
180
|
+
// Count skill dirs directly
|
|
181
|
+
const dirs = fs.readdirSync(reg.path, { withFileTypes: true })
|
|
182
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(reg.path, e.name, 'SKILL.md')));
|
|
183
|
+
console.log(` Skills: ${chalk.blue(dirs.length)} ${chalk.gray('(direct)')}`);
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.log(` ${chalk.yellow('Warning: Invalid registry')}`);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` ${chalk.red('Error: Cache missing')}`);
|
|
190
|
+
}
|
|
191
|
+
console.log();
|
|
71
192
|
}
|
|
193
|
+
|
|
72
194
|
} catch (error) {
|
|
73
|
-
console.error(`❌ Failed to list registries
|
|
195
|
+
console.error(chalk.red(`❌ Failed to list registries: ${error.message}`));
|
|
196
|
+
process.exit(1);
|
|
74
197
|
}
|
|
75
198
|
});
|
|
76
199
|
|
|
200
|
+
// team sync - Sync team registry
|
|
77
201
|
teamProgram
|
|
78
202
|
.command('sync [name]')
|
|
79
|
-
.description('Sync team registry')
|
|
203
|
+
.description('Sync team registry from Git')
|
|
80
204
|
.action(async (name) => {
|
|
81
|
-
|
|
82
|
-
|
|
205
|
+
try {
|
|
206
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
207
|
+
|
|
208
|
+
if (!fs.existsSync(configPath)) {
|
|
209
|
+
console.log(chalk.yellow('⚠️ No registries configured'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
214
|
+
const registries = config.registries || {};
|
|
215
|
+
|
|
216
|
+
const namesToSync = name
|
|
217
|
+
? [name]
|
|
218
|
+
: Object.keys(registries);
|
|
219
|
+
|
|
220
|
+
if (namesToSync.length === 0) {
|
|
221
|
+
console.log(chalk.yellow('⚠️ No registries to sync'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(chalk.blue(`🔄 Syncing ${namesToSync.length} registry(s)...`));
|
|
226
|
+
|
|
227
|
+
for (const registryName of namesToSync) {
|
|
228
|
+
const reg = registries[registryName];
|
|
229
|
+
|
|
230
|
+
if (!reg) {
|
|
231
|
+
console.log(chalk.yellow(`⚠️ Registry "${registryName}" not found`));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(chalk.gray(` Syncing ${registryName}...`));
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
if (fs.existsSync(reg.path)) {
|
|
239
|
+
// Pull updates
|
|
240
|
+
const git = simpleGit(reg.path);
|
|
241
|
+
await git.pull();
|
|
242
|
+
console.log(chalk.green(` ✓ ${registryName} updated`));
|
|
243
|
+
} else {
|
|
244
|
+
// Re-clone
|
|
245
|
+
fs.mkdirSync(reg.path, { recursive: true });
|
|
246
|
+
const git = simpleGit();
|
|
247
|
+
await git.clone(reg.url, reg.path, ['--depth', '1']);
|
|
248
|
+
console.log(chalk.green(` ✓ ${registryName} re-cloned`));
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(chalk.red(` ✗ ${registryName} failed: ${error.message}`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log(chalk.green('\n✅ Sync completed'));
|
|
256
|
+
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error(chalk.red(`❌ Sync failed: ${error.message}`));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
83
261
|
});
|
|
84
262
|
|
|
263
|
+
// team use - Set default team registry
|
|
85
264
|
teamProgram
|
|
86
265
|
.command('use <name>')
|
|
87
|
-
.description('Set default team registry')
|
|
266
|
+
.description('Set default team registry for current project')
|
|
88
267
|
.action(async (name) => {
|
|
89
|
-
|
|
90
|
-
|
|
268
|
+
try {
|
|
269
|
+
const configPath = path.join(JOYSKILL_DIR, 'config.json');
|
|
270
|
+
|
|
271
|
+
if (!fs.existsSync(configPath)) {
|
|
272
|
+
console.log(chalk.yellow('⚠️ No registries configured'));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
277
|
+
|
|
278
|
+
if (!config.registries[name]) {
|
|
279
|
+
console.log(chalk.yellow(`⚠️ Registry "${name}" not found`));
|
|
280
|
+
console.log(chalk.gray(' Run: joySkills team add <name> <git-url>'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Update project lockfile
|
|
285
|
+
const projectRoot = process.cwd();
|
|
286
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
287
|
+
await lockfileManager.load();
|
|
288
|
+
|
|
289
|
+
const reg = config.registries[name];
|
|
290
|
+
lockfileManager.bindRegistry(reg.registryId, name);
|
|
291
|
+
await lockfileManager.save();
|
|
292
|
+
|
|
293
|
+
// Create symlink to registry
|
|
294
|
+
const joyskillDir = path.join(projectRoot, '.joyskill');
|
|
295
|
+
const registryLink = path.join(joyskillDir, 'registry');
|
|
296
|
+
|
|
297
|
+
if (fs.existsSync(registryLink)) {
|
|
298
|
+
fs.rmSync(registryLink, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fs.mkdirSync(joyskillDir, { recursive: true });
|
|
302
|
+
fs.symlinkSync(reg.path, registryLink, 'dir');
|
|
303
|
+
|
|
304
|
+
console.log(chalk.green(`✅ Now using registry: ${name}`));
|
|
305
|
+
console.log(chalk.gray(` Registry ID: ${reg.registryId}`));
|
|
306
|
+
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(chalk.red(`❌ Failed to set registry: ${error.message}`));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
91
311
|
});
|
|
92
|
-
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { LocalManager } from '../local.js';
|
|
3
|
+
import { LockfileManager } from '../lockfile.js';
|
|
4
|
+
import { RegistryManager } from '../registry.js';
|
|
5
|
+
import simpleGit from 'simple-git';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
const CACHE_DIR = process.env.JOYSKILL_CACHE_DIR || path.join(os.homedir(), '.joyskill', 'cache');
|
|
13
|
+
|
|
14
|
+
export function upgradeCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('upgrade [skill]')
|
|
17
|
+
.description('Upgrade installed skills to latest versions')
|
|
18
|
+
.option('-a, --all', 'Upgrade all skills')
|
|
19
|
+
.option('--dry-run', 'Show what would be upgraded without making changes')
|
|
20
|
+
.option('-g, --global', 'Upgrade global skills')
|
|
21
|
+
.action(async (skillName, options) => {
|
|
22
|
+
try {
|
|
23
|
+
const projectRoot = process.cwd();
|
|
24
|
+
const isGlobal = options.global;
|
|
25
|
+
// Scan both standard dirs; upgrade will check all
|
|
26
|
+
const skillsDirs = isGlobal
|
|
27
|
+
? [path.join(os.homedir(), '.claude', 'skills'), path.join(os.homedir(), '.agent', 'skills')]
|
|
28
|
+
: [path.join(projectRoot, '.claude', 'skills'), path.join(projectRoot, '.agent', 'skills')];
|
|
29
|
+
// Use first existing dir as primary, but pass all dirs to findUpgradableSkills
|
|
30
|
+
const skillsDir = skillsDirs.find(d => fs.existsSync(d)) || skillsDirs[0];
|
|
31
|
+
|
|
32
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
33
|
+
await lockfileManager.load();
|
|
34
|
+
|
|
35
|
+
// Load registry if available
|
|
36
|
+
const registryPath = path.join(projectRoot, '.joyskill', 'registry');
|
|
37
|
+
let registryManager = null;
|
|
38
|
+
if (fs.existsSync(registryPath)) {
|
|
39
|
+
registryManager = new RegistryManager(registryPath);
|
|
40
|
+
await registryManager.load();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find upgradable skills
|
|
44
|
+
const upgradableSkills = await findUpgradableSkills(
|
|
45
|
+
lockfileManager,
|
|
46
|
+
registryManager,
|
|
47
|
+
skillsDir,
|
|
48
|
+
skillName
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (upgradableSkills.length === 0) {
|
|
52
|
+
console.log(chalk.green('✅ All skills are up to date'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Display available upgrades
|
|
57
|
+
console.log(chalk.blue('\n📦 Available Upgrades:'));
|
|
58
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
59
|
+
|
|
60
|
+
for (const skill of upgradableSkills) {
|
|
61
|
+
const current = chalk.gray(skill.currentVersion);
|
|
62
|
+
const latest = chalk.green(skill.latestVersion);
|
|
63
|
+
const indicator = skill.hasBreakingChange ? chalk.red('⚠️ Breaking') : chalk.blue('Patch/Minor');
|
|
64
|
+
console.log(` ${skill.name}: ${current} → ${latest} ${indicator}`);
|
|
65
|
+
if (skill.changelog) {
|
|
66
|
+
console.log(` ${chalk.gray(skill.changelog)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options.dryRun) {
|
|
71
|
+
console.log(chalk.yellow('\n⚠️ Dry run - no changes made'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Select skills to upgrade
|
|
76
|
+
let selectedSkills = [];
|
|
77
|
+
|
|
78
|
+
if (options.all) {
|
|
79
|
+
selectedSkills = upgradableSkills;
|
|
80
|
+
} else if (skillName) {
|
|
81
|
+
const skill = upgradableSkills.find(s => s.name === skillName);
|
|
82
|
+
if (!skill) {
|
|
83
|
+
console.log(chalk.green(`✅ ${skillName} is already up to date`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
selectedSkills = [skill];
|
|
87
|
+
} else {
|
|
88
|
+
const choices = upgradableSkills.map(s => ({
|
|
89
|
+
name: `${s.name} (${s.currentVersion} → ${s.latestVersion})`,
|
|
90
|
+
value: s,
|
|
91
|
+
checked: !s.hasBreakingChange
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
selectedSkills = await checkbox({
|
|
95
|
+
message: 'Select skills to upgrade:',
|
|
96
|
+
choices: choices
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (selectedSkills.length === 0) {
|
|
101
|
+
console.log(chalk.gray(' No skills selected'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Confirm breaking changes
|
|
106
|
+
const breakingChanges = selectedSkills.filter(s => s.hasBreakingChange);
|
|
107
|
+
if (breakingChanges.length > 0) {
|
|
108
|
+
console.log(chalk.yellow('\n⚠️ Breaking changes detected:'));
|
|
109
|
+
for (const skill of breakingChanges) {
|
|
110
|
+
console.log(` - ${skill.name}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const confirmed = await confirm({
|
|
114
|
+
message: 'These upgrades may have breaking changes. Continue?',
|
|
115
|
+
default: false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!confirmed) {
|
|
119
|
+
console.log(chalk.gray(' Cancelled'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Perform upgrades
|
|
125
|
+
console.log(chalk.blue(`\n🔄 Upgrading ${selectedSkills.length} skill(s)...`));
|
|
126
|
+
|
|
127
|
+
for (const skill of selectedSkills) {
|
|
128
|
+
await upgradeSkill(skill, skillsDir, lockfileManager);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await lockfileManager.save();
|
|
132
|
+
|
|
133
|
+
console.log(chalk.green(`\n✅ Successfully upgraded ${selectedSkills.length} skill(s)`));
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(chalk.red(`❌ Upgrade failed: ${error.message}`));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function findUpgradableSkills(lockfileManager, registryManager, skillsDir, specificSkill) {
|
|
143
|
+
const upgradable = [];
|
|
144
|
+
const installedSkills = lockfileManager.lockData.skills || {};
|
|
145
|
+
|
|
146
|
+
// Also check sibling dir (.claude ↔ .agent)
|
|
147
|
+
const siblingDir = skillsDir.includes('.claude')
|
|
148
|
+
? skillsDir.replace('.claude', '.agent')
|
|
149
|
+
: skillsDir.replace('.agent', '.claude');
|
|
150
|
+
|
|
151
|
+
const skillNames = specificSkill
|
|
152
|
+
? [specificSkill]
|
|
153
|
+
: Object.keys(installedSkills);
|
|
154
|
+
|
|
155
|
+
for (const name of skillNames) {
|
|
156
|
+
const current = installedSkills[name];
|
|
157
|
+
if (!current) continue;
|
|
158
|
+
|
|
159
|
+
// Check existence in either standard dir
|
|
160
|
+
const skillPath = [skillsDir, siblingDir].map(d => path.join(d, name)).find(p => fs.existsSync(p));
|
|
161
|
+
if (!skillPath) continue;
|
|
162
|
+
|
|
163
|
+
let latestVersion = null;
|
|
164
|
+
let hasBreakingChange = false;
|
|
165
|
+
let changelog = '';
|
|
166
|
+
|
|
167
|
+
// Check registry for updates
|
|
168
|
+
if (registryManager && registryManager.hasSkill(name)) {
|
|
169
|
+
const recommended = registryManager.getRecommendedVersion(name);
|
|
170
|
+
if (recommended && recommended.version !== current.version) {
|
|
171
|
+
latestVersion = recommended.version;
|
|
172
|
+
|
|
173
|
+
// Check for breaking change (major version bump)
|
|
174
|
+
const currentMajor = parseInt(current.version.split('.')[0]);
|
|
175
|
+
const latestMajor = parseInt(latestVersion.split('.')[0]);
|
|
176
|
+
hasBreakingChange = latestMajor > currentMajor;
|
|
177
|
+
|
|
178
|
+
if (recommended.changelog) {
|
|
179
|
+
changelog = recommended.changelog;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check GitHub source for updates
|
|
185
|
+
if (!latestVersion && current.source === 'github' && current.repository) {
|
|
186
|
+
const remoteVersion = await checkGitHubForUpdates(current.repository, name);
|
|
187
|
+
if (remoteVersion && remoteVersion !== current.version) {
|
|
188
|
+
latestVersion = remoteVersion;
|
|
189
|
+
const currentMajor = parseInt(current.version.split('.')[0]);
|
|
190
|
+
const latestMajor = parseInt(latestVersion.split('.')[0]);
|
|
191
|
+
hasBreakingChange = latestMajor > currentMajor;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (latestVersion) {
|
|
196
|
+
upgradable.push({
|
|
197
|
+
name,
|
|
198
|
+
currentVersion: current.version,
|
|
199
|
+
latestVersion,
|
|
200
|
+
hasBreakingChange,
|
|
201
|
+
changelog,
|
|
202
|
+
source: current.source,
|
|
203
|
+
repository: current.repository
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return upgradable;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function checkGitHubForUpdates(repository, skillName) {
|
|
212
|
+
try {
|
|
213
|
+
const [owner, repo] = repository.split('/');
|
|
214
|
+
const cacheKey = `${owner}-${repo}`;
|
|
215
|
+
const cachePath = path.join(CACHE_DIR, cacheKey);
|
|
216
|
+
|
|
217
|
+
if (!fs.existsSync(cachePath)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fetch latest from remote
|
|
222
|
+
const git = simpleGit(cachePath);
|
|
223
|
+
await git.fetch(['--depth', '1']);
|
|
224
|
+
|
|
225
|
+
// Check for tags
|
|
226
|
+
const tags = await git.tags(['--sort=-version:refname']);
|
|
227
|
+
if (tags.all.length > 0) {
|
|
228
|
+
// Remove 'v' prefix if present
|
|
229
|
+
return tags.all[0].replace(/^v/, '');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function upgradeSkill(skill, skillsDir, lockfileManager) {
|
|
239
|
+
console.log(chalk.gray(` Upgrading ${skill.name}...`));
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
if (skill.source === 'github' && skill.repository) {
|
|
243
|
+
// Re-install from GitHub
|
|
244
|
+
await upgradeFromGitHub(skill, skillsDir);
|
|
245
|
+
} else if (skill.source === 'registry') {
|
|
246
|
+
// Re-install from registry
|
|
247
|
+
await upgradeFromRegistry(skill, skillsDir);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update lockfile
|
|
251
|
+
lockfileManager.updateSkill(skill.name, {
|
|
252
|
+
...lockfileManager.getSkill(skill.name),
|
|
253
|
+
version: skill.latestVersion,
|
|
254
|
+
upgradedAt: new Date().toISOString()
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
console.log(chalk.green(` ✓ ${skill.name} upgraded to v${skill.latestVersion}`));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(chalk.red(` ✗ Failed to upgrade ${skill.name}: ${error.message}`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function upgradeFromGitHub(skill, skillsDir) {
|
|
264
|
+
const [owner, repo] = skill.repository.split('/');
|
|
265
|
+
const cacheKey = `${owner}-${repo}`;
|
|
266
|
+
const cachePath = path.join(CACHE_DIR, cacheKey);
|
|
267
|
+
|
|
268
|
+
// Pull latest
|
|
269
|
+
const git = simpleGit(cachePath);
|
|
270
|
+
await git.pull();
|
|
271
|
+
|
|
272
|
+
// Find skill in cache
|
|
273
|
+
const skillSourcePath = path.join(cachePath, skill.name);
|
|
274
|
+
if (!fs.existsSync(skillSourcePath)) {
|
|
275
|
+
throw new Error(`Skill not found in repository: ${skill.name}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Remove old version
|
|
279
|
+
const targetPath = path.join(skillsDir, skill.name);
|
|
280
|
+
if (fs.existsSync(targetPath)) {
|
|
281
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Copy new version
|
|
285
|
+
copyRecursive(skillSourcePath, targetPath);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function upgradeFromRegistry(skill, skillsDir) {
|
|
289
|
+
// Implementation depends on registry structure
|
|
290
|
+
// For now, just update the version in lockfile
|
|
291
|
+
console.log(chalk.yellow(` Note: Registry upgrade not fully implemented for ${skill.name}`));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function copyRecursive(src, dest) {
|
|
295
|
+
if (!fs.existsSync(dest)) {
|
|
296
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
300
|
+
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
const srcPath = path.join(src, entry.name);
|
|
303
|
+
const destPath = path.join(dest, entry.name);
|
|
304
|
+
|
|
305
|
+
if (entry.isDirectory()) {
|
|
306
|
+
copyRecursive(srcPath, destPath);
|
|
307
|
+
} else {
|
|
308
|
+
fs.copyFileSync(srcPath, destPath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|