@open-skills-hub/cli 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.
Files changed (57) hide show
  1. package/dist/commands/cache.d.ts +6 -0
  2. package/dist/commands/cache.d.ts.map +1 -0
  3. package/dist/commands/cache.js +145 -0
  4. package/dist/commands/cache.js.map +1 -0
  5. package/dist/commands/config.d.ts +6 -0
  6. package/dist/commands/config.d.ts.map +1 -0
  7. package/dist/commands/config.js +128 -0
  8. package/dist/commands/config.js.map +1 -0
  9. package/dist/commands/create.d.ts +7 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +449 -0
  12. package/dist/commands/create.js.map +1 -0
  13. package/dist/commands/feedback.d.ts +6 -0
  14. package/dist/commands/feedback.d.ts.map +1 -0
  15. package/dist/commands/feedback.js +137 -0
  16. package/dist/commands/feedback.js.map +1 -0
  17. package/dist/commands/get.d.ts +6 -0
  18. package/dist/commands/get.d.ts.map +1 -0
  19. package/dist/commands/get.js +122 -0
  20. package/dist/commands/get.js.map +1 -0
  21. package/dist/commands/index.d.ts +13 -0
  22. package/dist/commands/index.d.ts.map +1 -0
  23. package/dist/commands/index.js +13 -0
  24. package/dist/commands/index.js.map +1 -0
  25. package/dist/commands/publish.d.ts +7 -0
  26. package/dist/commands/publish.d.ts.map +1 -0
  27. package/dist/commands/publish.js +593 -0
  28. package/dist/commands/publish.js.map +1 -0
  29. package/dist/commands/scan.d.ts +6 -0
  30. package/dist/commands/scan.d.ts.map +1 -0
  31. package/dist/commands/scan.js +165 -0
  32. package/dist/commands/scan.js.map +1 -0
  33. package/dist/commands/search.d.ts +6 -0
  34. package/dist/commands/search.d.ts.map +1 -0
  35. package/dist/commands/search.js +80 -0
  36. package/dist/commands/search.js.map +1 -0
  37. package/dist/commands/validate.d.ts +7 -0
  38. package/dist/commands/validate.d.ts.map +1 -0
  39. package/dist/commands/validate.js +328 -0
  40. package/dist/commands/validate.js.map +1 -0
  41. package/dist/index.d.ts +6 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +107 -0
  44. package/dist/index.js.map +1 -0
  45. package/package.json +51 -0
  46. package/src/commands/cache.ts +166 -0
  47. package/src/commands/config.ts +142 -0
  48. package/src/commands/create.ts +490 -0
  49. package/src/commands/feedback.ts +161 -0
  50. package/src/commands/get.ts +141 -0
  51. package/src/commands/index.ts +13 -0
  52. package/src/commands/publish.ts +688 -0
  53. package/src/commands/scan.ts +190 -0
  54. package/src/commands/search.ts +92 -0
  55. package/src/commands/validate.ts +391 -0
  56. package/src/index.ts +118 -0
  57. package/tsconfig.json +13 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Open Skills Hub CLI - Create Command
