feat(deploy): 部署收尾刷新 current 連結農場

$JSC_HOME/current 是一組不帶版本的符號連結,每個外掛一條,指向快取裡帶版本號的實體目錄。技能文件裡所有跨外掛的腳本呼叫都以這一層為根,因為它不帶版本號、寫得進權限允許清單。

問題是沒有任何東西會更新這些連結,只有 wire-cli.sh 會更新 jsc-hooks 那一條。其餘幾條是人手動建的,建好之後就停在當時的版本。實際後果是部署完四個 domain 之後,快取裡是新版,連結卻還指著舊版:助理巡檢照文件的字面路徑跑,跑到的是舊腳本,而其中一個舊版底下根本沒有它要呼叫的檔案。失敗無聲,只有心跳停止,沒人盯就不會有人發現。

部署改成收尾時刷新整組連結。挑部署來做,是因為它本來就知道裝了哪些 domain、裝到哪個版本,資訊最齊。

四個設計決定:

基準 CLI 取 claude、codex、copilot、kiro 之中第一支找得到的,整輪只有那一支寫連結。連結農場只有一組,不可能同時指向五個 CLI 的副本;而五支 CLI 是平行跑的,五支都寫會互相覆寫,最後指到哪一份是隨機的、出事重現不出來。antigravity 一律不當基準,它的來源是本地 clone,而那份 clone 明文允許是維護者的開發樹,把全機器路徑指到做到一半的樹正好是這次要修的那種毛病。

版本目錄取版本排序最大、且真的有 plugin.json、且本身不是符號連結的那一層。要求 plugin.json 是因為裝到一半的目錄沒有它,挑到會讓連結指向不完整的外掛而且照樣不報錯。

解除安裝的判準是「連結還在、指向卻沒了」,不是「這輪解除安裝過這個 domain」。只解除安裝其中一支 CLI 時,連結可能還指著另一支手上完好的副本,那一條必須留著。

連結建立失敗印一行繼續,不記進失敗清單。這一段跑在外掛都裝好之後,部署本身已經成功;記成失敗會連帶跳過重啟閘門,操作者拿到的是一台明明裝好卻被說成失敗的機器。缺陷仍然看得見,因為輸出多了一行。

目標存在但不是符號連結時一律不覆寫,印 skip 要人工處理。ln -sfn 對著實體目錄下手會把連結建進那個目錄裡,農場當場壞掉還不會報錯。

新增 link 行讓呼叫端讀得到每一條連結指到哪裡,狀態五選一。技能文件與行為契約跟著更新,另修正一句因這次改動而失效的敘述:原本寫 current 底下沒有 jsc-cli,刷新之後那條連結會存在,改成講清楚它仍然靠不住,因為第一次建起它的正是這一輪。
This commit is contained in:
2026-09-03 12:19:59 +08:00
parent 253443e94e
commit a439636c4a
4 changed files with 179 additions and 13 deletions
+4 -4
View File
@@ -17,10 +17,10 @@
| 項目 | 內容 |
| --- | --- |
| 觸發時機 | 整組 jsc 技能要在這台機器的每一支已安裝 CLI 上安裝、更新或解除安裝時用。只處理單一技能不用;只想知道版本落後與否,看 doctor 就夠 |
| 關鍵步驟 | 先跑一次 `readlink -f "$JSC_HOME/current"` 解出 `current` 這個目錄的絕對路徑,解到那一層就停,不再往下解成帶版本號的快取路徑——那種路徑放不進允許清單,版本號寫成萬用字元也對不上;同一步再跑 `[ -d "{剛印出來的路徑}" ]` 確認目錄存在,`JSC_HOME` 沒設時它印的是 `/current`、結束碼 0,非空又是絕對路徑,只看那兩項擋不下來。整輪只解這一次,之後每一次跨外掛腳本呼叫都填成那個字面絕對路徑,不留 `$JSC_HOME` 也不留波浪號;技能自己那四支 `tools/*.sh` 不在 `current` 底下(那裡只有 `jsc-assist`、`jsc-gitea`、`jsc-hooks`),根目錄取自 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}...、安裝或更新後把 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 判定乾淨、兩份指引都寫成是 ok,偵測不到任何 CLI、整輪沒下過任何外掛命令是 blocked,marketplace 讀不到或每支 CLI 都失敗、冒煙結果出現 No such file 是 failed,部分 CLI 成功部分失敗、有 domain 被 skip、或 write-guides.sh 回 4 讓機器沒有最新指引是 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 收尾轉呼叫)、jsc-ask:ask、jsc-hooks:hooks-install |
| 完成條件 | `current` 那個目錄在第一步就解出一條存在的絕對路徑(用 `[ -d ]` 查過,而且沒有再往下解成帶版本號的快取路徑),技能自己那四支腳本也有一條字面絕對的外掛根目錄可用,後續每一支腳本都用這兩條之一組成的字面絕對路徑呼叫;每一支偵測到的 CLI 都回報結束碼與 result 行,每個 skip、warn、compat 行都照實列出;安裝或更新還要拿到 hooks-install 對每支 CLI 的總結,兩份指引都印出 wrote,收尾印出重啟指示與兩份指引路徑。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼 |
| 可驗證跡象 | 各 CLI 的外掛目錄多出或少掉 jsc-{domain}:claude 與 codex 在各自的 plugin 快取、copilot 在 installed-plugins、antigravity 與 kiro 走 $JSC_LOCAL_PLUGINS 的本地 clone 與 $JSC_KIRO_SKILLS 的複製。$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/`,沒有一支寫成裸的相對路徑 |
| 關鍵步驟 | 先跑一次 `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 收尾轉呼叫)、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 的複製。$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