#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" LOCK_FILE="${1:-${PROJECT_ROOT}/skills-lock.json}" OUTPUT_FILE="${2:-${SCRIPT_DIR}/install-global-skills.sh}" die() { printf 'Error: %s\n' "$1" >&2 exit 1 } for command_name in awk chmod mkdir mktemp mv rm sort; do if ! command -v "${command_name}" >/dev/null 2>&1; then die "Required command not found: ${command_name}" fi done [[ -f "${LOCK_FILE}" ]] || die "Skills lockfile not found: ${LOCK_FILE}" mapfile -t records < <( awk ' /^ "[^"]+": \{$/ { skill = $1 gsub(/[":]/, "", skill) next } $1 == "\"source\":" { source = $2 gsub(/[",]/, "", source) print source "\t" skill skill = "" } ' "${LOCK_FILE}" | LC_ALL=C sort -t $'\t' -k1,1 -k2,2 ) (( ${#records[@]} > 0 )) || die "No skills found in lockfile: ${LOCK_FILE}" temporary_file="$(mktemp "${OUTPUT_FILE}.tmp.XXXXXX")" cleanup() { rm -f -- "${temporary_file}" } trap cleanup EXIT { printf '%s\n' '#!/usr/bin/env bash' printf '\n' printf '%s\n' '# This file is generated by scripts/generate-global-skills-installer.sh.' printf '%s\n' '# Do not edit manually; regenerate it after changing skills-lock.json.' printf '%s\n' '# shellcheck shell=bash' printf '\n' printf '%s\n' 'set -Eeuo pipefail' printf '\n' printf 'SKILL_COUNT=%q\n' "${#records[@]}" printf 'DRY_RUN=0\n' printf 'COPY_MODE=0\n' printf 'SHARED_SKILLS_DIR="${SKILLS_GLOBAL_DIR:-${HOME:?HOME must be set}/.agents/skills}"\n' printf 'CODEX_HOME_DIR="${CODEX_HOME:-${HOME:?HOME must be set}/.codex}"\n' printf 'CODEX_SKILLS_DIR="${CODEX_HOME_DIR}/skills"\n' printf '\n' printf '%s\n' 'usage() {' printf '%s\n' ' cat <<'"'"'USAGE'"'"'' printf '%s\n' 'Usage: scripts/install-global-skills.sh [options]' printf '\n' printf '%s\n' 'Install the repository skill baseline globally and bridge it into Codex.' printf '\n' printf '%s\n' 'Options:' printf '%s\n' ' --dry-run Show commands and filesystem changes without applying them.' printf '%s\n' ' --copy Copy skills into Codex instead of creating symlinks.' printf '%s\n' ' --help Show this help text.' printf '\n' printf '%s\n' 'Environment:' printf '%s\n' ' SKILLS_GLOBAL_DIR Shared global skills directory (default: ~/.agents/skills).' printf '%s\n' ' CODEX_HOME Codex home directory (default: ~/.codex).' printf '%s\n' 'USAGE' printf '%s\n' '}' printf '\n' printf '%s\n' 'die() {' printf '%s\n' ' printf '"'"'Error: %s\n'"'"' "$1" >&2' printf '%s\n' ' exit 1' printf '%s\n' '}' printf '\n' printf '%s\n' 'print_command() {' printf '%s\n' ' printf '"'"'+'"'"'' printf '%s\n' ' printf '"'"' %q'"'"' "$@"' printf '%s\n' ' printf '"'"'\n'"'"'' printf '%s\n' '}' printf '\n' printf '%s\n' 'run_command() {' printf '%s\n' ' if (( DRY_RUN )); then' printf '%s\n' ' print_command "$@"' printf '%s\n' ' return 0' printf '%s\n' ' fi' printf '%s\n' ' "$@"' printf '%s\n' '}' printf '\n' printf '%s\n' 'require_command() {' printf '%s\n' ' command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"' printf '%s\n' '}' printf '\n' printf '%s\n' 'while (( $# > 0 )); do' printf '%s\n' ' case "$1" in' printf '%s\n' ' --dry-run)' printf '%s\n' ' DRY_RUN=1' printf '%s\n' ' ;;' printf '%s\n' ' --copy)' printf '%s\n' ' COPY_MODE=1' printf '%s\n' ' ;;' printf '%s\n' ' --help|-h)' printf '%s\n' ' usage' printf '%s\n' ' exit 0' printf '%s\n' ' ;;' printf '%s\n' ' *)' printf '%s\n' ' die "Unknown option: $1 (use --help for usage)"' printf '%s\n' ' ;;' printf '%s\n' ' esac' printf '%s\n' ' shift' printf '%s\n' 'done' printf '\n' printf '%s\n' 'require_command bash' printf '%s\n' 'require_command npx' printf '%s\n' 'require_command mkdir' printf '%s\n' 'require_command ln' printf '%s\n' 'require_command readlink' printf '%s\n' 'if (( COPY_MODE )); then' printf '%s\n' ' require_command cp' printf '%s\n' ' require_command diff' printf '%s\n' 'fi' printf '\n' printf '%s\n' 'printf '"'"'Installing %d skills globally...\n'"'"' "$SKILL_COUNT"' printf '\n' current_source="" source_skill_args=() for record in "${records[@]}"; do source="${record%%$'\t'*}" skill="${record#*$'\t'}" if [[ -n "${current_source}" && "${source}" != "${current_source}" ]]; then printf 'run_command npx --yes skills add %q --global --yes' "${current_source}" for skill_arg in "${source_skill_args[@]}"; do printf ' --skill %q' "${skill_arg}" done printf '\n\n' source_skill_args=() fi current_source="${source}" source_skill_args+=("${skill}") done printf 'run_command npx --yes skills add %q --global --yes' "${current_source}" for skill_arg in "${source_skill_args[@]}"; do printf ' --skill %q' "${skill_arg}" done printf '\n\n' printf '%s\n' 'SKILLS=(' for record in "${records[@]}"; do skill="${record#*$'\t'}" printf ' %q\n' "${skill}" done printf '%s\n' ')' printf '\n' printf '%s\n' 'if (( DRY_RUN )); then' printf '%s\n' ' printf '"'"'Would bridge skills from %s to %s\n'"'"' "$SHARED_SKILLS_DIR" "$CODEX_SKILLS_DIR"' printf '%s\n' ' for skill in "${SKILLS[@]}"; do' printf '%s\n' ' if (( COPY_MODE )); then' printf '%s\n' ' printf '"'"'+ copy %s -> %s/%s\n'"'"' "$SHARED_SKILLS_DIR/$skill" "$CODEX_SKILLS_DIR" "$skill"' printf '%s\n' ' else' printf '%s\n' ' printf '"'"'+ ln -s %s %s/%s\n'"'"' "$SHARED_SKILLS_DIR/$skill" "$CODEX_SKILLS_DIR" "$skill"' printf '%s\n' ' fi' printf '%s\n' ' done' printf '%s\n' ' exit 0' printf '%s\n' 'fi' printf '\n' printf '%s\n' 'run_command mkdir -p "$CODEX_SKILLS_DIR"' printf '%s\n' 'conflict_count=0' printf '%s\n' 'for skill in "${SKILLS[@]}"; do' printf '%s\n' ' source_path="$SHARED_SKILLS_DIR/$skill"' printf '%s\n' ' target_path="$CODEX_SKILLS_DIR/$skill"' printf '%s\n' ' [[ -d "$source_path" ]] || { printf '"'"'Error: Installed skill is missing: %s\n'"'"' "$source_path" >&2; conflict_count=$((conflict_count + 1)); continue; }' printf '%s\n' ' if [[ -L "$target_path" ]]; then' printf '%s\n' ' if [[ "$(readlink "$target_path")" == "$source_path" ]]; then' printf '%s\n' ' printf '"'"'Already linked: %s\n'"'"' "$skill"' printf '%s\n' ' continue' printf '%s\n' ' fi' printf '%s\n' ' printf '"'"'Error: Refusing to replace unmanaged symlink: %s\n'"'"' "$target_path" >&2' printf '%s\n' ' conflict_count=$((conflict_count + 1))' printf '%s\n' ' continue' printf '%s\n' ' fi' printf '%s\n' ' if [[ -e "$target_path" ]]; then' printf '%s\n' ' if (( COPY_MODE )) && [[ -d "$target_path" ]] && diff -qr -- "$source_path" "$target_path" >/dev/null; then' printf '%s\n' ' printf '"'"'Already copied: %s\n'"'"' "$skill"' printf '%s\n' ' continue' printf '%s\n' ' fi' printf '%s\n' ' printf '"'"'Error: Refusing to overwrite existing path: %s\n'"'"' "$target_path" >&2' printf '%s\n' ' conflict_count=$((conflict_count + 1))' printf '%s\n' ' continue' printf '%s\n' ' fi' printf '%s\n' ' if (( COPY_MODE )); then' printf '%s\n' ' run_command cp -R -- "$source_path" "$target_path"' printf '%s\n' ' printf '"'"'Copied: %s\n'"'"' "$skill"' printf '%s\n' ' else' printf '%s\n' ' run_command ln -s -- "$source_path" "$target_path"' printf '%s\n' ' printf '"'"'Linked: %s\n'"'"' "$skill"' printf '%s\n' ' fi' printf '%s\n' 'done' printf '\n' printf '%s\n' '(( conflict_count == 0 )) || die "${conflict_count} skill bridge conflict(s) require manual resolution"' printf '%s\n' 'printf '"'"'Global skills installation complete.\n'"'"'' } >"${temporary_file}" chmod 755 "${temporary_file}" mkdir -p -- "$(dirname -- "${OUTPUT_FILE}")" mv -- "${temporary_file}" "${OUTPUT_FILE}" trap - EXIT printf 'Generated %s from %s (%d skills).\n' "${OUTPUT_FILE}" "${LOCK_FILE}" "${#records[@]}"