528 lines
14 KiB
Bash
Executable File
528 lines
14 KiB
Bash
Executable File
#!/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] <source-directory>
|
||
|
||
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 "$@"
|