mirror of
https://github.com/tw93/Waza.git
synced 2026-09-14 19:54:25 +08:00
630 lines
24 KiB
Python
Executable File
630 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate Waza distribution metadata from a single source of truth.
|
|
|
|
Source of truth:
|
|
- VERSION top-level version string (single source)
|
|
- skills/<name>/SKILL.md frontmatter name, description, dispatch_intent
|
|
|
|
Generated files:
|
|
- .claude-plugin/marketplace.json full plugin manifest
|
|
- plugins/waza/.codex-plugin/plugin.json
|
|
Codex plugin manifest
|
|
- plugins/waza/skills/ Codex plugin skill mirror
|
|
- plugins/waza/rules/ Codex plugin rule mirror
|
|
- .agents/plugins/marketplace.json Codex repo marketplace
|
|
- README.md install URLs pinned to VERSION
|
|
- package.json npm/Pi package metadata pinned to VERSION
|
|
- skills/*/references/durable-context.md
|
|
direct-install copies of the shared
|
|
durable-context preamble (only skills
|
|
whose SKILL.md links it)
|
|
- scripts/setup-rule.sh default WAZA_REF pinned to VERSION
|
|
- scripts/setup-statusline.sh default WAZA_REF pinned to VERSION
|
|
|
|
Modes:
|
|
--write (default) regenerate every file from source
|
|
--check compare generated bytes against committed files;
|
|
exit non-zero with a diff on mismatch
|
|
|
|
Run as: python3 scripts/build_metadata.py [--check] [--root PATH]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import difflib
|
|
import json
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
from skill_frontmatter import ( # noqa: E402
|
|
iter_codex_plugin_files,
|
|
iter_codex_source_files,
|
|
parse_frontmatter,
|
|
)
|
|
|
|
|
|
# Hand-maintained marketplace/plugin constants. Kept here (not in frontmatter)
|
|
# because they describe the Waza project itself, not any single skill.
|
|
CLAUDE_MARKETPLACE_TOP = {
|
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
"name": "waza",
|
|
"description": (
|
|
"Personal skill collection for Claude Code, Codex, Antigravity, "
|
|
"OpenCode, and Pi: think, check, hunt, ui, read, write, learn, "
|
|
"and health for agent config and AI maintainability audits."
|
|
),
|
|
"owner": {
|
|
"name": "Tw93",
|
|
"email": "hitw93@gmail.com",
|
|
},
|
|
}
|
|
|
|
BUNDLE_DESCRIPTION = (
|
|
"Installs the full Waza toolkit. Registers all eight skills under the "
|
|
"waza namespace, callable as /waza:think, /waza:check, /waza:hunt, "
|
|
"/waza:ui, /waza:read, /waza:write, /waza:learn, and /waza:health. "
|
|
"For one skill on Claude Code v2.1.143 or newer, use /plugin install "
|
|
"waza-<name>@waza."
|
|
)
|
|
|
|
CATEGORY = "development"
|
|
CODEX_CATEGORY = "Developer Tools"
|
|
HOMEPAGE = "https://github.com/tw93/Waza"
|
|
REPOSITORY = "https://github.com/tw93/Waza"
|
|
|
|
AUTHOR = {
|
|
"name": "Tw93",
|
|
"email": "hitw93@gmail.com",
|
|
"url": "https://github.com/tw93",
|
|
}
|
|
|
|
CODEX_DESCRIPTION = (
|
|
"Engineering workflow skills for Codex: think, check, hunt, ui, read, "
|
|
"write, learn, and health."
|
|
)
|
|
# Shared durable-context preamble: source of truth in rules/, copied into each
|
|
# referencing skill's references/ so direct installs resolve the link locally.
|
|
DURABLE_CONTEXT_RULE = Path("rules/durable-context.md")
|
|
DURABLE_CONTEXT_COPY = Path("references/durable-context.md")
|
|
|
|
|
|
def read_version(root: Path) -> str:
|
|
version_file = root / "VERSION"
|
|
if not version_file.exists():
|
|
raise SystemExit(f"ERROR: missing VERSION file at {version_file}")
|
|
version = version_file.read_text().strip()
|
|
if not version:
|
|
raise SystemExit("ERROR: VERSION file is empty")
|
|
return version
|
|
|
|
|
|
def collect_skill_metadata(root: Path) -> list[dict]:
|
|
skill_files = sorted((root / "skills").glob("*/SKILL.md"))
|
|
if not skill_files:
|
|
raise SystemExit("ERROR: no SKILL.md files found under skills/")
|
|
skills: list[dict] = []
|
|
for path in skill_files:
|
|
fields = parse_frontmatter(path)
|
|
if fields["name"] != path.parent.name:
|
|
raise SystemExit(
|
|
f"ERROR: {path} frontmatter name={fields['name']!r} "
|
|
f"!= directory {path.parent.name!r}"
|
|
)
|
|
skills.append(fields)
|
|
return skills
|
|
|
|
|
|
def build_marketplace(version: str, skills: list[dict]) -> dict:
|
|
"""Build the Claude Code plugin marketplace metadata."""
|
|
plugins = [
|
|
{
|
|
"name": "waza",
|
|
"description": BUNDLE_DESCRIPTION,
|
|
"version": version,
|
|
"category": CATEGORY,
|
|
"source": "./",
|
|
"homepage": HOMEPAGE,
|
|
}
|
|
]
|
|
for skill in sorted(skills, key=lambda s: s["name"]):
|
|
plugins.append(
|
|
{
|
|
"name": f"waza-{skill['name']}",
|
|
"description": skill["description"],
|
|
"version": version,
|
|
"category": CATEGORY,
|
|
"source": f"./skills/{skill['name']}",
|
|
"homepage": HOMEPAGE,
|
|
# `skills` field declares which subdirectories under `source`
|
|
# contain SKILL.md entrypoints. "./" means the source dir itself
|
|
# is the skill root. Present on every per-skill entry; absent
|
|
# on the bundle (the bundle uses Claude Code's auto-discovery).
|
|
"skills": ["./"],
|
|
"strict": False,
|
|
}
|
|
)
|
|
return {**CLAUDE_MARKETPLACE_TOP, "plugins": plugins}
|
|
|
|
|
|
def render_json(data: dict) -> str:
|
|
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
|
|
|
|
def build_codex_plugin(version: str) -> dict:
|
|
return {
|
|
"name": "waza",
|
|
"version": version,
|
|
"description": CODEX_DESCRIPTION,
|
|
"author": AUTHOR,
|
|
"homepage": HOMEPAGE,
|
|
"repository": REPOSITORY,
|
|
"license": "MIT",
|
|
"keywords": [
|
|
"codex",
|
|
"skills",
|
|
"engineering-workflow",
|
|
"code-review",
|
|
"debugging",
|
|
"planning",
|
|
"writing",
|
|
],
|
|
"skills": "./skills/",
|
|
"interface": {
|
|
"displayName": "Waza",
|
|
"shortDescription": "Engineering workflow skills for Codex",
|
|
"longDescription": (
|
|
"Waza packages eight engineering habits as Codex skills: "
|
|
"think for planning, check for review, hunt for debugging, "
|
|
"ui for frontend work, read for source intake, write for "
|
|
"prose, learn for domain research, and health for agent "
|
|
"configuration audits."
|
|
),
|
|
"developerName": "Tw93",
|
|
"category": CODEX_CATEGORY,
|
|
"capabilities": [
|
|
"Interactive",
|
|
"Write",
|
|
],
|
|
"websiteURL": HOMEPAGE,
|
|
"defaultPrompt": [
|
|
"Use Waza think to plan this change",
|
|
"Use Waza check to review this diff",
|
|
"Use Waza hunt to debug this failure",
|
|
],
|
|
"brandColor": "#111827",
|
|
},
|
|
}
|
|
|
|
|
|
def build_agy_plugin() -> dict:
|
|
return {
|
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
"name": "waza",
|
|
"description": CODEX_DESCRIPTION.replace("Codex", "Antigravity CLI"),
|
|
}
|
|
|
|
|
|
def build_codex_marketplace() -> dict:
|
|
return {
|
|
"name": "waza",
|
|
"interface": {
|
|
"displayName": "Waza",
|
|
},
|
|
"plugins": [
|
|
{
|
|
"name": "waza",
|
|
"source": {
|
|
"source": "local",
|
|
"path": "./plugins/waza",
|
|
},
|
|
"policy": {
|
|
"installation": "AVAILABLE",
|
|
"authentication": "ON_INSTALL",
|
|
},
|
|
"category": CODEX_CATEGORY,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def build_package_json(version: str) -> str:
|
|
package = {
|
|
"name": "@tw93/waza",
|
|
"version": version,
|
|
"description": (
|
|
"Waza engineering skills for Claude Code, Codex, Antigravity, "
|
|
"OpenCode, Pi, and compatible coding agents."
|
|
),
|
|
"license": "MIT",
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git+https://github.com/tw93/Waza.git",
|
|
},
|
|
"homepage": "https://github.com/tw93/Waza#readme",
|
|
"keywords": [
|
|
"pi-package",
|
|
"agent-skills",
|
|
"waza",
|
|
"claude-code",
|
|
"codex",
|
|
"antigravity",
|
|
"opencode",
|
|
"antigravity-cli"
|
|
],
|
|
"files": [
|
|
"LICENSE",
|
|
"README.md",
|
|
"rules",
|
|
"scripts/setup-rule.sh",
|
|
"scripts/setup-statusline.sh",
|
|
"scripts/statusline.sh",
|
|
"skills",
|
|
"!**/__pycache__/**",
|
|
"!**/*.pyc",
|
|
],
|
|
"publishConfig": {
|
|
"access": "public",
|
|
},
|
|
"pi": {
|
|
# Pi scans a skills directory for both recursive SKILL.md and
|
|
# stray root-level *.md, so exclude the human routing index;
|
|
# it has no frontmatter description and is not a skill.
|
|
"skills": [
|
|
"./skills",
|
|
"!skills/RESOLVER.md",
|
|
],
|
|
},
|
|
}
|
|
return json.dumps(package, indent=2, ensure_ascii=False) + "\n"
|
|
|
|
|
|
ROUTING_TABLE_START = "<!-- routing-table:start -->"
|
|
ROUTING_TABLE_END = "<!-- routing-table:end -->"
|
|
|
|
|
|
def render_dispatcher(template: str, skills: list[dict]) -> str:
|
|
rows = ["| Intent | Skill | File |", "|--------|-------|------|"]
|
|
for skill in sorted(skills, key=lambda s: s["name"]):
|
|
intent = skill.get("dispatch_intent") or ""
|
|
if not intent:
|
|
raise SystemExit(
|
|
f"ERROR: skill {skill['name']} missing dispatch_intent in frontmatter"
|
|
)
|
|
rows.append(f"| {intent} | {skill['name']} | `skills/{skill['name']}/SKILL.md` |")
|
|
table = "\n".join(rows)
|
|
block = f"{ROUTING_TABLE_START}\n{table}\n{ROUTING_TABLE_END}"
|
|
if ROUTING_TABLE_START not in template or ROUTING_TABLE_END not in template:
|
|
raise SystemExit(
|
|
"ERROR: dispatcher template is missing routing-table markers"
|
|
)
|
|
pattern = re.compile(
|
|
re.escape(ROUTING_TABLE_START) + r".*?" + re.escape(ROUTING_TABLE_END),
|
|
re.DOTALL,
|
|
)
|
|
return pattern.sub(block, template)
|
|
|
|
|
|
# README installer entrypoints should follow the latest GitHub release asset.
|
|
# The downloaded scripts themselves still default WAZA_REF to a release tag, so
|
|
# users get a stable install without README churn on every version bump.
|
|
README_INSTALL_URL_RE = re.compile(
|
|
r"https://raw\.githubusercontent\.com/tw93/Waza/"
|
|
r"(?:main|v\d+\.\d+\.\d+)/scripts/(setup-(?:rule|statusline)\.sh)"
|
|
)
|
|
README_SWAP_TAG_RE = re.compile(
|
|
r"Curl URLs are pinned to the current release tag for reproducibility; "
|
|
r"swap `v\d+\.\d+\.\d+` for `main` if you want bleeding-edge scripts\."
|
|
)
|
|
WAZA_REF_RE = re.compile(r'WAZA_REF="\$\{WAZA_REF:-(?:main|v\d+\.\d+\.\d+)\}"')
|
|
|
|
|
|
def render_readme(current: str) -> str:
|
|
current = README_INSTALL_URL_RE.sub(
|
|
r"https://github.com/tw93/Waza/releases/latest/download/\1", current
|
|
)
|
|
return README_SWAP_TAG_RE.sub(
|
|
"Curl URLs use the latest GitHub release asset. Set `WAZA_REF=main` "
|
|
"before the command if you want bleeding-edge scripts.",
|
|
current,
|
|
)
|
|
|
|
|
|
def render_script_ref(current: str, version: str) -> str:
|
|
return WAZA_REF_RE.sub(f'WAZA_REF="${{WAZA_REF:-v{version}}}"', current)
|
|
|
|
|
|
def diff(label: str, expected: str, actual: str) -> str:
|
|
return "".join(
|
|
difflib.unified_diff(
|
|
actual.splitlines(keepends=True),
|
|
expected.splitlines(keepends=True),
|
|
fromfile=f"committed:{label}",
|
|
tofile=f"generated:{label}",
|
|
)
|
|
)
|
|
|
|
|
|
def bytes_diff(label: str, expected: bytes, actual: bytes) -> str:
|
|
return diff(
|
|
label,
|
|
expected.decode("utf-8", errors="replace"),
|
|
actual.decode("utf-8", errors="replace"),
|
|
)
|
|
|
|
|
|
def collect_skill_shared_assets(root: Path) -> dict[str, bytes]:
|
|
"""Per-skill copies of shared assets that direct installs need locally.
|
|
|
|
`npx skills add` copies only each skill directory, so every skill whose
|
|
SKILL.md links the shared durable-context preamble carries a references/
|
|
copy of it.
|
|
"""
|
|
generated: dict[str, bytes] = {}
|
|
durable_source = root / DURABLE_CONTEXT_RULE
|
|
durable_bytes = durable_source.read_bytes() if durable_source.exists() else None
|
|
for skill_file in sorted((root / "skills").glob("*/SKILL.md")):
|
|
skill_dir = skill_file.parent.relative_to(root)
|
|
if DURABLE_CONTEXT_COPY.as_posix() in skill_file.read_text():
|
|
if durable_bytes is None:
|
|
raise SystemExit(
|
|
f"ERROR: {skill_file} links {DURABLE_CONTEXT_COPY} but "
|
|
f"{DURABLE_CONTEXT_RULE} does not exist"
|
|
)
|
|
generated[(skill_dir / DURABLE_CONTEXT_COPY).as_posix()] = durable_bytes
|
|
return generated
|
|
|
|
|
|
def shared_asset_source(rel: str) -> str:
|
|
return DURABLE_CONTEXT_RULE.as_posix()
|
|
|
|
|
|
def collect_codex_plugin_tree(
|
|
root: Path,
|
|
codex_plugin_rendered: str,
|
|
agy_plugin_rendered: str,
|
|
generated_skill_files: dict[str, bytes],
|
|
) -> dict[str, bytes]:
|
|
"""Build the generated file set for the Codex and Antigravity plugin directory.
|
|
|
|
Codex installs only the directory referenced by marketplace source.path, so
|
|
the plugin tree contains real copies of the skill and rule files instead of
|
|
symlinks or references back to the repository root.
|
|
"""
|
|
generated = {
|
|
"plugins/waza/.codex-plugin/plugin.json": codex_plugin_rendered.encode(),
|
|
"plugins/waza/plugin.json": agy_plugin_rendered.encode(),
|
|
}
|
|
for source_name in ("skills", "rules"):
|
|
if not (root / source_name).exists():
|
|
raise SystemExit(
|
|
f"ERROR: missing required Codex plugin source tree {root / source_name}"
|
|
)
|
|
for _source_name, _source_rel, path in iter_codex_source_files(root):
|
|
rel = path.relative_to(root).as_posix()
|
|
generated[f"plugins/waza/{rel}"] = path.read_bytes()
|
|
for rel, content in generated_skill_files.items():
|
|
generated[f"plugins/waza/{rel}"] = content
|
|
return generated
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--root",
|
|
type=Path,
|
|
default=ROOT,
|
|
help="Repository root (default: parent of scripts/)",
|
|
)
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="Compare generated bytes to committed files; exit non-zero on drift.",
|
|
)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
|
|
version = read_version(root)
|
|
skills = collect_skill_metadata(root)
|
|
marketplace = build_marketplace(version, skills)
|
|
rendered = render_json(marketplace)
|
|
codex_plugin_rendered = render_json(build_codex_plugin(version))
|
|
agy_plugin_rendered = render_json(build_agy_plugin())
|
|
codex_marketplace_rendered = render_json(build_codex_marketplace())
|
|
package_rendered = build_package_json(version)
|
|
|
|
target = root / ".claude-plugin" / "marketplace.json"
|
|
codex_marketplace_target = root / ".agents" / "plugins" / "marketplace.json"
|
|
generated_json_files = [
|
|
(target, rendered, "Claude Code marketplace"),
|
|
(codex_marketplace_target, codex_marketplace_rendered, "Codex marketplace"),
|
|
]
|
|
package_json = root / "package.json"
|
|
package_actual = package_json.read_text() if package_json.exists() else ""
|
|
readme = root / "README.md"
|
|
readme_actual = readme.read_text() if readme.exists() else ""
|
|
readme_rendered = render_readme(readme_actual)
|
|
pinned_scripts = [
|
|
(
|
|
root / "scripts" / "setup-rule.sh",
|
|
"default WAZA_REF",
|
|
lambda actual: render_script_ref(actual, version),
|
|
),
|
|
(
|
|
root / "scripts" / "setup-statusline.sh",
|
|
"default WAZA_REF",
|
|
lambda actual: render_script_ref(actual, version),
|
|
),
|
|
]
|
|
script_pairs = []
|
|
for script, field_label, renderer in pinned_scripts:
|
|
actual = script.read_text() if script.exists() else ""
|
|
script_pairs.append((script, field_label, actual, renderer(actual)))
|
|
skill_shared_assets = collect_skill_shared_assets(root)
|
|
codex_plugin_tree = collect_codex_plugin_tree(
|
|
root,
|
|
codex_plugin_rendered,
|
|
agy_plugin_rendered,
|
|
skill_shared_assets,
|
|
)
|
|
|
|
dispatcher_template = root / "scripts" / "dispatcher-template.md"
|
|
dispatcher_target = root / "scripts" / "dispatcher.md"
|
|
if not dispatcher_template.exists():
|
|
raise SystemExit(f"ERROR: missing dispatcher template at {dispatcher_template}")
|
|
dispatcher_actual = (
|
|
dispatcher_target.read_text() if dispatcher_target.exists() else ""
|
|
)
|
|
dispatcher_rendered = render_dispatcher(dispatcher_template.read_text(), skills)
|
|
|
|
if args.check:
|
|
drift = False
|
|
for generated_path, expected, label in generated_json_files:
|
|
actual = generated_path.read_text() if generated_path.exists() else ""
|
|
if actual != expected:
|
|
rel = generated_path.relative_to(root).as_posix()
|
|
print(
|
|
f"DRIFT: {rel} is out of sync with VERSION + "
|
|
f"SKILL.md frontmatter.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(diff(rel, expected, actual))
|
|
drift = True
|
|
for rel, expected in codex_plugin_tree.items():
|
|
path = root / rel
|
|
actual = path.read_bytes() if path.exists() else b""
|
|
if actual != expected:
|
|
print(
|
|
f"DRIFT: {rel} is out of sync with repository skills/rules "
|
|
f"and Codex plugin metadata.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(bytes_diff(rel, expected, actual))
|
|
drift = True
|
|
codex_plugin_root = root / "plugins" / "waza"
|
|
if codex_plugin_root.exists():
|
|
expected_paths = set(codex_plugin_tree)
|
|
for _plugin_rel, path in iter_codex_plugin_files(codex_plugin_root):
|
|
rel = path.relative_to(root).as_posix()
|
|
if rel not in expected_paths:
|
|
print(
|
|
f"DRIFT: {rel} is an extra file in the generated "
|
|
"Codex plugin tree.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
drift = True
|
|
if readme_actual != readme_rendered:
|
|
print(
|
|
"DRIFT: README.md installer URLs must use latest release assets.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(diff("README.md", readme_rendered, readme_actual))
|
|
drift = True
|
|
if package_actual != package_rendered:
|
|
print(
|
|
f"DRIFT: package.json is out of sync with VERSION v{version} "
|
|
f"and Pi package metadata.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(diff("package.json", package_rendered, package_actual))
|
|
drift = True
|
|
for rel, expected in skill_shared_assets.items():
|
|
path = root / rel
|
|
actual = path.read_bytes() if path.exists() else b""
|
|
if actual != expected:
|
|
print(
|
|
f"DRIFT: {rel} is out of sync with {shared_asset_source(rel)}.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(bytes_diff(rel, expected, actual))
|
|
drift = True
|
|
for script, field_label, actual, rendered_script in script_pairs:
|
|
if actual != rendered_script:
|
|
label = script.relative_to(root).as_posix()
|
|
print(
|
|
f"DRIFT: {label} {field_label} is not pinned to v{version}.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(diff(label, rendered_script, actual))
|
|
drift = True
|
|
if dispatcher_actual != dispatcher_rendered:
|
|
label = dispatcher_target.relative_to(root).as_posix()
|
|
print(
|
|
f"DRIFT: {label} routing table is out of sync with "
|
|
f"SKILL.md dispatch_intent frontmatter.\n"
|
|
f"Run scripts/build_metadata.py (no flags) to regenerate.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.stderr.write(diff(label, dispatcher_rendered, dispatcher_actual))
|
|
drift = True
|
|
if drift:
|
|
return 1
|
|
for generated_path, _, _ in generated_json_files:
|
|
print(f"ok: {generated_path.relative_to(root)} matches generator")
|
|
print("ok: plugins/waza Codex plugin tree matches generator")
|
|
print("ok: README.md install URLs use latest release assets")
|
|
print(f"ok: package.json pinned to v{version}")
|
|
print(f"ok: installer defaults pinned to v{version}")
|
|
print("ok: skill-local shared assets match generator")
|
|
print(f"ok: {dispatcher_target.relative_to(root)} matches generator")
|
|
return 0
|
|
|
|
for generated_path, expected, _ in generated_json_files:
|
|
generated_path.parent.mkdir(parents=True, exist_ok=True)
|
|
generated_path.write_text(expected)
|
|
print(f"wrote: {generated_path.relative_to(root)} ({len(expected)} bytes)")
|
|
codex_plugin_root = root / "plugins" / "waza"
|
|
shutil.rmtree(codex_plugin_root, ignore_errors=True)
|
|
for rel, expected in codex_plugin_tree.items():
|
|
path = root / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(expected)
|
|
print(f"wrote: plugins/waza ({len(codex_plugin_tree)} generated files)")
|
|
if package_actual != package_rendered:
|
|
package_json.write_text(package_rendered)
|
|
print(f"wrote: package.json (pinned version to v{version})")
|
|
else:
|
|
print(f"ok: package.json already pinned to v{version}")
|
|
if readme_actual != readme_rendered:
|
|
readme.write_text(readme_rendered)
|
|
print("wrote: README.md (installer URLs use latest release assets)")
|
|
else:
|
|
print("ok: README.md install URLs already use latest release assets")
|
|
for script, field_label, actual, rendered_script in script_pairs:
|
|
if actual != rendered_script:
|
|
script.write_text(rendered_script)
|
|
print(f"wrote: {script.relative_to(root)} ({field_label}=v{version})")
|
|
else:
|
|
print(f"ok: {script.relative_to(root)} {field_label} already pinned")
|
|
for rel, expected in skill_shared_assets.items():
|
|
path = root / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not path.exists() or path.read_bytes() != expected:
|
|
path.write_bytes(expected)
|
|
print(f"wrote: {rel} (copied from {shared_asset_source(rel)})")
|
|
if dispatcher_actual != dispatcher_rendered:
|
|
dispatcher_target.write_text(dispatcher_rendered)
|
|
print(
|
|
f"wrote: {dispatcher_target.relative_to(root)} "
|
|
f"({len(dispatcher_rendered)} bytes)"
|
|
)
|
|
else:
|
|
print(f"ok: {dispatcher_target.relative_to(root)} already matches generator")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|