mirror of
https://github.com/github/spec-kit.git
synced 2026-03-19 11:53:08 +00:00
Compare commits
1 Commits
e56d37db8c
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5daaf23651 |
@@ -53,7 +53,7 @@ echo "✅ Done"
|
|||||||
|
|
||||||
echo -e "\n🤖 Installing Kiro CLI..."
|
echo -e "\n🤖 Installing Kiro CLI..."
|
||||||
# https://kiro.dev/docs/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_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
||||||
KIRO_INSTALLER_PATH="$(mktemp)"
|
KIRO_INSTALLER_PATH="$(mktemp)"
|
||||||
|
|
||||||
@@ -80,11 +80,6 @@ fi
|
|||||||
run_command "$kiro_binary --help > /dev/null"
|
run_command "$kiro_binary --help > /dev/null"
|
||||||
echo "✅ Done"
|
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..."
|
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
||||||
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|||||||
2
.github/workflows/scripts/create-github-release.sh
vendored
Executable file → Normal file
2
.github/workflows/scripts/create-github-release.sh
vendored
Executable file → Normal file
@@ -56,8 +56,6 @@ gh release create "$VERSION" \
|
|||||||
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-vibe-ps-"$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-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
||||||
--title "Spec Kit Templates - $VERSION_NO_V" \
|
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
.PARAMETER Agents
|
.PARAMETER Agents
|
||||||
Comma or space separated subset of agents to build (default: all)
|
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
|
.PARAMETER Scripts
|
||||||
Comma or space separated subset of script types to build (default: both)
|
Comma or space separated subset of script types to build (default: both)
|
||||||
@@ -201,93 +201,6 @@ 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 {
|
function Build-Variant {
|
||||||
param(
|
param(
|
||||||
[string]$Agent,
|
[string]$Agent,
|
||||||
@@ -328,6 +241,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Copy any script files that aren't in variant-specific directories
|
||||||
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
||||||
}
|
}
|
||||||
@@ -367,9 +281,11 @@ function Build-Variant {
|
|||||||
$agentsDir = Join-Path $baseDir ".github/agents"
|
$agentsDir = Join-Path $baseDir ".github/agents"
|
||||||
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
||||||
|
|
||||||
|
# Generate companion prompt files
|
||||||
$promptsDir = Join-Path $baseDir ".github/prompts"
|
$promptsDir = Join-Path $baseDir ".github/prompts"
|
||||||
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
||||||
|
|
||||||
|
# Create VS Code workspace settings
|
||||||
$vscodeDir = Join-Path $baseDir ".vscode"
|
$vscodeDir = Join-Path $baseDir ".vscode"
|
||||||
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
||||||
if (Test-Path "templates/vscode-settings.json") {
|
if (Test-Path "templates/vscode-settings.json") {
|
||||||
@@ -445,19 +361,14 @@ function Build-Variant {
|
|||||||
$cmdDir = Join-Path $baseDir ".agent/workflows"
|
$cmdDir = Join-Path $baseDir ".agent/workflows"
|
||||||
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
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' {
|
'generic' {
|
||||||
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
||||||
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
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 {
|
default {
|
||||||
throw "Unsupported agent '$Agent'."
|
throw "Unsupported agent '$Agent'."
|
||||||
}
|
}
|
||||||
@@ -470,7 +381,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define all agents and scripts
|
# 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')
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
function Normalize-List {
|
function Normalize-List {
|
||||||
@@ -480,6 +391,7 @@ function Normalize-List {
|
|||||||
return @()
|
return @()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Split by comma or space and remove duplicates while preserving order
|
||||||
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
||||||
return $items
|
return $items
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
|||||||
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||||
# Version argument should include leading 'v'.
|
# Version argument should include leading 'v'.
|
||||||
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
||||||
# AGENTS : space or comma separated subset of: claude gemini copilot 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)
|
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
||||||
# Examples:
|
# Examples:
|
||||||
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
||||||
@@ -113,6 +113,7 @@ generate_copilot_prompts() {
|
|||||||
local basename=$(basename "$agent_file" .agent.md)
|
local basename=$(basename "$agent_file" .agent.md)
|
||||||
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
||||||
|
|
||||||
|
# Create prompt file with agent frontmatter
|
||||||
cat > "$prompt_file" <<EOF
|
cat > "$prompt_file" <<EOF
|
||||||
---
|
---
|
||||||
agent: ${basename}
|
agent: ${basename}
|
||||||
@@ -121,76 +122,6 @@ EOF
|
|||||||
done
|
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() {
|
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}"
|
||||||
@@ -209,10 +140,12 @@ build_variant() {
|
|||||||
case $script in
|
case $script in
|
||||||
sh)
|
sh)
|
||||||
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
[[ -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
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
;;
|
;;
|
||||||
ps)
|
ps)
|
||||||
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
[[ -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
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -220,6 +153,11 @@ build_variant() {
|
|||||||
|
|
||||||
[[ -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"; }
|
[[ -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
|
case $agent in
|
||||||
claude)
|
claude)
|
||||||
mkdir -p "$base_dir/.claude/commands"
|
mkdir -p "$base_dir/.claude/commands"
|
||||||
@@ -231,7 +169,9 @@ build_variant() {
|
|||||||
copilot)
|
copilot)
|
||||||
mkdir -p "$base_dir/.github/agents"
|
mkdir -p "$base_dir/.github/agents"
|
||||||
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$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"
|
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
||||||
|
# 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"
|
||||||
;;
|
;;
|
||||||
@@ -288,9 +228,6 @@ build_variant() {
|
|||||||
vibe)
|
vibe)
|
||||||
mkdir -p "$base_dir/.vibe/prompts"
|
mkdir -p "$base_dir/.vibe/prompts"
|
||||||
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
|
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)
|
generic)
|
||||||
mkdir -p "$base_dir/.speckit/commands"
|
mkdir -p "$base_dir/.speckit/commands"
|
||||||
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
||||||
@@ -300,10 +237,11 @@ build_variant() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Determine agent list
|
# 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)
|
ALL_SCRIPTS=(sh ps)
|
||||||
|
|
||||||
norm_list() {
|
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")}'
|
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
||||||
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
||||||
| **Tabnine CLI** | `.tabnine/agent/commands/` | TOML | `tabnine` | Tabnine 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 |
|
| **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 |
|
| **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
|
- **Amp**: `amp` CLI
|
||||||
- **SHAI**: `shai` CLI
|
- **SHAI**: `shai` CLI
|
||||||
- **Tabnine CLI**: `tabnine` CLI
|
- **Tabnine CLI**: `tabnine` CLI
|
||||||
- **Kimi Code**: `kimi` CLI
|
|
||||||
|
|
||||||
### IDE-Based Agents
|
### IDE-Based Agents
|
||||||
|
|
||||||
@@ -339,7 +337,7 @@ Work within integrated development environments:
|
|||||||
|
|
||||||
### Markdown Format
|
### 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:**
|
**Standard format:**
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
|
|
||||||
|
|
||||||
## [0.2.0] - 2026-03-09
|
## [0.2.0] - 2026-03-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- feat: add Kimi Code CLI agent support
|
|
||||||
- fix: sync agent list comments with actual supported agents (#1785)
|
- fix: sync agent list comments with actual supported agents (#1785)
|
||||||
- feat(extensions): support multiple active catalogs simultaneously (#1720)
|
- feat(extensions): support multiple active catalogs simultaneously (#1720)
|
||||||
- Pavel/add tabnine cli support (#1503)
|
- Pavel/add tabnine cli support (#1503)
|
||||||
@@ -50,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: 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)
|
- fix: Split release process to sync pyproject.toml version with git tags (#1732)
|
||||||
|
|
||||||
|
|
||||||
## [0.1.14] - 2026-03-09
|
## [0.1.14] - 2026-03-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
||||||
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
|
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
|
||||||
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
|
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
|
||||||
| [Kimi Code](https://code.kimi.com/) | ✅ | |
|
|
||||||
| [Windsurf](https://windsurf.com/) | ✅ | |
|
| [Windsurf](https://windsurf.com/) | ✅ | |
|
||||||
| [Antigravity (agy)](https://antigravity.google/) | ✅ | |
|
| [Antigravity (agy)](https://antigravity.google/) | ✅ | |
|
||||||
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
| 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 |
|
| Command | Description |
|
||||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `init` | Initialize a new Specify project from the latest template |
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `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
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
| Argument/Option | Type | Description |
|
| Argument/Option | Type | Description |
|
||||||
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
| `<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/`) |
|
| `--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) |
|
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||||
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
||||||
|
|||||||
@@ -173,6 +173,6 @@ Finally, implement the solution:
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
|
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
|
||||||
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
|
- Check out [more examples](../templates) in the repository
|
||||||
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
||||||
|
|||||||
@@ -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
|
## Validation Rules
|
||||||
|
|
||||||
### Extension ID
|
### Extension ID
|
||||||
|
|||||||
@@ -432,26 +432,6 @@ Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simul
|
|||||||
specify extension catalog list
|
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)
|
### Adding a Catalog (Project-scoped)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ dependencies = [
|
|||||||
"truststore>=0.10.4",
|
"truststore>=0.10.4",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"packaging>=23.0",
|
"packaging>=23.0",
|
||||||
"pathspec>=0.12.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
#
|
#
|
||||||
# 5. Multi-Agent Support
|
# 5. Multi-Agent Support
|
||||||
# - Handles agent-specific file paths and naming conventions
|
# - 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
|
# - Can update single agents or all existing agent files
|
||||||
# - Creates default Claude file if no agent files exist
|
# - Creates default Claude file if no agent files exist
|
||||||
#
|
#
|
||||||
# Usage: ./update-agent-context.sh [agent_type]
|
# 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
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -78,7 +78,6 @@ KIRO_FILE="$REPO_ROOT/AGENTS.md"
|
|||||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||||
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
||||||
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
|
||||||
|
|
||||||
# Template file
|
# Template file
|
||||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||||
@@ -666,15 +665,12 @@ update_specific_agent() {
|
|||||||
vibe)
|
vibe)
|
||||||
update_agent_file "$VIBE_FILE" "Mistral Vibe"
|
update_agent_file "$VIBE_FILE" "Mistral Vibe"
|
||||||
;;
|
;;
|
||||||
kimi)
|
|
||||||
update_agent_file "$KIMI_FILE" "Kimi Code"
|
|
||||||
;;
|
|
||||||
generic)
|
generic)
|
||||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
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 "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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -773,11 +769,6 @@ update_all_existing_agents() {
|
|||||||
found_agent=true
|
found_agent=true
|
||||||
fi
|
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 no agent files exist, create a default Claude file
|
||||||
if [[ "$found_agent" == false ]]; then
|
if [[ "$found_agent" == false ]]; then
|
||||||
log_info "No existing agent files found, creating default Claude file..."
|
log_info "No existing agent files found, creating default Claude file..."
|
||||||
@@ -801,7 +792,7 @@ print_summary() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
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]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ if ($branchName.Length -gt $maxBranchLength) {
|
|||||||
if ($hasGit) {
|
if ($hasGit) {
|
||||||
$branchCreated = $false
|
$branchCreated = $false
|
||||||
try {
|
try {
|
||||||
git checkout -q -b $branchName 2>$null | Out-Null
|
git checkout -b $branchName 2>$null | Out-Null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$branchCreated = $true
|
$branchCreated = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
|
|||||||
2. Plan Data Extraction
|
2. Plan Data Extraction
|
||||||
3. Agent File Management (create from template or update existing)
|
3. Agent File Management (create from template or update existing)
|
||||||
4. Content Generation (technology stack, recent changes, timestamp)
|
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
|
.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).
|
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(
|
param(
|
||||||
[Parameter(Position=0)]
|
[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
|
[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'
|
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
||||||
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-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'
|
$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' }
|
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
||||||
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
||||||
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
|
'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.' }
|
'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 $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 $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 $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) {
|
if (-not $found) {
|
||||||
Write-Info 'No existing agent files found, creating default Claude file...'
|
Write-Info 'No existing agent files found, creating default Claude file...'
|
||||||
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
||||||
|
|||||||
@@ -265,13 +265,6 @@ AGENT_CONFIG = {
|
|||||||
"install_url": "https://github.com/mistralai/mistral-vibe",
|
"install_url": "https://github.com/mistralai/mistral-vibe",
|
||||||
"requires_cli": True,
|
"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": {
|
"generic": {
|
||||||
"name": "Generic (bring your own agent)",
|
"name": "Generic (bring your own agent)",
|
||||||
"folder": None, # Set dynamically via --ai-commands-dir
|
"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.
|
# SKILL_DESCRIPTIONS lookups work.
|
||||||
if command_name.startswith("speckit."):
|
if command_name.startswith("speckit."):
|
||||||
command_name = command_name[len("speckit."):]
|
command_name = command_name[len("speckit."):]
|
||||||
# Kimi CLI discovers skills by directory name and invokes them as
|
skill_name = f"speckit-{command_name}"
|
||||||
# /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}"
|
|
||||||
|
|
||||||
# Create skill directory (additive — never removes existing content)
|
# Create skill directory (additive — never removes existing content)
|
||||||
skill_dir = skills_dir / skill_name
|
skill_dir = skills_dir / skill_name
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ import zipfile
|
|||||||
import shutil
|
import shutil
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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
|
from datetime import datetime, timezone
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pathspec
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from packaging import version as pkg_version
|
from packaging import version as pkg_version
|
||||||
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
||||||
@@ -282,70 +280,6 @@ class ExtensionManager:
|
|||||||
self.extensions_dir = project_root / ".specify" / "extensions"
|
self.extensions_dir = project_root / ".specify" / "extensions"
|
||||||
self.registry = ExtensionRegistry(self.extensions_dir)
|
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(
|
def check_compatibility(
|
||||||
self,
|
self,
|
||||||
manifest: ExtensionManifest,
|
manifest: ExtensionManifest,
|
||||||
@@ -419,8 +353,7 @@ class ExtensionManager:
|
|||||||
if dest_dir.exists():
|
if dest_dir.exists():
|
||||||
shutil.rmtree(dest_dir)
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
ignore_fn = self._load_extensionignore(source_dir)
|
shutil.copytree(source_dir, dest_dir)
|
||||||
shutil.copytree(source_dir, dest_dir, ignore=ignore_fn)
|
|
||||||
|
|
||||||
# Register commands with AI agents
|
# Register commands with AI agents
|
||||||
registered_commands = {}
|
registered_commands = {}
|
||||||
@@ -702,12 +635,6 @@ class CommandRegistrar:
|
|||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
},
|
},
|
||||||
"codex": {
|
|
||||||
"dir": ".codex/prompts",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"windsurf": {
|
"windsurf": {
|
||||||
"dir": ".windsurf/workflows",
|
"dir": ".windsurf/workflows",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
@@ -727,7 +654,7 @@ class CommandRegistrar:
|
|||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
},
|
},
|
||||||
"roo": {
|
"roo": {
|
||||||
"dir": ".roo/commands",
|
"dir": ".roo/rules",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
@@ -773,12 +700,6 @@ class CommandRegistrar:
|
|||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".md"
|
||||||
},
|
|
||||||
"kimi": {
|
|
||||||
"dir": ".kimi/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,7 +893,6 @@ class CommandRegistrar:
|
|||||||
|
|
||||||
# Write command file
|
# Write command file
|
||||||
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
||||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
dest_file.write_text(output)
|
dest_file.write_text(output)
|
||||||
|
|
||||||
# Generate companion .prompt.md for Copilot agents
|
# Generate companion .prompt.md for Copilot agents
|
||||||
@@ -984,7 +904,6 @@ class CommandRegistrar:
|
|||||||
# Register aliases
|
# Register aliases
|
||||||
for alias in cmd_info.get("aliases", []):
|
for alias in cmd_info.get("aliases", []):
|
||||||
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
||||||
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
alias_file.write_text(output)
|
alias_file.write_text(output)
|
||||||
# Generate companion .prompt.md for alias too
|
# Generate companion .prompt.md for alias too
|
||||||
if agent_name == "copilot":
|
if agent_name == "copilot":
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ class TestAgentConfigConsistency:
|
|||||||
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
|
assert cfg["kiro-cli"]["dir"] == ".kiro/prompts"
|
||||||
assert "q" not in cfg
|
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):
|
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."""
|
"""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")
|
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):
|
def test_ai_help_includes_tabnine(self):
|
||||||
"""CLI help text for --ai should include tabnine."""
|
"""CLI help text for --ai should include tabnine."""
|
||||||
assert "tabnine" in AI_ASSISTANT_HELP
|
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
|
|
||||||
|
|||||||
@@ -410,11 +410,8 @@ class TestInstallAiSkills:
|
|||||||
skills_dir = _get_skills_dir(proj, agent_key)
|
skills_dir = _get_skills_dir(proj, agent_key)
|
||||||
assert skills_dir.exists()
|
assert skills_dir.exists()
|
||||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
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;
|
assert "speckit-specify" in skill_dirs
|
||||||
# all other agents use hyphen-separator (speckit-specify).
|
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -407,11 +407,6 @@ class TestCommandRegistrar:
|
|||||||
assert CommandRegistrar.AGENT_CONFIGS["kiro-cli"]["dir"] == ".kiro/prompts"
|
assert CommandRegistrar.AGENT_CONFIGS["kiro-cli"]["dir"] == ".kiro/prompts"
|
||||||
assert "q" not in CommandRegistrar.AGENT_CONFIGS
|
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):
|
def test_parse_frontmatter_valid(self):
|
||||||
"""Test parsing valid YAML frontmatter."""
|
"""Test parsing valid YAML frontmatter."""
|
||||||
content = """---
|
content = """---
|
||||||
@@ -1603,343 +1598,3 @@ class TestCatalogStack:
|
|||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert results[0]["_catalog_name"] == "org"
|
assert results[0]["_catalog_name"] == "org"
|
||||||
assert results[0]["_install_allowed"] is True
|
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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user