#!/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"