Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65ecaa9
Initial plan
Copilot Mar 9, 2026
2e8a4d6
feat(templates): add pluggable template system with packs, catalog, r…
Copilot Mar 9, 2026
6003a23
test(templates): add comprehensive unit tests for template pack system
Copilot Mar 9, 2026
abf4aeb
feat(presets): pluggable preset system with template/command override…
mnriem Mar 10, 2026
914a06a
Merge remote-tracking branch 'upstream/main' into pr-1787
mnriem Mar 10, 2026
35ced30
feat(presets): propagate command overrides to skills via init-options
mnriem Mar 10, 2026
445eefe
fix: address PR check failures (ruff F541, CodeQL URL substring)
mnriem Mar 10, 2026
52f137c
fix: address Copilot PR review comments
mnriem Mar 10, 2026
3ffef55
fix: narrow empty except blocks and add explanatory comments
mnriem Mar 10, 2026
da6e7d2
fix: address Copilot PR review comments (round 2)
mnriem Mar 10, 2026
1c143e6
fix: remove self-test from catalog.json (local-only preset)
mnriem Mar 10, 2026
6da1375
fix: address Copilot PR review comments (round 3)
mnriem Mar 10, 2026
f5f8311
fix: correct PresetError docstring from template to preset
mnriem Mar 10, 2026
d8bc72f
Merge remote-tracking branch 'upstream/main' into copilot/add-pluggab…
mnriem Mar 13, 2026
7259652
Removed CHANGELOG requirement
mnriem Mar 13, 2026
f7fbda5
Applying review recommendations
mnriem Mar 13, 2026
db66637
Applying review recommendations
mnriem Mar 13, 2026
1a0f8b1
Applying review recommendations
mnriem Mar 13, 2026
13a46dd
Applying review recommendations
mnriem Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ Recent changes to the Specify CLI and templates are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.1] - 2026-03-09

### Added

- feat(templates): Pluggable template system with template packs, catalog, and resolver
- Template pack manifest (`template-pack.yml`) with validation for artifact, command, and script types
- `TemplatePackManifest`, `TemplatePackRegistry`, `TemplatePackManager`, `TemplateCatalog`, `TemplateResolver` classes in `src/specify_cli/templates.py`
- CLI commands: `specify template search`, `specify template add`, `specify template list`, `specify template remove`, `specify template resolve`
- `--template` option for `specify init` to install template packs during initialization
- `resolve_template()` / `Resolve-Template` helpers in bash and PowerShell common scripts
- Template resolution priority stack: overrides → packs → extensions → core
- Template catalog files (`templates/catalog.json`, `templates/catalog.community.json`)
- Template pack scaffold directory (`templates/template/`)
- Scripts updated to use template resolution instead of hardcoded paths

## [0.2.0] - 2026-03-09

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.2.0"
version = "0.2.1"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [
Expand Down
41 changes: 41 additions & 0 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,44 @@ EOF
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }

# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/templates/packs/<pack-id>/templates/
# 3. .specify/extensions/<ext-id>/templates/
# 4. .specify/templates/ (core)
resolve_template() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"

# Priority 1: Project overrides
local override="$base/overrides/${template_name}.md"
[ -f "$override" ] && echo "$override" && return 0

# Priority 2: Installed packs (by directory order)
local packs_dir="$base/packs"
if [ -d "$packs_dir" ]; then
for pack in "$packs_dir"/*/; do
[ -d "$pack" ] || continue
local candidate="$pack/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi

# Priority 3: Extension-provided templates
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
local candidate="$ext/templates/${template_name}.md"
[ -f "$candidate" ] && echo "$candidate" && return 0
done
fi
Comment thread
mnriem marked this conversation as resolved.

# Priority 4: Core templates
local core="$base/${template_name}.md"
[ -f "$core" ] && echo "$core" && return 0

return 1
Comment thread
mnriem marked this conversation as resolved.
Outdated
}

5 changes: 3 additions & 2 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ clean_branch_name() {
# to searching for repository markers so the workflow still functions in repositories that
# were initialised with --no-git.
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"

if git rev-parse --show-toplevel >/dev/null 2>&1; then
REPO_ROOT=$(git rev-parse --show-toplevel)
Expand Down Expand Up @@ -296,9 +297,9 @@ fi
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"

TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT")
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi

# Set the SPECIFY_FEATURE environment variable for the current session
export SPECIFY_FEATURE="$BRANCH_NAME"
Expand Down
6 changes: 3 additions & 3 deletions scripts/bash/setup-plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
mkdir -p "$FEATURE_DIR"

# Copy plan template if it exists
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
if [[ -f "$TEMPLATE" ]]; then
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT")
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
cp "$TEMPLATE" "$IMPL_PLAN"
echo "Copied plan template to $IMPL_PLAN"
else
echo "Warning: Plan template not found at $TEMPLATE"
echo "Warning: Plan template not found"
# Create a basic plan file if template doesn't exist
touch "$IMPL_PLAN"
fi
Expand Down
42 changes: 42 additions & 0 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,45 @@ function Test-DirHasFiles {
}
}

# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/templates/packs/<pack-id>/templates/
# 3. .specify/extensions/<ext-id>/templates/
# 4. .specify/templates/ (core)
function Resolve-Template {
param(
[Parameter(Mandatory=$true)][string]$TemplateName,
[Parameter(Mandatory=$true)][string]$RepoRoot
)

$base = Join-Path $RepoRoot '.specify/templates'

# Priority 1: Project overrides
$override = Join-Path $base "overrides/$TemplateName.md"
if (Test-Path $override) { return $override }

# Priority 2: Installed packs (by directory order)
$packsDir = Join-Path $base 'packs'
if (Test-Path $packsDir) {
foreach ($pack in Get-ChildItem -Path $packsDir -Directory -ErrorAction SilentlyContinue) {
$candidate = Join-Path $pack.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) { return $candidate }
}
}

# Priority 3: Extension-provided templates
$extDir = Join-Path $RepoRoot '.specify/extensions'
if (Test-Path $extDir) {
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue) {
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
Comment thread
mnriem marked this conversation as resolved.
if (Test-Path $candidate) { return $candidate }
}
}

# Priority 4: Core templates
$core = Join-Path $base "$TemplateName.md"
if (Test-Path $core) { return $core }

return $null
}

7 changes: 5 additions & 2 deletions scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ if (-not $fallbackRoot) {
exit 1
}

# Load common functions (includes Resolve-Template)
. "$PSScriptRoot/common.ps1"

try {
$repoRoot = git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -eq 0) {
Expand Down Expand Up @@ -276,9 +279,9 @@ if ($hasGit) {
$featureDir = Join-Path $specsDir $branchName
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null

$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
$specFile = Join-Path $featureDir 'spec.md'
if (Test-Path $template) {
if ($template -and (Test-Path $template)) {
Copy-Item $template $specFile -Force
} else {
New-Item -ItemType File -Path $specFile | Out-Null
Expand Down
6 changes: 3 additions & 3 deletions scripts/powershell/setup-plan.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GI
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null

# Copy plan template if it exists, otherwise note it or create empty file
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
if (Test-Path $template) {
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
if ($template -and (Test-Path $template)) {
Copy-Item $template $paths.IMPL_PLAN -Force
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
} else {
Write-Warning "Plan template not found at $template"
Write-Warning "Plan template not found"
# Create a basic plan file if template doesn't exist
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
}
Expand Down
Loading