Files
cli/tools/check-requires.sh
jiantw83 4ed0bf11a2 fix(check-requires): 分開相依不符與判不出結論的結束碼
What:把原本的結束碼 1 拆成兩個。1 只代表相依版本不符或缺相依 plugin。新增 4 代表判不出結論,成因是 manifest 不存在、不是有效 JSON、或缺 python3。這三種成因的輸出從 status=blocked 改成 status=unknown。

Why:兩種成因共用同一個結束碼,呼叫端就只能用同一句話講兩件事。環境壞掉會被講成版本落後。操作者照著去補版本,補到最後也碰不到真正的問題點。

How:找不到 manifest、找不到 python3、JSON 解析失敗三處改回傳 4,狀態字串一併改成 unknown。檔頭的輸出格式表與結束碼表跟著改寫,並寫下 1 與 4 分開的理由。

Who:相依檢查結論分流。
2026-08-31 13:35:41 +08:00

141 lines
5.3 KiB
Bash
Executable File
Raw Permalink 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 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