Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
5daaf23651 chore: bump version to 0.2.0 2026-03-09 19:43:03 +00:00
20 changed files with 105 additions and 967 deletions

View File

@@ -8,15 +8,15 @@ run_command() {
local command_to_run="$*"
local output
local exit_code
# Capture all output (stdout and stderr)
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
exit_code=${exit_code:-0}
if [ $exit_code -ne 0 ]; then
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
echo -e "\033[0;31m$output\033[0m" >&2
exit $exit_code
fi
}
@@ -53,7 +53,7 @@ echo "✅ Done"
echo -e "\n🤖 Installing Kiro CLI..."
# https://kiro.dev/docs/cli/
KIRO_INSTALLER_URL="https://kiro.dev/install.sh"
KIRO_INSTALLER_URL="https://cli.kiro.dev/install"
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
KIRO_INSTALLER_PATH="$(mktemp)"
@@ -80,11 +80,6 @@ fi
run_command "$kiro_binary --help > /dev/null"
echo "✅ Done"
echo -e "\n🤖 Installing Kimi CLI..."
# https://code.kimi.com
run_command "pipx install kimi-cli"
echo "✅ Done"
echo -e "\n🤖 Installing CodeBuddy CLI..."
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
echo "✅ Done"

2
.github/workflows/scripts/create-github-release.sh vendored Executable file → Normal file
View File

@@ -56,8 +56,6 @@ gh release create "$VERSION" \
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
.genreleases/spec-kit-template-vibe-ps-"$VERSION".zip \
.genreleases/spec-kit-template-kimi-sh-"$VERSION".zip \
.genreleases/spec-kit-template-kimi-ps-"$VERSION".zip \
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
--title "Spec Kit Templates - $VERSION_NO_V" \

View File

