|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | """ |
3 | 3 | Skills Generator - generates Claude Code skills from .cursor/rules/. |
4 | | -Each rule becomes a separate SKILL.md file in .claude/skills/.generated/, |
5 | | -mirroring the folder structure of .cursor/rules/. |
| 4 | +Each rule becomes a separate SKILL.md file in .claude/skills/, |
| 5 | +with a flat name encoding the full path: generated_<category>_<skill-name>. |
6 | 6 | """ |
7 | 7 |
|
8 | 8 | import logging |
|
14 | 14 | from sync_ai_rules.core.generator_interface import OutputGenerator |
15 | 15 | from sync_ai_rules.core.rule_metadata import RuleMetadata |
16 | 16 |
|
17 | | -_SKILLS_DIR = ".claude/skills/.generated" |
| 17 | +_SKILLS_DIR = ".claude/skills" |
18 | 18 | _SOURCE_DIR = ".cursor/rules" |
19 | 19 | _YAML_UNSAFE = re.compile(r"[:\#\[\]\{\}&*!|>'\"%@`]") |
20 | 20 |
|
@@ -43,30 +43,33 @@ def is_multi_file(self) -> bool: |
43 | 43 | def generate_files( |
44 | 44 | self, rules: Dict[str, List[RuleMetadata]], project_root: str |
45 | 45 | ) -> None: |
46 | | - """Generate skill files mirroring the .cursor/rules/ folder structure.""" |
| 46 | + """Generate skill files as direct children of .claude/skills/.""" |
47 | 47 | skills_root = os.path.join(project_root, _SKILLS_DIR) |
48 | 48 |
|
49 | | - # Clean .generated directory on each run |
50 | | - if os.path.exists(skills_root): |
51 | | - shutil.rmtree(skills_root) |
| 49 | + # Remove any existing generated_* dirs before regenerating |
| 50 | + if os.path.isdir(skills_root): |
| 51 | + for entry in os.listdir(skills_root): |
| 52 | + if entry.startswith("generated_"): |
| 53 | + shutil.rmtree(os.path.join(skills_root, entry)) |
52 | 54 |
|
53 | 55 | for category_rules in rules.values(): |
54 | 56 | for rule in category_rules: |
55 | | - # Mirror the source folder structure: |
56 | | - # .cursor/rules/arch/my-rule.mdc → .generated/arch/my-rule/SKILL.md |
| 57 | + # Flatten full path into a single skill dir name: |
| 58 | + # .cursor/rules/arch/my-rule.mdc → .claude/skills/generated_arch_my-rule/SKILL.md |
| 59 | + # .cursor/rules/my-rule.mdc → .claude/skills/generated_my-rule/SKILL.md |
57 | 60 | rel_path = _strip_source_prefix(rule.relative_path) |
58 | | - skill_name = os.path.splitext(os.path.basename(rel_path))[0] |
59 | | - skill_dir = os.path.join( |
60 | | - skills_root, os.path.dirname(rel_path), skill_name |
61 | | - ) if os.path.dirname(rel_path) else os.path.join(skills_root, skill_name) |
| 61 | + path_parts = rel_path.replace(os.sep, "/").split("/") |
| 62 | + path_parts[-1] = os.path.splitext(path_parts[-1])[0] |
| 63 | + skill_name = "generated_" + "_".join(path_parts) |
| 64 | + skill_dir = os.path.join(skills_root, skill_name) |
62 | 65 | skill_file = os.path.join(skill_dir, "SKILL.md") |
63 | 66 |
|
64 | 67 | try: |
65 | 68 | content = _format_skill(rule, skill_name) |
66 | 69 | os.makedirs(skill_dir, exist_ok=True) |
67 | 70 | with open(skill_file, "w", encoding="utf-8") as f: |
68 | 71 | f.write(content) |
69 | | - print(f" ✓ Created skill: {os.path.relpath(skill_dir, skills_root)}") |
| 72 | + print(f" ✓ Created skill: {skill_name}") |
70 | 73 | except OSError as e: |
71 | 74 | logger.warning("Failed to write skill %s: %s", skill_name, e) |
72 | 75 | print(f" ✗ Failed to create skill: {skill_name}") |
|
0 commit comments