mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 10:53:08 +00:00
* fix: migrate Qwen Code CLI from TOML to Markdown format (#1589) Qwen Code CLI v0.10.0 deprecated TOML format and fully switched to Markdown as the core format for configuration and interaction files. - Update create-release-packages.sh: generate .md files with $ARGUMENTS instead of .toml files with {{args}} for qwen agent - Update create-release-packages.ps1: same change for PowerShell script - Update AGENTS.md: reflect Qwen's new Markdown format in docs and remove Qwen from TOML format section - Update tests/test_ai_skills.py: add commands_dir_qwen fixture and tests covering Markdown-format skills installation for Qwen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update CommandRegistrar qwen config to Markdown format extensions.py CommandRegistrar.AGENT_CONFIGS['qwen'] was still set to TOML format, causing `specify extension` to write .toml files into .qwen/commands, conflicting with Qwen Code CLI v0.10.0+ expectations. - Change qwen format from toml to markdown - Change qwen args from {{args}} to $ARGUMENTS - Change qwen extension from .toml to .md - Add test to assert qwen config is Markdown format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
349 lines
14 KiB
Bash
Executable File
349 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# create-release-packages.sh (workflow-local)
|
|
# Build Spec Kit template release archives for each supported AI assistant and script type.
|
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
|
# Version argument should include leading 'v'.
|
|
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
|
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi generic (default: all)
|
|
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
|
# Examples:
|
|
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
|
# AGENTS="copilot,gemini" $0 v0.2.0
|
|
# SCRIPTS=ps $0 v0.2.0
|
|
|
|
if [[ $# -ne 1 ]]; then
|
|
echo "Usage: $0 <version-with-v-prefix>" >&2
|
|
exit 1
|
|
fi
|
|
NEW_VERSION="$1"
|
|
if [[ ! $NEW_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "Version must look like v0.0.0" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Building release packages for $NEW_VERSION"
|
|
|
|
# Create and use .genreleases directory for all build artifacts
|
|
GENRELEASES_DIR=".genreleases"
|
|
mkdir -p "$GENRELEASES_DIR"
|
|
rm -rf "$GENRELEASES_DIR"/* || true
|
|
|
|
rewrite_paths() {
|
|
sed -E \
|
|
-e 's@(/?)memory/@.specify/memory/@g' \
|
|
-e 's@(/?)scripts/@.specify/scripts/@g' \
|
|
-e 's@(/?)templates/@.specify/templates/@g' \
|
|
-e 's@\.specify\.specify/@.specify/@g'
|
|
}
|
|
|
|
generate_commands() {
|
|
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
|
|
mkdir -p "$output_dir"
|
|
for template in templates/commands/*.md; do
|
|
[[ -f "$template" ]] || continue
|
|
local name description script_command agent_script_command body
|
|
name=$(basename "$template" .md)
|
|
|
|
# Normalize line endings
|
|
file_content=$(tr -d '\r' < "$template")
|
|
|
|
# Extract description and script command from YAML frontmatter
|
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
|
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
|
|
|
if [[ -z $script_command ]]; then
|
|
echo "Warning: no script command found for $script_variant in $template" >&2
|
|
script_command="(Missing script command for $script_variant)"
|
|
fi
|
|
|
|
# Extract agent_script command from YAML frontmatter if present
|
|
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
|
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
|
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
|
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
|
print
|
|
exit
|
|
}
|
|
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
|
')
|
|
|
|
# Replace {SCRIPT} placeholder with the script command
|
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
|
|
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
|
if [[ -n $agent_script_command ]]; then
|
|
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
|
fi
|
|
|
|
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
|
|
body=$(printf '%s\n' "$body" | awk '
|
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
|
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 }
|
|
')
|
|
|
|
# Apply other substitutions
|
|
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
|
|
|
case $ext in
|
|
toml)
|
|
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
|
|
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
|
|
md)
|
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
|
agent.md)
|
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
generate_copilot_prompts() {
|
|
local agents_dir=$1 prompts_dir=$2
|
|
mkdir -p "$prompts_dir"
|
|
|
|
# Generate a .prompt.md file for each .agent.md file
|
|
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
|
[[ -f "$agent_file" ]] || continue
|
|
|
|
local basename=$(basename "$agent_file" .agent.md)
|
|
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
|
|
|
cat > "$prompt_file" <<EOF
|
|
---
|
|
agent: ${basename}
|
|
---
|
|
EOF
|
|
done
|
|
}
|
|
|
|
# Create Kimi Code skills in .kimi/skills/<name>/SKILL.md format.
|
|
# Kimi CLI discovers skills as directories containing a SKILL.md file,
|
|
# invoked with /skill:<name> (e.g. /skill:speckit.specify).
|
|
create_kimi_skills() {
|
|
local skills_dir="$1"
|
|
local script_variant="$2"
|
|
|
|
for template in templates/commands/*.md; do
|
|
[[ -f "$template" ]] || continue
|
|
local name
|
|
name=$(basename "$template" .md)
|
|
local skill_name="speckit.${name}"
|
|
local skill_dir="${skills_dir}/${skill_name}"
|
|
mkdir -p "$skill_dir"
|
|
|
|
local file_content
|
|
file_content=$(tr -d '\r' < "$template")
|
|
|
|
# Extract description from frontmatter
|
|
local description
|
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
|
[[ -z "$description" ]] && description="Spec Kit: ${name} workflow"
|
|
|
|
# Extract script command
|
|
local script_command
|
|
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
|
[[ -z "$script_command" ]] && script_command="(Missing script command for $script_variant)"
|
|
|
|
# Extract agent_script command from frontmatter if present
|
|
local agent_script_command
|
|
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
|
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
|
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
|
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
|
print
|
|
exit
|
|
}
|
|
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
|
')
|
|
|
|
# Build body: replace placeholders, strip scripts sections, rewrite paths
|
|
local body
|
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
|
if [[ -n $agent_script_command ]]; then
|
|
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
|
fi
|
|
body=$(printf '%s\n' "$body" | awk '
|
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
|
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
|
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
|
|
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
|
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
|
{ print }
|
|
')
|
|
body=$(printf '%s\n' "$body" | sed 's/{ARGS}/\$ARGUMENTS/g' | sed 's/__AGENT__/kimi/g' | rewrite_paths)
|
|
|
|
# Strip existing frontmatter and prepend Kimi frontmatter
|
|
local template_body
|
|
template_body=$(printf '%s\n' "$body" | awk '/^---/{p++; if(p==2){found=1; next}} found')
|
|
|
|
{
|
|
printf -- '---\n'
|
|
printf 'name: "%s"\n' "$skill_name"
|
|
printf 'description: "%s"\n' "$description"
|
|
printf -- '---\n\n'
|
|
printf '%s\n' "$template_body"
|
|
} > "$skill_dir/SKILL.md"
|
|
done
|
|
}
|
|
|
|
build_variant() {
|
|
local agent=$1 script=$2
|
|
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
|
echo "Building $agent ($script) package..."
|
|
mkdir -p "$base_dir"
|
|
|
|
# Copy base structure but filter scripts by variant
|
|
SPEC_DIR="$base_dir/.specify"
|
|
mkdir -p "$SPEC_DIR"
|
|
|
|
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
|
|
|
# Only copy the relevant script variant directory
|
|
if [[ -d scripts ]]; then
|
|
mkdir -p "$SPEC_DIR/scripts"
|
|
case $script in
|
|
sh)
|
|
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
|
;;
|
|
ps)
|
|
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
|
|
|
case $agent in
|
|
claude)
|
|
mkdir -p "$base_dir/.claude/commands"
|
|
generate_commands claude md "\$ARGUMENTS" "$base_dir/.claude/commands" "$script" ;;
|
|
gemini)
|
|
mkdir -p "$base_dir/.gemini/commands"
|
|
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
|
|
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
|
|
copilot)
|
|
mkdir -p "$base_dir/.github/agents"
|
|
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
|
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
|
mkdir -p "$base_dir/.vscode"
|
|
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
|
|
;;
|
|
cursor-agent)
|
|
mkdir -p "$base_dir/.cursor/commands"
|
|
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
|
|
qwen)
|
|
mkdir -p "$base_dir/.qwen/commands"
|
|
generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
|
|
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
|
opencode)
|
|
mkdir -p "$base_dir/.opencode/command"
|
|
generate_commands opencode md "\$ARGUMENTS" "$base_dir/.opencode/command" "$script" ;;
|
|
windsurf)
|
|
mkdir -p "$base_dir/.windsurf/workflows"
|
|
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
|
codex)
|
|
mkdir -p "$base_dir/.codex/prompts"
|
|
generate_commands codex md "\$ARGUMENTS" "$base_dir/.codex/prompts" "$script" ;;
|
|
kilocode)
|
|
mkdir -p "$base_dir/.kilocode/workflows"
|
|
generate_commands kilocode md "\$ARGUMENTS" "$base_dir/.kilocode/workflows" "$script" ;;
|
|
auggie)
|
|
mkdir -p "$base_dir/.augment/commands"
|
|
generate_commands auggie md "\$ARGUMENTS" "$base_dir/.augment/commands" "$script" ;;
|
|
roo)
|
|
mkdir -p "$base_dir/.roo/commands"
|
|
generate_commands roo md "\$ARGUMENTS" "$base_dir/.roo/commands" "$script" ;;
|
|
codebuddy)
|
|
mkdir -p "$base_dir/.codebuddy/commands"
|
|
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
|
|
qodercli)
|
|
mkdir -p "$base_dir/.qoder/commands"
|
|
generate_commands qodercli md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
|
|
amp)
|
|
mkdir -p "$base_dir/.agents/commands"
|
|
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
|
|
shai)
|
|
mkdir -p "$base_dir/.shai/commands"
|
|
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
|
|
tabnine)
|
|
mkdir -p "$base_dir/.tabnine/agent/commands"
|
|
generate_commands tabnine toml "{{args}}" "$base_dir/.tabnine/agent/commands" "$script"
|
|
[[ -f agent_templates/tabnine/TABNINE.md ]] && cp agent_templates/tabnine/TABNINE.md "$base_dir/TABNINE.md" ;;
|
|
kiro-cli)
|
|
mkdir -p "$base_dir/.kiro/prompts"
|
|
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
|
|
agy)
|
|
mkdir -p "$base_dir/.agent/commands"
|
|
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;;
|
|
bob)
|
|
mkdir -p "$base_dir/.bob/commands"
|
|
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
|
vibe)
|
|
mkdir -p "$base_dir/.vibe/prompts"
|
|
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
|
|
kimi)
|
|
mkdir -p "$base_dir/.kimi/skills"
|
|
create_kimi_skills "$base_dir/.kimi/skills" "$script" ;;
|
|
generic)
|
|
mkdir -p "$base_dir/.speckit/commands"
|
|
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
|
esac
|
|
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
|
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
|
}
|
|
|
|
# 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_SCRIPTS=(sh ps)
|
|
|
|
norm_list() {
|
|
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
|
|
}
|
|
|
|
validate_subset() {
|
|
local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
|
|
local invalid=0
|
|
for it in "${items[@]}"; do
|
|
local found=0
|
|
for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done
|
|
if [[ $found -eq 0 ]]; then
|
|
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
|
|
invalid=1
|
|
fi
|
|
done
|
|
return $invalid
|
|
}
|
|
|
|
if [[ -n ${AGENTS:-} ]]; then
|
|
mapfile -t AGENT_LIST < <(printf '%s' "$AGENTS" | norm_list)
|
|
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
|
|
else
|
|
AGENT_LIST=("${ALL_AGENTS[@]}")
|
|
fi
|
|
|
|
if [[ -n ${SCRIPTS:-} ]]; then
|
|
mapfile -t SCRIPT_LIST < <(printf '%s' "$SCRIPTS" | norm_list)
|
|
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
|
|
else
|
|
SCRIPT_LIST=("${ALL_SCRIPTS[@]}")
|
|
fi
|
|
|
|
echo "Agents: ${AGENT_LIST[*]}"
|
|
echo "Scripts: ${SCRIPT_LIST[*]}"
|
|
|
|
for agent in "${AGENT_LIST[@]}"; do
|
|
for script in "${SCRIPT_LIST[@]}"; do
|
|
build_variant "$agent" "$script"
|
|
done
|
|
done
|
|
|
|
echo "Archives in $GENRELEASES_DIR:"
|
|
ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip
|