From c90c5c8511e4ce2f6b2c37336b602b9472c40818 Mon Sep 17 00:00:00 2001 From: Jone Date: Tue, 7 Apr 2026 16:08:51 +0200 Subject: [PATCH] add skill-installer skill --- .github/skills/skill-installer/SKILL.md | 54 +++++++ .../skill-installer/scripts/install-skill.sh | 145 ++++++++++++++++++ index.md | 1 + 3 files changed, 200 insertions(+) create mode 100644 .github/skills/skill-installer/SKILL.md create mode 100755 .github/skills/skill-installer/scripts/install-skill.sh diff --git a/.github/skills/skill-installer/SKILL.md b/.github/skills/skill-installer/SKILL.md new file mode 100644 index 0000000..8990cd8 --- /dev/null +++ b/.github/skills/skill-installer/SKILL.md @@ -0,0 +1,54 @@ +--- +name: skill-installer +description: > + Install a skill into this repository or into the global Copilot skills + directory. Defaults to repo-only and prompts for source, scope, and overwrite + behavior. +user-invocable: true +--- + +# Skill Installer + +Use this skill to install another skill from a local path, a directory, or a GitHub `SKILL.md` URL. + +## Default behavior + +- Target scope defaults to `repo` +- The installer asks for missing details interactively +- Existing files are never overwritten without confirmation +- If the source is a local directory, you can choose whether to copy supporting files too + +## Supported inputs + +- Local `SKILL.md` file +- Local skill directory +- GitHub blob URL to a `SKILL.md` file + +## Install script + +Run: + +```bash +bash .github/skills/skill-installer/scripts/install-skill.sh +``` + +Or provide a source directly: + +```bash +bash .github/skills/skill-installer/scripts/install-skill.sh https://github.com/casey/just/blob/master/skills/just/SKILL.md +``` + +The script installs to: + +- `.github/skills//` for repo-only installs +- `~/.copilot/skills//` for global installs + +## What it asks + +The installer prompts for: + +1. Source path or URL +2. Target scope: repo or global +3. Skill name if it cannot be inferred +4. Whether to copy supporting files +5. Whether to overwrite an existing install diff --git a/.github/skills/skill-installer/scripts/install-skill.sh b/.github/skills/skill-installer/scripts/install-skill.sh new file mode 100755 index 0000000..089185c --- /dev/null +++ b/.github/skills/skill-installer/scripts/install-skill.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../" && pwd)" +repo_skills_dir="$repo_root/.github/skills" +global_skills_dir="${HOME}/.copilot/skills" + +source_input="${1:-}" +scope_input="${2:-}" +name_input="${3:-}" + +prompt() { + local message="$1" + local default_value="${2:-}" + local reply + if [[ -n "$default_value" ]]; then + read -r -p "${message} [${default_value}]: " reply + printf '%s\n' "${reply:-$default_value}" + else + read -r -p "${message}: " reply + printf '%s\n' "$reply" + fi +} + +normalize_name() { + tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-' +} + +rewrite_github_blob_url() { + local url="$1" + if [[ "$url" =~ ^https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)$ ]]; then + printf 'https://raw.githubusercontent.com/%s/%s/%s/%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}" + else + printf '%s\n' "$url" + fi +} + +extract_skill_name() { + local file="$1" + sed -n 's/^name:[[:space:]]*//p' "$file" | head -n 1 +} + +download_source() { + local url="$1" + local out="$2" + curl -fsSL "$url" -o "$out" +} + +copy_skill_dir() { + local source_dir="$1" + local target_dir="$2" + local include_supporting_files="$3" + + mkdir -p "$target_dir" + if [[ "$include_supporting_files" == "yes" ]]; then + cp -R "$source_dir"/. "$target_dir"/ + else + cp "$source_dir/SKILL.md" "$target_dir/SKILL.md" + fi +} + +if [[ -z "$source_input" ]]; then + source_input="$(prompt "Source path, directory, or GitHub SKILL.md URL")" +fi + +if [[ -z "$scope_input" ]]; then + scope_input="$(prompt "Target scope (repo/global)" "repo")" +fi + +if [[ "$scope_input" != "repo" && "$scope_input" != "global" ]]; then + printf 'Invalid scope: %s\n' "$scope_input" >&2 + exit 1 +fi + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +staging_dir="$tmpdir/staging" +mkdir -p "$staging_dir" + +source_kind="" +source_path="$source_input" + +if [[ "$source_input" =~ ^https?:// ]]; then + source_kind="url" + source_path="$(rewrite_github_blob_url "$source_input")" + download_source "$source_path" "$staging_dir/SKILL.md" +elif [[ -d "$source_input" ]]; then + source_kind="directory" + skill_file="$source_input/SKILL.md" + if [[ ! -f "$skill_file" ]]; then + printf 'Missing SKILL.md in source directory: %s\n' "$source_input" >&2 + exit 1 + fi + include_supporting_files="$(prompt "Copy supporting files too? (yes/no)" "no")" + copy_skill_dir "$source_input" "$staging_dir" "$include_supporting_files" +elif [[ -f "$source_input" ]]; then + source_kind="file" + cp "$source_input" "$staging_dir/SKILL.md" +else + printf 'Source not found: %s\n' "$source_input" >&2 + exit 1 +fi + +if [[ ! -f "$staging_dir/SKILL.md" ]]; then + printf 'No SKILL.md was staged from %s source.\n' "$source_kind" >&2 + exit 1 +fi + +inferred_name="$(extract_skill_name "$staging_dir/SKILL.md")" +if [[ -z "$name_input" ]]; then + name_input="$(prompt "Skill name" "${inferred_name:-skill}")" +fi +skill_name="$(printf '%s' "$name_input" | normalize_name)" + +if [[ -z "$skill_name" ]]; then + printf 'Skill name cannot be empty.\n' >&2 + exit 1 +fi + +if [[ "$scope_input" == "repo" ]]; then + target_base="$repo_skills_dir" +else + target_base="$global_skills_dir" +fi + +target_dir="$target_base/$skill_name" +if [[ -e "$target_dir" ]]; then + overwrite="$(prompt "Target exists at ${target_dir}. Overwrite? (yes/no)" "no")" + if [[ "$overwrite" != "yes" ]]; then + printf 'Aborted.\n' + exit 1 + fi + rm -rf "$target_dir" +fi + +mkdir -p "$target_dir" +cp -R "$staging_dir"/. "$target_dir"/ + +if [[ ! -f "$target_dir/SKILL.md" ]]; then + printf 'Install failed: %s/SKILL.md missing after copy.\n' "$target_dir" >&2 + exit 1 +fi + +printf 'Installed %s skill to %s\n' "$skill_name" "$target_dir" diff --git a/index.md b/index.md index 3042b9e..25a1ec9 100644 --- a/index.md +++ b/index.md @@ -7,5 +7,6 @@ This is my AI discovery subdomain. There are only a few files here, as that is a - .well-known/ai.json - This file contains metadata about the AI stuff like profile, ... - AGENTS.md - Global AI agent instructions (used across all of Jone's projects) - .github/copilot-instructions.md - Repository-specific Copilot instructions (deployment, shell context, etc.) +- .github/skills/ - Repo-local Copilot skills, including the skill installer (skill-installer) That's all files :)