What:
改寫 `tools/deploy.sh` 的 `kiro_copy()`。原本只複製 `skills/.` 一個目錄,現在同時複製 `tools`、`references`、`templates`、`hooks` 四個子目錄與 `plugin.json`。四個子目錄逐一判斷來源是否存在,存在才複製;`plugin.json` 缺了不算錯誤,函式一律回傳 0。`deploy_kiro()` 印給使用者看的 `note` 訊息也一併改寫,說清楚退路實際複製了哪些內容。
Why:
`kiro-cli 2.18.1` 已經沒有 `plugin` 子指令,kiro 只能走本地複製這條退路。過去只複製 `skills/` 還能動,是因為舊版 SKILL.md 沒叫 kiro 跑同伴目錄裡的腳本。2026-08-27 放行的這批技能(cli 0.1.7、sdlc 0.1.9、git 0.0.8、gitea 0.1.5 等)把 `jsc-sdlc/tools/wp-gate.sh owns`、`jsc-gitea/tools/pr-watch.sh`、`jsc-git/tools/base-branch.sh --derive` 寫成 SKILL.md 的完成條件,kiro 於是拿到一份「指令要求跑腳本、腳本卻不在機器上」的技能,`jsc-sdlc:implement` 與 `jsc-git:pr` 會直接卡住,比不更新更糟。缺 `references/` 也讓 `consensus.md`、`branch.md`、`guidelines.md` 在 kiro 上讀不到。
How:
目標路徑維持 `$KIRO_SKILLS/jsc-{domain}/`,跨 plugin 的引用(例如 `jsc-gitea/tools/…`)以 `$KIRO_SKILLS` 為根就解析得到,不必改動任何 SKILL.md。每個子目錄都用「先 `mkdir -p` 目標,再複製 `src/.` 到 `dst/`」的寫法:目標目錄已經存在時,`cp -R src dst/` 會把來源塞進 `dst/{名稱}/{名稱}`,第二次更新就多疊一層。`plugin.json` 以單檔複製處理,讓 `version-guard.sh` 之類的呼叫端查得到本機版本。函式尾端明確 `return 0`,避免 `plugin.json` 不存在時的測試結果變成函式結束碼。
Who:
jsc-cli:deploy 技能的 kiro 部署退路。
292 lines
11 KiB
Bash
Executable File
292 lines
11 KiB
Bash
Executable File
#!/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 複製整個 plugin 的可用內容。
|
||
#
|
||
# 技能文件會直接引用同伴目錄,例如 jsc-sdlc 的 implement 要跑 jsc-sdlc/tools/wp-gate.sh
|
||
# 與 jsc-gitea/tools/pr-watch.sh,jsc-sdlc 的 analyze 要讀 references/consensus.md。
|
||
# 這些引用在 SKILL.md 裡是完成條件,不是選配。只複製 skills 目錄,kiro 會拿到一份
|
||
# 要求跑腳本、腳本卻不在機器上的技能,比不更新更糟——所以 tools、references、
|
||
# templates、hooks 與 plugin.json 一併複製。
|
||
#
|
||
# 目標路徑刻意維持 $KIRO_SKILLS/jsc-{domain}/,跨 plugin 的引用(jsc-gitea/tools/…)
|
||
# 以 $KIRO_SKILLS 為根就解析得到。
|
||
#
|
||
# 每個子目錄都用「先 mkdir,再複製 src/. 到 dst/」的寫法:目標目錄已存在時,
|
||
# cp -R src dst/ 會把來源塞進 dst/{名稱}/{名稱},第二次更新就多一層。
|
||
kiro_copy() { # $1=domain
|
||
sync_local "$1"
|
||
dest="$KIRO_SKILLS/jsc-$1"
|
||
run mkdir -p "$dest"
|
||
run cp -R "$LOCAL_DIR/$1/skills/." "$dest/"
|
||
for sub in tools references templates hooks; do
|
||
if [ -d "$LOCAL_DIR/$1/$sub" ]; then
|
||
run mkdir -p "$dest/$sub"
|
||
run cp -R "$LOCAL_DIR/$1/$sub/." "$dest/$sub/"
|
||
fi
|
||
done
|
||
# plugin.json 讓 version-guard.sh 之類的呼叫端查得到本機版本。缺了不算錯誤,
|
||
# 只是查不到版本而已,所以不進 run、也不影響整體結束碼。
|
||
[ -f "$LOCAL_DIR/$1/plugin.json" ] && run cp "$LOCAL_DIR/$1/plugin.json" "$dest/plugin.json"
|
||
return 0
|
||
}
|
||
|
||
# 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、tools、references、templates、hooks 與 plugin.json)"
|
||
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
|