install.sh 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env bash
  2. #
  3. # 把 Agent 仓库的 skills/ 同步到 ~/.claude/skills/
  4. #
  5. # 默认用 **symlink**:源头在 repo,edit-in-repo 立即生效,适合本机开发。
  6. # 跨机器部署/发布用 --copy:把文件 rsync 过去,脱离 repo。
  7. #
  8. # 默认行为:
  9. # - 目标不存在 → 创建
  10. # - 目标已是指向本 repo 的 symlink → 跳过(no-op)
  11. # - 目标已存在(其他 symlink / 目录 / 文件)→ **拒绝安装并报错**,让你自己决定如何处理
  12. # - 用 --force 才会强制覆盖(先 rm 再装)
  13. #
  14. # 不自动备份的原因:skill 已进版本控制,旧内容通常有 git 历史;
  15. # 盲目 backup 会污染 ~/.claude/skills/(Claude Code 会把 .bak.* 当 skill 索引)。
  16. #
  17. # 用法:
  18. # bash install.sh # symlink 模式(默认,冲突即失败)
  19. # bash install.sh --copy # copy 模式
  20. # bash install.sh --force # 冲突时强制覆盖
  21. # bash install.sh --target DIR # 改安装目录(默认 ~/.claude/skills)
  22. # bash install.sh --dry-run # 只打印会做什么,不动文件
  23. # bash install.sh --skills a,b # 只装指定的(默认全装)
  24. #
  25. set -euo pipefail
  26. REPO_SKILLS_DIR="$(cd "$(dirname "$0")" && pwd)"
  27. TARGET_DIR="${HOME}/.claude/skills"
  28. MODE="symlink"
  29. DRY_RUN=0
  30. FORCE=0
  31. ONLY_SKILLS=""
  32. while [[ $# -gt 0 ]]; do
  33. case "$1" in
  34. --copy) MODE="copy"; shift ;;
  35. --symlink) MODE="symlink"; shift ;;
  36. --target) TARGET_DIR="$2"; shift 2 ;;
  37. --dry-run) DRY_RUN=1; shift ;;
  38. --force) FORCE=1; shift ;;
  39. --skills) ONLY_SKILLS="$2"; shift 2 ;;
  40. -h|--help)
  41. grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -30
  42. exit 0 ;;
  43. *) echo "unknown arg: $1" >&2; exit 1 ;;
  44. esac
  45. done
  46. run() {
  47. if [[ $DRY_RUN -eq 1 ]]; then
  48. echo "[dry-run] $*"
  49. else
  50. eval "$@"
  51. fi
  52. }
  53. # 收集要安装的 skill 列表
  54. if [[ -n "$ONLY_SKILLS" ]]; then
  55. IFS=',' read -r -a skills <<< "$ONLY_SKILLS"
  56. else
  57. skills=()
  58. for d in "$REPO_SKILLS_DIR"/*/; do
  59. [[ -d "$d" ]] || continue
  60. name="$(basename "$d")"
  61. [[ "$name" == "__pycache__" ]] && continue
  62. skills+=("$name")
  63. done
  64. fi
  65. echo "mode : $MODE"
  66. echo "source : $REPO_SKILLS_DIR"
  67. echo "target : $TARGET_DIR"
  68. echo "force : $FORCE"
  69. echo "skills : ${skills[*]}"
  70. echo "---"
  71. run "mkdir -p '$TARGET_DIR'"
  72. conflict_count=0
  73. for name in "${skills[@]}"; do
  74. src="$REPO_SKILLS_DIR/$name"
  75. dst="$TARGET_DIR/$name"
  76. if [[ ! -d "$src" ]]; then
  77. echo "⚠ skip (not found in repo): $name"
  78. continue
  79. fi
  80. # 已经是指向本 repo 的 symlink → 跳过
  81. if [[ -L "$dst" ]]; then
  82. current="$(readlink "$dst")"
  83. if [[ "$current" == "$src" ]]; then
  84. echo "= $name (already linked, skip)"
  85. continue
  86. fi
  87. fi
  88. # 目标存在但不是我们想要的状态 → 要么 --force 覆盖,要么报错
  89. if [[ -e "$dst" || -L "$dst" ]]; then
  90. if [[ $FORCE -eq 0 ]]; then
  91. echo "✗ $name: target exists → $dst"
  92. echo " (现状: $( [[ -L "$dst" ]] && echo "symlink → $(readlink "$dst")" || echo "directory/file" ))"
  93. echo " 用 --force 覆盖;或手动处理后重跑。"
  94. conflict_count=$((conflict_count + 1))
  95. continue
  96. fi
  97. echo "↺ $name (forced: removing existing)"
  98. run "rm -rf '$dst'"
  99. fi
  100. case "$MODE" in
  101. symlink)
  102. run "ln -s '$src' '$dst'"
  103. echo "✓ $name (symlinked)"
  104. ;;
  105. copy)
  106. run "cp -R '$src' '$dst'"
  107. run "rm -rf '$dst/__pycache__'"
  108. echo "✓ $name (copied)"
  109. ;;
  110. esac
  111. done
  112. echo ""
  113. if [[ $conflict_count -gt 0 ]]; then
  114. echo "⚠ $conflict_count 个 skill 因冲突未安装。用 --force 覆盖或先手动处理。"
  115. exit 2
  116. fi
  117. echo "安装完成。"
  118. if [[ "$MODE" == "symlink" ]]; then
  119. echo "symlink 模式:在 $REPO_SKILLS_DIR 下改文件,$TARGET_DIR 自动跟随。"
  120. else
  121. echo "copy 模式:repo 改动后需要重跑本脚本才会同步。"
  122. fi