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 <noreply@anthropic.com>
This commit is contained in:
parent
afcae83970
commit
3fc735eeae
6 changed files with 994 additions and 5 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -43,3 +43,6 @@ agentdb.db
|
||||||
test_*.py
|
test_*.py
|
||||||
!test_agentdb_learning.py
|
!test_agentdb_learning.py
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
|
# Generated format adapter files
|
||||||
|
*.mdc
|
||||||
|
|
|
||||||
47
README.md
47
README.md
|
|
@ -24,9 +24,17 @@ Every AI coding tool — Claude Code, GitHub Copilot, Cursor, Windsurf, Codex, G
|
||||||
|
|
||||||
## Quick Start
|
## 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
|
```bash
|
||||||
# Claude Code / VS Code Copilot (global — works in all projects)
|
# 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
|
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.
|
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).
|
All 14 platforms: [see full list below](#all-platforms).
|
||||||
|
|
@ -287,11 +304,11 @@ python3 scripts/export_utils.py ./agent-skill-creator/ --variant desktop
|
||||||
### Update
|
### Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/.claude/skills/agent-skill-creator && git pull
|
|
||||||
# or
|
|
||||||
cd ~/.agents/skills/agent-skill-creator && git pull
|
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
|
## 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
|
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
|
### Export
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -434,13 +468,16 @@ All commands use exit code `0` for success, `1` for errors. All support `--json`
|
||||||
agent-skill-creator/
|
agent-skill-creator/
|
||||||
SKILL.md # The skill definition (what the agent reads)
|
SKILL.md # The skill definition (what the agent reads)
|
||||||
README.md # This file
|
README.md # This file
|
||||||
|
install.sh # Symlink self-installer (for cloned repos)
|
||||||
scripts/
|
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
|
validate.py # Spec compliance checker
|
||||||
security_scan.py # Security scanner
|
security_scan.py # Security scanner
|
||||||
staleness_check.py # Staleness detection (review, deps, drift)
|
staleness_check.py # Staleness detection (review, deps, drift)
|
||||||
export_utils.py # Cross-platform export
|
export_utils.py # Cross-platform export
|
||||||
skill_registry.py # Team skill registry
|
skill_registry.py # Team skill registry
|
||||||
install-template.sh # Template for generated installers (14 platforms)
|
|
||||||
references/ # Detailed docs (loaded by the agent on demand)
|
references/ # Detailed docs (loaded by the agent on demand)
|
||||||
pipeline-phases.md # Full creation pipeline
|
pipeline-phases.md # Full creation pipeline
|
||||||
architecture-guide.md # Skill structure decisions
|
architecture-guide.md # Skill structure decisions
|
||||||
|
|
|
||||||
14
SKILL.md
14
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.
|
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 <skill-install-dir> && git rev-parse HEAD 2>/dev/null
|
||||||
|
cd <skill-install-dir> && 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 <path> 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
|
### Generated SKILL.md Format
|
||||||
|
|
||||||
Every generated skill's SKILL.md must follow this structure:
|
Every generated skill's SKILL.md must follow this structure:
|
||||||
|
|
|
||||||
214
install.sh
Executable file
214
install.sh
Executable file
|
|
@ -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: <detection_dir>|<install_path>|<display_name>
|
||||||
|
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 <describe your workflow>\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "$UNINSTALL" = true ]; then
|
||||||
|
do_uninstall
|
||||||
|
else
|
||||||
|
do_install
|
||||||
|
fi
|
||||||
181
scripts/bootstrap.sh
Executable file
181
scripts/bootstrap.sh
Executable file
|
|
@ -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 <describe your workflow>\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
|
||||||
540
scripts/install-skill.sh
Executable file
540
scripts/install-skill.sh
Executable file
|
|
@ -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 <source> [options]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<source> Git URL (https://... or *.git) or local directory path
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--platform <name> 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: <source>"
|
||||||
|
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" <<MDCEOF
|
||||||
|
---
|
||||||
|
description: ${desc}
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
${body}
|
||||||
|
MDCEOF
|
||||||
|
success "Generated Cursor .mdc: $mdc_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_windsurf_rule() {
|
||||||
|
target_dir="$1"
|
||||||
|
is_global="$2"
|
||||||
|
skill_md="$SOURCE_DIR/SKILL.md"
|
||||||
|
|
||||||
|
body="$(awk 'BEGIN{c=0} /^---$/{c++;next} c>=2{print}' "$skill_md")"
|
||||||
|
|
||||||
|
if [ "$is_global" = "true" ]; then
|
||||||
|
global_file="$HOME/.codeium/windsurf/memories/global_rules.md"
|
||||||
|
if [ "$DRY_RUN" = true ]; then
|
||||||
|
info "[dry-run] Would append to Windsurf global_rules.md: $global_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$global_file")"
|
||||||
|
if [ -f "$global_file" ]; then
|
||||||
|
awk -v begin_marker="<!-- BEGIN ${SKILL_NAME} -->" \
|
||||||
|
-v end_marker="<!-- END ${SKILL_NAME} -->" '
|
||||||
|
BEGIN { skip=0 }
|
||||||
|
$0 == begin_marker { skip=1; next }
|
||||||
|
$0 == end_marker { skip=0; next }
|
||||||
|
!skip { print }
|
||||||
|
' "$global_file" > "${global_file}.tmp"
|
||||||
|
mv "${global_file}.tmp" "$global_file"
|
||||||
|
fi
|
||||||
|
cat >> "$global_file" <<WSEOF
|
||||||
|
|
||||||
|
<!-- BEGIN ${SKILL_NAME} -->
|
||||||
|
${body}
|
||||||
|
<!-- END ${SKILL_NAME} -->
|
||||||
|
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
|
||||||
Loading…
Reference in a new issue