| 1 | """SKILL.md metadata helpers — single source of truth for parsing skill frontmatter. |
| 2 | |
| 3 | Centralizes the version regex that previously lived in render.py and was |
| 4 | duplicated in tests/test_plugin_contract.py and tests/test_version_consistency.py. |
| 5 | """ |
| 6 | |
| 7 | import re |
| 8 | from pathlib import Path |
| 9 | |
| 10 | # Matches `version: "x.y.z"`, `version: 'x.y.z'`, or `version: x.y.z` in YAML |
| 11 | # frontmatter. Multiline so the pattern can be applied to a full SKILL.md text. |
| 12 | # Three alternation groups — exactly one captures per successful match. |
| 13 | _VERSION_RE = re.compile( |
| 14 | r'''^version:\s*(?:"([^"]+)"|'([^']+)'|(\S+))\s*$''', |
| 15 | re.MULTILINE, |
| 16 | ) |
| 17 | |
| 18 | |
| 19 | def read_skill_version(skill_md_path: Path) -> str | None: |
| 20 | """Return the version string from a SKILL.md's frontmatter, or None. |
| 21 | |
| 22 | Returns None if the file can't be read (missing, permission, decode error) |
| 23 | or if no `version:` line is found. Accepts double-quoted, single-quoted, |
| 24 | or unquoted YAML version scalars. |
| 25 | """ |
| 26 | try: |
| 27 | text = skill_md_path.read_text(encoding="utf-8") |
| 28 | except (OSError, UnicodeDecodeError): |
| 29 | return None |
| 30 | match = _VERSION_RE.search(text) |
| 31 | if not match: |
| 32 | return None |
| 33 | return match.group(1) or match.group(2) or match.group(3) |
| 34 |