Files
antigravity-skills-reference/tools/scripts/manage_skill_dates.py
sck_0 45844de534 refactor: reorganize repo docs and tooling layout
Consolidate the repository into clearer apps, tools, and layered docs areas so contributors can navigate and maintain it more reliably. Align validation, metadata sync, and CI around the same canonical workflow to reduce drift across local checks and GitHub Actions.
2026-03-06 15:01:38 +01:00

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
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()