203 lines
5.6 KiB
Bash
Executable file
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"
|