Files
cli/tools/write-guides.sh
T
jiantw83 98781a331f fix(deploy): 部署工具的重啟狀態檔路徑改為一支 CLI 一份
What:`tools/deploy.sh` 的 `restart` 那一行改印 `$JSC_HOME/restart-required.d/{CLI 代號}`,檔頭註解一併改寫成「轉呼叫 `restart-gate.sh require` 掛上這支 CLI 的閘門」。`tools/write-guides.sh` 產生的更新指引,狀態檔說明改成一支 CLI 一份、重啟只清自己那份、別支的閘門不受影響。`tools/config-spec.tsv` 的 `$JSC_HOME/restart-required` 那一列改成 `$JSC_HOME/restart-required.d` 目錄,說明改為「該 CLI 那份不存在代表這支沒有待重啟的部署」。

Why:`jsc-hooks` 這一輪把狀態檔改成一支 CLI 一份,路徑從單一檔案變成狀態目錄底下的一份。`deploy.sh` 印的是操作者接下來要看的檔案路徑,印錯就指向一個不存在的檔案;`write-guides.sh` 寫出的更新指引是下一輪部署的依據,留著舊路徑會讓人以為刪掉那個檔案就能解除閘門;`config-spec.tsv` 是 `/jsc-cli:doctor` 比對設定落點的依據,路徑對不上就查不到這份執行期暫態。

How:三處都只跟著改路徑與說明,掛閘門的動作本來就是轉呼叫 `jsc-hooks` 的 `restart-gate.sh require`,這裡不重寫一份判定,也不自己組狀態檔內容——路徑與格式的唯一來源留在 `restart-gate.sh`。`config-spec.tsv` 那一列的型別仍是 `global` 的執行期暫態、備份與還原都維持 `none`,欄位數不變。

Who:`jsc-cli:deploy` 的部署收尾與更新指引,對齊 `jsc-hooks` 一支 CLI 一份的重啟狀態檔。
2026-08-27 18:49:58 +08:00

265 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"