#!/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 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 import yaml from _project_paths import find_repo_root # 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 str(find_repo_root(__file__)) 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()