ai-docs/assets/setup.sh

360 lines
13 KiB
Bash
Executable File

#!/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/<org>/repo/-/raw/develop/assets"
# bash <(curl -fsSL "$ASSETS_BASE_URL/setup.sh") all ios
# ─────────────────────────────────────────────────────────────────────
VERSION="2.1.0"
# ── Configuration (override with env vars) ───────────────────────────
# Default paths use ~/.agents/ — the tool-agnostic directory.
# AGENTS_DIR now defaults to ~/.agents (not ~/.agents/agents) so agent loaders (e.g., VS Code Copilot Chat) can find them.
# 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}"
SKILLS_DIR="${SKILLS_DIR:-$HOME/.agents/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/<project>/-/raw/<branch>/<path>
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/<owner>/<repo>/<branch>/<path>
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/<project>/-/raw/<branch>/assets
GitHub: https://raw.githubusercontent.com/<owner>/<repo>/<branch>/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 <platform>-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."
echo "\n${YELLOW}NOTE:${NC} Agent files are now installed directly in $AGENTS_DIR for compatibility with agent loaders."
}
# -- 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 <<EOF
${BOLD}Mobile AI Assets Installer${NC} v${VERSION}
${BOLD}USAGE${NC}
setup.sh <command> [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)
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." ;;
esac