mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 10:53:08 +00:00
- Move save_init_options() before preset install so skills propagation works during 'specify init --preset --ai-skills' - Clean up downloaded ZIP after successful preset install during init - Validate --from URL scheme (require HTTPS, HTTP only for localhost) - Expose unregister_commands() on extensions.py CommandRegistrar wrapper instead of reaching into private _registrar field - Use _get_merged_packs() for search() and get_pack_info() so all active catalogs are searched, not just the highest-priority one - Fix fetch_catalog() cache to verify cached URL matches current URL - Fix PresetResolver: script resolution uses .sh extension, consistent file extensions throughout resolve(), and resolve_with_source() delegates to resolve() to honor template_type parameter - Fix bash common.sh: fall through to directory scan when python3 returns empty preset list - Fix PowerShell Resolve-Template: filter out dot-folders and sort extensions deterministically
228 lines
7.5 KiB
Bash
228 lines
7.5 KiB
Bash
#!/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
|
|
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
|
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=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
|
|
|
cat <<EOF
|
|
REPO_ROOT='$repo_root'
|
|
CURRENT_BRANCH='$current_branch'
|
|
HAS_GIT='$has_git_repo'
|
|
FEATURE_DIR='$feature_dir'
|
|
FEATURE_SPEC='$feature_dir/spec.md'
|
|
IMPL_PLAN='$feature_dir/plan.md'
|
|
TASKS='$feature_dir/tasks.md'
|
|
RESEARCH='$feature_dir/research.md'
|
|
DATA_MODEL='$feature_dir/data-model.md'
|
|
QUICKSTART='$feature_dir/quickstart.md'
|
|
CONTRACTS_DIR='$feature_dir/contracts'
|
|
EOF
|
|
}
|
|
|
|
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
|
|
# Resolve a template name to a file path using the priority stack:
|
|
# 1. .specify/templates/overrides/
|
|
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
|
# 3. .specify/extensions/<ext-id>/templates/
|
|
# 4. .specify/templates/ (core)
|
|
resolve_template() {
|
|
local template_name="$1"
|
|
local repo_root="$2"
|
|
local base="$repo_root/.specify/templates"
|
|
|
|
# Priority 1: Project overrides
|
|
local override="$base/overrides/${template_name}.md"
|
|
[ -f "$override" ] && echo "$override" && return 0
|
|
|
|
# Priority 2: Installed presets (sorted by priority from .registry)
|
|
local presets_dir="$repo_root/.specify/presets"
|
|
if [ -d "$presets_dir" ]; then
|
|
local registry_file="$presets_dir/.registry"
|
|
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
|
|
# Read preset IDs sorted by priority (lower number = higher precedence)
|
|
local sorted_presets
|
|
sorted_presets=$(python3 -c "
|
|
import json, sys
|
|
try:
|
|
data = json.load(open('$registry_file'))
|
|
presets = data.get('presets', {})
|
|
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
|
|
print(pid)
|
|
except Exception:
|
|
sys.exit(1)
|
|
" 2>/dev/null)
|
|
if [ $? -eq 0 ] && [ -n "$sorted_presets" ]; then
|
|
while IFS= read -r preset_id; do
|
|
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
|
|
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
done <<< "$sorted_presets"
|
|
else
|
|
# python3 returned empty list — fall through to directory scan
|
|
for preset in "$presets_dir"/*/; do
|
|
[ -d "$preset" ] || continue
|
|
local candidate="$preset/templates/${template_name}.md"
|
|
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
done
|
|
fi
|
|
else
|
|
# Fallback: alphabetical directory order (no python3 available)
|
|
for preset in "$presets_dir"/*/; do
|
|
[ -d "$preset" ] || continue
|
|
local candidate="$preset/templates/${template_name}.md"
|
|
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# Priority 3: Extension-provided templates
|
|
local ext_dir="$repo_root/.specify/extensions"
|
|
if [ -d "$ext_dir" ]; then
|
|
for ext in "$ext_dir"/*/; do
|
|
[ -d "$ext" ] || continue
|
|
local candidate="$ext/templates/${template_name}.md"
|
|
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
done
|
|
fi
|
|
|
|
# Priority 4: Core templates
|
|
local core="$base/${template_name}.md"
|
|
[ -f "$core" ] && echo "$core" && return 0
|
|
|
|
return 1
|
|
}
|
|
|