Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ trim_trailing_whitespace = false

[*.{sh,bash}]
indent_size = 4
switch_case_indent = true

[*.{ps1,psm1,psd1}]
indent_size = 4
Expand Down
22 changes: 17 additions & 5 deletions scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ while [[ $# -gt 0 ]]; do
fi
TEMPLATE_NAME="$1"
;;
--help|-h)
cat << 'EOF'
--help | -h)
cat <<'EOF'
Usage: check-prerequisites.sh [OPTIONS]

Consolidated prerequisite checking for Spec-Driven Development workflow.
Expand Down Expand Up @@ -94,15 +94,22 @@ done

# Source common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "$SCRIPT_DIR/common.sh"

# Get feature paths.
# In --paths-only mode this is pure resolution, so pass --no-persist to opt out
# of the feature.json write side effect (issue #3025).
if $PATHS_ONLY; then
_paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
_paths_output=$(get_feature_paths --no-persist) || {
echo "ERROR: Failed to resolve feature paths" >&2
exit 1
}
else
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
_paths_output=$(get_feature_paths) || {
echo "ERROR: Failed to resolve feature paths" >&2
exit 1
}
fi
eval "$_paths_output"
unset _paths_output
Expand Down Expand Up @@ -183,7 +190,12 @@ fi

TEMPLATE_CONTENT=""
if [[ -n "$TEMPLATE_NAME" ]]; then
if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
if TEMPLATE_CONTENT=$(
resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"
status=$?
printf x
exit "$status"
); then
TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
else
echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
Expand Down
93 changes: 63 additions & 30 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ get_repo_root() {
fi

# Final fallback to script location
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local script_dir
script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../../.." && pwd)
}

