#!/usr/bin/env python3 """le-app-design 硬依赖门:解析并活性检查三个上游 Skill。 用法: python3 check_deps.py [--out /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 结果写入路径(如 /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())