#!/usr/bin/env sh # test-clis.sh — 實際呼叫已安裝的 AI CLI,找出靜態設定看不出的環境問題。 # 用法: # test-clis.sh [cli...] # 輸出(TSV): # test{area}{cli}{test}{指令}{結束碼}{判定}{說明} # summary{ok}{warn}{fail}{skipped} # 判定: # ok 實際命令成功 # warn CLI 可用,但該功能在這支 CLI 或這個版本不是必要功能 # fail 命令失敗、逾時,或必要功能不存在 # skipped 未安裝或無法執行該項測試 # 結束碼:0=測試完成;2=用法錯誤;3=找不到 detect-clis.sh set -u HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) DETECT="$HERE/detect-clis.sh" TIMEOUT_SECONDS="${JSC_CLI_TEST_TIMEOUT:-10}" ok_count=0 warn_count=0 fail_count=0 skipped_count=0 [ -f "$DETECT" ] || { echo "找不到 detect-clis.sh:$DETECT" >&2; exit 3; } usage() { echo "用法:test-clis.sh [claude|codex|copilot|antigravity|kiro ...]" >&2 exit 2 } cli_bin() { case "$1" in antigravity) printf 'agy' ;; kiro) printf 'kiro-cli' ;; claude|codex|copilot) printf '%s' "$1" ;; *) return 1 ;; esac } emit() { # area cli test command code verdict detail printf 'test\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" case "$6" in ok) ok_count=$((ok_count + 1)) ;; warn) warn_count=$((warn_count + 1)) ;; fail) fail_count=$((fail_count + 1)) ;; skipped) skipped_count=$((skipped_count + 1)) ;; esac } run_capture() { # $@=command out_file=$(mktemp) || return 125 err_file=$(mktemp) || { rm -f "$out_file"; return 125; } if command -v timeout >/dev/null 2>&1; then timeout "$TIMEOUT_SECONDS" "$@" >"$out_file" 2>"$err_file" else "$@" >"$out_file" 2>"$err_file" fi code=$? text=$(cat "$out_file" "$err_file" 2>/dev/null | tr '\r\n\t' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//; s/.*TOKEN[^ ]*/[secret]/g' | cut -c1-180) rm -f "$out_file" "$err_file" RUN_CODE=$code RUN_TEXT=${text:-無輸出} return 0 } run_required() { # area cli test command... area=$1 shift cli=$1 test_name=$2 shift 2 cmd_text="$*" run_capture "$@" code=$RUN_CODE detail=$RUN_TEXT if [ "$code" -eq 0 ]; then emit "$area" "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" elif [ "$code" -eq 124 ]; then emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" else emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "$detail" fi } run_optional() { # area cli test command... area=$1 shift cli=$1 test_name=$2 shift 2 cmd_text="$*" run_capture "$@" code=$RUN_CODE detail=$RUN_TEXT if [ "$code" -eq 0 ]; then emit "$area" "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" elif [ "$code" -eq 124 ]; then emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" else emit "$area" "$cli" "$test_name" "$cmd_text" "$code" warn "$detail" fi } has_cli() { name=$1 "$DETECT" | awk -F '\t' -v name="$name" '$1 == name { found = 1 } END { exit found ? 0 : 1 }' } latest_dir() { for pat in "$@"; do # shellcheck disable=SC2086 ls -d $pat 2>/dev/null | sort | tail -n1 done | tail -n1 } claude_plugin_root() { reg="$HOME/.claude/plugins/installed_plugins.json" [ -f "$reg" ] || return 1 tr -d '\n' < "$reg" \ | sed -n 's/.*"jsc-cli@jsc"[[:space:]]*:[[:space:]]*\[[^]]*"installPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -n1 } skill_root() { case "$1" in claude) claude_plugin_root ;; codex) latest_dir "$HOME/.codex/plugins/cache/jsc/jsc-cli"/* ;; copilot) printf '%s\n' "$HOME/.copilot/installed-plugins/jsc/jsc-cli" ;; antigravity) latest_dir "$HOME/.antigravity"/*/jsc-cli "$HOME/.config/antigravity"/*/jsc-cli ;; kiro) printf '%s\n' "$HOME/.kiro/skills/jsc-cli" ;; *) return 1 ;; esac } skill_file() { root=$(skill_root "$1" 2>/dev/null || true) [ -n "$root" ] || return 1 for f in "$root/skills/doctor/SKILL.md" "$root/doctor/SKILL.md"; do [ -f "$f" ] && { printf '%s\n' "$f"; return 0; } done return 1 } hooks_wire() { if [ -n "${JSC_HOOKS_TOOLS:-}" ] && [ -f "$JSC_HOOKS_TOOLS/wire-cli.sh" ]; then printf '%s\n' "$JSC_HOOKS_TOOLS/wire-cli.sh" return 0 fi for f in "$HERE/../../hooks/tools/wire-cli.sh" "$HOME/.codex/plugins/cache/jsc/jsc-hooks"/*/tools/wire-cli.sh "$HOME/.claude/plugins/cache/jsc/jsc-hooks"/*/tools/wire-cli.sh "$HOME/.copilot/installed-plugins/jsc/jsc-hooks/tools/wire-cli.sh"; do [ -f "$f" ] && { printf '%s\n' "$f"; return 0; } done return 1 } test_skill() { cli=$1 if f=$(skill_file "$cli" 2>/dev/null); then emit skill "$cli" doctor-skill "$f" 0 ok "找到 doctor SKILL.md" else emit skill "$cli" doctor-skill "jsc-cli doctor skill" "-" fail "找不到已安裝的 doctor SKILL.md" fi } test_hooks() { cli=$1 if wire=$(hooks_wire 2>/dev/null); then run_required hook "$cli" hook-smoke sh "$wire" smoke "$cli" else emit hook "$cli" hook-smoke "wire-cli.sh smoke $cli" "-" skipped "找不到 wire-cli.sh" fi } test_one() { cli=$1 bin=$(cli_bin "$cli") || usage path=$(command -v "$bin" 2>/dev/null || true) if [ -z "$path" ]; then emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔" return 0 fi case "$cli" in antigravity) run_required cli "$cli" version "$path" --version run_optional cli "$cli" help "$path" --help run_required cli "$cli" plugin-list "$path" plugin list ;; kiro) run_required cli "$cli" version "$path" --version run_optional cli "$cli" help "$path" --help-all run_optional cli "$cli" plugin-list "$path" plugin list ;; *) run_required cli "$cli" version "$path" --version run_optional cli "$cli" help "$path" --help run_required cli "$cli" plugin-list "$path" plugin list ;; esac test_skill "$cli" test_hooks "$cli" } if [ "$#" -eq 0 ]; then set -- $("$DETECT" | cut -f1) fi if [ "$#" -eq 0 ]; then emit cli all executable "-" "-" skipped "未偵測到任何支援的 CLI" printf 'summary\t%s\t%s\t%s\t%s\n' "$ok_count" "$warn_count" "$fail_count" "$skipped_count" exit 0 fi for cli in "$@"; do case "$cli" in claude|codex|copilot|antigravity|kiro) if has_cli "$cli"; then test_one "$cli" else bin=$(cli_bin "$cli") || usage emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔" fi ;; *) usage ;; esac done printf 'summary\t%s\t%s\t%s\t%s\n' "$ok_count" "$warn_count" "$fail_count" "$skipped_count" exit 0