From 0b2c7b13a0c72cd51ee281b472ee4a646e6a4e27 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 28 Aug 2026 16:23:05 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(doctor):=20=E5=8A=A0=E5=85=A5=20CLI=20?= =?UTF-8?q?=E5=AF=A6=E6=B8=AC=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- skills/doctor/SKILL.md | 59 +++++++++----- templates/check-contents.md | 6 +- templates/check-page.md | 9 +- tools/test-clis.sh | 158 ++++++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 25 deletions(-) create mode 100755 tools/test-clis.sh diff --git a/README.md b/README.md index 1f834a7..456507b 100644 --- a/README.md +++ b/README.md @@ -23,6 +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/test-clis.sh` | 實際呼叫已偵測到的 AI CLI,跑版本、說明頁與 plugin 清單等唯讀命令(`test-clis.sh [cli...]`);印出每項命令、結束碼、判定與輸出摘要,最後一行 `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/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 走不走本地複製退路也依實際偵測結果標注 | @@ -53,7 +54,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 ### `doctor` -一次體檢執行環境,只讀不改。四項檢查:技能版本(`jsc-hooks/hooks/version-guard.sh report`)、Hook 接線(`jsc-hooks/tools/wire-cli.sh status`,唯讀子命令)、全域設定與自我設定(`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 實測(`tools/test-clis.sh` 實際呼叫版本、說明頁與 plugin 清單)、全域設定與自我設定(`tools/scan-config.sh` 比對 `tools/config-spec.tsv`)。每項各出一張表,整份結果寫進 wiki `CHECK_{HASH}`,`HASH` 取 `{主機名}/{登入帳號}`,只保留最新一次。修復交給 `/jsc-cli:setup`,體檢本身不動任何設定。 ### `setup` @@ -72,6 +73,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `JSC_DEPLOY_DRYRUN` | 設為 `1` 等同 `deploy.sh -n`,只印指令不執行 | 照常執行 | | `JSC_WIKI_REPO_CHECK` | 體檢頁 `CHECK_CONTENTS`、`CHECK_{HASH}` 所在的 `{owner}/{repo}` | 退回 `JSC_WIKI_REPO`;兩個都沒有就略過寫入,並把這一項列進待修 | | `JSC_CONFIG_SPEC` | 改讀別份設定規格表(測試 `scan-config.sh` 時用) | 用 `tools/config-spec.tsv` | +| `JSC_CLI_TEST_TIMEOUT` | `test-clis.sh` 單項 CLI 實測命令的逾時秒數 | 用 `10` | | `JSC_HOME` | hook 資料目錄,兩份指引與重啟狀態檔都寫在這裡 | 用 `~/.jsc` | | `JSC_RESTART_GATE` | 設成 `off` 可略過部署後的重啟提示閘門(判讀在 `jsc-hooks`,`jsc-cli` 只負責寫狀態檔) | 照常提示重啟 | diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index 2d89224..2ea568a 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -1,45 +1,64 @@ --- name: doctor -description: Health-check the execution environment in one pass and record the result, changing nothing. Four checks - plugin versions from jsc-hooks/hooks/version-guard.sh report, hook wiring from jsc-hooks/tools/wire-cli.sh status, 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 or wiring 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, 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. --- -# doctor — execution environment health check +# doctor - execution environment health check -Read-only. Every command below either reads a file or asks Gitea; none of them writes a setting. That is the contract with `jsc-cli:setup`: doctor states the facts, setup changes things. +Read-only. Every command below either reads local state, calls a read-only CLI command, or asks Gitea. None of them writes a setting. That is the contract with `jsc-cli:setup`: doctor states the facts, setup changes things. -Collection (steps 1 to 4) **MUST run as a sub agent** — one sub agent for all four, returning the raw TSV lines. Only the report and the wiki write stay in the main agent. +Collection (steps 1 to 5) **MUST run as a sub agent** - one sub agent for all five, returning the raw TSV lines. Only the report and the wiki write stay in the main agent. ## 1. Skill versions -Run `jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}{本機}{遠端}{落後|最新|超前|查詢失敗}` per plugin, then `behind{count}`. +Run `jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}{local}{remote}{落後|最新|超前|查詢失敗}` per plugin, then `behind{count}`. -A report with no `{domain}` row, or one carrying `noregistry{path}`, means this CLI has no local plugin registry. Report it as 無法驗證 — never as 最新. `behind0` proves nothing when no domain row precedes it. +A report with no `{domain}` row, or one carrying `noregistry{path}`, means this CLI has no local plugin registry. Report it as `無法驗證` - never as `最新`. `behind0` proves nothing when no domain row precedes it. Done when every installed domain has a status literal, or the CLI is reported as unverifiable. ## 2. Hook wiring -Run `jsc-hooks/tools/wire-cli.sh status {cli}` for every CLI that `tools/detect-clis.sh` found. Use `status` and nothing else: `wire-cli.sh` without a subcommand rewires, `purge` deletes, and `smoke` executes hooks — all three break the read-only contract. +Run `jsc-hooks/tools/wire-cli.sh status {cli}` for every CLI that `tools/detect-clis.sh` found. Use `status` and nothing else: `wire-cli.sh` without a subcommand rewires, `purge` deletes, and `smoke` executes hooks - all three break the read-only contract. -Exit codes: 0 wired, 1 degraded, 3 skipped (CLI not installed), 5 unwired. Each `item` line names one wiring point and whether it is present. +Exit codes: 0 wired, 1 degraded, 3 skipped because the CLI is not installed, 5 unwired. Each `item` line names one wiring point and whether it is present. -Only claude reaches `wired`. The other four have no pre-tool hook, so `degraded` is their healthy state — report the degradation reason as-is and never present it as a defect to fix. +Only claude reaches `wired`. The other four have no pre-tool hook, so `degraded` is their healthy state. Report the degradation reason as-is and never present it as a defect to fix. Done when every detected CLI has a status and its missing items are listed. -## 3. Global settings +## 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. + +Output: + +- `test{cli}{test}{command}{exit-code}{verdict}{detail}` +- `summary{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. + +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` -> `略過`. + +Done when every detected CLI has at least a version test row and the summary line is read. + +## 4. Global settings Run `tools/scan-config.sh scan global`. It checks every `scope=global` row of `tools/config-spec.tsv` and prints `itemscoperequiredactualexpectfixverdict`, closing with `summary{missing}{invalid}{unset}{skipped}`. -Verdicts: `ok`, `default` (unset, default works), `unset` (optional, feature degrades), `missing` (required, skills break), `invalid` (set but fails verification), `skipped` (offline). +Verdicts: `ok`, `default`, `unset`, `missing`, `invalid`, `skipped`. -Add `-o` when Gitea is unreachable; the Gitea-dependent rows then come back `skipped`. Report those rows as 未取得結論 and never as passes. +Map the report labels to the template as `ok` -> `通過`, `default` -> `走預設`, `unset` -> `未設定`, `missing` -> `缺漏`, `invalid` -> `設錯`, and `skipped` -> `略過`. -Also run `tools/scan-config.sh orphans` — variables used in the source but absent from the spec table. They are a maintenance note for the skill set, not a fault on this machine. +Add `-o` when Gitea is unreachable; the Gitea-dependent rows then come back `skipped`. Report those rows as inconclusive and never as passes. + +Also run `tools/scan-config.sh orphans` - variables used in the source but absent from the spec table. They are a maintenance note for the skill set, not a fault on this machine. Done when the summary line is read and every `missing` and `invalid` row is named. -## 4. Own settings +## 5. Own settings Run `tools/scan-config.sh scan project` from the current working directory. Same output format, `scope=project` rows only. @@ -49,23 +68,23 @@ When `.env` or `.envrc` exists, name the spec-table variables it overrides and s Done when the scanned directory is stated and every project row has a verdict. -## 5. Report and record +## 6. Report and record -Report all four tables per `templates/check-page.md`. Then build the 待修項目 table from every `missing`, `invalid` and `unwired` item, plus every domain reported 落後. Order them `missing` → `invalid` → `unwired` → `落後`. Nothing wrong → one row reading 無. +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 `無`. Write the page through `jsc-gitea:wiki`: - Wiki repo: `jsc-gitea/tools/gitea.sh wiki-repo CHECK`. -- Page name: `CHECK_` plus `gitea.sh hash-id "{hostname}/{user}"` — the host and the login account, not `{owner}/{repo}`. Doctor checks a machine, and it has to work in directories that are not repositories at all. +- Page name: `CHECK_` plus `gitea.sh hash-id "{hostname}/{user}"` - the host and the login account, not `{owner}/{repo}`. Doctor checks a machine, and it has to work in directories that are not repositories at all. - Overwrite the whole page. This page type keeps only the latest run. - Update `CHECK_CONTENTS` from `templates/check-contents.md` in the same pass. -`wiki-repo` exiting 3 means no wiki repo is configured for CHECK. Print the tables, skip the wiki write, and put `JSC_WIKI_REPO_CHECK` at the top of 待修項目 — that unset variable is itself a finding, so a failed write never fails the health check. +`wiki-repo` exiting 3 means no wiki repo is configured for CHECK. Print the tables, skip the wiki write, and put `JSC_WIKI_REPO_CHECK` at the top of the `待修項目` table - that unset variable is itself a finding, so a failed write never fails the health check. Done when either the wiki page URL is reported, or the skipped write is reported together with the reason. -## 6. Hand off +## 7. Hand off -State the counts: required items missing, settings invalid, CLIs unwired, 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, CLI runtime failures, 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. diff --git a/templates/check-contents.md b/templates/check-contents.md index fb4cb47..5b9bde1 100644 --- a/templates/check-contents.md +++ b/templates/check-contents.md @@ -2,6 +2,6 @@ > 由 `jsc-cli:doctor` 維護。每台執行環境一列;`HASH` 取 `{主機名}/{登入帳號}`,算法與其他頁面共用。 -| 體檢頁 | 主機 | 帳號 | 必要項缺漏 | 設定錯誤 | 最後體檢 | -| --- | --- | --- | --- | --- | --- | -| [[CHECK_{HASH}]] | {hostname} | {使用者帳號} | {n} | {n} | {yyyy-MM-dd HH:mm} | +| 體檢頁 | 主機 | 帳號 | 必要項缺漏 | 設定錯誤 | CLI 實測失敗 | 最後體檢 | +| --- | --- | --- | --- | --- | --- | --- | +| [[CHECK_{HASH}]] | {hostname} | {使用者帳號} | {n} | {n} | {n} | {yyyy-MM-dd HH:mm} | diff --git a/templates/check-page.md b/templates/check-page.md index a6de648..ef3ed79 100644 --- a/templates/check-page.md +++ b/templates/check-page.md @@ -13,6 +13,7 @@ | --- | --- | --- | --- | | 技能版本 | {n} | {n} | {n} | | Hook 接線 | {n} | {n} | {n} | +| CLI 實測 | {n} | {n} | {n} | | 全域設定 | {n} | {n} | {n} | | 自我設定 | {n} | {n} | {n} | @@ -28,6 +29,12 @@ | --- | --- | --- | --- | | {cli} | {wired、degraded、unwired、skipped} | {項目名,逗號分隔;無則寫「無」} | {降級原因或未偵測到執行檔} | +## CLI 實測 + +| CLI | 測試 | 指令 | 結束碼 | 判定 | 說明 | +| --- | --- | --- | --- | --- | --- | +| {cli} | {version、help、plugin-list} | {實際命令} | {結束碼} | {通過、降級、失敗、略過} | {輸出摘要或錯誤原因} | + ## 全域設定 | 項目 | 必要 | 現況 | 期望 | 修法 | 判定 | @@ -48,7 +55,7 @@ | 順序 | 項目 | 範圍 | 判定 | 修法 | 影響 | | --- | --- | --- | --- | --- | --- | -| {n} | {變數、檔案、hook 或 domain} | {全域、自我} | {缺漏、設錯、未接線、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} | +| {n} | {變數、檔案、hook、CLI 測試或 domain} | {全域、自我、CLI} | {缺漏、設錯、未接線、實測失敗、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} | ## 未登錄變數 diff --git a/tools/test-clis.sh b/tools/test-clis.sh new file mode 100755 index 0000000..565a3e8 --- /dev/null +++ b/tools/test-clis.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env sh +# test-clis.sh — 實際呼叫已安裝的 AI CLI,找出靜態設定看不出的環境問題。 +# 用法: +# test-clis.sh [cli...] +# 輸出(TSV): +# test{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() { # 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" + case "$5" 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() { # cli test command... + cli=$1 + test_name=$2 + shift 2 + cmd_text="$*" + run_capture "$@" + code=$RUN_CODE + detail=$RUN_TEXT + if [ "$code" -eq 0 ]; then + emit "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" + elif [ "$code" -eq 124 ]; then + emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" + else + emit "$cli" "$test_name" "$cmd_text" "$code" fail "$detail" + fi +} + +run_optional() { # cli test command... + cli=$1 + test_name=$2 + shift 2 + cmd_text="$*" + run_capture "$@" + code=$RUN_CODE + detail=$RUN_TEXT + if [ "$code" -eq 0 ]; then + emit "$cli" "$test_name" "$cmd_text" "$code" ok "$detail" + elif [ "$code" -eq 124 ]; then + emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" + else + emit "$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 }' +} + +test_one() { + cli=$1 + bin=$(cli_bin "$cli") || usage + path=$(command -v "$bin" 2>/dev/null || true) + if [ -z "$path" ]; then + emit "$cli" executable "$bin" "-" skipped "未偵測到執行檔" + return 0 + fi + + case "$cli" in + antigravity) + run_required "$cli" version "$path" --version + run_optional "$cli" help "$path" --help + run_required "$cli" plugin-list "$path" plugin list + ;; + kiro) + run_required "$cli" version "$path" --version + run_optional "$cli" help "$path" --help-all + run_optional "$cli" plugin-list "$path" plugin list + ;; + *) + run_required "$cli" version "$path" --version + run_optional "$cli" help "$path" --help + run_required "$cli" plugin-list "$path" plugin list + ;; + esac +} + +if [ "$#" -eq 0 ]; then + set -- $("$DETECT" | cut -f1) +fi + +if [ "$#" -eq 0 ]; then + emit 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" 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 From bfd01f71c48701cc1f2b7e024d3b44a0af3edb9b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 28 Aug 2026 16:23:10 +0800 Subject: [PATCH 2/4] chore(manifest): bump jsc-cli to 0.2.3 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2cdeae5..16ac502 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.2", + "version": "0.2.3", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b975952..a615bfd 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.2", + "version": "0.2.3", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills", "jsc": { diff --git a/plugin.json b/plugin.json index 1c9b741..57d3a71 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.2", + "version": "0.2.3", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills/", "jsc": { From f7be015934e8337a736cfb46de40e67b3e16b80e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 28 Aug 2026 16:31:06 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(doctor):=20=E7=B4=8D=E5=85=A5=20skill?= =?UTF-8?q?=20=E8=88=87=20hook=20=E5=AF=A6=E6=B8=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- skills/doctor/SKILL.md | 14 +++-- templates/check-page.md | 8 +-- tools/test-clis.sh | 118 ++++++++++++++++++++++++++++++++-------- 4 files changed, 108 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 456507b..82292d8 100644 --- a/README.md +++ b/README.md @@ -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/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/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 走不走本地複製退路也依實際偵測結果標注 | @@ -54,7 +54,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 ### `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` diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index 2ea568a..ee61a19 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -29,18 +29,20 @@ Done when every detected CLI has a status and its missing items are listed. ## 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: -- `test{cli}{test}{command}{exit-code}{verdict}{detail}` +- `test{area}{cli}{test}{command}{exit-code}{verdict}{detail}` - `summary{ok}{warn}{fail}{skipped}` +Areas: `cli`, `skill`, `hook`. + 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. -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. @@ -70,7 +72,7 @@ Done when the scanned directory is stated and every project row has a verdict. ## 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`: @@ -85,6 +87,6 @@ Done when either the wiki page URL is reported, or the skipped write is reported ## 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. diff --git a/templates/check-page.md b/templates/check-page.md index ef3ed79..9eb9e54 100644 --- a/templates/check-page.md +++ b/templates/check-page.md @@ -31,9 +31,9 @@ ## CLI 實測 -| CLI | 測試 | 指令 | 結束碼 | 判定 | 說明 | -| --- | --- | --- | --- | --- | --- | -| {cli} | {version、help、plugin-list} | {實際命令} | {結束碼} | {通過、降級、失敗、略過} | {輸出摘要或錯誤原因} | +| 類型 | CLI | 測試 | 指令 | 結束碼 | 判定 | 說明 | +| --- | --- | --- | --- | --- | --- | --- | +| {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} | {缺漏、設錯、未接線、實測失敗、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} | ## 未登錄變數 diff --git a/tools/test-clis.sh b/tools/test-clis.sh index 565a3e8..6c05f3c 100755 --- a/tools/test-clis.sh +++ b/tools/test-clis.sh @@ -3,7 +3,7 @@ # 用法: # test-clis.sh [cli...] # 輸出(TSV): -# test{cli}{test}{指令}{結束碼}{判定}{說明} +# test{area}{cli}{test}{指令}{結束碼}{判定}{說明} # summary{ok}{warn}{fail}{skipped} # 判定: # ok 實際命令成功 @@ -37,9 +37,9 @@ cli_bin() { esac } -emit() { # 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" - case "$5" in +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)) ;; @@ -63,7 +63,9 @@ run_capture() { # $@=command return 0 } -run_required() { # cli test command... +run_required() { # area cli test command... + area=$1 + shift cli=$1 test_name=$2 shift 2 @@ -72,15 +74,17 @@ run_required() { # cli test command... code=$RUN_CODE detail=$RUN_TEXT 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 - emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" + emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" else - emit "$cli" "$test_name" "$cmd_text" "$code" fail "$detail" + emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "$detail" fi } -run_optional() { # cli test command... +run_optional() { # area cli test command... + area=$1 + shift cli=$1 test_name=$2 shift 2 @@ -89,11 +93,11 @@ run_optional() { # cli test command... code=$RUN_CODE detail=$RUN_TEXT 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 - emit "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" + emit "$area" "$cli" "$test_name" "$cmd_text" "$code" fail "命令逾時(${TIMEOUT_SECONDS} 秒)" else - emit "$cli" "$test_name" "$cmd_text" "$code" warn "$detail" + emit "$area" "$cli" "$test_name" "$cmd_text" "$code" warn "$detail" fi } @@ -102,32 +106,98 @@ has_cli() { "$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" executable "$bin" "-" skipped "未偵測到執行檔" + emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔" return 0 fi case "$cli" in antigravity) - run_required "$cli" version "$path" --version - run_optional "$cli" help "$path" --help - run_required "$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 ;; kiro) - run_required "$cli" version "$path" --version - run_optional "$cli" help "$path" --help-all - run_optional "$cli" plugin-list "$path" plugin list + 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" version "$path" --version - run_optional "$cli" help "$path" --help - run_required "$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 @@ -135,7 +205,7 @@ if [ "$#" -eq 0 ]; then fi 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" exit 0 fi @@ -147,7 +217,7 @@ for cli in "$@"; do test_one "$cli" else bin=$(cli_bin "$cli") || usage - emit "$cli" executable "$bin" "-" skipped "未偵測到執行檔" + emit cli "$cli" executable "$bin" "-" skipped "未偵測到執行檔" fi ;; *) usage ;; From 12d8d7107c9484021153f1368b71962334a5d16b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 28 Aug 2026 16:31:06 +0800 Subject: [PATCH 4/4] chore(manifest): bump jsc-cli to 0.2.4 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 16ac502..7963e05 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.3", + "version": "0.2.4", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index a615bfd..84ac778 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.3", + "version": "0.2.4", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills", "jsc": { diff --git a/plugin.json b/plugin.json index 57d3a71..0305931 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.2.3", + "version": "0.2.4", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills/", "jsc": {