Files
MetaSkill/skills/le-app-design/scripts/check_deps.py
T

224 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""le-app-design 硬依赖门:解析并活性检查三个上游 Skill。
用法:
python3 check_deps.py [--out <RUN>/deps.json] [--quiet]
解析顺序: $LE_APP_DESIGN_DEP_ROOTS(冒号分隔) > ~/.agents/skills > ~/.zcode/skills
> ~/.codex/skills > ~/.claude/skills
(优先 ~/.agents/skills:impeccable 的 SKILL.md 自引用该路径。)
活性检查按依赖形态各自不同——这就是「真实调用形态」的机器定义:
ui-ux-pro-max 真跑一次 search.py 检索(stdout 含结果段即活)
frontend-design 校验 SKILL.md 原文完整可读(纯文档型 Skill,装载即调用)
impeccable 跑其自包含二进制 --version
全部通过 exit 0;任何缺失/失活 exit 1 并输出明确报告。仅 python3 标准库。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
SKILL_KEY = "le-app-design-dep-check"
UPSTREAMS = [
{
"key": "ui-ux-pro-max",
"repo": "nextlevelbuilder/ui-ux-pro-max-skill",
"license": "MIT",
"install": "git clone https://github.com/nextlevelbuilder/ui-ux-pro-max-skill && 拷贝/链接其 .claude/skills/ui-ux-pro-max 到 ~/.agents/skills/ui-ux-pro-max",
"kind": "cli-search",
},
{
"key": "frontend-design",
"repo": "anthropics/skills",
"license": "Apache-2.0",
"install": "git clone https://github.com/anthropics/skills && 拷贝/链接 skills/frontend-design 到 ~/.agents/skills/frontend-design",
"kind": "doc-guidance",
},
{
"key": "impeccable",
"repo": "pbakaus/impeccable",
"license": "Apache-2.0",
"install": "git clone https://github.com/pbakaus/impeccable && 拷贝/链接到 ~/.agents/skills/impeccable(含 scripts/ 自包含二进制)",
"kind": "cli-engine",
},
]
FRONTMATTER_MIN_BYTES = 2000 # frontend-design 原文约 10KB;残缺副本在此拦下
def candidate_roots() -> list[Path]:
roots: list[Path] = []
env = os.environ.get("LE_APP_DESIGN_DEP_ROOTS", "")
for part in env.split(os.pathsep):
if part.strip():
roots.append(Path(part).expanduser())
home = Path.home()
roots += [
home / ".agents" / "skills",
home / ".zcode" / "skills",
home / ".codex" / "skills",
home / ".claude" / "skills",
]
seen: set[str] = set()
unique: list[Path] = []
for r in roots:
key = str(r)
if key not in seen:
seen.add(key)
unique.append(r)
return unique
def read_frontmatter_name(skill_md: Path) -> str | None:
try:
text = skill_md.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
m = re.match(r"^---\s*\n(.*?\n)?---", text, re.DOTALL)
if not m:
return None
block = m.group(1) or ""
nm = re.search(r"^name:\s*['\"]?([A-Za-z0-9_-]+)", block, re.MULTILINE)
return nm.group(1) if nm else None
def resolve(key: str) -> tuple[Path | None, str]:
"""返回 (解析到的目录, 说明)。"""
for root in candidate_roots():
dep_dir = root / key
skill_md = dep_dir / "SKILL.md"
if skill_md.is_file():
name = read_frontmatter_name(skill_md)
if name == key:
return dep_dir, f"resolved at {dep_dir}"
return dep_dir, f"SKILL.md frontmatter name={name!r} != {key!r}(目录在但身份不符)"
return None, "not found in any candidate root"
def _run(cmd: list[str], timeout: int = 90) -> subprocess.CompletedProcess:
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
return subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout, env=env
)
def liveness(spec: dict, dep_dir: Path) -> tuple[bool, str]:
key, kind = spec["key"], spec["kind"]
try:
if kind == "cli-search":
script = dep_dir / "scripts" / "search.py"
if not script.is_file():
return False, f"missing {script}"
with tempfile.TemporaryDirectory() as td:
proc = subprocess.run(
[sys.executable, str(script), "mobile app", "--domain", "style", "-n", "1"],
capture_output=True, text=True, timeout=90, cwd=td,
env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"),
)
if proc.returncode != 0:
return False, f"search.py exit={proc.returncode}: {proc.stderr.strip()[:300]}"
if "Result" not in proc.stdout:
return False, "search.py exit=0 but no result section in stdout"
return True, "search.py live query ok"
if kind == "doc-guidance":
skill_md = dep_dir / "SKILL.md"
try:
data = skill_md.read_bytes()
except OSError as e:
return False, f"unreadable SKILL.md: {e}"
if len(data) < FRONTMATTER_MIN_BYTES:
return False, f"SKILL.md only {len(data)} bytes (<{FRONTMATTER_MIN_BYTES}); likely truncated copy"
text = data.decode("utf-8", errors="replace")
if "name: frontend-design" not in text or "description:" not in text:
return False, "SKILL.md frontmatter incomplete"
return True, f"original SKILL.md intact ({len(data)} bytes, sha256 {hashlib.sha256(data).hexdigest()[:12]}…)"
if kind == "cli-engine":
launcher = dep_dir / "scripts" / "impeccable"
if not launcher.is_file():
return False, f"missing launcher {launcher}"
proc = _run(["sh", str(launcher), "--version"])
if proc.returncode != 0:
return False, f"impeccable --version exit={proc.returncode}: {proc.stderr.strip()[:300]}"
version = (proc.stdout or "").strip() or (proc.stderr or "").strip()
return True, f"impeccable binary ok ({version[:60]})"
except subprocess.TimeoutExpired:
return False, f"{key} liveness check timed out"
except OSError as e:
return False, f"liveness check error: {e}"
return False, "unknown kind"
def main() -> int:
ap = argparse.ArgumentParser(description="le-app-design 依赖门")
ap.add_argument("--out", help="JSON 结果写入路径(如 <RUN>/deps.json)")
ap.add_argument("--quiet", action="store_true", help="只输出摘要一行")
args = ap.parse_args()
result = {
"skill": SKILL_KEY,
"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"roots_searched": [str(r) for r in candidate_roots()],
"deps": {},
"ok": False,
}
for spec in UPSTREAMS:
dep_dir, note = resolve(spec["key"])
entry = {
"repo": spec["repo"],
"license": spec["license"],
"kind": spec["kind"],
"path": str(dep_dir) if dep_dir else None,
"alive": False,
"check": note if dep_dir is None else "",
"install": spec["install"],
}
if dep_dir is not None:
alive, check = liveness(spec, dep_dir)
entry["alive"] = alive
entry["check"] = check
result["deps"][spec["key"]] = entry
result["ok"] = all(d["alive"] for d in result["deps"].values())
if args.out:
out_path = Path(args.out).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
lines = []
for key, d in result["deps"].items():
mark = "✅" if d["alive"] else "❌"
where = d["path"] or "未找到"
lines.append(f"{mark} {key:18} {where}")
if not d["alive"]:
lines.append(f" 原因: {d['check']}")
lines.append(f" 安装: {d['install']}")
verdict = "依赖门通过(三方解析且活性正常)" if result["ok"] else "依赖门未通过——禁止继续设计流程"
if args.quiet:
print(f"{'PASS' if result['ok'] else 'FAIL'}: " + ", ".join(
f"{k}={'ok' if v['alive'] else 'MISSING'}" for k, v in result["deps"].items()))
else:
print("\n".join(lines))
print(verdict)
if args.out:
print(f"JSON: {args.out}")
return 0 if result["ok"] else 1
if __name__ == "__main__":
sys.exit(main())