* chore: upgrade maintenance scripts to robust PyYAML parsing - Replaces fragile regex frontmatter parsing with PyYAML/yaml library - Ensures multi-line descriptions and complex characters are handled safely - Normalizes quoting and field ordering across all maintenance scripts - Updates validator to strictly enforce description quality * fix: restore and refine truncated skill descriptions - Recovered 223+ truncated descriptions from git history (6.5.0 regression) - Refined long descriptions into concise, complete sentences (<200 chars) - Added missing descriptions for brainstorming and orchestration skills - Manually fixed imagen skill description - Resolved dangling links in competitor-alternatives skill * chore: sync generated registry files and document fixes - Regenerated skills index with normalized forward-slash paths - Updated README and CATALOG to reflect restored descriptions - Documented restoration and script improvements in CHANGELOG.md * fix: restore missing skill and align metadata for full 955 count - Renamed SKILL.MD to SKILL.md in andruia-skill-smith to ensure indexing - Fixed risk level and missing section in andruia-skill-smith - Synchronized all registry files for final 955 skill count * chore(scripts): add cross-platform runners and hermetic test orchestration * fix(scripts): harden utf-8 output and clone target writeability * fix(skills): add missing date metadata for strict validation * chore(index): sync generated metadata dates * fix(catalog): normalize skill paths to prevent CI drift * chore: sync generated registry files * fix: enforce LF line endings for generated registry files
301 lines
11 KiB
Python
301 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Manage skill date_added metadata.
|
|
|
|
Usage:
|
|
python manage_skill_dates.py list # List all skills with their dates
|
|
python manage_skill_dates.py add-missing [--date YYYY-MM-DD] # Add dates to skills without them
|
|
python manage_skill_dates.py add-all [--date YYYY-MM-DD] # Add/update dates for all skills
|
|
python manage_skill_dates.py update <skill-id> YYYY-MM-DD # Update a specific skill's date
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import argparse
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Ensure UTF-8 output for Windows compatibility
|
|
if sys.platform == 'win32':
|
|
import io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
|
|
def get_project_root():
|
|
"""Get the project root directory."""
|
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
import yaml
|
|
|
|
def parse_frontmatter(content):
|
|
"""Parse frontmatter from SKILL.md content using PyYAML."""
|
|
fm_match = re.search(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
|
|
if not fm_match:
|
|
return None, content
|
|
|
|
fm_text = fm_match.group(1)
|
|
try:
|
|
metadata = yaml.safe_load(fm_text) or {}
|
|
return metadata, content
|
|
except yaml.YAMLError as e:
|
|
print(f"⚠️ YAML parsing error: {e}")
|
|
return None, content
|
|
|
|
def reconstruct_frontmatter(metadata):
|
|
"""Reconstruct frontmatter from metadata dict using PyYAML."""
|
|
# Ensure important keys are at the top if they exist
|
|
ordered = {}
|
|
priority_keys = ['id', 'name', 'description', 'category', 'risk', 'source', 'tags', 'date_added']
|
|
|
|
for key in priority_keys:
|
|
if key in metadata:
|
|
ordered[key] = metadata[key]
|
|
|
|
# Add any remaining keys
|
|
for key, value in metadata.items():
|
|
if key not in ordered:
|
|
ordered[key] = value
|
|
|
|
fm_text = yaml.dump(ordered, sort_keys=False, allow_unicode=True, width=1000).strip()
|
|
return f"---\n{fm_text}\n---"
|
|
|
|
def update_skill_frontmatter(skill_path, metadata):
|
|
"""Update a skill's frontmatter with new metadata."""
|
|
try:
|
|
with open(skill_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
old_metadata, body_content = parse_frontmatter(content)
|
|
if old_metadata is None:
|
|
print(f"❌ {skill_path}: Could not parse frontmatter")
|
|
return False
|
|
|
|
# Merge metadata
|
|
old_metadata.update(metadata)
|
|
|
|
# Reconstruct content
|
|
new_frontmatter = reconstruct_frontmatter(old_metadata)
|
|
|
|
# Find where the frontmatter ends in the original content
|
|
fm_end = content.find('---', 3) # Skip first ---
|
|
if fm_end == -1:
|
|
print(f"❌ {skill_path}: Could not locate frontmatter boundary")
|
|
return False
|
|
|
|
body_start = fm_end + 3
|
|
body = content[body_start:]
|
|
|
|
new_content = new_frontmatter + body
|
|
|
|
with open(skill_path, 'w', encoding='utf-8') as f:
|
|
f.write(new_content)
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Error updating {skill_path}: {str(e)}")
|
|
return False
|
|
|
|
def list_skills():
|
|
"""List all skills with their date_added values."""
|
|
skills_dir = os.path.join(get_project_root(), 'skills')
|
|
skills_with_dates = []
|
|
skills_without_dates = []
|
|
|
|
for root, dirs, files in os.walk(skills_dir):
|
|
# Skip hidden/disabled directories
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
|
|
if "SKILL.md" in files:
|
|
skill_name = os.path.basename(root)
|
|
skill_path = os.path.join(root, "SKILL.md")
|
|
|
|
try:
|
|
with open(skill_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
metadata, _ = parse_frontmatter(content)
|
|
if metadata is None:
|
|
continue
|
|
|
|
date_added = metadata.get('date_added', 'N/A')
|
|
|
|
if date_added == 'N/A':
|
|
skills_without_dates.append(skill_name)
|
|
else:
|
|
skills_with_dates.append((skill_name, date_added))
|
|
except Exception as e:
|
|
print(f"⚠️ Error reading {skill_path}: {str(e)}", file=sys.stderr)
|
|
|
|
# Sort by date
|
|
skills_with_dates.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
print(f"\n📅 Skills with Date Added ({len(skills_with_dates)}):")
|
|
print("=" * 60)
|
|
|
|
if skills_with_dates:
|
|
for skill_name, date in skills_with_dates:
|
|
print(f" {date} │ {skill_name}")
|
|
else:
|
|
print(" (none)")
|
|
|
|
print(f"\n⏳ Skills without Date Added ({len(skills_without_dates)}):")
|
|
print("=" * 60)
|
|
|
|
if skills_without_dates:
|
|
for skill_name in sorted(skills_without_dates):
|
|
print(f" {skill_name}")
|
|
else:
|
|
print(" (none)")
|
|
|
|
total = len(skills_with_dates) + len(skills_without_dates)
|
|
percentage = (len(skills_with_dates) / total * 100) if total > 0 else 0
|
|
print(f"\n📊 Coverage: {len(skills_with_dates)}/{total} ({percentage:.1f}%)")
|
|
|
|
def add_missing_dates(date_str=None):
|
|
"""Add date_added to skills that don't have it."""
|
|
if date_str is None:
|
|
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# Validate date format
|
|
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
print(f"❌ Invalid date format: {date_str}. Use YYYY-MM-DD.")
|
|
return False
|
|
|
|
skills_dir = os.path.join(get_project_root(), 'skills')
|
|
updated_count = 0
|
|
skipped_count = 0
|
|
|
|
for root, dirs, files in os.walk(skills_dir):
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
|
|
if "SKILL.md" in files:
|
|
skill_name = os.path.basename(root)
|
|
skill_path = os.path.join(root, "SKILL.md")
|
|
|
|
try:
|
|
with open(skill_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
metadata, _ = parse_frontmatter(content)
|
|
if metadata is None:
|
|
print(f"⚠️ {skill_name}: Could not parse frontmatter, skipping")
|
|
continue
|
|
|
|
if 'date_added' not in metadata:
|
|
if update_skill_frontmatter(skill_path, {'date_added': date_str}):
|
|
print(f"✅ {skill_name}: Added date_added: {date_str}")
|
|
updated_count += 1
|
|
else:
|
|
print(f"❌ {skill_name}: Failed to update")
|
|
else:
|
|
skipped_count += 1
|
|
except Exception as e:
|
|
print(f"❌ Error processing {skill_name}: {str(e)}")
|
|
|
|
print(f"\n✨ Updated {updated_count} skills, skipped {skipped_count} that already had dates")
|
|
return True
|
|
|
|
def add_all_dates(date_str=None):
|
|
"""Add/update date_added for all skills."""
|
|
if date_str is None:
|
|
date_str = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# Validate date format
|
|
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
print(f"❌ Invalid date format: {date_str}. Use YYYY-MM-DD.")
|
|
return False
|
|
|
|
skills_dir = os.path.join(get_project_root(), 'skills')
|
|
updated_count = 0
|
|
|
|
for root, dirs, files in os.walk(skills_dir):
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
|
|
if "SKILL.md" in files:
|
|
skill_name = os.path.basename(root)
|
|
skill_path = os.path.join(root, "SKILL.md")
|
|
|
|
try:
|
|
if update_skill_frontmatter(skill_path, {'date_added': date_str}):
|
|
print(f"✅ {skill_name}: Set date_added: {date_str}")
|
|
updated_count += 1
|
|
else:
|
|
print(f"❌ {skill_name}: Failed to update")
|
|
except Exception as e:
|
|
print(f"❌ Error processing {skill_name}: {str(e)}")
|
|
|
|
print(f"\n✨ Updated {updated_count} skills")
|
|
return True
|
|
|
|
def update_skill_date(skill_name, date_str):
|
|
"""Update a specific skill's date_added."""
|
|
# Validate date format
|
|
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
print(f"❌ Invalid date format: {date_str}. Use YYYY-MM-DD.")
|
|
return False
|
|
|
|
skills_dir = os.path.join(get_project_root(), 'skills')
|
|
skill_path = os.path.join(skills_dir, skill_name, 'SKILL.md')
|
|
|
|
if not os.path.exists(skill_path):
|
|
print(f"❌ Skill not found: {skill_name}")
|
|
return False
|
|
|
|
if update_skill_frontmatter(skill_path, {'date_added': date_str}):
|
|
print(f"✅ {skill_name}: Updated date_added to {date_str}")
|
|
return True
|
|
else:
|
|
print(f"❌ {skill_name}: Failed to update")
|
|
return False
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Manage skill date_added metadata",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python manage_skill_dates.py list
|
|
python manage_skill_dates.py add-missing
|
|
python manage_skill_dates.py add-missing --date 2024-01-15
|
|
python manage_skill_dates.py add-all --date 2025-01-01
|
|
python manage_skill_dates.py update my-skill-name 2024-06-01
|
|
"""
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
|
|
|
|
# list command
|
|
subparsers.add_parser('list', help='List all skills with their date_added values')
|
|
|
|
# add-missing command
|
|
add_missing_parser = subparsers.add_parser('add-missing', help='Add date_added to skills without it')
|
|
add_missing_parser.add_argument('--date', help='Date to use (YYYY-MM-DD), defaults to today')
|
|
|
|
# add-all command
|
|
add_all_parser = subparsers.add_parser('add-all', help='Add/update date_added for all skills')
|
|
add_all_parser.add_argument('--date', help='Date to use (YYYY-MM-DD), defaults to today')
|
|
|
|
# update command
|
|
update_parser = subparsers.add_parser('update', help='Update a specific skill date')
|
|
update_parser.add_argument('skill_name', help='Name of the skill')
|
|
update_parser.add_argument('date', help='Date to set (YYYY-MM-DD)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
return
|
|
|
|
if args.command == 'list':
|
|
list_skills()
|
|
elif args.command == 'add-missing':
|
|
add_missing_dates(args.date)
|
|
elif args.command == 'add-all':
|
|
add_all_dates(args.date)
|
|
elif args.command == 'update':
|
|
update_skill_date(args.skill_name, args.date)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|