From 5787bb5537918816f7d8c4696cff72ec067ac400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:27:43 -0700 Subject: [PATCH 01/13] Refactor with platform-specific constraints --- .github/workflows/release.yml | 215 ++++++++-------- .../scripts/create-release-packages.sh | 95 ++++--- docs/installation.md | 19 +- docs/local-development.md | 25 +- docs/quickstart.md | 8 + scripts/bash/check-task-prerequisites.sh | 15 ++ scripts/bash/common.sh | 37 +++ scripts/bash/create-new-feature.sh | 58 +++++ scripts/bash/get-feature-paths.sh | 7 + scripts/bash/setup-plan.sh | 17 ++ scripts/bash/update-agent-context.sh | 57 +++++ scripts/check-task-prerequisites.sh | 62 ----- scripts/common.sh | 77 ------ scripts/create-new-feature.sh | 96 ------- scripts/get-feature-paths.sh | 23 -- .../powershell/check-task-prerequisites.ps1 | 35 +++ scripts/powershell/common.ps1 | 65 +++++ scripts/powershell/create-new-feature.ps1 | 52 ++++ scripts/powershell/get-feature-paths.ps1 | 15 ++ scripts/powershell/setup-plan.ps1 | 21 ++ scripts/powershell/update-agent-context.ps1 | 91 +++++++ scripts/setup-plan.sh | 44 ---- scripts/update-agent-context.sh | 234 ------------------ src/specify_cli/__init__.py | 79 +++--- templates/commands/plan.md | 12 +- templates/commands/specify.md | 12 +- templates/commands/tasks.md | 12 +- templates/plan-template.md | 5 +- 28 files changed, 730 insertions(+), 758 deletions(-) create mode 100644 scripts/bash/check-task-prerequisites.sh create mode 100644 scripts/bash/common.sh create mode 100644 scripts/bash/create-new-feature.sh create mode 100644 scripts/bash/get-feature-paths.sh create mode 100644 scripts/bash/setup-plan.sh create mode 100644 scripts/bash/update-agent-context.sh delete mode 100755 scripts/check-task-prerequisites.sh delete mode 100755 scripts/common.sh delete mode 100755 scripts/create-new-feature.sh delete mode 100755 scripts/get-feature-paths.sh create mode 100644 scripts/powershell/check-task-prerequisites.ps1 create mode 100644 scripts/powershell/common.ps1 create mode 100644 scripts/powershell/create-new-feature.ps1 create mode 100644 scripts/powershell/get-feature-paths.ps1 create mode 100644 scripts/powershell/setup-plan.ps1 create mode 100644 scripts/powershell/update-agent-context.ps1 delete mode 100755 scripts/setup-plan.sh delete mode 100755 scripts/update-agent-context.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bdb071a..c7b8f5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,118 +13,115 @@ on: jobs: release: runs-on: ubuntu-latest - permissions: contents: write pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get latest tag - id: get_tag - run: | - # Get the latest tag, or use v0.0.0 if no tags exist - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") - echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - # Extract version number and increment - VERSION=$(echo $LATEST_TAG | sed 's/v//') - IFS='.' read -ra VERSION_PARTS <<< "$VERSION" - MAJOR=${VERSION_PARTS[0]:-0} - MINOR=${VERSION_PARTS[1]:-0} - PATCH=${VERSION_PARTS[2]:-0} - - # Increment patch version - PATCH=$((PATCH + 1)) - NEW_VERSION="v$MAJOR.$MINOR.$PATCH" - - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "New version will be: $NEW_VERSION" - - - name: Check if release already exists - id: check_release - run: | - if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..." - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..." - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create release package - if: steps.check_release.outputs.exists == 'false' - run: | - chmod +x .github/workflows/scripts/create-release-packages.sh - .github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }} - - - name: Generate release notes - if: steps.check_release.outputs.exists == 'false' - id: release_notes - run: | - # Get commits since last tag - LAST_TAG=${{ steps.get_tag.outputs.latest_tag }} - if [ "$LAST_TAG" = "v0.0.0" ]; then - # Check how many commits we have and use that as the limit - COMMIT_COUNT=$(git rev-list --count HEAD) - if [ "$COMMIT_COUNT" -gt 10 ]; then - COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD) + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Get latest tag + id: get_tag + run: | + # Get the latest tag, or use v0.0.0 if no tags exist + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + # Extract version number and increment + VERSION=$(echo $LATEST_TAG | sed 's/v//') + IFS='.' read -ra VERSION_PARTS <<< "$VERSION" + MAJOR=${VERSION_PARTS[0]:-0} + MINOR=${VERSION_PARTS[1]:-0} + PATCH=${VERSION_PARTS[2]:-0} + + # Increment patch version + PATCH=$((PATCH + 1)) + NEW_VERSION="v$MAJOR.$MINOR.$PATCH" + + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New version will be: $NEW_VERSION" + - name: Check if release already exists + id: check_release + run: | + if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..." else - COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s") + echo "exists=false" >> $GITHUB_OUTPUT + echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..." + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create release package variants + if: steps.check_release.outputs.exists == 'false' + run: | + chmod +x .github/workflows/scripts/create-release-packages.sh + .github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }} + - name: Generate release notes + if: steps.check_release.outputs.exists == 'false' + id: release_notes + run: | + # Get commits since last tag + LAST_TAG=${{ steps.get_tag.outputs.latest_tag }} + if [ "$LAST_TAG" = "v0.0.0" ]; then + # Check how many commits we have and use that as the limit + COMMIT_COUNT=$(git rev-list --count HEAD) + if [ "$COMMIT_COUNT" -gt 10 ]; then + COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD) + else + COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s") + fi + else + COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD) + fi + + # Create release notes + cat > release_notes.md << EOF + Template release ${{ steps.get_tag.outputs.new_version }} + + Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI. + + Now includes per-script variants for POSIX shell (sh) and PowerShell (ps). + + Download the template for your preferred AI assistant + script type: + - spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip + EOF + + echo "Generated release notes:" + cat release_notes.md + - name: Create GitHub Release + if: steps.check_release.outputs.exists == 'false' + run: | + # Remove 'v' prefix from version for release title + VERSION_NO_V=${{ steps.get_tag.outputs.new_version }} + VERSION_NO_V=${VERSION_NO_V#v} + + gh release create ${{ steps.get_tag.outputs.new_version }} \ + spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \ + --title "Spec Kit Templates - $VERSION_NO_V" \ + --notes-file release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Update version in pyproject.toml (for release artifacts only) + if: steps.check_release.outputs.exists == 'false' + run: | + # Update version in pyproject.toml (remove 'v' prefix for Python versioning) + VERSION=${{ steps.get_tag.outputs.new_version }} + PYTHON_VERSION=${VERSION#v} + + if [ -f "pyproject.toml" ]; then + sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml + echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)" fi - else - COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD) - fi - - # Create release notes - cat > release_notes.md << EOF - Template release ${{ steps.get_tag.outputs.new_version }} - - Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI. - - Download the template for your preferred AI assistant: - - spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip - EOF - - echo "Generated release notes:" - cat release_notes.md - - - name: Create GitHub Release - if: steps.check_release.outputs.exists == 'false' - run: | - # Remove 'v' prefix from version for release title - VERSION_NO_V=${{ steps.get_tag.outputs.new_version }} - VERSION_NO_V=${VERSION_NO_V#v} - - gh release create ${{ steps.get_tag.outputs.new_version }} \ - spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip \ - --title "Spec Kit Templates - $VERSION_NO_V" \ - --notes-file release_notes.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Update version in pyproject.toml (for release artifacts only) - if: steps.check_release.outputs.exists == 'false' - run: | - # Update version in pyproject.toml (remove 'v' prefix for Python versioning) - VERSION=${{ steps.get_tag.outputs.new_version }} - PYTHON_VERSION=${VERSION#v} - - if [ -f "pyproject.toml" ]; then - sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml - echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)" - fi - - # Note: No longer committing version changes back to main branch - # The version is only updated in the release artifacts diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 682222b..1a60b8c 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -2,7 +2,7 @@ set -euo pipefail # create-release-packages.sh (workflow-local) -# Build Spec Kit template release archives for each supported AI assistant. +# Build Spec Kit template release archives for each supported AI assistant and script type. # Usage: .github/workflows/scripts/create-release-packages.sh # Version argument should include leading 'v'. @@ -18,10 +18,7 @@ fi echo "Building release packages for $NEW_VERSION" -rm -rf sdd-package-base sdd-claude-package sdd-gemini-package sdd-copilot-package \ - spec-kit-template-claude-${NEW_VERSION}.zip \ - spec-kit-template-gemini-${NEW_VERSION}.zip \ - spec-kit-template-copilot-${NEW_VERSION}.zip || true +rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || true mkdir -p sdd-package-base SPEC_DIR="sdd-package-base/.specify" @@ -29,7 +26,7 @@ mkdir -p "$SPEC_DIR" [[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; } [[ -d scripts ]] && { cp -r scripts "$SPEC_DIR/"; echo "Copied scripts -> .specify/scripts"; } -[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \;; echo "Copied templates -> .specify/templates"; } +[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; } rewrite_paths() { sed -E \ @@ -39,54 +36,76 @@ rewrite_paths() { } generate_commands() { - local agent=$1 ext=$2 arg_format=$3 output_dir=$4 + local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5 mkdir -p "$output_dir" for template in templates/commands/*.md; do [[ -f "$template" ]] || continue - local name description body + local name description raw_body variant_line injected body name=$(basename "$template" .md) description=$(awk '/^description:/ {gsub(/^description: *"?/, ""); gsub(/"$/, ""); print; exit}' "$template" | tr -d '\r') - body=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template" | sed "s/{ARGS}/$arg_format/g" | rewrite_paths) + raw_body=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template") + # Find single-line variant comment matching the variant: or + variant_line=$(printf '%s\n' "$raw_body" | awk -v sv="$script_variant" '//, m); if (m[1]!="") {print m[1]; exit}}') + if [[ -z $variant_line ]]; then + echo "Warning: no variant line found for $script_variant in $template" >&2 + variant_line="(Missing variant command for $script_variant)" + fi + # Replace the token VARIANT-INJECT with the selected variant line + injected=$(printf '%s\n' "$raw_body" | sed "s/VARIANT-INJECT/${variant_line//\//\/}/") + # Remove all single-line variant comments + injected=$(printf '%s\n' "$injected" | sed '//, m); if(m[1]!=""){print m[1]; exit}}' "$plan_tpl") + if [[ -n $variant_line ]]; then + tmp_file=$(mktemp) + sed "s/VARIANT-INJECT/${variant_line//\//\/}/" "$plan_tpl" | sed "/__AGENT__/s//${agent}/g" | sed '/" "$target_file" | cut -d: -f1); manual_end=$(grep -n "" "$target_file" | cut -d: -f1); if [ -n "$manual_start" ] && [ -n "$manual_end" ]; then sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt; fi; + python3 - "$target_file" <<'EOF' +import re,sys,datetime +target=sys.argv[1] +with open(target) as f: content=f.read() +NEW_LANG="'$NEW_LANG'";NEW_FRAMEWORK="'$NEW_FRAMEWORK'";CURRENT_BRANCH="'$CURRENT_BRANCH'";NEW_DB="'$NEW_DB'";NEW_PROJECT_TYPE="'$NEW_PROJECT_TYPE'" +# Tech section +m=re.search(r'## Active Technologies\n(.*?)\n\n',content, re.DOTALL) +if m: + existing=m.group(1) + additions=[] + if '$NEW_LANG' and '$NEW_LANG' not in existing: additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)") + if '$NEW_DB' and '$NEW_DB' not in existing and '$NEW_DB'!='N/A': additions.append(f"- $NEW_DB ($CURRENT_BRANCH)") + if additions: + new_block=existing+"\n"+"\n".join(additions) + content=content.replace(m.group(0),f"## Active Technologies\n{new_block}\n\n") +# Recent changes +m2=re.search(r'## Recent Changes\n(.*?)(\n\n|$)',content, re.DOTALL) +if m2: + lines=[l for l in m2.group(1).strip().split('\n') if l] + lines.insert(0,f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK") + lines=lines[:3] + content=re.sub(r'## Recent Changes\n.*?(\n\n|$)', '## Recent Changes\n'+"\n".join(lines)+'\n\n', content, flags=re.DOTALL) +content=re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', 'Last updated: '+datetime.datetime.now().strftime('%Y-%m-%d'), content) +open(target+'.tmp','w').write(content) +EOF + mv "$target_file.tmp" "$target_file"; if [ -f /tmp/manual_additions.txt ]; then sed -i.bak '//,//d' "$target_file"; cat /tmp/manual_additions.txt >> "$target_file"; rm /tmp/manual_additions.txt "$target_file.bak"; fi; +fi; mv "$temp_file" "$target_file" 2>/dev/null || true; echo "✅ $agent_name context file updated successfully"; } +case "$AGENT_TYPE" in + claude) update_agent_file "$CLAUDE_FILE" "Claude Code" ;; + gemini) update_agent_file "$GEMINI_FILE" "Gemini CLI" ;; + copilot) update_agent_file "$COPILOT_FILE" "GitHub Copilot" ;; + "") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;; + *) echo "ERROR: Unknown agent type '$AGENT_TYPE'"; exit 1 ;; +esac +echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot]" diff --git a/scripts/check-task-prerequisites.sh b/scripts/check-task-prerequisites.sh deleted file mode 100755 index b692fcd..0000000 --- a/scripts/check-task-prerequisites.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# Check that implementation plan exists and find optional design documents -# Usage: ./check-task-prerequisites.sh [--json] - -set -e - -JSON_MODE=false -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; - esac -done - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Check if feature directory exists -if [[ ! -d "$FEATURE_DIR" ]]; then - echo "ERROR: Feature directory not found: $FEATURE_DIR" - echo "Run /specify first to create the feature structure." - exit 1 -fi - -# Check for implementation plan (required) -if [[ ! -f "$IMPL_PLAN" ]]; then - echo "ERROR: plan.md not found in $FEATURE_DIR" - echo "Run /plan first to create the plan." - exit 1 -fi - -if $JSON_MODE; then - # Build JSON array of available docs that actually exist - docs=() - [[ -f "$RESEARCH" ]] && docs+=("research.md") - [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") - ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/") - [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") - # join array into JSON - json_docs=$(printf '"%s",' "${docs[@]}") - json_docs="[${json_docs%,}]" - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" -else - # List available design documents (optional) - echo "FEATURE_DIR:$FEATURE_DIR" - echo "AVAILABLE_DOCS:" - - # Use common check functions - check_file "$RESEARCH" "research.md" - check_file "$DATA_MODEL" "data-model.md" - check_dir "$CONTRACTS_DIR" "contracts/" - check_file "$QUICKSTART" "quickstart.md" -fi - -# Always succeed - task generation should work with whatever docs are available \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh deleted file mode 100755 index 310ce4e..0000000 --- a/scripts/common.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Common functions and variables for all scripts - -# Get repository root -get_repo_root() { - git rev-parse --show-toplevel -} - -# Get current branch -get_current_branch() { - git rev-parse --abbrev-ref HEAD -} - -# Check if current branch is a feature branch -# Returns 0 if valid, 1 if not -check_feature_branch() { - local branch="$1" - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" - echo "Feature branches should be named like: 001-feature-name" - return 1 - fi - return 0 -} - -# Get feature directory path -get_feature_dir() { - local repo_root="$1" - local branch="$2" - echo "$repo_root/specs/$branch" -} - -# Get all standard paths for a feature -# Usage: eval $(get_feature_paths) -# Sets: REPO_ROOT, CURRENT_BRANCH, FEATURE_DIR, FEATURE_SPEC, IMPL_PLAN, TASKS -get_feature_paths() { - local repo_root=$(get_repo_root) - local current_branch=$(get_current_branch) - local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") - - echo "REPO_ROOT='$repo_root'" - echo "CURRENT_BRANCH='$current_branch'" - echo "FEATURE_DIR='$feature_dir'" - echo "FEATURE_SPEC='$feature_dir/spec.md'" - echo "IMPL_PLAN='$feature_dir/plan.md'" - echo "TASKS='$feature_dir/tasks.md'" - echo "RESEARCH='$feature_dir/research.md'" - echo "DATA_MODEL='$feature_dir/data-model.md'" - echo "QUICKSTART='$feature_dir/quickstart.md'" - echo "CONTRACTS_DIR='$feature_dir/contracts'" -} - -# Check if a file exists and report -check_file() { - local file="$1" - local description="$2" - if [[ -f "$file" ]]; then - echo " ✓ $description" - return 0 - else - echo " ✗ $description" - return 1 - fi -} - -# Check if a directory exists and has files -check_dir() { - local dir="$1" - local description="$2" - if [[ -d "$dir" ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]; then - echo " ✓ $description" - return 0 - else - echo " ✗ $description" - return 1 - fi -} \ No newline at end of file diff --git a/scripts/create-new-feature.sh b/scripts/create-new-feature.sh deleted file mode 100755 index 3cd43b9..0000000 --- a/scripts/create-new-feature.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# Create a new feature with branch, directory structure, and template -# Usage: ./create-new-feature.sh "feature description" -# ./create-new-feature.sh --json "feature description" - -set -e - -JSON_MODE=false - -# Collect non-flag args -ARGS=() -for arg in "$@"; do - case "$arg" in - --json) - JSON_MODE=true - ;; - --help|-h) - echo "Usage: $0 [--json] "; exit 0 ;; - *) - ARGS+=("$arg") ;; - esac -done - -FEATURE_DESCRIPTION="${ARGS[*]}" -if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] " >&2 - exit 1 -fi - -# Get repository root -REPO_ROOT=$(git rev-parse --show-toplevel) -SPECS_DIR="$REPO_ROOT/specs" - -# Create specs directory if it doesn't exist -mkdir -p "$SPECS_DIR" - -# Find the highest numbered feature directory -HIGHEST=0 -if [ -d "$SPECS_DIR" ]; then - for dir in "$SPECS_DIR"/*; do - if [ -d "$dir" ]; then - dirname=$(basename "$dir") - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") - number=$((10#$number)) - if [ "$number" -gt "$HIGHEST" ]; then - HIGHEST=$number - fi - fi - done -fi - -# Generate next feature number with zero padding -NEXT=$((HIGHEST + 1)) -FEATURE_NUM=$(printf "%03d" "$NEXT") - -# Create branch name from description -BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | \ - tr '[:upper:]' '[:lower:]' | \ - sed 's/[^a-z0-9]/-/g' | \ - sed 's/-\+/-/g' | \ - sed 's/^-//' | \ - sed 's/-$//') - -# Extract 2-3 meaningful words -WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') - -# Final branch name -BRANCH_NAME="${FEATURE_NUM}-${WORDS}" - -# Create and switch to new branch -git checkout -b "$BRANCH_NAME" - -# Create feature directory -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" -mkdir -p "$FEATURE_DIR" - -# Copy template if it exists -TEMPLATE="$REPO_ROOT/templates/spec-template.md" -SPEC_FILE="$FEATURE_DIR/spec.md" - -if [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$SPEC_FILE" -else - echo "Warning: Template not found at $TEMPLATE" >&2 - touch "$SPEC_FILE" -fi - -if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' \ - "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" -else - # Output results for the LLM to use (legacy key: value format) - echo "BRANCH_NAME: $BRANCH_NAME" - echo "SPEC_FILE: $SPEC_FILE" - echo "FEATURE_NUM: $FEATURE_NUM" -fi \ No newline at end of file diff --git a/scripts/get-feature-paths.sh b/scripts/get-feature-paths.sh deleted file mode 100755 index b1e1fbc..0000000 --- a/scripts/get-feature-paths.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# Get paths for current feature branch without creating anything -# Used by commands that need to find existing feature files - -set -e - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Output paths (don't create anything) -echo "REPO_ROOT: $REPO_ROOT" -echo "BRANCH: $CURRENT_BRANCH" -echo "FEATURE_DIR: $FEATURE_DIR" -echo "FEATURE_SPEC: $FEATURE_SPEC" -echo "IMPL_PLAN: $IMPL_PLAN" -echo "TASKS: $TASKS" \ No newline at end of file diff --git a/scripts/powershell/check-task-prerequisites.ps1 b/scripts/powershell/check-task-prerequisites.ps1 new file mode 100644 index 0000000..3be870f --- /dev/null +++ b/scripts/powershell/check-task-prerequisites.ps1 @@ -0,0 +1,35 @@ +#!/usr/bin/env pwsh +[CmdletBinding()] +param([switch]$Json) +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot/common.ps1" + +$paths = Get-FeaturePathsEnv +if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } + +if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { + Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" + Write-Output "Run /specify first to create the feature structure." + exit 1 +} +if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { + Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)" + Write-Output "Run /plan first to create the plan." + exit 1 +} + +if ($Json) { + $docs = @() + if (Test-Path $paths.RESEARCH) { $docs += 'research.md' } + if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' } + if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { $docs += 'contracts/' } + if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } + [PSCustomObject]@{ FEATURE_DIR=$paths.FEATURE_DIR; AVAILABLE_DOCS=$docs } | ConvertTo-Json -Compress +} else { + Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)" + Write-Output "AVAILABLE_DOCS:" + Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null + Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null + Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null + Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null +} diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 new file mode 100644 index 0000000..3e04a1e --- /dev/null +++ b/scripts/powershell/common.ps1 @@ -0,0 +1,65 @@ +#!/usr/bin/env pwsh +# Common PowerShell functions analogous to common.sh (moved to powershell/) + +function Get-RepoRoot { + git rev-parse --show-toplevel +} + +function Get-CurrentBranch { + git rev-parse --abbrev-ref HEAD +} + +function Test-FeatureBranch { + param([string]$Branch) + if ($Branch -notmatch '^[0-9]{3}-') { + Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" + Write-Output "Feature branches should be named like: 001-feature-name" + return $false + } + return $true +} + +function Get-FeatureDir { + param([string]$RepoRoot, [string]$Branch) + Join-Path $RepoRoot "specs/$Branch" +} + +function Get-FeaturePathsEnv { + $repoRoot = Get-RepoRoot + $currentBranch = Get-CurrentBranch + $featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch + [PSCustomObject]@{ + REPO_ROOT = $repoRoot + CURRENT_BRANCH = $currentBranch + FEATURE_DIR = $featureDir + FEATURE_SPEC = Join-Path $featureDir 'spec.md' + IMPL_PLAN = Join-Path $featureDir 'plan.md' + TASKS = Join-Path $featureDir 'tasks.md' + RESEARCH = Join-Path $featureDir 'research.md' + DATA_MODEL = Join-Path $featureDir 'data-model.md' + QUICKSTART = Join-Path $featureDir 'quickstart.md' + CONTRACTS_DIR = Join-Path $featureDir 'contracts' + } +} + +function Test-FileExists { + param([string]$Path, [string]$Description) + if (Test-Path -Path $Path -PathType Leaf) { + Write-Output " ✓ $Description" + return $true + } else { + Write-Output " ✗ $Description" + return $false + } +} + +function Test-DirHasFiles { + param([string]$Path, [string]$Description) + if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) { + Write-Output " ✓ $Description" + return $true + } else { + Write-Output " ✗ $Description" + return $false + } +} diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 new file mode 100644 index 0000000..b99f088 --- /dev/null +++ b/scripts/powershell/create-new-feature.ps1 @@ -0,0 +1,52 @@ +#!/usr/bin/env pwsh +# Create a new feature (moved to powershell/) +[CmdletBinding()] +param( + [switch]$Json, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$FeatureDescription +) +$ErrorActionPreference = 'Stop' + +if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { + Write-Error "Usage: ./create-new-feature.ps1 [-Json] "; exit 1 +} +$featureDesc = ($FeatureDescription -join ' ').Trim() + +$repoRoot = git rev-parse --show-toplevel +$specsDir = Join-Path $repoRoot 'specs' +New-Item -ItemType Directory -Path $specsDir -Force | Out-Null + +$highest = 0 +if (Test-Path $specsDir) { + Get-ChildItem -Path $specsDir -Directory | ForEach-Object { + if ($_.Name -match '^(\d{3})') { + $num = [int]$matches[1] + if ($num -gt $highest) { $highest = $num } + } + } +} +$next = $highest + 1 +$featureNum = ('{0:000}' -f $next) + +$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', '' +$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3 +$branchName = "$featureNum-$([string]::Join('-', $words))" + +git checkout -b $branchName | Out-Null + +$featureDir = Join-Path $specsDir $branchName +New-Item -ItemType Directory -Path $featureDir -Force | Out-Null + +$template = Join-Path $repoRoot 'templates/spec-template.md' +$specFile = Join-Path $featureDir 'spec.md' +if (Test-Path $template) { Copy-Item $template $specFile -Force } else { New-Item -ItemType File -Path $specFile | Out-Null } + +if ($Json) { + $obj = [PSCustomObject]@{ BRANCH_NAME = $branchName; SPEC_FILE = $specFile; FEATURE_NUM = $featureNum } + $obj | ConvertTo-Json -Compress +} else { + Write-Output "BRANCH_NAME: $branchName" + Write-Output "SPEC_FILE: $specFile" + Write-Output "FEATURE_NUM: $featureNum" +} diff --git a/scripts/powershell/get-feature-paths.ps1 b/scripts/powershell/get-feature-paths.ps1 new file mode 100644 index 0000000..fc09585 --- /dev/null +++ b/scripts/powershell/get-feature-paths.ps1 @@ -0,0 +1,15 @@ +#!/usr/bin/env pwsh +param() +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot/common.ps1" + +$paths = Get-FeaturePathsEnv +if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } + +Write-Output "REPO_ROOT: $($paths.REPO_ROOT)" +Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" +Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)" +Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" +Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" +Write-Output "TASKS: $($paths.TASKS)" diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 new file mode 100644 index 0000000..b026440 --- /dev/null +++ b/scripts/powershell/setup-plan.ps1 @@ -0,0 +1,21 @@ +#!/usr/bin/env pwsh +[CmdletBinding()] +param([switch]$Json) +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot/common.ps1" + +$paths = Get-FeaturePathsEnv +if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } + +New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null +$template = Join-Path $paths.REPO_ROOT 'templates/plan-template.md' +if (Test-Path $template) { Copy-Item $template $paths.IMPL_PLAN -Force } + +if ($Json) { + [PSCustomObject]@{ FEATURE_SPEC=$paths.FEATURE_SPEC; IMPL_PLAN=$paths.IMPL_PLAN; SPECS_DIR=$paths.FEATURE_DIR; BRANCH=$paths.CURRENT_BRANCH } | ConvertTo-Json -Compress +} else { + Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" + Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" + Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)" + Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" +} diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 new file mode 100644 index 0000000..8792152 --- /dev/null +++ b/scripts/powershell/update-agent-context.ps1 @@ -0,0 +1,91 @@ +#!/usr/bin/env pwsh +[CmdletBinding()] +param([string]$AgentType) +$ErrorActionPreference = 'Stop' + +$repoRoot = git rev-parse --show-toplevel +$currentBranch = git rev-parse --abbrev-ref HEAD +$featureDir = Join-Path $repoRoot "specs/$currentBranch" +$newPlan = Join-Path $featureDir 'plan.md' +if (-not (Test-Path $newPlan)) { Write-Error "ERROR: No plan.md found at $newPlan"; exit 1 } + +$claudeFile = Join-Path $repoRoot 'CLAUDE.md' +$geminiFile = Join-Path $repoRoot 'GEMINI.md' +$copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md' + +Write-Output "=== Updating agent context files for feature $currentBranch ===" + +function Get-PlanValue($pattern) { + if (-not (Test-Path $newPlan)) { return '' } + $line = Select-String -Path $newPlan -Pattern $pattern | Select-Object -First 1 + if ($line) { return ($line.Line -replace "^\*\*$pattern\*\*: ", '') } + return '' +} + +$newLang = Get-PlanValue 'Language/Version' +$newFramework = Get-PlanValue 'Primary Dependencies' +$newTesting = Get-PlanValue 'Testing' +$newDb = Get-PlanValue 'Storage' +$newProjectType = Get-PlanValue 'Project Type' + +function Initialize-AgentFile($targetFile, $agentName) { + if (Test-Path $targetFile) { return } + $template = Join-Path $repoRoot 'templates/agent-file-template.md' + if (-not (Test-Path $template)) { Write-Error "Template not found: $template"; return } + $content = Get-Content $template -Raw + $content = $content.Replace('[PROJECT NAME]', (Split-Path $repoRoot -Leaf)) + $content = $content.Replace('[DATE]', (Get-Date -Format 'yyyy-MM-dd')) + $content = $content.Replace('[EXTRACTED FROM ALL PLAN.MD FILES]', "- $newLang + $newFramework ($currentBranch)") + if ($newProjectType -match 'web') { $structure = "backend/`nfrontend/`ntests/" } else { $structure = "src/`ntests/" } + $content = $content.Replace('[ACTUAL STRUCTURE FROM PLANS]', $structure) + if ($newLang -match 'Python') { $commands = 'cd src && pytest && ruff check .' } + elseif ($newLang -match 'Rust') { $commands = 'cargo test && cargo clippy' } + elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' } + else { $commands = "# Add commands for $newLang" } + $content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands) + $content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "$newLang: Follow standard conventions") + $content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- $currentBranch: Added $newLang + $newFramework") + $content | Set-Content $targetFile -Encoding UTF8 +} + +function Update-AgentFile($targetFile, $agentName) { + if (-not (Test-Path $targetFile)) { Initialize-AgentFile $targetFile $agentName; return } + $content = Get-Content $targetFile -Raw + if ($newLang -and ($content -notmatch [regex]::Escape($newLang))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newLang + $newFramework ($currentBranch)`n" } + if ($newDb -and $newDb -ne 'N/A' -and ($content -notmatch [regex]::Escape($newDb))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newDb ($currentBranch)`n" } + if ($content -match '## Recent Changes\n([\s\S]*?)(\n\n|$)') { + $changesBlock = $matches[1].Trim().Split("`n") + $changesBlock = ,"- $currentBranch: Added $newLang + $newFramework" + $changesBlock + $changesBlock = $changesBlock | Where-Object { $_ } | Select-Object -First 3 + $joined = ($changesBlock -join "`n") + $content = [regex]::Replace($content, '## Recent Changes\n([\s\S]*?)(\n\n|$)', "## Recent Changes`n$joined`n`n") + } + $content = [regex]::Replace($content, 'Last updated: \d{4}-\d{2}-\d{2}', "Last updated: $(Get-Date -Format 'yyyy-MM-dd')") + $content | Set-Content $targetFile -Encoding UTF8 + Write-Output "✅ $agentName context file updated successfully" +} + +switch ($AgentType) { + 'claude' { Update-AgentFile $claudeFile 'Claude Code' } + 'gemini' { Update-AgentFile $geminiFile 'Gemini CLI' } + 'copilot' { Update-AgentFile $copilotFile 'GitHub Copilot' } + '' { + foreach ($pair in @(@{file=$claudeFile; name='Claude Code'}, @{file=$geminiFile; name='Gemini CLI'}, @{file=$copilotFile; name='GitHub Copilot'})) { + if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name } + } + if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile)) { + Write-Output 'No agent context files found. Creating Claude Code context file by default.' + Update-AgentFile $claudeFile 'Claude Code' + } + } + Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, or leave empty for all."; exit 1 } +} + +Write-Output '' +Write-Output 'Summary of changes:' +if ($newLang) { Write-Output "- Added language: $newLang" } +if ($newFramework) { Write-Output "- Added framework: $newFramework" } +if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" } + +Write-Output '' +Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot]' diff --git a/scripts/setup-plan.sh b/scripts/setup-plan.sh deleted file mode 100755 index ea0e023..0000000 --- a/scripts/setup-plan.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# Setup implementation plan structure for current branch -# Returns paths needed for implementation plan generation -# Usage: ./setup-plan.sh [--json] - -set -e - -JSON_MODE=false -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; - esac -done - -# Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths -eval $(get_feature_paths) - -# Check if on feature branch -check_feature_branch "$CURRENT_BRANCH" || exit 1 - -# Create specs directory if it doesn't exist -mkdir -p "$FEATURE_DIR" - -# Copy plan template if it exists -TEMPLATE="$REPO_ROOT/templates/plan-template.md" -if [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$IMPL_PLAN" -fi - -if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" -else - # Output all paths for LLM use - echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" - echo "SPECS_DIR: $FEATURE_DIR" - echo "BRANCH: $CURRENT_BRANCH" -fi \ No newline at end of file diff --git a/scripts/update-agent-context.sh b/scripts/update-agent-context.sh deleted file mode 100755 index 21b77aa..0000000 --- a/scripts/update-agent-context.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env bash -# Incrementally update agent context files based on new feature plan -# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md -# O(1) operation - only reads current context file and new plan.md - -set -e - -REPO_ROOT=$(git rev-parse --show-toplevel) -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH" -NEW_PLAN="$FEATURE_DIR/plan.md" - -# Determine which agent context files to update -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" -GEMINI_FILE="$REPO_ROOT/GEMINI.md" -COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" - -# Allow override via argument -AGENT_TYPE="$1" - -if [ ! -f "$NEW_PLAN" ]; then - echo "ERROR: No plan.md found at $NEW_PLAN" - exit 1 -fi - -echo "=== Updating agent context files for feature $CURRENT_BRANCH ===" - -# Extract tech from new plan -NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "") -NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "") - -# Function to update a single agent context file -update_agent_file() { - local target_file="$1" - local agent_name="$2" - - echo "Updating $agent_name context file: $target_file" - - # Create temp file for new context - local temp_file=$(mktemp) - - # If file doesn't exist, create from template - if [ ! -f "$target_file" ]; then - echo "Creating new $agent_name context file..." - - # Check if this is the SDD repo itself - if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then - cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file" - else - echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md" - return 1 - fi - - # Replace placeholders - sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file" - sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file" - sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file" - - # Add project structure based on type - if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then - sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file" - else - sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file" - fi - - # Add minimal commands - if [[ "$NEW_LANG" == *"Python"* ]]; then - COMMANDS="cd src && pytest && ruff check ." - elif [[ "$NEW_LANG" == *"Rust"* ]]; then - COMMANDS="cargo test && cargo clippy" - elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then - COMMANDS="npm test && npm run lint" - else - COMMANDS="# Add commands for $NEW_LANG" - fi - sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file" - - # Add code style - sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file" - - # Add recent changes - sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file" - - rm "$temp_file.bak" - else - echo "Updating existing $agent_name context file..." - - # Extract manual additions - local manual_start=$(grep -n "" "$target_file" | cut -d: -f1) - local manual_end=$(grep -n "" "$target_file" | cut -d: -f1) - - if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then - sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt - fi - - # Parse existing file and create updated version - python3 - << EOF -import re -import sys -from datetime import datetime - -# Read existing file -with open("$target_file", 'r') as f: - content = f.read() - -# Check if new tech already exists -tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL) -if tech_section: - existing_tech = tech_section.group(1) - - # Add new tech if not already present - new_additions = [] - if "$NEW_LANG" and "$NEW_LANG" not in existing_tech: - new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)") - if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A": - new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)") - - if new_additions: - updated_tech = existing_tech + "\n" + "\n".join(new_additions) - content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n") - -# Update project structure if needed -if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content: - struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL) - if struct_section: - updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI" - content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)', - f'\\1{updated_struct}\\2', content, flags=re.DOTALL) - -# Add new commands if language is new -if "$NEW_LANG" and f"# {NEW_LANG}" not in content: - commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL) - if not commands_section: - commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL) - - if commands_section: - new_commands = commands_section.group(1) - if "Python" in "$NEW_LANG": - new_commands += "\ncd src && pytest && ruff check ." - elif "Rust" in "$NEW_LANG": - new_commands += "\ncargo test && cargo clippy" - elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG": - new_commands += "\nnpm test && npm run lint" - - if "```bash" in content: - content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)', - f'\\1{new_commands}\\2', content, flags=re.DOTALL) - else: - content = re.sub(r'(## Commands\n).*?(\n\n)', - f'\\1{new_commands}\\2', content, flags=re.DOTALL) - -# Update recent changes (keep only last 3) -changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL) -if changes_section: - changes = changes_section.group(1).strip().split('\n') - changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK") - # Keep only last 3 - changes = changes[:3] - content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)', - f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL) - -# Update date -content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', - f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content) - -# Write to temp file -with open("$temp_file", 'w') as f: - f.write(content) -EOF - - # Restore manual additions if they exist - if [ -f /tmp/manual_additions.txt ]; then - # Remove old manual section from temp file - sed -i.bak '//,//d' "$temp_file" - # Append manual additions - cat /tmp/manual_additions.txt >> "$temp_file" - rm /tmp/manual_additions.txt "$temp_file.bak" - fi - fi - - # Move temp file to final location - mv "$temp_file" "$target_file" - echo "✅ $agent_name context file updated successfully" -} - -# Update files based on argument or detect existing files -case "$AGENT_TYPE" in - "claude") - update_agent_file "$CLAUDE_FILE" "Claude Code" - ;; - "gemini") - update_agent_file "$GEMINI_FILE" "Gemini CLI" - ;; - "copilot") - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - ;; - "") - # Update all existing files - [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code" - [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI" - [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot" - - # If no files exist, create based on current directory or ask user - if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then - echo "No agent context files found. Creating Claude Code context file by default." - update_agent_file "$CLAUDE_FILE" "Claude Code" - fi - ;; - *) - echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all." - exit 1 - ;; -esac -echo "" -echo "Summary of changes:" -if [ ! -z "$NEW_LANG" ]; then - echo "- Added language: $NEW_LANG" -fi -if [ ! -z "$NEW_FRAMEWORK" ]; then - echo "- Added framework: $NEW_FRAMEWORK" -fi -if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then - echo "- Added database: $NEW_DB" -fi - -echo "" -echo "Usage: $0 [claude|gemini|copilot]" -echo " - No argument: Update all existing agent context files" -echo " - claude: Update only CLAUDE.md" -echo " - gemini: Update only GEMINI.md" -echo " - copilot: Update only .github/copilot-instructions.md" \ No newline at end of file diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 8c08d61..9f5e5a2 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -58,6 +58,8 @@ AI_CHOICES = { "claude": "Claude Code", "gemini": "Gemini CLI" } +# Add script type choices +SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} # ASCII Art Banner BANNER = """ @@ -400,7 +402,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool: os.chdir(original_cwd) -def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None): +def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None): repo_owner = "github" repo_name = "spec-kit" if client is None: @@ -420,7 +422,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb raise typer.Exit(1) # Find the template asset for the specified AI assistant - pattern = f"spec-kit-template-{ai_assistant}" + pattern = f"spec-kit-template-{ai_assistant}-{script_type}" matching_assets = [ asset for asset in release_data.get("assets", []) if pattern in asset["name"] and asset["name"].endswith(".zip") @@ -492,7 +494,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb return zip_path, metadata -def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path: +def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path: """Download the latest release and extract it to create a new project. Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) """ @@ -505,6 +507,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr zip_path, meta = download_template_from_github( ai_assistant, current_dir, + script_type=script_type, verbose=verbose and tracker is None, show_progress=(tracker is None), client=client @@ -646,60 +649,44 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None: - """Ensure POSIX .sh scripts in the project .specify/scripts directory have execute bits (no-op on Windows).""" + """Ensure POSIX .sh scripts under .specify/scripts (recursively) have execute bits (no-op on Windows).""" if os.name == "nt": return # Windows: skip silently - scripts_dir = project_path / ".specify" / "scripts" - if not scripts_dir.is_dir(): + scripts_root = project_path / ".specify" / "scripts" + if not scripts_root.is_dir(): return failures: list[str] = [] updated = 0 - for script in scripts_dir.glob("*.sh"): + for script in scripts_root.rglob("*.sh"): try: - # Skip symlinks - if script.is_symlink(): + if script.is_symlink() or not script.is_file(): continue - # Must be a regular file - if not script.is_file(): - continue - # Quick shebang check try: with script.open("rb") as f: - first_two = f.read(2) - if first_two != b"#!": - continue + if f.read(2) != b"#!": + continue except Exception: continue - st = script.stat() - mode = st.st_mode - # If already any execute bit set, skip + st = script.stat(); mode = st.st_mode if mode & 0o111: continue - # Only add execute bits that correspond to existing read bits new_mode = mode - if mode & 0o400: # owner read - new_mode |= 0o100 - if mode & 0o040: # group read - new_mode |= 0o010 - if mode & 0o004: # other read - new_mode |= 0o001 - # Fallback: ensure at least owner execute + if mode & 0o400: new_mode |= 0o100 + if mode & 0o040: new_mode |= 0o010 + if mode & 0o004: new_mode |= 0o001 if not (new_mode & 0o100): new_mode |= 0o100 os.chmod(script, new_mode) updated += 1 except Exception as e: - failures.append(f"{script.name}: {e}") + failures.append(f"{script.relative_to(scripts_root)}: {e}") if tracker: detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "") - tracker.add("chmod", "Set script permissions") - if failures: - tracker.error("chmod", detail) - else: - tracker.complete("chmod", detail) + tracker.add("chmod", "Set script permissions recursively") + (tracker.error if failures else tracker.complete)("chmod", detail) else: if updated: - console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]") + console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]") if failures: console.print("[yellow]Some scripts could not be updated:[/yellow]") for f in failures: @@ -710,6 +697,7 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, or copilot"), + script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"), @@ -816,6 +804,24 @@ def init( console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check") raise typer.Exit(1) + # Determine script type (explicit, interactive, or OS default) + if script_type: + if script_type not in SCRIPT_TYPE_CHOICES: + console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}") + raise typer.Exit(1) + selected_script = script_type + else: + # Auto-detect default + default_script = "ps" if os.name == "nt" else "sh" + # Provide interactive selection similar to AI if stdin is a TTY + if sys.stdin.isatty(): + selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script) + else: + selected_script = default_script + + console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}") + console.print(f"[cyan]Selected script type:[/cyan] {selected_script}") + # Download and set up project # New tree-based progress (no emojis); include earlier substeps tracker = StepTracker("Initialize Specify Project") @@ -826,6 +832,8 @@ def init( tracker.complete("precheck", "ok") tracker.add("ai-select", "Select AI assistant") tracker.complete("ai-select", f"{selected_ai}") + tracker.add("script-select", "Select script type") + tracker.complete("script-select", selected_script) for key, label in [ ("fetch", "Fetch latest release"), ("download", "Download template"), @@ -848,7 +856,7 @@ def init( local_ssl_context = ssl_context if verify else False local_client = httpx.Client(verify=local_ssl_context) - download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client) + download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client) # Ensure scripts are executable (POSIX) ensure_executable_scripts(project_path, tracker=tracker) @@ -906,6 +914,7 @@ def init( elif selected_ai == "copilot": steps_lines.append(f"{step_num}. Open in Visual Studio Code and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with GitHub Copilot") + # Removed script variant step (scripts are transparent to users) step_num += 1 steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles") diff --git a/templates/commands/plan.md b/templates/commands/plan.md index c0e4a9e..14604f0 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -1,15 +1,9 @@ ---- -name: plan -description: "Plan how to implement the specified feature. This is the second step in the Spec-Driven Development lifecycle." ---- - -Plan how to implement the specified feature. - -This is the second step in the Spec-Driven Development lifecycle. + + Given the implementation details provided as an argument, do this: -1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. +1. VARIANT-INJECT 2. Read and analyze the feature specification to understand: - The feature requirements and user stories - Functional and non-functional requirements diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 6483977..dbd86d5 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -1,15 +1,9 @@ ---- -name: specify -description: "Start a new feature by creating a specification and feature branch. This is the first step in the Spec-Driven Development lifecycle." ---- - -Start a new feature by creating a specification and feature branch. - -This is the first step in the Spec-Driven Development lifecycle. + + Given the feature description provided as an argument, do this: -1. Run the script `scripts/create-new-feature.sh --json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. +1. VARIANT-INJECT 2. Load `templates/spec-template.md` to understand required sections. 3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. 4. Report completion with branch name, spec file path, and readiness for the next phase. diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 8275679..4677f25 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -1,15 +1,9 @@ ---- -name: tasks -description: "Break down the plan into executable tasks. This is the third step in the Spec-Driven Development lifecycle." ---- - -Break down the plan into executable tasks. - -This is the third step in the Spec-Driven Development lifecycle. + + Given the context provided as an argument, do this: -1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. +1. VARIANT-INJECT 2. Load and analyze available design documents: - Always read plan.md for tech stack and libraries - IF EXISTS: Read data-model.md for entities diff --git a/templates/plan-template.md b/templates/plan-template.md index f28a655..93ab96b 100644 --- a/templates/plan-template.md +++ b/templates/plan-template.md @@ -1,5 +1,8 @@ # Implementation Plan: [FEATURE] + + + **Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` @@ -171,7 +174,7 @@ ios/ or android/ - Quickstart test = story validation steps 5. **Update agent file incrementally** (O(1) operation): - - Run `/scripts/update-agent-context.sh [claude|gemini|copilot]` for your AI assistant + VARIANT-INJECT - If exists: Add only NEW tech from current plan - Preserve manual additions between markers - Update recent changes (keep last 3) From af3cf934e5569f381eb99e8f2768d922d140b1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:39:51 -0700 Subject: [PATCH 02/13] Update __init__.py --- src/specify_cli/__init__.py | 61 +++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 9f5e5a2..f4a786b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -30,7 +30,7 @@ import tempfile import shutil import json from pathlib import Path -from typing import Optional +from typing import Optional, Tuple import typer import httpx @@ -402,7 +402,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool: os.chdir(original_cwd) -def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None): +def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False) -> Tuple[Path, dict]: repo_owner = "github" repo_name = "spec-kit" if client is None: @@ -414,11 +414,19 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri try: response = client.get(api_url, timeout=30, follow_redirects=True) - response.raise_for_status() - release_data = response.json() - except httpx.RequestError as e: - if verbose: - console.print(f"[red]Error fetching release information:[/red] {e}") + status = response.status_code + if status != 200: + msg = f"GitHub API returned {status} for {api_url}" + if debug: + msg += f"\nResponse headers: {response.headers}\nBody (truncated 500): {response.text[:500]}" + raise RuntimeError(msg) + try: + release_data = response.json() + except ValueError as je: + raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}") + except Exception as e: + console.print(f"[red]Error fetching release information[/red]") + console.print(Panel(str(e), title="Fetch Error", border_style="red")) raise typer.Exit(1) # Find the template asset for the specified AI assistant @@ -429,11 +437,9 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri ] if not matching_assets: - if verbose: - console.print(f"[red]Error:[/red] No template found for AI assistant '{ai_assistant}'") - console.print(f"[yellow]Available assets:[/yellow]") - for asset in release_data.get("assets", []): - console.print(f" - {asset['name']}") + console.print(f"[red]No matching release asset found[/red] for pattern: [bold]{pattern}[/bold]") + asset_names = [a.get('name','?') for a in release_data.get('assets', [])] + console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow")) raise typer.Exit(1) # Use the first matching asset @@ -453,8 +459,10 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri console.print(f"[cyan]Downloading template...[/cyan]") try: - with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response: - response.raise_for_status() + with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response: + if response.status_code != 200: + body_sample = response.text[:400] + raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}") total_size = int(response.headers.get('content-length', 0)) with open(zip_path, 'wb') as f: if total_size == 0: @@ -477,11 +485,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri else: for chunk in response.iter_bytes(chunk_size=8192): f.write(chunk) - except httpx.RequestError as e: - if verbose: - console.print(f"[red]Error downloading template:[/red] {e}") + except Exception as e: + console.print(f"[red]Error downloading template[/red]") + detail = str(e) if zip_path.exists(): zip_path.unlink() + console.print(Panel(detail, title="Download Error", border_style="red")) raise typer.Exit(1) if verbose: console.print(f"Downloaded: {filename}") @@ -494,7 +503,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri return zip_path, metadata -def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path: +def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False) -> Path: """Download the latest release and extract it to create a new project. Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) """ @@ -510,7 +519,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ script_type=script_type, verbose=verbose and tracker is None, show_progress=(tracker is None), - client=client + client=client, + debug=debug ) if tracker: tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)") @@ -627,6 +637,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_ else: if verbose: console.print(f"[red]Error extracting template:[/red] {e}") + if debug: + console.print(Panel(str(e), title="Extraction Error", border_style="red")) # Clean up project directory if created and not current directory if not is_current_dir and project_path.exists(): shutil.rmtree(project_path) @@ -702,6 +714,7 @@ def init( no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"), skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"), + debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"), ): """ Initialize a new Specify project from the latest template. @@ -856,7 +869,7 @@ def init( local_ssl_context = ssl_context if verify else False local_client = httpx.Client(verify=local_ssl_context) - download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client) + download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug) # Ensure scripts are executable (POSIX) ensure_executable_scripts(project_path, tracker=tracker) @@ -879,6 +892,14 @@ def init( tracker.complete("final", "project ready") except Exception as e: tracker.error("final", str(e)) + console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red")) + if debug: + env_info = [ + f"Python: {sys.version.split()[0]}", + f"Platform: {sys.platform}", + f"CWD: {Path.cwd()}", + ] + console.print(Panel("\n".join(env_info), title="Debug Environment", border_style="magenta")) if not here and project_path.exists(): shutil.rmtree(project_path) raise typer.Exit(1) From c29e419b4f32a1e2580e0fbc9c98b747d51d89d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:15:41 -0700 Subject: [PATCH 03/13] Update config --- scripts/bash/setup-plan.sh | 2 +- src/specify_cli/__init__.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index 6760923..1da4265 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -7,7 +7,7 @@ source "$SCRIPT_DIR/common.sh" eval $(get_feature_paths) check_feature_branch "$CURRENT_BRANCH" || exit 1 mkdir -p "$FEATURE_DIR" -TEMPLATE="$REPO_ROOT/templates/plan-template.md" +TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" [[ -f "$TEMPLATE" ]] && cp "$TEMPLATE" "$IMPL_PLAN" if $JSON_MODE; then printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f4a786b..b317038 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -894,12 +894,14 @@ def init( tracker.error("final", str(e)) console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red")) if debug: - env_info = [ - f"Python: {sys.version.split()[0]}", - f"Platform: {sys.platform}", - f"CWD: {Path.cwd()}", + _env_pairs = [ + ("Python", sys.version.split()[0]), + ("Platform", sys.platform), + ("CWD", str(Path.cwd())), ] - console.print(Panel("\n".join(env_info), title="Debug Environment", border_style="magenta")) + _label_width = max(len(k) for k, _ in _env_pairs) + env_lines = [f"{k.ljust(_label_width)} → [bright_black]{v}[/bright_black]" for k, v in _env_pairs] + console.print(Panel("\n".join(env_lines), title="Debug Environment", border_style="magenta")) if not here and project_path.exists(): shutil.rmtree(project_path) raise typer.Exit(1) From 0a5b1ac5385d589e0794f9d491a1989c3b429d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:30:15 -0700 Subject: [PATCH 04/13] Fix package logic --- .../scripts/create-release-packages.sh | 81 +++++++++++++++++-- templates/commands/plan.md | 3 + templates/commands/specify.md | 3 + templates/commands/tasks.md | 3 + 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 1a60b8c..0e408d2 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -4,7 +4,14 @@ set -euo pipefail # create-release-packages.sh (workflow-local) # Build Spec Kit template release archives for each supported AI assistant and script type. # Usage: .github/workflows/scripts/create-release-packages.sh -# Version argument should include leading 'v'. +# Version argument should include leading 'v'. +# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built. +# AGENTS : space or comma separated subset of: claude gemini copilot (default: all) +# SCRIPTS : space or comma separated subset of: sh ps (default: both) +# Examples: +# AGENTS=claude SCRIPTS=sh $0 v0.2.0 +# AGENTS="copilot,gemini" $0 v0.2.0 +# SCRIPTS=ps $0 v0.2.0 if [[ $# -ne 1 ]]; then echo "Usage: $0 " >&2 @@ -40,10 +47,26 @@ generate_commands() { mkdir -p "$output_dir" for template in templates/commands/*.md; do [[ -f "$template" ]] || continue - local name description raw_body variant_line injected body + local name description raw_body variant_line injected body file_norm delim_count name=$(basename "$template" .md) - description=$(awk '/^description:/ {gsub(/^description: *"?/, ""); gsub(/"$/, ""); print; exit}' "$template" | tr -d '\r') - raw_body=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template") + # Normalize line endings first (remove CR) for consistent regex matching + file_norm=$(tr -d '\r' < "$template") + # Extract description from frontmatter + description=$(printf '%s\n' "$file_norm" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}') + # Count YAML frontmatter delimiter lines + delim_count=$(printf '%s\n' "$file_norm" | grep -c '^---$' || true) + if [[ $delim_count -ge 2 ]]; then + # Grab everything after the second --- line + raw_body=$(printf '%s\n' "$file_norm" | awk '/^---$/ {if(++c==2){next}; if(c>=2){print}}') + else + # Fallback: no proper frontmatter detected; use entire file content (still allowing variant parsing) + raw_body=$file_norm + fi + # If somehow still empty, fallback once more to whole normalized file + if [[ -z ${raw_body// /} ]]; then + echo "Warning: body extraction empty for $template; using full file" >&2 + raw_body=$file_norm + fi # Find single-line variant comment matching the variant: or variant_line=$(printf '%s\n' "$raw_body" | awk -v sv="$script_variant" '//, m); if (m[1]!="") {print m[1]; exit}}') if [[ -z $variant_line ]]; then @@ -54,6 +77,11 @@ generate_commands() { injected=$(printf '%s\n' "$raw_body" | sed "s/VARIANT-INJECT/${variant_line//\//\/}/") # Remove all single-line variant comments injected=$(printf '%s\n' "$injected" | sed '/ diff --git a/templates/commands/specify.md b/templates/commands/specify.md index dbd86d5..111e278 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -1,3 +1,6 @@ +--- +description: Create or update the feature specification from a natural language feature description. +--- diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 4677f25..3a58489 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -1,3 +1,6 @@ +--- +description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. +--- From ec7d87f1217f279d7b04931f7b0437d2c6e8a304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:45:28 -0700 Subject: [PATCH 05/13] Update packaging --- .../scripts/create-release-packages.sh | 48 ++++++------------- templates/commands/plan.md | 4 +- templates/commands/specify.md | 4 +- templates/commands/tasks.md | 4 +- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 0e408d2..e35448a 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -47,43 +47,22 @@ generate_commands() { mkdir -p "$output_dir" for template in templates/commands/*.md; do [[ -f "$template" ]] || continue - local name description raw_body variant_line injected body file_norm delim_count + local name description file_content variant_line injected body name=$(basename "$template" .md) - # Normalize line endings first (remove CR) for consistent regex matching - file_norm=$(tr -d '\r' < "$template") + # Normalize line endings and work with entire file content + file_content=$(tr -d '\r' < "$template") # Extract description from frontmatter - description=$(printf '%s\n' "$file_norm" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}') - # Count YAML frontmatter delimiter lines - delim_count=$(printf '%s\n' "$file_norm" | grep -c '^---$' || true) - if [[ $delim_count -ge 2 ]]; then - # Grab everything after the second --- line - raw_body=$(printf '%s\n' "$file_norm" | awk '/^---$/ {if(++c==2){next}; if(c>=2){print}}') - else - # Fallback: no proper frontmatter detected; use entire file content (still allowing variant parsing) - raw_body=$file_norm - fi - # If somehow still empty, fallback once more to whole normalized file - if [[ -z ${raw_body// /} ]]; then - echo "Warning: body extraction empty for $template; using full file" >&2 - raw_body=$file_norm - fi - # Find single-line variant comment matching the variant: or - variant_line=$(printf '%s\n' "$raw_body" | awk -v sv="$script_variant" '//, m); if (m[1]!="") {print m[1]; exit}}') + description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}') + # Find variant line content + variant_line=$(printf '%s\n' "$file_content" | grep -E ".*//") if [[ -z $variant_line ]]; then echo "Warning: no variant line found for $script_variant in $template" >&2 variant_line="(Missing variant command for $script_variant)" fi - # Replace the token VARIANT-INJECT with the selected variant line - injected=$(printf '%s\n' "$raw_body" | sed "s/VARIANT-INJECT/${variant_line//\//\/}/") - # Remove all single-line variant comments - injected=$(printf '%s\n' "$injected" | sed '//, m); if(m[1]!=""){print m[1]; exit}}' "$plan_tpl") + plan_norm=$(tr -d '\r' < "$plan_tpl") + variant_line=$(printf '%s\n' "$plan_norm" | grep -E ".*//; s/^[[:space:]]+//; s/[[:space:]]+$//") if [[ -n $variant_line ]]; then tmp_file=$(mktemp) - sed "s/VARIANT-INJECT/${variant_line//\//\/}/" "$plan_tpl" | sed "/__AGENT__/s//${agent}/g" | sed '/ - + + Given the implementation details provided as an argument, do this: diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 111e278..b1d404a 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -1,8 +1,8 @@ --- description: Create or update the feature specification from a natural language feature description. --- - - + + Given the feature description provided as an argument, do this: diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 3a58489..505ea9e 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -1,8 +1,8 @@ --- description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. --- - - + + Given the context provided as an argument, do this: From 117ec67e4711348fa4eaed9c0415771933731853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:05:55 -0700 Subject: [PATCH 06/13] Saner approach to scripts --- .../scripts/create-release-packages.sh | 36 +++++++++++++------ templates/commands/plan.md | 7 ++-- templates/commands/specify.md | 7 ++-- templates/commands/tasks.md | 7 ++-- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index e35448a..988b7f0 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -47,22 +47,36 @@ generate_commands() { mkdir -p "$output_dir" for template in templates/commands/*.md; do [[ -f "$template" ]] || continue - local name description file_content variant_line injected body + local name description script_command body name=$(basename "$template" .md) - # Normalize line endings and work with entire file content + + # Normalize line endings file_content=$(tr -d '\r' < "$template") - # Extract description from frontmatter + + # Extract description and script command from YAML frontmatter description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}') - # Find variant line content - variant_line=$(printf '%s\n' "$file_content" | grep -E ".*//") - if [[ -z $variant_line ]]; then - echo "Warning: no variant line found for $script_variant in $template" >&2 - variant_line="(Missing variant command for $script_variant)" + script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}') + + if [[ -z $script_command ]]; then + echo "Warning: no script command found for $script_variant in $template" >&2 + script_command="(Missing script command for $script_variant)" fi - # Replace VARIANT-INJECT and remove variant comments - body=$(printf '%s\n' "$file_content" | sed "s|VARIANT-INJECT|${variant_line}|" | sed '/ - Given the implementation details provided as an argument, do this: -1. VARIANT-INJECT +1. Run `{SCRIPT}` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. 2. Read and analyze the feature specification to understand: - The feature requirements and user stories - Functional and non-functional requirements diff --git a/templates/commands/specify.md b/templates/commands/specify.md index b1d404a..41b8f6f 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -1,12 +1,13 @@ --- description: Create or update the feature specification from a natural language feature description. +scripts: + sh: scripts/bash/create-new-feature.sh --json "{ARGS}" + ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}" --- - - Given the feature description provided as an argument, do this: -1. VARIANT-INJECT +1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. 2. Load `templates/spec-template.md` to understand required sections. 3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. 4. Report completion with branch name, spec file path, and readiness for the next phase. diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 505ea9e..29b4cd2 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -1,12 +1,13 @@ --- description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. +scripts: + sh: scripts/bash/check-task-prerequisites.sh --json + ps: scripts/powershell/check-task-prerequisites.ps1 -Json --- - - Given the context provided as an argument, do this: -1. VARIANT-INJECT +1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 2. Load and analyze available design documents: - Always read plan.md for tech stack and libraries - IF EXISTS: Read data-model.md for entities From 0ad2f169d2563e0a7124cc6e688d09ed73c82598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:34:13 -0700 Subject: [PATCH 07/13] Support Cursor --- .../scripts/create-release-packages.sh | 5 ++++- README.md | 21 ++++++++++++++----- src/specify_cli/__init__.py | 8 ++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 988b7f0..baa1fd8 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -117,13 +117,16 @@ build_variant() { copilot) mkdir -p "$base_dir/.github/prompts" generate_commands copilot prompt.md "\$ARGUMENTS" "$base_dir/.github/prompts" "$script" ;; + cursor) + mkdir -p "$base_dir/.cursor/commands" + generate_commands cursor md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;; esac ( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . ) echo "Created spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" } # Determine agent list -ALL_AGENTS=(claude gemini copilot) +ALL_AGENTS=(claude gemini copilot cursor) ALL_SCRIPTS=(sh ps) norm_list() { diff --git a/README.md b/README.md index a456a16..6b53eb4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ uvx --from git+https://github.com/github/spec-kit.git specify init ` | Argument | Name for your new project directory (optional if using `--here`) | -| `--ai` | Option | AI assistant to use: `claude`, `gemini`, or `copilot` | +| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, or `cursor` | +| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) | | `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code | | `--no-git` | Flag | Skip git repository initialization | | `--here` | Flag | Initialize project in the current directory instead of creating a new one | | `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) | +| `--debug` | Flag | Enable detailed debug output for troubleshooting | ### Examples @@ -96,12 +98,21 @@ specify init my-project # Initialize with specific AI assistant specify init my-project --ai claude +# Initialize with Cursor IDE support +specify init my-project --ai cursor + +# Initialize with PowerShell scripts (Windows/cross-platform) +specify init my-project --ai copilot --script ps + # Initialize in current directory specify init --here --ai copilot # Skip git initialization specify init my-project --ai gemini --no-git +# Enable debug output for troubleshooting +specify init my-project --ai claude --debug + # Check system requirements specify check ``` @@ -152,7 +163,7 @@ Our research and experimentation focus on: ## 🔧 Prerequisites - **Linux/macOS** (or WSL2 on Windows) -- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) +- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Cursor IDE](https://cursor.sh/) - [uv](https://docs.astral.sh/uv/) for package management - [Python 3.11+](https://www.python.org/downloads/) - [Git](https://git-scm.com/downloads) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index b317038..6bf3ea8 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -56,7 +56,8 @@ client = httpx.Client(verify=ssl_context) AI_CHOICES = { "copilot": "GitHub Copilot", "claude": "Claude Code", - "gemini": "Gemini CLI" + "gemini": "Gemini CLI", + "cursor": "Cursor" } # Add script type choices SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} @@ -708,7 +709,7 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), - ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, or copilot"), + ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, or cursor"), script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), @@ -721,7 +722,7 @@ def init( This command will: 1. Check that required tools are installed (git is optional) - 2. Let you choose your AI assistant (Claude Code, Gemini CLI, or GitHub Copilot) + 2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, or Cursor) 3. Download the appropriate template from GitHub 4. Extract the template to a new project directory or current directory 5. Initialize a fresh git repository (if not --no-git and no existing repo) @@ -732,6 +733,7 @@ def init( specify init my-project --ai claude specify init my-project --ai gemini specify init my-project --ai copilot --no-git + specify init my-project --ai cursor specify init --ignore-agent-tools my-project specify init --here --ai claude specify init --here From a55448057b4089557f20f060fe89998057695518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:39:00 -0700 Subject: [PATCH 08/13] Update release.yml --- .github/workflows/release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7b8f5e..186980b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,7 @@ jobs: cat > release_notes.md << EOF Template release ${{ steps.get_tag.outputs.new_version }} - Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI. + Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, and Cursor IDE. Now includes per-script variants for POSIX shell (sh) and PowerShell (ps). @@ -92,6 +92,8 @@ jobs: - spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip - spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip - spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip + - spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip EOF echo "Generated release notes:" @@ -110,6 +112,8 @@ jobs: spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip \ + spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip \ --title "Spec Kit Templates - $VERSION_NO_V" \ --notes-file release_notes.md env: From 6c83e9ff663a18e815e05a2172e874edf648fa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:39:45 -0700 Subject: [PATCH 09/13] Update wording --- .github/workflows/release.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 186980b..bb29563 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,7 @@ jobs: cat > release_notes.md << EOF Template release ${{ steps.get_tag.outputs.new_version }} - Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, and Cursor IDE. + Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, and Cursor. Now includes per-script variants for POSIX shell (sh) and PowerShell (ps). diff --git a/README.md b/README.md index 6b53eb4..63ebb59 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ specify init my-project # Initialize with specific AI assistant specify init my-project --ai claude -# Initialize with Cursor IDE support +# Initialize with Cursor support specify init my-project --ai cursor # Initialize with PowerShell scripts (Windows/cross-platform) @@ -163,7 +163,7 @@ Our research and experimentation focus on: ## 🔧 Prerequisites - **Linux/macOS** (or WSL2 on Windows) -- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Cursor IDE](https://cursor.sh/) +- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Cursor](https://cursor.sh/) - [uv](https://docs.astral.sh/uv/) for package management - [Python 3.11+](https://www.python.org/downloads/) - [Git](https://git-scm.com/downloads) From 736e28256227a9bd6cff609f02490d09325a120d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:20:20 -0700 Subject: [PATCH 10/13] Update with check changes --- README.md | 2 +- src/specify_cli/__init__.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63ebb59..153d1db 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The `specify` command supports the following options: | Command | Description | |-------------|----------------------------------------------------------------| | `init` | Initialize a new Specify project from the latest template | -| `check` | Check for installed tools (`git`, `claude`, `gemini`) | +| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`) | ### `specify init` Arguments & Options diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 6bf3ea8..d2f29c1 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -812,8 +812,7 @@ def init( if not check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli"): console.print("[red]Error:[/red] Gemini CLI is required for Gemini projects") agent_tool_missing = True - # GitHub Copilot check is not needed as it's typically available in supported IDEs - + if agent_tool_missing: console.print("\n[red]Required AI tool is missing![/red]") console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check") @@ -963,11 +962,18 @@ def check(): tracker.add("git", "Git version control") tracker.add("claude", "Claude Code CLI") tracker.add("gemini", "Gemini CLI") + tracker.add("code", "VS Code (for GitHub Copilot)") + tracker.add("cursor-agent", "Cursor IDE agent (optional)") # Check each tool git_ok = check_tool_for_tracker("git", "https://git-scm.com/downloads", tracker) claude_ok = check_tool_for_tracker("claude", "https://docs.anthropic.com/en/docs/claude-code/setup", tracker) gemini_ok = check_tool_for_tracker("gemini", "https://github.com/google-gemini/gemini-cli", tracker) + # Check for VS Code (code or code-insiders) + code_ok = check_tool_for_tracker("code", "https://code.visualstudio.com/", tracker) + if not code_ok: + code_ok = check_tool_for_tracker("code-insiders", "https://code.visualstudio.com/insiders/", tracker) + cursor_ok = check_tool_for_tracker("cursor-agent", "https://cursor.sh/", tracker) # Render the final tree console.print(tracker.render()) From 6f81f7d6a06da59c37c6bfa732d1b57c799d56a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:39:11 -0700 Subject: [PATCH 11/13] Update create-release-packages.sh --- .../scripts/create-release-packages.sh | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index baa1fd8..3f5e701 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -27,14 +27,6 @@ echo "Building release packages for $NEW_VERSION" rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || true -mkdir -p sdd-package-base -SPEC_DIR="sdd-package-base/.specify" -mkdir -p "$SPEC_DIR" - -[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; } -[[ -d scripts ]] && { cp -r scripts "$SPEC_DIR/"; echo "Copied scripts -> .specify/scripts"; } -[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; } - rewrite_paths() { sed -E \ -e 's@(/?)memory/@.specify/memory/@g' \ @@ -93,7 +85,31 @@ build_variant() { local base_dir="sdd-${agent}-package-${script}" echo "Building $agent ($script) package..." mkdir -p "$base_dir" - cp -r sdd-package-base/. "$base_dir"/ + + # Copy base structure but filter scripts by variant + SPEC_DIR="$base_dir/.specify" + mkdir -p "$SPEC_DIR" + + [[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; } + + # Only copy the relevant script variant directory + if [[ -d scripts ]]; then + mkdir -p "$SPEC_DIR/scripts" + case $script in + sh) + [[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; } + # Copy any script files that aren't in variant-specific directories + find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true + ;; + ps) + [[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; } + # Copy any script files that aren't in variant-specific directories + find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true + ;; + esac + fi + + [[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; } # Inject variant into plan-template.md within .specify/templates if present local plan_tpl="$base_dir/.specify/templates/plan-template.md" if [[ -f "$plan_tpl" ]]; then From 0e6f513c1403cfd09f89970dfea8c64e38b86152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Den=20Delimarsky=20=F0=9F=8C=BA?= <53200638+localden@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:42:45 -0700 Subject: [PATCH 12/13] Update update-agent-context.ps1 --- scripts/powershell/update-agent-context.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 8792152..7ac26a7 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -43,8 +43,8 @@ function Initialize-AgentFile($targetFile, $agentName) { elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' } else { $commands = "# Add commands for $newLang" } $content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands) - $content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "$newLang: Follow standard conventions") - $content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- $currentBranch: Added $newLang + $newFramework") + $content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "${newLang}: Follow standard conventions") + $content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- ${currentBranch}: Added ${newLang} + ${newFramework}") $content | Set-Content $targetFile -Encoding UTF8 } From 0c2b367ba09dac18da4b62c0b8906a459296366c Mon Sep 17 00:00:00 2001 From: Thai Nguyen Hung Date: Sat, 13 Sep 2025 09:28:46 +0700 Subject: [PATCH 13/13] fix(docs): remove redundant white space --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 4729164..34da702 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ Spec-Driven Development **flips the script** on traditional software development - [Installation Guide](installation.md) - [Quick Start Guide](quickstart.md) - - [Local Development](local-development.md) +- [Local Development](local-development.md) ## Core Philosophy