myskillshub 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "myskillshub",
3
+ "version": "1.0.1",
4
+ "description": "CLI tool for SkillHub skill management",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "myskillshub": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "lint": "eslint src/**/*.js",
12
+ "start": "node src/index.js"
13
+ },
14
+ "keywords": [
15
+ "cli",
16
+ "skill",
17
+ "skillshub"
18
+ ],
19
+ "author": "SkillHub Team",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "dependencies": {
25
+ "commander": "^11.1.0",
26
+ "axios": "^1.6.2",
27
+ "archiver": "^6.0.1",
28
+ "form-data": "^4.0.0",
29
+ "dotenv": "^16.3.1",
30
+ "chalk": "^4.1.2",
31
+ "ora": "^6.3.1"
32
+ },
33
+ "devDependencies": {
34
+ "jest": "^29.6.0",
35
+ "eslint": "^8.45.0"
36
+ }
37
+ }
@@ -0,0 +1,219 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const axios = require('axios');
4
+ const oraModule = require('ora');
5
+ const ora = oraModule.default || oraModule;
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+
9
+ const program = new Command();
10
+ const TARGET_DIRS = {
11
+ claude: '.claude/skills',
12
+ codex: '.agents/skills',
13
+ cursor: '.cursor/skills',
14
+ };
15
+
16
+ program
17
+ .name('info')
18
+ .description('Show remote and local version information for a skill')
19
+ .argument('<skill-slug>', 'Skill slug')
20
+ .option('--verbose', 'Verbose output')
21
+ .option('-a, --api-url <url>', 'SkillHub API URL', 'http://localhost:3000')
22
+ .option('-t, --target <target>', 'Check local target: claude|codex|cursor|all', 'all')
23
+ .action(async (skillSlug, options) => {
24
+ const spinner = ora('Fetching skill info...').start();
25
+
26
+ try {
27
+ const skill = await getSkillInfo(skillSlug, options.apiUrl);
28
+ const localInfo = await getLocalInfo(skillSlug, options.target);
29
+ spinner.succeed(chalk.green('Skill info loaded'));
30
+
31
+ displaySkillInfo(skill, localInfo);
32
+ } catch (error) {
33
+ spinner.fail(chalk.red(`Failed to get skill info: ${error.message}`));
34
+ if (options.verbose) {
35
+ console.error(error);
36
+ }
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ async function getSkillInfo(skillSlug, apiUrl) {
42
+ const response = await axios.get(`${apiUrl}/api/skills/${skillSlug}`);
43
+ if (!response.data?.success || !response.data?.data?.skill) {
44
+ throw new Error('Invalid skill info response');
45
+ }
46
+ return response.data.data.skill;
47
+ }
48
+
49
+ function resolveTargets(targetInput) {
50
+ const target = String(targetInput || 'all').trim().toLowerCase();
51
+ if (target === 'all') {
52
+ return Object.keys(TARGET_DIRS);
53
+ }
54
+ if (!TARGET_DIRS[target]) {
55
+ throw new Error(`Invalid target "${target}". Use one of: claude, codex, cursor, all`);
56
+ }
57
+ return [target];
58
+ }
59
+
60
+ async function getLocalInfo(skillSlug, targetInput) {
61
+ const targets = resolveTargets(targetInput);
62
+ const result = [];
63
+
64
+ for (const target of targets) {
65
+ const baseDir = path.join(process.cwd(), TARGET_DIRS[target], skillSlug);
66
+ const metadataPath = path.join(baseDir, '.myskillshub-install.json');
67
+ const canonicalSkillPath = path.join(baseDir, 'SKILL.md');
68
+
69
+ const metadata = await readInstallMetadata(metadataPath);
70
+ let resolvedSkillPath = canonicalSkillPath;
71
+ let resolvedVersion = metadata?.version || 'unknown';
72
+
73
+ if (!(await pathExists(resolvedSkillPath))) {
74
+ resolvedSkillPath = await findFirstSkillFile(baseDir);
75
+ }
76
+
77
+ if (resolvedSkillPath) {
78
+ const content = await fs.readFile(resolvedSkillPath, 'utf8');
79
+ const parsedVersion = extractVersionFromSkillContent(content);
80
+ if (!resolvedVersion || resolvedVersion === 'unknown') {
81
+ resolvedVersion = parsedVersion;
82
+ }
83
+
84
+ result.push({
85
+ target,
86
+ installed: true,
87
+ path: resolvedSkillPath,
88
+ version: resolvedVersion || parsedVersion || 'unknown',
89
+ });
90
+ continue;
91
+ }
92
+
93
+ result.push({
94
+ target,
95
+ installed: false,
96
+ path: baseDir,
97
+ version: '-',
98
+ });
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ async function readInstallMetadata(metadataPath) {
105
+ try {
106
+ const content = await fs.readFile(metadataPath, 'utf8');
107
+ const parsed = JSON.parse(content);
108
+ return typeof parsed === 'object' && parsed ? parsed : null;
109
+ } catch (error) {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ async function pathExists(targetPath) {
115
+ try {
116
+ await fs.access(targetPath);
117
+ return true;
118
+ } catch (error) {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ async function findFirstSkillFile(baseDir, depth = 0, maxDepth = 8) {
124
+ if (depth > maxDepth || !(await pathExists(baseDir))) {
125
+ return null;
126
+ }
127
+
128
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
129
+ const files = [];
130
+ const dirs = [];
131
+
132
+ for (const entry of entries) {
133
+ if (entry.name.startsWith('.')) {
134
+ continue;
135
+ }
136
+ if (entry.isFile() && entry.name.toUpperCase() === 'SKILL.MD') {
137
+ files.push(path.join(baseDir, entry.name));
138
+ continue;
139
+ }
140
+ if (entry.isDirectory()) {
141
+ dirs.push(path.join(baseDir, entry.name));
142
+ }
143
+ }
144
+
145
+ if (files.length > 0) {
146
+ files.sort((a, b) => a.length - b.length);
147
+ return files[0];
148
+ }
149
+
150
+ dirs.sort((a, b) => a.length - b.length);
151
+ for (const dirPath of dirs) {
152
+ const match = await findFirstSkillFile(dirPath, depth + 1, maxDepth);
153
+ if (match) {
154
+ return match;
155
+ }
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ function extractVersionFromSkillContent(content) {
162
+ if (!content.startsWith('---')) {
163
+ return 'unknown';
164
+ }
165
+ const end = content.indexOf('---', 3);
166
+ if (end < 0) {
167
+ return 'unknown';
168
+ }
169
+ const frontmatter = content.slice(3, end);
170
+ const lines = frontmatter.split('\n');
171
+ for (const rawLine of lines) {
172
+ const line = rawLine.trim();
173
+ const match = line.match(/^version\s*:\s*(.+)$/i);
174
+ if (!match) {
175
+ continue;
176
+ }
177
+ return String(match[1]).trim().replace(/^['"]|['"]$/g, '');
178
+ }
179
+ return 'unknown';
180
+ }
181
+
182
+ function displaySkillInfo(skill, localInfo) {
183
+ const versions = Array.isArray(skill.versions) ? skill.versions.map((item) => item.version).filter(Boolean) : [];
184
+ const latestVersion = skill.version || versions[0] || '-';
185
+ const installedItems = localInfo.filter((item) => item.installed);
186
+ const installedVersions = [...new Set(
187
+ installedItems
188
+ .map((item) => item.version)
189
+ .filter((version) => version && version !== '-' && version !== 'unknown')
190
+ )];
191
+ const installedSummary = installedVersions.length === 0
192
+ ? 'not installed'
193
+ : installedVersions.join(', ');
194
+ const hasUpdate = installedVersions.some((version) => version !== latestVersion);
195
+
196
+ console.log(chalk.bold('\n' + '='.repeat(60)));
197
+ console.log(chalk.bold.cyan('Skill Info'));
198
+ console.log(chalk.bold('='.repeat(60)));
199
+ console.log(chalk.white('Name:'), chalk.green(skill.name || '-'));
200
+ console.log(chalk.white('Slug:'), chalk.green(skill.slug || '-'));
201
+ console.log(chalk.white('Remote Latest:'), chalk.green(latestVersion));
202
+ console.log(chalk.white('Installed:'), installedVersions.length > 0 ? chalk.cyan(installedSummary) : chalk.gray(installedSummary));
203
+ if (installedVersions.length > 0) {
204
+ console.log(chalk.white('Update:'), hasUpdate ? chalk.yellow('available') : chalk.green('up-to-date'));
205
+ }
206
+ console.log(chalk.white('Versions:'), chalk.yellow(versions.length > 0 ? versions.join(', ') : latestVersion));
207
+
208
+ console.log(chalk.bold('\nLocal Install'));
209
+ localInfo.forEach((item) => {
210
+ if (!item.installed) {
211
+ console.log(chalk.gray(` [${item.target}] not installed (checked: ${item.path})`));
212
+ return;
213
+ }
214
+ console.log(chalk.green(` [${item.target}] ${item.version} (${item.path})`));
215
+ });
216
+ console.log(chalk.bold('='.repeat(60)));
217
+ }
218
+
219
+ module.exports = program;
@@ -0,0 +1,264 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const axios = require('axios');
4
+ const oraModule = require('ora');
5
+ const ora = oraModule.default || oraModule;
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+
9
+ const program = new Command();
10
+ const TARGET_DIRS = {
11
+ claude: '.claude/skills',
12
+ codex: '.agents/skills',
13
+ cursor: '.cursor/skills',
14
+ };
15
+ const TARGET_ALIASES = {
16
+ all: 'all',
17
+ claude: 'claude',
18
+ 'claude-code': 'claude',
19
+ codex: 'codex',
20
+ cursor: 'cursor',
21
+ };
22
+
23
+ program
24
+ .name('install')
25
+ .description('Install or view a skill in local tool directories')
26
+ .argument('<skill-ref>', 'Skill ref: <slug> or <slug>@<version>')
27
+ .option('--verbose', 'Verbose output')
28
+ .option('-a, --api-url <url>', 'SkillHub API URL', 'http://localhost:3000')
29
+ .option('-t, --target <target>', 'Install target: claude|codex|cursor|all', 'all')
30
+ .option('--view', 'View skill details without installing')
31
+ .action(async (skillRef, options) => {
32
+ const spinner = ora('Fetching skill information...').start();
33
+
34
+ try {
35
+ const parsed = parseSkillRef(skillRef);
36
+ const skillSlug = parsed.slug;
37
+ const installVersion = parsed.version || null;
38
+
39
+ // Get skill information
40
+ const skillInfo = await getSkillInfo(skillSlug, options.apiUrl);
41
+ spinner.succeed(chalk.green('Skill information fetched successfully'));
42
+
43
+ // Display skill details
44
+ displaySkillDetails(skillInfo, installVersion);
45
+
46
+ if (!options.view) {
47
+ spinner.start('Fetching skill package...');
48
+ const skillPackage = await getSkillPackage(skillSlug, options.apiUrl, installVersion);
49
+ const resolvedVersion = skillPackage.version || installVersion || skillInfo.version || '-';
50
+
51
+ spinner.text = `Installing skill to target: ${options.target}`;
52
+ const installedFiles = await installSkill(skillSlug, skillPackage.files, options.target, resolvedVersion);
53
+ spinner.succeed(chalk.green('Skill installed successfully!'));
54
+ console.log(chalk.cyan(`Installed skill: ${skillSlug}@${resolvedVersion}`));
55
+
56
+ console.log(chalk.bold('\nInstalled files:'));
57
+ installedFiles.forEach((item) => {
58
+ console.log(chalk.green(` [${item.target}] ${item.path}`));
59
+ });
60
+ }
61
+
62
+ } catch (error) {
63
+ spinner.fail(chalk.red(`Failed to process skill: ${error.message}`));
64
+ if (options.verbose) {
65
+ console.error(error);
66
+ }
67
+ process.exit(1);
68
+ }
69
+ });
70
+
71
+ function parseSkillRef(skillRef) {
72
+ const raw = String(skillRef || '').trim();
73
+ if (!raw) {
74
+ throw new Error('Skill ref is required');
75
+ }
76
+
77
+ const atIndex = raw.lastIndexOf('@');
78
+ if (atIndex <= 0) {
79
+ return { slug: raw, version: null };
80
+ }
81
+
82
+ const slug = raw.slice(0, atIndex).trim();
83
+ const version = raw.slice(atIndex + 1).trim();
84
+ if (!slug || !version) {
85
+ throw new Error(`Invalid skill ref "${skillRef}". Use <slug> or <slug>@<version>`);
86
+ }
87
+
88
+ return { slug, version };
89
+ }
90
+
91
+ async function getSkillInfo(skillSlug, apiUrl) {
92
+ const response = await axios.get(`${apiUrl}/api/skills/${skillSlug}`);
93
+ if (!response.data?.success || !response.data?.data?.skill) {
94
+ throw new Error('Invalid skill info response');
95
+ }
96
+ return response.data.data.skill;
97
+ }
98
+
99
+ async function getSkillPackage(skillSlug, apiUrl, skillVersion) {
100
+ const params = skillVersion ? { version: skillVersion } : {};
101
+
102
+ try {
103
+ const response = await axios.get(`${apiUrl}/api/skills/${skillSlug}/package`, { params });
104
+ if (!response.data?.success || !response.data?.data?.files) {
105
+ throw new Error('Invalid skill package response');
106
+ }
107
+ return response.data.data;
108
+ } catch (error) {
109
+ // 兼容旧服务端:回退到单文件安装
110
+ const response = await axios.get(`${apiUrl}/api/skills/${skillSlug}/content`, { params });
111
+ if (!response.data?.success || !response.data?.data?.content) {
112
+ throw error;
113
+ }
114
+ return {
115
+ files: [
116
+ {
117
+ relative_path: 'SKILL.md',
118
+ content_base64: Buffer.from(response.data.data.content, 'utf8').toString('base64')
119
+ }
120
+ ]
121
+ };
122
+ }
123
+ }
124
+
125
+ function resolveTargets(targetInput) {
126
+ const rawTarget = String(targetInput || 'all').trim().toLowerCase();
127
+ const target = TARGET_ALIASES[rawTarget];
128
+
129
+ if (!target) {
130
+ throw new Error(`Invalid target "${rawTarget}". Use one of: claude, codex, cursor, all`);
131
+ }
132
+
133
+ if (target === 'all') {
134
+ return Object.keys(TARGET_DIRS);
135
+ }
136
+
137
+ return [target];
138
+ }
139
+
140
+ function normalizeInstallPath(skillSlug, relativePath) {
141
+ const unified = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
142
+ let normalized = path.posix.normalize(unified);
143
+
144
+ if (
145
+ !normalized
146
+ || normalized === '.'
147
+ || normalized.startsWith('../')
148
+ || normalized.includes('/../')
149
+ ) {
150
+ throw new Error(`Invalid package file path: ${relativePath}`);
151
+ }
152
+
153
+ const lowerSlugPrefix = `${skillSlug.toLowerCase()}/`;
154
+ if (normalized.toLowerCase().startsWith(lowerSlugPrefix)) {
155
+ normalized = normalized.slice(skillSlug.length + 1);
156
+ }
157
+
158
+ if (!normalized || normalized === '.') {
159
+ throw new Error(`Invalid normalized package file path: ${relativePath}`);
160
+ }
161
+
162
+ return normalized;
163
+ }
164
+
165
+ async function installSkill(skillSlug, files, targetInput, resolvedVersion = 'unknown') {
166
+ if (skillSlug.includes('/') || skillSlug.includes('\\') || skillSlug.includes('..')) {
167
+ throw new Error('Invalid skill slug for local installation path');
168
+ }
169
+ if (!Array.isArray(files) || files.length === 0) {
170
+ throw new Error('No package files to install');
171
+ }
172
+
173
+ const targets = resolveTargets(targetInput);
174
+ const installed = [];
175
+
176
+ for (const target of targets) {
177
+ const outputDir = path.join(process.cwd(), TARGET_DIRS[target], skillSlug);
178
+ await fs.rm(outputDir, { recursive: true, force: true });
179
+ await fs.mkdir(outputDir, { recursive: true });
180
+ const writtenRelativePaths = [];
181
+
182
+ for (const file of files) {
183
+ const relativePath = normalizeInstallPath(
184
+ skillSlug,
185
+ file.relative_path || file.relativePath || file.path
186
+ );
187
+ const outputPath = path.join(outputDir, ...relativePath.split('/'));
188
+ const relativeToBase = path.relative(outputDir, outputPath);
189
+
190
+ if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
191
+ throw new Error(`Unsafe output path: ${outputPath}`);
192
+ }
193
+
194
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
195
+
196
+ const contentBuffer = file.content_base64
197
+ ? Buffer.from(file.content_base64, 'base64')
198
+ : Buffer.from(file.content || '', 'utf8');
199
+ await fs.writeFile(outputPath, contentBuffer);
200
+ writtenRelativePaths.push(relativePath);
201
+
202
+ installed.push({
203
+ target,
204
+ path: outputPath
205
+ });
206
+ }
207
+
208
+ const metadataPath = path.join(outputDir, '.myskillshub-install.json');
209
+ await fs.writeFile(
210
+ metadataPath,
211
+ JSON.stringify({
212
+ slug: skillSlug,
213
+ version: resolvedVersion,
214
+ installed_at: new Date().toISOString(),
215
+ files: writtenRelativePaths
216
+ }, null, 2),
217
+ 'utf8'
218
+ );
219
+ }
220
+
221
+ return installed;
222
+ }
223
+
224
+ function displaySkillDetails(skillInfo, skillVersion = null) {
225
+ console.log(chalk.bold('\n' + '='.repeat(50)));
226
+ console.log(chalk.bold.cyan('Skill Details'));
227
+ console.log(chalk.bold('='.repeat(50) + '\n'));
228
+
229
+ const categoryLabel = skillInfo.category?.name || 'Uncategorized';
230
+
231
+ console.log(chalk.white('Name:'), chalk.green(skillInfo.name || '-'));
232
+ console.log(chalk.white('Version:'), chalk.green(skillVersion || skillInfo.version || '-'));
233
+ console.log(chalk.white('Author:'), chalk.green(skillInfo.author || '-'));
234
+ console.log(chalk.white('Slug:'), chalk.green(skillInfo.slug || '-'));
235
+ console.log(chalk.white('Category:'), chalk.green(categoryLabel));
236
+
237
+ console.log('\n' + chalk.white('Description:'));
238
+ console.log(chalk.gray(' ' + (skillInfo.description || 'No description')));
239
+
240
+ console.log('\n' + chalk.white('Install Command:'));
241
+ console.log(chalk.blue(` myskillshub install ${skillInfo.slug} --target all`));
242
+
243
+ // Show rating
244
+ if (skillInfo.rating_avg !== undefined) {
245
+ const rating = Number(skillInfo.rating_avg).toFixed(1);
246
+ const stars = '★'.repeat(Math.floor(rating)) + '☆'.repeat(5 - Math.floor(rating));
247
+ console.log(chalk.white('Rating:'), chalk.yellow(`${stars} ${rating}/5`));
248
+ }
249
+
250
+ // Show install count
251
+ if (skillInfo.install_count !== undefined) {
252
+ console.log(chalk.white('Installs:'), chalk.green(String(skillInfo.install_count)));
253
+ }
254
+
255
+ // Show publish date
256
+ if (skillInfo.published_at || skillInfo.created_at) {
257
+ const date = new Date(skillInfo.published_at || skillInfo.created_at).toLocaleDateString();
258
+ console.log(chalk.white('Published:'), chalk.green(date));
259
+ }
260
+
261
+ console.log('\n' + chalk.bold('='.repeat(50)));
262
+ }
263
+
264
+ module.exports = program;
@@ -0,0 +1,158 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const axios = require('axios');
6
+ const oraModule = require('ora');
7
+ const ora = oraModule.default || oraModule;
8
+ const archiver = require('archiver');
9
+ const FormData = require('form-data');
10
+ const { createWriteStream } = require('fs');
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('publish')
16
+ .description('Publish a skill from local directory')
17
+ .argument('<directory>', 'Directory containing skill to publish')
18
+ .option('--verbose', 'Verbose output')
19
+ .option('-a, --api-url <url>', 'SkillHub API URL', 'http://localhost:3000')
20
+ .action(async (directory, options) => {
21
+ const spinner = ora('Starting publish process...').start();
22
+
23
+ try {
24
+ // Validate directory exists
25
+ if (!fs.existsSync(directory)) {
26
+ spinner.fail(chalk.red(`Directory not found: ${directory}`));
27
+ process.exit(1);
28
+ }
29
+
30
+ // Find skill.md file
31
+ const skillMdPath = path.join(directory, 'skill.md');
32
+ if (!fs.existsSync(skillMdPath)) {
33
+ spinner.fail(chalk.red('skill.md not found in the directory'));
34
+ process.exit(1);
35
+ }
36
+
37
+ spinner.text = 'Reading skill metadata...';
38
+ const skillMdContent = fs.readFileSync(skillMdPath, 'utf8');
39
+
40
+ // Parse skill.md content (basic parsing)
41
+ const skillData = parseSkillMd(skillMdContent);
42
+ spinner.succeed(chalk.green(`Found skill: ${skillData.name}`));
43
+
44
+ // Create ZIP package
45
+ spinner.text = 'Creating ZIP package...';
46
+ const zipPath = await createZipPackage(directory);
47
+ spinner.succeed(chalk.green(`ZIP package created: ${zipPath}`));
48
+
49
+ // Upload to SkillHub API
50
+ spinner.text = 'Uploading to SkillHub API...';
51
+ const result = await uploadSkill(zipPath, skillData, options.apiUrl);
52
+
53
+ spinner.succeed(chalk.green('Skill published successfully!'));
54
+ console.log(chalk.cyan(`Skill slug: ${result.slug}`));
55
+ console.log(chalk.cyan(`Skill URL: ${options.apiUrl}/skills/${result.slug}`));
56
+
57
+ } catch (error) {
58
+ spinner.fail(chalk.red(`Publish failed: ${error.message}`));
59
+ if (options.verbose) {
60
+ console.error(error);
61
+ }
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ function parseSkillMd(content) {
67
+ const lines = content.split('\n');
68
+ const skillData = {
69
+ name: '',
70
+ description: '',
71
+ version: '1.0.0',
72
+ author: '',
73
+ keywords: [],
74
+ installCommand: ''
75
+ };
76
+
77
+ let inDescription = false;
78
+
79
+ for (const line of lines) {
80
+ const trimmed = line.trim();
81
+ if (trimmed.startsWith('# ')) {
82
+ skillData.name = trimmed.substring(2);
83
+ } else if (trimmed.startsWith('## Description')) {
84
+ inDescription = true;
85
+ } else if (trimmed.startsWith('## ')) {
86
+ inDescription = false;
87
+ } else if (inDescription && trimmed) {
88
+ if (!skillData.description) {
89
+ skillData.description = trimmed;
90
+ } else {
91
+ skillData.description += ' ' + trimmed;
92
+ }
93
+ } else if (trimmed.startsWith('Version:')) {
94
+ skillData.version = trimmed.split(':')[1].trim();
95
+ } else if (trimmed.startsWith('Author:')) {
96
+ skillData.author = trimmed.split(':')[1].trim();
97
+ } else if (trimmed.startsWith('Keywords:')) {
98
+ skillData.keywords = trimmed.split(':')[1].split(',').map(k => k.trim());
99
+ } else if (trimmed.startsWith('Install:')) {
100
+ skillData.installCommand = trimmed.split(':')[1].trim();
101
+ }
102
+ }
103
+
104
+ return skillData;
105
+ }
106
+
107
+ function createZipPackage(directory) {
108
+ return new Promise((resolve, reject) => {
109
+ const zipPath = path.join(process.cwd(), `${path.basename(directory)}-package.zip`);
110
+ const output = createWriteStream(zipPath);
111
+ const archive = archiver('zip', {
112
+ zlib: { level: 9 }
113
+ });
114
+
115
+ output.on('close', () => {
116
+ resolve(zipPath);
117
+ });
118
+
119
+ output.on('error', (err) => {
120
+ reject(err);
121
+ });
122
+
123
+ archive.pipe(output);
124
+
125
+ // Add all files except node_modules and .git
126
+ archive.directory(directory, false, (data) => {
127
+ return data.stats.isDirectory() &&
128
+ !data.name.includes('node_modules') &&
129
+ !data.name.includes('.git');
130
+ });
131
+
132
+ archive.finalize();
133
+ });
134
+ }
135
+
136
+ async function uploadSkill(zipPath, skillData, apiUrl) {
137
+ const formData = new FormData();
138
+ formData.append('file', fs.createReadStream(zipPath));
139
+ formData.append('name', skillData.name);
140
+ formData.append('description', skillData.description);
141
+ formData.append('version', skillData.version);
142
+ formData.append('author', skillData.author);
143
+ formData.append('keywords', JSON.stringify(skillData.keywords));
144
+ formData.append('installCommand', skillData.installCommand);
145
+
146
+ const response = await axios.post(`${apiUrl}/api/skills/publish`, formData, {
147
+ headers: {
148
+ ...formData.getHeaders()
149
+ }
150
+ });
151
+
152
+ // Clean up ZIP file after successful upload
153
+ fs.unlinkSync(zipPath);
154
+
155
+ return response.data;
156
+ }
157
+
158
+ module.exports = program;
@@ -0,0 +1,116 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const axios = require('axios');
4
+ const oraModule = require('ora');
5
+ const ora = oraModule.default || oraModule;
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('search')
11
+ .description('Search for skills')
12
+ .argument('[query]', 'Search query')
13
+ .option('--verbose', 'Verbose output')
14
+ .option('-a, --api-url <url>', 'SkillHub API URL', 'http://localhost:3000')
15
+ .option('-c, --category <category>', 'Filter by category')
16
+ .option('-k, --keywords <keywords>', 'Filter by keywords (comma-separated)')
17
+ .option('-r, --rating <rating>', 'Minimum rating')
18
+ .option('-l, --limit <limit>', 'Maximum number of results', '10')
19
+ .action(async (query, options) => {
20
+ const spinner = ora('Searching skills...').start();
21
+
22
+ try {
23
+ const searchParams = {
24
+ query: query || '',
25
+ limit: parseInt(options.limit),
26
+ category: options.category || '',
27
+ keywords: options.keywords ? options.keywords.split(',').map(k => k.trim()) : [],
28
+ rating: options.rating ? parseFloat(options.rating) : null
29
+ };
30
+
31
+ const results = await searchSkills(searchParams, options.apiUrl);
32
+ spinner.succeed(chalk.green(`Found ${results.length} skills`));
33
+
34
+ if (results.length === 0) {
35
+ console.log(chalk.yellow('No skills found matching your criteria.'));
36
+ return;
37
+ }
38
+
39
+ displaySearchResults(results);
40
+
41
+ } catch (error) {
42
+ spinner.fail(chalk.red(`Search failed: ${error.message}`));
43
+ if (options.verbose) {
44
+ console.error(error);
45
+ }
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ async function searchSkills(params, apiUrl) {
51
+ const queryParams = new URLSearchParams({
52
+ search: params.query,
53
+ limit: params.limit.toString(),
54
+ category: params.category
55
+ });
56
+
57
+ if (params.keywords.length > 0) {
58
+ queryParams.append('keywords', params.keywords.join(','));
59
+ }
60
+
61
+ if (params.rating !== null) {
62
+ queryParams.append('rating', params.rating.toString());
63
+ }
64
+
65
+ const response = await axios.get(`${apiUrl}/api/skills?${queryParams}`);
66
+ return response.data?.data?.skills || [];
67
+ }
68
+
69
+ function displaySearchResults(skills) {
70
+ console.log(chalk.bold('\n' + '='.repeat(80)));
71
+ console.log(chalk.bold.cyan('Search Results'));
72
+ console.log(chalk.bold('='.repeat(80) + '\n'));
73
+
74
+ skills.forEach((skill, index) => {
75
+ const indexNumber = chalk.blue(`${(index + 1).toString().padStart(2)}.`);
76
+ const title = chalk.bold(skill.name);
77
+ const version = chalk.gray(`v${skill.version}`);
78
+ const author = chalk.yellow(`by ${skill.author}`);
79
+ const category = chalk.cyan(skill.category?.name || skill.category || 'Uncategorized');
80
+
81
+ console.log(`${indexNumber} ${title} ${version}`);
82
+ console.log(` ${author} • ${category}`);
83
+
84
+ // Show rating if available
85
+ if (skill.rating_avg !== undefined && Number(skill.rating_avg) > 0) {
86
+ const rating = Number(skill.rating_avg).toFixed(1);
87
+ const stars = '★'.repeat(Math.floor(rating)) + '☆'.repeat(5 - Math.floor(rating));
88
+ console.log(` Rating: ${chalk.yellow(stars)} ${rating}/5`);
89
+ }
90
+
91
+ // Show first few keywords
92
+ if (skill.tags && skill.tags.length > 0) {
93
+ const keywords = skill.tags.slice(0, 3).map(k => chalk.green(k)).join(', ');
94
+ const remaining = skill.tags.length > 3 ? ` +${skill.tags.length - 3} more` : '';
95
+ console.log(` Keywords: ${keywords}${remaining}`);
96
+ }
97
+
98
+ // Show description (truncated)
99
+ if (skill.description) {
100
+ const truncatedDesc = skill.description.length > 100
101
+ ? skill.description.substring(0, 100) + '...'
102
+ : skill.description;
103
+ console.log(` ${chalk.gray(truncatedDesc)}`);
104
+ }
105
+
106
+ console.log();
107
+ });
108
+
109
+ console.log(chalk.bold('='.repeat(80)));
110
+
111
+ // Show usage tips
112
+ console.log(chalk.yellow('\nPro tip: Use `myskillshub install <slug> --target all` to install a skill'));
113
+ console.log(chalk.yellow('Use `myskillshub info <slug>` to view versions and local install status\n'));
114
+ }
115
+
116
+ module.exports = program;
package/src/index.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('dotenv').config();
4
+ const { program } = require('commander');
5
+ const chalk = require('chalk');
6
+ const { version: cliVersion } = require('../package.json');
7
+ const publishCommand = require('./commands/publish');
8
+ const installCommand = require('./commands/install');
9
+ const searchCommand = require('./commands/search');
10
+ const infoCommand = require('./commands/info');
11
+
12
+ program
13
+ .name('myskillshub')
14
+ .description('CLI tool for SkillHub skill management')
15
+ .version(cliVersion, '-v, --version', 'Display CLI version');
16
+
17
+ // Register commands
18
+ program.addCommand(publishCommand);
19
+ program.addCommand(installCommand);
20
+ program.addCommand(searchCommand);
21
+ program.addCommand(infoCommand);
22
+
23
+ // Handle unknown commands
24
+ program.on('command:*', function (operands) {
25
+ console.error(chalk.red(`Unknown command: ${operands[0]}`));
26
+ console.log(chalk.yellow('Available commands:'));
27
+ program.commands.forEach(cmd => {
28
+ console.log(chalk.blue(` ${cmd.name()}`));
29
+ });
30
+ process.exit(1);
31
+ });
32
+
33
+ // Parse command line arguments
34
+ program.parse();