diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh index 98e387c2..6f7c99e0 100644 --- a/scripts/bash/check-prerequisites.sh +++ b/scripts/bash/check-prerequisites.sh @@ -79,15 +79,28 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get feature paths and validate branch -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then if $JSON_MODE; then # Minimal JSON paths payload (no validation performed) - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" + if has_jq; then + jq -cn \ + --arg repo_root "$REPO_ROOT" \ + --arg branch "$CURRENT_BRANCH" \ + --arg feature_dir "$FEATURE_DIR" \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg tasks "$TASKS" \ + '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}' + else + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")" + fi else echo "REPO_ROOT: $REPO_ROOT" echo "BRANCH: $CURRENT_BRANCH" @@ -141,14 +154,25 @@ fi # Output results if $JSON_MODE; then # Build JSON array of documents - if [[ ${#docs[@]} -eq 0 ]]; then - json_docs="[]" + if has_jq; then + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .) + fi + jq -cn \ + --arg feature_dir "$FEATURE_DIR" \ + --argjson docs "$json_docs" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}' else - json_docs=$(printf '"%s",' "${docs[@]}") - json_docs="[${json_docs%,}]" + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '"%s",' "${docs[@]}") + json_docs="[${json_docs%,}]" + fi + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" fi - - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" else # Text output echo "FEATURE_DIR:$FEATURE_DIR" diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 2c3165e4..7161f43b 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -120,7 +120,7 @@ find_feature_dir_by_prefix() { # 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 + return 1 fi } @@ -134,21 +134,42 @@ get_feature_paths() { fi # Use prefix-based lookup to support multiple branches per spec - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") + 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 - cat </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"; } diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 54697024..725f84c8 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -162,6 +162,17 @@ clean_branch_name() { echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' } +# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable). +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\t'/\\t}" + s="${s//$'\r'/\\r}" + printf '%s' "$s" +} + # Resolve repository root. Prefer git information when available, but fall back # to searching for repository markers so the workflow still functions in repositories that # were initialised with --no-git. @@ -300,14 +311,22 @@ TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" SPEC_FILE="$FEATURE_DIR/spec.md" if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi -# Set the SPECIFY_FEATURE environment variable for the current session -export SPECIFY_FEATURE="$BRANCH_NAME" +# Inform the user how to persist the feature variable in their own shell +printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" + if command -v jq >/dev/null 2>&1; then + jq -cn \ + --arg branch_name "$BRANCH_NAME" \ + --arg spec_file "$SPEC_FILE" \ + --arg feature_num "$FEATURE_NUM" \ + '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}' + else + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" + fi else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" - echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" + printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" fi diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index d01c6d6c..60cf372c 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -28,7 +28,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get all paths and variables from common functions -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output # Check if we're on a proper feature branch (only for git repos) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 @@ -49,8 +51,18 @@ fi # Output results if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" + if has_jq; then + jq -cn \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg specs_dir "$FEATURE_DIR" \ + --arg branch "$CURRENT_BRANCH" \ + --arg has_git "$HAS_GIT" \ + '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}' + else + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")" + fi else echo "FEATURE_SPEC: $FEATURE_SPEC" echo "IMPL_PLAN: $IMPL_PLAN" diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index b0022fd4..341e4e68 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -53,7 +53,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get all paths and variables from common functions -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code AGENT_TYPE="${1:-}" @@ -71,12 +73,14 @@ AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" QODER_FILE="$REPO_ROOT/QODER.md" -AMP_FILE="$REPO_ROOT/AGENTS.md" +# AMP, Kiro CLI, and IBM Bob all share AGENTS.md — use AGENTS_FILE to avoid +# updating the same file multiple times. +AMP_FILE="$AGENTS_FILE" SHAI_FILE="$REPO_ROOT/SHAI.md" TABNINE_FILE="$REPO_ROOT/TABNINE.md" -KIRO_FILE="$REPO_ROOT/AGENTS.md" +KIRO_FILE="$AGENTS_FILE" AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" -BOB_FILE="$REPO_ROOT/AGENTS.md" +BOB_FILE="$AGENTS_FILE" VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md" KIMI_FILE="$REPO_ROOT/KIMI.md" @@ -112,6 +116,8 @@ log_warning() { # Cleanup function for temporary files cleanup() { local exit_code=$? + # Disarm traps to prevent re-entrant loop + trap - EXIT INT TERM rm -f /tmp/agent_update_*_$$ rm -f /tmp/manual_additions_$$ exit $exit_code @@ -607,67 +613,67 @@ update_specific_agent() { case "$agent_type" in claude) - update_agent_file "$CLAUDE_FILE" "Claude Code" + update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 ;; gemini) - update_agent_file "$GEMINI_FILE" "Gemini CLI" + update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1 ;; copilot) - update_agent_file "$COPILOT_FILE" "GitHub Copilot" + update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1 ;; cursor-agent) - update_agent_file "$CURSOR_FILE" "Cursor IDE" + update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1 ;; qwen) - update_agent_file "$QWEN_FILE" "Qwen Code" + update_agent_file "$QWEN_FILE" "Qwen Code" || return 1 ;; opencode) - update_agent_file "$AGENTS_FILE" "opencode" + update_agent_file "$AGENTS_FILE" "opencode" || return 1 ;; codex) - update_agent_file "$AGENTS_FILE" "Codex CLI" + update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1 ;; windsurf) - update_agent_file "$WINDSURF_FILE" "Windsurf" + update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1 ;; kilocode) - update_agent_file "$KILOCODE_FILE" "Kilo Code" + update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1 ;; auggie) - update_agent_file "$AUGGIE_FILE" "Auggie CLI" + update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1 ;; roo) - update_agent_file "$ROO_FILE" "Roo Code" + update_agent_file "$ROO_FILE" "Roo Code" || return 1 ;; codebuddy) - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1 ;; qodercli) - update_agent_file "$QODER_FILE" "Qoder CLI" + update_agent_file "$QODER_FILE" "Qoder CLI" || return 1 ;; amp) - update_agent_file "$AMP_FILE" "Amp" + update_agent_file "$AMP_FILE" "Amp" || return 1 ;; shai) - update_agent_file "$SHAI_FILE" "SHAI" + update_agent_file "$SHAI_FILE" "SHAI" || return 1 ;; tabnine) - update_agent_file "$TABNINE_FILE" "Tabnine CLI" + update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1 ;; kiro-cli) - update_agent_file "$KIRO_FILE" "Kiro CLI" + update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1 ;; agy) - update_agent_file "$AGY_FILE" "Antigravity" + update_agent_file "$AGY_FILE" "Antigravity" || return 1 ;; bob) - update_agent_file "$BOB_FILE" "IBM Bob" + update_agent_file "$BOB_FILE" "IBM Bob" || return 1 ;; vibe) - update_agent_file "$VIBE_FILE" "Mistral Vibe" + update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1 ;; kimi) - update_agent_file "$KIMI_FILE" "Kimi Code" + update_agent_file "$KIMI_FILE" "Kimi Code" || return 1 ;; generic) log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." @@ -682,106 +688,53 @@ update_specific_agent() { update_all_existing_agents() { local found_agent=false - - # Check each possible agent file and update if it exists - if [[ -f "$CLAUDE_FILE" ]]; then - update_agent_file "$CLAUDE_FILE" "Claude Code" - found_agent=true - fi - - if [[ -f "$GEMINI_FILE" ]]; then - update_agent_file "$GEMINI_FILE" "Gemini CLI" - found_agent=true - fi - - if [[ -f "$COPILOT_FILE" ]]; then - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - found_agent=true - fi - - if [[ -f "$CURSOR_FILE" ]]; then - update_agent_file "$CURSOR_FILE" "Cursor IDE" - found_agent=true - fi - - if [[ -f "$QWEN_FILE" ]]; then - update_agent_file "$QWEN_FILE" "Qwen Code" - found_agent=true - fi - - if [[ -f "$AGENTS_FILE" ]]; then - update_agent_file "$AGENTS_FILE" "Codex/opencode" - found_agent=true - fi - - if [[ -f "$WINDSURF_FILE" ]]; then - update_agent_file "$WINDSURF_FILE" "Windsurf" - found_agent=true - fi - - if [[ -f "$KILOCODE_FILE" ]]; then - update_agent_file "$KILOCODE_FILE" "Kilo Code" - found_agent=true - fi + local _updated_paths=() - if [[ -f "$AUGGIE_FILE" ]]; then - update_agent_file "$AUGGIE_FILE" "Auggie CLI" + # Helper: skip non-existent files and files already updated (dedup by + # realpath so that variables pointing to the same file — e.g. AMP_FILE, + # KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once). + # Uses a linear array instead of associative array for bash 3.2 compatibility. + update_if_new() { + local file="$1" name="$2" + [[ -f "$file" ]] || return 0 + local real_path + real_path=$(realpath "$file" 2>/dev/null || echo "$file") + local p + if [[ ${#_updated_paths[@]} -gt 0 ]]; then + for p in "${_updated_paths[@]}"; do + [[ "$p" == "$real_path" ]] && return 0 + done + fi + update_agent_file "$file" "$name" || return 1 + _updated_paths+=("$real_path") found_agent=true - fi - - if [[ -f "$ROO_FILE" ]]; then - update_agent_file "$ROO_FILE" "Roo Code" - found_agent=true - fi + } - if [[ -f "$CODEBUDDY_FILE" ]]; then - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - found_agent=true - fi + update_if_new "$CLAUDE_FILE" "Claude Code" + update_if_new "$GEMINI_FILE" "Gemini CLI" + update_if_new "$COPILOT_FILE" "GitHub Copilot" + update_if_new "$CURSOR_FILE" "Cursor IDE" + update_if_new "$QWEN_FILE" "Qwen Code" + update_if_new "$AGENTS_FILE" "Codex/opencode" + update_if_new "$AMP_FILE" "Amp" + update_if_new "$KIRO_FILE" "Kiro CLI" + update_if_new "$BOB_FILE" "IBM Bob" + update_if_new "$WINDSURF_FILE" "Windsurf" + update_if_new "$KILOCODE_FILE" "Kilo Code" + update_if_new "$AUGGIE_FILE" "Auggie CLI" + update_if_new "$ROO_FILE" "Roo Code" + update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" + update_if_new "$SHAI_FILE" "SHAI" + update_if_new "$TABNINE_FILE" "Tabnine CLI" + update_if_new "$QODER_FILE" "Qoder CLI" + update_if_new "$AGY_FILE" "Antigravity" + update_if_new "$VIBE_FILE" "Mistral Vibe" + update_if_new "$KIMI_FILE" "Kimi Code" - if [[ -f "$SHAI_FILE" ]]; then - update_agent_file "$SHAI_FILE" "SHAI" - found_agent=true - fi - - if [[ -f "$TABNINE_FILE" ]]; then - update_agent_file "$TABNINE_FILE" "Tabnine CLI" - found_agent=true - fi - - if [[ -f "$QODER_FILE" ]]; then - update_agent_file "$QODER_FILE" "Qoder CLI" - found_agent=true - fi - - if [[ -f "$KIRO_FILE" ]]; then - update_agent_file "$KIRO_FILE" "Kiro CLI" - found_agent=true - fi - - if [[ -f "$AGY_FILE" ]]; then - update_agent_file "$AGY_FILE" "Antigravity" - found_agent=true - fi - if [[ -f "$BOB_FILE" ]]; then - update_agent_file "$BOB_FILE" "IBM Bob" - found_agent=true - fi - - if [[ -f "$VIBE_FILE" ]]; then - update_agent_file "$VIBE_FILE" "Mistral Vibe" - found_agent=true - fi - - if [[ -f "$KIMI_FILE" ]]; then - update_agent_file "$KIMI_FILE" "Kimi Code" - found_agent=true - fi - # If no agent files exist, create a default Claude file if [[ "$found_agent" == false ]]; then log_info "No existing agent files found, creating default Claude file..." - update_agent_file "$CLAUDE_FILE" "Claude Code" + update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 fi } print_summary() {