From e16da89569affb0ec2fd96ddb54481151fe43ffd Mon Sep 17 00:00:00 2001 From: zfc Date: Thu, 12 Mar 2026 21:33:59 +0800 Subject: [PATCH] fix: preserve existing agent guide files during install --- README.md | 15 ++-- install.sh | 27 +++++-- tests/install.sh.test | 164 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 tests/install.sh.test diff --git a/README.md b/README.md index d3216c3..f0e0852 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ RequirementsDoc ──▶ PRD ──▶ FeatureSummary ──▶ DevelopmentPlan ## 安装 & 更新 -一行命令搞定安装和更新。脚本会智能处理:新 skill 直接装,已有的对比更新,本地魔改自动备份。 +一行命令搞定安装和更新。脚本会智能处理:新 skill 直接装,已有的对比更新,本地魔改自动备份;项目根目录的 `AGENTS.md` / `CLAUDE.md` 仅在缺失时生成,已存在则跳过。 ```bash # Claude Code + Codex(推荐) @@ -78,9 +78,9 @@ bash /tmp/install-skills.sh both ### 安装脚本行为 -- `both` 模式:同时安装 `.codex/skills/` 和 `.claude/skills/`,并生成 `AGENTS.md` + `CLAUDE.md` -- `codex` 模式:安装到 `.codex/skills/`,并在项目根目录生成或更新 `AGENTS.md` -- `claude` 模式:安装到 `.claude/skills/`,并在项目根目录生成或更新 `CLAUDE.md` +- `both` 模式:同时安装 `.codex/skills/` 和 `.claude/skills/`,并生成缺失的 `AGENTS.md` + `CLAUDE.md`;已有则跳过 +- `codex` 模式:安装到 `.codex/skills/`,并在项目根目录生成缺失的 `AGENTS.md`;已有则跳过 +- `claude` 模式:安装到 `.claude/skills/`,并在项目根目录生成缺失的 `CLAUDE.md`;已有则跳过 - 如果传入自定义目标目录,脚本会优先安装 Codex 版 skills;目标路径包含 `.claude/skills` 时自动切到 Claude 版 ### 推荐用法 @@ -98,6 +98,13 @@ bash /tmp/install-skills.sh both | 本地魔改过 + 上游有更新 | 写入上游新版,本地版备份为 `SKILL.md.local.bak` | | 本地和上游一致 | 跳过 | +项目根目录 guide 文件单独处理: + +| 情况 | 处理方式 | +|------|---------| +| `AGENTS.md` / `CLAUDE.md` 不存在 | 从模板创建 | +| `AGENTS.md` / `CLAUDE.md` 已存在 | 跳过,保持原样 | + 恢复本地版本:`mv SKILL.md.local.bak SKILL.md` 对比差异:`diff SKILL.md SKILL.md.local.bak` diff --git a/install.sh b/install.sh index a7502cc..bf0624c 100644 --- a/install.sh +++ b/install.sh @@ -91,6 +91,23 @@ sync_file() { fi } +sync_guide_file() { + local src="$1" + local dst="$2" + local create_msg="$3" + local skip_msg="$4" + + if [ ! -f "$dst" ]; then + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + log_info "$create_msg" + TOTAL_NEW=$((TOTAL_NEW + 1)) + else + log_info "$skip_msg" + TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1)) + fi +} + install_layout() { local input="$1" local skill_dir @@ -147,11 +164,11 @@ install_layout() { guide_src_path="$TMP_DIR/$GUIDE_SRC" if [ -f "$guide_src_path" ]; then - sync_file \ + sync_guide_file \ "$guide_src_path" \ "$GUIDE_DST" \ - "✨ 新增项目引导: $GUIDE_DST" \ - "🔄 更新项目引导: $GUIDE_DST (本地版本已备份为 ${GUIDE_DST}.local.bak)" + "✨ 新增项目引导: ${GUIDE_DST}" \ + "⏭️ 跳过项目引导: ${GUIDE_DST}(已存在,保持原样)" fi INSTALLED_TARGETS="${INSTALLED_TARGETS}${INSTALLED_TARGETS:+, }$TARGET" @@ -188,12 +205,12 @@ echo " 📁 目标目录: $INSTALLED_TARGETS" echo " 📦 版本: $VERSION" echo "" echo " ✨ 新增: $TOTAL_NEW" -echo " 🔄 更新: $TOTAL_UPDATED(本地版本已备份为 .local.bak)" +echo " 🔄 更新: ${TOTAL_UPDATED}(本地版本已备份为 .local.bak)" echo " ⏭️ 无变化: $TOTAL_SKIPPED" echo "" if [ "$TOTAL_UPDATED" -gt 0 ]; then - log_warn "有 $TOTAL_UPDATED 个文件被更新,本地修改已备份为 .local.bak" + log_warn "有 ${TOTAL_UPDATED} 个文件被更新,本地修改已备份为 .local.bak" log_warn "如需恢复本地版本: mv SKILL.md.local.bak SKILL.md" log_warn "如需对比差异: diff SKILL.md SKILL.md.local.bak" fi diff --git a/tests/install.sh.test b/tests/install.sh.test new file mode 100644 index 0000000..b5ade05 --- /dev/null +++ b/tests/install.sh.test @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTALL_SCRIPT="$REPO_ROOT/install.sh" +TMP_ROOT="" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_file_contains() { + local file="$1" + local expected="$2" + grep -F "$expected" "$file" >/dev/null || fail "expected '$expected' in $file" +} + +assert_output_contains() { + local output="$1" + local expected="$2" + [[ "$output" == *"$expected"* ]] || fail "expected output to contain '$expected'" +} + +assert_equals() { + local actual="$1" + local expected="$2" + [[ "$actual" == "$expected" ]] || fail "expected '$expected', got '$actual'" +} + +make_upstream_repo() { + local dir="$1" + local version="$2" + + mkdir -p "$dir/.codex/skills/demo" "$dir/.claude/skills/demo" + + cat >"$dir/.codex/skills/demo/SKILL.md" <"$dir/.claude/skills/demo/SKILL.md" <"$dir/AGENTS.md.template" <"$dir/CLAUDE.md.template" <"$wrapper_dir/git" <"$TMP_ROOT/project-codex/AGENTS.md" + guide_before="$(cat "$TMP_ROOT/project-codex/AGENTS.md")" + ( + cd "$UPSTREAM_REPO" + printf '# demo codex v2\n' > .codex/skills/demo/SKILL.md + printf '# AGENTS template v2\n' > AGENTS.md.template + git add .codex/skills/demo/SKILL.md AGENTS.md.template + git commit -qm "upstream v2" + ) + + output="$(run_install "$TMP_ROOT/project-codex" codex)" + assert_equals "$(cat "$TMP_ROOT/project-codex/AGENTS.md")" "$guide_before" + [[ ! -f "$TMP_ROOT/project-codex/AGENTS.md.local.bak" ]] || fail "AGENTS.md.local.bak should not exist" + assert_output_contains "$output" "跳过项目引导" + assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md" "demo codex v2" + [[ -f "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md.local.bak" ]] || fail "skill backup should exist" + + mkdir -p "$TMP_ROOT/project-claude" + output="$(run_install "$TMP_ROOT/project-claude" claude)" + assert_file_contains "$TMP_ROOT/project-claude/.claude/skills/demo/SKILL.md" "demo claude v1" + assert_file_contains "$TMP_ROOT/project-claude/CLAUDE.md" "CLAUDE template v1" + + printf 'custom claude\n' >"$TMP_ROOT/project-claude/CLAUDE.md" + ( + cd "$UPSTREAM_REPO" + printf '# demo claude v2\n' > .claude/skills/demo/SKILL.md + printf '# CLAUDE template v2\n' > CLAUDE.md.template + git add .claude/skills/demo/SKILL.md CLAUDE.md.template + git commit -qm "upstream claude v2" + ) + + output="$(run_install "$TMP_ROOT/project-claude" claude)" + assert_equals "$(cat "$TMP_ROOT/project-claude/CLAUDE.md")" "custom claude" + [[ ! -f "$TMP_ROOT/project-claude/CLAUDE.md.local.bak" ]] || fail "CLAUDE.md.local.bak should not exist" + assert_output_contains "$output" "跳过项目引导" + assert_file_contains "$TMP_ROOT/project-claude/.claude/skills/demo/SKILL.md" "demo claude v2" + + mkdir -p "$TMP_ROOT/project-both" + printf 'team agents\n' >"$TMP_ROOT/project-both/AGENTS.md" + printf 'team claude\n' >"$TMP_ROOT/project-both/CLAUDE.md" + output="$(run_install "$TMP_ROOT/project-both" both)" + assert_equals "$(cat "$TMP_ROOT/project-both/AGENTS.md")" "team agents" + assert_equals "$(cat "$TMP_ROOT/project-both/CLAUDE.md")" "team claude" + assert_file_contains "$TMP_ROOT/project-both/.codex/skills/demo/SKILL.md" "demo codex v2" + assert_file_contains "$TMP_ROOT/project-both/.claude/skills/demo/SKILL.md" "demo claude v2" + assert_output_contains "$output" "跳过项目引导: AGENTS.md" + assert_output_contains "$output" "跳过项目引导: CLAUDE.md" + + echo "PASS" +} + +main "$@"