Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10e56aa67c | ||
|
|
8b49d5d0fb | ||
|
|
66688dffa3 | ||
|
|
b18ef208cb | ||
|
|
5828e58f84 | ||
|
|
dd57e9d444 | ||
|
|
558e682865 | ||
|
|
c5e0c1840b | ||
|
|
63bc6b495d | ||
|
|
70b3db27db | ||
|
|
6e94588615 | ||
|
|
ad9c93c13b | ||
|
|
f979b64338 | ||
|
|
b1591282f6 | ||
|
|
856680e3bc | ||
|
|
60b015a094 | ||
|
|
0c2b367ba0 | ||
|
|
6b8b1a8b93 | ||
|
|
0e6f513c14 | ||
|
|
6f81f7d6a0 | ||
|
|
c875bd0f30 | ||
|
|
736e282562 | ||
|
|
542751fcd1 | ||
|
|
6c83e9ff66 | ||
|
|
a55448057b | ||
|
|
88cded5c4d | ||
|
|
0ad2f169d2 | ||
|
|
fa3171ca6e | ||
|
|
117ec67e47 | ||
|
|
5bd7027526 | ||
|
|
ec7d87f121 | ||
|
|
85e5eedef8 | ||
|
|
0a5b1ac538 | ||
|
|
eaf4caa231 | ||
|
|
c29e419b4f | ||
|
|
af3cf934e5 | ||
|
|
5787bb5537 | ||
|
|
0c419e5198 | ||
|
|
d605d1e008 | ||
|
|
57024454bf | ||
|
|
1ae6b55c87 | ||
|
|
bfeb40cebc | ||
|
|
22b7098edb | ||
|
|
38ad8b0bac | ||
|
|
fe4de3ca45 | ||
|
|
73a9af70a4 | ||
|
|
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
|
|
||||||
|
|||||||
200
.github/workflows/scripts/create-release-packages.sh
vendored
200
.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,168 @@ 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")
|
||||||
|
# Extract script command from YAML frontmatter
|
||||||
|
script_command=$(printf '%s\n' "$plan_norm" | awk -v sv="$script" '/^[[:space:]]*'"$script"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script"':[[:space:]]*/, ""); print; exit}')
|
||||||
|
if [[ -n $script_command ]]; then
|
||||||
|
# Always prefix with .specify/ for plan usage
|
||||||
|
script_command=".specify/$script_command"
|
||||||
|
tmp_file=$(mktemp)
|
||||||
|
# Replace {SCRIPT} placeholder with the script command and __AGENT__ with agent name
|
||||||
|
substituted=$(sed "s|{SCRIPT}|${script_command}|g" "$plan_tpl" | tr -d '\r' | sed "s|__AGENT__|${agent}|g")
|
||||||
|
# Strip YAML frontmatter from plan template output (keep body only)
|
||||||
|
stripped=$(printf '%s\n' "$substituted" | awk 'BEGIN{fm=0;dash=0} /^---$/ {dash++; if(dash==1){fm=1; next} else if(dash==2){fm=0; next}} {if(!fm) print}')
|
||||||
|
printf '%s\n' "$stripped" > "$plan_tpl"
|
||||||
|
else
|
||||||
|
echo "Warning: no plan-template script command found for $script in YAML frontmatter" >&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" ;;
|
||||||
|
qwen)
|
||||||
|
mkdir -p "$base_dir/.qwen/commands"
|
||||||
|
generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
|
||||||
|
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
||||||
|
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 qwen)
|
||||||
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
|
|
||||||
|
|||||||
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to the Specify CLI will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.0.5] - 2025-09-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Qwen Code support as additional AI assistant option
|
||||||
|
|
||||||
|
## [0.0.4] - 2025-09-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- SOCKS proxy support for corporate environments via `httpx[socks]` dependency
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
N/A
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
N/A
|
||||||
@@ -11,7 +11,7 @@ These are one time installations required to be able to test your changes locall
|
|||||||
1. Install [Python 3.11+](https://www.python.org/downloads/)
|
1. Install [Python 3.11+](https://www.python.org/downloads/)
|
||||||
1. Install [uv](https://docs.astral.sh/uv/) for package management
|
1. Install [uv](https://docs.astral.sh/uv/) for package management
|
||||||
1. Install [Git](https://git-scm.com/downloads)
|
1. Install [Git](https://git-scm.com/downloads)
|
||||||
1. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) are recommended, but we're working on adding support for other agents as well.
|
1. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Qwen Code](https://github.com/QwenLM/qwen-code). We're working on adding support for other agents as well.
|
||||||
|
|
||||||
## Submitting a pull request
|
## Submitting a pull request
|
||||||
|
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
- [🤔 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)
|
||||||
|
- [📽️ Video Overview](#️-video-overview)
|
||||||
|
- [🔧 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 +46,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 +54,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 +62,68 @@ 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).
|
||||||
|
|
||||||
|
## 📽️ Video Overview
|
||||||
|
|
||||||
|
Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)!
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||||
|
|
||||||
|
## 🔧 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 +170,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), [Cursor](https://cursor.sh/), or [Qwen CLI](https://github.com/QwenLM/qwen-code)
|
||||||
- [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)
|
||||||
@@ -147,11 +207,12 @@ You will be prompted to select the AI agent you are using. You can also proactiv
|
|||||||
specify init <project_name> --ai claude
|
specify init <project_name> --ai claude
|
||||||
specify init <project_name> --ai gemini
|
specify init <project_name> --ai gemini
|
||||||
specify init <project_name> --ai copilot
|
specify init <project_name> --ai copilot
|
||||||
|
specify init <project_name> --ai qwen
|
||||||
# Or in current directory:
|
# Or in current directory:
|
||||||
specify init --here --ai claude
|
specify init --here --ai claude
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI will check if you have Claude Code or Gemini CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
The CLI will check if you have Claude Code, Gemini CLI, or Qwen CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify init <project_name> --ai claude --ignore-agent-tools
|
specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
@@ -214,7 +275,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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Spec-Driven Development **flips the script** on traditional software development
|
|||||||
|
|
||||||
- [Installation Guide](installation.md)
|
- [Installation Guide](installation.md)
|
||||||
- [Quick Start Guide](quickstart.md)
|
- [Quick Start Guide](quickstart.md)
|
||||||
- [Local Development](local-development.md)
|
- [Local Development](local-development.md)
|
||||||
|
|
||||||
## Core Philosophy
|
## Core Philosophy
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
BIN
media/spec-kit-video-header.jpg
Normal file
BIN
media/spec-kit-video-header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.0.3"
|
version = "0.0.5"
|
||||||
description = "Setup tool for Specify spec-driven development projects"
|
description = "Setup tool for Specify spec-driven development projects"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typer",
|
"typer",
|
||||||
"rich",
|
"rich",
|
||||||
"httpx",
|
"httpx[socks]",
|
||||||
"platformdirs",
|
"platformdirs",
|
||||||
"readchar",
|
"readchar",
|
||||||
"truststore>=0.10.4",
|
"truststore>=0.10.4",
|
||||||
|
|||||||
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
|
||||||
62
scripts/bash/update-agent-context.sh
Normal file
62
scripts/bash/update-agent-context.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/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"; CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||||
|
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/.specify/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" ;;
|
||||||
|
cursor) update_agent_file "$CURSOR_FILE" "Cursor IDE" ;;
|
||||||
|
"") [ -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"; \
|
||||||
|
[ -f "$CURSOR_FILE" ] && update_agent_file "$CURSOR_FILE" "Cursor IDE"; \
|
||||||
|
if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ] && [ ! -f "$CURSOR_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
|
||||||
|
*) echo "ERROR: Unknown agent type '$AGENT_TYPE' (expected claude|gemini|copilot|cursor)"; 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|cursor]"
|
||||||
@@ -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)"
|
||||||
|
}
|
||||||
98
scripts/powershell/update-agent-context.ps1
Normal file
98
scripts/powershell/update-agent-context.ps1
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/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'
|
||||||
|
$cursorFile = Join-Path $repoRoot '.cursor/rules/specify-rules.mdc'
|
||||||
|
|
||||||
|
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 '.specify/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' }
|
||||||
|
'cursor' { Update-AgentFile $cursorFile 'Cursor IDE' }
|
||||||
|
'' {
|
||||||
|
foreach ($pair in @(
|
||||||
|
@{file=$claudeFile; name='Claude Code'},
|
||||||
|
@{file=$geminiFile; name='Gemini CLI'},
|
||||||
|
@{file=$copilotFile; name='GitHub Copilot'},
|
||||||
|
@{file=$cursorFile; name='Cursor IDE'}
|
||||||
|
)) {
|
||||||
|
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) -and -not (Test-Path $cursorFile)) {
|
||||||
|
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, cursor 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|cursor]'
|
||||||
@@ -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,15 @@ 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",
|
||||||
|
"qwen": "Qwen Code"
|
||||||
}
|
}
|
||||||
|
# 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 +342,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 +417,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 +429,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 +474,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 +500,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 +518,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 +531,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 +652,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 +676,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 +723,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, cursor, or qwen"),
|
||||||
|
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, Cursor, or Qwen Code)
|
||||||
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 +747,8 @@ 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 my-project --ai qwen
|
||||||
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 +827,35 @@ 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
|
elif selected_ai == "qwen":
|
||||||
|
if not check_tool("qwen", "Install from: https://github.com/QwenLM/qwen-code"):
|
||||||
|
console.print("[red]Error:[/red] Qwen CLI is required for Qwen Code projects")
|
||||||
|
agent_tool_missing = True
|
||||||
|
# GitHub Copilot and Cursor checks are not needed as they're 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 +866,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 +890,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 +913,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)
|
||||||
@@ -895,7 +957,14 @@ def init(
|
|||||||
steps_lines.append(" - See GEMINI.md for all available commands")
|
steps_lines.append(" - See GEMINI.md for all available commands")
|
||||||
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")
|
||||||
|
elif selected_ai == "qwen":
|
||||||
|
steps_lines.append(f"{step_num}. Use / commands with Qwen CLI")
|
||||||
|
steps_lines.append(" - Run qwen /specify to create specifications")
|
||||||
|
steps_lines.append(" - Run qwen /plan to create implementation plans")
|
||||||
|
steps_lines.append(" - Run qwen /tasks to generate tasks")
|
||||||
|
steps_lines.append(" - See QWEN.md for all available commands")
|
||||||
|
|
||||||
|
# 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 +975,45 @@ 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("qwen", "Qwen Code 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)
|
||||||
|
qwen_ok = check_tool_for_tracker("qwen", "https://github.com/QwenLM/qwen-code", 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 or qwen_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
|
||||||
@@ -21,7 +19,7 @@ Given the implementation details provided as an argument, do this:
|
|||||||
4. Execute the implementation plan template:
|
4. Execute the implementation plan template:
|
||||||
- Load `/templates/plan-template.md` (already copied to IMPL_PLAN path)
|
- Load `/templates/plan-template.md` (already copied to IMPL_PLAN path)
|
||||||
- Set Input path to FEATURE_SPEC
|
- Set Input path to FEATURE_SPEC
|
||||||
- Run the Execution Flow (main) function steps 1-10
|
- Run the Execution Flow (main) function steps 1-9
|
||||||
- The template is self-contained and executable
|
- The template is self-contained and executable
|
||||||
- Follow error handling and gate checks as specified
|
- Follow error handling and gate checks as specified
|
||||||
- Let the template guide artifact generation in $SPECS_DIR:
|
- Let the template guide artifact generation in $SPECS_DIR:
|
||||||
|
|||||||
@@ -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,3 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: "Implementation plan template for feature development"
|
||||||
|
scripts:
|
||||||
|
sh: scripts/bash/update-agent-context.sh __AGENT__
|
||||||
|
ps: scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__
|
||||||
|
---
|
||||||
|
|
||||||
# Implementation Plan: [FEATURE]
|
# Implementation Plan: [FEATURE]
|
||||||
|
|
||||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||||
@@ -10,18 +17,19 @@
|
|||||||
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
|
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
|
||||||
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
|
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
|
||||||
→ Set Structure Decision based on project type
|
→ Set Structure Decision based on project type
|
||||||
3. Evaluate Constitution Check section below
|
3. Fill the Constitution Check section based on the content of the constitution document.
|
||||||
|
4. Evaluate Constitution Check section below
|
||||||
→ If violations exist: Document in Complexity Tracking
|
→ If violations exist: Document in Complexity Tracking
|
||||||
→ If no justification possible: ERROR "Simplify approach first"
|
→ If no justification possible: ERROR "Simplify approach first"
|
||||||
→ Update Progress Tracking: Initial Constitution Check
|
→ Update Progress Tracking: Initial Constitution Check
|
||||||
4. Execute Phase 0 → research.md
|
5. Execute Phase 0 → research.md
|
||||||
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
|
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
|
||||||
5. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, or `GEMINI.md` for Gemini CLI).
|
6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, or `QWEN.md` for Qwen Code).
|
||||||
6. Re-evaluate Constitution Check section
|
7. Re-evaluate Constitution Check section
|
||||||
→ If new violations: Refactor design, return to Phase 1
|
→ If new violations: Refactor design, return to Phase 1
|
||||||
→ Update Progress Tracking: Post-Design Constitution Check
|
→ Update Progress Tracking: Post-Design Constitution Check
|
||||||
7. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
|
8. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
|
||||||
8. STOP - Ready for /tasks command
|
9. STOP - Ready for /tasks command
|
||||||
```
|
```
|
||||||
|
|
||||||
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
|
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
|
||||||
@@ -45,35 +53,7 @@
|
|||||||
## Constitution Check
|
## Constitution Check
|
||||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
**Simplicity**:
|
[Gates determined based on constitution file]
|
||||||
- Projects: [#] (max 3 - e.g., api, cli, tests)
|
|
||||||
- Using framework directly? (no wrapper classes)
|
|
||||||
- Single data model? (no DTOs unless serialization differs)
|
|
||||||
- Avoiding patterns? (no Repository/UoW without proven need)
|
|
||||||
|
|
||||||
**Architecture**:
|
|
||||||
- EVERY feature as library? (no direct app code)
|
|
||||||
- Libraries listed: [name + purpose for each]
|
|
||||||
- CLI per library: [commands with --help/--version/--format]
|
|
||||||
- Library docs: llms.txt format planned?
|
|
||||||
|
|
||||||
**Testing (NON-NEGOTIABLE)**:
|
|
||||||
- RED-GREEN-Refactor cycle enforced? (test MUST fail first)
|
|
||||||
- Git commits show tests before implementation?
|
|
||||||
- Order: Contract→Integration→E2E→Unit strictly followed?
|
|
||||||
- Real dependencies used? (actual DBs, not mocks)
|
|
||||||
- Integration tests for: new libraries, contract changes, shared schemas?
|
|
||||||
- FORBIDDEN: Implementation before test, skipping RED phase
|
|
||||||
|
|
||||||
**Observability**:
|
|
||||||
- Structured logging included?
|
|
||||||
- Frontend logs → backend? (unified stream)
|
|
||||||
- Error context sufficient?
|
|
||||||
|
|
||||||
**Versioning**:
|
|
||||||
- Version number assigned? (MAJOR.MINOR.BUILD)
|
|
||||||
- BUILD increments on every change?
|
|
||||||
- Breaking changes handled? (parallel tests, migration plan)
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -171,7 +151,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
|
- Run `{SCRIPT}` for your AI assistant
|
||||||
- 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)
|
||||||
@@ -184,7 +164,7 @@ ios/ or android/
|
|||||||
*This section describes what the /tasks command will do - DO NOT execute during /plan*
|
*This section describes what the /tasks command will do - DO NOT execute during /plan*
|
||||||
|
|
||||||
**Task Generation Strategy**:
|
**Task Generation Strategy**:
|
||||||
- Load `/templates/tasks-template.md` as base
|
- Load `.specify/templates/tasks-template.md` as base
|
||||||
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
|
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
|
||||||
- Each contract → contract test task [P]
|
- Each contract → contract test task [P]
|
||||||
- Each entity → model creation task [P]
|
- Each entity → model creation task [P]
|
||||||
|
|||||||
Reference in New Issue
Block a user