Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b8b1a8b93 | ||
|
|
0e6f513c14 | ||
|
|
6f81f7d6a0 | ||
|
|
c875bd0f30 | ||
|
|
736e282562 | ||
|
|
542751fcd1 | ||
|
|
6c83e9ff66 | ||
|
|
a55448057b | ||
|
|
88cded5c4d | ||
|
|
0ad2f169d2 | ||
|
|
fa3171ca6e | ||
|
|
117ec67e47 | ||
|
|
5bd7027526 | ||
|
|
ec7d87f121 | ||
|
|
85e5eedef8 | ||
|
|
0a5b1ac538 | ||
|
|
eaf4caa231 | ||
|
|
c29e419b4f | ||
|
|
af3cf934e5 | ||
|
|
5787bb5537 | ||
|
|
d605d1e008 | ||
|
|
57024454bf | ||
|
|
1ae6b55c87 | ||
|
|
bfeb40cebc | ||
|
|
22b7098edb | ||
|
|
38ad8b0bac | ||
|
|
24ba30444e | ||
|
|
584175351a |
219
.github/workflows/release.yml
vendored
219
.github/workflows/release.yml
vendored
@@ -13,118 +13,119 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get latest tag
|
||||||
- name: Get latest tag
|
id: get_tag
|
||||||
id: get_tag
|
run: |
|
||||||
run: |
|
# Get the latest tag, or use v0.0.0 if no tags exist
|
||||||
# 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")
|
||||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
|
# Extract version number and increment
|
||||||
# Extract version number and increment
|
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
||||||
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
||||||
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
MAJOR=${VERSION_PARTS[0]:-0}
|
||||||
MAJOR=${VERSION_PARTS[0]:-0}
|
MINOR=${VERSION_PARTS[1]:-0}
|
||||||
MINOR=${VERSION_PARTS[1]:-0}
|
PATCH=${VERSION_PARTS[2]:-0}
|
||||||
PATCH=${VERSION_PARTS[2]:-0}
|
|
||||||
|
# Increment patch version
|
||||||
# Increment patch version
|
PATCH=$((PATCH + 1))
|
||||||
PATCH=$((PATCH + 1))
|
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
|
||||||
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
|
|
||||||
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "New version will be: $NEW_VERSION"
|
||||||
echo "New version will be: $NEW_VERSION"
|
- name: Check if release already exists
|
||||||
|
id: check_release
|
||||||
- name: Check if release already exists
|
run: |
|
||||||
id: check_release
|
if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then
|
||||||
run: |
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then
|
echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..."
|
||||||
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)
|
|
||||||
else
|
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, Gemini CLI, and Cursor.
|
||||||
|
|
||||||
|
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
|
||||||
|
- 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:"
|
||||||
|
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 \
|
||||||
|
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:
|
||||||
|
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
|
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
|
|
||||||
|
|||||||
189
.github/workflows/scripts/create-release-packages.sh
vendored
189
.github/workflows/scripts/create-release-packages.sh
vendored
@@ -2,9 +2,16 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# create-release-packages.sh (workflow-local)
|
# 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>
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||||
# 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
|
if [[ $# -ne 1 ]]; then
|
||||||
echo "Usage: $0 <version-with-v-prefix>" >&2
|
echo "Usage: $0 <version-with-v-prefix>" >&2
|
||||||
@@ -18,18 +25,7 @@ fi
|
|||||||
|
|
||||||
echo "Building release packages for $NEW_VERSION"
|
echo "Building release packages for $NEW_VERSION"
|
||||||
|
|
||||||
rm -rf sdd-package-base sdd-claude-package sdd-gemini-package sdd-copilot-package \
|
rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || true
|
||||||
spec-kit-template-claude-${NEW_VERSION}.zip \
|
|
||||||
spec-kit-template-gemini-${NEW_VERSION}.zip \
|
|
||||||
spec-kit-template-copilot-${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() {
|
rewrite_paths() {
|
||||||
sed -E \
|
sed -E \
|
||||||
@@ -39,54 +35,157 @@ rewrite_paths() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate_commands() {
|
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"
|
mkdir -p "$output_dir"
|
||||||
for template in templates/commands/*.md; do
|
for template in templates/commands/*.md; do
|
||||||
[[ -f "$template" ]] || continue
|
[[ -f "$template" ]] || continue
|
||||||
local name description body
|
local name description script_command body
|
||||||
name=$(basename "$template" .md)
|
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)
|
# Normalize line endings
|
||||||
|
file_content=$(tr -d '\r' < "$template")
|
||||||
|
|
||||||
|
# Extract description and script command from YAML frontmatter
|
||||||
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
||||||
|
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 {SCRIPT} placeholder with the script command
|
||||||
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
||||||
|
|
||||||
|
# Remove the scripts: section from frontmatter while preserving YAML structure
|
||||||
|
body=$(printf '%s\n' "$body" | awk '
|
||||||
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
||||||
|
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
||||||
|
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
||||||
|
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
||||||
|
{ print }
|
||||||
|
')
|
||||||
|
|
||||||
|
# Apply other substitutions
|
||||||
|
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
||||||
|
|
||||||
case $ext in
|
case $ext in
|
||||||
toml)
|
toml)
|
||||||
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;;
|
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;;
|
||||||
md)
|
md)
|
||||||
echo "$body" > "$output_dir/$name.$ext" ;;
|
echo "$body" > "$output_dir/$name.$ext" ;;
|
||||||
prompt.md)
|
prompt.md)
|
||||||
sed "s/{ARGS}/$arg_format/g" "$template" | rewrite_paths > "$output_dir/$name.$ext" ;;
|
echo "$body" > "$output_dir/$name.$ext" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create Claude package
|
build_variant() {
|
||||||
echo "Building Claude package..."
|
local agent=$1 script=$2
|
||||||
mkdir -p sdd-claude-package
|
local base_dir="sdd-${agent}-package-${script}"
|
||||||
cp -r sdd-package-base/. sdd-claude-package/
|
echo "Building $agent ($script) package..."
|
||||||
mkdir -p sdd-claude-package/.claude/commands
|
mkdir -p "$base_dir"
|
||||||
generate_commands claude md "\$ARGUMENTS" sdd-claude-package/.claude/commands
|
|
||||||
echo "Created Claude package"
|
# 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
|
||||||
|
plan_norm=$(tr -d '\r' < "$plan_tpl")
|
||||||
|
variant_line=$(printf '%s\n' "$plan_norm" | grep -E "<!--[[:space:]]*VARIANT:$script" | head -1 | sed -E "s/.*VARIANT:$script[[:space:]]+//; s/-->.*//; s/^[[:space:]]+//; s/[[:space:]]+$//")
|
||||||
|
if [[ -n $variant_line ]]; then
|
||||||
|
tmp_file=$(mktemp)
|
||||||
|
sed "s|VARIANT-INJECT|${variant_line}|" "$plan_tpl" | tr -d '\r' | sed "s|__AGENT__|${agent}|g" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d' > "$tmp_file" && mv "$tmp_file" "$plan_tpl"
|
||||||
|
else
|
||||||
|
echo "Warning: no plan-template variant for $script (pattern not matched)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
case $agent in
|
||||||
|
claude)
|
||||||
|
mkdir -p "$base_dir/.claude/commands"
|
||||||
|
generate_commands claude md "\$ARGUMENTS" "$base_dir/.claude/commands" "$script" ;;
|
||||||
|
gemini)
|
||||||
|
mkdir -p "$base_dir/.gemini/commands"
|
||||||
|
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
|
||||||
|
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
# Create Gemini package
|
# Determine agent list
|
||||||
echo "Building Gemini package..."
|
ALL_AGENTS=(claude gemini copilot cursor)
|
||||||
mkdir -p sdd-gemini-package
|
ALL_SCRIPTS=(sh ps)
|
||||||
cp -r sdd-package-base/. sdd-gemini-package/
|
|
||||||
mkdir -p sdd-gemini-package/.gemini/commands
|
|
||||||
generate_commands gemini toml "{{args}}" sdd-gemini-package/.gemini/commands
|
|
||||||
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md sdd-gemini-package/GEMINI.md
|
|
||||||
echo "Created Gemini package"
|
|
||||||
|
|
||||||
# Create Copilot package
|
norm_list() {
|
||||||
echo "Building Copilot package..."
|
# convert comma+space separated -> space separated unique while preserving order of first occurrence
|
||||||
mkdir -p sdd-copilot-package
|
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}'
|
||||||
cp -r sdd-package-base/. sdd-copilot-package/
|
}
|
||||||
mkdir -p sdd-copilot-package/.github/prompts
|
|
||||||
generate_commands copilot prompt.md "\$ARGUMENTS" sdd-copilot-package/.github/prompts
|
|
||||||
echo "Created Copilot package"
|
|
||||||
|
|
||||||
( cd sdd-claude-package && zip -r ../spec-kit-template-claude-${NEW_VERSION}.zip . )
|
validate_subset() {
|
||||||
( cd sdd-gemini-package && zip -r ../spec-kit-template-gemini-${NEW_VERSION}.zip . )
|
local type=$1; shift; local -n allowed=$1; shift; local items=($@)
|
||||||
( cd sdd-copilot-package && zip -r ../spec-kit-template-copilot-${NEW_VERSION}.zip . )
|
local ok=1
|
||||||
|
for it in "${items[@]}"; do
|
||||||
|
local found=0
|
||||||
|
for a in "${allowed[@]}"; do [[ $it == $a ]] && { found=1; break; }; done
|
||||||
|
if [[ $found -eq 0 ]]; then
|
||||||
|
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $ok
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n ${AGENTS:-} ]]; then
|
||||||
|
AGENT_LIST=($(printf '%s' "$AGENTS" | norm_list))
|
||||||
|
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
|
||||||
|
else
|
||||||
|
AGENT_LIST=(${ALL_AGENTS[@]})
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n ${SCRIPTS:-} ]]; then
|
||||||
|
SCRIPT_LIST=($(printf '%s' "$SCRIPTS" | norm_list))
|
||||||
|
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
|
||||||
|
else
|
||||||
|
SCRIPT_LIST=(${ALL_SCRIPTS[@]})
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Agents: ${AGENT_LIST[*]}"
|
||||||
|
echo "Scripts: ${SCRIPT_LIST[*]}"
|
||||||
|
|
||||||
|
for agent in "${AGENT_LIST[@]}"; do
|
||||||
|
for script in "${SCRIPT_LIST[@]}"; do
|
||||||
|
build_variant "$agent" "$script"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
echo "Archives:"
|
echo "Archives:"
|
||||||
ls -1 spec-kit-template-*-${NEW_VERSION}.zip
|
ls -1 spec-kit-template-*-${NEW_VERSION}.zip
|
||||||
unzip -l spec-kit-template-copilot-${NEW_VERSION}.zip | head -10 || true
|
|
||||||
|
|||||||
62
README.md
62
README.md
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
|
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
|
||||||
- [⚡ Get started](#-get-started)
|
- [⚡ Get started](#-get-started)
|
||||||
|
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
||||||
- [📚 Core philosophy](#-core-philosophy)
|
- [📚 Core philosophy](#-core-philosophy)
|
||||||
- [🌟 Development phases](#-development-phases)
|
- [🌟 Development phases](#-development-phases)
|
||||||
- [🎯 Experimental goals](#-experimental-goals)
|
- [🎯 Experimental goals](#-experimental-goals)
|
||||||
@@ -44,7 +45,7 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
|
|||||||
|
|
||||||
### 2. Create the spec
|
### 2. Create the spec
|
||||||
|
|
||||||
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
|
Use the **`/specify`** command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
|
/specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
|
||||||
@@ -52,7 +53,7 @@ Use the `/specify` command to describe what you want to build. Focus on the **wh
|
|||||||
|
|
||||||
### 3. Create a technical implementation plan
|
### 3. Create a technical implementation plan
|
||||||
|
|
||||||
Use the `/plan` command to provide your tech stack and architecture choices.
|
Use the **`/plan`** command to provide your tech stack and architecture choices.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
|
/plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
|
||||||
@@ -60,10 +61,62 @@ Use the `/plan` command to provide your tech stack and architecture choices.
|
|||||||
|
|
||||||
### 4. Break down and implement
|
### 4. Break down and implement
|
||||||
|
|
||||||
Use `/tasks` to create an actionable task list, then ask your agent to implement the feature.
|
Use **`/tasks`** to create an actionable task list, then ask your agent to implement the feature.
|
||||||
|
|
||||||
For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md).
|
For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md).
|
||||||
|
|
||||||
|
## 🔧 Specify CLI Reference
|
||||||
|
|
||||||
|
The `specify` command supports the following options:
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|-------------|----------------------------------------------------------------|
|
||||||
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`) |
|
||||||
|
|
||||||
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
|
| Argument/Option | Type | Description |
|
||||||
|
|------------------------|----------|------------------------------------------------------------------------------|
|
||||||
|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`) |
|
||||||
|
| `--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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic project initialization
|
||||||
|
specify init my-project
|
||||||
|
|
||||||
|
# Initialize with specific AI assistant
|
||||||
|
specify init my-project --ai claude
|
||||||
|
|
||||||
|
# Initialize with Cursor 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
|
||||||
|
```
|
||||||
|
|
||||||
## 📚 Core philosophy
|
## 📚 Core philosophy
|
||||||
|
|
||||||
Spec-Driven Development is a structured process that emphasizes:
|
Spec-Driven Development is a structured process that emphasizes:
|
||||||
@@ -110,7 +163,7 @@ Our research and experimentation focus on:
|
|||||||
## 🔧 Prerequisites
|
## 🔧 Prerequisites
|
||||||
|
|
||||||
- **Linux/macOS** (or WSL2 on Windows)
|
- **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](https://cursor.sh/)
|
||||||
- [uv](https://docs.astral.sh/uv/) for package management
|
- [uv](https://docs.astral.sh/uv/) for package management
|
||||||
- [Python 3.11+](https://www.python.org/downloads/)
|
- [Python 3.11+](https://www.python.org/downloads/)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
@@ -214,7 +267,6 @@ At this stage, your project folder contents should resemble the following:
|
|||||||
│ └── 001-create-taskify
|
│ └── 001-create-taskify
|
||||||
│ └── spec.md
|
│ └── spec.md
|
||||||
└── templates
|
└── templates
|
||||||
├── CLAUDE-template.md
|
|
||||||
├── plan-template.md
|
├── plan-template.md
|
||||||
├── spec-template.md
|
├── spec-template.md
|
||||||
└── tasks-template.md
|
└── tasks-template.md
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Linux/macOS** (or WSL2 on Windows)
|
- **Linux/macOS** (or Windows; PowerShell scripts now supported without WSL)
|
||||||
- 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/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli)
|
||||||
- [uv](https://docs.astral.sh/uv/) for package management
|
- [uv](https://docs.astral.sh/uv/) for package management
|
||||||
- [Python 3.11+](https://www.python.org/downloads/)
|
- [Python 3.11+](https://www.python.org/downloads/)
|
||||||
@@ -34,6 +34,21 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <project_name
|
|||||||
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Specify Script Type (Shell vs PowerShell)
|
||||||
|
|
||||||
|
All automation scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants.
|
||||||
|
|
||||||
|
Auto behavior:
|
||||||
|
- Windows default: `ps`
|
||||||
|
- Other OS default: `sh`
|
||||||
|
- Interactive mode: you'll be prompted unless you pass `--script`
|
||||||
|
|
||||||
|
Force a specific script type:
|
||||||
|
```bash
|
||||||
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script sh
|
||||||
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script ps
|
||||||
|
```
|
||||||
|
|
||||||
### Ignore Agent Tools Check
|
### Ignore Agent Tools Check
|
||||||
|
|
||||||
If you prefer to get the templates without checking for the right tools:
|
If you prefer to get the templates without checking for the right tools:
|
||||||
@@ -49,6 +64,8 @@ After initialization, you should see the following commands available in your AI
|
|||||||
- `/plan` - Generate implementation plans
|
- `/plan` - Generate implementation plans
|
||||||
- `/tasks` - Break down into actionable tasks
|
- `/tasks` - Break down into actionable tasks
|
||||||
|
|
||||||
|
The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Git Credential Manager on Linux
|
### Git Credential Manager on Linux
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
|
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
|
||||||
|
|
||||||
|
> Scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants. The CLI auto-selects based on OS unless you pass `--script sh|ps`.
|
||||||
|
|
||||||
## 1. Clone and Switch Branches
|
## 1. Clone and Switch Branches
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -18,13 +20,13 @@ You can execute the CLI via the module entrypoint without installing anything:
|
|||||||
```bash
|
```bash
|
||||||
# From repo root
|
# From repo root
|
||||||
python -m src.specify_cli --help
|
python -m src.specify_cli --help
|
||||||
python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools
|
python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools --script sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If you prefer invoking the script file style (uses shebang):
|
If you prefer invoking the script file style (uses shebang):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python src/specify_cli/__init__.py init demo-project
|
python src/specify_cli/__init__.py init demo-project --script ps
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Use Editable Install (Isolated Environment)
|
## 3. Use Editable Install (Isolated Environment)
|
||||||
@@ -34,7 +36,7 @@ Create an isolated environment using `uv` so dependencies resolve exactly like e
|
|||||||
```bash
|
```bash
|
||||||
# Create & activate virtual env (uv auto-manages .venv)
|
# Create & activate virtual env (uv auto-manages .venv)
|
||||||
uv venv
|
uv venv
|
||||||
source .venv/bin/activate # or on Windows: .venv\\Scripts\\activate
|
source .venv/bin/activate # or on Windows PowerShell: .venv\Scripts\Activate.ps1
|
||||||
|
|
||||||
# Install project in editable mode
|
# Install project in editable mode
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -50,7 +52,7 @@ Re-running after code edits requires no reinstall because of editable mode.
|
|||||||
`uvx` can run from a local path (or a Git ref) to simulate user flows:
|
`uvx` can run from a local path (or a Git ref) to simulate user flows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools
|
uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools --script sh
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also point uvx at a specific branch without merging:
|
You can also point uvx at a specific branch without merging:
|
||||||
@@ -58,7 +60,7 @@ You can also point uvx at a specific branch without merging:
|
|||||||
```bash
|
```bash
|
||||||
# Push your working branch first
|
# Push your working branch first
|
||||||
git push origin your-feature-branch
|
git push origin your-feature-branch
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@your-feature-branch specify init demo-branch-test
|
uvx --from git+https://github.com/github/spec-kit.git@your-feature-branch specify init demo-branch-test --script ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4a. Absolute Path uvx (Run From Anywhere)
|
### 4a. Absolute Path uvx (Run From Anywhere)
|
||||||
@@ -67,13 +69,13 @@ If you're in another directory, use an absolute path instead of `.`:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from /mnt/c/GitHub/spec-kit specify --help
|
uvx --from /mnt/c/GitHub/spec-kit specify --help
|
||||||
uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --ignore-agent-tools
|
uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --ignore-agent-tools --script sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Set an environment variable for convenience:
|
Set an environment variable for convenience:
|
||||||
```bash
|
```bash
|
||||||
export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit
|
export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit
|
||||||
uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools
|
uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools --script ps
|
||||||
```
|
```
|
||||||
|
|
||||||
(Optional) Define a shell function:
|
(Optional) Define a shell function:
|
||||||
@@ -91,7 +93,7 @@ After running an `init`, check that shell scripts are executable on POSIX system
|
|||||||
ls -l scripts | grep .sh
|
ls -l scripts | grep .sh
|
||||||
# Expect owner execute bit (e.g. -rwxr-xr-x)
|
# Expect owner execute bit (e.g. -rwxr-xr-x)
|
||||||
```
|
```
|
||||||
On Windows this step is a no-op.
|
On Windows you will instead use the `.ps1` scripts (no chmod needed).
|
||||||
|
|
||||||
## 6. Run Lint / Basic Checks (Add Your Own)
|
## 6. Run Lint / Basic Checks (Add Your Own)
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ When testing `init --here` in a dirty directory, create a temp workspace:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir /tmp/spec-test && cd /tmp/spec-test
|
mkdir /tmp/spec-test && cd /tmp/spec-test
|
||||||
python -m src.specify_cli init --here --ai claude --ignore-agent-tools # if repo copied here
|
python -m src.specify_cli init --here --ai claude --ignore-agent-tools --script sh # if repo copied here
|
||||||
```
|
```
|
||||||
Or copy only the modified CLI portion if you want a lighter sandbox.
|
Or copy only the modified CLI portion if you want a lighter sandbox.
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ If you need to bypass TLS validation while experimenting:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify check --skip-tls
|
specify check --skip-tls
|
||||||
specify init demo --skip-tls --ai gemini --ignore-agent-tools
|
specify init demo --skip-tls --ai gemini --ignore-agent-tools --script ps
|
||||||
```
|
```
|
||||||
(Use only for local experimentation.)
|
(Use only for local experimentation.)
|
||||||
|
|
||||||
@@ -153,8 +155,9 @@ rm -rf .venv dist build *.egg-info
|
|||||||
| Symptom | Fix |
|
| Symptom | Fix |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
|
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
|
||||||
| Scripts not executable (Linux) | Re-run init (logic adds bits) or `chmod +x scripts/*.sh` |
|
| Scripts not executable (Linux) | Re-run init or `chmod +x scripts/*.sh` |
|
||||||
| Git step skipped | You passed `--no-git` or Git not installed |
|
| Git step skipped | You passed `--no-git` or Git not installed |
|
||||||
|
| Wrong script type downloaded | Pass `--script sh` or `--script ps` explicitly |
|
||||||
| TLS errors on corporate network | Try `--skip-tls` (not for production) |
|
| TLS errors on corporate network | Try `--skip-tls` (not for production) |
|
||||||
|
|
||||||
## 13. Next Steps
|
## 13. Next Steps
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This guide will help you get started with Spec-Driven Development using Spec Kit.
|
This guide will help you get started with Spec-Driven Development using Spec Kit.
|
||||||
|
|
||||||
|
> NEW: All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
|
||||||
|
|
||||||
## The 4-Step Process
|
## The 4-Step Process
|
||||||
|
|
||||||
### 1. Install Specify
|
### 1. Install Specify
|
||||||
@@ -12,6 +14,12 @@ Initialize your project depending on the coding agent you're using:
|
|||||||
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
|
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pick script type explicitly (optional):
|
||||||
|
```bash
|
||||||
|
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script ps # Force PowerShell
|
||||||
|
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
|
||||||
|
```
|
||||||
|
|
||||||
### 2. Create the Spec
|
### 2. Create the Spec
|
||||||
|
|
||||||
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
|
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
|
||||||
|
|||||||
15
scripts/bash/check-task-prerequisites.sh
Normal file
15
scripts/bash/check-task-prerequisites.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
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
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
eval $(get_feature_paths)
|
||||||
|
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||||
|
if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR"; echo "Run /specify first."; exit 1; fi
|
||||||
|
if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR"; echo "Run /plan first."; exit 1; fi
|
||||||
|
if $JSON_MODE; then
|
||||||
|
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");
|
||||||
|
json_docs=$(printf '"%s",' "${docs[@]}"); json_docs="[${json_docs%,}]"; printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
||||||
|
else
|
||||||
|
echo "FEATURE_DIR:$FEATURE_DIR"; echo "AVAILABLE_DOCS:"; check_file "$RESEARCH" "research.md"; check_file "$DATA_MODEL" "data-model.md"; check_dir "$CONTRACTS_DIR" "contracts/"; check_file "$QUICKSTART" "quickstart.md"; fi
|
||||||
37
scripts/bash/common.sh
Normal file
37
scripts/bash/common.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# (Moved to scripts/bash/) Common functions and variables for all scripts
|
||||||
|
|
||||||
|
get_repo_root() { git rev-parse --show-toplevel; }
|
||||||
|
get_current_branch() { git rev-parse --abbrev-ref HEAD; }
|
||||||
|
|
||||||
|
check_feature_branch() {
|
||||||
|
local branch="$1"
|
||||||
|
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||||
|
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||||
|
echo "Feature branches should be named like: 001-feature-name" >&2
|
||||||
|
return 1
|
||||||
|
fi; return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get_feature_dir() { echo "$1/specs/$2"; }
|
||||||
|
|
||||||
|
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")
|
||||||
|
cat <<EOF
|
||||||
|
REPO_ROOT='$repo_root'
|
||||||
|
CURRENT_BRANCH='$current_branch'
|
||||||
|
FEATURE_DIR='$feature_dir'
|
||||||
|
FEATURE_SPEC='$feature_dir/spec.md'
|
||||||
|
IMPL_PLAN='$feature_dir/plan.md'
|
||||||
|
TASKS='$feature_dir/tasks.md'
|
||||||
|
RESEARCH='$feature_dir/research.md'
|
||||||
|
DATA_MODEL='$feature_dir/data-model.md'
|
||||||
|
QUICKSTART='$feature_dir/quickstart.md'
|
||||||
|
CONTRACTS_DIR='$feature_dir/contracts'
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
|
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
58
scripts/bash/create-new-feature.sh
Normal file
58
scripts/bash/create-new-feature.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# (Moved to scripts/bash/) Create a new feature with branch, directory structure, and template
|
||||||
|
set -e
|
||||||
|
|
||||||
|
JSON_MODE=false
|
||||||
|
ARGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--json) JSON_MODE=true ;;
|
||||||
|
--help|-h) echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
|
||||||
|
*) ARGS+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
|
echo "Usage: $0 [--json] <feature_description>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
|
mkdir -p "$SPECS_DIR"
|
||||||
|
|
||||||
|
HIGHEST=0
|
||||||
|
if [ -d "$SPECS_DIR" ]; then
|
||||||
|
for dir in "$SPECS_DIR"/*; do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
dirname=$(basename "$dir")
|
||||||
|
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||||
|
number=$((10#$number))
|
||||||
|
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEXT=$((HIGHEST + 1))
|
||||||
|
FEATURE_NUM=$(printf "%03d" "$NEXT")
|
||||||
|
|
||||||
|
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
|
||||||
|
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
||||||
|
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
||||||
|
|
||||||
|
git checkout -b "$BRANCH_NAME"
|
||||||
|
|
||||||
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
|
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
|
||||||
|
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||||
|
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else 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
|
||||||
|
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||||
|
echo "SPEC_FILE: $SPEC_FILE"
|
||||||
|
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||||
|
fi
|
||||||
7
scripts/bash/get-feature-paths.sh
Normal file
7
scripts/bash/get-feature-paths.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
eval $(get_feature_paths)
|
||||||
|
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||||
|
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"
|
||||||
17
scripts/bash/setup-plan.sh
Normal file
17
scripts/bash/setup-plan.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
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
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
eval $(get_feature_paths)
|
||||||
|
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||||
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
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' \
|
||||||
|
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
|
||||||
|
else
|
||||||
|
echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "SPECS_DIR: $FEATURE_DIR"; echo "BRANCH: $CURRENT_BRANCH"
|
||||||
|
fi
|
||||||
57
scripts/bash/update-agent-context.sh
Normal file
57
scripts/bash/update-agent-context.sh
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
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"
|
||||||
|
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
||||||
|
AGENT_TYPE="$1"
|
||||||
|
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
|
||||||
|
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||||
|
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_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 "")
|
||||||
|
update_agent_file() { local target_file="$1" agent_name="$2"; echo "Updating $agent_name context file: $target_file"; local temp_file=$(mktemp); if [ ! -f "$target_file" ]; then
|
||||||
|
echo "Creating new $agent_name context file..."; 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"; return 1; fi;
|
||||||
|
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";
|
||||||
|
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;
|
||||||
|
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";
|
||||||
|
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"; 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..."; manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1); manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$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 '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/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]"
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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] <feature_description>"; exit 0 ;;
|
|
||||||
*)
|
|
||||||
ARGS+=("$arg") ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
||||||
echo "Usage: $0 [--json] <feature_description>" >&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
|
|
||||||
@@ -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"
|
|
||||||
35
scripts/powershell/check-task-prerequisites.ps1
Normal file
35
scripts/powershell/check-task-prerequisites.ps1
Normal file
@@ -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
|
||||||
|
}
|
||||||
65
scripts/powershell/common.ps1
Normal file
65
scripts/powershell/common.ps1
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
52
scripts/powershell/create-new-feature.ps1
Normal file
52
scripts/powershell/create-new-feature.ps1
Normal file
@@ -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] <feature description>"; 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"
|
||||||
|
}
|
||||||
15
scripts/powershell/get-feature-paths.ps1
Normal file
15
scripts/powershell/get-feature-paths.ps1
Normal file
@@ -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)"
|
||||||
21
scripts/powershell/setup-plan.ps1
Normal file
21
scripts/powershell/setup-plan.ps1
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
91
scripts/powershell/update-agent-context.ps1
Normal file
91
scripts/powershell/update-agent-context.ps1
Normal file
@@ -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]'
|
||||||
@@ -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
|
|
||||||
@@ -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 "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1)
|
|
||||||
local manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$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 '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/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"
|
|
||||||
@@ -30,7 +30,7 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
import httpx
|
import httpx
|
||||||
@@ -56,8 +56,14 @@ client = httpx.Client(verify=ssl_context)
|
|||||||
AI_CHOICES = {
|
AI_CHOICES = {
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"claude": "Claude Code",
|
"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"}
|
||||||
|
|
||||||
|
# Claude CLI local installation path after migrate-installer
|
||||||
|
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
||||||
|
|
||||||
# ASCII Art Banner
|
# ASCII Art Banner
|
||||||
BANNER = """
|
BANNER = """
|
||||||
@@ -335,8 +341,28 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_tool_for_tracker(tool: str, install_hint: str, tracker: StepTracker) -> bool:
|
||||||
|
"""Check if a tool is installed and update tracker."""
|
||||||
|
if shutil.which(tool):
|
||||||
|
tracker.complete(tool, "available")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
tracker.error(tool, f"not found - {install_hint}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_tool(tool: str, install_hint: str) -> bool:
|
def check_tool(tool: str, install_hint: str) -> bool:
|
||||||
"""Check if a tool is installed."""
|
"""Check if a tool is installed."""
|
||||||
|
|
||||||
|
# Special handling for Claude CLI after `claude migrate-installer`
|
||||||
|
# See: https://github.com/github/spec-kit/issues/123
|
||||||
|
# The migrate-installer command REMOVES the original executable from PATH
|
||||||
|
# and creates an alias at ~/.claude/local/claude instead
|
||||||
|
# This path should be prioritized over other claude executables in PATH
|
||||||
|
if tool == "claude":
|
||||||
|
if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file():
|
||||||
|
return True
|
||||||
|
|
||||||
if shutil.which(tool):
|
if shutil.which(tool):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -390,7 +416,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
|||||||
os.chdir(original_cwd)
|
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, debug: bool = False) -> Tuple[Path, dict]:
|
||||||
repo_owner = "github"
|
repo_owner = "github"
|
||||||
repo_name = "spec-kit"
|
repo_name = "spec-kit"
|
||||||
if client is None:
|
if client is None:
|
||||||
@@ -402,26 +428,32 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = client.get(api_url, timeout=30, follow_redirects=True)
|
response = client.get(api_url, timeout=30, follow_redirects=True)
|
||||||
response.raise_for_status()
|
status = response.status_code
|
||||||
release_data = response.json()
|
if status != 200:
|
||||||
except httpx.RequestError as e:
|
msg = f"GitHub API returned {status} for {api_url}"
|
||||||
if verbose:
|
if debug:
|
||||||
console.print(f"[red]Error fetching release information:[/red] {e}")
|
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)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Find the template asset for the specified AI assistant
|
# 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 = [
|
matching_assets = [
|
||||||
asset for asset in release_data.get("assets", [])
|
asset for asset in release_data.get("assets", [])
|
||||||
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
||||||
]
|
]
|
||||||
|
|
||||||
if not matching_assets:
|
if not matching_assets:
|
||||||
if verbose:
|
console.print(f"[red]No matching release asset found[/red] for pattern: [bold]{pattern}[/bold]")
|
||||||
console.print(f"[red]Error:[/red] No template found for AI assistant '{ai_assistant}'")
|
asset_names = [a.get('name','?') for a in release_data.get('assets', [])]
|
||||||
console.print(f"[yellow]Available assets:[/yellow]")
|
console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
|
||||||
for asset in release_data.get("assets", []):
|
|
||||||
console.print(f" - {asset['name']}")
|
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Use the first matching asset
|
# Use the first matching asset
|
||||||
@@ -441,8 +473,10 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
|||||||
console.print(f"[cyan]Downloading template...[/cyan]")
|
console.print(f"[cyan]Downloading template...[/cyan]")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
|
with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response:
|
||||||
response.raise_for_status()
|
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))
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
with open(zip_path, 'wb') as f:
|
with open(zip_path, 'wb') as f:
|
||||||
if total_size == 0:
|
if total_size == 0:
|
||||||
@@ -465,11 +499,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
|||||||
else:
|
else:
|
||||||
for chunk in response.iter_bytes(chunk_size=8192):
|
for chunk in response.iter_bytes(chunk_size=8192):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
except httpx.RequestError as e:
|
except Exception as e:
|
||||||
if verbose:
|
console.print(f"[red]Error downloading template[/red]")
|
||||||
console.print(f"[red]Error downloading template:[/red] {e}")
|
detail = str(e)
|
||||||
if zip_path.exists():
|
if zip_path.exists():
|
||||||
zip_path.unlink()
|
zip_path.unlink()
|
||||||
|
console.print(Panel(detail, title="Download Error", border_style="red"))
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
if verbose:
|
if verbose:
|
||||||
console.print(f"Downloaded: {filename}")
|
console.print(f"Downloaded: {filename}")
|
||||||
@@ -482,7 +517,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
|||||||
return zip_path, metadata
|
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, debug: bool = False) -> Path:
|
||||||
"""Download the latest release and extract it to create a new project.
|
"""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)
|
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
|
||||||
"""
|
"""
|
||||||
@@ -495,9 +530,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
|
|||||||
zip_path, meta = download_template_from_github(
|
zip_path, meta = download_template_from_github(
|
||||||
ai_assistant,
|
ai_assistant,
|
||||||
current_dir,
|
current_dir,
|
||||||
|
script_type=script_type,
|
||||||
verbose=verbose and tracker is None,
|
verbose=verbose and tracker is None,
|
||||||
show_progress=(tracker is None),
|
show_progress=(tracker is None),
|
||||||
client=client
|
client=client,
|
||||||
|
debug=debug
|
||||||
)
|
)
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
||||||
@@ -614,6 +651,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
|
|||||||
else:
|
else:
|
||||||
if verbose:
|
if verbose:
|
||||||
console.print(f"[red]Error extracting template:[/red] {e}")
|
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
|
# Clean up project directory if created and not current directory
|
||||||
if not is_current_dir and project_path.exists():
|
if not is_current_dir and project_path.exists():
|
||||||
shutil.rmtree(project_path)
|
shutil.rmtree(project_path)
|
||||||
@@ -636,60 +675,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:
|
def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None:
|
||||||
"""Ensure POSIX .sh scripts in the project 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":
|
if os.name == "nt":
|
||||||
return # Windows: skip silently
|
return # Windows: skip silently
|
||||||
scripts_dir = project_path / "scripts"
|
scripts_root = project_path / ".specify" / "scripts"
|
||||||
if not scripts_dir.is_dir():
|
if not scripts_root.is_dir():
|
||||||
return
|
return
|
||||||
failures: list[str] = []
|
failures: list[str] = []
|
||||||
updated = 0
|
updated = 0
|
||||||
for script in scripts_dir.glob("*.sh"):
|
for script in scripts_root.rglob("*.sh"):
|
||||||
try:
|
try:
|
||||||
# Skip symlinks
|
if script.is_symlink() or not script.is_file():
|
||||||
if script.is_symlink():
|
|
||||||
continue
|
continue
|
||||||
# Must be a regular file
|
|
||||||
if not script.is_file():
|
|
||||||
continue
|
|
||||||
# Quick shebang check
|
|
||||||
try:
|
try:
|
||||||
with script.open("rb") as f:
|
with script.open("rb") as f:
|
||||||
first_two = f.read(2)
|
if f.read(2) != b"#!":
|
||||||
if first_two != b"#!":
|
continue
|
||||||
continue
|
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
st = script.stat()
|
st = script.stat(); mode = st.st_mode
|
||||||
mode = st.st_mode
|
|
||||||
# If already any execute bit set, skip
|
|
||||||
if mode & 0o111:
|
if mode & 0o111:
|
||||||
continue
|
continue
|
||||||
# Only add execute bits that correspond to existing read bits
|
|
||||||
new_mode = mode
|
new_mode = mode
|
||||||
if mode & 0o400: # owner read
|
if mode & 0o400: new_mode |= 0o100
|
||||||
new_mode |= 0o100
|
if mode & 0o040: new_mode |= 0o010
|
||||||
if mode & 0o040: # group read
|
if mode & 0o004: new_mode |= 0o001
|
||||||
new_mode |= 0o010
|
|
||||||
if mode & 0o004: # other read
|
|
||||||
new_mode |= 0o001
|
|
||||||
# Fallback: ensure at least owner execute
|
|
||||||
if not (new_mode & 0o100):
|
if not (new_mode & 0o100):
|
||||||
new_mode |= 0o100
|
new_mode |= 0o100
|
||||||
os.chmod(script, new_mode)
|
os.chmod(script, new_mode)
|
||||||
updated += 1
|
updated += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failures.append(f"{script.name}: {e}")
|
failures.append(f"{script.relative_to(scripts_root)}: {e}")
|
||||||
if tracker:
|
if tracker:
|
||||||
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
|
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
|
||||||
tracker.add("chmod", "Set script permissions")
|
tracker.add("chmod", "Set script permissions recursively")
|
||||||
if failures:
|
(tracker.error if failures else tracker.complete)("chmod", detail)
|
||||||
tracker.error("chmod", detail)
|
|
||||||
else:
|
|
||||||
tracker.complete("chmod", detail)
|
|
||||||
else:
|
else:
|
||||||
if updated:
|
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:
|
if failures:
|
||||||
console.print("[yellow]Some scripts could not be updated:[/yellow]")
|
console.print("[yellow]Some scripts could not be updated:[/yellow]")
|
||||||
for f in failures:
|
for f in failures:
|
||||||
@@ -699,18 +722,20 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
|
|||||||
@app.command()
|
@app.command()
|
||||||
def init(
|
def init(
|
||||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
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"),
|
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"),
|
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"),
|
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)"),
|
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.
|
Initialize a new Specify project from the latest template.
|
||||||
|
|
||||||
This command will:
|
This command will:
|
||||||
1. Check that required tools are installed (git is optional)
|
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
|
3. Download the appropriate template from GitHub
|
||||||
4. Extract the template to a new project directory or current directory
|
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)
|
5. Initialize a fresh git repository (if not --no-git and no existing repo)
|
||||||
@@ -721,6 +746,7 @@ def init(
|
|||||||
specify init my-project --ai claude
|
specify init my-project --ai claude
|
||||||
specify init my-project --ai gemini
|
specify init my-project --ai gemini
|
||||||
specify init my-project --ai copilot --no-git
|
specify init my-project --ai copilot --no-git
|
||||||
|
specify init my-project --ai cursor
|
||||||
specify init --ignore-agent-tools my-project
|
specify init --ignore-agent-tools my-project
|
||||||
specify init --here --ai claude
|
specify init --here --ai claude
|
||||||
specify init --here
|
specify init --here
|
||||||
@@ -799,13 +825,30 @@ def init(
|
|||||||
if not check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli"):
|
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")
|
console.print("[red]Error:[/red] Gemini CLI is required for Gemini projects")
|
||||||
agent_tool_missing = True
|
agent_tool_missing = True
|
||||||
# GitHub Copilot check is not needed as it's typically available in supported IDEs
|
|
||||||
|
|
||||||
if agent_tool_missing:
|
if agent_tool_missing:
|
||||||
console.print("\n[red]Required AI tool is missing![/red]")
|
console.print("\n[red]Required AI tool is missing![/red]")
|
||||||
console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check")
|
console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check")
|
||||||
raise typer.Exit(1)
|
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
|
# Download and set up project
|
||||||
# New tree-based progress (no emojis); include earlier substeps
|
# New tree-based progress (no emojis); include earlier substeps
|
||||||
tracker = StepTracker("Initialize Specify Project")
|
tracker = StepTracker("Initialize Specify Project")
|
||||||
@@ -816,6 +859,8 @@ def init(
|
|||||||
tracker.complete("precheck", "ok")
|
tracker.complete("precheck", "ok")
|
||||||
tracker.add("ai-select", "Select AI assistant")
|
tracker.add("ai-select", "Select AI assistant")
|
||||||
tracker.complete("ai-select", f"{selected_ai}")
|
tracker.complete("ai-select", f"{selected_ai}")
|
||||||
|
tracker.add("script-select", "Select script type")
|
||||||
|
tracker.complete("script-select", selected_script)
|
||||||
for key, label in [
|
for key, label in [
|
||||||
("fetch", "Fetch latest release"),
|
("fetch", "Fetch latest release"),
|
||||||
("download", "Download template"),
|
("download", "Download template"),
|
||||||
@@ -838,7 +883,7 @@ def init(
|
|||||||
local_ssl_context = ssl_context if verify else False
|
local_ssl_context = ssl_context if verify else False
|
||||||
local_client = httpx.Client(verify=local_ssl_context)
|
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, debug=debug)
|
||||||
|
|
||||||
# Ensure scripts are executable (POSIX)
|
# Ensure scripts are executable (POSIX)
|
||||||
ensure_executable_scripts(project_path, tracker=tracker)
|
ensure_executable_scripts(project_path, tracker=tracker)
|
||||||
@@ -861,6 +906,16 @@ def init(
|
|||||||
tracker.complete("final", "project ready")
|
tracker.complete("final", "project ready")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tracker.error("final", str(e))
|
tracker.error("final", str(e))
|
||||||
|
console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red"))
|
||||||
|
if debug:
|
||||||
|
_env_pairs = [
|
||||||
|
("Python", sys.version.split()[0]),
|
||||||
|
("Platform", sys.platform),
|
||||||
|
("CWD", str(Path.cwd())),
|
||||||
|
]
|
||||||
|
_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():
|
if not here and project_path.exists():
|
||||||
shutil.rmtree(project_path)
|
shutil.rmtree(project_path)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
@@ -896,6 +951,7 @@ def init(
|
|||||||
elif selected_ai == "copilot":
|
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")
|
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
|
step_num += 1
|
||||||
steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles")
|
steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles")
|
||||||
|
|
||||||
@@ -906,37 +962,43 @@ def init(
|
|||||||
# Removed farewell line per user request
|
# Removed farewell line per user request
|
||||||
|
|
||||||
|
|
||||||
# Add skip_tls option to check
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")):
|
def check():
|
||||||
"""Check that all required tools are installed."""
|
"""Check that all required tools are installed."""
|
||||||
show_banner()
|
show_banner()
|
||||||
console.print("[bold]Checking Specify requirements...[/bold]\n")
|
console.print("[bold]Checking for installed tools...[/bold]\n")
|
||||||
|
|
||||||
# Check if we have internet connectivity by trying to reach GitHub API
|
# Create tracker for checking tools
|
||||||
console.print("[cyan]Checking internet connectivity...[/cyan]")
|
tracker = StepTracker("Check Available Tools")
|
||||||
verify = not skip_tls
|
|
||||||
local_ssl_context = ssl_context if verify else False
|
|
||||||
local_client = httpx.Client(verify=local_ssl_context)
|
|
||||||
try:
|
|
||||||
response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True)
|
|
||||||
console.print("[green]✓[/green] Internet connection available")
|
|
||||||
except httpx.RequestError:
|
|
||||||
console.print("[red]✗[/red] No internet connection - required for downloading templates")
|
|
||||||
console.print("[yellow]Please check your internet connection[/yellow]")
|
|
||||||
|
|
||||||
console.print("\n[cyan]Optional tools:[/cyan]")
|
|
||||||
git_ok = check_tool("git", "https://git-scm.com/downloads")
|
|
||||||
|
|
||||||
console.print("\n[cyan]Optional AI tools:[/cyan]")
|
# Add all tools we want to check
|
||||||
claude_ok = check_tool("claude", "Install from: https://docs.anthropic.com/en/docs/claude-code/setup")
|
tracker.add("git", "Git version control")
|
||||||
gemini_ok = check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli")
|
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)")
|
||||||
|
|
||||||
console.print("\n[green]✓ Specify CLI is ready to use![/green]")
|
# 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())
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
console.print("\n[bold green]Specify CLI is ready to use![/bold green]")
|
||||||
|
|
||||||
|
# Recommendations
|
||||||
if not git_ok:
|
if not git_ok:
|
||||||
console.print("[yellow]Consider installing git for repository management[/yellow]")
|
console.print("[dim]Tip: Install git for repository management[/dim]")
|
||||||
if not (claude_ok or gemini_ok):
|
if not (claude_ok or gemini_ok):
|
||||||
console.print("[yellow]Consider installing an AI assistant for the best experience[/yellow]")
|
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: plan
|
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||||
description: "Plan how to implement the specified feature. This is the second step in the Spec-Driven Development lifecycle."
|
scripts:
|
||||||
|
sh: scripts/bash/setup-plan.sh --json
|
||||||
|
ps: scripts/powershell/setup-plan.ps1 -Json
|
||||||
---
|
---
|
||||||
|
|
||||||
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:
|
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. 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:
|
2. Read and analyze the feature specification to understand:
|
||||||
- The feature requirements and user stories
|
- The feature requirements and user stories
|
||||||
- Functional and non-functional requirements
|
- Functional and non-functional requirements
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: specify
|
description: Create or update the feature specification from a natural language feature description.
|
||||||
description: "Start a new feature by creating a specification and feature branch. This is the first step in the Spec-Driven Development lifecycle."
|
scripts:
|
||||||
|
sh: scripts/bash/create-new-feature.sh --json "{ARGS}"
|
||||||
|
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
||||||
---
|
---
|
||||||
|
|
||||||
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:
|
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. 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.
|
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.
|
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.
|
4. Report completion with branch name, spec file path, and readiness for the next phase.
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: tasks
|
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||||
description: "Break down the plan into executable tasks. This is the third step in the Spec-Driven Development lifecycle."
|
scripts:
|
||||||
|
sh: scripts/bash/check-task-prerequisites.sh --json
|
||||||
|
ps: scripts/powershell/check-task-prerequisites.ps1 -Json
|
||||||
---
|
---
|
||||||
|
|
||||||
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:
|
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. 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:
|
2. Load and analyze available design documents:
|
||||||
- Always read plan.md for tech stack and libraries
|
- Always read plan.md for tech stack and libraries
|
||||||
- IF EXISTS: Read data-model.md for entities
|
- IF EXISTS: Read data-model.md for entities
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Implementation Plan: [FEATURE]
|
# Implementation Plan: [FEATURE]
|
||||||
|
|
||||||
|
<!-- VARIANT:sh - Run `/scripts/bash/update-agent-context.sh __AGENT__` for your AI assistant -->
|
||||||
|
<!-- VARIANT:ps - Run `/scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__` for your AI assistant -->
|
||||||
|
|
||||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||||
|
|
||||||
@@ -171,7 +174,7 @@ ios/ or android/
|
|||||||
- Quickstart test = story validation steps
|
- Quickstart test = story validation steps
|
||||||
|
|
||||||
5. **Update agent file incrementally** (O(1) operation):
|
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
|
- If exists: Add only NEW tech from current plan
|
||||||
- Preserve manual additions between markers
|
- Preserve manual additions between markers
|
||||||
- Update recent changes (keep last 3)
|
- Update recent changes (keep last 3)
|
||||||
|
|||||||
Reference in New Issue
Block a user