Merge pull request #191 from github/update-cli

Local dev guide and CLI updates
This commit is contained in:
Den Delimarsky
2025-09-11 23:06:54 -07:00
committed by GitHub
7 changed files with 363 additions and 298 deletions

View File

@@ -1,191 +0,0 @@
name: Manual Release
on:
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
jobs:
manual_release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate new version
id: version
run: |
# Get the latest tag, or use v0.0.0 if no tags exist
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
# Extract version number
VERSION=$(echo $LATEST_TAG | sed 's/v//')
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
# Increment based on input
case "${{ github.event.inputs.version_bump }}" in
"major")
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
"minor")
MINOR=$((MINOR + 1))
PATCH=0
;;
"patch")
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version will be: $NEW_VERSION (was $LATEST_TAG)"
- name: Create release package
run: |
# Create base package directory structure
mkdir -p sdd-package-base
# Copy common folders to base
echo "Packaging SDD common components..."
if [ -d "memory" ]; then
cp -r memory sdd-package-base/
echo "✓ Copied memory folder ($(find memory -type f | wc -l) files)"
else
echo "⚠️ memory folder not found"
fi
if [ -d "scripts" ]; then
cp -r scripts sdd-package-base/
echo "✓ Copied scripts folder ($(find scripts -type f | wc -l) files)"
else
echo "⚠️ scripts folder not found"
fi
# Create Claude Code package
echo "Creating Claude Code package..."
mkdir -p sdd-claude-package
cp -r sdd-package-base/* sdd-claude-package/
if [ -d "agent_templates/claude" ]; then
cp -r agent_templates/claude sdd-claude-package/.claude
echo "✓ Added Claude Code commands ($(find agent_templates/claude -type f | wc -l) files)"
else
echo "⚠️ agent_templates/claude folder not found"
fi
# Create Gemini CLI package
echo "Creating Gemini CLI package..."
mkdir -p sdd-gemini-package
cp -r sdd-package-base/* sdd-gemini-package/
if [ -d "agent_templates/gemini" ]; then
cp -r agent_templates/gemini sdd-gemini-package/.gemini
# Move GEMINI.md to root for easier access
if [ -f "sdd-gemini-package/.gemini/GEMINI.md" ]; then
mv sdd-gemini-package/.gemini/GEMINI.md sdd-gemini-package/GEMINI.md
echo "✓ Moved GEMINI.md to root of Gemini package"
fi
# Remove empty .gemini folder if it only contained GEMINI.md
if [ -d "sdd-gemini-package/.gemini" ] && [ -z "$(find sdd-gemini-package/.gemini -type f)" ]; then
rm -rf sdd-gemini-package/.gemini
echo "✓ Removed empty .gemini folder"
fi
echo "✓ Added Gemini CLI commands ($(find agent_templates/gemini -type f | wc -l) files)"
else
echo "⚠️ agent_templates/gemini folder not found"
fi
# Create GitHub Copilot package
echo "Creating GitHub Copilot package..."
mkdir -p sdd-copilot-package
cp -r sdd-package-base/* sdd-copilot-package/
if [ -d "agent_templates/copilot" ]; then
mkdir -p sdd-copilot-package/.github
cp -r agent_templates/copilot/* sdd-copilot-package/.github/
echo "✓ Added Copilot instructions to .github ($(find agent_templates/copilot -type f | wc -l) files)"
else
echo "⚠️ agent_templates/copilot folder not found"
fi
# Create archive files for each package
echo "Creating archive files..."
cd sdd-claude-package && zip -r ../spec-kit-template-claude-${{ steps.version.outputs.new_version }}.zip . && cd ..
cd sdd-gemini-package && zip -r ../spec-kit-template-gemini-${{ steps.version.outputs.new_version }}.zip . && cd ..
cd sdd-copilot-package && zip -r ../spec-kit-template-copilot-${{ steps.version.outputs.new_version }}.zip . && cd ..
echo ""
echo "📦 Packages created:"
echo "Claude: $(ls -lh spec-kit-template-claude-*.zip | awk '{print $5}')"
echo "Gemini: $(ls -lh spec-kit-template-gemini-*.zip | awk '{print $5}')"
echo "Copilot: $(ls -lh spec-kit-template-copilot-*.zip | awk '{print $5}')"
echo "Copilot: $(ls -lh sdd-template-copilot-*.zip | awk '{print $5}')"
- name: Generate detailed release notes
run: |
LAST_TAG=${{ steps.version.outputs.latest_tag }}
# Get commit range
if [ "$LAST_TAG" = "v0.0.0" ]; then
COMMIT_RANGE="HEAD~10..HEAD"
COMMITS=$(git log --oneline --pretty=format:"- %s" $COMMIT_RANGE 2>/dev/null || echo "- Initial release")
else
COMMIT_RANGE="$LAST_TAG..HEAD"
COMMITS=$(git log --oneline --pretty=format:"- %s" $COMMIT_RANGE 2>/dev/null || echo "- No changes since last release")
fi
# Count files in each directory
CLAUDE_COUNT=$(find agent_templates/claude -type f 2>/dev/null | wc -l || echo "0")
GEMINI_COUNT=$(find agent_templates/gemini -type f 2>/dev/null | wc -l || echo "0")
COPILOT_COUNT=$(find agent_templates/copilot -type f 2>/dev/null | wc -l || echo "0")
MEMORY_COUNT=$(find memory -type f 2>/dev/null | wc -l || echo "0")
SCRIPTS_COUNT=$(find scripts -type f 2>/dev/null | wc -l || echo "0")
cat > release_notes.md << EOF
Template release ${{ steps.version.outputs.new_version }}
Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI.
Download the template for your preferred AI assistant:
- spec-kit-template-copilot-${{ steps.version.outputs.new_version }}.zip
- spec-kit-template-claude-${{ steps.version.outputs.new_version }}.zip
- spec-kit-template-gemini-${{ steps.version.outputs.new_version }}.zip
Changes since $LAST_TAG:
$COMMITS
EOF
- name: Create GitHub Release
run: |
# Remove 'v' prefix from version for release title
VERSION_NO_V=${{ steps.version.outputs.new_version }}
VERSION_NO_V=${VERSION_NO_V#v}
gh release create ${{ steps.version.outputs.new_version }} \
spec-kit-template-copilot-${{ steps.version.outputs.new_version }}.zip \
spec-kit-template-claude-${{ steps.version.outputs.new_version }}.zip \
spec-kit-template-gemini-${{ steps.version.outputs.new_version }}.zip \
--title "Spec Kit Templates - $VERSION_NO_V" \
--notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -61,105 +61,8 @@ jobs:
- name: Create release package
if: steps.check_release.outputs.exists == 'false'
run: |
# Create base package directory structure
mkdir -p sdd-package-base
# Copy common folders to base
if [ -d "memory" ]; then
cp -r memory sdd-package-base/
echo "Copied memory folder"
fi
if [ -d "scripts" ]; then
cp -r scripts sdd-package-base/
echo "Copied scripts folder"
fi
if [ -d "templates" ]; then
mkdir -p sdd-package-base/templates
# Copy templates folder but exclude the commands directory
find templates -type f -not -path "templates/commands/*" -exec cp --parents {} sdd-package-base/ \;
echo "Copied templates folder (excluding commands directory)"
fi
# Generate command files for each agent from source templates
generate_commands() {
local agent=$1
local ext=$2
local arg_format=$3
local output_dir=$4
mkdir -p "$output_dir"
for template in templates/commands/*.md; do
if [[ -f "$template" ]]; then
name=$(basename "$template" .md)
description=$(awk '/^description:/ {gsub(/^description: *"?/, ""); gsub(/"$/, ""); print; exit}' "$template" | tr -d '\r')
content=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template" | sed "s/{ARGS}/$arg_format/g")
case $ext in
"toml")
{
echo "description = \"$description\""
echo ""
echo "prompt = \"\"\""
echo "$content"
echo "\"\"\""
} > "$output_dir/$name.$ext"
;;
"md")
echo "$content" > "$output_dir/$name.$ext"
;;
"prompt.md")
{
echo "# $(echo "$description" | sed 's/\. .*//')"
echo ""
echo "$content"
} > "$output_dir/$name.$ext"
;;
esac
fi
done
}
# Create Claude Code 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 Code package"
# Create Gemini CLI 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"
if [ -f "agent_templates/gemini/GEMINI.md" ]; then
cp agent_templates/gemini/GEMINI.md sdd-gemini-package/GEMINI.md
fi
echo "Created Gemini CLI package"
# Create GitHub 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 GitHub Copilot package"
# Create archive files for each package
cd sdd-claude-package && zip -r ../spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip . && cd ..
cd sdd-gemini-package && zip -r ../spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip . && cd ..
cd sdd-copilot-package && zip -r ../spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip . && cd ..
# List contents for verification
echo "Claude package contents:"
unzip -l spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip | head -10
echo "Gemini package contents:"
unzip -l spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip | head -10
echo "Copilot package contents:"
unzip -l spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip | head -10
chmod +x scripts/create-release-packages.sh
./scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }}
- name: Generate release notes
if: steps.check_release.outputs.exists == 'false'

