From 763709ed7baa9596b21f61a31ef2d75b786887fa Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 4 Sep 2026 10:55:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=AA=A2=E6=A0=B8):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=85=B3=E6=9C=AC=E8=B7=AF=E5=BE=91=E6=AA=A2=E6=A0=B8=EF=BC=8C?= =?UTF-8?q?=E5=88=A4=E6=96=87=E4=BB=B6=E8=A3=A1=E5=AF=AB=E7=9A=84=E8=B7=AF?= =?UTF-8?q?=E5=BE=91=E8=A7=A3=E4=B8=8D=E8=A7=A3=E5=BE=97=E5=87=BA=E4=BE=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lint-scripts.sh 掃的是 tools/ 與 hooks/ 目錄裡的檔案,它證明得了「這個存取庫 裡有這支腳本」,卻證明不了「文件裡寫的那條路徑解得出那支腳本」。兩件事差 很遠,而中間那道縫剛好放得下一整類缺失。 2026-09-04 實際踩到:一份技能本文把自家腳本寫成不帶前綴的相對路徑,而那支 技能的基底目錄是 skills/{名稱}/,照字面解出來的位置沒有那個檔案,結束碼 127。 人工覆核看的是「tools/ 底下有沒有這支」,有,於是每一輪都過。覆核的問題本身 問錯了,所以錯幾次都不會被發現。 三種判定:指到不存在的檔案算缺失;不帶 domain 名但檔案在這個存取庫底下只算 提示;跨 domain 而那個存取庫不在這台機器上也只算提示。中間那一種為什麼不算 缺失——全套技能組有三百多處那樣寫,判成缺失會讓每個存取庫同時亮紅,而一份 全紅的報告跟沒有報告一樣。先量出來、指得出是哪幾行,改不改由人決定。 跨 domain 的路徑靠 plugins-root.sh 找別的存取庫,推不出來的那幾筆印成提示, 不當缺失:這台機器沒裝,跟路徑寫錯,是兩件事。 Co-Authored-By: Claude Opus 5 --- tools/check-skill-paths.sh | 141 +++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 tools/check-skill-paths.sh diff --git a/tools/check-skill-paths.sh b/tools/check-skill-paths.sh new file mode 100755 index 0000000..d9defbd --- /dev/null +++ b/tools/check-skill-paths.sh @@ -0,0 +1,141 @@ +#!/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