Files
cli/README.md
jiantw83 5b0eb784b4 fix(doctor,setup): 兩支技能補上路徑守則,呼叫一律字面絕對路徑
這兩支技能的腳本呼叫原本全是裸相對路徑,整份文件沒有任何路徑守則。相對路徑會對著操作者當下的工作目錄解,而那裡從來不是外掛根目錄,所以每一個呼叫點都是模型就地猜前綴的地方。部署技能原本三處相對路徑被補成帶變數路徑,就是同一個機制。

實際掃到的處數比預估多:體檢 18 處、修復 20 處。多出來的是兩類原本沒想到的——裸檔名的 Gitea 呼叫,以及被當成參數傳的資料檔。後者同樣會解錯地方,而且解錯了不會報錯。

兩支各補路徑守則與前置步驟,解出兩條根目錄:跨外掛走 current 那一層(解到那一層就停,再往下解會落到帶版本號的快取路徑,那種路徑進不了允許清單),技能自己的腳本與範本走 CLI 載入技能時講明的外掛基底目錄。

兩支都不靠 current 底下那條 jsc-cli 連結,但理由各自不同,不是照抄部署那一支的。體檢不能靠,因為它正是機器可疑時才跑的技能——觸發時機本身就是連結可能出錯的那幾個時刻,而連結指著舊版時拿到的是舊的掃描腳本與舊的規格表,掃出來的每一列都像真的發現,不會有任何錯誤訊息。修復更不能靠,因為它寫檔,而且它要修的其中一列正是那個決定連結位置的變數:那種機器上根目錄根本解不出來,而修它要用的腳本掛在外掛基底目錄底下、不依賴那個變數,所以解不出來既不停這一輪也不擋那一項修復。何況拿舊的寫入腳本改設定檔,再用同一份舊腳本重驗,兩邊當然對得上,整輪會報成已修。

失敗分支也與部署不同。部署解不出根目錄就停手;這兩支都不停——體檢把它記成一項發現然後跑完不需要跨外掛的檢查,因為唯讀體檢中途停掉操作者什麼都拿不到;修復把它當成待修的那一項,修好再重解一次。

兩份範本與說明文件一併改。範本是這兩支技能在同一輪一起讀的,而且指示寫入,留著裸路徑等於留一個繞過新規則的入口。範本裡另外補掉兩個路徑洞:指向範本自己的佔位符原本沒有路徑,以及一個連目錄都沒有的裸檔名。

行為契約四列跟著改,並補上可稽核的跡象:回報裡的腳本路徑全是字面絕對路徑,跨外掛的路徑中間是 current 那一層而不是帶版本號的快取路徑。
2026-09-03 13:07:07 +08:00

