diff --git a/lib/skills.sh b/lib/skills.sh index 6e97f09..d1ce254 100644 --- a/lib/skills.sh +++ b/lib/skills.sh @@ -109,58 +109,135 @@ install_skills_to_persistent_source() { fi } +# Resolve the skills dir for a given runtime without mutating the currently +# sourced runtime functions permanently. We source the runtime file in a +# subshell, call its runtime_skills_dir(), and echo the result. +_resolve_skills_dir_for_runtime() { + local rt="$1" + local rt_file="$SCRIPT_DIR/runtimes/${rt}.sh" + [ -f "$rt_file" ] || { echo ""; return 1; } + ( + # shellcheck disable=SC1090 + source "$rt_file" + runtime_skills_dir + ) +} + install_skills() { + # Primary skills dir — set from the currently sourced runtime (drives the + # summary output and the kimaki mirror source). Multi-runtime installs + # populate every detected runtime's skills dir below, but the primary + # stays the canonical one the rest of the script refers to. SKILLS_DIR="$(runtime_skills_dir)" - if [ "$INSTALL_SKILLS" = true ]; then - log "Phase 8.5: Installing agent skills..." + if [ "$INSTALL_SKILLS" != true ]; then + log "Phase 8.5: Skipping agent skills (--no-skills)" + return + fi + + log "Phase 8.5: Installing agent skills..." + + # Build the unique list of skills dirs to populate. claude-code and + # studio-code both resolve to $SITE_PATH/.claude/skills, so de-dupe. + local -a runtimes=("${DETECTED_RUNTIMES[@]:-$RUNTIME}") + local -a skills_dirs=() + local seen_dir rt dir + for rt in "${runtimes[@]}"; do + dir="$(_resolve_skills_dir_for_runtime "$rt")" + [ -n "$dir" ] || continue + local already=false + for seen_dir in "${skills_dirs[@]}"; do + [ "$seen_dir" = "$dir" ] && { already=true; break; } + done + [ "$already" = true ] || skills_dirs+=("$dir") + done + + # Always guarantee the primary is in the list (for belt-and-braces when + # RUNTIME was set explicitly but somehow isn't in DETECTED_RUNTIMES). + local already=false + for seen_dir in "${skills_dirs[@]}"; do + [ "$seen_dir" = "$SKILLS_DIR" ] && { already=true; break; } + done + [ "$already" = true ] || skills_dirs+=("$SKILLS_DIR") + + if [ ${#skills_dirs[@]} -gt 1 ]; then + log " Detected ${#runtimes[@]} runtime(s): ${runtimes[*]}" + log " Populating ${#skills_dirs[@]} unique skills dir(s)" + fi + + # Install into every detected runtime's skills dir. + local target_dir + for target_dir in "${skills_dirs[@]}"; do + if [ ${#skills_dirs[@]} -gt 1 ]; then + log "→ Installing skills into $target_dir" + fi + SKILLS_DIR="$target_dir" run_cmd mkdir -p "$SKILLS_DIR" install_skills_from_local_repo - install_skills_from_repo "https://github.com/WordPress/agent-skills.git" "WordPress agent skills" - install_skills_from_repo "https://github.com/Extra-Chill/data-machine-skills.git" "Data Machine skills" + done - # Copy skills to Kimaki's directory if Kimaki is the chat bridge. - # Kimaki overrides OpenCode's skill discovery to only look in its - # own bundled skills dir, so the runtime skills dir alone isn't enough. - if [ "$CHAT_BRIDGE" = "kimaki" ]; then - if [ "$DRY_RUN" = true ]; then - KIMAKI_SKILLS_DIR="/usr/lib/node_modules/kimaki/skills" - echo -e "${BLUE}[dry-run]${NC} Would copy skills to $KIMAKI_SKILLS_DIR/ (if Kimaki installed)" - elif command -v kimaki &> /dev/null; then - KIMAKI_SKILLS_DIR="$(npm root -g 2>/dev/null)/kimaki/skills" - if [ -d "$KIMAKI_SKILLS_DIR" ]; then - for skill_dir in "$SKILLS_DIR"/*/; do - skill_name=$(basename "$skill_dir") - if [ -f "$skill_dir/SKILL.md" ]; then - cp -r "$skill_dir" "$KIMAKI_SKILLS_DIR/$skill_name" - fi - done - log "Skills also copied to Kimaki: $KIMAKI_SKILLS_DIR/" - fi - fi + # Reset SKILLS_DIR back to the primary for downstream consumers + # (kimaki mirror source, print_skills_summary, summary.sh). + SKILLS_DIR="$(runtime_skills_dir)" - # Mirror skills into the persistent kimaki-config/skills/ dir so - # post-upgrade.sh can restore them on every kimaki restart after - # `npm update -g kimaki` wipes $(npm root -g)/kimaki/skills/. - # Path mirrors the plugin-persistence pattern: - # Local: $KIMAKI_DATA_DIR/kimaki-config/skills/ (defaults to ~/.kimaki/kimaki-config/skills/) - # VPS: /opt/kimaki-config/skills/ - install_skills_to_persistent_source + # Copy skills to Kimaki's directory if Kimaki is the chat bridge. + # Kimaki overrides OpenCode's skill discovery to only look in its + # own bundled skills dir, so the runtime skills dir alone isn't enough. + if [ "$CHAT_BRIDGE" = "kimaki" ]; then + if [ "$DRY_RUN" = true ]; then + KIMAKI_SKILLS_DIR="/usr/lib/node_modules/kimaki/skills" + echo -e "${BLUE}[dry-run]${NC} Would copy skills to $KIMAKI_SKILLS_DIR/ (if Kimaki installed)" + elif command -v kimaki &> /dev/null; then + KIMAKI_SKILLS_DIR="$(npm root -g 2>/dev/null)/kimaki/skills" + if [ -d "$KIMAKI_SKILLS_DIR" ]; then + for skill_dir in "$SKILLS_DIR"/*/; do + skill_name=$(basename "$skill_dir") + if [ -f "$skill_dir/SKILL.md" ]; then + cp -r "$skill_dir" "$KIMAKI_SKILLS_DIR/$skill_name" + fi + done + log "Skills also copied to Kimaki: $KIMAKI_SKILLS_DIR/" + fi fi - else - log "Phase 8.5: Skipping agent skills (--no-skills)" + + # Mirror skills into the persistent kimaki-config/skills/ dir so + # post-upgrade.sh can restore them on every kimaki restart after + # `npm update -g kimaki` wipes $(npm root -g)/kimaki/skills/. + # Path mirrors the plugin-persistence pattern: + # Local: $KIMAKI_DATA_DIR/kimaki-config/skills/ (defaults to ~/.kimaki/kimaki-config/skills/) + # VPS: /opt/kimaki-config/skills/ + install_skills_to_persistent_source fi } print_skills_summary() { echo "" - log "Skills installed to $SKILLS_DIR/" - if [ "$DRY_RUN" = false ]; then - ls -1 "$SKILLS_DIR" 2>/dev/null | while read -r skill; do - log " - $skill" + + # Collect unique skills dirs across detected runtimes, same logic as + # install_skills. Falls back to SKILLS_DIR if DETECTED_RUNTIMES is empty. + local -a runtimes=("${DETECTED_RUNTIMES[@]:-$RUNTIME}") + local -a skills_dirs=() + local seen_dir rt dir + for rt in "${runtimes[@]}"; do + dir="$(_resolve_skills_dir_for_runtime "$rt")" + [ -n "$dir" ] || continue + local already=false + for seen_dir in "${skills_dirs[@]}"; do + [ "$seen_dir" = "$dir" ] && { already=true; break; } done - fi + [ "$already" = true ] || skills_dirs+=("$dir") + done + [ ${#skills_dirs[@]} -gt 0 ] || skills_dirs=("$SKILLS_DIR") + + for dir in "${skills_dirs[@]}"; do + log "Skills installed to $dir/" + if [ "$DRY_RUN" = false ]; then + ls -1 "$dir" 2>/dev/null | while read -r skill; do + log " - $skill" + done + fi + done } diff --git a/setup.sh b/setup.sh index ab2876b..c2be80b 100755 --- a/setup.sh +++ b/setup.sh @@ -53,6 +53,7 @@ INSTALL_SKILLS=true SKILLS_ONLY=false RUNTIME_ONLY=false RUNTIME="" +DETECTED_RUNTIMES=() IS_STUDIO=false while [[ $# -gt 0 ]]; do @@ -223,18 +224,35 @@ fi # Runtime resolution # ============================================================================ -# Auto-detect runtime if not specified -if [ -z "$RUNTIME" ]; then +# Auto-detect runtime(s). +# +# RUNTIME is the "primary" runtime — the one that drives runtime_install, +# runtime_generate_config, runtime_install_hooks, and the chat-bridge default. +# First-match cascade: studio-code > claude-code > opencode. +# +# DETECTED_RUNTIMES is the list of ALL runtimes whose binary is present. On a +# machine with both claude and opencode installed, skills get installed into +# every detected runtime's skills dir (see install_skills in lib/skills.sh). +# Explicit --runtime narrows both lists to that single runtime. +if [ -n "$RUNTIME" ]; then + # User passed --runtime explicitly — respect it, single-runtime mode. + DETECTED_RUNTIMES=("$RUNTIME") +else if command -v studio &>/dev/null && [ "${IS_STUDIO:-false}" = true ]; then - RUNTIME="studio-code" - elif command -v claude &>/dev/null; then - RUNTIME="claude-code" - elif command -v opencode &>/dev/null; then - RUNTIME="opencode" - else - # Default to opencode (will be installed) - RUNTIME="opencode" + DETECTED_RUNTIMES+=("studio-code") + fi + if command -v claude &>/dev/null; then + DETECTED_RUNTIMES+=("claude-code") + fi + if command -v opencode &>/dev/null; then + DETECTED_RUNTIMES+=("opencode") + fi + if [ ${#DETECTED_RUNTIMES[@]} -eq 0 ]; then + # Nothing installed yet — default to opencode (will be installed). + DETECTED_RUNTIMES=("opencode") fi + # Primary = first match in the cascade above. + RUNTIME="${DETECTED_RUNTIMES[0]}" fi # Source the selected runtime diff --git a/upgrade.sh b/upgrade.sh index af6a563..e08019c 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -89,6 +89,7 @@ MULTISITE=false MULTISITE_TYPE="subdirectory" MODE="existing" RUNTIME="" +DETECTED_RUNTIMES=() IS_STUDIO=false CHAT_BRIDGE="" @@ -180,18 +181,26 @@ if [ -z "$EXISTING_WP" ]; then fi fi -# Auto-detect runtime (same logic as setup.sh) -if [ -z "$RUNTIME" ]; then +# Auto-detect runtime(s). Same model as setup.sh: DETECTED_RUNTIMES is the +# full list (drives multi-runtime skills install); RUNTIME is the primary +# (first-match cascade). Explicit --runtime narrows to a single runtime. +if [ -n "$RUNTIME" ]; then + DETECTED_RUNTIMES=("$RUNTIME") +else if command -v studio &>/dev/null && [ -f "$EXISTING_WP/STUDIO.md" ]; then - RUNTIME="studio-code" - elif command -v opencode &>/dev/null; then - RUNTIME="opencode" - elif command -v claude &>/dev/null; then - RUNTIME="claude-code" - else + DETECTED_RUNTIMES+=("studio-code") + fi + if command -v claude &>/dev/null; then + DETECTED_RUNTIMES+=("claude-code") + fi + if command -v opencode &>/dev/null; then + DETECTED_RUNTIMES+=("opencode") + fi + if [ ${#DETECTED_RUNTIMES[@]} -eq 0 ]; then warn "No runtime binary found — defaulting to opencode" - RUNTIME="opencode" + DETECTED_RUNTIMES=("opencode") fi + RUNTIME="${DETECTED_RUNTIMES[0]}" fi RUNTIME_FILE="$SCRIPT_DIR/runtimes/${RUNTIME}.sh"