#!/usr/bin/env bash # ============================================================================= # FranklinWH Toolbox - Kiro CLI 配置单向同步脚本(A → B) # ============================================================================= # 作用:把「版本库配置源」单向推送到「线上部署目标」。 # # A 端(版本库源):deploy/kiro/ # B 端(部署目标):${KIRO_HOME}/.kiro 默认 /var/www/fwh-toolbox/server/.kiro # # 同步内容(一律 A → B,不回写): # * steering/ 覆盖同步(rsync --delete,B 端该目录与 A 端一致) # * agents/ 覆盖同步(rsync --delete) # * 自定义 skill 扫描 A 端 skills/ 下的目录,逐个覆盖同步到 B 端 # (每个 skill 目录内部 --delete,不影响 B 端其它 skill) # * skills.sh 生态 skill 读取 skills-sh.list,在 B 端用 `npx skills add` 安装 # (要装新的:先在 skills-sh.list 加一行,再跑本脚本) # # 不同步:settings/(kiro-cli 自动生成)。 # 安全:顶层 skills/ 不做 --delete,命令安装的 skill 目录不会被误删。 # # 用法: # ./sync_kiro_config.sh # 执行同步 # KIRO_HOME=/path ./sync_kiro_config.sh # 指定 B 端 HOME # KIRO_DRY_RUN=y ./sync_kiro_config.sh # 预演,只打印动作不落盘 # KIRO_SKILLS_INSTALL=n ./sync_kiro_config.sh # 跳过 skills.sh 命令安装,只同步文件 # # 环境变量: # KIRO_HOME B 端 HOME(决定 .kiro 位置) (默认 /var/www/fwh-toolbox/server) # KIRO_RUN_AS_USER B 端属主 / 运行 kiro-cli 的用户 (默认当前用户) # A_DIR A 端目录 (默认 /deploy/kiro) # KIRO_SKILLS_LIST skills.sh 安装清单文件 (默认 ${A_DIR}/skills-sh.list) # KIRO_AGENT skills CLI 目标 agent (默认 kiro-cli) # KIRO_SKILLS_INSTALL 是否命令安装 skills.sh skill (默认 y) # KIRO_DRY_RUN 只打印不落盘 (y/n) (默认 n) # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # deploy/ KIRO_HOME="${KIRO_HOME:-/var/www/fwh-toolbox/server}" KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}" A_DIR="${A_DIR:-${DEPLOY_DIR}/kiro}" B_DIR="${KIRO_HOME%/}/.kiro" KIRO_SKILLS_LIST="${KIRO_SKILLS_LIST:-${A_DIR}/skills-sh.list}" KIRO_AGENT="${KIRO_AGENT:-kiro-cli}" KIRO_SKILLS_INSTALL="${KIRO_SKILLS_INSTALL:-y}" KIRO_DRY_RUN="${KIRO_DRY_RUN:-n}" # A→B 覆盖同步的配置子目录 SYNC_SUBDIRS=(steering agents) c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m' info() { echo -e "${c_blue}[INFO]${c_reset} $*"; } ok() { echo -e "${c_green}[ OK ]${c_reset} $*"; } warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; } err() { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; } has() { command -v "$1" >/dev/null 2>&1; } is_dry() { [[ "${KIRO_DRY_RUN}" == "y" ]]; } # 执行文件系统命令(dry-run 只打印) run_fs() { if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} $*"; return 0; fi bash -lc "$*" } # 以 B 端运行用户身份执行(HOME=KIRO_HOME),用于 npx skills add as_target_user() { local cmd="$*" if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} (as ${KIRO_RUN_AS_USER}, HOME=${KIRO_HOME}) ${cmd}" return 0 fi if [[ "${KIRO_RUN_AS_USER}" == "$(id -un)" ]]; then HOME="${KIRO_HOME}" bash -lc "${cmd}" else sudo -u "${KIRO_RUN_AS_USER}" -H env HOME="${KIRO_HOME}" bash -lc "${cmd}" fi } # 扫描 A 端 skills/ 下实际存在的自定义 skill 目录名 custom_skill_names() { local d [[ -d "${A_DIR}/skills" ]] || return 0 for d in "${A_DIR}/skills"/*/; do [[ -d "${d}" ]] && basename "${d}" done } # 读取 skills.sh 清单:去注释/空行,输出每行 "/@" read_skills_list() { [[ -f "${KIRO_SKILLS_LIST}" ]] || return 0 sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "${KIRO_SKILLS_LIST}" \ | grep -vE '^[[:space:]]*$' } # rsync A→B,一个子路径(带 --delete,作用范围仅该子目录) rsync_dir() { local rel="$1" # 如 steering、skills/fwh-toolbox-rules local src="${A_DIR}/${rel}" local dst="${B_DIR}/${rel}" [[ -d "${src}" ]] || { warn "跳过(源不存在):${src}"; return 0; } run_fs "sudo mkdir -p '${dst}'" if has rsync; then run_fs "sudo rsync -a --delete '${src}/' '${dst}/'" else run_fs "sudo rm -rf '${dst}'/* && sudo cp -rf '${src}/.' '${dst}/'" fi } # --------------------------------------------------------------------------- # 1. 同步 steering / agents(覆盖) # --------------------------------------------------------------------------- sync_config_dirs() { local sub for sub in "${SYNC_SUBDIRS[@]}"; do info "同步 ${sub}/ (A→B 覆盖)" rsync_dir "${sub}" done } # --------------------------------------------------------------------------- # 2. 同步自定义 skill(逐个目录覆盖,扫描 A 端) # --------------------------------------------------------------------------- sync_custom_skills() { local name count=0 while IFS= read -r name; do [[ -z "${name}" ]] && continue info "同步自定义 skill:skills/${name}/ (A→B 覆盖)" rsync_dir "skills/${name}" count=$((count+1)) done < <(custom_skill_names) [[ ${count} -eq 0 ]] && info "A 端无自定义 skill" || ok "自定义 skill 同步完成(共 ${count} 个)" } # --------------------------------------------------------------------------- # 3. 命令安装 skills.sh 生态 skill(读清单,在 B 端安装) # --------------------------------------------------------------------------- install_skills_sh() { if [[ "${KIRO_SKILLS_INSTALL}" != "y" ]]; then info "跳过 skills.sh 命令安装(KIRO_SKILLS_INSTALL=${KIRO_SKILLS_INSTALL})" return 0 fi if [[ ! -f "${KIRO_SKILLS_LIST}" ]]; then warn "未找到清单文件:${KIRO_SKILLS_LIST},跳过命令安装" return 0 fi if ! has npx; then warn "无 npx,跳过 skills.sh 命令安装" return 0 fi info "从清单安装 skills.sh skill:${KIRO_SKILLS_LIST}" local entry repo skill count=0 while IFS= read -r entry; do [[ -z "${entry}" ]] && continue if [[ "${entry}" != *"@"* ]]; then warn "清单行格式不符(应为 owner/repo@skill),跳过:${entry}" continue fi repo="${entry%@*}" skill="${entry##*@}" info "安装 skill:${repo} --skill ${skill} → agent ${KIRO_AGENT}" as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes" \ || warn "skill 安装失败(可稍后重试):${entry}" count=$((count+1)) done < <(read_skills_list) ok "skills.sh skill 命令安装完成(共 ${count} 个)" } # --------------------------------------------------------------------------- # main # --------------------------------------------------------------------------- main() { info "== Kiro 配置单向同步(A → B)==" info "A 端(版本库) :${A_DIR}" info "B 端(部署目标):${B_DIR}" info "自定义 skill :$(custom_skill_names | tr '\n' ' ')" info "skills.sh 清单 :${KIRO_SKILLS_LIST}(命令安装=${KIRO_SKILLS_INSTALL})" info "dry-run=${KIRO_DRY_RUN}" if [[ ! -d "${A_DIR}" ]]; then err "A 端不存在:${A_DIR}"; exit 1; fi sync_config_dirs sync_custom_skills install_skills_sh # 属主归还给 B 端运行用户 run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${B_DIR}'" ok "同步结束" } main "$@"