install.sh 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env bash
  2. #
  3. # 把 Agent 仓库的 skills/ 同步到 Claude Code 或 Codex 的 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 会污染客户端的 skills 目录(Claude Code / Codex 都可能把 .bak.* 当 skill 索引)。
  16. #
  17. # 用法:
  18. # bash install.sh # 安装到 Claude Code(默认 ~/.claude/skills)
  19. # bash install.sh --codex # 安装到 Codex(默认 ${CODEX_HOME:-~/.codex}/skills)
  20. # bash install.sh --claude # 明确安装到 Claude Code
  21. # bash install.sh --copy # copy 模式(默认 symlink)
  22. # bash install.sh --force # 冲突时强制覆盖
  23. # bash install.sh --target DIR # 改安装目录
  24. # bash install.sh --dry-run # 只打印会做什么,不动文件
  25. # bash install.sh --skills a,b # 只装指定的(默认全装)
  26. #
  27. set -euo pipefail
  28. REPO_SKILLS_DIR="$(cd "$(dirname "$0")" && pwd)"
  29. PLATFORM="claude"
  30. TARGET_DIR=""
  31. MODE="symlink"
  32. DRY_RUN=0
  33. FORCE=0
  34. ONLY_SKILLS=""
  35. while [[ $# -gt 0 ]]; do
  36. case "$1" in
  37. --claude) PLATFORM="claude"; shift ;;
  38. --codex) PLATFORM="codex"; shift ;;
  39. --platform)
  40. PLATFORM="$2"; shift 2 ;;
  41. --copy) MODE="copy"; shift ;;
  42. --symlink) MODE="symlink"; shift ;;
  43. --target) TARGET_DIR="$2"; shift 2 ;;
  44. --dry-run) DRY_RUN=1; shift ;;
  45. --force) FORCE=1; shift ;;
  46. --skills) ONLY_SKILLS="$2"; shift 2 ;;
  47. -h|--help)
  48. grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -30
  49. exit 0 ;;
  50. *) echo "unknown arg: $1" >&2; exit 1 ;;
  51. esac
  52. done
  53. case "$PLATFORM" in
  54. claude)
  55. DEFAULT_TARGET_DIR="${HOME}/.claude/skills"
  56. ;;
  57. codex)
  58. DEFAULT_TARGET_DIR="${CODEX_HOME:-${HOME}/.codex}/skills"
  59. ;;
  60. *)
  61. echo "unknown platform: $PLATFORM (expected: claude or codex)" >&2
  62. exit 1
  63. ;;
  64. esac
  65. if [[ -z "$TARGET_DIR" ]]; then
  66. TARGET_DIR="$DEFAULT_TARGET_DIR"
  67. fi
  68. run() {
  69. if [[ $DRY_RUN -eq 1 ]]; then
  70. echo "[dry-run] $*"
  71. else
  72. eval "$@"
  73. fi
  74. }
  75. # 收集要安装的 skill 列表
  76. if [[ -n "$ONLY_SKILLS" ]]; then
  77. IFS=',' read -r -a skills <<< "$ONLY_SKILLS"
  78. else
  79. skills=()
  80. for d in "$REPO_SKILLS_DIR"/*/; do
  81. [[ -d "$d" ]] || continue
  82. name="$(basename "$d")"
  83. [[ "$name" == "__pycache__" ]] && continue
  84. skills+=("$name")
  85. done
  86. fi
  87. echo "platform : $PLATFORM"
  88. echo "mode : $MODE"
  89. echo "source : $REPO_SKILLS_DIR"
  90. echo "target : $TARGET_DIR"
  91. echo "force : $FORCE"
  92. echo "skills : ${skills[*]}"
  93. echo "---"
  94. run "mkdir -p '$TARGET_DIR'"
  95. conflict_count=0
  96. for name in "${skills[@]}"; do
  97. src="$REPO_SKILLS_DIR/$name"
  98. dst="$TARGET_DIR/$name"
  99. if [[ ! -d "$src" ]]; then
  100. echo "⚠ skip (not found in repo): $name"
  101. continue
  102. fi
  103. # 已经是指向本 repo 的 symlink → 跳过
  104. if [[ -L "$dst" ]]; then
  105. current="$(readlink "$dst")"
  106. if [[ "$current" == "$src" ]]; then
  107. echo "= $name (already linked, skip)"
  108. continue
  109. fi
  110. fi
  111. # 目标存在但不是我们想要的状态 → 要么 --force 覆盖,要么报错
  112. if [[ -e "$dst" || -L "$dst" ]]; then
  113. if [[ $FORCE -eq 0 ]]; then
  114. echo "✗ $name: target exists → $dst"
  115. echo " (现状: $( [[ -L "$dst" ]] && echo "symlink → $(readlink "$dst")" || echo "directory/file" ))"
  116. echo " 用 --force 覆盖;或手动处理后重跑。"
  117. conflict_count=$((conflict_count + 1))
  118. continue
  119. fi
  120. echo "↺ $name (forced: removing existing)"
  121. run "rm -rf '$dst'"
  122. fi
  123. case "$MODE" in
  124. symlink)
  125. run "ln -s '$src' '$dst'"
  126. echo "✓ $name (symlinked)"
  127. ;;
  128. copy)
  129. run "cp -R '$src' '$dst'"
  130. run "rm -rf '$dst/__pycache__'"
  131. echo "✓ $name (copied)"
  132. ;;
  133. esac
  134. done
  135. echo ""
  136. if [[ $conflict_count -gt 0 ]]; then
  137. echo "⚠ $conflict_count 个 skill 因冲突未安装。用 --force 覆盖或先手动处理。"
  138. exit 2
  139. fi
  140. echo "安装完成。"
  141. if [[ "$MODE" == "symlink" ]]; then
  142. echo "symlink 模式:在 $REPO_SKILLS_DIR 下改文件,$TARGET_DIR 自动跟随。"
  143. else
  144. echo "copy 模式:repo 改动后需要重跑本脚本才会同步。"
  145. fi
  146. if [[ "$PLATFORM" == "codex" ]]; then
  147. echo "Codex 需要重启后才会重新索引新安装的 skills。"
  148. fi