mirror of
https://github.com/github/spec-kit.git
synced 2026-03-19 20:03:07 +00:00
Compare commits
7 Commits
e56d37db8c
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d4f63298 | ||
|
|
990a1513c2 | ||
|
|
9a82f098e8 | ||
|
|
5c77268136 | ||
|
|
ad591607ea | ||
|
|
50c605ed5f | ||
|
|
623cd79b38 |
@@ -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:**
|
||||||
|
|
||||||
|
|||||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -7,49 +7,6 @@ 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
|
|
||||||
|
|
||||||
### 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)
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
## [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 |
|
||||||
@@ -422,7 +421,7 @@ specify init . --force --ai claude
|
|||||||
specify init --here --force --ai claude
|
specify init --here --force --ai claude
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, or Kiro CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify init <project_name> --ai claude --ignore-agent-tools
|
specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.2.0"
|
version = "0.1.14"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -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 or Antigravity
|
||||||
# - 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)
|
||||||
|
|
||||||
.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,11 +1188,6 @@ 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> — 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)
|
# Create skill directory (additive — never removes existing content)
|
||||||
|
|||||||
@@ -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