| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- #!/usr/bin/env bash
- #
- # 把 Agent 仓库的 skills/ 同步到 ~/.claude/skills/
- #
- # 默认用 **symlink**:源头在 repo,edit-in-repo 立即生效,适合本机开发。
- # 跨机器部署/发布用 --copy:把文件 rsync 过去,脱离 repo。
- #
- # 默认行为:
- # - 目标不存在 → 创建
- # - 目标已是指向本 repo 的 symlink → 跳过(no-op)
- # - 目标已存在(其他 symlink / 目录 / 文件)→ **拒绝安装并报错**,让你自己决定如何处理
- # - 用 --force 才会强制覆盖(先 rm 再装)
- #
- # 不自动备份的原因:skill 已进版本控制,旧内容通常有 git 历史;
- # 盲目 backup 会污染 ~/.claude/skills/(Claude Code 会把 .bak.* 当 skill 索引)。
- #
- # 用法:
- # bash install.sh # symlink 模式(默认,冲突即失败)
- # bash install.sh --copy # copy 模式
- # bash install.sh --force # 冲突时强制覆盖
- # bash install.sh --target DIR # 改安装目录(默认 ~/.claude/skills)
- # bash install.sh --dry-run # 只打印会做什么,不动文件
- # bash install.sh --skills a,b # 只装指定的(默认全装)
- #
- set -euo pipefail
- REPO_SKILLS_DIR="$(cd "$(dirname "$0")" && pwd)"
- TARGET_DIR="${HOME}/.claude/skills"
- MODE="symlink"
- DRY_RUN=0
- FORCE=0
- ONLY_SKILLS=""
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --copy) MODE="copy"; shift ;;
- --symlink) MODE="symlink"; shift ;;
- --target) TARGET_DIR="$2"; shift 2 ;;
- --dry-run) DRY_RUN=1; shift ;;
- --force) FORCE=1; shift ;;
- --skills) ONLY_SKILLS="$2"; shift 2 ;;
- -h|--help)
- grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -30
- exit 0 ;;
- *) echo "unknown arg: $1" >&2; exit 1 ;;
- esac
- done
- run() {
- if [[ $DRY_RUN -eq 1 ]]; then
- echo "[dry-run] $*"
- else
- eval "$@"
- fi
- }
- # 收集要安装的 skill 列表
- if [[ -n "$ONLY_SKILLS" ]]; then
- IFS=',' read -r -a skills <<< "$ONLY_SKILLS"
- else
- skills=()
- for d in "$REPO_SKILLS_DIR"/*/; do
- [[ -d "$d" ]] || continue
- name="$(basename "$d")"
- [[ "$name" == "__pycache__" ]] && continue
- skills+=("$name")
- done
- fi
- echo "mode : $MODE"
- echo "source : $REPO_SKILLS_DIR"
- echo "target : $TARGET_DIR"
- echo "force : $FORCE"
- echo "skills : ${skills[*]}"
- echo "---"
- run "mkdir -p '$TARGET_DIR'"
- conflict_count=0
- for name in "${skills[@]}"; do
- src="$REPO_SKILLS_DIR/$name"
- dst="$TARGET_DIR/$name"
- if [[ ! -d "$src" ]]; then
- echo "⚠ skip (not found in repo): $name"
- continue
- fi
- # 已经是指向本 repo 的 symlink → 跳过
- if [[ -L "$dst" ]]; then
- current="$(readlink "$dst")"
- if [[ "$current" == "$src" ]]; then
- echo "= $name (already linked, skip)"
- continue
- fi
- fi
- # 目标存在但不是我们想要的状态 → 要么 --force 覆盖,要么报错
- if [[ -e "$dst" || -L "$dst" ]]; then
- if [[ $FORCE -eq 0 ]]; then
- echo "✗ $name: target exists → $dst"
- echo " (现状: $( [[ -L "$dst" ]] && echo "symlink → $(readlink "$dst")" || echo "directory/file" ))"
- echo " 用 --force 覆盖;或手动处理后重跑。"
- conflict_count=$((conflict_count + 1))
- continue
- fi
- echo "↺ $name (forced: removing existing)"
- run "rm -rf '$dst'"
- fi
- case "$MODE" in
- symlink)
- run "ln -s '$src' '$dst'"
- echo "✓ $name (symlinked)"
- ;;
- copy)
- run "cp -R '$src' '$dst'"
- run "rm -rf '$dst/__pycache__'"
- echo "✓ $name (copied)"
- ;;
- esac
- done
- echo ""
- if [[ $conflict_count -gt 0 ]]; then
- echo "⚠ $conflict_count 个 skill 因冲突未安装。用 --force 覆盖或先手动处理。"
- exit 2
- fi
- echo "安装完成。"
- if [[ "$MODE" == "symlink" ]]; then
- echo "symlink 模式:在 $REPO_SKILLS_DIR 下改文件,$TARGET_DIR 自动跟随。"
- else
- echo "copy 模式:repo 改动后需要重跑本脚本才会同步。"
- fi
|