#!/usr/bin/env sh # check-requires.sh — 更新單一 plugin 之前,檢查它宣告的 jsc.requires 最低版本。 # 用法: # check-requires.sh {claude|codex|copilot|antigravity|kiro} {manifest} # 輸出(單行,可供程式判讀): # status=ok reason={沒有宣告相依版本|相依版本符合:...} # status=blocked reason={缺哪一個 plugin、差哪一版} # status=unknown reason={manifest 讀不到、不是有效 JSON,或缺 python3} # 結束碼:0=通過(沒有宣告相依,或全部符合) # 1=相依版本不符或缺相依 plugin # 2=用法錯誤(參數個數不對,或 CLI 代號不在五個之內) # 4=判不出結論(manifest 不存在、不是有效 JSON、缺 python3) # 1 與 4 分開的理由:兩者都不是「通過」,但成因完全不同。混成同一個碼,呼叫端就只能 # 用同一句話講兩件事,環境壞掉會被說成版本落後,操作者照著去補版本永遠補不到問題點。 # deploy.sh update 在每個 domain 更新前呼叫一次,但擋下不代表跳過:deploy.sh 照樣更新, # 只印一行提醒說缺哪一版。跳過會讓落後的 domain 永遠更新不到,形成死鎖。 # 真正的阻擋在 jsc-hooks 的 version-guard.sh,技能被叫用時才擋。 set -u usage() { echo "用法:check-requires.sh {claude|codex|copilot|antigravity|kiro} {manifest}" >&2 exit 2 } [ "$#" -eq 2 ] || usage CLI=$1 MANIFEST=$2 case "$CLI" in claude|codex|copilot|antigravity|kiro) ;; *) usage ;; esac [ -f "$MANIFEST" ] || { printf 'status=unknown reason=找不到 manifest:%s\n' "$MANIFEST" exit 4 } command -v python3 >/dev/null 2>&1 || { # 直接讓 shell 回 127 的話,呼叫端會看到一個沒宣告過的結束碼,也讀不到原因。 printf 'status=unknown reason=找不到 python3,無法解析 manifest:%s\n' "$MANIFEST" exit 4 } JSC_HOME_DIR="${JSC_HOME:-$HOME/.jsc}" LOCAL_DIR="${JSC_LOCAL_PLUGINS:-$JSC_HOME_DIR/plugins}" KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}" python3 - "$CLI" "$MANIFEST" "$LOCAL_DIR" "$KIRO_SKILLS" <<'PY' import glob import json import os import re import sys cli, manifest, local_dir, kiro_skills = sys.argv[1:5] try: with open(manifest, encoding="utf-8") as fh: data = json.load(fh) except Exception as exc: print(f"status=unknown reason=manifest 不是有效 JSON:{manifest}:{exc}") sys.exit(4) requires = ((data.get("jsc") or {}).get("requires") or {}) if not requires: print("status=ok reason=沒有宣告相依版本") sys.exit(0) def version_tuple(value): value = str(value or "").strip() parts = value.split(".") out = [] for part in parts[:3]: match = re.match(r"^(\d+)", part) out.append(int(match.group(1)) if match else 0) while len(out) < 3: out.append(0) return tuple(out) def current_version(plugin): domain = plugin[4:] if plugin.startswith("jsc-") else plugin candidates = [] patterns = [] if cli == "claude": registry = os.path.expanduser("~/.claude/plugins/installed_plugins.json") try: with open(registry, encoding="utf-8") as fh: entries = (json.load(fh).get("plugins") or {}).get(f"jsc-{domain}@jsc") or [] for entry in entries: path = os.path.join(entry.get("installPath") or "", "plugin.json") with open(path, encoding="utf-8") as fh: version = json.load(fh).get("version", "") if version: return version, path except Exception: pass patterns.append(os.path.expanduser(f"~/.claude/plugins/cache/jsc/jsc-{domain}/*/plugin.json")) elif cli == "codex": patterns.append(os.path.expanduser(f"~/.codex/plugins/cache/jsc/jsc-{domain}/*/plugin.json")) elif cli == "copilot": patterns.append(os.path.expanduser(f"~/.copilot/installed-plugins/jsc/jsc-{domain}/plugin.json")) elif cli == "antigravity": patterns.append(os.path.join(local_dir, domain, "plugin.json")) patterns.append(os.path.expanduser(f"~/.antigravity/plugins/jsc-{domain}/plugin.json")) elif cli == "kiro": patterns.append(os.path.join(kiro_skills, f"jsc-{domain}", "plugin.json")) for pattern in patterns: for path in glob.glob(pattern): try: with open(path, encoding="utf-8") as fh: version = json.load(fh).get("version", "") except Exception: continue if version: candidates.append((version_tuple(version), version, path)) if not candidates: return "", "" candidates.sort() return candidates[-1][1], candidates[-1][2] failures = [] for plugin, constraint in sorted(requires.items()): required = str(constraint).strip() minimum = required[2:].strip() if required.startswith(">=") else required current, path = current_version(plugin) if not current: failures.append(f"{plugin} 需要 {required},目前未安裝") continue if version_tuple(current) < version_tuple(minimum): failures.append(f"{plugin} 需要 {required},目前 {current}({path})") if failures: print("status=blocked reason=" + ";".join(failures)) sys.exit(1) print("status=ok reason=相依版本符合:" + ", ".join(f"{k} {v}" for k, v in sorted(requires.items()))) sys.exit(0) PY