|
| 1 | +#!/bin/bash |
| 2 | +# ensure-codex-skills.sh - Sync Claude Code plugin skills to ~/.agents/skills/ for Codex |
| 3 | +# SessionStart hook: runs in background, idempotent |
| 4 | +# Uses manifest to track managed symlinks, never touches user-created files |
| 5 | + |
| 6 | +set +e # Don't exit on errors — background hook must complete gracefully |
| 7 | + |
| 8 | +# ========== Config ========== |
| 9 | + |
| 10 | +AGENTS_SKILLS="$HOME/.agents/skills" |
| 11 | +CODEX_SKILLS="$HOME/.codex/skills" |
| 12 | +CLAUDE_SKILLS="$HOME/.claude/skills" |
| 13 | +MANIFEST_DIR="$HOME/.cache/codex-skills-sync" |
| 14 | +MANIFEST_FILE="$MANIFEST_DIR/managed.txt" |
| 15 | + |
| 16 | +# Only sync skills from our own marketplace (taptap-plugins) |
| 17 | +# All other marketplaces (claude-plugins-official, pua-skills, etc.) are excluded |
| 18 | +INCLUDE_MARKETPLACE="taptap-plugins" |
| 19 | + |
| 20 | +# Excluded plugins within our marketplace (not published, under development) |
| 21 | +EXCLUDE_PLUGINS="ralph" |
| 22 | + |
| 23 | +# Excluded skill names (cleaned on every run from both ~/.codex/skills and ~/.agents/skills) |
| 24 | +# Covers old hardlinks/real dirs that migration can't detect |
| 25 | +EXCLUDE_SKILLS="ralph-loop ralph-pause ralph-resume ralph-status ralph-adjust ralph-decompose ralph-workflow cancel-ralph prd-to-json" |
| 26 | + |
| 27 | +# Log |
| 28 | +LOG_DIR="$HOME/.claude/plugins/logs" |
| 29 | +mkdir -p "$LOG_DIR" |
| 30 | +LOG_FILE="$LOG_DIR/ensure-codex-skills-$(date +%Y-%m-%d).log" |
| 31 | +exec >> "$LOG_FILE" 2>&1 |
| 32 | +echo "" |
| 33 | +echo "===== $(date '+%Y-%m-%d %H:%M:%S') =====" |
| 34 | + |
| 35 | +# ========== Step 0: Skip if Codex not installed ========== |
| 36 | + |
| 37 | +if [ ! -d "$CODEX_SKILLS" ] && ! command -v codex >/dev/null 2>&1; then |
| 38 | + echo "⏭️ Codex 未安装,跳过" |
| 39 | + exit 0 |
| 40 | +fi |
| 41 | + |
| 42 | +mkdir -p "$AGENTS_SKILLS" |
| 43 | +mkdir -p "$MANIFEST_DIR" |
| 44 | + |
| 45 | +# ========== Step 1: Clean excluded skills from both directories ========== |
| 46 | + |
| 47 | +for skill in $EXCLUDE_SKILLS; do |
| 48 | + for dir in "$CODEX_SKILLS" "$AGENTS_SKILLS"; do |
| 49 | + if [ -e "$dir/$skill" ] || [ -L "$dir/$skill" ]; then |
| 50 | + rm -rf "${dir:?}/${skill:?}" |
| 51 | + echo "🗑️ 已删除排除 skill: $dir/$skill" |
| 52 | + fi |
| 53 | + done |
| 54 | +done |
| 55 | + |
| 56 | +# ========== Step 2: Clean old managed symlinks from ~/.agents/skills/ ========== |
| 57 | + |
| 58 | +if [ -f "$MANIFEST_FILE" ]; then |
| 59 | + while IFS= read -r name; do |
| 60 | + [ -z "$name" ] && continue |
| 61 | + target="$AGENTS_SKILLS/$name" |
| 62 | + # Only remove if it's a symlink (we created it), never remove real dirs |
| 63 | + if [ -L "$target" ]; then |
| 64 | + rm -f "$target" |
| 65 | + fi |
| 66 | + done < "$MANIFEST_FILE" |
| 67 | +fi |
| 68 | + |
| 69 | +# ========== Step 3: Migration — clean old symlinks from both dirs ========== |
| 70 | + |
| 71 | +MIGRATION_MARKER="$MANIFEST_DIR/.v3_migrated" |
| 72 | +if [ ! -f "$MIGRATION_MARKER" ]; then |
| 73 | + # Clean symlinks pointing to plugin/marketplace paths from BOTH directories |
| 74 | + # In ~/.agents/skills/: only keep symlinks pointing to our marketplace (taptap-plugins) |
| 75 | + # In ~/.codex/skills/: remove all symlinks pointing to plugin paths |
| 76 | + for dir in "$AGENTS_SKILLS" "$CODEX_SKILLS"; do |
| 77 | + [ -d "$dir" ] || continue |
| 78 | + for entry in "$dir"/*/; do |
| 79 | + [ -e "$entry" ] || [ -L "${entry%/}" ] || continue |
| 80 | + skill_name="$(basename "$entry")" |
| 81 | + |
| 82 | + target_path="${dir}/${skill_name}" |
| 83 | + if [ -L "$target_path" ]; then |
| 84 | + link_target="$(readlink "$target_path" 2>/dev/null || true)" |
| 85 | + case "$link_target" in |
| 86 | + */.claude/plugins/*|*/claude-plugins-marketplace/*) |
| 87 | + # In agents dir: keep our own marketplace symlinks (will be re-managed) |
| 88 | + # In codex dir: remove all plugin symlinks |
| 89 | + if [ "$dir" = "$CODEX_SKILLS" ]; then |
| 90 | + rm -f "$target_path" |
| 91 | + echo "🔗 已清理旧 symlink: $dir/$skill_name" |
| 92 | + elif echo "$link_target" | grep -qv "/$INCLUDE_MARKETPLACE/"; then |
| 93 | + # agents dir: remove symlinks from other marketplaces |
| 94 | + rm -f "$target_path" |
| 95 | + echo "🔗 已清理非本 marketplace symlink: $skill_name" |
| 96 | + fi |
| 97 | + ;; |
| 98 | + esac |
| 99 | + fi |
| 100 | + done |
| 101 | + done |
| 102 | + |
| 103 | + # Clean hardlinks in ~/.agents/skills/ (same inode as ~/.codex/skills/) |
| 104 | + if [ -d "$CODEX_SKILLS" ]; then |
| 105 | + for entry in "$AGENTS_SKILLS"/*/; do |
| 106 | + [ -d "$entry" ] || continue |
| 107 | + skill_name="$(basename "$entry")" |
| 108 | + [ -L "$AGENTS_SKILLS/$skill_name" ] && continue |
| 109 | + agents_file="$AGENTS_SKILLS/$skill_name/SKILL.md" |
| 110 | + codex_file="$CODEX_SKILLS/$skill_name/SKILL.md" |
| 111 | + if [ -f "$agents_file" ] && [ -f "$codex_file" ]; then |
| 112 | + agents_inode="$(stat -f '%i' "$agents_file" 2>/dev/null || stat -c '%i' "$agents_file" 2>/dev/null)" |
| 113 | + codex_inode="$(stat -f '%i' "$codex_file" 2>/dev/null || stat -c '%i' "$codex_file" 2>/dev/null)" |
| 114 | + if [ -n "$agents_inode" ] && [ "$agents_inode" = "$codex_inode" ]; then |
| 115 | + rm -rf "${AGENTS_SKILLS:?}/${skill_name:?}" |
| 116 | + echo "🔗 已清理硬链接: $skill_name" |
| 117 | + fi |
| 118 | + fi |
| 119 | + done |
| 120 | + fi |
| 121 | + |
| 122 | + touch "$MIGRATION_MARKER" |
| 123 | + echo "✅ 迁移完成(v3)" |
| 124 | +fi |
| 125 | + |
| 126 | +# ========== Step 4: Sync plugin skills to ~/.agents/skills/ ========== |
| 127 | + |
| 128 | +new_manifest="" |
| 129 | + |
| 130 | +MP_DIR="$HOME/.claude/plugins/marketplaces/$INCLUDE_MARKETPLACE" |
| 131 | +for skill_md in \ |
| 132 | + "$MP_DIR"/plugins/*/skills/*/SKILL.md \ |
| 133 | + "$MP_DIR"/.claude/skills/*/SKILL.md; do |
| 134 | + [ -f "$skill_md" ] || continue |
| 135 | + skill_dir="$(dirname "$skill_md")" |
| 136 | + skill_name="$(basename "$skill_dir")" |
| 137 | + |
| 138 | + # Check if plugin is excluded |
| 139 | + excluded=false |
| 140 | + for ep in $EXCLUDE_PLUGINS; do |
| 141 | + if echo "$skill_dir" | grep -q "/plugins/$ep/"; then |
| 142 | + excluded=true |
| 143 | + break |
| 144 | + fi |
| 145 | + done |
| 146 | + [ "$excluded" = "true" ] && continue |
| 147 | + |
| 148 | + # Check if skill name is excluded |
| 149 | + for es in $EXCLUDE_SKILLS; do |
| 150 | + if [ "$skill_name" = "$es" ]; then |
| 151 | + excluded=true |
| 152 | + break |
| 153 | + fi |
| 154 | + done |
| 155 | + [ "$excluded" = "true" ] && continue |
| 156 | + |
| 157 | + # Skip if already exists as a real directory (user-created, not our symlink) |
| 158 | + if [ -e "$AGENTS_SKILLS/$skill_name" ] && [ ! -L "$AGENTS_SKILLS/$skill_name" ]; then |
| 159 | + continue |
| 160 | + fi |
| 161 | + |
| 162 | + # Create symlink in ~/.agents/skills/ |
| 163 | + ln -sf "$skill_dir" "$AGENTS_SKILLS/$skill_name" |
| 164 | + new_manifest="$new_manifest |
| 165 | +$skill_name" |
| 166 | +done |
| 167 | + |
| 168 | +# ========== Step 5: Sync ~/.claude/skills/ ========== |
| 169 | + |
| 170 | +if [ -d "$CLAUDE_SKILLS" ]; then |
| 171 | + for skill_dir in "$CLAUDE_SKILLS"/*/; do |
| 172 | + [ -d "$skill_dir" ] || continue |
| 173 | + [ -f "$skill_dir/SKILL.md" ] || continue |
| 174 | + skill_name="$(basename "$skill_dir")" |
| 175 | + |
| 176 | + # Check exclusions |
| 177 | + excluded=false |
| 178 | + for es in $EXCLUDE_SKILLS; do |
| 179 | + [ "$skill_name" = "$es" ] && excluded=true && break |
| 180 | + done |
| 181 | + [ "$excluded" = "true" ] && continue |
| 182 | + |
| 183 | + # Skip if already exists (real dir, user symlink, or already managed) |
| 184 | + if [ -e "$AGENTS_SKILLS/$skill_name" ] || [ -L "$AGENTS_SKILLS/$skill_name" ]; then |
| 185 | + continue |
| 186 | + fi |
| 187 | + |
| 188 | + ln -sf "${skill_dir%/}" "$AGENTS_SKILLS/$skill_name" |
| 189 | + new_manifest="$new_manifest |
| 190 | +$skill_name" |
| 191 | + done |
| 192 | +fi |
| 193 | + |
| 194 | +# ========== Step 6: Update manifest ========== |
| 195 | + |
| 196 | +echo "$new_manifest" | sed '/^$/d' | sort -u > "$MANIFEST_FILE" |
| 197 | +managed_count="$(wc -l < "$MANIFEST_FILE" | tr -d ' ')" |
| 198 | +echo "✅ 同步完成: $managed_count 个 managed symlinks in ~/.agents/skills/" |
| 199 | + |
| 200 | +echo "===== 完成 =====" |
0 commit comments