Files
cli/tools/scan-config.sh
jiantw83 fb80559159 feat(wiki): 體檢目錄頁改走專用存取庫,並補齊設定規格表
What:CHECK_CONTENTS 改由 wiki-repo CONTENTS 解析並透過 wiki-contents.sh upsert
寫入,CHECK_{HASH} 仍走 wiki-repo CHECK。目錄頁新增一欄裸 HASH 當比對鍵。主機名
改由程式取短名,不再交給模型自由填。

Why:比對鍵原本是含網址的儲存格,換主機或換存取庫就比對不到,每跑一次體檢就替同一台
機器多附一列,畫面上還看不出來。主機名短名與 FQDN 不一致時,同一台機器會分裂成兩張頁,
而助理巡檢那邊是用程式取值的,兩邊對不起來。

How:設定規格表同一輪補齊三處既有缺漏——補上漏掉的 JSC_WIKI_REPO_MONITOR,體檢本來
看不到它而孤兒掃描還會誤報;刪掉指向不存在頁面的 MAINTAIN 內容頁字樣;MAINTAIN 那一列
改成不需要使用者處理,免得體檢叫人去設一支管不到任何頁的變數。整列保留,刪掉會讓孤兒
掃描開始誤報那個變數。

Who:jsc-cli
2026-09-02 11:03:07 +08:00

