ai.jone.foo/.github/skills/skill-installer/scripts/install-skill.sh
2026-04-07 16:48:47 +02:00

203 lines
5.6 KiB
Bash
Executable file

#!/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
}
parse_github_blob_url() {
local url="$1"
if [[ "$url" =~ ^https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)$ ]]; then
printf '%s\t%s\t%s\t%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"
return 0
fi
return 1
}
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"
}
list_github_contents() {
local owner="$1"
local repo="$2"
local ref="$3"
local path="$4"
curl -fsSL -H 'Accept: application/vnd.github+json' \
"https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}" | \
python3 -c $'import json,sys\ndata = json.load(sys.stdin)\nitems = data if isinstance(data, list) else [data]\nfor item in items:\n print("%s\\t%s\\t%s" % (item.get("type", ""), item.get("path", ""), item.get("download_url", "")))'
}
download_github_contents() {
local owner="$1"
local repo="$2"
local ref="$3"
local path="$4"
local target_root="$5"
while IFS=$'\t' read -r item_type item_path download_url; do
[[ -z "$item_type" ]] && continue
local relative_path="${item_path#${path}/}"
local destination_path="$target_root/$relative_path"
case "$item_type" in
dir)
download_github_contents "$owner" "$repo" "$ref" "$item_path" "$target_root"
;;
file)
mkdir -p "$(dirname "$destination_path")"
download_source "$download_url" "$destination_path"
;;
*)
printf 'Skipping unsupported GitHub content type: %s (%s)\n' "$item_type" "$item_path" >&2
;;
esac
done < <(list_github_contents "$owner" "$repo" "$ref" "$path")
}
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
if github_blob_parts="$(parse_github_blob_url "$source_input")"; then
source_kind="github-blob-url"
IFS=$'\t' read -r github_owner github_repo github_ref github_path <<<"$github_blob_parts"
github_skill_dir="$(dirname "$github_path")"
download_github_contents "$github_owner" "$github_repo" "$github_ref" "$github_skill_dir" "$staging_dir"
else
source_kind="url"
source_path="$(rewrite_github_blob_url "$source_input")"
download_source "$source_path" "$staging_dir/SKILL.md"
fi
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
echo "<$source_input>" >"$target_dir/source.md"
printf 'Installed %s skill to %s\n' "$skill_name" "$target_dir"