cli-ai-skills 1.0.0
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 +257 -0
- package/bin/cli.js +108 -0
- package/lib/commands/doctor.js +119 -0
- package/lib/commands/install.js +240 -0
- package/lib/commands/list.js +68 -0
- package/lib/commands/uninstall.js +217 -0
- package/lib/commands/update.js +142 -0
- package/lib/core/detector.js +136 -0
- package/lib/core/downloader.js +161 -0
- package/lib/core/installer.js +150 -0
- package/lib/core/version-checker.js +129 -0
- package/lib/ui/progress-gauge.js +132 -0
- package/lib/ui/prompts.js +202 -0
- package/package.json +55 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync} = require('child_process');
|
|
5
|
+
|
|
6
|
+
class PlatformDetector {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.homeDir = os.homedir();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect all available platforms
|
|
13
|
+
* @returns {Object} Platform detection results
|
|
14
|
+
*/
|
|
15
|
+
async detectAll() {
|
|
16
|
+
return {
|
|
17
|
+
copilot: await this.detectCopilot(),
|
|
18
|
+
claude: await this.detectClaude()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect GitHub Copilot CLI
|
|
24
|
+
* @returns {Object} Detection result with installation status and paths
|
|
25
|
+
*/
|
|
26
|
+
async detectCopilot() {
|
|
27
|
+
const result = {
|
|
28
|
+
installed: false,
|
|
29
|
+
cliInstalled: false,
|
|
30
|
+
version: null,
|
|
31
|
+
globalPath: path.join(this.homeDir, '.copilot', 'skills'),
|
|
32
|
+
localPath: path.join(process.cwd(), '.github', 'skills')
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Check if gh copilot is installed
|
|
36
|
+
try {
|
|
37
|
+
const version = execSync('gh copilot --version 2>&1', { encoding: 'utf8' });
|
|
38
|
+
result.cliInstalled = true;
|
|
39
|
+
result.version = version.trim();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
result.cliInstalled = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if global skills directory exists
|
|
45
|
+
result.installed = await fs.pathExists(path.join(this.homeDir, '.copilot'));
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect Claude Code
|
|
52
|
+
* @returns {Object} Detection result with installation status and paths
|
|
53
|
+
*/
|
|
54
|
+
async detectClaude() {
|
|
55
|
+
const result = {
|
|
56
|
+
installed: false,
|
|
57
|
+
version: null,
|
|
58
|
+
globalPath: path.join(this.homeDir, '.claude', 'skills'),
|
|
59
|
+
localPath: path.join(process.cwd(), '.claude', 'skills')
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Check if .claude directory exists
|
|
63
|
+
const claudeDir = path.join(this.homeDir, '.claude');
|
|
64
|
+
result.installed = await fs.pathExists(claudeDir);
|
|
65
|
+
|
|
66
|
+
// Try to get version from config
|
|
67
|
+
if (result.installed) {
|
|
68
|
+
const configPath = path.join(claudeDir, 'settings.local.json');
|
|
69
|
+
if (await fs.pathExists(configPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const config = await fs.readJson(configPath);
|
|
72
|
+
result.version = config.version || 'unknown';
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Ignore JSON parse errors
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get installation paths based on scope
|
|
84
|
+
* @param {string} scope - 'global' or 'local'
|
|
85
|
+
* @param {Object} platforms - Platform detection results
|
|
86
|
+
* @returns {Object} Installation paths
|
|
87
|
+
*/
|
|
88
|
+
getInstallPaths(scope, platforms) {
|
|
89
|
+
const paths = {};
|
|
90
|
+
|
|
91
|
+
if (scope === 'global') {
|
|
92
|
+
if (platforms.copilot) {
|
|
93
|
+
paths.copilot = platforms.copilot.globalPath;
|
|
94
|
+
}
|
|
95
|
+
if (platforms.claude) {
|
|
96
|
+
paths.claude = platforms.claude.globalPath;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// local
|
|
100
|
+
if (platforms.copilot) {
|
|
101
|
+
paths.copilot = platforms.copilot.localPath;
|
|
102
|
+
}
|
|
103
|
+
if (platforms.claude) {
|
|
104
|
+
paths.claude = platforms.claude.localPath;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return paths;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Ensure installation directories exist
|
|
113
|
+
* @param {Object} paths - Installation paths
|
|
114
|
+
*/
|
|
115
|
+
async ensureDirectories(paths) {
|
|
116
|
+
for (const [platform, dirPath] of Object.entries(paths)) {
|
|
117
|
+
await fs.ensureDir(dirPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if directory is writable
|
|
123
|
+
* @param {string} dirPath - Directory path to check
|
|
124
|
+
* @returns {boolean} True if writable
|
|
125
|
+
*/
|
|
126
|
+
async isWritable(dirPath) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.access(dirPath, fs.constants.W_OK);
|
|
129
|
+
return true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = PlatformDetector;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
|
|
8
|
+
class SkillDownloader {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.repoOwner = 'ericgandrade';
|
|
11
|
+
this.repoName = 'cli-ai-skills';
|
|
12
|
+
this.branch = 'main';
|
|
13
|
+
this.cacheDir = path.join(os.homedir(), '.cli-ai-skills', 'cache');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List available skills from GitHub
|
|
18
|
+
* @returns {Array} List of skill objects
|
|
19
|
+
*/
|
|
20
|
+
async listAvailableSkills() {
|
|
21
|
+
const spinner = ora('Fetching available skills...').start();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const url = `https://api.github.com/repos/${this.repoOwner}/${this.repoName}/contents/.github/skills`;
|
|
25
|
+
const response = await axios.get(url, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
28
|
+
'User-Agent': 'cli-ai-skills-installer'
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const skills = [];
|
|
33
|
+
|
|
34
|
+
for (const item of response.data) {
|
|
35
|
+
if (item.type === 'dir') {
|
|
36
|
+
// Get SKILL.md to extract metadata
|
|
37
|
+
const skillMeta = await this.getSkillMetadata(item.name, '.github');
|
|
38
|
+
skills.push({
|
|
39
|
+
name: item.name,
|
|
40
|
+
version: skillMeta.version,
|
|
41
|
+
description: skillMeta.description
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
spinner.succeed(`Found ${skills.length} skills`);
|
|
47
|
+
return skills;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
spinner.fail('Failed to fetch skills');
|
|
50
|
+
throw new Error(`GitHub API error: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get skill metadata from SKILL.md
|
|
56
|
+
* @param {string} skillName - Name of the skill
|
|
57
|
+
* @param {string} basePath - '.github' or '.claude'
|
|
58
|
+
* @returns {Object} Skill metadata
|
|
59
|
+
*/
|
|
60
|
+
async getSkillMetadata(skillName, basePath = '.github') {
|
|
61
|
+
try {
|
|
62
|
+
const url = `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/${this.branch}/${basePath}/skills/${skillName}/SKILL.md`;
|
|
63
|
+
const response = await axios.get(url);
|
|
64
|
+
|
|
65
|
+
// Extract YAML frontmatter
|
|
66
|
+
const frontmatterMatch = response.data.match(/^---\n([\s\S]*?)\n---/);
|
|
67
|
+
if (frontmatterMatch) {
|
|
68
|
+
return yaml.load(frontmatterMatch[1]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { version: 'unknown', description: 'No description' };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { version: 'unknown', description: 'No description' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Download a skill directory from GitHub
|
|
79
|
+
* @param {string} skillName - Name of the skill
|
|
80
|
+
* @param {string} platform - 'copilot' or 'claude'
|
|
81
|
+
* @returns {string} Path to downloaded skill
|
|
82
|
+
*/
|
|
83
|
+
async downloadSkill(skillName, platform = 'copilot') {
|
|
84
|
+
const spinner = ora(`Downloading ${skillName}...`).start();
|
|
85
|
+
|
|
86
|
+
const basePath = platform === 'copilot' ? '.github/skills' : '.claude/skills';
|
|
87
|
+
const skillPath = path.join(this.cacheDir, platform, skillName);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Ensure cache directory exists
|
|
91
|
+
await fs.ensureDir(skillPath);
|
|
92
|
+
|
|
93
|
+
// Get directory contents from GitHub API
|
|
94
|
+
const url = `https://api.github.com/repos/${this.repoOwner}/${this.repoName}/contents/${basePath}/${skillName}`;
|
|
95
|
+
const response = await axios.get(url, {
|
|
96
|
+
headers: {
|
|
97
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
98
|
+
'User-Agent': 'cli-ai-skills-installer'
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Download each file
|
|
103
|
+
await this.downloadDirectory(response.data, skillPath);
|
|
104
|
+
|
|
105
|
+
spinner.succeed(`Downloaded ${skillName}`);
|
|
106
|
+
return skillPath;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
spinner.fail(`Failed to download ${skillName}`);
|
|
109
|
+
throw new Error(`Download error: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Recursively download directory contents
|
|
115
|
+
* @param {Array} contents - GitHub API contents response
|
|
116
|
+
* @param {string} targetPath - Target directory path
|
|
117
|
+
*/
|
|
118
|
+
async downloadDirectory(contents, targetPath) {
|
|
119
|
+
for (const item of contents) {
|
|
120
|
+
const itemPath = path.join(targetPath, item.name);
|
|
121
|
+
|
|
122
|
+
if (item.type === 'file') {
|
|
123
|
+
// Download file
|
|
124
|
+
const response = await axios.get(item.download_url, {
|
|
125
|
+
responseType: 'arraybuffer',
|
|
126
|
+
headers: {
|
|
127
|
+
'User-Agent': 'cli-ai-skills-installer'
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
await fs.writeFile(itemPath, response.data);
|
|
131
|
+
} else if (item.type === 'dir') {
|
|
132
|
+
// Recursively download subdirectory
|
|
133
|
+
await fs.ensureDir(itemPath);
|
|
134
|
+
const subDirResponse = await axios.get(item.url, {
|
|
135
|
+
headers: {
|
|
136
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
137
|
+
'User-Agent': 'cli-ai-skills-installer'
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
await this.downloadDirectory(subDirResponse.data, itemPath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear download cache
|
|
147
|
+
*/
|
|
148
|
+
async clearCache() {
|
|
149
|
+
await fs.remove(this.cacheDir);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get cache directory path
|
|
154
|
+
* @returns {string} Cache directory path
|
|
155
|
+
*/
|
|
156
|
+
getCacheDir() {
|
|
157
|
+
return this.cacheDir;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = SkillDownloader;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
|
|
6
|
+
class SkillInstaller {
|
|
7
|
+
constructor(detector, downloader, versionChecker) {
|
|
8
|
+
this.detector = detector;
|
|
9
|
+
this.downloader = downloader;
|
|
10
|
+
this.versionChecker = versionChecker;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Install a skill
|
|
15
|
+
* @param {string} skillName - Name of the skill
|
|
16
|
+
* @param {Object} options - Installation options
|
|
17
|
+
* @returns {Object} Installation result
|
|
18
|
+
*/
|
|
19
|
+
async install(skillName, options = {}) {
|
|
20
|
+
const {
|
|
21
|
+
platforms = ['copilot', 'claude'],
|
|
22
|
+
scope = 'global',
|
|
23
|
+
method = 'symlink',
|
|
24
|
+
force = false
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
const results = {
|
|
28
|
+
skillName,
|
|
29
|
+
success: false,
|
|
30
|
+
platforms: {},
|
|
31
|
+
errors: []
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Get platform paths
|
|
36
|
+
const platformInfo = await this.detector.detectAll();
|
|
37
|
+
const installPaths = this.detector.getInstallPaths(scope, platformInfo);
|
|
38
|
+
|
|
39
|
+
// Ensure directories exist
|
|
40
|
+
await this.detector.ensureDirectories(installPaths);
|
|
41
|
+
|
|
42
|
+
// Install for each selected platform
|
|
43
|
+
for (const platform of platforms) {
|
|
44
|
+
if (!installPaths[platform]) {
|
|
45
|
+
results.errors.push(`${platform} not available`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Download skill for this platform
|
|
51
|
+
const sourcePath = await this.downloader.downloadSkill(skillName, platform);
|
|
52
|
+
const targetPath = path.join(installPaths[platform], skillName);
|
|
53
|
+
|
|
54
|
+
// Check if already exists
|
|
55
|
+
const exists = await fs.pathExists(targetPath);
|
|
56
|
+
if (exists && !force) {
|
|
57
|
+
// Get version info
|
|
58
|
+
const sourceVersion = await this.versionChecker.extractVersionFromSkill(sourcePath);
|
|
59
|
+
const targetVersion = await this.versionChecker.extractVersionFromSkill(targetPath);
|
|
60
|
+
|
|
61
|
+
results.platforms[platform] = {
|
|
62
|
+
status: 'exists',
|
|
63
|
+
currentVersion: targetVersion,
|
|
64
|
+
availableVersion: sourceVersion,
|
|
65
|
+
skipped: true
|
|
66
|
+
};
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Remove existing if force
|
|
71
|
+
if (exists) {
|
|
72
|
+
await fs.remove(targetPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Install based on method
|
|
76
|
+
if (method === 'symlink' && scope === 'global') {
|
|
77
|
+
// Create symlink (only for global installations)
|
|
78
|
+
await fs.symlink(sourcePath, targetPath, 'dir');
|
|
79
|
+
results.platforms[platform] = {
|
|
80
|
+
status: 'installed',
|
|
81
|
+
method: 'symlink',
|
|
82
|
+
path: targetPath
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
// Copy files (for local or when symlink not desired)
|
|
86
|
+
await fs.copy(sourcePath, targetPath);
|
|
87
|
+
results.platforms[platform] = {
|
|
88
|
+
status: 'installed',
|
|
89
|
+
method: 'copy',
|
|
90
|
+
path: targetPath
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
results.errors.push(`${platform}: ${error.message}`);
|
|
96
|
+
results.platforms[platform] = {
|
|
97
|
+
status: 'failed',
|
|
98
|
+
error: error.message
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
results.success = Object.values(results.platforms).some(p => p.status === 'installed');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
results.errors.push(error.message);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Uninstall a skill
|
|
113
|
+
* @param {string} skillName - Name of the skill
|
|
114
|
+
* @param {Object} options - Uninstall options
|
|
115
|
+
*/
|
|
116
|
+
async uninstall(skillName, options = {}) {
|
|
117
|
+
const { platforms = ['copilot', 'claude'], scope = 'global' } = options;
|
|
118
|
+
|
|
119
|
+
const platformInfo = await this.detector.detectAll();
|
|
120
|
+
const installPaths = this.detector.getInstallPaths(scope, platformInfo);
|
|
121
|
+
|
|
122
|
+
const results = { removed: [], failed: [] };
|
|
123
|
+
|
|
124
|
+
for (const platform of platforms) {
|
|
125
|
+
if (!installPaths[platform]) continue;
|
|
126
|
+
|
|
127
|
+
const skillPath = path.join(installPaths[platform], skillName);
|
|
128
|
+
|
|
129
|
+
if (await fs.pathExists(skillPath)) {
|
|
130
|
+
try {
|
|
131
|
+
await fs.remove(skillPath);
|
|
132
|
+
results.removed.push(platform);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
results.failed.push({ platform, error: error.message });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get version from downloaded skill
|
|
144
|
+
*/
|
|
145
|
+
async getDownloadedVersion(skillPath) {
|
|
146
|
+
return await this.versionChecker.extractVersionFromSkill(skillPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = SkillInstaller;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const semver = require('semver');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
|
|
6
|
+
class VersionChecker {
|
|
7
|
+
/**
|
|
8
|
+
* Extract version from SKILL.md frontmatter
|
|
9
|
+
* @param {string} skillPath - Path to skill directory (containing SKILL.md)
|
|
10
|
+
* @returns {string|null} Version string or null
|
|
11
|
+
*/
|
|
12
|
+
async extractVersionFromSkill(skillPath) {
|
|
13
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
14
|
+
|
|
15
|
+
if (!await fs.pathExists(skillFile)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(skillFile, 'utf8');
|
|
21
|
+
|
|
22
|
+
// Extract YAML frontmatter
|
|
23
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
24
|
+
if (!frontmatterMatch) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const frontmatter = yaml.load(frontmatterMatch[1]);
|
|
29
|
+
return frontmatter.version || null;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compare two versions
|
|
37
|
+
* @param {string} current - Current version
|
|
38
|
+
* @param {string} latest - Latest version
|
|
39
|
+
* @returns {string} 'outdated', 'latest', 'newer', or 'unknown'
|
|
40
|
+
*/
|
|
41
|
+
compareVersions(current, latest) {
|
|
42
|
+
if (!semver.valid(current) || !semver.valid(latest)) {
|
|
43
|
+
return 'unknown';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (semver.gt(latest, current)) {
|
|
47
|
+
return 'outdated'; // Update available
|
|
48
|
+
} else if (semver.eq(current, latest)) {
|
|
49
|
+
return 'latest';
|
|
50
|
+
} else {
|
|
51
|
+
return 'newer'; // Installed version is newer (beta/dev)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if skill needs update
|
|
57
|
+
* @param {string} installedPath - Path to installed skill
|
|
58
|
+
* @param {string} latestVersion - Latest available version
|
|
59
|
+
* @returns {Object} Update status
|
|
60
|
+
*/
|
|
61
|
+
async checkUpdate(installedPath, latestVersion) {
|
|
62
|
+
const currentVersion = await this.extractVersionFromSkill(installedPath);
|
|
63
|
+
|
|
64
|
+
if (!currentVersion) {
|
|
65
|
+
return {
|
|
66
|
+
installed: false,
|
|
67
|
+
needsUpdate: false,
|
|
68
|
+
currentVersion: null,
|
|
69
|
+
latestVersion
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const status = this.compareVersions(currentVersion, latestVersion);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
installed: true,
|
|
77
|
+
needsUpdate: status === 'outdated',
|
|
78
|
+
currentVersion,
|
|
79
|
+
latestVersion,
|
|
80
|
+
status
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get installed version for a skill
|
|
86
|
+
* @param {string} skillName - Name of the skill
|
|
87
|
+
* @param {string} platform - Platform (copilot or claude)
|
|
88
|
+
* @returns {string|null} Version string or null
|
|
89
|
+
*/
|
|
90
|
+
async getInstalledVersion(skillName, platform) {
|
|
91
|
+
const os = require('os');
|
|
92
|
+
const homeDir = os.homedir();
|
|
93
|
+
|
|
94
|
+
let skillPath;
|
|
95
|
+
if (platform === 'copilot') {
|
|
96
|
+
skillPath = path.join(homeDir, '.copilot', 'skills', skillName);
|
|
97
|
+
} else if (platform === 'claude') {
|
|
98
|
+
skillPath = path.join(homeDir, '.claude', 'skills', skillName);
|
|
99
|
+
} else {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return await this.extractVersionFromSkill(skillPath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check version status for a skill
|
|
108
|
+
* @param {string} skillName - Name of the skill
|
|
109
|
+
* @param {string} platform - Platform (copilot or claude)
|
|
110
|
+
* @param {string} latestVersion - Latest version (optional, will fetch if not provided)
|
|
111
|
+
* @returns {string} Status: 'outdated', 'latest', 'newer', 'not_installed', 'unknown'
|
|
112
|
+
*/
|
|
113
|
+
async checkVersion(skillName, platform, latestVersion = null) {
|
|
114
|
+
const installedVersion = await this.getInstalledVersion(skillName, platform);
|
|
115
|
+
|
|
116
|
+
if (!installedVersion) {
|
|
117
|
+
return 'not_installed';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!latestVersion) {
|
|
121
|
+
// If latest version not provided, we can't compare
|
|
122
|
+
return 'unknown';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.compareVersions(installedVersion, latestVersion);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = VersionChecker;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visual progress gauge for multi-step operations
|
|
5
|
+
* Creates a progress bar like: [████████████░░░░░░] 60% - Step 3/5: Installing skills
|
|
6
|
+
*/
|
|
7
|
+
class ProgressGauge {
|
|
8
|
+
constructor(totalSteps) {
|
|
9
|
+
this.totalSteps = totalSteps;
|
|
10
|
+
this.currentStep = 0;
|
|
11
|
+
this.barLength = 20; // Total characters in the progress bar
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update progress to next step
|
|
16
|
+
* @param {string} stepName - Description of current step
|
|
17
|
+
*/
|
|
18
|
+
next(stepName) {
|
|
19
|
+
this.currentStep++;
|
|
20
|
+
this.render(stepName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set progress to specific step
|
|
25
|
+
* @param {number} step - Step number (1-indexed)
|
|
26
|
+
* @param {string} stepName - Description of current step
|
|
27
|
+
*/
|
|
28
|
+
setStep(step, stepName) {
|
|
29
|
+
this.currentStep = step;
|
|
30
|
+
this.render(stepName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render the progress gauge
|
|
35
|
+
* @param {string} stepName - Description of current step
|
|
36
|
+
*/
|
|
37
|
+
render(stepName = '') {
|
|
38
|
+
const percentage = Math.round((this.currentStep / this.totalSteps) * 100);
|
|
39
|
+
const filledLength = Math.round((this.currentStep / this.totalSteps) * this.barLength);
|
|
40
|
+
const emptyLength = this.barLength - filledLength;
|
|
41
|
+
|
|
42
|
+
// Create the visual bar
|
|
43
|
+
const filledBar = '█'.repeat(filledLength);
|
|
44
|
+
const emptyBar = '░'.repeat(emptyLength);
|
|
45
|
+
const bar = chalk.cyan(filledBar) + chalk.dim(emptyBar);
|
|
46
|
+
|
|
47
|
+
// Format the output
|
|
48
|
+
const percentageStr = chalk.bold(`${percentage}%`);
|
|
49
|
+
const stepStr = chalk.dim(`Step ${this.currentStep}/${this.totalSteps}`);
|
|
50
|
+
const nameStr = stepName ? chalk.white(`: ${stepName}`) : '';
|
|
51
|
+
|
|
52
|
+
console.log(`[${bar}] ${percentageStr} - ${stepStr}${nameStr}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mark as complete
|
|
57
|
+
*/
|
|
58
|
+
complete(message = 'Complete!') {
|
|
59
|
+
this.currentStep = this.totalSteps;
|
|
60
|
+
const bar = chalk.green('█'.repeat(this.barLength));
|
|
61
|
+
console.log(`[${bar}] ${chalk.green.bold('100%')} - ${chalk.green(message)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a simple inline gauge (single line, no steps)
|
|
66
|
+
* @param {number} current - Current value
|
|
67
|
+
* @param {number} total - Total value
|
|
68
|
+
* @param {string} label - Label to display
|
|
69
|
+
* @returns {string} Formatted gauge string
|
|
70
|
+
*/
|
|
71
|
+
static inline(current, total, label = '') {
|
|
72
|
+
const percentage = Math.round((current / total) * 100);
|
|
73
|
+
const barLength = 15;
|
|
74
|
+
const filledLength = Math.round((current / total) * barLength);
|
|
75
|
+
const emptyLength = barLength - filledLength;
|
|
76
|
+
|
|
77
|
+
const filledBar = chalk.cyan('█'.repeat(filledLength));
|
|
78
|
+
const emptyBar = chalk.dim('░'.repeat(emptyLength));
|
|
79
|
+
|
|
80
|
+
const labelStr = label ? `${label} ` : '';
|
|
81
|
+
return `${labelStr}[${filledBar}${emptyBar}] ${chalk.bold(percentage + '%')}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a compact gauge for lists
|
|
86
|
+
* @param {number} current - Current value
|
|
87
|
+
* @param {number} total - Total value
|
|
88
|
+
* @returns {string} Compact gauge (just the bar)
|
|
89
|
+
*/
|
|
90
|
+
static compact(current, total) {
|
|
91
|
+
const percentage = Math.round((current / total) * 100);
|
|
92
|
+
const barLength = 10;
|
|
93
|
+
const filledLength = Math.round((current / total) * barLength);
|
|
94
|
+
const emptyLength = barLength - filledLength;
|
|
95
|
+
|
|
96
|
+
const filledBar = '█'.repeat(filledLength);
|
|
97
|
+
const emptyBar = '░'.repeat(emptyLength);
|
|
98
|
+
|
|
99
|
+
return `[${chalk.cyan(filledBar)}${chalk.dim(emptyBar)}]`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render a multi-operation progress summary
|
|
104
|
+
* @param {Object} stats - Statistics object { completed, failed, total }
|
|
105
|
+
*/
|
|
106
|
+
static summary(stats) {
|
|
107
|
+
const { completed = 0, failed = 0, total = 0 } = stats;
|
|
108
|
+
const skipped = total - completed - failed;
|
|
109
|
+
|
|
110
|
+
console.log(chalk.cyan('\n📊 Progress Summary:\n'));
|
|
111
|
+
|
|
112
|
+
if (completed > 0) {
|
|
113
|
+
const gauge = ProgressGauge.compact(completed, total);
|
|
114
|
+
console.log(` ${gauge} ${chalk.green(`✅ Completed: ${completed}`)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (failed > 0) {
|
|
118
|
+
const gauge = ProgressGauge.compact(failed, total);
|
|
119
|
+
console.log(` ${gauge} ${chalk.red(`❌ Failed: ${failed}`)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (skipped > 0) {
|
|
123
|
+
const gauge = ProgressGauge.compact(skipped, total);
|
|
124
|
+
console.log(` ${gauge} ${chalk.yellow(`⏭️ Skipped: ${skipped}`)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const overallGauge = ProgressGauge.compact(completed, total);
|
|
128
|
+
console.log(`\n ${overallGauge} ${chalk.bold(`Overall: ${completed}/${total} successful`)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = ProgressGauge;
|