這兩支技能的腳本呼叫原本全是裸相對路徑,整份文件沒有任何路徑守則。相對路徑會對著操作者當下的工作目錄解,而那裡從來不是外掛根目錄,所以每一個呼叫點都是模型就地猜前綴的地方。部署技能原本三處相對路徑被補成帶變數路徑,就是同一個機制。 實際掃到的處數比預估多:體檢 18 處、修復 20 處。多出來的是兩類原本沒想到的——裸檔名的 Gitea 呼叫,以及被當成參數傳的資料檔。後者同樣會解錯地方,而且解錯了不會報錯。 兩支各補路徑守則與前置步驟,解出兩條根目錄:跨外掛走 current 那一層(解到那一層就停,再往下解會落到帶版本號的快取路徑,那種路徑進不了允許清單),技能自己的腳本與範本走 CLI 載入技能時講明的外掛基底目錄。 兩支都不靠 current 底下那條 jsc-cli 連結,但理由各自不同,不是照抄部署那一支的。體檢不能靠,因為它正是機器可疑時才跑的技能——觸發時機本身就是連結可能出錯的那幾個時刻,而連結指著舊版時拿到的是舊的掃描腳本與舊的規格表,掃出來的每一列都像真的發現,不會有任何錯誤訊息。修復更不能靠,因為它寫檔,而且它要修的其中一列正是那個決定連結位置的變數:那種機器上根目錄根本解不出來,而修它要用的腳本掛在外掛基底目錄底下、不依賴那個變數,所以解不出來既不停這一輪也不擋那一項修復。何況拿舊的寫入腳本改設定檔,再用同一份舊腳本重驗,兩邊當然對得上,整輪會報成已修。 失敗分支也與部署不同。部署解不出根目錄就停手;這兩支都不停——體檢把它記成一項發現然後跑完不需要跨外掛的檢查,因為唯讀體檢中途停掉操作者什麼都拿不到;修復把它當成待修的那一項,修好再重解一次。 兩份範本與說明文件一併改。範本是這兩支技能在同一輪一起讀的,而且指示寫入,留著裸路徑等於留一個繞過新規則的入口。範本裡另外補掉兩個路徑洞:指向範本自己的佔位符原本沒有路徑,以及一個連目錄都沒有的裸檔名。 行為契約四列跟著改,並補上可稽核的跡象:回報裡的腳本路徑全是字面絕對路徑,跨外掛的路徑中間是 current 那一層而不是帶版本號的快取路徑。
96 lines
17 KiB
Markdown
96 lines
17 KiB
Markdown
# 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`)
|