feat(installer): preserve local skill customizations
This commit is contained in:
@@ -99,10 +99,12 @@ bash /tmp/install-skills.sh both
|
|||||||
| 情况 | 处理方式 |
|
| 情况 | 处理方式 |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| 新 skill(本地没有) | 直接安装 |
|
| 新 skill(本地没有) | 直接安装 |
|
||||||
| 本地未改 + 上游有更新 | 直接覆盖 |
|
| 本地未改 + 上游有更新 | 自动更新,并把旧版本备份为 `SKILL.md.local.bak` |
|
||||||
| 本地魔改过 + 上游有更新 | 写入上游新版,本地版备份为 `SKILL.md.local.bak` |
|
| 本地改过 + 上游有更新 | 保留本地版本,把上游新版写入 `SKILL.md.upstream.new` |
|
||||||
| 本地和上游一致 | 跳过 |
|
| 本地和上游一致 | 跳过 |
|
||||||
|
|
||||||
|
安装脚本会在 `.codex/skills/.install-state/` 或 `.claude/skills/.install-state/` 记录上次安装的上游文件指纹。只有本地文件仍等于上次安装版本时才自动更新;如果用户改过同名 skill,本地特性默认不会被覆盖。
|
||||||
|
|
||||||
项目根目录 guide 文件单独处理:
|
项目根目录 guide 文件单独处理:
|
||||||
|
|
||||||
| 情况 | 处理方式 |
|
| 情况 | 处理方式 |
|
||||||
@@ -112,6 +114,8 @@ bash /tmp/install-skills.sh both
|
|||||||
|
|
||||||
恢复本地版本:`mv SKILL.md.local.bak SKILL.md`
|
恢复本地版本:`mv SKILL.md.local.bak SKILL.md`
|
||||||
对比差异:`diff SKILL.md SKILL.md.local.bak`
|
对比差异:`diff SKILL.md SKILL.md.local.bak`
|
||||||
|
合并本地特性与上游新版:`diff SKILL.md SKILL.md.upstream.new`
|
||||||
|
确认要强制覆盖本地版本时:`bash install.sh codex --force-overwrite`
|
||||||
|
|
||||||
## 使用示例
|
## 使用示例
|
||||||
|
|
||||||
|
|||||||
+92
-6
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# spec-coding-skills 安装/更新脚本
|
# spec-coding-skills 安装/更新脚本
|
||||||
# 用法: bash <(curl -sL <raw-url>/install.sh) [codex|claude|both]
|
# 用法: bash <(curl -sL <raw-url>/install.sh) [codex|claude|both] [--force-overwrite]
|
||||||
# 或: bash install.sh [codex|claude|both|目标目录]
|
# 或: bash install.sh [codex|claude|both|目标目录] [--force-overwrite]
|
||||||
# ============================================================
|
# ============================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -23,7 +23,30 @@ REQUEST="${1:-$DEFAULT_MODE}"
|
|||||||
TOTAL_NEW=0
|
TOTAL_NEW=0
|
||||||
TOTAL_UPDATED=0
|
TOTAL_UPDATED=0
|
||||||
TOTAL_SKIPPED=0
|
TOTAL_SKIPPED=0
|
||||||
|
TOTAL_PRESERVED=0
|
||||||
INSTALLED_TARGETS=""
|
INSTALLED_TARGETS=""
|
||||||
|
FORCE_OVERWRITE="${SPEC_SKILLS_FORCE_OVERWRITE:-0}"
|
||||||
|
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
REQUEST="$DEFAULT_MODE"
|
||||||
|
REQUEST_SET=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--force|--force-overwrite)
|
||||||
|
FORCE_OVERWRITE=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ "$REQUEST_SET" -eq 0 ]; then
|
||||||
|
REQUEST="$arg"
|
||||||
|
REQUEST_SET=1
|
||||||
|
else
|
||||||
|
log_warn "未知参数: $arg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
resolve_layout() {
|
resolve_layout() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
@@ -70,6 +93,53 @@ resolve_layout() {
|
|||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
trap "rm -rf $TMP_DIR" EXIT
|
trap "rm -rf $TMP_DIR" EXIT
|
||||||
|
|
||||||
|
file_hash() {
|
||||||
|
if command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$1" | awk '{print $1}'
|
||||||
|
elif command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$1" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
log_warn "缺少 shasum 或 sha256sum,无法进行本地差异保护"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
state_file_for() {
|
||||||
|
local dst="$1"
|
||||||
|
local rel_path
|
||||||
|
|
||||||
|
rel_path="${dst#$TARGET/}"
|
||||||
|
if [ "$rel_path" = "$dst" ]; then
|
||||||
|
rel_path="$(basename "$dst")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s/.install-state/%s.sha256' "$TARGET" "$rel_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_install_state() {
|
||||||
|
local src="$1"
|
||||||
|
local dst="$2"
|
||||||
|
local state_file
|
||||||
|
|
||||||
|
state_file="$(state_file_for "$dst")"
|
||||||
|
mkdir -p "$(dirname "$state_file")"
|
||||||
|
file_hash "$src" >"$state_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_unmodified_since_last_install() {
|
||||||
|
local dst="$1"
|
||||||
|
local state_file
|
||||||
|
local dst_hash
|
||||||
|
local expected_hash
|
||||||
|
|
||||||
|
state_file="$(state_file_for "$dst")"
|
||||||
|
[ -f "$state_file" ] || return 1
|
||||||
|
|
||||||
|
dst_hash="$(file_hash "$dst")"
|
||||||
|
expected_hash="$(cat "$state_file")"
|
||||||
|
[ "$dst_hash" = "$expected_hash" ]
|
||||||
|
}
|
||||||
|
|
||||||
sync_file() {
|
sync_file() {
|
||||||
local src="$1"
|
local src="$1"
|
||||||
local dst="$2"
|
local dst="$2"
|
||||||
@@ -79,14 +149,23 @@ sync_file() {
|
|||||||
if [ ! -f "$dst" ]; then
|
if [ ! -f "$dst" ]; then
|
||||||
mkdir -p "$(dirname "$dst")"
|
mkdir -p "$(dirname "$dst")"
|
||||||
cp "$src" "$dst"
|
cp "$src" "$dst"
|
||||||
|
write_install_state "$src" "$dst"
|
||||||
log_info "$create_msg"
|
log_info "$create_msg"
|
||||||
TOTAL_NEW=$((TOTAL_NEW + 1))
|
TOTAL_NEW=$((TOTAL_NEW + 1))
|
||||||
elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then
|
elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then
|
||||||
cp "$dst" "$dst.local.bak"
|
if [ "$FORCE_OVERWRITE" = "1" ] || is_unmodified_since_last_install "$dst"; then
|
||||||
cp "$src" "$dst"
|
cp "$dst" "$dst.local.bak"
|
||||||
log_warn "$update_msg"
|
cp "$src" "$dst"
|
||||||
TOTAL_UPDATED=$((TOTAL_UPDATED + 1))
|
write_install_state "$src" "$dst"
|
||||||
|
log_warn "$update_msg"
|
||||||
|
TOTAL_UPDATED=$((TOTAL_UPDATED + 1))
|
||||||
|
else
|
||||||
|
cp "$src" "${dst}.upstream.new"
|
||||||
|
log_warn "🛡️ 保留本地差异: ${dst};远端新版已写入 ${dst}.upstream.new"
|
||||||
|
TOTAL_PRESERVED=$((TOTAL_PRESERVED + 1))
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
|
write_install_state "$src" "$dst"
|
||||||
TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1))
|
TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1))
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -206,6 +285,7 @@ echo " 📦 版本: $VERSION"
|
|||||||
echo ""
|
echo ""
|
||||||
echo " ✨ 新增: $TOTAL_NEW"
|
echo " ✨ 新增: $TOTAL_NEW"
|
||||||
echo " 🔄 更新: ${TOTAL_UPDATED}(本地版本已备份为 .local.bak)"
|
echo " 🔄 更新: ${TOTAL_UPDATED}(本地版本已备份为 .local.bak)"
|
||||||
|
echo " 🛡️ 保留本地差异: $TOTAL_PRESERVED"
|
||||||
echo " ⏭️ 无变化: $TOTAL_SKIPPED"
|
echo " ⏭️ 无变化: $TOTAL_SKIPPED"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -214,3 +294,9 @@ if [ "$TOTAL_UPDATED" -gt 0 ]; then
|
|||||||
log_warn "如需恢复本地版本: mv SKILL.md.local.bak SKILL.md"
|
log_warn "如需恢复本地版本: mv SKILL.md.local.bak SKILL.md"
|
||||||
log_warn "如需对比差异: diff SKILL.md SKILL.md.local.bak"
|
log_warn "如需对比差异: diff SKILL.md SKILL.md.local.bak"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$TOTAL_PRESERVED" -gt 0 ]; then
|
||||||
|
log_warn "有 ${TOTAL_PRESERVED} 个本地改动文件被保护,远端新版已写入 *.upstream.new"
|
||||||
|
log_warn "如需合并: diff SKILL.md SKILL.md.upstream.new"
|
||||||
|
log_warn "如需强制覆盖: 重新运行并追加 --force-overwrite,或设置 SPEC_SKILLS_FORCE_OVERWRITE=1"
|
||||||
|
fi
|
||||||
|
|||||||
+20
-3
@@ -84,10 +84,10 @@ EOF
|
|||||||
|
|
||||||
run_install() {
|
run_install() {
|
||||||
local target_dir="$1"
|
local target_dir="$1"
|
||||||
local mode="$2"
|
shift
|
||||||
(
|
(
|
||||||
cd "$target_dir"
|
cd "$target_dir"
|
||||||
PATH="$GIT_WRAPPER_DIR:$PATH" FAKE_UPSTREAM_REPO="$UPSTREAM_REPO" bash "$INSTALL_SCRIPT" "$mode"
|
PATH="$GIT_WRAPPER_DIR:$PATH" FAKE_UPSTREAM_REPO="$UPSTREAM_REPO" bash "$INSTALL_SCRIPT" "$@"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +127,23 @@ main() {
|
|||||||
assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md" "demo codex v2"
|
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"
|
[[ -f "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md.local.bak" ]] || fail "skill backup should exist"
|
||||||
|
|
||||||
|
printf 'custom demo codex\n' >"$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md"
|
||||||
|
(
|
||||||
|
cd "$UPSTREAM_REPO"
|
||||||
|
printf '# demo codex v3\n' > .codex/skills/demo/SKILL.md
|
||||||
|
git add .codex/skills/demo/SKILL.md
|
||||||
|
git commit -qm "upstream codex v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
output="$(run_install "$TMP_ROOT/project-codex" codex)"
|
||||||
|
assert_equals "$(cat "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md")" "custom demo codex"
|
||||||
|
assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md.upstream.new" "demo codex v3"
|
||||||
|
assert_output_contains "$output" "保留本地差异"
|
||||||
|
|
||||||
|
output="$(run_install "$TMP_ROOT/project-codex" codex --force-overwrite)"
|
||||||
|
assert_file_contains "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md" "demo codex v3"
|
||||||
|
assert_equals "$(cat "$TMP_ROOT/project-codex/.codex/skills/demo/SKILL.md.local.bak")" "custom demo codex"
|
||||||
|
|
||||||
mkdir -p "$TMP_ROOT/project-claude"
|
mkdir -p "$TMP_ROOT/project-claude"
|
||||||
output="$(run_install "$TMP_ROOT/project-claude" 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/skills/demo/SKILL.md" "demo claude v1"
|
||||||
@@ -153,7 +170,7 @@ main() {
|
|||||||
output="$(run_install "$TMP_ROOT/project-both" both)"
|
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/AGENTS.md")" "team agents"
|
||||||
assert_equals "$(cat "$TMP_ROOT/project-both/CLAUDE.md")" "team claude"
|
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/.codex/skills/demo/SKILL.md" "demo codex v3"
|
||||||
assert_file_contains "$TMP_ROOT/project-both/.claude/skills/demo/SKILL.md" "demo claude 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" "跳过项目引导: AGENTS.md"
|
||||||
assert_output_contains "$output" "跳过项目引导: CLAUDE.md"
|
assert_output_contains "$output" "跳过项目引导: CLAUDE.md"
|
||||||
|
|||||||
Reference in New Issue
Block a user