feat(version-guard): 技能使用前檢查版本,落後遠端就擋下並提示更新
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-8
@@ -1,22 +1,70 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{ "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" start" } ] }
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" start"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{ "hooks": [
|
||||
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\"" },
|
||||
{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check" }
|
||||
] }
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/ste100-guard.sh\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/sdlc-gate.sh\" check"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{ "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" } ] }
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{ "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark" } ] }
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-timer.sh\" mark"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Skill",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/version-guard.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{ "matcher": "Skill", "hooks": [ { "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/skill-usage.sh\"" } ] }
|
||||
{
|
||||
"matcher": "Skill",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/skill-usage.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env sh
|
||||
# version-guard.sh — 技能使用前的版本前置檢查(PreToolUse,matcher: Skill)。
|
||||
#
|
||||
# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。
|
||||
#
|
||||
# 判準與取值:
|
||||
# - 比對對象是「遠端發佈版本」與「本機**實際載入**的版本」。
|
||||
# 實際載入版本要從 installed_plugins.json 的 installPath 讀該版目錄下的
|
||||
# plugin.json,不能只看註冊在 installed_plugins.json 的版本欄位——那兩者
|
||||
# 可能不同,只看註冊值會放過真正被載入的舊版。
|
||||
# - 只擋落後。本機版本等於或超前遠端一律放行:開發技能組時本機本來就會
|
||||
# 超前 master,擋下去會讓維護者自己動不了。
|
||||
# - 遠端版本查不到(離線、站台維護、repo 改名)一律**擋**(fail-closed),
|
||||
# 避免「查不到就當作沒事」而讓落後版本靜靜跑下去。逃生門見下。
|
||||
#
|
||||
# 豁免(這些技能永遠放行):
|
||||
# jsc-cli:deploy 更新整組技能的入口,擋了就沒有任何方法更新,會死鎖
|
||||
# jsc-hooks:hooks-install 更新後要重新接線,擋了會讓更新做一半卡住
|
||||
# jsc-cli:models SDLC 閘門依賴它產生 model-tags.tsv
|
||||
# jsc-meta:* 開發技能組本身的工具,擋了就修不了技能組
|
||||
#
|
||||
# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。
|
||||
#
|
||||
# 快取:$JSC_HOME/version-cache/{domain},單行「{版本} {epoch}」,
|
||||
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
read_stdin
|
||||
|
||||
[ "${JSC_VERSION_GUARD:-}" = "off" ] && exit 0
|
||||
|
||||
# 只管 Skill 工具
|
||||
tool=$(json_str tool_name)
|
||||
[ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0
|
||||
|
||||
skill=$(json_str skill)
|
||||
[ -n "$skill" ] || exit 0
|
||||
|
||||
# 只管本技能組(jsc-{domain}:{name})
|
||||
case "$skill" in
|
||||
jsc-*:*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
domain=${skill#jsc-}
|
||||
domain=${domain%%:*}
|
||||
[ -n "$domain" ] || exit 0
|
||||
|
||||
# 豁免清單
|
||||
case "$skill" in
|
||||
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-cli:models|jsc-meta:*) exit 0 ;;
|
||||
esac
|
||||
|
||||
TTL="${JSC_VERSION_TTL:-600}"
|
||||
cache_dir="$JSC_HOME/version-cache"
|
||||
mkdir -p "$cache_dir" 2>/dev/null || true
|
||||
|
||||
deny() { # $1=訊息
|
||||
printf '[jsc][版本檢查][ERR]:%s\n' "$1" >&2
|
||||
printf '更新指令:claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc\n' "$domain" >&2
|
||||
printf '更新整組:/jsc-cli:deploy | 確定要略過檢查:JSC_VERSION_GUARD=off\n' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 本機實際載入版本:從 installPath 的 plugin.json 讀,不用註冊欄位
|
||||
local_ver=$(python3 - "$domain" <<'PY' 2>/dev/null
|
||||
import json, os, sys
|
||||
domain = sys.argv[1]
|
||||
try:
|
||||
reg = json.load(open(os.path.expanduser("~/.claude/plugins/installed_plugins.json")))
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
entries = reg.get("plugins", {}).get(f"jsc-{domain}@jsc") or []
|
||||
for e in entries:
|
||||
p = os.path.join(e.get("installPath", ""), "plugin.json")
|
||||
try:
|
||||
print(json.load(open(p)).get("version", "")); sys.exit(0)
|
||||
except Exception:
|
||||
continue
|
||||
# 讀不到實際檔案時退回註冊版本,並在後面標記為次要來源
|
||||
if entries and entries[0].get("version"):
|
||||
print(entries[0]["version"])
|
||||
PY
|
||||
)
|
||||
[ -n "$local_ver" ] || deny "讀不到本機 jsc-$domain 的實際載入版本(installed_plugins.json 或該版目錄的 plugin.json 不可用)"
|
||||
|
||||
# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST
|
||||
remote_src=$(python3 - <<'PY' 2>/dev/null
|
||||
import json, os, re
|
||||
try:
|
||||
d = json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json")))
|
||||
except Exception:
|
||||
raise SystemExit
|
||||
url = (d.get("jsc", {}).get("source", {}) or {}).get("url", "")
|
||||
m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$", url)
|
||||
if m:
|
||||
print(m.group(1), m.group(2))
|
||||
PY
|
||||
)
|
||||
host=$(printf '%s' "$remote_src" | cut -d' ' -f1)
|
||||
owner=$(printf '%s' "$remote_src" | cut -d' ' -f2)
|
||||
if [ -z "$host" ] || [ -z "$owner" ]; then
|
||||
host="${GITEA_HOST:-}"; owner="${JSC_GITEA_OWNER:-plugins}"
|
||||
fi
|
||||
[ -n "$host" ] || deny "推導不出 Gitea 站台(known_marketplaces.json 無 jsc 來源,GITEA_HOST 也未設定)"
|
||||
|
||||
# 快取
|
||||
cache="$cache_dir/$domain"
|
||||
now=$(now_epoch)
|
||||
remote_ver=""
|
||||
if [ -f "$cache" ]; then
|
||||
c_ver=$(cut -d' ' -f1 "$cache" 2>/dev/null)
|
||||
c_at=$(cut -d' ' -f2 "$cache" 2>/dev/null)
|
||||
if [ -n "$c_ver" ] && [ -n "$c_at" ] && [ $((now - c_at)) -lt "$TTL" ]; then
|
||||
remote_ver="$c_ver"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$remote_ver" ]; then
|
||||
url="$host/$owner/$domain/raw/branch/master/plugin.json"
|
||||
# 暫時性網路失敗不該誤判成版本問題,所以失敗重試一次再放棄
|
||||
for _try in 1 2; do
|
||||
body=$(curl -sS --max-time 10 "$url" 2>/dev/null) && \
|
||||
remote_ver=$(printf '%s' "$body" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
[ -n "$remote_ver" ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ -n "$remote_ver" ] || deny "查不到 jsc-$domain 的遠端發佈版本($url)。無法確認本機是否為最新,依 fail-closed 規則擋下"
|
||||
printf '%s %s\n' "$remote_ver" "$now" > "$cache" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 語意化比較:只擋「本機 < 遠端」
|
||||
cmp=$(awk -v a="$local_ver" -v b="$remote_ver" '
|
||||
BEGIN {
|
||||
n = split(a, x, "."); m = split(b, y, ".")
|
||||
for (i = 1; i <= 3; i++) {
|
||||
xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0)
|
||||
if (xi < yi) { print -1; exit }
|
||||
if (xi > yi) { print 1; exit }
|
||||
}
|
||||
print 0
|
||||
}')
|
||||
|
||||
[ "$cmp" = "-1" ] || exit 0
|
||||
deny "jsc-$domain 本機版本 $local_ver 落後遠端發佈版本 $remote_ver,本次技能呼叫已擋下"
|
||||
Reference in New Issue
Block a user