skillsets 0.8.0 → 0.9.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.
@@ -16,6 +16,7 @@ export interface AuditResults {
16
16
  mcpServers: AuditResult;
17
17
  runtimeDeps: AuditResult;
18
18
  installNotes: AuditResult;
19
+ license: AuditResult;
19
20
  ccExtensions: AuditResult;
20
21
  skillsetName?: string;
21
22
  skillsetVersion?: string;
@@ -17,6 +17,7 @@ export function isAuditPassing(results, enforceMcp) {
17
17
  (enforceMcp ? results.mcpServers.status === 'PASS' : true) &&
18
18
  (enforceMcp ? results.runtimeDeps.status === 'PASS' : true) &&
19
19
  results.installNotes.status === 'PASS' &&
20
+ results.license.status === 'PASS' &&
20
21
  (enforceMcp ? results.ccExtensions.status === 'PASS' : true);
21
22
  }
22
23
  function statusIcon(status) {
@@ -38,7 +39,7 @@ export function hasWarnings(results) {
38
39
  results.manifest, results.requiredFiles, results.contentStructure,
39
40
  results.fileSize, results.binary, results.secrets, results.readmeLinks,
40
41
  results.versionCheck, results.mcpServers, results.runtimeDeps,
41
- results.installNotes, results.ccExtensions,
42
+ results.installNotes, results.license, results.ccExtensions,
42
43
  ];
43
44
  return checks.some(c => c.status === 'WARNING');
44
45
  }
@@ -73,6 +74,7 @@ export function generateReport(results, enforceMcp = false) {
73
74
  | MCP Servers | ${statusIcon(results.mcpServers.status)} | ${results.mcpServers.details} |
74
75
  | Runtime Dependencies | ${statusIcon(results.runtimeDeps.status)} | ${results.runtimeDeps.details} |
75
76
  | Install Notes | ${statusIcon(results.installNotes.status)} | ${results.installNotes.details} |
77
+ | License | ${statusIcon(results.license.status)} | ${results.license.details} |
76
78
  | CC Extensions | ${statusIcon(results.ccExtensions.status)} | ${results.ccExtensions.details} |
77
79
 
78
80
  ---
@@ -130,7 +132,11 @@ ${results.runtimeDeps.findings || 'Runtime dependency declarations are consisten
130
132
 
131
133
  ${results.installNotes.findings || 'Install notes present and valid.'}
132
134
 
133
- ### 11. CC Extensions
135
+ ### 11. License
136
+
137
+ ${results.license.findings || 'License file present and populated.'}
138
+
139
+ ### 12. CC Extensions
134
140
 
135
141
  ${results.ccExtensions.findings || 'CC extension declarations are consistent with manifest.'}
136
142
 
@@ -52,12 +52,16 @@ function getAllFiles(dir, baseDir = dir) {
52
52
  }
53
53
  return files;
54
54
  }
55
+ const TEXT_BASENAMES = new Set(['LICENSE', 'LICENCE', 'Makefile', 'Dockerfile', 'Gemfile']);
55
56
  function isBinaryFile(filePath) {
56
57
  const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
57
58
  if (TEXT_EXTENSIONS.has(ext))
58
59
  return false;
59
60
  if (filePath.endsWith('.example'))
60
61
  return false;
62
+ const basename = filePath.substring(filePath.lastIndexOf('/') + 1);
63
+ if (TEXT_BASENAMES.has(basename))
64
+ return false;
61
65
  // Check for null bytes in first 512 bytes
62
66
  try {
63
67
  const buffer = Buffer.alloc(512);
@@ -70,9 +74,18 @@ function isBinaryFile(filePath) {
70
74
  return false;
71
75
  }
72
76
  }
77
+ /** Find the README_<NAME>.md file in content/ */
78
+ function findReadme(cwd) {
79
+ const contentDir = join(cwd, 'content');
80
+ if (!existsSync(contentDir))
81
+ return null;
82
+ const entries = readdirSync(contentDir);
83
+ const readme = entries.find(f => /^README_[^/]+\.md$/i.test(f));
84
+ return readme ? join(contentDir, readme) : null;
85
+ }
73
86
  function scanReadmeLinks(cwd) {
74
- const readmePath = join(cwd, 'content', 'README.md');
75
- if (!existsSync(readmePath))
87
+ const readmePath = findReadme(cwd);
88
+ if (!readmePath)
76
89
  return [];
77
90
  const relativeLinks = [];
78
91
  const content = readFileSync(readmePath, 'utf-8');
@@ -243,6 +256,7 @@ export async function audit(options = {}) {
243
256
  mcpServers: { status: 'PASS', details: '' },
244
257
  runtimeDeps: { status: 'PASS', details: '' },
245
258
  installNotes: { status: 'PASS', details: '' },
259
+ license: { status: 'PASS', details: '' },
246
260
  ccExtensions: { status: 'PASS', details: '' },
247
261
  isUpdate: false,
248
262
  files: [],
@@ -270,9 +284,10 @@ export async function audit(options = {}) {
270
284
  // 2. Required files
271
285
  spinner.text = 'Checking required files...';
272
286
  const hasContent = existsSync(join(cwd, 'content'));
273
- const hasReadme = existsSync(join(cwd, 'content', 'README.md'));
287
+ const hasReadme = !!findReadme(cwd);
274
288
  const hasQuickstart = existsSync(join(cwd, 'content', 'QUICKSTART.md'));
275
289
  const hasInstallNotes = existsSync(join(cwd, 'content', 'INSTALL_NOTES.md'));
290
+ const hasLicense = existsSync(join(cwd, 'content', 'LICENSE'));
276
291
  const hasSkillsetYaml = existsSync(join(cwd, 'skillset.yaml'));
277
292
  const missingFiles = [];
278
293
  if (!hasSkillsetYaml)
@@ -280,11 +295,13 @@ export async function audit(options = {}) {
280
295
  if (!hasContent)
281
296
  missingFiles.push('content/');
282
297
  if (!hasReadme)
283
- missingFiles.push('content/README.md');
298
+ missingFiles.push('content/README_<NAME>.md');
284
299
  if (!hasQuickstart)
285
300
  missingFiles.push('content/QUICKSTART.md');
286
301
  if (!hasInstallNotes)
287
302
  missingFiles.push('content/INSTALL_NOTES.md');
303
+ if (!hasLicense)
304
+ missingFiles.push('content/LICENSE');
288
305
  if (missingFiles.length === 0) {
289
306
  results.requiredFiles = { status: 'PASS', details: 'All present' };
290
307
  }
@@ -320,6 +337,24 @@ export async function audit(options = {}) {
320
337
  else {
321
338
  results.installNotes = { status: 'FAIL', details: 'Missing' };
322
339
  }
340
+ // 2c. License validation
341
+ spinner.text = 'Validating license...';
342
+ if (hasLicense) {
343
+ const licenseContent = readFileSync(join(cwd, 'content', 'LICENSE'), 'utf-8').trim();
344
+ if (licenseContent.length === 0) {
345
+ results.license = {
346
+ status: 'FAIL',
347
+ details: 'Empty LICENSE file',
348
+ findings: 'content/LICENSE must contain a valid license. Populate it before submitting.',
349
+ };
350
+ }
351
+ else {
352
+ results.license = { status: 'PASS', details: 'Valid' };
353
+ }
354
+ }
355
+ else {
356
+ results.license = { status: 'FAIL', details: 'Missing' };
357
+ }
323
358
  // 3. Content structure
324
359
  spinner.text = 'Verifying content structure...';
325
360
  const hasClaudeDir = existsSync(join(cwd, 'content', '.claude'));
@@ -467,6 +502,7 @@ export async function audit(options = {}) {
467
502
  [results.mcpServers, 'MCP Servers'],
468
503
  [results.runtimeDeps, 'Runtime Deps'],
469
504
  [results.installNotes, 'Install Notes'],
505
+ [results.license, 'License'],
470
506
  [results.ccExtensions, 'CC Extensions'],
471
507
  ];
472
508
  console.log('\n' + chalk.bold('Audit Summary:'));
@@ -76,9 +76,6 @@ npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}
76
76
 
77
77
  [List the key files and their purposes]
78
78
 
79
- ## License
80
-
81
- [Your license]
82
79
  `;
83
80
  const QUICKSTART_TEMPLATE = `# Quickstart
84
81
 
@@ -92,7 +89,7 @@ After installing via \`npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}\`, custo
92
89
  your-project/
93
90
  ├── .claude/ # Skills, agents, resources
94
91
  ├── CLAUDE.md # Project config ← START HERE
95
- └── README.md # Documentation
92
+ └── README_{{NAME}}.md # Skillset documentation
96
93
  \`\`\`
97
94
 
98
95
  ---
@@ -356,7 +353,7 @@ export async function init(options) {
356
353
  }
357
354
  // Auto-detect existing files — core skillset files and primitives
358
355
  const coreFiles = [
359
- 'CLAUDE.md', 'README.md', 'QUICKSTART.md', 'INSTALL_NOTES.md',
356
+ 'CLAUDE.md', 'QUICKSTART.md', 'INSTALL_NOTES.md',
360
357
  '.claude/', '.mcp.json',
361
358
  ];
362
359
  const detectedCore = coreFiles.filter((f) => {
@@ -416,13 +413,15 @@ export async function init(options) {
416
413
  .replace('{{PRODUCTION_URL}}', productionUrl)
417
414
  .replace('{{TAGS}}', tagsYaml);
418
415
  writeFileSync(join(cwd, 'skillset.yaml'), skillsetYaml);
419
- // Generate content/README.md (if not copying existing)
420
- if (!existsSync(join(cwd, 'content', 'README.md'))) {
416
+ // Generate content/README_<NAME>.md (if not copying existing)
417
+ // Named README avoids clobbering the user's own README.md on install
418
+ const readmeFilename = `README_${name.toUpperCase()}.md`;
419
+ if (!existsSync(join(cwd, 'content', readmeFilename))) {
421
420
  const readme = README_TEMPLATE
422
421
  .replace(/\{\{NAME\}\}/g, name)
423
422
  .replace(/\{\{DESCRIPTION\}\}/g, description)
424
423
  .replace(/\{\{AUTHOR_HANDLE\}\}/g, authorHandle);
425
- writeFileSync(join(cwd, 'content', 'README.md'), readme);
424
+ writeFileSync(join(cwd, 'content', readmeFilename), readme);
426
425
  }
427
426
  // Generate content/QUICKSTART.md (if not copying existing)
428
427
  if (!existsSync(join(cwd, 'content', 'QUICKSTART.md'))) {
@@ -437,14 +436,19 @@ export async function init(options) {
437
436
  .replace(/\{\{NAME\}\}/g, name);
438
437
  writeFileSync(join(cwd, 'content', 'INSTALL_NOTES.md'), installNotes);
439
438
  }
439
+ // Generate empty content/LICENSE (if not copying existing)
440
+ if (!existsSync(join(cwd, 'content', 'LICENSE'))) {
441
+ writeFileSync(join(cwd, 'content', 'LICENSE'), '');
442
+ }
440
443
  spinner.succeed('Skillset structure created');
441
444
  // Summary
442
445
  console.log(chalk.green('\n✓ Initialized skillset submission:\n'));
443
446
  console.log(' skillset.yaml - Manifest (edit as needed)');
444
447
  console.log(' content/ - Installable files');
445
- console.log(' ├── README.md - Documentation');
448
+ console.log(` ├── ${readmeFilename} - Documentation`);
446
449
  console.log(' ├── QUICKSTART.md - Post-install guide');
447
450
  console.log(' ├── INSTALL_NOTES.md - Pre-install notes');
451
+ console.log(' ├── LICENSE - License (populate before audit)');
448
452
  if (filesToCopy.length > 0) {
449
453
  filesToCopy.forEach((f) => console.log(` └── ${f}`));
450
454
  }
@@ -234,8 +234,9 @@ Submitted via \`npx skillsets submit\`
234
234
  ### Checklist
235
235
 
236
236
  - [x] \`skillset.yaml\` validated against schema
237
- - [x] \`README.md\` with installation and usage instructions
237
+ - [x] \`README_${skillset.name.toUpperCase()}.md\` with installation and usage instructions
238
238
  - [x] \`content/INSTALL_NOTES.md\` with install notes
239
+ - [x] \`content/LICENSE\` populated
239
240
  - [x] \`AUDIT_REPORT.md\` generated and passing
240
241
  - [x] \`content/\` directory with skillset files
241
242
 
@@ -14,7 +14,12 @@ export async function view(skillsetId) {
14
14
  }
15
15
  const [namespace, name] = skillsetId.split('/');
16
16
  const encodedPath = encodeURIComponent(namespace) + '/' + encodeURIComponent(name);
17
- const readmeUrl = `${GITHUB_RAW_BASE}/skillsets/${encodedPath}/content/README.md`;
17
+ const readmeFile = metadata.files
18
+ ? Object.keys(metadata.files).find(f => /^content\/README_[^/]+\.md$/i.test(f))
19
+ : null;
20
+ const readmeUrl = readmeFile
21
+ ? `${GITHUB_RAW_BASE}/skillsets/${encodedPath}/${readmeFile}`
22
+ : `${GITHUB_RAW_BASE}/skillsets/${encodedPath}/content/README.md`;
18
23
  const auditUrl = `${GITHUB_RAW_BASE}/skillsets/${encodedPath}/AUDIT_REPORT.md`;
19
24
  const [readmeResponse, auditResponse] = await Promise.all([
20
25
  fetch(readmeUrl),
@@ -1,4 +1,4 @@
1
1
  export declare const SKILLSET_YAML_TEMPLATE = "schema_version: \"1.0\"\nbatch_id: \"{{BATCH_ID}}\"\n\n# Identity\nname: \"{{NAME}}\"\nversion: \"1.0.0\"\ndescription: \"{{DESCRIPTION}}\"\n\nauthor:\n handle: \"{{AUTHOR_HANDLE}}\"\n url: \"{{AUTHOR_URL}}\"\n\n# Verification\nverification:\n production_links:\n - url: \"{{PRODUCTION_URL}}\"\n audit_report: \"./AUDIT_REPORT.md\"\n\n# Discovery\ntags:\n{{TAGS}}\n\ncompatibility:\n claude_code_version: \">=1.0.0\"\n languages:\n - \"any\"\n\n# Lifecycle\nstatus: \"active\"\n\n# Content\nentry_point: \"./content/CLAUDE.md\"\n";
2
- export declare const README_TEMPLATE = "# {{NAME}}\n\n{{DESCRIPTION}}\n\n## Installation\n\n```bash\nnpx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}\n```\n\n## Usage\n\n[Describe how to use your skillset]\n\n## What's Included\n\n[List the key files and their purposes]\n\n## License\n\n[Your license]\n";
3
- export declare const QUICKSTART_TEMPLATE = "# Quickstart\n\nAfter installing via `npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}`, customize the workflow for your project.\n\n---\n\n## What Was Installed\n\n```\nyour-project/\n\u251C\u2500\u2500 .claude/ # Skills, agents, resources\n\u251C\u2500\u2500 CLAUDE.md # Project config \u2190 START HERE\n\u2514\u2500\u2500 README.md # Documentation\n```\n\n---\n\n## Getting Started\n\n1. **Edit CLAUDE.md** \u2014 Replace placeholder content with your project's specifics\n2. **Customize .claude/** \u2014 Adapt skills, agents, and resources for your stack\n3. **Run** \u2014 `claude` to start using the skillset\n\n---\n\n## Customization Checklist\n\n- [ ] Update Identity & Constraints in CLAUDE.md\n- [ ] Configure style guides in .claude/resources/\n- [ ] Adapt agent definitions in .claude/agents/\n- [ ] Set up any required infrastructure (Docker, API keys, etc.)\n\n---\n\n## Resources\n\n[Add links to documentation, examples, or support channels]\n";
2
+ export declare const README_TEMPLATE = "# {{NAME}}\n\n{{DESCRIPTION}}\n\n## Installation\n\n```bash\nnpx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}\n```\n\n## Usage\n\n[Describe how to use your skillset]\n\n## What's Included\n\n[List the key files and their purposes]\n\n";
3
+ export declare const QUICKSTART_TEMPLATE = "# Quickstart\n\nAfter installing via `npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}`, customize the workflow for your project.\n\n---\n\n## What Was Installed\n\n```\nyour-project/\n\u251C\u2500\u2500 .claude/ # Skills, agents, resources\n\u251C\u2500\u2500 CLAUDE.md # Project config \u2190 START HERE\n\u2514\u2500\u2500 README_{{NAME}}.md # Skillset documentation\n```\n\n---\n\n## Getting Started\n\n1. **Edit CLAUDE.md** \u2014 Replace placeholder content with your project's specifics\n2. **Customize .claude/** \u2014 Adapt skills, agents, and resources for your stack\n3. **Run** \u2014 `claude` to start using the skillset\n\n---\n\n## Customization Checklist\n\n- [ ] Update Identity & Constraints in CLAUDE.md\n- [ ] Configure style guides in .claude/resources/\n- [ ] Adapt agent definitions in .claude/agents/\n- [ ] Set up any required infrastructure (Docker, API keys, etc.)\n\n---\n\n## Resources\n\n[Add links to documentation, examples, or support channels]\n";
4
4
  export declare const INSTALL_NOTES_TEMPLATE = "# {{NAME}}\n\n<!--\nInstall notes for pre-install display. Max 4000 characters total.\nWhat does this skillset do? What should users know before installing?\nThe dependency section below is populated by /audit-skill during review.\n-->\n\n## Dependencies\n\n<!-- Populated automatically by /audit-skill -->\n";
@@ -49,9 +49,6 @@ npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}
49
49
 
50
50
  [List the key files and their purposes]
51
51
 
52
- ## License
53
-
54
- [Your license]
55
52
  `;
56
53
  export const QUICKSTART_TEMPLATE = `# Quickstart
57
54
 
@@ -65,7 +62,7 @@ After installing via \`npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}\`, custo
65
62
  your-project/
66
63
  ├── .claude/ # Skills, agents, resources
67
64
  ├── CLAUDE.md # Project config ← START HERE
68
- └── README.md # Documentation
65
+ └── README_{{NAME}}.md # Skillset documentation
69
66
  \`\`\`
70
67
 
71
68
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsets",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "CLI tool for discovering and installing verified skillsets",
5
5
  "type": "module",
6
6
  "bin": {