Files
cli/tools/deploy.sh
T
jiantw83 49cd4d3ee4 fix(deploy): 修正 domain 前綴歧義與 kiro-cli 探測
What: deploy.sh 收到帶 jsc- 前綴的 domain 名時自動去掉前綴再組 jsc-{domain}@jsc;deploy_kiro 先探測這個版本的 kiro-cli 認不認得 plugin 子指令,不認得就整批直接走本地複製退路。
Why: marketplace.json 的 plugins[].name 本身就帶 jsc- 前綴,SKILL.md 只寫「domain 名單來自 marketplace」沒講清楚要不要去前綴,實際執行時餵進去兜成 jsc-jsc-ask@jsc 雙重前綴,claude、copilot、antigravity、kiro 四支 CLI 的更新全部第一輪失敗。另外 kiro-cli 2.18.1 這個版本已經完全沒有 plugin 子指令,逐一嘗試再退回複製會先洗出一長串看似失敗、實則設計內的錯誤訊息。
How: 腳本層正規化 domain 參數(去前綴),比只改文件更可靠——不管呼叫端傳哪種格式都對。kiro 的探測用 kiro-cli --help-all 抓子指令清單,一次性判斷,不逐一撞錯誤才退回;探測本身不算部署動作,不印 cmd/exit。SKILL.md 同步補上前綴說明。
Who: jsc-cli:deploy 的執行正確性與輸出可讀性。
2026-08-26 11:30:18 +08:00

262 lines
9.4 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
}
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
;;
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