From 3fc735eeae013e349abb5fe180c61b5e699e444a Mon Sep 17 00:00:00 2001 From: Francy Lisboa Charuto Date: Wed, 4 Mar 2026 10:08:20 -0300 Subject: [PATCH] feat: Add brain-dead multi-tool install scripts (bootstrap, symlink installer, universal skill installer) - scripts/bootstrap.sh: curl-pipe-sh one-liner that clones to ~/.agents/skills/ and symlinks to all detected global platforms - install.sh: symlink self-installer for cloned repos with --dry-run and --uninstall - scripts/install-skill.sh: universal installer for any skill (git URL or local path) to all detected platforms with format adapters - SKILL.md: add silent git-based update check instruction - README.md: document all new install options and update project structure - .gitignore: add *.mdc for generated adapter files Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + README.md | 47 +++- SKILL.md | 14 + install.sh | 214 ++++++++++++++++ scripts/bootstrap.sh | 181 +++++++++++++ scripts/install-skill.sh | 540 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 994 insertions(+), 5 deletions(-) create mode 100755 install.sh create mode 100755 scripts/bootstrap.sh create mode 100755 scripts/install-skill.sh diff --git a/.gitignore b/.gitignore index a0e2a01..d9c2f15 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ agentdb.db test_*.py !test_agentdb_learning.py tests/ + +# Generated format adapter files +*.mdc diff --git a/README.md b/README.md index d40bec8..ba8e027 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,17 @@ Every AI coding tool — Claude Code, GitHub Copilot, Cursor, Windsurf, Codex, G ## Quick Start -### 1. Install (one command) +### 1. Install -Pick the line that matches your tool: +**Option A — One-liner (installs to all detected tools):** + +```bash +curl -fsSL https://raw.githubusercontent.com/FrancyJGLisboa/agent-skill-creator/main/scripts/bootstrap.sh | sh +``` + +This clones to `~/.agents/skills/agent-skill-creator` and symlinks to every detected global platform (Claude Code, Gemini CLI, Goose, OpenCode, Copilot). Run `git pull` once to update everywhere. + +**Option B — Git clone (pick your tool):** ```bash # Claude Code / VS Code Copilot (global — works in all projects) @@ -39,6 +47,15 @@ git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git .cursor/rule git clone https://github.com/FrancyJGLisboa/agent-skill-creator.git ~/.agents/skills/agent-skill-creator ``` +**Option C — Already cloned? Symlink to all tools:** + +```bash +cd agent-skill-creator +./install.sh # Symlink to all detected platforms +./install.sh --dry-run # Preview without changes +./install.sh --uninstall # Remove all symlinks +``` + One install at `~/.claude/skills/` works for both Claude Code and VS Code Copilot. One install at `~/.agents/skills/` works for Codex CLI, Gemini CLI, Kiro, Antigravity, and other tools that read the universal path. All 14 platforms: [see full list below](#all-platforms). @@ -287,11 +304,11 @@ python3 scripts/export_utils.py ./agent-skill-creator/ --variant desktop ### Update ```bash -cd ~/.claude/skills/agent-skill-creator && git pull -# or cd ~/.agents/skills/agent-skill-creator && git pull ``` +If you used `bootstrap.sh` or `./install.sh`, all symlinks update automatically — just `git pull` once from the canonical location. The skill also performs a silent git-based version check when loaded and will mention if a newer version is available. + --- ## Quality Gates @@ -403,6 +420,23 @@ python3 scripts/staleness_check.py ./skill/ --check-drift # + schema drif python3 scripts/staleness_check.py ./skill/ --json # Machine-readable output ``` +### Install Any Skill (Universal Installer) + +```bash +# From git URL — clones and symlinks to all detected platforms +./scripts/install-skill.sh https://github.com/someone/sales-report-skill.git + +# From local path — copies and symlinks to all detected platforms +./scripts/install-skill.sh ./sales-report-skill + +# To a specific platform only +./scripts/install-skill.sh ./sales-report-skill --platform cursor --project + +# Preview / remove +./scripts/install-skill.sh ./sales-report-skill --dry-run +./scripts/install-skill.sh ./sales-report-skill --uninstall +``` + ### Export ```bash @@ -434,13 +468,16 @@ All commands use exit code `0` for success, `1` for errors. All support `--json` agent-skill-creator/ SKILL.md # The skill definition (what the agent reads) README.md # This file + install.sh # Symlink self-installer (for cloned repos) scripts/ + bootstrap.sh # Curl one-liner bootstrap (installs everywhere) + install-skill.sh # Universal skill installer (any skill, any tool) + install-template.sh # Template for generated installers (14 platforms) validate.py # Spec compliance checker security_scan.py # Security scanner staleness_check.py # Staleness detection (review, deps, drift) export_utils.py # Cross-platform export skill_registry.py # Team skill registry - install-template.sh # Template for generated installers (14 platforms) references/ # Detailed docs (loaded by the agent on demand) pipeline-phases.md # Full creation pipeline architecture-guide.md # Skill structure decisions diff --git a/SKILL.md b/SKILL.md index bffea8a..50be244 100644 --- a/SKILL.md +++ b/SKILL.md @@ -414,6 +414,20 @@ The registry is a git repo. Version history, access control, and review workflow See `references/pipeline-phases.md` for detailed Phase 5 instructions. +### Update Check + +When loading this skill, silently check if the local copy is behind the remote. Run: + +```bash +cd && git rev-parse HEAD 2>/dev/null +cd && git ls-remote origin HEAD 2>/dev/null | cut -f1 +``` + +If both commands succeed and the hashes differ, mention to the user: +"A newer version of agent-skill-creator is available. Run `git pull` in to update." + +Do not block or interrupt for this. If either command fails (no git, no network, not a git repo), skip silently. + ### Generated SKILL.md Format Every generated skill's SKILL.md must follow this structure: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..03e9784 --- /dev/null +++ b/install.sh @@ -0,0 +1,214 @@ +#!/bin/sh +# install.sh — Symlink agent-skill-creator to all detected global platforms +# +# For users who already cloned the repo. Creates symlinks so `git pull` in the +# cloned directory updates all tools automatically. +# +# Usage: +# ./install.sh # Symlink to all detected platforms +# ./install.sh --dry-run # Preview without making changes +# ./install.sh --uninstall # Remove all symlinks pointing to this repo +# +# POSIX-compatible (works in bash, dash, zsh, ash). + +set -eu + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +SKILL_NAME="agent-skill-creator" +REPO_DIR="$(cd "$(dirname "$0")" && pwd)" + +# --------------------------------------------------------------------------- +# Colors (disabled when stdout is not a terminal) +# --------------------------------------------------------------------------- +if [ -t 1 ]; then + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + RED='\033[0;31m' + BOLD='\033[1m' + NC='\033[0m' +else + GREEN='' YELLOW='' BLUE='' RED='' BOLD='' NC='' +fi + +info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +success() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } +error() { printf "${RED}[ERROR]${NC} %s\n" "$1" >&2; } + +# --------------------------------------------------------------------------- +# Options +# --------------------------------------------------------------------------- +DRY_RUN=false +UNINSTALL=false + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=true ;; + --uninstall) UNINSTALL=true ;; + -h|--help) + printf "Usage: %s [--dry-run] [--uninstall]\n\n" "$0" + printf "Options:\n" + printf " --dry-run Preview without making changes\n" + printf " --uninstall Remove all symlinks pointing to this repo\n" + printf " -h, --help Show this help message\n" + exit 0 + ;; + *) + error "Unknown option: $1" + exit 1 + ;; + esac + shift +done + +# --------------------------------------------------------------------------- +# All global platform paths (user-level only) +# --------------------------------------------------------------------------- +all_platform_entries() { + # Format: || + cat <<'PLATFORMS' +$HOME/.claude|$HOME/.claude/skills/$SKILL_NAME|Claude Code +$HOME/.gemini|$HOME/.gemini/skills/$SKILL_NAME|Gemini CLI +$HOME/.config/goose|$HOME/.config/goose/skills/$SKILL_NAME|Goose +$HOME/.config/opencode|$HOME/.config/opencode/skills/$SKILL_NAME|OpenCode +$HOME/.copilot|$HOME/.copilot/skills/$SKILL_NAME|GitHub Copilot +PLATFORMS +} + +# Expand variables in platform entries +eval_path() { + eval echo "$1" +} + +# --------------------------------------------------------------------------- +# Create a symlink (with fallback to copy) +# --------------------------------------------------------------------------- +create_symlink() { + target="$1" + link_path="$2" + + if [ "$target" = "$link_path" ]; then + return 0 + fi + + mkdir -p "$(dirname "$link_path")" + + if [ -e "$link_path" ] || [ -L "$link_path" ]; then + rm -rf "$link_path" + fi + + if ln -s "$target" "$link_path" 2>/dev/null; then + return 0 + else + warn "Symlink failed for $link_path — falling back to copy" + cp -R "$target" "$link_path" + fi +} + +# --------------------------------------------------------------------------- +# Uninstall: remove all symlinks pointing to REPO_DIR +# --------------------------------------------------------------------------- +do_uninstall() { + printf "\n${BOLD}Uninstalling agent-skill-creator symlinks${NC}\n\n" + + canonical="$HOME/.agents/skills/$SKILL_NAME" + removed=0 + + # Check canonical location + if [ -L "$canonical" ]; then + link_target="$(readlink "$canonical" 2>/dev/null || true)" + if [ "$link_target" = "$REPO_DIR" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would remove: $canonical" + else + rm "$canonical" + success "Removed: $canonical" + fi + removed=$((removed + 1)) + fi + fi + + # Check each platform path + all_platform_entries | while IFS='|' read -r detect_dir install_path display_name; do + dest="$(eval_path "$install_path")" + if [ -L "$dest" ]; then + link_target="$(readlink "$dest" 2>/dev/null || true)" + if [ "$link_target" = "$REPO_DIR" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would remove: $dest" + else + rm "$dest" + success "Removed: $dest ($display_name)" + fi + fi + fi + done + + if [ "$DRY_RUN" = true ]; then + printf "\n${YELLOW}Dry run — no changes made.${NC}\n" + else + printf "\nDone. Symlinks removed.\n" + fi +} + +# --------------------------------------------------------------------------- +# Install: create symlinks to all detected platforms +# --------------------------------------------------------------------------- +do_install() { + printf "\n${BOLD}Agent Skill Creator — Symlink Installer${NC}\n\n" + info "Source: $REPO_DIR" + + count=0 + installed="" + + # Always install to canonical location + canonical="$HOME/.agents/skills/$SKILL_NAME" + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would symlink: $canonical → $REPO_DIR" + else + create_symlink "$REPO_DIR" "$canonical" + success "Canonical: $canonical" + fi + count=$((count + 1)) + + # Install to each detected global platform + all_platform_entries | while IFS='|' read -r detect_dir install_path display_name; do + dir="$(eval_path "$detect_dir")" + dest="$(eval_path "$install_path")" + + if [ -d "$dir" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would symlink: $dest → $REPO_DIR ($display_name)" + else + create_symlink "$REPO_DIR" "$dest" + success "Symlinked for $display_name → $dest" + fi + fi + done + + # Summary + printf "\n${BOLD}Done!${NC}\n\n" + + if [ "$DRY_RUN" = true ]; then + printf "${YELLOW}Dry run — no changes made.${NC}\n\n" + else + printf " Symlinks point to: ${BOLD}%s${NC}\n" "$REPO_DIR" + printf " Run ${BOLD}git pull${NC} from that directory to update all tools.\n\n" + fi + + printf "${BOLD}How to use:${NC}\n" + printf " Open your AI agent and type:\n" + printf " /agent-skill-creator \n\n" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +if [ "$UNINSTALL" = true ]; then + do_uninstall +else + do_install +fi diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..90f6853 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,181 @@ +#!/bin/sh +# bootstrap.sh — One-liner bootstrap for agent-skill-creator +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/FrancyJGLisboa/agent-skill-creator/main/scripts/bootstrap.sh | sh +# +# Clones agent-skill-creator to ~/.agents/skills/ and symlinks to all detected +# global platforms. POSIX-compatible (works in bash, dash, zsh, ash). + +set -eu + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +REPO_URL="https://github.com/FrancyJGLisboa/agent-skill-creator.git" +SKILL_NAME="agent-skill-creator" +CANONICAL_DIR="$HOME/.agents/skills/$SKILL_NAME" + +# --------------------------------------------------------------------------- +# Colors (disabled when stdout is not a terminal) +# --------------------------------------------------------------------------- +if [ -t 1 ]; then + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + BOLD='\033[1m' + NC='\033[0m' +else + GREEN='' YELLOW='' BLUE='' BOLD='' NC='' +fi + +info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +success() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } + +# --------------------------------------------------------------------------- +# Detect globally-installed platforms (user-level only, skip project-level) +# --------------------------------------------------------------------------- +detect_global_platforms() { + platforms="" + # Claude Code + if [ -d "$HOME/.claude" ]; then + platforms="$platforms claude-code" + fi + # Gemini CLI + if [ -d "$HOME/.gemini" ]; then + platforms="$platforms gemini" + fi + # Goose + if [ -d "$HOME/.config/goose" ]; then + platforms="$platforms goose" + fi + # OpenCode + if [ -d "$HOME/.config/opencode" ]; then + platforms="$platforms opencode" + fi + # GitHub Copilot + if [ -d "$HOME/.copilot" ]; then + platforms="$platforms copilot" + fi + echo "$platforms" +} + +# --------------------------------------------------------------------------- +# Resolve user-level install path for a platform +# --------------------------------------------------------------------------- +platform_path() { + case "$1" in + claude-code) echo "$HOME/.claude/skills/$SKILL_NAME" ;; + gemini) echo "$HOME/.gemini/skills/$SKILL_NAME" ;; + goose) echo "$HOME/.config/goose/skills/$SKILL_NAME" ;; + opencode) echo "$HOME/.config/opencode/skills/$SKILL_NAME" ;; + copilot) echo "$HOME/.copilot/skills/$SKILL_NAME" ;; + esac +} + +# --------------------------------------------------------------------------- +# Friendly display name for a platform +# --------------------------------------------------------------------------- +platform_display() { + case "$1" in + claude-code) echo "Claude Code" ;; + gemini) echo "Gemini CLI" ;; + goose) echo "Goose" ;; + opencode) echo "OpenCode" ;; + copilot) echo "GitHub Copilot" ;; + esac +} + +# --------------------------------------------------------------------------- +# Create a symlink (with fallback to copy) +# --------------------------------------------------------------------------- +create_symlink() { + target="$1" # what the link points to + link_path="$2" # where the link lives + + # Skip if target and link are the same path + if [ "$target" = "$link_path" ]; then + return 0 + fi + + mkdir -p "$(dirname "$link_path")" + + # Remove existing (file, symlink, or directory) + if [ -e "$link_path" ] || [ -L "$link_path" ]; then + rm -rf "$link_path" + fi + + if ln -s "$target" "$link_path" 2>/dev/null; then + return 0 + else + warn "Symlink failed for $link_path — falling back to copy" + cp -R "$target" "$link_path" + fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + printf "\n${BOLD}Agent Skill Creator — Bootstrap Installer${NC}\n\n" + + # Check for git + if ! command -v git >/dev/null 2>&1; then + warn "git is not installed. Please install git and try again." + exit 1 + fi + + # Clone or update the canonical location + if [ -d "$CANONICAL_DIR/.git" ]; then + info "Updating existing install at $CANONICAL_DIR" + cd "$CANONICAL_DIR" && git pull --ff-only 2>/dev/null || true + else + info "Cloning $SKILL_NAME to $CANONICAL_DIR" + mkdir -p "$(dirname "$CANONICAL_DIR")" + rm -rf "$CANONICAL_DIR" + git clone "$REPO_URL" "$CANONICAL_DIR" + fi + + success "Installed at $CANONICAL_DIR" + + # Detect global platforms and create symlinks + platforms="$(detect_global_platforms)" + installed="" + count=0 + + for platform in $platforms; do + dest="$(platform_path "$platform")" + create_symlink "$CANONICAL_DIR" "$dest" + name="$(platform_display "$platform")" + success "Symlinked for $name → $dest" + installed="$installed $name," + count=$((count + 1)) + done + + # --------------------------------------------------------------------------- + # Summary + # --------------------------------------------------------------------------- + printf "\n${BOLD}Done!${NC}\n\n" + printf " Canonical location: ${BOLD}%s${NC}\n" "$CANONICAL_DIR" + + if [ $count -gt 0 ]; then + # Trim trailing comma + installed="$(echo "$installed" | sed 's/,$//')" + printf " Symlinked to %d platform(s):%s\n" "$count" "$installed" + fi + + printf "\n${BOLD}How to use:${NC}\n" + printf " Open your AI agent and type:\n" + printf " /agent-skill-creator \n\n" + printf " To update later:\n" + printf " cd %s && git pull\n\n" "$CANONICAL_DIR" + + if [ $count -eq 0 ]; then + warn "No global platforms detected. The skill is installed at the universal path." + printf " Tools like Codex CLI, Gemini CLI, Kiro, and Antigravity\n" + printf " read from ~/.agents/skills/ automatically.\n\n" + fi +} + +main diff --git a/scripts/install-skill.sh b/scripts/install-skill.sh new file mode 100755 index 0000000..56173b3 --- /dev/null +++ b/scripts/install-skill.sh @@ -0,0 +1,540 @@ +#!/bin/sh +# install-skill.sh — Install any skill (git URL or local path) to all detected platforms +# +# Usage: +# ./scripts/install-skill.sh https://github.com/someone/sales-report-skill.git +# ./scripts/install-skill.sh ./sales-report-skill +# ./scripts/install-skill.sh ./sales-report-skill --platform cursor --project +# ./scripts/install-skill.sh ./sales-report-skill --dry-run +# ./scripts/install-skill.sh ./sales-report-skill --uninstall +# +# POSIX-compatible (works in bash, dash, zsh, ash). + +set -eu + +# --------------------------------------------------------------------------- +# Colors (disabled when stdout is not a terminal) +# --------------------------------------------------------------------------- +if [ -t 1 ]; then + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + RED='\033[0;31m' + BOLD='\033[1m' + NC='\033[0m' +else + GREEN='' YELLOW='' BLUE='' RED='' BOLD='' NC='' +fi + +info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +success() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } +error() { printf "${RED}[ERROR]${NC} %s\n" "$1" >&2; } + +# --------------------------------------------------------------------------- +# Options +# --------------------------------------------------------------------------- +SOURCE="" +PLATFORM="" +PROJECT_LEVEL=false +DRY_RUN=false +UNINSTALL=false + +usage() { + cat <<'USAGE' +Usage: install-skill.sh [options] + +Arguments: + Git URL (https://... or *.git) or local directory path + +Options: + --platform Install to a specific platform only + --project Use project-level paths (for Cursor, Windsurf, etc.) + --all Install to all detected platforms (default) + --dry-run Preview without making changes + --uninstall Remove the skill from all platforms + -h, --help Show this help message + +Examples: + install-skill.sh https://github.com/someone/sales-report-skill.git + install-skill.sh ./sales-report-skill + install-skill.sh ./sales-report-skill --platform cursor --project +USAGE +} + +while [ $# -gt 0 ]; do + case "$1" in + --platform) + shift + PLATFORM="${1:-}" + if [ -z "$PLATFORM" ]; then + error "--platform requires a value" + exit 1 + fi + ;; + --project) PROJECT_LEVEL=true ;; + --all) PLATFORM="" ;; + --dry-run) DRY_RUN=true ;; + --uninstall) UNINSTALL=true ;; + -h|--help) usage; exit 0 ;; + -*) error "Unknown option: $1"; exit 1 ;; + *) + if [ -z "$SOURCE" ]; then + SOURCE="$1" + else + error "Unexpected argument: $1" + exit 1 + fi + ;; + esac + shift +done + +if [ -z "$SOURCE" ]; then + error "Missing required argument: " + usage + exit 1 +fi + +# --------------------------------------------------------------------------- +# Resolve source: git clone or validate local path +# --------------------------------------------------------------------------- +is_git_url() { + case "$1" in + *://*) return 0 ;; + *.git) return 0 ;; + esac + return 1 +} + +resolve_source() { + if is_git_url "$SOURCE"; then + # Extract skill name from URL + skill_basename="$(basename "$SOURCE" .git)" + canonical_dir="$HOME/.agents/skills/$skill_basename" + + if [ -d "$canonical_dir/.git" ]; then + info "Updating existing install at $canonical_dir" + if [ "$DRY_RUN" = false ]; then + cd "$canonical_dir" && git pull --ff-only 2>/dev/null || true + cd - >/dev/null + fi + else + info "Cloning $SOURCE" + if [ "$DRY_RUN" = false ]; then + mkdir -p "$(dirname "$canonical_dir")" + rm -rf "$canonical_dir" + git clone "$SOURCE" "$canonical_dir" + fi + fi + SOURCE_DIR="$canonical_dir" + else + # Local path + if [ ! -d "$SOURCE" ]; then + error "Source directory not found: $SOURCE" + exit 1 + fi + SOURCE_DIR="$(cd "$SOURCE" && pwd)" + fi +} + +# --------------------------------------------------------------------------- +# Extract skill name from directory or SKILL.md frontmatter +# --------------------------------------------------------------------------- +extract_skill_name() { + skill_name="" + skill_md="$SOURCE_DIR/SKILL.md" + + # Try to extract from SKILL.md frontmatter + if [ -f "$skill_md" ]; then + in_fm=false + lnum=0 + while IFS= read -r line; do + lnum=$((lnum + 1)) + if [ "$lnum" -eq 1 ] && [ "$line" = "---" ]; then + in_fm=true + continue + fi + if $in_fm && [ "$line" = "---" ]; then break; fi + if $in_fm; then + case "$line" in + name:*) + skill_name="$(echo "$line" | sed 's/^name:[[:space:]]*//' | sed 's/^["'"'"']//' | sed 's/["'"'"']$//')" + ;; + esac + fi + done < "$skill_md" + fi + + # Fallback to directory basename + if [ -z "$skill_name" ]; then + skill_name="$(basename "$SOURCE_DIR")" + fi + + SKILL_NAME="$skill_name" +} + +# --------------------------------------------------------------------------- +# Validate SKILL.md exists +# --------------------------------------------------------------------------- +validate_source() { + if [ ! -f "$SOURCE_DIR/SKILL.md" ]; then + error "No SKILL.md found in $SOURCE_DIR" + error "A valid skill must contain a SKILL.md file." + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Platform detection and path resolution +# --------------------------------------------------------------------------- +detect_all_global_platforms() { + platforms="" + if [ -d "$HOME/.claude" ]; then platforms="$platforms claude-code"; fi + if [ -d "$HOME/.gemini" ]; then platforms="$platforms gemini"; fi + if [ -d "$HOME/.config/goose" ]; then platforms="$platforms goose"; fi + if [ -d "$HOME/.config/opencode" ]; then platforms="$platforms opencode"; fi + if [ -d "$HOME/.copilot" ]; then platforms="$platforms copilot"; fi + echo "$platforms" +} + +detect_all_project_platforms() { + platforms="" + if [ -d ".cursor" ]; then platforms="$platforms cursor"; fi + if [ -d ".windsurf" ]; then platforms="$platforms windsurf"; fi + if [ -d ".clinerules" ] || [ -d ".cline" ]; then platforms="$platforms cline"; fi + if [ -d ".kiro" ]; then platforms="$platforms kiro"; fi + if [ -d ".trae" ]; then platforms="$platforms trae"; fi + if [ -d ".roo" ]; then platforms="$platforms roo-code"; fi + if [ -d ".github" ]; then platforms="$platforms copilot"; fi + echo "$platforms" +} + +resolve_platform_path() { + plat="$1" + name="$2" + if [ "$PROJECT_LEVEL" = true ]; then + case "$plat" in + claude-code) echo ".claude/skills/$name" ;; + copilot) echo ".github/skills/$name" ;; + cursor) echo ".cursor/rules/$name" ;; + windsurf) echo ".windsurf/rules/$name" ;; + cline) echo ".clinerules/$name" ;; + gemini) echo ".gemini/skills/$name" ;; + kiro) echo ".kiro/skills/$name" ;; + trae) echo ".trae/rules/$name" ;; + roo-code) echo ".roo/rules/$name" ;; + goose) echo ".agents/skills/$name" ;; + opencode) echo ".agents/skills/$name" ;; + *) echo ".agents/skills/$name" ;; + esac + else + case "$plat" in + claude-code) echo "$HOME/.claude/skills/$name" ;; + copilot) echo "$HOME/.copilot/skills/$name" ;; + cursor) echo "$HOME/.cursor/rules/$name" ;; + windsurf) echo "$HOME/.codeium/windsurf/skills/$name" ;; + cline) echo "$HOME/.cline/rules/$name" ;; + gemini) echo "$HOME/.gemini/skills/$name" ;; + goose) echo "$HOME/.config/goose/skills/$name" ;; + opencode) echo "$HOME/.config/opencode/skills/$name" ;; + kiro) echo "$HOME/.agents/skills/$name" ;; + trae) echo "$HOME/.agents/skills/$name" ;; + roo-code) echo "$HOME/.agents/skills/$name" ;; + *) echo "$HOME/.agents/skills/$name" ;; + esac + fi +} + +platform_display() { + case "$1" in + claude-code) echo "Claude Code" ;; + gemini) echo "Gemini CLI" ;; + goose) echo "Goose" ;; + opencode) echo "OpenCode" ;; + copilot) echo "GitHub Copilot" ;; + cursor) echo "Cursor" ;; + windsurf) echo "Windsurf" ;; + cline) echo "Cline" ;; + kiro) echo "Kiro" ;; + trae) echo "Trae" ;; + roo-code) echo "Roo Code" ;; + *) echo "$1" ;; + esac +} + +# --------------------------------------------------------------------------- +# Format adapters (for Tier 2 platforms) +# --------------------------------------------------------------------------- +generate_cursor_mdc() { + target_dir="$1" + skill_md="$SOURCE_DIR/SKILL.md" + + desc="" + in_fm=false + lnum=0 + while IFS= read -r line; do + lnum=$((lnum + 1)) + if [ "$lnum" -eq 1 ]; then in_fm=true; continue; fi + if $in_fm && [ "$line" = "---" ]; then break; fi + if $in_fm; then + case "$line" in + description:*) desc="$(echo "$line" | sed 's/^description:[[:space:]]*//')" ;; + esac + fi + done < "$skill_md" + + mdc_file="${target_dir}/${SKILL_NAME}.mdc" + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would generate Cursor .mdc: $mdc_file" + return 0 + fi + + body="$(awk 'BEGIN{c=0} /^---$/{c++;next} c>=2{print}' "$skill_md")" + mkdir -p "$target_dir" + cat > "$mdc_file" < "${global_file}.tmp" + mv "${global_file}.tmp" "$global_file" + fi + cat >> "$global_file" < +${body} + +WSEOF + success "Appended to Windsurf global_rules.md" + else + rule_file="${target_dir}/${SKILL_NAME}.md" + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would generate Windsurf rule: $rule_file" + return 0 + fi + mkdir -p "$target_dir" + printf '%s\n' "$body" > "$rule_file" + success "Generated Windsurf rule: $rule_file" + fi +} + +generate_plain_rule() { + target_dir="$1" + filename="$2" + skill_md="$SOURCE_DIR/SKILL.md" + + plain_file="${target_dir}/${filename}" + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would generate plain rule: $plain_file" + return 0 + fi + mkdir -p "$target_dir" + awk 'BEGIN{c=0} /^---$/{c++;next} c>=2{print}' "$skill_md" > "$plain_file" + success "Generated plain rule: $plain_file" +} + +run_adapters() { + plat="$1" + dest="$2" + case "$plat" in + cursor) + generate_cursor_mdc "$dest" + ;; + windsurf) + if [ "$PROJECT_LEVEL" = true ]; then + generate_windsurf_rule "$(pwd)/.windsurf/rules" "false" + else + generate_windsurf_rule "" "true" + fi + ;; + cline|roo-code|trae) + generate_plain_rule "$dest" "${SKILL_NAME}.md" + ;; + esac +} + +# --------------------------------------------------------------------------- +# Create a symlink (with fallback to copy) +# --------------------------------------------------------------------------- +create_symlink() { + target="$1" + link_path="$2" + + if [ "$target" = "$link_path" ]; then return 0; fi + + mkdir -p "$(dirname "$link_path")" + + if [ -e "$link_path" ] || [ -L "$link_path" ]; then + rm -rf "$link_path" + fi + + if ln -s "$target" "$link_path" 2>/dev/null; then + return 0 + else + warn "Symlink failed for $link_path — falling back to copy" + cp -R "$target" "$link_path" + fi +} + +# --------------------------------------------------------------------------- +# Install to a single platform +# --------------------------------------------------------------------------- +install_to_platform() { + plat="$1" + dest="$(resolve_platform_path "$plat" "$SKILL_NAME")" + display="$(platform_display "$plat")" + + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would install to $display: $dest" + run_adapters "$plat" "$dest" + return 0 + fi + + create_symlink "$SOURCE_DIR" "$dest" + success "Installed for $display → $dest" + run_adapters "$plat" "$dest" +} + +# --------------------------------------------------------------------------- +# Uninstall from all platforms +# --------------------------------------------------------------------------- +do_uninstall() { + printf "\n${BOLD}Uninstalling skill: %s${NC}\n\n" "$SKILL_NAME" + + # Canonical location + canonical="$HOME/.agents/skills/$SKILL_NAME" + if [ -e "$canonical" ] || [ -L "$canonical" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would remove: $canonical" + else + rm -rf "$canonical" + success "Removed: $canonical" + fi + fi + + # Check all global platforms + for plat in claude-code gemini goose opencode copilot; do + dest="$(resolve_platform_path "$plat" "$SKILL_NAME")" + if [ -e "$dest" ] || [ -L "$dest" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would remove: $dest" + else + rm -rf "$dest" + success "Removed: $dest ($(platform_display "$plat"))" + fi + fi + done + + # Check project-level platforms + for plat in cursor windsurf cline kiro trae roo-code copilot; do + PROJECT_LEVEL=true + dest="$(resolve_platform_path "$plat" "$SKILL_NAME")" + if [ -e "$dest" ] || [ -L "$dest" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would remove: $dest" + else + rm -rf "$dest" + success "Removed: $dest ($(platform_display "$plat"))" + fi + fi + done + + printf "\nDone.\n" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + printf "\n${BOLD}Universal Skill Installer${NC}\n\n" + + resolve_source + if [ "$DRY_RUN" = false ]; then + validate_source + elif [ -d "$SOURCE_DIR" ]; then + validate_source + fi + extract_skill_name + info "Skill: $SKILL_NAME" + info "Source: $SOURCE_DIR" + + if [ "$UNINSTALL" = true ]; then + do_uninstall + return 0 + fi + + # Install to canonical location first (if from git, already there) + canonical="$HOME/.agents/skills/$SKILL_NAME" + if ! is_git_url "$SOURCE" && [ "$SOURCE_DIR" != "$canonical" ]; then + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would copy to canonical: $canonical" + else + mkdir -p "$(dirname "$canonical")" + rm -rf "$canonical" + cp -R "$SOURCE_DIR" "$canonical" + success "Copied to canonical: $canonical" + fi + fi + + # Determine which platforms to install to + if [ -n "$PLATFORM" ]; then + # Single platform + install_to_platform "$PLATFORM" + else + # All detected platforms + if [ "$PROJECT_LEVEL" = true ]; then + platforms="$(detect_all_project_platforms)" + else + platforms="$(detect_all_global_platforms)" + fi + + count=0 + for plat in $platforms; do + install_to_platform "$plat" + count=$((count + 1)) + done + + if [ $count -eq 0 ]; then + warn "No platforms detected. Skill installed at canonical path only." + fi + fi + + # Summary + printf "\n${BOLD}Done!${NC}\n" + printf " Canonical: %s\n" "$canonical" + printf " Invoke with: /${SKILL_NAME}\n\n" + + if [ "$DRY_RUN" = true ]; then + printf "${YELLOW}Dry run — no changes made.${NC}\n\n" + fi +} + +main