mirror of
https://github.com/github/spec-kit.git
synced 2026-01-30 04:32:02 +00:00
Merge pull request #986 from github/localden/vscode
(feat): Enhancements to Spec Kit
This commit is contained in:
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,4 +1,6 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
416
.github/workflows/scripts/create-release-packages.ps1
vendored
Normal file
416
.github/workflows/scripts/create-release-packages.ps1
vendored
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
#requires -Version 7.0
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
create-release-packages.ps1 (workflow-local)
|
||||||
|
Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
Version string with leading 'v' (e.g., v0.2.0)
|
||||||
|
|
||||||
|
.PARAMETER Agents
|
||||||
|
Comma or space separated subset of agents to build (default: all)
|
||||||
|
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q
|
||||||
|
|
||||||
|
.PARAMETER Scripts
|
||||||
|
Comma or space separated subset of script types to build (default: both)
|
||||||
|
Valid scripts: sh, ps
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude,copilot -Scripts sh
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude -Scripts ps
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true, Position=0)]
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Agents = "",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Scripts = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Validate version format
|
||||||
|
if ($Version -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
|
Write-Error "Version must look like v0.0.0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Building release packages for $Version"
|
||||||
|
|
||||||
|
# Create and use .genreleases directory for all build artifacts
|
||||||
|
$GenReleasesDir = ".genreleases"
|
||||||
|
if (Test-Path $GenReleasesDir) {
|
||||||
|
Remove-Item -Path $GenReleasesDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
|
||||||
|
|
||||||
|
function Rewrite-Paths {
|
||||||
|
param([string]$Content)
|
||||||
|
|
||||||
|
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
|
||||||
|
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
|
||||||
|
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
|
||||||
|
return $Content
|
||||||
|
}
|
||||||
|
|
||||||
|
function Generate-Commands {
|
||||||
|
param(
|
||||||
|
[string]$Agent,
|
||||||
|
[string]$Extension,
|
||||||
|
[string]$ArgFormat,
|
||||||
|
[string]$OutputDir,
|
||||||
|
[string]$ScriptVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
|
|
||||||
|
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($template in $templates) {
|
||||||
|
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
||||||
|
|
||||||
|
# Read file content and normalize line endings
|
||||||
|
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
||||||
|
|
||||||
|
# Extract description from YAML frontmatter
|
||||||
|
$description = ""
|
||||||
|
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
||||||
|
$description = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract script command from YAML frontmatter
|
||||||
|
$scriptCommand = ""
|
||||||
|
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
||||||
|
$scriptCommand = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($scriptCommand)) {
|
||||||
|
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
|
||||||
|
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract agent_script command from YAML frontmatter if present
|
||||||
|
$agentScriptCommand = ""
|
||||||
|
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
||||||
|
$agentScriptCommand = $matches[1].Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Replace {SCRIPT} placeholder with the script command
|
||||||
|
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
||||||
|
|
||||||
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
||||||
|
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
||||||
|
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the scripts: and agent_scripts: sections from frontmatter
|
||||||
|
$lines = $body -split "`n"
|
||||||
|
$outputLines = @()
|
||||||
|
$inFrontmatter = $false
|
||||||
|
$skipScripts = $false
|
||||||
|
$dashCount = 0
|
||||||
|
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
if ($line -match '^---$') {
|
||||||
|
$outputLines += $line
|
||||||
|
$dashCount++
|
||||||
|
if ($dashCount -eq 1) {
|
||||||
|
$inFrontmatter = $true
|
||||||
|
} else {
|
||||||
|
$inFrontmatter = $false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inFrontmatter) {
|
||||||
|
if ($line -match '^(scripts|agent_scripts):$') {
|
||||||
|
$skipScripts = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) {
|
||||||
|
$skipScripts = $false
|
||||||
|
}
|
||||||
|
if ($skipScripts -and $line -match '^\s+') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputLines += $line
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $outputLines -join "`n"
|
||||||
|
|
||||||
|
# Apply other substitutions
|
||||||
|
$body = $body -replace '\{ARGS\}', $ArgFormat
|
||||||
|
$body = $body -replace '__AGENT__', $Agent
|
||||||
|
$body = Rewrite-Paths -Content $body
|
||||||
|
|
||||||
|
# Generate output file based on extension
|
||||||
|
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
|
||||||
|
|
||||||
|
switch ($Extension) {
|
||||||
|
'toml' {
|
||||||
|
$body = $body -replace '\\', '\\'
|
||||||
|
$output = "description = `"$description`"`n`nprompt = `"`"`"`n$body`n`"`"`""
|
||||||
|
Set-Content -Path $outputFile -Value $output -NoNewline
|
||||||
|
}
|
||||||
|
'md' {
|
||||||
|
Set-Content -Path $outputFile -Value $body -NoNewline
|
||||||
|
}
|
||||||
|
'agent.md' {
|
||||||
|
Set-Content -Path $outputFile -Value $body -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Generate-CopilotPrompts {
|
||||||
|
param(
|
||||||
|
[string]$AgentsDir,
|
||||||
|
[string]$PromptsDir
|
||||||
|
)
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
|
||||||
|
|
||||||
|
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($agentFile in $agentFiles) {
|
||||||
|
$basename = $agentFile.Name -replace '\.agent\.md$', ''
|
||||||
|
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
|
||||||
|
|
||||||
|
$content = @"
|
||||||
|
---
|
||||||
|
agent: $basename
|
||||||
|
---
|
||||||
|
"@
|
||||||
|
Set-Content -Path $promptFile -Value $content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-Variant {
|
||||||
|
param(
|
||||||
|
[string]$Agent,
|
||||||
|
[string]$Script
|
||||||
|
)
|
||||||
|
|
||||||
|
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
|
||||||
|
Write-Host "Building $Agent ($Script) package..."
|
||||||
|
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy base structure but filter scripts by variant
|
||||||
|
$specDir = Join-Path $baseDir ".specify"
|
||||||
|
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy memory directory
|
||||||
|
if (Test-Path "memory") {
|
||||||
|
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
|
||||||
|
Write-Host "Copied memory -> .specify"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only copy the relevant script variant directory
|
||||||
|
if (Test-Path "scripts") {
|
||||||
|
$scriptsDestDir = Join-Path $specDir "scripts"
|
||||||
|
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
|
||||||
|
|
||||||
|
switch ($Script) {
|
||||||
|
'sh' {
|
||||||
|
if (Test-Path "scripts/bash") {
|
||||||
|
Copy-Item -Path "scripts/bash" -Destination $scriptsDestDir -Recurse -Force
|
||||||
|
Write-Host "Copied scripts/bash -> .specify/scripts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'ps' {
|
||||||
|
if (Test-Path "scripts/powershell") {
|
||||||
|
Copy-Item -Path "scripts/powershell" -Destination $scriptsDestDir -Recurse -Force
|
||||||
|
Write-Host "Copied scripts/powershell -> .specify/scripts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy any script files that aren't in variant-specific directories
|
||||||
|
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy templates (excluding commands directory and vscode-settings.json)
|
||||||
|
if (Test-Path "templates") {
|
||||||
|
$templatesDestDir = Join-Path $specDir "templates"
|
||||||
|
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
|
||||||
|
|
||||||
|
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
|
||||||
|
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
|
||||||
|
} | ForEach-Object {
|
||||||
|
$relativePath = $_.FullName.Substring((Resolve-Path "templates").Path.Length + 1)
|
||||||
|
$destFile = Join-Path $templatesDestDir $relativePath
|
||||||
|
$destFileDir = Split-Path $destFile -Parent
|
||||||
|
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
|
||||||
|
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||||
|
}
|
||||||
|
Write-Host "Copied templates -> .specify/templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate agent-specific command files
|
||||||
|
switch ($Agent) {
|
||||||
|
'claude' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".claude/commands"
|
||||||
|
Generate-Commands -Agent 'claude' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'gemini' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".gemini/commands"
|
||||||
|
Generate-Commands -Agent 'gemini' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
if (Test-Path "agent_templates/gemini/GEMINI.md") {
|
||||||
|
Copy-Item -Path "agent_templates/gemini/GEMINI.md" -Destination (Join-Path $baseDir "GEMINI.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'copilot' {
|
||||||
|
$agentsDir = Join-Path $baseDir ".github/agents"
|
||||||
|
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
||||||
|
|
||||||
|
# Generate companion prompt files
|
||||||
|
$promptsDir = Join-Path $baseDir ".github/prompts"
|
||||||
|
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
||||||
|
|
||||||
|
# Create VS Code workspace settings
|
||||||
|
$vscodeDir = Join-Path $baseDir ".vscode"
|
||||||
|
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
||||||
|
if (Test-Path "templates/vscode-settings.json") {
|
||||||
|
Copy-Item -Path "templates/vscode-settings.json" -Destination (Join-Path $vscodeDir "settings.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'cursor-agent' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".cursor/commands"
|
||||||
|
Generate-Commands -Agent 'cursor-agent' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'qwen' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
||||||
|
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
if (Test-Path "agent_templates/qwen/QWEN.md") {
|
||||||
|
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'opencode' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".opencode/command"
|
||||||
|
Generate-Commands -Agent 'opencode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'windsurf' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".windsurf/workflows"
|
||||||
|
Generate-Commands -Agent 'windsurf' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'codex' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".codex/prompts"
|
||||||
|
Generate-Commands -Agent 'codex' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'kilocode' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".kilocode/workflows"
|
||||||
|
Generate-Commands -Agent 'kilocode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'auggie' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".augment/commands"
|
||||||
|
Generate-Commands -Agent 'auggie' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'roo' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".roo/commands"
|
||||||
|
Generate-Commands -Agent 'roo' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'codebuddy' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".codebuddy/commands"
|
||||||
|
Generate-Commands -Agent 'codebuddy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'amp' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".agents/commands"
|
||||||
|
Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'q' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".amazonq/prompts"
|
||||||
|
Generate-Commands -Agent 'q' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create zip archive
|
||||||
|
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
|
||||||
|
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
|
||||||
|
Write-Host "Created $zipFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define all agents and scripts
|
||||||
|
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q')
|
||||||
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
|
function Normalize-List {
|
||||||
|
param([string]$Input)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($Input)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split by comma or space and remove duplicates while preserving order
|
||||||
|
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
||||||
|
return $items
|
||||||
|
}
|
||||||
|
|
||||||
|
function Validate-Subset {
|
||||||
|
param(
|
||||||
|
[string]$Type,
|
||||||
|
[string[]]$Allowed,
|
||||||
|
[string[]]$Items
|
||||||
|
)
|
||||||
|
|
||||||
|
$ok = $true
|
||||||
|
foreach ($item in $Items) {
|
||||||
|
if ($item -notin $Allowed) {
|
||||||
|
Write-Error "Unknown $Type '$item' (allowed: $($Allowed -join ', '))"
|
||||||
|
$ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ok
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine agent list
|
||||||
|
if (-not [string]::IsNullOrEmpty($Agents)) {
|
||||||
|
$AgentList = Normalize-List -Input $Agents
|
||||||
|
if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$AgentList = $AllAgents
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine script list
|
||||||
|
if (-not [string]::IsNullOrEmpty($Scripts)) {
|
||||||
|
$ScriptList = Normalize-List -Input $Scripts
|
||||||
|
if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$ScriptList = $AllScripts
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Agents: $($AgentList -join ', ')"
|
||||||
|
Write-Host "Scripts: $($ScriptList -join ', ')"
|
||||||
|
|
||||||
|
# Build all variants
|
||||||
|
foreach ($agent in $AgentList) {
|
||||||
|
foreach ($script in $ScriptList) {
|
||||||
|
Build-Variant -Agent $agent -Script $script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nArchives in ${GenReleasesDir}:"
|
||||||
|
Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object {
|
||||||
|
Write-Host " $($_.Name)"
|
||||||
|
}
|
||||||
@@ -95,12 +95,32 @@ generate_commands() {
|
|||||||
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
|
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
|
||||||
md)
|
md)
|
||||||
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
||||||
prompt.md)
|
agent.md)
|
||||||
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate_copilot_prompts() {
|
||||||
|
local agents_dir=$1 prompts_dir=$2
|
||||||
|
mkdir -p "$prompts_dir"
|
||||||
|
|
||||||
|
# Generate a .prompt.md file for each .agent.md file
|
||||||
|
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
||||||
|
[[ -f "$agent_file" ]] || continue
|
||||||
|
|
||||||
|
local basename=$(basename "$agent_file" .agent.md)
|
||||||
|
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
||||||
|
|
||||||
|
# Create prompt file with agent frontmatter
|
||||||
|
cat > "$prompt_file" <<EOF
|
||||||
|
---
|
||||||
|
agent: ${basename}
|
||||||
|
---
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
build_variant() {
|
build_variant() {
|
||||||
local agent=$1 script=$2
|
local agent=$1 script=$2
|
||||||
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
||||||
@@ -146,8 +166,10 @@ build_variant() {
|
|||||||
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
|
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" ;;
|
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
|
||||||
copilot)
|
copilot)
|
||||||
mkdir -p "$base_dir/.github/prompts"
|
mkdir -p "$base_dir/.github/agents"
|
||||||
generate_commands copilot prompt.md "\$ARGUMENTS" "$base_dir/.github/prompts" "$script"
|
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
||||||
|
# Generate companion prompt files
|
||||||
|
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
||||||
# Create VS Code workspace settings
|
# Create VS Code workspace settings
|
||||||
mkdir -p "$base_dir/.vscode"
|
mkdir -p "$base_dir/.vscode"
|
||||||
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
|
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
|
||||||
|
|||||||
@@ -20,5 +20,8 @@
|
|||||||
"MD050": {
|
"MD050": {
|
||||||
"style": "asterisk"
|
"style": "asterisk"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"ignores": [
|
||||||
|
".genreleases/"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
17
AGENTS.md
17
AGENTS.md
@@ -33,7 +33,7 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
|-------|-----------|---------|----------|-------------|
|
|-------|-----------|---------|----------|-------------|
|
||||||
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
||||||
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini 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 |
|
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
|
||||||
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
|
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
|
||||||
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
|
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
|
||||||
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
||||||
@@ -325,6 +325,8 @@ Work within integrated development environments:
|
|||||||
|
|
||||||
Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp
|
Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp
|
||||||
|
|
||||||
|
**Standard format:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
description: "Command description"
|
description: "Command description"
|
||||||
@@ -333,6 +335,17 @@ description: "Command description"
|
|||||||
Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**GitHub Copilot Chat Mode format:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: "Command description"
|
||||||
|
mode: speckit.command-name
|
||||||
|
---
|
||||||
|
|
||||||
|
Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
||||||
|
```
|
||||||
|
|
||||||
### TOML Format
|
### TOML Format
|
||||||
|
|
||||||
Used by: Gemini, Qwen
|
Used by: Gemini, Qwen
|
||||||
@@ -349,7 +362,7 @@ Command content with {SCRIPT} and {{args}} placeholders.
|
|||||||
|
|
||||||
- **CLI agents**: Usually `.<agent-name>/commands/`
|
- **CLI agents**: Usually `.<agent-name>/commands/`
|
||||||
- **IDE agents**: Follow IDE-specific patterns:
|
- **IDE agents**: Follow IDE-specific patterns:
|
||||||
- Copilot: `.github/prompts/`
|
- Copilot: `.github/agents/`
|
||||||
- Cursor: `.cursor/commands/`
|
- Cursor: `.cursor/commands/`
|
||||||
- Windsurf: `.windsurf/workflows/`
|
- Windsurf: `.windsurf/workflows/`
|
||||||
|
|
||||||
|
|||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -7,6 +7,22 @@ All notable changes to the Specify CLI and templates are documented here.
|
|||||||
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).
|
||||||
|
|
||||||
|
## [0.0.22] - 2025-11-07
|
||||||
|
|
||||||
|
- Support for VS Code/Copilot agents, and moving away from prompts to proper agents with hand-offs.
|
||||||
|
- Move to use `AGENTS.md` for Copilot workloads, since it's already supported out-of-the-box.
|
||||||
|
- Adds support for the version command. ([#486](https://github.com/github/spec-kit/issues/486))
|
||||||
|
- Fixes potential bug with the `create-new-feature.ps1` script that ignores existing feature branches when determining next feature number ([#975](https://github.com/github/spec-kit/issues/975))
|
||||||
|
- Add graceful fallback and logging for GitHub API rate-limiting during template fetch ([#970](https://github.com/github/spec-kit/issues/970))
|
||||||
|
|
||||||
|
## [0.0.21] - 2025-10-21
|
||||||
|
|
||||||
|
- Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)).
|
||||||
|
- Adds support for Amp CLI.
|
||||||
|
- Adds support for VS Code hand-offs and moves prompts to be full-fledged chat modes.
|
||||||
|
- Adds support for `version` command (addresses [#811](https://github.com/github/spec-kit/issues/811) and [#486](https://github.com/github/spec-kit/issues/486), thank you [@mcasalaina](https://github.com/mcasalaina) and [@dentity007](https://github.com/dentity007)).
|
||||||
|
- Adds support for rendering the rate limit errors from the CLI when encountered ([#970](https://github.com/github/spec-kit/issues/970), thank you [@psmman](https://github.com/psmman)).
|
||||||
|
|
||||||
## [0.0.20] - 2025-10-14
|
## [0.0.20] - 2025-10-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ To test your templates, commands, and other changes locally, follow these steps:
|
|||||||
1. **Create release packages**
|
1. **Create release packages**
|
||||||
|
|
||||||
Run the following command to generate the local packages:
|
Run the following command to generate the local packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
./.github/workflows/scripts/create-release-packages.sh v1.0.0
|
./.github/workflows/scripts/create-release-packages.sh v1.0.0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.0.20"
|
version = "0.0.22"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -80,9 +80,56 @@ find_repo_root() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to get highest number from specs directory
|
||||||
|
get_highest_from_specs() {
|
||||||
|
local specs_dir="$1"
|
||||||
|
local 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
|
||||||
|
|
||||||
|
echo "$highest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get highest number from git branches
|
||||||
|
get_highest_from_branches() {
|
||||||
|
local highest=0
|
||||||
|
|
||||||
|
# Get all branches (local and remote)
|
||||||
|
branches=$(git branch -a 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$branches" ]; then
|
||||||
|
while IFS= read -r branch; do
|
||||||
|
# Clean branch name: remove leading markers and remote prefixes
|
||||||
|
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||||
|
|
||||||
|
# Extract feature number if branch matches pattern ###-*
|
||||||
|
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||||
|
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||||
|
number=$((10#$number))
|
||||||
|
if [ "$number" -gt "$highest" ]; then
|
||||||
|
highest=$number
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$branches"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$highest"
|
||||||
|
}
|
||||||
|
|
||||||
# Function to check existing branches (local and remote) and return next available number
|
# Function to check existing branches (local and remote) and return next available number
|
||||||
check_existing_branches() {
|
check_existing_branches() {
|
||||||
local short_name="$1"
|
local short_name="$1"
|
||||||
|
local specs_dir="$2"
|
||||||
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
git fetch --all --prune 2>/dev/null || true
|
git fetch --all --prune 2>/dev/null || true
|
||||||
@@ -95,8 +142,8 @@ check_existing_branches() {
|
|||||||
|
|
||||||
# Check specs directory as well
|
# Check specs directory as well
|
||||||
local spec_dirs=""
|
local spec_dirs=""
|
||||||
if [ -d "$SPECS_DIR" ]; then
|
if [ -d "$specs_dir" ]; then
|
||||||
spec_dirs=$(find "$SPECS_DIR" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n)
|
spec_dirs=$(find "$specs_dir" -maxdepth 1 -type d -name "[0-9]*-${short_name}" 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/-.*//' | sort -n)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Combine all sources and get the highest number
|
# Combine all sources and get the highest number
|
||||||
@@ -111,6 +158,12 @@ check_existing_branches() {
|
|||||||
echo $((max_num + 1))
|
echo $((max_num + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to clean and format a branch name
|
||||||
|
clean_branch_name() {
|
||||||
|
local name="$1"
|
||||||
|
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||||
|
}
|
||||||
|
|
||||||
# Resolve repository root. Prefer git information when available, but fall back
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
# to searching for repository markers so the workflow still functions in repositories that
|
# to searching for repository markers so the workflow still functions in repositories that
|
||||||
# were initialised with --no-git.
|
# were initialised with --no-git.
|
||||||
@@ -176,14 +229,15 @@ generate_branch_name() {
|
|||||||
echo "$result"
|
echo "$result"
|
||||||
else
|
else
|
||||||
# Fallback to original logic if no meaningful words found
|
# Fallback to original logic if no meaningful words found
|
||||||
echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
local cleaned=$(clean_branch_name "$description")
|
||||||
|
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate branch name
|
# Generate branch name
|
||||||
if [ -n "$SHORT_NAME" ]; then
|
if [ -n "$SHORT_NAME" ]; then
|
||||||
# Use provided short name, just clean it up
|
# Use provided short name, just clean it up
|
||||||
BRANCH_SUFFIX=$(echo "$SHORT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
|
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||||
else
|
else
|
||||||
# Generate from description with smart filtering
|
# Generate from description with smart filtering
|
||||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||||
@@ -193,19 +247,10 @@ fi
|
|||||||
if [ -z "$BRANCH_NUMBER" ]; then
|
if [ -z "$BRANCH_NUMBER" ]; then
|
||||||
if [ "$HAS_GIT" = true ]; then
|
if [ "$HAS_GIT" = true ]; then
|
||||||
# Check existing branches on remotes
|
# Check existing branches on remotes
|
||||||
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX")
|
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX" "$SPECS_DIR")
|
||||||
else
|
else
|
||||||
# Fall back to local directory check
|
# Fall back to local directory check
|
||||||
HIGHEST=0
|
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||||
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
|
|
||||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ AGENT_TYPE="${1:-}"
|
|||||||
# Agent-specific file paths
|
# Agent-specific file paths
|
||||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||||
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
|
||||||
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||||
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
||||||
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
|
|||||||
@@ -59,6 +59,46 @@ function Find-RepositoryRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-HighestNumberFromSpecs {
|
||||||
|
param([string]$SpecsDir)
|
||||||
|
|
||||||
|
$highest = 0
|
||||||
|
if (Test-Path $SpecsDir) {
|
||||||
|
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
||||||
|
if ($_.Name -match '^(\d+)') {
|
||||||
|
$num = [int]$matches[1]
|
||||||
|
if ($num -gt $highest) { $highest = $num }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $highest
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HighestNumberFromBranches {
|
||||||
|
param()
|
||||||
|
|
||||||
|
$highest = 0
|
||||||
|
try {
|
||||||
|
$branches = git branch -a 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
foreach ($branch in $branches) {
|
||||||
|
# Clean branch name: remove leading markers and remote prefixes
|
||||||
|
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
||||||
|
|
||||||
|
# Extract feature number if branch matches pattern ###-*
|
||||||
|
if ($cleanBranch -match '^(\d+)-') {
|
||||||
|
$num = [int]$matches[1]
|
||||||
|
if ($num -gt $highest) { $highest = $num }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# If git command fails, return 0
|
||||||
|
Write-Verbose "Could not check Git branches: $_"
|
||||||
|
}
|
||||||
|
return $highest
|
||||||
|
}
|
||||||
|
|
||||||
function Get-NextBranchNumber {
|
function Get-NextBranchNumber {
|
||||||
param(
|
param(
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
@@ -127,6 +167,12 @@ function Get-NextBranchNumber {
|
|||||||
# Return next number
|
# Return next number
|
||||||
return $maxNum + 1
|
return $maxNum + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanBranchName {
|
||||||
|
param([string]$Name)
|
||||||
|
|
||||||
|
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||||
|
}
|
||||||
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
||||||
if (-not $fallbackRoot) {
|
if (-not $fallbackRoot) {
|
||||||
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
||||||
@@ -189,7 +235,7 @@ function Get-BranchName {
|
|||||||
return $result
|
return $result
|
||||||
} else {
|
} else {
|
||||||
# Fallback to original logic if no meaningful words found
|
# Fallback to original logic if no meaningful words found
|
||||||
$result = $Description.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
$result = ConvertTo-CleanBranchName -Name $Description
|
||||||
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
|
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
|
||||||
return [string]::Join('-', $fallbackWords)
|
return [string]::Join('-', $fallbackWords)
|
||||||
}
|
}
|
||||||
@@ -198,7 +244,7 @@ function Get-BranchName {
|
|||||||
# Generate branch name
|
# Generate branch name
|
||||||
if ($ShortName) {
|
if ($ShortName) {
|
||||||
# Use provided short name, just clean it up
|
# Use provided short name, just clean it up
|
||||||
$branchSuffix = $ShortName.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
$branchSuffix = ConvertTo-CleanBranchName -Name $ShortName
|
||||||
} else {
|
} else {
|
||||||
# Generate from description with smart filtering
|
# Generate from description with smart filtering
|
||||||
$branchSuffix = Get-BranchName -Description $featureDesc
|
$branchSuffix = Get-BranchName -Description $featureDesc
|
||||||
@@ -211,16 +257,7 @@ if ($Number -eq 0) {
|
|||||||
$Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir
|
$Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir
|
||||||
} else {
|
} else {
|
||||||
# Fall back to local directory check
|
# Fall back to local directory check
|
||||||
$highest = 0
|
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$Number = $highest + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,4 +59,3 @@ if ($Json) {
|
|||||||
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
||||||
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
|
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ $NEW_PLAN = $IMPL_PLAN
|
|||||||
# Agent file paths
|
# Agent file paths
|
||||||
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
||||||
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
||||||
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md'
|
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md'
|
||||||
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
||||||
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
||||||
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from typer.core import TyperGroup
|
|||||||
import readchar
|
import readchar
|
||||||
import ssl
|
import ssl
|
||||||
import truststore
|
import truststore
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
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)
|
||||||
@@ -64,6 +65,63 @@ def _github_auth_headers(cli_token: str | None = None) -> dict:
|
|||||||
token = _github_token(cli_token)
|
token = _github_token(cli_token)
|
||||||
return {"Authorization": f"Bearer {token}"} if token else {}
|
return {"Authorization": f"Bearer {token}"} if token else {}
|
||||||
|
|
||||||
|
def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
|
||||||
|
"""Extract and parse GitHub rate-limit headers."""
|
||||||
|
info = {}
|
||||||
|
|
||||||
|
# Standard GitHub rate-limit headers
|
||||||
|
if "X-RateLimit-Limit" in headers:
|
||||||
|
info["limit"] = headers.get("X-RateLimit-Limit")
|
||||||
|
if "X-RateLimit-Remaining" in headers:
|
||||||
|
info["remaining"] = headers.get("X-RateLimit-Remaining")
|
||||||
|
if "X-RateLimit-Reset" in headers:
|
||||||
|
reset_epoch = int(headers.get("X-RateLimit-Reset", "0"))
|
||||||
|
if reset_epoch:
|
||||||
|
reset_time = datetime.fromtimestamp(reset_epoch, tz=timezone.utc)
|
||||||
|
info["reset_epoch"] = reset_epoch
|
||||||
|
info["reset_time"] = reset_time
|
||||||
|
info["reset_local"] = reset_time.astimezone()
|
||||||
|
|
||||||
|
# Retry-After header (seconds or HTTP-date)
|
||||||
|
if "Retry-After" in headers:
|
||||||
|
retry_after = headers.get("Retry-After")
|
||||||
|
try:
|
||||||
|
info["retry_after_seconds"] = int(retry_after)
|
||||||
|
except ValueError:
|
||||||
|
# HTTP-date format - not implemented, just store as string
|
||||||
|
info["retry_after"] = retry_after
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str:
|
||||||
|
"""Format a user-friendly error message with rate-limit information."""
|
||||||
|
rate_info = _parse_rate_limit_headers(headers)
|
||||||
|
|
||||||
|
lines = [f"GitHub API returned status {status_code} for {url}"]
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if rate_info:
|
||||||
|
lines.append("[bold]Rate Limit Information:[/bold]")
|
||||||
|
if "limit" in rate_info:
|
||||||
|
lines.append(f" • Rate Limit: {rate_info['limit']} requests/hour")
|
||||||
|
if "remaining" in rate_info:
|
||||||
|
lines.append(f" • Remaining: {rate_info['remaining']}")
|
||||||
|
if "reset_local" in rate_info:
|
||||||
|
reset_str = rate_info["reset_local"].strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
lines.append(f" • Resets at: {reset_str}")
|
||||||
|
if "retry_after_seconds" in rate_info:
|
||||||
|
lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Add troubleshooting guidance
|
||||||
|
lines.append("[bold]Troubleshooting Tips:[/bold]")
|
||||||
|
lines.append(" • If you're on a shared CI or corporate environment, you may be rate-limited.")
|
||||||
|
lines.append(" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN")
|
||||||
|
lines.append(" environment variable to increase rate limits.")
|
||||||
|
lines.append(" • Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
# Agent configuration with name, folder, install URL, and CLI tool requirement
|
# Agent configuration with name, folder, install URL, and CLI tool requirement
|
||||||
AGENT_CONFIG = {
|
AGENT_CONFIG = {
|
||||||
"copilot": {
|
"copilot": {
|
||||||
@@ -577,10 +635,11 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
)
|
)
|
||||||
status = response.status_code
|
status = response.status_code
|
||||||
if status != 200:
|
if status != 200:
|
||||||
msg = f"GitHub API returned {status} for {api_url}"
|
# Format detailed error message with rate-limit info
|
||||||
|
error_msg = _format_rate_limit_error(status, response.headers, api_url)
|
||||||
if debug:
|
if debug:
|
||||||
msg += f"\nResponse headers: {response.headers}\nBody (truncated 500): {response.text[:500]}"
|
error_msg += f"\n\n[dim]Response body (truncated 500):[/dim]\n{response.text[:500]}"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(error_msg)
|
||||||
try:
|
try:
|
||||||
release_data = response.json()
|
release_data = response.json()
|
||||||
except ValueError as je:
|
except ValueError as je:
|
||||||
@@ -627,8 +686,11 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
headers=_github_auth_headers(github_token),
|
headers=_github_auth_headers(github_token),
|
||||||
) as response:
|
) as response:
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
body_sample = response.text[:400]
|
# Handle rate-limiting on download as well
|
||||||
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
|
error_msg = _format_rate_limit_error(response.status_code, response.headers, download_url)
|
||||||
|
if debug:
|
||||||
|
error_msg += f"\n\n[dim]Response body (truncated 400):[/dim]\n{response.text[:400]}"
|
||||||
|
raise RuntimeError(error_msg)
|
||||||
total_size = int(response.headers.get('content-length', 0))
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
with open(zip_path, 'wb') as f:
|
with open(zip_path, 'wb') as f:
|
||||||
if total_size == 0:
|
if total_size == 0:
|
||||||
@@ -1202,6 +1264,85 @@ def check():
|
|||||||
if not any(agent_results.values()):
|
if not any(agent_results.values()):
|
||||||
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]")
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def version():
|
||||||
|
"""Display version and system information."""
|
||||||
|
import platform
|
||||||
|
import importlib.metadata
|
||||||
|
|
||||||
|
show_banner()
|
||||||
|
|
||||||
|
# Get CLI version from package metadata
|
||||||
|
cli_version = "unknown"
|
||||||
|
try:
|
||||||
|
cli_version = importlib.metadata.version("specify-cli")
|
||||||
|
except Exception:
|
||||||
|
# Fallback: try reading from pyproject.toml if running from source
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
||||||
|
if pyproject_path.exists():
|
||||||
|
with open(pyproject_path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
cli_version = data.get("project", {}).get("version", "unknown")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fetch latest template release version
|
||||||
|
repo_owner = "github"
|
||||||
|
repo_name = "spec-kit"
|
||||||
|
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
||||||
|
|
||||||
|
template_version = "unknown"
|
||||||
|
release_date = "unknown"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.get(
|
||||||
|
api_url,
|
||||||
|
timeout=10,
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=_github_auth_headers(),
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
release_data = response.json()
|
||||||
|
template_version = release_data.get("tag_name", "unknown")
|
||||||
|
# Remove 'v' prefix if present
|
||||||
|
if template_version.startswith("v"):
|
||||||
|
template_version = template_version[1:]
|
||||||
|
release_date = release_data.get("published_at", "unknown")
|
||||||
|
if release_date != "unknown":
|
||||||
|
# Format the date nicely
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(release_date.replace('Z', '+00:00'))
|
||||||
|
release_date = dt.strftime("%Y-%m-%d")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
||||||
|
info_table.add_column("Key", style="cyan", justify="right")
|
||||||
|
info_table.add_column("Value", style="white")
|
||||||
|
|
||||||
|
info_table.add_row("CLI Version", cli_version)
|
||||||
|
info_table.add_row("Template Version", template_version)
|
||||||
|
info_table.add_row("Released", release_date)
|
||||||
|
info_table.add_row("", "")
|
||||||
|
info_table.add_row("Python", platform.python_version())
|
||||||
|
info_table.add_row("Platform", platform.system())
|
||||||
|
info_table.add_row("Architecture", platform.machine())
|
||||||
|
info_table.add_row("OS Version", platform.version())
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
info_table,
|
||||||
|
title="[bold cyan]Specify CLI Information[/bold cyan]",
|
||||||
|
border_style="cyan",
|
||||||
|
padding=(1, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app()
|
app()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
|
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
|
||||||
|
handoffs:
|
||||||
|
- label: Build Technical Plan
|
||||||
|
agent: speckit.plan
|
||||||
|
prompt: Create a plan for the spec. I am building with...
|
||||||
scripts:
|
scripts:
|
||||||
sh: scripts/bash/check-prerequisites.sh --json --paths-only
|
sh: scripts/bash/check-prerequisites.sh --json --paths-only
|
||||||
ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly
|
ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync
|
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
|
||||||
|
handoffs:
|
||||||
|
- label: Build Specification
|
||||||
|
agent: speckit.specify
|
||||||
|
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||||
---
|
---
|
||||||
|
|
||||||
## User Input
|
## User Input
|
||||||
|
|||||||
@@ -135,4 +135,3 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- Report final status with summary of completed work
|
- Report final status with summary of completed work
|
||||||
|
|
||||||
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
---
|
---
|
||||||
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||||
|
handoffs:
|
||||||
|
- label: Create Tasks
|
||||||
|
agent: speckit.tasks
|
||||||
|
prompt: Break the plan into tasks
|
||||||
|
send: true
|
||||||
|
- label: Create Checklist
|
||||||
|
agent: speckit.checklist
|
||||||
|
prompt: Create a checklist for the following domain...
|
||||||
scripts:
|
scripts:
|
||||||
sh: scripts/bash/setup-plan.sh --json
|
sh: scripts/bash/setup-plan.sh --json
|
||||||
ps: scripts/powershell/setup-plan.ps1 -Json
|
ps: scripts/powershell/setup-plan.ps1 -Json
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
---
|
---
|
||||||
description: Create or update the feature specification from a natural language feature description.
|
description: Create or update the feature specification from a natural language feature description.
|
||||||
|
handoffs:
|
||||||
|
- label: Build Technical Plan
|
||||||
|
agent: speckit.plan
|
||||||
|
prompt: Create a plan for the spec. I am building with...
|
||||||
|
- label: Clarify Spec Requirements
|
||||||
|
agent: speckit.clarify
|
||||||
|
prompt: Clarify specification requirements
|
||||||
|
send: true
|
||||||
scripts:
|
scripts:
|
||||||
sh: scripts/bash/create-new-feature.sh --json "{ARGS}"
|
sh: scripts/bash/create-new-feature.sh --json "{ARGS}"
|
||||||
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
---
|
---
|
||||||
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||||
|
handoffs:
|
||||||
|
- label: Analyze For Consistency
|
||||||
|
agent: speckit.analyze
|
||||||
|
prompt: Run a project analysis for consistency
|
||||||
|
send: true
|
||||||
|
- label: Implement Project
|
||||||
|
agent: speckit.implement
|
||||||
|
prompt: Start the implementation in phases
|
||||||
|
send: true
|
||||||
scripts:
|
scripts:
|
||||||
sh: scripts/bash/check-prerequisites.sh --json
|
sh: scripts/bash/check-prerequisites.sh --json
|
||||||
ps: scripts/powershell/check-prerequisites.ps1 -Json
|
ps: scripts/powershell/check-prerequisites.ps1 -Json
|
||||||
|
|||||||
31
templates/commands/taskstoissues.md
Normal file
31
templates/commands/taskstoissues.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
|
||||||
|
tools: ['github/github-mcp-server/create_issue']
|
||||||
|
scripts:
|
||||||
|
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
|
||||||
|
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
You **MUST** consider the user input before proceeding (if not empty).
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||||
|
1. From the executed script, extract the path to **tasks**.
|
||||||
|
1. Get the Git remote by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --get remote.origin.url
|
||||||
|
```
|
||||||
|
|
||||||
|
**ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL**
|
||||||
|
|
||||||
|
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
|
||||||
|
|
||||||
|
**UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL**
|
||||||
Reference in New Issue
Block a user