From 4978f7effd74876bae5fde1d82b8157ccf596bb4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 22 Feb 2026 21:15:18 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- assets/setup.sh | 563 +++++++++++++++++++----------------------------- 1 file changed, 225 insertions(+), 338 deletions(-) diff --git a/assets/setup.sh b/assets/setup.sh index a7b30ef..c11ee22 100755 --- a/assets/setup.sh +++ b/assets/setup.sh @@ -1,358 +1,245 @@ -#!/usr/bin/env bash +#!/bin/bash +# AI Assets Installer — Agents & Skills for Cursor, Windsurf, Cline, etc. +# Run from your repo root (where /assets/ lives) + set -euo pipefail -# ───────────────────────────────────────────────────────────────────── -# Mobile AI Assets Installer v2.0.0 -# -# One script to install skills, agents, and instructions. -# Agents and instructions are discovered automatically from directories. -# Skills are read from simple text files (one install entry per line). -# -# Local: ./assets/setup.sh all ios -# Remote: export ASSETS_BASE_URL="https://gitlab.com//repo/-/raw/develop/assets" -# bash <(curl -fsSL "$ASSETS_BASE_URL/setup.sh") all ios -# ───────────────────────────────────────────────────────────────────── +echo "=== AI Assets Installer (Agents & Skills) ===" +echo "Popular tools supported in 2026: Cursor, Windsurf, Cline, RooCode, etc." +echo -VERSION="2.1.0" +ASSETS_DIR="${ASSETS_DIR:-./assets}" +AGENTS_DIR="$ASSETS_DIR/agents" +SKILLS_DIR="$ASSETS_DIR/skills" +GLOBAL_ASSETS_DIR="${GLOBAL_ASSETS_DIR:-$HOME/.agents}" -# ── Configuration (override with env vars) ─────────────────────────── -# Default paths use ~/.agents/ — the tool-agnostic directory. -# The npx skills CLI copies into tool-specific dirs (~/.copilot/, -# ~/.claude/, ~/.cursor/) automatically. -ASSETS_BASE_URL="${ASSETS_BASE_URL:-}" -AGENTS_DIR="${AGENTS_DIR:-$HOME/.agents/agents}" -SKILLS_DIR="${SKILLS_DIR:-$HOME/.agents/skills}" -INSTRUCTIONS_DIR="${INSTRUCTIONS_DIR:-./instructions}" -REPO_TOKEN="${REPO_TOKEN:-}" +# ================== PRODUCTS (easy to edit) ================== +# Format: "Display Name:folder_name" +PRODUCTS=( + "Cursor:.cursor" + "Windsurf:.windsurf" + "Cline:.cline" + "RooCode:.roo" + "Continue:.continue" + "VS Code / Copilot:.vscode" + "GitHub / Copilot:.github" + "Claude Code:.claude" + # Add more here, e.g. "Aider:.aider" +) -# ── Colors ─────────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -BOLD='\033[1m' -NC='\033[0m' +# ================== SELECT HELPERS ================== +select_items() { + local dir=$1 + local type=$2 + local items=() -# ── Helpers ────────────────────────────────────────────────────────── -info() { printf "${BLUE}▸${NC} %s\n" "$*"; } -ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } -warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } -fail() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; } -heading() { printf "\n${BOLD}── %s ──${NC}\n" "$*"; } + if [ ! -d "$dir" ]; then + echo "⚠️ $dir not found — skipping $type." >&2 + return + fi -# ── Mode Detection ─────────────────────────────────────────────────── -MODE="" -ASSETS_DIR="" - -detect_mode() { - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || script_dir="" - - if [[ -n "$script_dir" && -d "$script_dir/agents" ]]; then - MODE="local" - ASSETS_DIR="$script_dir" - elif [[ -n "$ASSETS_BASE_URL" ]]; then - MODE="remote" + if [ "$type" = "agents" ]; then + while IFS= read -r item; do + items+=("$item") + done < <(find "$dir" -mindepth 1 -maxdepth 1 -type f -name '*.md' -exec basename {} \; | sort) else - fail "Set ASSETS_BASE_URL to run without a clone. - export ASSETS_BASE_URL=\"https://gitlab.com/org/repo/-/raw/develop/assets\" - bash <(curl -fsSL \"\$ASSETS_BASE_URL/setup.sh\") agents" - fi -} - -# ── Remote File Discovery ─────────────────────────────────────────── -# Build the hosting platform API URL from the raw-file base URL. -# Supports GitLab and GitHub URL formats. -derive_api_url() { - local subdir="$1" - - # GitLab: https://gitlab.com//-/raw// - if [[ "$ASSETS_BASE_URL" =~ ^(https?://[^/]+)/(.+)/-/raw/([^/]+)/(.+)$ ]]; then - local host="${BASH_REMATCH[1]}" - local project="${BASH_REMATCH[2]}" - local branch="${BASH_REMATCH[3]}" - local base_path="${BASH_REMATCH[4]}" - local encoded - encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$project', safe=''))" 2>/dev/null) || return 1 - echo "${host}/api/v4/projects/${encoded}/repository/tree?path=${base_path}/${subdir}&ref=${branch}&per_page=100" - return 0 + while IFS= read -r item; do + items+=("$item") + done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort) fi - # GitHub: https://raw.githubusercontent.com//// - if [[ "$ASSETS_BASE_URL" =~ ^https://raw\.githubusercontent\.com/([^/]+/[^/]+)/([^/]+)/(.+)$ ]]; then - local owner_repo="${BASH_REMATCH[1]}" - local branch="${BASH_REMATCH[2]}" - local base_path="${BASH_REMATCH[3]}" - echo "https://api.github.com/repos/${owner_repo}/contents/${base_path}/${subdir}?ref=${branch}" - return 0 + if [ ${#items[@]} -eq 0 ]; then + echo "No $type found." >&2 + return fi - return 1 -} + echo "Available $type (${#items[@]}):" >&2 + for i in "${!items[@]}"; do + printf " %2d) %s\n" $((i+1)) "${items[i]}" >&2 + done -# Query the hosting API to list files in a remote directory. -list_remote_files() { - local subdir="$1" ext="$2" - - local api_url - api_url=$(derive_api_url "$subdir") || fail "Cannot parse ASSETS_BASE_URL into an API URL. - Supported formats: - GitLab: https://gitlab.com//-/raw//assets - GitHub: https://raw.githubusercontent.com////assets" - - local auth_header="" - [[ -n "$REPO_TOKEN" ]] && auth_header="Authorization: Bearer $REPO_TOKEN" - - local response - response=$(curl -fsSL ${auth_header:+-H "$auth_header"} "$api_url" 2>/dev/null) \ - || fail "API request failed. If the repo is private, set REPO_TOKEN. - export REPO_TOKEN=\"glpat-...\" # GitLab personal access token - export REPO_TOKEN=\"ghp_...\" # GitHub personal access token" - - # Extract filenames from JSON. Works for both GitLab and GitHub responses. - local files - files=$(echo "$response" \ - | grep -o '"name":"[^"]*"' \ - | sed 's/"name":"//;s/"//' \ - | grep "${ext}\$" || true) - - echo "$files" -} - -# ── Fetch / Download ──────────────────────────────────────────────── -fetch() { - local path="$1" - if [[ "$MODE" == "local" ]]; then - cat "$ASSETS_DIR/$path" + read -r -p "Select (all / 1,3,5): " sel + if [[ "$sel" =~ ^[Aa][Ll][Ll]$ ]] || [ -z "$sel" ]; then + printf '%s\n' "${items[@]}" else - curl -fsSL ${REPO_TOKEN:+-H "Authorization: Bearer $REPO_TOKEN"} "$ASSETS_BASE_URL/$path" + local selected=() + IFS=',' read -ra nums <<< "$sel" + for n in "${nums[@]}"; do + idx=$((n-1)) + if (( idx >= 0 && idx < ${#items[@]} )); then + selected+=("${items[idx]}") + fi + done + printf '%s\n' "${selected[@]}" fi } -download_to() { - local src="$1" dest="$2" - mkdir -p "$(dirname "$dest")" - if [[ "$MODE" == "local" ]]; then - cp "$ASSETS_DIR/$src" "$dest" - else - curl -fsSL ${REPO_TOKEN:+-H "Authorization: Bearer $REPO_TOKEN"} "$ASSETS_BASE_URL/$src" -o "$dest" - fi -} +# ================== USER CHOICES ================== +INSTALL_TYPE="${INSTALL_TYPE:-}" +INSTALL_MODE="${INSTALL_MODE:-}" +if [ -z "$INSTALL_TYPE" ]; then + echo "=== Step 1: What do you want to install? ===" + echo "1) Agents" + echo "2) Skills" + read -r -p "Choose (1/2): " install_choice + case "$install_choice" in + 1) INSTALL_TYPE="agents" ;; + 2) INSTALL_TYPE="skills" ;; + *) echo "Invalid selection. Use 1 or 2."; exit 1 ;; + esac +fi -# Download an entire remote directory (one level deep). -download_dir_to() { - local src_subdir="$1" dest_dir="$2" - mkdir -p "$dest_dir" - if [[ "$MODE" == "local" ]]; then - cp -R "$ASSETS_DIR/$src_subdir/"* "$dest_dir/" 2>/dev/null || true - else - local files - files=$(list_remote_files "$src_subdir" "") - while IFS= read -r file; do - [[ -z "$file" ]] && continue - curl -fsSL ${REPO_TOKEN:+-H "Authorization: Bearer $REPO_TOKEN"} "$ASSETS_BASE_URL/$src_subdir/$file" -o "$dest_dir/$file" - done <<< "$files" - fi -} +echo -e "\n=== Step 2: Install Mode ===" +if [ -z "$INSTALL_MODE" ]; then + echo "1) Direct copy into product folders (~/.cursor, ~/.claude, ...)" + echo "2) Global install only ($GLOBAL_ASSETS_DIR)" + echo "3) Global install + symlink into product folders" + read -r -p "Choose (1/2/3): " mode +else + mode="$INSTALL_MODE" +fi -# ── Commands ───────────────────────────────────────────────────────── - -# -- skills [platform] ──────────────────────────────────────────────── -# 1. Install registry skills from -skills.txt -# 2. Auto-discover and copy custom skills from assets/skills/ -cmd_skills() { - local platform="${1:-shared}" - local manifest="${platform}-skills.txt" - - # ── Registry skills ── - heading "Registry Skills ($platform)" - - local content - content="$(fetch "$manifest" 2>/dev/null)" || content="" - - local reg_count=0 - if [[ -n "$content" ]]; then - while IFS= read -r line; do - [[ -z "$line" || "$line" == \#* ]] && continue - info "npx skills add $line" - read -r -a args <<< "$line" - npx skills add "${args[@]}" - reg_count=$((reg_count + 1)) - done <<< "$content" - fi - - if [[ $reg_count -eq 0 ]]; then - warn "No entries in $manifest." - else - ok "$reg_count registry skill(s) installed." - fi - - # ── Custom / local skills ── - heading "Custom Skills → $SKILLS_DIR" - mkdir -p "$SKILLS_DIR" - - local skill_dirs count=0 - - if [[ "$MODE" == "local" ]]; then - if [[ -d "$ASSETS_DIR/skills" ]]; then - skill_dirs=$(find "$ASSETS_DIR/skills" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort) - else - skill_dirs="" - fi - else - # Remote: list subdirectories in skills/ via API. - # The API returns entries — filter for directories (type "tree" in GitLab, "dir" in GitHub). - local api_url - api_url=$(derive_api_url "skills") || { warn "Cannot list remote skills directories."; return 0; } - - local auth_header="" - [[ -n "$REPO_TOKEN" ]] && auth_header="Authorization: Bearer $REPO_TOKEN" - - local response - response=$(curl -fsSL ${auth_header:+-H "$auth_header"} "$api_url" 2>/dev/null) || { warn "Could not list remote skills."; return 0; } - - # GitLab uses "type":"tree" for dirs, GitHub uses "type":"dir" - skill_dirs=$(echo "$response" \ - | grep -o '"name":"[^"]*"[^}]*"type":"\(tree\|dir\)"' \ - | grep -o '"name":"[^"]*"' \ - | sed 's/"name":"//;s/"//' || true) - fi - - if [[ -z "$skill_dirs" ]]; then - warn "No custom skill folders found in assets/skills/." - else - while IFS= read -r skill; do - [[ -z "$skill" ]] && continue - info "$skill" - download_dir_to "skills/$skill" "$SKILLS_DIR/$skill" - count=$((count + 1)) - done <<< "$skill_dirs" - ok "$count custom skill(s) installed." - fi - - printf "\n" - ok "Restart your editor if skills don't appear." -} - -# -- agents ─────────────────────────────────────────────────────────── -# Discovers .agent.md files automatically (local: find, remote: API). -cmd_agents() { - heading "Agents → $AGENTS_DIR" - mkdir -p "$AGENTS_DIR" - - local files count=0 - - if [[ "$MODE" == "local" ]]; then - files=$(find "$ASSETS_DIR/agents" -maxdepth 1 -type f -name '*.agent.md' -exec basename {} \; | sort) - else - files=$(list_remote_files "agents" ".agent.md") - fi - - if [[ -z "$files" ]]; then - warn "No agent files found." - return 0 - fi - - while IFS= read -r file; do - [[ -z "$file" ]] && continue - info "$file" - download_to "agents/$file" "$AGENTS_DIR/$file" - count=$((count + 1)) - done <<< "$files" - - ok "$count agent(s) installed." -} - -# -- instructions ───────────────────────────────────────────────────── -# Discovers .instructions.md files automatically. -cmd_instructions() { - heading "Instructions → $INSTRUCTIONS_DIR" - mkdir -p "$INSTRUCTIONS_DIR" - - local files count=0 - - if [[ "$MODE" == "local" ]]; then - files=$(find "$ASSETS_DIR/instructions" -maxdepth 1 -type f -name '*.instructions.md' -exec basename {} \; | sort) - else - files=$(list_remote_files "instructions" ".instructions.md") - fi - - if [[ -z "$files" ]]; then - warn "No instruction files found." - return 0 - fi - - while IFS= read -r file; do - [[ -z "$file" ]] && continue - info "$file" - download_to "instructions/$file" "$INSTRUCTIONS_DIR/$file" - count=$((count + 1)) - done <<< "$files" - - ok "$count instruction(s) installed." -} - -# -- all [platform] ─────────────────────────────────────────────────── -cmd_all() { - local platform="${1:-shared}" - cmd_skills "$platform" - cmd_agents - cmd_instructions - printf "\n" - ok "All done." -} - -# -- help ───────────────────────────────────────────────────────────── -cmd_help() { - cat < [platform] - -${BOLD}COMMANDS${NC} - skills [platform] Install registry + custom skills (auto-discovered) - agents Install all agent prompt files (auto-discovered) - instructions Install all instruction files (auto-discovered) - all [platform] Install everything at once - help Show this message - -${BOLD}PLATFORMS${NC} (for skills) - ios iOS-specific skills - android Android-specific skills - shared Cross-platform skills (default) - -${BOLD}EXAMPLES${NC} - ${GREEN}# Cloned repo${NC} - ./assets/setup.sh skills ios - ./assets/setup.sh agents - ./assets/setup.sh all ios - - ${GREEN}# No clone — run directly from the remote repo${NC} - export ASSETS_BASE_URL="https://gitlab.com/org/repo/-/raw/develop/assets" - bash <(curl -fsSL "\$ASSETS_BASE_URL/setup.sh") skills ios - bash <(curl -fsSL "\$ASSETS_BASE_URL/setup.sh") agents - bash <(curl -fsSL "\$ASSETS_BASE_URL/setup.sh") all ios - -${BOLD}ENVIRONMENT VARIABLES${NC} - ASSETS_BASE_URL Base URL for remote downloads (required without clone) - AGENTS_DIR Install location for agents (default: ~/.agents/agents) - SKILLS_DIR Install location for custom skills (default: ~/.agents/skills) - INSTRUCTIONS_DIR Install location for instructions (default: ./instructions) - REPO_TOKEN Auth token for private repos (optional) - -EOF -} - -# ── Main ───────────────────────────────────────────────────────────── -detect_mode - -case "${1:-help}" in - skills) cmd_skills "${2:-shared}" ;; - agents) cmd_agents ;; - instructions) cmd_instructions ;; - all) cmd_all "${2:-shared}" ;; - help|--help|-h) cmd_help ;; - *) fail "Unknown command: $1. Run 'setup.sh help' for usage." ;; +GLOBAL_INSTALL=false +SYMLINK_TO_PRODUCTS=false +case "$mode" in + 1|direct) + GLOBAL_INSTALL=false + SYMLINK_TO_PRODUCTS=false + ;; + 2|global) + GLOBAL_INSTALL=true + SYMLINK_TO_PRODUCTS=false + ;; + 3|symlink|global+symlink|global_symlink|products) + GLOBAL_INSTALL=true + SYMLINK_TO_PRODUCTS=true + ;; + *) + echo "Invalid install mode. Use 1, 2, or 3." + exit 1 + ;; esac + +if [ "$INSTALL_TYPE" = "agents" ]; then + echo -e "\n=== Step 3: Agents ===" + selected_agents=($(select_items "$AGENTS_DIR" "agents")) + selected_skills=() +elif [ "$INSTALL_TYPE" = "skills" ]; then + echo -e "\n=== Step 3: Skills ===" + selected_skills=($(select_items "$SKILLS_DIR" "skills")) + selected_agents=() +else + echo "Invalid INSTALL_TYPE: $INSTALL_TYPE (expected: agents or skills)" + exit 1 +fi + +echo -e "\n=== Step 4: Target Products ===" +for i in "${!PRODUCTS[@]}"; do + name="${PRODUCTS[i]%%:*}" + printf " %2d) %s\n" $((i+1)) "$name" +done +read -r -p "Select products (all / 1,2,4): " prod_sel +selected_products=() +if [[ "$prod_sel" =~ ^[Aa][Ll][Ll]$ ]] || [ -z "$prod_sel" ]; then + selected_products=("${PRODUCTS[@]}") +else + IFS=',' read -ra pnums <<< "$prod_sel" + for n in "${pnums[@]}"; do + idx=$((n-1)) + if (( idx >= 0 && idx < ${#PRODUCTS[@]} )); then + selected_products+=("${PRODUCTS[idx]}") + fi + done +fi + +# ================== CONFIRMATION ================== +echo -e "\n=== SUMMARY ===" +echo "Component : $INSTALL_TYPE" +if [ "$INSTALL_TYPE" = "agents" ]; then + echo "Agents : ${selected_agents[*]:-none}" +else + echo "Skills : ${selected_skills[*]:-none}" +fi +echo "Products : $(printf '%s ' "${selected_products[@]%%:*}")" +if [ "$GLOBAL_INSTALL" = false ]; then + echo "Mode : Direct to product folders" +elif [ "$SYMLINK_TO_PRODUCTS" = true ]; then + echo "Mode : Global install + product symlink" +else + echo "Mode : Global install only" +fi +echo "Global Dir : $GLOBAL_ASSETS_DIR" +echo +read -r -p "Install now? (y/N) " confirm +[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Cancelled."; exit 0; } + +# ================== DO THE INSTALL ================== +echo -e "\nInstalling...\n" + +global_component_copied=false + +for entry in "${selected_products[@]}"; do + name="${entry%%:*}" + folder="${entry#*:}" + + if [ "$GLOBAL_INSTALL" = true ]; then + base_dir="$GLOBAL_ASSETS_DIR" + product_dir="$HOME/$folder" + else + base_dir="$HOME/$folder" + product_dir="$base_dir" + fi + + mkdir -p "$base_dir" + + echo "→ $name ($folder)" + + if [ "$GLOBAL_INSTALL" = false ] || [ "$global_component_copied" = false ]; then + if [ "$INSTALL_TYPE" = "agents" ]; then + for item in "${selected_agents[@]}"; do + src="$AGENTS_DIR/$item" + if [ -f "$src" ]; then + dest="$base_dir/agents/$item" + mkdir -p "$(dirname "$dest")" + cp -f "$src" "$dest" + echo " ✓ Agent: $item" + elif [ -d "$src" ]; then + dest="$base_dir/agents/$item" + rm -rf "$dest" + mkdir -p "$(dirname "$dest")" + cp -r "$src" "$dest" + echo " ✓ Agent: $item" + fi + done + fi + + if [ "$INSTALL_TYPE" = "skills" ]; then + for item in "${selected_skills[@]}"; do + src="$SKILLS_DIR/$item" + if [ -d "$src" ]; then + dest="$base_dir/skills/$item" + rm -rf "$dest" + mkdir -p "$(dirname "$dest")" + cp -r "$src" "$dest" + echo " ✓ Skill: $item" + fi + done + fi + + if [ "$GLOBAL_INSTALL" = true ]; then + global_component_copied=true + fi + fi + + # Symlink global content into selected product folder if requested + if [ "$SYMLINK_TO_PRODUCTS" = true ]; then + mkdir -p "$product_dir" + if [ -d "$base_dir/$INSTALL_TYPE" ]; then + ln -sfn "$base_dir/$INSTALL_TYPE" "$product_dir/$INSTALL_TYPE" + echo " → Symlinked $INSTALL_TYPE into $product_dir" + fi + fi +done + +echo -e "\n✅ Done! Your $INSTALL_TYPE are now available in the selected tools." +echo " Tip: git add the .cursor/, .windsurf/, etc. folders if you want to version them."