重构插件入口与runtime安装流程
This commit is contained in:
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install Codex Game Studios runtime files into a game project.
|
||||
|
||||
Current runtime scope:
|
||||
- Copy bundled Codex custom agents from this plugin's `.codex/agents/`
|
||||
directory into the target project's `.codex/agents/` directory.
|
||||
|
||||
This installer intentionally does not install hooks, rules, MCP servers, apps,
|
||||
or AGENTS.md templates yet.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import filecmp
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_AGENTS_DIR = PLUGIN_ROOT / ".codex" / "agents"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallResult:
|
||||
installed: list[Path]
|
||||
unchanged: list[Path]
|
||||
conflicts: list[Path]
|
||||
|
||||
|
||||
def relative_to_target(path: Path, target: Path) -> str:
|
||||
try:
|
||||
return str(path.relative_to(target))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def discover_agent_files() -> list[Path]:
|
||||
if not SOURCE_AGENTS_DIR.exists():
|
||||
raise FileNotFoundError(f"Source agent directory not found: {SOURCE_AGENTS_DIR}")
|
||||
|
||||
agent_files = sorted(SOURCE_AGENTS_DIR.glob("*.toml"))
|
||||
if not agent_files:
|
||||
raise FileNotFoundError(f"No custom agent TOML files found in: {SOURCE_AGENTS_DIR}")
|
||||
|
||||
return agent_files
|
||||
|
||||
|
||||
def install_agents(target: Path, *, force: bool, dry_run: bool) -> InstallResult:
|
||||
agent_files = discover_agent_files()
|
||||
target_agents_dir = target / ".codex" / "agents"
|
||||
|
||||
planned: list[tuple[Path, Path]] = []
|
||||
unchanged: list[Path] = []
|
||||
conflicts: list[Path] = []
|
||||
|
||||
for source in agent_files:
|
||||
destination = target_agents_dir / source.name
|
||||
|
||||
if source.resolve() == destination.resolve():
|
||||
unchanged.append(destination)
|
||||
continue
|
||||
|
||||
if destination.exists():
|
||||
if filecmp.cmp(source, destination, shallow=False):
|
||||
unchanged.append(destination)
|
||||
continue
|
||||
|
||||
if not force:
|
||||
conflicts.append(destination)
|
||||
continue
|
||||
|
||||
planned.append((source, destination))
|
||||
|
||||
if conflicts and not force:
|
||||
return InstallResult(installed=[], unchanged=unchanged, conflicts=conflicts)
|
||||
|
||||
installed = [destination for _, destination in planned]
|
||||
if not dry_run:
|
||||
target_agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
for source, destination in planned:
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
return InstallResult(installed=installed, unchanged=unchanged, conflicts=conflicts)
|
||||
|
||||
|
||||
def print_path_list(label: str, paths: list[Path], target: Path) -> None:
|
||||
if not paths:
|
||||
return
|
||||
print(f"{label}:")
|
||||
for path in paths:
|
||||
print(f"- {relative_to_target(path, target)}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Target game project root. Defaults to the current directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite existing agent files when their content differs.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would be installed without writing files.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
target = Path(args.target).expanduser().resolve()
|
||||
|
||||
print("Codex Game Studios runtime installer")
|
||||
print(f"Plugin root: {PLUGIN_ROOT}")
|
||||
print(f"Target project: {target}")
|
||||
print("Runtime scope: custom agents only")
|
||||
|
||||
try:
|
||||
result = install_agents(target, force=args.force, dry_run=args.dry_run)
|
||||
except FileNotFoundError as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print_path_list("Installed" if not args.dry_run else "Would install", result.installed, target)
|
||||
print_path_list("Unchanged", result.unchanged, target)
|
||||
print_path_list("Conflicts", result.conflicts, target)
|
||||
|
||||
if result.conflicts:
|
||||
print(
|
||||
"ERROR: Existing agent files differ. Re-run with --force to overwrite them.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
print("Done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
125
scripts/validate_plugin.py
Executable file
125
scripts/validate_plugin.py
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user