227 lines
9.8 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
# scan-config.sh — 依 config-spec.tsv 盤點 jsc 技能組的設定現況。唯讀,不寫任何設定。
# 用法:
# scan-config.sh spec [global|project|all] # 印規格表(去掉註解與 internal 列)
# scan-config.sh scan [global|project|all] # 逐項檢查現況,印 TSV
# scan-config.sh orphans # 掃原始碼,找出沒登錄進規格表的變數
# 選項:
# -o 離線模式:需要連 Gitea 的檢查一律標 skipped,不發送請求
#
# scan 的輸出(TSV,每行一項):
# item<TAB>scope<TAB>required<TAB>actual<TAB>expect<TAB>fix<TAB>verdict
# verdict = ok 設定妥當
# default 未設定,走預設值,可以正常運作
# unset 選擇性項目未設定,沒有預設值,相關功能會降級
# missing 必要項目缺了,相關技能跑不動
# invalid 有值但驗不過(目錄不在、認證失敗、格式不對)
# skipped 離線模式略過,未取得結論
# 最後固定一行 summary<TAB>{missing 數}<TAB>{invalid 數}<TAB>{unset 數}<TAB>{skipped 數}
#
# 帶 TOKEN 的項目一律只印 set 或 unset,不印值:體檢報告會寫進 wiki,憑證不能跟著上去。
#
# 結束碼: 0=掃描完成(有沒有問題都算完成,判斷交給呼叫端) 2=用法錯誤 3=找不到規格表
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SPEC="${JSC_CONFIG_SPEC:-$HERE/config-spec.tsv}"
TAB=$(printf '\t')
OFFLINE=0
[ -f "$SPEC" ] || { echo "找不到規格表:$SPEC(可用 JSC_CONFIG_SPEC 指定)" >&2; exit 3; }
usage() {
echo "用法:scan-config.sh [-o] {spec|scan|orphans} [global|project|all]" >&2
exit 2
}
case "${1:-}" in
-o) OFFLINE=1; shift ;;
esac
cmd="${1:-}"
scope_want="${2:-all}"
case "$cmd" in spec|scan|orphans) ;; *) usage ;; esac
case "$scope_want" in global|project|all) ;; *) usage ;; esac
# 找出 jsc-gitea 的 tools/gitea.sh。所有 Gitea 操作一律經由它(技能準則),不自行拼 API 呼叫。
# 找不到就回傳 1,呼叫端把需要連線的檢查標成 skipped,不讓整份體檢失敗。
gitea_sh() {
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
fi
_root="${CLAUDE_PLUGIN_ROOT:-$HERE/..}"
for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
done
_c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
"$_root"/../../gitea/*/tools/gitea.sh \
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
| sort | tail -n1)
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
_c=$(command -v gitea.sh 2>/dev/null || true)
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
return 1
}
# 規格表的資料列(去註解、去空行)
spec_rows() { grep -v '^#' "$SPEC" | grep -v '^[[:space:]]*$'; }
# 這一列要不要納入本次掃描。$1=kind $2=scope
# internal 列只為登錄而存在(讓 orphans 認得出它們不是漏網之魚),永遠不進體檢報告。
row_wanted() {
[ "$1" != internal ] || return 1
[ "$scope_want" = all ] || [ "$2" = "$scope_want" ]
}
if [ "$cmd" = spec ]; then
spec_rows | while IFS="$TAB" read -r key kind scope required def verify fix desc; do
row_wanted "$kind" "$scope" || continue
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$key" "$scope" "$required" "$def" "$verify" "$fix" "$desc"
done
exit 0
fi
if [ "$cmd" = orphans ]; then
# 規格表沒有的變數 = 有人加了設定卻忘了登錄。體檢照樣印出來,維護者才補得上。
root="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
[ -d "$root" ] || { echo "找不到 plugins 根目錄:$root(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 3; }
known=$(spec_rows | cut -f1 | sed 's/^\$//' | sort -u)
# 前面加詞邊界,否則帶別種前綴的變數(例如 PERSONA_ 開頭那批)會被從中間切出一段誤報。
# 這行註解本身也不寫出完整變數字面:掃描連自己的原始碼一起掃,寫了就會掃到自己。
grep -rhoE '\b(JSC|GITEA)_[A-Z0-9_]+' \
--include='*.sh' --include='*.md' --include='*.json' \
--exclude-dir=.git --exclude-dir=.jsc-monorepo-archive "$root" 2>/dev/null \
| sort | uniq -c | sort -rn \
| while read -r count name; do
# 原始碼寫的是樣板字面(JSC_WIKI_REPO_{TYPE}),抓出來會多一條尾巴底線;
# 去掉再比對,否則每次體檢都會多報一個不存在的變數。
name=${name%_}
printf '%s\n' "$known" | grep -qx "$name" && continue
# 只排掉樣板佔位字那一個名字,不做前綴比對:CONTENTS、MONITOR 這些是真的頁面類型,
# 排寬了就等於把整族存取庫變數從孤兒掃描裡挖掉,漏登錄再也看不出來。
case "$name" in JSC_WIKI_REPO_TYPE) continue ;; esac
printf 'orphan\t%s\t%s\n' "$name" "$count"
done
exit 0
fi
# --- scan ---
n_missing=0; n_invalid=0; n_unset=0; n_skipped=0
GITEA=$(gitea_sh 2>/dev/null || true)
# 連線類檢查的共用前置:離線、或找不到 gitea.sh,都直接標 skipped。
online_ready() {
[ "$OFFLINE" = 0 ] || return 1
[ -n "$GITEA" ] || return 1
}
# 值展開:規格表的檔案類 key 會帶 $JSC_HOME 這種變數,照字面找檔案永遠找不到。
# 開頭的 ~ 要先換成 $HOME:雙引號裡的 ~ 不展開,留著會讓 ~/.jsc 這種預設值一律驗不過。
# set +u 是必要的:預設值裡的 $JSC_HOME 常常正是「還沒設定」的那一個,
# 展開它在 set -u 底下會直接中斷整份掃描,體檢就停在半路。
expand() {
_e="$1"
case "$_e" in "~/"*) _e="$HOME/${_e#\~/}" ;; esac
# JSC_HOME 常常正是還沒設定的那一個,展開別列的 $JSC_HOME/... 卻要它有值;
# 只在這個子 shell 套用 JSC_HOME 規格列自己的預設值,不外流到主流程,
# 免得 JSC_HOME 那一列自己的檢查被連帶改成「已設定」。
( set +u; : "${JSC_HOME:=$HOME/.jsc}"; eval "printf '%s' \"$_e\"" ) 2>/dev/null
}
emit() { # item scope required actual expect fix verdict
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7"
case "$7" in
missing) n_missing=$((n_missing + 1)) ;;
invalid) n_invalid=$((n_invalid + 1)) ;;
unset) n_unset=$((n_unset + 1)) ;;
skipped) n_skipped=$((n_skipped + 1)) ;;
esac
}
# 對一個已取得的值跑 verify。印出 verdict(ok/invalid/skipped)。$1=verify $2=值
run_verify() {
case "$1" in
set|none) echo ok ;;
dir) [ -d "$2" ] && echo ok || echo invalid ;;
file) [ -f "$2" ] && echo ok || echo invalid ;;
gitea-api)
online_ready || { echo skipped; return; }
GITEA_HOST="$2" sh "$GITEA" api GET /version >/dev/null 2>&1 && echo ok || echo invalid ;;
gitea-auth)
online_ready || { echo skipped; return; }
sh "$GITEA" owners >/dev/null 2>&1 && echo ok || echo invalid ;;
wiki-repo)
case "$2" in
*/*)
online_ready || { echo ok; return; }
sh "$GITEA" default-branch "$2" >/dev/null 2>&1 && echo ok || echo invalid ;;
*) echo invalid ;;
esac ;;
*) echo ok ;;
esac
}
# 迴圈不可以放在管線右邊:那會變成子 shell,計數加不回來,summary 永遠是 0。
rows=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; }
spec_rows > "$rows"
while IFS="$TAB" read -r key kind scope required def verify fix desc; do
[ -n "${key:-}" ] || continue
row_wanted "$kind" "$scope" || continue
if [ "$kind" = env ]; then
eval "val=\${$key:-}"
secret=0
case "$key" in *TOKEN*) secret=1 ;; esac
if [ -n "$val" ]; then
verdict=$(run_verify "$verify" "$val")
if [ "$secret" = 1 ]; then actual=set; else actual="$val"; fi
emit "$key" "$scope" "$required" "$actual" "$desc" "$fix" "$verdict"
continue
fi
# 未設定:有預設值就用預設值再驗一次,驗得過才算走得下去。
if [ "$def" != "-" ]; then
# 預設值寫成另一個變數名(JSC_WIKI_REPO_LOG 退回 JSC_WIKI_REPO)時,要跟去看那一個。
# 退路自己也空著卻回報「走預設值」,會讓體檢說得過去、實際上功能整個不能用。
case "$def" in
[A-Z]*)
if printf '%s' "$def" | grep -qx '[A-Z][A-Z0-9_]*'; then
eval "fallback=\${$def:-}"
if [ -z "$fallback" ]; then
emit "$key" "$scope" "$required" "未設定(退路 $def 也未設定)" "$desc" "$fix" unset
continue
fi
emit "$key" "$scope" "$required" "未設定(退回 $def=$fallback)" "$desc" "$fix" default
continue
fi ;;
esac
dval=$(expand "$def")
case "$verify" in
# 預設路徑還沒建立,算「尚未啟用」而不是「設錯了」:invalid 專指有值卻驗不過。
dir|file) if [ "$(run_verify "$verify" "$dval")" = ok ]; then verdict=default; else verdict=unset; fi ;;
*) verdict=default ;;
esac
emit "$key" "$scope" "$required" "未設定(預設 $def)" "$desc" "$fix" "$verdict"
continue
fi
[ "$required" = yes ] && verdict=missing || verdict=unset
emit "$key" "$scope" "$required" 未設定 "$desc" "$fix" "$verdict"
continue
fi
# kind=file:規格表的 key 本身就是路徑
path=$(expand "$key" 2>/dev/null || printf '%s' "$key")
if [ -e "$path" ]; then
emit "$key" "$scope" "$required" "$path" "$desc" "$fix" ok
elif [ "$required" = yes ]; then
emit "$key" "$scope" "$required" 不存在 "$desc" "$fix" missing
else
emit "$key" "$scope" "$required" 不存在 "$desc" "$fix" unset
fi
done < "$rows"
rm -f "$rows"
printf 'summary\t%s\t%s\t%s\t%s\n' "$n_missing" "$n_invalid" "$n_unset" "$n_skipped"
exit 0