Files
cli/tools/deploy.sh
T
jiantw83 63d4185a40 fix(deploy): codex 更新補上逐網域 plugin add
What:deploy_codex 的 update 分支在 marketplace upgrade 之後,補上逐網域的
plugin add,與 install 分支一致。

Why:codex 沒有 plugin update 子指令,只有 add、list、marketplace、remove。
marketplace upgrade 只重抓 marketplace 快照,而 jsc 的 marketplace.json 只列各
網域的 git URL、不含版本,內容不會變,codex 一律回「already up to date」並結束碼 0。
已安裝外掛的版本在 plugin add 當下決定,快取不會被連帶重抓。結果是十個網域裡八個
版本完全沒動,指令卻全部成功——只看結束碼會判定成功,是最難察覺的那種失敗。
實機驗證過:更新前 review 是 0.0.2,跑完 marketplace upgrade 仍是 0.0.2。

How:update 分支照 install 的寫法逐網域跑 plugin add。實測 codex 的 add 會就地
升級到快照裡的最新版(review 0.0.2 升到 0.0.5),不需要先 remove。同時在函式上方
寫明這個限制與理由,避免後人再把那一行當成多餘的重複而刪掉。

Who:跨 CLI 技能組批次部署。
2026-08-27 10:28:10 +08:00

