@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,190 @@
1
+ /**
2
+ * Open Skills Hub CLI - Scan Command
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import Table from 'cli-table3';
11
+ import {
12
+ getConfig,
13
+ initScanner,
14
+ getScanner,
15
+ } from '@open-skills-hub/core';
16
+
17
+ export const scanCommand = new Command('scan')
18
+ .description('Scan content for security issues')
19
+ .argument('<file>', 'Path to file to scan')
20
+ .option('--json', 'Output as JSON')
21
+ .option('-q, --quick', 'Quick check (high severity only)')
22
+ .option('--rules', 'Show available rules')
23
+ .action(async (file, options) => {
24
+ // Show rules if requested
25
+ if (options.rules) {
26
+ const config = getConfig();
27
+ initScanner({
28
+ enabled: true,
29
+ timeout: 30000,
30
+ maxFileSize: 10485760,
31
+ });
32
+ const scanner = getScanner();
33
+ const rules = scanner.getEnabledRules();
34
+
35
+ console.log(chalk.bold('\nAvailable Security Rules:\n'));
36
+
37
+ const table = new Table({
38
+ head: [
39
+ chalk.cyan('ID'),
40
+ chalk.cyan('Name'),
41
+ chalk.cyan('Category'),
42
+ chalk.cyan('Severity'),
43
+ ],
44
+ colWidths: [10, 30, 12, 10],
45
+ });
46
+
47
+ for (const rule of rules) {
48
+ const severityColor = {
49
+ high: chalk.red,
50
+ medium: chalk.yellow,
51
+ low: chalk.blue,
52
+ }[rule.severity] ?? chalk.white;
53
+
54
+ table.push([
55
+ rule.id,
56
+ rule.name,
57
+ rule.category,
58
+ severityColor(rule.severity),
59
+ ]);
60
+ }
61
+
62
+ console.log(table.toString());
63
+ console.log(chalk.gray(`\nTotal: ${rules.length} rules`));
64
+ return;
65
+ }
66
+
67
+ const spinner = ora('Scanning file...').start();
68
+
69
+ try {
70
+ // Read file
71
+ const filePath = path.resolve(file);
72
+ if (!fs.existsSync(filePath)) {
73
+ spinner.fail(`File not found: ${filePath}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ const content = fs.readFileSync(filePath, 'utf-8');
78
+
79
+ // Initialize scanner
80
+ const config = getConfig();
81
+ initScanner({
82
+ enabled: true,
83
+ timeout: config.get().scanner.timeout,
84
+ maxFileSize: config.get().scanner.maxFileSize,
85
+ });
86
+ const scanner = getScanner();
87
+
88
+ let result;
89
+ if (options.quick) {
90
+ // Quick check
91
+ const quickResult = await scanner.quickCheck(content);
92
+ spinner.stop();
93
+
94
+ if (quickResult.safe) {
95
+ console.log(chalk.green('\n✓ No high-severity issues found'));
96
+ } else {
97
+ console.log(chalk.red(`\n✗ Found ${quickResult.highSeverityCount} high-severity issue(s)`));
98
+ console.log(chalk.yellow(` Security level: ${quickResult.level}`));
99
+ }
100
+
101
+ process.exit(quickResult.safe ? 0 : 1);
102
+ }
103
+
104
+ // Full scan
105
+ result = await scanner.scan(content);
106
+ spinner.stop();
107
+
108
+ // JSON output
109
+ if (options.json) {
110
+ console.log(JSON.stringify({
111
+ score: result.summary.score,
112
+ level: result.summary.level,
113
+ issueCount: result.summary.issueCount,
114
+ issues: result.issues,
115
+ recommendations: result.summary.recommendations,
116
+ }, null, 2));
117
+ return;
118
+ }
119
+
120
+ // Display results
121
+ console.log('\n' + chalk.bold.blue('═'.repeat(60)));
122
+ console.log(chalk.bold.white(' Security Scan Results'));
123
+ console.log(chalk.bold.blue('═'.repeat(60)) + '\n');
124
+
125
+ // Score and level
126
+ const levelColor = {
127
+ safe: chalk.green,
128
+ low: chalk.blue,
129
+ medium: chalk.yellow,
130
+ high: chalk.red,
131
+ }[result.summary.level] ?? chalk.white;
132
+
133
+ console.log(`${chalk.bold('Score:')} ${result.summary.score}/100`);
134
+ console.log(`${chalk.bold('Level:')} ${levelColor(result.summary.level.toUpperCase())}`);
135
+ console.log(`${chalk.bold('File:')} ${filePath}`);
136
+ console.log();
137
+
138
+ // Issue counts
139
+ console.log(chalk.bold('Issues:'));
140
+ console.log(` ${chalk.red('●')} High: ${result.summary.issueCount.high}`);
141
+ console.log(` ${chalk.yellow('●')} Medium: ${result.summary.issueCount.medium}`);
142
+ console.log(` ${chalk.blue('●')} Low: ${result.summary.issueCount.low}`);
143
+ console.log();
144
+
145
+ // Issue details
146
+ if (result.issues.length > 0) {
147
+ console.log(chalk.bold('Issue Details:\n'));
148
+
149
+ for (const issue of result.issues) {
150
+ const severityColor = {
151
+ high: chalk.red,
152
+ medium: chalk.yellow,
153
+ low: chalk.blue,
154
+ }[issue.severity] ?? chalk.white;
155
+
156
+ console.log(severityColor(`[${issue.severity.toUpperCase()}]`) + ` ${chalk.bold(issue.ruleName)} (${issue.ruleId})`);
157
+ console.log(chalk.gray(` Line ${issue.line}: `) + issue.content);
158
+ console.log(chalk.gray(` ${issue.message}`));
159
+ if (issue.suggestion) {
160
+ console.log(chalk.cyan(` 💡 ${issue.suggestion}`));
161
+ }
162
+ console.log();
163
+ }
164
+ }
165
+
166
+ // Recommendations
167
+ if (result.summary.recommendations.length > 0) {
168
+ console.log(chalk.bold('Recommendations:\n'));
169
+ for (const rec of result.summary.recommendations) {
170
+ console.log(` • ${rec}`);
171
+ }
172
+ console.log();
173
+ }
174
+
175
+ // Summary
176
+ console.log(chalk.bold.blue('─'.repeat(60)));
177
+ if (result.summary.level === 'safe') {
178
+ console.log(chalk.green.bold('✓ Content passed security scan'));
179
+ } else if (result.summary.level === 'high') {
180
+ console.log(chalk.red.bold('✗ Content has high-risk security issues'));
181
+ process.exit(1);
182
+ } else {
183
+ console.log(chalk.yellow.bold('⚠ Content has some security concerns'));
184
+ }
185
+
186
+ } catch (error) {
187
+ spinner.fail('Scan failed');
188
+ throw error;
189
+ }
190
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Open Skills Hub CLI - Search Command
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import Table from 'cli-table3';
9
+ import { getStorage, getConfig } from '@open-skills-hub/core';
10
+
11
+ export const searchCommand = new Command('search')
12
+ .description('Search for skills')
13
+ .argument('[query]', 'Search query')
14
+ .option('-c, --category <category>', 'Filter by category')
15
+ .option('-a, --author <author>', 'Filter by author')
16
+ .option('-s, --sort <sort>', 'Sort by: relevance, uses, updated, created, name', 'relevance')
17
+ .option('-o, --order <order>', 'Sort order: asc, desc', 'desc')
18
+ .option('-l, --limit <limit>', 'Number of results', '20')
19
+ .option('--json', 'Output as JSON')
20
+ .action(async (query, options) => {
21
+ const spinner = ora('Searching skills...').start();
22
+
23
+ try {
24
+ const storage = await getStorage();
25
+ await storage.initialize();
26
+
27
+ const result = await storage.searchSkills({
28
+ query,
29
+ category: options.category,
30
+ author: options.author,
31
+ sort: options.sort,
32
+ order: options.order,
33
+ limit: parseInt(options.limit, 10),
34
+ });
35
+
36
+ spinner.stop();
37
+
38
+ if (options.json) {
39
+ console.log(JSON.stringify(result, null, 2));
40
+ return;
41
+ }
42
+
43
+ if (result.items.length === 0) {
44
+ console.log(chalk.yellow('\nNo skills found matching your query.'));
45
+ console.log(chalk.gray('Try a different search term or browse by category.'));
46
+ return;
47
+ }
48
+
49
+ console.log(chalk.bold(`\nFound ${result.pagination.total} skills:\n`));
50
+
51
+ const table = new Table({
52
+ head: [
53
+ chalk.cyan('Name'),
54
+ chalk.cyan('Description'),
55
+ chalk.cyan('Version'),
56
+ chalk.cyan('Security'),
57
+ chalk.cyan('Uses'),
58
+ ],
59
+ colWidths: [25, 40, 10, 10, 10],
60
+ wordWrap: true,
61
+ });
62
+
63
+ for (const skill of result.items) {
64
+ const securityColor = {
65
+ safe: chalk.green,
66
+ low: chalk.blue,
67
+ medium: chalk.yellow,
68
+ high: chalk.red,
69
+ }[skill.securityLevel ?? 'safe'] ?? chalk.white;
70
+
71
+ table.push([
72
+ chalk.bold(skill.fullName),
73
+ skill.description.substring(0, 80) + (skill.description.length > 80 ? '...' : ''),
74
+ skill.latestVersion,
75
+ securityColor(skill.securityLevel ?? 'unknown'),
76
+ skill.stats.totalUses.toString(),
77
+ ]);
78
+ }
79
+
80
+ console.log(table.toString());
81
+
82
+ if (result.pagination.hasMore) {
83
+ console.log(chalk.gray(`\nShowing ${result.items.length} of ${result.pagination.total} results.`));
84
+ console.log(chalk.gray('Use --limit to see more results.'));
85
+ }
86
+
87
+ await storage.close();
88
+ } catch (error) {
89
+ spinner.fail('Search failed');
90
+ throw error;
91
+ }
92
+ });
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Open Skills Hub CLI - Validate Command
3
+ * Validates skill file or directory structure
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 { parse as parseYaml } from 'yaml';
11
+ import { isValidSkillName } from '@open-skills-hub/core';
12
+
13
+ interface ValidationResult {
14
+ valid: boolean;
15
+ errors: string[];
16
+ warnings: string[];
17
+ info: {
18
+ name?: string;
19
+ description?: string;
20
+ license?: string;
21
+ hasScripts: boolean;
22
+ hasReferences: boolean;
23
+ hasAssets: boolean;
24
+ fileCount: number;
25
+ totalSize: number;
26
+ };
27
+ }
28
+
29
+ interface ParsedFrontmatter {
30
+ name?: string;
31
+ description?: string;
32
+ license?: string;
33
+ compatibility?: string;
34
+ metadata?: Record<string, string>;
35
+ 'allowed-tools'?: string;
36
+ allowedTools?: string[];
37
+ }
38
+
39
+ function parseFrontmatter(content: string): { frontmatter: ParsedFrontmatter; markdown: string } | null {
40
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
41
+ if (!match || !match[1]) {
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ const frontmatter = parseYaml(match[1]) as ParsedFrontmatter;
47
+ return {
48
+ frontmatter,
49
+ markdown: match[2]?.trim() || '',
50
+ };
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function formatSize(bytes: number): string {
57
+ if (bytes < 1024) return `${bytes} B`;
58
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
59
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
60
+ }
61
+
62
+ function countFiles(dirPath: string): { count: number; size: number } {
63
+ let count = 0;
64
+ let size = 0;
65
+
66
+ if (!fs.existsSync(dirPath)) {
67
+ return { count: 0, size: 0 };
68
+ }
69
+
70
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const fullPath = path.join(dirPath, entry.name);
73
+ if (entry.isDirectory()) {
74
+ const sub = countFiles(fullPath);
75
+ count += sub.count;
76
+ size += sub.size;
77
+ } else if (entry.isFile()) {
78
+ count++;
79
+ size += fs.statSync(fullPath).size;
80
+ }
81
+ }
82
+
83
+ return { count, size };
84
+ }
85
+
86
+ function validateSkill(skillPath: string): ValidationResult {
87
+ const result: ValidationResult = {
88
+ valid: true,
89
+ errors: [],
90
+ warnings: [],
91
+ info: {
92
+ hasScripts: false,
93
+ hasReferences: false,
94
+ hasAssets: false,
95
+ fileCount: 0,
96
+ totalSize: 0,
97
+ },
98
+ };
99
+
100
+ const resolvedPath = path.resolve(skillPath);
101
+
102
+ // Check if path exists
103
+ if (!fs.existsSync(resolvedPath)) {
104
+ result.errors.push(`Path not found: ${resolvedPath}`);
105
+ result.valid = false;
106
+ return result;
107
+ }
108
+
109
+ const stat = fs.statSync(resolvedPath);
110
+ const isDirectory = stat.isDirectory();
111
+
112
+ let skillMdPath: string;
113
+ let skillDir: string;
114
+
115
+ if (isDirectory) {
116
+ skillDir = resolvedPath;
117
+ skillMdPath = path.join(resolvedPath, 'SKILL.md');
118
+
119
+ // Also check for lowercase
120
+ if (!fs.existsSync(skillMdPath)) {
121
+ const altPath = path.join(resolvedPath, 'skill.md');
122
+ if (fs.existsSync(altPath)) {
123
+ skillMdPath = altPath;
124
+ result.warnings.push('SKILL.md should be uppercase (found skill.md)');
125
+ }
126
+ }
127
+ } else {
128
+ skillDir = path.dirname(resolvedPath);
129
+ skillMdPath = resolvedPath;
130
+ }
131
+
132
+ // Check SKILL.md exists
133
+ if (!fs.existsSync(skillMdPath)) {
134
+ result.errors.push('SKILL.md not found');
135
+ result.valid = false;
136
+ return result;
137
+ }
138
+
139
+ // Read and parse SKILL.md
140
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
141
+ result.info.totalSize = content.length;
142
+ result.info.fileCount = 1;
143
+
144
+ // Check for frontmatter
145
+ const parsed = parseFrontmatter(content);
146
+ if (!parsed) {
147
+ result.errors.push('Invalid or missing YAML frontmatter (must start with ---)');
148
+ result.valid = false;
149
+ return result;
150
+ }
151
+
152
+ const { frontmatter, markdown } = parsed;
153
+
154
+ // Validate name field
155
+ if (!frontmatter.name) {
156
+ result.errors.push('Missing required field: name');
157
+ result.valid = false;
158
+ } else {
159
+ result.info.name = frontmatter.name;
160
+
161
+ // Check name format
162
+ if (!isValidSkillName(frontmatter.name)) {
163
+ result.errors.push(`Invalid name format: "${frontmatter.name}". Must be lowercase letters, numbers, and hyphens only.`);
164
+ result.valid = false;
165
+ }
166
+
167
+ // Check name matches directory
168
+ if (isDirectory) {
169
+ const dirName = path.basename(skillDir);
170
+ if (frontmatter.name !== dirName) {
171
+ result.warnings.push(`Name "${frontmatter.name}" doesn't match directory name "${dirName}"`);
172
+ }
173
+ }
174
+
175
+ // Check name length
176
+ if (frontmatter.name.length > 64) {
177
+ result.errors.push(`Name too long: ${frontmatter.name.length} chars (max 64)`);
178
+ result.valid = false;
179
+ }
180
+
181
+ // Check for invalid patterns
182
+ if (frontmatter.name.startsWith('-')) {
183
+ result.errors.push('Name cannot start with a hyphen');
184
+ result.valid = false;
185
+ }
186
+ if (frontmatter.name.endsWith('-')) {
187
+ result.errors.push('Name cannot end with a hyphen');
188
+ result.valid = false;
189
+ }
190
+ if (frontmatter.name.includes('--')) {
191
+ result.errors.push('Name cannot contain consecutive hyphens');
192
+ result.valid = false;
193
+ }
194
+ }
195
+
196
+ // Validate description field
197
+ if (!frontmatter.description) {
198
+ result.errors.push('Missing required field: description');
199
+ result.valid = false;
200
+ } else {
201
+ result.info.description = frontmatter.description;
202
+
203
+ if (frontmatter.description.length > 1024) {
204
+ result.errors.push(`Description too long: ${frontmatter.description.length} chars (max 1024)`);
205
+ result.valid = false;
206
+ }
207
+
208
+ if (frontmatter.description.length < 10) {
209
+ result.warnings.push('Description is very short. Consider adding more detail.');
210
+ }
211
+ }
212
+
213
+ // Validate optional fields
214
+ if (frontmatter.license) {
215
+ result.info.license = frontmatter.license;
216
+ }
217
+
218
+ if (frontmatter.compatibility && frontmatter.compatibility.length > 500) {
219
+ result.errors.push(`Compatibility too long: ${frontmatter.compatibility.length} chars (max 500)`);
220
+ result.valid = false;
221
+ }
222
+
223
+ // Check markdown content
224
+ if (!markdown || markdown.length === 0) {
225
+ result.warnings.push('SKILL.md has no body content after frontmatter');
226
+ } else if (markdown.length > 50000) {
227
+ result.warnings.push(`SKILL.md body is very large (${(markdown.length / 1000).toFixed(1)}K chars). Consider splitting into reference files.`);
228
+ }
229
+
230
+ // Check directory structure (only for directories)
231
+ if (isDirectory) {
232
+ const scriptsDir = path.join(skillDir, 'scripts');
233
+ const referencesDir = path.join(skillDir, 'references');
234
+ const referenceDir = path.join(skillDir, 'reference'); // Alternative name
235
+ const assetsDir = path.join(skillDir, 'assets');
236
+
237
+ if (fs.existsSync(scriptsDir)) {
238
+ result.info.hasScripts = true;
239
+ const stats = countFiles(scriptsDir);
240
+ result.info.fileCount += stats.count;
241
+ result.info.totalSize += stats.size;
242
+ }
243
+
244
+ if (fs.existsSync(referencesDir)) {
245
+ result.info.hasReferences = true;
246
+ const stats = countFiles(referencesDir);
247
+ result.info.fileCount += stats.count;
248
+ result.info.totalSize += stats.size;
249
+ } else if (fs.existsSync(referenceDir)) {
250
+ result.info.hasReferences = true;
251
+ const stats = countFiles(referenceDir);
252
+ result.info.fileCount += stats.count;
253
+ result.info.totalSize += stats.size;
254
+ }
255
+
256
+ if (fs.existsSync(assetsDir)) {
257
+ result.info.hasAssets = true;
258
+ const stats = countFiles(assetsDir);
259
+ result.info.fileCount += stats.count;
260
+ result.info.totalSize += stats.size;
261
+ }
262
+
263
+ // Check for LICENSE file
264
+ const licensePath = path.join(skillDir, 'LICENSE.txt');
265
+ const licensePathAlt = path.join(skillDir, 'LICENSE');
266
+ if (!fs.existsSync(licensePath) && !fs.existsSync(licensePathAlt)) {
267
+ if (frontmatter.license) {
268
+ result.warnings.push('License specified in frontmatter but no LICENSE.txt file found');
269
+ }
270
+ }
271
+
272
+ // Check total size
273
+ if (result.info.totalSize > 150 * 1024 * 1024) {
274
+ result.errors.push(`Total size exceeds 150MB limit: ${formatSize(result.info.totalSize)}`);
275
+ result.valid = false;
276
+ } else if (result.info.totalSize > 50 * 1024 * 1024) {
277
+ result.warnings.push(`Large skill size: ${formatSize(result.info.totalSize)}. Consider optimizing.`);
278
+ }
279
+ }
280
+
281
+ return result;
282
+ }
283
+
284
+ export const validateCommand = new Command('validate')
285
+ .description('Validate a skill file or directory')
286
+ .argument('<path>', 'Path to skill file (.md) or directory containing SKILL.md')
287
+ .option('--json', 'Output result as JSON')
288
+ .action((skillPath, options) => {
289
+ const result = validateSkill(skillPath);
290
+
291
+ if (options.json) {
292
+ console.log(JSON.stringify(result, null, 2));
293
+ process.exit(result.valid ? 0 : 1);
294
+ }
295
+
296
+ // Pretty output
297
+ const resolvedPath = path.resolve(skillPath);
298
+ console.log(`\nValidating ${chalk.cyan(resolvedPath)}...\n`);
299
+
300
+ // SKILL.md check
301
+ if (result.errors.some(e => e.includes('SKILL.md not found'))) {
302
+ console.log(chalk.red('✗ SKILL.md not found'));
303
+ process.exit(1);
304
+ } else {
305
+ console.log(chalk.green('✓ SKILL.md exists'));
306
+ }
307
+
308
+ // Frontmatter check
309
+ if (result.errors.some(e => e.includes('frontmatter'))) {
310
+ console.log(chalk.red('✗ Frontmatter is invalid'));
311
+ for (const error of result.errors.filter(e => e.includes('frontmatter'))) {
312
+ console.log(chalk.red(` └── ${error}`));
313
+ }
314
+ } else {
315
+ console.log(chalk.green('✓ Frontmatter is valid'));
316
+
317
+ // Show field details
318
+ if (result.info.name) {
319
+ const nameValid = !result.errors.some(e => e.toLowerCase().includes('name'));
320
+ const icon = nameValid ? chalk.green('✓') : chalk.red('✗');
321
+ console.log(` ├── name: ${result.info.name} ${icon}`);
322
+
323
+ // Show name errors
324
+ for (const error of result.errors.filter(e => e.toLowerCase().includes('name') && !e.includes('frontmatter'))) {
325
+ console.log(chalk.red(` │ └── ${error}`));
326
+ }
327
+ }
328
+
329
+ if (result.info.description) {
330
+ const descValid = !result.errors.some(e => e.toLowerCase().includes('description'));
331
+ const icon = descValid ? chalk.green('✓') : chalk.red('✗');
332
+ const preview = result.info.description.length > 40
333
+ ? result.info.description.substring(0, 40) + '...'
334
+ : result.info.description;
335
+ console.log(` ├── description: ${preview} ${icon}`);
336
+
337
+ // Show description errors
338
+ for (const error of result.errors.filter(e => e.toLowerCase().includes('description'))) {
339
+ console.log(chalk.red(` │ └── ${error}`));
340
+ }
341
+ } else {
342
+ console.log(chalk.red(` ├── description: (missing)`));
343
+ console.log(chalk.red(` │ └── Error: Description is required`));
344
+ }
345
+
346
+ if (result.info.license) {
347
+ console.log(` └── license: ${result.info.license} ${chalk.green('✓')}`);
348
+ }
349
+ }
350
+
351
+ // Directory structure check
352
+ const stat = fs.statSync(resolvedPath);
353
+ if (stat.isDirectory()) {
354
+ const hasAnyDirs = result.info.hasScripts || result.info.hasReferences || result.info.hasAssets;
355
+
356
+ if (hasAnyDirs || result.info.fileCount > 1) {
357
+ console.log(chalk.green('✓ Directory structure is valid'));
358
+ if (result.info.hasScripts) {
359
+ console.log(chalk.gray(' ├── scripts/ ✓'));
360
+ }
361
+ if (result.info.hasReferences) {
362
+ console.log(chalk.gray(' ├── references/ ✓'));
363
+ }
364
+ if (result.info.hasAssets) {
365
+ console.log(chalk.gray(' ├── assets/ ✓'));
366
+ }
367
+ console.log(chalk.gray(` └── ${result.info.fileCount} files, ${formatSize(result.info.totalSize)}`));
368
+ }
369
+ }
370
+
371
+ // Warnings
372
+ if (result.warnings.length > 0) {
373
+ console.log('');
374
+ for (const warning of result.warnings) {
375
+ console.log(chalk.yellow(`⚠ ${warning}`));
376
+ }
377
+ }
378
+
379
+ // Summary
380
+ console.log('');
381
+ if (result.valid) {
382
+ console.log(chalk.green.bold('✓ Skill is valid and ready to publish!'));
383
+ console.log(chalk.gray(`\nRun: skills publish ${skillPath}`));
384
+ } else {
385
+ const errorCount = result.errors.length;
386
+ console.log(chalk.red.bold(`Found ${errorCount} error${errorCount > 1 ? 's' : ''}. Please fix before publishing.`));
387
+ }
388
+ console.log('');
389
+
390
+ process.exit(result.valid ? 0 : 1);
391
+ });