From d1da14c77857bdf96abcf05c877d68a7f98d4d53 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 16:34:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(write-guides):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8C=87=E5=BC=95=E7=94=A2=E7=94=9F=E8=85=B3=E6=9C=AC=EF=BC=8C?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=94=B6=E5=B0=BE=E5=AF=AB=E4=B8=8B=E6=9C=AC?= =?UTF-8?q?=E6=A9=9F=E7=9A=84=E6=9B=B4=E6=96=B0=E8=88=87=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 `tools/write-guides.sh`(`write-guides.sh [-n] {install|update} {domain}...`),產生 `$JSC_HOME/update-guide.md` 與 `$JSC_HOME/remove-guide.md` 兩份指引,兩份都整份覆寫。輸出 TSV 四種行別:`cli`(偵測到的 CLI)、`plan`(dry-run 時會寫入的檔案)、`wrote`(實際寫入的檔案)、`note`(非致命說明);結束碼 0 寫成、2 參數錯誤、4 目錄或檔案寫不進去。 Why:更新與移除這兩件事原本只存在於技能內文裡。CLI 壞掉、沒有工作階段、或是換人接手的時候,機器上找不到任何一份寫著「這台機器要怎麼更新、怎麼移除」的東西,只能回頭讀技能。指引落成本機檔案,不開工作階段也照著走得完。 How:內容一律依實際偵測結果生成,不寫死。CLI 清單來自 `detect-clis.sh`;每支 CLI 的指令字面直接取自 `deploy.sh -n` 的輸出,所以指引寫的就是 `deploy.sh` 真正會跑的指令——各 CLI 的差異只有 `deploy.sh` 一個真實來源,這裡再抄一份就會有兩套指令,改了一邊忘了另一邊,指引就開始騙人。kiro 走不走本地複製退路,也是讀 `deploy.sh` 的 `note` 行判斷,不自己再探測一次。獨立成一支腳本、不併進 `deploy.sh`:`deploy.sh` 的職責是「對單一 CLI 部署」,一輪部署會逐個 CLI 呼叫它,而指引寫的是整台機器的樣貌,只該產生一次;併進去還會與 `deploy.sh -n` 形成雙向遞迴。寫檔走暫存檔再 `mv`,寫一半不會留下半份指引。移除指引另外列出 plugin 指令管不到的殘留物(`$JSC_HOME`、本地 clone、kiro 技能目錄、rc 檔的 `# jsc-config` 段落)與各自清掉的影響。 Who:`/jsc-cli:deploy` 的 install 與 update 收尾,以及日後要手動更新或整組移除的操作者。 --- tools/write-guides.sh | 264 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100755 tools/write-guides.sh diff --git a/tools/write-guides.sh b/tools/write-guides.sh new file mode 100755 index 0000000..3142e26 --- /dev/null +++ b/tools/write-guides.sh @@ -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{cli}{path}{version} 這台機器上偵測到的 CLI +# plan{path} dry-run 時會寫入的檔案 +# wrote{path} 實際寫入的檔案 +# note{原因} 非致命的說明(例:一個 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,一行一筆 namepathversion。 +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` 這個檔案存在,就代表有一輪部署還沒重啟。重啟提示由 `jsc-hooks` 判讀,逃生門是 `JSC_RESTART_GATE=off`。\n\n' "$JSC_HOME/restart-required" + 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"