#!/usr/bin/env bash # ============================================================ # spec-coding-skills 安装/更新脚本 # 用法: bash <(curl -sL /install.sh) [codex|claude|both] [--force-overwrite] # 或: bash install.sh [codex|claude|both|目标目录] [--force-overwrite] # ============================================================ set -euo pipefail REPO_URL="https://git.internal.intelligrow.cn/zhangfucai/spec-coding-skills.git" DEFAULT_MODE="codex" GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_title() { echo -e "${CYAN}$1${NC}"; } MODE="" TARGET="" SKILLS_SRC="" GUIDE_SRC="" GUIDE_DST="" REQUEST="${1:-$DEFAULT_MODE}" TOTAL_NEW=0 TOTAL_UPDATED=0 TOTAL_SKIPPED=0 TOTAL_PRESERVED=0 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() { local input="$1" case "$input" in ""|codex) MODE="codex" TARGET=".codex/skills" SKILLS_SRC=".codex/skills" GUIDE_SRC="AGENTS.md.template" GUIDE_DST="AGENTS.md" ;; claude) MODE="claude" TARGET=".claude/skills" SKILLS_SRC=".claude/skills" GUIDE_SRC="CLAUDE.md.template" GUIDE_DST="CLAUDE.md" ;; both) log_warn "both 不是单一布局,请在主流程中单独处理" exit 1 ;; *) TARGET="$input" case "$TARGET" in *".claude/skills"*) MODE="claude" SKILLS_SRC=".claude/skills" GUIDE_SRC="CLAUDE.md.template" GUIDE_DST="CLAUDE.md" ;; *) MODE="codex" SKILLS_SRC=".codex/skills" GUIDE_SRC="AGENTS.md.template" GUIDE_DST="AGENTS.md" ;; esac ;; esac } TMP_DIR=$(mktemp -d) 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() { local src="$1" local dst="$2" local create_msg="$3" local update_msg="$4" if [ ! -f "$dst" ]; then mkdir -p "$(dirname "$dst")" cp "$src" "$dst" write_install_state "$src" "$dst" log_info "$create_msg" TOTAL_NEW=$((TOTAL_NEW + 1)) elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then if [ "$FORCE_OVERWRITE" = "1" ] || is_unmodified_since_last_install "$dst"; then cp "$dst" "$dst.local.bak" cp "$src" "$dst" 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 write_install_state "$src" "$dst" TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1)) 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 local skill_name local src_file local rel_path local dst_dir local dst_file local tpl_file local tpl_name local dst local guide_src_path resolve_layout "$input" if [ ! -d "$TMP_DIR/$SKILLS_SRC" ]; then log_warn "上游仓库中不存在技能目录: $SKILLS_SRC" exit 1 fi log_info "同步 $MODE: $TARGET" mkdir -p "$TARGET" for skill_dir in "$TMP_DIR/$SKILLS_SRC"/*/; do [ -d "$skill_dir" ] || continue skill_name=$(basename "$skill_dir") dst_dir="$TARGET/$skill_name" [ -f "$skill_dir/SKILL.md" ] || continue while IFS= read -r -d '' src_file; do rel_path="${src_file#$skill_dir}" dst_file="$dst_dir/$rel_path" sync_file \ "$src_file" \ "$dst_file" \ "✨ 新增: $skill_name/$rel_path ($MODE)" \ "🔄 更新: $skill_name/$rel_path ($MODE) (本地版本已备份为 $(basename "$rel_path").local.bak)" done < <(find "$skill_dir" -type f -print0) done for tpl_file in "$TMP_DIR/$SKILLS_SRC"/*.template "$TMP_DIR/$SKILLS_SRC"/*.md; do [ -f "$tpl_file" ] || continue tpl_name=$(basename "$tpl_file") dst="$TARGET/$tpl_name" sync_file \ "$tpl_file" \ "$dst" \ "✨ 新增模板: $tpl_name ($MODE)" \ "🔄 更新模板: $tpl_name ($MODE) (本地版本已备份为 ${tpl_name}.local.bak)" done guide_src_path="$TMP_DIR/$GUIDE_SRC" if [ -f "$guide_src_path" ]; then sync_guide_file \ "$guide_src_path" \ "$GUIDE_DST" \ "✨ 新增项目引导: ${GUIDE_DST}" \ "⏭️ 跳过项目引导: ${GUIDE_DST}(已存在,保持原样)" fi INSTALLED_TARGETS="${INSTALLED_TARGETS}${INSTALLED_TARGETS:+, }$TARGET" } # ---------- 拉取最新 ---------- log_title "📦 spec-coding-skills 安装/更新" echo "" log_info "拉取最新版本..." git clone --depth 1 --quiet "$REPO_URL" "$TMP_DIR" VERSION=$(git -C "$TMP_DIR" describe --tags --always 2>/dev/null || git -C "$TMP_DIR" rev-parse --short HEAD) log_info "版本: $VERSION" case "$REQUEST" in both) log_info "模式: both" install_layout codex install_layout claude ;; *) resolve_layout "$REQUEST" log_info "模式: $MODE" install_layout "$REQUEST" ;; esac # ---------- 汇总 ---------- echo "" log_title "✅ 完成!" echo "" echo " 🧭 模式: $REQUEST" echo " 📁 目标目录: $INSTALLED_TARGETS" echo " 📦 版本: $VERSION" echo "" echo " ✨ 新增: $TOTAL_NEW" echo " 🔄 更新: ${TOTAL_UPDATED}(本地版本已备份为 .local.bak)" echo " 🛡️ 保留本地差异: $TOTAL_PRESERVED" echo " ⏭️ 无变化: $TOTAL_SKIPPED" echo "" if [ "$TOTAL_UPDATED" -gt 0 ]; then 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 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