#!/usr/bin/env bash set -euo pipefail # Installs git hook blocks for validation into a target repository. # Usage: setup-hooks.sh [TARGET_REPO] # TARGET_REPO — path to the git repo to configure (default: current directory) # Idempotent: safe to re-run; always replaces each managed block with the current version. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET="${1:-$(pwd)}" MARKER="# managed by setup-hooks.sh" END_MARKER="# end setup-hooks" if [[ ! -d "$TARGET/.git" ]]; then echo "Error: $TARGET is not a git repository" >&2 exit 1 fi # --- Tool checks (optional — warn, don't fail) --- warn_if_missing() { local tool="$1" if ! command -v "$tool" &>/dev/null; then echo "Warning: $tool not installed — ${tool} checks in pre-commit will be skipped (install $tool to enable)" >&2 fi } warn_if_missing shellcheck warn_if_missing jq warn_if_missing yq # --- Marker-block helpers --- write_block() { local hook_file="$1" local block_content="$2" if grep -qF "$MARKER" "$hook_file"; then awk -v start="$MARKER" -v end="$END_MARKER" ' $0 == start { skip=1; next } skip && $0 == end { skip=0; next } !skip { print } ' "$hook_file" > "${hook_file}.tmp" && mv "${hook_file}.tmp" "$hook_file" fi printf '\n%s\n%s\n%s\n' "$MARKER" "$block_content" "$END_MARKER" >> "$hook_file" } ensure_hook() { local hook_file="$1" if [[ ! -f "$hook_file" ]]; then printf '#!/usr/bin/env bash\nset -euo pipefail\n' > "$hook_file" chmod +x "$hook_file" fi } # --- commit-msg: conventional commits --- COMMIT_MSG_HOOK="$TARGET/.git/hooks/commit-msg" ensure_hook "$COMMIT_MSG_HOOK" commit_msg_block() { cat <<'BLOCK' msg=$(cat "$1") pattern='^(feat|fix|docs|chore|refactor|test|perf|ci|build|revert)(\(.+\))?!?: .+' if ! echo "$msg" | grep -qE "$pattern"; then echo "ERROR: Commit message must follow Conventional Commits format." >&2 echo " Examples: feat: add login, fix(auth): correct token expiry, chore!: drop python dep" >&2 exit 1 fi BLOCK } write_block "$COMMIT_MSG_HOOK" "$(commit_msg_block)" echo "Updated: $COMMIT_MSG_HOOK (conventional commits check)" # --- pre-commit: shellcheck + jq/yq + SKILL.md frontmatter --- PRE_COMMIT_HOOK="$TARGET/.git/hooks/pre-commit" ensure_hook "$PRE_COMMIT_HOOK" pre_commit_block() { cat <<'BLOCK' staged=$(git diff --cached --name-only --diff-filter=ACM) # shellcheck on staged .sh files if command -v shellcheck &>/dev/null; then echo "$staged" | grep '\.sh$' | while IFS= read -r f; do [[ -f "$f" ]] && shellcheck "$f" done else echo "Warning: shellcheck not installed — shell script linting skipped" >&2 fi # jq validation on staged .json files if command -v jq &>/dev/null; then echo "$staged" | grep '\.json$' | while IFS= read -r f; do [[ -f "$f" ]] && jq . "$f" > /dev/null done else echo "Warning: jq not installed — JSON validation skipped" >&2 fi # yq validation on staged .yaml/.yml files if command -v yq &>/dev/null; then echo "$staged" | grep -E '\.(yaml|yml)$' | while IFS= read -r f; do [[ -f "$f" ]] && yq eval '.' "$f" > /dev/null done else echo "Warning: yq not installed — YAML validation skipped" >&2 fi # SKILL.md frontmatter: must have name: and description: echo "$staged" | grep 'SKILL\.md$' | while IFS= read -r f; do if [[ -f "$f" ]]; then if ! grep -q '^name:' "$f" || ! grep -q '^description' "$f"; then echo "ERROR: $f is missing required frontmatter fields (name: and description:)" >&2 exit 1 fi fi done BLOCK } write_block "$PRE_COMMIT_HOOK" "$(pre_commit_block)" echo "Updated: $PRE_COMMIT_HOOK (shellcheck + jq/yq + SKILL.md validation)" # --- pre-push: test suite + manifest cross-reference --- PRE_PUSH_HOOK="$TARGET/.git/hooks/pre-push" ensure_hook "$PRE_PUSH_HOOK" pre_push_block() { local script_dir="$SCRIPT_DIR" cat <