#!/usr/bin/env bash 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 # ───────────────────────────────────────────────────────────────────── VERSION="2.1.0" # ── Configuration (override with env vars) ─────────────────────────── ASSETS_BASE_URL="${ASSETS_BASE_URL:-}" AGENTS_DIR="${AGENTS_DIR:-$HOME/.copilot/agents}" SKILLS_DIR="${SKILLS_DIR:-$HOME/.copilot/skills}" INSTRUCTIONS_DIR="${INSTRUCTIONS_DIR:-./instructions}" REPO_TOKEN="${REPO_TOKEN:-}" # ── Colors ─────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' # ── 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" "$*"; } # ── 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" 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 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 fi return 1 } # 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" else curl -fsSL ${REPO_TOKEN:+-H "Authorization: Bearer $REPO_TOKEN"} "$ASSETS_BASE_URL/$path" 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 } # 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 } # ── 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: ~/.copilot/agents) SKILLS_DIR Install location for custom skills (default: ~/.copilot/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." ;; esac