Files
humanizer/scripts/validate-package.py
T
Cursor Agentandjakub-zieba 9f2cc4b29f Add a Cursor plugin manifest for marketplace submission
Package Humanizer as a Cursor plugin without moving the root SKILL.md, so Claude, the Skills CLI, and Cursor can all load the same prompt.

Co-authored-by: jakub-zieba <jakub-zieba@users.noreply.github.com>
2026-09-27 21:20:45 -07:00

128 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check Humanizer's package files without external dependencies."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def read_package_file(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except OSError as error:
raise SystemExit(f"Cannot read {path.relative_to(ROOT)}: {error}")
SKILL_PATH = ROOT / "SKILL.md"
SKILL = read_package_file(SKILL_PATH)
README = read_package_file(ROOT / "README.md")
try:
PLUGIN = json.loads(read_package_file(ROOT / ".claude-plugin" / "plugin.json"))
except json.JSONDecodeError as error:
raise SystemExit(f"Fix the JSON in .claude-plugin/plugin.json: {error}")
try:
CURSOR_PLUGIN = json.loads(
read_package_file(ROOT / ".cursor-plugin" / "plugin.json")
)
except json.JSONDecodeError as error:
raise SystemExit(f"Fix the JSON in .cursor-plugin/plugin.json: {error}")
def require_match(match: re.Match[str] | None, message: str) -> re.Match[str]:
if match is None:
raise SystemExit(message)
return match
yaml_metadata = require_match(
re.match(r"\A---\n(.*?)\n---\n", SKILL, re.DOTALL),
"SKILL.md must begin with YAML metadata",
).group(1)
for unsupported_field in ("version:", "compatibility:", "allowed-tools:"):
if re.search(rf"(?m)^{re.escape(unsupported_field)}", yaml_metadata):
raise SystemExit(f"Remove unsupported YAML field: {unsupported_field[:-1]}")
skill_version = require_match(
re.search(r'(?m)^\s+version:\s*["\']?([0-9]+\.[0-9]+\.[0-9]+)["\']?\s*$', yaml_metadata),
"Add metadata.version to SKILL.md as a three-part version",
).group(1)
readme_version = require_match(
re.search(r"(?m)^- \*\*([0-9]+\.[0-9]+\.[0-9]+)\*\*", README),
"Add a version entry to README.md",
).group(1)
package_versions = {
skill_version,
readme_version,
str(PLUGIN.get("version", "")),
str(CURSOR_PLUGIN.get("version", "")),
}
if len(package_versions) != 1:
raise SystemExit(
f"Use one package version in all files: {sorted(package_versions)}"
)
skill_files = {path.relative_to(ROOT) for path in ROOT.rglob("SKILL.md")}
if SKILL_PATH.is_symlink() or skill_files != {Path("SKILL.md")}:
raise SystemExit("Keep one regular SKILL.md at the repo root")
if PLUGIN.get("skills") != ["./"]:
raise SystemExit("Point the Claude plugin skill loader at the repo root")
if CURSOR_PLUGIN.get("name") != "humanizer":
raise SystemExit("Set the Cursor plugin name to humanizer")
if "skills" in CURSOR_PLUGIN:
raise SystemExit("Omit skills from the Cursor plugin so it loads the root SKILL.md")
pattern_numbers = [
int(number)
for number in re.findall(r"(?m)^### ([0-9]+)\. ", SKILL)
]
pattern_count = len(pattern_numbers)
if pattern_count == 0 or pattern_numbers != list(range(1, pattern_count + 1)):
raise SystemExit(f"Number SKILL.md patterns from 1 upward without gaps: {pattern_numbers}")
readme_numbers = [
int(number) for number in re.findall(r"(?m)^\| ([0-9]+) \|", README)
]
if sorted(readme_numbers) != pattern_numbers:
raise SystemExit(
f"List patterns 1 through {pattern_count} once each in the README tables: {sorted(readme_numbers)}"
)
if f"## The {pattern_count} patterns" not in README:
raise SystemExit(f"Title the README pattern section 'The {pattern_count} patterns'")
# A renumber can leave a section reference pointing at the wrong pattern or at
# nothing. README references sit in older version notes, so read SKILL.md only.
skill_references = sorted({int(number) for number in re.findall(r"§([0-9]+)", SKILL)})
missing_patterns = [
number for number in skill_references if not 1 <= number <= pattern_count
]
if missing_patterns:
raise SystemExit(
f"Point every SKILL.md section reference at a pattern from 1 to "
f"{pattern_count}: {missing_patterns}"
)
dash_pattern = require_match(
re.search(r"(?m)^### ([0-9]+)\. [^\n]*[Dd]ashes", SKILL),
"Name the dash pattern in one SKILL.md heading",
).group(1)
sample_dash_rule = require_match(
re.search(r"(?m)^.*if the sample uses dashes.*$", SKILL),
"Keep the voice rule that lets a writing sample keep its dashes",
).group(0)
if f"§{dash_pattern}" not in sample_dash_rule:
raise SystemExit(
f"Point the sample-dash rule at §{dash_pattern}, the dash pattern"
)
if len(SKILL.splitlines()) > 400:
raise SystemExit("Keep SKILL.md at 400 lines or fewer")
print(f"Humanizer package v{skill_version} is valid")