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/doctor/SKILL.md b/skills/doctor/SKILL.md new file mode 100644 index 0000000..2d89224 --- /dev/null +++ b/skills/doctor/SKILL.md @@ -0,0 +1,71 @@ +--- +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. +--- + +# 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. + +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. + +## 1. Skill versions + +Run `jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}{本機}{遠端}{落後|最新|超前|查詢失敗}` 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. + +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. + +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. + +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 + +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). + +Add `-o` when Gitea is unreachable; the Gitea-dependent rows then come back `skipped`. Report those rows as 未取得結論 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 + +Run `tools/scan-config.sh scan project` from the current working directory. Same output format, `scope=project` rows only. + +Say which directory was scanned in the report. A project-scope result is meaningless without it, because the answer changes with every `cd`. + +When `.env` or `.envrc` exists, name the spec-table variables it overrides and state the value actually in effect. A global setting silently overridden here is the failure this check exists to catch. + +Done when the scanned directory is stated and every project row has a verdict. + +## 5. 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 無. + +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. +- 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. + +Done when either the wiki page URL is reported, or the skipped write is reported together with the reason. + +## 6. 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. + +Done when the counts are stated and the recommendation is given or explicitly withheld. 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/templates/check-contents.md b/templates/check-contents.md new file mode 100644 index 0000000..fb4cb47 --- /dev/null +++ b/templates/check-contents.md @@ -0,0 +1,7 @@ +# 體檢目錄 + +> 由 `jsc-cli:doctor` 維護。每台執行環境一列;`HASH` 取 `{主機名}/{登入帳號}`,算法與其他頁面共用。 + +| 體檢頁 | 主機 | 帳號 | 必要項缺漏 | 設定錯誤 | 最後體檢 | +| --- | --- | --- | --- | --- | --- | +| [[CHECK_{HASH}]] | {hostname} | {使用者帳號} | {n} | {n} | {yyyy-MM-dd HH:mm} | diff --git a/templates/check-page.md b/templates/check-page.md new file mode 100644 index 0000000..a6de648 --- /dev/null +++ b/templates/check-page.md @@ -0,0 +1,59 @@ +# 執行環境體檢 — {hostname}/{使用者帳號} + +> 由 `jsc-cli:doctor` 維護。這是體檢頁 `CHECK_{HASH}`,只保留最新一次結果,重跑就整頁覆寫。 +> 修復請執行 `/jsc-cli:setup`,它讀這頁的「待修項目」逐項處理。 + +- 體檢時間:{yyyy-MM-dd HH:mm} +- 工作目錄:{絕對路徑} +- 執行 CLI:{claude、codex、copilot、antigravity、kiro 五選一} + +## 結論 + +| 分類 | 通過 | 待修 | 略過 | +| --- | --- | --- | --- | +| 技能版本 | {n} | {n} | {n} | +| Hook 接線 | {n} | {n} | {n} | +| 全域設定 | {n} | {n} | {n} | +| 自我設定 | {n} | {n} | {n} | + +## 技能版本 + +| Domain | 本機 | 遠端 | 狀態 | +| --- | --- | --- | --- | +| {domain} | {版本} | {版本} | {落後、最新、超前、查詢失敗、無法驗證} | + +## Hook 接線 + +| CLI | 狀態 | 缺漏項目 | 說明 | +| --- | --- | --- | --- | +| {cli} | {wired、degraded、unwired、skipped} | {項目名,逗號分隔;無則寫「無」} | {降級原因或未偵測到執行檔} | + +## 全域設定 + +| 項目 | 必要 | 現況 | 期望 | 修法 | 判定 | +| --- | --- | --- | --- | --- | --- | +| {變數或檔案} | {是、否} | {實際值或未設定} | {該是什麼} | {自動、詢問、手動} | {通過、走預設、未設定、缺漏、設錯、略過} | + +## 自我設定 + +> 工作目錄:{絕對路徑} + +| 項目 | 必要 | 現況 | 期望 | 修法 | 判定 | +| --- | --- | --- | --- | --- | --- | +| {變數或檔案} | {是、否} | {實際值或未設定} | {該是什麼} | {自動、詢問、手動} | {通過、走預設、未設定、缺漏、設錯、略過} | + +## 待修項目 + +> `/jsc-cli:setup` 從這張表接手。沒有待修項目時整張表寫一列「無」。 + +| 順序 | 項目 | 範圍 | 判定 | 修法 | 影響 | +| --- | --- | --- | --- | --- | --- | +| {n} | {變數、檔案、hook 或 domain} | {全域、自我} | {缺漏、設錯、未接線、落後} | {自動、詢問、手動} | {不修的話哪些技能跑不動} | + +## 未登錄變數 + +> 原始碼有用到、`config-spec.tsv` 沒登錄的變數。體檢不判定它們,只提醒維護者補登錄。 + +| 變數 | 出現次數 | +| --- | --- | +| {變數名} | {n} | 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 diff --git a/tools/config-spec.tsv b/tools/config-spec.tsv new file mode 100644 index 0000000..bdcba2c --- /dev/null +++ b/tools/config-spec.tsv @@ -0,0 +1,66 @@ +# config-spec.tsv — jsc 技能組的設定規格表。體檢(/jsc-cli:doctor)與設定(/jsc-cli:setup)共用這一份。 +# +# 這張表是「必要或選擇」的唯一判準。掃描原始碼分不出必要與選擇,也分不出哪些是執行期內部 +# 變數,所以判準用手寫規格表,掃描只負責抓出漏登錄的項目(scan-config.sh orphans)。 +# 新增環境變數或設定檔時,同時補一列進來,體檢才看得到它。 +# +# 欄位:keykindscoperequireddefaultverifyfixdesc +# kind env=環境變數 file=檔案或目錄 internal=執行期內部變數(體檢略過,只為登錄而存在) +# scope global=整台機器 project=當前工作目錄 runtime=hook 執行當下才存在 +# required yes=缺了就有技能跑不動 no=選擇性,缺了走預設或降級 +# default 未設定時的實際值;沒有預設寫 - +# verify set=有值即可 dir=目錄要在 file=檔案要在 gitea-api=站台連得上 +# gitea-auth=認證過得了 wiki-repo=值可解析成 {owner}/{repo} none=不驗 +# fix auto=工具算得出,可直接寫入 ask=值要人給,問完才寫 manual=只能人手動處理 -=不需修 +# desc 一句繁中說明,直接印給使用者看 +# +GITEA_HOST env global yes - gitea-api ask Gitea 站台位址,所有 wiki 與 PR 操作的去處 +GITEA_TOKEN env global yes - gitea-auth ask Gitea API token;未設定時退回 tea CLI 的登入金鑰 +JSC_HOME env global no ~/.jsc dir auto hook 資料目錄,放工作階段計時、用量統計、版本快取 +JSC_WIKI_REPO env global no - wiki-repo ask 未逐類設定時的共用 wiki {owner}/{repo} +JSC_WIKI_REPO_QUESTION env global no JSC_WIKI_REPO wiki-repo ask QUESTION_CONTENTS、QUESTION_{HASH} 所在存取庫 +JSC_WIKI_REPO_PLAN env global no JSC_WIKI_REPO wiki-repo ask PLAN_CONTENTS、PLAN_{HASH} 所在存取庫 +JSC_WIKI_REPO_ANALYZE env global no JSC_WIKI_REPO wiki-repo ask ANALYZE_CONTENTS、ANALYZE_{HASH} 所在存取庫 +JSC_WIKI_REPO_DELIVER env global no JSC_WIKI_REPO wiki-repo ask DELIVER_CONTENTS、DELIVER_{HASH} 所在存取庫 +JSC_WIKI_REPO_MAINTAIN env global no JSC_WIKI_REPO wiki-repo ask MAINTAIN_CONTENTS、MAINTAIN_{HASH} 所在存取庫 +JSC_WIKI_REPO_REPO env global no JSC_WIKI_REPO wiki-repo ask REPO_CONTENTS、REPO_{HASH} 所在存取庫 +JSC_WIKI_REPO_LOG env global no JSC_WIKI_REPO wiki-repo ask LOG_CONTENTS、LOG_{HASH} 所在存取庫 +JSC_WIKI_REPO_LEARN env global no JSC_WIKI_REPO wiki-repo ask LEARN_CONTENTS、LEARN_{HASH} 所在存取庫 +JSC_WIKI_REPO_ERROR env global no JSC_WIKI_REPO wiki-repo ask ERROR_CONTENTS、ERROR_{HASH} 所在存取庫 +JSC_WIKI_REPO_CHECK env global no JSC_WIKI_REPO wiki-repo ask CHECK_CONTENTS、CHECK_{HASH} 所在存取庫,體檢紀錄寫在這裡 +JSC_WIKI_REPO_REPORT env global no JSC_WIKI_REPO wiki-repo ask REPORT_CONTENTS、REPORT_{HASH} 所在存取庫,年月週日報表寫在這裡 +JSC_VERSION_GUARD env global no on set ask 設成 off 可完全略過版本前置檢查,離線工作時用 +JSC_VERSION_TTL env global no 600 set ask 版本查詢快取秒數 +JSC_LOCAL_PLUGINS env global no $JSC_HOME/plugins dir auto antigravity 安裝來源的本地 plugin 目錄 +JSC_PLUGINS_ROOT env global no - dir ask 技能組工作目錄根位置,jsc-meta 的工具用它找各 domain +JSC_CLAUDE_SETTINGS_DIR env global no ~/.claude dir ask claude 使用者層設定檔目錄,測試 purge 時才需覆寫 +JSC_COPILOT_INSTRUCTIONS env global no ~/.config/copilot/copilot-instructions.md file ask copilot 指引檔位置,STE100 規則段落寫在這裡 +JSC_ANTIGRAVITY_RULES env global no ~/.antigravity/AGENTS.md file ask antigravity 全域規則檔位置 +JSC_KIRO_SKILLS env global no - dir ask kiro 技能目錄,接線與部署都會用到 +JSC_GITEA_TOOLS env global no - dir ask jsc-gitea/tools 的位置,跨 domain 呼叫 gitea.sh 時用 +$JSC_HOME/model-tags.tsv file global yes - file auto SDLC 階段閘門讀的能力標籤表,由 /jsc-cli:models 產生 +$JSC_HOME/models.conf file global no - file ask 各 SDLC 階段的偏好模型鏈,專案 .jsc/models 可覆寫 +$JSC_HOME/html-styles.conf file global no - file ask 各類頁面的 HTML 匯出版型與樣式,專案 .jsc/html-styles 可覆寫 +.jsc/models file project no - file ask 本專案的 SDLC 偏好模型鏈,覆寫 $JSC_HOME/models.conf +.jsc/html-styles file project no - file ask 本專案的 HTML 匯出版型與樣式,覆寫 $JSC_HOME/html-styles.conf +.claude/settings.json file project no - file manual claude 的專案層設定;jsc hook 由 plugin 的 hooks.json 接線,這裡不該有 jsc 殘留接線 +AGENTS.md file project no - file manual codex 與 antigravity 讀的專案層指引檔 +.env file project no - none manual 專案層環境變數;覆寫全域設定時,體檢會標出實際生效值 +.envrc file project no - none manual direnv 設定檔;覆寫全域設定時,體檢會標出實際生效值 +JSC_CLI internal runtime no - none - 接線時帶入的 CLI 代號,hook 用來分辨宿主 +JSC_SKILL internal runtime no - none - 目前呼叫的技能名,版本前置檢查與用量統計用 +JSC_SESSION_ID internal runtime no - none - 工作階段代號,計時與 token 統計用 +JSC_TOOL_NAME internal runtime no - none - 目前的工具名,PreToolUse hook 用來比對 matcher +JSC_SCRIPT_DIR internal runtime no - none - 呼叫端腳本所在目錄,由 lib.sh 算出 +JSC_HOOKS_DIR internal runtime no - none - hooks 目錄位置,包裝啟動器用 +JSC_MODEL internal runtime no - none - 目前模型 id,SDLC 閘門用來比對能力標籤 +JSC_GITEA_OWNER internal runtime no - none - 批次同步存取庫時鎖定的 owner +JSC_DEPLOY_DRYRUN internal runtime no - none - 部署試跑旗標,只印指令不執行 +JSC_WP_GATE internal runtime no - none - 工作包閘門的逃生門,實作在 jsc-sdlc/tools/wp-gate.sh +JSC_CONFIG_SPEC internal runtime no - none - 改讀別份設定規格表,測試 scan-config.sh 時用 +JSC_SYNC_DRY_RUN internal runtime no - none - 同步 domain 存取庫的試跑旗標,只印不動檔案 +JSC_MK_DOMAIN internal runtime no - none - sync-marketplace.sh 傳給 python 的 domain 名 +JSC_MK_URL internal runtime no - none - sync-marketplace.sh 傳給 python 的存取庫網址 +JSC_MK_DESC internal runtime no - none - sync-marketplace.sh 傳給 python 的 plugin 描述 +JSC_MK_IN internal runtime no - none - sync-marketplace.sh 讀入的 marketplace 檔路徑 +JSC_MK_OUT internal runtime no - none - sync-marketplace.sh 寫出的 marketplace 檔路徑 diff --git a/tools/scan-config.sh b/tools/scan-config.sh new file mode 100755 index 0000000..dad88eb --- /dev/null +++ b/tools/scan-config.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env sh +# scan-config.sh — 依 config-spec.tsv 盤點 jsc 技能組的設定現況。唯讀,不寫任何設定。 +# 用法: +# scan-config.sh spec [global|project|all] # 印規格表(去掉註解與 internal 列) +# scan-config.sh scan [global|project|all] # 逐項檢查現況,印 TSV +# scan-config.sh orphans # 掃原始碼,找出沒登錄進規格表的變數 +# 選項: +# -o 離線模式:需要連 Gitea 的檢查一律標 skipped,不發送請求 +# +# scan 的輸出(TSV,每行一項): +# itemscoperequiredactualexpectfixverdict +# verdict = ok 設定妥當 +# default 未設定,走預設值,可以正常運作 +# unset 選擇性項目未設定,沒有預設值,相關功能會降級 +# missing 必要項目缺了,相關技能跑不動 +# invalid 有值但驗不過(目錄不在、認證失敗、格式不對) +# skipped 離線模式略過,未取得結論 +# 最後固定一行 summary{missing 數}{invalid 數}{unset 數}{skipped 數} +# +# 帶 TOKEN 的項目一律只印 set 或 unset,不印值:體檢報告會寫進 wiki,憑證不能跟著上去。 +# +# 結束碼: 0=掃描完成(有沒有問題都算完成,判斷交給呼叫端) 2=用法錯誤 3=找不到規格表 +set -eu + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SPEC="${JSC_CONFIG_SPEC:-$HERE/config-spec.tsv}" +TAB=$(printf '\t') +OFFLINE=0 + +[ -f "$SPEC" ] || { echo "找不到規格表:$SPEC(可用 JSC_CONFIG_SPEC 指定)" >&2; exit 3; } + +usage() { + echo "用法:scan-config.sh [-o] {spec|scan|orphans} [global|project|all]" >&2 + exit 2 +} + +case "${1:-}" in + -o) OFFLINE=1; shift ;; +esac + +cmd="${1:-}" +scope_want="${2:-all}" +case "$cmd" in spec|scan|orphans) ;; *) usage ;; esac +case "$scope_want" in global|project|all) ;; *) usage ;; esac + +# 找出 jsc-gitea 的 tools/gitea.sh。所有 Gitea 操作一律經由它(技能準則),不自行拼 API 呼叫。 +# 找不到就回傳 1,呼叫端把需要連線的檢查標成 skipped,不讓整份體檢失敗。 +gitea_sh() { + if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then + printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0 + fi + _root="${CLAUDE_PLUGIN_ROOT:-$HERE/..}" + for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do + [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } + done + _c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \ + "$_root"/../../gitea/*/tools/gitea.sh \ + "$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \ + | sort | tail -n1) + [ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; } + _c=$(command -v gitea.sh 2>/dev/null || true) + [ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; } + return 1 +} + +# 規格表的資料列(去註解、去空行) +spec_rows() { grep -v '^#' "$SPEC" | grep -v '^[[:space:]]*$'; } + +# 這一列要不要納入本次掃描。$1=kind $2=scope +# internal 列只為登錄而存在(讓 orphans 認得出它們不是漏網之魚),永遠不進體檢報告。 +row_wanted() { + [ "$1" != internal ] || return 1 + [ "$scope_want" = all ] || [ "$2" = "$scope_want" ] +} + +if [ "$cmd" = spec ]; then + spec_rows | while IFS="$TAB" read -r key kind scope required def verify fix desc; do + row_wanted "$kind" "$scope" || continue + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$key" "$scope" "$required" "$def" "$verify" "$fix" "$desc" + done + exit 0 +fi + +if [ "$cmd" = orphans ]; then + # 規格表沒有的變數 = 有人加了設定卻忘了登錄。體檢照樣印出來,維護者才補得上。 + root="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}" + [ -d "$root" ] || { echo "找不到 plugins 根目錄:$root(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 3; } + known=$(spec_rows | cut -f1 | sed 's/^\$//' | sort -u) + # 前面加詞邊界,否則帶別種前綴的變數(例如 PERSONA_ 開頭那批)會被從中間切出一段誤報。 + # 這行註解本身也不寫出完整變數字面:掃描連自己的原始碼一起掃,寫了就會掃到自己。 + grep -rhoE '\b(JSC|GITEA)_[A-Z0-9_]+' \ + --include='*.sh' --include='*.md' --include='*.json' \ + --exclude-dir=.git --exclude-dir=.jsc-monorepo-archive "$root" 2>/dev/null \ + | sort | uniq -c | sort -rn \ + | while read -r count name; do + # 原始碼寫的是樣板字面(JSC_WIKI_REPO_{TYPE}),抓出來會多一條尾巴底線; + # 去掉再比對,否則每次體檢都會多報一個不存在的變數。 + name=${name%_} + printf '%s\n' "$known" | grep -qx "$name" && continue + case "$name" in JSC_WIKI_REPO_TYPE) continue ;; esac + printf 'orphan\t%s\t%s\n' "$name" "$count" + done + exit 0 +fi + +# --- scan --- + +n_missing=0; n_invalid=0; n_unset=0; n_skipped=0 +GITEA=$(gitea_sh 2>/dev/null || true) + +# 連線類檢查的共用前置:離線、或找不到 gitea.sh,都直接標 skipped。 +online_ready() { + [ "$OFFLINE" = 0 ] || return 1 + [ -n "$GITEA" ] || return 1 +} + +# 值展開:規格表的檔案類 key 會帶 $JSC_HOME 這種變數,照字面找檔案永遠找不到。 +# 開頭的 ~ 要先換成 $HOME:雙引號裡的 ~ 不展開,留著會讓 ~/.jsc 這種預設值一律驗不過。 +# set +u 是必要的:預設值裡的 $JSC_HOME 常常正是「還沒設定」的那一個, +# 展開它在 set -u 底下會直接中斷整份掃描,體檢就停在半路。 +expand() { + _e="$1" + case "$_e" in "~/"*) _e="$HOME/${_e#\~/}" ;; esac + ( set +u; eval "printf '%s' \"$_e\"" ) 2>/dev/null +} + +emit() { # item scope required actual expect fix verdict + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" + case "$7" in + missing) n_missing=$((n_missing + 1)) ;; + invalid) n_invalid=$((n_invalid + 1)) ;; + unset) n_unset=$((n_unset + 1)) ;; + skipped) n_skipped=$((n_skipped + 1)) ;; + esac +} + +# 對一個已取得的值跑 verify。印出 verdict(ok/invalid/skipped)。$1=verify $2=值 +run_verify() { + case "$1" in + set|none) echo ok ;; + dir) [ -d "$2" ] && echo ok || echo invalid ;; + file) [ -f "$2" ] && echo ok || echo invalid ;; + gitea-api) + online_ready || { echo skipped; return; } + GITEA_HOST="$2" sh "$GITEA" api GET /version >/dev/null 2>&1 && echo ok || echo invalid ;; + gitea-auth) + online_ready || { echo skipped; return; } + sh "$GITEA" owners >/dev/null 2>&1 && echo ok || echo invalid ;; + wiki-repo) + case "$2" in + */*) + online_ready || { echo ok; return; } + sh "$GITEA" default-branch "$2" >/dev/null 2>&1 && echo ok || echo invalid ;; + *) echo invalid ;; + esac ;; + *) echo ok ;; + esac +} + +# 迴圈不可以放在管線右邊:那會變成子 shell,計數加不回來,summary 永遠是 0。 +rows=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; } +spec_rows > "$rows" + +while IFS="$TAB" read -r key kind scope required def verify fix desc; do + [ -n "${key:-}" ] || continue + row_wanted "$kind" "$scope" || continue + + if [ "$kind" = env ]; then + eval "val=\${$key:-}" + secret=0 + case "$key" in *TOKEN*) secret=1 ;; esac + if [ -n "$val" ]; then + verdict=$(run_verify "$verify" "$val") + if [ "$secret" = 1 ]; then actual=set; else actual="$val"; fi + emit "$key" "$scope" "$required" "$actual" "$desc" "$fix" "$verdict" + continue + fi + # 未設定:有預設值就用預設值再驗一次,驗得過才算走得下去。 + if [ "$def" != "-" ]; then + # 預設值寫成另一個變數名(JSC_WIKI_REPO_LOG 退回 JSC_WIKI_REPO)時,要跟去看那一個。 + # 退路自己也空著卻回報「走預設值」,會讓體檢說得過去、實際上功能整個不能用。 + case "$def" in + [A-Z]*) + if printf '%s' "$def" | grep -qx '[A-Z][A-Z0-9_]*'; then + eval "fallback=\${$def:-}" + if [ -z "$fallback" ]; then + emit "$key" "$scope" "$required" "未設定(退路 $def 也未設定)" "$desc" "$fix" unset + continue + fi + emit "$key" "$scope" "$required" "未設定(退回 $def=$fallback)" "$desc" "$fix" default + continue + fi ;; + esac + dval=$(expand "$def") + case "$verify" in + # 預設路徑還沒建立,算「尚未啟用」而不是「設錯了」:invalid 專指有值卻驗不過。 + dir|file) if [ "$(run_verify "$verify" "$dval")" = ok ]; then verdict=default; else verdict=unset; fi ;; + *) verdict=default ;; + esac + emit "$key" "$scope" "$required" "未設定(預設 $def)" "$desc" "$fix" "$verdict" + continue + fi + [ "$required" = yes ] && verdict=missing || verdict=unset + emit "$key" "$scope" "$required" 未設定 "$desc" "$fix" "$verdict" + continue + fi + + # kind=file:規格表的 key 本身就是路徑 + path=$(expand "$key" 2>/dev/null || printf '%s' "$key") + if [ -e "$path" ]; then + emit "$key" "$scope" "$required" "$path" "$desc" "$fix" ok + elif [ "$required" = yes ]; then + emit "$key" "$scope" "$required" 不存在 "$desc" "$fix" missing + else + emit "$key" "$scope" "$required" 不存在 "$desc" "$fix" unset + fi +done < "$rows" +rm -f "$rows" + +printf 'summary\t%s\t%s\t%s\t%s\n' "$n_missing" "$n_invalid" "$n_unset" "$n_skipped" +exit 0