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.
@@ -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> <path>')
11
- .description('Add a team registry')
12
- .action(async (name, path) => {
13
- const projectRoot = process.cwd();
14
- const lockfileManager = new LockfileManager(projectRoot);
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
- console.log(`Adding team registry: ${name} -> ${path}`);
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
- try {
19
- await lockfileManager.load();
20
- lockfileManager.bindRegistry(path, name);
21
- await lockfileManager.save();
22
- console.log(`✅ Successfully added registry ${name}`);
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:`, error);
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
- await lockfileManager.load();
39
- lockfileManager.unbindRegistry(name);
40
- await lockfileManager.save();
41
- console.log(`✅ Successfully removed registry ${name}`);
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:`, error);
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
- await lockfileManager.load();
59
-
60
- const registries = lockfileManager.lockData.registries || {};
61
- const registryNames = Object.keys(registries);
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
- for (const name of registryNames) {
69
- const registry = registries[name];
70
- console.log(`${name}: ${registry.path}`);
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:`, error);
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
- console.log(`Syncing registry: ${name || 'all'}`);
82
- console.log('(Registry sync functionality coming soon)');
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
- console.log(`Setting default registry to: ${name}`);
90
- console.log('(Registry use functionality coming soon)');
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
+ }