sync_kiro_config.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. #!/usr/bin/env bash
  2. # =============================================================================
  3. # FranklinWH Toolbox - Kiro CLI 配置单向同步脚本(A → B)
  4. # =============================================================================
  5. # 作用:把「版本库配置源」单向推送到「线上部署目标」。
  6. #
  7. # A 端(版本库源):deploy/kiro/
  8. # B 端(部署目标):${KIRO_HOME}/.kiro 默认 /var/www/fwh-toolbox/server/.kiro
  9. #
  10. # 同步内容(一律 A → B,不回写):
  11. # * steering/ 覆盖同步(rsync --delete,B 端该目录与 A 端一致)
  12. # * agents/ 覆盖同步(rsync --delete)
  13. # * 自定义 skill 扫描 A 端 skills/ 下的目录,逐个覆盖同步到 B 端
  14. # (每个 skill 目录内部 --delete,不影响 B 端其它 skill)
  15. # * skills.sh 生态 skill 读取 skills-sh.list,在 B 端用 `npx skills add` 安装
  16. # (要装新的:先在 skills-sh.list 加一行,再跑本脚本)
  17. #
  18. # 不同步:settings/(kiro-cli 自动生成)。
  19. # 安全:顶层 skills/ 不做 --delete,命令安装的 skill 目录不会被误删。
  20. # 另:禁止把配置同步到用户级 ~/.kiro(B 端不能落在任何真实用户 HOME 下),
  21. # 命中即报错退出;确需解除请设 KIRO_ALLOW_USER_HOME=y。
  22. #
  23. # 用法:
  24. # ./sync_kiro_config.sh # 执行同步
  25. # KIRO_HOME=/path ./sync_kiro_config.sh # 指定 B 端 HOME
  26. # KIRO_DRY_RUN=y ./sync_kiro_config.sh # 预演,只打印动作不落盘
  27. # KIRO_SKILLS_INSTALL=n ./sync_kiro_config.sh # 跳过 skills.sh 命令安装,只同步文件
  28. #
  29. # 环境变量:
  30. # KIRO_HOME B 端 HOME(决定 .kiro 位置) (默认 /var/www/fwh-toolbox/server)
  31. # KIRO_RUN_AS_USER B 端属主 / 运行 kiro-cli 的用户 (默认当前用户)
  32. # A_DIR A 端目录 (默认 <repo>/deploy/kiro)
  33. # KIRO_SKILLS_LIST skills.sh 安装清单文件 (默认 ${A_DIR}/skills-sh.list)
  34. # KIRO_AGENT skills CLI 目标 agent (默认 kiro-cli)
  35. # KIRO_SKILLS_INSTALL 是否命令安装 skills.sh skill (默认 y)
  36. # KIRO_DRY_RUN 只打印不落盘 (y/n) (默认 n)
  37. # KIRO_ALLOW_USER_HOME 解除「禁止同步到用户级 ~/.kiro」保护 (y/n) (默认 n)
  38. # =============================================================================
  39. set -euo pipefail
  40. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  41. DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # deploy/
  42. KIRO_HOME="${KIRO_HOME:-/var/www/fwh-toolbox/server}"
  43. KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}"
  44. A_DIR="${A_DIR:-${DEPLOY_DIR}/kiro}"
  45. B_DIR="${KIRO_HOME%/}/.kiro"
  46. KIRO_SKILLS_LIST="${KIRO_SKILLS_LIST:-${A_DIR}/skills-sh.list}"
  47. KIRO_AGENT="${KIRO_AGENT:-kiro-cli}"
  48. KIRO_SKILLS_INSTALL="${KIRO_SKILLS_INSTALL:-y}"
  49. KIRO_DRY_RUN="${KIRO_DRY_RUN:-n}"
  50. # 占位符替换目标值(须与 deploy_kiro_cli.sh 保持一致):
  51. # __KIRO_HOME__ → ${KIRO_HOME} (fwh-pyrun、templates 的父路径)
  52. # __WIKI_DOCS__ → ${KIRO_WIKI_DOCS} (产品文档知识库目录)
  53. # __OFFICECLI_BIN__ → ${KIRO_OFFICECLI_BIN} (officecli 可执行文件)
  54. KIRO_WIKI_DOCS="${KIRO_WIKI_DOCS:-/home/jerry-remote/web/FranklinWH-Wiki/client/docs}"
  55. KIRO_OFFICECLI_BIN="${KIRO_OFFICECLI_BIN:-/home/jerry-remote/.local/bin/officecli}"
  56. # A→B 覆盖同步的配置子目录
  57. SYNC_SUBDIRS=(steering agents)
  58. c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
  59. info() { echo -e "${c_blue}[INFO]${c_reset} $*"; }
  60. ok() { echo -e "${c_green}[ OK ]${c_reset} $*"; }
  61. warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
  62. err() { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
  63. has() { command -v "$1" >/dev/null 2>&1; }
  64. is_dry() { [[ "${KIRO_DRY_RUN}" == "y" ]]; }
  65. # 执行文件系统命令(dry-run 只打印)
  66. run_fs() {
  67. if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} $*"; return 0; fi
  68. bash -lc "$*"
  69. }
  70. # 以 B 端运行用户身份执行(HOME=KIRO_HOME),用于 npx skills add
  71. as_target_user() {
  72. local cmd="$*"
  73. if is_dry; then
  74. echo -e "${c_yellow}[DRY]${c_reset} (as ${KIRO_RUN_AS_USER}, HOME=${KIRO_HOME}) ${cmd}"
  75. return 0
  76. fi
  77. if [[ "${KIRO_RUN_AS_USER}" == "$(id -un)" ]]; then
  78. HOME="${KIRO_HOME}" bash -lc "${cmd}"
  79. else
  80. sudo -u "${KIRO_RUN_AS_USER}" -H env HOME="${KIRO_HOME}" bash -lc "${cmd}"
  81. fi
  82. }
  83. # 扫描 A 端 skills/ 下实际存在的自定义 skill 目录名
  84. custom_skill_names() {
  85. local d
  86. [[ -d "${A_DIR}/skills" ]] || return 0
  87. for d in "${A_DIR}/skills"/*/; do
  88. [[ -d "${d}" ]] && basename "${d}"
  89. done
  90. }
  91. # 读取 skills.sh 清单:去注释/空行,输出每行 "<owner>/<repo>@<skill>"
  92. read_skills_list() {
  93. [[ -f "${KIRO_SKILLS_LIST}" ]] || return 0
  94. sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "${KIRO_SKILLS_LIST}" \
  95. | grep -vE '^[[:space:]]*$'
  96. }
  97. # rsync A→B,一个子路径(带 --delete,作用范围仅该子目录)
  98. rsync_dir() {
  99. local rel="$1" # 如 steering、skills/fwh-toolbox-rules
  100. local src="${A_DIR}/${rel}"
  101. local dst="${B_DIR}/${rel}"
  102. [[ -d "${src}" ]] || { warn "跳过(源不存在):${src}"; return 0; }
  103. run_fs "sudo mkdir -p '${dst}'"
  104. if has rsync; then
  105. run_fs "sudo rsync -a --delete '${src}/' '${dst}/'"
  106. else
  107. run_fs "sudo rm -rf '${dst}'/* && sudo cp -rf '${src}/.' '${dst}/'"
  108. fi
  109. }
  110. # ---------------------------------------------------------------------------
  111. # 1. 同步 steering / agents(覆盖)
  112. # ---------------------------------------------------------------------------
  113. sync_config_dirs() {
  114. local sub
  115. for sub in "${SYNC_SUBDIRS[@]}"; do
  116. info "同步 ${sub}/ (A→B 覆盖)"
  117. rsync_dir "${sub}"
  118. done
  119. }
  120. # ---------------------------------------------------------------------------
  121. # 2. 同步自定义 skill(逐个目录覆盖,扫描 A 端)
  122. # ---------------------------------------------------------------------------
  123. sync_custom_skills() {
  124. local name count=0
  125. while IFS= read -r name; do
  126. [[ -z "${name}" ]] && continue
  127. info "同步自定义 skill:skills/${name}/ (A→B 覆盖)"
  128. rsync_dir "skills/${name}"
  129. count=$((count+1))
  130. done < <(custom_skill_names)
  131. [[ ${count} -eq 0 ]] && info "A 端无自定义 skill" || ok "自定义 skill 同步完成(共 ${count} 个)"
  132. }
  133. # ---------------------------------------------------------------------------
  134. # 3. 命令安装 skills.sh 生态 skill(读清单,在 B 端安装)
  135. # ---------------------------------------------------------------------------
  136. install_skills_sh() {
  137. if [[ "${KIRO_SKILLS_INSTALL}" != "y" ]]; then
  138. info "跳过 skills.sh 命令安装(KIRO_SKILLS_INSTALL=${KIRO_SKILLS_INSTALL})"
  139. return 0
  140. fi
  141. if [[ ! -f "${KIRO_SKILLS_LIST}" ]]; then
  142. warn "未找到清单文件:${KIRO_SKILLS_LIST},跳过命令安装"
  143. return 0
  144. fi
  145. if ! has npx; then
  146. warn "无 npx,跳过 skills.sh 命令安装"
  147. return 0
  148. fi
  149. info "从清单安装 skills.sh skill:${KIRO_SKILLS_LIST}"
  150. local entry repo skill count=0
  151. while IFS= read -r entry; do
  152. [[ -z "${entry}" ]] && continue
  153. if [[ "${entry}" != *"@"* ]]; then
  154. warn "清单行格式不符(应为 owner/repo@skill),跳过:${entry}"
  155. continue
  156. fi
  157. repo="${entry%@*}"
  158. skill="${entry##*@}"
  159. info "安装 skill:${repo} --skill ${skill} → agent ${KIRO_AGENT}"
  160. as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes" \
  161. || warn "skill 安装失败(可稍后重试):${entry}"
  162. count=$((count+1))
  163. done < <(read_skills_list)
  164. ok "skills.sh skill 命令安装完成(共 ${count} 个)"
  165. }
  166. # ---------------------------------------------------------------------------
  167. # 4. 替换占位符(与 deploy_kiro_cli.sh 一致)
  168. # A 端源文件存的是带占位符的模板,rsync 到 B 端后必须替换为真实路径,
  169. # 否则 agents/*.json 的命令白名单、officecli 路径、skills 文档会残留 __XXX__。
  170. # 作用范围:B 端 agents/*.json 与 skills/**/*.md。
  171. # ---------------------------------------------------------------------------
  172. replace_placeholders() {
  173. info "替换占位符:__KIRO_HOME__=${KIRO_HOME%/} __WIKI_DOCS__=${KIRO_WIKI_DOCS%/} __OFFICECLI_BIN__=${KIRO_OFFICECLI_BIN}"
  174. # 用 # 作为 sed 分隔符(路径含 /);替换值不含 #
  175. local sed_expr="s#__KIRO_HOME__#${KIRO_HOME%/}#g; s#__WIKI_DOCS__#${KIRO_WIKI_DOCS%/}#g; s#__OFFICECLI_BIN__#${KIRO_OFFICECLI_BIN}#g"
  176. if [[ -d "${B_DIR}/agents" ]]; then
  177. run_fs "sudo find '${B_DIR}/agents' -maxdepth 1 -type f -name '*.json' -exec sed -i -E '${sed_expr}' {} +"
  178. fi
  179. if [[ -d "${B_DIR}/skills" ]]; then
  180. run_fs "sudo find '${B_DIR}/skills' -type f -name '*.md' -exec sed -i -E '${sed_expr}' {} +"
  181. fi
  182. ok "占位符替换完成"
  183. }
  184. # ---------------------------------------------------------------------------
  185. # 安全守卫:禁止把配置同步到「用户级目录」
  186. # 本脚本是 A→B 的部署同步,B 端应为服务账户的部署目录(如
  187. # /var/www/fwh-toolbox/server/.kiro),绝不能指向某个真实用户的 HOME
  188. # 下的 ~/.kiro,否则 rsync --delete 会覆盖/删除用户自己的 steering、
  189. # agents、skills(曾因此丢失用户级配置)。
  190. #
  191. # 判定规则(任一命中即拒绝):
  192. # * B_DIR 解析后正好等于 “<某 HOME>/.kiro”,或位于其下;
  193. # * 该 HOME 来源:当前执行用户 $HOME、KIRO_RUN_AS_USER 的 HOME、
  194. # 以及常见用户家目录前缀 /home/*、/Users/*、/root。
  195. # 用物理路径比较,避免 ..、软链接、结尾斜杠绕过。可用
  196. # KIRO_ALLOW_USER_HOME=y 显式解除(仅限明确知道后果时)。
  197. # ---------------------------------------------------------------------------
  198. abspath() {
  199. # 输出物理绝对路径;路径可能尚不存在,故对父目录逐级回退解析
  200. local p="$1"
  201. if command -v realpath >/dev/null 2>&1; then
  202. realpath -m -- "${p}" 2>/dev/null && return 0
  203. fi
  204. if command -v readlink >/dev/null 2>&1; then
  205. readlink -f -- "${p}" 2>/dev/null && return 0
  206. fi
  207. # 兜底:去掉结尾斜杠,原样返回
  208. echo "${p%/}"
  209. }
  210. guard_not_user_home() {
  211. if [[ "${KIRO_ALLOW_USER_HOME:-n}" == "y" ]]; then
  212. warn "已设置 KIRO_ALLOW_USER_HOME=y,跳过用户级目录保护(请自行确认后果)"
  213. return 0
  214. fi
  215. local b_abs; b_abs="$(abspath "${B_DIR}")"
  216. # 收集需要保护的 HOME 列表
  217. local -a homes=()
  218. [[ -n "${HOME:-}" ]] && homes+=("${HOME}")
  219. local run_home
  220. run_home="$(getent passwd "${KIRO_RUN_AS_USER}" 2>/dev/null | cut -d: -f6)"
  221. [[ -n "${run_home}" ]] && homes+=("${run_home}")
  222. local h h_abs
  223. for h in "${homes[@]}"; do
  224. [[ -z "${h}" || "${h}" == "/" ]] && continue
  225. h_abs="$(abspath "${h}")"
  226. # B_DIR 恰为 <HOME>/.kiro,或位于 <HOME>/ 之下
  227. if [[ "${b_abs}" == "${h_abs}/.kiro" || "${b_abs}" == "${h_abs}/.kiro/"* ]]; then
  228. err "拒绝同步:B 端目标 ${b_abs} 位于用户 HOME(${h_abs})下,属于用户级 .kiro 目录。"
  229. err "本脚本只允许同步到服务部署目录。若确需覆盖用户级目录,请设 KIRO_ALLOW_USER_HOME=y。"
  230. exit 1
  231. fi
  232. done
  233. # 额外拦截:B_DIR 直接落在常见「用户家目录」前缀下(覆盖非当前用户的情况)
  234. case "${b_abs}" in
  235. /home/*/.kiro|/home/*/.kiro/*|/Users/*/.kiro|/Users/*/.kiro/*|/root/.kiro|/root/.kiro/*)
  236. err "拒绝同步:B 端目标 ${b_abs} 看起来是用户级 .kiro 目录(位于 /home、/Users 或 /root 下)。"
  237. err "本脚本只允许同步到服务部署目录。若确需覆盖用户级目录,请设 KIRO_ALLOW_USER_HOME=y。"
  238. exit 1
  239. ;;
  240. esac
  241. ok "用户级目录保护检查通过:B 端 ${b_abs} 非用户级 .kiro"
  242. }
  243. # ---------------------------------------------------------------------------
  244. # main
  245. # ---------------------------------------------------------------------------
  246. main() {
  247. info "== Kiro 配置单向同步(A → B)=="
  248. info "A 端(版本库) :${A_DIR}"
  249. info "B 端(部署目标):${B_DIR}"
  250. info "自定义 skill :$(custom_skill_names | tr '\n' ' ')"
  251. info "skills.sh 清单 :${KIRO_SKILLS_LIST}(命令安装=${KIRO_SKILLS_INSTALL})"
  252. info "dry-run=${KIRO_DRY_RUN}"
  253. if [[ ! -d "${A_DIR}" ]]; then err "A 端不存在:${A_DIR}"; exit 1; fi
  254. # 写操作前的安全守卫:禁止同步到用户级 ~/.kiro
  255. guard_not_user_home
  256. sync_config_dirs
  257. sync_custom_skills
  258. install_skills_sh
  259. replace_placeholders
  260. # 属主归还给 B 端运行用户
  261. run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${B_DIR}'"
  262. ok "同步结束"
  263. }
  264. main "$@"