diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0d27e69..55bb34c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.1.0", + "version": "0.1.2", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f58b51a..879441f 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.1.0", + "version": "0.1.2", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills" } diff --git a/README.md b/README.md index e5b4927..10ec102 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `tools/deploy.sh` | 對單一 CLI 執行安裝、更新或解除安裝(`deploy.sh [-n] {mode} {cli} {domain}...`,mode 為 install / update / uninstall);印出每個指令與其結束碼,最後一行 `result` 標 ok 或 fail。`-n` 只印指令不執行。上表五個 CLI 的指令差異全部收在這支腳本裡。站台取自 `GITEA_HOST`,本地 clone 目錄取自 `JSC_LOCAL_PLUGINS`,兩者的預設值見下表 | | `tools/list-models.sh` | 讀各 CLI 設定檔列出模型(TSV:cli / model / in-use);設定檔缺失就不輸出該 CLI 的列,一律 exit 0。設定檔位置只寫在這支腳本裡 | | `tools/model-config.sh` | 解析 SDLC 各階段的偏好模型鏈(`get {stage}`、`list`、`resolve {stage}` 印出目前 CLI 可用的第一個模型);專案 `.jsc/models` 優先於 `$JSC_HOME/models.conf`,格式見 `references/model-tags.md`。鏈只影響建議與偏好順序,不影響閘門放行 | +| `tools/config-spec.tsv` | 設定規格表:每個環境變數與設定檔一列,標明必要或選擇、預設值、驗證方式、修法。體檢與設定共用這一份,新增設定時要同步補一列 | +| `tools/scan-config.sh` | 依規格表盤點設定現況(`scan {global\|project\|all}` 印 TSV 與 summary、`spec` 印規格表、`orphans` 找出漏登錄的變數);`-o` 為離線模式,需要連 Gitea 的檢查一律標 skipped。唯讀,不寫任何設定;帶 TOKEN 的項目只印 set 或 unset | +| `tools/apply-config.sh` | 把設定寫進 shell rc 檔(`set {KEY} {VALUE}`、`unset {KEY}`)或建立目錄(`mkdir {PATH}`);`show` 印出目前設定,`rcfiles` 印出會寫入的檔案。內容一律收在 `# jsc-config` 標記段落之間,整段重寫不疊加,段落外不動。動檔案前先備份到 `$JSC_HOME/backup/config/{yyyyMMdd_HHmmss}/`,備份失敗就不寫;寫完重讀驗證。fish 自動改用 `set -gx` 語法 | | `tools/model-tags.sh` | 解析 `references/model-tags.md` 的能力標籤與 SDLC 階段必要標籤(`dump`、`sync`、`stage {階段}`、`model {模型 id}`、`gate {階段} {模型 id}`);`sync` 寫出 `$JSC_HOME/model-tags.tsv` 供 `jsc-hooks` 的 sdlc-gate 讀取 | ## Skills 目錄 @@ -46,6 +49,14 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 技能庫批次安裝、更新、解除安裝:偵測 CLI → **先比對各 plugin 的本機與已發佈版本並列表,只要有任一個落後就把「更新」設為推薦選項** → 決策樹選模式 → 每個 CLI 一個 sub agent 呼叫 `tools/deploy.sh` 執行原生 plugin 指令(統一 marketplace `jsc`,token `jsc-{domain}@jsc`)。domain 名單動態取自 `plugins/meta` 的 marketplace.json,不硬編碼。 +### `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`,體檢本身不動任何設定。 + +### `setup` + +修復 `/jsc-cli:doctor` 找出的問題,一次一項,逐項確認才動手。待修清單優先讀 wiki `CHECK_{HASH}`,沒有頁面就當場重掃一份。依修法分流:`auto` 用 `tools/apply-config.sh` 直接寫、`ask` 先用決策樹問到值再寫、`manual` 印出步驟交給操作者。複合修復交回原主:版本落後找 `/jsc-cli:deploy`、hook 未接線找 `/jsc-hooks:hooks-install`、缺 `model-tags.tsv` 找 `/jsc-cli:models`。每一項寫完都重驗一次,最後覆寫 CHECK 頁。 + ## 環境變數 @@ -57,6 +68,8 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `JSC_LOCAL_PLUGINS` | antigravity 與 kiro 退路用的本地 clone 目錄 | 用 `$JSC_HOME/plugins`(即 `~/.jsc/plugins`) | | `JSC_KIRO_SKILLS` | kiro 退路複製 skills 的目標目錄 | 用 `~/.kiro/skills` | | `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_LOCAL_PLUGINS` 的預設值刻意避開 `~/plugins`:那是維護者放技能組開發 checkout 的地方,`git pull` 下去會蓋掉未提交的工作。這個變數指到的目錄若是開發中的樹(有未提交變更,或有未推送的 commit),`deploy.sh` 只印一行 `skip` 並直接用現地內容安裝,不執行 `git pull`。 diff --git a/plugin.json b/plugin.json index 6029883..23af6d1 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-cli", - "version": "0.1.0", + "version": "0.1.2", "description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署", "skills": "./skills/" } diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md new file mode 100644 index 0000000..e3a7710 --- /dev/null +++ b/skills/setup/SKILL.md @@ -0,0 +1,66 @@ +--- +name: setup +description: Fix what jsc-cli:doctor found, one confirmed item at a time. Read the 待修項目 table from wiki CHECK_{HASH}, or rebuild it with tools/scan-config.sh and jsc-hooks/tools/wire-cli.sh status when no page exists. Route each item by its fix column - auto writes it through tools/apply-config.sh, ask collects the value through the jsc-ask decision tree first, manual prints the steps for the operator. Delegate compound repairs to their owners: jsc-cli:deploy for a plugin whose version is behind, jsc-hooks:hooks-install for unwired hooks, jsc-cli:models for a missing model-tags.tsv. Re-verify every item after writing and rewrite the CHECK page; use when doctor reports something to fix, not for a read-only checkup. +--- + +# setup — guide or apply the fixes doctor found + +This skill writes. Every write is confirmed first, backed up, and verified afterwards. + +## 1. Get the work list + +Read the 待修項目 table from wiki `CHECK_{HASH}` — repo from `jsc-gitea/tools/gitea.sh wiki-repo CHECK`, page name from `gitea.sh hash-id "{hostname}/{user}"`. + +No page, or `wiki-repo` exits 3 → rebuild the list here: `tools/scan-config.sh scan all` for settings, `jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI for wiring, `jsc-hooks/hooks/version-guard.sh report` for versions. Rebuilding **MUST run as a sub agent**. + +State which source the list came from. A stale page and a live scan can disagree, and the operator has to know which one is on screen. + +Done when every item carries its scope, verdict and fix route. + +## 2. Confirm each item + +Ask per the `jsc-ask:ask` decision tree, one item at a time, in the table's order. Every option states its impact scope: which file gets written, which skills start working, what stays broken when skipped. + +An `ask` item needs its value in the same question — the wiki repo as `{owner}/{repo}`, the Gitea host, the directory path. Never invent one. + +Skipping is always an option and is recorded as skipped, not as fixed. + +Done when every item is either confirmed with a value or recorded as skipped. + +## 3. Apply + +| Route | Action | +| --- | --- | +| `auto` on a variable | `tools/apply-config.sh set {KEY} {VALUE}` | +| `auto` on a directory | `tools/apply-config.sh mkdir {PATH}` | +| `ask` | same two commands, with the value the user just gave | +| `manual` | print the exact steps and the file to edit; the operator does it | +| domain 落後 | call `jsc-cli:deploy`, mode `update` | +| hook unwired | call `jsc-hooks:hooks-install` | +| `$JSC_HOME/model-tags.tsv` missing | call `jsc-cli:models` | + +`apply-config.sh` writes into the `# jsc-config` block of every existing shell rc file, backs each one up to `$JSC_HOME/backup/config/{timestamp}/` before touching it, and rewrites the block whole. It never edits anything outside that block. + +Report the `backup` path it prints. That path is the whole undo story for this run. + +Done when every confirmed item has a `wrote`, `created` or delegated result. + +## 4. Re-verify + +Rerun the check that produced each item — `tools/scan-config.sh scan {scope}` for settings, `wire-cli.sh status {cli}` for wiring, `version-guard.sh report` for versions. + +An item that still fails is reported as 未修好 with the reason. Never mark it fixed because the write succeeded: writing the variable and the variable verifying are two different facts. + +A newly written rc block does not affect the running shell. Tell the operator to open a new shell or `source` the rc file, and give them the `export` line for the current session. A re-verify that reads the current environment will still show the variable unset — say so rather than reporting a false failure. + +Done when every applied item has a fresh verdict from its own checker. + +## 5. Record + +Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `templates/check-page.md`, and refresh the `CHECK_CONTENTS` row. The page keeps only the latest run, so this overwrites the pre-fix picture on purpose. + +No wiki repo configured → report the tables on screen and say the record was skipped. + +Then state the counts: fixed, skipped, delegated, and 未修好. Recommend `/jsc-cli:doctor` for a clean re-check when anything was delegated. + +Done when the page is written or the skip is reported, and the four counts are stated. diff --git a/tools/apply-config.sh b/tools/apply-config.sh new file mode 100755 index 0000000..7945c99 --- /dev/null +++ b/tools/apply-config.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env sh +# apply-config.sh — 把設定寫進 shell rc 檔或建立設定目錄(供 /jsc-cli:setup 呼叫)。 +# 用法: +# apply-config.sh set {KEY} {VALUE} # 寫入或更新一個環境變數(export) +# apply-config.sh unset {KEY} # 從 jsc 段落移除一個環境變數 +# apply-config.sh mkdir {PATH} # 建立目錄(值可帶 $VAR 與開頭的 ~) +# apply-config.sh show # 印出目前 jsc 段落的內容(每行 KEYVALUE) +# apply-config.sh rcfiles # 印出這次會寫入的 rc 檔路徑 +# +# 寫入位置:每個既有的 shell rc 檔(~/.bashrc、~/.zshrc、~/.config/fish/config.fish) +# 都寫一份,全部不存在時才建立 ~/.bashrc。內容一律收在標記段落之間: +# # jsc-config +# export KEY='值' +# # /jsc-config +# 段落整段重寫,重跑只取代不疊加。段落外的內容一律不動——rc 檔是使用者自己的檔案, +# jsc 只負責自己那一段。 +# +# 動到任何檔案之前先原樣備份到 $JSC_HOME/backup/config/{yyyyMMdd_HHmmss}/,備份失敗就不寫入。 +# fish 的語法與 POSIX shell 不同,寫進去的是 set -gx KEY 值。 +# +# 結束碼: 0=成功 2=用法錯誤 4=備份或寫入失敗 +set -eu + +JSC_HOME="${JSC_HOME:-$HOME/.jsc}" +MARK_OPEN='# jsc-config' +MARK_SHUT='# /jsc-config' +TAB=$(printf '\t') + +usage() { + echo "用法:apply-config.sh {set {KEY} {VALUE}|unset {KEY}|mkdir {PATH}|show|rcfiles}" >&2 + exit 2 +} + +cmd="${1:-}" +case "$cmd" in set|unset|mkdir|show|rcfiles) ;; *) usage ;; esac + +rc_files() { + found="" + for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do + [ -f "$f" ] && { printf '%s\n' "$f"; found=1; } + done + [ -n "$found" ] || printf '%s\n' "$HOME/.bashrc" +} + +if [ "$cmd" = rcfiles ]; then rc_files; exit 0; fi + +# 目前段落裡的設定,格式 KEYVALUE。取第一個既有 rc 檔為準:寫入時每個檔案內容相同。 +read_pairs() { + for f in $(rc_files); do + [ -f "$f" ] || continue + awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" ' + $0==o { inb=1; next } + $0==s { inb=0; next } + inb { + line=$0 + sub(/^export /, "", line) # POSIX shell + sub(/^set -gx /, "", line) # fish + if (line ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { + eq=index(line, "="); k=substr(line, 1, eq-1); v=substr(line, eq+1) + } else { + sp=index(line, " "); if (sp==0) next + k=substr(line, 1, sp-1); v=substr(line, sp+1) + } + gsub(/^'"'"'|'"'"'$/, "", v) + if (k != "") print k "\t" v + } + ' "$f" + return 0 + done +} + +if [ "$cmd" = show ]; then read_pairs; exit 0; fi + +if [ "$cmd" = mkdir ]; then + raw="${2:-}"; [ -n "$raw" ] || usage + case "$raw" in "~/"*) raw="$HOME/${raw#\~/}" ;; esac + path=$( set +u; eval "printf '%s' \"$raw\"" ) + [ -n "$path" ] || { echo "路徑展開後是空的:${2:-}" >&2; exit 4; } + mkdir -p "$path" 2>/dev/null || { echo "無法建立目錄:$path" >&2; exit 4; } + printf 'created\t%s\n' "$path" + exit 0 +fi + +key="${2:-}" +[ -n "$key" ] || usage +case "$key" in + [A-Za-z_]*) ;; + *) echo "變數名不合法:$key" >&2; exit 2 ;; +esac +value="${3:-}" +if [ "$cmd" = set ] && [ -z "$value" ]; then echo "set 需要值" >&2; exit 2; fi + +# 合併:既有設定 + 這次的異動,重寫整段。少了這一步,寫第二個變數會蓋掉第一個。 +pairs=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 4; } +read_pairs > "$pairs" 2>/dev/null || true +merged=$(mktemp) || { rm -f "$pairs"; echo "無法建立暫存檔" >&2; exit 4; } +while IFS="$TAB" read -r k v; do + [ -n "${k:-}" ] || continue + [ "$k" = "$key" ] && continue + printf '%s\t%s\n' "$k" "$v" >> "$merged" +done < "$pairs" +rm -f "$pairs" +if [ "$cmd" = set ]; then printf '%s\t%s\n' "$key" "$value" >> "$merged"; fi + +# 備份:動到的每個檔案先原樣複製一份,備份不了就整個不寫。 +stamp=$(date +%Y%m%d_%H%M%S) +backup_dir="$JSC_HOME/backup/config/$stamp" +mkdir -p "$backup_dir" 2>/dev/null || { rm -f "$merged"; echo "無法建立備份目錄:$backup_dir" >&2; exit 4; } + +# 依 rc 檔語法組出段落內容。fish 用 set -gx,其餘用 export。 +block_for() { + _f="$1" + while IFS="$TAB" read -r k v; do + [ -n "${k:-}" ] || continue + case "$_f" in + *config.fish) printf "set -gx %s '%s'\n" "$k" "$v" ;; + *) printf "export %s='%s'\n" "$k" "$v" ;; + esac + done < "$merged" +} + +rc_list=$(mktemp) || { rm -f "$merged"; echo "無法建立暫存檔" >&2; exit 4; } +rc_files > "$rc_list" +rc=0 +while IFS= read -r f; do + [ -n "$f" ] || continue + if [ -f "$f" ]; then + cp -p "$f" "$backup_dir/$(basename "$f")" 2>/dev/null \ + || { echo "無法備份 $f,沒有備份就不寫入" >&2; rc=4; continue; } + else + mkdir -p "$(dirname "$f")" 2>/dev/null || { echo "無法建立 $(dirname "$f")" >&2; rc=4; continue; } + touch "$f" 2>/dev/null || { echo "無法建立 $f" >&2; rc=4; continue; } + fi + content=$(block_for "$f") + block=$(printf '%s\n%s\n%s' "$MARK_OPEN" "$content" "$MARK_SHUT") + if grep -qF "$MARK_OPEN" "$f" 2>/dev/null; then + awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" -v b="$block" ' + $0==o { print b; skip=1; next } + $0==s { skip=0; next } + skip { next } + { print } + ' "$f" > "$f.jsc-tmp" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法改寫 $f" >&2; rc=4; continue; } + mv "$f.jsc-tmp" "$f" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法覆寫 $f" >&2; rc=4; continue; } + else + ( printf '\n%s\n' "$block" >> "$f" ) 2>/dev/null || { echo "無法寫入 $f" >&2; rc=4; continue; } + fi + # 寫完重讀驗證:說寫好了卻沒寫進去,是最難查的失敗 + if [ "$cmd" = set ]; then + grep -qF "$key" "$f" 2>/dev/null || { echo "$f 寫入後讀不到 $key" >&2; rc=4; continue; } + fi + printf 'wrote\t%s\n' "$f" +done < "$rc_list" +rm -f "$rc_list" "$merged" + +printf 'backup\t%s\n' "$backup_dir" +[ "$rc" = 0 ] || exit 4 +printf 'note\t%s\n' "新的設定要開新的 shell 或重新 source rc 檔才生效;本輪工作階段可先手動 export" +exit 0