#!/usr/bin/env sh # check-skill-paths.sh — 檢查一個 domain 存取庫的文件裡,寫出來的腳本路徑解不解得出來。 # # 用法: check-skill-paths.sh # # 檢查兩項(掃 {domain-path} 底下的 skills/*/SKILL.md 與 references/*.md): # 一、帶 domain 名的路徑(jsc-{domain}/tools/x.sh、{domain}/hooks/x.sh)——那個檔案要 # 真的在對應的存取庫底下。指到不存在的檔案算缺失。 # 二、不帶 domain 名的路徑(tools/x.sh、hooks/x.sh)——檔案在這個存取庫底下就只算提示, # 不算缺失,理由見下面「為什麼不帶前綴只算提示」。檔案根本不在,才算缺失。 # # 為什麼要有這支: lint-scripts.sh 掃的是 tools/ 與 hooks/ 目錄裡的檔案,它證明得了 # 「這個存取庫裡有這支腳本、語法沒問題」,卻證明不了「文件裡寫的那條路徑解得出那支腳本」。 # 兩件事差很遠。2026-09-04 實際踩到: 一份技能本文把自家腳本寫成不帶前綴的 tools/x.sh, # 而那支技能的基底目錄是 skills/{名稱}/,照字面解出來是 skills/{名稱}/tools/x.sh, # 結束碼 127。人工覆核那一輪看的是「tools/ 底下有沒有 x.sh」,有,於是就過了—— # 覆核的問題問錯了,所以每一輪都會再過一次。 # # 為什麼不帶前綴只算提示,不算缺失: # 不帶前綴的相對路徑會相對於**當下的工作目錄**解。在自己那個存取庫裡跑,剛好解得對; # 換一個工作目錄就解到別人的 tools/ 底下,或者解不出來。兩種都不會有明確的錯誤訊息, # 前者更糟——它會跑起來,跑的是另一支腳本。無人值守那一輪還多一層: 相對路徑進不了 # 權限允許清單(那邊比對的是完整字面絕對路徑),會被靜靜擋掉。 # 話說回來,這是整個技能組共通的寫法,一次上百處。把它判成缺失會讓每一個存取庫都紅, # 而一份全紅的報告跟沒有報告一樣。所以先量出來、指得出是哪幾行,改不改由人決定。 # # 判定範圍與其極限: 只認 tools/ 與 hooks/ 兩個目錄底下的 *.sh。templates/ 與 references/ # 底下的檔案不在這一支的範圍內——那些是資料不是入口,指錯了不會變成一支跑起來的別的程式。 # 跨 domain 的路徑要那個 domain 的存取庫也在這台機器上才驗得到;不在就印成提示, # 不當缺失。這台機器沒裝,跟路徑寫錯,是兩件事。 # # 輸出: 一行一項,格式 {檔案}:{行號}:{類別}:{路徑}(stdout);統計摘要走 stderr。 # 類別 missing=指到不存在的檔案,unrooted=不帶前綴、相對於工作目錄解, # unknown=跨 domain 但那個存取庫不在這台機器上。 # 結束碼: 0=掃到文件且沒有缺失(提示不算缺失) # 1=有缺失(missing 那幾行) # 2=用法錯誤(本腳本只吃一個參數) # 3=domain 路徑不存在,或底下一份可掃的文件都沒有——**什麼都沒掃**,不等於通過 set -u usage() { echo 'usage: check-skill-paths.sh ' >&2 exit 2 } [ "$#" -eq 1 ] || usage DOMAIN=${1%/} [ -n "$DOMAIN" ] || usage [ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; } HERE=$(CDPATH= cd -P -- "$(dirname -- "$0")" && pwd -P) # 根目錄推導與別的檢核腳本共用同一套規則。跨 domain 的路徑要靠它才找得到別的存取庫; # 推不出來不是致命的,那幾筆改印成 unknown 提示。 ROOT='' if [ -f "$HERE/plugins-root.sh" ]; then # shellcheck source=/dev/null . "$HERE/plugins-root.sh" ROOT=$(jsc_plugins_root 2>/dev/null) || ROOT='' fi TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; } HITS=$(mktemp) || { rm -f "$TMP"; echo "無法建立暫存檔" >&2; exit 3; } trap 'rm -f "$TMP" "$HITS"' EXIT # skills/*/SKILL.md 與 references/*.md 兩處。README 不掃: 那裡的路徑是給人看的目錄, # 不是叫誰去跑的入口。 find "$DOMAIN/skills" -type f -name 'SKILL.md' -print 2>/dev/null > "$TMP" find "$DOMAIN/references" -type f -name '*.md' -print 2>/dev/null >> "$TMP" sort -o "$TMP" "$TMP" [ -s "$TMP" ] || { echo "底下沒有 skills/*/SKILL.md 也沒有 references/*.md,無文件可掃:$DOMAIN" >&2; exit 3; } TOTAL=$(wc -l < "$TMP" | tr -d ' ') # 這個存取庫自己的 domain 名。目錄名可能帶 jsc- 前綴(安裝後)或不帶(工作目錄版面), # 兩種都要認得,否則自家的路徑會被當成跨 domain 而驗不到。 SELF=$(basename -- "$DOMAIN") SELF=${SELF#jsc-} # 找某個 domain 的存取庫目錄。工作目錄版面不帶前綴、安裝後帶前綴,兩種都試。 domain_dir() { # $1=domain 名 [ -n "$ROOT" ] || return 1 if [ -d "$ROOT/jsc-$1" ]; then printf '%s' "$ROOT/jsc-$1"; return 0; fi if [ -d "$ROOT/$1" ]; then printf '%s' "$ROOT/$1"; return 0; fi return 1 } while IFS= read -r f; do [ -n "$f" ] || continue # 第一趟:帶 domain 名的路徑。前面可能還接著根目錄代入點(例如 {CURRENT}/), # 那不影響判定——要驗的是 {domain}/{tools|hooks}/{檔名} 這一段。 grep -noE '(jsc-)?[a-z][a-z0-9-]*/(tools|hooks)/[a-z0-9-]+\.sh' "$f" 2>/dev/null | while IFS=: read -r ln path; do dom=${path%%/*} dom=${dom#jsc-} rest=${path#*/} if [ "$dom" = "$SELF" ]; then [ -f "$DOMAIN/$rest" ] || printf '%s:%s:missing:%s\n' "$f" "$ln" "$path" continue fi if dir=$(domain_dir "$dom"); then [ -f "$dir/$rest" ] || printf '%s:%s:missing:%s\n' "$f" "$ln" "$path" else printf '%s:%s:unknown:%s\n' "$f" "$ln" "$path" fi done # 第二趟:不帶 domain 名的路徑。字元集把斜線排掉,第一趟已經算過的那幾筆才不會再算一次。 grep -noE '(^|[^-a-zA-Z0-9/_.{])(tools|hooks)/[a-z0-9-]+\.sh' "$f" 2>/dev/null | while IFS=: read -r ln raw; do path=$(printf '%s' "$raw" | sed 's|^[^t h]*||; s|^\(tools\|hooks\)|\1|') case "$path" in tools/*|hooks/*) ;; *) path=${raw#?} ;; esac case "$path" in tools/*|hooks/*) ;; *) continue ;; esac if [ -f "$DOMAIN/$path" ]; then printf '%s:%s:unrooted:%s\n' "$f" "$ln" "$path" else printf '%s:%s:missing:%s\n' "$f" "$ln" "$path" fi done done < "$TMP" | sort -u > "$HITS" N_MISS=$(grep -c ':missing:' "$HITS" 2>/dev/null || true) N_UNROOT=$(grep -c ':unrooted:' "$HITS" 2>/dev/null || true) N_UNKNOWN=$(grep -c ':unknown:' "$HITS" 2>/dev/null || true) : "${N_MISS:=0}" "${N_UNROOT:=0}" "${N_UNKNOWN:=0}" [ -s "$HITS" ] && cat "$HITS" if [ "$N_MISS" -gt 0 ]; then echo "腳本路徑檢查有缺失:共掃 $TOTAL 份文件,$N_MISS 條指到不存在的檔案,另有 $N_UNROOT 條不帶 domain 名、$N_UNKNOWN 條跨 domain 但存取庫不在這台機器上" >&2 exit 1 fi echo "腳本路徑檢查通過:共掃 $TOTAL 份文件,沒有指到不存在的檔案;另有 $N_UNROOT 條不帶 domain 名、$N_UNKNOWN 條跨 domain 但存取庫不在這台機器上,兩種都只是提示" >&2 exit 0