Skip to content

Commit 2e8a4d6

Browse files
Copilotmnriem
andcommitted
feat(templates): add pluggable template system with packs, catalog, resolver, and CLI commands
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
1 parent 65ecaa9 commit 2e8a4d6

15 files changed

Lines changed: 1450 additions & 11 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ Recent changes to the Specify CLI and templates are documented here.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.2.1] - 2026-03-09
11+
12+
### Added
13+
14+
- feat(templates): Pluggable template system with template packs, catalog, and resolver
15+
- Template pack manifest (`template-pack.yml`) with validation for artifact, command, and script types
16+
- `TemplatePackManifest`, `TemplatePackRegistry`, `TemplatePackManager`, `TemplateCatalog`, `TemplateResolver` classes in `src/specify_cli/templates.py`
17+
- CLI commands: `specify template search`, `specify template add`, `specify template list`, `specify template remove`, `specify template resolve`
18+
- `--template` option for `specify init` to install template packs during initialization
19+
- `resolve_template()` / `Resolve-Template` helpers in bash and PowerShell common scripts
20+
- Template resolution priority stack: overrides → packs → extensions → core
21+
- Template catalog files (`templates/catalog.json`, `templates/catalog.community.json`)
22+
- Template pack scaffold directory (`templates/template/`)
23+
- Scripts updated to use template resolution instead of hardcoded paths
24+
1025
## [0.2.0] - 2026-03-09
1126

1227
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
requires-python = ">=3.11"
66
dependencies = [

scripts/bash/common.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,44 @@ EOF
154154
check_file() { [[ -f "$1" ]] && echo "$2" || echo "$2"; }
155155
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo "$2" || echo "$2"; }
156156

157+
# Resolve a template name to a file path using the priority stack:
158+
# 1. .specify/templates/overrides/
159+
# 2. .specify/templates/packs/<pack-id>/templates/
160+
# 3. .specify/extensions/<ext-id>/templates/
161+
# 4. .specify/templates/ (core)
162+
resolve_template() {
163+
local template_name="$1"
164+
local repo_root="$2"
165+
local base="$repo_root/.specify/templates"
166+
167+
# Priority 1: Project overrides
168+
local override="$base/overrides/${template_name}.md"
169+
[ -f "$override" ] && echo "$override" && return 0
170+
171+
# Priority 2: Installed packs (by directory order)
172+
local packs_dir="$base/packs"
173+
if [ -d "$packs_dir" ]; then
174+
for pack in "$packs_dir"/*/; do
175+
[ -d "$pack" ] || continue
176+
local candidate="$pack/templates/${template_name}.md"
177+
[ -f "$candidate" ] && echo "$candidate" && return 0
178+
done
179+
fi
180+
181+
# Priority 3: Extension-provided templates
182+
local ext_dir="$repo_root/.specify/extensions"
183+
if [ -d "$ext_dir" ]; then
184+
for ext in "$ext_dir"/*/; do
185+
[ -d "$ext" ] || continue
186+
local candidate="$ext/templates/${template_name}.md"
187+
[ -f "$candidate" ] && echo "$candidate" && return 0
188+
done
189+
fi
190+
191+
# Priority 4: Core templates
192+
local core="$base/${template_name}.md"
193+
[ -f "$core" ] && echo "$core" && return 0
194+
195+
return 1
196+
}
197+

scripts/bash/create-new-feature.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ clean_branch_name() {
166166
# to searching for repository markers so the workflow still functions in repositories that
167167
# were initialised with --no-git.
168168
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
169+
source "$SCRIPT_DIR/common.sh"
169170

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

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

303304
# Set the SPECIFY_FEATURE environment variable for the current session
304305
export SPECIFY_FEATURE="$BRANCH_NAME"

scripts/bash/setup-plan.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
3737
mkdir -p "$FEATURE_DIR"
3838

3939
# Copy plan template if it exists
40-
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
41-
if [[ -f "$TEMPLATE" ]]; then
40+
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT")
41+
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
4242
cp "$TEMPLATE" "$IMPL_PLAN"
4343
echo "Copied plan template to $IMPL_PLAN"
4444
else
45-
echo "Warning: Plan template not found at $TEMPLATE"
45+
echo "Warning: Plan template not found"
4646
# Create a basic plan file if template doesn't exist
4747
touch "$IMPL_PLAN"
4848
fi

scripts/powershell/common.ps1

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,45 @@ function Test-DirHasFiles {
135135
}
136136
}
137137

138+
# Resolve a template name to a file path using the priority stack:
139+
# 1. .specify/templates/overrides/
140+
# 2. .specify/templates/packs/<pack-id>/templates/
141+
# 3. .specify/extensions/<ext-id>/templates/
142+
# 4. .specify/templates/ (core)
143+
function Resolve-Template {
144+
param(
145+
[Parameter(Mandatory=$true)][string]$TemplateName,
146+
[Parameter(Mandatory=$true)][string]$RepoRoot
147+
)
148+
149+
$base = Join-Path $RepoRoot '.specify/templates'
150+
151+
# Priority 1: Project overrides
152+
$override = Join-Path $base "overrides/$TemplateName.md"
153+
if (Test-Path $override) { return $override }
154+
155+
# Priority 2: Installed packs (by directory order)
156+
$packsDir = Join-Path $base 'packs'
157+
if (Test-Path $packsDir) {
158+
foreach ($pack in Get-ChildItem -Path $packsDir -Directory -ErrorAction SilentlyContinue) {
159+
$candidate = Join-Path $pack.FullName "templates/$TemplateName.md"
160+
if (Test-Path $candidate) { return $candidate }
161+
}
162+
}
163+
164+
# Priority 3: Extension-provided templates
165+
$extDir = Join-Path $RepoRoot '.specify/extensions'
166+
if (Test-Path $extDir) {
167+
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue) {
168+
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
169+
if (Test-Path $candidate) { return $candidate }
170+
}
171+
}
172+
173+
# Priority 4: Core templates
174+
$core = Join-Path $base "$TemplateName.md"
175+
if (Test-Path $core) { return $core }
176+
177+
return $null
178+
}
179+

scripts/powershell/create-new-feature.ps1

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ if (-not $fallbackRoot) {
141141
exit 1
142142
}
143143

144+
# Load common functions (includes Resolve-Template)
145+
. "$PSScriptRoot/common.ps1"
146+
144147
try {
145148
$repoRoot = git rev-parse --show-toplevel 2>$null
146149
if ($LASTEXITCODE -eq 0) {
@@ -276,9 +279,9 @@ if ($hasGit) {
276279
$featureDir = Join-Path $specsDir $branchName
277280
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
278281

279-
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
282+
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
280283
$specFile = Join-Path $featureDir 'spec.md'
281-
if (Test-Path $template) {
284+
if ($template -and (Test-Path $template)) {
282285
Copy-Item $template $specFile -Force
283286
} else {
284287
New-Item -ItemType File -Path $specFile | Out-Null

scripts/powershell/setup-plan.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GI
3232
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
3333

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

0 commit comments

Comments
 (0)