|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Bootstrap Claude Code skills/commands into ~/.claude/ from every PyAuto |
| 3 | +# organism repo that hosts skills. |
| 4 | +# |
| 5 | +# This installer lives in PyAutoBrain (the reasoning/orchestration organ). It |
| 6 | +# scans every organ repo's skills/ dir and symlinks their skills+commands into |
| 7 | +# ~/.claude/. Roots that aren't checked out are simply skipped. |
| 8 | +# |
| 9 | +# Discovery roots (scanned in order): |
| 10 | +# - admin_jammy/skills/ — vestigial: admin_jammy hosts no skills and is |
| 11 | +# slated to leave PyAutoLabs/; kept only so an old |
| 12 | +# checkout still resolves, auto-skipped once gone |
| 13 | +# - PyAutoMind/skills/ — registry-coupled skills (create_issue, handoff) |
| 14 | +# - PyAutoBrain/skills/ — development-workflow skills (start_*/ship_*/plan_branches/…) |
| 15 | +# - PyAutoHeart/skills/ — status / readiness / diagnostic skills |
| 16 | +# - PyAutoBuild/skills/ — release-execution skills ONLY (pre_build) |
| 17 | +# - autolens_profiling/skills/ — science-profiling skills (profile_likelihood) |
| 18 | +# |
| 19 | +# (PyAutoBuild's root is for its own release/packaging-execution skills only — it |
| 20 | +# owns NO dev-workflow skills; the ship_* skills merely call its release step.) |
| 21 | +# |
| 22 | +# Auto-discovers skills in each root: |
| 23 | +# - A directory with SKILL.md → installed as ~/.claude/skills/<name>/ |
| 24 | +# - A directory with <name>.md (and no SKILL.md) → installed as a flat |
| 25 | +# command at ~/.claude/commands/<name>.md |
| 26 | +# |
| 27 | +# Safe to re-run — existing symlinks are replaced, non-symlink files are skipped. |
| 28 | +# |
| 29 | +# Usage: |
| 30 | +# bash PyAutoBrain/bin/install.sh |
| 31 | + |
| 32 | +set -euo pipefail |
| 33 | + |
| 34 | +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" |
| 35 | +PYAUTO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" |
| 36 | +ADMIN_SKILLS_DIR="$PYAUTO_ROOT/admin_jammy/skills" |
| 37 | +MIND_SKILLS_DIR="$PYAUTO_ROOT/PyAutoMind/skills" |
| 38 | +BRAIN_SKILLS_DIR="$PYAUTO_ROOT/PyAutoBrain/skills" |
| 39 | +HEART_SKILLS_DIR="$PYAUTO_ROOT/PyAutoHeart/skills" |
| 40 | +BUILD_SKILLS_DIR="$PYAUTO_ROOT/PyAutoBuild/skills" |
| 41 | +PROFILING_SKILLS_DIR="$PYAUTO_ROOT/autolens_profiling/skills" |
| 42 | + |
| 43 | +# ---------- Execution-environment note ---------- |
| 44 | +# |
| 45 | +# Skill discovery is identical in every execution environment (local-dev, |
| 46 | +# web-github, ci-only). When a root repo is not checked out, it is simply |
| 47 | +# skipped — the skills from the roots that ARE present still install. |
| 48 | + |
| 49 | +if [ -d "$PYAUTO_ROOT/PyAutoFit" ] || [ -d "$HOME/Code/PyAutoLabs/PyAutoFit" ]; then |
| 50 | + echo "Environment: local-dev (PyAuto repos detected)" |
| 51 | +else |
| 52 | + echo "Environment: web-github / ci-only (clone roots on demand)" |
| 53 | +fi |
| 54 | +echo "" |
| 55 | + |
| 56 | +mkdir -p "$HOME/.claude/skills" "$HOME/.claude/commands" |
| 57 | + |
| 58 | +# ---------- Prune stale symlinks ---------- |
| 59 | +# |
| 60 | +# Self-healing: when a skill is removed or re-homed, its old ~/.claude symlink |
| 61 | +# would otherwise dangle. Remove only BROKEN symlinks whose target points into a |
| 62 | +# PyAuto root — never touch real files or symlinks that resolve, and never touch |
| 63 | +# links pointing outside the managed roots (e.g. user-added skills). |
| 64 | + |
| 65 | +prune_stale_symlinks() { |
| 66 | + local dir="$1" |
| 67 | + [ -d "$dir" ] || return 0 |
| 68 | + for link in "$dir"/*; do |
| 69 | + [ -L "$link" ] || continue |
| 70 | + [ -e "$link" ] && continue # resolves fine — keep |
| 71 | + local tgt; tgt="$(readlink "$link")" |
| 72 | + case "$tgt" in |
| 73 | + "$PYAUTO_ROOT"/*|"$HOME/Code/PyAutoLabs"/*) |
| 74 | + echo " PRUNE $(basename "$link") (stale → $tgt)"; rm -f "$link" ;; |
| 75 | + esac |
| 76 | + done |
| 77 | +} |
| 78 | + |
| 79 | +prune_stale_symlinks "$HOME/.claude/skills" |
| 80 | +prune_stale_symlinks "$HOME/.claude/commands" |
| 81 | + |
| 82 | +# ---------- Install one source dir's skills/commands ---------- |
| 83 | + |
| 84 | +install_from_dir() { |
| 85 | + local source_dir="$1" |
| 86 | + local label="$2" |
| 87 | + |
| 88 | + if [ ! -d "$source_dir" ]; then |
| 89 | + echo "${label}: (source dir not present, skipping)" |
| 90 | + return |
| 91 | + fi |
| 92 | + |
| 93 | + echo "$label" |
| 94 | + local installed_count=0 |
| 95 | + |
| 96 | + for entry in "$source_dir"/*/; do |
| 97 | + [ -d "$entry" ] || continue |
| 98 | + local name |
| 99 | + name=$(basename "$entry") |
| 100 | + |
| 101 | + # Skip the install.sh dir itself, or any non-skill dirs |
| 102 | + [ "$name" = "skills" ] && continue |
| 103 | + |
| 104 | + if [ -f "$entry/SKILL.md" ]; then |
| 105 | + # Skill — symlink the directory |
| 106 | + local dst="$HOME/.claude/skills/$name" |
| 107 | + _link_symlink "$entry" "$dst" "skill" |
| 108 | + installed_count=$((installed_count + 1)) |
| 109 | + elif [ -f "$entry/$name.md" ]; then |
| 110 | + # Command — symlink the flat .md file |
| 111 | + local dst="$HOME/.claude/commands/$name.md" |
| 112 | + _link_symlink "$entry/$name.md" "$dst" "command" |
| 113 | + installed_count=$((installed_count + 1)) |
| 114 | + else |
| 115 | + echo " SKIP $name (no SKILL.md or $name.md found)" |
| 116 | + fi |
| 117 | + done |
| 118 | + |
| 119 | + echo " ($installed_count installed from $label)" |
| 120 | + echo "" |
| 121 | +} |
| 122 | + |
| 123 | +_link_symlink() { |
| 124 | + local src="$1" |
| 125 | + local dst="$2" |
| 126 | + local kind="$3" |
| 127 | + local name |
| 128 | + name=$(basename "$dst") |
| 129 | + |
| 130 | + if [ -L "$dst" ]; then |
| 131 | + rm "$dst" |
| 132 | + elif [ -e "$dst" ]; then |
| 133 | + echo " SKIP $name ($kind — non-symlink exists at $dst)" |
| 134 | + return |
| 135 | + fi |
| 136 | + |
| 137 | + ln -s "$src" "$dst" |
| 138 | + echo " LINK $name ($kind)" |
| 139 | +} |
| 140 | + |
| 141 | +# ---------- Run installs ---------- |
| 142 | + |
| 143 | +install_from_dir "$ADMIN_SKILLS_DIR" "admin_jammy/skills/ — vestigial (hosts no skills)" |
| 144 | +install_from_dir "$MIND_SKILLS_DIR" "PyAutoMind/skills/ — registry-coupled skills (Mind)" |
| 145 | +install_from_dir "$BRAIN_SKILLS_DIR" "PyAutoBrain/skills/ — development-workflow skills (Brain)" |
| 146 | +install_from_dir "$HEART_SKILLS_DIR" "PyAutoHeart/skills/ — status / readiness skills (Heart)" |
| 147 | +install_from_dir "$BUILD_SKILLS_DIR" "PyAutoBuild/skills/ — release-execution skills (Hands)" |
| 148 | +install_from_dir "$PROFILING_SKILLS_DIR" "autolens_profiling/skills/ — science-profiling skills" |
| 149 | + |
| 150 | +# ---------- Summary ---------- |
| 151 | + |
| 152 | +echo "Done. Restart Claude Code to pick up changes." |
0 commit comments