feat(doctor): 納入 skill 與 hook 實測

This commit is contained in:
2026-08-28 16:31:06 +08:00
parent bfd01f71c4
commit f7be015934
4 changed files with 108 additions and 36 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
| 工具 | 用途 | | 工具 | 用途 |
| --- | --- | | --- | --- |
| `tools/detect-clis.sh` | 列出已安裝的 AI CLI 與執行檔路徑(TSV:name / path / version;antigravity 的執行檔為 `agy`、kiro 為 `kiro-cli`) | | `tools/detect-clis.sh` | 列出已安裝的 AI CLI 與執行檔路徑(TSV:name / path / version;antigravity 的執行檔為 `agy`、kiro 為 `kiro-cli`) |
| `tools/test-clis.sh` | 實際呼叫已偵測到的 AI CLI,跑版本、說明頁與 plugin 清單等唯讀命令(`test-clis.sh [cli...]`);印出每項命令、結束碼、判定與輸出摘要,最後一行 `summary` 標出通過、降級、失敗、略過數。`JSC_CLI_TEST_TIMEOUT` 可調整單項命令逾時秒數,預設 10 秒 | | `tools/test-clis.sh` | 實際呼叫已偵測到的 AI CLI,跑版本、說明頁、plugin 清單、doctor skill 載入與 hook smoke(`test-clis.sh [cli...]`);每列標出 `cli`、`skill`、`hook` 類型、結束碼、判定與輸出摘要,最後一行 `summary` 標出通過、降級、失敗、略過數。`JSC_CLI_TEST_TIMEOUT` 可調整單項命令逾時秒數,預設 10 秒 |
| `tools/deploy.sh` | 對單一 CLI 執行安裝、更新或解除安裝(`deploy.sh [-n] {mode} {cli} {domain}...`,mode 為 install / update / uninstall);印出每個指令與其結束碼,最後一行 `result` 標 ok 或 fail。`-n` 只印指令不執行。上表五個 CLI 的指令差異全部收在這支腳本裡。install 或 update 全數成功時,收尾轉呼叫 `jsc-hooks` 的 `restart-gate.sh require` 掛上重啟閘門,並印一行 `restart` 標出狀態檔位置;uninstall 不寫。尋找 `restart-gate.sh` 時優先用 `$JSC_HOME/current/jsc-hooks`、本地 clone 與 Kiro skills,最後才掃各 CLI 快取,避免部署收尾綁死單一 CLI 的版號路徑。狀態檔的路徑、格式與判讀全在 `restart-gate.sh`,這支腳本不自己拼——格式只留一個真實來源。站台取自 `GITEA_HOST`,本地 clone 目錄取自 `JSC_LOCAL_PLUGINS`,兩者的預設值見下表 | | `tools/deploy.sh` | 對單一 CLI 執行安裝、更新或解除安裝(`deploy.sh [-n] {mode} {cli} {domain}...`,mode 為 install / update / uninstall);印出每個指令與其結束碼,最後一行 `result` 標 ok 或 fail。`-n` 只印指令不執行。上表五個 CLI 的指令差異全部收在這支腳本裡。install 或 update 全數成功時,收尾轉呼叫 `jsc-hooks` 的 `restart-gate.sh require` 掛上重啟閘門,並印一行 `restart` 標出狀態檔位置;uninstall 不寫。尋找 `restart-gate.sh` 時優先用 `$JSC_HOME/current/jsc-hooks`、本地 clone 與 Kiro skills,最後才掃各 CLI 快取,避免部署收尾綁死單一 CLI 的版號路徑。狀態檔的路徑、格式與判讀全在 `restart-gate.sh`,這支腳本不自己拼——格式只留一個真實來源。站台取自 `GITEA_HOST`,本地 clone 目錄取自 `JSC_LOCAL_PLUGINS`,兩者的預設值見下表 |
| `tools/check-requires.sh` | `check-requires.sh {cli} {manifest}` 檢查 manifest 的 `jsc.requires` 最低版本。沒有宣告就通過;版本不符或缺相依 plugin 就回 `status=blocked`。`deploy.sh update` 在每個 domain 更新前呼叫它,不符就跳過該 domain 並列出原因 | | `tools/check-requires.sh` | `check-requires.sh {cli} {manifest}` 檢查 manifest 的 `jsc.requires` 最低版本。沒有宣告就通過;版本不符或缺相依 plugin 就回 `status=blocked`。`deploy.sh update` 在每個 domain 更新前呼叫它,不符就跳過該 domain 並列出原因 |
| `tools/write-guides.sh` | 產生這台機器專屬的更新指引 `$JSC_HOME/update-guide.md` 與移除指引 `$JSC_HOME/remove-guide.md`(`write-guides.sh [-n] {install\|update} {domain}...`),一輪部署跑一次。CLI 清單取自 `detect-clis.sh`,每支 CLI 的指令字面直接取自 `deploy.sh -n` 的輸出,所以指引寫的就是實際會跑的指令;kiro 走不走本地複製退路也依實際偵測結果標注 | | `tools/write-guides.sh` | 產生這台機器專屬的更新指引 `$JSC_HOME/update-guide.md` 與移除指引 `$JSC_HOME/remove-guide.md`(`write-guides.sh [-n] {install\|update} {domain}...`),一輪部署跑一次。CLI 清單取自 `detect-clis.sh`,每支 CLI 的指令字面直接取自 `deploy.sh -n` 的輸出,所以指引寫的就是實際會跑的指令;kiro 走不走本地複製退路也依實際偵測結果標注 |
@@ -54,7 +54,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
### `doctor` ### `doctor`
一次體檢執行環境,只讀不改。五項檢查:技能版本(`jsc-hooks/hooks/version-guard.sh report`)、Hook 接線(`jsc-hooks/tools/wire-cli.sh status`,唯讀子命令)、CLI 實測(`tools/test-clis.sh` 實際呼叫版本、說明頁與 plugin 清單)、全域設定與自我設定(`tools/scan-config.sh` 比對 `tools/config-spec.tsv`)。每項各出一張表,整份結果寫進 wiki `CHECK_{HASH}`,`HASH` 取 `{主機名}/{登入帳號}`,只保留最新一次。修復交給 `/jsc-cli:setup`,體檢本身不動任何設定。 一次體檢執行環境,只讀不改。五項檢查:技能版本(`jsc-hooks/hooks/version-guard.sh report`)、Hook 接線(`jsc-hooks/tools/wire-cli.sh status`,唯讀子命令)、CLI、skill、hook 實測(`tools/test-clis.sh` 實際呼叫版本、說明頁、plugin 清單、doctor skill 載入與 hook smoke)、全域設定與自我設定(`tools/scan-config.sh` 比對 `tools/config-spec.tsv`)。每項各出一張表,整份結果寫進 wiki `CHECK_{HASH}`,`HASH` 取 `{主機名}/{登入帳號}`,只保留最新一次。修復交給 `/jsc-cli:setup`,體檢本身不動任何設定。
### `setup` ### `setup`
+8 -6
View File
@@ -1,6 +1,6 @@
--- ---
name: doctor name: doctor
description: Health-check the execution environment in one pass and record the result, changing nothing. Five checks - plugin versions from jsc-hooks/hooks/version-guard.sh report, hook wiring from jsc-hooks/tools/wire-cli.sh status, executable CLI tests from tools/test-clis.sh, global settings and current-directory settings from tools/scan-config.sh against tools/config-spec.tsv. Report one findings table per check, then write the whole run to wiki CHECK_{HASH} where HASH comes from {hostname}/{user}; the page keeps only the latest run. Use after installing or updating the skill set, when a skill fails on a settings, wiring or CLI runtime problem, or before handing a machine over; not for applying fixes, which is jsc-cli:setup. description: Health-check the execution environment in one pass and record the result, changing nothing. Five checks - plugin versions from jsc-hooks/hooks/version-guard.sh report, hook wiring from jsc-hooks/tools/wire-cli.sh status, CLI, skill and hook runtime tests from tools/test-clis.sh, global settings and current-directory settings from tools/scan-config.sh against tools/config-spec.tsv. Report one findings table per check, then write the whole run to wiki CHECK_{HASH}; use for checkups, not fixes.
--- ---
# doctor - execution environment health check # doctor - execution environment health check
@@ -29,18 +29,20 @@ Done when every detected CLI has a status and its missing items are listed.
## 3. CLI runtime tests ## 3. CLI runtime tests
Run `tools/test-clis.sh` with no CLI arguments. It calls `tools/detect-clis.sh`, then runs real read-only commands for every detected CLI. Run `tools/test-clis.sh` with no CLI arguments. It calls `tools/detect-clis.sh`, then runs real commands for every detected CLI. It covers three areas: CLI commands, skill loading, and hook runtime smoke.
Output: Output:
- `test<TAB>{cli}<TAB>{test}<TAB>{command}<TAB>{exit-code}<TAB>{verdict}<TAB>{detail}` - `test<TAB>{area}<TAB>{cli}<TAB>{test}<TAB>{command}<TAB>{exit-code}<TAB>{verdict}<TAB>{detail}`
- `summary<TAB>{ok}<TAB>{warn}<TAB>{fail}<TAB>{skipped}` - `summary<TAB>{ok}<TAB>{warn}<TAB>{fail}<TAB>{skipped}`
Areas: `cli`, `skill`, `hook`.
Verdicts: `ok`, `warn`, `fail`, `skipped`. Verdicts: `ok`, `warn`, `fail`, `skipped`.
Exit codes: 0 completed, 2 usage error, 3 missing `detect-clis.sh`. Any other script exit code is itself a doctor finding. Exit codes: 0 completed, 2 usage error, 3 missing `detect-clis.sh`. Any other script exit code is itself a doctor finding.
Treat `fail` as a machine problem. Treat `warn` as degraded capability: name it in the report, but do not put it in the fix table unless the failing skill needs that feature. Treat `skipped` as no conclusion. Map the report labels to the template as `ok` -> `通過`, `warn` -> `降級`, `fail` -> `失敗`, and `skipped` -> `略過`. Treat every `fail` as a machine problem, including `skill` and `hook` rows. Treat `warn` as degraded capability: name it in the report, but do not put it in the fix table unless the failing skill needs that feature. Treat `skipped` as no conclusion. Map the report labels to the template as `ok` -> `通過`, `warn` -> `降級`, `fail` -> `失敗`, and `skipped` -> `略過`.
Done when every detected CLI has at least a version test row and the summary line is read. Done when every detected CLI has at least a version test row and the summary line is read.
@@ -70,7 +72,7 @@ Done when the scanned directory is stated and every project row has a verdict.
## 6. Report and record ## 6. Report and record
Report all five tables per `templates/check-page.md`. Then build the `待修項目` table from every `missing`, `invalid`, `unwired` and CLI runtime `fail` item, plus every domain reported `落後`. Order them `missing` -> `invalid` -> `unwired` -> `runtime-fail` -> `落後`. Nothing wrong -> one row reading `無`. Report all five tables per `templates/check-page.md`. Then build the `待修項目` table from every `missing`, `invalid`, `unwired` and runtime `fail` item, plus every domain reported `落後`. Order them `missing` -> `invalid` -> `unwired` -> `runtime-fail` -> `落後`. Nothing wrong -> one row reading `無`.
Write the page through `jsc-gitea:wiki`: Write the page through `jsc-gitea:wiki`:
@@ -85,6 +87,6 @@ Done when either the wiki page URL is reported, or the skipped write is reported
## 7. Hand off ## 7. Hand off
State the counts: required items missing, settings invalid, CLIs unwired, CLI runtime failures, domains behind. Recommend `/jsc-cli:setup` when any of those is above zero. Never fix anything here. State the counts: required items missing, settings invalid, CLIs unwired, runtime failures grouped by `cli`, `skill` and `hook`, and domains behind. Recommend `/jsc-cli:setup` when any of those is above zero. Never fix anything here.
Done when the counts are stated and the recommendation is given or explicitly withheld. Done when the counts are stated and the recommendation is given or explicitly withheld.
+4 -4
View File
@@ -31,9 +31,9 @@
## CLI 實測 ## CLI 實測
| CLI | 測試 | 指令 | 結束碼 | 判定 | 說明 | | 類型 | CLI | 測試 | 指令 | 結束碼 | 判定 | 說明 |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| {cli} | {version、help、plugin-list} | {實際命令} | {結束碼} | {通過、降級、失敗、略過} | {輸出摘要或錯誤原因} | | {cli、skill、hook} | {cli} | {version、help、plugin-list、doctor-skill、hook-smoke} | {實際命令或檔案} | {結束碼} | {通過、降級、失敗、略過} | {輸出摘要或錯誤原因} |
## 全域設定 ## 全域設定
@@ -55,7 +55,7 @@
| 順序 | 項目 | 範圍 | 判定 | 修法 | 影響 | | 順序 | 項目 | 範圍 | 判定 | 修法 | 影響 |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| {n} | {變數、檔案、hook、CLI 測試或 domain} | {全域、自我、CLI} | {缺漏、設錯、未接線、實測失敗、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} | | {n} | {變數、檔案、hook、CLI 測試、skill 測試或 domain} | {全域、自我、CLI、skill、hook} | {缺漏、設錯、未接線、實測失敗、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} |
## 未登錄變數 ## 未登錄變數
+94 -24
View File
@@ -3,7 +3,7 @@
# 用法: # 用法:
# test-clis.sh [cli...] # test-clis.sh [cli...]
# 輸出(TSV): # 輸出(TSV):
# test<TAB>{cli}<TAB>{test}<TAB>{指令}<TAB>{結束碼}<TAB>{判定}<TAB>{說明} # test<TAB>{area}<TAB>{cli}<TAB>{test}<TAB>{指令}<TAB>{結束碼}<TAB>{判定}<TAB>{說明}
# summary<TAB>{ok}<TAB>{warn}<TAB>{fail}<TAB>{skipped} # summary<TAB>{ok}<TAB>{warn}<TAB>{fail}<TAB>{skipped}
# 判定: # 判定:
# ok 實際命令成功 # ok 實際命令成功
@@ -37,9 +37,9 @@ cli_bin() {
esac esac
} }
emit() { # cli test command code verdict detail emit() { # area cli test command code verdict detail
printf 'test\t%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" 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 "$5" in case "$6" in
ok) ok_count=$((ok_count + 1)) ;; ok) ok_count=$((ok_count + 1)) ;;
warn) warn_count=$((warn_count + 1)) ;; warn) warn_count=$((warn_count + 1)) ;;
fail) fail_count=$((fail_count + 1)) ;; fail) fail_count=$((fail_count + 1)) ;;
@@ -63,7 +63,9 @@ run_capture() { # $@=command
return 0 return 0
} }
run_required() { # cli test command... run_required() { # area cli test command...
area=$1
shift
cli=$1 cli=$1
test_name=$2 test_name=$2
shift 2 shift 2
@@ -72,15 +74,17 @@ run_required() { # cli test command...
code=$RUN_CODE code=$RUN_CODE
detail=$RUN_TEXT detail=$RUN_TEXT
if [ "$code" -eq 0 ]; then if [ "$code" -eq 0 ]; then
emit "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" ok "$detail"
elif [ "$code" -eq 124 ]; then elif [ "$code" -eq 124 ]; then
emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)"
else else
emit "$cli" "$test_name" "$cmd_text" "$code" fail "$detail" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "$detail"
fi fi
} }
run_optional() { # cli test command... run_optional() { # area cli test command...
area=$1
shift
cli=$1 cli=$1
test_name=$2 test_name=$2
shift 2 shift 2
@@ -89,11 +93,11 @@ run_optional() { # cli test command...
code=$RUN_CODE code=$RUN_CODE
detail=$RUN_TEXT detail=$RUN_TEXT
if [ "$code" -eq 0 ]; then if [ "$code" -eq 0 ]; then
emit "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" ok "$detail"
elif [ "$code" -eq 124 ]; then elif [ "$code" -eq 124 ]; then
emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)"
else else
emit "$cli" "$test_name" "$cmd_text" "$code" warn "$detail" emit "$area" "$cli" "$test_name" "$cmd_text" "$code" warn "$detail"
fi fi
} }
@@ -102,32 +106,98 @@ has_cli() {
"$DETECT" | awk -F '\t' -v name="$name" '$1 == name { found = 1 } END { exit found ? 0 : 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() { test_one() {
cli=$1 cli=$1
bin=$(cli_bin "$cli") || usage bin=$(cli_bin "$cli") || usage
path=$(command -v "$bin" 2>/dev/null || true) path=$(command -v "$bin" 2>/dev/null || true)
if [ -z "$path" ]; then if [ -z "$path" ]; then
emit "$cli" executable "$bin" "-" skipped "未偵測到執行檔" emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔"
return 0 return 0
fi fi
case "$cli" in case "$cli" in
antigravity) antigravity)
run_required "$cli" version "$path" --version run_required cli "$cli" version "$path" --version
run_optional "$cli" help "$path" --help run_optional cli "$cli" help "$path" --help
run_required "$cli" plugin-list "$path" plugin list run_required cli "$cli" plugin-list "$path" plugin list
;; ;;
kiro) kiro)
run_required "$cli" version "$path" --version run_required cli "$cli" version "$path" --version
run_optional "$cli" help "$path" --help-all run_optional cli "$cli" help "$path" --help-all
run_optional "$cli" plugin-list "$path" plugin list run_optional cli "$cli" plugin-list "$path" plugin list
;; ;;
*) *)
run_required "$cli" version "$path" --version run_required cli "$cli" version "$path" --version
run_optional "$cli" help "$path" --help run_optional cli "$cli" help "$path" --help
run_required "$cli" plugin-list "$path" plugin list run_required cli "$cli" plugin-list "$path" plugin list
;; ;;
esac esac
test_skill "$cli"
test_hooks "$cli"
} }
if [ "$#" -eq 0 ]; then if [ "$#" -eq 0 ]; then
@@ -135,7 +205,7 @@ if [ "$#" -eq 0 ]; then
fi fi
if [ "$#" -eq 0 ]; then if [ "$#" -eq 0 ]; then
emit all executable "-" "-" skipped "未偵測到任何支援的 CLI" 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" printf 'summary\t%s\t%s\t%s\t%s\n' "$ok_count" "$warn_count" "$fail_count" "$skipped_count"
exit 0 exit 0
fi fi
@@ -147,7 +217,7 @@ for cli in "$@"; do
test_one "$cli" test_one "$cli"
else else
bin=$(cli_bin "$cli") || usage bin=$(cli_bin "$cli") || usage
emit "$cli" executable "$bin" "-" skipped "未偵測到執行檔" emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔"
fi fi
;; ;;
*) usage ;; *) usage ;;