View File

@@ -12,6 +12,7 @@ Spec-Driven Development **flips the script** on traditional software development
- [Installation Guide](installation.md)
- [Quick Start Guide](quickstart.md)
- [Local Development](local-development.md)
## Core Philosophy

165
docs/local-development.md Normal file
View File

@@ -0,0 +1,165 @@
# Local Development Guide
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
## 1. Clone and Switch Branches
```bash
git clone https://github.com/github/spec-kit.git
cd spec-kit
# Work on a feature branch
git checkout -b your-feature-branch
```
## 2. Run the CLI Directly (Fastest Feedback)
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
```
If you prefer invoking the script file style (uses shebang):
```bash
python src/specify_cli/__init__.py init demo-project
```
## 3. Use Editable Install (Isolated Environment)
Create an isolated environment using `uv` so dependencies resolve exactly like end users get them:
```bash
# Create & activate virtual env (uv auto-manages .venv)
uv venv
source .venv/bin/activate # or on Windows: .venv\\Scripts\\activate
# Install project in editable mode
uv pip install -e .
# Now 'specify' entrypoint is available
specify --help
```
Re-running after code edits requires no reinstall because of editable mode.
## 4. Invoke with uvx Directly From Git (Current Branch)
`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
```
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
```
### 4a. Absolute Path uvx (Run From Anywhere)
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
```
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
```
(Optional) Define a shell function:
```bash
specify-dev() { uvx --from /mnt/c/GitHub/spec-kit specify "$@"; }
# Then
specify-dev --help
```
## 5. Testing Script Permission Logic
After running an `init`, check that shell scripts are executable on POSIX systems:
```bash
ls -l scripts | grep .sh
# Expect owner execute bit (e.g. -rwxr-xr-x)
```
On Windows this step is a no-op.
## 6. Run Lint / Basic Checks (Add Your Own)
Currently no enforced lint config is bundled, but you can quickly sanity check importability:
```bash
python -c "import specify_cli; print('Import OK')"
```
## 7. Build a Wheel Locally (Optional)
Validate packaging before publishing:
```bash
uv build
ls dist/
```
Install the built artifact into a fresh throwaway environment if needed.
## 8. Using a Temporary Workspace
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
```
Or copy only the modified CLI portion if you want a lighter sandbox.
## 9. Debug Network / TLS Skips
If you need to bypass TLS validation while experimenting:
```bash
specify check --skip-tls
specify init demo --skip-tls --ai gemini --ignore-agent-tools
```
(Use only for local experimentation.)
## 10. Rapid Edit Loop Summary
| Action | Command |
|--------|---------|
| Run CLI directly | `python -m src.specify_cli --help` |
| Editable install | `uv pip install -e .` then `specify ...` |
| Local uvx run (repo root) | `uvx --from . specify ...` |
| Local uvx run (abs path) | `uvx --from /mnt/c/GitHub/spec-kit specify ...` |
| Git branch uvx | `uvx --from git+URL@branch specify ...` |
| Build wheel | `uv build` |
## 11. Cleaning Up
Remove build artifacts / virtual env quickly:
```bash
rm -rf .venv dist build *.egg-info
```
## 12. Common Issues
| Symptom | Fix |
|---------|-----|
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
| Scripts not executable (Linux) | Re-run init (logic adds bits) or `chmod +x scripts/*.sh` |
| Git step skipped | You passed `--no-git` or Git not installed |
| TLS errors on corporate network | Try `--skip-tls` (not for production) |
## 13. Next Steps
- Update docs and run through Quick Start using your modified CLI
- Open a PR when satisfied
- (Optional) Tag a release once changes land in `main`

View File

@@ -1,10 +1,17 @@
# Home page
- name: Home
href: index.md
- name: Installation
href: installation.md
- name: Quick Start
href: quickstart.md
- name: Contributing
href: CONTRIBUTING.md
- name: Support
href: SUPPORT.md
# Getting started section
- name: Getting Started
items:
- name: Installation
href: installation.md
- name: Quick Start
href: quickstart.md
# Development workflows
- name: Development
items:
- name: Local Development
href: local-development.md

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
# create-release-packages.sh
# Build Spec Kit template release archives for each supported AI assistant.
# Usage: ./scripts/create-release-packages.sh <version>
# <version> should include the leading 'v' (e.g. v0.0.4)
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version-with-v-prefix>" >&2
exit 1
fi
NEW_VERSION="$1"
if [[ ! $NEW_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version must look like v0.0.0" >&2
exit 1
fi
echo "Building release packages for $NEW_VERSION"
# Clean any previous build dirs
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
mkdir -p sdd-package-base
# Copy common folders to base
if [[ -d memory ]]; then
cp -r memory sdd-package-base/
echo "Copied memory folder"
fi
if [[ -d scripts ]]; then
# Exclude this script itself from being copied
rsync -a --exclude 'create-release-packages.sh' scripts/ sdd-package-base/scripts/
echo "Copied scripts folder (excluding create-release-packages.sh)"
fi
if [[ -d templates ]]; then
mkdir -p sdd-package-base/templates
# Copy all template files excluding commands (processed separately per assistant)
find templates -type f -not -path "templates/commands/*" -exec cp --parents {} sdd-package-base/ \;
echo "Copied templates folder (excluding commands directory)"
fi
# Function to generate assistant command files/prompts
# Args: agent ext arg_format output_dir
generate_commands() {
local agent=$1
local ext=$2
local arg_format=$3
local output_dir=$4
mkdir -p "$output_dir"
for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue
local name
name=$(basename "$template" .md)
local description
description=$(awk '/^description:/ {gsub(/^description: *"?/, ""); gsub(/"$/, ""); print; exit}' "$template" | tr -d '\r')
local content
content=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template" | sed "s/{ARGS}/$arg_format/g")
case $ext in
"toml")
{
echo "description = \"$description\""; echo ""; echo "prompt = \"\"\""; echo "$content"; echo "\"\"\"";
} > "$output_dir/$name.$ext"
;;
"md")
echo "$content" > "$output_dir/$name.$ext"
;;
"prompt.md")
# Preserve front matter exactly, just substitute {ARGS}
sed "s/{ARGS}/$arg_format/g" "$template" > "$output_dir/$name.$ext"
;;
esac
done
}
# 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 Code package"
# 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"
if [[ -f agent_templates/gemini/GEMINI.md ]]; then
cp agent_templates/gemini/GEMINI.md sdd-gemini-package/GEMINI.md
fi
echo "Created Gemini CLI package"
# 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 GitHub Copilot package"
# Archives
( 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 . )
echo "Package archives created:"
ls -1 spec-kit-template-*-${NEW_VERSION}.zip
# Basic verification snippet
unzip -l spec-kit-template-copilot-${NEW_VERSION}.zip | head -10 || true

View File

@@ -635,6 +635,67 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
return project_path
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)."""
if os.name == "nt":
return # Windows: skip silently
scripts_dir = project_path / "scripts"
if not scripts_dir.is_dir():
return
failures: list[str] = []
updated = 0
for script in scripts_dir.glob("*.sh"):
try:
# Skip symlinks
if script.is_symlink():
continue
# Must be a regular file
if not script.is_file():
continue
# Quick shebang check
try:
with script.open("rb") as f:
first_two = f.read(2)
if first_two != b"#!":
continue
except Exception:
continue
st = script.stat()
mode = st.st_mode
# If already any execute bit set, skip
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 not (new_mode & 0o100):
new_mode |= 0o100
os.chmod(script, new_mode)
updated += 1
except Exception as e:
failures.append(f"{script.name}: {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)
else:
if updated:
console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]")
if failures:
console.print("[yellow]Some scripts could not be updated:[/yellow]")
for f in failures:
console.print(f" - {f}")
@app.command()
def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
@@ -761,6 +822,7 @@ def init(
("extract", "Extract template"),
("zip-list", "Archive contents"),
("extracted-summary", "Extraction summary"),
("chmod", "Ensure scripts executable"),
("cleanup", "Cleanup"),
("git", "Initialize git repository"),
("final", "Finalize")
@@ -778,6 +840,9 @@ def init(
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client)
# Ensure scripts are executable (POSIX)
ensure_executable_scripts(project_path, tracker=tracker)
# Git step
if not no_git:
tracker.start("git")