@@ -8,13 +8,13 @@
.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, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, generic
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, generic
.PARAMETER Scripts
Comma or space separated subset of script types to build (default: both)
@@ -33,10 +33,10 @@
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Version,
[Parameter(Mandatory=$false)]
[string]$Agents = "",
[Parameter(Mandatory=$false)]
[string]$Scripts = ""
)
@@ -60,7 +60,7 @@ 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/'
@@ -75,55 +75,55 @@ function Generate-Commands {
[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
@@ -135,7 +135,7 @@ function Generate-Commands {
}
continue
}
if ($inFrontmatter) {
if ($line -match '^(scripts|agent_scripts):$') {
$skipScripts = $true
@@ -148,20 +148,20 @@ function Generate-Commands {
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 '\\', '\\'
@@ -183,15 +183,15 @@ function Generate-CopilotPrompts {
[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
@@ -201,118 +201,31 @@ agent: $basename
}
}
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
# Kimi CLI discovers skills as directories containing a SKILL.md file,
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
function New-KimiSkills {
param(
[string]$SkillsDir,
[string]$ScriptVariant
)
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
foreach ($template in $templates) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
$skillName = "speckit.$name"
$skillDir = Join-Path $SkillsDir $skillName
New-Item -ItemType Directory -Force -Path $skillDir | Out-Null
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
# Extract description
$description = "Spec Kit: $name workflow"
if ($fileContent -match '(?m)^description:\s*(.+)$') {
$description = $matches[1]
}
# Extract script command
$scriptCommand = "(Missing script command for $ScriptVariant)"
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
$scriptCommand = $matches[1]
}
# Extract agent_script command from frontmatter if present
$agentScriptCommand = ""
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
$agentScriptCommand = $matches[1].Trim()
}
# Replace {SCRIPT}, strip scripts sections, rewrite paths
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
}
$lines = $body -split "`n"
$outputLines = @()
$inFrontmatter = $false
$skipScripts = $false
$dashCount = 0
foreach ($line in $lines) {
if ($line -match '^---$') {
$outputLines += $line
$dashCount++
$inFrontmatter = ($dashCount -eq 1)
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"
$body = $body -replace '\{ARGS\}', '$ARGUMENTS'
$body = $body -replace '__AGENT__', 'kimi'
$body = Rewrite-Paths -Content $body
# Strip existing frontmatter, keep only body
$templateBody = ""
$fmCount = 0
$inBody = $false
foreach ($line in ($body -split "`n")) {
if ($line -match '^---$') {
$fmCount++
if ($fmCount -eq 2) { $inBody = $true }
continue
}
if ($inBody) { $templateBody += "$line`n" }
}
$skillContent = "---`nname: `"$skillName`"`ndescription: `"$description`"`n---`n`n$templateBody"
Set-Content -Path (Join-Path $skillDir "SKILL.md") -Value $skillContent -NoNewline
}
}
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") {
@@ -327,17 +240,18 @@ function Build-Variant {
}
}
}
# 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 {
@@ -349,7 +263,7 @@ function Build-Variant {
}
Write-Host "Copied templates -> .specify/templates"
}
# Generate agent-specific command files
switch ($Agent) {
'claude' {
@@ -366,10 +280,12 @@ function Build-Variant {
'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") {
@@ -445,24 +361,19 @@ function Build-Variant {
$cmdDir = Join-Path $baseDir ".agent/workflows"
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'vibe' {
$cmdDir = Join-Path $baseDir ".vibe/prompts"
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'kimi' {
$skillsDir = Join-Path $baseDir ".kimi/skills"
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
New-KimiSkills -SkillsDir $skillsDir -ScriptVariant $Script
}
'generic' {
$cmdDir = Join-Path $baseDir ".speckit/commands"
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
'vibe' {
$cmdDir = Join-Path $baseDir ".vibe/prompts"
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
default {
throw "Unsupported agent '$Agent'."
}
}
# Create zip archive
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
@@ -470,16 +381,17 @@ function Build-Variant {
}
# Define all agents and scripts
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'generic')
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'generic')
$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
}
@@ -490,7 +402,7 @@ function Validate-Subset {
[string[]]$Allowed,
[string[]]$Items
)
$ok = $true
foreach ($item in $Items) {
if ($item -notin $Allowed) {

View File

@@ -6,7 +6,7 @@ set -euo pipefail
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
# Version argument should include leading 'v'.
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic (default: all)
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli generic (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
@@ -45,19 +45,19 @@ generate_commands() {
[[ -f "$template" ]] || continue
local name description script_command agent_script_command body
name=$(basename "$template" .md)
# Normalize line endings
file_content=$(tr -d '\r' < "$template")
# Extract description and script command from YAML frontmatter
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
if [[ -z $script_command ]]; then
echo "Warning: no script command found for $script_variant in $template" >&2
script_command="(Missing script command for $script_variant)"
fi
# Extract agent_script command from YAML frontmatter if present
agent_script_command=$(printf '%s\n' "$file_content" | awk '
/^agent_scripts:$/ { in_agent_scripts=1; next }
@@ -68,15 +68,15 @@ generate_commands() {
}
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
')
# Replace {SCRIPT} placeholder with the script command
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
if [[ -n $agent_script_command ]]; then
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
body=$(printf '%s\n' "$body" | awk '
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
@@ -86,10 +86,10 @@ generate_commands() {
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
{ print }
')
# Apply other substitutions
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
case $ext in
toml)
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
@@ -105,14 +105,15 @@ generate_commands() {
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}
@@ -121,104 +122,41 @@ EOF
done
}
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
# Kimi CLI discovers skills as directories containing a SKILL.md file,
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
create_kimi_skills() {
local skills_dir="$1"
local script_variant="$2"
for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue
local name
name=$(basename "$template" .md)
local skill_name="speckit.${name}"
local skill_dir="${skills_dir}/${skill_name}"
mkdir -p "$skill_dir"
local file_content
file_content=$(tr -d '\r' < "$template")
# Extract description from frontmatter
local description
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
[[ -z "$description" ]] && description="Spec Kit: ${name} workflow"
# Extract script command
local script_command
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
[[ -z "$script_command" ]] && script_command="(Missing script command for $script_variant)"
# Extract agent_script command from frontmatter if present
local agent_script_command
agent_script_command=$(printf '%s\n' "$file_content" | awk '
/^agent_scripts:$/ { in_agent_scripts=1; next }
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
print
exit
}
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
')
# Build body: replace placeholders, strip scripts sections, rewrite paths
local body
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
if [[ -n $agent_script_command ]]; then
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi
body=$(printf '%s\n' "$body" | awk '
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
{ print }
')
body=$(printf '%s\n' "$body" | sed 's/{ARGS}/\$ARGUMENTS/g' | sed 's/__AGENT__/kimi/g' | rewrite_paths)
# Strip existing frontmatter and prepend Kimi frontmatter
local template_body
template_body=$(printf '%s\n' "$body" | awk '/^---/{p++; if(p==2){found=1; next}} found')
{
printf -- '---\n'
printf 'name: "%s"\n' "$skill_name"
printf 'description: "%s"\n' "$description"
printf -- '---\n\n'
printf '%s\n' "$template_body"
} > "$skill_dir/SKILL.md"
done
}
build_variant() {
local agent=$1 script=$2
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
echo "Building $agent ($script) package..."
mkdir -p "$base_dir"
# Copy base structure but filter scripts by variant
SPEC_DIR="$base_dir/.specify"
mkdir -p "$SPEC_DIR"
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
# Only copy the relevant script variant directory
if [[ -d scripts ]]; then
mkdir -p "$SPEC_DIR/scripts"
case $script in
sh)
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
# Copy any script files that aren't in variant-specific directories
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
;;
ps)
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
# Copy any script files that aren't in variant-specific directories
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
;;
esac
fi
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
# NOTE: We substitute {ARGS} internally. Outward tokens differ intentionally:
# * Markdown/prompt (claude, copilot, cursor-agent, opencode): $ARGUMENTS
# * TOML (gemini, qwen, tabnine): {{args}}
# This keeps formats readable without extra abstraction.
case $agent in
claude)
@@ -231,7 +169,9 @@ build_variant() {
copilot)
mkdir -p "$base_dir/.github/agents"
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
mkdir -p "$base_dir/.vscode"
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
;;
@@ -288,9 +228,6 @@ build_variant() {
vibe)
mkdir -p "$base_dir/.vibe/prompts"
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
kimi)
mkdir -p "$base_dir/.kimi/skills"
create_kimi_skills "$base_dir/.kimi/skills" "$script" ;;
generic)
mkdir -p "$base_dir/.speckit/commands"
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
@@ -300,10 +237,11 @@ build_variant() {
}
# Determine agent list
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic)
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli generic)
ALL_SCRIPTS=(sh ps)
norm_list() {
# convert comma+space separated -> line separated unique while preserving order of first occurrence
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
}

View File

@@ -48,7 +48,6 @@ Specify supports multiple AI agents by generating agent-specific command files a
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
| **Tabnine CLI** | `.tabnine/agent/commands/` | TOML | `tabnine` | Tabnine CLI |
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
@@ -325,7 +324,6 @@ Require a command-line tool to be installed:
- **Amp**: `amp` CLI
- **SHAI**: `shai` CLI
- **Tabnine CLI**: `tabnine` CLI
- **Kimi Code**: `kimi` CLI
### IDE-Based Agents
@@ -339,7 +337,7 @@ Work within integrated development environments:
### Markdown Format
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob
**Standard format:**

View File

@@ -7,62 +7,10 @@ Recent 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.1] - 2026-03-11
### Changed
- Added February 2026 newsletter (#1812)
- feat: add Kimi Code CLI agent support (#1790)
- docs: fix broken links in quickstart guide (#1759) (#1797)
- docs: add catalog cli help documentation (#1793) (#1794)
- fix: use quiet checkout to avoid exception on git checkout (#1792)
- feat(extensions): support .extensionignore to exclude files during install (#1781)
- feat: add Codex support for extension command registration (#1767)
- chore: bump version to 0.2.0 (#1786)
- fix: sync agent list comments with actual supported agents (#1785)
- feat(extensions): support multiple active catalogs simultaneously (#1720)
- Pavel/add tabnine cli support (#1503)
- Add Understanding extension to community catalog (#1778)
- Add ralph extension to community catalog (#1780)
- Update README with project initialization instructions (#1772)
- feat: add review extension to community catalog (#1775)
- Add fleet extension to community catalog (#1771)
- Integration of Mistral vibe support into speckit (#1725)
- fix: Remove duplicate options in specify.md (#1765)
- fix: use global branch numbering instead of per-short-name detection (#1757)
- Add Community Walkthroughs section to README (#1766)
- feat(extensions): add Jira Integration to community catalog (#1764)
- Add Azure DevOps Integration extension to community catalog (#1734)
- Fix docs: update Antigravity link and add initialization example (#1748)
- fix: wire after_tasks and after_implement hook events into command templates (#1702)
- make c ignores consistent with c++ (#1747)
- chore: bump version to 0.1.13 (#1746)
- feat: add kiro-cli and AGENT_CONFIG consistency coverage (#1690)
- feat: add verify extension to community catalog (#1726)
- Add Retrospective Extension to community catalog README table (#1741)
- fix(scripts): add empty description validation and branch checkout error handling (#1559)
- fix: correct Copilot extension command registration (#1724)
- fix(implement): remove Makefile from C ignore patterns (#1558)
- Add sync extension to community catalog (#1728)
- fix(checklist): clarify file handling behavior for append vs create (#1556)
- fix(clarify): correct conflicting question limit from 10 to 5 (#1557)
- chore: bump version to 0.1.12 (#1737)
- fix: use RELEASE_PAT so tag push triggers release workflow (#1736)
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [Unreleased]
### Added
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
## [0.2.0] - 2026-03-09
### Changed
- feat: add Kimi Code CLI agent support
- fix: sync agent list comments with actual supported agents (#1785)
- feat(extensions): support multiple active catalogs simultaneously (#1720)
- Pavel/add tabnine cli support (#1503)
@@ -95,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- fix: release-trigger uses release branch + PR instead of direct push to main (#1733)
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
## [0.1.14] - 2026-03-09
### Added

View File

@@ -179,7 +179,6 @@ See Spec-Driven Development in action across different scenarios with these comm
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
| [Kimi Code](https://code.kimi.com/) | ✅ | |
| [Windsurf](https://windsurf.com/) | ✅ | |
| [Antigravity (agy)](https://antigravity.google/) | ✅ | |
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
@@ -193,14 +192,14 @@ The `specify` command supports the following options:
| Command | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`) |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`) |
### `specify init` Arguments & Options
| Argument/Option | Type | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, or `generic` (requires `--ai-commands-dir`) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, or `generic` (requires `--ai-commands-dir`) |
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |

View File

@@ -173,6 +173,6 @@ Finally, implement the solution:
## Next Steps
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
- Check out [more examples](../templates) in the repository
- Explore the [source code on GitHub](https://github.com/github/spec-kit)

View File

@@ -332,67 +332,6 @@ echo "$config"
---
## Excluding Files with `.extensionignore`
Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy.
### Format
The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library:
- Blank lines are ignored
- Lines starting with `#` are comments
- `*` matches anything **except** `/` (does not cross directory boundaries)
- `**` matches zero or more directories (e.g., `docs/**/*.draft.md`)
- `?` matches any single character except `/`
- A trailing `/` restricts a pattern to directories only
- Patterns containing `/` (other than a trailing slash) are anchored to the extension root
- Patterns without `/` match at any depth in the tree
- `!` negates a previously excluded pattern (re-includes a file)
- Backslashes in patterns are normalised to forward slashes for cross-platform compatibility
- The `.extensionignore` file itself is always excluded automatically
### Example
```gitignore
# .extensionignore
# Development files
tests/
.github/
.gitignore
# Build artifacts
__pycache__/
*.pyc
dist/
# Documentation source (keep only the built README)
docs/
CONTRIBUTING.md
```
### Pattern Matching
| Pattern | Matches | Does NOT match |
|---------|---------|----------------|
| `*.pyc` | Any `.pyc` file in any directory | — |
| `tests/` | The `tests` directory (and all its contents) | A file named `tests` |
| `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) |
| `.env` | The `.env` file at any level | — |
| `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — |
| `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — |
### Unsupported Features
The following `.gitignore` features are **not applicable** in this context:
- **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories)
- **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here
- **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`).
---
## Validation Rules
### Extension ID

View File

@@ -432,26 +432,6 @@ Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simul
specify extension catalog list
```
### Managing Catalogs via CLI
You can view the main catalog management commands using `--help`:
```text
specify extension catalog --help
Usage: specify extension catalog [OPTIONS] COMMAND [ARGS]...
Manage extension catalogs
╭─ Options ────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
│ list List all active extension catalogs. │
│ add Add a catalog to .specify/extension-catalogs.yml. │
│ remove Remove a catalog from .specify/extension-catalogs.yml. │
╰──────────────────────────────────────────────────────────────────────────────────╯
```
### Adding a Catalog (Project-scoped)
```bash

View File

@@ -1,54 +0,0 @@
# Spec Kit - February 2026 Newsletter
This edition covers Spec Kit activity in February 2026. Versions v0.1.7 through v0.1.13 shipped during the month, addressing bugs and adding features including a dual-catalog extension system and additional agent integrations. Community activity included blog posts, tutorials, and meetup sessions. A category summary is in the table below, followed by details.
| **Spec Kit Core (Feb 2026)** | **Community & Content** | **Roadmap & Next** |
| --- | --- | --- |
| Versions **v0.1.7** through **v0.1.13** shipped with bug fixes and features, including a **dual-catalog extension system** and new agent integrations. Over 300 issues were closed (of ~800 filed). The repo reached 71k stars and 6.4k forks. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit/issues) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit) | Eduardo Luz published a LinkedIn article on SDD and Spec Kit [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en). Erick Matsen blogged a walkthrough of building a bioinformatics pipeline with Spec Kit [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html). Microsoft MVP [Eric Boyd](https://ericboyd.com/) (not the Microsoft AI Platform VP of the same name) presented at the Cleveland .NET User Group [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit). | **v0.2.0** was released in early March, consolidating February's work. It added extensions for Jira and Azure DevOps, community plugin support, and agents for Tabnine CLI and Kiro CLI [\[github.com\]](https://github.com/github/spec-kit/releases). Future work includes spec lifecycle management and progress toward a stable 1.0 release [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html). |
***
## Spec Kit Project Updates
Spec Kit released versions **v0.1.7** through **v0.1.13** during February. Version 0.1.7 (early February) updated documentation for the newly introduced **dual-catalog extension system**, which allows both core and community extension catalogs to coexist. Subsequent patches (0.1.8, 0.1.9, etc.) bumped dependencies such as GitHub Actions versions and resolved minor issues. **v0.1.10** fixed YAML front-matter handling in generated files. By late February, **v0.1.12** and **v0.1.13** shipped with additional fixes in preparation for the next version bump. [\[github.com\]](https://github.com/github/spec-kit/releases)
The main architectural addition was the **modular extension system** with separate "core" and "community" extension catalogs for third-party add-ons. Multiple community-contributed extensions were merged during the month, including a **Jira extension** for issue tracker integration, an **Azure DevOps extension**, and utility extensions for code review, retrospective documentation, and CI/CD sync. The pending 0.2.0 release changelog lists over a dozen changes from February, including the extension additions and support for **multiple agent catalogs concurrently**. [\[github.com\]](https://github.com/github/spec-kit/releases)
By end of February, **over 330 issues/feature requests had been closed on GitHub** (out of ~870 filed to date). External contributors submitted pull requests including the **Tabnine CLI support**, which was merged in late February. The repository reached ~71k stars and crossed 6,000 forks. [\[github.com\]](https://github.com/github/spec-kit/issues) [\[github.com\]](https://github.com/github/spec-kit/releases) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
On the stability side, February's work focused on tightening core workflows and fixing edge-case bugs in the specification, planning, and task-generation commands. The team addressed file-handling issues (e.g., clarifying how output files are created/appended) and improved the reliability of the automated release pipeline. The project also added **Kiro CLI** to the supported agent list and updated integration scripts for Cursor and Code Interpreter, bringing the total number of supported AI coding assistants to over 20. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit)
## Community & Content
**Eduardo Luz** published a LinkedIn article on Feb 15 titled *"Specification Driven Development (SDD) and the GitHub Spec Kit: Elevating Software Engineering."* The article draws on his experience as a senior engineer to describe common causes of technical debt and inconsistent designs, and how SDD addresses them. It walks through Spec Kit's **four-layer approach** (Constitution, Design, Tasks, Implementation) and discusses treating specifications as a source of truth. The post generated discussion among software architects on LinkedIn about reducing misunderstandings and rework through spec-driven workflows. [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en)
**Erick Matsen** (Fred Hutchinson Cancer Center) posted a detailed walkthrough on Feb 10 titled *"Spec-Driven Development with spec-kit."* He describes building a **bioinformatics pipeline** in a single day using Spec Kit's workflow (from `speckit.constitution` to `speckit.implement`). The post includes command outputs and notes on decisions made along the way, such as refining the spec to add domain-specific requirements. He writes: "I really recommend this approach. This feels like the way software development should be." [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html) [\[github.com\]](https://github.com/mnriem/spec-kit-dotnet-cli-demo)
Several other tutorials and guides appeared during the month. An article on *IntuitionLabs* (updated Feb 21) provided a guide to Spec Kit covering the philosophy behind SDD and a walkthrough of the four-phase workflow with examples. A piece by Ry Walker (Feb 22) summarized key aspects of Spec Kit, noting its agent-agnostic design and 71k-star count. Microsoft's Developer Blog post from late 2025 (*"Diving Into Spec-Driven Development with GitHub Spec Kit"* by Den Delimarsky) continued to circulate among new users. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
On **Feb 25**, the Cleveland C# .NET User Group hosted a session titled *"Spec Driven Development with GitHub Spec Kit."* The talk was delivered by Microsoft MVP **[Eric Boyd](https://ericboyd.com/)** (Cleveland-based .NET developer; not to be confused with the Microsoft AI Platform VP of the same name). Boyd covered how specs change an AI coding assistant's output, patterns for iterating and refining specs over multiple cycles, and moving from ad-hoc prompting to a repeatable spec-driven workflow. Other groups, including GDG Madison, also listed sessions on spec-driven development in late February and early March. [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit)
On GitHub, the **Spec Kit Discussions forum** saw activity around installation troubleshooting, handling multi-feature projects with Spec Kit's branching model, and feature suggestions. One thread discussed how Spec Kit treats each spec as a short-lived artifact tied to a feature branch, which led to discussion about future support for long-running "spec of record" use cases. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
## SDD Ecosystem
Other spec-driven development tools also saw activity in February.
AWS **Kiro** released version 0.10 on Feb 18 with two new spec workflows: a **Design-First** mode (starting from architecture/pseudocode to derive requirements) and a **Bugfix** mode (structured root-cause analysis producing a `bugfix.md` spec file). Kiro also added hunk-level code review for AI-generated changes and pre/post task hooks for custom automation. AWS expanded Kiro to GovCloud regions on Feb 17 for government compliance use cases. [\[kiro.dev\]](https://kiro.dev/changelog/)
**OpenSpec** (by Fission AI), a lightweight SDD framework, reached ~29.3k stars and nearly 2k forks. Its community published guides and comparisons during the month, including *"Spec-Driven Development Made Easy: A Practical Guide with OpenSpec."* OpenSpec emphasizes simplicity and flexibility, integrating with multiple AI coding assistants via YAML configs.
**Tessl** remained in private beta. As described by Thoughtworks writer Birgitta Boeckeler, Tessl pursues a **spec-as-source** model where specifications are maintained long-term and directly generate code files one-to-one, with generated code labeled as "do not edit." This contrasts with Spec Kit's current approach of creating specs per feature/branch. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
An **arXiv preprint** (January 2026) categorized SDD implementations into three levels: *spec-first*, *spec-anchored*, and *spec-as-source*. Spec Kit was identified as primarily spec-first with elements of spec-anchored. Tech media published reviews including a *Vibe Coding* "GitHub Spec Kit Review (2026)" and a blog post titled *"Putting Spec Kit Through Its Paces: Radical Idea or Reinvented Waterfall?"* which concluded that SDD with AI assistance is more iterative than traditional Waterfall. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
## Roadmap
**v0.2.0** was released on March 10, 2026, consolidating the month's work. It includes new extensions (Jira, Azure DevOps, review, sync), support for multiple extension catalogs and community plugins, and additional agent integrations (Tabnine CLI, Kiro CLI). [\[github.com\]](https://github.com/github/spec-kit/releases)
Areas under discussion or in progress for future development:
- **Spec lifecycle management** -- supporting longer-lived specifications that can evolve across multiple iterations, rather than being tied to a single feature branch. Users have raised this in GitHub Discussions, and the concept of "spec-anchored" development is under consideration. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
- **CI/CD integration** -- incorporating Spec Kit verification (e.g., `speckit.checklist` or `speckit.verify`) into pull request workflows and project management tools. February's Jira and Azure DevOps extensions are a step in this direction. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Continued agent support** -- adding integrations as new AI coding assistants emerge. The project currently supports over 20 agents and has been adding new ones (Kiro CLI, Tabnine CLI) as they become available. [\[github.com\]](https://github.com/github/spec-kit)
- **Community ecosystem** -- the open extension model allows external contributors to add functionality directly. February's Jira and Azure DevOps plugins were community-contributed. The Spec Kit README now links to community walkthrough demos for .NET, Spring Boot, and other stacks. [\[github.com\]](https://github.com/github/spec-kit)

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.2.1"
version = "0.2.0"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
@@ -13,7 +13,6 @@ dependencies = [
"truststore>=0.10.4",
"pyyaml>=6.0",
"packaging>=23.0",
"pathspec>=0.12.0",
]
[project.scripts]

View File

@@ -30,12 +30,12 @@
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Antigravity or Generic
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Antigravity or Generic
# - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic
# Leave empty to update all existing agent files
set -e
@@ -78,7 +78,6 @@ KIRO_FILE="$REPO_ROOT/AGENTS.md"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$REPO_ROOT/AGENTS.md"
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
KIMI_FILE="$REPO_ROOT/KIMI.md"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
@@ -666,15 +665,12 @@ update_specific_agent() {
vibe)
update_agent_file "$VIBE_FILE" "Mistral Vibe"
;;
kimi)
update_agent_file "$KIMI_FILE" "Kimi Code"
;;
generic)
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
;;
*)
log_error "Unknown agent type '$agent_type'"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic"
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic"
exit 1
;;
esac
@@ -773,11 +769,6 @@ update_all_existing_agents() {
found_agent=true
fi
if [[ -f "$KIMI_FILE" ]]; then
update_agent_file "$KIMI_FILE" "Kimi Code"
found_agent=true
fi
# If no agent files exist, create a default Claude file
if [[ "$found_agent" == false ]]; then
log_info "No existing agent files found, creating default Claude file..."
@@ -801,7 +792,7 @@ print_summary() {
fi
echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic]"
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic]"
}
#==============================================================================

View File

@@ -250,7 +250,7 @@ if ($branchName.Length -gt $maxBranchLength) {
if ($hasGit) {
$branchCreated = $false
try {
git checkout -q -b $branchName 2>$null | Out-Null
git checkout -b $branchName 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
$branchCreated = $true
}

View File

@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
2. Plan Data Extraction
3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, generic)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, generic)
.PARAMETER AgentType
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
#>
param(
[Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','generic')]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','generic')]
[string]$AgentType
)
@@ -63,7 +63,6 @@ $KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
@@ -407,9 +406,8 @@ function Update-SpecificAgent {
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|generic'; return $false }
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|generic'; return $false }
}
}
@@ -434,7 +432,6 @@ function Update-AllExistingAgents {
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
if (Test-Path $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }; $found = $true }
if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }

View File

@@ -265,13 +265,6 @@ AGENT_CONFIG = {
"install_url": "https://github.com/mistralai/mistral-vibe",
"requires_cli": True,
},
"kimi": {
"name": "Kimi Code",
"folder": ".kimi/",
"commands_subdir": "skills", # Kimi uses /skill:<name> with .kimi/skills/<name>/SKILL.md
"install_url": "https://code.kimi.com/",
"requires_cli": True,
},
"generic": {
"name": "Generic (bring your own agent)",
"folder": None, # Set dynamically via --ai-commands-dir
@@ -1195,12 +1188,7 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
# SKILL_DESCRIPTIONS lookups work.
if command_name.startswith("speckit."):
command_name = command_name[len("speckit."):]
# Kimi CLI discovers skills by directory name and invokes them as
# /skill:<name> — use dot separator to match packaging convention.
if selected_ai == "kimi":
skill_name = f"speckit.{command_name}"
else:
skill_name = f"speckit-{command_name}"
skill_name = f"speckit-{command_name}"
# Create skill directory (additive — never removes existing content)
skill_dir = skills_dir / skill_name

View File

@@ -14,12 +14,10 @@ import zipfile
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Dict, List, Any, Callable, Set
from typing import Optional, Dict, List, Any
from datetime import datetime, timezone
import re
import pathspec
import yaml
from packaging import version as pkg_version
from packaging.specifiers import SpecifierSet, InvalidSpecifier
@@ -282,70 +280,6 @@ class ExtensionManager:
self.extensions_dir = project_root / ".specify" / "extensions"
self.registry = ExtensionRegistry(self.extensions_dir)
@staticmethod
def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]:
"""Load .extensionignore and return an ignore function for shutil.copytree.
The .extensionignore file uses .gitignore-compatible patterns (one per line).
Lines starting with '#' are comments. Blank lines are ignored.
The .extensionignore file itself is always excluded.
Pattern semantics mirror .gitignore:
- '*' matches anything except '/'
- '**' matches zero or more directories
- '?' matches any single character except '/'
- Trailing '/' restricts a pattern to directories only
- Patterns with '/' (other than trailing) are anchored to the root
- '!' negates a previously excluded pattern
Args:
source_dir: Path to the extension source directory
Returns:
An ignore function compatible with shutil.copytree, or None
if no .extensionignore file exists.
"""
ignore_file = source_dir / ".extensionignore"
if not ignore_file.exists():
return None
lines: List[str] = ignore_file.read_text().splitlines()
# Normalise backslashes in patterns so Windows-authored files work
normalised: List[str] = []
for line in lines:
stripped = line.strip()
if stripped and not stripped.startswith("#"):
normalised.append(stripped.replace("\\", "/"))
else:
# Preserve blanks/comments so pathspec line numbers stay stable
normalised.append(line)
# Always ignore the .extensionignore file itself
normalised.append(".extensionignore")
spec = pathspec.GitIgnoreSpec.from_lines(normalised)
def _ignore(directory: str, entries: List[str]) -> Set[str]:
ignored: Set[str] = set()
rel_dir = Path(directory).relative_to(source_dir)
for entry in entries:
rel_path = str(rel_dir / entry) if str(rel_dir) != "." else entry
# Normalise to forward slashes for consistent matching
rel_path_fwd = rel_path.replace("\\", "/")
entry_full = Path(directory) / entry
if entry_full.is_dir():
# Append '/' so directory-only patterns (e.g. tests/) match
if spec.match_file(rel_path_fwd + "/"):
ignored.add(entry)
else:
if spec.match_file(rel_path_fwd):
ignored.add(entry)
return ignored
return _ignore
def check_compatibility(
self,
manifest: ExtensionManifest,
@@ -419,8 +353,7 @@ class ExtensionManager:
if dest_dir.exists():
shutil.rmtree(dest_dir)
ignore_fn = self._load_extensionignore(source_dir)
shutil.copytree(source_dir, dest_dir, ignore=ignore_fn)
shutil.copytree(source_dir, dest_dir)
# Register commands with AI agents
registered_commands = {}
@@ -702,12 +635,6 @@ class CommandRegistrar:
"args": "$ARGUMENTS",
"extension": ".md"
},
"codex": {
"dir": ".codex/prompts",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"windsurf": {
"dir": ".windsurf/workflows",
"format": "markdown",
@@ -727,7 +654,7 @@ class CommandRegistrar:
"extension": ".md"
},
"roo": {
"dir": ".roo/commands",
"dir": ".roo/rules",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
@@ -773,12 +700,6 @@ class CommandRegistrar:
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"kimi": {
"dir": ".kimi/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md"
}
}
@@ -972,7 +893,6 @@ class CommandRegistrar:
# Write command file
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
dest_file.parent.mkdir(parents=True, exist_ok=True)
dest_file.write_text(output)
# Generate companion .prompt.md for Copilot agents
@@ -984,7 +904,6 @@ class CommandRegistrar:
# Register aliases
for alias in cmd_info.get("aliases", []):
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
alias_file.parent.mkdir(parents=True, exist_ok=True)
alias_file.write_text(output)
# Generate companion .prompt.md for alias too
if agent_name == "copilot":

View File

@@ -28,13 +28,6 @@ class TestAgentConfigConsistency:
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
assert "q" not in cfg
def test_extension_registrar_includes_codex(self):
"""Extension command registrar should include codex targeting .codex/prompts."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "codex" in cfg
assert cfg["codex"]["dir"] == ".codex/prompts"
def test_release_agent_lists_include_kiro_cli_and_exclude_q(self):
"""Bash and PowerShell release scripts should agree on agent key set for Kiro."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
@@ -171,58 +164,3 @@ class TestAgentConfigConsistency:
def test_ai_help_includes_tabnine(self):
"""CLI help text for --ai should include tabnine."""
assert "tabnine" in AI_ASSISTANT_HELP
# --- Kimi Code CLI consistency checks ---
def test_kimi_in_agent_config(self):
"""AGENT_CONFIG should include kimi with correct folder and commands_subdir."""
assert "kimi" in AGENT_CONFIG
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi/"
assert AGENT_CONFIG["kimi"]["commands_subdir"] == "skills"
assert AGENT_CONFIG["kimi"]["requires_cli"] is True
def test_kimi_in_extension_registrar(self):
"""Extension command registrar should include kimi using .kimi/skills and SKILL.md."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "kimi" in cfg
kimi_cfg = cfg["kimi"]
assert kimi_cfg["dir"] == ".kimi/skills"
assert kimi_cfg["extension"] == "/SKILL.md"
def test_kimi_in_release_agent_lists(self):
"""Bash and PowerShell release scripts should include kimi in agent lists."""
sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8")
ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8")
sh_match = re.search(r"ALL_AGENTS=\(([^)]*)\)", sh_text)
assert sh_match is not None
sh_agents = sh_match.group(1).split()
ps_match = re.search(r"\$AllAgents = @\(([^)]*)\)", ps_text)
assert ps_match is not None
ps_agents = re.findall(r"'([^']+)'", ps_match.group(1))
assert "kimi" in sh_agents
assert "kimi" in ps_agents
def test_kimi_in_powershell_validate_set(self):
"""PowerShell update-agent-context script should include 'kimi' in ValidateSet."""
ps_text = (REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1").read_text(encoding="utf-8")
validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text)
assert validate_set_match is not None
validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1))
assert "kimi" in validate_set_values
def test_kimi_in_github_release_output(self):
"""GitHub release script should include kimi template packages."""
gh_release_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-github-release.sh").read_text(encoding="utf-8")
assert "spec-kit-template-kimi-sh-" in gh_release_text
assert "spec-kit-template-kimi-ps-" in gh_release_text
def test_ai_help_includes_kimi(self):
"""CLI help text for --ai should include kimi."""
assert "kimi" in AI_ASSISTANT_HELP

View File

@@ -410,11 +410,8 @@ class TestInstallAiSkills:
skills_dir = _get_skills_dir(proj, agent_key)
assert skills_dir.exists()
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
# Kimi uses dot-separator (speckit.specify) to match /skill:speckit.* invocation;
# all other agents use hyphen-separator (speckit-specify).
expected_skill_name = "speckit.specify" if agent_key == "kimi" else "speckit-specify"
assert expected_skill_name in skill_dirs
assert (skills_dir / expected_skill_name / "SKILL.md").exists()
assert "speckit-specify" in skill_dirs
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()

View File

@@ -407,11 +407,6 @@ class TestCommandRegistrar:
assert CommandRegistrar.AGENT_CONFIGS["kiro-cli"]["dir"] == ".kiro/prompts"
assert "q" not in CommandRegistrar.AGENT_CONFIGS
def test_codex_agent_config_present(self):
"""Codex should be mapped to .codex/prompts."""
assert "codex" in CommandRegistrar.AGENT_CONFIGS
assert CommandRegistrar.AGENT_CONFIGS["codex"]["dir"] == ".codex/prompts"
def test_parse_frontmatter_valid(self):
"""Test parsing valid YAML frontmatter."""
content = """---
@@ -1603,343 +1598,3 @@ class TestCatalogStack:
assert len(results) == 1
assert results[0]["_catalog_name"] == "org"
assert results[0]["_install_allowed"] is True
class TestExtensionIgnore:
"""Test .extensionignore support during extension installation."""
def _make_extension(self, temp_dir, valid_manifest_data, extra_files=None, ignore_content=None):
"""Helper to create an extension directory with optional extra files and .extensionignore."""
import yaml
ext_dir = temp_dir / "ignored-ext"
ext_dir.mkdir()
# Write manifest
with open(ext_dir / "extension.yml", "w") as f:
yaml.dump(valid_manifest_data, f)
# Create commands directory with a command file
commands_dir = ext_dir / "commands"
commands_dir.mkdir()
(commands_dir / "hello.md").write_text(
"---\ndescription: \"Test hello command\"\n---\n\n# Hello\n\n$ARGUMENTS\n"
)
# Create any extra files/dirs
if extra_files:
for rel_path, content in extra_files.items():
p = ext_dir / rel_path
p.parent.mkdir(parents=True, exist_ok=True)
if content is None:
# Create directory
p.mkdir(parents=True, exist_ok=True)
else:
p.write_text(content)
# Write .extensionignore
if ignore_content is not None:
(ext_dir / ".extensionignore").write_text(ignore_content)
return ext_dir
def test_no_extensionignore(self, temp_dir, valid_manifest_data):
"""Without .extensionignore, all files are copied."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={"README.md": "# Hello", "tests/test_foo.py": "pass"},
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "README.md").exists()
assert (dest / "tests" / "test_foo.py").exists()
def test_extensionignore_excludes_files(self, temp_dir, valid_manifest_data):
"""Files matching .extensionignore patterns are excluded."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"README.md": "# Hello",
"tests/test_foo.py": "pass",
"tests/test_bar.py": "pass",
".github/workflows/ci.yml": "on: push",
},
ignore_content="tests/\n.github/\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
# Included
assert (dest / "README.md").exists()
assert (dest / "extension.yml").exists()
assert (dest / "commands" / "hello.md").exists()
# Excluded
assert not (dest / "tests").exists()
assert not (dest / ".github").exists()
def test_extensionignore_glob_patterns(self, temp_dir, valid_manifest_data):
"""Glob patterns like *.pyc are respected."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"README.md": "# Hello",
"helpers.pyc": b"\x00".decode("latin-1"),
"commands/cache.pyc": b"\x00".decode("latin-1"),
},
ignore_content="*.pyc\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "README.md").exists()
assert not (dest / "helpers.pyc").exists()
assert not (dest / "commands" / "cache.pyc").exists()
def test_extensionignore_comments_and_blanks(self, temp_dir, valid_manifest_data):
"""Comments and blank lines in .extensionignore are ignored."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={"README.md": "# Hello", "notes.txt": "some notes"},
ignore_content="# This is a comment\n\nnotes.txt\n\n# Another comment\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "README.md").exists()
assert not (dest / "notes.txt").exists()
def test_extensionignore_itself_excluded(self, temp_dir, valid_manifest_data):
""".extensionignore is never copied to the destination."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
ignore_content="# nothing special here\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "extension.yml").exists()
assert not (dest / ".extensionignore").exists()
def test_extensionignore_relative_path_match(self, temp_dir, valid_manifest_data):
"""Patterns matching relative paths work correctly."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"docs/guide.md": "# Guide",
"docs/internal/draft.md": "draft",
"README.md": "# Hello",
},
ignore_content="docs/internal/draft.md\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "docs" / "guide.md").exists()
assert not (dest / "docs" / "internal" / "draft.md").exists()
def test_extensionignore_dotdot_pattern_is_noop(self, temp_dir, valid_manifest_data):
"""Patterns with '..' should not escape the extension root."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={"README.md": "# Hello"},
ignore_content="../sibling/\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
# Everything should still be copied — the '..' pattern matches nothing inside
assert (dest / "README.md").exists()
assert (dest / "extension.yml").exists()
assert (dest / "commands" / "hello.md").exists()
def test_extensionignore_absolute_path_pattern_is_noop(self, temp_dir, valid_manifest_data):
"""Absolute path patterns should not match anything."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={"README.md": "# Hello", "passwd": "sensitive"},
ignore_content="/etc/passwd\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
# Nothing matches — /etc/passwd is anchored to root and there's no 'etc' dir
assert (dest / "README.md").exists()
assert (dest / "passwd").exists()
def test_extensionignore_empty_file(self, temp_dir, valid_manifest_data):
"""An empty .extensionignore should exclude only itself."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={"README.md": "# Hello", "notes.txt": "notes"},
ignore_content="",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "README.md").exists()
assert (dest / "notes.txt").exists()
assert (dest / "extension.yml").exists()
# .extensionignore itself is still excluded
assert not (dest / ".extensionignore").exists()
def test_extensionignore_windows_backslash_patterns(self, temp_dir, valid_manifest_data):
"""Backslash patterns (Windows-style) are normalised to forward slashes."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"docs/internal/draft.md": "draft",
"docs/guide.md": "# Guide",
},
ignore_content="docs\\internal\\draft.md\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert (dest / "docs" / "guide.md").exists()
assert not (dest / "docs" / "internal" / "draft.md").exists()
def test_extensionignore_star_does_not_cross_directories(self, temp_dir, valid_manifest_data):
"""'*' should NOT match across directory boundaries (gitignore semantics)."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"docs/api.draft.md": "draft",
"docs/sub/api.draft.md": "nested draft",
},
ignore_content="docs/*.draft.md\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
# docs/*.draft.md should only match directly inside docs/, NOT subdirs
assert not (dest / "docs" / "api.draft.md").exists()
assert (dest / "docs" / "sub" / "api.draft.md").exists()
def test_extensionignore_doublestar_crosses_directories(self, temp_dir, valid_manifest_data):
"""'**' should match across directory boundaries."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"docs/api.draft.md": "draft",
"docs/sub/api.draft.md": "nested draft",
"docs/guide.md": "guide",
},
ignore_content="docs/**/*.draft.md\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
assert not (dest / "docs" / "api.draft.md").exists()
assert not (dest / "docs" / "sub" / "api.draft.md").exists()
assert (dest / "docs" / "guide.md").exists()
def test_extensionignore_negation_pattern(self, temp_dir, valid_manifest_data):
"""'!' negation re-includes a previously excluded file."""
ext_dir = self._make_extension(
temp_dir,
valid_manifest_data,
extra_files={
"docs/guide.md": "# Guide",
"docs/internal.md": "internal",
"docs/api.md": "api",
},
ignore_content="docs/*.md\n!docs/api.md\n",
)
proj_dir = temp_dir / "project"
proj_dir.mkdir()
(proj_dir / ".specify").mkdir()
manager = ExtensionManager(proj_dir)
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
dest = proj_dir / ".specify" / "extensions" / "test-ext"
# docs/*.md excludes all .md in docs, but !docs/api.md re-includes it
assert not (dest / "docs" / "guide.md").exists()
assert not (dest / "docs" / "internal.md").exists()
assert (dest / "docs" / "api.md").exists()