#!/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"