3
+ * Creates a new skill from template
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import chalk from 'chalk';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import inquirer from 'inquirer';
11
+ import { isValidSkillName } from '@open-skills-hub/core';
12
+
13
+ type TemplateType = 'basic' | 'with-scripts' | 'advanced';
14
+
15
+ interface SkillOptions {
16
+ name: string;
17
+ description: string;
18
+ license?: string;
19
+ author?: string;
20
+ template: TemplateType;
21
+ derivedFrom?: string;
22
+ }
23
+
24
+ function generateSkillMd(options: SkillOptions): string {
25
+ const frontmatter: string[] = [
26
+ '---',
27
+ `name: ${options.name}`,
28
+ `description: ${options.description}`,
29
+ ];
30
+
31
+ if (options.license) {
32
+ frontmatter.push(`license: ${options.license}`);
33
+ }
34
+
35
+ if (options.derivedFrom) {
36
+ frontmatter.push(`derivedFrom:`);
37
+ frontmatter.push(` name: ${options.derivedFrom.split('@')[0]}`);
38
+ frontmatter.push(` version: ${options.derivedFrom.split('@')[1] || '1.0.0'}`);
39
+ }
40
+
41
+ frontmatter.push(`metadata:`);
42
+ if (options.author) {
43
+ frontmatter.push(` author: ${options.author}`);
44
+ }
45
+ frontmatter.push(` version: "1.0.0"`);
46
+ frontmatter.push('---');
47
+
48
+ const body = `
49
+ # ${toTitleCase(options.name)}
50
+
51
+ ${options.description}
52
+
53
+ ## Overview
54
+
55
+ This skill helps with [describe the main purpose].
56
+
57
+ ## When to Use
58
+
59
+ Use this skill when:
60
+ - [Condition 1]
61
+ - [Condition 2]
62
+ - [Condition 3]
63
+
64
+ ## Instructions
65
+
66
+ ### Step 1: [First Step]
67
+
68
+ [Describe what to do]
69
+
70
+ ### Step 2: [Second Step]
71
+
72
+ [Describe what to do]
73
+
74
+ ## Examples
75
+
76
+ ### Example 1
77
+
78
+ **Input:**
79
+ \`\`\`
80
+ [Example input]
81
+ \`\`\`
82
+
83
+ **Output:**
84
+ \`\`\`
85
+ [Example output]
86
+ \`\`\`
87
+
88
+ ## Notes
89
+
90
+ - [Important note 1]
91
+ - [Important note 2]
92
+ `;
93
+
94
+ return frontmatter.join('\n') + '\n' + body;
95
+ }
96
+
97
+ function generateReadme(options: SkillOptions): string {
98
+ return `# ${toTitleCase(options.name)}
99
+
100
+ ${options.description}
101
+
102
+ ## Installation
103
+
104
+ \`\`\`bash
105
+ skills use ${options.name}
106
+ \`\`\`
107
+
108
+ ## Usage
109
+
110
+ This skill can be activated when [describe activation conditions].
111
+
112
+ ## Structure
113
+
114
+ \`\`\`
115
+ ${options.name}/
116
+ ├── SKILL.md # Skill definition
117
+ ${options.template !== 'basic' ? `├── scripts/ # Executable scripts
118
+ ` : ''}${options.template === 'advanced' ? `├── references/ # Reference documentation
119
+ ├── assets/ # Static resources
120
+ ` : ''}└── README.md # This file
121
+ \`\`\`
122
+
123
+ ## License
124
+
125
+ ${options.license || 'MIT'}
126
+ `;
127
+ }
128
+
129
+ function generateLicense(license: string, author: string): string {
130
+ const year = new Date().getFullYear();
131
+
132
+ if (license === 'MIT') {
133
+ return `MIT License
134
+
135
+ Copyright (c) ${year} ${author}
136
+
137
+ Permission is hereby granted, free of charge, to any person obtaining a copy
138
+ of this software and associated documentation files (the "Software"), to deal
139
+ in the Software without restriction, including without limitation the rights
140
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
141
+ copies of the Software, and to permit persons to whom the Software is
142
+ furnished to do so, subject to the following conditions:
143
+
144
+ The above copyright notice and this permission notice shall be included in all
145
+ copies or substantial portions of the Software.
146
+
147
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
148
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
149
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
150
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
151
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
152
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
153
+ SOFTWARE.
154
+ `;
155
+ }
156
+
157
+ if (license === 'Apache-2.0') {
158
+ return `Apache License
159
+ Version 2.0, January 2004
160
+ http://www.apache.org/licenses/
161
+
162
+ Copyright ${year} ${author}
163
+
164
+ Licensed under the Apache License, Version 2.0 (the "License");
165
+ you may not use this file except in compliance with the License.
166
+ You may obtain a copy of the License at
167
+
168
+ http://www.apache.org/licenses/LICENSE-2.0
169
+
170
+ Unless required by applicable law or agreed to in writing, software
171
+ distributed under the License is distributed on an "AS IS" BASIS,
172
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
173
+ See the License for the specific language governing permissions and
174
+ limitations under the License.
175
+ `;
176
+ }
177
+
178
+ return `Copyright (c) ${year} ${author}
179
+
180
+ ${license}
181
+ `;
182
+ }
183
+
184
+ function generateSampleScript(): string {
185
+ return `#!/usr/bin/env python3
186
+ """
187
+ Sample Script for ${'{skill_name}'}
188
+
189
+ Usage:
190
+ python main.py <input> [options]
191
+
192
+ Description:
193
+ This script demonstrates the basic structure for skill scripts.
194
+ """
195
+
196
+ import sys
197
+ import argparse
198
+
199
+
200
+ def main():
201
+ parser = argparse.ArgumentParser(description='Sample skill script')
202
+ parser.add_argument('input', help='Input file or data')
203
+ parser.add_argument('--output', '-o', help='Output file (default: stdout)')
204
+ parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
205
+
206
+ args = parser.parse_args()
207
+
208
+ try:
209
+ # TODO: Implement your logic here
210
+ result = process(args.input, verbose=args.verbose)
211
+
212
+ if args.output:
213
+ with open(args.output, 'w') as f:
214
+ f.write(result)
215
+ else:
216
+ print(result)
217
+
218
+ except Exception as e:
219
+ print(f"Error: {e}", file=sys.stderr)
220
+ sys.exit(1)
221
+
222
+
223
+ def process(input_data: str, verbose: bool = False) -> str:
224
+ """
225
+ Process the input data.
226
+
227
+ Args:
228
+ input_data: The input to process
229
+ verbose: Whether to print verbose output
230
+
231
+ Returns:
232
+ Processed result as string
233
+ """
234
+ if verbose:
235
+ print(f"Processing: {input_data}", file=sys.stderr)
236
+
237
+ # TODO: Implement actual processing logic
238
+ return f"Processed: {input_data}"
239
+
240
+
241
+ if __name__ == '__main__':
242
+ main()
243
+ `;
244
+ }
245
+
246
+ function generateReference(): string {
247
+ return `# Reference Documentation
248
+
249
+ ## API Reference
250
+
251
+ ### Functions
252
+
253
+ #### \`process(input, options)\`
254
+
255
+ Processes the input data according to the specified options.
256
+
257
+ **Parameters:**
258
+ - \`input\` (string): The input data to process
259
+ - \`options\` (object): Processing options
260
+ - \`verbose\` (boolean): Enable verbose output
261
+
262
+ **Returns:**
263
+ - \`string\`: The processed result
264
+
265
+ **Example:**
266
+ \`\`\`python
267
+ result = process("hello", {"verbose": True})
268
+ \`\`\`
269
+
270
+ ## Configuration
271
+
272
+ ### Environment Variables
273
+
274
+ | Variable | Description | Default |
275
+ |----------|-------------|---------|
276
+ | \`DEBUG\` | Enable debug mode | \`false\` |
277
+
278
+ ## Troubleshooting
279
+
280
+ ### Common Issues
281
+
282
+ #### Issue 1: [Description]
283
+
284
+ **Solution:** [How to fix]
285
+
286
+ #### Issue 2: [Description]
287
+
288
+ **Solution:** [How to fix]
289
+ `;
290
+ }
291
+
292
+ function toTitleCase(str: string): string {
293
+ return str
294
+ .split('-')
295
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
296
+ .join(' ');
297
+ }
298
+
299
+ function validateSkillName(name: string): string | true {
300
+ if (!name) return 'Name is required';
301
+ if (name.length > 64) return 'Name must be 64 characters or less';
302
+ if (!isValidSkillName(name)) {
303
+ return 'Name must be lowercase letters, numbers, and hyphens only';
304
+ }
305
+ if (name.startsWith('-') || name.endsWith('-')) {
306
+ return 'Name cannot start or end with a hyphen';
307
+ }
308
+ if (name.includes('--')) {
309
+ return 'Name cannot contain consecutive hyphens';
310
+ }
311
+ return true;
312
+ }
313
+
314
+ export const createCommand = new Command('create')
315
+ .description('Create a new skill from template')
316
+ .argument('[name]', 'Skill name (lowercase, hyphens allowed)')
317
+ .option('-t, --template <template>', 'Template type: basic, with-scripts, advanced', 'basic')
318
+ .option('-d, --description <description>', 'Skill description')
319
+ .option('-l, --license <license>', 'License (MIT, Apache-2.0, etc.)')
320
+ .option('-a, --author <author>', 'Author name')
321
+ .option('--from <skill>', 'Derive from existing skill (name@version)')
322
+ .option('-y, --yes', 'Skip prompts and use defaults')
323
+ .option('-o, --output <path>', 'Output directory (default: current directory)')
324
+ .action(async (nameArg, options) => {
325
+ let skillOptions: SkillOptions;
326
+
327
+ if (options.yes && nameArg) {
328
+ // Non-interactive mode
329
+ const validation = validateSkillName(nameArg);
330
+ if (validation !== true) {
331
+ console.log(chalk.red(`Error: ${validation}`));
332
+ process.exit(1);
333
+ }
334
+
335
+ skillOptions = {
336
+ name: nameArg,
337
+ description: options.description || `A skill for ${nameArg}`,
338
+ license: options.license || 'MIT',
339
+ author: options.author || process.env.USER || 'anonymous',
340
+ template: options.template as TemplateType,
341
+ derivedFrom: options.from,
342
+ };
343
+ } else {
344
+ // Interactive mode
345
+ console.log(chalk.cyan('\n📦 Create New Skill\n'));
346
+
347
+ const answers = await inquirer.prompt([
348
+ {
349
+ type: 'input',
350
+ name: 'name',
351
+ message: 'Skill name:',
352
+ default: nameArg,
353
+ validate: validateSkillName,
354
+ },
355
+ {
356
+ type: 'input',
357
+ name: 'description',
358
+ message: 'Description:',
359
+ default: options.description,
360
+ validate: (input: string) => {
361
+ if (!input) return 'Description is required';
362
+ if (input.length > 1024) return 'Description must be 1024 characters or less';
363
+ return true;
364
+ },
365
+ },
366
+ {
367
+ type: 'list',
368
+ name: 'template',
369
+ message: 'Template:',
370
+ default: options.template,
371
+ choices: [
372
+ { name: 'Basic (SKILL.md only)', value: 'basic' },
373
+ { name: 'With Scripts (includes scripts/)', value: 'with-scripts' },
374
+ { name: 'Advanced (full structure)', value: 'advanced' },
375
+ ],
376
+ },
377
+ {
378
+ type: 'input',
379
+ name: 'license',
380
+ message: 'License:',
381
+ default: options.license || 'MIT',
382
+ },
383
+ {
384
+ type: 'input',
385
+ name: 'author',
386
+ message: 'Author:',
387
+ default: options.author || process.env.USER || '',
388
+ },
389
+ {
390
+ type: 'input',
391
+ name: 'derivedFrom',
392
+ message: 'Derived from (leave empty for none):',
393
+ default: options.from || '',
394
+ },
395
+ ]);
396
+
397
+ skillOptions = {
398
+ name: answers.name,
399
+ description: answers.description,
400
+ license: answers.license,
401
+ author: answers.author,
402
+ template: answers.template,
403
+ derivedFrom: answers.derivedFrom || undefined,
404
+ };
405
+ }
406
+
407
+ // Determine output directory
408
+ const outputBase = options.output || process.cwd();
409
+ const skillDir = path.join(outputBase, skillOptions.name);
410
+
411
+ // Check if directory exists
412
+ if (fs.existsSync(skillDir)) {
413
+ console.log(chalk.red(`\nError: Directory already exists: ${skillDir}`));
414
+ process.exit(1);
415
+ }
416
+
417
+ // Create directory structure
418
+ console.log(chalk.cyan(`\nCreating skill in ${skillDir}...\n`));
419
+
420
+ fs.mkdirSync(skillDir, { recursive: true });
421
+
422
+ // Create SKILL.md
423
+ const skillMdContent = generateSkillMd(skillOptions);
424
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMdContent);
425
+ console.log(chalk.green(' ✓ SKILL.md'));
426
+
427
+ // Create README.md
428
+ const readmeContent = generateReadme(skillOptions);
429
+ fs.writeFileSync(path.join(skillDir, 'README.md'), readmeContent);
430
+ console.log(chalk.green(' ✓ README.md'));
431
+
432
+ // Create LICENSE.txt
433
+ if (skillOptions.license) {
434
+ const licenseContent = generateLicense(skillOptions.license, skillOptions.author || 'Anonymous');
435
+ fs.writeFileSync(path.join(skillDir, 'LICENSE.txt'), licenseContent);
436
+ console.log(chalk.green(' ✓ LICENSE.txt'));
437
+ }
438
+
439
+ // Create template-specific directories
440
+ if (skillOptions.template === 'with-scripts' || skillOptions.template === 'advanced') {
441
+ const scriptsDir = path.join(skillDir, 'scripts');
442
+ fs.mkdirSync(scriptsDir, { recursive: true });
443
+
444
+ const sampleScript = generateSampleScript().replace('{skill_name}', skillOptions.name);
445
+ fs.writeFileSync(path.join(scriptsDir, 'main.py'), sampleScript);
446
+ console.log(chalk.green(' ✓ scripts/main.py'));
447
+ }
448
+
449
+ if (skillOptions.template === 'advanced') {
450
+ // Create references directory
451
+ const refsDir = path.join(skillDir, 'references');
452
+ fs.mkdirSync(refsDir, { recursive: true });
453
+
454
+ const referenceContent = generateReference();
455
+ fs.writeFileSync(path.join(refsDir, 'REFERENCE.md'), referenceContent);
456
+ console.log(chalk.green(' ✓ references/REFERENCE.md'));
457
+
458
+ // Create assets directory
459
+ const assetsDir = path.join(skillDir, 'assets');
460
+ fs.mkdirSync(assetsDir, { recursive: true });
461
+ fs.writeFileSync(path.join(assetsDir, '.gitkeep'), '');
462
+ console.log(chalk.green(' ✓ assets/'));
463
+ }
464
+
465
+ // Print success message
466
+ console.log(chalk.green('\n✓ Skill created successfully!\n'));
467
+
468
+ console.log(' ' + chalk.cyan(skillOptions.name) + '/');
469
+ console.log(' ├── SKILL.md');
470
+ console.log(' ├── README.md');
471
+ if (skillOptions.license) {
472
+ console.log(' ├── LICENSE.txt');
473
+ }
474
+ if (skillOptions.template === 'with-scripts' || skillOptions.template === 'advanced') {
475
+ console.log(' ├── scripts/');
476
+ console.log(' │ └── main.py');
477
+ }
478
+ if (skillOptions.template === 'advanced') {
479
+ console.log(' ├── references/');
480
+ console.log(' │ └── REFERENCE.md');
481
+ console.log(' └── assets/');
482
+ }
483
+
484
+ console.log(chalk.cyan('\nNext steps:'));
485
+ console.log(` cd ${skillOptions.name}`);
486
+ console.log(' # Edit SKILL.md with your skill content');
487
+ console.log(' skills validate .');
488
+ console.log(' skills publish .');
489
+ console.log('');
490
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Open Skills Hub CLI - Feedback Command
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import inquirer from 'inquirer';
9
+ import {
10
+ getStorage,
11
+ getConfig,
12
+ generateUUID,
13
+ now,
14
+ parseSkillFullName,
15
+ buildSkillFullName,
16
+ } from '@open-skills-hub/core';
17
+ import type { Feedback, FeedbackQueueItem } from '@open-skills-hub/core';
18
+
19
+ export const feedbackCommand = new Command('feedback')
20
+ .description('Submit feedback for a skill')
21
+ .argument('<name>', 'Skill name')
22
+ .option('-v, --version <version>', 'Skill version')
23
+ .option('-r, --rating <rating>', 'Rating (1-5)')
24
+ .option('-t, --type <type>', 'Feedback type: success, failure, suggestion, bug', 'success')
25
+ .option('-c, --comment <comment>', 'Feedback comment')
26
+ .option('-i, --interactive', 'Interactive mode')
27
+ .action(async (name, options) => {
28
+ const spinner = ora('Loading skill info...').start();
29
+
30
+ try {
31
+ const storage = await getStorage();
32
+ await storage.initialize();
33
+
34
+ // Parse name
35
+ const { scope, name: skillName } = parseSkillFullName(name);
36
+ const fullName = buildSkillFullName(skillName, scope);
37
+
38
+ // Get skill
39
+ const skill = await storage.getSkillByName(fullName);
40
+ if (!skill) {
41
+ spinner.fail(`Skill '${fullName}' not found`);
42
+ process.exit(1);
43
+ }
44
+
45
+ // Get version
46
+ let version;
47
+ if (options.version) {
48
+ version = await storage.getVersion(skill.id, options.version);
49
+ if (!version) {
50
+ spinner.fail(`Version '${options.version}' not found`);
51
+ process.exit(1);
52
+ }
53
+ } else {
54
+ version = await storage.getLatestVersion(skill.id);
55
+ if (!version) {
56
+ spinner.fail('No versions found');
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ spinner.succeed(`Found ${fullName}@${version.version}`);
62
+
63
+ // Interactive mode
64
+ let rating = options.rating ? parseInt(options.rating, 10) : undefined;
65
+ let feedbackType = options.type;
66
+ let comment = options.comment;
67
+
68
+ if (options.interactive) {
69
+ const answers = await inquirer.prompt([
70
+ {
71
+ type: 'list',
72
+ name: 'type',
73
+ message: 'What type of feedback?',
74
+ choices: [
75
+ { name: '✓ Success - It worked well', value: 'success' },
76
+ { name: '✗ Failure - It didn\'t work', value: 'failure' },
77
+ { name: '💡 Suggestion - I have an idea', value: 'suggestion' },
78
+ { name: '🐛 Bug - Something is broken', value: 'bug' },
79
+ ],
80
+ default: feedbackType,
81
+ },
82
+ {
83
+ type: 'list',
84
+ name: 'rating',
85
+ message: 'How would you rate this skill?',
86
+ choices: [
87
+ { name: '⭐⭐⭐⭐⭐ (5) - Excellent', value: 5 },
88
+ { name: '⭐⭐⭐⭐ (4) - Good', value: 4 },
89
+ { name: '⭐⭐⭐ (3) - Average', value: 3 },
90
+ { name: '⭐⭐ (2) - Below average', value: 2 },
91
+ { name: '⭐ (1) - Poor', value: 1 },
92
+ ],
93
+ default: rating ?? 4,
94
+ },
95
+ {
96
+ type: 'input',
97
+ name: 'comment',
98
+ message: 'Any additional comments? (optional)',
99
+ default: comment,
100
+ },
101
+ ]);
102
+
103
+ feedbackType = answers.type;
104
+ rating = answers.rating;
105
+ comment = answers.comment || undefined;
106
+ }
107
+
108
+ // Validate rating
109
+ if (!rating || rating < 1 || rating > 5) {
110
+ console.log(chalk.red('Rating must be between 1 and 5'));
111
+ process.exit(1);
112
+ }
113
+
114
+ // Submit feedback
115
+ const submitSpinner = ora('Submitting feedback...').start();
116
+
117
+ const feedback: Feedback = {
118
+ id: generateUUID(),
119
+ skillId: skill.id,
120
+ skillVersion: version.version,
121
+ feedbackType: feedbackType as Feedback['feedbackType'],
122
+ rating,
123
+ comment,
124
+ context: {},
125
+ status: 'pending',
126
+ createdAt: now(),
127
+ };
128
+
129
+ await storage.createFeedback(feedback);
130
+
131
+ // Update skill rating
132
+ const feedbacks = await storage.getFeedbacks(skill.id, { limit: 1000 });
133
+ const ratings = feedbacks.items.map(f => f.rating).filter((r): r is number => r !== undefined);
134
+ const averageRating = ratings.length > 0
135
+ ? ratings.reduce((sum, r) => sum + r, 0) / ratings.length
136
+ : 0;
137
+
138
+ await storage.updateSkill(skill.id, {
139
+ rating: {
140
+ average: Math.round(averageRating * 10) / 10,
141
+ count: ratings.length,
142
+ },
143
+ });
144
+
145
+ submitSpinner.succeed('Feedback submitted');
146
+
147
+ console.log('\n' + chalk.gray('─'.repeat(50)));
148
+ console.log(chalk.bold.green('✓ Thank you for your feedback!\n'));
149
+ console.log(` ${chalk.cyan('Skill:')} ${fullName}@${version.version}`);
150
+ console.log(` ${chalk.cyan('Type:')} ${feedbackType}`);
151
+ console.log(` ${chalk.cyan('Rating:')} ${'⭐'.repeat(rating)} (${rating}/5)`);
152
+ if (comment) {
153
+ console.log(` ${chalk.cyan('Comment:')} ${comment}`);
154
+ }
155
+ console.log(chalk.gray('─'.repeat(50)) + '\n');
156
+
157
+ await storage.close();
158
+ } catch (error) {
159
+ throw error;
160
+ }
161
+ });