96 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# jsc-cli — CLI 偵測與技能庫部署
jsc 技能組的 CLI domain:找出已安裝的 AI CLI、列出各 CLI 可用模型並加上能力標籤,以及對每個 CLI 批次安裝、更新、解除安裝整組 jsc plugins。
## 安裝、更新、移除
Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安裝 token 為 `jsc-cli@jsc`。每個指令一行:
| CLI | 安裝 | 更新 | 移除 |
| --- | --- | --- | --- |
| claude | `claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && claude plugin install jsc-cli@jsc` | `claude plugin marketplace update jsc && claude plugin update jsc-cli@jsc` | `claude plugin uninstall jsc-cli@jsc` |
| codex | `codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && codex plugin add jsc-cli@jsc` | `codex plugin marketplace upgrade jsc` | `codex plugin remove jsc-cli@jsc` |
| copilot | `copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && copilot plugin install jsc-cli@jsc` | `copilot plugin marketplace update jsc && copilot plugin update jsc-cli@jsc` | `copilot plugin uninstall jsc-cli@jsc` |
| antigravity | `git clone https://gitea.jsc.idv.tw/plugins/cli.git ~/plugins/cli && agy plugin install ~/plugins/cli` | `git -C ~/plugins/cli pull && agy plugin uninstall jsc-cli && agy plugin install ~/plugins/cli` | `agy plugin uninstall jsc-cli` |
| kiro | `kiro-cli plugin marketplace add https://gitea.jsc.idv.tw/plugins/meta.git && kiro-cli plugin install jsc-cli@jsc` | `kiro-cli plugin marketplace update jsc && kiro-cli plugin update jsc-cli@jsc` | `kiro-cli plugin uninstall jsc-cli@jsc` |
> antigravity 不支援 gitea URL 安裝,改用本地 clone 路徑。批次操作五個 CLI:使用 `/jsc-cli:deploy`。
> 舊入口 `plugins/jsc` 已移除,marketplace 正本移到 `plugins/meta`。marketplace 名稱仍是 `jsc`(取自 marketplace.json 的 `name` 欄位,與存取庫名無關),安裝 token 不變;已從舊入口安裝過的人先執行 `claude plugin marketplace remove jsc`,再依上表重新 add。
## 工具
| 工具 | 用途 |
| --- | --- |
| `tools/detect-clis.sh` | 列出已安裝的 AI CLI 與執行檔路徑(TSV:name / path / version;antigravity 的執行檔為 `agy`、kiro 為 `kiro-cli`) |
| `tools/deploy.sh` | 對單一 CLI 執行安裝、更新或解除安裝(`deploy.sh [-n] {mode} {cli} {domain}...`,mode 為 install / update / uninstall);印出每個指令與其結束碼,最後一行 `result` 標 ok 或 fail。`-n` 只印指令不執行。上表五個 CLI 的指令差異全部收在這支腳本裡。Codex 更新 `jsc-cli` 與 `jsc-hooks` 後會把舊版快取路徑補成指向新版的相容連結,避免正在跑的部署流程找不到 helper 腳本,也避免尚未重啟的工作階段在 Stop hook 階段找不到舊路徑。收尾還會刷新 `$JSC_HOME/current/` 那一組不帶版本號的符號連結(技能文件的跨外掛路徑都以那一層當根):install 與 update 把每一條指到這次裝的版本目錄,uninstall 清掉指向已消失的那幾條,一條印一行 `link`,第五欄就是指向。一台機器只有一組農場,所以只有基準 CLI 那一輪會動它,基準取 claude、codex、copilot、kiro 之中第一支裝得到的;連結刷新失敗只記一行、不讓部署變成失敗。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` 與結束碼 1。`deploy.sh update` 在每個 domain 更新前呼叫它一次:結束碼 1 只印一行 `warn`,那個 domain 照樣更新——跳過會讓落後的 domain 永遠等不到相依版本,也就永遠更新不到,真正的阻擋由 `jsc-hooks` 的 `version-guard.sh` 在技能被叫用時執行;結束碼 2 以上是檢查腳本自己出錯,讀不到結論就不當成通過,印 `skip` 並跳過該 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/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 讀取。`gate` 的結束碼 0 為 PASS、1 為缺標籤、2 為模型或階段不在表上,`/jsc-cli:delegate` 依這三碼決定收下或退回 |
| `tools/build-todo.sh` | 把三支檢查腳本的輸出合併成一張「待修項目」表(`--config` 收 `scan-config.sh scan`、`--wiring {cli}=` 收 `wire-cli.sh status`、`--version` 收 `version-guard.sh report`);類別固定排成 missing、invalid、unwired、落後,一項都沒有時仍印一列「無」。`/jsc-cli:doctor` 與 `/jsc-cli:setup` 共用這一份合併規則,兩邊各寫一次就會各自漂移。兩支都以 `{CLI_ROOT}/tools/build-todo.sh` 這種字面絕對路徑呼叫它,`{CLI_ROOT}` 是 CLI 載入技能時講明的 jsc-cli 外掛基底目錄;不留 `$JSC_HOME`、波浪號或裸的相對路徑,帶變數的路徑進不了允許清單,相對路徑則會對著操作者當下的工作目錄解 |
## Skills 目錄
呼叫方式:Claude / Antigravity `/jsc-cli:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。
<!-- JSC-SKILLS:START -->
### `models`
**三支腳本併行取得資料**:`tools/detect-clis.sh` 取已安裝 CLI、`tools/list-models.sh` 讀出各 CLI 可使用的模型、`tools/model-config.sh list` 取各階段的偏好模型鏈。接著依 `references/model-tags.md` 加上能力標籤,用 `tools/model-tags.sh sync` 把標籤表寫進 `$JSC_HOME/model-tags.tsv` 供 sdlc-gate 讀取,並列出 SDLC 各階段的必要標籤(plan、analyze 需 `reasoning-max`;implement 需 `coding`;maintain 任意)。`jsc-sdlc` 閘門一律以能力標籤判定,偏好鏈只用來建議切換目標。
### `delegate`
把單一明確任務交給另一個已安裝的 AI agent CLI 當作 subagent 執行。CLI 清單一律取自 `tools/detect-clis.sh`,模型夠不夠格一律由 `tools/model-tags.sh gate` 的結束碼判定,不讓模型自評標籤。可指定目標 CLI,也可依任務需求用能力標籤篩選模型,或強制指定模型。每個目標分開派工,預設只讀,寫入範圍以路徑清單明列;回傳採固定的 TSV 契約(`result`、`summary`、`criterion`、`wrote`、`error`),供主 agent 逐項驗證。此技能不負責模型盤點或 plugin 部署。
### `deploy`
技能庫批次安裝、更新、解除安裝:**偵測 CLI、版本建議、marketplace domain 清單三者併行取得** → 秀出 `jsc-hooks/hooks/version-guard.sh report` 的版本證據表,再依 `recommend` 的一行結論(`recommend<TAB>{update|none|unverifiable}`,只印這一行,不混印表格)標出推薦選項;輸出裡找不到 `recommend` 行就退回 `report` 自行推導並在回報寫明是降級路徑 → 決策樹選模式(呼叫方已確認過模式就沿用,不重問)→ 每個 CLI 一個 sub agent,**全部同時啟動**呼叫 `tools/deploy.sh` 執行原生 plugin 指令(統一 marketplace `jsc`,token `jsc-{domain}@jsc`)→ update 前逐一檢查 `jsc.requires`,版本不符只印 `warn` 並照樣更新該 domain、回報還缺哪一版,阻擋交給 `version-guard.sh` 在技能被叫用時執行;檢查腳本自己出錯才印 `skip` 跳過該 domain → Codex 更新 `jsc-cli` 與 `jsc-hooks` 時保留舊快取相容連結 → install、update 後把偵測到的 CLI 清單交給 `jsc-hooks:hooks-install`,取它的彙總結果,只自行加判一條「smoke 出現 `No such file` 一律視為更新失敗」。domain 名單動態取自 `plugins/meta` 的 marketplace.json,不硬編碼。
### `doctor`
一次體檢執行環境,只讀不改。開跑先解出兩條根目錄,整輪的腳本呼叫一律填成字面絕對路徑:`{JSC_ROOT}` 取 `readlink -f "$JSC_HOME/current"`(解到那一層就停,不再往下解成帶版本號的快取路徑),`{CLI_ROOT}` 取 CLI 載入技能時講明的外掛基底目錄。這一支解不出 `{JSC_ROOT}` 不停手,直接把 `JSC_HOME` 記成一項發現:唯讀體檢中途停掉,操作者什麼都拿不到。技能自己的 `tools/` 與 `templates/` 一律走 `{CLI_ROOT}`,不走 `current` 底下那條 `jsc-cli` 連結——體檢正是機器可疑時才跑的,連結指著舊版時拿到的是舊的 scan-config.sh 與舊的 config-spec.tsv,掃出來的每一列都像真的發現,而且不會有任何錯誤訊息。四項檢查**同時啟動,各一個 sub agent**:技能版本(`{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh report`)、Hook 接線(`{JSC_ROOT}/jsc-hooks/tools/wire-cli.sh status`,唯讀子命令)、設定現況(`{CLI_ROOT}/tools/scan-config.sh scan all` 比對 `{CLI_ROOT}/tools/config-spec.tsv`,一次掃完全域與專案兩個範圍)、漏登錄變數(`{CLI_ROOT}/tools/scan-config.sh orphans`)。唯讀契約下放程式層:呼叫 `wire-cli.sh` 一律帶 `JSC_READONLY=1`,打錯子命令也不會改到機器。每項各出一張表,專案那張一定寫出掃的是哪個目錄;待修項目由 `{CLI_ROOT}/tools/build-todo.sh` 合併三份輸出並排序。整份結果寫進 wiki `CHECK_{HASH}`,`HASH` 取 `{短主機名}/{登入帳號}`,兩個值都由程式取,主機名一律切掉網域,只保留最新一次。目錄頁 `CHECK_CONTENTS` 住另一個庫(`JSC_WIKI_REPO_CONTENTS`),版面是 H1 加 `>` 引言,再一台機器一個 H2 區塊,頁上沒有 markdown 表格;改由 `{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 CHECK_{HASH}` 只寫本機那一個區塊,欄位是 `- {欄位名}:{值}` 的條列。鍵是 H2 標題,也就是體檢頁頁名 `CHECK_{HASH}`,不是任何含網址的值:網址會隨站台、存取庫與頁名編碼改變,頁名只由主機加帳號決定,拿網址當鍵就比不中,同一台機器每體檢一次就多附一個區塊。第二個參數 `1` 是 `<key-col>`,只在頁面還是舊表格、需要自動轉檔時用得到,指舊表格裡持有 `[CHECK_{HASH}](網址)` 的第 1 欄。「體檢頁」那一條的連結給人點,填絕對網址。修復交給 `/jsc-cli:setup`,體檢本身不動任何設定。
### `setup`
修復 `/jsc-cli:doctor` 找出的問題,一次一項,逐項確認才動手。路徑寫法與 doctor 相同:`{JSC_ROOT}` 與 `{CLI_ROOT}` 各解一次,之後每一支腳本都用字面絕對路徑呼叫。這一支寫檔,所以更不能走 `current` 底下那條 `jsc-cli` 連結——`JSC_HOME` 本身就是它要修的一列,那種機器上 `{JSC_ROOT}` 根本解不出來,而修它要用的 apply-config.sh 掛在 `{CLI_ROOT}` 底下,不靠 `JSC_HOME`,所以解不出來既不停這一輪也不擋那一項修復;何況拿舊的 apply-config.sh 寫 rc 檔,再用同一份舊的 `show` 重驗,兩邊當然對得上,整輪會報成已修。待修清單優先讀 wiki `CHECK_{HASH}`,沒有頁面就當場重掃:**三支檢查腳本併行跑**,再用 `{CLI_ROOT}/tools/build-todo.sh` 合併成同一張表。依修法分流:`auto` 用 `{CLI_ROOT}/tools/apply-config.sh` 直接寫、`ask` 先用決策樹問到值再寫、`manual` 印出步驟交給操作者。複合修復交回原主:版本落後找 `/jsc-cli:deploy`(連同已確認的模式與版本報告一起傳過去,不讓它重問重查)、hook 未接線找 `/jsc-hooks:hooks-install`、缺 `model-tags.tsv` 找 `/jsc-cli:models`。逐項確認維持循序,**寫完的重驗併行**;環境變數類只驗「rc 段落裡確實有那一行」,環境層面交給下一次 `/jsc-cli:doctor`。最後覆寫體檢頁 `CHECK_{HASH}`,並用 `{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 CHECK_{HASH}` 以 H2 標題(也就是體檢頁頁名)當鍵,更新目錄頁 `CHECK_CONTENTS` 的本機那一個區塊,兩頁分屬不同存取庫。
<!-- JSC-SKILLS:END -->
## 環境變數
| 變數 | 用途 | 未設定時 |
| --- | --- | --- |
| `GITEA_HOST` | Gitea 站台(可省略 scheme,預設 https) | 用正本站台 `https://gitea.jsc.idv.tw` |
| `JSC_GITEA_OWNER` | 技能組存取庫的 owner | 用 `plugins` |
| `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_{HASH}` 所在的 `{owner}/{repo}`;只管內容頁,管不到目錄頁 | 退回 `JSC_WIKI_REPO`;兩個都沒有就略過寫入,並把這一項列進待修 |
| `JSC_WIKI_REPO_CONTENTS` | 目錄頁 `CHECK_CONTENTS` 所在的 `{owner}/{repo}`。目錄頁與內容頁分屬不同存取庫:所有 `{TYPE}_CONTENTS` 一律住這一個庫,內容頁才看各自的型別變數 | 退回 `JSC_WIKI_REPO`;兩個都沒有就略過目錄頁寫入,並把這一項列進待修。不退回 `JSC_WIKI_REPO_CHECK` |
| `JSC_CONFIG_SPEC` | 改讀別份設定規格表(測試 `scan-config.sh` 時用) | 用 `tools/config-spec.tsv` |
| `JSC_HOME` | hook 資料目錄,兩份指引與重啟狀態檔都寫在這裡 | 用 `~/.jsc` |
| `JSC_RESTART_GATE` | 設成 `off` 可略過部署後的重啟提示閘門(判讀在 `jsc-hooks`,`jsc-cli` 只負責寫狀態檔) | 照常提示重啟 |
`JSC_LOCAL_PLUGINS` 的預設值刻意避開 `~/plugins`:那是維護者放技能組開發 checkout 的地方,`git pull` 下去會蓋掉未提交的工作。這個變數指到的目錄若是開發中的樹(有未提交變更,或有未推送的 commit),`deploy.sh` 只印一行 `skip` 並直接用現地內容安裝,不執行 `git pull`。
## 部署留在機器上的檔案
| 檔案 | 何時產生 | 用途 |
| --- | --- | --- |
| `$JSC_HOME/update-guide.md` | install、update 收尾 | 下次更新的依據:偵測到的 CLI、每支的安裝方式與實際指令、marketplace token、domain 清單 |
| `$JSC_HOME/remove-guide.md` | install、update 收尾 | 整組移除的依據:各 CLI 的移除指令,加上 `$JSC_HOME`、本地 clone、kiro 技能目錄、rc 檔 `# jsc-config` 段落這些殘留物 |
| `$JSC_HOME/restart-required.d/{cli}` | install、update 全數成功時 | 一支 CLI 一份,內容是四行 key=value(`at`、`mode`、`domains`、`cli`)。`jsc-hooks` 只讀當前 CLI 那一份來提示重啟,別支的不影響這一支;逃生門 `JSC_RESTART_GATE=off` 也在那邊判讀。路徑與格式的唯一來源是 `jsc-hooks/hooks/restart-gate.sh`,`deploy.sh` 只轉呼叫它的 `require` |
兩份指引一律整份覆寫,內容依產生當下的偵測結果生成,不寫死。`uninstall` 不產生指引、也不寫重啟狀態檔。
## 相關 domain
- [`jsc-gitea`](https://gitea.jsc.idv.tw/plugins/gitea):取得 domain 名單(`tools/gitea.sh repos plugins`)
- [`jsc-hooks`](https://gitea.jsc.idv.tw/plugins/hooks):deploy 完成後重新接線 hooks(`jsc-hooks:hooks-install`)