#!/usr/bin/env sh # find-skill-refs.sh — 掃描一個技能在所有 jsc domain 存取庫裡的引用,供刪除技能前盤點用。 # # 用法: find-skill-refs.sh # # 掃描樣式: # 1. 技能名稱字面 {skill-name}——**純子字串比對**,不看邊界。 # 所以短名稱也會命中「名稱+後綴」的其他技能名,以及純敘述文字裡的同一個詞。 # 這是故意的:盤點寧可多列,漏一個引用會讓刪除技能少改檔案。 # 呼叫端必須逐檔看過命中內容再判斷,不可把清單當成「一定要改的檔案」。 # 2. 指令形式 /jsc-{domain}:{skill-name} # 這兩個樣式已涵蓋其他 SKILL.md 的引用、domain README 的「Skills 目錄」小節、 # plugins/meta 兩份 marketplace.json 與其在各 domain 存取庫的同步副本、tools/ 腳本、 # jsc-hooks 接線等位置——只要檔案裡出現技能名稱或指令形式就會被列出。 # # 掃描範圍: **只掃正本 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 ' >&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)}" [ -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}" 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