Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions sync_ai_rules/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from sync_ai_rules.core.rule_metadata import RuleMetadata
from sync_ai_rules.file_updater import update_documentation_file

_GITATTRIBUTES_HEADER = "# Auto-generated by sync-ai-rules hook. Do not edit.\n"


def get_category(file_path: str, source_dir: str) -> str:
"""Extract category from file path relative to source directory."""
Expand Down Expand Up @@ -58,6 +60,15 @@ def scan_and_parse(parser, source_dir: str, project_root: str) -> List[RuleMetad
return rules


def _write_gitattributes(project_root: str, patterns: List[str]) -> None:
"""Write .gitattributes marking generated files as linguist-generated."""
lines = [_GITATTRIBUTES_HEADER]
lines.extend(f"{p} linguist-generated\n" for p in patterns)
file_path = os.path.join(project_root, ".gitattributes")
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)


def main():
"""Main orchestration: load pipelines → parse → generate → update files."""
# Setup
Expand Down Expand Up @@ -101,6 +112,14 @@ def main():
status = "✓" if success else "✗"
print(f" {status} {message}")

# Collect gitattributes patterns from all generators and write .gitattributes
all_patterns: List[str] = []
for pipeline in plugin_manager.pipelines:
all_patterns.extend(pipeline.generator.gitattributes_patterns)

if all_patterns:
_write_gitattributes(project_root, sorted(set(all_patterns)))

print("\n✓ Rules synchronization completed!")


Expand Down
9 changes: 6 additions & 3 deletions sync_ai_rules/core/generator_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ def generate(self, rules: Dict[str, List[RuleMetadata]], config: Dict[str, Any])
def get_section_markers(self) -> tuple[str, str]:
"""Return start and end markers for auto-generated section."""

@property
def gitattributes_patterns(self) -> List[str]:
"""Glob patterns (repo-root-relative) to mark as linguist-generated in .gitattributes."""
return []

@property
def is_multi_file(self) -> bool:
"""Whether this generator creates files directly via generate_files()."""
return False

def generate_files(
self, rules: Dict[str, List[RuleMetadata]], project_root: str
) -> None:
def generate_files(self, rules: Dict[str, List[RuleMetadata]], project_root: str) -> None:
"""Generate multiple files directly. Only called when is_multi_file is True."""
raise NotImplementedError(
f"{type(self).__name__} sets is_multi_file=True but does not implement generate_files()"
Expand Down
4 changes: 4 additions & 0 deletions sync_ai_rules/generators/base_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def default_filenames(self) -> List[str]:
".github/copilot-instructions.md",
]

@property
def gitattributes_patterns(self) -> List[str]:
return self.default_filenames

def _format_heading(self, category: str) -> str:
"""Format category as heading."""
return category.replace("-", " ").replace("_", " ").title()
Expand Down
12 changes: 7 additions & 5 deletions sync_ai_rules/generators/skills_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ def generate(self, rules: Dict[str, List[RuleMetadata]], config: Dict[str, Any])
total = sum(len(r) for r in rules.values())
return f"Generated {total} skills in {len(rules)} categories"

@property
def gitattributes_patterns(self) -> List[str]:
return [".claude/skills/generated_*/SKILL.md"]

@property
def is_multi_file(self) -> bool:
return True

def generate_files(
self, rules: Dict[str, List[RuleMetadata]], project_root: str
) -> None:
def generate_files(self, rules: Dict[str, List[RuleMetadata]], project_root: str) -> None:
"""Generate skill files as direct children of .claude/skills/."""
skills_root = os.path.join(project_root, _SKILLS_DIR)

Expand Down Expand Up @@ -82,11 +84,11 @@ def _strip_source_prefix(relative_path: str) -> str:
"""Strip the .cursor/rules/ prefix from a rule's relative path."""
prefix = _SOURCE_DIR + os.sep
if relative_path.startswith(prefix):
return relative_path[len(prefix):]
return relative_path[len(prefix) :]
# Also handle forward-slash separators
prefix_fwd = _SOURCE_DIR + "/"
if relative_path.startswith(prefix_fwd):
return relative_path[len(prefix_fwd):]
return relative_path[len(prefix_fwd) :]
return relative_path


Expand Down
4 changes: 4 additions & 0 deletions test/after/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Auto-generated by sync-ai-rules hook. Do not edit.
.claude/skills/generated_*/SKILL.md linguist-generated
.github/copilot-instructions.md linguist-generated
AGENTS.md linguist-generated