Files
cli/tools/write-guides.sh
T
jiantw83 d1da14c778 feat(write-guides): 新增指引產生腳本,部署收尾寫下本機的更新與移除指引
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 收尾,以及日後要手動更新或整組移除的操作者。
2026-08-27 16:34:16 +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` 這個檔案存在,就代表有一輪部署還沒重啟。重啟提示由 `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"