Expand All @@ -95,7 +96,10 @@ get_current_branch() {
read_feature_json_feature_directory() {
local repo_root="$1"
local fj="$repo_root/.specify/feature.json"
[[ -f "$fj" ]] || { printf '%s' ''; return 0; }
[[ -f "$fj" ]] || {
printf '%s' ''
return 0
}

# Try parsers in order (jq -> python3 -> grep/sed), falling through on
# failure. Selection is by *parse success*, not mere availability: on
Expand All @@ -119,9 +123,9 @@ read_feature_json_feature_directory() {
if [[ -z "$_fd" ]]; then
# Last-resort single-line grep/sed fallback. The `|| true` guards against
# grep returning 1 (no match) aborting under `set -e` / `pipefail`.
_fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
| head -n 1 \
| sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
_fd=$({ grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } |
head -n 1 |
sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/')
fi

printf '%s' "$_fd"
Expand Down Expand Up @@ -154,9 +158,9 @@ _persist_feature_json() {

# Write feature.json — prefer jq for safe JSON, fall back to printf
if command -v jq >/dev/null 2>&1; then
jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' > "$fj"
jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' >"$fj"
else
printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" > "$fj"
printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" >"$fj"
fi
}

Expand Down Expand Up @@ -266,14 +270,18 @@ get_invoke_separator() {
local jq_separator
if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
case "$jq_separator" in
"."|"-") separator="$jq_separator"; parsed=1 ;;
"." | "-")
separator="$jq_separator"
parsed=1
;;
esac
fi
fi

if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
local py_separator
if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
if py_separator=$(
python3 - "$integration_json" <<'PY' 2>/dev/null
import json
import sys

Expand All @@ -291,9 +299,12 @@ try:
except Exception:
sys.exit(1)
PY
); then
); then
case "$py_separator" in
"."|"-") separator="$py_separator"; parsed=1 ;;
"." | "-")
separator="$py_separator"
parsed=1
;;
esac
fi
fi
Expand Down Expand Up @@ -344,7 +355,7 @@ PY
}
' "$integration_json" 2>/dev/null)
case "$awk_separator" in
"."|"-") separator="$awk_separator" ;;
"." | "-") separator="$awk_separator" ;;
esac
fi
fi
Expand Down Expand Up @@ -391,10 +402,10 @@ json_escape() {
# so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
local LC_ALL=C
local i char code
for (( i=0; i<${#s}; i++ )); do
for ((i = 0; i < ${#s}; i++)); do
char="${s:$i:1}"
printf -v code '%d' "'$char" 2>/dev/null || code=256
if (( code >= 1 && code <= 31 )); then
if ((code >= 1 && code <= 31)); then
printf '\\u%04x' "$code"
else
printf '%s' "$char"
Expand Down Expand Up @@ -519,7 +530,7 @@ resolve_template() {
local repo_root="$2"
local base="$repo_root/.specify/templates"

case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
case "$template_name" in "" | *[!a-z0-9-]*) return 1 ;; esac

# Priority 1: Project overrides
local override="$base/overrides/${template_name}.md"
Expand Down Expand Up @@ -566,7 +577,7 @@ except Exception:
[ -f "$candidate" ] && echo "$candidate" && return 0
candidate="$presets_dir/$preset_id/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done <<< "$sorted_presets"
done <<<"$sorted_presets"
fi
# python3 succeeded but registry has no presets — nothing to search
else
Expand Down Expand Up @@ -604,7 +615,7 @@ except Exception:
local candidate="$ext/templates/${template_name}.md"
[ -f "$candidate" ] || candidate="$ext/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done <<< "$sorted_extensions"
done <<<"$sorted_extensions"
fi

# Priority 4: Core templates
Expand All @@ -628,7 +639,7 @@ resolve_template_content() {
local repo_root="$2"
local base="$repo_root/.specify/templates"

case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
case "$template_name" in "" | *[!a-z0-9-]*) return 1 ;; esac

# Collect all layers (highest priority first)
local -a layer_paths=()
Expand Down Expand Up @@ -776,7 +787,7 @@ except Exception as exc:
fi
if [ -n "$result" ]; then
local declaration
IFS=$'\t' read -r declaration strategy manifest_file <<< "$result"
IFS=$'\t' read -r declaration strategy manifest_file <<<"$result"
[ "$declaration" = "found" ] && manifest_declared=true
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
fi
Expand All @@ -786,7 +797,7 @@ except Exception as exc:
local candidate=""
if [ -n "$manifest_file" ]; then
case "$manifest_file" in
/*|*../*) manifest_file="" ;;
/* | *../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
Expand All @@ -809,7 +820,7 @@ except Exception as exc:
break
fi
fi
done <<< "$sorted_presets"
done <<<"$sorted_presets"
fi
fi

Expand All @@ -831,7 +842,7 @@ except Exception as exc:
effective_base_found=true
break
fi
done <<< "$sorted_extensions"
done <<<"$sorted_extensions"
fi

# Priority 4: Core templates (always "replace")
Expand Down Expand Up @@ -872,7 +883,7 @@ except Exception as exc:
# to find the nearest replace layer. Only compose layers above that base.
local base_idx=-1
local i
for (( i=0; i<count; i++ )); do
for ((i = 0; i < count; i++)); do
if [ "${layer_strategies[$i]}" = "replace" ]; then
base_idx=$i
break
Expand All @@ -886,18 +897,28 @@ except Exception as exc:

# Read the base content; compose layers above the base (higher priority)
local content
if ! content=$(cat "${layer_paths[$base_idx]}"; status=$?; printf x; exit "$status"); then
if ! content=$(
cat "${layer_paths[$base_idx]}"
status=$?
printf x
exit "$status"
); then
echo "Error: failed to read template layer ${layer_paths[$base_idx]}" >&2
return 2
fi
content="${content%x}"

for (( i=base_idx-1; i>=0; i-- )); do
for ((i = base_idx - 1; i >= 0; i--)); do
local path="${layer_paths[$i]}"
local strat="${layer_strategies[$i]}"
local layer_content
# Preserve trailing newlines
if ! layer_content=$(cat "$path"; status=$?; printf x; exit "$status"); then
if ! layer_content=$(
cat "$path"
status=$?
printf x
exit "$status"
); then
echo "Error: failed to read template layer $path" >&2
return 2
fi
Expand All @@ -906,17 +927,26 @@ except Exception as exc:
case "$strat" in
replace) content="$layer_content" ;;
prepend)
content=$(printf '%s\n\n%s' "$layer_content" "$content"; printf x)
content=$(
printf '%s\n\n%s' "$layer_content" "$content"
printf x
)
content="${content%x}"
;;
append)
content=$(printf '%s\n\n%s' "$content" "$layer_content"; printf x)
content=$(
printf '%s\n\n%s' "$content" "$layer_content"
printf x
)
content="${content%x}"
;;
wrap)
case "$layer_content" in
*'{CORE_TEMPLATE}'*) ;;
*) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 2 ;;
*)
echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2
return 2
;;
esac
# Consume the wrapper left to right instead of rewriting it in
# place. Rewriting re-scanned the string just modified, so base
Expand All @@ -933,7 +963,10 @@ except Exception as exc:
done
content="${wrapped}${rest}"
;;
*) echo "Error: unknown strategy '$strat'" >&2; return 2 ;;
*)
echo "Error: unknown strategy '$strat'" >&2
return 2
;;
esac
done

Expand Down
20 changes: 14 additions & 6 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ while [ $i -le $# ]; do
--timestamp)
USE_TIMESTAMP=true
;;
--help|-h)
--help | -h)
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
echo ""
echo "Options:"
Expand Down Expand Up @@ -166,7 +166,7 @@ fit_branch_name() {
local branch_name="${feature_num}-${branch_suffix}"

if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
local prefix_length=$(( ${#feature_num} + 1 ))
local prefix_length=$((${#feature_num} + 1))
local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
local truncated_suffix
truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
Expand All @@ -191,6 +191,7 @@ shell_quote() {

# Resolve repository root using common.sh functions which prioritize .specify
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "$SCRIPT_DIR/common.sh"

REPO_ROOT=$(get_repo_root) || exit 1
Expand All @@ -214,7 +215,8 @@ generate_branch_name() {
# acronym probe below uses ASCII word boundaries like the Python twin's
# (?<![0-9A-Za-z_]) lookarounds.
local -x LC_ALL=C
local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
local clean_name
clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')

# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
local meaningful_words=()
Expand Down Expand Up @@ -251,7 +253,8 @@ generate_branch_name() {
echo "$result"
else
# Fallback to original logic if no meaningful words found
local cleaned=$(clean_branch_name "$description")
local cleaned
cleaned=$(clean_branch_name "$description")
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi
}
Expand Down Expand Up @@ -361,7 +364,12 @@ if [ "$DRY_RUN" != true ]; then
SPEC_TEMPLATE_CONTENT=""
if [ ! -f "$SPEC_FILE" ]; then
NEEDS_SPEC=true
if SPEC_TEMPLATE_CONTENT=$(resolve_template_content "spec-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
if SPEC_TEMPLATE_CONTENT=$(
resolve_template_content "spec-template" "$REPO_ROOT"
status=$?
printf x
exit "$status"
); then
SPEC_TEMPLATE_CONTENT="${SPEC_TEMPLATE_CONTENT%x}"
SPEC_TEMPLATE_FOUND=true
else
Expand All @@ -376,7 +384,7 @@ if [ "$DRY_RUN" != true ]; then

if [ "$NEEDS_SPEC" = true ]; then
if [ "$SPEC_TEMPLATE_FOUND" = true ]; then
printf '%s' "$SPEC_TEMPLATE_CONTENT" > "$SPEC_FILE"
printf '%s' "$SPEC_TEMPLATE_CONTENT" >"$SPEC_FILE"
else
echo "Warning: Spec template not found; created empty spec file" >&2
touch "$SPEC_FILE"
Expand Down
Loading
Loading