feat(cli): 部署收尾產生更新與移除指引,並掛上部署後重啟閘門 #27

Merged
admin merged 6 commits from feat/skillset-governance/deploy-guides-and-restart-gate into feat/skillset-governance/main 2026-08-27 08:39:02 +00:00
Showing only changes of commit d1da14c778 - Show all commits
+264
View File
@@ -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` 這個檔案存在,就代表有一輪部署還沒重啟。重啟提示由 `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"