feat(檢核): 新增腳本路徑檢核,判文件裡寫的路徑解不解得出來
lint-scripts.sh 掃的是 tools/ 與 hooks/ 目錄裡的檔案,它證明得了「這個存取庫
裡有這支腳本」,卻證明不了「文件裡寫的那條路徑解得出那支腳本」。兩件事差
很遠,而中間那道縫剛好放得下一整類缺失。
2026-09-04 實際踩到:一份技能本文把自家腳本寫成不帶前綴的相對路徑,而那支
技能的基底目錄是 skills/{名稱}/,照字面解出來的位置沒有那個檔案,結束碼 127。
人工覆核看的是「tools/ 底下有沒有這支」,有,於是每一輪都過。覆核的問題本身
問錯了,所以錯幾次都不會被發現。
三種判定:指到不存在的檔案算缺失;不帶 domain 名但檔案在這個存取庫底下只算
提示;跨 domain 而那個存取庫不在這台機器上也只算提示。中間那一種為什麼不算
缺失——全套技能組有三百多處那樣寫,判成缺失會讓每個存取庫同時亮紅,而一份
全紅的報告跟沒有報告一樣。先量出來、指得出是哪幾行,改不改由人決定。
跨 domain 的路徑靠 plugins-root.sh 找別的存取庫,推不出來的那幾筆印成提示,
不當缺失:這台機器沒裝,跟路徑寫錯,是兩件事。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env sh
|
||||
# check-skill-paths.sh — 檢查一個 domain 存取庫的文件裡,寫出來的腳本路徑解不解得出來。
|
||||
#
|
||||
# 用法: check-skill-paths.sh <domain-path>
|
||||
#
|
||||
# 檢查兩項(掃 {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 <domain-path>' >&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
|
||||
Reference in New Issue
Block a user