Files
meta/tools/check-skill-paths.sh
jiantw83andClaude Opus 5 763709ed7b 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>
2026-09-04 10:55:14 +08:00

142 lines
7.0 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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