feat(skill-creator): Add path reference validation
- Add find_path_references() to scan SKILL.md for bundled resource paths - Add validate_path_references() to verify referenced files exist - Smart filtering for example/documentation contexts - Update SKILL.md Step 6 with validation details - Bump version to 1.2.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,8 @@ To begin implementation, start with the reusable resources identified above: `sc
|
||||
|
||||
Also, delete any example files and directories not needed for the skill. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
|
||||
|
||||
**When updating an existing skill**: Scan all existing reference files to check if they need corresponding updates. New features often require updates to architecture, workflow, or other existing documentation to maintain consistency.
|
||||
|
||||
#### Reference File Naming
|
||||
|
||||
Filenames must be self-explanatory without reading contents.
|
||||
@@ -276,13 +278,41 @@ The packaging script will:
|
||||
- YAML frontmatter format and required fields
|
||||
- Skill naming conventions and directory structure
|
||||
- Description completeness and quality
|
||||
- File organization and resource references
|
||||
- **Path reference integrity** - all `scripts/`, `references/`, and `assets/` paths mentioned in SKILL.md must exist
|
||||
|
||||
2. **Package** the skill if validation passes, creating a zip file named after the skill (e.g., `my-skill.zip`) that includes all files and maintains the proper directory structure for distribution.
|
||||
|
||||
**Common validation failure:** If SKILL.md references `scripts/my_script.py` but the file doesn't exist, validation will fail with "Missing referenced files: scripts/my_script.py". Ensure all bundled resources exist before packaging.
|
||||
|
||||
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
|
||||
|
||||
### Step 7: Iterate
|
||||
### Step 7: Update Marketplace
|
||||
|
||||
After packaging, update the marketplace registry to include the new or updated skill.
|
||||
|
||||
**For new skills**, add an entry to `.claude-plugin/marketplace.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "skill-name",
|
||||
"description": "Copy from SKILL.md frontmatter description",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"version": "1.0.0",
|
||||
"category": "developer-tools",
|
||||
"keywords": ["relevant", "keywords"],
|
||||
"skills": ["./skill-name"]
|
||||
}
|
||||
```
|
||||
|
||||
**For updated skills**, bump the version in `plugins[].version` following semver:
|
||||
- Patch (1.0.x): Bug fixes, typo corrections
|
||||
- Minor (1.x.0): New features, additional references
|
||||
- Major (x.0.0): Breaking changes, restructured workflows
|
||||
|
||||
**Also update** `metadata.version` and `metadata.description` if the overall plugin collection changed significantly.
|
||||
|
||||
### Step 8: Iterate
|
||||
|
||||
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
|
||||
|
||||
|
||||
@@ -8,33 +8,90 @@ import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_path_references(content: str) -> list[str]:
|
||||
"""
|
||||
Extract path references from SKILL.md content.
|
||||
Looks for patterns like scripts/xxx, references/xxx, assets/xxx
|
||||
|
||||
Filters out:
|
||||
- Placeholder paths (xxx, example, etc.)
|
||||
- Paths in example contexts (lines containing "Example:", "e.g.", etc.)
|
||||
- Generic documentation examples
|
||||
"""
|
||||
# Pattern to match bundled resource paths (scripts/, references/, assets/)
|
||||
pattern = r'(?:scripts|references|assets)/[\w./-]+'
|
||||
|
||||
# Find all matches with their line context
|
||||
unique_paths = set()
|
||||
for line in content.split('\n'):
|
||||
# Skip lines that are clearly examples or documentation
|
||||
line_lower = line.lower()
|
||||
if any(x in line_lower for x in [
|
||||
'example:', 'examples:', 'e.g.', 'for example',
|
||||
'- **example', '- example:', 'such as',
|
||||
'pattern:', 'usage:', '❌', '✅',
|
||||
'- **allowed', '- **best practice', 'would be helpful',
|
||||
'like `scripts/', 'like `references/', 'like `assets/',
|
||||
]):
|
||||
continue
|
||||
|
||||
# Find paths in this line
|
||||
matches = re.findall(pattern, line)
|
||||
for path in matches:
|
||||
# Skip obvious placeholders
|
||||
if any(x in path.lower() for x in ['example', 'xxx', '<', '>', 'my-', 'my_']):
|
||||
continue
|
||||
unique_paths.add(path)
|
||||
|
||||
return list(unique_paths)
|
||||
|
||||
|
||||
def validate_path_references(skill_path: Path, content: str) -> tuple[bool, list[str]]:
|
||||
"""
|
||||
Verify all path references in SKILL.md actually exist.
|
||||
|
||||
Returns:
|
||||
(all_exist, missing_paths)
|
||||
"""
|
||||
referenced_paths = find_path_references(content)
|
||||
missing = []
|
||||
|
||||
for ref_path in referenced_paths:
|
||||
full_path = skill_path / ref_path
|
||||
if not full_path.exists():
|
||||
missing.append(ref_path)
|
||||
|
||||
return len(missing) == 0, missing
|
||||
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Basic validation of a skill"""
|
||||
skill_path = Path(skill_path)
|
||||
|
||||
|
||||
# Check SKILL.md exists
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
|
||||
# Read and validate frontmatter
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith('---'):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
|
||||
# Extract frontmatter
|
||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if not match:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
|
||||
frontmatter = match.group(1)
|
||||
|
||||
|
||||
# Check required fields
|
||||
if 'name:' not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if 'description:' not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
|
||||
# Extract name for validation
|
||||
name_match = re.search(r'name:\s*(.+)', frontmatter)
|
||||
if name_match:
|
||||
@@ -53,6 +110,11 @@ def validate_skill(skill_path):
|
||||
if '<' in description or '>' in description:
|
||||
return False, "Description cannot contain angle brackets (< or >)"
|
||||
|
||||
# Validate path references exist
|
||||
paths_valid, missing_paths = validate_path_references(skill_path, content)
|
||||
if not paths_valid:
|
||||
return False, f"Missing referenced files: {', '.join(missing_paths)}"
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user