mirror of
https://github.com/home-assistant/core.git
synced 2026-01-25 09:02:38 +01:00
160 lines
4.6 KiB
Python
160 lines
4.6 KiB
Python
|
|
#!/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"<!-- Automatically generated by {Path(__file__).name}, do not edit -->\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"<REFERENCE {ref_file.name}>")
|
||
|
|
result_lines.append(ref_content)
|
||
|
|
result_lines.append(f"<END REFERENCE {ref_file.name}>")
|
||
|
|
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))
|