#!/usr/bin/env bash # Common functions and variables for all scripts # Get repository root, with fallback for non-git repositories get_repo_root() { if git rev-parse --show-toplevel >/dev/null 2>&1; then git rev-parse --show-toplevel else # Fall back to script location for non-git repos local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" (cd "$script_dir/../../.." && pwd) fi } # Get current branch, with fallback for non-git repositories get_current_branch() { # First check if SPECIFY_FEATURE environment variable is set if [[ -n "${SPECIFY_FEATURE:-}" ]]; then echo "$SPECIFY_FEATURE" return fi # Then check git if available if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then git rev-parse --abbrev-ref HEAD return fi # For non-git repos, try to find the latest feature directory local repo_root=$(get_repo_root) local specs_dir="$repo_root/specs" if [[ -d "$specs_dir" ]]; then local latest_feature="" local highest=0 for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") if [[ "$dirname" =~ ^([0-9]{3})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) if [[ "$number" -gt "$highest" ]]; then highest=$number latest_feature=$dirname fi fi fi done if [[ -n "$latest_feature" ]]; then echo "$latest_feature" return fi fi echo "main" # Final fallback } # Check if we have git available has_git() { git rev-parse --show-toplevel >/dev/null 2>&1 } check_feature_branch() { local branch="$1" local has_git_repo="$2" # For non-git repos, we can't enforce branch naming but still provide output if [[ "$has_git_repo" != "true" ]]; then echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 return 0 fi if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 echo "Feature branches should be named like: 001-feature-name" >&2 return 1 fi return 0 } get_feature_dir() { echo "$1/specs/$2"; } # Find feature directory by numeric prefix instead of exact branch match # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) find_feature_dir_by_prefix() { local repo_root="$1" local branch_name="$2" local specs_dir="$repo_root/specs" # Extract numeric prefix from branch (e.g., "004" from "004-whatever") if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then # If branch doesn't have numeric prefix, fall back to exact match echo "$specs_dir/$branch_name" return fi local prefix="${BASH_REMATCH[1]}" # Search for directories in specs/ that start with this prefix local matches=() if [[ -d "$specs_dir" ]]; then for dir in "$specs_dir"/"$prefix"-*; do if [[ -d "$dir" ]]; then matches+=("$(basename "$dir")") fi done fi # Handle results if [[ ${#matches[@]} -eq 0 ]]; then # No match found - return the branch name path (will fail later with clear error) echo "$specs_dir/$branch_name" elif [[ ${#matches[@]} -eq 1 ]]; then # Exactly one match - perfect! echo "$specs_dir/${matches[0]}" else # Multiple matches - this shouldn't happen with proper naming convention echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 echo "Please ensure only one spec directory exists per numeric prefix." >&2 return 1 fi } get_feature_paths() { local repo_root=$(get_repo_root) local current_branch=$(get_current_branch) local has_git_repo="false" if has_git; then has_git_repo="true" fi # Use prefix-based lookup to support multiple branches per spec local feature_dir if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then echo "ERROR: Failed to resolve feature directory" >&2 return 1 fi # Use printf '%q' to safely quote values, preventing shell injection # via crafted branch names or paths containing special characters printf 'REPO_ROOT=%q\n' "$repo_root" printf 'CURRENT_BRANCH=%q\n' "$current_branch" printf 'HAS_GIT=%q\n' "$has_git_repo" printf 'FEATURE_DIR=%q\n' "$feature_dir" printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md" printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md" printf 'TASKS=%q\n' "$feature_dir/tasks.md" printf 'RESEARCH=%q\n' "$feature_dir/research.md" printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md" printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md" printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts" } # Check if jq is available for safe JSON construction has_jq() { command -v jq >/dev/null 2>&1 } # Escape a string for safe embedding in a JSON value (fallback when jq is unavailable). # Handles backslash, double-quote, and control characters (newline, tab, carriage return). json_escape() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" s="${s//$'\t'/\\t}" s="${s//$'\r'/\\r}" printf '%s' "$s" } check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }