#!/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,每行一項): # itemscoperequiredactualexpectfixverdict # verdict = ok 設定妥當 # default 未設定,走預設值,可以正常運作 # unset 選擇性項目未設定,沒有預設值,相關功能會降級 # missing 必要項目缺了,相關技能跑不動 # invalid 有值但驗不過(目錄不在、認證失敗、格式不對) # skipped 離線模式略過,未取得結論 # 最後固定一行 summary{missing 數}{invalid 數}{unset 數}{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