126 lines
3.8 KiB
Python
Executable File
126 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate Codex Game Studios plugin structure and package hygiene."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FORBIDDEN_NAMES = {".DS_Store", ".env", "plan.md"}
|
|
FORBIDDEN_PARTS = {".git", "__pycache__"}
|
|
REQUIRED_SKILLS = {
|
|
"brainstorm",
|
|
"setup-runtime",
|
|
"using-codex-game-studios",
|
|
}
|
|
REQUIRED_RUNTIME_AGENTS = {
|
|
"art-director.toml",
|
|
"creative-director.toml",
|
|
"producer.toml",
|
|
"technical-director.toml",
|
|
}
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
print(f"ERROR: {message}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def run_git(args: list[str]) -> list[str]:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
cwd=ROOT,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(result.stderr.strip() or "git command failed")
|
|
return [line for line in result.stdout.splitlines() if line]
|
|
|
|
|
|
def assert_manifest() -> None:
|
|
manifest_path = ROOT / ".codex-plugin" / "plugin.json"
|
|
if not manifest_path.is_file():
|
|
fail("Missing .codex-plugin/plugin.json")
|
|
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
if manifest.get("name") != "codex-game-studios":
|
|
fail("plugin.json name must be codex-game-studios")
|
|
if manifest.get("skills") != "./skills/":
|
|
fail('plugin.json must expose skills via "./skills/"')
|
|
|
|
for prompt in manifest.get("interface", {}).get("defaultPrompt", []):
|
|
if len(prompt) > 128:
|
|
fail(f"defaultPrompt exceeds 128 chars: {prompt}")
|
|
|
|
|
|
def assert_skill(skill_name: str) -> None:
|
|
skill_dir = ROOT / "skills" / skill_name
|
|
skill_path = skill_dir / "SKILL.md"
|
|
agent_path = skill_dir / "agents" / "openai.yaml"
|
|
|
|
if not skill_path.is_file():
|
|
fail(f"Missing skill file: {skill_path.relative_to(ROOT)}")
|
|
if not agent_path.is_file():
|
|
fail(f"Missing skill UI metadata: {agent_path.relative_to(ROOT)}")
|
|
|
|
text = skill_path.read_text(encoding="utf-8")
|
|
if not text.startswith("---\n"):
|
|
fail(f"Missing YAML frontmatter: {skill_path.relative_to(ROOT)}")
|
|
if f"name: {skill_name}" not in text:
|
|
fail(f"Skill name mismatch in {skill_path.relative_to(ROOT)}")
|
|
if "description:" not in text.split("---", 2)[1]:
|
|
fail(f"Missing skill description in {skill_path.relative_to(ROOT)}")
|
|
|
|
|
|
def assert_runtime_agents() -> None:
|
|
agent_dir = ROOT / "runtime" / "agents"
|
|
if not agent_dir.is_dir():
|
|
fail("Missing runtime/agents directory")
|
|
|
|
present = {path.name for path in agent_dir.glob("*.toml")}
|
|
missing = sorted(REQUIRED_RUNTIME_AGENTS - present)
|
|
if missing:
|
|
fail(f"Missing runtime agents: {', '.join(missing)}")
|
|
|
|
|
|
def assert_package_hygiene() -> None:
|
|
try:
|
|
tracked = run_git(["ls-files"])
|
|
untracked = run_git(["ls-files", "--others", "--exclude-standard"])
|
|
candidates = tracked + untracked
|
|
except RuntimeError:
|
|
candidates = [
|
|
str(path.relative_to(ROOT))
|
|
for path in ROOT.rglob("*")
|
|
if path.is_file() or path.is_dir()
|
|
]
|
|
|
|
bad: list[str] = []
|
|
for rel_path in candidates:
|
|
path = Path(rel_path)
|
|
if path.name in FORBIDDEN_NAMES or FORBIDDEN_PARTS.intersection(path.parts):
|
|
bad.append(rel_path)
|
|
|
|
if bad:
|
|
fail("Forbidden package candidates:\n- " + "\n- ".join(sorted(bad)))
|
|
|
|
|
|
def main() -> int:
|
|
assert_manifest()
|
|
for skill_name in sorted(REQUIRED_SKILLS):
|
|
assert_skill(skill_name)
|
|
assert_runtime_agents()
|
|
assert_package_hygiene()
|
|
print("Codex Game Studios plugin validation passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|