Files
yao-meta-skill/scripts/cross_packager.py
T
2026-03-31 19:59:29 +08:00

172 lines
6.4 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import shutil
import zipfile
from pathlib import Path
def read_simple_yaml(path: Path) -> dict:
lines = path.read_text(encoding="utf-8").splitlines()
data: dict = {}
stack: list[tuple[int, dict]] = [(0, data)]
for raw_line in lines:
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip(" "))
line = raw_line.strip()
while len(stack) > 1 and indent <= stack[-1][0]:
stack.pop()
parent = stack[-1][1]
if line.startswith("- "):
item = line[2:].strip().strip("'\"")
existing = parent.setdefault("__list__", [])
existing.append(item)
continue
if ":" not in line:
continue
key, value = line.split(":", 1)
key = key.strip()
value = value.strip()
if value == "":
child: dict = {}
parent[key] = child
stack.append((indent, child))
else:
parent[key] = value.strip("'\"")
return data
def read_frontmatter(skill_md: Path) -> dict:
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
parts = text.split("---", 2)
if len(parts) < 3:
return {}
data = {}
for line in parts[1].splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip("'\"")
return data
def read_interface(skill_dir: Path) -> dict:
path = skill_dir / "agents" / "interface.yaml"
if not path.exists():
return {}
raw = read_simple_yaml(path)
compatibility = raw.get("compatibility", {})
targets = compatibility.get("adapter_targets", {})
if isinstance(targets, dict) and "__list__" in targets:
compatibility["adapter_targets"] = targets["__list__"]
raw["compatibility"] = compatibility
return raw
def build_manifest(skill_dir: Path, platform: str) -> dict:
frontmatter = read_frontmatter(skill_dir / "SKILL.md")
interface = read_interface(skill_dir).get("interface", {})
compatibility = read_interface(skill_dir).get("compatibility", {})
return {
"name": frontmatter.get("name", skill_dir.name),
"description": frontmatter.get("description", ""),
"version": frontmatter.get("version", "1.0.0"),
"platform": platform,
"skill_root": skill_dir.name,
"display_name": interface.get("display_name", skill_dir.name),
"short_description": interface.get("short_description", ""),
"default_prompt": interface.get("default_prompt", ""),
"canonical_metadata": "agents/interface.yaml",
"adapter_targets": compatibility.get("adapter_targets", []),
}
def write_yaml_like(path: Path, payload: dict) -> None:
interface = payload.get("interface", {})
lines = ["interface:"]
for key in ("display_name", "short_description", "default_prompt"):
value = interface.get(key, "")
lines.append(f' {key}: "{value}"')
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_adapter(skill_dir: Path, out_dir: Path, platform: str) -> Path:
target_dir = out_dir / "targets" / platform
target_dir.mkdir(parents=True, exist_ok=True)
payload = build_manifest(skill_dir, platform)
if platform == "openai":
meta_dir = target_dir / "agents"
meta_dir.mkdir(parents=True, exist_ok=True)
write_yaml_like(
meta_dir / "openai.yaml",
{
"interface": {
"display_name": payload["display_name"],
"short_description": payload["short_description"],
"default_prompt": payload["default_prompt"],
}
},
)
payload["install_hint"] = f"Use the packaged skill and include targets/openai/agents/openai.yaml when the client expects OpenAI-style interface metadata."
elif platform == "claude":
notes = target_dir / "README.md"
notes.write_text(
f"# Claude-Compatible Package\n\nUse `{skill_dir.name}` with its neutral source files. This target does not require vendor metadata by default.\n",
encoding="utf-8",
)
payload["install_hint"] = f"Use the packaged skill directly; this target relies on SKILL.md and optional neutral metadata."
else:
payload["install_hint"] = f"Use {skill_dir.name} as an Agent Skills compatible package."
path = target_dir / "adapter.json"
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def make_zip(skill_dir: Path, out_dir: Path) -> Path:
zip_path = out_dir / f"{skill_dir.name}.zip"
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for path in skill_dir.rglob("*"):
if path.is_file():
zf.write(path, arcname=str(path.relative_to(skill_dir.parent)))
return zip_path
def copy_manifest(skill_dir: Path, out_dir: Path) -> Path:
manifest_path = out_dir / "manifest.json"
manifest_path.write_text(
json.dumps(build_manifest(skill_dir, "generic"), ensure_ascii=False, indent=2),
encoding="utf-8",
)
return manifest_path
def main() -> None:
parser = argparse.ArgumentParser(description="Generate lightweight cross-platform packaging artifacts.")
parser.add_argument("skill_dir", help="Path to the skill directory")
parser.add_argument("--platform", action="append", default=[], help="Target platform: openai, claude, generic")
parser.add_argument("--output-dir", default="dist", help="Output directory")
parser.add_argument("--zip", action="store_true", help="Create a zip package")
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
out_dir = Path(args.output_dir).resolve()
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True)
manifest = copy_manifest(skill_dir, out_dir)
generated = [str(manifest)]
for platform in (args.platform or ["generic"]):
generated.append(str(write_adapter(skill_dir, out_dir, platform)))
if args.zip:
generated.append(str(make_zip(skill_dir, out_dir)))
print(json.dumps({"output_dir": str(out_dir), "generated": generated}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()