Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62ea8022e0 | ||
|
|
cd832460ee | ||
|
|
07c1c44e5a | ||
|
|
56dbffa25c | ||
|
|
5b0eb784b4 | ||
|
|
45187f5173 | ||
|
|
47f248adec | ||
|
|
a439636c4a | ||
|
|
253443e94e | ||
|
|
bd23690aa5 | ||
|
|
b41ceb00d9 | ||
|
|
9d0e1ea3dd | ||
|
|
4479a090a6 | ||
|
|
d5bc40be13 | ||
|
|
8444307534 | ||
|
|
db2a2f8206 | ||
|
|
03e59c69bd | ||
|
|
936fc5cdaa | ||
|
|
9f4ed977b2 | ||
|
|
c76daa61ca | ||
|
|
86225f355f | ||
|
|
fb80559159 | ||
|
|
225e33d2c8 | ||
|
|
214b8a22c5 | ||
|
|
e87d6b36fe | ||
|
|
36e1bebe1f | ||
|
|
749b1ad6d3 | ||
|
|
daa4bcf9e1 | ||
|
|
7d58afa2ee | ||
|
|
b5d72f5245 | ||
|
|
99e0554995 | ||
|
|
5f1c4cc6d7 | ||
|
|
ed4092bb3d | ||
|
|
4ed0bf11a2 | ||
|
|
76b3f2dcb6 | ||
|
|
8d17f231cf | ||
|
|
f19b9494b4 | ||
|
|
8112905364 | ||
|
|
e30bbf5780 | ||
|
|
e8bf4ddaaf | ||
|
|
369e6e59f6 | ||
|
|
5e4c413dec | ||
|
|
ca58604afc | ||
|
|
90b89047e3 | ||
|
|
fc446bfe55 | ||
|
|
3833055499 | ||
|
|
4aa691a9cb | ||
|
|
a89484c9fa | ||
|
|
c4e75f7f83 | ||
|
|
382f946167 | ||
|
|
d918ccc6dc | ||
|
|
a94a942712 | ||
|
|
2e85dc4978 | ||
|
|
284292ffcb | ||
|
|
98781a331f | ||
|
|
07a652b309 | ||
|
|
6bbcc2a695 | ||
|
|
0f9aeefead | ||
|
|
afc4c5b50a | ||
|
|
9377c2fbc7 | ||
|
|
eca467696f | ||
|
|
c397994ce8 | ||
|
|
d1da14c778 | ||
|
|
7af195b342 | ||
|
|
ebca8883fc | ||
|
|
e01ec1b3f5 | ||
|
|
1840af2ace | ||
|
|
96cd7bd14d | ||
|
|
bbd48b5d95 | ||
|
|
ffff2152c2 | ||
|
|
7e6591e644 | ||
|
|
9a7fbeb229 | ||
|
|
63d4185a40 | ||
|
|
581b9de5bc | ||
|
|
497a16b877 | ||
|
|
2720bf4d33 | ||
|
|
a2f1f65ff8 | ||
|
|
7077d65766 | ||
|
|
27ddf4e5a6 | ||
|
|
49cd4d3ee4 | ||
|
|
9067c643b2 | ||
|
|
e655f9a963 | ||
|
|
2adf9a172e | ||
|
|
08ad10353b | ||
|
|
17329a94d3 | ||
|
|
7df56159e1 | ||
|
|
3082e5167f | ||
|
|
a739ae20f8 | ||
|
|
26ffaa8a07 | ||
|
|
cea4d701aa |
@@ -13,6 +13,14 @@
|
||||
},
|
||||
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
||||
},
|
||||
{
|
||||
"name": "jsc-assist",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
|
||||
},
|
||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
|
||||
},
|
||||
{
|
||||
"name": "jsc-cli",
|
||||
"source": {
|
||||
@@ -43,7 +51,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||
},
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||
},
|
||||
{
|
||||
"name": "jsc-log",
|
||||
@@ -75,7 +83,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||
},
|
||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
||||
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||
},
|
||||
{
|
||||
"name": "jsc-sdlc",
|
||||
@@ -83,7 +91,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||
},
|
||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
||||
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
},
|
||||
"description": "決策樹問詢與問詢紀錄(QUESTION_* wiki 頁)"
|
||||
},
|
||||
{
|
||||
"name": "jsc-assist",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/assist.git"
|
||||
},
|
||||
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_* wiki 頁)"
|
||||
},
|
||||
{
|
||||
"name": "jsc-cli",
|
||||
"source": {
|
||||
@@ -43,7 +51,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/hooks.git"
|
||||
},
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄"
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查"
|
||||
},
|
||||
{
|
||||
"name": "jsc-log",
|
||||
@@ -75,7 +83,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/review.git"
|
||||
},
|
||||
"description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組"
|
||||
"description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組"
|
||||
},
|
||||
{
|
||||
"name": "jsc-sdlc",
|
||||
@@ -83,7 +91,7 @@
|
||||
"source": "url",
|
||||
"url": "https://gitea.jsc.idv.tw/plugins/sdlc.git"
|
||||
},
|
||||
"description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)"
|
||||
"description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jsc-cli",
|
||||
"version": "0.0.6",
|
||||
"description": "CLI 偵測、模型能力標籤與技能庫批次部署",
|
||||
"version": "0.3.5",
|
||||
"description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
"name": "JSC"
|
||||
@@ -13,5 +13,12 @@
|
||||
"cli",
|
||||
"skills",
|
||||
"cross-tool"
|
||||
]
|
||||
],
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.7",
|
||||
"jsc-gitea": ">=0.1.7",
|
||||
"jsc-hooks": ">=0.2.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"name": "jsc-cli",
|
||||
"version": "0.0.6",
|
||||
"description": "CLI 偵測、模型能力標籤與技能庫批次部署",
|
||||
"skills": "./skills"
|
||||
"version": "0.3.5",
|
||||
"description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署",
|
||||
"skills": "./skills",
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.7",
|
||||
"jsc-gitea": ">=0.1.7",
|
||||
"jsc-hooks": ">=0.2.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,17 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
|
||||
| 工具 | 用途 |
|
||||
| --- | --- |
|
||||
| `tools/detect-clis.sh` | 列出已安裝的 AI CLI 與執行檔路徑(TSV:name / path / version;kiro 的執行檔為 `kiro-cli`) |
|
||||
| `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/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 讀取 |
|
||||
| `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 目錄
|
||||
|
||||
@@ -34,14 +42,53 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
||||
|
||||
### `models`
|
||||
|
||||
讀取各已安裝 CLI 可使用的模型並加上能力標籤(`references/model-tags.md`),用 `tools/model-tags.sh sync` 把標籤表寫進 `$JSC_HOME/model-tags.tsv` 供 sdlc-gate 讀取,列出 SDLC 各階段的必要標籤(plan、analyze 需 `reasoning-max`;implement 需 `coding`;maintain 任意),並用 `tools/model-config.sh` 列出各階段的偏好模型鏈。`jsc-sdlc` 閘門一律以能力標籤判定,偏好鏈只用來建議切換目標。
|
||||
**三支腳本併行取得資料**:`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 → **先比對各 plugin 的本機與已發佈版本並列表,只要有任一個落後就把「更新」設為推薦選項** → 決策樹選模式 → 每個 CLI 一個 sub agent 執行原生 plugin 指令(統一 marketplace `jsc`,token `jsc-{domain}@jsc`)。domain 名單動態取自 `plugins/meta` 的 marketplace.json,不硬編碼。
|
||||
技能庫批次安裝、更新、解除安裝:**偵測 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`)
|
||||
|
||||
+10
-3
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"name": "jsc-cli",
|
||||
"version": "0.0.6",
|
||||
"description": "CLI 偵測、模型能力標籤與技能庫批次部署",
|
||||
"skills": "./skills/"
|
||||
"version": "0.3.5",
|
||||
"description": "CLI 偵測、模型能力標籤、子代理派工與技能庫批次部署",
|
||||
"skills": "./skills/",
|
||||
"jsc": {
|
||||
"requires": {
|
||||
"jsc-ask": ">=0.0.7",
|
||||
"jsc-gitea": ">=0.1.7",
|
||||
"jsc-hooks": ">=0.2.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# jsc-cli 技能行為清單
|
||||
|
||||
本頁記錄 jsc-cli 每支技能的行為基準,供技能驗證比對。技能異動時,在同一個 PR 內一起更新這一頁。
|
||||
|
||||
## delegate
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 一件邊界清楚的工作要交給另一支已安裝的 AI agent CLI 執行時用。盤點模型(models)、部署技能組(deploy)、必須留在目前 agent 手上的工作都不用;給不出通過或失敗判準的目標也不用 |
|
||||
| 關鍵步驟 | 寫下一句目標與可判定通過或失敗的驗收條件、同時起跑 detect-clis.sh 與 list-models.sh 取得 CLI 清單與模型清單、先選定目標 CLI 再用 model-tags.sh 的 gate 或 model 驗證模型能力標籤、組出帶目標、驗收條件、目標 CLI、模型代號、最小脈絡、寫入範圍與 TSV 輸出合約的提示、開一個 sub agent 執行、查驗回傳的結束碼與 result、summary、criterion、wrote 各行、把成功、失敗、需要使用者補充分三段回報,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-cli:delegate 寫下這一輪的結果。收尾那一筆走每一條出口,連停在能力閘門那一條也要寫;status 五選一,sub agent 回 0 且每條驗收條件都 pass、wrote 也都在範圍內是 ok,能力鎖擋下所有候選模型、或一支 CLI 都沒偵測到、整輪沒開 sub agent 是 blocked,sub agent 非零退出、回傳格式不符、有驗收條件 fail、或 wrote 越界是 failed,sub agent 回 needs-input、工作只做一半等使用者補資料是 degraded,目標給不出通過或失敗判準、或請求裡包了兩個以上獨立目標而主動停手是 aborted。detail 只放目標 CLI 與模型代號,不放 sub agent 的輸出。腳本不在這台機器就安靜跳過,回報失敗不得改變這支技能的結果 |
|
||||
| 外部呼叫 | jsc-cli/tools/detect-clis.sh、jsc-cli/tools/list-models.sh、jsc-cli/tools/model-tags.sh(gate 與 model 兩個子命令)、jsc-hooks/tools/report-status.sh skill-end、一個代跑工作的 sub agent |
|
||||
| 完成條件 | 目標 CLI 與模型各只有一個,且模型通過能力要求;sub agent 回傳的每一條驗收條件都有判定;每個 wrote 路徑都落在寫入範圍內;報告分三段列出結果。中途停下時,停下的原因要寫在報告裡。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
|
||||
| 可驗證跡象 | 技能自己只寫這一筆事件。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-cli:delegate,status 與 exit 就是這一輪的結果。另一項可檢查的是 sub agent 依寫入範圍實際改動的檔案,逐條列在回傳的 wrote 行上,照那些路徑去看即可比對;寫入範圍為空的唯讀委派沒有其他寫入跡象,只有回報內容與那一筆事件 |
|
||||
|
||||
## deploy
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 整組 jsc 技能要在這台機器的每一支已安裝 CLI 上安裝、更新或解除安裝時用。只處理單一技能不用;只想知道版本落後與否,看 doctor 就夠 |
|
||||
| 關鍵步驟 | 先跑一次 `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,解到那一層就停,不再往下解成帶版本號的快取路徑——那種路徑放不進允許清單,版本號寫成萬用字元也對不上;同一步再跑 `[ -d "{剛印出來的路徑}" ]` 確認目錄存在,`JSC_HOME` 沒設時它印的是 `/current`、結束碼 0,非空又是絕對路徑,只看那兩項擋不下來。整輪只解這一次,之後每一次跨外掛腳本呼叫都填成那個字面絕對路徑,不留 `$JSC_HOME` 也不留波浪號;技能自己那四支 `tools/*.sh` 一律不走 `current`,即使那裡擺著 `jsc-cli` 那一條也一樣——第四步會為每個部署到的 domain 刷新連結,所以跑過一輪之後那一條通常在,但第一次建起它的正是這一輪,沒部署過的機器、或上一輪 link 回 fail 的機器,那一條不在或還停在舊版,四支腳本會解到不存在的路徑或自己的舊副本;根目錄取自 CLI 載入這支技能時講明的外掛基底目錄,原樣當字面絕對路徑用,一個指令都不跑,四支一律寫成 `{外掛根目錄}/tools/{腳本}`;那個基底目錄帶版本號,四次呼叫都會跳權限詢問,這一支有人在現場(第三步要問模式)所以按得掉,無人值守的技能不得照抄,叫用文字沒講明基底目錄就回報外掛根目錄不明並停手,不猜前綴——權限層靜態比對路徑,帶未展開變數的呼叫一律要人核准,無人看管的輪次會停在第一支腳本,補權限規則也擋不住,因為規則字面同樣是靜態比對;解不出來就停手回報。接著同時取得三項事實(detect-clis.sh 的 CLI 清單、version-guard.sh 的 report 版本表與 recommend 結論、marketplace.json 的 domain 清單)、把版本表原樣秀出並定出建議、依 jsc-ask 決策樹問出模式(呼叫端已帶模式就沿用並標明來源)、每支 CLI 各開一個 sub agent 同時跑 tools/deploy.sh {mode} {cli} {domain}...、讀 deploy.sh 收尾印出的 link 行確認 `current` 連結農場刷新到位(install 與 update 把每一條指到這次裝的版本目錄,uninstall 清掉指向已消失的那幾條;一台機器只有一組農場而五支 CLI 各有副本,所以只有基準 CLI 那一輪會動它,基準是 claude、codex、copilot、kiro 之中這台機器第一支裝得到的,其餘四支各印一行 link 標明基準是誰,平行覆寫會讓最後指到哪一份變成隨機;狀態 ok 要把指向抄進收尾報告,removed 不必處置,skip 帶 domain 是那個路徑上擺著非符號連結的東西、腳本刻意不覆寫而要人工處理,fail 是版本目錄取不到或連結建不起來、部署本身仍然成立但那條文件路徑可能還停在舊版,整輪判 degraded,連結失敗一律不記成部署失敗,否則會連重啟閘門與 result 行一起跳過,把一台裝好的機器講成失敗)、安裝或更新後把 CLI 清單交給 jsc-hooks:hooks-install、整台機器跑一次 tools/write-guides.sh、彙整每支 CLI 的結果並要求重新啟動工作階段,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-cli:deploy 寫下這一輪的結果。收尾那一筆接在彙整回報之後,不取代它;走每一條出口,連停在偵測不到 CLI 那一條也要寫。status 五選一,每支 CLI 都回 0、hooks-install 判定乾淨、兩份指引都寫成、基準 CLI 的 link 行全是 ok 或 removed 是 ok,偵測不到任何 CLI、整輪沒下過任何外掛命令是 blocked,marketplace 讀不到或每支 CLI 都失敗、冒煙結果出現 No such file 是 failed,部分 CLI 成功部分失敗、有 domain 被 skip、write-guides.sh 回 4 讓機器沒有最新指引、或有 link 行回 fail 與帶 domain 的 skip 是 degraded,使用者沒選模式或在第一支 CLI 開跑前停手是 aborted。detail 只放模式與各項筆數,cmd 與 exit 行留在回報裡 |
|
||||
| 外部呼叫 | `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,加上同一步的 `[ -d ]` 確認,是整輪唯一容許帶變數的兩個指令;跨外掛腳本一律用它組成的字面絕對路徑呼叫:{current 目錄}/jsc-hooks/hooks/version-guard.sh 的 report 與 recommend、{current 目錄}/jsc-gitea/tools/gitea.sh 讀 plugins/meta 的 marketplace.json、{current 目錄}/jsc-hooks/tools/report-status.sh skill-end。技能自己那四支腳本走外掛根目錄組成的字面絕對路徑:{外掛根目錄}/tools/detect-clis.sh、{外掛根目錄}/tools/deploy.sh、{外掛根目錄}/tools/write-guides.sh、{外掛根目錄}/tools/check-requires.sh(由 deploy.sh 在每個 domain 更新前轉呼叫)。第四步那段內文提到的 `jsc-hooks/hooks/version-guard.sh` 是在講擋人發生在哪一層,不是這支技能要下的呼叫,整輪只有第一步那一次真的跑它。另有 jsc-hooks/hooks/restart-gate.sh require(由 deploy.sh 收尾轉呼叫,install 與 update 一律呼叫,整輪判定成 fail 也照呼叫——外掛已經換了一部分,那時候更需要重啟;原本只在成功時呼叫,實測有一輪被一條與部署無關的 git pull 判成 fail,那一支 CLI 就沒有被掛上閘門)、jsc-ask:ask、jsc-hooks:hooks-install。`current` 連結農場的刷新不是另一支腳本,是 deploy.sh 自己收尾做的,技能只讀它印的 link 行,不自己下 ln 或 rm |
|
||||
| 完成條件 | `current` 那個目錄在第一步就解出一條存在的絕對路徑(用 `[ -d ]` 查過,而且沒有再往下解成帶版本號的快取路徑),技能自己那四支腳本也有一條字面絕對的外掛根目錄可用,後續每一支腳本都用這兩條之一組成的字面絕對路徑呼叫;每一支偵測到的 CLI 都回報結束碼與 result 行,每個 skip、warn、compat、link 行都照實列出;基準 CLI 那一輪每個 domain 都有一條 link 行,狀態 ok 的把指向抄進收尾報告,狀態 fail 與帶 domain 的 skip 都點名外掛與原因並整輪判 degraded,非基準的那幾支各有一行標明基準是誰;安裝或更新還要拿到 hooks-install 對每支 CLI 的總結,兩份指引都印出 wrote,收尾印出重啟指示、兩份指引路徑,以及每個外掛的連結現在指到哪一個版本目錄。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
|
||||
| 可驗證跡象 | 各 CLI 的外掛目錄多出或少掉 jsc-{domain}:claude 與 codex 在各自的 plugin 快取、copilot 在 installed-plugins、antigravity 與 kiro 走 $JSC_LOCAL_PLUGINS 的本地 clone 與 $JSC_KIRO_SKILLS 的複製;那一份本地 clone 一台機器只有一份而五支 CLI 平行跑,所以 deploy.sh 對每個 domain 取一把 mkdir 鎖再 pull,拿不到鎖或 pull 回非零時印一行 warn 並改用磁碟上現有的內容、不判整輪失敗(clone 不存在那一種照舊算失敗,磁碟上沒東西可裝)。$JSC_HOME/plugins/.lock-{domain} 在一輪跑完之後一個都不該留著。$JSC_HOME/current/ 底下每個外掛一條符號連結,安裝或更新之後拿 `readlink` 讀出來的指向,就是基準 CLI 這一輪裝到的那個帶版本號目錄,跟 link 行第五欄逐字相同,也跟 version-guard.sh report 那張表上該外掛的本機版本對得起來——這一項驗得到連結指向正確:連結上的版本號與剛裝上的版本號不一致,就是刷新沒做到;解除安裝之後,指向已消失的那幾條不再留在目錄裡,`readlink -e` 對每一條都解得出存在的目錄,沒有一條是斷的;那個目錄底下也不會出現實體目錄,非符號連結的項目腳本刻意不動並留下一行 skip。$JSC_HOME/restart-required.d/{cli} 出現這次的重啟狀態檔;$JSC_HOME/update-guide.md 與 $JSC_HOME/remove-guide.md 被重寫;各 CLI 的 hook 設定檔由 hooks-install 改寫;$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-cli:deploy,status 與 exit 就是這一輪的結果。回報與逐行紀錄裡出現的腳本路徑全是字面絕對路徑,找不到 `$JSC_HOME`、`$` 開頭或波浪號開頭的呼叫,唯一的例外是開頭那一次 `readlink -f "$JSC_HOME/current"` 與同一步的 `[ -d ]` 確認;跨外掛那幾支的路徑中段是 `current`,不是 `cache/jsc/{外掛}/{版本}`,技能自己那四支則一律是外掛根目錄接 `tools/`,沒有一支寫成裸的相對路徑 |
|
||||
|
||||
## doctor
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 裝完或更新完技能組、技能因設定或接線問題失敗、機器要交接前用。要動手修不用這支,那是 jsc-cli:setup |
|
||||
| 關鍵步驟 | 先跑一次 `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,解到那一層就停,不再往下解成帶版本號的快取路徑——那種路徑放不進允許清單,版本號寫成萬用字元也對不上;同一步再跑 `[ -d "{剛印出來的路徑}" ]` 確認目錄存在,`JSC_HOME` 是有預設值的選擇性項目,沒設時它印的是 `/current`、結束碼 0,非空又是絕對路徑,只看那兩項擋不下來。這一支跟 deploy 不同,解不出來不停手:`JSC_HOME` 直接記成全域設定與待修項目上的一項發現,用得到跨外掛腳本的檢查記成無法驗證,其餘照跑到完——唯讀體檢中途停掉,操作者什麼都拿不到,而路徑解不開的機器正是最需要體檢的那一台。整輪只解這一次,之後每一次跨外掛腳本呼叫都填成那個字面絕對路徑,不留 `$JSC_HOME` 也不留波浪號,也不留裸的相對路徑——相對路徑會對著操作者的專案目錄解,那裡從來不是外掛根目錄,每個這種呼叫點都是前綴被猜出來的地方。技能自己的 `tools/` 與 `templates/` 一律不走 `current`,即使那裡擺著 `jsc-cli` 那一條也一樣:這一支正是機器可疑時才跑的技能,它的觸發時機自己就寫著裝完或更新完、技能因設定或接線失敗、機器要交接,而 deploy 判 degraded 那一輪留下的正是回 fail 或被 skip、還指著舊版的連結;從那條連結拿到的 scan-config.sh 與 config-spec.tsv 是這台機器沒在跑的版本,掃出來的每一列都像真的發現,舊掃描器跑得完,一句錯誤訊息都不會有。改走外掛根目錄就沒有這個問題,連結壞掉、過期或從來沒建起來的機器上,那三支腳本照樣跑得動,`JSC_HOME` 本身也才掃得到、報得出來。根目錄取自 CLI 載入這支技能時講明的外掛基底目錄,原樣當字面絕對路徑用,一個指令都不跑,寫成 `{外掛根目錄}/tools/{腳本}` 與 `{外掛根目錄}/templates/{檔案}`;那個基底目錄帶版本號,這幾次呼叫都會跳權限詢問,這一支有操作者在現場讀五個區塊與待修項目表所以按得掉,無人值守的技能不得照抄,叫用文字沒講明基底目錄就回報外掛根目錄不明並停手,不猜前綴。接著同時開四個 sub agent 收版本、hook 接線、設定與未登錄變數,每個 sub agent 回傳原始輸出行、把四份輸出各存成檔、依 templates/check-page.md 印出五個區塊並寫明掃描的專案目錄、用 tools/build-todo.sh 把三份輸出合成待修項目表、用程式取短主機名與登入帳號(主機名切掉第一個點之後的網域)交給 hash-id 算出 HASH、用 wiki-repo CHECK 解出的存取庫透過 jsc-gitea:wiki 整頁覆寫 CHECK_{HASH}、寫完再用 wiki-url 取該頁絕對網址、把要寫進兩頁的每個連結交給 jsc-gitea/tools/link-check.sh 驗證且只有結束碼 0 才往下寫、「體檢頁」那一條的連結寫成 `[CHECK_{HASH}]({絕對網址})`、用 wiki-contents.sh upsert CHECK 1 CHECK_{HASH} 以 H2 標題也就是體檢頁頁名當鍵,把本機那一個區塊寫進 CONTENTS 存取庫的 CHECK_CONTENTS,區塊檔是 `## CHECK_{HASH}` 那一行、一個空行,再照 templates/check-contents.md 的欄位順序每欄一條 `- {欄位名}:{值}`、報出四項計數並視情況建議 /jsc-cli:setup,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-cli:doctor 寫下這一輪的結果。這一筆是這支技能唯一的寫入動作,記的是查到什麼,不動設定、不動接線、不動版本,唯讀合約照樣成立;走每一條出口都要寫。status 五選一,四項檢查都有結論、五個區塊與待修項目表都在畫面上、兩頁都寫成是 ok,任何一項報成無法驗證、離線讓 Gitea 相關列變成 skipped、或 wiki-repo 回 3 而略過寫頁是 degraded——唯讀技能讀不到來源就是這一種,金鑰失效回 7 或其他 API 失敗回 8 讓紀錄寫不成是 failed,使用者在寫頁之前喊停是 aborted。blocked 這支用不到:沒 CLI、沒登錄檔、沒 wiki 存放庫的機器一樣查得出四項發現,報成 blocked 會把做完的一輪講成沒做事。detail 只放四項計數 |
|
||||
| 外部呼叫 | `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,加上同一步的 `[ -d ]` 確認,是整輪唯一容許帶變數的兩個指令;跨外掛腳本一律用它組成的字面絕對路徑呼叫:{current 目錄}/jsc-hooks/hooks/version-guard.sh report、{current 目錄}/jsc-hooks/tools/wire-cli.sh status(一律帶 JSC_READONLY=1)、{current 目錄}/jsc-gitea/tools/gitea.sh 的 wiki-repo、hash-id 與 wiki-url、{current 目錄}/jsc-gitea/tools/link-check.sh、{current 目錄}/jsc-gitea/tools/wiki-contents.sh upsert、{current 目錄}/jsc-hooks/tools/report-status.sh skill-end。技能自己的檔案走外掛根目錄組成的字面絕對路徑:{外掛根目錄}/tools/detect-clis.sh、{外掛根目錄}/tools/scan-config.sh 的 scan all 與 orphans、{外掛根目錄}/tools/build-todo.sh,比對用的 {外掛根目錄}/tools/config-spec.tsv 與兩份版型 {外掛根目錄}/templates/check-page.md、{外掛根目錄}/templates/check-contents.md 也一樣。另有 jsc-gitea:wiki |
|
||||
| 完成條件 | `current` 那個目錄在第一步就解出一條存在的絕對路徑(用 `[ -d ]` 查過,而且沒有再往下解成帶版本號的快取路徑),解不出來就記成 `JSC_HOME` 那一項發現而不是停手;技能自己的 `tools/` 與 `templates/` 也有一條字面絕對的外掛根目錄可用,後續每一支腳本、每一份版型都用這兩條之一組成的字面絕對路徑呼叫。四項檢查各有結論,或明寫無法驗證與原因;五個區塊與待修項目表都在畫面上;每個要寫進頁面的連結都經 link-check.sh 驗過,結束碼 0 才寫,1 就兩頁都不寫並列出 DEAD 那幾筆,3 把 GITEA_HOST 排進待修項目最前面,7 停下來回報金鑰問題而不判成死連結;連結一律寫成文字加絕對網址的形式,H2 標題本身不放連結;兩頁各自寫成功,或寫入略過連同結束碼一起回報,wiki-contents.sh 宣告的 0、1、2、3、4、7、8 每一碼都有分流,結束碼 1 是組不出頁面內容或寫入失敗,頁上找不到本機那一個區塊不算錯、腳本改成附加,建不建新頁的判斷留在腳本裡,技能不自己建;必要項缺漏、設定錯誤、CLI 未接線、domain 落後四項計數都講出來。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
|
||||
| 可驗證跡象 | wiki 的 CHECK_{HASH} 頁(雜湊來源是 {短主機名}/{登入帳號})被整頁覆寫成這次的結果;另一個存取庫的 CHECK_CONTENTS 多出本機那一個 H2 區塊,或該區塊的缺漏數與最後體檢時間被更新;區塊標題是 `## CHECK_{HASH}`,也就是比對用的鍵,標題上沒有連結也沒有網址,「體檢頁」那一條是 `[CHECK_{HASH}]({絕對網址})` 這種文字加連結的寫法,點下去連得到體檢頁,頁面上找不到同 wiki 的雙括號連結;欄位一律是 `- {欄位名}:{值}` 的條列,頁上沒有 markdown 表格,同一台機器重跑幾次都只有這一個區塊,別台機器的區塊一個位元組都沒變;連結驗不過的那一輪,兩頁都維持上一輪的內容。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-cli:doctor,status 與 exit 就是這一輪的結果,那也是這支技能唯一寫得出來的檔案痕跡。機器本身的設定、接線與版本都不動:這支技能不寫任何設定。回報與逐行紀錄裡出現的腳本路徑全是字面絕對路徑,找不到 `$JSC_HOME`、`$` 開頭或波浪號開頭的呼叫,也沒有一支寫成裸的相對路徑,唯一的例外是開頭那一次 `readlink -f "$JSC_HOME/current"` 與同一步的 `[ -d ]` 確認;跨外掛那幾支的路徑中段是 `current`,不是 `cache/jsc/{外掛}/{版本}`,技能自己那三支腳本與兩份版型則一律是外掛根目錄接 `tools/` 或 `templates/` |
|
||||
|
||||
## models
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | 要盤點各 CLI 可用模型、確認模型合不合 SDLC 階段的能力要求、或檢視階段閘門設定時用。切換模型不用,改 .jsc/models 與 models.conf 也不用 |
|
||||
| 關鍵步驟 | 同時起跑三個收集器(detect-clis.sh、list-models.sh 以 sub agent 執行、model-config.sh list)、對讀不到設定的 CLI 補上標「預設推定」的預設模型、依 references/model-tags.md 為每個模型掛能力標籤、印出 CLI、模型、標籤、使用中四欄表、跑 tools/model-tags.sh sync 把標籤表寫進 $JSC_HOME/model-tags.tsv、附上 SDLC 階段需求表與階段偏好模型表,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-cli:models 寫下這一輪的結果。收尾那一筆走每一條出口,連停在偵測不到 CLI 那一條也要寫;status 五選一,每支 CLI 都有模型清單、每個模型都掛到標籤、sync 回 0 印出路徑、兩張階段表都在是 ok,偵測不到任何 CLI、盤點那半段整個沒開始是 blocked,sync 非零而 model-tags.tsv 沒寫成、SDLC 閘門判不出來是 failed,讀不到某支 CLI 的設定而改用預設推定、有模型查不到標籤只能排進發問、或 model-config.sh 失敗讓偏好表變成未取得是 degraded,使用者在 sync 寫檔之前停手是 aborted。detail 只放 CLI 與模型筆數 |
|
||||
| 外部呼叫 | jsc-cli/tools/detect-clis.sh、jsc-cli/tools/list-models.sh、jsc-cli/tools/model-config.sh list、jsc-cli/tools/model-tags.sh sync、jsc-hooks/tools/report-status.sh skill-end、references/model-tags.md;標籤表上查不到的模型改用 jsc-ask:ask 發問 |
|
||||
| 完成條件 | 每支偵測到的 CLI 都有模型清單或一組預設推定;每個模型都掛到標籤,或已排進發問;sync 印出寫入路徑,失敗則連同結束碼回報;兩張階段表都列滿 plan、analyze、implement、maintain 四個階段。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
|
||||
| 可驗證跡象 | $JSC_HOME/model-tags.tsv 被重寫,內容就是這次掛好的標籤表;jsc-hooks/hooks/sdlc-gate.sh 讀的正是這份檔,檔案不在,SDLC 階段閘門就判不出來。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-cli:models,status 與 exit 就是這一輪的結果。各 CLI 的模型設定檔不動 |
|
||||
|
||||
## setup
|
||||
|
||||
| 項目 | 內容 |
|
||||
| --- | --- |
|
||||
| 觸發時機 | doctor 報出待修項目、要實際動手修這台機器時用。只想做唯讀體檢不用這支,那是 jsc-cli:doctor |
|
||||
| 關鍵步驟 | 先跑一次 `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,解到那一層就停,不再往下解成帶版本號的快取路徑——那種路徑放不進允許清單,版本號寫成萬用字元也對不上;同一步再跑 `[ -d "{剛印出來的路徑}" ]` 確認目錄存在。全技能組裡最常碰到 `JSC_HOME` 沒設的就是這一支:它是有預設值的選擇性項目,在待修項目表上是一列 auto,而把它寫進去正是這支技能要做的修復,所以那種機器本來就是叫用這支技能的常態理由;沒設時那道指令印的是 `/current`、結束碼 0,非空又是絕對路徑,兩項都擋不下來。解不出來既不停這一輪,也不擋那一項修復:寫 `JSC_HOME` 要用的 apply-config.sh、scan-config.sh、build-todo.sh 全掛在外掛根目錄底下,根本不靠 `JSC_HOME`,先把那一項修好、再重解一次 `current` 就能往下走,仍然構不到的(第五步的 wiki 紀錄、轉呼叫出去的修復)記成未修好並寫明原因,不用猜的。整輪只解這一次,之後每一次跨外掛腳本呼叫都填成那個字面絕對路徑,不留 `$JSC_HOME` 也不留波浪號,也不留裸的相對路徑——相對路徑會對著操作者的專案目錄解,那裡從來不是外掛根目錄。技能自己的 `tools/` 與 `templates/` 一律不走 `current`,即使那裡擺著 `jsc-cli` 那一條也一樣,理由有兩條:一是上面那條,要修的機器往往正是 `JSC_HOME` 壞掉的機器,修復工具走連結農場,就會被它們存在的理由本身擋住;二是這一支會拿舊腳本去寫檔,第三步把每個落後的 domain 交給 jsc-cli:deploy,正因為這台機器的版本可能落後,而連結農場受同一份落後管轄,從那裡拿 apply-config.sh,等於拿這台機器剛被判定已經跟不上的工具去修它,而且結果是寫進 rc 檔,不只是印在畫面上——舊腳本會照它那一版的鍵集重寫每個 rc 檔的 `# jsc-config` 區塊,把舊的當成正確版本備份起來,第四步再用同一份舊的 show 重驗,當然對得上,於是整輪報成已修。根目錄取自 CLI 載入這支技能時講明的外掛基底目錄,原樣當字面絕對路徑用,一個指令都不跑,寫成 `{外掛根目錄}/tools/{腳本}` 與 `{外掛根目錄}/templates/{檔案}`;那個基底目錄帶版本號,這幾次呼叫都會跳權限詢問,這一支有操作者在現場(第二步逐項問到答案才寫)所以按得掉,無人值守的技能不得照抄,叫用文字沒講明基底目錄就在寫任何東西之前回報外掛根目錄不明並停手,不猜前綴。接著用程式取短主機名與登入帳號算出 HASH,從 wiki-repo CHECK 解出的存取庫讀 CHECK_{HASH} 的待修項目表,讀不到就以 sub agent 同時重跑設定、接線、版本三個檢查器再用 build-todo.sh 合併、依 jsc-ask 決策樹逐項循序確認、依 fix 欄分流(auto 與 ask 走 apply-config.sh 的 set 或 mkdir、manual 印出步驟交給操作者、domain 落後轉呼叫 jsc-cli:deploy 並附上手上的版本報告、hook 未接線轉呼叫 jsc-hooks:hooks-install、缺 model-tags.tsv 轉呼叫 jsc-cli:models)、同時重驗每個已套用項目、重寫 CHECK_{HASH}、再用 wiki-url 取它的絕對網址、把要寫進兩頁的每個連結交給 jsc-gitea/tools/link-check.sh 驗證且只有結束碼 0 才往下寫、「體檢頁」那一條的連結寫成 `[CHECK_{HASH}]({絕對網址})`、以 wiki-contents.sh upsert CHECK 1 CHECK_{HASH} 用 H2 標題也就是體檢頁頁名當鍵,更新 CONTENTS 存取庫的 CHECK_CONTENTS 上本機那一個區塊,區塊檔是 `## CHECK_{HASH}` 那一行、一個空行,再照 templates/check-contents.md 的欄位順序每欄一條 `- {欄位名}:{值}`、報出已修、略過、轉呼叫、未修好四項計數,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-cli:setup 寫下這一輪的結果。收尾那一筆走每一條出口,連停在讀不到待修項目那一條也要寫;status 五選一,每一項都修好也重驗過、沒有略過、兩頁都寫成是 ok,環境不允許寫入、apply-config.sh 每一項都回 4 而一個 rc 檔都沒動到是 blocked,已套用的項目重驗仍失敗、apply-config.sh 回 2 是這支技能自己下錯命令、或金鑰失效回 7 與其他 API 失敗回 8 讓紀錄改寫不成是 failed,使用者否決某一項而那一項記成略過、或轉呼叫出去的修正沒收尾是 degraded,使用者在逐項確認到一半喊停、剩下的項目沒問到是 aborted。detail 只放四項計數,不放使用者輸入的值 |
|
||||
| 外部呼叫 | `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,加上同一步的 `[ -d ]` 確認,是整輪唯一容許帶變數的兩個指令;跨外掛腳本一律用它組成的字面絕對路徑呼叫:{current 目錄}/jsc-hooks/tools/wire-cli.sh status(一律帶 JSC_READONLY=1)、{current 目錄}/jsc-hooks/hooks/version-guard.sh report、{current 目錄}/jsc-gitea/tools/gitea.sh 的 wiki-repo、hash-id 與 wiki-url、{current 目錄}/jsc-gitea/tools/link-check.sh、{current 目錄}/jsc-gitea/tools/wiki-contents.sh upsert、{current 目錄}/jsc-hooks/tools/report-status.sh skill-end。技能自己的檔案走外掛根目錄組成的字面絕對路徑:{外掛根目錄}/tools/scan-config.sh、{外掛根目錄}/tools/detect-clis.sh、{外掛根目錄}/tools/build-todo.sh、{外掛根目錄}/tools/apply-config.sh 的 set、mkdir 與 show,比對用的 {外掛根目錄}/tools/config-spec.tsv 與兩份版型 {外掛根目錄}/templates/check-page.md、{外掛根目錄}/templates/check-contents.md 也一樣。另有 jsc-ask:ask、jsc-gitea:wiki、jsc-cli:deploy、jsc-hooks:hooks-install、jsc-cli:models |
|
||||
| 完成條件 | `current` 那個目錄在第一步就解出一條存在的絕對路徑(用 `[ -d ]` 查過,而且沒有再往下解成帶版本號的快取路徑),解不出來就把它記成 `JSC_HOME` 那一項待修並照修,不停手也不擋住其他項;技能自己的 `tools/` 與 `templates/` 也有一條字面絕對的外掛根目錄可用,後續每一支腳本、每一份版型都用這兩條之一組成的字面絕對路徑呼叫。每一項都有已修、略過、轉呼叫或未修好的結果;每個已套用項目都由自己那一列指定的檢查器重驗過;每個寫進去的環境變數都附上 export 那一行;每個要寫進頁面的連結都經 link-check.sh 驗過,結束碼 0 才寫,1 就兩頁都不寫並列出 DEAD 那幾筆,3 回報 GITEA_HOST 仍未修好,7 停下來回報金鑰問題而不判成死連結;連結一律寫成文字加絕對網址的形式,H2 標題本身不放連結;兩頁各自寫好或略過都有回報,wiki-contents.sh 宣告的 0、1、2、3、4、7、8 每一碼都有分流,結束碼 1 是組不出頁面內容或寫入失敗,頁上找不到本機那一個區塊不算錯、腳本改成附加,建不建新頁的判斷留在腳本裡,技能不自己建;四項計數都講出來。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
|
||||
| 可驗證跡象 | 各 shell rc 檔的 `# jsc-config` 區塊被改寫,改寫前的備份落在 $JSC_HOME/backup/config/{時間戳}/;auto 路線建立的目錄實際出現在磁碟上;wiki CHECK_{HASH} 被改寫成修完後的狀態,另一個存取庫的 CHECK_CONTENTS 只有本機那一個 H2 區塊跟著更新,區塊標題是當鍵用的 `## CHECK_{HASH}`,標題上沒有連結也沒有網址,「體檢頁」那一條是 `[CHECK_{HASH}]({絕對網址})` 這種文字加連結的寫法、點下去連得到體檢頁,頁面上找不到同 wiki 的雙括號連結,欄位一律是 `- {欄位名}:{值}` 的條列、頁上沒有 markdown 表格;連結驗不過的那一輪,兩頁都維持上一輪的內容;轉呼叫出去的項目留下各自技能的跡象,也就是 deploy 的重啟狀態檔、hooks-install 改寫的接線設定、models 產生的 model-tags.tsv;$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-cli:setup,status 與 exit 就是這一輪的結果。回報與逐行紀錄裡出現的腳本路徑全是字面絕對路徑,找不到 `$JSC_HOME`、`$` 開頭或波浪號開頭的呼叫,也沒有一支寫成裸的相對路徑,唯一的例外是開頭那一次 `readlink -f "$JSC_HOME/current"` 與同一步的 `[ -d ]` 確認;跨外掛那幾支的路徑中段是 `current`,不是 `cache/jsc/{外掛}/{版本}`,技能自己那四支腳本與兩份版型則一律是外掛根目錄接 `tools/` 或 `templates/`;備份目錄 $JSC_HOME/backup/config/{時間戳}/ 底下那幾份 rc 檔,是由外掛根目錄那一份 apply-config.sh 寫出來的,跟這台機器目前跑的 jsc-cli 版本同一份 |
|
||||
@@ -18,10 +18,13 @@
|
||||
| --- | --- |
|
||||
| claude-fable-5 | reasoning-max, reasoning-high, coding, long-context, vision |
|
||||
| claude-opus-5 | reasoning-max, reasoning-high, coding, long-context, vision |
|
||||
| claude-opus-4.5 | reasoning-max, reasoning-high, coding, long-context, vision |
|
||||
| claude-sonnet-5 | reasoning-high, coding, fast, long-context, vision |
|
||||
| claude-sonnet-4.5 | reasoning-high, coding, long-context, vision |
|
||||
| claude-haiku-4-5 | coding, fast, cheap, long-context, vision |
|
||||
| gpt-5.x / o 系列(codex 預設) | reasoning-max, reasoning-high, coding, long-context, vision |
|
||||
| gpt-5.x-mini / codex-mini | coding, fast, cheap |
|
||||
| gpt-5.4-mini | coding, fast, cheap |
|
||||
| gemini-3-pro | reasoning-max, reasoning-high, coding, long-context, vision |
|
||||
| gemini-3-flash | coding, fast, cheap, long-context, vision |
|
||||
| copilot 內建(gpt/claude 選項) | 依所選底層模型比照上表 |
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: delegate
|
||||
description: Delegate a single bounded task to another installed AI agent CLI as a subagent, with an explicit target CLI, required capability tags, or a forced model. Use when another CLI should do the work and return structured results; not for model inventory, plugin deployment, or tasks that must stay in the current agent.
|
||||
---
|
||||
|
||||
# delegate — hand a task to another CLI
|
||||
|
||||
Use this skill when the work should move to another installed AI agent CLI instead of staying in the current agent.
|
||||
|
||||
Detection and tag filtering are decided in code, not in prose: `jsc-cli/tools/detect-clis.sh` owns the CLI list and `jsc-cli/tools/model-tags.sh` owns the capability table. This skill only says when to call them and what each exit code means.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Bound the goal. Write one sentence for the goal, and a numbered list of acceptance criteria that a reader can mark pass or fail without asking a follow-up question.
|
||||
|
||||
Two or more independent goals → split them and run this skill once per goal. A goal is not bounded enough when it has no acceptance criterion that can be checked from the subagent's returned text alone.
|
||||
|
||||
Done when exactly one goal sentence and at least one pass-or-fail acceptance criterion are written down.
|
||||
|
||||
2. Collect the CLI list and the model list. They read different files and share no state, so **start both at once and wait for both**. Step 3 has no other source for a model id: without the second script there is nothing to hand to `model-tags.sh gate`.
|
||||
|
||||
`jsc-cli/tools/detect-clis.sh` prints `{name}<TAB>{path}<TAB>{version}` per installed CLI.
|
||||
|
||||
| Result | Action |
|
||||
| --- | --- |
|
||||
| exit 0, at least one row | Take the candidate CLIs from those rows only |
|
||||
| exit 0, no row | Stop. Report that no AI agent CLI is installed, and name the five it probes: claude, codex, copilot, antigravity, kiro |
|
||||
| any non-zero exit | Stop. Report the exit code and the stderr text. Never fall back to a guessed CLI list |
|
||||
|
||||
`jsc-cli/tools/list-models.sh` prints `{cli}<TAB>{model}<TAB>{in-use}` per model, read from each CLI's own config, and always exits 0. It stays silent for a CLI whose config it cannot read, so a CLI with no row is a CLI with no known model, not a failure.
|
||||
|
||||
| Result | Action |
|
||||
| --- | --- |
|
||||
| exit 0 | Take the candidate models for the target CLI from that CLI's own rows, the `in-use` row first |
|
||||
| any non-zero exit | The script itself failed. Stop and report the exit code and the stderr text; never invent a model id |
|
||||
|
||||
Done when the candidate CLI list comes from the first TSV and holds at least one name, the model rows from the second are in hand, or the skill has stopped with the reason.
|
||||
|
||||
3. Pick the target CLI, then resolve its model against the requirement. Both read step 2's two TSVs and nothing else, so they belong to one pass over that data.
|
||||
|
||||
**Pick the target CLI first.** The user naming a CLI keeps only that CLI; a name absent from step 2's TSV stops the skill with the message that the CLI is not installed on this machine. No name from the user → keep every detected CLI as a candidate, and settle on one before any model is checked: the candidate model list is defined by the target CLI.
|
||||
|
||||
**Then resolve the model.** Never let a model judge its own tags — the verdict comes from the script's exit code.
|
||||
|
||||
The candidate models are the step 2 `list-models.sh` rows whose first column is the chosen target CLI, checked one at a time in that order, `in-use` first. A user-forced model replaces that list with itself alone. No row for the target CLI and no forced model → stop, and say the fix is to record a model for that CLI through `/jsc-cli:models`; a guessed model id would be gated against a table entry that has nothing to do with the CLI that will actually run the task.
|
||||
|
||||
Requirement stated as an SDLC stage → run `jsc-cli/tools/model-tags.sh gate {stage} {model-id}` per candidate. Exit 2 covers three different faults, so read the output line to tell them apart:
|
||||
|
||||
| Exit | Output | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | `PASS` | Keep the model and stop checking further candidates |
|
||||
| 1 | `FAIL:{tags}` | Drop that model, name the missing tags in the report, and move to the next candidate |
|
||||
| 2 | `UNKNOWN-MODEL` | Drop that model and move to the next candidate. Do not guess its tags. Say the fix is to add it through `/jsc-cli:models` |
|
||||
| 2 | `UNKNOWN-STAGE` | Stop. The stage name is wrong; name the four valid ones: plan, analyze, implement, maintain |
|
||||
| 2 | usage text on stderr, no verdict line | Stop. The call itself is malformed — report it as a defect in this skill, and never read it as a model that failed the requirement |
|
||||
| other | — | Stop. Report the exit code and the stderr text |
|
||||
|
||||
Every candidate exhausted without a `PASS` → stop, and report each candidate with its own verdict.
|
||||
|
||||
Requirement stated as raw capability tags → run `jsc-cli/tools/model-tags.sh model {model-id}`, which always exits 0 and prints the model's tags, or prints nothing when the model is not on the table. Keep the model only when its tags cover every required tag. Empty output gets the same treatment as `UNKNOWN-MODEL` above.
|
||||
|
||||
A forced model goes through the same check. Failing it stops the delegation rather than downgrading the requirement.
|
||||
|
||||
Done when exactly one target CLI is chosen and one model id from that CLI has cleared the requirement — a `PASS` from `gate`, or tags covering every required tag — or the skill has stopped with the reason.
|
||||
|
||||
4. Build the subagent prompt. It carries the goal, the acceptance criteria from step 1, the target CLI, the model id, the minimum context needed, the write scope, and the output contract below.
|
||||
|
||||
The write scope is a list of paths the subagent may write, one per line, each an absolute path or a path relative to the current working directory. An empty list means read-only, and the prompt says so in those words. Anything outside the list is out of scope, including temporary files.
|
||||
|
||||
The output contract is a TSV block, and the prompt states it verbatim:
|
||||
|
||||
| Line | Meaning |
|
||||
| --- | --- |
|
||||
| `result<TAB>{ok\|fail\|needs-input}` | Exactly one, and the last line |
|
||||
| `summary<TAB>{one line}` | Exactly one |
|
||||
| `criterion<TAB>{n}<TAB>{pass\|fail}<TAB>{evidence}` | One per acceptance criterion from step 1 |
|
||||
| `wrote<TAB>{path}` | One per written path, zero when read-only |
|
||||
| `error<TAB>{message}` | One per failure, zero on success |
|
||||
|
||||
Done when the prompt holds all seven parts and the write scope is either a path list or the read-only sentence.
|
||||
|
||||
5. Spawn one subagent for the goal, targeted at the CLI and the model from step 3. One goal, one subagent. Done when the subagent has returned and its exit status has been captured.
|
||||
|
||||
6. Verify the returned result before reporting it. Every check below must pass:
|
||||
|
||||
| Check | Failure handling |
|
||||
| --- | --- |
|
||||
| Exit status is 0 | Non-zero → record a failure carrying the target CLI, the exit code and the stderr text |
|
||||
| Exit status was obtained at all | Not obtainable → treat it as a failure, exactly as a non-zero exit |
|
||||
| Output holds one `result` line and one `summary` line | Missing either → record a failure reading 回傳格式不符 |
|
||||
| One `criterion` line per acceptance criterion, all `pass` | Any `fail` or missing line → report the outcome as failure, naming the criteria |
|
||||
| Every `wrote` path is inside the step 4 write scope | Any path outside → report it as an out-of-scope write and name the path |
|
||||
|
||||
Done when every row above has a verdict, and a failing row has produced a recorded failure.
|
||||
|
||||
7. Report the verified result: the target CLI, the model id, the capability requirement and how it was met, and the outcome. Keep success, failure and 需要使用者補充 in three separate sections, so a partial result is never read as a finished one.
|
||||
|
||||
Done when the CLI, the model, the requirement and the per-criterion verdicts are all in the report, and every failure recorded in step 6 appears in the failure section.
|
||||
|
||||
8. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 or step 3 included. Call
|
||||
|
||||
`jsc-hooks/tools/report-status.sh skill-end jsc-cli:delegate {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome — the subagent's own status, or the `detect-clis.sh` or `model-tags.sh` call that ruled the run — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the target CLI and the model id fit there, the subagent's output does not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.
|
||||
|
||||
| status | When this skill uses it |
|
||||
| --- | --- |
|
||||
| `ok` | The subagent exited 0, its output held one `result` and one `summary` line, every acceptance criterion came back `pass`, and every `wrote` path sat inside the write scope |
|
||||
| `blocked` | The capability gate stopped the delegation before it started, so no subagent ran: every candidate model returned `FAIL` or `UNKNOWN-MODEL` from `model-tags.sh gate`, `detect-clis.sh` exited 0 with no row, the CLI the user named is not on this machine, or the target CLI has no model row and no forced model |
|
||||
| `failed` | The delegation ran and broke: the subagent exited non-zero, its exit status could not be obtained at all, its output was missing the `result` or `summary` line, a criterion came back `fail`, or a `wrote` path landed outside the write scope. `detect-clis.sh` or `list-models.sh` exiting non-zero sits here too |
|
||||
| `degraded` | The subagent returned `result needs-input`: part of the goal is done and the rest waits on the user, so the report has a 需要使用者補充 section that is not empty. The delegation happened, the goal did not close |
|
||||
| `aborted` | The premise did not hold, so the skill stopped on its own: step 1 could give the goal no pass-or-fail acceptance criterion, or the request carried two or more independent goals and has to be split. Also used when the user stops the run before step 5 spawns the subagent |
|
||||
|
||||
Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.
|
||||
|
||||
## Do not use this skill
|
||||
|
||||
- Do not use it for model inventory. That is `/jsc-cli:models`.
|
||||
- Do not use it for plugin deployment. That is `/jsc-cli:deploy`.
|
||||
- Do not use it for tasks that must stay inside the current agent.
|
||||
- Do not use it for a goal that step 1 could not give a pass-or-fail acceptance criterion.
|
||||
+148
-22
@@ -1,32 +1,158 @@
|
||||
---
|
||||
name: deploy
|
||||
description: Batch install, update, or uninstall the whole jsc skill set on every installed AI CLI. Detect CLIs via detect-clis.sh, report every plugin's local-versus-published version first and recommend update when any one of them is behind, then ask the user for the mode via decision tree, then run each CLI's native plugin commands with the unified jsc marketplace (token jsc-{domain}@jsc). Domain list comes from the plugins/meta marketplace.json, never hardcoded. Use for rollout or removal of the jsc plugins; not for a single skill.
|
||||
description: Batch install, update, or uninstall the whole jsc skill set on every installed AI CLI. Detect CLIs, read the version recommendation and the marketplace domain list in parallel, then ask the user for the mode via decision tree unless the caller already passed one, then run each CLI's native plugin commands in parallel with the unified jsc marketplace (token jsc-{domain}@jsc). Domain list comes from the plugins/meta marketplace.json, never hardcoded. After install or update, hand the detected CLI list to jsc-hooks:hooks-install, write this machine's update and remove guides via write-guides.sh, and demand a session restart. Use for rollout or removal of the jsc plugins; not for a single skill.
|
||||
---
|
||||
|
||||
# deploy — batch install, update, or uninstall the skill set
|
||||
|
||||
## Path rule — every script call is a literal absolute path
|
||||
|
||||
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — `$JSC_HOME/...`, `~/.jsc/...`, or anything like them. The permission layer matches paths statically. It never expands a variable or a tilde, so such a path matches no allow rule, and the call falls through to an approval prompt. An unattended round has nobody to approve it. The run then dies at its first script, before it deploys anything.
|
||||
|
||||
Measured on a real machine, not assumed. `$JSC_HOME/current/jsc-assist/tools/patrol.sh` was blocked and never ran. `~/.jsc/current/...` was blocked and never ran. `/root/.jsc/current/...` ran. Adding an allow rule that itself starts with `$JSC_HOME` to `~/.claude/settings.json` changed nothing: the call stayed blocked. A wider allow list is not the fix, because the rule text is matched statically too.
|
||||
|
||||
Portability is no reason to put the variable back. Step 0 resolves the root once, at run time, on whatever machine this runs on — that is where portability comes from. Rewriting `{JSC_ROOT}/jsc-hooks/...` back to `$JSC_HOME/current/jsc-hooks/...` for tidiness re-breaks every unattended round.
|
||||
|
||||
## Step 0 — resolve the two roots, once
|
||||
|
||||
Before step 1, run this one command:
|
||||
|
||||
`readlink -f "$JSC_HOME/current"`
|
||||
|
||||
It prints one absolute directory: the absolute path of the `current` directory itself. Call it `{JSC_ROOT}` for the rest of this document. **Stop at that directory — never resolve one level further.** `current` is an ordinary directory, and the symbolic links are its entries, one per plugin; resolving one of those entries lands on the versioned plugin cache (`/root/.claude/plugins/cache/jsc/jsc-hooks/0.4.2`, say), and a versioned path is exactly the kind no allow rule can hold — a rule with `*` where the version segment goes matches nothing, measured. `{JSC_ROOT}` is the version-free root, and staying at it is the whole point. This is the only place a variable may appear. The shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
|
||||
|
||||
Substitute `{JSC_ROOT}` with that directory in every later call, so what runs is a literal absolute path. `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh` becomes, for example, `/root/.jsc/current/jsc-hooks/hooks/version-guard.sh`.
|
||||
|
||||
Resolve it once, at the start of the run. Do not re-resolve it per call. Do not add a tool that prints it.
|
||||
|
||||
Empty output, a non-zero exit, or a path that is not an existing directory → stop and report that `$JSC_HOME/current` does not resolve. **The third item is the one the first two wave through**, so check it: with `JSC_HOME` unset the command prints `/current` and exits 0 — non-empty, absolute, and nowhere — and every literal path built from it then names a place that is not there. Run `[ -d "{the path just printed}" ]` in the same approved step as the resolve, and treat only an existing directory as a root. Every cross-plugin script this skill calls lives under it.
|
||||
|
||||
### The second root — this skill's own `tools/`
|
||||
|
||||
**Do not reach this skill's own `tools/` through `{JSC_ROOT}`, even when a `jsc-cli` link is sitting there.** Step 4 refreshes the farm for every deployed domain, so after a round has run, `{JSC_ROOT}/jsc-cli` usually exists — but this skill cannot rely on it, because the round that first creates it is this very one. On a machine that has never deployed, and on any machine where the last round's link write came back `fail`, `{JSC_ROOT}/jsc-cli` is absent or stale, and `tools/detect-clis.sh`, `tools/deploy.sh`, `tools/check-requires.sh` and `tools/write-guides.sh` would resolve to nothing or to a superseded copy of themselves. None of the four may be written as a bare relative path either — the rule above wants a literal absolute path at every call site, and a call site with no way to build one is where a prefix gets guessed.
|
||||
|
||||
They sit at `{plugin root}/tools/`, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it `{CLI_ROOT}`, and write all four calls as `{CLI_ROOT}/tools/{script}` — `/root/.claude/plugins/cache/jsc/jsc-cli/0.3.2/tools/deploy.sh`, for example. No command runs for this one, and it is taken once, like `{JSC_ROOT}`.
|
||||
|
||||
That base directory carries a version segment, so no allow rule covers it and each of those four calls raises an approval prompt. **That is acceptable in this skill and in no unattended one**: `deploy` runs with a person in front of it — step 3 asks them for the mode, step 4 rewrites every CLI's plugin set — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop, because a guessed prefix runs some other version's copy of these scripts, or nothing at all.
|
||||
|
||||
Done when `{JSC_ROOT}` holds one existing absolute directory and `{CLI_ROOT}` holds one literal absolute path.
|
||||
|
||||
## Inputs a caller may pass
|
||||
|
||||
`jsc-cli:setup` already confirmed the mode with the user and already holds a fresh version report. Re-asking and re-querying would put a second decision tree in front of someone who just answered it.
|
||||
|
||||
| Input | Effect |
|
||||
| --- | --- |
|
||||
| mode (`install` / `update` / `uninstall`) | Step 3 skips the question and states which caller set the mode |
|
||||
| version report | Step 1 skips its version collector; step 2 shows the report it was handed and names its source |
|
||||
|
||||
Nothing passed in → run every step as written below.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run `tools/detect-clis.sh` to find the installed CLIs and their executable paths.
|
||||
2. **Check versions before asking anything**, so the recommendation is based on fact rather than a guess:
|
||||
1. Run `jsc-hooks/hooks/version-guard.sh report`. It prints one line per installed jsc plugin — `{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}` — and a final `behind<TAB>{落後個數}`.
|
||||
2. Show that table to the user as-is. It is the evidence behind the recommendation, so never summarise it away.
|
||||
3. **`behind` ≥ 1 → mark `update` as the recommended option**, and name every domain that is behind together with its local and remote version. One domain behind is enough; do not wait for a majority.
|
||||
4. `behind` = 0 → recommend nothing; present the three options neutrally.
|
||||
5. `查詢失敗` on any domain → say so explicitly. An unverified domain is not the same as an up-to-date one, and must not be counted as either.
|
||||
3. Ask the user for the mode per the `jsc-ask:ask` rules: `install` / `update` / `uninstall`. Every option states its impact scope: which CLIs it touches and which configs it writes.
|
||||
4. Get the domain list (**never hardcode it**; this skill follows automatically when domains are added or removed): read `plugins[].name` from the unified marketplace via
|
||||
`jsc-gitea/tools/gitea.sh api GET /repos/plugins/meta/raw/.claude-plugin/marketplace.json`.
|
||||
The marketplace is unified as `jsc` (`{MKT}` = `https://gitea.jsc.idv.tw/plugins/meta.git`); the install token is `jsc-{domain}@jsc`.
|
||||
5. Run the matching commands for each CLI (add the marketplace once per CLI, then install per domain). This step **MUST run as a sub agent** (one sub agent per CLI):
|
||||
1. Collect the three facts the rest of the run needs. They are independent, so start all three at once and wait for all three.
|
||||
|
||||
| CLI | install | update | uninstall |
|
||||
| --- | --- | --- | --- |
|
||||
| claude | `claude plugin marketplace add {MKT}`, then per domain `claude plugin install jsc-{domain}@jsc` | `claude plugin marketplace update jsc`, then per domain `claude plugin update jsc-{domain}@jsc` | per domain `claude plugin uninstall jsc-{domain}@jsc`, finally `claude plugin marketplace remove jsc` |
|
||||
| codex | `codex plugin marketplace add {MKT}`, then per domain `codex plugin add jsc-{domain}@jsc` | `codex plugin marketplace upgrade jsc` | per domain `codex plugin remove jsc-{domain}@jsc`, finally `codex plugin marketplace remove jsc` |
|
||||
| copilot | `copilot plugin marketplace add {MKT}`, then per domain `copilot plugin install jsc-{domain}@jsc` | `copilot plugin marketplace update jsc`, then per domain `copilot plugin update jsc-{domain}@jsc` | per domain `copilot plugin uninstall jsc-{domain}@jsc`, finally `copilot plugin marketplace remove jsc` |
|
||||
| antigravity | per domain `git clone https://gitea.jsc.idv.tw/plugins/{domain}.git ~/plugins/{domain}`, then `agy plugin install ~/plugins/{domain}` (agy cannot install from a gitea URL) | `git -C ~/plugins/{domain} pull`, then `agy plugin uninstall jsc-{domain}` and install again | `agy plugin uninstall jsc-{domain}` |
|
||||
| kiro | same plugin commands as copilot (executable `kiro-cli`); if unsupported, copy each repo's `skills/` into the kiro skills directory | pull again, then copy again | delete the matching skills directories |
|
||||
1. **Installed CLIs** — `{CLI_ROOT}/tools/detect-clis.sh`, printing `{name}<TAB>{path}<TAB>{version}`. Exit 0 with at least one row → take the CLI list from it. Exit 0 with no row → stop, and report that none of claude, codex, copilot, antigravity, kiro is installed. Any non-zero exit → stop and report the exit code and stderr; never guess a CLI list.
|
||||
2. **Version evidence and recommendation** — two subcommands of `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh`, both needed, run together: `report` prints the per-plugin rows `{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}` closing with `behind<TAB>{count}`, and `recommend` prints one single line and nothing else — `recommend<TAB>{update|none|unverifiable}`. `recommend` deliberately never reprints the table, so its second column stays readable by `cut`; the version table that steps 2, 3 and 7 show comes from `report`, and the conclusion comes from `recommend`. Skip this collector when the caller passed a version report.
|
||||
3. **Domain list** — read `plugins[].name` from the unified marketplace (**never hardcode it**; this skill then follows automatically when domains are added or removed):
|
||||
`{JSC_ROOT}/jsc-gitea/tools/gitea.sh api GET /repos/plugins/meta/raw/.claude-plugin/marketplace.json`.
|
||||
Exit 0 with at least one `plugins[].name` → use that list. Exit 0 with an empty or unparseable list → stop and report that the marketplace holds no plugin entry. Any non-zero exit → stop and report the exit code and stderr; a partial domain list would install a partial skill set and look successful.
|
||||
|
||||
6. After install or update, call `jsc-hooks:hooks-install` to rewire the hooks.
|
||||
7. Report the result and any failure reason for every CLI × mode.
|
||||
The marketplace is unified as `jsc`; the install token is `jsc-{domain}@jsc`. Each `plugins[].name` already carries the `jsc-` prefix (e.g. `jsc-ask`) — pass it to `{CLI_ROOT}/tools/deploy.sh` as-is, prefixed or not; the script normalizes it.
|
||||
|
||||
Done when the CLI list holds at least one CLI, the domain list holds at least one name, and the recommendation is either in hand or explicitly inherited from the caller.
|
||||
|
||||
2. Show the `report` version table to the user as-is. It is the evidence behind the recommendation, so never summarise it away. A report handed in by a caller is shown the same way, with a line naming that caller as its source.
|
||||
|
||||
Branch on the `recommend` line:
|
||||
|
||||
| Exit | `recommend` value | What it means and what to say |
|
||||
| --- | --- | --- |
|
||||
| 0 | `update` | At least one domain is behind. Mark `update` as the recommended option in step 3, and name every behind domain with its local and remote version. One domain behind is enough; do not wait for a majority |
|
||||
| 0 | `none` | Every domain row is `最新` or `超前`. Recommend nothing; present the three options neutrally |
|
||||
| 0 | `unverifiable` | The version check could not run — no local plugin registry, or every remote lookup failed. Say so plainly and base no recommendation on it. Unverified is not the same as up to date |
|
||||
| 0 | no `recommend` line in the output at all | This jsc-hooks build has no `recommend` subcommand — an older `version-guard.sh` treats the argument as a hook invocation and exits 0 without printing anything. Derive the same three values yourself from the `report` table already collected in step 1.2: any `落後` row → `update`; no `{domain}` row at all, or a `noregistry` line → `unverifiable`; otherwise `none`. Say in step 7's report that the recommendation came from this fallback path, not from `recommend` |
|
||||
| non-zero | — | Report the version check as `unverifiable` with the exit code and stderr, and carry on to step 3 without a recommendation |
|
||||
|
||||
Any single domain row reading `查詢失敗` is called out by name even when the overall value is `none`. An unverified domain is not the same as an up-to-date one, and must not be counted as either.
|
||||
|
||||
Done when the table is on screen and the recommendation is stated as exactly one of `update`, `none` or `unverifiable`.
|
||||
|
||||
3. Ask the user for the mode per the `jsc-ask:ask` rules: `install` / `update` / `uninstall`. Every option states its impact scope: which CLIs it touches and which configs it writes. A caller that already passed a mode skips this step, and the report names that caller instead.
|
||||
|
||||
Done when the user has named exactly one of `install`, `update` or `uninstall`, or the inherited mode is named with its source.
|
||||
|
||||
4. Run `{CLI_ROOT}/tools/deploy.sh {mode} {cli} {domain}...` once per detected CLI, passing the whole domain list in one call so the marketplace command runs only once. This step **MUST run as a sub agent**, one sub agent per CLI, and **all of them start together** — the CLIs write to separate plugin directories, so serialising them only adds up their install times.
|
||||
|
||||
The script prints `cmd` and `exit` lines for every command, one `requires` line before each domain update, optional `compat` lines for Codex cache links, `link` lines for the `current` link farm (see 4a), and one `result` line at the end; `-n` prints the commands without running them.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Every command for that CLI succeeded. Record its `result` line |
|
||||
| 1 | At least one command failed. Record that CLI as failed and quote every `exit` line whose code is non-zero |
|
||||
| 2 | Usage error — the mode, the CLI name or the domain list is wrong. Report it as a defect in this skill, and do not retry with a guessed argument |
|
||||
| other | Record that CLI as failed with the exit code and stderr |
|
||||
|
||||
On update, `{CLI_ROOT}/tools/check-requires.sh {cli} {manifest}` checks each domain's `jsc.requires` before that domain is updated. Exit 0 updates the domain as usual. Exit 1 — a missing or too-old required jsc plugin — prints a `warn` line and the domain **is still updated**: skipping it would leave a behind domain permanently unable to reach the version its dependency needs. The block lives one layer up, at skill invocation time, where `jsc-hooks/hooks/version-guard.sh` stops that domain's skills. **That last mention is a description of where the block happens, not a call this skill makes** — nothing here runs `version-guard.sh` except step 1.2, which is written as `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh`, so do not read it as a call site that was left un-prefixed. Exit 4 — the manifest is unreadable, is not valid JSON, or python3 is missing — prints a `note` line and also still updates the domain: no verdict is not the same fact as behind, so it gets its own line rather than a `warn` that would send the operator hunting for a version problem that is not there. Exit 2 or any other code — a `check-requires.sh` usage error or a broken script — prints a `skip` line and leaves that domain untouched, because a checker that failed outright is not a pass. Codex update preserves old `jsc-cli` and `jsc-hooks` cache version paths as symlinks to the newest installed version, so a still-running Codex deploy can keep using its helper scripts and a still-running Codex session whose hook_run_id points at the old cache can finish without `No such file`. Antigravity cannot install from a Gitea URL, so the script clones each domain into the local plugin directory (`JSC_LOCAL_PLUGINS`, default `$JSC_HOME/plugins`) and installs from that path — keep that clone, because update pulls the same one. That default deliberately avoids a development checkout: when the directory holds uncommitted changes or unpushed commits, the script prints a `skip` line, leaves the tree untouched, and installs the on-disk content.
|
||||
|
||||
**That one clone is shared by all five CLIs, and this step runs them in parallel, so the script takes a per-domain lock around its `git pull` and a `warn` line is what a contended or failed pull looks like.** Every CLI reaches that clone: the four that install from it, and `check-requires.sh`, which reads each domain's manifest there. Measured: two CLIs collided inside one round, one could not create `ORIG_HEAD.lock` and the other could not move its remote refs, and **both runs came back `fail` while every plugin had in fact installed** — a report saying failure over a machine that succeeded, for a reason that has nothing to do with deploying. So a pull that cannot get the lock within thirty seconds, or that exits non-zero on a clone already on disk, prints `warn` and **does not fail the run**: the content is there, it may simply be older than the remote. Read every such `warn` line into the report as "this CLI installed what was already on disk" — it is the one line between that and a silent install of a stale version. A `git clone` that fails is different and still fails the run: nothing is on disk to install.
|
||||
|
||||
**The `link` lines say where `{JSC_ROOT}` now points, and they are the reason step 0's literal paths keep working.** `{JSC_ROOT}` is a farm of version-free symbolic links, one per plugin, each pointing at that plugin's versioned directory in a CLI's plugin cache. Every literal absolute path this skill builds rests on it, so a link left on an old version silently runs an old script — and a script that old version never shipped is simply absent, which stops a heartbeat without printing anything. `deploy.sh` refreshes the whole farm at the end of its run: install and update repoint every link at the version just installed, uninstall clears the ones whose target is gone.
|
||||
|
||||
Only one CLI's run touches the farm. The machine has one farm and five CLIs hold five copies, so the script picks the first of claude, codex, copilot, kiro that is installed and lets only that run write; every other CLI prints one `link<TAB>-<TAB>skip` line naming the base CLI, which is how a deliberate skip is told apart from a farm nobody refreshed. Read the base CLI's sub agent output for the real result. Each line is `link<TAB>{domain}<TAB>{status}<TAB>{link path}<TAB>{target or reason}`.
|
||||
|
||||
| `link` status | What it means and what to do |
|
||||
| --- | --- |
|
||||
| `ok` | The link now points at column 5, the version directory this round installed. Carry the target into step 7 for at least the plugins this skill itself reaches through `{JSC_ROOT}` |
|
||||
| `removed` | Uninstall left the target gone, so the dangling link was cleared. Nothing to do |
|
||||
| `skip` with a domain in column 2 | Something that is not a symbolic link already sits at that path, and the script deliberately refused to overwrite it. **Report it as an operator action** — until it is cleared by hand, that plugin's documented path keeps resolving to whatever is sitting there |
|
||||
| `skip` with `-` in column 2 | This run is not the base CLI, or no base CLI is installed at all. The second case means no documented path got refreshed this round, so say so |
|
||||
| `fail` | The version directory could not be found, or the link could not be written. The deploy still stands, but that plugin's documented path may still be on an old version. Name the plugin and the reason, and judge the round `degraded` |
|
||||
| `dryrun` | `-n` only: column 5 is where the link would point. Nothing was written |
|
||||
|
||||
A failed link never fails the deploy. By the time the farm is refreshed the plugins are installed and working, and marking the round failed would skip the restart gate and the `result` line too, handing the operator a fully deployed machine described as a failure. The stale link is a real defect, but its remedy is a line the operator can see and act on.
|
||||
|
||||
Done when every detected CLI has reported an exit code and a `result` line, every skipped domain has a checker-failure reason or a local-tree reason, every `warn` domain is named with either the version it still has to catch up to or the reason its local clone was not refreshed this round, and every `link` line has been read — with each `ok` target in hand for step 7 and each `fail` or domain-level `skip` named as an operator action.
|
||||
|
||||
5. After install or update, call `jsc-hooks:hooks-install` and **hand it the CLI list from step 1.1**, so it does not probe the same five executables a second time. `hooks-install` still detects for itself when it receives no list — that fallback is what keeps it usable on its own.
|
||||
|
||||
Take its aggregate result rather than re-reading each CLI's smoke detail; the installer already judged purge, wiring, smoke and scan per CLI, and refreshes `{JSC_ROOT}/jsc-hooks` on the way — the path `deploy.sh` follows to reach `restart-gate.sh`. Keep exactly one extra judgement here, because it is a deploy-side fact the installer does not rule on: **a smoke result containing `No such file` is a failed update**, since it means a rewritten hook path cannot execute. Report it and do not let the deploy finish as successful.
|
||||
|
||||
Done when hooks-install has returned an aggregate verdict for every CLI in the list, and every `No such file` in it is reported as an update failure.
|
||||
|
||||
6. After install or update, run `{CLI_ROOT}/tools/write-guides.sh {mode} {domain}...` **once for the whole machine**, after every CLI in step 4 has finished. It rewrites `$JSC_HOME/update-guide.md` and `$JSC_HOME/remove-guide.md` from the live detection result, so the later update and removal runs have the real commands for this machine. Skip it for `uninstall`: the guides describe an installed skill set.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Both `wrote` lines printed. Name both paths in step 7 |
|
||||
| 2 | Usage error — the mode or the domain list is wrong. Report it as a defect in this skill; the deploy itself still stands |
|
||||
| 4 | `$JSC_HOME` or one of the two files could not be written. Name the path and the stderr, and say the machine has no up-to-date guide until this is fixed |
|
||||
| other | Report the guide write as failed with the exit code, and say which of the two files did print a `wrote` line |
|
||||
|
||||
Done when both `wrote` lines are printed, or the failure is reported with the exit code and the paths involved.
|
||||
|
||||
7. Report the run and close it, in one block. The result and any failure reason for every CLI × mode, plus every `skip` line, every `warn` line, every Codex `compat` line, every `link` line that is not a plain base-CLI skip, and every CLI that could not be version-checked in step 2.
|
||||
|
||||
State the farm explicitly: name `{JSC_ROOT}` and, per plugin, the version directory its link now points at, taken from the `ok` targets. That one list is what lets the next round's operator check by eye that a documented path leads to the version just deployed, instead of finding out through a heartbeat that quietly stopped.
|
||||
|
||||
For install or update, the same block ends with the restart instruction, in these words: 「請關閉目前的工作階段並重新啟動,新的技能內容才會載入」. `deploy.sh` recorded this round in `$JSC_HOME/restart-required.d/{cli}` — one file per CLI — and prints its path on a `restart` line. **It writes that file on every install and update, including a run it judged `fail`**: a partial failure means part of what is on disk changed, which makes a restart more necessary, not less. It used to write it only on success, and one round proved the cost — an unrelated `git pull` failure turned the run into a `fail`, that branch never reached the gate, and the one CLI running the freshly replaced code was the only one never told to restart. Report a `restart` line missing from a `fail` run as a defect in that script rather than raising the gate by hand; `jsc-hooks` reads only that CLI's own file and keeps reminding until that CLI restarts, with `JSC_RESTART_GATE=off` as the escape hatch. Restarting one CLI clears its own file and leaves the others' gates standing. Name the two guide paths from step 6 in that same closing block, so the operator knows where this machine's update and removal commands now live.
|
||||
|
||||
Done when every detected CLI appears in the report with its `result` status, every plugin's refreshed link target is named, and — for install or update — the restart instruction is printed with both guide paths named, or step 6's failure is repeated in their place.
|
||||
|
||||
8. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call
|
||||
|
||||
`{JSC_ROOT}/jsc-hooks/tools/report-status.sh skill-end jsc-cli:deploy {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome — the worst `deploy.sh` exit of the run, or the collector that stopped step 1 — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the mode and the per-CLI counts fit there, the `cmd` and `exit` lines do not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.
|
||||
|
||||
Record it after step 7's block, never instead of it. The event stream carries one status; the operator still needs the per-CLI report on screen.
|
||||
|
||||
| status | When this skill uses it |
|
||||
| --- | --- |
|
||||
| `ok` | Every detected CLI's `deploy.sh` exited 0, hooks-install returned a clean verdict for each of them with no `No such file` in any smoke result, both guides printed their `wrote` line, and the base CLI's `link` lines are all `ok` or `removed` |
|
||||
| `blocked` | Nothing was deployed because there was nothing to deploy to: `detect-clis.sh` exited 0 with no row, so none of claude, codex, copilot, antigravity, kiro is installed and the run stops before any plugin command |
|
||||
| `failed` | The run broke: the marketplace read in step 1.3 exited non-zero or returned no plugin entry, or every detected CLI's `deploy.sh` came back non-zero. Also used when a smoke result carries `No such file`, which this skill judges as a failed update even when hooks-install did not |
|
||||
| `degraded` | The deploy landed on part of the machine only: some CLIs exited 0 while others failed, a domain was left untouched by a `skip` line from `check-requires.sh`, `write-guides.sh` exited 4 so the plugins are installed but this machine has no up-to-date update and remove guide, or a `link` line came back `fail` or skipped a plugin whose link path is not a symbolic link — the plugins are installed, but a documented path may still lead to the old version |
|
||||
| `aborted` | The user chose none of `install`, `update` or `uninstall` at step 3, or stopped the run before step 4 launched the first CLI, so no plugin command ran |
|
||||
|
||||
Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
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, settings from tools/scan-config.sh against tools/config-spec.tsv, and orphan variables. Report one findings table per check, build the 待修項目 table with tools/build-todo.sh, then write the whole run to wiki CHECK_{HASH} where HASH comes from {short hostname}/{user}; the page keeps only the latest run, while its CHECK_CONTENTS block is upserted through wiki-contents.sh into the contents repo. 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.
|
||||
|
||||
The read-only contract is enforced in code, not by prose: every `{JSC_ROOT}/jsc-hooks/tools/wire-cli.sh` call in this skill runs with `JSC_READONLY=1` in its environment. A mistyped subcommand then refuses to write instead of rewiring the machine that was only supposed to be measured.
|
||||
|
||||
## Path rule — every script call is a literal absolute path
|
||||
|
||||
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — `$JSC_HOME/...`, `~/.jsc/...`, or anything like them — and never a bare relative one either. The permission layer matches paths statically: it expands no variable and no tilde, so such a path matches no allow rule and the call falls through to an approval prompt. A bare relative path is the worse form, because it resolves against whatever directory the CLI happens to be in — and this skill deliberately runs from the operator's project directory, which is never a plugin root — so every bare call site is a place where a prefix gets guessed.
|
||||
|
||||
A guessed prefix is the specific danger here, and it is quiet. This skill measures a machine and writes what it found onto a page other people act on, so a call that reaches the wrong copy of a script does not fail — it reports. An old `tools/scan-config.sh` reached through some other version's directory grades this machine against that version's `tools/config-spec.tsv`, and every row it prints looks exactly like a real finding. A checkup that cannot say which script produced its rows is worth less than no checkup, because nobody doubts it.
|
||||
|
||||
Portability is no reason to put the variable back. Step 0 resolves the roots once, at run time, on whatever machine this runs on — that is where portability comes from.
|
||||
|
||||
## Step 0 — resolve the two roots, once
|
||||
|
||||
Before step 1, run this one command:
|
||||
|
||||
`readlink -f "$JSC_HOME/current"`
|
||||
|
||||
It prints one absolute directory: the `current` directory itself, a farm of version-free symbolic links with one entry per plugin. Call it `{JSC_ROOT}` for the rest of this document. **Stop at that directory — never resolve one level further.** Resolving one of those entries lands on the versioned plugin cache (`/root/.claude/plugins/cache/jsc/jsc-hooks/0.4.2`, say), and a versioned path is exactly the kind no allow rule can hold: a rule with `*` where the version segment goes matches nothing, measured. `{JSC_ROOT}` is the version-free root, and staying at it is the whole point. This is the only place a variable may appear; the shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
|
||||
|
||||
Confirm the directory exists, in the same approved step: `[ -d "{the path just printed}" ]`. **That is the check the other two wave through.** `JSC_HOME` is an optional variable with a default, so an unset one is an ordinary machine state rather than an exotic one — it has its own row in `{CLI_ROOT}/tools/config-spec.tsv`, and finding it unset is a normal outcome of this very checkup. With it unset the command prints `/current` and exits 0 — non-empty, absolute, and nowhere — and every literal path built from it then names a place that is not there.
|
||||
|
||||
**Unlike `jsc-cli:deploy`, an unresolvable `{JSC_ROOT}` never stops this run.** It is a finding, and findings are what this skill produces: report `JSC_HOME` in the 全域設定 block and at the top of the 待修項目 table. Mark every check that needed a cross-plugin script as 無法驗證 with that as its reason, and let the checks that do not need one finish. A read-only checkup that aborts tells the operator nothing, and the machine most in need of a checkup is the machine whose paths do not resolve.
|
||||
|
||||
Substitute `{JSC_ROOT}` in every cross-plugin call, so what runs is a literal absolute path. `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh` becomes, for example, `/root/.jsc/current/jsc-hooks/hooks/version-guard.sh`. Resolve it once, at the start of the run. Do not re-resolve it per call, and do not add a tool that prints it.
|
||||
|
||||
### The second root — this skill's own `tools/` and `templates/`
|
||||
|
||||
**Do not reach this skill's own files through `{JSC_ROOT}`, even though a `jsc-cli` link is normally sitting there.** `jsc-cli:deploy` refreshes the whole farm at the end of every round, so on a machine that has deployed, that link exists. This skill still may not lean on it, for a reason of its own: **doctor is the skill that gets run when the machine is suspect.** Its own triggers say so — after an install or update, after a skill failed on a settings or wiring problem, before a handover. A deploy that ended `degraded` is one whose `link` lines came back `fail`, or `skip` on a path holding something that is not a symbolic link at all, leaving a plugin's documented path on an old version — and doctor is what runs next. Reached through that link, `tools/scan-config.sh` and its `tools/config-spec.tsv` come from a version this machine is not running, and the findings table then describes a machine that does not exist. Nothing errors: an old scanner runs perfectly well.
|
||||
|
||||
The second root is free of that. `tools/scan-config.sh`, `tools/detect-clis.sh` and `tools/build-todo.sh` reached through it keep working on a machine whose farm is stale, broken, or never built — which is precisely the machine being measured — so `JSC_HOME` itself still gets scanned and still gets reported.
|
||||
|
||||
They sit at `{plugin root}/tools/` and `{plugin root}/templates/`, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it `{CLI_ROOT}`, and write every own-plugin path as `{CLI_ROOT}/tools/{script}` or `{CLI_ROOT}/templates/{file}` — `/root/.claude/plugins/cache/jsc/jsc-cli/0.3.3/tools/scan-config.sh`, for example. No command runs for this one, and it is taken once, like `{JSC_ROOT}`.
|
||||
|
||||
That base directory carries a version segment, so no allow rule covers it and each of those calls raises an approval prompt. **That is acceptable in this skill and in no unattended one**: doctor runs with the operator in front of it — the five blocks and the 待修項目 table are printed for a person to read and act on — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop, because a guessed prefix reports some other version's idea of this machine as if it were this machine.
|
||||
|
||||
Done when `{JSC_ROOT}` holds one existing absolute directory or its failure is recorded as a `JSC_HOME` finding, and `{CLI_ROOT}` holds one literal absolute path.
|
||||
|
||||
## 1. Collect, four checks in parallel
|
||||
|
||||
Start all four collectors at once. They share no data, so serialising them only makes the check four times slower. Each one **MUST run as a sub agent**, and each returns its raw output lines unchanged — no summarising inside the sub agent, because step 2.2 parses those lines.
|
||||
|
||||
Done when all four sub agents have returned, and each has either its raw lines or an explicit failure reason.
|
||||
|
||||
### 1.1 Skill versions
|
||||
|
||||
Run `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh report`. It prints `{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}` per plugin, then `behind<TAB>{count}`.
|
||||
|
||||
A report with no `{domain}` row, or one carrying `noregistry<TAB>{path}`, means this CLI has no local plugin registry. Report it as 無法驗證 — never as 最新. `behind<TAB>0` proves nothing when no domain row precedes it.
|
||||
|
||||
`report` only reads, so it exits 0 even when every lookup fails. Any non-zero exit means the script itself could not run: report the whole version check as 無法驗證 together with the exit code, and let the other three checks finish.
|
||||
|
||||
Done when every installed domain has a status literal, or the check is reported as unverifiable with its reason.
|
||||
|
||||
### 1.2 Hook wiring
|
||||
|
||||
First run `{CLI_ROOT}/tools/detect-clis.sh`. Exit 0 with at least one row → those are the CLIs to check. Exit 0 with no row → report the wiring table as empty and say no AI agent CLI was detected; that is a finding, not a pass. Any non-zero exit → report the wiring check as 無法驗證 with the exit code and stderr.
|
||||
|
||||
Then run `JSC_READONLY=1 {JSC_ROOT}/jsc-hooks/tools/wire-cli.sh status {cli}` for every detected CLI. 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 | Meaning | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | wired | Record as wired |
|
||||
| 1 | degraded | Record as degraded and quote the `reason` text |
|
||||
| 2 | usage error | Stop this CLI's wiring check. The subcommand or the CLI name is wrong — report it as a defect in this skill, not as a machine fault |
|
||||
| 3 | skipped | The CLI is not installed. Drop it from the wiring table |
|
||||
| 5 | unwired | Record every `item` line whose state is `missing` |
|
||||
| other | unexpected | Report that CLI's wiring as 無法驗證 with the exit code and stderr. Never read it as wired |
|
||||
|
||||
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 carries one of those verdicts and its missing items are listed.
|
||||
|
||||
### 1.3 Settings, global and project in one scan
|
||||
|
||||
Run `{CLI_ROOT}/tools/scan-config.sh scan all` from the current working directory. One call covers both scopes: the spec table is read once and the `scope` column separates the rows. It prints `{項目}<TAB>{範圍}<TAB>{必要}<TAB>{現況}<TAB>{說明}<TAB>{修法}<TAB>{判定}`, closing with `summary<TAB>{missing}<TAB>{invalid}<TAB>{unset}<TAB>{skipped}`.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Scan finished. Split the rows by the `範圍` column into a global table and a project table |
|
||||
| 2 | Usage error. Report it as a defect in this skill and skip the settings check |
|
||||
| 3 | The spec table is missing. Name the path it looked for and `JSC_CONFIG_SPEC`, then skip the settings check |
|
||||
| other | Report the settings check as 無法驗證 with the exit code and stderr |
|
||||
|
||||
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.
|
||||
|
||||
Done when the summary line is read, the rows are split into the two scopes, and every `missing` and `invalid` row is named.
|
||||
|
||||
### 1.4 Orphan variables
|
||||
|
||||
Run `{CLI_ROOT}/tools/scan-config.sh orphans` — variables used in the source but absent from the spec table. Same exit codes as 1.3; exit 3 here also covers a missing plugins root, so name `JSC_PLUGINS_ROOT` in that case.
|
||||
|
||||
They are a maintenance note for the skill set, not a fault on this machine, so they never enter the 待修項目 table.
|
||||
|
||||
Done when the orphan list is returned or the check is reported as skipped with its reason.
|
||||
|
||||
## 2. Report the five blocks, then build the 待修項目 table
|
||||
|
||||
### 2.1 The five blocks
|
||||
|
||||
Report all five blocks per `{CLI_ROOT}/templates/check-page.md`: 技能版本 from 1.1, Hook 接線 from 1.2, 全域設定 and 自我設定 from 1.3's two scopes, and 未登錄變數 from 1.4. Step 1.4 is the only place the orphan list is collected, so leaving it out here drops it from the run entirely — it never enters the 待修項目 table of 2.2, which is exactly why it needs its own block.
|
||||
|
||||
The project rows carry the working directory in their heading. A project-scope result is meaningless without it, because the answer changes with every `cd` — so name the directory that step 1.3 scanned, even when the project table is empty.
|
||||
|
||||
When `.env` or `.envrc` exists in that directory, 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.
|
||||
|
||||
### 2.2 The 待修項目 table
|
||||
|
||||
Save each collector's raw lines to a file, then run
|
||||
|
||||
`{CLI_ROOT}/tools/build-todo.sh --config {scan-all 輸出} --wiring {cli}={status 輸出} --version {report 輸出}`
|
||||
|
||||
one `--wiring` per detected CLI. The script merges the three sources and orders them `missing` → `invalid` → `unwired` → `落後`. Nothing wrong → it prints one row whose class reads 無.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Render the `todo` rows as the 待修項目 table and the `summary` line as 3.2's counts |
|
||||
| 2 | Usage error. Report it as a defect in this skill; fall back to no 待修項目 table and say the merge did not run |
|
||||
| 3 | An input file was unreadable. Name the file, rerun that one collector, and say so when it still fails |
|
||||
| other | Report the merge as failed with the exit code, and keep 2.1's tables on screen |
|
||||
|
||||
A check that step 1 reported as 無法驗證 contributes no rows. Say that in the report: an unverified check and a clean check look identical in this table, and only the sentence tells them apart.
|
||||
|
||||
Done when all five blocks of 2.1 are on screen with the scanned directory stated — 未登錄變數 included, showing either its rows or the reason 1.4 gave for skipping — and 2.2's 待修項目 table is on screen with its rows in that order, or 2.2's merge failure is reported with its reason while 2.1's blocks stay on screen.
|
||||
|
||||
## 3. Record, then hand off
|
||||
|
||||
### 3.1 Record
|
||||
|
||||
This run writes two pages, and they live in **two different wiki repos**. Resolve each repo on its own and never reuse one for the other.
|
||||
|
||||
**The HASH — take it from the machine, do not compose it by hand.** `jsc-assist`'s `MONITOR_{HASH}` claims the same hash source and takes it in code, so the two pages only ever line up when this skill takes it the same way:
|
||||
|
||||
```sh
|
||||
host=$(hostname 2>/dev/null || uname -n 2>/dev/null || printf 'unknown'); host=${host%%.*}
|
||||
user=${USER:-$(id -un 2>/dev/null || printf 'unknown')}
|
||||
```
|
||||
|
||||
`${host%%.*}` is the point of the snippet: `hostname` prints the FQDN on some machines and the short name on others, so a hand-written value gives one machine two pages that never merge again. Pass `"{host}/{user}"` to `{JSC_ROOT}/jsc-gitea/tools/gitea.sh hash-id` and use exactly what it prints. It is 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.
|
||||
|
||||
**`CHECK_{HASH}` — content page.** Repo: `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CHECK`. Write it through `jsc-gitea:wiki` and overwrite the whole page, because this page type keeps only the latest run of this one machine. Whole-page overwrite is correct here and forbidden on the page below.
|
||||
|
||||
**`CHECK_CONTENTS` — contents page, in the contents repo.** Repo: `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CONTENTS`. Every contents page lives there now; it never falls back to `JSC_WIKI_REPO_CHECK`. It is an H1, a `>` preamble and one H2 block per machine — no markdown table anywhere on it. Do not hand-edit it — write the block with
|
||||
|
||||
`{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} {CLI_ROOT}/templates/check-contents.md`
|
||||
|
||||
The block file holds the whole H2 block: the `## CHECK_{HASH}` line, a blank line, then one bullet per field in the order `{CLI_ROOT}/templates/check-contents.md` lists them, written as `- {欄位名}:{值}` with a full-width colon. Every field of the template gets a bullet, `HASH` included — the heading is the key, and a field only in the heading is a field the next reader cannot read.
|
||||
|
||||
The script reads the page back, replaces this machine's block or appends it, then writes the whole page. That keeps the rule in one place: one block per run, never a whole-page overwrite, never another machine's block — those blocks are other people's records, and this run read them from nowhere else.
|
||||
|
||||
**The key is the H2 heading, `CHECK_{HASH}`.** It is the name of the content page this block points at: the literal `CHECK_` plus exactly what `hash-id` printed — 40 uppercase hex characters, not shortened, not otherwise prefixed, not wrapped in a link, no date appended. The script compares the whole heading text after trimming, so the key and that heading must match character for character.
|
||||
|
||||
The page name is the key because it is the one value that does not move. It is decided by `{host}/{user}` alone, so it survives a changed `GITEA_HOST`, a `JSC_WIKI_REPO_CHECK` moved to another repo, and a different Gitea encoding of the page name — all of which change the URL. Key on anything holding a URL and the comparison never matches, so every run appends a second block for the same machine — silently, because the page still looks right.
|
||||
|
||||
`1` is `<key-col>`, and it only matters while a page is still the old markdown table: it is the 1-based index of the column that held the identity, the `[CHECK_{HASH}]({URL})` cell in column 1, whose text becomes the H2 heading when the script converts that table to blocks. On a page already in block form the script ignores it.
|
||||
|
||||
The `體檢頁` bullet stays the human-facing link and is never the key; the heading itself carries no link and no URL. Write the bullet as `[CHECK_{HASH}]({absolute URL})` — text plus link, the one link form this skill set uses. The URL comes from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url {CHECK repo} CHECK_{HASH}` and is never composed by hand. Fetch it after `CHECK_{HASH}` is written: `wiki-url` exits 4 on a page that does not exist yet.
|
||||
|
||||
A same-wiki link form resolves only inside its own wiki, and the two pages are no longer in the same one. It fails without an error, reading on screen as plain text or a dead link, so nobody finds it and nobody fixes it.
|
||||
|
||||
**Verify every link before writing it.** Collect every URL heading into `CHECK_{HASH}` or into the `CHECK_CONTENTS` block, then hand the whole list to `{JSC_ROOT}/jsc-gitea/tools/link-check.sh`. It prints `{OK|DEAD|SKIP}<TAB>{URL}<TAB>{reason}` per line. Only exit 0 may be written.
|
||||
|
||||
| Exit | Meaning | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | Every link answers | Write the page |
|
||||
| 1 | At least one link is unreachable | Write nothing, on either page. Report the `DEAD` lines to the caller |
|
||||
| 2 | Usage error: no URL was given | Report it as a defect in this skill and pass the URLs |
|
||||
| 3 | The list holds a Gitea URL but `GITEA_HOST` is unset | Skip both writes and put `GITEA_HOST` at the top of 待修項目. Never write without verifying |
|
||||
| 7 | Gitea authentication failed | Stop and report the key problem. This is not a dead link |
|
||||
|
||||
Exit 7 stays apart from exit 1 on purpose: with a dead key, Gitea's answer for a private repo looks the same as "page absent". Merge the two and one expired key marks every live page dead, and the blocks pointing at them get rewritten or dropped.
|
||||
|
||||
The script asks the API and never reads a web status code. A private repo's web URL answers 404 to a request with no credentials, so status codes turn good links into dead ones.
|
||||
|
||||
| Exit | Meaning | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | `updated` or `added` | Report which one it printed, with the repo and page it named |
|
||||
| 1 | The page content could not be built, or the write failed | Nothing landed. Report it with the stderr, and keep 2.1's tables on screen. A page with no block to replace is not this case: the script appends instead |
|
||||
| 2 | Usage error | Report it as a defect in this skill. Do not retry with guessed arguments. A `{CLI_ROOT}/templates/check-contents.md` that is not on disk also lands here — then name the path the script looked for, confirm the plugin install is complete, and rerun |
|
||||
| 3 | No contents repo configured | Skip this write and put `JSC_WIKI_REPO_CONTENTS` at the top of 待修項目 |
|
||||
| 4 | Page absent and no template given | Unreachable the way this skill calls the script — the command above always passes `{CLI_ROOT}/templates/check-contents.md`. A template that is not on disk comes back as exit 2, not 4. So treat a 4 as a malformed call: report it as a defect in this skill, name the command that produced it, and do not retry with guessed arguments |
|
||||
| 7 | Key invalid or no permission | Nothing was read and nothing written. Name the exit code and create nothing |
|
||||
| 8 | Any other API failure | Same as 7: the old blocks are unknown, so name the exit code and create nothing |
|
||||
|
||||
Exits 7 and 8 never mean the page is missing. Writing a fresh template over a directory whose blocks were never read wipes every other machine's block, with no merge and no backup behind it — which is exactly why the script creates a page only when its own read reported that page absent, and why it owns that branch instead of this prose.
|
||||
|
||||
`wiki-repo` exiting 3 means that page type has no wiki repo configured: `JSC_WIKI_REPO_CHECK` for the content page, `JSC_WIKI_REPO_CONTENTS` for the contents page. Print the tables, skip that one write, and put the unset variable at the top of 待修項目 — it is itself a finding, so a failed write never fails the health check. Any other non-zero exit from `wiki-repo`, `wiki-url`, `hash-id` or the wiki write is reported the same way: tables on screen, write skipped, exit code named.
|
||||
|
||||
### 3.2 Hand off
|
||||
|
||||
State the four counts from 2.2's `summary` line: 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 every link written into either page passed `link-check.sh` first — or the `DEAD` list is on screen and that write was skipped — each of the two pages is reported with its URL, or its skipped write is reported together with its reason, **and** the four counts are stated with the recommendation given or explicitly withheld.
|
||||
|
||||
### 3.3 Record how the run ended
|
||||
|
||||
This is the last thing this skill does, and it runs on every path out of the skill. Call
|
||||
|
||||
`{JSC_ROOT}/jsc-hooks/tools/report-status.sh skill-end jsc-cli:doctor {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome, and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the four counts fit there, the five blocks do not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.
|
||||
|
||||
This one call writes, and it is the only write this skill makes. It records what the run found; it changes no setting, no wiring and no version, so the read-only contract of the opening paragraph still holds.
|
||||
|
||||
| status | When this skill uses it |
|
||||
| --- | --- |
|
||||
| `ok` | All four checks reached a conclusion, the five blocks and the 待修項目 table are on screen, and both pages were written |
|
||||
| `degraded` | The checkup ran but part of it has no conclusion, and this is the common outcome for a read-only skill that cannot reach a source. Any check reported as 無法驗證 lands here — `version-guard.sh report` exiting non-zero, `scan-config.sh` exiting 3 on a missing spec table, `wire-cli.sh status` returning an unexpected code, a Gitea-dependent row coming back `skipped` in offline mode — and so does a `wiki-repo` exit 3 that skipped a page write, which this skill treats as a finding rather than a fault |
|
||||
| `failed` | Reading the machine worked, then recording it broke on an error: `link-check.sh`, `gitea.sh` or `wiki-contents.sh` returned 7 on an invalid key, or 8 on any other API failure. Both are errors, never an absent page, and neither leaves a usable record |
|
||||
| `aborted` | The user stopped the run before the record was written, for example by declining to supply `GITEA_HOST` and asking to end the checkup there |
|
||||
|
||||
`blocked` has no place in this skill. Nothing gates a read-only checkup: a machine with no CLI installed, no plugin registry and no wiki repo still produces four findings, and reporting that as `blocked` would hide a run that did its whole job.
|
||||
|
||||
Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.
|
||||
+46
-16
@@ -1,26 +1,56 @@
|
||||
---
|
||||
name: models
|
||||
description: List every model usable by each installed AI CLI (claude, codex, copilot, antigravity, kiro) and attach capability tags from references/model-tags.md. Syncs the tag table to $JSC_HOME/model-tags.tsv via tools/model-tags.sh so jsc-sdlc gates can be enforced in code, states which tags each SDLC stage requires (plan and analyze need reasoning-max, implement needs coding, maintain any), and resolves each stage's preferred model chain via tools/model-config.sh (project .jsc/models overrides $JSC_HOME/models.conf) for switch suggestions only. Use when checking model fitness, inventorying models, or reviewing stage gating; not for switching models or editing the config files.
|
||||
description: 'List every model usable by each installed AI CLI (claude, codex, copilot, antigravity, kiro) and attach capability tags from references/model-tags.md. Syncs the tag table to $JSC_HOME/model-tags.tsv via tools/model-tags.sh, so jsc-sdlc gates are enforced in code. States each SDLC stage''s required tags: plan and analyze need reasoning-max, implement needs coding, maintain any. Resolves each stage''s preferred model chain via tools/model-config.sh (project .jsc/models overrides $JSC_HOME/models.conf), for switch suggestions only. Use when checking model fitness, inventorying models, or reviewing stage gating; not for switching models or editing the config files.'
|
||||
---
|
||||
|
||||
# models — list CLI models with capability tags
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run `jsc-cli/tools/detect-clis.sh` to get the installed CLIs.
|
||||
2. For each CLI, read the available models and the model currently in use. This step **MUST run as a sub agent**:
|
||||
1. Start three collectors at once. They read different files and share no state, so the stage preference chain is fetched here rather than waited for at the end.
|
||||
|
||||
| CLI | Source |
|
||||
| --- | --- |
|
||||
| claude | `model` in `~/.claude/settings.json`; known families are in the claude rows of model-tags.md |
|
||||
| codex | `model` in `~/.codex/config.toml` |
|
||||
| copilot | model options listed in the copilot config (`~/.config/copilot/`) |
|
||||
| antigravity | models listed in the agy config |
|
||||
| kiro | models listed in the kiro config |
|
||||
1. `jsc-cli/tools/detect-clis.sh` — the installed CLIs, as `{name}<TAB>{path}<TAB>{version}`. Exit 0 with at least one row → that is the CLI list. Exit 0 with no row → no AI agent CLI is installed on this machine: report that, name the five it probes, skip steps 2 to 4, and go straight to step 5, because the tag table and the stage requirements are still worth writing out. Any non-zero exit → stop and report the exit code and stderr.
|
||||
2. `jsc-cli/tools/list-models.sh` — each CLI's models and the model currently in use, as `cli<TAB>model<TAB>in-use`, read from each CLI's own config. It stays silent for a CLI whose config it cannot read and always exits 0; a non-zero exit means the script itself failed, so report the model inventory as 無法取得 with the exit code. This collector **MUST run as a sub agent**.
|
||||
3. `jsc-cli/tools/model-config.sh list` — one line per stage, `stage<TAB>chain<TAB>source`, with `-` for unconfigured stages. Exit 0 → use the rows in step 6. Exit 2 → usage error, report it as a defect in this skill and show step 6's 階段偏好模型 table as 未取得. Any other exit → same handling, with the exit code named.
|
||||
|
||||
If a config is unreadable, list that CLI's known default models and mark each one with the literal label 「預設推定」 (assumed default).
|
||||
3. Attach capability tags to every model per `references/model-tags.md`. Handle unlisted models per the closing rule of that file.
|
||||
4. Output a table with four columns: CLI, model, tags, currently in use.
|
||||
5. Run `tools/model-tags.sh sync` to write the tag table to `$JSC_HOME/model-tags.tsv`, and report the path. This file is what `jsc-hooks/hooks/sdlc-gate.sh` reads, so the SDLC gate stays broken until it exists. Done when the command prints the path.
|
||||
6. Append the SDLC stage requirement table (plan and analyze need `reasoning-max`; implement needs `coding`; maintain accepts any), and state that gating is done in code by `sdlc-gate.sh lock {stage}` against the transcript's actual model id — **the models listed here are never allowed to self-assess their own tags**.
|
||||
7. Run `jsc-cli/tools/model-config.sh list` and append a 「階段偏好模型」 table right after the stage requirement table, with three columns: stage, chain, source (`project` / `global`). State below the table that the chain does **not** grant passage: it only names the model to suggest switching to when the gate blocks, and expresses preference among models that already satisfy the required tags. Done when the table shows all four stages, with `-` for unconfigured ones.
|
||||
Done when all three collectors have returned, and each has either its rows or an explicit failure reason.
|
||||
|
||||
2. Reconcile the two lists. For every detected CLI that collector 1.2 returned no rows for, list that CLI's known default models and mark each one with the literal label 「預設推定」 (assumed default). Done when every detected CLI has either a model list from its config or a set of assumed defaults.
|
||||
|
||||
3. Attach capability tags to every model per `references/model-tags.md`. A model missing from that table is not tagged by guesswork: add it to the table from the vendor's documentation, or queue it as a `jsc-ask:ask` question. Done when every listed model carries at least one tag and every unlisted model is either added to the table or queued as a `jsc-ask:ask` question.
|
||||
|
||||
4. Output a table with four columns: CLI, model, tags, currently in use. Done when the table holds one row per model from step 2.
|
||||
|
||||
5. Run `tools/model-tags.sh sync` to write the tag table to `$JSC_HOME/model-tags.tsv`. This file is what `jsc-hooks/hooks/sdlc-gate.sh` reads, so the SDLC gate stays broken until it exists.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Report the path it printed |
|
||||
| 1 | `references/model-tags.md` could not be parsed, or `$JSC_HOME` could not be written, so nothing was written. Name the reference path and the stderr, and state that the SDLC gate stays broken until this is fixed |
|
||||
| 2 | Usage error — the subcommand or its arguments are wrong, and the script printed its usage line instead of running. Report it as a defect in this skill, and do not retry with a guessed argument. This is the same code the script uses for `UNKNOWN-MODEL` and `UNKNOWN-STAGE`, so it never means a model failed a requirement |
|
||||
| other | Report the sync as failed with the exit code and stderr. Never report a path that was not printed |
|
||||
|
||||
Done when the written path is reported, or the failure is reported with its exit code.
|
||||
|
||||
6. Append the two stage tables, in this order.
|
||||
|
||||
1. The SDLC stage requirement table (plan and analyze need `reasoning-max`; implement needs `coding`; maintain accepts any), stating that gating is done in code by `sdlc-gate.sh lock {stage}` against the transcript's actual model id — **the models listed here are never allowed to self-assess their own tags**.
|
||||
2. A 「階段偏好模型」 table right after it, built from collector 1.3's rows, with three columns: stage, chain, source (`project` / `global`). State below the table that the chain does **not** grant passage: it only names the model to suggest switching to when the gate blocks, and expresses preference among models that already satisfy the required tags.
|
||||
|
||||
Done when the requirement table shows all four stages with their required tags, and the 階段偏好模型 table shows the same four stages with `-` for unconfigured ones.
|
||||
|
||||
7. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call
|
||||
|
||||
`jsc-hooks/tools/report-status.sh skill-end jsc-cli:models {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome — usually `model-tags.sh sync` — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the CLI and model counts fit there, the four-column table does not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.
|
||||
|
||||
| status | When this skill uses it |
|
||||
| --- | --- |
|
||||
| `ok` | Every detected CLI has a model list, every model carries a tag, `sync` exited 0 and printed the path, and both stage tables are on screen |
|
||||
| `blocked` | Nothing could be inventoried because nothing is installed: `detect-clis.sh` exited 0 with no row, so steps 2 to 4 have no CLI to work on. The tag table and the stage requirements are still printed, so say in `{detail}` that the inventory half of the run never started |
|
||||
| `failed` | `model-tags.sh sync` returned 1, 2 or any other non-zero code, so `$JSC_HOME/model-tags.tsv` was not written and the SDLC gate stays broken until it is. `detect-clis.sh` exiting non-zero sits here too |
|
||||
| `degraded` | The tag table was written but the picture is incomplete: `list-models.sh` stayed silent for a CLI so step 2 fell back to 「預設推定」 defaults, a model is missing from `references/model-tags.md` and was queued as a `jsc-ask:ask` question instead of tagged, or `model-config.sh list` failed so the 階段偏好模型 table shows 未取得 |
|
||||
| `aborted` | The user stopped the run before `sync` wrote the file, so the gate reads whatever the previous run left behind |
|
||||
|
||||
Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
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 by running the three checkers in parallel and merging them with tools/build-todo.sh. 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, handing jsc-cli:deploy the mode and the version report it already has. 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.
|
||||
|
||||
## Path rule — every script call is a literal absolute path
|
||||
|
||||
Write every script call in this skill as a literal absolute path. Never hand the shell a path that still holds a variable or a tilde — `$JSC_HOME/...`, `~/.jsc/...`, or anything like them — and never a bare relative one either. The permission layer matches paths statically: it expands no variable and no tilde, so such a path matches no allow rule and the call falls through to an approval prompt. A bare relative path is the worse form, because it resolves against whatever directory the CLI happens to be in — the operator's project directory, which is never a plugin root — so every bare call site is a place where a prefix gets guessed.
|
||||
|
||||
**This skill writes, and that is what turns a guessed prefix from an annoyance into a wrong machine.** An old `tools/apply-config.sh` reached through some other version's directory rewrites the `# jsc-config` block of every rc file here to that version's idea of the key set, backs the old block up as though that were correct, and then step 4 re-verifies it with `show` from the same wrong copy — which agrees, because it is the same copy. The run reports that item as 已修 and the operator believes it, and nothing in this document catches it. Only the path does.
|
||||
|
||||
Portability is no reason to put the variable back. Step 0 resolves the roots once, at run time, on whatever machine this runs on — that is where portability comes from.
|
||||
|
||||
## Step 0 — resolve the two roots, once
|
||||
|
||||
Before step 1, run this one command:
|
||||
|
||||
`readlink -f "$JSC_HOME/current"`
|
||||
|
||||
It prints one absolute directory: the `current` directory itself, a farm of version-free symbolic links with one entry per plugin. Call it `{JSC_ROOT}` for the rest of this document. **Stop at that directory — never resolve one level further.** Resolving one of those entries lands on the versioned plugin cache (`/root/.claude/plugins/cache/jsc/jsc-gitea/0.4.2`, say), and a versioned path is exactly the kind no allow rule can hold: a rule with `*` where the version segment goes matches nothing, measured. `{JSC_ROOT}` is the version-free root, and staying at it is the whole point. This is the only place a variable may appear; the shell expands it inside the command itself, so no unexpanded path ever reaches the permission layer.
|
||||
|
||||
Confirm the directory exists, in the same approved step: `[ -d "{the path just printed}" ]`. **No skill meets an unset `JSC_HOME` as often as this one.** It is an optional variable with a default, it has an `auto` row in `{CLI_ROOT}/tools/config-spec.tsv`, and writing it is one of the repairs this skill performs — so a machine whose `JSC_HOME` is unset or wrong is the ordinary reason this skill was called. With it unset the command prints `/current` and exits 0: non-empty, absolute, and nowhere. The emptiness check and the exit code both wave that through, and every literal path built from it names a place that is not there.
|
||||
|
||||
**An unresolvable `{JSC_ROOT}` stops neither this run nor the repair.** Everything needed to write `JSC_HOME` — `{CLI_ROOT}/tools/apply-config.sh`, `{CLI_ROOT}/tools/scan-config.sh`, `{CLI_ROOT}/tools/build-todo.sh` — hangs off the second root below, which does not depend on `JSC_HOME` at all. Fix that item first, re-resolve `{JSC_ROOT}` once afterwards, and carry on; whatever is still unreachable — the wiki record of step 5, a delegated repair — is recorded as 未修好 with that as its reason, never guessed at.
|
||||
|
||||
Substitute `{JSC_ROOT}` in every cross-plugin call, so what runs is a literal absolute path. `{JSC_ROOT}/jsc-gitea/tools/gitea.sh` becomes, for example, `/root/.jsc/current/jsc-gitea/tools/gitea.sh`. Resolve it once. Do not re-resolve it per call, and do not add a tool that prints it.
|
||||
|
||||
### The second root — this skill's own `tools/` and `templates/`
|
||||
|
||||
**Do not reach this skill's own files through `{JSC_ROOT}`, even though a `jsc-cli` link is normally sitting there.** `jsc-cli:deploy` refreshes the whole farm at the end of every round, so on a machine that has deployed, that link exists. This skill still may not lean on it, for two reasons of its own.
|
||||
|
||||
The first is the paragraph above: the machine this skill is called to repair is often the machine whose `JSC_HOME` is unset or wrong, and on it the farm cannot be reached while the repair itself must still run. Route the repair tools through the farm and the one fault they exist to fix becomes the fault that stops them.
|
||||
|
||||
The second is what this skill does with a stale script. Step 3 hands every 落後 domain to `jsc-cli:deploy` precisely because the versions on this machine may be behind — and the farm is governed by that same staleness. Reaching `apply-config.sh` through it would repair the machine with the very tools that machine has just been judged to have outgrown, and the result is written into rc files rather than merely printed.
|
||||
|
||||
They sit at `{plugin root}/tools/` and `{plugin root}/templates/`, and the plugin root is the base directory the CLI states when it loads this skill. Take that literal path verbatim, call it `{CLI_ROOT}`, and write every own-plugin path as `{CLI_ROOT}/tools/{script}` or `{CLI_ROOT}/templates/{file}` — `/root/.claude/plugins/cache/jsc/jsc-cli/0.3.3/tools/apply-config.sh`, for example. No command runs for this one, and it is taken once, like `{JSC_ROOT}`.
|
||||
|
||||
That base directory carries a version segment, so no allow rule covers it and each of those calls raises an approval prompt. **That is acceptable in this skill and in no unattended one**: setup runs with the operator in front of it — step 2 puts every item to them one at a time, and nothing is written before they answer — so there is somebody to approve. Never carry this branch into a skill that runs from a scheduler, and never guess a prefix when the invocation states no base directory: report that this skill's own plugin root is unknown and stop **before writing anything**, because a guessed prefix writes this machine with some other version's script.
|
||||
|
||||
Done when `{JSC_ROOT}` holds one existing absolute directory or its failure is recorded as the `JSC_HOME` item, and `{CLI_ROOT}` holds one literal absolute path.
|
||||
|
||||
## 1. Get the work list
|
||||
|
||||
Read the 待修項目 table from wiki `CHECK_{HASH}` — repo from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CHECK`, page name from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh hash-id "{host}/{user}"`, where `host` is the **short hostname** and `user` the login account, both taken from the machine:
|
||||
|
||||
```sh
|
||||
host=$(hostname 2>/dev/null || uname -n 2>/dev/null || printf 'unknown'); host=${host%%.*}
|
||||
user=${USER:-$(id -un 2>/dev/null || printf 'unknown')}
|
||||
```
|
||||
|
||||
`${host%%.*}` matters: `hostname` prints the FQDN on some machines, and a hash built on the long name reads a page doctor never wrote. This is the same value doctor hashes, so it must be taken the same way.
|
||||
|
||||
No page, or `wiki-repo` exits 3, or any other non-zero exit from `wiki-repo`, `hash-id` or the wiki read → rebuild the list here. Rebuilding **MUST run as a sub agent**, and its three checkers **start together**: they read different files and share no state, so serialising them only triples the wait.
|
||||
|
||||
| Checker | Command | Exit branching |
|
||||
| --- | --- | --- |
|
||||
| Settings | `{CLI_ROOT}/tools/scan-config.sh scan all` | 0 → use the rows; 2 → usage error, report it as a defect in this skill; 3 → spec table missing, name the path and `JSC_CONFIG_SPEC`; other → report settings as 無法驗證 |
|
||||
| Wiring | `{CLI_ROOT}/tools/detect-clis.sh`, then `JSC_READONLY=1 {JSC_ROOT}/jsc-hooks/tools/wire-cli.sh status {cli}` per detected CLI — this step only takes stock, and `wire-cli.sh` without a subcommand rewires, so the read-only contract is carried in the environment rather than trusted to a correctly typed subcommand | detect-clis exit 0 with no row → no CLI to wire, say so and skip; detect-clis non-zero → report wiring as 無法驗證 with the exit code. Per CLI: 0 wired and 1 degraded → nothing to fix; 2 → usage error, defect in this skill; 3 → CLI not installed, drop it; 5 → collect its `missing` items; 6 → readonly refused the call, which means the subcommand was mistyped into a writing one — nothing on the machine changed; fix the command and rerun that CLI; other → report that CLI as 無法驗證 |
|
||||
| Versions | `{JSC_ROOT}/jsc-hooks/hooks/version-guard.sh report` | 0 → use the rows, and treat a report with no `{domain}` row or a `noregistry` line as 無法驗證 — other exits → report versions as 無法驗證 with the exit code |
|
||||
|
||||
Merge the three with `{CLI_ROOT}/tools/build-todo.sh --config {設定輸出} --wiring {cli}={接線輸出} --version {版本輸出}` so the ordering rule lives in one place. Exit 0 → the `todo` rows are the work list; exit 2 → usage error, report it as a defect in this skill; exit 3 → name the unreadable input and rerun that one checker; any other exit → stop and report, because a half-merged list would silently drop a whole class of items.
|
||||
|
||||
Keep the version report from that run. Step 3 hands it to `jsc-cli:deploy` instead of making it query again.
|
||||
|
||||
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 class, scope, current state and fix route, and the source of the list is named.
|
||||
|
||||
## 2. Confirm each item
|
||||
|
||||
Ask per the `jsc-ask:ask` decision tree, one item at a time, in the table's order. Confirmation stays strictly sequential: each answer can change what the next item should be, and a batch of questions fired at once takes that away from the operator.
|
||||
|
||||
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 | `{CLI_ROOT}/tools/apply-config.sh set {KEY} {VALUE}` |
|
||||
| `auto` on a directory | `{CLI_ROOT}/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` with mode `update` **and the version report from step 1**, so it neither re-asks the mode nor re-queries the versions |
|
||||
| hook unwired | call `jsc-hooks:hooks-install` |
|
||||
| `$JSC_HOME/model-tags.tsv` missing | call `jsc-cli:models` |
|
||||
|
||||
`apply-config.sh` exit codes:
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Written. Record the `wrote` or `created` result and the `backup` path |
|
||||
| 2 | Usage error — the subcommand, the key or the value is wrong. Record the item as 未修好 with that reason, and do not retry with a guessed argument |
|
||||
| 4 | Backup or write failed, so nothing was written. Record the item as 未修好 and name the rc file and the stderr, then say the machine is unchanged |
|
||||
| other | Record the item as 未修好 with the exit code and stderr. Never mark it fixed on an unrecognised exit |
|
||||
|
||||
`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`, delegated or 未修好 result.
|
||||
|
||||
## 4. Re-verify
|
||||
|
||||
Re-verify every applied item. The items are independent, so **run the re-verifications in parallel** — one batch, one wait. Only the confirmation in step 2 has to stay sequential.
|
||||
|
||||
Pick the check by what was actually written, because the two kinds of write become true at different moments:
|
||||
|
||||
| What was written | Re-verify with | Why this check |
|
||||
| --- | --- | --- |
|
||||
| An environment variable in a shell rc file | `{CLI_ROOT}/tools/apply-config.sh show`, confirming the `KEY<TAB>VALUE` line is in the `# jsc-config` block | The block is a fact that is already true. The variable reaching the environment is not — a rc file does not touch the running shell |
|
||||
| A directory | `{CLI_ROOT}/tools/scan-config.sh scan {scope}`, confirming the row is no longer `missing` or `invalid` | The directory exists the moment it is created |
|
||||
| Wiring, versions, model tags (delegated) | The owner skill's own returned result | The owner already ran its own verification |
|
||||
|
||||
The same exit branching as step 1 applies to `scan-config.sh` and to `apply-config.sh`.
|
||||
|
||||
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.
|
||||
|
||||
For every environment variable written, print the matching `export KEY=VALUE` line for the current session and tell the operator to open a new shell or `source` the rc file. The environment-level proof is handed to the next `/jsc-cli:doctor` run, which starts in a fresh shell — asserting it here would read the shell that could not have picked the value up yet, and report a false failure every time.
|
||||
|
||||
Done when every applied item has a fresh verdict from the checker its own row names, and every environment variable carries its `export` line.
|
||||
|
||||
## 5. Record
|
||||
|
||||
The two pages live in **two different wiki repos**. Resolve each one on its own.
|
||||
|
||||
Rewrite `CHECK_{HASH}` through `jsc-gitea:wiki` with the post-fix state, per `{CLI_ROOT}/templates/check-page.md` — repo from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CHECK`. That page is a **content page** and keeps only the latest run, so this overwrites the pre-fix picture on purpose.
|
||||
|
||||
`CHECK_CONTENTS` is a **contents page**, it lives in the contents repo (`{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-repo CONTENTS`, never a fallback to `JSC_WIKI_REPO_CHECK`), and it gets the opposite treatment. It is an H1, a `>` preamble and one H2 block per machine — no markdown table anywhere on it. Write the block with
|
||||
|
||||
`{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 "CHECK_{HASH}" {block file} {CLI_ROOT}/templates/check-contents.md`
|
||||
|
||||
which reads the page back and refreshes this machine's block, or appends it when missing. Never overwrite the whole page, and never touch another machine's block.
|
||||
|
||||
The block file holds the whole H2 block: the `## CHECK_{HASH}` line, a blank line, then one bullet per field in the order `{CLI_ROOT}/templates/check-contents.md` lists them, written as `- {欄位名}:{值}` with a full-width colon. Every field of the template gets a bullet, `HASH` included — the heading is the key, and a field only in the heading is a field the next reader cannot read.
|
||||
|
||||
**The key is the H2 heading, `CHECK_{HASH}`.** It is the name of the content page this block points at: the literal `CHECK_` plus exactly what `hash-id` printed for `{host}/{user}` in step 1 — 40 uppercase hex characters, not shortened, not otherwise prefixed, not wrapped in a link, no date appended. The script compares the whole heading text after trimming, so the key and that heading must match character for character.
|
||||
|
||||
A key holding a URL would be a moving key: the URL changes with `GITEA_HOST`, with a move of `JSC_WIKI_REPO_CHECK` to another repo, and with Gitea's encoding of the page name. The page name moves with none of them — `{host}/{user}` alone decides it. Key on the URL and the comparison never matches, so every run appends a second block for the same machine instead of updating it.
|
||||
|
||||
`1` is `<key-col>`, and it only matters while a page is still the old markdown table: it is the 1-based index of the column that held the identity, the `[CHECK_{HASH}]({URL})` cell in column 1, whose text becomes the H2 heading when the script converts that table to blocks. On a page already in block form the script ignores it.
|
||||
|
||||
The `體檢頁` bullet stays the human-facing link and is never the key; the heading itself carries no link and no URL. Write the bullet as `[CHECK_{HASH}]({absolute URL})` — text plus link, the one link form this skill set uses. The URL comes from `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url {CHECK repo} CHECK_{HASH}`, fetched after `CHECK_{HASH}` is rewritten, and is never composed by hand.
|
||||
|
||||
A same-wiki link form resolves only inside its own wiki, and the two pages are no longer in the same one. It fails without an error, reading on screen as plain text or a dead link, so nobody finds it and nobody fixes it.
|
||||
|
||||
**Verify every link before writing it.** Collect every URL heading into `CHECK_{HASH}` or into the `CHECK_CONTENTS` block, then hand the whole list to `{JSC_ROOT}/jsc-gitea/tools/link-check.sh`. It prints `{OK|DEAD|SKIP}<TAB>{URL}<TAB>{reason}` per line. Only exit 0 may be written.
|
||||
|
||||
| Exit | Meaning | Action |
|
||||
| --- | --- | --- |
|
||||
| 0 | Every link answers | Write the page |
|
||||
| 1 | At least one link is unreachable | Write nothing, on either page. Report the `DEAD` lines and leave both pages as they are |
|
||||
| 2 | Usage error: no URL was given | Report it as a defect in this skill and pass the URLs |
|
||||
| 3 | The list holds a Gitea URL but `GITEA_HOST` is unset | Skip both writes and report `GITEA_HOST` as still unfixed. Never write without verifying |
|
||||
| 7 | Gitea authentication failed | Stop and report the key problem. This is not a dead link |
|
||||
|
||||
Exit 7 stays apart from exit 1 on purpose: with a dead key, Gitea's answer for a private repo looks the same as "page absent". Merge the two and one expired key marks every live page dead, and the blocks pointing at them get rewritten or dropped.
|
||||
|
||||
The script asks the API and never reads a web status code. A private repo's web URL answers 404 to a request with no credentials, so status codes turn good links into dead ones.
|
||||
|
||||
| Exit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Report the `updated` or `added` result with the repo and page it named |
|
||||
| 1 | The page content could not be built, or the write failed, and nothing landed. Report it with the stderr. A page with no block to replace is not this case: the script appends instead |
|
||||
| 2 | Usage error. Report it as a defect in this skill; do not retry with guessed arguments. A `{CLI_ROOT}/templates/check-contents.md` that is not on disk also lands here — then name the path the script looked for, confirm the plugin install is complete, and rerun |
|
||||
| 3 | No contents repo configured. Skip this write and report `JSC_WIKI_REPO_CONTENTS` as still unfixed |
|
||||
| 4 | Unreachable the way this skill calls the script — the command above always passes `{CLI_ROOT}/templates/check-contents.md`, and a template that is not on disk comes back as exit 2. So treat a 4 as a malformed call: report it as a defect in this skill, name the command that produced it, and do not retry with guessed arguments |
|
||||
| 7 | Key invalid or no permission. Nothing was read or written; name the exit code and create nothing |
|
||||
| 8 | Any other API failure. Same as 7 |
|
||||
|
||||
Exits 7 and 8 never mean the page is missing: the whole-page overwrite that is correct for `CHECK_{HASH}` would here destroy every other machine's block, unread and unrecoverable. The script creates a page only when its own read reported that page absent, and it owns that branch.
|
||||
|
||||
`{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url` has its own exits, and they are read before the upsert runs. Exit 4 means `CHECK_{HASH}` is not on the wiki yet, so rewrite that page first and fetch the URL again. Any other non-zero exit: name the exit code and stop — never hand-build the URL, because a guessed link goes into the block and points nowhere.
|
||||
|
||||
No wiki repo configured, or any non-zero exit from `wiki-repo`, `hash-id`, `wiki-url` or the wiki write → report the tables on screen, say the record was skipped, and name the exit code.
|
||||
|
||||
Then state the counts: fixed, skipped, delegated, and 未修好. Recommend `/jsc-cli:doctor` for a clean re-check when anything was delegated.
|
||||
|
||||
Done when every link written into either page passed `link-check.sh` first — or the `DEAD` list is on screen and that write was skipped — each of the two pages is written or its skip is reported, and the four counts are stated.
|
||||
|
||||
## 6. Record how the run ended
|
||||
|
||||
This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call
|
||||
|
||||
`{JSC_ROOT}/jsc-hooks/tools/report-status.sh skill-end jsc-cli:setup {status} {exit code} [detail]`
|
||||
|
||||
`{exit code}` is the exit code of whatever decided the outcome — usually the `apply-config.sh` call that ruled the run — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters: the four counts fit there, the item table does not, and no value the user typed goes in it. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports.
|
||||
|
||||
| status | When this skill uses it |
|
||||
| --- | --- |
|
||||
| `ok` | Every item on the list was confirmed and applied, each one re-verified by the checker its own row names, nothing was skipped, and both pages were written |
|
||||
| `blocked` | The environment refused every write, so nothing on the machine changed: `apply-config.sh` returned 4 on each item because the backup or the write failed. A run that could not touch a single rc file did no work, so it is never reported as `failed` half-done |
|
||||
| `failed` | Writes landed but the run broke: an applied item still fails its re-verification in step 4, `apply-config.sh` returned 2 on a malformed call this skill made, or `link-check.sh`, `gitea.sh` or `wiki-contents.sh` returned 7 or 8 and the record could not be rewritten |
|
||||
| `degraded` | The run finished with part of the list untouched. The usual case is the user turning an item down at step 2 — a skip is recorded as skipped and never as fixed — and a delegated repair that its owner skill did not close counts the same way. The machine is better than it was, and the 未修好 and 略過 counts are above zero |
|
||||
| `aborted` | The user stopped the sequential confirmation partway and asked to end the run, so the remaining items were never put to them |
|
||||
|
||||
Done when exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it.
|
||||
@@ -0,0 +1,29 @@
|
||||
# 體檢目錄
|
||||
|
||||
> 由 `jsc-cli:doctor` 維護。每台執行環境一個區塊;`HASH` 取 `{短主機名}/{登入帳號}`,算法與其他頁面共用。主機名取不含網域的短名,FQDN 要先切掉第一個點之後的部分,否則同一台機器會多出第二頁。
|
||||
>
|
||||
> 本頁落在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,與體檢頁 `CHECK_{HASH}` 不同庫。目錄頁全部住這裡,不退回 `JSC_WIKI_REPO_CHECK`。
|
||||
>
|
||||
> 路徑寫法:底下每一支腳本都以字面絕對路徑呼叫,不留 `$JSC_HOME`、不留波浪號,也不留裸的相對路徑——權限層靜態比對路徑,帶變數的比不中任何規則,相對路徑則會對著操作者當下的工作目錄解,而那裡從來不是外掛根目錄。`{JSC_ROOT}` 是 `readlink -f "$JSC_HOME/current"` 解出的那個目錄,解到那一層就停,不再往下解成帶版本號的快取路徑;`{CLI_ROOT}` 是 CLI 載入技能時講明的 jsc-cli 外掛基底目錄。兩者都在技能開跑時各解一次,之後逐字代入。
|
||||
>
|
||||
> 寫入語意:一個區塊代表一台執行環境,也就是一組主機加帳號。一律用 `{JSC_ROOT}/jsc-gitea/tools/wiki-contents.sh upsert CHECK 1 CHECK_{HASH} {區塊檔} {CLI_ROOT}/templates/check-contents.md` 寫,它先讀回整頁,該執行環境已經有區塊就整塊換掉,沒有才在頁尾附加一個區塊。禁止整頁覆蓋,也不得改動別人的區塊。體檢頁 `CHECK_{HASH}` 只留最新一次結果、可以整頁改寫,這份目錄頁不行。
|
||||
>
|
||||
> 參數說明:第二個參數 `1` 是 `<key-col>`,只在這一頁還留著舊的 markdown 表格時用得到,指舊表格裡持有身分那一欄的序號,也就是持有 `[CHECK_{HASH}](網址)` 的第 1 欄,自動轉檔時取那一格的文字當 H2 標題;頁面已經是條列格式就完全忽略它。第三個參數 `<key>` 是 H2 標題文字,也就是體檢頁頁名 `CHECK_{HASH}`。第四個參數是區塊檔,不是列檔:內容為 `## CHECK_{HASH}` 那一行、一個空行,再接各條欄位。
|
||||
>
|
||||
> 鍵是 H2 標題 `CHECK_{HASH}`:`{JSC_ROOT}/jsc-gitea/tools/hash-id` 印出什麼就接在 `CHECK_` 後面,完整 40 碼大寫十六進位,不截短、不加別的前後綴、不包成連結、不加日期。腳本比對的是去掉頭尾空白後的整段標題文字,鍵一定要跟標題一字不差。
|
||||
>
|
||||
> 鍵用頁名才穩。頁名只由 `{短主機名}/{登入帳號}` 決定;`GITEA_HOST` 換掉、`JSC_WIKI_REPO_CHECK` 換過存取庫、Gitea 對頁名的網址編碼有差,網址就跟著變,頁名一個字都不動。拿含網址的值當鍵,比對就永遠比不中,同一台機器每體檢一次就多附一個區塊,畫面上還看不出來。
|
||||
>
|
||||
> 連結寫法:「體檢頁」那一條的連結只給人點,不當鍵用;H2 標題本身不放連結、不放網址。連結一律寫成 `[{文字}]({連結})`,也就是 `[CHECK_{HASH}]({wiki-url 印出的絕對網址})`,網址取 `{JSC_ROOT}/jsc-gitea/tools/gitea.sh wiki-url` 印出的那一串,不自己組路徑。同 wiki 的雙括號寫法一概不用:兩頁分屬不同存取庫,連不過去,畫面上還看不出壞掉。
|
||||
>
|
||||
> 連結驗證:這個區塊要寫進去的每一個連結,先交給 `{JSC_ROOT}/jsc-gitea/tools/link-check.sh`,結束碼 0 才寫。有任何一筆 DEAD 就整個區塊都不寫,把連不到的清單回報給呼叫端。結束碼 3 代表 `GITEA_HOST` 沒設定,先設好再寫,不得跳過驗證;結束碼 7 代表金鑰失效,停下來回報金鑰問題,不要當成死連結。驗證一律走 API,不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把好連結判成壞的,整批砍掉還在的頁。
|
||||
|
||||
## CHECK_{HASH}
|
||||
|
||||
- 體檢頁:[CHECK_{HASH}]({wiki-url 印出的絕對網址})
|
||||
- 主機:{短主機名}
|
||||
- 帳號:{使用者帳號}
|
||||
- HASH:{HASH}
|
||||
- 必要項缺漏:{n}
|
||||
- 設定錯誤:{n}
|
||||
- 最後體檢:{yyyy-MM-dd HH:mm}
|
||||
@@ -0,0 +1,61 @@
|
||||
# 執行環境體檢 — {hostname}/{使用者帳號}
|
||||
|
||||
> 由 `jsc-cli:doctor` 維護。這是體檢頁 `CHECK_{HASH}`,只保留最新一次結果,重跑就整頁覆寫。
|
||||
> 修復請執行 `/jsc-cli:setup`,它讀這頁的「待修項目」逐項處理。
|
||||
> 底下提到的腳本都以字面絕對路徑呼叫,不留 `$JSC_HOME`、不留波浪號,也不留裸的相對路徑;`{CLI_ROOT}` 是 CLI 載入技能時講明的 jsc-cli 外掛基底目錄,技能開跑時取一次,之後逐字代入。
|
||||
|
||||
- 體檢時間:{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} | {項目名,逗號分隔;無則寫「無」} | {降級原因或未偵測到執行檔} |
|
||||
|
||||
## 全域設定
|
||||
|
||||
| 項目 | 必要 | 現況 | 期望 | 修法 | 判定 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| {變數或檔案} | {是、否} | {實際值或未設定} | {該是什麼} | {自動、詢問、手動} | {通過、走預設、未設定、缺漏、設錯、略過} |
|
||||
|
||||
## 自我設定
|
||||
|
||||
> 工作目錄:{絕對路徑}
|
||||
|
||||
| 項目 | 必要 | 現況 | 期望 | 修法 | 判定 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| {變數或檔案} | {是、否} | {實際值或未設定} | {該是什麼} | {自動、詢問、手動} | {通過、走預設、未設定、缺漏、設錯、略過} |
|
||||
|
||||
## 待修項目
|
||||
|
||||
> 前六欄直接來自 `{CLI_ROOT}/tools/build-todo.sh` 的 `todo` 列,順序與類別由那支腳本決定,這裡不另行排序。
|
||||
> 「影響」欄由 `jsc-cli:doctor` 補上。`/jsc-cli:setup` 從這張表接手;沒有待修項目時腳本會印一列「無」。
|
||||
|
||||
| 順序 | 類別 | 項目 | 範圍 | 現況 | 修法 | 影響 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| {n} | {missing、invalid、unwired、落後、無} | {變數、檔案、接線項目或 plugin 名} | {global、project、cli 代號、版本} | {實際值、缺少接線的檔案或本機與遠端版本} | {auto、ask、manual 或接手的技能名} | {不修的話哪些技能跑不動} |
|
||||
|
||||
## 未登錄變數
|
||||
|
||||
> 原始碼有用到、`{CLI_ROOT}/tools/config-spec.tsv` 沒登錄的變數。體檢不判定它們,只提醒維護者補登錄。
|
||||
|
||||
| 變數 | 出現次數 |
|
||||
| --- | --- |
|
||||
| {變數名} | {n} |
|
||||
Executable
+158
@@ -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 段落的內容(每行 KEY<TAB>VALUE)
|
||||
# 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
|
||||
|
||||
# 目前段落裡的設定,格式 KEY<TAB>VALUE。取第一個既有 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
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env sh
|
||||
# build-todo.sh — 把三支檢查腳本的輸出合併成一張「待修項目」表。
|
||||
#
|
||||
# /jsc-cli:doctor 與 /jsc-cli:setup 都要這張表,合併規則只留一個真實來源,
|
||||
# 兩支技能各寫一次就會各自漂移,一邊排序、另一邊漏掉某一類。
|
||||
#
|
||||
# 用法:
|
||||
# build-todo.sh [--config {檔案}]... [--wiring {cli}={檔案}]... [--version {檔案}]...
|
||||
# 每個選項都可以重複。檔案給「-」代表讀標準輸入(整份只能有一個 -)。
|
||||
# 三種輸入都省略時視為用法錯誤:空跑會印出「沒有待修項目」,那是假通過。
|
||||
#
|
||||
# 輸入格式(由各自的腳本產生,本腳本不自己執行它們,維持唯讀):
|
||||
# --config jsc-cli/tools/scan-config.sh scan 的輸出
|
||||
# {項目}<TAB>{範圍}<TAB>{必要}<TAB>{現況}<TAB>{說明}<TAB>{修法}<TAB>{判定}
|
||||
# 只取判定為 missing 與 invalid 的列,summary 列略過。
|
||||
# --wiring jsc-hooks/tools/wire-cli.sh status {cli} 的輸出,前面掛上該 CLI 代號。
|
||||
# 首行 status=... reason=...;其後 item<TAB>{項目}<TAB>{路徑}<TAB>{present|missing}
|
||||
# 只有 status=unwired 才進待修表,每個 missing 項目一列。
|
||||
# degraded 是 codex、copilot、antigravity、kiro 的健康狀態,不是缺失。
|
||||
# --version jsc-hooks/hooks/version-guard.sh report 的輸出
|
||||
# {domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}
|
||||
# 只取「落後」的列。noregistry 與 behind 列略過。
|
||||
#
|
||||
# 輸出(TSV,一行一筆):
|
||||
# todo<TAB>{序號}<TAB>{類別}<TAB>{項目}<TAB>{範圍}<TAB>{現況}<TAB>{修法}
|
||||
# summary<TAB>{missing 數}<TAB>{invalid 數}<TAB>{unwired 數}<TAB>{落後數}
|
||||
# 類別排序固定為 missing、invalid、unwired、落後;同類別內照輸入順序。
|
||||
# 一項都沒有時仍印一列 todo,類別欄為「無」,呼叫端照樣有東西可以呈現。
|
||||
#
|
||||
# 結束碼:0=合併完成(有沒有待修項目都算完成,判斷交給呼叫端)
|
||||
# 2=用法錯誤(沒給任何輸入、選項寫錯、--wiring 少了 {cli}= 前綴)
|
||||
# 3=指定的輸入檔讀不到
|
||||
set -u
|
||||
|
||||
TAB=$(printf '\t')
|
||||
|
||||
usage() {
|
||||
[ -n "${WORK:-}" ] && rm -rf "$WORK"
|
||||
echo "用法:build-todo.sh [--config {檔案}]... [--wiring {cli}={檔案}]... [--version {檔案}]..." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
WORK=$(mktemp -d 2>/dev/null) || { echo "無法建立暫存目錄" >&2; exit 3; }
|
||||
F_MISSING="$WORK/missing"; F_INVALID="$WORK/invalid"
|
||||
F_UNWIRED="$WORK/unwired"; F_BEHIND="$WORK/behind"
|
||||
: > "$F_MISSING"; : > "$F_INVALID"; : > "$F_UNWIRED"; : > "$F_BEHIND"
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
|
||||
die() { cleanup; echo "$1" >&2; exit "$2"; }
|
||||
|
||||
# 輸入檔的存在性先在主 shell 檢查。放進管線裡檢查的話,die 只會結束子 shell,
|
||||
# 主流程照樣往下跑,最後印出一張少了整類項目卻看起來正常的表。
|
||||
check_input() { # $1=路徑
|
||||
[ "$1" = "-" ] && return 0
|
||||
[ -f "$1" ] || die "讀不到輸入檔:$1" 3
|
||||
[ -r "$1" ] || die "輸入檔沒有讀取權限:$1" 3
|
||||
}
|
||||
|
||||
# 取得一份輸入的內容。「-」讀標準輸入,其餘一律當檔案路徑。
|
||||
slurp() { # $1=路徑
|
||||
if [ "$1" = "-" ]; then cat; else cat "$1"; fi
|
||||
}
|
||||
|
||||
# scan-config.sh scan 的輸出 → missing 與 invalid 兩類。
|
||||
take_config() { # $1=路徑
|
||||
slurp "$1" | while IFS="$TAB" read -r key scope req actual desc fix verdict; do
|
||||
[ -n "${key:-}" ] || continue
|
||||
[ "$key" = summary ] && continue
|
||||
case "${verdict:-}" in
|
||||
missing) printf '%s\t%s\t%s\t%s\n' "$key" "${scope:--}" "${actual:--}" "${fix:--}" >> "$F_MISSING" ;;
|
||||
invalid) printf '%s\t%s\t%s\t%s\n' "$key" "${scope:--}" "${actual:--}" "${fix:--}" >> "$F_INVALID" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# wire-cli.sh status 的輸出 → unwired 一類。$1=cli $2=路徑
|
||||
take_wiring() {
|
||||
_txt="$WORK/wiring.$$"
|
||||
slurp "$2" > "$_txt"
|
||||
# 只有 status=unwired 才是待修。wired 沒事,degraded 是四支非 Claude CLI 的健康狀態,
|
||||
# skipped 代表那支 CLI 根本沒裝,三者都不該出現在待修表上。
|
||||
grep -q '^status=unwired' "$_txt" || { rm -f "$_txt"; return 0; }
|
||||
while IFS="$TAB" read -r kind item path state; do
|
||||
[ "${kind:-}" = item ] || continue
|
||||
[ "${state:-}" = missing ] || continue
|
||||
printf '%s\t%s\t%s\t%s\n' "${item:--}" "$1" "缺少接線:${path:--}" "jsc-hooks:hooks-install" >> "$F_UNWIRED"
|
||||
done < "$_txt"
|
||||
rm -f "$_txt"
|
||||
}
|
||||
|
||||
# version-guard.sh report 的輸出 → 落後一類。
|
||||
take_version() { # $1=路徑
|
||||
slurp "$1" | while IFS="$TAB" read -r domain local_v remote_v state; do
|
||||
[ -n "${domain:-}" ] || continue
|
||||
case "$domain" in behind|noregistry) continue ;; esac
|
||||
[ "${state:-}" = "落後" ] || continue
|
||||
printf '%s\t%s\t%s\t%s\n' "jsc-$domain" "版本" "本機 ${local_v:--}、遠端 ${remote_v:--}" "jsc-cli:deploy update" >> "$F_BEHIND"
|
||||
done
|
||||
}
|
||||
|
||||
got=0
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--config) [ "$#" -ge 2 ] || usage; check_input "$2"; take_config "$2"; got=1; shift 2 ;;
|
||||
--version) [ "$#" -ge 2 ] || usage; check_input "$2"; take_version "$2"; got=1; shift 2 ;;
|
||||
--wiring)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
case "$2" in *=*) ;; *) usage ;; esac
|
||||
_cli=${2%%=*}; _file=${2#*=}
|
||||
[ -n "$_cli" ] && [ -n "$_file" ] || usage
|
||||
check_input "$_file"; take_wiring "$_cli" "$_file"; got=1; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ "$got" = 1 ] || usage
|
||||
|
||||
n_missing=$(wc -l < "$F_MISSING" | tr -d ' ')
|
||||
n_invalid=$(wc -l < "$F_INVALID" | tr -d ' ')
|
||||
n_unwired=$(wc -l < "$F_UNWIRED" | tr -d ' ')
|
||||
n_behind=$(wc -l < "$F_BEHIND" | tr -d ' ')
|
||||
|
||||
seq_no=0
|
||||
emit_class() { # $1=類別 $2=檔案
|
||||
while IFS="$TAB" read -r item scope actual fix; do
|
||||
[ -n "${item:-}" ] || continue
|
||||
seq_no=$((seq_no + 1))
|
||||
printf 'todo\t%s\t%s\t%s\t%s\t%s\t%s\n' "$seq_no" "$1" "$item" "$scope" "$actual" "$fix"
|
||||
done < "$2"
|
||||
}
|
||||
|
||||
emit_class missing "$F_MISSING"
|
||||
emit_class invalid "$F_INVALID"
|
||||
emit_class unwired "$F_UNWIRED"
|
||||
emit_class 落後 "$F_BEHIND"
|
||||
|
||||
[ "$seq_no" -gt 0 ] || printf 'todo\t1\t無\t沒有待修項目\t-\t-\t-\n'
|
||||
printf 'summary\t%s\t%s\t%s\t%s\n' "$n_missing" "$n_invalid" "$n_unwired" "$n_behind"
|
||||
|
||||
cleanup
|
||||
exit 0
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env sh
|
||||
# check-requires.sh — 更新單一 plugin 之前,檢查它宣告的 jsc.requires 最低版本。
|
||||
# 用法:
|
||||
# check-requires.sh {claude|codex|copilot|antigravity|kiro} {manifest}
|
||||
# 輸出(單行,可供程式判讀):
|
||||
# status=ok reason={沒有宣告相依版本|相依版本符合:...}
|
||||
# status=blocked reason={缺哪一個 plugin、差哪一版}
|
||||
# status=unknown reason={manifest 讀不到、不是有效 JSON,或缺 python3}
|
||||
# 結束碼:0=通過(沒有宣告相依,或全部符合)
|
||||
# 1=相依版本不符或缺相依 plugin
|
||||
# 2=用法錯誤(參數個數不對,或 CLI 代號不在五個之內)
|
||||
# 4=判不出結論(manifest 不存在、不是有效 JSON、缺 python3)
|
||||
# 1 與 4 分開的理由:兩者都不是「通過」,但成因完全不同。混成同一個碼,呼叫端就只能
|
||||
# 用同一句話講兩件事,環境壞掉會被說成版本落後,操作者照著去補版本永遠補不到問題點。
|
||||
# deploy.sh update 在每個 domain 更新前呼叫一次,但擋下不代表跳過:deploy.sh 照樣更新,
|
||||
# 只印一行提醒說缺哪一版。跳過會讓落後的 domain 永遠更新不到,形成死鎖。
|
||||
# 真正的阻擋在 jsc-hooks 的 version-guard.sh,技能被叫用時才擋。
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo "用法:check-requires.sh {claude|codex|copilot|antigravity|kiro} {manifest}" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
CLI=$1
|
||||
MANIFEST=$2
|
||||
|
||||
case "$CLI" in
|
||||
claude|codex|copilot|antigravity|kiro) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
[ -f "$MANIFEST" ] || {
|
||||
printf 'status=unknown reason=找不到 manifest:%s\n' "$MANIFEST"
|
||||
exit 4
|
||||
}
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || {
|
||||
# 直接讓 shell 回 127 的話,呼叫端會看到一個沒宣告過的結束碼,也讀不到原因。
|
||||
printf 'status=unknown reason=找不到 python3,無法解析 manifest:%s\n' "$MANIFEST"
|
||||
exit 4
|
||||
}
|
||||
|
||||
JSC_HOME_DIR="${JSC_HOME:-$HOME/.jsc}"
|
||||
LOCAL_DIR="${JSC_LOCAL_PLUGINS:-$JSC_HOME_DIR/plugins}"
|
||||
KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}"
|
||||
|
||||
python3 - "$CLI" "$MANIFEST" "$LOCAL_DIR" "$KIRO_SKILLS" <<'PY'
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
cli, manifest, local_dir, kiro_skills = sys.argv[1:5]
|
||||
|
||||
try:
|
||||
with open(manifest, encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
except Exception as exc:
|
||||
print(f"status=unknown reason=manifest 不是有效 JSON:{manifest}:{exc}")
|
||||
sys.exit(4)
|
||||
|
||||
requires = ((data.get("jsc") or {}).get("requires") or {})
|
||||
if not requires:
|
||||
print("status=ok reason=沒有宣告相依版本")
|
||||
sys.exit(0)
|
||||
|
||||
def version_tuple(value):
|
||||
value = str(value or "").strip()
|
||||
parts = value.split(".")
|
||||
out = []
|
||||
for part in parts[:3]:
|
||||
match = re.match(r"^(\d+)", part)
|
||||
out.append(int(match.group(1)) if match else 0)
|
||||
while len(out) < 3:
|
||||
out.append(0)
|
||||
return tuple(out)
|
||||
|
||||
def current_version(plugin):
|
||||
domain = plugin[4:] if plugin.startswith("jsc-") else plugin
|
||||
candidates = []
|
||||
patterns = []
|
||||
if cli == "claude":
|
||||
registry = os.path.expanduser("~/.claude/plugins/installed_plugins.json")
|
||||
try:
|
||||
with open(registry, encoding="utf-8") as fh:
|
||||
entries = (json.load(fh).get("plugins") or {}).get(f"jsc-{domain}@jsc") or []
|
||||
for entry in entries:
|
||||
path = os.path.join(entry.get("installPath") or "", "plugin.json")
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
version = json.load(fh).get("version", "")
|
||||
if version:
|
||||
return version, path
|
||||
except Exception:
|
||||
pass
|
||||
patterns.append(os.path.expanduser(f"~/.claude/plugins/cache/jsc/jsc-{domain}/*/plugin.json"))
|
||||
elif cli == "codex":
|
||||
patterns.append(os.path.expanduser(f"~/.codex/plugins/cache/jsc/jsc-{domain}/*/plugin.json"))
|
||||
elif cli == "copilot":
|
||||
patterns.append(os.path.expanduser(f"~/.copilot/installed-plugins/jsc/jsc-{domain}/plugin.json"))
|
||||
elif cli == "antigravity":
|
||||
patterns.append(os.path.join(local_dir, domain, "plugin.json"))
|
||||
patterns.append(os.path.expanduser(f"~/.antigravity/plugins/jsc-{domain}/plugin.json"))
|
||||
elif cli == "kiro":
|
||||
patterns.append(os.path.join(kiro_skills, f"jsc-{domain}", "plugin.json"))
|
||||
|
||||
for pattern in patterns:
|
||||
for path in glob.glob(pattern):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
version = json.load(fh).get("version", "")
|
||||
except Exception:
|
||||
continue
|
||||
if version:
|
||||
candidates.append((version_tuple(version), version, path))
|
||||
if not candidates:
|
||||
return "", ""
|
||||
candidates.sort()
|
||||
return candidates[-1][1], candidates[-1][2]
|
||||
|
||||
failures = []
|
||||
for plugin, constraint in sorted(requires.items()):
|
||||
required = str(constraint).strip()
|
||||
minimum = required[2:].strip() if required.startswith(">=") else required
|
||||
current, path = current_version(plugin)
|
||||
if not current:
|
||||
failures.append(f"{plugin} 需要 {required},目前未安裝")
|
||||
continue
|
||||
if version_tuple(current) < version_tuple(minimum):
|
||||
failures.append(f"{plugin} 需要 {required},目前 {current}({path})")
|
||||
|
||||
if failures:
|
||||
print("status=blocked reason=" + ";".join(failures))
|
||||
sys.exit(1)
|
||||
|
||||
print("status=ok reason=相依版本符合:" + ", ".join(f"{k} {v}" for k, v in sorted(requires.items())))
|
||||
sys.exit(0)
|
||||
PY
|
||||
@@ -0,0 +1,82 @@
|
||||
# config-spec.tsv — jsc 技能組的設定規格表。體檢(/jsc-cli:doctor)與設定(/jsc-cli:setup)共用這一份。
|
||||
#
|
||||
# 這張表是「必要或選擇」的唯一判準。掃描原始碼分不出必要與選擇,也分不出哪些是執行期內部
|
||||
# 變數,所以判準用手寫規格表,掃描只負責抓出漏登錄的項目(scan-config.sh orphans)。
|
||||
# 新增環境變數或設定檔時,同時補一列進來,體檢才看得到它。
|
||||
#
|
||||
# 欄位:key<TAB>kind<TAB>scope<TAB>required<TAB>default<TAB>verify<TAB>fix<TAB>desc
|
||||
# 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 一句繁中說明,直接印給使用者看
|
||||
#
|
||||
# wiki 存取庫分兩路:目錄頁({TYPE}_CONTENTS)全部住 JSC_WIKI_REPO_CONTENTS 解出的專用存取庫,
|
||||
# 內容頁({TYPE}_{HASH})才看自己的型別變數。目錄頁不退回型別變數,型別變數也管不到目錄頁。
|
||||
#
|
||||
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_CONTENTS env global no JSC_WIKI_REPO wiki-repo ask 全部目錄頁({TYPE}_CONTENTS)所在存取庫;沒有型別變數可退,只退 JSC_WIKI_REPO
|
||||
JSC_WIKI_REPO_QUESTION env global no JSC_WIKI_REPO wiki-repo ask QUESTION_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_PLAN env global no JSC_WIKI_REPO wiki-repo ask PLAN_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_ANALYZE env global no JSC_WIKI_REPO wiki-repo ask ANALYZE_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_DELIVER env global no JSC_WIKI_REPO wiki-repo ask DELIVER_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_MAINTAIN env global no JSC_WIKI_REPO none - 保留待用,不必設定:MAINTAIN 只有目錄頁 MAINTAIN_CONTENTS,沒有內容頁,目錄頁又已改看 JSC_WIKI_REPO_CONTENTS,這一列目前沒有頁面可管;留著這一列是為了讓孤兒掃描認得這個變數
|
||||
JSC_WIKI_REPO_REPO env global no JSC_WIKI_REPO wiki-repo ask REPO_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_LOG env global no JSC_WIKI_REPO wiki-repo ask LOG_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_LEARN env global no JSC_WIKI_REPO wiki-repo ask LEARN_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_ERROR env global no JSC_WIKI_REPO wiki-repo ask ERROR_{HASH} 內容頁所在存取庫
|
||||
JSC_WIKI_REPO_CHECK env global no JSC_WIKI_REPO wiki-repo ask CHECK_{HASH} 內容頁所在存取庫,體檢紀錄寫在這裡
|
||||
JSC_WIKI_REPO_REPORT env global no JSC_WIKI_REPO wiki-repo ask REPORT_{HASH} 內容頁所在存取庫,年月週日報表寫在這裡
|
||||
JSC_VERSION_GUARD env global no on set ask 設成 off 可完全略過版本前置檢查,離線工作時用
|
||||
JSC_VERSION_TTL env global no 600 set ask 版本查詢快取秒數
|
||||
JSC_RESTART_GATE env global no on set ask 設成 off 可略過部署後的重啟提示閘門,判讀在 jsc-hooks
|
||||
JSC_WIKI_REPO_SKILLSET env global no JSC_WIKI_REPO wiki-repo ask SKILLSET_{HASH} 內容頁所在的 {owner}/{repo},技能組異動報告寫在這裡
|
||||
JSC_WIKI_REPO_TOOLING env global no JSC_WIKI_REPO wiki-repo ask TOOLING_{HASH} 內容頁所在的 {owner}/{repo},技能盤點寫在這裡
|
||||
JSC_WIKI_REPO_MONITOR env global no JSC_WIKI_REPO wiki-repo ask MONITOR_{HASH} 內容頁所在存取庫,助理巡檢紀錄寫在這裡
|
||||
JSC_LANG_GUARD env global no on set ask 設成 off 可關閉繁中編碼與簡體字守門,誤判時用
|
||||
JSC_COMMENT_SCOPE env global no on set ask 設成 off 可關閉註解夾帶文件編號的守門,誤判時用
|
||||
JSC_CHANGED_FILE internal runtime no - none - 非 Claude CLI 傳入的變更檔路徑,註解範圍與繁中編碼守門讀它
|
||||
JSC_SIMPLIFIED_FILE internal runtime no - none - 自訂簡體字表路徑,ste100-lint.sh 讀它取代內建字表
|
||||
JSC_PR_WATCH_INTERVAL env global no 60 set ask pr-watch.sh 輪詢 PR 狀態的間隔秒數,實作在 jsc-gitea/tools/pr-watch.sh
|
||||
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_HOME/update-guide.md file global no - file manual 這台機器的更新指引,由 jsc-cli/tools/write-guides.sh 產生;缺了就重跑 /jsc-cli:deploy
|
||||
$JSC_HOME/remove-guide.md file global no - file manual 這台機器的移除指引,由 jsc-cli/tools/write-guides.sh 產生;缺了就重跑 /jsc-cli:deploy
|
||||
$JSC_HOME/restart-required.d file global no - none - 部署收尾寫下的重啟狀態檔目錄,一支 CLI 一份,執行期暫態;該 CLI 那份不存在代表這支沒有待重啟的部署,jsc-hooks 讀它提示重啟
|
||||
.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 檔路徑
|
||||
|
Executable
+674
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env sh
|
||||
# deploy.sh — 對單一 CLI 執行 jsc 技能組的安裝、更新或解除安裝。
|
||||
# 用法:
|
||||
# deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]
|
||||
# -n 或 --dry-run(或 JSC_DEPLOY_DRYRUN=1):只印指令,不執行。
|
||||
# {domain} 裸名或帶 jsc- 前綴皆可(例:ask 或 jsc-ask),腳本會自動去掉前綴再組
|
||||
# jsc-{domain}@jsc;marketplace.json 的 plugins[].name 本身就帶前綴,不必事先剝掉。
|
||||
# 輸出(TSV,一行一筆):
|
||||
# cmd<TAB>{指令} 即將執行的指令
|
||||
# exit<TAB>{結束碼}<TAB>{指令} 該指令的結束碼;dry-run 時結束碼印「-」
|
||||
# skip<TAB>{domain}<TAB>{原因} 本地 clone 是開發中的樹,略過 git pull
|
||||
# warn<TAB>{domain}<TAB>{原因} 照樣裝下去、但要人知道的事:相依版本不符,
|
||||
# 或本機複本這一輪沒有重新拉取(拿不到
|
||||
# 更新鎖,或 git pull 回非零),那時候
|
||||
# 裝的是磁碟上現有的內容,可能不是最新版
|
||||
# compat<TAB>codex<TAB>{舊路徑}<TAB>{新路徑} codex 舊版快取路徑補成指向新版的相容連結
|
||||
# note<TAB>{cli}<TAB>{原因} 非逐指令的說明(例:kiro 整批改走複製退路的理由)
|
||||
# link<TAB>{domain}<TAB>{狀態}<TAB>{連結路徑}<TAB>{指向或原因} current 連結農場這一條的刷新結果
|
||||
# restart<TAB>{路徑} 這次寫下的重啟狀態檔。install 與 update
|
||||
# 一律寫,整輪判定成 fail 也照寫:外掛
|
||||
# 已經換了一部分,這時候更需要重啟
|
||||
# requires<TAB>{domain}<TAB>{檢查結果} update 前的 jsc.requires 檢查
|
||||
# result<TAB>{cli}<TAB>{mode}<TAB>{domain 清單}<TAB>{ok|fail}
|
||||
# 結束碼:全部指令成功 0;任一指令失敗 1;參數錯誤 2。skip、warn、note、link 不算失敗,
|
||||
# 但呼叫端要據實回報。
|
||||
# link 那幾行講的是 $JSC_HOME/current/ 這一組不帶版本號的符號連結:一個 domain 一條,
|
||||
# 指向該外掛在快取裡帶版本號的實體目錄。技能文件裡所有跨外掛的腳本呼叫都以那一層當根,
|
||||
# 因為它不帶版本號、寫得進權限允許清單。install 與 update 收尾會把整組連結刷新一次,
|
||||
# uninstall 收尾會清掉指向已消失的那幾條。狀態欄的五個值:
|
||||
# ok 連結已經指到這次實際安裝的版本目錄
|
||||
# removed 指向已消失,這一條清掉了
|
||||
# skip 這一輪不動它,原因寫在最後一欄
|
||||
# fail 該指的目標取不到或連結建不起來,這一條維持原樣,指向可能還是舊版本
|
||||
# dryrun 乾跑,只算出會指到哪裡,沒有真的寫
|
||||
# update 前每個 domain 都先跑一次同目錄的 check-requires.sh,它的四種結束碼分流如下:
|
||||
# 0 相依符合,或沒有宣告相依 → 照常更新這個 domain
|
||||
# 1 相依版本不符或缺相依 plugin
|
||||
# → 印一行 warn,這個 domain 照樣更新(跳過會讓落後的 domain 永遠更新不到)
|
||||
# 4 判不出結論(manifest 讀不到、不是有效 JSON、缺 python3)
|
||||
# → 印一行 note,這個 domain 照樣更新。沒有證據不等於落後,話要跟 warn 分開講
|
||||
# 2 或其他 檢查腳本自己出錯(用法錯誤或腳本壞掉)→ 印一行 skip,跳過這個 domain 並記為失敗
|
||||
# marketplace 指令一輪只跑一次:install 與 update 先跑,uninstall 最後跑。
|
||||
# 各 CLI 的細節都收在這裡,SKILL.md 只描述何時呼叫與參數:
|
||||
# antigravity 不接受 gitea URL,先 clone 到本地再從路徑安裝,更新時 pull 同一份。
|
||||
# kiro 的執行檔是 kiro-cli;先探測這個版本認不認得 plugin 子指令(部分版本已經完全
|
||||
# 移除,例:2.18.1),認得才逐一嘗試、失敗才退回複製,不認得就整批直接走複製,
|
||||
# 不逐一撞一次「unrecognized subcommand」才退回。
|
||||
# install 或 update 全數成功時,收尾轉呼叫 jsc-hooks 的 restart-gate.sh require,掛上這支 CLI
|
||||
# 的重啟閘門(狀態檔一支 CLI 一份,落在 $JSC_HOME/restart-required.d/{cli})。擋人邏輯不在
|
||||
# 這裡:由 jsc-hooks 讀該 CLI 那一份提示使用者重啟,逃生門 JSC_RESTART_GATE=off 也由那邊判讀。
|
||||
# 更新指引與移除指引由同目錄的 write-guides.sh 產生,一輪部署跑一次,不在這支腳本裡:
|
||||
# 這支腳本的職責是「對單一 CLI 部署」,指引寫的是整台機器的樣貌。
|
||||
# 環境變數:
|
||||
# JSC_HOME hook 資料目錄,重啟狀態檔寫在這裡(預設 ~/.jsc)
|
||||
# GITEA_HOST Gitea 站台,可省略 scheme(預設 https://gitea.jsc.idv.tw)
|
||||
# JSC_GITEA_OWNER 存取庫的 owner(預設 plugins)
|
||||
# JSC_LOCAL_PLUGINS antigravity/kiro 用的本地 clone 目錄
|
||||
# 預設 $JSC_HOME/plugins(即 ~/.jsc/plugins),刻意不用 ~/plugins:
|
||||
# 那是維護者放開發 checkout 的地方,pull 下去會蓋掉未提交的工作。
|
||||
# 指到開發中的樹(有未提交變更或未推送的 commit)時只印 skip,不 pull。
|
||||
# JSC_KIRO_SKILLS kiro 退路用的 skills 目錄(預設 ~/.kiro/skills)
|
||||
# JSC_DEPLOY_DRYRUN 設為 1 等同 -n
|
||||
set -u
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
# Gitea 站台一律讀 GITEA_HOST(技能準則指定的變數),未設定才用正本站台。
|
||||
HOST="${GITEA_HOST:-https://gitea.jsc.idv.tw}"
|
||||
case "$HOST" in http://*|https://*) ;; *) HOST="https://$HOST" ;; esac
|
||||
HOST="${HOST%/}"
|
||||
OWNER="${JSC_GITEA_OWNER:-plugins}"
|
||||
MKT="$HOST/$OWNER/meta.git"
|
||||
REPO_BASE="$HOST/$OWNER"
|
||||
LOCAL_DIR="${JSC_LOCAL_PLUGINS:-${JSC_HOME:-$HOME/.jsc}/plugins}"
|
||||
KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}"
|
||||
CURRENT_DIR="${JSC_HOME:-$HOME/.jsc}/current"
|
||||
DRYRUN="${JSC_DEPLOY_DRYRUN:-0}"
|
||||
FAILED=0
|
||||
|
||||
# CLI 代號 → 實際執行檔。唯一真實來源是 jsc-hooks 的 hooks/lib.sh cli_bin()。
|
||||
# 這裡保留一份副本,因為這支腳本是整組技能的安裝入口:jsc-hooks 還沒裝上來時
|
||||
# 也要能跑,不能 source 一個可能不存在的檔案。lib.sh 的對應表改了就同步改這裡。
|
||||
cli_bin() { # $1=CLI 代號
|
||||
case "$1" in
|
||||
antigravity) printf 'agy' ;;
|
||||
kiro) printf 'kiro-cli' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
codex_plugin_cache_root() { # $1=plugin name
|
||||
printf '%s/plugins/cache/jsc/%s\n' "${CODEX_HOME:-$HOME/.codex}" "$1"
|
||||
}
|
||||
|
||||
codex_plugin_versions() { # $1=plugin name
|
||||
_dir=$(codex_plugin_cache_root "$1")
|
||||
[ -d "$_dir" ] || return 0
|
||||
find "$_dir" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec basename {} \; 2>/dev/null | sort -V
|
||||
}
|
||||
|
||||
codex_latest_plugin_dir() { # $1=plugin name $2=required file under version dir
|
||||
_plugin="$1"
|
||||
_required="$2"
|
||||
_dir=$(codex_plugin_cache_root "$_plugin")
|
||||
[ -d "$_dir" ] || return 1
|
||||
for _p in "$_dir"/*; do
|
||||
[ -d "$_p" ] || continue
|
||||
[ ! -L "$_p" ] || continue
|
||||
[ -f "$_p/$_required" ] || continue
|
||||
printf '%s\n' "$_p"
|
||||
done | sort -V | tail -n1
|
||||
}
|
||||
|
||||
codex_preserve_old_plugin_cache() { # $1=plugin name $2=required file under version dir;stdin=更新前版本清單
|
||||
[ "$DRYRUN" = 1 ] && return 0
|
||||
_plugin="$1"
|
||||
_required="$2"
|
||||
_new=$(codex_latest_plugin_dir "$_plugin" "$_required" || true)
|
||||
[ -n "$_new" ] || return 0
|
||||
_root=$(codex_plugin_cache_root "$_plugin")
|
||||
while IFS= read -r _version; do
|
||||
[ -n "$_version" ] || continue
|
||||
_old="$_root/$_version"
|
||||
[ "$_old" != "$_new" ] || continue
|
||||
if [ -e "$_old" ] && [ ! -L "$_old" ]; then
|
||||
continue
|
||||
fi
|
||||
ln -sfn "$_new" "$_old" || continue
|
||||
printf 'compat\tcodex\t%s\t%s\n' "$_old" "$_new"
|
||||
done
|
||||
}
|
||||
|
||||
# 連結農場一台機器只有一組,五支 CLI 卻各裝各的副本,所以只能挑一支當基準。
|
||||
# 挑法是固定的偏好順序,取這台機器上第一支裝得到的:
|
||||
# claude 擺第一——現有那幾條連結本來就指向它的快取,換基準等於把每一條寫進技能文件的
|
||||
# 路徑悄悄換到另一份副本上,而文件不會跟著改。
|
||||
# codex 次之,同樣是 CLI 自己的外掛快取寫下來的帶版本目錄,版本號是它裝到什麼就是什麼。
|
||||
# copilot 與 kiro 只當退路:它們的路徑不帶版本號,連結指得到,卻查不出指的是哪一版。
|
||||
# antigravity 一律不當基準。它的來源是本地 clone,那份 clone 允許是維護者的開發樹,
|
||||
# sync_local 碰到未提交的變更還會刻意不 pull;把全機器的路徑指到一份做到一半的樹,
|
||||
# 正好就是這次要修掉的那種無聲跑錯腳本。
|
||||
# 用 command -v 判斷裝沒裝,跟 detect-clis.sh 的認定一致,所以每一支平行跑的 deploy.sh
|
||||
# 都會算出同一個基準,整輪只有一支真的去寫連結。五支都寫的話,平行行程會互相覆寫,
|
||||
# 最後指到哪一份是隨機的,出了事也重現不出來。
|
||||
link_base_cli() {
|
||||
for _b in claude codex copilot kiro; do
|
||||
if command -v "$(cli_bin "$_b")" >/dev/null 2>&1; then
|
||||
printf '%s' "$_b"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 某個外掛在快取根目錄底下的實體版本目錄。claude 與 codex 的快取一個版本一層,
|
||||
# 取版本排序最大、而且真的有 plugin.json 的那一層:裝到一半的目錄沒有 plugin.json,
|
||||
# 挑到它會讓連結指向一份不完整的外掛,而且照樣不會報錯。
|
||||
# 本身就是符號連結的項目也要跳過——舊版相容連結就是那樣,指過去只是多繞一層,
|
||||
# 原目標一被清掉還會跟著一起斷。
|
||||
latest_version_dir() { # $1=某個外掛的快取根目錄
|
||||
[ -d "$1" ] || return 1
|
||||
_vfound=$(
|
||||
for _vp in "$1"/*; do
|
||||
[ -d "$_vp" ] || continue
|
||||
[ -L "$_vp" ] && continue
|
||||
[ -f "$_vp/plugin.json" ] || continue
|
||||
printf '%s\n' "$_vp"
|
||||
done | sort -V | tail -n1
|
||||
)
|
||||
[ -n "$_vfound" ] || return 1
|
||||
printf '%s' "$_vfound"
|
||||
}
|
||||
|
||||
# 基準 CLI 底下這個 domain 的實體目錄。這裡只收得起當基準的那四支,
|
||||
# antigravity 不列:它從來不會被選成基準,補一條沒人走的分支只會讓人以為那也是選項。
|
||||
plugin_dir() { # $1=CLI 代號 $2=domain;印出實體目錄,取不到回 1
|
||||
case "$1" in
|
||||
claude) latest_version_dir "$HOME/.claude/plugins/cache/jsc/jsc-$2" ;;
|
||||
codex) latest_version_dir "$(codex_plugin_cache_root "jsc-$2")" ;;
|
||||
copilot)
|
||||
_pd="${COPILOT_HOME:-$HOME/.copilot}/installed-plugins/jsc/jsc-$2"
|
||||
[ -d "$_pd" ] || return 1
|
||||
printf '%s' "$_pd"
|
||||
;;
|
||||
kiro)
|
||||
_pd="$KIRO_SKILLS/jsc-$2"
|
||||
[ -d "$_pd" ] || return 1
|
||||
printf '%s' "$_pd"
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
link_line() { # $1=domain 或 - $2=狀態 $3=連結路徑 $4=指向或原因
|
||||
printf 'link\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4"
|
||||
}
|
||||
|
||||
# 刷新一條連結。
|
||||
# 失敗只記一行、不中止整輪:這一段跑在外掛都裝好之後,部署本身已經成功了。把整輪記成
|
||||
# 失敗會連帶跳過重啟閘門與 result 行,操作者拿到的是一台明明裝好、卻被說成失敗的機器,
|
||||
# 反而更難處置。連結沒刷新確實是缺陷,但補救方式是一行看得見的紀錄加上人來判斷,
|
||||
# 不是把一次成功的部署丟掉。
|
||||
refresh_one() { # $1=基準 CLI $2=domain
|
||||
_rl="$CURRENT_DIR/jsc-$2"
|
||||
# 目標已經在、又不是符號連結時一律不覆寫。ln -sfn 對著一個實體目錄下手,會把連結建進
|
||||
# 那個目錄裡面(變成 current/jsc-{domain}/jsc-{domain}),農場當場就壞了還不會報錯;
|
||||
# 改成 rm -rf 清掉更糟,那是這支腳本沒建過、也可能沒有第二份的內容。
|
||||
if [ -e "$_rl" ] && [ ! -L "$_rl" ]; then
|
||||
link_line "$2" skip "$_rl" '目標已存在而且不是符號連結,這一條不覆寫,請人工確認'
|
||||
return 0
|
||||
fi
|
||||
if ! _rt=$(plugin_dir "$1" "$2"); then
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
link_line "$2" dryrun "$_rl" "乾跑沒有真的安裝,$1 底下還取不到 jsc-$2 的版本目錄"
|
||||
else
|
||||
link_line "$2" fail "$_rl" "$1 底下找不到 jsc-$2 的版本目錄,這一條維持原樣"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
link_line "$2" dryrun "$_rl" "$_rt"
|
||||
return 0
|
||||
fi
|
||||
if mkdir -p "$CURRENT_DIR" 2>/dev/null && ln -sfn "$_rt" "$_rl" 2>/dev/null; then
|
||||
link_line "$2" ok "$_rl" "$_rt"
|
||||
else
|
||||
link_line "$2" fail "$_rl" '連結建不起來,這一條維持原樣,指向可能還是舊版本'
|
||||
fi
|
||||
}
|
||||
|
||||
# 解除安裝之後清掉指向已經消失的那一條。判準是連結還在、指向卻沒了:
|
||||
# 這一輪只解除安裝其中一支 CLI 時,連結可能還指著另一支 CLI 手上完好的副本,那一條要留著。
|
||||
# 斷掉的連結不清,等於對外宣稱一支已經不在機器上的外掛還裝著,下一輪體檢會照著它報錯狀態。
|
||||
prune_one() { # $1=domain
|
||||
_ql="$CURRENT_DIR/jsc-$1"
|
||||
[ -L "$_ql" ] || return 0
|
||||
[ -e "$_ql" ] && return 0
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
link_line "$1" dryrun "$_ql" '指向已消失,這一條會被清掉'
|
||||
return 0
|
||||
fi
|
||||
if rm -f "$_ql" 2>/dev/null; then
|
||||
link_line "$1" removed "$_ql" '-'
|
||||
else
|
||||
link_line "$1" fail "$_ql" '指向已消失卻清不掉,這一條還是斷的'
|
||||
fi
|
||||
}
|
||||
|
||||
# 整組連結的刷新收尾。跑在各 CLI 的部署動作全部結束之後,不夾在每個 domain 的安裝指令
|
||||
# 中間:連結要指的是寫完的內容,中途去指有機會指到只寫了一半的目錄。
|
||||
refresh_links() {
|
||||
if ! _lbase=$(link_base_cli); then
|
||||
link_line - skip "$CURRENT_DIR" '這台機器一支基準 CLI 都沒裝到,沒有版本目錄可指'
|
||||
return 0
|
||||
fi
|
||||
if [ "$CLI" != "$_lbase" ]; then
|
||||
link_line - skip "$CURRENT_DIR" "這一輪的基準 CLI 是 $_lbase,$CLI 這一支不動連結"
|
||||
return 0
|
||||
fi
|
||||
for _ldom in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install|update) refresh_one "$_lbase" "$_ldom" ;;
|
||||
uninstall) prune_one "$_ldom" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "用法:deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 找 jsc-hooks 的 restart-gate.sh。優先用穩定連結與安裝後複製目錄,再找並排工作樹,
|
||||
# 最後才掃各 CLI 的 plugin 快取。找不到就回非零,由呼叫端據實回報。
|
||||
restart_gate_sh() {
|
||||
if [ -n "${JSC_HOOKS_DIR:-}" ] && [ -f "$JSC_HOOKS_DIR/restart-gate.sh" ]; then
|
||||
printf '%s\n' "$JSC_HOOKS_DIR/restart-gate.sh"; return 0
|
||||
fi
|
||||
_root=$(cd "$(dirname "$0")/.." 2>/dev/null && pwd) || return 1
|
||||
for _c in \
|
||||
"${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks/hooks/restart-gate.sh" \
|
||||
"${JSC_LOCAL_PLUGINS:-${JSC_HOME:-$HOME/.jsc}/plugins}/hooks/hooks/restart-gate.sh" \
|
||||
"${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}/jsc-hooks/hooks/restart-gate.sh" \
|
||||
"$_root/../hooks/hooks/restart-gate.sh" \
|
||||
"$_root/../jsc-hooks/hooks/restart-gate.sh"
|
||||
do
|
||||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
done
|
||||
_c=$(ls -d \
|
||||
"$HOME"/.claude/plugins/cache/*/jsc-hooks/*/hooks/restart-gate.sh \
|
||||
"$HOME"/.codex/plugins/cache/*/jsc-hooks/*/hooks/restart-gate.sh \
|
||||
"$HOME"/.kiro/skills/jsc-hooks/hooks/restart-gate.sh \
|
||||
2>/dev/null | sort | tail -n1)
|
||||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||||
return 1
|
||||
}
|
||||
|
||||
# 相依版本不符只回報、不跳過。跳過的話,落後的 domain 永遠等不到那一版相依,
|
||||
# 也就永遠更新不到,兩個 domain 互相等就形成死鎖。阻擋改放在技能呼叫那一層:
|
||||
# jsc-hooks 的 version-guard.sh 會在版本不足時擋下該 domain 的技能,更新照跑不會壞事。
|
||||
# 檢查腳本自己出錯(退出碼 2 以上:用法錯誤或腳本壞掉)是另一回事,那不是相依不符,
|
||||
# 讀不到結論就不能當成通過,照舊記 FAILED 並跳過這個 domain。
|
||||
check_requires() { # $1=domain;0=繼續更新,1=略過這個 domain(只有檢查腳本自己出錯才會回 1)
|
||||
[ "$MODE" = update ] || return 0
|
||||
sync_local "$1"
|
||||
manifest="$LOCAL_DIR/$1/plugin.json"
|
||||
out=$(sh "$HERE/check-requires.sh" "$CLI" "$manifest" 2>&1)
|
||||
code=$?
|
||||
printf 'requires\t%s\t%s\n' "$1" "$out"
|
||||
if [ "$code" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$code" -eq 1 ]; then
|
||||
printf 'warn\t%s\t%s\n' "$1" "相依版本不符,這次照樣更新;補齊相依版本以前,叫用這個 domain 的技能會被 jsc-hooks 的 version-guard.sh 擋下。要補的版本:$out"
|
||||
return 0
|
||||
fi
|
||||
# 判不出結論跟版本落後要講不同的話。把兩者混成同一句,環境壞掉會被說成版本落後,
|
||||
# 操作者照著去補版本永遠補不到問題點。照樣更新的理由與 version-guard.sh 一致:
|
||||
# 沒有證據不等於落後,缺基礎設施就停掉更新,等於讓環境永遠修不好。
|
||||
if [ "$code" -eq 4 ]; then
|
||||
printf 'note\t%s\t%s\n' "$1" "判不出相依版本,這次照樣更新;成因不是版本落後,先修環境再重跑檢查:$out"
|
||||
return 0
|
||||
fi
|
||||
FAILED=1
|
||||
printf 'skip\t%s\t%s\n' "$1" "相依版本檢查失敗,未更新:$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 掛上部署後的重啟閘門。狀態檔的路徑、格式與判讀全在 jsc-hooks 的 restart-gate.sh,
|
||||
# 這裡只轉呼叫它的 require 子命令,比照 jsc-sdlc 轉呼叫 sdlc-gate.sh wp-lock 的慣例。
|
||||
# 兩邊各拼一份格式就會對不上:2026-08-27 這裡曾自己寫四欄 TSV,而 hooks 那端讀的是
|
||||
# key=value,狀態檔存在卻解不出欄位。格式只能有一個真實來源。
|
||||
# 找不到 restart-gate.sh 時不要自己補寫一份:閘門本來就由 jsc-hooks 判讀,它不在就沒有
|
||||
# 判定點,寫下去只是留一個沒人讀的檔案,還會讓下一輪誤以為閘門掛上了。
|
||||
mark_restart() {
|
||||
[ "$DRYRUN" = 1 ] && return 0
|
||||
case "$MODE" in install|update) ;; *) return 0 ;; esac
|
||||
if ! rg=$(restart_gate_sh); then
|
||||
printf 'note\t%s\t%s\n' "$CLI" "找不到 jsc-hooks 的 restart-gate.sh,這次沒有掛上重啟閘門"
|
||||
return 0
|
||||
fi
|
||||
if JSC_CLI="$CLI" sh "$rg" require "$MODE" $DOMAINS </dev/null; then
|
||||
printf 'restart\t%s\n' "${JSC_HOME:-$HOME/.jsc}/restart-required.d/$CLI"
|
||||
else
|
||||
printf 'note\t%s\t%s\n' "$CLI" "restart-gate.sh require 失敗,這次沒有掛上重啟閘門"
|
||||
fi
|
||||
}
|
||||
|
||||
# 執行一個指令,並印出指令本身與結束碼。失敗就記進 FAILED。
|
||||
run() { # $@=指令
|
||||
printf 'cmd\t%s\n' "$*"
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'exit\t-\t%s\n' "$*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
code=$?
|
||||
printf 'exit\t%s\t%s\n' "$code" "$*"
|
||||
[ "$code" -eq 0 ] || FAILED=1
|
||||
return "$code"
|
||||
}
|
||||
|
||||
# 同 run,但失敗不記進 FAILED——留給「失敗還有退路」的指令用。
|
||||
run_soft() { # $@=指令
|
||||
printf 'cmd\t%s\n' "$*"
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'exit\t-\t%s\n' "$*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
code=$?
|
||||
printf 'exit\t%s\t%s\n' "$code" "$*"
|
||||
return "$code"
|
||||
}
|
||||
|
||||
# 這份 clone 是不是「開發中的樹」。有原因就印出原因,沒有就不印。
|
||||
# 判斷兩件事:有未提交變更,或有還沒推上去的 commit。兩者被 pull 蓋掉都救不回來。
|
||||
local_hold() { # $1=存取庫路徑
|
||||
if [ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]; then
|
||||
printf '有未提交變更'
|
||||
return 0
|
||||
fi
|
||||
up=$(git -C "$1" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null) || return 0
|
||||
[ -n "$up" ] || return 0
|
||||
ahead=$(git -C "$1" rev-list --count "$up..HEAD" 2>/dev/null) || return 0
|
||||
[ "${ahead:-0}" -eq 0 ] || printf '有 %s 個未推送的 commit' "$ahead"
|
||||
}
|
||||
|
||||
# 取一個 domain 的更新鎖。
|
||||
#
|
||||
# 這一份本機複本一台機器只有一份,而五支 CLI 的部署是平行跑的——平行是對的,它們寫的是
|
||||
# 不同的外掛目錄。但這裡不是:五支都會來 pull 同一個目錄,而 git 對同一個存取庫的併發寫入
|
||||
# 沒有保護。
|
||||
# 2026-09-07 實測踩到:同一輪裡兩支 CLI 撞在一起,一支拿不到 ORIG_HEAD.lock、一支的遠端
|
||||
# refs 換不上去,兩支的整輪判定都變成 fail——而外掛其實全部裝好了。那是最難查的一種失效:
|
||||
# 報告說失敗,實際成功,而真正的原因跟部署無關。
|
||||
# 一個 domain 一把鎖,用 mkdir 取:那是檔案系統這一層唯一原子的建立動作,兩支同時 mkdir
|
||||
# 只有一支成功。等不到就改用磁碟上的內容——別人正在拉同一份,硬等下去只是排隊。
|
||||
clone_lock() { # $1=鎖目錄 $2=最多試幾次(每次之間睡一秒,所以約等於秒數);0=拿到了
|
||||
_try=0
|
||||
while :; do
|
||||
mkdir "$1" 2>/dev/null && return 0
|
||||
_try=$((_try + 1))
|
||||
[ "$_try" -ge "$2" ] && break
|
||||
sleep 1
|
||||
done
|
||||
# 等不到還要分一種情形:上一輪中途死掉會留下一個沒有人放的鎖,那種鎖永遠等不到。
|
||||
# 判準取鎖的年紀,門檻放寬到等待秒數的四倍——比任何一次正常的 pull 都久。
|
||||
if [ -d "$1" ]; then
|
||||
_age=$(( $(date +%s) - $(date -r "$1" +%s 2>/dev/null || date +%s) ))
|
||||
if [ "$_age" -gt $(( $2 * 4 )) ]; then
|
||||
rmdir "$1" 2>/dev/null || true
|
||||
mkdir "$1" 2>/dev/null && return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# 截一段訊息到指定長度。cut -c 數的是位元組不是字元,一個多位元組字剛好被切成兩半時
|
||||
# 尾巴會留一個替代字元,而亂碼不影響結束碼、沒有人會來報。截完再過一次 iconv -c 丟掉
|
||||
# 那個不完整的序列;iconv 不在這台機器上就退回原樣。
|
||||
clip1() { # $1=位元組上限;讀標準輸入
|
||||
_raw=$(tr '\n' ' ' | cut -c"1-$1")
|
||||
_cln=$(printf '%s' "$_raw" | iconv -c -f UTF-8 -t UTF-8 2>/dev/null)
|
||||
if [ -n "$_cln" ]; then printf '%s' "$_cln"; else printf '%s' "$_raw"; fi
|
||||
}
|
||||
|
||||
# 把某 domain 的存取庫抓到本地:有 .git 就 pull,沒有就 clone。
|
||||
# 目標是開發中的樹時只印 skip,改用現地內容安裝,不 pull:這支腳本可以被指到任何
|
||||
# 目錄,蓋掉維護者未提交或未推送的工作救不回來,安裝一份舊內容還能重跑。
|
||||
sync_local() { # $1=domain
|
||||
dir="$LOCAL_DIR/$1"
|
||||
lock="$LOCAL_DIR/.lock-$1"
|
||||
if [ -d "$dir/.git" ]; then
|
||||
hold=$(local_hold "$dir")
|
||||
if [ -n "$hold" ]; then
|
||||
printf 'skip\t%s\t%s %s,未執行 git pull\n' "$1" "$dir" "$hold"
|
||||
return 0
|
||||
fi
|
||||
# 乾跑一步都不能動,連鎖都不取:取鎖是建目錄,那已經是寫入了。
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'cmd\t%s\n' "git -C $dir pull"
|
||||
printf 'exit\t-\t%s\n' "git -C $dir pull"
|
||||
return 0
|
||||
fi
|
||||
if ! clone_lock "$lock" 30; then
|
||||
printf 'warn\t%s\t%s\n' "$1" "$dir 的更新鎖等了 30 秒還拿不到,別的 CLI 正在拉同一份;這一輪改用磁碟上現有的內容,沒有重新拉取"
|
||||
return 0
|
||||
fi
|
||||
printf 'cmd\t%s\n' "git -C $dir pull"
|
||||
_out=$(git -C "$dir" pull 2>&1); _rc=$?
|
||||
rmdir "$lock" 2>/dev/null || true
|
||||
printf 'exit\t%s\t%s\n' "$_rc" "git -C $dir pull"
|
||||
if [ "$_rc" -ne 0 ]; then
|
||||
# 複本已經在磁碟上,拉不動不等於裝不了:安裝改用現有內容,那可能是舊版。
|
||||
# 這裡刻意不記 FAILED。記了整輪會判成 fail,而 fail 那條路徑會連重啟閘門一起跳過
|
||||
# ——外掛換了一半而沒有人被告知要重啟,比一句「拉不動」嚴重得多。
|
||||
# 但一定要印出來:安靜地裝一份舊內容,是這一組工具最怕的那種失效。
|
||||
printf 'warn\t%s\t%s\n' "$1" "git pull 回 $_rc,這一輪用磁碟上現有的內容安裝,可能不是最新版:$(printf '%s' "$_out" | clip1 160)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
run git clone "$REPO_BASE/$1.git" "$dir"
|
||||
return 0
|
||||
fi
|
||||
if ! clone_lock "$lock" 60; then
|
||||
FAILED=1
|
||||
printf 'warn\t%s\t%s\n' "$1" "$dir 還不存在,而 clone 鎖等了 60 秒拿不到,這個 domain 這一輪沒有本機複本可以裝"
|
||||
return 1
|
||||
fi
|
||||
run git clone "$REPO_BASE/$1.git" "$dir"
|
||||
_rc=$?
|
||||
rmdir "$lock" 2>/dev/null || true
|
||||
return "$_rc"
|
||||
}
|
||||
|
||||
# claude、copilot、kiro-cli 共用的 plugin 指令組。
|
||||
marketplace_cli() { # $1=執行檔
|
||||
case "$MODE" in
|
||||
install)
|
||||
run "$1" plugin marketplace add "$MKT"
|
||||
for d in $DOMAINS; do run "$1" plugin install "jsc-$d@jsc"; done
|
||||
;;
|
||||
update)
|
||||
run "$1" plugin marketplace update jsc
|
||||
for d in $DOMAINS; do check_requires "$d" && run "$1" plugin update "jsc-$d@jsc"; done
|
||||
;;
|
||||
uninstall)
|
||||
for d in $DOMAINS; do run "$1" plugin uninstall "jsc-$d@jsc"; done
|
||||
run "$1" plugin marketplace remove jsc
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# codex 沒有 plugin update 子指令,只有 add、list、marketplace、remove。
|
||||
# marketplace upgrade 只重抓 marketplace 快照,而 jsc 的 marketplace.json 只列各網域的
|
||||
# git URL、不含版本,所以那份檔案內容不會變,codex 一律回「already up to date」並結束碼 0。
|
||||
# 已安裝外掛的版本是 plugin add 當下決定的,快取不會被連帶重抓——換句話說,只跑
|
||||
# marketplace upgrade 的話,指令全部成功而版本一個都沒動,是最難察覺的那種失敗。
|
||||
# 正確做法是照樣逐網域 plugin add:codex 的 add 會就地升級到快照裡的最新版。
|
||||
deploy_codex() {
|
||||
bin=$(cli_bin codex)
|
||||
case "$MODE" in
|
||||
install)
|
||||
run "$bin" plugin marketplace add "$MKT"
|
||||
for d in $DOMAINS; do run "$bin" plugin add "jsc-$d@jsc"; done
|
||||
;;
|
||||
update)
|
||||
_old_cli_versions=$(codex_plugin_versions jsc-cli)
|
||||
_old_hooks_versions=$(codex_plugin_versions jsc-hooks)
|
||||
run "$bin" plugin marketplace upgrade jsc
|
||||
for d in $DOMAINS; do
|
||||
check_requires "$d" && run "$bin" plugin add "jsc-$d@jsc"
|
||||
if [ "$d" = cli ]; then
|
||||
printf '%s\n' "$_old_cli_versions" | codex_preserve_old_plugin_cache jsc-cli tools/check-requires.sh
|
||||
elif [ "$d" = hooks ]; then
|
||||
printf '%s\n' "$_old_hooks_versions" | codex_preserve_old_plugin_cache jsc-hooks hooks/session-timer.sh
|
||||
fi
|
||||
done
|
||||
;;
|
||||
uninstall)
|
||||
for d in $DOMAINS; do run "$bin" plugin remove "jsc-$d@jsc"; done
|
||||
run "$bin" plugin marketplace remove jsc
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
deploy_antigravity() {
|
||||
bin=$(cli_bin antigravity)
|
||||
for d in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install)
|
||||
sync_local "$d"
|
||||
run "$bin" plugin install "$LOCAL_DIR/$d"
|
||||
;;
|
||||
update)
|
||||
check_requires "$d" || continue
|
||||
sync_local "$d"
|
||||
run "$bin" plugin uninstall "jsc-$d"
|
||||
run "$bin" plugin install "$LOCAL_DIR/$d"
|
||||
;;
|
||||
uninstall)
|
||||
run "$bin" plugin uninstall "jsc-$d"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# plugin 指令不支援時的退路:從本地 clone 複製整個 plugin 的可用內容。
|
||||
#
|
||||
# 技能文件會直接引用同伴目錄,例如 jsc-sdlc 的 implement 要跑 jsc-sdlc/tools/wp-gate.sh
|
||||
# 與 jsc-gitea/tools/pr-watch.sh,jsc-sdlc 的 analyze 要讀 references/consensus.md。
|
||||
# 這些引用在 SKILL.md 裡是完成條件,不是選配。只複製 skills 目錄,kiro 會拿到一份
|
||||
# 要求跑腳本、腳本卻不在機器上的技能,比不更新更糟——所以 tools、references、
|
||||
# templates、hooks 與 plugin.json 一併複製。
|
||||
#
|
||||
# 目標路徑刻意維持 $KIRO_SKILLS/jsc-{domain}/,跨 plugin 的引用(jsc-gitea/tools/…)
|
||||
# 以 $KIRO_SKILLS 為根就解析得到。
|
||||
#
|
||||
# 每個子目錄都用「先 mkdir,再複製 src/. 到 dst/」的寫法:目標目錄已存在時,
|
||||
# cp -R src dst/ 會把來源塞進 dst/{名稱}/{名稱},第二次更新就多一層。
|
||||
kiro_copy() { # $1=domain
|
||||
sync_local "$1"
|
||||
dest="$KIRO_SKILLS/jsc-$1"
|
||||
run mkdir -p "$dest"
|
||||
run cp -R "$LOCAL_DIR/$1/skills/." "$dest/"
|
||||
for sub in tools references templates hooks; do
|
||||
if [ -d "$LOCAL_DIR/$1/$sub" ]; then
|
||||
run mkdir -p "$dest/$sub"
|
||||
run cp -R "$LOCAL_DIR/$1/$sub/." "$dest/$sub/"
|
||||
fi
|
||||
done
|
||||
# plugin.json 讓 version-guard.sh 之類的呼叫端查得到本機版本。缺了不算錯誤,
|
||||
# 只是查不到版本而已,所以不進 run、也不影響整體結束碼。
|
||||
[ -f "$LOCAL_DIR/$1/plugin.json" ] && run cp "$LOCAL_DIR/$1/plugin.json" "$dest/plugin.json"
|
||||
return 0
|
||||
}
|
||||
|
||||
# kiro-cli 是否認得 plugin 子指令:一次性偵測,探測本身不印 cmd/exit(不是部署動作,
|
||||
# 印出來只會讓人誤以為那也是一次失敗的部署嘗試)。部分版本(例:2.18.1)完全沒有這個
|
||||
# 子指令,逐一嘗試再退回複製,會先洗出一長串看似失敗、實則設計內的錯誤訊息。
|
||||
kiro_has_plugin_cmd() {
|
||||
"$bin" --help-all 2>/dev/null | grep -qE '^ plugin( |$)'
|
||||
}
|
||||
|
||||
deploy_kiro() {
|
||||
bin=$(cli_bin kiro)
|
||||
if kiro_has_plugin_cmd; then
|
||||
case "$MODE" in
|
||||
install) run_soft "$bin" plugin marketplace add "$MKT" ;;
|
||||
update) run_soft "$bin" plugin marketplace update jsc ;;
|
||||
esac
|
||||
for d in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install)
|
||||
run_soft "$bin" plugin install "jsc-$d@jsc" || kiro_copy "$d"
|
||||
;;
|
||||
update)
|
||||
check_requires "$d" || continue
|
||||
run_soft "$bin" plugin update "jsc-$d@jsc" || kiro_copy "$d"
|
||||
;;
|
||||
uninstall)
|
||||
run_soft "$bin" plugin uninstall "jsc-$d@jsc" || run rm -rf "$KIRO_SKILLS/jsc-$d"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf 'note\tkiro\t%s\n' "此版本 kiro-cli 沒有 plugin 子指令,改走本地複製(git pull 或 clone 後,複製 skills、tools、references、templates、hooks 與 plugin.json)"
|
||||
for d in $DOMAINS; do
|
||||
case "$MODE" in
|
||||
install) kiro_copy "$d" ;;
|
||||
update) check_requires "$d" && kiro_copy "$d" ;;
|
||||
uninstall) run rm -rf "$KIRO_SKILLS/jsc-$d" ;;
|
||||
esac
|
||||
done
|
||||
[ "$MODE" = uninstall ] && run_soft "$bin" plugin marketplace remove jsc
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-n|--dry-run) DRYRUN=1; shift ;;
|
||||
esac
|
||||
|
||||
[ $# -ge 3 ] || usage
|
||||
MODE=$1
|
||||
CLI=$2
|
||||
shift 2
|
||||
|
||||
# 每個 domain 一律去掉開頭的 jsc-:往下每處都自己組 jsc-$d@jsc,收到已帶前綴的名字
|
||||
# (marketplace.json 的 plugins[].name 就是這樣,例如 jsc-ask)會兜成 jsc-jsc-ask@jsc,
|
||||
# 讓 claude/copilot/antigravity/kiro 全部裝不上、更新不了。這裡正規化一次,
|
||||
# 呼叫端不管傳哪種格式都能正常動作,不必每個呼叫端各自記得先去前綴。
|
||||
_domains=""
|
||||
for _d in "$@"; do
|
||||
case "$_d" in
|
||||
jsc-*) _d="${_d#jsc-}" ;;
|
||||
esac
|
||||
_domains="$_domains $_d"
|
||||
done
|
||||
DOMAINS="${_domains# }"
|
||||
|
||||
case "$MODE" in
|
||||
install|update|uninstall) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
case "$CLI" in
|
||||
claude) marketplace_cli "$(cli_bin claude)" ;;
|
||||
copilot) marketplace_cli "$(cli_bin copilot)" ;;
|
||||
codex) deploy_codex ;;
|
||||
antigravity) deploy_antigravity ;;
|
||||
kiro) deploy_kiro ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
refresh_links
|
||||
|
||||
# 重啟閘門一律先掛,不等整輪判定。
|
||||
#
|
||||
# 部分失敗代表磁碟上的外掛已經換了一部分,這時候更需要重啟,不是更不需要。
|
||||
# 2026-09-07 實測踩到:一條與部署無關的 git pull 失敗把整輪判成 fail,而原本的寫法是
|
||||
# 「判定成功才掛閘門」——於是那一支 CLI 沒有被掛上閘門,沒有人被告知要重啟,而它跑的
|
||||
# 是舊版程式碼。一道只在成功時才生效的提醒,在最需要它的那一次不會出現。
|
||||
mark_restart
|
||||
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
printf 'result\t%s\t%s\t%s\tok\n' "$CLI" "$MODE" "$DOMAINS"
|
||||
exit 0
|
||||
fi
|
||||
printf 'result\t%s\t%s\t%s\tfail\n' "$CLI" "$MODE" "$DOMAINS"
|
||||
exit 1
|
||||
@@ -2,6 +2,9 @@
|
||||
# detect-clis.sh — 找出已安裝的 AI CLI 工具與執行檔路徑。
|
||||
# 輸出(TSV): name<TAB>path<TAB>version(未安裝的不輸出)
|
||||
# 支援: claude / codex / copilot / antigravity(agy) / kiro
|
||||
# 結束碼: 0=一律回 0。probe 找不到執行檔就跳過那一列,不算失敗,所以沒有其他碼。
|
||||
# 一台機器一個 CLI 都沒裝也是 0,只是不輸出任何一列。呼叫端要看輸出有幾列,
|
||||
# 不要拿結束碼判斷有沒有裝到 CLI。
|
||||
set -u
|
||||
probe() { # $1=顯示名 $2=執行檔名 $3=版本參數
|
||||
p=$(command -v "$2" 2>/dev/null) || return 0
|
||||
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env sh
|
||||
# list-models.sh — 讀各 AI CLI 的設定檔,列出模型與目前使用中的模型。
|
||||
# 輸出(TSV,一行一個模型):cli<TAB>model<TAB>in-use(in-use 為 yes 或 no)
|
||||
# 設定檔不存在或裡面沒寫模型,就不輸出該 CLI 的列;一律 exit 0。
|
||||
# 設定檔位置只寫在這裡,SKILL.md 不再抄一份:
|
||||
# claude ${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json 的 model;ANTHROPIC_MODEL 可覆寫使用中的模型
|
||||
# codex ${CODEX_HOME:-~/.codex}/config.toml:頂層 model 為使用中,[profiles.*] 的 model 併入清單
|
||||
# copilot ~/.copilot/settings.json,退回 ~/.config/copilot/settings.json
|
||||
# antigravity ~/.antigravity/settings.json,退回 ~/.config/antigravity/settings.json
|
||||
# kiro ~/.kiro/settings/cli.json,退回 ~/.config/kiro/settings.json
|
||||
# 五個 CLI 都沒有「列出可用模型」的指令,所以清單只到設定檔寫出來的模型。
|
||||
# 沒有列的 CLI 由 skills/models 的 sub agent 補上預設模型並標註「預設推定」。
|
||||
# 結束碼: 0=一律回 0。設定檔不存在、讀不到或裡面沒寫模型,都只是少掉那個 CLI 的列,
|
||||
# 不算失敗,所以沒有其他碼。呼叫端要看輸出有沒有該 CLI 的列,
|
||||
# 不要拿結束碼判斷讀不讀得到設定檔。
|
||||
set -u
|
||||
|
||||
# 取第一個存在的檔案;都不存在就不印。
|
||||
first_file() { # $@=候選路徑
|
||||
for f in "$@"; do
|
||||
[ -f "$f" ] && { printf '%s\n' "$f"; return 0; }
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# 取 JSON 檔裡某個鍵的所有字串值,一行一個,順序照檔案。
|
||||
json_val() { # $1=檔案 $2=鍵名
|
||||
[ -n "$1" ] || return 0
|
||||
[ -f "$1" ] || return 0
|
||||
grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" "$1" 2>/dev/null \
|
||||
| sed -e 's/^[^:]*:[[:space:]]*"//' -e 's/"$//'
|
||||
return 0
|
||||
}
|
||||
|
||||
# 取 TOML 檔裡所有 model 設定值,一行一個,順序照檔案。
|
||||
# 只認行首的 model=,所以 model_reasoning_effort 這類鍵不會誤中。
|
||||
toml_val() { # $1=檔案
|
||||
[ -n "$1" ] || return 0
|
||||
[ -f "$1" ] || return 0
|
||||
grep -E '^[[:space:]]*model[[:space:]]*=' "$1" 2>/dev/null \
|
||||
| sed -e 's/^[^=]*=[[:space:]]*//' -e 's/[[:space:]]*#.*$//' \
|
||||
-e 's/^"//' -e 's/"[[:space:]]*$//' -e "s/^'//" -e "s/'[[:space:]]*$//"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 印出某 CLI 的列:使用中的模型排第一,重複的只留一筆。
|
||||
emit() { # $1=cli $2=使用中的模型(可空) $3=模型清單(換行分隔,可空)
|
||||
printf '%s\n%s\n' "$2" "$3" | awk -v cli="$1" -v cur="$2" '
|
||||
{ gsub(/^[ \t]+|[ \t]+$/, "") }
|
||||
$0 == "" { next }
|
||||
seen[$0]++ { next }
|
||||
{ printf "%s\t%s\t%s\n", cli, $0, ($0 == cur ? "yes" : "no") }'
|
||||
}
|
||||
|
||||
# claude
|
||||
cfg=$(first_file "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur="${ANTHROPIC_MODEL:-}"
|
||||
[ -n "$cur" ] || cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit claude "$cur" "$list"
|
||||
|
||||
# codex
|
||||
cfg=$(first_file "${CODEX_HOME:-$HOME/.codex}/config.toml")
|
||||
list=$(toml_val "$cfg")
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit codex "$cur" "$list"
|
||||
|
||||
# copilot
|
||||
cfg=$(first_file "$HOME/.copilot/settings.json" "$HOME/.config/copilot/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit copilot "$cur" "$list"
|
||||
|
||||
# antigravity
|
||||
cfg=$(first_file "$HOME/.antigravity/settings.json" "$HOME/.config/antigravity/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit antigravity "$cur" "$list"
|
||||
|
||||
# kiro
|
||||
cfg=$(first_file "$HOME/.kiro/settings/cli.json" "$HOME/.config/kiro/settings.json")
|
||||
list=$(json_val "$cfg" model)
|
||||
cur=$(printf '%s\n' "$list" | head -n1)
|
||||
emit kiro "$cur" "$list"
|
||||
|
||||
exit 0
|
||||
+10
-8
@@ -10,6 +10,8 @@
|
||||
# model-config.sh get {stage} 印出該階段解析後的模型鏈(逗號分隔);未設定不印。兩種情況都 exit 0。
|
||||
# model-config.sh list 每階段一行:stage<TAB>chain<TAB>source(project 或 global);未設定的 chain 與 source 印「-」。
|
||||
# model-config.sh resolve {stage} 印出該階段「目前 CLI 可用的第一個模型」單一名稱;鏈為空或無法判定時不印,exit 0。
|
||||
# 結束碼:0=查詢完成(查得到、查不到都算完成,判斷交給呼叫端)
|
||||
# 2=用法錯誤(子命令不認得,或階段不在 plan、analyze、implement、maintain 之內)
|
||||
set -u
|
||||
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
@@ -65,13 +67,13 @@ current_cli() {
|
||||
}
|
||||
|
||||
# resolve 子指令:印出「目前 CLI 可用的第一個模型」單一名稱。
|
||||
# 簡化說明:這個 repo 目前沒有「列出某 CLI 實際安裝/可用模型」的 shell 級機制
|
||||
# ——現有的 skills/models 是靠 sub agent 讀各 CLI 的設定檔(~/.claude/settings.json、
|
||||
# ~/.codex/config.toml 等),屬於 LLM 才能做的判讀,沒辦法在這支 POSIX sh 腳本裡重現。
|
||||
# 因此這裡只用 current_cli() 抓到的「目前 CLI 名稱」這個既有訊號做記錄,
|
||||
# 實際判斷可用性一律簡化為「直接取模型鏈的第一個模型」,不逐一檢查該模型是否真的能用。
|
||||
# 之後若要做到依 CLI 實際可用模型過濾,可以在這裡比對 current_cli 的結果與
|
||||
# references/model-tags.md/detect-clis.sh 的輸出,逐一嘗試鏈上模型直到找到可用的。
|
||||
# 簡化說明:可用性一律簡化為「直接取模型鏈的第一個模型」,不逐一檢查該模型是否真的能用。
|
||||
# tools/list-models.sh 已經能讀各 CLI 設定檔列出模型,但五個 CLI 都沒有
|
||||
# 「列出可用模型」的指令,設定檔沒寫出來的模型仍然查不到;拿這份清單當白名單過濾,
|
||||
# 會把合法但沒寫進設定檔的模型誤判成不可用,比不過濾更糟。
|
||||
# 因此這裡只用 current_cli() 抓到的「目前 CLI 名稱」這個既有訊號做記錄。
|
||||
# 之後若要依 CLI 實際可用模型過濾,等各 CLI 提供列出模型的指令,再比對
|
||||
# list-models.sh 的輸出,逐一嘗試鏈上模型直到找到可用的。
|
||||
resolve_first_usable() { # $1=階段
|
||||
chain=$(resolve "$1" | cut -f1)
|
||||
[ -n "$chain" ] || return 0
|
||||
@@ -83,7 +85,7 @@ resolve_first_usable() { # $1=階段
|
||||
|
||||
usage() {
|
||||
echo "用法:model-config.sh get {plan|analyze|implement|maintain} | list | resolve {plan|analyze|implement|maintain}" >&2
|
||||
exit 1
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
|
||||
+8
-1
@@ -16,6 +16,13 @@
|
||||
# stage<TAB>{階段}<TAB>{必要標籤以逗號分隔,不限標籤時為 any}
|
||||
# model<TAB>{模型鍵}<TAB>{能力標籤以逗號分隔}
|
||||
#
|
||||
# 全域結束碼(每個子命令都照這一套,呼叫端只要認碼就好):
|
||||
# 0 通過或正常完成
|
||||
# 1 模型缺標籤(gate 印 FAIL:{標籤}),或 sync 解析不到對照表因而沒寫出檔案
|
||||
# 2 用法錯誤:子命令或參數不對(含 UNKNOWN-MODEL 與 UNKNOWN-STAGE 這兩種「無法判定」)
|
||||
# 「模型不合格」與「這支腳本被叫錯」刻意分成 1 與 2 兩碼:兩者共用 exit 1 的話,呼叫端會把
|
||||
# 自己打錯的指令讀成「這個模型不夠格」,把好模型默默丟掉,而且錯在哪永遠不會浮出來。
|
||||
#
|
||||
# gate 的輸出與 exit code:
|
||||
# PASS exit 0 模型具備該階段全部必要標籤
|
||||
# FAIL:{缺少的標籤} exit 1 模型缺標籤,不得執行該階段
|
||||
@@ -131,7 +138,7 @@ gate() { # $1=階段 $2=模型 id
|
||||
|
||||
usage() {
|
||||
echo "用法:model-tags.sh dump | sync | stage {階段} | model {模型 id} | gate {階段} {模型 id}" >&2
|
||||
exit 1
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/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,每行一項):
|
||||
# item<TAB>scope<TAB>required<TAB>actual<TAB>expect<TAB>fix<TAB>verdict
|
||||
# verdict = ok 設定妥當
|
||||
# default 未設定,走預設值,可以正常運作
|
||||
# unset 選擇性項目未設定,沒有預設值,相關功能會降級
|
||||
# missing 必要項目缺了,相關技能跑不動
|
||||
# invalid 有值但驗不過(目錄不在、認證失敗、格式不對)
|
||||
# skipped 離線模式略過,未取得結論
|
||||
# 最後固定一行 summary<TAB>{missing 數}<TAB>{invalid 數}<TAB>{unset 數}<TAB>{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
|
||||
# 只排掉樣板佔位字那一個名字,不做前綴比對:CONTENTS、MONITOR 這些是真的頁面類型,
|
||||
# 排寬了就等於把整族存取庫變數從孤兒掃描裡挖掉,漏登錄再也看不出來。
|
||||
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
|
||||
# JSC_HOME 常常正是還沒設定的那一個,展開別列的 $JSC_HOME/... 卻要它有值;
|
||||
# 只在這個子 shell 套用 JSC_HOME 規格列自己的預設值,不外流到主流程,
|
||||
# 免得 JSC_HOME 那一列自己的檢查被連帶改成「已設定」。
|
||||
( set +u; : "${JSC_HOME:=$HOME/.jsc}"; 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
|
||||
Executable
+264
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env sh
|
||||
# write-guides.sh — 產生這台機器專屬的更新指引與移除指引。
|
||||
# 用法:
|
||||
# write-guides.sh [-n] {install|update} {domain} [domain...]
|
||||
# -n 或 --dry-run:只印會寫到哪兩個檔案,不寫入。
|
||||
# {domain} 裸名或帶 jsc- 前綴皆可,交給 deploy.sh 正規化。
|
||||
# 產出(兩個檔案,整份覆寫):
|
||||
# $JSC_HOME/update-guide.md 下次更新照著做的指引
|
||||
# $JSC_HOME/remove-guide.md 要整組移除時照著做的指引
|
||||
# 輸出(TSV,一行一筆):
|
||||
# cli<TAB>{cli}<TAB>{path}<TAB>{version} 這台機器上偵測到的 CLI
|
||||
# plan<TAB>{path} dry-run 時會寫入的檔案
|
||||
# wrote<TAB>{path} 實際寫入的檔案
|
||||
# note<TAB>{原因} 非致命的說明(例:一個 CLI 都沒偵測到)
|
||||
# 結束碼:兩份都寫成 0;參數錯誤 2;目錄或檔案寫不進去 4。
|
||||
#
|
||||
# 指引內容一律依實際偵測結果生成,不寫死:
|
||||
# CLI 清單來自 detect-clis.sh;每支 CLI 的實際指令來自 deploy.sh 的 dry-run(-n)輸出。
|
||||
# 指令字面因此與 deploy.sh 真正會跑的完全一致——各 CLI 的差異只有 deploy.sh 一個真實來源,
|
||||
# 這裡再抄一份就會有兩套指令,改了一邊忘了另一邊,指引就開始騙人。
|
||||
# kiro 走不走本地複製退路,也是讀 deploy.sh 的 note 行判斷,不自己再探測一次。
|
||||
# 環境變數:
|
||||
# JSC_HOME 指引寫入的目錄(預設 ~/.jsc)
|
||||
# GITEA_HOST Gitea 站台,可省略 scheme(預設 https://gitea.jsc.idv.tw)
|
||||
# JSC_GITEA_OWNER 存取庫的 owner(預設 plugins)
|
||||
# JSC_LOCAL_PLUGINS antigravity/kiro 用的本地 clone 目錄(預設 $JSC_HOME/plugins)
|
||||
# JSC_KIRO_SKILLS kiro 退路用的 skills 目錄(預設 ~/.kiro/skills)
|
||||
set -u
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
DETECT="$HERE/detect-clis.sh"
|
||||
DEPLOY="$HERE/deploy.sh"
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
HOST="${GITEA_HOST:-https://gitea.jsc.idv.tw}"
|
||||
case "$HOST" in http://*|https://*) ;; *) HOST="https://$HOST" ;; esac
|
||||
HOST="${HOST%/}"
|
||||
OWNER="${JSC_GITEA_OWNER:-plugins}"
|
||||
MKT="$HOST/$OWNER/meta.git"
|
||||
LOCAL_DIR="${JSC_LOCAL_PLUGINS:-$JSC_HOME/plugins}"
|
||||
KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}"
|
||||
UPDATE_GUIDE="$JSC_HOME/update-guide.md"
|
||||
REMOVE_GUIDE="$JSC_HOME/remove-guide.md"
|
||||
DRYRUN=0
|
||||
TAB=$(printf '\t')
|
||||
|
||||
usage() {
|
||||
echo "用法:write-guides.sh [-n] {install|update} {domain} [domain...]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-n|--dry-run) DRYRUN=1; shift ;;
|
||||
esac
|
||||
|
||||
[ $# -ge 2 ] || usage
|
||||
MODE=$1
|
||||
shift
|
||||
case "$MODE" in install|update) ;; *) usage ;; esac
|
||||
[ -x "$DETECT" ] || [ -f "$DETECT" ] || { echo "找不到 detect-clis.sh:$DETECT" >&2; exit 4; }
|
||||
[ -f "$DEPLOY" ] || { echo "找不到 deploy.sh:$DEPLOY" >&2; exit 4; }
|
||||
|
||||
DOMAINS=""
|
||||
for _d in "$@"; do
|
||||
case "$_d" in jsc-*) _d="${_d#jsc-}" ;; esac
|
||||
DOMAINS="$DOMAINS $_d"
|
||||
done
|
||||
DOMAINS="${DOMAINS# }"
|
||||
|
||||
if [ "$DRYRUN" = 1 ]; then
|
||||
printf 'plan\t%s\n' "$UPDATE_GUIDE"
|
||||
printf 'plan\t%s\n' "$REMOVE_GUIDE"
|
||||
fi
|
||||
|
||||
# 偵測到的 CLI,一行一筆 name<TAB>path<TAB>version。
|
||||
CLIS=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 4; }
|
||||
NOTES=$(mktemp) || { rm -f "$CLIS"; echo "無法建立暫存檔" >&2; exit 4; }
|
||||
CMDS=$(mktemp) || { rm -f "$CLIS" "$NOTES"; echo "無法建立暫存檔" >&2; exit 4; }
|
||||
cleanup() { rm -f "$CLIS" "$NOTES" "$CMDS"; }
|
||||
|
||||
sh "$DETECT" > "$CLIS" 2>/dev/null || true
|
||||
while IFS="$TAB" read -r name path ver; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
printf 'cli\t%s\t%s\t%s\n' "$name" "$path" "$ver"
|
||||
done < "$CLIS"
|
||||
[ -s "$CLIS" ] || printf 'note\t%s\n' "一個 CLI 都沒偵測到,指引只會寫下 marketplace 與 domain 清單"
|
||||
|
||||
# 跑一次 deploy.sh 的 dry-run,把 cmd 行與 note 行分開收好。
|
||||
# $1=mode $2=cli;cmd 行寫進 $CMDS,note 行寫進 $NOTES,兩個檔案每次都重寫。
|
||||
harvest() {
|
||||
: > "$CMDS"
|
||||
: > "$NOTES"
|
||||
JSC_DEPLOY_DRYRUN=1 sh "$DEPLOY" -n "$1" "$2" $DOMAINS 2>/dev/null \
|
||||
| while IFS="$TAB" read -r kind a b; do
|
||||
case "$kind" in
|
||||
cmd) printf '%s\n' "$a" >> "$CMDS" ;;
|
||||
note) printf '%s\n' "$b" >> "$NOTES" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# 這支 CLI 的 plugin 安裝方式,一句話。kiro 讀 $NOTES 判斷走不走複製退路。
|
||||
# codex 分模式講:它的 update 有陷阱(沒有 plugin update),移除段落講這件事只會讓人分心。
|
||||
cli_method() { # $1=cli $2=mode
|
||||
case "$1" in
|
||||
claude|copilot)
|
||||
printf '原生 plugin 指令,來源是統一 marketplace `jsc`' ;;
|
||||
codex)
|
||||
if [ "$2" = uninstall ]; then
|
||||
printf '原生 plugin 指令;移除用 `plugin remove`,不是 `plugin uninstall`'
|
||||
else
|
||||
printf '原生 plugin 指令;沒有 `plugin update` 子指令,更新一律重跑 `plugin add` 就地升級'
|
||||
fi ;;
|
||||
antigravity)
|
||||
printf '不接受 Gitea URL,先把存取庫 clone 到 `%s`,再從本地路徑安裝' "$LOCAL_DIR" ;;
|
||||
kiro)
|
||||
if [ -s "$NOTES" ]; then
|
||||
printf '這個版本沒有 `plugin` 子指令,整批走本地複製退路(複製到 `%s`)' "$KIRO_SKILLS"
|
||||
else
|
||||
printf '原生 plugin 指令;單一 domain 失敗才退回本地複製(複製到 `%s`)' "$KIRO_SKILLS"
|
||||
fi ;;
|
||||
*)
|
||||
printf '原生 plugin 指令' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 指令區塊:一支 CLI 一個 sh 圍欄,內容就是 deploy.sh 會跑的每一行。
|
||||
emit_cmds() { # $1=mode $2=cli
|
||||
harvest "$1" "$2"
|
||||
printf '### %s\n\n' "$2"
|
||||
printf '%s\n\n' "$(cli_method "$2" "$1")"
|
||||
if [ -s "$NOTES" ]; then
|
||||
while IFS= read -r n; do
|
||||
[ -n "$n" ] || continue
|
||||
printf '> %s\n\n' "$n"
|
||||
done < "$NOTES"
|
||||
fi
|
||||
printf '```sh\n'
|
||||
if [ -s "$CMDS" ]; then
|
||||
cat "$CMDS"
|
||||
else
|
||||
printf '# deploy.sh 這一輪沒有要對 %s 執行的指令\n' "$2"
|
||||
fi
|
||||
printf '```\n\n'
|
||||
}
|
||||
|
||||
domain_list() {
|
||||
_out=""
|
||||
for d in $DOMAINS; do
|
||||
[ -z "$_out" ] && _out="\`jsc-$d\`" || _out="$_out、\`jsc-$d\`"
|
||||
done
|
||||
printf '%s' "$_out"
|
||||
}
|
||||
|
||||
domain_count() {
|
||||
set -- $DOMAINS
|
||||
printf '%s' "$#"
|
||||
}
|
||||
|
||||
STAMP=$(date '+%Y-%m-%d %H:%M:%S %z' 2>/dev/null || date)
|
||||
# 主機與帳號分兩欄寫。併成一欄要用斜線隔開,那是 STE100 的並列斜線違規。
|
||||
HOSTNAME_NOW=$(hostname 2>/dev/null || echo 未知主機)
|
||||
USER_NOW=$(id -un 2>/dev/null || echo 未知帳號)
|
||||
|
||||
# 兩份指引共用的抬頭:說清楚這份檔案是誰產生的、什麼時候產生的、依據是什麼。
|
||||
header() { # $1=標題 $2=一句用途
|
||||
printf '# %s\n\n' "$1"
|
||||
printf '%s\n\n' "$2"
|
||||
printf '本檔由 `jsc-cli/tools/write-guides.sh` 產生,內容依產生當下這台機器的偵測結果生成。重跑 `/jsc-cli:deploy` 會整份覆寫。\n\n'
|
||||
printf '| 項目 | 內容 |\n| --- | --- |\n'
|
||||
printf '| 產生時間 | %s |\n' "$STAMP"
|
||||
printf '| 產生時機 | `/jsc-cli:deploy` 的 %s 收尾 |\n' "$MODE"
|
||||
printf '| 主機 | %s |\n' "$HOSTNAME_NOW"
|
||||
printf '| 登入帳號 | %s |\n' "$USER_NOW"
|
||||
printf '| Marketplace | `jsc`(`%s`) |\n' "$MKT"
|
||||
printf '| 安裝 token | `jsc-{domain}@jsc` |\n'
|
||||
printf '| Domain 數量 | %s |\n' "$(domain_count)"
|
||||
printf '| Domain 清單 | %s |\n' "$(domain_list)"
|
||||
printf '| 資料目錄 | `%s` |\n\n' "$JSC_HOME"
|
||||
}
|
||||
|
||||
cli_table() {
|
||||
printf '## 這台機器偵測到的 CLI\n\n'
|
||||
if [ -s "$CLIS" ]; then
|
||||
printf '| CLI | 執行檔 | 版本 | plugin 安裝方式 |\n| --- | --- | --- | --- |\n'
|
||||
while IFS="$TAB" read -r name path ver; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
harvest "$MODE" "$name"
|
||||
printf '| %s | `%s` | %s | %s |\n' "$name" "$path" "${ver:-未知}" "$(cli_method "$name" "$MODE")"
|
||||
done < "$CLIS"
|
||||
printf '\n'
|
||||
else
|
||||
printf '一個 CLI 都沒偵測到。裝好任一支 CLI 之後重跑 `/jsc-cli:deploy`,這份指引才會有指令可循。\n\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# 各 CLI 的指令段落。$1=mode
|
||||
cli_sections() {
|
||||
[ -s "$CLIS" ] || return 0
|
||||
while IFS="$TAB" read -r name path ver; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
emit_cmds "$1" "$name"
|
||||
done < "$CLIS"
|
||||
}
|
||||
|
||||
write_update_guide() {
|
||||
header 'jsc 技能組更新指引' '整組 jsc plugins 要更新時,照這份指引走。首選一律是 `/jsc-cli:deploy` 選 `update`;下面的指令是同一件事的手動版本,CLI 壞掉或不想開工作階段時用。'
|
||||
cli_table
|
||||
printf '## 更新指令\n\n'
|
||||
printf '每支 CLI 各自一組,指令與 `jsc-cli/tools/deploy.sh update {cli} {domain}...` 實際會跑的完全相同。\n\n'
|
||||
cli_sections update
|
||||
printf '## 更新完要做的事\n\n'
|
||||
printf '1. 重新接線 hooks:`/jsc-hooks:hooks-install`。plugin 換版後接線檔會過期。\n'
|
||||
printf '2. 關閉目前的工作階段並重新啟動。新的技能內容要重開工作階段才載入得到。\n'
|
||||
printf '3. 體檢一次:`/jsc-cli:doctor`。確認版本、接線與設定都對得上。\n\n'
|
||||
printf '`%s/{cli}` 這份檔案存在,就代表那一支 CLI 有一輪部署還沒重啟。狀態檔一支 CLI 一份,重啟只清自己那份,別支的閘門不受影響。重啟提示由 `jsc-hooks` 判讀,逃生門是 `JSC_RESTART_GATE=off`。\n\n' "$JSC_HOME/restart-required.d"
|
||||
printf '## 移除\n\n'
|
||||
printf '要整組移除看 `%s`。\n' "$REMOVE_GUIDE"
|
||||
}
|
||||
|
||||
write_remove_guide() {
|
||||
header 'jsc 技能組移除指引' '整組 jsc plugins 要移除時,照這份指引走。首選一律是 `/jsc-cli:deploy` 選 `uninstall`;下面的指令是同一件事的手動版本。'
|
||||
cli_table
|
||||
printf '## 移除指令\n\n'
|
||||
printf '每支 CLI 各自一組,指令與 `jsc-cli/tools/deploy.sh uninstall {cli} {domain}...` 實際會跑的完全相同。marketplace 指令一輪只跑一次,排在各 domain 之後。\n\n'
|
||||
cli_sections uninstall
|
||||
printf '## 移除後的殘留物\n\n'
|
||||
printf 'plugin 指令只管 plugin 自己。下面這些是 jsc 另外寫在機器上的東西,要不要清掉自己決定:\n\n'
|
||||
printf '| 路徑 | 內容 | 清掉的影響 |\n| --- | --- | --- |\n'
|
||||
printf '| `%s` | hook 資料目錄:工作階段計時、用量統計、版本快取、模型標籤表、備份 |' "$JSC_HOME"
|
||||
printf ' 用量統計與設定備份一起消失,救不回來 |\n'
|
||||
printf '| `%s` | antigravity 與 kiro 用的本地 clone | 下次安裝要重新 clone |\n' "$LOCAL_DIR"
|
||||
printf '| `%s` | kiro 複製退路的技能目錄 | kiro 的 jsc 技能完全消失 |\n' "$KIRO_SKILLS"
|
||||
printf '| shell rc 檔的 `# jsc-config` 段落 | `/jsc-cli:setup` 寫入的環境變數 | jsc 相關環境變數失效,段落外的內容不受影響 |\n\n'
|
||||
printf 'rc 檔那一段用 `jsc-cli/tools/apply-config.sh unset {KEY}` 逐項移除,或直接手動刪掉 `# jsc-config` 與 `# /jsc-config` 之間的內容。動手前先備份。\n\n'
|
||||
printf '## 移除完要做的事\n\n'
|
||||
printf '1. 關閉目前的工作階段並重新啟動。已載入的技能要重開工作階段才會消失。\n'
|
||||
printf '2. 確認殘留:`/jsc-cli:doctor` 若還列得出 jsc 版本,代表某支 CLI 還留著 plugin。\n'
|
||||
}
|
||||
|
||||
[ "$DRYRUN" = 1 ] && { cleanup; exit 0; }
|
||||
|
||||
mkdir -p "$JSC_HOME" 2>/dev/null || { cleanup; echo "無法建立目錄:$JSC_HOME" >&2; exit 4; }
|
||||
|
||||
rc=0
|
||||
tmp="$JSC_HOME/.update-guide.md.tmp"
|
||||
if write_update_guide > "$tmp" 2>/dev/null && mv "$tmp" "$UPDATE_GUIDE" 2>/dev/null; then
|
||||
printf 'wrote\t%s\n' "$UPDATE_GUIDE"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
echo "無法寫入 $UPDATE_GUIDE" >&2
|
||||
rc=4
|
||||
fi
|
||||
|
||||
tmp="$JSC_HOME/.remove-guide.md.tmp"
|
||||
if write_remove_guide > "$tmp" 2>/dev/null && mv "$tmp" "$REMOVE_GUIDE" 2>/dev/null; then
|
||||
printf 'wrote\t%s\n' "$REMOVE_GUIDE"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
echo "無法寫入 $REMOVE_GUIDE" >&2
|
||||
rc=4
|
||||
fi
|
||||
|
||||
cleanup
|
||||
exit "$rc"
|
||||
Reference in New Issue
Block a user