#!/usr/bin/env sh # build-todo.sh — 把三支檢查腳本的輸出合併成一張「待修項目」表。 # # /jsc-cli:doctor 與 /jsc-cli:setup 都要這張表,合併規則只留一個真實來源, # 兩支技能各寫一次就會各自漂移,一邊排序、另一邊漏掉某一類。 # # 用法: # build-todo.sh [--config {檔案}]... [--wiring {cli}={檔案}]... [--version {檔案}]... # 每個選項都可以重複。檔案給「-」代表讀標準輸入(整份只能有一個 -)。 # 三種輸入都省略時視為用法錯誤:空跑會印出「沒有待修項目」,那是假通過。 # # 輸入格式(由各自的腳本產生,本腳本不自己執行它們,維持唯讀): # --config jsc-cli/tools/scan-config.sh scan 的輸出 # {項目}{範圍}{必要}{現況}{說明}{修法}{判定} # 只取判定為 missing 與 invalid 的列,summary 列略過。 # --wiring jsc-hooks/tools/wire-cli.sh status {cli} 的輸出,前面掛上該 CLI 代號。 # 首行 status=... reason=...;其後 item{項目}{路徑}{present|missing} # 只有 status=unwired 才進待修表,每個 missing 項目一列。 # degraded 是 codex、copilot、antigravity、kiro 的健康狀態,不是缺失。 # --version jsc-hooks/hooks/version-guard.sh report 的輸出 # {domain}{本機}{遠端}{落後|最新|超前|查詢失敗} # 只取「落後」的列。noregistry 與 behind 列略過。 # # 輸出(TSV,一行一筆): # todo{序號}{類別}{項目}{範圍}{現況}{修法} # summary{missing 數}{invalid 數}{unwired 數}{落後數} # 類別排序固定為 missing、invalid、unwired、落後;同類別內照輸入順序。 # 一項都沒有時仍印一列 todo,類別欄為「無」,呼叫端照樣有東西可以呈現。 # # 結束碼:0=合併完成(有沒有待修項目都算完成,判斷交給呼叫端) # 2=用法錯誤(沒給任何輸入、選項寫錯、--wiring 少了 {cli}= 前綴) # 3=指定的輸入檔讀不到 set -u TAB=$(printf '\t') usage() { [ -n "${WORK:-}" ] && rm -rf "$WORK" echo "用法:build-todo.sh [--config {檔案}]... [--wiring {cli}={檔案}]... [--version {檔案}]..." >&2 exit 2 } WORK=$(mktemp -d 2>/dev/null) || { echo "無法建立暫存目錄" >&2; exit 3; } F_MISSING="$WORK/missing"; F_INVALID="$WORK/invalid" F_UNWIRED="$WORK/unwired"; F_BEHIND="$WORK/behind" : > "$F_MISSING"; : > "$F_INVALID"; : > "$F_UNWIRED"; : > "$F_BEHIND" cleanup() { rm -rf "$WORK"; } die() { cleanup; echo "$1" >&2; exit "$2"; } # 輸入檔的存在性先在主 shell 檢查。放進管線裡檢查的話,die 只會結束子 shell, # 主流程照樣往下跑,最後印出一張少了整類項目卻看起來正常的表。 check_input() { # $1=路徑 [ "$1" = "-" ] && return 0 [ -f "$1" ] || die "讀不到輸入檔:$1" 3 [ -r "$1" ] || die "輸入檔沒有讀取權限:$1" 3 } # 取得一份輸入的內容。「-」讀標準輸入,其餘一律當檔案路徑。 slurp() { # $1=路徑 if [ "$1" = "-" ]; then cat; else cat "$1"; fi } # scan-config.sh scan 的輸出 → missing 與 invalid 兩類。 take_config() { # $1=路徑 slurp "$1" | while IFS="$TAB" read -r key scope req actual desc fix verdict; do [ -n "${key:-}" ] || continue [ "$key" = summary ] && continue case "${verdict:-}" in missing) printf '%s\t%s\t%s\t%s\n' "$key" "${scope:--}" "${actual:--}" "${fix:--}" >> "$F_MISSING" ;; invalid) printf '%s\t%s\t%s\t%s\n' "$key" "${scope:--}" "${actual:--}" "${fix:--}" >> "$F_INVALID" ;; esac done } # wire-cli.sh status 的輸出 → unwired 一類。$1=cli $2=路徑 take_wiring() { _txt="$WORK/wiring.$$" slurp "$2" > "$_txt" # 只有 status=unwired 才是待修。wired 沒事,degraded 是四支非 Claude CLI 的健康狀態, # skipped 代表那支 CLI 根本沒裝,三者都不該出現在待修表上。 grep -q '^status=unwired' "$_txt" || { rm -f "$_txt"; return 0; } while IFS="$TAB" read -r kind item path state; do [ "${kind:-}" = item ] || continue [ "${state:-}" = missing ] || continue printf '%s\t%s\t%s\t%s\n' "${item:--}" "$1" "缺少接線:${path:--}" "jsc-hooks:hooks-install" >> "$F_UNWIRED" done < "$_txt" rm -f "$_txt" } # version-guard.sh report 的輸出 → 落後一類。 take_version() { # $1=路徑 slurp "$1" | while IFS="$TAB" read -r domain local_v remote_v state; do [ -n "${domain:-}" ] || continue case "$domain" in behind|noregistry) continue ;; esac [ "${state:-}" = "落後" ] || continue printf '%s\t%s\t%s\t%s\n' "jsc-$domain" "版本" "本機 ${local_v:--}、遠端 ${remote_v:--}" "jsc-cli:deploy update" >> "$F_BEHIND" done } got=0 while [ "$#" -gt 0 ]; do case "$1" in --config) [ "$#" -ge 2 ] || usage; check_input "$2"; take_config "$2"; got=1; shift 2 ;; --version) [ "$#" -ge 2 ] || usage; check_input "$2"; take_version "$2"; got=1; shift 2 ;; --wiring) [ "$#" -ge 2 ] || usage case "$2" in *=*) ;; *) usage ;; esac _cli=${2%%=*}; _file=${2#*=} [ -n "$_cli" ] && [ -n "$_file" ] || usage check_input "$_file"; take_wiring "$_cli" "$_file"; got=1; shift 2 ;; *) usage ;; esac done [ "$got" = 1 ] || usage n_missing=$(wc -l < "$F_MISSING" | tr -d ' ') n_invalid=$(wc -l < "$F_INVALID" | tr -d ' ') n_unwired=$(wc -l < "$F_UNWIRED" | tr -d ' ') n_behind=$(wc -l < "$F_BEHIND" | tr -d ' ') seq_no=0 emit_class() { # $1=類別 $2=檔案 while IFS="$TAB" read -r item scope actual fix; do [ -n "${item:-}" ] || continue seq_no=$((seq_no + 1)) printf 'todo\t%s\t%s\t%s\t%s\t%s\t%s\n' "$seq_no" "$1" "$item" "$scope" "$actual" "$fix" done < "$2" } emit_class missing "$F_MISSING" emit_class invalid "$F_INVALID" emit_class unwired "$F_UNWIRED" emit_class 落後 "$F_BEHIND" [ "$seq_no" -gt 0 ] || printf 'todo\t1\t無\t沒有待修項目\t-\t-\t-\n' printf 'summary\t%s\t%s\t%s\t%s\n' "$n_missing" "$n_invalid" "$n_unwired" "$n_behind" cleanup exit 0