#!/usr/bin/env python3 """Generate .github/copilot-instructions.md from AGENTS.md and skills. Necessary until copilot can handle skills. """ from __future__ import annotations from pathlib import Path import re import sys GENERATED_MESSAGE = ( f"\n\n" ) SKILLS_DIR = Path(".claude/skills") AGENTS_FILE = Path("AGENTS.md") OUTPUT_FILE = Path(".github/copilot-instructions.md") # Pattern to match markdown links to local files: [text](filename) # Excludes URLs (http://, https://) and anchors (#) LOCAL_LINK_PATTERN = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") def expand_file_references(content: str, skill_dir: Path) -> str: """Expand file references in skill content. Finds markdown links to local files and replaces them with the file content wrapped in reference tags. """ lines = content.split("\n") result_lines: list[str] = [] for line in lines: result_lines.append(line) matches = list(LOCAL_LINK_PATTERN.finditer(line)) if not matches: continue # Check if any match is a local file reference for match in matches: link_path = match.group(2) # Skip URLs and anchors if link_path.startswith(("http://", "https://", "#", "/")): continue # Try to find the referenced file ref_file = skill_dir / link_path if ref_file.exists(): ref_content = ref_file.read_text().strip() result_lines.append(f"") result_lines.append(ref_content) result_lines.append(f"") result_lines.append("") break return "\n".join(result_lines) def gather_skills() -> list[tuple[str, str]]: """Gather all skills from the skills directory. Returns a list of tuples (skill_name, skill_content). """ skills: list[tuple[str, str]] = [] if not SKILLS_DIR.exists(): return skills for skill_dir in sorted(SKILLS_DIR.iterdir()): if not skill_dir.is_dir(): continue skill_file = skill_dir / "SKILL.md" if not skill_file.exists(): continue skill_content = skill_file.read_text() # Extract skill name from frontmatter if present skill_name = skill_dir.name if skill_content.startswith("---"): # Parse YAML frontmatter end_idx = skill_content.find("---", 3) if end_idx != -1: frontmatter = skill_content[3:end_idx] for line in frontmatter.split("\n"): if line.startswith("name:"): skill_name = line[5:].strip() break # Remove frontmatter from content skill_content = skill_content[end_idx + 3 :].strip() # Expand file references in the skill content skill_content = expand_file_references(skill_content, skill_dir) skills.append((skill_name, skill_content)) return skills def generate_output() -> str: """Generate the copilot-instructions.md content.""" if not AGENTS_FILE.exists(): print(f"Error: {AGENTS_FILE} not found") sys.exit(1) output_parts: list[str] = [GENERATED_MESSAGE] # Add AGENTS.md content agents_content = AGENTS_FILE.read_text() output_parts.append(agents_content.strip()) output_parts.append("") # Add each skill skills = gather_skills() for skill_name, skill_content in skills: output_parts.append("") output_parts.append(f"# Skill: {skill_name}") output_parts.append("") output_parts.append(skill_content) output_parts.append("") return "\n".join(output_parts) def main(validate: bool = False) -> int: """Run the script.""" if not Path("homeassistant").is_dir(): print("Run this from HA root dir") return 1 content = generate_output() if validate: if not OUTPUT_FILE.exists(): print(f"Error: {OUTPUT_FILE} does not exist") return 1 existing = OUTPUT_FILE.read_text() if existing != content: print(f"Error: {OUTPUT_FILE} is out of date") print("Please run: python -m script.gen_copilot_instructions") return 1 print(f"{OUTPUT_FILE} is up to date") return 0 OUTPUT_FILE.write_text(content) print(f"Generated {OUTPUT_FILE}") return 0 if __name__ == "__main__": _validate = len(sys.argv) > 1 and sys.argv[1] == "validate" sys.exit(main(_validate))