Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1970a0bd | ||
|
|
60b8d8fad2 | ||
|
|
2aa30cdbb2 | ||
|
|
d7d2c145c7 | ||
|
|
caee341af9 | ||
|
|
dc8fdc2dc8 | ||
|
|
6a3ff650f1 | ||
|
|
8784f39755 | ||
|
|
fbacd0b0df | ||
|
|
db9d97bcbd | ||
|
|
537332725b | ||
|
|
65ccbb62ca | ||
|
|
5659c869b5 | ||
|
|
d8bf98a88d | ||
|
|
e482072520 | ||
|
|
aaa6df9653 | ||
|
|
8c3e9db3bf | ||
|
|
d1d5c82a8e | ||
|
|
0889635e66 | ||
|
|
2825bb1247 | ||
|
|
3a0ae75bfb | ||
|
|
312703260c | ||
|
|
286ad553fd | ||
|
|
a39185c8be | ||
|
|
e29488d91f | ||
|
|
95fba17d20 | ||
|
|
b1688b9633 | ||
|
|
70413f5214 |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
|||||||
cat > release_notes.md << EOF
|
cat > release_notes.md << EOF
|
||||||
Template release ${{ steps.get_tag.outputs.new_version }}
|
Template release ${{ steps.get_tag.outputs.new_version }}
|
||||||
|
|
||||||
Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, Cursor, Qwen, and opencode.
|
Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, Cursor, Qwen, opencode, Windsurf, and Codex.
|
||||||
|
|
||||||
Now includes per-script variants for POSIX shell (sh) and PowerShell (ps).
|
Now includes per-script variants for POSIX shell (sh) and PowerShell (ps).
|
||||||
|
|
||||||
@@ -98,6 +98,10 @@ jobs:
|
|||||||
- spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
- spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
- spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
- spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
- spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
- spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
|
- spec-kit-template-windsurf-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
|
- spec-kit-template-windsurf-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
|
- spec-kit-template-codex-sh-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
|
- spec-kit-template-codex-ps-${{ steps.get_tag.outputs.new_version }}.zip
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Generated release notes:"
|
echo "Generated release notes:"
|
||||||
@@ -122,6 +126,10 @@ jobs:
|
|||||||
spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
|
spec-kit-template-windsurf-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
|
spec-kit-template-windsurf-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
|
spec-kit-template-codex-sh-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
|
spec-kit-template-codex-ps-${{ steps.get_tag.outputs.new_version }}.zip \
|
||||||
--title "Spec Kit Templates - $VERSION_NO_V" \
|
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||||
--notes-file release_notes.md
|
--notes-file release_notes.md
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
|||||||
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||||
# Version argument should include leading 'v'.
|
# Version argument should include leading 'v'.
|
||||||
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
||||||
# AGENTS : space or comma separated subset of: claude gemini copilot qwen opencode (default: all)
|
# AGENTS : space or comma separated subset of: claude gemini copilot cursor qwen opencode windsurf codex (default: all)
|
||||||
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
||||||
# Examples:
|
# Examples:
|
||||||
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
||||||
@@ -154,13 +154,19 @@ build_variant() {
|
|||||||
opencode)
|
opencode)
|
||||||
mkdir -p "$base_dir/.opencode/command"
|
mkdir -p "$base_dir/.opencode/command"
|
||||||
generate_commands opencode md "\$ARGUMENTS" "$base_dir/.opencode/command" "$script" ;;
|
generate_commands opencode md "\$ARGUMENTS" "$base_dir/.opencode/command" "$script" ;;
|
||||||
|
windsurf)
|
||||||
|
mkdir -p "$base_dir/.windsurf/workflows"
|
||||||
|
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
||||||
|
codex)
|
||||||
|
mkdir -p "$base_dir/.codex/commands"
|
||||||
|
generate_commands codex md "\$ARGUMENTS" "$base_dir/.codex/commands" "$script" ;;
|
||||||
esac
|
esac
|
||||||
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
||||||
echo "Created spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
echo "Created spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine agent list
|
# Determine agent list
|
||||||
ALL_AGENTS=(claude gemini copilot cursor qwen opencode)
|
ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf codex)
|
||||||
ALL_SCRIPTS=(sh ps)
|
ALL_SCRIPTS=(sh ps)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
234
AGENTS.md
Normal file
234
AGENTS.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## About Spec Kit and Specify
|
||||||
|
|
||||||
|
**GitHub Spec Kit** is a comprehensive toolkit for implementing Spec-Driven Development (SDD) - a methodology that emphasizes creating clear specifications before implementation. The toolkit includes templates, scripts, and workflows that guide development teams through a structured approach to building software.
|
||||||
|
|
||||||
|
**Specify CLI** is the command-line interface that bootstraps projects with the Spec Kit framework. It sets up the necessary directory structures, templates, and AI agent integrations to support the Spec-Driven Development workflow.
|
||||||
|
|
||||||
|
The toolkit supports multiple AI coding assistants, allowing teams to use their preferred tools while maintaining consistent project structure and development practices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General practices
|
||||||
|
|
||||||
|
- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.
|
||||||
|
|
||||||
|
## Adding New Agent Support
|
||||||
|
|
||||||
|
This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
Specify supports multiple AI agents by generating agent-specific command files and directory structures when initializing projects. Each agent has its own conventions for:
|
||||||
|
|
||||||
|
- **Command file formats** (Markdown, TOML, etc.)
|
||||||
|
- **Directory structures** (`.claude/commands/`, `.windsurf/workflows/`, etc.)
|
||||||
|
- **Command invocation patterns** (slash commands, CLI tools, etc.)
|
||||||
|
- **Argument passing conventions** (`$ARGUMENTS`, `{{args}}`, etc.)
|
||||||
|
|
||||||
|
### Current Supported Agents
|
||||||
|
|
||||||
|
| Agent | Directory | Format | CLI Tool | Description |
|
||||||
|
|-------|-----------|---------|----------|-------------|
|
||||||
|
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
||||||
|
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
|
||||||
|
| **GitHub Copilot** | `.github/prompts/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
|
||||||
|
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
|
||||||
|
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
|
||||||
|
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
||||||
|
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
||||||
|
|
||||||
|
### Step-by-Step Integration Guide
|
||||||
|
|
||||||
|
Follow these steps to add a new agent (using Windsurf as an example):
|
||||||
|
|
||||||
|
#### 1. Update AI_CHOICES Constant
|
||||||
|
|
||||||
|
Add the new agent to the `AI_CHOICES` dictionary in `src/specify_cli/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
AI_CHOICES = {
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"gemini": "Gemini CLI",
|
||||||
|
"cursor": "Cursor",
|
||||||
|
"qwen": "Qwen Code",
|
||||||
|
"opencode": "opencode",
|
||||||
|
"windsurf": "Windsurf" # Add new agent here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Update CLI Help Text
|
||||||
|
|
||||||
|
Update all help text and examples to include the new agent:
|
||||||
|
|
||||||
|
- Command option help: `--ai` parameter description
|
||||||
|
- Function docstrings and examples
|
||||||
|
- Error messages with agent lists
|
||||||
|
|
||||||
|
#### 3. Update Release Package Script
|
||||||
|
|
||||||
|
Modify `.github/workflows/scripts/create-release-packages.sh`:
|
||||||
|
|
||||||
|
##### Add to ALL_AGENTS array:
|
||||||
|
```bash
|
||||||
|
ALL_AGENTS=(claude gemini copilot cursor qwen opencode windsurf)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Add case statement for directory structure:
|
||||||
|
```bash
|
||||||
|
case $agent in
|
||||||
|
# ... existing cases ...
|
||||||
|
windsurf)
|
||||||
|
mkdir -p "$base_dir/.windsurf/workflows"
|
||||||
|
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Update Agent Context Scripts
|
||||||
|
|
||||||
|
##### Bash script (`scripts/bash/update-agent-context.sh`):
|
||||||
|
|
||||||
|
Add file variable:
|
||||||
|
```bash
|
||||||
|
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to case statement:
|
||||||
|
```bash
|
||||||
|
case "$AGENT_TYPE" in
|
||||||
|
# ... existing cases ...
|
||||||
|
windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;;
|
||||||
|
"")
|
||||||
|
# ... existing checks ...
|
||||||
|
[ -f "$WINDSURF_FILE" ] && update_agent_file "$WINDSURF_FILE" "Windsurf";
|
||||||
|
# Update default creation condition
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
##### PowerShell script (`scripts/powershell/update-agent-context.ps1`):
|
||||||
|
|
||||||
|
Add file variable:
|
||||||
|
```powershell
|
||||||
|
$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to switch statement:
|
||||||
|
```powershell
|
||||||
|
switch ($AgentType) {
|
||||||
|
# ... existing cases ...
|
||||||
|
'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' }
|
||||||
|
'' {
|
||||||
|
foreach ($pair in @(
|
||||||
|
# ... existing pairs ...
|
||||||
|
@{file=$windsurfFile; name='Windsurf'}
|
||||||
|
)) {
|
||||||
|
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
|
||||||
|
}
|
||||||
|
# Update default creation condition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Update CLI Tool Checks (Optional)
|
||||||
|
|
||||||
|
For agents that require CLI tools, add checks in the `check()` command and agent validation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In check() command
|
||||||
|
tracker.add("windsurf", "Windsurf IDE (optional)")
|
||||||
|
windsurf_ok = check_tool_for_tracker("windsurf", "https://windsurf.com/", tracker)
|
||||||
|
|
||||||
|
# In init validation (only if CLI tool required)
|
||||||
|
elif selected_ai == "windsurf":
|
||||||
|
if not check_tool("windsurf", "Install from: https://windsurf.com/"):
|
||||||
|
console.print("[red]Error:[/red] Windsurf CLI is required for Windsurf projects")
|
||||||
|
agent_tool_missing = True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Skip CLI checks for IDE-based agents (Copilot, Windsurf).
|
||||||
|
|
||||||
|
## Agent Categories
|
||||||
|
|
||||||
|
### CLI-Based Agents
|
||||||
|
Require a command-line tool to be installed:
|
||||||
|
- **Claude Code**: `claude` CLI
|
||||||
|
- **Gemini CLI**: `gemini` CLI
|
||||||
|
- **Cursor**: `cursor-agent` CLI
|
||||||
|
- **Qwen Code**: `qwen` CLI
|
||||||
|
- **opencode**: `opencode` CLI
|
||||||
|
|
||||||
|
### IDE-Based Agents
|
||||||
|
Work within integrated development environments:
|
||||||
|
- **GitHub Copilot**: Built into VS Code/compatible editors
|
||||||
|
- **Windsurf**: Built into Windsurf IDE
|
||||||
|
|
||||||
|
## Command File Formats
|
||||||
|
|
||||||
|
### Markdown Format
|
||||||
|
Used by: Claude, Cursor, opencode, Windsurf
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: "Command description"
|
||||||
|
---
|
||||||
|
|
||||||
|
Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
||||||
|
```
|
||||||
|
|
||||||
|
### TOML Format
|
||||||
|
Used by: Gemini, Qwen
|
||||||
|
|
||||||
|
```toml
|
||||||
|
description = "Command description"
|
||||||
|
|
||||||
|
prompt = """
|
||||||
|
Command content with {SCRIPT} and {{args}} placeholders.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Conventions
|
||||||
|
|
||||||
|
- **CLI agents**: Usually `.<agent-name>/commands/`
|
||||||
|
- **IDE agents**: Follow IDE-specific patterns:
|
||||||
|
- Copilot: `.github/prompts/`
|
||||||
|
- Cursor: `.cursor/commands/`
|
||||||
|
- Windsurf: `.windsurf/workflows/`
|
||||||
|
|
||||||
|
## Argument Patterns
|
||||||
|
|
||||||
|
Different agents use different argument placeholders:
|
||||||
|
- **Markdown/prompt-based**: `$ARGUMENTS`
|
||||||
|
- **TOML-based**: `{{args}}`
|
||||||
|
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
||||||
|
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
||||||
|
|
||||||
|
## Testing New Agent Integration
|
||||||
|
|
||||||
|
1. **Build test**: Run package creation script locally
|
||||||
|
2. **CLI test**: Test `specify init --ai <agent>` command
|
||||||
|
3. **File generation**: Verify correct directory structure and files
|
||||||
|
4. **Command validation**: Ensure generated commands work with the agent
|
||||||
|
5. **Context update**: Test agent context update scripts
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
1. **Forgetting update scripts**: Both bash and PowerShell scripts must be updated
|
||||||
|
2. **Missing CLI checks**: Only add for agents that actually have CLI tools
|
||||||
|
3. **Wrong argument format**: Use correct placeholder format for each agent type
|
||||||
|
4. **Directory naming**: Follow agent-specific conventions exactly
|
||||||
|
5. **Help text inconsistency**: Update all user-facing text consistently
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
When adding new agents:
|
||||||
|
- Consider the agent's native command/workflow patterns
|
||||||
|
- Ensure compatibility with the Spec-Driven Development process
|
||||||
|
- Document any special requirements or limitations
|
||||||
|
- Update this guide with lessons learned
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This documentation should be updated whenever new agents are added to maintain accuracy and completeness.*
|
||||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -5,7 +5,36 @@ All notable changes to the Specify CLI will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.0.11] - 2025-09-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Codex CLI support (thank you [@honjo-hiroaki-gtt](https://github.com/honjo-hiroaki-gtt) for the contribution in [#14](https://github.com/github/spec-kit/pull/14))
|
||||||
|
- Codex-aware context update tooling (Bash and PowerShell) so feature plans refresh `AGENTS.md` alongside existing assistants without manual edits.
|
||||||
|
|
||||||
|
## [0.0.10] - 2025-09-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Addressed [#378](https://github.com/github/spec-kit/issues/378) where a GitHub token may be attached to the request when it was empty.
|
||||||
|
|
||||||
|
## [0.0.9] - 2025-09-19
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved agent selector UI with cyan highlighting for agent keys and gray parentheses for full names
|
||||||
|
|
||||||
|
## [0.0.8] - 2025-09-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Windsurf IDE support as additional AI assistant option (thank you [@raedkit](https://github.com/raedkit) for the work in [#151](https://github.com/github/spec-kit/pull/151))
|
||||||
|
- GitHub token support for API requests to handle corporate environments and rate limiting (contributed by [@zryfish](https://github.com/@zryfish) in [#243](https://github.com/github/spec-kit/pull/243))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated README with Windsurf examples and GitHub token usage
|
||||||
|
- Enhanced release workflow to include Windsurf templates
|
||||||
|
|
||||||
## [0.0.7] - 2025-09-18
|
## [0.0.7] - 2025-09-18
|
||||||
|
|
||||||
@@ -40,4 +69,3 @@ N/A
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
N/A
|
N/A
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -101,20 +101,21 @@ The `specify` command supports the following options:
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
|-------------|----------------------------------------------------------------|
|
|-------------|----------------------------------------------------------------|
|
||||||
| `init` | Initialize a new Specify project from the latest template |
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `opencode`, `cursor-agent`) |
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`) |
|
||||||
|
|
||||||
### `specify init` Arguments & Options
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
| Argument/Option | Type | Description |
|
| Argument/Option | Type | Description |
|
||||||
|------------------------|----------|------------------------------------------------------------------------------|
|
|------------------------|----------|------------------------------------------------------------------------------|
|
||||||
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`) |
|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`) |
|
||||||
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `opencode`, or `cursor` |
|
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor`, `qwen`, `opencode`, or `windsurf` |
|
||||||
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||||
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
||||||
| `--no-git` | Flag | Skip git repository initialization |
|
| `--no-git` | Flag | Skip git repository initialization |
|
||||||
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
|
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
|
||||||
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
||||||
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
||||||
|
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@@ -128,6 +129,9 @@ specify init my-project --ai claude
|
|||||||
# Initialize with Cursor support
|
# Initialize with Cursor support
|
||||||
specify init my-project --ai cursor
|
specify init my-project --ai cursor
|
||||||
|
|
||||||
|
# Initialize with Windsurf support
|
||||||
|
specify init my-project --ai windsurf
|
||||||
|
|
||||||
# Initialize with PowerShell scripts (Windows/cross-platform)
|
# Initialize with PowerShell scripts (Windows/cross-platform)
|
||||||
specify init my-project --ai copilot --script ps
|
specify init my-project --ai copilot --script ps
|
||||||
|
|
||||||
@@ -140,6 +144,9 @@ specify init my-project --ai gemini --no-git
|
|||||||
# Enable debug output for troubleshooting
|
# Enable debug output for troubleshooting
|
||||||
specify init my-project --ai claude --debug
|
specify init my-project --ai claude --debug
|
||||||
|
|
||||||
|
# Use GitHub token for API requests (helpful for corporate environments)
|
||||||
|
specify init my-project --ai claude --github-token ghp_your_token_here
|
||||||
|
|
||||||
# Check system requirements
|
# Check system requirements
|
||||||
specify check
|
specify check
|
||||||
```
|
```
|
||||||
@@ -202,7 +209,7 @@ Our research and experimentation focus on:
|
|||||||
## 🔧 Prerequisites
|
## 🔧 Prerequisites
|
||||||
|
|
||||||
- **Linux/macOS** (or WSL2 on Windows)
|
- **Linux/macOS** (or WSL2 on Windows)
|
||||||
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Cursor](https://cursor.sh/), [Qwen CLI](https://github.com/QwenLM/qwen-code) or [opencode](https://opencode.ai/)
|
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Cursor](https://cursor.sh/), [Qwen CLI](https://github.com/QwenLM/qwen-code), [opencode](https://opencode.ai/), [Codex CLI](https://github.com/openai/codex), or [Windsurf](https://windsurf.com/)
|
||||||
- [uv](https://docs.astral.sh/uv/) for package management
|
- [uv](https://docs.astral.sh/uv/) for package management
|
||||||
- [Python 3.11+](https://www.python.org/downloads/)
|
- [Python 3.11+](https://www.python.org/downloads/)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
@@ -239,13 +246,17 @@ You will be prompted to select the AI agent you are using. You can also proactiv
|
|||||||
specify init <project_name> --ai claude
|
specify init <project_name> --ai claude
|
||||||
specify init <project_name> --ai gemini
|
specify init <project_name> --ai gemini
|
||||||
specify init <project_name> --ai copilot
|
specify init <project_name> --ai copilot
|
||||||
|
specify init <project_name> --ai cursor
|
||||||
specify init <project_name> --ai qwen
|
specify init <project_name> --ai qwen
|
||||||
specify init <project_name> --ai opencode
|
specify init <project_name> --ai opencode
|
||||||
|
specify init <project_name> --ai codex
|
||||||
|
specify init <project_name> --ai windsurf
|
||||||
# Or in current directory:
|
# Or in current directory:
|
||||||
specify init --here --ai claude
|
specify init --here --ai claude
|
||||||
|
specify init --here --ai codex
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI will check if you have Claude Code, Gemini CLI, Qwen CLI or opencode installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, or Codex CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify init <project_name> --ai claude --ignore-agent-tools
|
specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.0.7"
|
version = "0.0.10"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -18,7 +18,21 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
|
# to the script location so the workflow still functions in repositories that
|
||||||
|
# were initialised with --no-git.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FALLBACK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
HAS_GIT=true
|
||||||
|
else
|
||||||
|
REPO_ROOT="$FALLBACK_ROOT"
|
||||||
|
HAS_GIT=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
mkdir -p "$SPECS_DIR"
|
mkdir -p "$SPECS_DIR"
|
||||||
|
|
||||||
@@ -40,7 +54,11 @@ BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/
|
|||||||
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
|
||||||
|
|
||||||
|
if [ "$HAS_GIT" = true ]; then
|
||||||
git checkout -b "$BRANCH_NAME"
|
git checkout -b "$BRANCH_NAME"
|
||||||
|
else
|
||||||
|
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel)
|
|||||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
|
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
|
||||||
NEW_PLAN="$FEATURE_DIR/plan.md"
|
NEW_PLAN="$FEATURE_DIR/plan.md"
|
||||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"; CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"; QWEN_FILE="$REPO_ROOT/QWEN.md"; AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"; CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"; QWEN_FILE="$REPO_ROOT/QWEN.md"; AGENTS_FILE="$REPO_ROOT/AGENTS.md"; WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||||
AGENT_TYPE="$1"
|
AGENT_TYPE="$1"
|
||||||
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
|
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
|
||||||
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||||
@@ -54,13 +54,16 @@ case "$AGENT_TYPE" in
|
|||||||
cursor) update_agent_file "$CURSOR_FILE" "Cursor IDE" ;;
|
cursor) update_agent_file "$CURSOR_FILE" "Cursor IDE" ;;
|
||||||
qwen) update_agent_file "$QWEN_FILE" "Qwen Code" ;;
|
qwen) update_agent_file "$QWEN_FILE" "Qwen Code" ;;
|
||||||
opencode) update_agent_file "$AGENTS_FILE" "opencode" ;;
|
opencode) update_agent_file "$AGENTS_FILE" "opencode" ;;
|
||||||
|
codex) update_agent_file "$AGENTS_FILE" "Codex CLI" ;;
|
||||||
|
windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;;
|
||||||
"") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; \
|
"") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; \
|
||||||
[ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; \
|
[ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; \
|
||||||
[ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; \
|
[ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; \
|
||||||
[ -f "$CURSOR_FILE" ] && update_agent_file "$CURSOR_FILE" "Cursor IDE"; \
|
[ -f "$CURSOR_FILE" ] && update_agent_file "$CURSOR_FILE" "Cursor IDE"; \
|
||||||
[ -f "$QWEN_FILE" ] && update_agent_file "$QWEN_FILE" "Qwen Code"; \
|
[ -f "$QWEN_FILE" ] && update_agent_file "$QWEN_FILE" "Qwen Code"; \
|
||||||
[ -f "$AGENTS_FILE" ] && update_agent_file "$AGENTS_FILE" "opencode"; \
|
[ -f "$AGENTS_FILE" ] && update_agent_file "$AGENTS_FILE" "Codex/opencode"; \
|
||||||
if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ] && [ ! -f "$CURSOR_FILE" ] && [ ! -f "$QWEN_FILE" ] && [ ! -f "$AGENTS_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
|
[ -f "$WINDSURF_FILE" ] && update_agent_file "$WINDSURF_FILE" "Windsurf"; \
|
||||||
*) echo "ERROR: Unknown agent type '$AGENT_TYPE' (expected claude|gemini|copilot|cursor|qwen|opencode)"; exit 1 ;;
|
if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ] && [ ! -f "$CURSOR_FILE" ] && [ ! -f "$QWEN_FILE" ] && [ ! -f "$AGENTS_FILE" ] && [ ! -f "$WINDSURF_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
|
||||||
|
*) echo "ERROR: Unknown agent type '$AGENT_TYPE' (expected claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf)"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode]"
|
echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf]"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ $copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md'
|
|||||||
$cursorFile = Join-Path $repoRoot '.cursor/rules/specify-rules.mdc'
|
$cursorFile = Join-Path $repoRoot '.cursor/rules/specify-rules.mdc'
|
||||||
$qwenFile = Join-Path $repoRoot 'QWEN.md'
|
$qwenFile = Join-Path $repoRoot 'QWEN.md'
|
||||||
$agentsFile = Join-Path $repoRoot 'AGENTS.md'
|
$agentsFile = Join-Path $repoRoot 'AGENTS.md'
|
||||||
|
$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md'
|
||||||
|
|
||||||
Write-Output "=== Updating agent context files for feature $currentBranch ==="
|
Write-Output "=== Updating agent context files for feature $currentBranch ==="
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ switch ($AgentType) {
|
|||||||
'cursor' { Update-AgentFile $cursorFile 'Cursor IDE' }
|
'cursor' { Update-AgentFile $cursorFile 'Cursor IDE' }
|
||||||
'qwen' { Update-AgentFile $qwenFile 'Qwen Code' }
|
'qwen' { Update-AgentFile $qwenFile 'Qwen Code' }
|
||||||
'opencode' { Update-AgentFile $agentsFile 'opencode' }
|
'opencode' { Update-AgentFile $agentsFile 'opencode' }
|
||||||
|
'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' }
|
||||||
|
'codex' { Update-AgentFile $agentsFile 'Codex CLI' }
|
||||||
'' {
|
'' {
|
||||||
foreach ($pair in @(
|
foreach ($pair in @(
|
||||||
@{file=$claudeFile; name='Claude Code'},
|
@{file=$claudeFile; name='Claude Code'},
|
||||||
@@ -82,16 +85,18 @@ switch ($AgentType) {
|
|||||||
@{file=$copilotFile; name='GitHub Copilot'},
|
@{file=$copilotFile; name='GitHub Copilot'},
|
||||||
@{file=$cursorFile; name='Cursor IDE'},
|
@{file=$cursorFile; name='Cursor IDE'},
|
||||||
@{file=$qwenFile; name='Qwen Code'},
|
@{file=$qwenFile; name='Qwen Code'},
|
||||||
@{file=$agentsFile; name='opencode'}
|
@{file=$agentsFile; name='opencode'},
|
||||||
|
@{file=$windsurfFile; name='Windsurf'},
|
||||||
|
@{file=$agentsFile; name='Codex CLI'}
|
||||||
)) {
|
)) {
|
||||||
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
|
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
|
||||||
}
|
}
|
||||||
if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile) -and -not (Test-Path $cursorFile) -and -not (Test-Path $qwenFile) -and -not (Test-Path $agentsFile)) {
|
if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile) -and -not (Test-Path $cursorFile) -and -not (Test-Path $qwenFile) -and -not (Test-Path $agentsFile) -and -not (Test-Path $windsurfFile)) {
|
||||||
Write-Output 'No agent context files found. Creating Claude Code context file by default.'
|
Write-Output 'No agent context files found. Creating Claude Code context file by default.'
|
||||||
Update-AgentFile $claudeFile 'Claude Code'
|
Update-AgentFile $claudeFile 'Claude Code'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, cursor, qwen, opencode or leave empty for all."; exit 1 }
|
Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, cursor, qwen, opencode, windsurf, codex or leave empty for all."; exit 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ''
|
Write-Output ''
|
||||||
@@ -101,4 +106,4 @@ if ($newFramework) { Write-Output "- Added framework: $newFramework" }
|
|||||||
if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" }
|
if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" }
|
||||||
|
|
||||||
Write-Output ''
|
Write-Output ''
|
||||||
Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot|cursor|qwen|opencode]'
|
Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot|cursor|qwen|opencode|windsurf|codex]'
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ import truststore
|
|||||||
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
client = httpx.Client(verify=ssl_context)
|
client = httpx.Client(verify=ssl_context)
|
||||||
|
|
||||||
|
def _github_token(cli_token: str | None = None) -> str | None:
|
||||||
|
"""Return sanitized GitHub token (cli arg takes precedence) or None."""
|
||||||
|
return ((cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") or "").strip()) or None
|
||||||
|
|
||||||
|
def _github_auth_headers(cli_token: str | None = None) -> dict:
|
||||||
|
"""Return Authorization header dict only when a non-empty token exists."""
|
||||||
|
token = _github_token(cli_token)
|
||||||
|
return {"Authorization": f"Bearer {token}"} if token else {}
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
AI_CHOICES = {
|
AI_CHOICES = {
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
@@ -59,7 +68,9 @@ AI_CHOICES = {
|
|||||||
"gemini": "Gemini CLI",
|
"gemini": "Gemini CLI",
|
||||||
"cursor": "Cursor",
|
"cursor": "Cursor",
|
||||||
"qwen": "Qwen Code",
|
"qwen": "Qwen Code",
|
||||||
"opencode": "opencode"
|
"opencode": "opencode",
|
||||||
|
"codex": "Codex CLI",
|
||||||
|
"windsurf": "Windsurf",
|
||||||
}
|
}
|
||||||
# Add script type choices
|
# Add script type choices
|
||||||
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
||||||
@@ -128,7 +139,7 @@ class StepTracker:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
tree = Tree(f"[bold cyan]{self.title}[/bold cyan]", guide_style="grey50")
|
tree = Tree(f"[cyan]{self.title}[/cyan]", guide_style="grey50")
|
||||||
for step in self.steps:
|
for step in self.steps:
|
||||||
label = step["label"]
|
label = step["label"]
|
||||||
detail_text = step["detail"].strip() if step["detail"] else ""
|
detail_text = step["detail"].strip() if step["detail"] else ""
|
||||||
@@ -221,14 +232,14 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def
|
|||||||
def create_selection_panel():
|
def create_selection_panel():
|
||||||
"""Create the selection panel with current selection highlighted."""
|
"""Create the selection panel with current selection highlighted."""
|
||||||
table = Table.grid(padding=(0, 2))
|
table = Table.grid(padding=(0, 2))
|
||||||
table.add_column(style="bright_cyan", justify="left", width=3)
|
table.add_column(style="cyan", justify="left", width=3)
|
||||||
table.add_column(style="white", justify="left")
|
table.add_column(style="white", justify="left")
|
||||||
|
|
||||||
for i, key in enumerate(option_keys):
|
for i, key in enumerate(option_keys):
|
||||||
if i == selected_index:
|
if i == selected_index:
|
||||||
table.add_row("▶", f"[bright_cyan]{key}: {options[key]}[/bright_cyan]")
|
table.add_row("▶", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
||||||
else:
|
else:
|
||||||
table.add_row(" ", f"[white]{key}: {options[key]}[/white]")
|
table.add_row(" ", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
||||||
|
|
||||||
table.add_row("", "")
|
table.add_row("", "")
|
||||||
table.add_row("", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]")
|
table.add_row("", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]")
|
||||||
@@ -418,7 +429,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
|||||||
os.chdir(original_cwd)
|
os.chdir(original_cwd)
|
||||||
|
|
||||||
|
|
||||||
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False) -> Tuple[Path, dict]:
|
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, github_token: str = None) -> Tuple[Path, dict]:
|
||||||
repo_owner = "github"
|
repo_owner = "github"
|
||||||
repo_name = "spec-kit"
|
repo_name = "spec-kit"
|
||||||
if client is None:
|
if client is None:
|
||||||
@@ -429,7 +440,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = client.get(api_url, timeout=30, follow_redirects=True)
|
response = client.get(
|
||||||
|
api_url,
|
||||||
|
timeout=30,
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=_github_auth_headers(github_token),
|
||||||
|
)
|
||||||
status = response.status_code
|
status = response.status_code
|
||||||
if status != 200:
|
if status != 200:
|
||||||
msg = f"GitHub API returned {status} for {api_url}"
|
msg = f"GitHub API returned {status} for {api_url}"
|
||||||
@@ -446,20 +462,21 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Find the template asset for the specified AI assistant
|
# Find the template asset for the specified AI assistant
|
||||||
|
assets = release_data.get("assets", [])
|
||||||
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
||||||
matching_assets = [
|
matching_assets = [
|
||||||
asset for asset in release_data.get("assets", [])
|
asset for asset in assets
|
||||||
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
||||||
]
|
]
|
||||||
|
|
||||||
if not matching_assets:
|
asset = matching_assets[0] if matching_assets else None
|
||||||
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', [])]
|
if asset is None:
|
||||||
|
console.print(f"[red]No matching release asset found[/red] for [bold]{ai_assistant}[/bold] (expected pattern: [bold]{pattern}[/bold])")
|
||||||
|
asset_names = [a.get('name', '?') for a in assets]
|
||||||
console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
|
console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Use the first matching asset
|
|
||||||
asset = matching_assets[0]
|
|
||||||
download_url = asset["browser_download_url"]
|
download_url = asset["browser_download_url"]
|
||||||
filename = asset["name"]
|
filename = asset["name"]
|
||||||
file_size = asset["size"]
|
file_size = asset["size"]
|
||||||
@@ -469,13 +486,18 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
console.print(f"[cyan]Size:[/cyan] {file_size:,} bytes")
|
console.print(f"[cyan]Size:[/cyan] {file_size:,} bytes")
|
||||||
console.print(f"[cyan]Release:[/cyan] {release_data['tag_name']}")
|
console.print(f"[cyan]Release:[/cyan] {release_data['tag_name']}")
|
||||||
|
|
||||||
# Download the file
|
|
||||||
zip_path = download_dir / filename
|
zip_path = download_dir / filename
|
||||||
if verbose:
|
if verbose:
|
||||||
console.print(f"[cyan]Downloading template...[/cyan]")
|
console.print(f"[cyan]Downloading template...[/cyan]")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response:
|
with client.stream(
|
||||||
|
"GET",
|
||||||
|
download_url,
|
||||||
|
timeout=60,
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=_github_auth_headers(github_token),
|
||||||
|
) as response:
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
body_sample = response.text[:400]
|
body_sample = response.text[:400]
|
||||||
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
|
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
|
||||||
@@ -519,7 +541,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
return zip_path, metadata
|
return zip_path, metadata
|
||||||
|
|
||||||
|
|
||||||
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False) -> 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, github_token: str = None) -> Path:
|
||||||
"""Download the latest release and extract it to create a new project.
|
"""Download the latest release and extract it to create a new project.
|
||||||
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
|
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
|
||||||
"""
|
"""
|
||||||
@@ -536,7 +558,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
verbose=verbose and tracker is None,
|
verbose=verbose and tracker is None,
|
||||||
show_progress=(tracker is None),
|
show_progress=(tracker is None),
|
||||||
client=client,
|
client=client,
|
||||||
debug=debug
|
debug=debug,
|
||||||
|
github_token=github_token
|
||||||
)
|
)
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
||||||
@@ -720,24 +743,24 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
|
|||||||
for f in failures:
|
for f in failures:
|
||||||
console.print(f" - {f}")
|
console.print(f" - {f}")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def init(
|
def init(
|
||||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
|
||||||
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, qwen or opencode"),
|
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor, qwen, opencode, codex, or windsurf"),
|
||||||
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
||||||
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
||||||
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
||||||
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
|
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
|
||||||
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
|
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
|
||||||
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
|
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
|
||||||
|
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize a new Specify project from the latest template.
|
Initialize a new Specify project from the latest template.
|
||||||
|
|
||||||
This command will:
|
This command will:
|
||||||
1. Check that required tools are installed (git is optional)
|
1. Check that required tools are installed (git is optional)
|
||||||
2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code or opencode)
|
2. Let you choose your AI assistant (Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, or Windsurf)
|
||||||
3. Download the appropriate template from GitHub
|
3. Download the appropriate template from GitHub
|
||||||
4. Extract the template to a new project directory or current directory
|
4. Extract the template to a new project directory or current directory
|
||||||
5. Initialize a fresh git repository (if not --no-git and no existing repo)
|
5. Initialize a fresh git repository (if not --no-git and no existing repo)
|
||||||
@@ -751,8 +774,11 @@ def init(
|
|||||||
specify init my-project --ai cursor
|
specify init my-project --ai cursor
|
||||||
specify init my-project --ai qwen
|
specify init my-project --ai qwen
|
||||||
specify init my-project --ai opencode
|
specify init my-project --ai opencode
|
||||||
|
specify init my-project --ai codex
|
||||||
|
specify init my-project --ai windsurf
|
||||||
specify init --ignore-agent-tools my-project
|
specify init --ignore-agent-tools my-project
|
||||||
specify init --here --ai claude
|
specify init --here --ai claude
|
||||||
|
specify init --here --ai codex
|
||||||
specify init --here
|
specify init --here
|
||||||
"""
|
"""
|
||||||
# Show banner first
|
# Show banner first
|
||||||
@@ -790,12 +816,21 @@ def init(
|
|||||||
console.print(f"[red]Error:[/red] Directory '{project_name}' already exists")
|
console.print(f"[red]Error:[/red] Directory '{project_name}' already exists")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
console.print(Panel.fit(
|
# Create formatted setup info with column alignment
|
||||||
"[bold cyan]Specify Project Setup[/bold cyan]\n"
|
current_dir = Path.cwd()
|
||||||
f"{'Initializing in current directory:' if here else 'Creating new project:'} [green]{project_path.name}[/green]"
|
|
||||||
+ (f"\n[dim]Path: {project_path}[/dim]" if here else ""),
|
setup_lines = [
|
||||||
border_style="cyan"
|
"[cyan]Specify Project Setup[/cyan]",
|
||||||
))
|
"",
|
||||||
|
f"{'Project':<15} [green]{project_path.name}[/green]",
|
||||||
|
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add target path only if different from working dir
|
||||||
|
if not here:
|
||||||
|
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
||||||
|
|
||||||
|
console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2)))
|
||||||
|
|
||||||
# Check git only if we might need it (not --no-git)
|
# Check git only if we might need it (not --no-git)
|
||||||
# Only set to True if the user wants it and the tool is available
|
# Only set to True if the user wants it and the tool is available
|
||||||
@@ -838,6 +873,10 @@ def init(
|
|||||||
if not check_tool("opencode", "Install from: https://opencode.ai"):
|
if not check_tool("opencode", "Install from: https://opencode.ai"):
|
||||||
console.print("[red]Error:[/red] opencode CLI is required for opencode projects")
|
console.print("[red]Error:[/red] opencode CLI is required for opencode projects")
|
||||||
agent_tool_missing = True
|
agent_tool_missing = True
|
||||||
|
elif selected_ai == "codex":
|
||||||
|
if not check_tool("codex", "Install from: https://github.com/openai/codex"):
|
||||||
|
console.print("[red]Error:[/red] Codex CLI is required for Codex projects")
|
||||||
|
agent_tool_missing = True
|
||||||
# GitHub Copilot and Cursor checks are not needed as they're typically available in supported IDEs
|
# GitHub Copilot and Cursor checks are not needed as they're typically available in supported IDEs
|
||||||
|
|
||||||
if agent_tool_missing:
|
if agent_tool_missing:
|
||||||
@@ -897,7 +936,7 @@ def init(
|
|||||||
local_ssl_context = ssl_context if verify else False
|
local_ssl_context = ssl_context if verify else False
|
||||||
local_client = httpx.Client(verify=local_ssl_context)
|
local_client = httpx.Client(verify=local_ssl_context)
|
||||||
|
|
||||||
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug)
|
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
|
||||||
|
|
||||||
# Ensure scripts are executable (POSIX)
|
# Ensure scripts are executable (POSIX)
|
||||||
ensure_executable_scripts(project_path, tracker=tracker)
|
ensure_executable_scripts(project_path, tracker=tracker)
|
||||||
@@ -951,11 +990,11 @@ def init(
|
|||||||
step_num = 2
|
step_num = 2
|
||||||
|
|
||||||
steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:")
|
steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:")
|
||||||
steps_lines.append(" 2.1 [bold cyan]/constitution[/] - Establish project principles")
|
steps_lines.append(" 2.1 [cyan]/constitution[/] - Establish project principles")
|
||||||
steps_lines.append(" 2.2 [bold cyan]/specify[/] - Create specifications")
|
steps_lines.append(" 2.2 [cyan]/specify[/] - Create specifications")
|
||||||
steps_lines.append(" 2.3 [bold cyan]/plan[/] - Create implementation plans")
|
steps_lines.append(" 2.3 [cyan]/plan[/] - Create implementation plans")
|
||||||
steps_lines.append(" 2.4 [bold cyan]/tasks[/] - Generate actionable tasks")
|
steps_lines.append(" 2.4 [cyan]/tasks[/] - Generate actionable tasks")
|
||||||
steps_lines.append(" 2.5 [bold cyan]/implement[/] - Execute implementation")
|
steps_lines.append(" 2.5 [cyan]/implement[/] - Execute implementation")
|
||||||
|
|
||||||
steps_panel = Panel("\n".join(steps_lines), title="Next steps", border_style="cyan", padding=(1,2))
|
steps_panel = Panel("\n".join(steps_lines), title="Next steps", border_style="cyan", padding=(1,2))
|
||||||
console.print()
|
console.print()
|
||||||
@@ -975,7 +1014,9 @@ def check():
|
|||||||
tracker.add("qwen", "Qwen Code CLI")
|
tracker.add("qwen", "Qwen Code CLI")
|
||||||
tracker.add("code", "VS Code (for GitHub Copilot)")
|
tracker.add("code", "VS Code (for GitHub Copilot)")
|
||||||
tracker.add("cursor-agent", "Cursor IDE agent (optional)")
|
tracker.add("cursor-agent", "Cursor IDE agent (optional)")
|
||||||
|
tracker.add("windsurf", "Windsurf IDE (optional)")
|
||||||
tracker.add("opencode", "opencode")
|
tracker.add("opencode", "opencode")
|
||||||
|
tracker.add("codex", "Codex CLI")
|
||||||
|
|
||||||
git_ok = check_tool_for_tracker("git", "https://git-scm.com/downloads", tracker)
|
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)
|
claude_ok = check_tool_for_tracker("claude", "https://docs.anthropic.com/en/docs/claude-code/setup", tracker)
|
||||||
@@ -985,7 +1026,9 @@ def check():
|
|||||||
if not code_ok:
|
if not code_ok:
|
||||||
code_ok = check_tool_for_tracker("code-insiders", "https://code.visualstudio.com/insiders/", tracker)
|
code_ok = check_tool_for_tracker("code-insiders", "https://code.visualstudio.com/insiders/", tracker)
|
||||||
cursor_ok = check_tool_for_tracker("cursor-agent", "https://cursor.sh/", tracker)
|
cursor_ok = check_tool_for_tracker("cursor-agent", "https://cursor.sh/", tracker)
|
||||||
|
windsurf_ok = check_tool_for_tracker("windsurf", "https://windsurf.com/", tracker)
|
||||||
opencode_ok = check_tool_for_tracker("opencode", "https://opencode.ai/", tracker)
|
opencode_ok = check_tool_for_tracker("opencode", "https://opencode.ai/", tracker)
|
||||||
|
codex_ok = check_tool_for_tracker("codex", "https://github.com/openai/codex", tracker)
|
||||||
|
|
||||||
console.print(tracker.render())
|
console.print(tracker.render())
|
||||||
|
|
||||||
@@ -993,7 +1036,7 @@ def check():
|
|||||||
|
|
||||||
if not git_ok:
|
if not git_ok:
|
||||||
console.print("[dim]Tip: Install git for repository management[/dim]")
|
console.print("[dim]Tip: Install git for repository management[/dim]")
|
||||||
if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or opencode_ok):
|
if not (claude_ok or gemini_ok or cursor_ok or qwen_ok or windsurf_ok or opencode_ok or codex_ok):
|
||||||
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ scripts:
|
|||||||
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
||||||
---
|
---
|
||||||
|
|
||||||
Given the feature description provided as an argument, do this:
|
The text the user typed after `/specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
|
||||||
|
|
||||||
|
Given that feature description, do this:
|
||||||
|
|
||||||
1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
1. Run the script `{SCRIPT}` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
||||||
**IMPORTANT** You must only ever run this script once. The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for.
|
**IMPORTANT** You must only ever run this script once. The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for.
|
||||||
|
|||||||
Reference in New Issue
Block a user