#!/usr/bin/env bash set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" repo_root="$(cd -- "$script_dir/.." && pwd -P)" state_dir="${COPILOT_RESOURCES_STATE_DIR:-$HOME/.copilot-resources-state}" temp_dir="" usage() { cat <<'EOF' Usage: install/publish.sh --source PATH [--kind KIND] [--name NAME] [--force] Supported kinds: skill, prompt, instruction, agent, hook The publish workflow normalizes target names, aligns skill metadata with the published directory name, and blocks duplicate shared resources by content and effective display name. EOF } fail() { printf '%s\n' "$1" >&2 exit 1 } append_log() { mkdir -p -- "$state_dir" printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "$1" \ "$2" \ "$3" \ "$4" \ "$5" \ "$6" >> "$state_dir/publish-log.tsv" } normalize_stem() { local raw="$1" raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" raw="$(printf '%s' "$raw" | sed -E 's/[[:space:]_]+/-/g; s/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-+//; s/-+$//')" if [[ -z "$raw" ]]; then raw="resource" fi printf '%s\n' "$raw" } strip_kind_suffix() { local kind="$1" local raw="$2" case "$kind" in prompt) raw="${raw%.prompt.md}" ;; instruction) raw="${raw%.instructions.md}" ;; agent) raw="${raw%.agent.md}" ;; hook) raw="${raw%.json}" ;; esac printf '%s\n' "$raw" } kind_root_dir() { local kind="$1" case "$kind" in skill) printf '%s\n' "$repo_root/resources/skills" ;; prompt) printf '%s\n' "$repo_root/resources/prompts" ;; instruction) printf '%s\n' "$repo_root/resources/instructions" ;; agent) printf '%s\n' "$repo_root/resources/agents" ;; hook) printf '%s\n' "$repo_root/resources/hooks" ;; *) fail "Unsupported kind: $kind" ;; esac } kind_suffix() { local kind="$1" case "$kind" in prompt) printf '.prompt.md\n' ;; instruction) printf '.instructions.md\n' ;; agent) printf '.agent.md\n' ;; hook) printf '.json\n' ;; skill) printf '\n' ;; *) fail "Unsupported kind: $kind" ;; esac } extract_frontmatter_field() { local file="$1" local field="$2" [[ -f "$file" ]] || return 0 awk -v field="$field" ' BEGIN { in_yaml = 0 delimiter_count = 0 } /^[[:space:]]*---[[:space:]]*$/ { delimiter_count++ if (delimiter_count == 1) { in_yaml = 1 next } if (in_yaml) { exit } } in_yaml { pattern = "^[[:space:]]*" field ":[[:space:]]*" if ($0 ~ pattern) { sub(pattern, "", $0) sub(/[[:space:]]+#.*$/, "", $0) gsub(/^["\047]|["\047]$/, "", $0) print $0 exit } } ' "$file" } ensure_skill_name_field() { local file="$1" local normalized_name="$2" local temp_file [[ -f "$file" ]] || fail "Skill publish requires SKILL.md: $file" [[ "$(head -n 1 "$file")" =~ ^---[[:space:]]*$ ]] || fail "Skill frontmatter must start with ---: $file" temp_file="$(mktemp)" if ! awk -v normalized_name="$normalized_name" ' BEGIN { in_yaml = 0 delimiter_count = 0 wrote_name = 0 frontmatter_found = 0 } /^[[:space:]]*---[[:space:]]*$/ { delimiter_count++ print if (delimiter_count == 1) { in_yaml = 1 frontmatter_found = 1 next } if (in_yaml && !wrote_name) { print "name: " normalized_name wrote_name = 1 } in_yaml = 0 next } in_yaml { if ($0 ~ /^[[:space:]]*name:[[:space:]]*/) { print "name: " normalized_name wrote_name = 1 next } } { print } END { if (!frontmatter_found || delimiter_count < 2) { exit 42 } } ' "$file" > "$temp_file"; then rm -f -- "$temp_file" fail "Skill frontmatter is invalid or incomplete: $file" fi mv -- "$temp_file" "$file" } artifact_fingerprint() { local path="$1" if [[ -d "$path" ]]; then ( cd -- "$path" find . -type f | LC_ALL=C sort | while IFS= read -r rel_path; do file_hash="$(shasum -a 256 "$rel_path" | awk '{print $1}')" printf '%s %s\n' "$file_hash" "$rel_path" done ) | shasum -a 256 | awk '{print $1}' return 0 fi shasum -a 256 "$path" | awk '{print $1}' } artifact_display_name() { local kind="$1" local path="$2" local display_name="" local base_name="" case "$kind" in skill) display_name="$(extract_frontmatter_field "$path/SKILL.md" name)" if [[ -z "$display_name" ]]; then display_name="$(basename -- "$path")" fi ;; prompt|instruction|agent) display_name="$(extract_frontmatter_field "$path" name)" if [[ -z "$display_name" ]]; then base_name="$(basename -- "$path")" display_name="$(strip_kind_suffix "$kind" "$base_name")" fi ;; hook) base_name="$(basename -- "$path")" display_name="${base_name%.json}" ;; *) fail "Unsupported kind: $kind" ;; esac printf '%s\n' "$display_name" } list_kind_artifacts() { local kind="$1" local root_dir root_dir="$(kind_root_dir "$kind")" [[ -d "$root_dir" ]] || return 0 case "$kind" in skill) find "$root_dir" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort ;; prompt) find "$root_dir" -mindepth 1 -maxdepth 1 -type f -name '*.prompt.md' | LC_ALL=C sort ;; instruction) find "$root_dir" -mindepth 1 -maxdepth 1 -type f -name '*.instructions.md' | LC_ALL=C sort ;; agent) find "$root_dir" -mindepth 1 -maxdepth 1 -type f -name '*.agent.md' | LC_ALL=C sort ;; hook) find "$root_dir" -mindepth 1 -maxdepth 1 -type f -name '*.json' | LC_ALL=C sort ;; esac } detect_kind() { local source="$1" local base base="$(basename -- "$source")" if [[ -d "$source" && -f "$source/SKILL.md" ]]; then printf 'skill\n' return 0 fi case "$base" in *.prompt.md) printf 'prompt\n' ;; *.instructions.md) printf 'instruction\n' ;; *.agent.md) printf 'agent\n' ;; *.json) printf 'hook\n' ;; SKILL.md) printf 'skill\n' ;; *) return 1 ;; esac } cleanup() { if [[ -n "${temp_dir:-}" && -d "${temp_dir:-}" ]]; then rm -rf -- "${temp_dir:-}" fi } main() { local source="" local kind="" local name="" local force="false" local candidate_path="" local normalized_name="" local target="" local origin_label="local-first" local source_base="" local candidate_fingerprint="" local candidate_display_name="" local existing_fingerprint="" local existing_display_name="" trap cleanup EXIT while [[ $# -gt 0 ]]; do case "$1" in --source) source="$2" shift 2 ;; --kind) kind="$2" shift 2 ;; --name) name="$2" shift 2 ;; --force) force="true" shift ;; --help) usage exit 0 ;; *) fail "Unknown argument: $1" ;; esac done [[ -n "$source" ]] || fail "--source is required" [[ -e "$source" ]] || fail "Source does not exist: $source" if [[ -z "$kind" ]]; then kind="$(detect_kind "$source")" || fail "Could not detect resource kind from source path" fi temp_dir="$(mktemp -d)" case "$kind" in skill) if [[ -f "$source" ]]; then source="$(dirname -- "$source")" fi [[ -f "$source/SKILL.md" ]] || fail "Skill publish requires a directory containing SKILL.md" name="${name:-$(extract_frontmatter_field "$source/SKILL.md" name)}" source_base="$(basename -- "$source")" normalized_name="$(normalize_stem "${name:-$source_base}")" target="$repo_root/resources/skills/$normalized_name" candidate_path="$temp_dir/$normalized_name" cp -R -- "$source" "$candidate_path" ensure_skill_name_field "$candidate_path/SKILL.md" "$normalized_name" ;; prompt) source_base="$(basename -- "$source")" normalized_name="$(normalize_stem "$(strip_kind_suffix "$kind" "${name:-$source_base}")")" target="$repo_root/resources/prompts/$normalized_name$(kind_suffix "$kind")" candidate_path="$temp_dir/$normalized_name$(kind_suffix "$kind")" cp -- "$source" "$candidate_path" ;; instruction) source_base="$(basename -- "$source")" normalized_name="$(normalize_stem "$(strip_kind_suffix "$kind" "${name:-$source_base}")")" target="$repo_root/resources/instructions/$normalized_name$(kind_suffix "$kind")" candidate_path="$temp_dir/$normalized_name$(kind_suffix "$kind")" cp -- "$source" "$candidate_path" ;; agent) source_base="$(basename -- "$source")" normalized_name="$(normalize_stem "$(strip_kind_suffix "$kind" "${name:-$source_base}")")" target="$repo_root/resources/agents/$normalized_name$(kind_suffix "$kind")" candidate_path="$temp_dir/$normalized_name$(kind_suffix "$kind")" cp -- "$source" "$candidate_path" ;; hook) source_base="$(basename -- "$source")" normalized_name="$(normalize_stem "$(strip_kind_suffix "$kind" "${name:-$source_base}")")" target="$repo_root/resources/hooks/$normalized_name$(kind_suffix "$kind")" candidate_path="$temp_dir/$normalized_name$(kind_suffix "$kind")" cp -- "$source" "$candidate_path" ;; *) fail "Unsupported kind: $kind" ;; esac candidate_fingerprint="$(artifact_fingerprint "$candidate_path")" candidate_display_name="$(artifact_display_name "$kind" "$candidate_path")" if [[ -e "$target" ]]; then existing_fingerprint="$(artifact_fingerprint "$target")" if [[ "$existing_fingerprint" == "$candidate_fingerprint" ]]; then append_log "$kind" "$source" "$target" "$origin_label" "$candidate_fingerprint" "noop" cat <