#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" KIT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MERGE_COPILOT_INSTRUCTIONS=0 DRY_RUN=0 PROJECT_ROOT="" usage() { cat <<'EOF' Usage: bootstrap/install.sh [PROJECT_ROOT] [--merge-copilot-instructions] [--dry-run] Options: --merge-copilot-instructions Opt-in: safely merge .github/copilot-instructions.md --dry-run Print planned changes without modifying files -h, --help Show this help message EOF } log_action() { local message="$1" if [[ "$DRY_RUN" -eq 1 ]]; then echo "[dry-run] $message" else echo "$message" fi } run_or_dry() { if [[ "$DRY_RUN" -eq 1 ]]; then log_action "$*" return 0 fi "$@" } while [[ $# -gt 0 ]]; do case "$1" in --merge-copilot-instructions) MERGE_COPILOT_INSTRUCTIONS=1 shift ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; -*) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; *) if [[ -z "$PROJECT_ROOT" ]]; then PROJECT_ROOT="$1" shift else echo "Unexpected extra argument: $1" >&2 usage >&2 exit 1 fi ;; esac done PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}" MANAGED_BEGIN='' MANAGED_END='' PROJECT_BEGIN='' PROJECT_END='' count_occurrence() { local needle="$1" local file="$2" grep -cF "$needle" "$file" || true } build_managed_block() { local kit_file="$1" { printf '%s\n' "$MANAGED_BEGIN" cat "$kit_file" printf '\n%s\n' "$MANAGED_END" } } append_project_section_if_missing() { local file="$1" local project_begin_count local project_end_count project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" project_end_count="$(count_occurrence "$PROJECT_END" "$file")" if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then cat >> "$file" < $PROJECT_END EOF elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then return 1 fi } validate_merged_copilot_file() { local file="$1" local managed_begin_count local managed_end_count local project_begin_count local project_end_count managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$file")" managed_end_count="$(count_occurrence "$MANAGED_END" "$file")" project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" project_end_count="$(count_occurrence "$PROJECT_END" "$file")" [[ "$managed_begin_count" -eq 1 ]] || return 1 [[ "$managed_end_count" -eq 1 ]] || return 1 [[ "$project_begin_count" -eq 1 ]] || return 1 [[ "$project_end_count" -eq 1 ]] || return 1 } merge_copilot_instructions() { local kit_file="$KIT_ROOT/base/.github/copilot-instructions.md" local host_file="$PROJECT_ROOT/.github/copilot-instructions.md" local backup_file="$host_file.bak" local tmp_file local managed_block local managed_begin_count local managed_end_count if [[ "$MERGE_COPILOT_INSTRUCTIONS" -ne 1 ]]; then return 0 fi if [[ ! -f "$kit_file" ]]; then log_action "Skipped Copilot merge: kit template not found at $kit_file" return 0 fi if [[ "$DRY_RUN" -eq 1 ]]; then if [[ ! -f "$host_file" ]]; then log_action "Would create and initialize $host_file with managed + project-specific sections" return 0 fi managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" log_action "Would back up $host_file to $host_file.bak" if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then log_action "Would update managed myagent-kit section in $host_file" elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then log_action "Would append managed myagent-kit section to $host_file" else log_action "Would fail: malformed managed marker pair in $host_file" return 1 fi local project_begin_count local project_end_count project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$host_file")" project_end_count="$(count_occurrence "$PROJECT_END" "$host_file")" if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then log_action "Would append project-specific editable section to $host_file" elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then log_action "Would fail: malformed project-specific marker pair in $host_file" return 1 fi return 0 fi managed_block="$(build_managed_block "$kit_file")" tmp_file="$(mktemp)" if [[ ! -f "$host_file" ]]; then { printf '%s\n' "$managed_block" printf '\n%s\n' "$PROJECT_BEGIN" printf '\n' printf '%s\n' "$PROJECT_END" } > "$host_file" validate_merged_copilot_file "$host_file" log_action "Created and initialized $host_file" rm -f "$tmp_file" return 0 fi cp "$host_file" "$backup_file" managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then awk \ -v begin="$MANAGED_BEGIN" \ -v end="$MANAGED_END" \ -v block="$managed_block" ' BEGIN { in_block = 0 replaced = 0 } $0 == begin { if (!replaced) { print block replaced = 1 } in_block = 1 next } $0 == end { in_block = 0 next } !in_block { print } END { if (!replaced) { print "" print block } } ' "$host_file" > "$tmp_file" elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then { cat "$host_file" printf '\n%s\n' "$managed_block" } > "$tmp_file" else echo "Malformed managed marker pair in $host_file" >&2 cp "$backup_file" "$host_file" rm -f "$tmp_file" return 1 fi mv "$tmp_file" "$host_file" append_project_section_if_missing "$host_file" validate_merged_copilot_file "$host_file" log_action "Merged $host_file (backup: $backup_file)" } run_or_dry mkdir -p "$PROJECT_ROOT/.github/prompts" \ "$PROJECT_ROOT/.github/instructions" \ "$PROJECT_ROOT/.github/ISSUE_TEMPLATE" run_or_dry cp "$KIT_ROOT/base/.github/prompts/"*.prompt.md "$PROJECT_ROOT/.github/prompts/" run_or_dry cp "$KIT_ROOT/base/.github/instructions/"*.instructions.md "$PROJECT_ROOT/.github/instructions/" run_or_dry cp "$KIT_ROOT/base/.github/ISSUE_TEMPLATE/"*.md "$PROJECT_ROOT/.github/ISSUE_TEMPLATE/" if [[ -f "$KIT_ROOT/base/.github/pull_request_template.md" ]]; then run_or_dry cp "$KIT_ROOT/base/.github/pull_request_template.md" "$PROJECT_ROOT/.github/pull_request_template.md" fi merge_copilot_instructions log_action "Installed myagent-kit assets into $PROJECT_ROOT/.github"