|
20 | 20 | - live-session manifests: same name, description, version, homepage and |
21 | 21 | license as each other, the marketplace entries, and (homepage, license) the |
22 | 22 | core plugin |
| 23 | +- .codex-plugin/plugin.json: same name, description, version, homepage, |
| 24 | + license and keywords as the others; its interface websiteURL, privacyPolicyURL |
| 25 | + and termsOfServiceURL are the site, its privacy/ and its terms/ pages, and |
| 26 | + build_site.py renders both pages (HOL's registry scan reads them, #511); its |
| 27 | + skills path and icon files exist |
23 | 28 |
|
24 | 29 | Run: python tests/check_manifest_meta.py (exit 0 ok, 1 on mismatch) |
25 | 30 | """ |
26 | 31 | from __future__ import annotations |
27 | 32 |
|
| 33 | +import ast |
28 | 34 | import html |
29 | 35 | import json |
30 | 36 | import re |
@@ -119,6 +125,40 @@ def check() -> list[str]: |
119 | 125 | if live_cursor.get(key) != cursor.get(key): |
120 | 126 | errors.append(f"live-session {key} {live_cursor.get(key)!r} != core {cursor.get(key)!r}") |
121 | 127 |
|
| 128 | + codex = load(".codex-plugin/plugin.json") |
| 129 | + for key in ("name", "description", "version", "homepage", "license"): |
| 130 | + if codex.get(key) != cursor.get(key): |
| 131 | + errors.append(f".codex-plugin/plugin.json {key} {codex.get(key)!r} != " |
| 132 | + f".cursor-plugin/plugin.json {cursor.get(key)!r}") |
| 133 | + if set(codex.get("keywords", [])) != kw_claude: |
| 134 | + errors.append(".codex-plugin/plugin.json keywords differ from .claude-plugin/plugin.json") |
| 135 | + if "mcpServers" in codex: |
| 136 | + errors.append(".codex-plugin/plugin.json declares mcpServers; the MCP server belongs to " |
| 137 | + "the opt-in live-session/ plugin (#483)") |
| 138 | + skills = codex.get("skills", "") |
| 139 | + if not isinstance(skills, str) or not (root / skills).is_dir(): |
| 140 | + errors.append(f".codex-plugin/plugin.json skills {skills!r} is not a directory") |
| 141 | + # Read LEGAL_PAGES without importing build_site.py (it needs Jinja2). |
| 142 | + tree = ast.parse((root / "scripts" / "site" / "build_site.py").read_text(encoding="utf-8")) |
| 143 | + LEGAL_PAGES = next((ast.literal_eval(n.value) for n in tree.body if isinstance(n, ast.Assign) |
| 144 | + and any(getattr(t, "id", "") == "LEGAL_PAGES" for t in n.targets)), ()) |
| 145 | + iface = codex.get("interface") or {} |
| 146 | + site_root = home.rstrip("/") + "/" |
| 147 | + want = {"websiteURL": site_root, |
| 148 | + "privacyPolicyURL": f"{site_root}privacy/", |
| 149 | + "termsOfServiceURL": f"{site_root}terms/"} |
| 150 | + for key, url in want.items(): |
| 151 | + if iface.get(key) != url: |
| 152 | + errors.append(f".codex-plugin/plugin.json interface.{key} {iface.get(key)!r} != {url!r}") |
| 153 | + for slug in ("privacy", "terms"): |
| 154 | + if slug not in LEGAL_PAGES: |
| 155 | + errors.append(f"scripts/site/build_site.py LEGAL_PAGES does not render {slug}/, " |
| 156 | + "which the Codex manifest links") |
| 157 | + for key in ("composerIcon", "logo"): |
| 158 | + path = iface.get(key) |
| 159 | + if path and not (root / path).is_file(): |
| 160 | + errors.append(f".codex-plugin/plugin.json interface.{key} {path!r} does not exist") |
| 161 | + |
122 | 162 | mcp = live_cursor.get("mcpServers") |
123 | 163 | if not isinstance(mcp, str) or not (root / "live-session" / mcp).is_file(): |
124 | 164 | errors.append(f"live-session mcpServers {mcp!r} is not a file in live-session/") |
@@ -147,7 +187,8 @@ def main() -> int: |
147 | 187 | if errors: |
148 | 188 | return 1 |
149 | 189 | print("Manifest metadata consistent: description, homepage, license, keywords, logo, " |
150 | | - "live-session plugin and its pinned MCP server") |
| 190 | + "Codex manifest and its site/privacy/terms URLs, live-session plugin and its " |
| 191 | + "pinned MCP server") |
151 | 192 | return 0 |
152 | 193 |
|
153 | 194 |
|
|
0 commit comments