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 +41 -0
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +472 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +452 -0
- package/dist/commands/install.d.ts +6 -0
- package/dist/commands/install.js +54 -0
- package/dist/commands/list.d.ts +7 -0
- package/dist/commands/list.js +72 -0
- package/dist/commands/search.d.ts +6 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +292 -0
- package/dist/commands/verify.d.ts +5 -0
- package/dist/commands/verify.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -0
- package/dist/lib/api.d.ts +10 -0
- package/dist/lib/api.js +29 -0
- package/dist/lib/checksum.d.ts +16 -0
- package/dist/lib/checksum.js +53 -0
- package/dist/lib/constants.d.ts +6 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +11 -0
- package/dist/lib/filesystem.d.ts +13 -0
- package/dist/lib/filesystem.js +75 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.js +1 -0
- package/package.json +54 -0
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
|
+
}
|