fix(cli): 補齊稽核缺失並修掉護欄失效
What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env sh
|
||||
# deploy.sh — 對單一 CLI 執行 jsc 技能組的安裝、更新或解除安裝。
|
||||
# 用法:
|
||||
# deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]
|
||||
# -n 或 --dry-run(或 JSC_DEPLOY_DRYRUN=1):只印指令,不執行。
|
||||
# 輸出(TSV,一行一筆):
|
||||
# cmd<TAB>{指令} 即將執行的指令
|
||||
# exit<TAB>{結束碼}<TAB>{指令} 該指令的結束碼;dry-run 時結束碼印「-」
|
||||
# skip<TAB>{domain}<TAB>{原因} 本地 clone 是開發中的樹,略過 git pull
|
||||
# result<TAB>{cli}<TAB>{mode}<TAB>{domain 清單}<TAB>{ok|fail}
|
||||
# 結束碼:全部指令成功 0;任一指令失敗 1;參數錯誤 2。skip 不算失敗,但呼叫端要據實回報。
|
||||
# marketplace 指令一輪只跑一次:install 與 update 先跑,uninstall 最後跑。
|
||||
# 各 CLI 的細節都收在這裡,SKILL.md 只描述何時呼叫與參數:
|
||||
# antigravity 不接受 gitea URL,先 clone 到本地再從路徑安裝,更新時 pull 同一份。
|
||||
# kiro 的執行檔是 kiro-cli;plugin 指令失敗時退回複製 skills 目錄。
|
||||
# 環境變數:
|
||||
# GITEA_HOST Gitea 站台,可省略 scheme(預設 https://gitea.jsc.idv.tw)
|
||||
# JSC_GITEA_OWNER 存取庫的 owner(預設 plugins)
|
||||
# JSC_LOCAL_PLUGINS antigravity/kiro 用的本地 clone 目錄
|
||||
# 預設 $JSC_HOME/plugins(即 ~/.jsc/plugins),刻意不用 ~/plugins:
|
||||
# 那是維護者放開發 checkout 的地方,pull 下去會蓋掉未提交的工作。
|
||||
# 指到開發中的樹(有未提交變更或未推送的 commit)時只印 skip,不 pull。
|
||||
# JSC_KIRO_SKILLS kiro 退路用的 skills 目錄(預設 ~/.kiro/skills)
|
||||
# JSC_DEPLOY_DRYRUN 設為 1 等同 -n
|
||||
set -u
|
||||
|
||||
# Gitea 站台一律讀 GITEA_HOST(技能準則指定的變數),未設定才用正本站台。
|
||||
HOST="${GITEA_HOST:-https://gitea.jsc.idv.tw}"
|
||||
case "$HOST" in http://*|https://*) ;; *) HOST="https://$HOST" ;; esac
|
||||
HOST="${HOST%/}"
|
||||
OWNER="${JSC_GITEA_OWNER:-plugins}"
|
||||
MKT="$HOST/$OWNER/meta.git"
|
||||
REPO_BASE="$HOST/$OWNER"
|
||||
LOCAL_DIR="${JSC_LOCAL_PLUGINS:-${JSC_HOME:-$HOME/.jsc}/plugins}"
|
||||
KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}"
|
||||
DRYRUN="${JSC_DEPLOY_DRYRUN:-0}"
|
||||
FAILED=0
|
||||
|
||||
# CLI 代號 → 實際執行檔。唯一真實來源是 jsc-hooks 的 hooks/lib.sh cli_bin()。
|
||||
# 這裡保留一份副本,因為這支腳本是整組技能的安裝入口:jsc-hooks 還沒裝上來時
|
||||
# 也要能跑,不能 source 一個可能不存在的檔案。lib.sh 的對應表改了就同步改這裡。
|
||||
cli_bin() { # $1=CLI 代號
|
||||
case "$1" in
|
||||
antigravity) printf 'agy' ;;
|
||||
kiro) printf 'kiro-cli' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "用法:deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 執行一個指令,並印出指令本身與結束碼。失敗就記進 FAILED。
|
||||
run() { # $@=指令
|
||||
printf 'cmd\t%s\n' "$*"
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'exit\t-\t%s\n' "$*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
code=$?
|
||||
printf 'exit\t%s\t%s\n' "$code" "$*"
|
||||
[ "$code" -eq 0 ] || FAILED=1
|
||||
return "$code"
|
||||
}
|
||||
|
||||
# 同 run,但失敗不記進 FAILED——留給「失敗還有退路」的指令用。
|
||||
run_soft() { # $@=指令
|
||||
printf 'cmd\t%s\n' "$*"
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'exit\t-\t%s\n' "$*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
code=$?
|
||||
printf 'exit\t%s\t%s\n' "$code" "$*"
|
||||
return "$code"
|
||||
}
|
||||
|
||||
# 這份 clone 是不是「開發中的樹」。有原因就印出原因,沒有就不印。
|
||||
# 判斷兩件事:有未提交變更,或有還沒推上去的 commit。兩者被 pull 蓋掉都救不回來。
|
||||
local_hold() { # $1=存取庫路徑
|
||||
if [ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]; then
|
||||
printf '有未提交變更'
|
||||
return 0
|
||||
fi
|
||||
up=$(git -C "$1" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null) || return 0
|
||||
[ -n "$up" ] || return 0
|
||||
ahead=$(git -C "$1" rev-list --count "$up..HEAD" 2>/dev/null) || return 0
|
||||
[ "${ahead:-0}" -eq 0 ] || printf '有 %s 個未推送的 commit' "$ahead"
|
||||
}
|
||||
|
||||
# 把某 domain 的存取庫抓到本地:有 .git 就 pull,沒有就 clone。
|
||||
# 目標是開發中的樹時只印 skip,改用現地內容安裝,不 pull:這支腳本可以被指到任何
|
||||
# 目錄,蓋掉維護者未提交或未推送的工作救不回來,安裝一份舊內容還能重跑。
|
||||
sync_local() { # $1=domain
|
||||
dir="$LOCAL_DIR/$1"
|
||||
if [ -d "$dir/.git" ]; then
|
||||
hold=$(local_hold "$dir")
|
||||
if [ -n "$hold" ]; then
|
||||
printf 'skip\t%s\t%s %s,未執行 git pull\n' "$1" "$dir" "$hold"
|
||||
return 0
|
||||
fi
|
||||
run git -C "$dir" pull
|
||||
else
|
||||
run git clone "$REPO_BASE/$1.git" "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
# claude、copilot、kiro-cli 共用的 plugin 指令組。
|
||||
marketplace_cli() { # $1=執行檔
|
||||
case "$MODE" in
|
||||
install)
|
||||
run "$1" plugin marketplace add "$MKT"
|
||||
for d in $DOMAINS; do run "$1" plugin install "jsc-$d@jsc"; done
|
||||
;;
|
||||
update)
|
||||
run "$1" plugin marketplace update jsc
|
||||
for d in $DOMAINS; do run "$1" plugin update "jsc-$d@jsc"; done
|
||||
;;
|
||||
uninstall)
|
||||
for d in $DOMAINS; do run "$1" plugin uninstall "jsc-$d@jsc"; done
|
||||
run "$1" plugin marketplace remove jsc
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
deploy_codex() {
|
||||
bin=$(cli_bin codex)
|
||||
case "$MODE" in
|
||||
install)
|
||||
run "$bin" plugin marketplace add "$MKT"
|
||||
for d in $DOMAINS; do run "$bin" plugin add "jsc-$d@jsc"; done
|
||||
;;
|
||||
update)
|
||||
run "$bin" plugin marketplace upgrade jsc
|
||||
;;
|
||||
uninstall)
|
||||
for d in $DOMAINS; do run "$bin" plugin remove "jsc-$d@jsc"; done
|
||||
run "$bin" plugin marketplace remove jsc
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
deploy_antigravity() {
|
||||
bin=$(cli_bin antigravity)
|
||||
for d in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install)
|
||||
sync_local "$d"
|
||||
run "$bin" plugin install "$LOCAL_DIR/$d"
|
||||
;;
|
||||
update)
|
||||
sync_local "$d"
|
||||
run "$bin" plugin uninstall "jsc-$d"
|
||||
run "$bin" plugin install "$LOCAL_DIR/$d"
|
||||
;;
|
||||
uninstall)
|
||||
run "$bin" plugin uninstall "jsc-$d"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# plugin 指令不支援時的退路:從本地 clone 複製 skills 目錄。
|
||||
kiro_copy() { # $1=domain
|
||||
sync_local "$1"
|
||||
run mkdir -p "$KIRO_SKILLS/jsc-$1"
|
||||
run cp -R "$LOCAL_DIR/$1/skills/." "$KIRO_SKILLS/jsc-$1/"
|
||||
}
|
||||
|
||||
deploy_kiro() {
|
||||
bin=$(cli_bin kiro)
|
||||
case "$MODE" in
|
||||
install) run_soft "$bin" plugin marketplace add "$MKT" ;;
|
||||
update) run_soft "$bin" plugin marketplace update jsc ;;
|
||||
esac
|
||||
for d in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install)
|
||||
run_soft "$bin" plugin install "jsc-$d@jsc" || kiro_copy "$d"
|
||||
;;
|
||||
update)
|
||||
run_soft "$bin" plugin update "jsc-$d@jsc" || kiro_copy "$d"
|
||||
;;
|
||||
uninstall)
|
||||
run_soft "$bin" plugin uninstall "jsc-$d@jsc" || run rm -rf "$KIRO_SKILLS/jsc-$d"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ "$MODE" = uninstall ] && run_soft "$bin" plugin marketplace remove jsc
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-n|--dry-run) DRYRUN=1; shift ;;
|
||||
esac
|
||||
|
||||
[ $# -ge 3 ] || usage
|
||||
MODE=$1
|
||||
CLI=$2
|
||||
shift 2
|
||||
DOMAINS="$*"
|
||||
|
||||
case "$MODE" in
|
||||
install|update|uninstall) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
case "$CLI" in
|
||||
claude) marketplace_cli "$(cli_bin claude)" ;;
|
||||
copilot) marketplace_cli "$(cli_bin copilot)" ;;
|
||||
codex) deploy_codex ;;
|
||||
antigravity) deploy_antigravity ;;
|
||||
kiro) deploy_kiro ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
printf 'result\t%s\t%s\t%s\tok\n' "$CLI" "$MODE" "$DOMAINS"
|
||||
exit 0
|
||||
fi
|
||||
printf 'result\t%s\t%s\t%s\tfail\n' "$CLI" "$MODE" "$DOMAINS"
|
||||
exit 1
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env sh
|
||||
# list-models.sh — 讀各 AI CLI 的設定檔,列出模型與目前使用中的模型。
|
||||
# 輸出(TSV,一行一個模型):cli<TAB>model<TAB>in-use(in-use 為 yes 或 no)
|
||||
# 設定檔不存在或裡面沒寫模型,就不輸出該 CLI 的列;一律 exit 0。
|
||||
# 設定檔位置只寫在這裡,SKILL.md 不再抄一份:
|
||||
# claude ${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json 的 model;ANTHROPIC_MODEL 可覆寫使用中的模型
|
||||
# codex ${CODEX_HOME:-~/.codex}/config.toml:頂層 model 為使用中,[profiles.*] 的 model 併入清單
|
||||
# copilot ~/.copilot/settings.json,退回 ~/.config/copilot/settings.json
|
||||
# antigravity ~/.antigravity/settings.json,退回 ~/.config/antigravity/settings.json
|
||||
# kiro ~/.kiro/settings/cli.json,退回 ~/.config/kiro/settings.json
|
||||
# 五個 CLI 都沒有「列出可用模型」的指令,所以清單只到設定檔寫出來的模型。
|
||||
# 沒有列的 CLI 由 skills/models 的 sub agent 補上預設模型並標註「預設推定」。
|
||||
set -u
|
||||
|
||||
# 取第一個存在的檔案;都不存在就不印。
|
||||
first_file() { # $@=候選路徑
|
||||
for f in "$@"; do
|
||||
[ -f "$f" ] && { printf '%s\n' "$f"; return 0; }
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# 取 JSON 檔裡某個鍵的所有字串值,一行一個,順序照檔案。
|
||||
json_val() { # $1=檔案 $2=鍵名
|
||||
[ -n "$1" ] || return 0
|
||||
[ -f "$1" ] || return 0
|
||||
grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" "$1" 2>/dev/null \
|
||||
| sed -e 's/^[^:]*:[[:space:]]*"//' -e 's/"$//'
|
||||
return 0
|
||||
}
|
||||
|
||||
# 取 TOML 檔裡所有 model 設定值,一行一個,順序照檔案。
|
||||
# 只認行首的 model=,所以 model_reasoning_effort 這類鍵不會誤中。
|
||||
toml_val() { # $1=檔案
|
||||
[ -n "$1" ] || return 0
|
||||
[ -f "$1" ] || return 0
|
||||
grep -E '^[[:space:]]*model[[:space:]]*=' "$1" 2>/dev/null \
|
||||
| sed -e 's/^[^=]*=[[:space:]]*//' -e 's/[[:space:]]*#.*$//' \
|
||||
-e 's/^"//' -e 's/"[[:space:]]*$//' -e "s/^'//" -e "s/'[[:space:]]*$//"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 印出某 CLI 的列:使用中的模型排第一,重複的只留一筆。
|
||||
emit() { # $1=cli $2=使用中的模型(可空) $3=模型清單(換行分隔,可空)
|
||||
printf '%s\n%s\n' "$2" "$3" | awk -v cli="$1" -v cur="$2" '
|
||||
{ gsub(/^[ \t]+|[ \t]+$/, "") }
|
||||
$0 == "" { next }
|
||||
seen[$0]++ { next }
|
||||
{ printf "%s\t%s\t%s\n", cli, $0, ($0 == cur ? "yes" : "no") }'
|
||||
}
|
||||
|
||||
# claude
|
||||
cfg=$(first_file "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur="${ANTHROPIC_MODEL:-}"
|
||||
[ -n "$cur" ] || cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit claude "$cur" "$list"
|
||||
|
||||
# codex
|
||||
cfg=$(first_file "${CODEX_HOME:-$HOME/.codex}/config.toml")
|
||||
list=$(toml_val "$cfg")
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit codex "$cur" "$list"
|
||||
|
||||
# copilot
|
||||
cfg=$(first_file "$HOME/.copilot/settings.json" "$HOME/.config/copilot/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit copilot "$cur" "$list"
|
||||
|
||||
# antigravity
|
||||
cfg=$(first_file "$HOME/.antigravity/settings.json" "$HOME/.config/antigravity/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit antigravity "$cur" "$list"
|
||||
|
||||
# kiro
|
||||
cfg=$(first_file "$HOME/.kiro/settings/cli.json" "$HOME/.config/kiro/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit kiro "$cur" "$list"
|
||||
|
||||
exit 0
|
||||
@@ -65,13 +65,13 @@ current_cli() {
|
||||
}
|
||||
|
||||
# resolve 子指令:印出「目前 CLI 可用的第一個模型」單一名稱。
|
||||
# 簡化說明:這個 repo 目前沒有「列出某 CLI 實際安裝/可用模型」的 shell 級機制
|
||||
# ——現有的 skills/models 是靠 sub agent 讀各 CLI 的設定檔(~/.claude/settings.json、
|
||||
# ~/.codex/config.toml 等),屬於 LLM 才能做的判讀,沒辦法在這支 POSIX sh 腳本裡重現。
|
||||
# 因此這裡只用 current_cli() 抓到的「目前 CLI 名稱」這個既有訊號做記錄,
|
||||
# 實際判斷可用性一律簡化為「直接取模型鏈的第一個模型」,不逐一檢查該模型是否真的能用。
|
||||
# 之後若要做到依 CLI 實際可用模型過濾,可以在這裡比對 current_cli 的結果與
|
||||
# references/model-tags.md/detect-clis.sh 的輸出,逐一嘗試鏈上模型直到找到可用的。
|
||||
# 簡化說明:可用性一律簡化為「直接取模型鏈的第一個模型」,不逐一檢查該模型是否真的能用。
|
||||
# tools/list-models.sh 已經能讀各 CLI 設定檔列出模型,但五個 CLI 都沒有
|
||||
# 「列出可用模型」的指令,設定檔沒寫出來的模型仍然查不到;拿這份清單當白名單過濾,
|
||||
# 會把合法但沒寫進設定檔的模型誤判成不可用,比不過濾更糟。
|
||||
# 因此這裡只用 current_cli() 抓到的「目前 CLI 名稱」這個既有訊號做記錄。
|
||||
# 之後若要依 CLI 實際可用模型過濾,等各 CLI 提供列出模型的指令,再比對
|
||||
# list-models.sh 的輸出,逐一嘗試鏈上模型直到找到可用的。
|
||||
resolve_first_usable() { # $1=階段
|
||||
chain=$(resolve "$1" | cut -f1)
|
||||
[ -n "$chain" ] || return 0
|
||||
|
||||
Reference in New Issue
Block a user