269 lines
10 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
# deploy.sh — 對單一 CLI 執行 jsc 技能組的安裝、更新或解除安裝。
# 用法:
# deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]
# -n 或 --dry-run(或 JSC_DEPLOY_DRYRUN=1):只印指令,不執行。
# {domain} 裸名或帶 jsc- 前綴皆可(例:ask 或 jsc-ask),腳本會自動去掉前綴再組
# jsc-{domain}@jsc;marketplace.json 的 plugins[].name 本身就帶前綴,不必事先剝掉。
# 輸出(TSV,一行一筆):
# cmd<TAB>{指令} 即將執行的指令
# exit<TAB>{結束碼}<TAB>{指令} 該指令的結束碼;dry-run 時結束碼印「-」
# skip<TAB>{domain}<TAB>{原因} 本地 clone 是開發中的樹,略過 git pull
# note<TAB>{cli}<TAB>{原因} 非逐指令的說明(例:kiro 整批改走複製退路的理由)
# result<TAB>{cli}<TAB>{mode}<TAB>{domain 清單}<TAB>{ok|fail}
# 結束碼:全部指令成功 0;任一指令失敗 1;參數錯誤 2。skip、note 不算失敗,但呼叫端要據實回報。
# marketplace 指令一輪只跑一次:install 與 update 先跑,uninstall 最後跑。
# 各 CLI 的細節都收在這裡,SKILL.md 只描述何時呼叫與參數:
# antigravity 不接受 gitea URL,先 clone 到本地再從路徑安裝,更新時 pull 同一份。
# kiro 的執行檔是 kiro-cli;先探測這個版本認不認得 plugin 子指令(部分版本已經完全
# 移除,例:2.18.1),認得才逐一嘗試、失敗才退回複製,不認得就整批直接走複製,
# 不逐一撞一次「unrecognized subcommand」才退回。
# 環境變數:
# GITEA_HOST Gitea 站台,可省略 scheme(預設 https://gitea.jsc.idv.tw)
# JSC_GITEA_OWNER 存取庫的 owner(預設 plugins)
# JSC_LOCAL_PLUGINS antigravity/kiro 用的本地 clone 目錄
# 預設 $JSC_HOME/plugins(即 ~/.jsc/plugins),刻意不用 ~/plugins:
# 那是維護者放開發 checkout 的地方,pull 下去會蓋掉未提交的工作。
# 指到開發中的樹(有未提交變更或未推送的 commit)時只印 skip,不 pull。
# JSC_KIRO_SKILLS kiro 退路用的 skills 目錄(預設 ~/.kiro/skills)
# JSC_DEPLOY_DRYRUN 設為 1 等同 -n
set -u
# Gitea 站台一律讀 GITEA_HOST(技能準則指定的變數),未設定才用正本站台。
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"
REPO_BASE="$HOST/$OWNER"
LOCAL_DIR="${JSC_LOCAL_PLUGINS:-${JSC_HOME:-$HOME/.jsc}/plugins}"
KIRO_SKILLS="${JSC_KIRO_SKILLS:-$HOME/.kiro/skills}"
DRYRUN="${JSC_DEPLOY_DRYRUN:-0}"
FAILED=0
# CLI 代號 → 實際執行檔。唯一真實來源是 jsc-hooks 的 hooks/lib.sh cli_bin()。
# 這裡保留一份副本,因為這支腳本是整組技能的安裝入口:jsc-hooks 還沒裝上來時
# 也要能跑,不能 source 一個可能不存在的檔案。lib.sh 的對應表改了就同步改這裡。
cli_bin() { # $1=CLI 代號
case "$1" in
antigravity) printf 'agy' ;;
kiro) printf 'kiro-cli' ;;
*) printf '%s' "$1" ;;
esac
}
usage() {
echo "用法:deploy.sh [-n] {install|update|uninstall} {claude|codex|copilot|antigravity|kiro} {domain} [domain...]" >&2
exit 2
}
# 執行一個指令,並印出指令本身與結束碼。失敗就記進 FAILED。
run() { # $@=指令
printf 'cmd\t%s\n' "$*"
if [ "$DRYRUN" = 1 ]; then
printf 'exit\t-\t%s\n' "$*"
return 0
fi
"$@"
code=$?
printf 'exit\t%s\t%s\n' "$code" "$*"
[ "$code" -eq 0 ] || FAILED=1
return "$code"
}
# 同 run,但失敗不記進 FAILED——留給「失敗還有退路」的指令用。
run_soft() { # $@=指令
printf 'cmd\t%s\n' "$*"
if [ "$DRYRUN" = 1 ]; then
printf 'exit\t-\t%s\n' "$*"
return 0
fi
"$@"
code=$?
printf 'exit\t%s\t%s\n' "$code" "$*"
return "$code"
}
# 這份 clone 是不是「開發中的樹」。有原因就印出原因,沒有就不印。
# 判斷兩件事:有未提交變更,或有還沒推上去的 commit。兩者被 pull 蓋掉都救不回來。
local_hold() { # $1=存取庫路徑
if [ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]; then
printf '有未提交變更'
return 0
fi
up=$(git -C "$1" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null) || return 0
[ -n "$up" ] || return 0
ahead=$(git -C "$1" rev-list --count "$up..HEAD" 2>/dev/null) || return 0
[ "${ahead:-0}" -eq 0 ] || printf '有 %s 個未推送的 commit' "$ahead"
}
# 把某 domain 的存取庫抓到本地:有 .git 就 pull,沒有就 clone。
# 目標是開發中的樹時只印 skip,改用現地內容安裝,不 pull:這支腳本可以被指到任何
# 目錄,蓋掉維護者未提交或未推送的工作救不回來,安裝一份舊內容還能重跑。
sync_local() { # $1=domain
dir="$LOCAL_DIR/$1"
if [ -d "$dir/.git" ]; then
hold=$(local_hold "$dir")
if [ -n "$hold" ]; then
printf 'skip\t%s\t%s %s,未執行 git pull\n' "$1" "$dir" "$hold"
return 0
fi
run git -C "$dir" pull
else
run git clone "$REPO_BASE/$1.git" "$dir"
fi
}
# claude、copilot、kiro-cli 共用的 plugin 指令組。
marketplace_cli() { # $1=執行檔
case "$MODE" in
install)
run "$1" plugin marketplace add "$MKT"
for d in $DOMAINS; do run "$1" plugin install "jsc-$d@jsc"; done
;;
update)
run "$1" plugin marketplace update jsc
for d in $DOMAINS; do run "$1" plugin update "jsc-$d@jsc"; done
;;
uninstall)
for d in $DOMAINS; do run "$1" plugin uninstall "jsc-$d@jsc"; done
run "$1" plugin marketplace remove jsc
;;
esac
}
# codex 沒有 plugin update 子指令,只有 add、list、marketplace、remove。
# marketplace upgrade 只重抓 marketplace 快照,而 jsc 的 marketplace.json 只列各網域的
# git URL、不含版本,所以那份檔案內容不會變,codex 一律回「already up to date」並結束碼 0。
# 已安裝外掛的版本是 plugin add 當下決定的,快取不會被連帶重抓——換句話說,只跑
# marketplace upgrade 的話,指令全部成功而版本一個都沒動,是最難察覺的那種失敗。
# 正確做法是照樣逐網域 plugin add:codex 的 add 會就地升級到快照裡的最新版。
deploy_codex() {
bin=$(cli_bin codex)
case "$MODE" in
install)
run "$bin" plugin marketplace add "$MKT"
for d in $DOMAINS; do run "$bin" plugin add "jsc-$d@jsc"; done
;;
update)
run "$bin" plugin marketplace upgrade jsc
for d in $DOMAINS; do run "$bin" plugin add "jsc-$d@jsc"; done
;;
uninstall)
for d in $DOMAINS; do run "$bin" plugin remove "jsc-$d@jsc"; done
run "$bin" plugin marketplace remove jsc
;;
esac
}
deploy_antigravity() {
bin=$(cli_bin antigravity)
for d in $DOMAINS; do
case "$MODE" in
install)
sync_local "$d"
run "$bin" plugin install "$LOCAL_DIR/$d"
;;
update)
sync_local "$d"
run "$bin" plugin uninstall "jsc-$d"
run "$bin" plugin install "$LOCAL_DIR/$d"
;;
uninstall)
run "$bin" plugin uninstall "jsc-$d"
;;
esac
done
}
# plugin 指令不支援時的退路:從本地 clone 複製 skills 目錄。
kiro_copy() { # $1=domain
sync_local "$1"
run mkdir -p "$KIRO_SKILLS/jsc-$1"
run cp -R "$LOCAL_DIR/$1/skills/." "$KIRO_SKILLS/jsc-$1/"
}
# kiro-cli 是否認得 plugin 子指令:一次性偵測,探測本身不印 cmd/exit(不是部署動作,
# 印出來只會讓人誤以為那也是一次失敗的部署嘗試)。部分版本(例:2.18.1)完全沒有這個
# 子指令,逐一嘗試再退回複製,會先洗出一長串看似失敗、實則設計內的錯誤訊息。
kiro_has_plugin_cmd() {
"$bin" --help-all 2>/dev/null | grep -qE '^ plugin( |$)'
}
deploy_kiro() {
bin=$(cli_bin kiro)
if kiro_has_plugin_cmd; then
case "$MODE" in
install) run_soft "$bin" plugin marketplace add "$MKT" ;;
update) run_soft "$bin" plugin marketplace update jsc ;;
esac
for d in $DOMAINS; do
case "$MODE" in
install)
run_soft "$bin" plugin install "jsc-$d@jsc" || kiro_copy "$d"
;;
update)
run_soft "$bin" plugin update "jsc-$d@jsc" || kiro_copy "$d"
;;
uninstall)
run_soft "$bin" plugin uninstall "jsc-$d@jsc" || run rm -rf "$KIRO_SKILLS/jsc-$d"
;;
esac
done
return 0
fi
printf 'note\tkiro\t%s\n' "此版本 kiro-cli 沒有 plugin 子指令,改走本地複製(git pull 或 clone 後複製 skills 目錄)"
for d in $DOMAINS; do
case "$MODE" in
install|update) kiro_copy "$d" ;;
uninstall) run rm -rf "$KIRO_SKILLS/jsc-$d" ;;
esac
done
[ "$MODE" = uninstall ] && run_soft "$bin" plugin marketplace remove jsc
return 0
}
case "${1:-}" in
-n|--dry-run) DRYRUN=1; shift ;;
esac
[ $# -ge 3 ] || usage
MODE=$1
CLI=$2
shift 2
# 每個 domain 一律去掉開頭的 jsc-:往下每處都自己組 jsc-$d@jsc,收到已帶前綴的名字
# (marketplace.json 的 plugins[].name 就是這樣,例如 jsc-ask)會兜成 jsc-jsc-ask@jsc,
# 讓 claude/copilot/antigravity/kiro 全部裝不上、更新不了。這裡正規化一次,
# 呼叫端不管傳哪種格式都能正常動作,不必每個呼叫端各自記得先去前綴。
_domains=""
for _d in "$@"; do
case "$_d" in
jsc-*) _d="${_d#jsc-}" ;;
esac
_domains="$_domains $_d"
done
DOMAINS="${_domains# }"
case "$MODE" in
install|update|uninstall) ;;
*) usage ;;
esac
case "$CLI" in
claude) marketplace_cli "$(cli_bin claude)" ;;
copilot) marketplace_cli "$(cli_bin copilot)" ;;
codex) deploy_codex ;;
antigravity) deploy_antigravity ;;
kiro) deploy_kiro ;;
*) usage ;;
esac
if [ "$FAILED" -eq 0 ]; then
printf 'result\t%s\t%s\t%s\tok\n' "$CLI" "$MODE" "$DOMAINS"
exit 0
fi
printf 'result\t%s\t%s\t%s\tfail\n' "$CLI" "$MODE" "$DOMAINS"
exit 1