Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e5eedef8 | ||
|
|
0a5b1ac538 | ||
|
|
eaf4caa231 | ||
|
|
c29e419b4f | ||
|
|
af3cf934e5 | ||
|
|
5787bb5537 | ||
|
|
d605d1e008 | ||
|
|
57024454bf | ||
|
|
1ae6b55c87 | ||
|
|
bfeb40cebc | ||
|
|
22b7098edb | ||
|
|
38ad8b0bac | ||
|
|
24ba30444e | ||
|
|
584175351a |
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -13,18 +13,15 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest tag
|
||||
id: get_tag
|
||||
run: |
|
||||
@@ -45,7 +42,6 @@ jobs:
|
||||
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "New version will be: $NEW_VERSION"
|
||||
|
||||
- name: Check if release already exists
|
||||
id: check_release
|
||||
run: |
|
||||
@@ -58,13 +54,11 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release package
|
||||
- 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
|
||||
@@ -89,15 +83,19 @@ jobs:
|
||||
|
||||
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
|
||||
Now includes per-script variants for POSIX shell (sh) and PowerShell (ps).
|
||||
|
||||
Download the template for your preferred AI assistant + script type:
|
||||
- spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
- spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
- spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
- spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
- spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
- spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||
EOF
|
||||
|
||||
echo "Generated release notes:"
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.check_release.outputs.exists == 'false'
|
||||
run: |
|
||||
@@ -106,14 +104,16 @@ jobs:
|
||||
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 \
|
||||
spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||
--notes-file release_notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update version in pyproject.toml (for release artifacts only)
|
||||
if: steps.check_release.outputs.exists == 'false'
|
||||
run: |
|
||||
@@ -125,6 +125,3 @@ jobs:
|
||||
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
|
||||
|
||||
160
.github/workflows/scripts/create-release-packages.sh
vendored
160
.github/workflows/scripts/create-release-packages.sh
vendored
@@ -2,9 +2,16 @@
|
||||
set -euo pipefail
|
||||
|
||||
# create-release-packages.sh (workflow-local)
|
||||
# Build Spec Kit template release archives for each supported AI assistant.
|
||||
# Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||
# Version argument should include leading 'v'.
|
||||
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
||||
# AGENTS : space or comma separated subset of: claude gemini copilot (default: all)
|
||||
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
||||
# Examples:
|
||||
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
||||
# AGENTS="copilot,gemini" $0 v0.2.0
|
||||
# SCRIPTS=ps $0 v0.2.0
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version-with-v-prefix>" >&2
|
||||
@@ -18,10 +25,7 @@ fi
|
||||
|
||||
echo "Building release packages for $NEW_VERSION"
|
||||
|
||||
rm -rf sdd-package-base sdd-claude-package sdd-gemini-package sdd-copilot-package \
|
||||
spec-kit-template-claude-${NEW_VERSION}.zip \
|
||||
spec-kit-template-gemini-${NEW_VERSION}.zip \
|
||||
spec-kit-template-copilot-${NEW_VERSION}.zip || true
|
||||
rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || true
|
||||
|
||||
mkdir -p sdd-package-base
|
||||
SPEC_DIR="sdd-package-base/.specify"
|
||||
@@ -29,7 +33,7 @@ mkdir -p "$SPEC_DIR"
|
||||
|
||||
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
||||
[[ -d scripts ]] && { cp -r scripts "$SPEC_DIR/"; echo "Copied scripts -> .specify/scripts"; }
|
||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \;; echo "Copied templates -> .specify/templates"; }
|
||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
||||
|
||||
rewrite_paths() {
|
||||
sed -E \
|
||||
@@ -39,54 +43,136 @@ rewrite_paths() {
|
||||
}
|
||||
|
||||
generate_commands() {
|
||||
local agent=$1 ext=$2 arg_format=$3 output_dir=$4
|
||||
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
|
||||
mkdir -p "$output_dir"
|
||||
for template in templates/commands/*.md; do
|
||||
[[ -f "$template" ]] || continue
|
||||
local name description body
|
||||
local name description raw_body variant_line injected body file_norm delim_count
|
||||
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 first (remove CR) for consistent regex matching
|
||||
file_norm=$(tr -d '\r' < "$template")
|
||||
# Extract description from frontmatter
|
||||
description=$(printf '%s\n' "$file_norm" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
||||
# Count YAML frontmatter delimiter lines
|
||||
delim_count=$(printf '%s\n' "$file_norm" | grep -c '^---$' || true)
|
||||
if [[ $delim_count -ge 2 ]]; then
|
||||
# Grab everything after the second --- line
|
||||
raw_body=$(printf '%s\n' "$file_norm" | awk '/^---$/ {if(++c==2){next}; if(c>=2){print}}')
|
||||
else
|
||||
# Fallback: no proper frontmatter detected; use entire file content (still allowing variant parsing)
|
||||
raw_body=$file_norm
|
||||
fi
|
||||
# If somehow still empty, fallback once more to whole normalized file
|
||||
if [[ -z ${raw_body// /} ]]; then
|
||||
echo "Warning: body extraction empty for $template; using full file" >&2
|
||||
raw_body=$file_norm
|
||||
fi
|
||||
# Find single-line variant comment matching the variant: <!-- VARIANT:sh ... --> or <!-- VARIANT:ps ... -->
|
||||
variant_line=$(printf '%s\n' "$raw_body" | awk -v sv="$script_variant" '/<!--[[:space:]]+VARIANT:'sv'/ {match($0, /VARIANT:'"sv"'[[:space:]]+(.*)-->/, m); if (m[1]!="") {print m[1]; exit}}')
|
||||
if [[ -z $variant_line ]]; then
|
||||
echo "Warning: no variant line found for $script_variant in $template" >&2
|
||||
variant_line="(Missing variant command for $script_variant)"
|
||||
fi
|
||||
# Replace the token VARIANT-INJECT with the selected variant line
|
||||
injected=$(printf '%s\n' "$raw_body" | sed "s/VARIANT-INJECT/${variant_line//\//\/}/")
|
||||
# Remove all single-line variant comments
|
||||
injected=$(printf '%s\n' "$injected" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d')
|
||||
# Guard: if after stripping variant lines and injection the body became empty, restore original (minus variant comments) to avoid empty prompt files
|
||||
if [[ -z ${injected// /} ]]; then
|
||||
echo "Warning: resulting injected body empty for $template; writing unmodified body" >&2
|
||||
injected=$raw_body
|
||||
fi
|
||||
# Apply arg substitution and path rewrite
|
||||
body=$(printf '%s\n' "$injected" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
||||
case $ext in
|
||||
toml)
|
||||
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;;
|
||||
md)
|
||||
echo "$body" > "$output_dir/$name.$ext" ;;
|
||||
prompt.md)
|
||||
sed "s/{ARGS}/$arg_format/g" "$template" | rewrite_paths > "$output_dir/$name.$ext" ;;
|
||||
echo "$body" > "$output_dir/$name.$ext" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Create Claude package
|
||||
echo "Building Claude package..."
|
||||
mkdir -p sdd-claude-package
|
||||
cp -r sdd-package-base/. sdd-claude-package/
|
||||
mkdir -p sdd-claude-package/.claude/commands
|
||||
generate_commands claude md "\$ARGUMENTS" sdd-claude-package/.claude/commands
|
||||
echo "Created Claude package"
|
||||
build_variant() {
|
||||
local agent=$1 script=$2
|
||||
local base_dir="sdd-${agent}-package-${script}"
|
||||
echo "Building $agent ($script) package..."
|
||||
mkdir -p "$base_dir"
|
||||
cp -r sdd-package-base/. "$base_dir"/
|
||||
# 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
|
||||
variant_line=$(awk -v sv="$script" '/<!--[[:space:]]*VARIANT:'"$script"'/ {match($0, /VARIANT:'"$script"'[[:space:]]+(.*)-->/, m); if(m[1]!=""){print m[1]; exit}}' "$plan_tpl")
|
||||
if [[ -n $variant_line ]]; then
|
||||
tmp_file=$(mktemp)
|
||||
sed "s/VARIANT-INJECT/${variant_line//\//\/}/" "$plan_tpl" | sed "/__AGENT__/s//${agent}/g" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d' > "$tmp_file" && mv "$tmp_file" "$plan_tpl"
|
||||
else
|
||||
echo "Warning: no plan-template variant for $script" >&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" ;;
|
||||
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
|
||||
echo "Building Gemini package..."
|
||||
mkdir -p sdd-gemini-package
|
||||
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"
|
||||
# Determine agent list
|
||||
ALL_AGENTS=(claude gemini copilot)
|
||||
ALL_SCRIPTS=(sh ps)
|
||||
|
||||
# Create Copilot package
|
||||
echo "Building Copilot package..."
|
||||
mkdir -p sdd-copilot-package
|
||||
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"
|
||||
norm_list() {
|
||||
# convert comma+space separated -> space separated unique while preserving order of first occurrence
|
||||
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}'
|
||||
}
|
||||
|
||||
( cd sdd-claude-package && zip -r ../spec-kit-template-claude-${NEW_VERSION}.zip . )
|
||||
( cd sdd-gemini-package && zip -r ../spec-kit-template-gemini-${NEW_VERSION}.zip . )
|
||||
( cd sdd-copilot-package && zip -r ../spec-kit-template-copilot-${NEW_VERSION}.zip . )
|
||||
validate_subset() {
|
||||
local type=$1; shift; local -n allowed=$1; shift; local items=($@)
|
||||
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:"
|
||||
ls -1 spec-kit-template-*-${NEW_VERSION}.zip
|
||||
unzip -l spec-kit-template-copilot-${NEW_VERSION}.zip | head -10 || true
|
||||
|
||||
43
README.md
43
README.md
@@ -16,6 +16,7 @@
|
||||
|
||||
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
|
||||
- [⚡ Get started](#-get-started)
|
||||
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
||||
- [📚 Core philosophy](#-core-philosophy)
|
||||
- [🌟 Development phases](#-development-phases)
|
||||
- [🎯 Experimental goals](#-experimental-goals)
|
||||
@@ -64,6 +65,47 @@ Use `/tasks` to create an actionable task list, then ask your agent to implement
|
||||
|
||||
For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md).
|
||||
|
||||
## 🔧 Specify CLI Reference
|
||||
|
||||
The `specify` command supports the following options:
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|-------------|----------------------------------------------------------------|
|
||||
| `init` | Initialize a new Specify project from the latest template |
|
||||
| `check` | Check for installed tools (`git`, `claude`, `gemini`) |
|
||||
|
||||
### `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`, or `copilot` |
|
||||
| `--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) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Basic project initialization
|
||||
specify init my-project
|
||||
|
||||
# Initialize with specific AI assistant
|
||||
specify init my-project --ai claude
|
||||
|
||||
# Initialize in current directory
|
||||
specify init --here --ai copilot
|
||||
|
||||
# Skip git initialization
|
||||
specify init my-project --ai gemini --no-git
|
||||
|
||||
# Check system requirements
|
||||
specify check
|
||||
```
|
||||
|
||||
## 📚 Core philosophy
|
||||
|
||||
Spec-Driven Development is a structured process that emphasizes:
|
||||
@@ -214,7 +256,6 @@ At this stage, your project folder contents should resemble the following:
|
||||
│ └── 001-create-taskify
|
||||
│ └── spec.md
|
||||
└── templates
|
||||
├── CLAUDE-template.md
|
||||
├── plan-template.md
|
||||
├── spec-template.md
|
||||
└── tasks-template.md
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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)
|
||||
- [uv](https://docs.astral.sh/uv/) for package management
|
||||
- [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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
- `/tasks` - Break down into actionable tasks
|
||||
|
||||
The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 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.
|
||||
|
||||
> 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
|
||||
|
||||
```bash
|
||||
@@ -18,13 +20,13 @@ You can execute the CLI via the module entrypoint without installing anything:
|
||||
```bash
|
||||
# From repo root
|
||||
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):
|
||||
|
||||
```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)
|
||||
@@ -34,7 +36,7 @@ Create an isolated environment using `uv` so dependencies resolve exactly like e
|
||||
```bash
|
||||
# Create & activate virtual env (uv auto-manages .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
|
||||
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:
|
||||
|
||||
```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:
|
||||
@@ -58,7 +60,7 @@ You can also point uvx at a specific branch without merging:
|
||||
```bash
|
||||
# Push your working branch first
|
||||
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)
|
||||
@@ -67,13 +69,13 @@ If you're in another directory, use an absolute path instead of `.`:
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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:
|
||||
@@ -91,7 +93,7 @@ After running an `init`, check that shell scripts are executable on POSIX system
|
||||
ls -l scripts | grep .sh
|
||||
# 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)
|
||||
|
||||
@@ -116,7 +118,7 @@ When testing `init --here` in a dirty directory, create a temp workspace:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
@@ -126,7 +128,7 @@ If you need to bypass TLS validation while experimenting:
|
||||
|
||||
```bash
|
||||
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.)
|
||||
|
||||
@@ -153,8 +155,9 @@ rm -rf .venv dist build *.egg-info
|
||||
| Symptom | Fix |
|
||||
|---------|-----|
|
||||
| `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 |
|
||||
| Wrong script type downloaded | Pass `--script sh` or `--script ps` explicitly |
|
||||
| TLS errors on corporate network | Try `--skip-tls` (not for production) |
|
||||
|
||||
## 13. Next Steps
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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
|
||||
|
||||
### 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>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
|
||||
|
||||
15
scripts/bash/check-task-prerequisites.sh
Normal file
15
scripts/bash/check-task-prerequisites.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
JSON_MODE=false
|
||||
for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
eval $(get_feature_paths)
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR"; echo "Run /specify first."; exit 1; fi
|
||||
if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR"; echo "Run /plan first."; exit 1; fi
|
||||
if $JSON_MODE; then
|
||||
docs=(); [[ -f "$RESEARCH" ]] && docs+=("research.md"); [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md"); ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/"); [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md");
|
||||
json_docs=$(printf '"%s",' "${docs[@]}"); json_docs="[${json_docs%,}]"; printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
||||
else
|
||||
echo "FEATURE_DIR:$FEATURE_DIR"; echo "AVAILABLE_DOCS:"; check_file "$RESEARCH" "research.md"; check_file "$DATA_MODEL" "data-model.md"; check_dir "$CONTRACTS_DIR" "contracts/"; check_file "$QUICKSTART" "quickstart.md"; fi
|
||||
37
scripts/bash/common.sh
Normal file
37
scripts/bash/common.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# (Moved to scripts/bash/) Common functions and variables for all scripts
|
||||
|
||||
get_repo_root() { git rev-parse --show-toplevel; }
|
||||
get_current_branch() { git rev-parse --abbrev-ref HEAD; }
|
||||
|
||||
check_feature_branch() {
|
||||
local branch="$1"
|
||||
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||
echo "Feature branches should be named like: 001-feature-name" >&2
|
||||
return 1
|
||||
fi; return 0
|
||||
}
|
||||
|
||||
get_feature_dir() { echo "$1/specs/$2"; }
|
||||
|
||||
get_feature_paths() {
|
||||
local repo_root=$(get_repo_root)
|
||||
local current_branch=$(get_current_branch)
|
||||
local feature_dir=$(get_feature_dir "$repo_root" "$current_branch")
|
||||
cat <<EOF
|
||||
REPO_ROOT='$repo_root'
|
||||
CURRENT_BRANCH='$current_branch'
|
||||
FEATURE_DIR='$feature_dir'
|
||||
FEATURE_SPEC='$feature_dir/spec.md'
|
||||
IMPL_PLAN='$feature_dir/plan.md'
|
||||
TASKS='$feature_dir/tasks.md'
|
||||
RESEARCH='$feature_dir/research.md'
|
||||
DATA_MODEL='$feature_dir/data-model.md'
|
||||
QUICKSTART='$feature_dir/quickstart.md'
|
||||
CONTRACTS_DIR='$feature_dir/contracts'
|
||||
EOF
|
||||
}
|
||||
|
||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
58
scripts/bash/create-new-feature.sh
Normal file
58
scripts/bash/create-new-feature.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# (Moved to scripts/bash/) Create a new feature with branch, directory structure, and template
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) JSON_MODE=true ;;
|
||||
--help|-h) echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
|
||||
*) ARGS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
HIGHEST=0
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
for dir in "$SPECS_DIR"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
|
||||
done
|
||||
fi
|
||||
|
||||
NEXT=$((HIGHEST + 1))
|
||||
FEATURE_NUM=$(printf "%03d" "$NEXT")
|
||||
|
||||
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
|
||||
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
||||
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
||||
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
fi
|
||||
7
scripts/bash/get-feature-paths.sh
Normal file
7
scripts/bash/get-feature-paths.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
eval $(get_feature_paths)
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
echo "REPO_ROOT: $REPO_ROOT"; echo "BRANCH: $CURRENT_BRANCH"; echo "FEATURE_DIR: $FEATURE_DIR"; echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "TASKS: $TASKS"
|
||||
17
scripts/bash/setup-plan.sh
Normal file
17
scripts/bash/setup-plan.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
JSON_MODE=false
|
||||
for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
eval $(get_feature_paths)
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
|
||||
[[ -f "$TEMPLATE" ]] && cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
if $JSON_MODE; then
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
|
||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
|
||||
else
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "SPECS_DIR: $FEATURE_DIR"; echo "BRANCH: $CURRENT_BRANCH"
|
||||
fi
|
||||
57
scripts/bash/update-agent-context.sh
Normal file
57
scripts/bash/update-agent-context.sh
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
|
||||
NEW_PLAN="$FEATURE_DIR/plan.md"
|
||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
||||
AGENT_TYPE="$1"
|
||||
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
|
||||
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||
NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
|
||||
update_agent_file() { local target_file="$1" agent_name="$2"; echo "Updating $agent_name context file: $target_file"; local temp_file=$(mktemp); if [ ! -f "$target_file" ]; then
|
||||
echo "Creating new $agent_name context file..."; if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"; else echo "ERROR: Template not found"; return 1; fi;
|
||||
sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"; sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"; sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file";
|
||||
if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"; else sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"; fi;
|
||||
if [[ "$NEW_LANG" == *"Python"* ]]; then COMMANDS="cd src && pytest && ruff check ."; elif [[ "$NEW_LANG" == *"Rust"* ]]; then COMMANDS="cargo test && cargo clippy"; elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then COMMANDS="npm test && npm run lint"; else COMMANDS="# Add commands for $NEW_LANG"; fi; sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file";
|
||||
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"; sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"; rm "$temp_file.bak";
|
||||
else
|
||||
echo "Updating existing $agent_name context file..."; manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1); manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1); if [ -n "$manual_start" ] && [ -n "$manual_end" ]; then sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt; fi;
|
||||
python3 - "$target_file" <<'EOF'
|
||||
import re,sys,datetime
|
||||
target=sys.argv[1]
|
||||
with open(target) as f: content=f.read()
|
||||
NEW_LANG="'$NEW_LANG'";NEW_FRAMEWORK="'$NEW_FRAMEWORK'";CURRENT_BRANCH="'$CURRENT_BRANCH'";NEW_DB="'$NEW_DB'";NEW_PROJECT_TYPE="'$NEW_PROJECT_TYPE'"
|
||||
# Tech section
|
||||
m=re.search(r'## Active Technologies\n(.*?)\n\n',content, re.DOTALL)
|
||||
if m:
|
||||
existing=m.group(1)
|
||||
additions=[]
|
||||
if '$NEW_LANG' and '$NEW_LANG' not in existing: additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
|
||||
if '$NEW_DB' and '$NEW_DB' not in existing and '$NEW_DB'!='N/A': additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
|
||||
if additions:
|
||||
new_block=existing+"\n"+"\n".join(additions)
|
||||
content=content.replace(m.group(0),f"## Active Technologies\n{new_block}\n\n")
|
||||
# Recent changes
|
||||
m2=re.search(r'## Recent Changes\n(.*?)(\n\n|$)',content, re.DOTALL)
|
||||
if m2:
|
||||
lines=[l for l in m2.group(1).strip().split('\n') if l]
|
||||
lines.insert(0,f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
|
||||
lines=lines[:3]
|
||||
content=re.sub(r'## Recent Changes\n.*?(\n\n|$)', '## Recent Changes\n'+"\n".join(lines)+'\n\n', content, flags=re.DOTALL)
|
||||
content=re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', 'Last updated: '+datetime.datetime.now().strftime('%Y-%m-%d'), content)
|
||||
open(target+'.tmp','w').write(content)
|
||||
EOF
|
||||
mv "$target_file.tmp" "$target_file"; if [ -f /tmp/manual_additions.txt ]; then sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$target_file"; cat /tmp/manual_additions.txt >> "$target_file"; rm /tmp/manual_additions.txt "$target_file.bak"; fi;
|
||||
fi; mv "$temp_file" "$target_file" 2>/dev/null || true; echo "✅ $agent_name context file updated successfully"; }
|
||||
case "$AGENT_TYPE" in
|
||||
claude) update_agent_file "$CLAUDE_FILE" "Claude Code" ;;
|
||||
gemini) update_agent_file "$GEMINI_FILE" "Gemini CLI" ;;
|
||||
copilot) update_agent_file "$COPILOT_FILE" "GitHub Copilot" ;;
|
||||
"") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
|
||||
*) echo "ERROR: Unknown agent type '$AGENT_TYPE'"; exit 1 ;;
|
||||
esac
|
||||
echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot]"
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check that implementation plan exists and find optional design documents
|
||||
# Usage: ./check-task-prerequisites.sh [--json]
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) JSON_MODE=true ;;
|
||||
--help|-h) echo "Usage: $0 [--json]"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if on feature branch
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
|
||||
# Check if feature directory exists
|
||||
if [[ ! -d "$FEATURE_DIR" ]]; then
|
||||
echo "ERROR: Feature directory not found: $FEATURE_DIR"
|
||||
echo "Run /specify first to create the feature structure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for implementation plan (required)
|
||||
if [[ ! -f "$IMPL_PLAN" ]]; then
|
||||
echo "ERROR: plan.md not found in $FEATURE_DIR"
|
||||
echo "Run /plan first to create the plan."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $JSON_MODE; then
|
||||
# Build JSON array of available docs that actually exist
|
||||
docs=()
|
||||
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
||||
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
||||
([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/")
|
||||
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
||||
# join array into JSON
|
||||
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||
json_docs="[${json_docs%,}]"
|
||||
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
||||
else
|
||||
# List available design documents (optional)
|
||||
echo "FEATURE_DIR:$FEATURE_DIR"
|
||||
echo "AVAILABLE_DOCS:"
|
||||
|
||||
# Use common check functions
|
||||
check_file "$RESEARCH" "research.md"
|
||||
check_file "$DATA_MODEL" "data-model.md"
|
||||
check_dir "$CONTRACTS_DIR" "contracts/"
|
||||
check_file "$QUICKSTART" "quickstart.md"
|
||||
fi
|
||||
|
||||
# Always succeed - task generation should work with whatever docs are available
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Common functions and variables for all scripts
|
||||
|
||||
# Get repository root
|
||||
get_repo_root() {
|
||||
git rev-parse --show-toplevel
|
||||
}
|
||||
|
||||
# Get current branch
|
||||
get_current_branch() {
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
}
|
||||
|
||||
# Check if current branch is a feature branch
|
||||
# Returns 0 if valid, 1 if not
|
||||
check_feature_branch() {
|
||||
local branch="$1"
|
||||
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||
echo "ERROR: Not on a feature branch. Current branch: $branch"
|
||||
echo "Feature branches should be named like: 001-feature-name"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get feature directory path
|
||||
get_feature_dir() {
|
||||
local repo_root="$1"
|
||||
local branch="$2"
|
||||
echo "$repo_root/specs/$branch"
|
||||
}
|
||||
|
||||
# Get all standard paths for a feature
|
||||
# Usage: eval $(get_feature_paths)
|
||||
# Sets: REPO_ROOT, CURRENT_BRANCH, FEATURE_DIR, FEATURE_SPEC, IMPL_PLAN, TASKS
|
||||
get_feature_paths() {
|
||||
local repo_root=$(get_repo_root)
|
||||
local current_branch=$(get_current_branch)
|
||||
local feature_dir=$(get_feature_dir "$repo_root" "$current_branch")
|
||||
|
||||
echo "REPO_ROOT='$repo_root'"
|
||||
echo "CURRENT_BRANCH='$current_branch'"
|
||||
echo "FEATURE_DIR='$feature_dir'"
|
||||
echo "FEATURE_SPEC='$feature_dir/spec.md'"
|
||||
echo "IMPL_PLAN='$feature_dir/plan.md'"
|
||||
echo "TASKS='$feature_dir/tasks.md'"
|
||||
echo "RESEARCH='$feature_dir/research.md'"
|
||||
echo "DATA_MODEL='$feature_dir/data-model.md'"
|
||||
echo "QUICKSTART='$feature_dir/quickstart.md'"
|
||||
echo "CONTRACTS_DIR='$feature_dir/contracts'"
|
||||
}
|
||||
|
||||
# Check if a file exists and report
|
||||
check_file() {
|
||||
local file="$1"
|
||||
local description="$2"
|
||||
if [[ -f "$file" ]]; then
|
||||
echo " ✓ $description"
|
||||
return 0
|
||||
else
|
||||
echo " ✗ $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a directory exists and has files
|
||||
check_dir() {
|
||||
local dir="$1"
|
||||
local description="$2"
|
||||
if [[ -d "$dir" ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]; then
|
||||
echo " ✓ $description"
|
||||
return 0
|
||||
else
|
||||
echo " ✗ $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a new feature with branch, directory structure, and template
|
||||
# Usage: ./create-new-feature.sh "feature description"
|
||||
# ./create-new-feature.sh --json "feature description"
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
|
||||
# Collect non-flag args
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
|
||||
*)
|
||||
ARGS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get repository root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
|
||||
# Create specs directory if it doesn't exist
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
# Find the highest numbered feature directory
|
||||
HIGHEST=0
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
for dir in "$SPECS_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$HIGHEST" ]; then
|
||||
HIGHEST=$number
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Generate next feature number with zero padding
|
||||
NEXT=$((HIGHEST + 1))
|
||||
FEATURE_NUM=$(printf "%03d" "$NEXT")
|
||||
|
||||
# Create branch name from description
|
||||
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | \
|
||||
tr '[:upper:]' '[:lower:]' | \
|
||||
sed 's/[^a-z0-9]/-/g' | \
|
||||
sed 's/-\+/-/g' | \
|
||||
sed 's/^-//' | \
|
||||
sed 's/-$//')
|
||||
|
||||
# Extract 2-3 meaningful words
|
||||
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
||||
|
||||
# Final branch name
|
||||
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
||||
|
||||
# Create and switch to new branch
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
# Create feature directory
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy template if it exists
|
||||
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
|
||||
if [ -f "$TEMPLATE" ]; then
|
||||
cp "$TEMPLATE" "$SPEC_FILE"
|
||||
else
|
||||
echo "Warning: Template not found at $TEMPLATE" >&2
|
||||
touch "$SPEC_FILE"
|
||||
fi
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' \
|
||||
"$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||
else
|
||||
# Output results for the LLM to use (legacy key: value format)
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
fi
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Get paths for current feature branch without creating anything
|
||||
# Used by commands that need to find existing feature files
|
||||
|
||||
set -e
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if on feature branch
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
|
||||
# Output paths (don't create anything)
|
||||
echo "REPO_ROOT: $REPO_ROOT"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "FEATURE_DIR: $FEATURE_DIR"
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "TASKS: $TASKS"
|
||||
35
scripts/powershell/check-task-prerequisites.ps1
Normal file
35
scripts/powershell/check-task-prerequisites.ps1
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env pwsh
|
||||
[CmdletBinding()]
|
||||
param([switch]$Json)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. "$PSScriptRoot/common.ps1"
|
||||
|
||||
$paths = Get-FeaturePathsEnv
|
||||
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
|
||||
|
||||
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
|
||||
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
|
||||
Write-Output "Run /specify first to create the feature structure."
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
|
||||
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
|
||||
Write-Output "Run /plan first to create the plan."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Json) {
|
||||
$docs = @()
|
||||
if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
|
||||
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
|
||||
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { $docs += 'contracts/' }
|
||||
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
|
||||
[PSCustomObject]@{ FEATURE_DIR=$paths.FEATURE_DIR; AVAILABLE_DOCS=$docs } | ConvertTo-Json -Compress
|
||||
} else {
|
||||
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
|
||||
Write-Output "AVAILABLE_DOCS:"
|
||||
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
|
||||
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
|
||||
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
|
||||
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
|
||||
}
|
||||
65
scripts/powershell/common.ps1
Normal file
65
scripts/powershell/common.ps1
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Common PowerShell functions analogous to common.sh (moved to powershell/)
|
||||
|
||||
function Get-RepoRoot {
|
||||
git rev-parse --show-toplevel
|
||||
}
|
||||
|
||||
function Get-CurrentBranch {
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
}
|
||||
|
||||
function Test-FeatureBranch {
|
||||
param([string]$Branch)
|
||||
if ($Branch -notmatch '^[0-9]{3}-') {
|
||||
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
||||
Write-Output "Feature branches should be named like: 001-feature-name"
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-FeatureDir {
|
||||
param([string]$RepoRoot, [string]$Branch)
|
||||
Join-Path $RepoRoot "specs/$Branch"
|
||||
}
|
||||
|
||||
function Get-FeaturePathsEnv {
|
||||
$repoRoot = Get-RepoRoot
|
||||
$currentBranch = Get-CurrentBranch
|
||||
$featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch
|
||||
[PSCustomObject]@{
|
||||
REPO_ROOT = $repoRoot
|
||||
CURRENT_BRANCH = $currentBranch
|
||||
FEATURE_DIR = $featureDir
|
||||
FEATURE_SPEC = Join-Path $featureDir 'spec.md'
|
||||
IMPL_PLAN = Join-Path $featureDir 'plan.md'
|
||||
TASKS = Join-Path $featureDir 'tasks.md'
|
||||
RESEARCH = Join-Path $featureDir 'research.md'
|
||||
DATA_MODEL = Join-Path $featureDir 'data-model.md'
|
||||
QUICKSTART = Join-Path $featureDir 'quickstart.md'
|
||||
CONTRACTS_DIR = Join-Path $featureDir 'contracts'
|
||||
}
|
||||
}
|
||||
|
||||
function Test-FileExists {
|
||||
param([string]$Path, [string]$Description)
|
||||
if (Test-Path -Path $Path -PathType Leaf) {
|
||||
Write-Output " ✓ $Description"
|
||||
return $true
|
||||
} else {
|
||||
Write-Output " ✗ $Description"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DirHasFiles {
|
||||
param([string]$Path, [string]$Description)
|
||||
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) {
|
||||
Write-Output " ✓ $Description"
|
||||
return $true
|
||||
} else {
|
||||
Write-Output " ✗ $Description"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
52
scripts/powershell/create-new-feature.ps1
Normal file
52
scripts/powershell/create-new-feature.ps1
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Create a new feature (moved to powershell/)
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Json,
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$FeatureDescription
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
||||
Write-Error "Usage: ./create-new-feature.ps1 [-Json] <feature description>"; exit 1
|
||||
}
|
||||
$featureDesc = ($FeatureDescription -join ' ').Trim()
|
||||
|
||||
$repoRoot = git rev-parse --show-toplevel
|
||||
$specsDir = Join-Path $repoRoot 'specs'
|
||||
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
||||
|
||||
$highest = 0
|
||||
if (Test-Path $specsDir) {
|
||||
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
||||
if ($_.Name -match '^(\d{3})') {
|
||||
$num = [int]$matches[1]
|
||||
if ($num -gt $highest) { $highest = $num }
|
||||
}
|
||||
}
|
||||
}
|
||||
$next = $highest + 1
|
||||
$featureNum = ('{0:000}' -f $next)
|
||||
|
||||
$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||
$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3
|
||||
$branchName = "$featureNum-$([string]::Join('-', $words))"
|
||||
|
||||
git checkout -b $branchName | Out-Null
|
||||
|
||||
$featureDir = Join-Path $specsDir $branchName
|
||||
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
||||
|
||||
$template = Join-Path $repoRoot 'templates/spec-template.md'
|
||||
$specFile = Join-Path $featureDir 'spec.md'
|
||||
if (Test-Path $template) { Copy-Item $template $specFile -Force } else { New-Item -ItemType File -Path $specFile | Out-Null }
|
||||
|
||||
if ($Json) {
|
||||
$obj = [PSCustomObject]@{ BRANCH_NAME = $branchName; SPEC_FILE = $specFile; FEATURE_NUM = $featureNum }
|
||||
$obj | ConvertTo-Json -Compress
|
||||
} else {
|
||||
Write-Output "BRANCH_NAME: $branchName"
|
||||
Write-Output "SPEC_FILE: $specFile"
|
||||
Write-Output "FEATURE_NUM: $featureNum"
|
||||
}
|
||||
15
scripts/powershell/get-feature-paths.ps1
Normal file
15
scripts/powershell/get-feature-paths.ps1
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env pwsh
|
||||
param()
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. "$PSScriptRoot/common.ps1"
|
||||
|
||||
$paths = Get-FeaturePathsEnv
|
||||
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
|
||||
|
||||
Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
|
||||
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
||||
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
|
||||
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
||||
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
|
||||
Write-Output "TASKS: $($paths.TASKS)"
|
||||
21
scripts/powershell/setup-plan.ps1
Normal file
21
scripts/powershell/setup-plan.ps1
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env pwsh
|
||||
[CmdletBinding()]
|
||||
param([switch]$Json)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. "$PSScriptRoot/common.ps1"
|
||||
|
||||
$paths = Get-FeaturePathsEnv
|
||||
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
|
||||
|
||||
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
||||
$template = Join-Path $paths.REPO_ROOT 'templates/plan-template.md'
|
||||
if (Test-Path $template) { Copy-Item $template $paths.IMPL_PLAN -Force }
|
||||
|
||||
if ($Json) {
|
||||
[PSCustomObject]@{ FEATURE_SPEC=$paths.FEATURE_SPEC; IMPL_PLAN=$paths.IMPL_PLAN; SPECS_DIR=$paths.FEATURE_DIR; BRANCH=$paths.CURRENT_BRANCH } | ConvertTo-Json -Compress
|
||||
} else {
|
||||
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
||||
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
|
||||
Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)"
|
||||
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
||||
}
|
||||
91
scripts/powershell/update-agent-context.ps1
Normal file
91
scripts/powershell/update-agent-context.ps1
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env pwsh
|
||||
[CmdletBinding()]
|
||||
param([string]$AgentType)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = git rev-parse --show-toplevel
|
||||
$currentBranch = git rev-parse --abbrev-ref HEAD
|
||||
$featureDir = Join-Path $repoRoot "specs/$currentBranch"
|
||||
$newPlan = Join-Path $featureDir 'plan.md'
|
||||
if (-not (Test-Path $newPlan)) { Write-Error "ERROR: No plan.md found at $newPlan"; exit 1 }
|
||||
|
||||
$claudeFile = Join-Path $repoRoot 'CLAUDE.md'
|
||||
$geminiFile = Join-Path $repoRoot 'GEMINI.md'
|
||||
$copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md'
|
||||
|
||||
Write-Output "=== Updating agent context files for feature $currentBranch ==="
|
||||
|
||||
function Get-PlanValue($pattern) {
|
||||
if (-not (Test-Path $newPlan)) { return '' }
|
||||
$line = Select-String -Path $newPlan -Pattern $pattern | Select-Object -First 1
|
||||
if ($line) { return ($line.Line -replace "^\*\*$pattern\*\*: ", '') }
|
||||
return ''
|
||||
}
|
||||
|
||||
$newLang = Get-PlanValue 'Language/Version'
|
||||
$newFramework = Get-PlanValue 'Primary Dependencies'
|
||||
$newTesting = Get-PlanValue 'Testing'
|
||||
$newDb = Get-PlanValue 'Storage'
|
||||
$newProjectType = Get-PlanValue 'Project Type'
|
||||
|
||||
function Initialize-AgentFile($targetFile, $agentName) {
|
||||
if (Test-Path $targetFile) { return }
|
||||
$template = Join-Path $repoRoot 'templates/agent-file-template.md'
|
||||
if (-not (Test-Path $template)) { Write-Error "Template not found: $template"; return }
|
||||
$content = Get-Content $template -Raw
|
||||
$content = $content.Replace('[PROJECT NAME]', (Split-Path $repoRoot -Leaf))
|
||||
$content = $content.Replace('[DATE]', (Get-Date -Format 'yyyy-MM-dd'))
|
||||
$content = $content.Replace('[EXTRACTED FROM ALL PLAN.MD FILES]', "- $newLang + $newFramework ($currentBranch)")
|
||||
if ($newProjectType -match 'web') { $structure = "backend/`nfrontend/`ntests/" } else { $structure = "src/`ntests/" }
|
||||
$content = $content.Replace('[ACTUAL STRUCTURE FROM PLANS]', $structure)
|
||||
if ($newLang -match 'Python') { $commands = 'cd src && pytest && ruff check .' }
|
||||
elseif ($newLang -match 'Rust') { $commands = 'cargo test && cargo clippy' }
|
||||
elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' }
|
||||
else { $commands = "# Add commands for $newLang" }
|
||||
$content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands)
|
||||
$content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "$newLang: Follow standard conventions")
|
||||
$content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- $currentBranch: Added $newLang + $newFramework")
|
||||
$content | Set-Content $targetFile -Encoding UTF8
|
||||
}
|
||||
|
||||
function Update-AgentFile($targetFile, $agentName) {
|
||||
if (-not (Test-Path $targetFile)) { Initialize-AgentFile $targetFile $agentName; return }
|
||||
$content = Get-Content $targetFile -Raw
|
||||
if ($newLang -and ($content -notmatch [regex]::Escape($newLang))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newLang + $newFramework ($currentBranch)`n" }
|
||||
if ($newDb -and $newDb -ne 'N/A' -and ($content -notmatch [regex]::Escape($newDb))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newDb ($currentBranch)`n" }
|
||||
if ($content -match '## Recent Changes\n([\s\S]*?)(\n\n|$)') {
|
||||
$changesBlock = $matches[1].Trim().Split("`n")
|
||||
$changesBlock = ,"- $currentBranch: Added $newLang + $newFramework" + $changesBlock
|
||||
$changesBlock = $changesBlock | Where-Object { $_ } | Select-Object -First 3
|
||||
$joined = ($changesBlock -join "`n")
|
||||
$content = [regex]::Replace($content, '## Recent Changes\n([\s\S]*?)(\n\n|$)', "## Recent Changes`n$joined`n`n")
|
||||
}
|
||||
$content = [regex]::Replace($content, 'Last updated: \d{4}-\d{2}-\d{2}', "Last updated: $(Get-Date -Format 'yyyy-MM-dd')")
|
||||
$content | Set-Content $targetFile -Encoding UTF8
|
||||
Write-Output "✅ $agentName context file updated successfully"
|
||||
}
|
||||
|
||||
switch ($AgentType) {
|
||||
'claude' { Update-AgentFile $claudeFile 'Claude Code' }
|
||||
'gemini' { Update-AgentFile $geminiFile 'Gemini CLI' }
|
||||
'copilot' { Update-AgentFile $copilotFile 'GitHub Copilot' }
|
||||
'' {
|
||||
foreach ($pair in @(@{file=$claudeFile; name='Claude Code'}, @{file=$geminiFile; name='Gemini CLI'}, @{file=$copilotFile; name='GitHub Copilot'})) {
|
||||
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
|
||||
}
|
||||
if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile)) {
|
||||
Write-Output 'No agent context files found. Creating Claude Code context file by default.'
|
||||
Update-AgentFile $claudeFile 'Claude Code'
|
||||
}
|
||||
}
|
||||
Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, or leave empty for all."; exit 1 }
|
||||
}
|
||||
|
||||
Write-Output ''
|
||||
Write-Output 'Summary of changes:'
|
||||
if ($newLang) { Write-Output "- Added language: $newLang" }
|
||||
if ($newFramework) { Write-Output "- Added framework: $newFramework" }
|
||||
if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" }
|
||||
|
||||
Write-Output ''
|
||||
Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot]'
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup implementation plan structure for current branch
|
||||
# Returns paths needed for implementation plan generation
|
||||
# Usage: ./setup-plan.sh [--json]
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) JSON_MODE=true ;;
|
||||
--help|-h) echo "Usage: $0 [--json]"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if on feature branch
|
||||
check_feature_branch "$CURRENT_BRANCH" || exit 1
|
||||
|
||||
# Create specs directory if it doesn't exist
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy plan template if it exists
|
||||
TEMPLATE="$REPO_ROOT/templates/plan-template.md"
|
||||
if [ -f "$TEMPLATE" ]; then
|
||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
fi
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
|
||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
|
||||
else
|
||||
# Output all paths for LLM use
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "SPECS_DIR: $FEATURE_DIR"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
fi
|
||||
@@ -1,234 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Incrementally update agent context files based on new feature plan
|
||||
# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md
|
||||
# O(1) operation - only reads current context file and new plan.md
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
|
||||
NEW_PLAN="$FEATURE_DIR/plan.md"
|
||||
|
||||
# Determine which agent context files to update
|
||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
||||
|
||||
# Allow override via argument
|
||||
AGENT_TYPE="$1"
|
||||
|
||||
if [ ! -f "$NEW_PLAN" ]; then
|
||||
echo "ERROR: No plan.md found at $NEW_PLAN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||
|
||||
# Extract tech from new plan
|
||||
NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
|
||||
NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
|
||||
|
||||
# Function to update a single agent context file
|
||||
update_agent_file() {
|
||||
local target_file="$1"
|
||||
local agent_name="$2"
|
||||
|
||||
echo "Updating $agent_name context file: $target_file"
|
||||
|
||||
# Create temp file for new context
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
# If file doesn't exist, create from template
|
||||
if [ ! -f "$target_file" ]; then
|
||||
echo "Creating new $agent_name context file..."
|
||||
|
||||
# Check if this is the SDD repo itself
|
||||
if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then
|
||||
cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"
|
||||
else
|
||||
echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Replace placeholders
|
||||
sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"
|
||||
sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"
|
||||
sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file"
|
||||
|
||||
# Add project structure based on type
|
||||
if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then
|
||||
sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"
|
||||
else
|
||||
sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"
|
||||
fi
|
||||
|
||||
# Add minimal commands
|
||||
if [[ "$NEW_LANG" == *"Python"* ]]; then
|
||||
COMMANDS="cd src && pytest && ruff check ."
|
||||
elif [[ "$NEW_LANG" == *"Rust"* ]]; then
|
||||
COMMANDS="cargo test && cargo clippy"
|
||||
elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then
|
||||
COMMANDS="npm test && npm run lint"
|
||||
else
|
||||
COMMANDS="# Add commands for $NEW_LANG"
|
||||
fi
|
||||
sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file"
|
||||
|
||||
# Add code style
|
||||
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"
|
||||
|
||||
# Add recent changes
|
||||
sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"
|
||||
|
||||
rm "$temp_file.bak"
|
||||
else
|
||||
echo "Updating existing $agent_name context file..."
|
||||
|
||||
# Extract manual additions
|
||||
local manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1)
|
||||
local manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1)
|
||||
|
||||
if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then
|
||||
sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt
|
||||
fi
|
||||
|
||||
# Parse existing file and create updated version
|
||||
python3 - << EOF
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# Read existing file
|
||||
with open("$target_file", 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if new tech already exists
|
||||
tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL)
|
||||
if tech_section:
|
||||
existing_tech = tech_section.group(1)
|
||||
|
||||
# Add new tech if not already present
|
||||
new_additions = []
|
||||
if "$NEW_LANG" and "$NEW_LANG" not in existing_tech:
|
||||
new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
|
||||
if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A":
|
||||
new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
|
||||
|
||||
if new_additions:
|
||||
updated_tech = existing_tech + "\n" + "\n".join(new_additions)
|
||||
content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n")
|
||||
|
||||
# Update project structure if needed
|
||||
if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content:
|
||||
struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL)
|
||||
if struct_section:
|
||||
updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI"
|
||||
content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)',
|
||||
f'\\1{updated_struct}\\2', content, flags=re.DOTALL)
|
||||
|
||||
# Add new commands if language is new
|
||||
if "$NEW_LANG" and f"# {NEW_LANG}" not in content:
|
||||
commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL)
|
||||
if not commands_section:
|
||||
commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL)
|
||||
|
||||
if commands_section:
|
||||
new_commands = commands_section.group(1)
|
||||
if "Python" in "$NEW_LANG":
|
||||
new_commands += "\ncd src && pytest && ruff check ."
|
||||
elif "Rust" in "$NEW_LANG":
|
||||
new_commands += "\ncargo test && cargo clippy"
|
||||
elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG":
|
||||
new_commands += "\nnpm test && npm run lint"
|
||||
|
||||
if "```bash" in content:
|
||||
content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)',
|
||||
f'\\1{new_commands}\\2', content, flags=re.DOTALL)
|
||||
else:
|
||||
content = re.sub(r'(## Commands\n).*?(\n\n)',
|
||||
f'\\1{new_commands}\\2', content, flags=re.DOTALL)
|
||||
|
||||
# Update recent changes (keep only last 3)
|
||||
changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL)
|
||||
if changes_section:
|
||||
changes = changes_section.group(1).strip().split('\n')
|
||||
changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
|
||||
# Keep only last 3
|
||||
changes = changes[:3]
|
||||
content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)',
|
||||
f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL)
|
||||
|
||||
# Update date
|
||||
content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}',
|
||||
f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content)
|
||||
|
||||
# Write to temp file
|
||||
with open("$temp_file", 'w') as f:
|
||||
f.write(content)
|
||||
EOF
|
||||
|
||||
# Restore manual additions if they exist
|
||||
if [ -f /tmp/manual_additions.txt ]; then
|
||||
# Remove old manual section from temp file
|
||||
sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$temp_file"
|
||||
# Append manual additions
|
||||
cat /tmp/manual_additions.txt >> "$temp_file"
|
||||
rm /tmp/manual_additions.txt "$temp_file.bak"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Move temp file to final location
|
||||
mv "$temp_file" "$target_file"
|
||||
echo "✅ $agent_name context file updated successfully"
|
||||
}
|
||||
|
||||
# Update files based on argument or detect existing files
|
||||
case "$AGENT_TYPE" in
|
||||
"claude")
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
;;
|
||||
"gemini")
|
||||
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||
;;
|
||||
"copilot")
|
||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||
;;
|
||||
"")
|
||||
# Update all existing files
|
||||
[ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
[ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||
[ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||
|
||||
# If no files exist, create based on current directory or ask user
|
||||
if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then
|
||||
echo "No agent context files found. Creating Claude Code context file by default."
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
echo "Summary of changes:"
|
||||
if [ ! -z "$NEW_LANG" ]; then
|
||||
echo "- Added language: $NEW_LANG"
|
||||
fi
|
||||
if [ ! -z "$NEW_FRAMEWORK" ]; then
|
||||
echo "- Added framework: $NEW_FRAMEWORK"
|
||||
fi
|
||||
if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then
|
||||
echo "- Added database: $NEW_DB"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Usage: $0 [claude|gemini|copilot]"
|
||||
echo " - No argument: Update all existing agent context files"
|
||||
echo " - claude: Update only CLAUDE.md"
|
||||
echo " - gemini: Update only GEMINI.md"
|
||||
echo " - copilot: Update only .github/copilot-instructions.md"
|
||||
@@ -30,7 +30,7 @@ import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
@@ -58,6 +58,11 @@ AI_CHOICES = {
|
||||
"claude": "Claude Code",
|
||||
"gemini": "Gemini CLI"
|
||||
}
|
||||
# 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
|
||||
BANNER = """
|
||||
@@ -335,8 +340,28 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
|
||||
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:
|
||||
"""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):
|
||||
return True
|
||||
else:
|
||||
@@ -390,7 +415,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None):
|
||||
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False) -> Tuple[Path, dict]:
|
||||
repo_owner = "github"
|
||||
repo_name = "spec-kit"
|
||||
if client is None:
|
||||
@@ -402,26 +427,32 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
||||
|
||||
try:
|
||||
response = client.get(api_url, timeout=30, follow_redirects=True)
|
||||
response.raise_for_status()
|
||||
status = response.status_code
|
||||
if status != 200:
|
||||
msg = f"GitHub API returned {status} for {api_url}"
|
||||
if debug:
|
||||
msg += f"\nResponse headers: {response.headers}\nBody (truncated 500): {response.text[:500]}"
|
||||
raise RuntimeError(msg)
|
||||
try:
|
||||
release_data = response.json()
|
||||
except httpx.RequestError as e:
|
||||
if verbose:
|
||||
console.print(f"[red]Error fetching release information:[/red] {e}")
|
||||
except ValueError as je:
|
||||
raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error fetching release information[/red]")
|
||||
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Find the template asset for the specified AI assistant
|
||||
pattern = f"spec-kit-template-{ai_assistant}"
|
||||
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
||||
matching_assets = [
|
||||
asset for asset in release_data.get("assets", [])
|
||||
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
||||
]
|
||||
|
||||
if not matching_assets:
|
||||
if verbose:
|
||||
console.print(f"[red]Error:[/red] No template found for AI assistant '{ai_assistant}'")
|
||||
console.print(f"[yellow]Available assets:[/yellow]")
|
||||
for asset in release_data.get("assets", []):
|
||||
console.print(f" - {asset['name']}")
|
||||
console.print(f"[red]No matching release asset found[/red] for pattern: [bold]{pattern}[/bold]")
|
||||
asset_names = [a.get('name','?') for a in release_data.get('assets', [])]
|
||||
console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Use the first matching asset
|
||||
@@ -441,8 +472,10 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
||||
console.print(f"[cyan]Downloading template...[/cyan]")
|
||||
|
||||
try:
|
||||
with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response:
|
||||
response.raise_for_status()
|
||||
with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response:
|
||||
if response.status_code != 200:
|
||||
body_sample = response.text[:400]
|
||||
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
with open(zip_path, 'wb') as f:
|
||||
if total_size == 0:
|
||||
@@ -465,11 +498,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
||||
else:
|
||||
for chunk in response.iter_bytes(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
except httpx.RequestError as e:
|
||||
if verbose:
|
||||
console.print(f"[red]Error downloading template:[/red] {e}")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error downloading template[/red]")
|
||||
detail = str(e)
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
console.print(Panel(detail, title="Download Error", border_style="red"))
|
||||
raise typer.Exit(1)
|
||||
if verbose:
|
||||
console.print(f"Downloaded: {filename}")
|
||||
@@ -482,7 +516,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
|
||||
return zip_path, metadata
|
||||
|
||||
|
||||
def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path:
|
||||
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False) -> Path:
|
||||
"""Download the latest release and extract it to create a new project.
|
||||
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
|
||||
"""
|
||||
@@ -495,9 +529,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
|
||||
zip_path, meta = download_template_from_github(
|
||||
ai_assistant,
|
||||
current_dir,
|
||||
script_type=script_type,
|
||||
verbose=verbose and tracker is None,
|
||||
show_progress=(tracker is None),
|
||||
client=client
|
||||
client=client,
|
||||
debug=debug
|
||||
)
|
||||
if tracker:
|
||||
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
||||
@@ -614,6 +650,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
|
||||
else:
|
||||
if verbose:
|
||||
console.print(f"[red]Error extracting template:[/red] {e}")
|
||||
if debug:
|
||||
console.print(Panel(str(e), title="Extraction Error", border_style="red"))
|
||||
# Clean up project directory if created and not current directory
|
||||
if not is_current_dir and project_path.exists():
|
||||
shutil.rmtree(project_path)
|
||||
@@ -636,60 +674,44 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
|
||||
|
||||
|
||||
def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None:
|
||||
"""Ensure POSIX .sh scripts in the project scripts directory have execute bits (no-op on Windows)."""
|
||||
"""Ensure POSIX .sh scripts under .specify/scripts (recursively) have execute bits (no-op on Windows)."""
|
||||
if os.name == "nt":
|
||||
return # Windows: skip silently
|
||||
scripts_dir = project_path / "scripts"
|
||||
if not scripts_dir.is_dir():
|
||||
scripts_root = project_path / ".specify" / "scripts"
|
||||
if not scripts_root.is_dir():
|
||||
return
|
||||
failures: list[str] = []
|
||||
updated = 0
|
||||
for script in scripts_dir.glob("*.sh"):
|
||||
for script in scripts_root.rglob("*.sh"):
|
||||
try:
|
||||
# Skip symlinks
|
||||
if script.is_symlink():
|
||||
if script.is_symlink() or not script.is_file():
|
||||
continue
|
||||
# Must be a regular file
|
||||
if not script.is_file():
|
||||
continue
|
||||
# Quick shebang check
|
||||
try:
|
||||
with script.open("rb") as f:
|
||||
first_two = f.read(2)
|
||||
if first_two != b"#!":
|
||||
if f.read(2) != b"#!":
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
st = script.stat()
|
||||
mode = st.st_mode
|
||||
# If already any execute bit set, skip
|
||||
st = script.stat(); mode = st.st_mode
|
||||
if mode & 0o111:
|
||||
continue
|
||||
# Only add execute bits that correspond to existing read bits
|
||||
new_mode = mode
|
||||
if mode & 0o400: # owner read
|
||||
new_mode |= 0o100
|
||||
if mode & 0o040: # group read
|
||||
new_mode |= 0o010
|
||||
if mode & 0o004: # other read
|
||||
new_mode |= 0o001
|
||||
# Fallback: ensure at least owner execute
|
||||
if mode & 0o400: new_mode |= 0o100
|
||||
if mode & 0o040: new_mode |= 0o010
|
||||
if mode & 0o004: new_mode |= 0o001
|
||||
if not (new_mode & 0o100):
|
||||
new_mode |= 0o100
|
||||
os.chmod(script, new_mode)
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
failures.append(f"{script.name}: {e}")
|
||||
failures.append(f"{script.relative_to(scripts_root)}: {e}")
|
||||
if tracker:
|
||||
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
|
||||
tracker.add("chmod", "Set script permissions")
|
||||
if failures:
|
||||
tracker.error("chmod", detail)
|
||||
else:
|
||||
tracker.complete("chmod", detail)
|
||||
tracker.add("chmod", "Set script permissions recursively")
|
||||
(tracker.error if failures else tracker.complete)("chmod", detail)
|
||||
else:
|
||||
if updated:
|
||||
console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]")
|
||||
console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]")
|
||||
if failures:
|
||||
console.print("[yellow]Some scripts could not be updated:[/yellow]")
|
||||
for f in failures:
|
||||
@@ -700,10 +722,12 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
|
||||
def init(
|
||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
||||
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, or copilot"),
|
||||
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
||||
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
||||
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
||||
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
|
||||
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.
|
||||
@@ -806,6 +830,24 @@ def init(
|
||||
console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Determine script type (explicit, interactive, or OS default)
|
||||
if script_type:
|
||||
if script_type not in SCRIPT_TYPE_CHOICES:
|
||||
console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
|
||||
raise typer.Exit(1)
|
||||
selected_script = script_type
|
||||
else:
|
||||
# Auto-detect default
|
||||
default_script = "ps" if os.name == "nt" else "sh"
|
||||
# Provide interactive selection similar to AI if stdin is a TTY
|
||||
if sys.stdin.isatty():
|
||||
selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script)
|
||||
else:
|
||||
selected_script = default_script
|
||||
|
||||
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
|
||||
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
||||
|
||||
# Download and set up project
|
||||
# New tree-based progress (no emojis); include earlier substeps
|
||||
tracker = StepTracker("Initialize Specify Project")
|
||||
@@ -816,6 +858,8 @@ def init(
|
||||
tracker.complete("precheck", "ok")
|
||||
tracker.add("ai-select", "Select AI assistant")
|
||||
tracker.complete("ai-select", f"{selected_ai}")
|
||||
tracker.add("script-select", "Select script type")
|
||||
tracker.complete("script-select", selected_script)
|
||||
for key, label in [
|
||||
("fetch", "Fetch latest release"),
|
||||
("download", "Download template"),
|
||||
@@ -838,7 +882,7 @@ def init(
|
||||
local_ssl_context = ssl_context if verify else False
|
||||
local_client = httpx.Client(verify=local_ssl_context)
|
||||
|
||||
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client)
|
||||
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug)
|
||||
|
||||
# Ensure scripts are executable (POSIX)
|
||||
ensure_executable_scripts(project_path, tracker=tracker)
|
||||
@@ -861,6 +905,16 @@ def init(
|
||||
tracker.complete("final", "project ready")
|
||||
except Exception as e:
|
||||
tracker.error("final", str(e))
|
||||
console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red"))
|
||||
if debug:
|
||||
_env_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():
|
||||
shutil.rmtree(project_path)
|
||||
raise typer.Exit(1)
|
||||
@@ -896,6 +950,7 @@ def init(
|
||||
elif selected_ai == "copilot":
|
||||
steps_lines.append(f"{step_num}. Open in Visual Studio Code and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with GitHub Copilot")
|
||||
|
||||
# Removed script variant step (scripts are transparent to users)
|
||||
step_num += 1
|
||||
steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles")
|
||||
|
||||
@@ -906,37 +961,36 @@ def init(
|
||||
# Removed farewell line per user request
|
||||
|
||||
|
||||
# Add skip_tls option to check
|
||||
@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."""
|
||||
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
|
||||
console.print("[cyan]Checking internet connectivity...[/cyan]")
|
||||
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]")
|
||||
# Create tracker for checking tools
|
||||
tracker = StepTracker("Check Available Tools")
|
||||
|
||||
console.print("\n[cyan]Optional tools:[/cyan]")
|
||||
git_ok = check_tool("git", "https://git-scm.com/downloads")
|
||||
# Add all tools we want to check
|
||||
tracker.add("git", "Git version control")
|
||||
tracker.add("claude", "Claude Code CLI")
|
||||
tracker.add("gemini", "Gemini CLI")
|
||||
|
||||
console.print("\n[cyan]Optional AI tools:[/cyan]")
|
||||
claude_ok = check_tool("claude", "Install from: https://docs.anthropic.com/en/docs/claude-code/setup")
|
||||
gemini_ok = check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli")
|
||||
# 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)
|
||||
|
||||
console.print("\n[green]✓ Specify CLI is ready to use![/green]")
|
||||
# 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:
|
||||
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):
|
||||
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():
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
---
|
||||
name: plan
|
||||
description: "Plan how to implement the specified feature. This is the second step in the Spec-Driven Development lifecycle."
|
||||
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||
---
|
||||
|
||||
Plan how to implement the specified feature.
|
||||
|
||||
This is the second step in the Spec-Driven Development lifecycle.
|
||||
<!-- VARIANT:sh 1. Run `scripts/bash/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. -->
|
||||
<!-- VARIANT:ps 1. Run `scripts/powershell/setup-plan.ps1 -Json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. -->
|
||||
|
||||
Given the implementation details provided as an argument, do this:
|
||||
|
||||
1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute.
|
||||
1. VARIANT-INJECT
|
||||
2. Read and analyze the feature specification to understand:
|
||||
- The feature requirements and user stories
|
||||
- Functional and non-functional requirements
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
---
|
||||
name: specify
|
||||
description: "Start a new feature by creating a specification and feature branch. This is the first step in the Spec-Driven Development lifecycle."
|
||||
description: Create or update the feature specification from a natural language feature description.
|
||||
---
|
||||
|
||||
Start a new feature by creating a specification and feature branch.
|
||||
|
||||
This is the first step in the Spec-Driven Development lifecycle.
|
||||
<!-- VARIANT:sh 1. Run the script `scripts/bash/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. -->
|
||||
<!-- VARIANT:ps 1. Run the script `scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. -->
|
||||
|
||||
Given the feature description provided as an argument, do this:
|
||||
|
||||
1. Run the script `scripts/create-new-feature.sh --json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
||||
1. VARIANT-INJECT
|
||||
2. Load `templates/spec-template.md` to understand required sections.
|
||||
3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
|
||||
4. Report completion with branch name, spec file path, and readiness for the next phase.
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
---
|
||||
name: tasks
|
||||
description: "Break down the plan into executable tasks. This is the third step in the Spec-Driven Development lifecycle."
|
||||
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||
---
|
||||
|
||||
Break down the plan into executable tasks.
|
||||
|
||||
This is the third step in the Spec-Driven Development lifecycle.
|
||||
<!-- VARIANT:sh 1. Run `scripts/bash/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. -->
|
||||
<!-- VARIANT:ps 1. Run `scripts/powershell/check-task-prerequisites.ps1 -Json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. -->
|
||||
|
||||
Given the context provided as an argument, do this:
|
||||
|
||||
1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute.
|
||||
1. VARIANT-INJECT
|
||||
2. Load and analyze available design documents:
|
||||
- Always read plan.md for tech stack and libraries
|
||||
- IF EXISTS: Read data-model.md for entities
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Implementation Plan: [FEATURE]
|
||||
|
||||
<!-- VARIANT:sh - Run `/scripts/bash/update-agent-context.sh __AGENT__` for your AI assistant -->
|
||||
<!-- VARIANT:ps - Run `/scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__` for your AI assistant -->
|
||||
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
@@ -171,7 +174,7 @@ ios/ or android/
|
||||
- Quickstart test = story validation steps
|
||||
|
||||
5. **Update agent file incrementally** (O(1) operation):
|
||||
- Run `/scripts/update-agent-context.sh [claude|gemini|copilot]` for your AI assistant
|
||||
VARIANT-INJECT
|
||||
- If exists: Add only NEW tech from current plan
|
||||
- Preserve manual additions between markers
|
||||
- Update recent changes (keep last 3)
|
||||
|
||||
Reference in New Issue
Block a user