Files
spec-kit/src/apm_cli/compilation/template_builder.py
2025-09-16 16:08:42 +02:00

138 lines
4.4 KiB
Python

"""Template building system for AGENTS.md compilation."""
import re
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from ..primitives.models import Instruction, Chatmode
@dataclass
class TemplateData:
"""Data structure for template generation."""
instructions_content: str
# Removed volatile timestamp for deterministic builds
version: str
chatmode_content: Optional[str] = None
def build_conditional_sections(instructions: List[Instruction]) -> str:
"""Build sections grouped by applyTo patterns.
Args:
instructions (List[Instruction]): List of instruction primitives.
Returns:
str: Formatted conditional sections content.
"""
if not instructions:
return ""
# Group instructions by pattern - use raw patterns
pattern_groups = _group_instructions_by_pattern(instructions)
sections = []
for pattern, pattern_instructions in pattern_groups.items():
sections.append(f"## Files matching `{pattern}`")
sections.append("")
# Combine content from all instructions for this pattern
for instruction in pattern_instructions:
content = instruction.content.strip()
if content:
# Add source file comment before the content
try:
# Try to get relative path for cleaner display
if instruction.file_path.is_absolute():
relative_path = instruction.file_path.relative_to(Path.cwd())
else:
relative_path = instruction.file_path
except (ValueError, OSError):
# Fall back to absolute or given path if relative fails
relative_path = instruction.file_path
sections.append(f"<!-- Source: {relative_path} -->")
sections.append(content)
sections.append(f"<!-- End source: {relative_path} -->")
sections.append("")
return "\n".join(sections)
def find_chatmode_by_name(chatmodes: List[Chatmode], chatmode_name: str) -> Optional[Chatmode]:
"""Find a chatmode by name.
Args:
chatmodes (List[Chatmode]): List of available chatmodes.
chatmode_name (str): Name of the chatmode to find.
Returns:
Optional[Chatmode]: The found chatmode, or None if not found.
"""
for chatmode in chatmodes:
if chatmode.name == chatmode_name:
return chatmode
return None
def _group_instructions_by_pattern(instructions: List[Instruction]) -> Dict[str, List[Instruction]]:
"""Group instructions by applyTo patterns.
Args:
instructions (List[Instruction]): List of instructions to group.
Returns:
Dict[str, List[Instruction]]: Grouped instructions with raw patterns as keys.
"""
pattern_groups: Dict[str, List[Instruction]] = {}
for instruction in instructions:
if not instruction.apply_to:
continue
pattern = instruction.apply_to
if pattern not in pattern_groups:
pattern_groups[pattern] = []
pattern_groups[pattern].append(instruction)
return pattern_groups
def generate_agents_md_template(template_data: TemplateData) -> str:
"""Generate the complete AGENTS.md file content.
Args:
template_data (TemplateData): Data for template generation.
Returns:
str: Complete AGENTS.md file content.
"""
sections = []
# Header
sections.append("# AGENTS.md")
sections.append(f"<!-- Generated by APM CLI from .apm/ primitives -->")
from .constants import BUILD_ID_PLACEHOLDER
sections.append(BUILD_ID_PLACEHOLDER)
sections.append(f"<!-- APM Version: {template_data.version} -->")
sections.append("")
# Chatmode content (if provided)
if template_data.chatmode_content:
sections.append(template_data.chatmode_content.strip())
sections.append("")
# Instructions content (grouped by patterns)
if template_data.instructions_content:
sections.append(template_data.instructions_content)
# Footer
sections.append("---")
sections.append("*This file was generated by APM CLI. Do not edit manually.*")
sections.append("*To regenerate: `specify apm compile`*")
sections.append("")
return "\n".join(sections)