#!/bin/bash # # Skill Migrator - Migrate LLM tools to OpenCode skills directory # set -euo pipefail # Configuration GLOBAL_SKILLS_DIR="${HOME}/.config/opencode/skills" LOCAL_SKILLS_DIR=".opencode/skills" TARGET_MODE="global" DRY_RUN=false # Non-interactive mode flags MIGRATE_ALL=false AUTO_CONFIRM=false CONFLICT_STRATEGY="" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Data structures declare -a DISCOVERED_SKILLS=() declare -a DISCOVERED_PATHS=() declare -a MIGRATED_SKILLS=() declare -a SKIPPED_SKILLS=() declare -a BACKED_UP_SKILLS=() declare -a ERROR_SKILLS=() # Show help show_help() { cat << EOF Skill Migrator - Migrate LLM tools to OpenCode Usage: $(basename "$0") [OPTIONS] Migrate skills from a source directory to OpenCode's skills directory. Options: -a, --all Migrate all discovered skills without prompting -c, --conflict-strategy Set conflict resolution strategy: skip, overwrite, backup -y, --yes Auto-confirm all prompts without interaction -l, --local Install to local project (.opencode/skills/) -g, --global Install to global directory (default: ~/.config/opencode/skills/) -d, --dry-run Preview changes without migrating -h, --help Show this help message Interactive Options: By default, the script will prompt you to: - Select which skills to migrate - Resolve conflicts (overwrite/skip/backup) - Confirm before proceeding Use -a, -y, and -c flags for non-interactive/automated usage. Examples: # Interactive mode (default) $(basename "$0") ~/my-skills # Non-interactive: migrate all skills $(basename "$0") ~/my-skills --all --yes # Non-interactive: migrate all, skip existing $(basename "$0") ~/my-skills --all --yes --conflict-strategy skip # Non-interactive: migrate all, overwrite existing $(basename "$0") ~/my-skills --all --yes --conflict-strategy overwrite # Dry run (preview only) $(basename "$0") ~/my-skills --all --dry-run # Install to local project $(basename "$0") ~/my-skills --local --all --yes EOF } # Print colored output print_error() { echo -e "${RED}✗${NC} $1" } print_success() { echo -e "${GREEN}✓${NC} $1" } print_warning() { echo -e "${YELLOW}⚠${NC} $1" } print_info() { echo -e "${BLUE}ℹ${NC} $1" } # Parse command line arguments parse_args() { SOURCE_DIR="" while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -a|--all) MIGRATE_ALL=true shift ;; -y|--yes) AUTO_CONFIRM=true shift ;; -c|--conflict-strategy) if [[ -n "${2:-}" && ! "$2" =~ ^- ]]; then CONFLICT_STRATEGY="$2" # Validate conflict strategy if [[ "$CONFLICT_STRATEGY" != "skip" && "$CONFLICT_STRATEGY" != "overwrite" && "$CONFLICT_STRATEGY" != "backup" ]]; then print_error "Invalid conflict strategy: $CONFLICT_STRATEGY" print_error "Valid options: skip, overwrite, backup" exit 1 fi shift 2 else print_error "--conflict-strategy requires a value (skip, overwrite, backup)" exit 1 fi ;; -l|--local) TARGET_MODE="local" shift ;; -g|--global) TARGET_MODE="global" shift ;; -d|--dry-run) DRY_RUN=true shift ;; -*) print_error "Unknown option: $1" show_help exit 1 ;; *) if [[ -z "$SOURCE_DIR" ]]; then SOURCE_DIR="$1" else print_error "Multiple source directories specified" exit 1 fi shift ;; esac done if [[ -z "$SOURCE_DIR" ]]; then print_error "No source directory specified" show_help exit 1 fi } # Get target directory get_target_dir() { if [[ "$TARGET_MODE" == "local" ]]; then if [[ ! -d "$LOCAL_SKILLS_DIR" ]]; then if [[ "$DRY_RUN" == false ]]; then mkdir -p "$LOCAL_SKILLS_DIR" fi fi echo "$LOCAL_SKILLS_DIR" else if [[ ! -d "$GLOBAL_SKILLS_DIR" ]]; then if [[ "$DRY_RUN" == false ]]; then mkdir -p "$GLOBAL_SKILLS_DIR" fi fi echo "$GLOBAL_SKILLS_DIR" fi } # Discover skills recursively discover_skills() { local source_dir="$1" print_info "Discovering skills in: $source_dir" # Find all directories containing SKILL.md local found_skills=0 while IFS= read -r -d '' skill_path; do local skill_dir skill_dir=$(dirname "$skill_path") local skill_name skill_name=$(basename "$skill_dir") # Skip the skill-migrator itself if [[ "$skill_name" == "skill-migrator" ]]; then continue fi DISCOVERED_SKILLS+=("$skill_name") DISCOVERED_PATHS+=("$skill_dir") ((found_skills++)) || true done < <(find "$source_dir" -type f -name "SKILL.md" -print0 2>/dev/null) if [[ ${#DISCOVERED_SKILLS[@]} -eq 0 ]]; then print_warning "No skills found in: $source_dir" print_info "Looking for directories containing 'SKILL.md'" exit 0 fi print_success "Found $found_skills skill(s)" } # Display discovered skills display_discovered() { echo "" echo "Discovered Skills:" echo "==================" local i=1 for skill_name in "${DISCOVERED_SKILLS[@]}"; do printf "%2d. %-30s (%s)\n" "$i" "$skill_name" "${DISCOVERED_PATHS[$((i-1))]}" ((i++)) || true done echo "" } # Prompt user for skill selection select_skills() { # If --all flag is set, select all skills automatically if [[ "$MIGRATE_ALL" == true ]]; then SELECTED_INDICES=($(seq 1 ${#DISCOVERED_SKILLS[@]})) print_info "Auto-selecting all ${#DISCOVERED_SKILLS[@]} skill(s) (--all flag set)" return fi echo "Select skills to migrate:" echo " [a] - Migrate all" echo " [1-${#DISCOVERED_SKILLS[@]}] - Select specific skill(s) by number (comma-separated)" echo " [q] - Quit" echo "" read -p "Your choice: " choice case "$choice" in a|A) SELECTED_INDICES=($(seq 1 ${#DISCOVERED_SKILLS[@]})) ;; q|Q) print_info "Migration cancelled" exit 0 ;; *) SELECTED_INDICES=() IFS=',' read -ra selections <<< "$choice" for sel in "${selections[@]}"; do sel=$(echo "$sel" | tr -d ' ') if [[ "$sel" =~ ^[0-9]+$ ]] && [[ "$sel" -ge 1 ]] && [[ "$sel" -le ${#DISCOVERED_SKILLS[@]} ]]; then SELECTED_INDICES+=("$sel") else print_warning "Invalid selection: $sel" fi done if [[ ${#SELECTED_INDICES[@]} -eq 0 ]]; then print_error "No valid skills selected" exit 1 fi ;; esac } # Resolve conflict for existing skill resolve_conflict() { local skill_name="$1" local target_dir="$2" local target_path="$target_dir/$skill_name" if [[ ! -d "$target_path" ]]; then echo "migrate" return fi # If conflict strategy is set via flag, use it if [[ -n "$CONFLICT_STRATEGY" ]]; then # Print to stderr so it doesn't get captured in command substitution print_info "Conflict for '$skill_name': using --conflict-strategy=$CONFLICT_STRATEGY" >&2 echo "$CONFLICT_STRATEGY" return fi echo "" print_warning "Skill already exists: $skill_name" echo " Location: $target_path" echo "" echo "Options:" echo " [o] - Overwrite (replace existing)" echo " [s] - Skip (keep existing)" echo " [b] - Backup (create backup, then overwrite)" echo "" while true; do read -p "Your choice [o/s/b]: " conflict_choice case "$conflict_choice" in o|O) echo "overwrite" return ;; s|S) echo "skip" return ;; b|B) echo "backup" return ;; *) print_warning "Invalid choice. Please enter o, s, or b" ;; esac done } # Backup existing skill backup_skill() { local skill_name="$1" local target_dir="$2" local target_path="$target_dir/$skill_name" local timestamp timestamp=$(date +"%Y%m%d_%H%M%S") local backup_path="${target_path}.backup.${timestamp}" if [[ "$DRY_RUN" == false ]]; then mv "$target_path" "$backup_path" fi BACKED_UP_SKILLS+=("$skill_name -> $backup_path") echo "$backup_path" } # Migrate a single skill migrate_skill() { local skill_name="$1" local source_path="$2" local target_dir="$3" local target_path="$target_dir/$skill_name" print_info "Migrating: $skill_name" # Check for conflicts local resolution resolution=$(resolve_conflict "$skill_name" "$target_dir") case "$resolution" in skip) SKIPPED_SKILLS+=("$skill_name (user skipped)") print_warning "Skipped: $skill_name" return 0 ;; backup) local backup_path backup_path=$(backup_skill "$skill_name" "$target_dir") print_info "Backed up to: $backup_path" ;; migrate|overwrite) # Proceed with migration ;; esac # Perform migration if [[ "$DRY_RUN" == false ]]; then if [[ -d "$target_path" ]]; then rm -rf "$target_path" fi cp -r "$source_path" "$target_path" fi MIGRATED_SKILLS+=("$skill_name") print_success "Migrated: $skill_name" return 0 } # Generate migration report generate_report() { echo "" echo "========================================" echo " MIGRATION REPORT" echo "========================================" echo "" if [[ ${#MIGRATED_SKILLS[@]} -gt 0 ]]; then print_success "Successfully Migrated (${#MIGRATED_SKILLS[@]}):" for skill in "${MIGRATED_SKILLS[@]}"; do echo " • $skill" done echo "" fi if [[ ${#SKIPPED_SKILLS[@]} -gt 0 ]]; then print_warning "Skipped (${#SKIPPED_SKILLS[@]}):" for skill in "${SKIPPED_SKILLS[@]}"; do echo " • $skill" done echo "" fi if [[ ${#BACKED_UP_SKILLS[@]} -gt 0 ]]; then print_info "Backed Up (${#BACKED_UP_SKILLS[@]}):" for backup in "${BACKED_UP_SKILLS[@]}"; do echo " • $backup" done echo "" fi if [[ ${#ERROR_SKILLS[@]} -gt 0 ]]; then print_error "Failed (${#ERROR_SKILLS[@]}):" for skill in "${ERROR_SKILLS[@]}"; do echo " • $skill" done echo "" fi if [[ "$DRY_RUN" == true ]]; then echo "Note: This was a DRY RUN. No changes were made." echo " Run without --dry-run to perform actual migration." fi echo "" echo "Target Directory: $TARGET_DIR" echo "========================================" } # Main function main() { # Check for help flag early for arg in "$@"; do if [[ "$arg" == "-h" ]] || [[ "$arg" == "--help" ]]; then show_help exit 0 fi done echo "========================================" echo " SKILL MIGRATOR" echo "========================================" echo "" # Parse arguments parse_args "$@" # Validate source directory if [[ ! -d "$SOURCE_DIR" ]]; then print_error "Source directory does not exist: $SOURCE_DIR" exit 1 fi # Check dry run if [[ "$DRY_RUN" == true ]]; then print_warning "DRY RUN MODE - No changes will be made" echo "" fi # Get target directory TARGET_DIR=$(get_target_dir) print_info "Source: $SOURCE_DIR" print_info "Target: $TARGET_DIR ($TARGET_MODE)" echo "" # Discover skills discover_skills "$SOURCE_DIR" # Display discovered skills display_discovered # Select skills to migrate SELECTED_INDICES=() select_skills # Confirm migration echo "" echo "Ready to migrate ${#SELECTED_INDICES[@]} skill(s)." if [[ "$DRY_RUN" == false ]]; then if [[ "$AUTO_CONFIRM" == true ]]; then print_info "Auto-confirming (--yes flag set)" else read -p "Continue? [Y/n]: " confirm if [[ "$confirm" =~ ^[Nn]$ ]]; then print_info "Migration cancelled" exit 0 fi fi fi echo "" # Migrate selected skills local failed=0 for idx in "${SELECTED_INDICES[@]}"; do local array_idx=$((idx - 1)) local skill_name="${DISCOVERED_SKILLS[$array_idx]}" local source_path="${DISCOVERED_PATHS[$array_idx]}" if ! migrate_skill "$skill_name" "$source_path" "$TARGET_DIR"; then ERROR_SKILLS+=("$skill_name") ((failed++)) || true fi done # Generate report generate_report # Exit with appropriate code if [[ $failed -gt 0 ]]; then exit 1 fi exit 0 } # Run main function main "$@"