#!/usr/bin/env python3 """Le SPEC 依赖检查:skill 目录 + openspec CLI。 用法: python3 deps_check.py # 人读输出, 退出码 0=required 全就位, 1=有缺失 python3 deps_check.py --json # 机器可读 """ import argparse import json import os import shutil import subprocess import sys from pathlib import Path # 依赖清单: (name, level, source, install_cmd) SKILL_DEPS = [ ("grilling", "required", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill grilling grill-me grill-with-docs domain-modeling -g"), ("domain-modeling", "recommended", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill domain-modeling -g"), ("grill-me", "optional", "mattpocock/skills", None), # 入口薄壳 ("grill-with-docs", "optional", "mattpocock/skills", None), # 入口薄壳 ("think", "required", "tw93/Waza", "npx skills@latest add tw93/Waza --skill think -g"), ("openspec-propose", "recommended", "Fission-AI/OpenSpec", "npx skills@latest add Fission-AI/OpenSpec --skill openspec-propose openspec-new-change openspec-update-change openspec-verify-change openspec-archive-change openspec-apply-change openspec-explore -g"), ("openspec-new-change", "recommended", "Fission-AI/OpenSpec", None), ("openspec-update-change", "recommended", "Fission-AI/OpenSpec", None), ("openspec-verify-change", "recommended", "Fission-AI/OpenSpec", None), ("openspec-archive-change", "recommended", "Fission-AI/OpenSpec", None), ] # 候选 skill 根目录(顺序即优先级); 可用 LESPEC_SKILL_PATHS 覆盖, 冒号分隔 DEFAULT_ROOTS = [ "~/.agents/skills", "~/.zcode/skills", "~/.claude/skills", "~/.codex/skills", ] def skill_roots(): env = os.environ.get("LESPEC_SKILL_PATHS") if env: return [Path(p).expanduser() for p in env.split(":") if p.strip()] return [Path(p).expanduser() for p in DEFAULT_ROOTS] def find_skill(name): """返回 (resolved_skill_md_path | None, root_dir)。软链解析到真源。""" for root in skill_roots(): cand = root / name / "SKILL.md" if cand.exists(): try: return cand.resolve(), root except OSError: return cand, root return None, None def check_openspec_cli(): exe = shutil.which("openspec") if not exe: return {"name": "openspec-cli", "level": "required", "ok": False, "detail": "openspec not in PATH", "install": "npm install -g @fission-ai/openspec@latest"} try: out = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=15) ver = (out.stdout or out.returncode and out.stderr or "").strip().splitlines()[-1] \ if (out.stdout or out.stderr) else "?" return {"name": "openspec-cli", "level": "required", "ok": out.returncode == 0, "detail": f"{exe} (v{ver})", "install": "npm install -g @fission-ai/openspec@latest"} except Exception as e: # timeout / spawn 失败 return {"name": "openspec-cli", "level": "required", "ok": False, "detail": f"exec failed: {e}", "install": "npm install -g @fission-ai/openspec@latest"} def main(): ap = argparse.ArgumentParser() ap.add_argument("--json", action="store_true", help="JSON 输出") args = ap.parse_args() results = [] for name, level, source, install in SKILL_DEPS: path, root = find_skill(name) results.append({ "name": name, "kind": "skill", "level": level, "ok": path is not None, "detail": str(path) if path else f"not found under {':'.join(str(r) for r in skill_roots())}", "source": source, "install": install, }) results.append(check_openspec_cli()) missing_required = [r for r in results if not r["ok"] and r["level"] == "required"] missing_recommended = [r for r in results if not r["ok"] and r["level"] == "recommended"] if args.json: print(json.dumps({ "ok": not missing_required, "missing_required": [r["name"] for r in missing_required], "missing_recommended": [r["name"] for r in missing_recommended], "items": results, }, ensure_ascii=False, indent=2)) else: mark = lambda r: "✅" if r["ok"] else ("❌" if r["level"] == "required" else "⚠️ ") print(f"{'':2}{'状态':4} {'级别':12} {'依赖':24} 详情") for r in results: print(f"{mark(r)} {r['level']:<12} {r['name']:<24} {r['detail']}") print() if not missing_required and not missing_recommended: print("全部依赖就位。") for r in missing_required + missing_recommended: if r.get("install"): print(f"补装 {r['name']}: {r['install']}") print("提示: optional 级(grill-me/grill-with-docs)是入口薄壳, 缺失不影响运行。") sys.exit(1 if missing_required else 0) if __name__ == "__main__": main()