fix(meta): 補齊稽核缺失並修掉護欄失效

What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、
把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。

Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。
完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。

How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有
documented exit codes,並以真實執行驗證每條路徑。

Who:jsc-meta:skill-check 例行稽核(2026-08-25)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 14:58:54 +08:00
co-authored by Claude Opus 5
parent 287e3ff113
commit 9ab5b42864
12 changed files with 490 additions and 51 deletions
+58 -7
View File
@@ -4,15 +4,28 @@
# 用法: find-skill-refs.sh <domain> <skill-name>
#
# 掃描樣式:
# 1. 技能名稱字面 {skill-name}
# 1. 技能名稱字面 {skill-name}——**純子字串比對**,不看邊界。
# 所以短名稱也會命中「名稱+後綴」的其他技能名,以及純敘述文字裡的同一個詞。
# 這是故意的:盤點寧可多列,漏一個引用會讓刪除技能少改檔案。
# 呼叫端必須逐檔看過命中內容再判斷,不可把清單當成「一定要改的檔案」。
# 2. 指令形式 /jsc-{domain}:{skill-name}
# 這兩個樣式已涵蓋其他 SKILL.md 的引用、domain README 的「Skills 目錄」小節、
# plugins/meta 兩份 marketplace.json 與其在各 domain 存取庫的同步副本、tools/ 腳本、
# jsc-hooks 接線等位置——只要檔案裡出現技能名稱或指令形式就會被列出。
#
# 掃描範圍: /root/plugins 底下所有 domain 存取庫,排除 .git 目錄。
# 輸出: 命中檔案清單(去重、排序),一行一個路徑;exit 0 一律成功,即使沒有命中。
set -eu
# 掃描範圍: **只掃正本 marketplace 上的 domain 存取庫**。
# 工作區可能還放著不屬於技能組的存取庫(例 shared、persona、code、doc)與
# 點開頭的目錄(例 .jsc-monorepo-archive、.kiro),掃進去會讓刪除技能改到
# 技能組以外的檔案。domain 清單取自 marketplace.json,不寫死。
# 每個 domain 先找 {root}/{domain},再找 {root}/jsc-{domain};兩者都沒有就略過。
# 每個存取庫內排除 .git 目錄。
# 根目錄預設取本腳本位置的上兩層(meta/tools -> meta -> 根),換機器不必改腳本;
# 用 JSC_PLUGINS_ROOT 覆寫,指向別處的技能組工作目錄。
# 輸出: 命中檔案清單(去重、排序),一行一個路徑(stdout);掃描摘要走 stderr。
# 結束碼: 0=有命中 1=掃完但零命中 2=用法錯誤
# 3=找不到根目錄、讀不到 domain 清單、本機一個 domain 存取庫都沒有,或掃描失敗
# 零命中與掃描失敗必須分開:拿掃描失敗當「沒有引用」會讓刪除技能少改檔案。
set -u
usage() {
echo 'usage: find-skill-refs.sh <domain> <skill-name>' >&2
@@ -22,13 +35,51 @@ usage() {
[ "$#" -eq 2 ] || usage
DOMAIN=$1
SKILL=$2
[ -n "$DOMAIN" ] && [ -n "$SKILL" ] || usage
ROOT=/root/plugins
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄: $ROOT" >&2; exit 1; }
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 3; }
# 正本 domain 清單:優先讀本機任一份 marketplace 副本(每個 repo 都帶同一份)。
mkt=""
for cand in "$HERE/../.claude-plugin/marketplace.json" "$ROOT"/*/.claude-plugin/marketplace.json; do
[ -f "$cand" ] && { mkt="$cand"; break; }
done
[ -n "$mkt" ] || { echo "找不到 marketplace.json,無法判定 domain 清單" >&2; exit 3; }
DOMAINS=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"jsc-\([a-z0-9-]*\)".*/\1/p' "$mkt" | sort -u)
[ -n "$DOMAINS" ] || { echo "marketplace.json 裡沒有 jsc-{domain} 條目:$mkt" >&2; exit 3; }
# 只收本機存在、且不是點開頭目錄的 domain 存取庫路徑。
TARGETS=""
for d in $DOMAINS; do
for cand in "$ROOT/$d" "$ROOT/jsc-$d"; do
[ -d "$cand" ] || continue
TARGETS="$TARGETS $cand"
break
done
done
[ -n "$TARGETS" ] || { echo "本機找不到任何 domain 存取庫:$ROOT(先跑 sync-domains.sh)" >&2; exit 3; }
PATTERN1="$SKILL"
PATTERN2="/jsc-${DOMAIN}:${SKILL}"
grep -rlE --exclude-dir=.git -e "$PATTERN1" -e "$PATTERN2" "$ROOT" 2>/dev/null | sort -u
TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; }
trap 'rm -f "$TMP"' EXIT
# grep 的結束碼要單獨看:接管線的話 sort 永遠回 0,掃描失敗就被吃掉了。
# shellcheck disable=SC2086
grep -rlI --exclude-dir=.git -e "$PATTERN1" -e "$PATTERN2" $TARGETS > "$TMP" 2>/dev/null
status=$?
[ "$status" -le 1 ] || { echo "掃描失敗:$ROOT(grep 結束碼 $status)" >&2; exit 3; }
OUT=$(sort -u "$TMP")
count=$(printf '%s' "$TARGETS" | wc -w | tr -d ' ')
if [ -z "$OUT" ]; then
echo "掃描完成,零命中:$count 個 domain 存取庫" >&2
exit 1
fi
printf '%s\n' "$OUT"
printf '掃描完成,命中 %s 個檔案(掃了 %s 個 domain 存取庫;技能名稱為純子字串比對,請逐檔確認)\n' \
"$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')" "$count" >&2
exit 0
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env sh
# list-skills.sh — 列出本機所有 domain 存取庫的技能,供選技能與稽核用。
#
# 用法: list-skills.sh(不吃參數)
#
# 掃描: {root}/*/skills/*/SKILL.md,讀 frontmatter 的 name 與 description。
# domain 取存取庫目錄名,開頭的 jsc- 會去掉。
# **只列正本 marketplace 上的 domain**:工作區可能還放著不屬於技能組的
# 存取庫(例 shared、persona、code、doc),列出來會讓選技能的流程
# 誤把它們當成技能組的一部分。domain 清單取自 marketplace.json,不寫死。
# 清單只反映本機檔案;要確保檔案齊全請先跑 sync-domains.sh。
#
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
#
# 輸出: 一行一個技能,格式 {domain}<TAB>{name}<TAB>{description},依 domain、name 排序。
# 結束碼: 0=至少列出一個技能 1=找不到根目錄、讀不到 domain 清單,或一個技能都沒有
set -u
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 1; }
# 正本 domain 清單:優先讀本機任一份 marketplace 副本(每個 repo 都帶同一份)。
mkt=""
for cand in "$HERE/../.claude-plugin/marketplace.json" "$ROOT"/*/.claude-plugin/marketplace.json; do
[ -f "$cand" ] && { mkt="$cand"; break; }
done
[ -n "$mkt" ] || { echo "找不到 marketplace.json,無法判定 domain 清單" >&2; exit 1; }
DOMAINS=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"jsc-\([a-z0-9-]*\)".*/\1/p' "$mkt" | sort -u)
[ -n "$DOMAINS" ] || { echo "marketplace.json 裡沒有 jsc-{domain} 條目:$mkt" >&2; exit 1; }
OUT=$(
for f in "$ROOT"/*/skills/*/SKILL.md; do
[ -f "$f" ] || continue
repo=${f#"$ROOT"/}
repo=${repo%%/*}
domain=${repo#jsc-}
# 不在正本清單上的存取庫直接跳過。
printf '%s\n' "$DOMAINS" | grep -qx "$domain" || continue
name=$(awk 'BEGIN{f=0} /^---$/{f++; next} f==1 && /^name:/{sub(/^name: */,""); print; exit}' "$f")
desc=$(awk 'BEGIN{f=0} /^---$/{f++; next} f==1 && /^description:/{sub(/^description: */,""); print; exit}' "$f")
[ -n "$name" ] || continue
printf '%s\t%s\t%s\n' "$domain" "$name" "$desc"
done | sort -t"$(printf '\t')" -k1,1 -k2,2
)
[ -n "$OUT" ] || { echo "掃不到任何 SKILL.md:$ROOT/*/skills/*/SKILL.md(先跑 sync-domains.sh)" >&2; exit 1; }
printf '%s\n' "$OUT"
count=$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')
echo "共 $count 個技能" >&2
exit 0
+25 -2
View File
@@ -5,15 +5,36 @@
# 1. 中國用語(references/ste100.md 的替換表左欄)
# 2. 中文句內的半形標點(, ; ! ? 緊鄰中日韓字元)
# 3. 常見 AI 套話(總的來說、綜上所述、希望這對你有幫助 等)
# 4. 簡體字(只收沒有繁體正當用法的字,避免誤報)
# 5. 中文並列項用斜線(半形 / 或全形 / 夾在中日韓字元之間),應改頓號「、」
# 輸出: {檔案}:{行號}:{類別}:{命中內容};全部通過 exit 0,有命中 exit 1。
# 限制: 只掃 .md 檔;程式碼圍欄內的內容可能誤報,人工複核。
# 限制: 掃 .md 與 .json 檔;程式碼圍欄內的內容可能誤報,人工複核。
# references/ste100.md 本身跳過——規則文件裡的詞是被討論,不是被使用。
# 並列斜線會誤報兩類:含中文的路徑或分支名範例(例 feat/報表/P2),
# 以及英文項目並列(準則允許)。命中後先判斷是不是真的並列項。
set -u
TERMS='默認|支持某|兼容|信息|數據庫|服務器|軟件|硬件|網絡|質量|卸載|反饋|視頻|屏幕|鼠標|打印|立馬|靠譜|賦能|閉環|抓手|復盤'
CLICHES='總的來說|綜上所述|希望這對你有幫助|好問題|在當今|瞬息萬變|標誌著|奠定了基礎|體現了|不僅僅是|讓我們一起'
# 刻意排除繁體也在用的字(后、台、干、只、里、面、制、志),只留簡化後才出現的字形。
SIMPLIFIED='应|为|这|说|发|国|过|对|开|关|问|题|东|车|马|鸟|龙|飞|见|无|产|业|务|书|写|学|习|报|纸|认|识|证|际|网|络|计|划|实|现|给|条|约|级|组|织|变|换|电|脑|两|双|单|构|价|钱|众|议|论|传|统|么|儿|们|从|来|时|间|长|门|闻|声|员|图|团|转|输|达|运|进|远|连|边|还|经|该|营|规|则|范|围|参|数|类|结|态|设|备|辑|译|码|库|档'
hit=0
# 不帶參數會空跑並回 0,看起來像「全部通過」。這種假通過比不檢查更危險,所以擋掉。
if [ "$#" -eq 0 ]; then
echo '用法: ste100-lint.sh <file|dir> [...]' >&2
echo '沒有給檢查對象。空跑會回 0,看起來像通過,所以這裡直接視為錯誤。' >&2
exit 2
fi
files() {
for p in "$@"; do
if [ -d "$p" ]; then find "$p" -name '*.md' -not -path '*/.git/*'; else echo "$p"; fi
if [ -d "$p" ]; then
find "$p" \( -name '*.md' -o -name '*.json' \) \
-not -path '*/.git/*' -not -path '*/references/ste100.md'
else
case "$p" in
*/references/ste100.md|references/ste100.md) ;;
*) echo "$p" ;;
esac
fi
done
}
check() { # $1=檔案 $2=類別 $3=grep 模式 $4=grep 旗標
@@ -24,5 +45,7 @@ for f in $(files "$@"); do
check "$f" "中國用語" "$TERMS" E
check "$f" "半形標點" '[\x{4e00}-\x{9fff}][,;!?]|[,;][\x{4e00}-\x{9fff}]' P
check "$f" "AI 套話" "$CLICHES" E
check "$f" "簡體字" "$SIMPLIFIED" E
check "$f" "並列斜線" '[\x{4e00}-\x{9fff}][ ]?[//][ ]?[\x{4e00}-\x{9fff}]' P
done
exit $hit
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env sh
# sync-domains.sh — 依 Gitea 正本 marketplace 把所有 domain 存取庫同步到本機。
#
# 用法: sync-domains.sh(不吃參數)
#
# 流程:
# 1. 透過 jsc-gitea/tools/gitea.sh 讀正本 marketplace
# (plugins/meta 的 .claude-plugin/marketplace.json),取得權威 domain 清單。
# 2. 本機缺少的 domain:用 gitea.sh clone-url plugins/{domain} 取網址後 clone。
# 3. 本機已有的 domain:git pull。工作區有未提交變更就跳過 pull,只在 stderr 提醒——
# 正在改的 repo 不該被自動 pull 覆蓋。
#
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
# 存取庫目錄名: 先找 {root}/{domain},再找 {root}/jsc-{domain};都沒有才 clone 成 {root}/{domain}。
#
# 輸出: 一行一個 domain,格式 {domain}<TAB>{path}(stdout);警告與失敗說明走 stderr。
# 結束碼: 0=正本讀到、每個 domain 都在本機,而且每個既有存取庫都更新到最新
# 1=讀不到正本 marketplace 或找不到 gitea.sh(不輸出任何 domain)
# 2=正本讀到,但有 domain 沒能取得或 clone 失敗(已輸出其餘 domain)
# 3=每個 domain 都在本機,但有存取庫沒更新到最新:工作區脏而跳過 pull,
# 或 pull 失敗。stderr 會逐一列出這些路徑。**exit 0 才代表「全部最新」**;
# 拿 3 當乾淨會讓後續步驟改在落後的檔案上。
# 同時發生時回較嚴重的那個:2 蓋過 3。
# 環境變數: JSC_PLUGINS_ROOT(根目錄)
# JSC_SYNC_DRY_RUN=1(只印出會做什麼,不動檔案;不執行 pull/clone,
# 所以也不因為「沒更新」回 3)
set -u
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
DRY="${JSC_SYNC_DRY_RUN:-}"
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 1; }
GITEA=""
for g in "$ROOT/gitea/tools/gitea.sh" "$ROOT/jsc-gitea/tools/gitea.sh"; do
[ -f "$g" ] && { GITEA="$g"; break; }
done
[ -n "$GITEA" ] || { echo "找不到 gitea.sh:請先取得 jsc-gitea 存取庫" >&2; exit 1; }
mkt=$(sh "$GITEA" api GET /repos/plugins/meta/raw/.claude-plugin/marketplace.json 2>/dev/null) || mkt=""
[ -n "$mkt" ] || { echo "讀不到正本 marketplace:檢查 GITEA_HOST、GITEA_TOKEN 或 tea 登入" >&2; exit 1; }
DOMAINS=$(printf '%s\n' "$mkt" | sed -n 's/.*"name": *"jsc-\([a-z0-9-]*\)".*/\1/p' | sort -u)
[ -n "$DOMAINS" ] || { echo "正本 marketplace 裡沒有 jsc-{domain} 條目" >&2; exit 1; }
repo_path() { # $1=domain;印出既有路徑(return 0)或預設路徑(return 1)
for d in "$ROOT/$1" "$ROOT/jsc-$1"; do
[ -d "$d" ] && { printf '%s' "$d"; return 0; }
done
printf '%s' "$ROOT/$1"
return 1
}
rc=0
stale=0
for domain in $DOMAINS; do
if path=$(repo_path "$domain"); then
if [ -n "$(git -C "$path" status --porcelain 2>/dev/null)" ]; then
echo "工作區有未提交變更,跳過 pull,未更新到最新:$path" >&2
stale=1
elif [ "$DRY" = 1 ]; then
echo "[dry-run] git -C $path pull --ff-only" >&2
elif ! git -C "$path" pull --quiet --ff-only >/dev/null 2>&1; then
echo "pull 失敗,保留本機版本,未更新到最新:$path" >&2
stale=1
fi
else
url=$(sh "$GITEA" clone-url "plugins/$domain" 2>/dev/null) || url=""
if [ -z "$url" ]; then
echo "取不到 clone 網址,略過:plugins/$domain" >&2
rc=2
continue
fi
if [ "$DRY" = 1 ]; then
echo "[dry-run] git clone $url $path" >&2
elif ! git clone --quiet "$url" "$path" >/dev/null 2>&1; then
echo "clone 失敗:$url" >&2
rc=2
continue
fi
fi
printf '%s\t%s\n' "$domain" "$path"
done
# 2(取不到或 clone 失敗)比 3(沒更新到最新)嚴重,優先回報。
[ "$rc" -eq 0 ] && [ "$stale" -eq 1 ] && rc=3
exit $rc
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env sh
# sync-marketplace.sh — 把一個 plugin 條目寫進正本 marketplace,再同步到所有 domain 存取庫。
#
# 用法: sync-marketplace.sh <domain> <repo-url> <description>
# 例: sync-marketplace.sh doc https://gitea.jsc.idv.tw/plugins/doc.git "文件產出與同步"
#
# 行為:
# 1. 在 plugins/meta 的兩份正本(.claude-plugin/marketplace.json 與
# .agents/plugins/marketplace.json)插入或更新 jsc-{domain} 條目;已存在就覆寫網址與描述。
# 條目依 name 排序,格式維持兩格縮排、不轉義非 ASCII、結尾一個換行。
# 2. 兩份正本與所有副本都由同一份算好的內容複製過去,保證位元組完全一致。
# 3. 逐一複製到每個 domain 存取庫的相同兩個路徑(含新 domain 自己)。
# 4. 全部寫完後逐檔比對,有任何一份不一致就報錯。
#
# 依賴: **python3**(只用標準函式庫的 json 模組)。
# 本 repo 的腳本一律 shell 優先,這裡是唯一例外:條目的插入、排序與重新序列化要動 JSON,
# 純 shell 拼 JSON 會壞掉(縮排、逸出、非 ASCII 描述)。所以保留 python3。
# python3 不在 PATH 時:腳本不寫任何檔案,印出缺少 python3 的訊息,回 1。
# 呼叫端要先裝 python3 再重跑,不可改用 sed 手動改 marketplace。
#
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
# 存取庫目錄名: 先找 {root}/{domain},再找 {root}/jsc-{domain}。
#
# 輸出: 一行一個實際寫入的檔案路徑(stdout);略過與失敗說明走 stderr。
# 結束碼: 0=全部寫入且逐檔一致 2=用法錯誤
# 1=缺 python3、正本讀寫失敗,或有檔案比對不一致
# 3=寫入成功,但有 domain 存取庫不在本機(先跑 sync-domains.sh 再重跑)
set -u
usage() {
echo 'usage: sync-marketplace.sh <domain> <repo-url> <description>' >&2
exit 2
}
[ "$#" -eq 3 ] || usage
DOMAIN=$1
URL=$2
DESC=$3
[ -n "$DOMAIN" ] && [ -n "$URL" ] && [ -n "$DESC" ] || usage
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
META=$(CDPATH= cd -- "$HERE/.." && pwd)
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
REL_CLAUDE='.claude-plugin/marketplace.json'
REL_AGENTS='.agents/plugins/marketplace.json'
CANON="$META/$REL_CLAUDE"
[ -f "$CANON" ] || { echo "找不到正本 marketplace:$CANON" >&2; exit 1; }
# 先擋掉缺 python3 的環境,才不會寫到一半才失敗。
command -v python3 >/dev/null 2>&1 || {
echo '找不到 python3:本腳本用 python3 的 json 模組改 marketplace 條目。請先安裝 python3 再重跑。' >&2
exit 1
}
WORKDIR=$(mktemp -d) || exit 1
trap 'rm -rf "$WORKDIR"' EXIT
NEW="$WORKDIR/marketplace.json"
# 條目的插入與更新交給 json 解析器;用 sed 拼 JSON 會壞掉。
JSC_MK_DOMAIN="$DOMAIN" JSC_MK_URL="$URL" JSC_MK_DESC="$DESC" \
JSC_MK_IN="$CANON" JSC_MK_OUT="$NEW" python3 - <<'PY' || { echo "更新正本內容失敗:$CANON" >&2; exit 1; }
import json, os
name = "jsc-" + os.environ["JSC_MK_DOMAIN"]
entry = {
"name": name,
"source": {"source": "url", "url": os.environ["JSC_MK_URL"]},
"description": os.environ["JSC_MK_DESC"],
}
with open(os.environ["JSC_MK_IN"], encoding="utf-8") as fh:
data = json.load(fh)
plugins = [p for p in data.get("plugins", []) if p.get("name") != name]
plugins.append(entry)
data["plugins"] = sorted(plugins, key=lambda p: p.get("name", ""))
with open(os.environ["JSC_MK_OUT"], "w", encoding="utf-8") as fh:
fh.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
PY
rc=0
write_copy() { # $1=目標檔案
target=$1
dir=$(dirname "$target")
mkdir -p "$dir" 2>/dev/null || { echo "無法建立目錄:$dir" >&2; rc=1; return 1; }
cp "$NEW" "$target" 2>/dev/null || { echo "無法寫入:$target" >&2; rc=1; return 1; }
printf '%s\n' "$target"
}
# ---- 1. 兩份正本 ----
write_copy "$META/$REL_CLAUDE"
write_copy "$META/$REL_AGENTS"
# ---- 2. 每個 domain 存取庫的副本 ----
DOMAINS=$(sed -n 's/.*"name": *"jsc-\([a-z0-9-]*\)".*/\1/p' "$NEW" | sort -u)
for d in $DOMAINS; do
path=""
for cand in "$ROOT/$d" "$ROOT/jsc-$d"; do
[ -d "$cand" ] && { path="$cand"; break; }
done
if [ -z "$path" ]; then
echo "存取庫不在本機,略過:$d(先跑 sync-domains.sh)" >&2
[ "$rc" -eq 0 ] && rc=3
continue
fi
[ "$path" = "$META" ] && continue
write_copy "$path/$REL_CLAUDE"
write_copy "$path/$REL_AGENTS"
done
# ---- 3. 逐檔比對,確認位元組一致 ----
for d in "$META" $(for x in $DOMAINS; do
for cand in "$ROOT/$x" "$ROOT/jsc-$x"; do [ -d "$cand" ] && { echo "$cand"; break; }; done
done); do
for rel in "$REL_CLAUDE" "$REL_AGENTS"; do
[ -f "$d/$rel" ] || continue
cmp -s "$NEW" "$d/$rel" || { echo "內容不一致:$d/$rel" >&2; rc=1; }
done
done
exit $rc
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env sh
# verify-skill-removed.sh — 刪除技能後,實地檢查各 CLI 的磁碟上有沒有殘留。
#
# 用法: verify-skill-removed.sh <domain> <skill-name>
#
# 行為: 先用 jsc-cli/tools/detect-clis.sh 找出已安裝的 CLI,再逐一 grep 該 CLI 的技能快取
# 與 hook 設定,找技能名稱字面與指令形式 /jsc-{domain}:{name}。
# 未安裝的 CLI 不檢查;設定位置不存在就跳過(安靜降級,不算殘留)。
#
# 檢查位置(每個 CLI 的技能快取與 hook 設定):
# claude — ~/.claude/plugins/、~/.claude/settings.json、~/.claude/settings.local.json
# codex — ${CODEX_HOME:-~/.codex}/(config.toml 的 notify、AGENTS.md、plugins/)
# copilot — ~/.config/copilot/
# antigravity — ~/.antigravity/、~/.config/antigravity/
# kiro — ~/.kiro/、工作目錄的 .kiro/hooks/
#
# 輸出: 檢查過的位置一行一個(stderr),殘留一行一個 {file}:{line}:{內容}(stdout)。
# 沒有殘留會印「無殘留」到 stderr,區別於「什麼都沒檢查」。
# 結束碼: 0=沒有殘留 1=有殘留 2=用法錯誤 3=沒偵測到任何 CLI 或找不到可檢查的位置
set -u
usage() {
echo 'usage: verify-skill-removed.sh <domain> <skill-name>' >&2
exit 2
}
[ "$#" -eq 2 ] || usage
DOMAIN=$1
SKILL=$2
[ -n "$DOMAIN" ] && [ -n "$SKILL" ] || usage
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
DETECT=""
for d in "$ROOT/cli/tools/detect-clis.sh" "$ROOT/jsc-cli/tools/detect-clis.sh"; do
[ -f "$d" ] && { DETECT="$d"; break; }
done
[ -n "$DETECT" ] || { echo "找不到 detect-clis.sh:請先取得 jsc-cli 存取庫" >&2; exit 3; }
CLIS=$(sh "$DETECT" 2>/dev/null | cut -f1)
[ -n "$CLIS" ] || { echo "沒偵測到任何已安裝的 CLI,無處可查" >&2; exit 3; }
cli_paths() { # $1=CLI 代號;印出該 CLI 的技能快取與 hook 設定位置
case "$1" in
claude)
printf '%s\n' "$HOME/.claude/plugins" "$HOME/.claude/settings.json" \
"$HOME/.claude/settings.local.json" ;;
codex)
printf '%s\n' "${CODEX_HOME:-$HOME/.codex}" ;;
copilot)
printf '%s\n' "$HOME/.config/copilot" ;;
antigravity)
printf '%s\n' "$HOME/.antigravity" "$HOME/.config/antigravity" ;;
kiro)
printf '%s\n' "$HOME/.kiro" "./.kiro" ;;
esac
}
checked=0
hit=0
for cli in $CLIS; do
for p in $(cli_paths "$cli"); do
[ -e "$p" ] || continue
checked=$((checked + 1))
echo "檢查 $cli:$p" >&2
out=$(grep -rnI --exclude-dir=.git -e "$SKILL" -e "/jsc-${DOMAIN}:${SKILL}" "$p" 2>/dev/null)
if [ -n "$out" ]; then
printf '%s\n' "$out"
hit=1
fi
done
done
[ "$checked" -gt 0 ] || { echo "偵測到 CLI,但沒有任何設定位置存在,無處可查" >&2; exit 3; }
[ "$hit" -eq 0 ] && echo "無殘留:已檢查 $checked 個位置" >&2
exit $hit