skillsets 0.1.0 → 0.1.2

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.
@@ -78,6 +78,27 @@ function isBinaryFile(filePath) {
78
78
  return false;
79
79
  }
80
80
  }
81
+ function scanReadmeLinks(cwd) {
82
+ const readmePath = join(cwd, 'README.md');
83
+ if (!existsSync(readmePath))
84
+ return [];
85
+ const relativeLinks = [];
86
+ const content = readFileSync(readmePath, 'utf-8');
87
+ const lines = content.split('\n');
88
+ // Match markdown links: [text](url)
89
+ const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
90
+ for (let i = 0; i < lines.length; i++) {
91
+ let match;
92
+ while ((match = linkRegex.exec(lines[i])) !== null) {
93
+ const url = match[2];
94
+ // Flag relative links to content/.claude/ that aren't using GitHub URLs
95
+ if (url.startsWith('content/.claude/') || url.startsWith('./content/.claude/')) {
96
+ relativeLinks.push({ line: i + 1, link: url });
97
+ }
98
+ }
99
+ }
100
+ return relativeLinks;
101
+ }
81
102
  function scanForSecrets(dir) {
82
103
  const secrets = [];
83
104
  const files = getAllFiles(dir);
@@ -167,6 +188,7 @@ function generateReport(results, cwd) {
167
188
  results.contentStructure.status === 'PASS' &&
168
189
  results.fileSize.status !== 'FAIL' &&
169
190
  results.secrets.status === 'PASS' &&
191
+ results.readmeLinks.status === 'PASS' &&
170
192
  results.versionCheck.status === 'PASS';
171
193
  const submissionType = results.isUpdate
172
194
  ? `Update (${results.existingVersion} → ${results.skillsetVersion})`
@@ -197,6 +219,7 @@ function generateReport(results, cwd) {
197
219
  | File Size Check | ${statusIcon(results.fileSize.status)} | ${results.fileSize.details} |
198
220
  | Binary Detection | ${statusIcon(results.binary.status)} | ${results.binary.details} |
199
221
  | Secret Detection | ${statusIcon(results.secrets.status)} | ${results.secrets.details} |
222
+ | README Links | ${statusIcon(results.readmeLinks.status)} | ${results.readmeLinks.details} |
200
223
  | Version Check | ${statusIcon(results.versionCheck.status)} | ${results.versionCheck.details} |
201
224
 
202
225
  ---
@@ -236,6 +259,12 @@ ${results.secrets.findings || 'No secrets detected.'}
236
259
 
237
260
  ${results.secretsFound.length > 0 ? '**Potential Secrets Found:**\n' + results.secretsFound.map(s => `- ${s.file}:${s.line} (${s.pattern})`).join('\n') : ''}
238
261
 
262
+ ### 7. README Link Check
263
+
264
+ ${results.readmeLinks.findings || 'All links use valid GitHub URLs.'}
265
+
266
+ ${results.relativeLinks.length > 0 ? '**Relative Links Found:**\n' + results.relativeLinks.map(l => `- Line ${l.line}: ${l.link}`).join('\n') : ''}
267
+
239
268
  ---
240
269
 
241
270
  ## File Inventory
@@ -285,11 +314,13 @@ export async function audit() {
285
314
  binary: { status: 'PASS', details: '' },
286
315
  secrets: { status: 'PASS', details: '' },
287
316
  versionCheck: { status: 'PASS', details: '' },
317
+ readmeLinks: { status: 'PASS', details: '' },
288
318
  isUpdate: false,
289
319
  files: [],
290
320
  largeFiles: [],
291
321
  binaryFiles: [],
292
322
  secretsFound: [],
323
+ relativeLinks: [],
293
324
  };
294
325
  // 1. Manifest validation
295
326
  spinner.text = 'Validating manifest...';
@@ -396,7 +427,20 @@ export async function audit() {
396
427
  findings: 'Remove all API keys, tokens, and passwords before submitting.',
397
428
  };
398
429
  }
399
- // 7. Version check (for updates)
430
+ // 7. README link check
431
+ spinner.text = 'Checking README links...';
432
+ results.relativeLinks = scanReadmeLinks(cwd);
433
+ if (results.relativeLinks.length === 0) {
434
+ results.readmeLinks = { status: 'PASS', details: 'All links valid' };
435
+ }
436
+ else {
437
+ results.readmeLinks = {
438
+ status: 'FAIL',
439
+ details: `${results.relativeLinks.length} relative link(s)`,
440
+ findings: 'README links to content/.claude/ must use full GitHub URLs.\nFormat: https://github.com/skillsets-cc/main/blob/main/skillsets/%40username/skillset-name/content/.claude/...',
441
+ };
442
+ }
443
+ // 8. Version check (for updates)
400
444
  spinner.text = 'Checking registry...';
401
445
  if (results.skillsetName && results.authorHandle) {
402
446
  const skillsetId = `${results.authorHandle}/${results.skillsetName}`;
@@ -441,6 +485,7 @@ export async function audit() {
441
485
  results.contentStructure.status === 'PASS' &&
442
486
  results.fileSize.status !== 'FAIL' &&
443
487
  results.secrets.status === 'PASS' &&
488
+ results.readmeLinks.status === 'PASS' &&
444
489
  results.versionCheck.status === 'PASS';
445
490
  console.log('\n' + chalk.bold('Audit Summary:'));
446
491
  console.log('');
@@ -457,6 +502,7 @@ export async function audit() {
457
502
  console.log(` ${icon(results.fileSize.status)} File Sizes: ${results.fileSize.details}`);
458
503
  console.log(` ${icon(results.binary.status)} Binary Files: ${results.binary.details}`);
459
504
  console.log(` ${icon(results.secrets.status)} Secrets: ${results.secrets.details}`);
505
+ console.log(` ${icon(results.readmeLinks.status)} README Links: ${results.readmeLinks.details}`);
460
506
  console.log(` ${icon(results.versionCheck.status)} Version: ${results.versionCheck.details}`);
461
507
  console.log('');
462
508
  if (allPassed) {
@@ -76,7 +76,7 @@ This skillset has been verified in production.
76
76
  [List projects or products built using this skillset]
77
77
  `;
78
78
  const AUDIT_SKILL_MD = `---
79
- name: skillset-audit
79
+ name: audit-skill
80
80
  description: Qualitative review of skillset content against Claude Code best practices. Evaluates all primitives (skills, agents, hooks, MCP, CLAUDE.md) for proper frontmatter, descriptions, and structure. Appends analysis to AUDIT_REPORT.md.
81
81
  ---
82
82
 
@@ -419,8 +419,8 @@ export async function init(options) {
419
419
  // Generate PROOF.md
420
420
  const proof = PROOF_TEMPLATE.replace('{{PRODUCTION_URL}}', productionUrl);
421
421
  writeFileSync(join(cwd, 'PROOF.md'), proof);
422
- // Install skillset-audit skill to .claude/skills/
423
- const skillDir = join(cwd, '.claude', 'skills', 'skillset-audit');
422
+ // Install audit-skill skill to .claude/skills/
423
+ const skillDir = join(cwd, '.claude', 'skills', 'audit-skill');
424
424
  mkdirSync(skillDir, { recursive: true });
425
425
  writeFileSync(join(skillDir, 'SKILL.md'), AUDIT_SKILL_MD);
426
426
  writeFileSync(join(skillDir, 'CRITERIA.md'), AUDIT_CRITERIA_MD);
@@ -438,12 +438,12 @@ export async function init(options) {
438
438
  console.log(' └── (add your .claude/ and/or CLAUDE.md here)');
439
439
  }
440
440
  console.log(' .claude/skills/ - Audit skill installed');
441
- console.log(' └── skillset-audit/');
441
+ console.log(' └── audit-skill/');
442
442
  console.log(chalk.cyan('\nNext steps:'));
443
443
  console.log(' 1. Edit PROOF.md with production evidence');
444
444
  console.log(' 2. Ensure content/ has your skillset files');
445
445
  console.log(' 3. Run: npx skillsets audit');
446
- console.log(' 4. Run: /skillset-audit (qualitative review)');
446
+ console.log(' 4. Run: /audit-skill [AUDIT_REPORT.md] [path/to/reference-repo]');
447
447
  }
448
448
  catch (error) {
449
449
  spinner.fail('Failed to create structure');
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { execSync } from 'child_process';
3
+ import { execSync, spawnSync } from 'child_process';
4
4
  import { existsSync, readFileSync, mkdirSync, cpSync, rmSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import yaml from 'js-yaml';
@@ -59,11 +59,17 @@ function parseSkillsetYaml(cwd) {
59
59
  try {
60
60
  const content = readFileSync(yamlPath, 'utf-8');
61
61
  const data = yaml.load(content);
62
- return {
63
- name: data.name,
64
- author: data.author?.handle?.replace('@', ''),
65
- version: data.version,
66
- };
62
+ const name = data.name;
63
+ const author = data.author?.handle?.replace('@', '');
64
+ const version = data.version;
65
+ // Validate format to prevent command injection
66
+ if (!name || !/^[A-Za-z0-9_-]+$/.test(name))
67
+ return null;
68
+ if (!author || !/^[A-Za-z0-9_-]+$/.test(author))
69
+ return null;
70
+ if (!version || !/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version))
71
+ return null;
72
+ return { name, author, version };
67
73
  }
68
74
  catch {
69
75
  return null;
@@ -185,7 +191,7 @@ export async function submit() {
185
191
  execSync(`gh repo clone ${REGISTRY_REPO} "${tempDir}" -- --depth=1`, { stdio: 'ignore' });
186
192
  // Create branch
187
193
  spinner.text = 'Creating branch...';
188
- execSync(`git checkout -b "${branchName}"`, { cwd: tempDir, stdio: 'ignore' });
194
+ spawnSync('git', ['checkout', '-b', branchName], { cwd: tempDir, stdio: 'ignore' });
189
195
  // Create skillset directory
190
196
  const skillsetDir = join(tempDir, 'skillsets', `@${skillset.author}`, skillset.name);
191
197
  mkdirSync(skillsetDir, { recursive: true });
@@ -199,14 +205,14 @@ export async function submit() {
199
205
  }
200
206
  // Commit
201
207
  spinner.text = 'Committing changes...';
202
- execSync('git add .', { cwd: tempDir, stdio: 'ignore' });
208
+ spawnSync('git', ['add', '.'], { cwd: tempDir, stdio: 'ignore' });
203
209
  const commitMsg = isUpdate
204
210
  ? `Update ${skillsetId} to v${skillset.version}`
205
211
  : `Add ${skillsetId}`;
206
- execSync(`git commit -m "${commitMsg}"`, { cwd: tempDir, stdio: 'ignore' });
212
+ spawnSync('git', ['commit', '-m', commitMsg], { cwd: tempDir, stdio: 'ignore' });
207
213
  // Push to fork
208
214
  spinner.text = 'Pushing to fork...';
209
- execSync(`git push -u origin "${branchName}" --force`, { cwd: tempDir, stdio: 'ignore' });
215
+ spawnSync('git', ['push', '-u', 'origin', branchName, '--force'], { cwd: tempDir, stdio: 'ignore' });
210
216
  // Create PR
211
217
  spinner.text = 'Creating pull request...';
212
218
  const prTitle = isUpdate
@@ -254,13 +260,19 @@ _Add any additional context for reviewers here._
254
260
  ---
255
261
  Submitted via \`npx skillsets submit\`
256
262
  `;
257
- const prResult = execSync(`gh pr create --repo ${REGISTRY_REPO} --title "${prTitle}" --body "${prBody.replace(/"/g, '\\"')}"`, { cwd: tempDir, encoding: 'utf-8' });
263
+ const prResult = spawnSync('gh', ['pr', 'create', '--repo', REGISTRY_REPO, '--title', prTitle, '--body', prBody], {
264
+ cwd: tempDir,
265
+ encoding: 'utf-8',
266
+ });
267
+ if (prResult.status !== 0) {
268
+ throw new Error(prResult.stderr || 'Failed to create PR');
269
+ }
258
270
  // Cleanup
259
271
  spinner.text = 'Cleaning up...';
260
272
  rmSync(tempDir, { recursive: true, force: true });
261
273
  spinner.succeed('Pull request created');
262
274
  // Extract PR URL from result
263
- const prUrl = prResult.trim();
275
+ const prUrl = (prResult.stdout || '').trim();
264
276
  console.log(chalk.green(`\n✓ ${isUpdate ? 'Update' : 'Submission'} complete!\n`));
265
277
  console.log(` Skillset: ${chalk.bold(skillsetId)}`);
266
278
  if (isUpdate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsets",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool for discovering and installing verified skillsets",
5
5
  "type": "module",
6
6
  "bin": {