skillsets 0.1.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 ADDED
@@ -0,0 +1,41 @@
1
+ # Skillsets CLI
2
+
3
+ Command-line tool for discovering, installing, and contributing verified Claude Code skillsets.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Browse available skillsets
9
+ npx skillsets list
10
+
11
+ # Search by keyword
12
+ npx skillsets search "sdlc"
13
+
14
+ # Install a skillset
15
+ npx skillsets install @supercollectible/The_Skillset
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Purpose |
21
+ |---------|---------|
22
+ | `list` | Browse all available skillsets |
23
+ | `search <query>` | Fuzzy search by name, description, or tags |
24
+ | `install <id>` | Install skillset to current directory |
25
+ | `verify` | Verify installed skillset checksums |
26
+ | `init` | Scaffold a new skillset for contribution |
27
+ | `audit` | Validate skillset before submission |
28
+ | `submit` | Open PR to registry (requires `gh` CLI) |
29
+
30
+ ## Development
31
+
32
+ ```bash
33
+ npm install # Install dependencies
34
+ npm run build # Build TypeScript
35
+ npm test # Run tests (43 tests)
36
+ ```
37
+
38
+ ## Documentation
39
+
40
+ - [CLI Style Guide](../.claude/resources/cli_styleguide.md) - Development patterns
41
+ - [CLAUDE.md](../CLAUDE.md) - Project overview
@@ -0,0 +1 @@
1
+ export declare function audit(): Promise<void>;
@@ -0,0 +1,472 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
4
+ import { join, relative } from 'path';
5
+ import yaml from 'js-yaml';
6
+ import { fetchSkillsetMetadata } from '../lib/api.js';
7
+ /**
8
+ * Compare semver versions. Returns:
9
+ * -1 if a < b, 0 if a == b, 1 if a > b
10
+ */
11
+ function compareVersions(a, b) {
12
+ const partsA = a.split('.').map(Number);
13
+ const partsB = b.split('.').map(Number);
14
+ for (let i = 0; i < 3; i++) {
15
+ const numA = partsA[i] || 0;
16
+ const numB = partsB[i] || 0;
17
+ if (numA < numB)
18
+ return -1;
19
+ if (numA > numB)
20
+ return 1;
21
+ }
22
+ return 0;
23
+ }
24
+ const MAX_FILE_SIZE = 1048576; // 1MB
25
+ const TEXT_EXTENSIONS = new Set([
26
+ '.md', '.txt', '.json', '.yaml', '.yml',
27
+ '.js', '.ts', '.tsx', '.jsx',
28
+ '.py', '.sh', '.bash',
29
+ '.astro', '.html', '.css', '.scss',
30
+ '.toml', '.conf', '.env.example',
31
+ '.gitignore', '.editorconfig',
32
+ ]);
33
+ const SECRET_PATTERNS = [
34
+ { name: 'API Key', pattern: /api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9]{20,}/gi },
35
+ { name: 'Password', pattern: /password\s*[:=]\s*['"]?[^'"\s]{8,}/gi },
36
+ { name: 'Secret', pattern: /secret\s*[:=]\s*['"]?[a-zA-Z0-9]{20,}/gi },
37
+ { name: 'Token', pattern: /token\s*[:=]\s*['"]?[a-zA-Z0-9]{20,}/gi },
38
+ { name: 'AWS Key', pattern: /AKIA[0-9A-Z]{16}/g },
39
+ { name: 'GitHub Token', pattern: /ghp_[a-zA-Z0-9]{36}/g },
40
+ { name: 'OpenAI Key', pattern: /sk-[a-zA-Z0-9]{48}/g },
41
+ ];
42
+ function getAllFiles(dir, baseDir = dir) {
43
+ const files = [];
44
+ if (!existsSync(dir))
45
+ return files;
46
+ const entries = readdirSync(dir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ const fullPath = join(dir, entry.name);
49
+ const relativePath = relative(baseDir, fullPath);
50
+ if (entry.isDirectory()) {
51
+ // Skip node_modules, .git, etc.
52
+ if (['node_modules', '.git', '__pycache__'].includes(entry.name))
53
+ continue;
54
+ files.push(...getAllFiles(fullPath, baseDir));
55
+ }
56
+ else {
57
+ const stat = statSync(fullPath);
58
+ files.push({ path: relativePath, size: stat.size });
59
+ }
60
+ }
61
+ return files;
62
+ }
63
+ function isBinaryFile(filePath) {
64
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
65
+ if (TEXT_EXTENSIONS.has(ext))
66
+ return false;
67
+ if (filePath.endsWith('.example'))
68
+ return false;
69
+ // Check for null bytes in first 512 bytes
70
+ try {
71
+ const buffer = Buffer.alloc(512);
72
+ const fd = require('fs').openSync(filePath, 'r');
73
+ require('fs').readSync(fd, buffer, 0, 512, 0);
74
+ require('fs').closeSync(fd);
75
+ return buffer.includes(0);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function scanForSecrets(dir) {
82
+ const secrets = [];
83
+ const files = getAllFiles(dir);
84
+ for (const { path: filePath } of files) {
85
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
86
+ if (!['.md', '.txt', '.json', '.yaml', '.yml', '.js', '.ts', '.py'].includes(ext))
87
+ continue;
88
+ if (filePath.includes('AUDIT_REPORT'))
89
+ continue;
90
+ try {
91
+ const content = readFileSync(join(dir, filePath), 'utf-8');
92
+ const lines = content.split('\n');
93
+ for (let i = 0; i < lines.length; i++) {
94
+ for (const { name, pattern } of SECRET_PATTERNS) {
95
+ if (pattern.test(lines[i])) {
96
+ secrets.push({ file: filePath, line: i + 1, pattern: name });
97
+ }
98
+ // Reset regex lastIndex
99
+ pattern.lastIndex = 0;
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ // Skip unreadable files
105
+ }
106
+ }
107
+ return secrets;
108
+ }
109
+ function validateManifest(cwd) {
110
+ const manifestPath = join(cwd, 'skillset.yaml');
111
+ if (!existsSync(manifestPath)) {
112
+ return { valid: false, errors: ['skillset.yaml not found'] };
113
+ }
114
+ try {
115
+ const content = readFileSync(manifestPath, 'utf-8');
116
+ const data = yaml.load(content);
117
+ const errors = [];
118
+ // Required fields
119
+ if (data.schema_version !== '1.0') {
120
+ errors.push('schema_version must be "1.0"');
121
+ }
122
+ if (!data.name || !/^[A-Za-z0-9_-]+$/.test(data.name)) {
123
+ errors.push('name must be alphanumeric with hyphens/underscores');
124
+ }
125
+ if (!data.version || !/^[0-9]+\.[0-9]+\.[0-9]+$/.test(data.version)) {
126
+ errors.push('version must be semantic (e.g., 1.0.0)');
127
+ }
128
+ if (!data.description || data.description.length < 10 || data.description.length > 200) {
129
+ errors.push('description must be 10-200 characters');
130
+ }
131
+ if (!data.author?.handle || !/^@[A-Za-z0-9_-]+$/.test(data.author.handle)) {
132
+ errors.push('author.handle must start with @ (e.g., @username)');
133
+ }
134
+ if (!data.verification?.production_url) {
135
+ errors.push('verification.production_url is required');
136
+ }
137
+ if (!data.verification?.audit_report) {
138
+ errors.push('verification.audit_report is required');
139
+ }
140
+ if (!Array.isArray(data.tags) || data.tags.length < 1 || data.tags.length > 10) {
141
+ errors.push('tags must be an array with 1-10 items');
142
+ }
143
+ else {
144
+ for (const tag of data.tags) {
145
+ if (!/^[a-z0-9-]+$/.test(tag)) {
146
+ errors.push(`tag "${tag}" must be lowercase alphanumeric with hyphens`);
147
+ }
148
+ }
149
+ }
150
+ return { valid: errors.length === 0, errors, data };
151
+ }
152
+ catch (error) {
153
+ return { valid: false, errors: [`YAML parse error: ${error.message}`] };
154
+ }
155
+ }
156
+ function formatSize(bytes) {
157
+ if (bytes < 1024)
158
+ return `${bytes} B`;
159
+ if (bytes < 1024 * 1024)
160
+ return `${(bytes / 1024).toFixed(1)} KB`;
161
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
162
+ }
163
+ function generateReport(results, cwd) {
164
+ const timestamp = new Date().toISOString();
165
+ const allPassed = results.manifest.status === 'PASS' &&
166
+ results.requiredFiles.status === 'PASS' &&
167
+ results.contentStructure.status === 'PASS' &&
168
+ results.fileSize.status !== 'FAIL' &&
169
+ results.secrets.status === 'PASS' &&
170
+ results.versionCheck.status === 'PASS';
171
+ const submissionType = results.isUpdate
172
+ ? `Update (${results.existingVersion} → ${results.skillsetVersion})`
173
+ : 'New submission';
174
+ const statusIcon = (status) => {
175
+ if (status === 'PASS')
176
+ return '✓ PASS';
177
+ if (status === 'WARNING')
178
+ return '⚠ WARNING';
179
+ return '✗ FAIL';
180
+ };
181
+ let report = `# Audit Report
182
+
183
+ **Generated:** ${timestamp}
184
+ **Skillset:** ${results.skillsetName || 'Unknown'} v${results.skillsetVersion || '0.0.0'}
185
+ **Author:** ${results.authorHandle || 'Unknown'}
186
+ **Type:** ${submissionType}
187
+
188
+ ---
189
+
190
+ ## Validation Summary
191
+
192
+ | Check | Status | Details |
193
+ |-------|--------|---------|
194
+ | Manifest Validation | ${statusIcon(results.manifest.status)} | ${results.manifest.details} |
195
+ | Required Files | ${statusIcon(results.requiredFiles.status)} | ${results.requiredFiles.details} |
196
+ | Content Structure | ${statusIcon(results.contentStructure.status)} | ${results.contentStructure.details} |
197
+ | File Size Check | ${statusIcon(results.fileSize.status)} | ${results.fileSize.details} |
198
+ | Binary Detection | ${statusIcon(results.binary.status)} | ${results.binary.details} |
199
+ | Secret Detection | ${statusIcon(results.secrets.status)} | ${results.secrets.details} |
200
+ | Version Check | ${statusIcon(results.versionCheck.status)} | ${results.versionCheck.details} |
201
+
202
+ ---
203
+
204
+ ## Detailed Findings
205
+
206
+ ### 1. Manifest Validation
207
+
208
+ ${results.manifest.findings || 'All fields validated successfully.'}
209
+
210
+ ### 2. Required Files
211
+
212
+ ${results.requiredFiles.findings || 'All required files present.'}
213
+
214
+ ### 3. Content Structure
215
+
216
+ ${results.contentStructure.findings || 'Valid content structure detected.'}
217
+
218
+ ### 4. File Size Analysis
219
+
220
+ ${results.fileSize.findings || 'No large files detected.'}
221
+
222
+ ${results.largeFiles.length > 0 ? '**Large Files (>1MB):**\n' + results.largeFiles.map(f => `- ${f.path} (${formatSize(f.size)})`).join('\n') : ''}
223
+
224
+ **Total Files:** ${results.files.length}
225
+ **Total Size:** ${formatSize(results.files.reduce((sum, f) => sum + f.size, 0))}
226
+
227
+ ### 5. Binary File Detection
228
+
229
+ ${results.binary.findings || 'No binary files detected.'}
230
+
231
+ ${results.binaryFiles.length > 0 ? '**Binary Files Detected:**\n' + results.binaryFiles.map(f => `- ${f}`).join('\n') : ''}
232
+
233
+ ### 6. Secret Pattern Detection
234
+
235
+ ${results.secrets.findings || 'No secrets detected.'}
236
+
237
+ ${results.secretsFound.length > 0 ? '**Potential Secrets Found:**\n' + results.secretsFound.map(s => `- ${s.file}:${s.line} (${s.pattern})`).join('\n') : ''}
238
+
239
+ ---
240
+
241
+ ## File Inventory
242
+
243
+ | File | Size |
244
+ |------|------|
245
+ ${results.files.map(f => `| ${f.path} | ${formatSize(f.size)} |`).join('\n')}
246
+
247
+ ---
248
+
249
+ ## Submission Status
250
+
251
+ ${allPassed ? '**✓ READY FOR SUBMISSION**' : '**✗ NOT READY - Please fix the issues above**'}
252
+
253
+ ${allPassed
254
+ ? 'All validation checks passed. You can now submit this skillset to the registry.'
255
+ : 'Please address the failed checks before submitting.'}
256
+
257
+ ---
258
+
259
+ ## Next Steps
260
+
261
+ ${allPassed
262
+ ? `1. Review this audit report
263
+ 2. Ensure PROOF.md has adequate production evidence
264
+ 3. Run: \`npx skillsets submit\``
265
+ : `1. Fix the issues flagged above
266
+ 2. Re-run: \`npx skillsets audit\`
267
+ 3. Repeat until all checks pass`}
268
+
269
+ ---
270
+
271
+ **Generated by:** \`npx skillsets audit\`
272
+ **Schema Version:** 1.0
273
+ **Report Date:** ${timestamp}
274
+ `;
275
+ return report;
276
+ }
277
+ export async function audit() {
278
+ const spinner = ora('Auditing skillset...').start();
279
+ const cwd = process.cwd();
280
+ const results = {
281
+ manifest: { status: 'FAIL', details: '' },
282
+ requiredFiles: { status: 'FAIL', details: '' },
283
+ contentStructure: { status: 'FAIL', details: '' },
284
+ fileSize: { status: 'PASS', details: '' },
285
+ binary: { status: 'PASS', details: '' },
286
+ secrets: { status: 'PASS', details: '' },
287
+ versionCheck: { status: 'PASS', details: '' },
288
+ isUpdate: false,
289
+ files: [],
290
+ largeFiles: [],
291
+ binaryFiles: [],
292
+ secretsFound: [],
293
+ };
294
+ // 1. Manifest validation
295
+ spinner.text = 'Validating manifest...';
296
+ const manifestResult = validateManifest(cwd);
297
+ if (manifestResult.valid) {
298
+ results.manifest = { status: 'PASS', details: 'All fields valid' };
299
+ results.skillsetName = manifestResult.data.name;
300
+ results.skillsetVersion = manifestResult.data.version;
301
+ results.authorHandle = manifestResult.data.author?.handle;
302
+ }
303
+ else {
304
+ results.manifest = {
305
+ status: 'FAIL',
306
+ details: `${manifestResult.errors.length} error(s)`,
307
+ findings: manifestResult.errors.map(e => `- ${e}`).join('\n'),
308
+ };
309
+ }
310
+ // 2. Required files
311
+ spinner.text = 'Checking required files...';
312
+ const hasReadme = existsSync(join(cwd, 'README.md'));
313
+ const hasContent = existsSync(join(cwd, 'content'));
314
+ const hasSkillsetYaml = existsSync(join(cwd, 'skillset.yaml'));
315
+ const missingFiles = [];
316
+ if (!hasSkillsetYaml)
317
+ missingFiles.push('skillset.yaml');
318
+ if (!hasReadme)
319
+ missingFiles.push('README.md');
320
+ if (!hasContent)
321
+ missingFiles.push('content/');
322
+ if (missingFiles.length === 0) {
323
+ results.requiredFiles = { status: 'PASS', details: 'All present' };
324
+ }
325
+ else {
326
+ results.requiredFiles = {
327
+ status: 'FAIL',
328
+ details: `Missing: ${missingFiles.join(', ')}`,
329
+ findings: missingFiles.map(f => `- Missing: ${f}`).join('\n'),
330
+ };
331
+ }
332
+ // 3. Content structure
333
+ spinner.text = 'Verifying content structure...';
334
+ const hasClaudeDir = existsSync(join(cwd, 'content', '.claude'));
335
+ const hasClaudeMd = existsSync(join(cwd, 'content', 'CLAUDE.md'));
336
+ if (hasClaudeDir || hasClaudeMd) {
337
+ const found = [hasClaudeDir && '.claude/', hasClaudeMd && 'CLAUDE.md'].filter(Boolean);
338
+ results.contentStructure = {
339
+ status: 'PASS',
340
+ details: `Found: ${found.join(', ')}`,
341
+ };
342
+ }
343
+ else {
344
+ results.contentStructure = {
345
+ status: 'FAIL',
346
+ details: 'No .claude/ or CLAUDE.md',
347
+ findings: 'content/ must contain either .claude/ directory or CLAUDE.md file',
348
+ };
349
+ }
350
+ // 4. File size check
351
+ spinner.text = 'Checking file sizes...';
352
+ results.files = getAllFiles(cwd);
353
+ results.largeFiles = results.files.filter(f => f.size > MAX_FILE_SIZE);
354
+ if (results.largeFiles.length === 0) {
355
+ results.fileSize = { status: 'PASS', details: 'No files >1MB' };
356
+ }
357
+ else {
358
+ results.fileSize = {
359
+ status: 'WARNING',
360
+ details: `${results.largeFiles.length} large file(s)`,
361
+ findings: 'Consider compressing or moving large files to external hosting.',
362
+ };
363
+ }
364
+ // 5. Binary detection
365
+ spinner.text = 'Detecting binary files...';
366
+ const contentDir = join(cwd, 'content');
367
+ if (existsSync(contentDir)) {
368
+ const contentFiles = getAllFiles(contentDir);
369
+ for (const { path: filePath } of contentFiles) {
370
+ const fullPath = join(contentDir, filePath);
371
+ if (isBinaryFile(fullPath)) {
372
+ results.binaryFiles.push(filePath);
373
+ }
374
+ }
375
+ }
376
+ if (results.binaryFiles.length === 0) {
377
+ results.binary = { status: 'PASS', details: 'No binaries' };
378
+ }
379
+ else {
380
+ results.binary = {
381
+ status: 'WARNING',
382
+ details: `${results.binaryFiles.length} binary file(s)`,
383
+ findings: 'Binary files should be justified in your PR description.',
384
+ };
385
+ }
386
+ // 6. Secret detection
387
+ spinner.text = 'Scanning for secrets...';
388
+ results.secretsFound = scanForSecrets(cwd);
389
+ if (results.secretsFound.length === 0) {
390
+ results.secrets = { status: 'PASS', details: 'No secrets detected' };
391
+ }
392
+ else {
393
+ results.secrets = {
394
+ status: 'FAIL',
395
+ details: `${results.secretsFound.length} potential secret(s)`,
396
+ findings: 'Remove all API keys, tokens, and passwords before submitting.',
397
+ };
398
+ }
399
+ // 7. Version check (for updates)
400
+ spinner.text = 'Checking registry...';
401
+ if (results.skillsetName && results.authorHandle) {
402
+ const skillsetId = `${results.authorHandle}/${results.skillsetName}`;
403
+ try {
404
+ const existing = await fetchSkillsetMetadata(skillsetId);
405
+ if (existing) {
406
+ results.isUpdate = true;
407
+ results.existingVersion = existing.version;
408
+ if (compareVersions(results.skillsetVersion || '0.0.0', existing.version) > 0) {
409
+ results.versionCheck = {
410
+ status: 'PASS',
411
+ details: `Update: ${existing.version} → ${results.skillsetVersion}`,
412
+ };
413
+ }
414
+ else {
415
+ results.versionCheck = {
416
+ status: 'FAIL',
417
+ details: `Version must be > ${existing.version}`,
418
+ findings: `Current version ${results.skillsetVersion} is not greater than existing ${existing.version}. Bump the version in skillset.yaml.`,
419
+ };
420
+ }
421
+ }
422
+ else {
423
+ results.versionCheck = { status: 'PASS', details: 'New submission' };
424
+ }
425
+ }
426
+ catch {
427
+ results.versionCheck = { status: 'PASS', details: 'Registry unavailable (skipped)' };
428
+ }
429
+ }
430
+ else {
431
+ results.versionCheck = { status: 'PASS', details: 'Skipped (no manifest)' };
432
+ }
433
+ // Generate report
434
+ spinner.text = 'Generating audit report...';
435
+ const report = generateReport(results, cwd);
436
+ writeFileSync(join(cwd, 'AUDIT_REPORT.md'), report);
437
+ spinner.succeed('Audit complete');
438
+ // Summary
439
+ const allPassed = results.manifest.status === 'PASS' &&
440
+ results.requiredFiles.status === 'PASS' &&
441
+ results.contentStructure.status === 'PASS' &&
442
+ results.fileSize.status !== 'FAIL' &&
443
+ results.secrets.status === 'PASS' &&
444
+ results.versionCheck.status === 'PASS';
445
+ console.log('\n' + chalk.bold('Audit Summary:'));
446
+ console.log('');
447
+ const icon = (status) => {
448
+ if (status === 'PASS')
449
+ return chalk.green('✓');
450
+ if (status === 'WARNING')
451
+ return chalk.yellow('⚠');
452
+ return chalk.red('✗');
453
+ };
454
+ console.log(` ${icon(results.manifest.status)} Manifest: ${results.manifest.details}`);
455
+ console.log(` ${icon(results.requiredFiles.status)} Required Files: ${results.requiredFiles.details}`);
456
+ console.log(` ${icon(results.contentStructure.status)} Content Structure: ${results.contentStructure.details}`);
457
+ console.log(` ${icon(results.fileSize.status)} File Sizes: ${results.fileSize.details}`);
458
+ console.log(` ${icon(results.binary.status)} Binary Files: ${results.binary.details}`);
459
+ console.log(` ${icon(results.secrets.status)} Secrets: ${results.secrets.details}`);
460
+ console.log(` ${icon(results.versionCheck.status)} Version: ${results.versionCheck.details}`);
461
+ console.log('');
462
+ if (allPassed) {
463
+ console.log(chalk.green('✓ READY FOR SUBMISSION'));
464
+ console.log(chalk.gray('\nGenerated: AUDIT_REPORT.md'));
465
+ console.log(chalk.cyan('\nNext: npx skillsets submit'));
466
+ }
467
+ else {
468
+ console.log(chalk.red('✗ NOT READY - Fix issues above'));
469
+ console.log(chalk.gray('\nGenerated: AUDIT_REPORT.md'));
470
+ console.log(chalk.cyan('\nRe-run after fixes: npx skillsets audit'));
471
+ }
472
+ }
@@ -0,0 +1,5 @@
1
+ interface InitOptions {
2
+ yes?: boolean;
3
+ }
4
+ export declare function init(options: InitOptions): Promise<void>;
5
+ export {};