#!/usr/bin/env bash # ============================================================================= # FranklinWH Toolbox - Kiro CLI 配置部署脚本 # ============================================================================= # 作用:把版本库内的 kiro-cli 配置(agents / steering / skills)部署到 # 后端运行用户的 HOME 下的 .kiro 目录,供后端以 ACP 子进程方式拉起的 kiro-cli # 加载使用。 # # 注意:settings/ 由 kiro-cli 自动生成,不纳入部署(不覆盖目标机自身的设置)。 # # 配置来源(版本库内,权威配置): # deploy/kiro/ # ├── agents/ Agent 定义(*.json) # ├── steering/ Steering 规则(*.md) # └── skills/ 仅自定义 skill(skills.sh 生态的通过命令安装,不入此目录) # # 部署目标(与 install_kiro_cli.sh 的 KIRO_HOME 保持一致): # ${KIRO_HOME}/.kiro 默认 /var/www/fwh-toolbox/server/.kiro # ├── agents/ ← 拷贝同步 # ├── steering/ ← 拷贝同步 # ├── settings/ ← 不部署(kiro-cli 自动生成) # └── skills/ # ├── ← 用 `npx skills add` 命令安装(可控、可更新) # └── <自定义 skill> ← 直接拷贝(skills.sh 上不存在,无法命令安装) # # Skill 分类原则: # * 来自 skills.sh 生态的 skill → 命令安装。清单外置在 ${KIRO_CONFIG_SRC}/skills-sh.list # (每行 "/@",# 注释/空行忽略;文件内含安装命令备注)。 # 新增/调整只需改该清单文件,无需改本脚本。 # * 非 skills.sh 的自定义 skill → 直接拷贝。**不再硬编码清单**,脚本扫描 # ${KIRO_CONFIG_SRC}/skills/ 下实际存在的目录逐个拷贝。新增只需把目录放进去。 # # 前置条件: # * 目标用户存在,且 kiro-cli 已安装(见 install_kiro_cli.sh)。 # * 命令安装 skill 需要 Node.js / npx(skills CLI 通过 npx 调用)。 # * rsync(可选,用于同步;缺失时回退到 cp)。 # # 环境变量: # KIRO_RUN_AS_USER 部署并运行 kiro-cli 的系统用户 (默认当前用户) # KIRO_HOME 目标用户 HOME(决定 .kiro 位置) (默认 /var/www/fwh-toolbox/server) # KIRO_CONFIG_SRC 配置来源目录 (默认 /deploy/kiro) # KIRO_SKILLS_LIST skills.sh 安装清单文件 (默认 ${KIRO_CONFIG_SRC}/skills-sh.list) # KIRO_AGENT skills CLI 的目标 agent (默认 kiro-cli) # KIRO_SKILLS_INSTALL 是否命令安装 skills.sh skill (y/n) (默认 y) # KIRO_WIKI_DOCS 产品文档知识库目录(替换 __WIKI_DOCS__) (默认当前 Wiki docs 路径) # KIRO_OFFICECLI_BIN officecli 可执行文件(替换 __OFFICECLI_BIN__)(默认当前 officecli 路径) # KIRO_DRY_RUN 只打印将执行的动作,不落盘 (y/n) (默认 n) # ============================================================================= set -euo pipefail # --------------------------------------------------------------------------- # 路径与默认值 # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # deploy/ PROJECT_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" # 仓库根目录 KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}" KIRO_HOME="${KIRO_HOME:-/var/www/fwh-toolbox/server}" KIRO_CONFIG_SRC="${KIRO_CONFIG_SRC:-${DEPLOY_DIR}/kiro}" KIRO_AGENT="${KIRO_AGENT:-kiro-cli}" KIRO_SKILLS_INSTALL="${KIRO_SKILLS_INSTALL:-y}" KIRO_DRY_RUN="${KIRO_DRY_RUN:-n}" # agent.json 占位符替换值(部署时可覆盖;默认取当前环境路径) # __KIRO_HOME__ → ${KIRO_HOME}(fwh-pyrun、templates 的父路径) # __WIKI_DOCS__ → ${KIRO_WIKI_DOCS}(产品文档知识库目录) # __OFFICECLI_BIN__ → ${KIRO_OFFICECLI_BIN}(officecli 可执行文件) KIRO_WIKI_DOCS="${KIRO_WIKI_DOCS:-/home/jerry-remote/web/FranklinWH-Wiki/client/docs}" KIRO_OFFICECLI_BIN="${KIRO_OFFICECLI_BIN:-/home/jerry-remote/.local/bin/officecli}" # 文件系统操作是否使用 sudo(默认 y,保持原行为)。当目标目录当前用户可写时可设 n。 KIRO_USE_SUDO="${KIRO_USE_SUDO:-y}" if [[ "${KIRO_USE_SUDO}" == "y" ]]; then SUDO="sudo "; else SUDO=""; fi KIRO_DEST="${KIRO_HOME%/}/.kiro" # --------------------------------------------------------------------------- # Skill 来源 # --------------------------------------------------------------------------- # skills.sh 生态:从清单文件读取(每行 "/@",# 注释/空行忽略) KIRO_SKILLS_LIST="${KIRO_SKILLS_LIST:-${KIRO_CONFIG_SRC}/skills-sh.list}" # 自定义 skill:不再硬编码,直接扫描 ${KIRO_CONFIG_SRC}/skills/ 下实际存在的目录进行拷贝 # 拷贝同步的配置子目录(settings 由 kiro-cli 自动生成,不部署) CONFIG_SUBDIRS=(agents steering) # --------------------------------------------------------------------------- # 日志 # --------------------------------------------------------------------------- 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" ]]; } # 解析目标用户 HOME(仅用于日志/一致性校验;实际目标由 KIRO_HOME 决定) resolve_user_home() { getent passwd "${KIRO_RUN_AS_USER}" | cut -d: -f6 } # 以目标用户身份执行命令(设置 HOME=KIRO_HOME,保证 .kiro 落到目标位置) # dry-run 时只打印。 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 } # 以目标用户身份 / 或 sudo 执行文件系统操作(run_fs "") run_fs() { local cmd="$*" if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} ${cmd}" return 0 fi bash -lc "${cmd}" } # 检查并(尽力)安装沙箱运行 Python 所需依赖:bwrap、python3。 # 自动安装需要 sudo 与受支持的包管理器(apt/dnf/yum/zypper/pacman)。 # 失败时给出告警但不中断部署(与 npx 缺失的处理一致)。 ensure_sandbox_deps() { local missing=() has bwrap || missing+=("bwrap") has python3 || missing+=("python3") if [[ ${#missing[@]} -eq 0 ]]; then ok "沙箱依赖已就绪:bwrap、python3" return 0 fi warn "缺少沙箱依赖:${missing[*]},尝试自动安装 ..." # 将逻辑包名映射到各发行版的实际包名 local pkgs_apt=() pkgs_rpm=() pkgs_pacman=() local m for m in "${missing[@]}"; do case "$m" in bwrap) pkgs_apt+=("bubblewrap"); pkgs_rpm+=("bubblewrap"); pkgs_pacman+=("bubblewrap") ;; python3) pkgs_apt+=("python3"); pkgs_rpm+=("python3"); pkgs_pacman+=("python") ;; esac done if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} 将安装:${missing[*]}" return 0 fi if has apt-get; then run_fs "sudo apt-get update -y && sudo apt-get install -y ${pkgs_apt[*]}" || true elif has dnf; then run_fs "sudo dnf install -y ${pkgs_rpm[*]}" || true elif has yum; then run_fs "sudo yum install -y ${pkgs_rpm[*]}" || true elif has zypper; then run_fs "sudo zypper install -y ${pkgs_rpm[*]}" || true elif has pacman; then run_fs "sudo pacman -Sy --noconfirm ${pkgs_pacman[*]}" || true else err "未识别的包管理器,无法自动安装:${missing[*]}" err "请手动安装后重试(bwrap 对应包名通常为 bubblewrap)。" return 1 fi # 复检 local still=() has bwrap || still+=("bwrap") has python3 || still+=("python3") if [[ ${#still[@]} -gt 0 ]]; then err "以下依赖安装后仍不可用:${still[*]}。Python3 沙箱能力将不可用。" return 1 fi ok "沙箱依赖安装完成:${missing[*]}" } # --------------------------------------------------------------------------- # 0. 前置检查 # --------------------------------------------------------------------------- ensure_prereqs() { if [[ ! -d "${KIRO_CONFIG_SRC}" ]]; then err "配置来源不存在:${KIRO_CONFIG_SRC}" return 1 fi if ! getent passwd "${KIRO_RUN_AS_USER}" >/dev/null; then err "系统用户不存在:${KIRO_RUN_AS_USER}" return 1 fi if [[ "${KIRO_SKILLS_INSTALL}" == "y" ]] && ! has npx; then warn "未找到 npx(Node.js)。skills.sh skill 的命令安装将被跳过。" warn "如需命令安装,请先安装 Node.js(提供 npx),或设 KIRO_SKILLS_INSTALL=n。" fi # Python3 沙箱依赖(bwrap + python3),供 fwh-toolbox-agent 的 fwh-pyrun 使用 ensure_sandbox_deps || warn "沙箱依赖未完全就绪,fwh-pyrun 可能无法运行。" } # --------------------------------------------------------------------------- # 1. 准备目标目录(属主为目标用户) # --------------------------------------------------------------------------- prepare_dest() { info "准备目标目录:${KIRO_DEST}(属主 ${KIRO_RUN_AS_USER})" run_fs "${SUDO}mkdir -p '${KIRO_DEST}/skills'" run_fs "${SUDO}chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'" } # --------------------------------------------------------------------------- # 2. 同步配置子目录(agents / settings / steering) # --------------------------------------------------------------------------- sync_config_dirs() { local sub src for sub in "${CONFIG_SUBDIRS[@]}"; do src="${KIRO_CONFIG_SRC}/${sub}" if [[ ! -d "${src}" ]]; then warn "跳过(源不存在):${src}" continue fi info "同步配置:${sub}/ → ${KIRO_DEST}/${sub}/" run_fs "${SUDO}mkdir -p '${KIRO_DEST}/${sub}'" if has rsync; then run_fs "${SUDO}rsync -a --delete '${src}/' '${KIRO_DEST}/${sub}/'" else run_fs "${SUDO}rm -rf '${KIRO_DEST}/${sub}'/* && ${SUDO}cp -rf '${src}/.' '${KIRO_DEST}/${sub}/'" fi done run_fs "${SUDO}chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'" ok "配置目录同步完成" } # --------------------------------------------------------------------------- # 3. 命令安装 skills.sh 生态的 skill(从清单文件读取) # --------------------------------------------------------------------------- # 解析清单文件:去掉 # 注释与空行,输出每行 "/@" read_skills_list() { [[ -f "${KIRO_SKILLS_LIST}" ]] || return 0 sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "${KIRO_SKILLS_LIST}" \ | grep -vE '^[[:space:]]*$' } install_skills_sh() { if [[ "${KIRO_SKILLS_INSTALL}" != "y" ]]; then info "跳过 skills.sh skill 命令安装(KIRO_SKILLS_INSTALL=${KIRO_SKILLS_INSTALL})" return 0 fi if [[ ! -f "${KIRO_SKILLS_LIST}" ]]; then warn "未找到 skills.sh 清单文件:${KIRO_SKILLS_LIST},跳过命令安装" return 0 fi if ! has npx; then warn "无 npx,跳过 skills.sh skill 命令安装" return 0 fi info "从清单安装 skills.sh skill:${KIRO_SKILLS_LIST}" local entry repo skill ok_count=0 fail_count=0 failed_list="" while IFS= read -r entry; do [[ -z "${entry}" ]] && continue if [[ "${entry}" != *"@"* ]]; then warn "清单行格式不符(应为 owner/repo@skill),跳过:${entry}" continue fi repo="${entry%@*}" skill="${entry##*@}" if install_one_skill "${repo}" "${skill}"; then ok_count=$((ok_count+1)) else fail_count=$((fail_count+1)) failed_list+=" ${entry}" fi done < <(read_skills_list) if [[ ${fail_count} -eq 0 ]]; then ok "skills.sh skill 安装完成(成功 ${ok_count} 个)" else warn "skills.sh skill 安装:成功 ${ok_count} 个,失败 ${fail_count} 个 →${failed_list}" warn "可重跑本脚本重试,或手动执行:npx skills add / --skill --agent ${KIRO_AGENT} --yes" fi } # 安装单个 skill 并校验是否真正落地;失败自动重试一次。 # 参数:$1=/ $2= # 校验方式:安装后检查目标 skills 目录下是否出现该 skill 目录(含 SKILL.md)。 install_one_skill() { local repo="$1" skill="$2" local dest_skill="${KIRO_DEST}/skills/${skill}" local attempt for attempt in 1 2; do info "命令安装 skill:${repo} --skill ${skill} → agent ${KIRO_AGENT}(第 ${attempt} 次)" if is_dry; then as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes" return 0 fi # 逐条独立执行;stdin 接 /dev/null,避免 TUI 在管道/循环中争用输入导致后续条目静默失败 as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes < /dev/null" || true # 安装后校验:目标目录出现且含 SKILL.md 视为成功 if [[ -f "${dest_skill}/SKILL.md" ]]; then ok "skill 就绪:${skill}" return 0 fi warn "skill 未检测到落地:${skill}(第 ${attempt} 次),准备重试 ..." sleep 1 done err "skill 安装失败:${repo}@${skill}" return 1 } # --------------------------------------------------------------------------- # 4. 拷贝自定义 skill(扫描 ${KIRO_CONFIG_SRC}/skills/ 实际存在的目录) # --------------------------------------------------------------------------- copy_custom_skills() { local skills_src="${KIRO_CONFIG_SRC}/skills" if [[ ! -d "${skills_src}" ]]; then info "无自定义 skill 目录(${skills_src}),跳过拷贝" return 0 fi local dir name dest count=0 for dir in "${skills_src}"/*/; do [[ -d "${dir}" ]] || continue name="$(basename "${dir}")" dest="${KIRO_DEST}/skills/${name}" info "拷贝自定义 skill:${name} → ${dest}" if has rsync; then run_fs "${SUDO}rsync -a --delete '${skills_src}/${name}/' '${dest}/'" else run_fs "${SUDO}rm -rf '${dest}' && ${SUDO}mkdir -p '${dest}' && ${SUDO}cp -rf '${skills_src}/${name}/.' '${dest}/'" fi count=$((count+1)) done run_fs "${SUDO}chown -R '${KIRO_RUN_AS_USER}' '${KIRO_DEST}/skills'" ok "自定义 skill 拷贝完成(共 ${count} 个)" } # --------------------------------------------------------------------------- # 渲染占位符(部署后按目标环境替换 agents 与 skills 中的路径) # --------------------------------------------------------------------------- # 版本库中的 agents/*.json 与 skills/**/*.md 用占位符表示环境相关路径, # 部署到目标后在此统一替换为实值: # __KIRO_HOME__ → ${KIRO_HOME} (fwh-pyrun、templates 的父路径) # __WIKI_DOCS__ → ${KIRO_WIKI_DOCS} (产品文档知识库目录) # __OFFICECLI_BIN__ → ${KIRO_OFFICECLI_BIN}(officecli 可执行文件) # 注意:需在 agents / skills 全部拷贝到目标之后调用。 render_placeholders() { info "替换占位符:__KIRO_HOME__=${KIRO_HOME%/} __WIKI_DOCS__=${KIRO_WIKI_DOCS} __OFFICECLI_BIN__=${KIRO_OFFICECLI_BIN}" # 用 # 作为 sed 分隔符(路径含 /);当前替换值不含 # local sed_expr="s#__KIRO_HOME__#${KIRO_HOME%/}#g; s#__WIKI_DOCS__#${KIRO_WIKI_DOCS%/}#g; s#__OFFICECLI_BIN__#${KIRO_OFFICECLI_BIN}#g" # agents/*.json if [[ -d "${KIRO_DEST}/agents" ]]; then run_fs "${SUDO}find '${KIRO_DEST}/agents' -maxdepth 1 -type f -name '*.json' -exec sed -i -E '${sed_expr}' {} +" fi # skills/**/*.md if [[ -d "${KIRO_DEST}/skills" ]]; then run_fs "${SUDO}find '${KIRO_DEST}/skills' -type f -name '*.md' -exec sed -i -E '${sed_expr}' {} +" fi run_fs "${SUDO}chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'" ok "占位符替换完成(agents + skills)" } # --------------------------------------------------------------------------- # 4.2 部署 PPT 公司模板(随部署路径走) # --------------------------------------------------------------------------- # 源:${KIRO_CONFIG_SRC}/templates/* # 目标:${KIRO_HOME}/templates/(agent.json 中以 __KIRO_HOME__/templates 引用) install_templates() { local src="${KIRO_CONFIG_SRC}/templates" local dest="${KIRO_HOME%/}/templates" if [[ ! -d "${src}" ]]; then info "无模板目录(${src}),跳过模板部署" return 0 fi info "部署模板:${src}/ → ${dest}/" run_fs "${SUDO}mkdir -p '${dest}'" if has rsync; then run_fs "${SUDO}rsync -a '${src}/' '${dest}/'" else run_fs "${SUDO}cp -rf '${src}/.' '${dest}/'" fi run_fs "${SUDO}chown -R '${KIRO_RUN_AS_USER}' '${dest}'" ok "模板部署完成:${dest}" } # --------------------------------------------------------------------------- # 4.5 部署 fwh-pyrun 包装脚本(Python3 沙箱执行器,供 fwh-toolbox-agent 调用) # --------------------------------------------------------------------------- # 源:${KIRO_CONFIG_SRC}/bin/fwh-pyrun # 目标:${KIRO_HOME}/bin/fwh-pyrun(须与 agent.json 中 allowedCommands 的路径一致) # 同时确保沙箱目录 /tmp/fwh-toolbox-agent 存在(属主为运行用户)。 install_pyrun() { local src="${KIRO_CONFIG_SRC}/bin/fwh-pyrun" local dest_dir="${KIRO_HOME%/}/bin" local dest="${dest_dir}/fwh-pyrun" if [[ ! -f "${src}" ]]; then warn "未找到 fwh-pyrun 源(${src}),跳过部署(Python3 沙箱能力不可用)" return 0 fi info "部署 fwh-pyrun:${src} → ${dest}" run_fs "${SUDO}mkdir -p '${dest_dir}'" run_fs "${SUDO}cp -f '${src}' '${dest}'" run_fs "${SUDO}chmod 0755 '${dest}'" run_fs "${SUDO}chown '${KIRO_RUN_AS_USER}' '${dest}'" # 沙箱可写目录(唯一持久写入点),属主为运行用户 run_fs "${SUDO}mkdir -p /tmp/fwh-toolbox-agent && ${SUDO}chown '${KIRO_RUN_AS_USER}' /tmp/fwh-toolbox-agent" ok "fwh-pyrun 部署完成:${dest}" } # --------------------------------------------------------------------------- # 5. 校验 # --------------------------------------------------------------------------- verify() { info "校验部署结果 ..." if is_dry; then info "(dry-run) 跳过实际校验" return 0 fi local sub for sub in "${CONFIG_SUBDIRS[@]}" skills; do if [[ -d "${KIRO_DEST}/${sub}" ]]; then ok "存在:${KIRO_DEST}/${sub}" else warn "缺失:${KIRO_DEST}/${sub}" fi done local name dir for dir in "${KIRO_CONFIG_SRC}/skills"/*/; do [[ -d "${dir}" ]] || continue name="$(basename "${dir}")" [[ -f "${KIRO_DEST}/skills/${name}/SKILL.md" ]] \ && ok "自定义 skill 就绪:${name}" \ || warn "自定义 skill 缺 SKILL.md:${name}" done if has npx && [[ "${KIRO_SKILLS_INSTALL}" == "y" ]]; then info "已安装 skill(项目级,HOME=${KIRO_HOME}):" as_target_user "cd '${KIRO_HOME}' && npx --yes skills list 2>/dev/null" || true fi # fwh-pyrun 沙箱执行器校验 local pyrun="${KIRO_HOME%/}/bin/fwh-pyrun" if [[ -x "${pyrun}" ]]; then ok "fwh-pyrun 就绪:${pyrun}" else warn "fwh-pyrun 缺失或不可执行:${pyrun}(Python3 沙箱能力不可用)" fi has bwrap && ok "bwrap 可用:$(command -v bwrap)" || warn "bwrap 不可用(Python3 沙箱能力不可用)" has python3 && ok "python3 可用:$(command -v python3)" || warn "python3 不可用(Python3 沙箱能力不可用)" # 模板校验 if [[ -d "${KIRO_HOME%/}/templates" ]]; then ok "模板目录就绪:${KIRO_HOME%/}/templates" else warn "模板目录缺失:${KIRO_HOME%/}/templates" fi # 占位符残留校验(替换后不应再出现 __XXX__) if grep -rlE '__(KIRO_HOME|WIKI_DOCS|OFFICECLI_BIN)__' "${KIRO_DEST}/agents" "${KIRO_DEST}/skills" >/dev/null 2>&1; then warn "agents/skills 中仍存在未替换的占位符,请检查 render_placeholders" else ok "占位符已全部替换(agents + skills)" fi } # --------------------------------------------------------------------------- # main # --------------------------------------------------------------------------- main() { info "== 部署 Kiro CLI 配置 ==" info "运行用户 = ${KIRO_RUN_AS_USER}(passwd HOME=$(resolve_user_home))" info "配置来源 = ${KIRO_CONFIG_SRC}" info "部署目标 = ${KIRO_DEST}" info "目标 agent = ${KIRO_AGENT}" info "命令安装 = ${KIRO_SKILLS_INSTALL} dry-run = ${KIRO_DRY_RUN}" if [[ "$(resolve_user_home)" != "${KIRO_HOME%/}" ]]; then warn "KIRO_HOME(${KIRO_HOME}) 与该用户 passwd 家目录($(resolve_user_home)) 不一致;" warn "请确认 config.env 的 KIRO_HOME 与后端拉起 kiro-cli 时的 HOME 一致。" fi ensure_prereqs prepare_dest sync_config_dirs install_skills_sh copy_custom_skills install_templates install_pyrun render_placeholders verify echo echo -e "${c_green}Kiro CLI 配置部署结束${c_reset}" cat <