mirror of
https://github.com/github/spec-kit.git
synced 2026-01-31 13:03:36 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e568c1201 | ||
|
|
f4fcd82920 | ||
|
|
3dcbb6e3a9 | ||
|
|
392f8b7dfa | ||
|
|
24b6d31471 | ||
|
|
960e4c0a32 | ||
|
|
df15b8e2a2 | ||
|
|
161a415274 | ||
|
|
e20d79e78b | ||
|
|
a26ee578ae | ||
|
|
e6d6f3cdee | ||
|
|
598148ca67 | ||
|
|
b40b41cf50 | ||
|
|
1f3d9b5fdd | ||
|
|
f7fe48bd6a | ||
|
|
d6136cb22f | ||
|
|
dafab39483 | ||
|
|
09274437fc | ||
|
|
5f1fc6b445 | ||
|
|
779e1f8afd | ||
|
|
177dcadd8c | ||
|
|
ba861cd165 | ||
|
|
926836e0fc | ||
|
|
af88930ffc | ||
|
|
89f5f9c0b9 | ||
|
|
9809b1a4ab | ||
|
|
7b536b578d | ||
|
|
7522eb3f9d | ||
|
|
d550634d8e | ||
|
|
72cb885eb7 | ||
|
|
a877af5575 | ||
|
|
2508d926c0 | ||
|
|
9f123e013a | ||
|
|
60bd9dc849 | ||
|
|
e77d99abd2 | ||
|
|
d4d3139d5f | ||
|
|
65f8787b48 | ||
|
|
9786e588b7 | ||
|
|
0ac76c8c7e | ||
|
|
115b4335d9 | ||
|
|
37e87c78a0 | ||
|
|
14a574a6a8 | ||
|
|
dbd1437aea | ||
|
|
317ae4dad9 | ||
|
|
8e9d25e9be | ||
|
|
c59be99dc4 | ||
|
|
15a5630047 | ||
|
|
8de5db7a3e | ||
|
|
09cf4f6cc4 |
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"
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ fi
|
|||||||
cat > release_notes.md << EOF
|
cat > release_notes.md << EOF
|
||||||
This is the latest set of releases that you can use with your agent of choice. We recommend using the Specify CLI to scaffold your projects, however you can download these independently and manage them yourself.
|
This is the latest set of releases that you can use with your agent of choice. We recommend using the Specify CLI to scaffold your projects, however you can download these independently and manage them yourself.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
$COMMITS
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Generated release notes:"
|
echo "Generated release notes:"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -62,6 +62,29 @@ When working on spec-kit:
|
|||||||
3. Test script functionality in the `scripts/` directory
|
3. Test script functionality in the `scripts/` directory
|
||||||
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
||||||
|
|
||||||
|
### Testing template and command changes locally
|
||||||
|
|
||||||
|
Running `uv run specify init` pulls released packages, which won’t include your local changes.
|
||||||
|
To test your templates, commands, and other changes locally, follow these steps:
|
||||||
|
|
||||||
|
1. **Create release packages**
|
||||||
|
|
||||||
|
Run the following command to generate the local packages:
|
||||||
|
|
||||||
|
```
|
||||||
|
./.github/workflows/scripts/create-release-packages.sh v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copy the relevant package to your test project**
|
||||||
|
|
||||||
|
```
|
||||||
|
cp -r .genreleases/sdd-copilot-package-sh/. <path-to-test-project>/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Open and test the agent**
|
||||||
|
|
||||||
|
Navigate to your test project folder and open the agent to verify your implementation.
|
||||||
|
|
||||||
## AI contributions in Spec Kit
|
## AI contributions in Spec Kit
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ set -e
|
|||||||
|
|
||||||
JSON_MODE=false
|
JSON_MODE=false
|
||||||
SHORT_NAME=""
|
SHORT_NAME=""
|
||||||
|
BRANCH_NUMBER=""
|
||||||
ARGS=()
|
ARGS=()
|
||||||
i=1
|
i=1
|
||||||
while [ $i -le $# ]; do
|
while [ $i -le $# ]; do
|
||||||
@@ -26,17 +27,31 @@ while [ $i -le $# ]; do
|
|||||||
fi
|
fi
|
||||||
SHORT_NAME="$next_arg"
|
SHORT_NAME="$next_arg"
|
||||||
;;
|
;;
|
||||||
|
--number)
|
||||||
|
if [ $((i + 1)) -gt $# ]; then
|
||||||
|
echo 'Error: --number requires a value' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
next_arg="${!i}"
|
||||||
|
if [[ "$next_arg" == --* ]]; then
|
||||||
|
echo 'Error: --number requires a value' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BRANCH_NUMBER="$next_arg"
|
||||||
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>"
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --json Output in JSON format"
|
echo " --json Output in JSON format"
|
||||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
|
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||||
echo " --help, -h Show this help message"
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||||
echo " $0 'Implement OAuth2 integration for API'"
|
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -48,7 +63,7 @@ done
|
|||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] <feature_description>" >&2
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -65,6 +80,90 @@ 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
|
||||||
|
check_existing_branches() {
|
||||||
|
local short_name="$1"
|
||||||
|
local specs_dir="$2"
|
||||||
|
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
git fetch --all --prune 2>/dev/null || true
|
||||||
|
|
||||||
|
# Find all branches matching the pattern using git ls-remote (more reliable)
|
||||||
|
local remote_branches=$(git ls-remote --heads origin 2>/dev/null | grep -E "refs/heads/[0-9]+-${short_name}$" | sed 's/.*\/\([0-9]*\)-.*/\1/' | sort -n)
|
||||||
|
|
||||||
|
# Also check local branches
|
||||||
|
local local_branches=$(git branch 2>/dev/null | grep -E "^[* ]*[0-9]+-${short_name}$" | sed 's/^[* ]*//' | sed 's/-.*//' | sort -n)
|
||||||
|
|
||||||
|
# Check specs directory as well
|
||||||
|
local spec_dirs=""
|
||||||
|
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)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Combine all sources and get the highest number
|
||||||
|
local max_num=0
|
||||||
|
for num in $remote_branches $local_branches $spec_dirs; do
|
||||||
|
if [ "$num" -gt "$max_num" ]; then
|
||||||
|
max_num=$num
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Return next number
|
||||||
|
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.
|
||||||
@@ -87,20 +186,6 @@ cd "$REPO_ROOT"
|
|||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
mkdir -p "$SPECS_DIR"
|
mkdir -p "$SPECS_DIR"
|
||||||
|
|
||||||
HIGHEST=0
|
|
||||||
if [ -d "$SPECS_DIR" ]; then
|
|
||||||
for dir in "$SPECS_DIR"/*; do
|
|
||||||
[ -d "$dir" ] || continue
|
|
||||||
dirname=$(basename "$dir")
|
|
||||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
|
||||||
number=$((10#$number))
|
|
||||||
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
NEXT=$((HIGHEST + 1))
|
|
||||||
FEATURE_NUM=$(printf "%03d" "$NEXT")
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
generate_branch_name() {
|
generate_branch_name() {
|
||||||
local description="$1"
|
local description="$1"
|
||||||
@@ -144,19 +229,33 @@ 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")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Determine branch number
|
||||||
|
if [ -z "$BRANCH_NUMBER" ]; then
|
||||||
|
if [ "$HAS_GIT" = true ]; then
|
||||||
|
# Check existing branches on remotes
|
||||||
|
BRANCH_NUMBER=$(check_existing_branches "$BRANCH_SUFFIX" "$SPECS_DIR")
|
||||||
|
else
|
||||||
|
# Fall back to local directory check
|
||||||
|
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||||
|
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
FEATURE_NUM=$(printf "%03d" "$BRANCH_NUMBER")
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -391,12 +391,25 @@ update_existing_agent_file() {
|
|||||||
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
|
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if sections exist in the file
|
||||||
|
local has_active_technologies=0
|
||||||
|
local has_recent_changes=0
|
||||||
|
|
||||||
|
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
|
||||||
|
has_active_technologies=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
|
||||||
|
has_recent_changes=1
|
||||||
|
fi
|
||||||
|
|
||||||
# Process file line by line
|
# Process file line by line
|
||||||
local in_tech_section=false
|
local in_tech_section=false
|
||||||
local in_changes_section=false
|
local in_changes_section=false
|
||||||
local tech_entries_added=false
|
local tech_entries_added=false
|
||||||
local changes_entries_added=false
|
local changes_entries_added=false
|
||||||
local existing_changes_count=0
|
local existing_changes_count=0
|
||||||
|
local file_ended=false
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
# Handle Active Technologies section
|
# Handle Active Technologies section
|
||||||
@@ -457,6 +470,22 @@ update_existing_agent_file() {
|
|||||||
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
|
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
|
||||||
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||||
|
tech_entries_added=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If sections don't exist, add them at the end of the file
|
||||||
|
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||||
|
echo "" >> "$temp_file"
|
||||||
|
echo "## Active Technologies" >> "$temp_file"
|
||||||
|
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||||
|
tech_entries_added=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
|
||||||
|
echo "" >> "$temp_file"
|
||||||
|
echo "## Recent Changes" >> "$temp_file"
|
||||||
|
echo "$new_change_entry" >> "$temp_file"
|
||||||
|
changes_entries_added=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Move temp file to target atomically
|
# Move temp file to target atomically
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
param(
|
param(
|
||||||
[switch]$Json,
|
[switch]$Json,
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
|
[int]$Number = 0,
|
||||||
[switch]$Help,
|
[switch]$Help,
|
||||||
[Parameter(ValueFromRemainingArguments = $true)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$FeatureDescription
|
[string[]]$FeatureDescription
|
||||||
@@ -12,11 +13,12 @@ $ErrorActionPreference = 'Stop'
|
|||||||
|
|
||||||
# Show help if requested
|
# Show help if requested
|
||||||
if ($Help) {
|
if ($Help) {
|
||||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
|
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Options:"
|
Write-Host "Options:"
|
||||||
Write-Host " -Json Output in JSON format"
|
Write-Host " -Json Output in JSON format"
|
||||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
|
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||||
Write-Host " -Help Show this help message"
|
Write-Host " -Help Show this help message"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Examples:"
|
Write-Host "Examples:"
|
||||||
@@ -56,6 +58,121 @@ function Find-RepositoryRoot {
|
|||||||
$current = $parent
|
$current = $parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
param(
|
||||||
|
[string]$ShortName,
|
||||||
|
[string]$SpecsDir
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
try {
|
||||||
|
git fetch --all --prune 2>$null | Out-Null
|
||||||
|
} catch {
|
||||||
|
# Ignore fetch errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find remote branches matching the pattern using git ls-remote
|
||||||
|
$remoteBranches = @()
|
||||||
|
try {
|
||||||
|
$remoteRefs = git ls-remote --heads origin 2>$null
|
||||||
|
if ($remoteRefs) {
|
||||||
|
$remoteBranches = $remoteRefs | Where-Object { $_ -match "refs/heads/(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
|
||||||
|
if ($_ -match "refs/heads/(\d+)-") {
|
||||||
|
[int]$matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check local branches
|
||||||
|
$localBranches = @()
|
||||||
|
try {
|
||||||
|
$allBranches = git branch 2>$null
|
||||||
|
if ($allBranches) {
|
||||||
|
$localBranches = $allBranches | Where-Object { $_ -match "^\*?\s*(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
|
||||||
|
if ($_ -match "(\d+)-") {
|
||||||
|
[int]$matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check specs directory
|
||||||
|
$specDirs = @()
|
||||||
|
if (Test-Path $SpecsDir) {
|
||||||
|
try {
|
||||||
|
$specDirs = Get-ChildItem -Path $SpecsDir -Directory | Where-Object { $_.Name -match "^(\d+)-$([regex]::Escape($ShortName))$" } | ForEach-Object {
|
||||||
|
if ($_.Name -match "^(\d+)-") {
|
||||||
|
[int]$matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Combine all sources and get the highest number
|
||||||
|
$maxNum = 0
|
||||||
|
foreach ($num in ($remoteBranches + $localBranches + $specDirs)) {
|
||||||
|
if ($num -gt $maxNum) {
|
||||||
|
$maxNum = $num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return next number
|
||||||
|
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."
|
||||||
@@ -79,18 +196,6 @@ Set-Location $repoRoot
|
|||||||
$specsDir = Join-Path $repoRoot 'specs'
|
$specsDir = Join-Path $repoRoot 'specs'
|
||||||
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
||||||
|
|
||||||
$highest = 0
|
|
||||||
if (Test-Path $specsDir) {
|
|
||||||
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
|
||||||
if ($_.Name -match '^(\d{3})') {
|
|
||||||
$num = [int]$matches[1]
|
|
||||||
if ($num -gt $highest) { $highest = $num }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$next = $highest + 1
|
|
||||||
$featureNum = ('{0:000}' -f $next)
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
function Get-BranchName {
|
function Get-BranchName {
|
||||||
param([string]$Description)
|
param([string]$Description)
|
||||||
@@ -130,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)
|
||||||
}
|
}
|
||||||
@@ -139,12 +244,24 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determine branch number
|
||||||
|
if ($Number -eq 0) {
|
||||||
|
if ($hasGit) {
|
||||||
|
# Check existing branches on remotes
|
||||||
|
$Number = Get-NextBranchNumber -ShortName $branchSuffix -SpecsDir $specsDir
|
||||||
|
} else {
|
||||||
|
# Fall back to local directory check
|
||||||
|
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureNum = ('{0:000}' -f $Number)
|
||||||
$branchName = "$featureNum-$branchSuffix"
|
$branchName = "$featureNum-$branchSuffix"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -31,16 +39,36 @@ Given that feature description, do this:
|
|||||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||||
|
|
||||||
2. Run the script `{SCRIPT}` from repo root **with the short-name argument** and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute.
|
2. **Check for existing branches before creating new one**:
|
||||||
|
|
||||||
|
a. First, fetch all remote branches to ensure we have the latest information:
|
||||||
|
```bash
|
||||||
|
git fetch --all --prune
|
||||||
|
```
|
||||||
|
|
||||||
|
b. Find the highest feature number across all sources for the short-name:
|
||||||
|
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
|
||||||
|
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
|
||||||
|
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
|
||||||
|
|
||||||
|
c. Determine the next available number:
|
||||||
|
- Extract all numbers from all three sources
|
||||||
|
- Find the highest number N
|
||||||
|
- Use N+1 for the new branch number
|
||||||
|
|
||||||
|
d. Run the script `{SCRIPT}` with the calculated number and short-name:
|
||||||
|
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||||
|
- Bash example: `{SCRIPT} --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||||
|
- PowerShell example: `{SCRIPT} -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||||
|
|
||||||
**IMPORTANT**:
|
**IMPORTANT**:
|
||||||
|
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
|
||||||
- Append the short-name argument to the `{SCRIPT}` command with the 2-4 word short name you created in step 1
|
- Only match branches/directories with the exact short-name pattern
|
||||||
- Bash: `--short-name "your-generated-short-name"`
|
- If no existing branches/directories found with this short-name, start with number 1
|
||||||
- PowerShell: `-ShortName "your-generated-short-name"`
|
- You must only ever run this script once per feature
|
||||||
- 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")
|
|
||||||
- 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
|
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
||||||
|
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||||
|
- 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")
|
||||||
|
|
||||||
3. Load `templates/spec-template.md` to understand required sections.
|
3. Load `templates/spec-template.md` to understand required sections.
|
||||||
|
|
||||||
|
|||||||
@@ -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