diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7cfad50
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,101 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Test
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ go-version: ['1.22', '1.23', '1.24', '1.25']
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ cache: true
+
+ - name: Download dependencies
+ run: go mod download
+
+ - name: Verify dependencies
+ run: go mod verify
+
+ - name: Build
+ run: go build -v ./...
+
+ - name: Run unit tests
+ run: go test -v -race ./internal/...
+
+ - name: Run acceptance tests
+ run: go test -v -race ./test/acceptance/...
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25'
+
+ - name: Run golangci-lint
+ uses: golangci/golangci-lint-action@v6
+ with:
+ version: latest
+ args: --timeout=5m
+
+ build:
+ name: Build Artifacts
+ runs-on: ubuntu-latest
+ needs: [test, lint]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25'
+
+ - name: Build for multiple platforms
+ run: |
+ mkdir -p dist
+
+ # Linux amd64
+ GOOS=linux GOARCH=amd64 go build -o dist/render-linux-amd64 ./cmd/render
+
+ # Linux arm64
+ GOOS=linux GOARCH=arm64 go build -o dist/render-linux-arm64 ./cmd/render
+
+ # macOS amd64
+ GOOS=darwin GOARCH=amd64 go build -o dist/render-darwin-amd64 ./cmd/render
+
+ # macOS arm64
+ GOOS=darwin GOARCH=arm64 go build -o dist/render-darwin-arm64 ./cmd/render
+
+ # Windows amd64
+ GOOS=windows GOARCH=amd64 go build -o dist/render-windows-amd64.exe ./cmd/render
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: binaries
+ path: dist/
+ retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..10ca381
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,99 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: Create Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25'
+
+ - name: Run tests
+ run: |
+ go test -v -race ./internal/...
+ go test -v -race ./test/acceptance/...
+
+ - name: Build release binaries
+ run: |
+ VERSION=${GITHUB_REF#refs/tags/}
+ mkdir -p dist
+
+ # Linux amd64
+ GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION}" \
+ -o dist/render-linux-amd64 ./cmd/render
+
+ # Linux arm64
+ GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${VERSION}" \
+ -o dist/render-linux-arm64 ./cmd/render
+
+ # macOS amd64
+ GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION}" \
+ -o dist/render-darwin-amd64 ./cmd/render
+
+ # macOS arm64
+ GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${VERSION}" \
+ -o dist/render-darwin-arm64 ./cmd/render
+
+ # Windows amd64
+ GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION}" \
+ -o dist/render-windows-amd64.exe ./cmd/render
+
+ - name: Create archives
+ run: |
+ cd dist
+
+ # Create tar.gz for Linux and macOS
+ for f in render-linux-* render-darwin-*; do
+ if [ -f "$f" ]; then
+ tar -czvf "${f}.tar.gz" "$f"
+ rm "$f"
+ fi
+ done
+
+ # Create zip for Windows
+ for f in render-windows-*.exe; do
+ if [ -f "$f" ]; then
+ zip "${f%.exe}.zip" "$f"
+ rm "$f"
+ fi
+ done
+
+ - name: Generate checksums
+ run: |
+ cd dist
+ sha256sum * > checksums.txt
+
+ - name: Create Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ VERSION=${GITHUB_REF#refs/tags/}
+
+ # Determine if prerelease
+ PRERELEASE_FLAG=""
+ if [[ "$VERSION" == *"-rc"* ]] || [[ "$VERSION" == *"-beta"* ]] || [[ "$VERSION" == *"-alpha"* ]]; then
+ PRERELEASE_FLAG="--prerelease"
+ fi
+
+ # Create release with auto-generated notes
+ gh release create "$VERSION" \
+ --generate-notes \
+ $PRERELEASE_FLAG \
+ dist/*.tar.gz \
+ dist/*.zip \
+ dist/checksums.txt
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f4d7a2c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,725 @@
+/render
+.idea/
+.vscode/
+__pycache__/
+.scratchpad/
+
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### Images template
+# JPEG
+*.jpg
+*.jpeg
+*.jpe
+*.jif
+*.jfif
+*.jfi
+
+# JPEG 2000
+*.jp2
+*.j2k
+*.jpf
+*.jpx
+*.jpm
+*.mj2
+
+# JPEG XR
+*.jxr
+*.hdp
+*.wdp
+
+# Graphics Interchange Format
+*.gif
+
+# RAW
+*.raw
+
+# Web P
+*.webp
+
+# Portable Network Graphics
+*.png
+
+# Animated Portable Network Graphics
+*.apng
+
+# Multiple-image Network Graphics
+*.mng
+
+# Tagged Image File Format
+*.tiff
+*.tif
+
+# Scalable Vector Graphics
+*.svg
+*.svgz
+
+# Portable Document Format
+*.pdf
+
+# X BitMap
+*.xbm
+
+# BMP
+*.bmp
+*.dib
+
+# ICO
+*.ico
+
+# 3D Images
+*.3dm
+*.max
+
+### Go template
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env
+
+### Text template
+*.doc
+*.docx
+*.log
+*.msg
+*.pages
+*.rtf
+*.txt
+*.wpd
+*.wps
+
+### Windows template
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Linux template
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### VisualStudio template
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+### Archives template
+# It's better to unpack these files and commit the raw source because
+# git has its own built in compression methods.
+*.7z
+*.jar
+*.rar
+*.zip
+*.gz
+*.gzip
+*.tgz
+*.bzip
+*.bzip2
+*.bz2
+*.xz
+*.lzma
+*.cab
+*.xar
+*.zst
+*.tzst
+
+# Packing-only formats
+*.iso
+*.tar
+
+# Package management formats
+*.dmg
+*.xpi
+*.gem
+*.egg
+*.deb
+*.rpm
+*.msi
+*.msm
+*.msp
+*.txz
+
+### macOS template
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..027dc97
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,3 @@
+version: "2"
+run:
+ timeout: 5m
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..0872564
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+# Exclude generated output directories from all hooks
+exclude: '(^|/)output/'
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-json
+ - id: check-added-large-files
+ - id: mixed-line-ending
+ args: ['--fix=lf']
+
+ - repo: local
+ hooks:
+ - id: golangci-lint
+ name: golangci-lint
+ entry: golangci-lint run --timeout=5m
+ language: system
+ types: [go]
+ pass_filenames: false
+
+ - repo: local
+ hooks:
+ - id: go-build
+ name: go build
+ entry: bash -c 'go build $(go list ./... | grep -v -E "(output|test/acceptance)")'
+ language: system
+ types: [go]
+ pass_filenames: false
+
+ - id: go-test
+ name: go test
+ entry: bash -c 'go test $(go list ./... | grep -v /output/)'
+ language: system
+ types: [go]
+ pass_filenames: false
diff --git a/.whitesource b/.whitesource
new file mode 100644
index 0000000..9c7ae90
--- /dev/null
+++ b/.whitesource
@@ -0,0 +1,14 @@
+{
+ "scanSettings": {
+ "baseBranches": []
+ },
+ "checkRunSettings": {
+ "vulnerableCheckRunConclusionLevel": "failure",
+ "displayMode": "diff",
+ "useMendCheckNames": true
+ },
+ "issueSettings": {
+ "minSeverityLevel": "LOW",
+ "issueType": "DEPENDENCY"
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index bbb580b..e53aae0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,281 @@
# render
-A jig that renders content using a template and data. Useful for an AI assistant that generates the same code
+
+A CLI tool that uses Go text templates to generate output files from JSON or YAML data sources. Useful for AI agents and humans generating repetitive code, configuration files, and documentation.
+
+## Installation
+
+```bash
+go install github.com/wernerstrydom/render/cmd/render@latest
+```
+
+Or build from source:
+
+```bash
+git clone https://github.com/wernerstrydom/render.git
+cd render
+go build -o bin/render ./cmd/render
+```
+
+## Usage
+
+render supports three modes of operation:
+
+### File Mode
+
+Render a single template file:
+
+```bash
+render file -t template.txt -d data.json -o output.txt
+render file --template config.yaml.tmpl --data values.json --output config.yaml
+```
+
+### Directory Mode
+
+Render a directory of templates. Files with `.tmpl` extension are rendered (extension stripped), other files are copied verbatim:
+
+```bash
+render dir -t ./templates -d data.json -o ./output
+render dir --template ./src --data config.yaml --output ./dist
+```
+
+### Each Mode
+
+Render templates for each element in an array selected via jq query. The output path is itself a Go template:
+
+```bash
+render each -t user.tmpl -d users.json -q '.users[]' -o '{{.username}}.txt'
+render each -t ./user-templates -d data.yaml -q '.items[]' -o './output/{{.id}}'
+```
+
+## Flags
+
+| Flag | Short | Description |
+|------|-------|-------------|
+| `--template` | `-t` | Path to template file or directory |
+| `--data` | `-d` | Path to JSON or YAML data file |
+| `--output` | `-o` | Output file or directory path |
+| `--query` | `-q` | jq query to select elements (each mode only) |
+| `--force` | `-f` | Overwrite existing output files |
+| `--dry-run` | | Show what would be written without writing (dir mode only) |
+
+## Configuration File
+
+When using `dir` or `each` mode with a template directory, you can create a `.render.yaml` file (or `.render.yml`, `render.json`) to control how output paths are transformed. This is useful for renaming files or directories based on template data.
+
+### Path Mappings
+
+The `paths` key maps source paths to output path templates:
+
+```yaml
+# .render.yaml
+paths:
+ # Rename a specific file using template data
+ "model.go.tmpl": "{{ .name | snakeCase }}.go"
+
+ # Rename a directory prefix
+ "src": "{{ .package }}"
+```
+
+Path templates have access to:
+- All data from your JSON/YAML data file
+- All template functions (snakeCase, pascalCase, etc.)
+
+### Examples
+
+**Rename a file based on data:**
+
+```yaml
+# Template: user.go.tmpl
+# Data: {"name": "UserProfile"}
+# .render.yaml
+paths:
+ "user.go.tmpl": "{{ .name | snakeCase }}.go"
+# Output: user_profile.go
+```
+
+**Rename a directory:**
+
+```yaml
+# Template directory contains: src/main.go.tmpl
+# Data: {"package": "myapp"}
+# .render.yaml
+paths:
+ "src": "{{ .package }}"
+# Output: myapp/main.go
+```
+
+**Multiple mappings:**
+
+```yaml
+paths:
+ "model.go.tmpl": "{{ .name | snakeCase }}.go"
+ "templates": "{{ .outputDir }}"
+```
+
+The config file itself is never copied to the output directory.
+
+## Template Functions
+
+In addition to Go's standard template functions, render provides:
+
+### Casing Functions
+
+| Function | Description | Example |
+|----------|-------------|---------|
+| `lower` | Lowercase | `{{ lower "Hello" }}` → `hello` |
+| `upper` | Uppercase | `{{ upper "Hello" }}` → `HELLO` |
+| `title` | Title Case | `{{ title "hello world" }}` → `Hello World` |
+| `camelCase` | camelCase | `{{ camelCase "hello world" }}` → `helloWorld` |
+| `pascalCase` | PascalCase | `{{ pascalCase "hello world" }}` → `HelloWorld` |
+| `snakeCase` | snake_case | `{{ snakeCase "HelloWorld" }}` → `hello_world` |
+| `kebabCase` | kebab-case | `{{ kebabCase "HelloWorld" }}` → `hello-world` |
+| `upperSnakeCase` | UPPER_SNAKE | `{{ upperSnakeCase "hello" }}` → `HELLO` |
+| `upperKebabCase` | UPPER-KEBAB | `{{ upperKebabCase "hello" }}` → `HELLO` |
+
+### String Functions
+
+| Function | Description |
+|----------|-------------|
+| `trim` | Remove leading/trailing whitespace |
+| `trimPrefix` | Remove prefix |
+| `trimSuffix` | Remove suffix |
+| `replace` | Replace all occurrences |
+| `contains` | Check if string contains substring |
+| `hasPrefix` | Check prefix |
+| `hasSuffix` | Check suffix |
+| `repeat` | Repeat string N times |
+| `reverse` | Reverse string |
+| `substr` | Substring (start, end) |
+| `truncate` | Truncate to length |
+| `padLeft` | Pad left to length |
+| `padRight` | Pad right to length |
+| `indent` | Indent with spaces |
+| `nindent` | Newline + indent |
+
+### Splitting and Joining
+
+| Function | Description |
+|----------|-------------|
+| `split` | Split string by separator |
+| `join` | Join array with separator |
+| `lines` | Split into lines |
+| `first` | First element |
+| `last` | Last element |
+| `rest` | All but first |
+| `initial` | All but last |
+| `nth` | Nth element |
+
+### Conversion Functions
+
+| Function | Description |
+|----------|-------------|
+| `toString` | Convert to string |
+| `toInt` | Convert to int |
+| `toFloat` | Convert to float |
+| `toBool` | Convert to bool |
+| `toJson` | Convert to JSON string |
+| `toPrettyJson` | Convert to pretty JSON |
+| `fromJson` | Parse JSON string |
+
+### Unicode Functions
+
+| Function | Description |
+|----------|-------------|
+| `nfc` | NFC normalization |
+| `nfd` | NFD normalization |
+| `nfkc` | NFKC normalization |
+| `nfkd` | NFKD normalization |
+| `ascii` | Convert to ASCII |
+| `slug` | URL-safe slug |
+
+### Logic and Comparison
+
+| Function | Description |
+|----------|-------------|
+| `eq`, `ne` | Equal, not equal |
+| `lt`, `le` | Less than, less or equal |
+| `gt`, `ge` | Greater than, greater or equal |
+| `and`, `or`, `not` | Boolean operations |
+| `default` | Default value if empty |
+| `empty` | Check if empty |
+| `coalesce` | First non-empty value |
+| `ternary` | Conditional value |
+
+### Collection Functions
+
+| Function | Description |
+|----------|-------------|
+| `list` | Create a list |
+| `dict` | Create a dictionary |
+| `keys` | Get map keys |
+| `values` | Get map values |
+| `hasKey` | Check if key exists |
+| `get` | Get value by key |
+| `merge` | Merge maps |
+| `append` | Append to list |
+| `uniq` | Remove duplicates |
+| `sortAlpha` | Sort alphabetically |
+| `len` | Length of collection |
+
+### Math Functions
+
+| Function | Description |
+|----------|-------------|
+| `add`, `sub` | Addition, subtraction |
+| `mul`, `div` | Multiplication, division |
+| `mod` | Modulo |
+| `max`, `min` | Maximum, minimum |
+| `floor`, `ceil`, `round` | Rounding |
+
+### Regex Functions
+
+| Function | Description |
+|----------|-------------|
+| `regexMatch` | Check if matches |
+| `regexFind` | Find first match |
+| `regexFindAll` | Find all matches |
+| `regexReplace` | Replace matches |
+| `regexSplit` | Split by pattern |
+
+## Examples
+
+### Generate Configuration Files
+
+```bash
+# values.yaml
+database:
+ host: localhost
+ port: 5432
+ name: myapp
+
+# config.yaml.tmpl
+database:
+ connection_string: "postgresql://{{ .database.host }}:{{ .database.port }}/{{ .database.name }}"
+
+# Generate
+render file -t config.yaml.tmpl -d values.yaml -o config.yaml
+```
+
+### Generate Multiple Files from Array
+
+```bash
+# users.json
+{
+ "users": [
+ {"name": "Alice", "role": "admin"},
+ {"name": "Bob", "role": "user"}
+ ]
+}
+
+# user.tmpl
+Name: {{ .name }}
+Role: {{ .role }}
+Username: {{ snakeCase .name }}
+
+# Generate one file per user
+render each -t user.tmpl -d users.json -q '.users[]' -o '{{ snakeCase .name }}.txt'
+```
+
+## License
+
+MIT License - see LICENSE file for details.
diff --git a/_examples/.gitignore b/_examples/.gitignore
new file mode 100644
index 0000000..ea1472e
--- /dev/null
+++ b/_examples/.gitignore
@@ -0,0 +1 @@
+output/
diff --git a/_examples/README.md b/_examples/README.md
new file mode 100644
index 0000000..72ed351
--- /dev/null
+++ b/_examples/README.md
@@ -0,0 +1,136 @@
+# Render Examples
+
+This directory contains examples demonstrating how to use the `render` CLI tool
+for various code generation scenarios. Each example shows a practical use case
+where render provides significant advantages over manual file creation.
+
+## Why Use Render?
+
+When an AI assistant needs to generate repetitive code structures, using `render`
+offers several advantages over writing files individually:
+
+1. **Speed**: Generate dozens of files in a single command
+2. **Consistency**: Templates enforce uniform patterns across files
+3. **Maintainability**: Update the template once, regenerate everything
+4. **Reduced Errors**: Less opportunity for copy-paste mistakes
+5. **Context Efficiency**: Smaller token usage than writing each file
+
+## Examples
+
+| Example | Description | Key Features |
+|---------|-------------|--------------|
+| [go-workspace-grpc](./go-workspace-grpc/) | Multi-module Go workspace with gRPC | Each mode, path transformation |
+| [maven-multimodule-grpc](./maven-multimodule-grpc/) | Maven multi-module Java project | Directory mode, package paths |
+| [go-driver](./go-driver/) | Driver interface pattern | Two-pass, interface + implementations |
+| [go-workspace-makefile](./go-workspace-makefile/) | Makefile for Go workspaces | File mode, loops, conditionals |
+| [go-cli-scaffold](./go-cli-scaffold/) | CLI with Cobra/Viper | Command structure, flags |
+| [python-fastapi-service](./python-fastapi-service/) | Python FastAPI Service | Two-pass rendering, Models/Routers |
+| [docker-compose](./docker-compose/) | Docker Compose stacks | Complex config from simple data |
+| [kubernetes-manifests](./kubernetes-manifests/) | K8s manifests per environment | Nested loops, environment config |
+| [github-actions](./github-actions/) | CI/CD workflow generation | Conditionals, matrix builds |
+| [terraform-modules](./terraform-modules/) | Terraform infrastructure | Modules, environments |
+
+## Quick Start
+
+```bash
+# Navigate to any example directory
+cd examples/go-cli-scaffold
+
+# Preview what would be generated
+render templates cli.yaml -o output --dry-run
+
+# Generate the files
+render templates cli.yaml -o output
+
+# Explore the generated structure
+tree output/
+```
+
+## Template Syntax
+
+Render uses Go's `text/template` syntax with additional functions:
+
+```go
+// Variable interpolation
+{{ .name }}
+
+// Nested access
+{{ .config.database.host }}
+
+// Pipes and functions
+{{ .name | pascalCase }}
+{{ .items | join ", " }}
+
+// Conditionals
+{{ if .enabled }}...{{ end }}
+{{ if eq .type "service" }}...{{ end }}
+
+// Loops
+{{ range .services }}
+ {{ .name }}
+{{ end }}
+
+// With context
+{{ with .database }}
+ Host: {{ .host }}
+{{ end }}
+```
+
+## Available Functions
+
+Render includes 100+ template functions:
+
+- **Casing**: `camelCase`, `pascalCase`, `snakeCase`, `kebabCase`
+- **String**: `lower`, `upper`, `title`, `trim`, `replace`
+- **Collections**: `first`, `last`, `join`, `split`, `len`
+- **Math**: `add`, `sub`, `mul`, `div`
+- **Logic**: `eq`, `ne`, `lt`, `gt`, `and`, `or`, `not`
+- **JSON**: `toJson`, `toPrettyJson`, `fromJson`
+
+See the [main documentation](../README.md) for the complete function reference.
+
+## Modes of Operation
+
+### File Mode
+Single template to single output file.
+```bash
+render template.tmpl data.yaml -o output.txt
+```
+
+### Directory Mode
+Template directory to output directory, preserving structure.
+```bash
+render templates/ data.yaml -o output/
+```
+
+### Each Mode
+Template rendered for each array element with dynamic paths.
+```bash
+render template.tmpl items.yaml -o "{{.name}}/file.txt"
+```
+
+## Path Transformation
+
+Use `.render.yaml` in your template directory to transform output paths:
+
+```yaml
+paths:
+ # Rename files dynamically
+ "model.go.tmpl": "{{ .name | snakeCase }}.go"
+
+ # Transform directory prefixes
+ "src/main/java": "src/main/java/{{ .package | replace \".\" \"/\" }}"
+```
+
+## When to Use Render vs. Write Tool
+
+**Use Render when:**
+- Generating multiple similar files
+- Creating environment-specific configurations
+- Scaffolding project structures
+- Maintaining templates for reuse
+
+**Use Write Tool when:**
+- Creating a single unique file
+- Making targeted edits to existing files
+- Content doesn't follow a repeatable pattern
diff --git a/_examples/docker-compose/README.md b/_examples/docker-compose/README.md
new file mode 100644
index 0000000..a5d09ed
--- /dev/null
+++ b/_examples/docker-compose/README.md
@@ -0,0 +1,20 @@
+# Docker Compose Example
+
+This example demonstrates how to generate a `docker-compose.yaml` file from a simplified stack definition.
+
+## Why Use Render?
+
+Hand-writing `docker-compose.yaml` for large stacks is tedious and error-prone, especially when managing environment variables and volumes across multiple services. `render` allows you to:
+- **Simplify Configuration**: Define your stack in a clean YAML format.
+- **Enforce Best Practices**: Your template can include standard logging, restart policies, or networks that apply to all services.
+
+## Usage
+
+```bash
+./render.sh
+```
+
+## Structure
+
+- `stack.yaml`: The high-level definition of your services, networks, and volumes.
+- `templates/docker-compose.yaml.tmpl`: The template that transforms the stack definition into a valid Docker Compose file.
diff --git a/_examples/docker-compose/render.sh b/_examples/docker-compose/render.sh
new file mode 100755
index 0000000..85cf5f0
--- /dev/null
+++ b/_examples/docker-compose/render.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# Ensure we are in the example directory
+cd "$(dirname "$0")"
+
+# Path to the render binary
+RENDER="../../bin/render"
+
+# Build render if it doesn't exist
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd ../.. && go build -o bin/render ./cmd/render/main.go)
+fi
+
+echo "Generating docker-compose.yaml..."
+$RENDER templates stack.yaml -o output --force
+
+echo "Generation complete! Check the 'output' directory."
diff --git a/_examples/docker-compose/stack.yaml b/_examples/docker-compose/stack.yaml
new file mode 100644
index 0000000..2f5ddc2
--- /dev/null
+++ b/_examples/docker-compose/stack.yaml
@@ -0,0 +1,25 @@
+stack_name: "my-microservices"
+services:
+ - name: "web"
+ image: "nginx:latest"
+ ports:
+ - "80:80"
+ env:
+ NODE_ENV: "production"
+ depends_on:
+ - "redis"
+ - name: "worker"
+ image: "python:3.9-slim"
+ command: "python worker.py"
+ env:
+ REDIS_HOST: "redis"
+ - name: "redis"
+ image: "redis:alpine"
+ volumes:
+ - "redis-data:/data"
+
+networks:
+ - "app-network"
+
+volumes:
+ - "redis-data"
diff --git a/_examples/docker-compose/templates/docker-compose.yaml.tmpl b/_examples/docker-compose/templates/docker-compose.yaml.tmpl
new file mode 100644
index 0000000..718a6f2
--- /dev/null
+++ b/_examples/docker-compose/templates/docker-compose.yaml.tmpl
@@ -0,0 +1,45 @@
+version: "3.8"
+
+services:
+{{- range .services }}
+ {{ .name }}:
+ image: {{ .image }}
+ {{- if .ports }}
+ ports:
+ {{- range .ports }}
+ - "{{ . }}"
+ {{- end }}
+ {{- end }}
+ {{- if .command }}
+ command: {{ .command }}
+ {{- end }}
+ {{- if .env }}
+ environment:
+ {{- range $key, $value := .env }}
+ {{ $key }}: "{{ $value }}"
+ {{- end }}
+ {{- end }}
+ {{- if .volumes }}
+ volumes:
+ {{- range .volumes }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+ {{- if .depends_on }}
+ depends_on:
+ {{- range .depends_on }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+networks:
+{{- range .networks }}
+ {{ . }}:
+ driver: bridge
+{{- end }}
+
+volumes:
+{{- range .volumes }}
+ {{ . }}:
+{{- end }}
diff --git a/_examples/github-actions/README.md b/_examples/github-actions/README.md
new file mode 100644
index 0000000..f205901
--- /dev/null
+++ b/_examples/github-actions/README.md
@@ -0,0 +1,53 @@
+# GitHub Actions Workflow Generator
+
+This example demonstrates generating GitHub Actions workflows for different
+project types. Define your project structure and generate appropriate CI/CD
+workflows with best practices built in.
+
+## Why Use Render?
+
+Setting up GitHub Actions involves:
+- Understanding best practices for each language/framework
+- Configuring caching, artifacts, and deployments correctly
+- Maintaining consistency across multiple repositories
+
+With `render`, generate proven workflow patterns instantly.
+
+## Workflow Types
+
+- **go**: Go project with testing, linting, and releases
+- **node**: Node.js project with npm/yarn support
+- **python**: Python project with pip, pytest, and typing
+- **docker**: Multi-platform Docker builds with caching
+
+## Usage
+
+```bash
+# Generate workflows for a Go project
+render templates project.yaml -o .github/workflows
+
+# Generate for multiple project types
+render templates/go.yaml.tmpl config.yaml -o .github/workflows/go.yaml
+render templates/docker.yaml.tmpl config.yaml -o .github/workflows/docker.yaml
+```
+
+## Template Features Demonstrated
+
+- **File Mode**: Generate specific workflows
+- **Conditionals**: Include/exclude steps based on config
+- **Environment Variables**: Secure secrets handling
+- **Matrix Builds**: Test across versions
+
+## Workflow Features
+
+- **Security**: Minimal permissions, dependency scanning
+- **Caching**: Smart caching for faster builds
+- **Releases**: Automatic versioning and changelog
+- **Docker**: Multi-platform with layer caching
+- **Notifications**: Slack/email on failure
+
+## Real-World Use Case
+
+An AI assistant asked to "set up CI/CD for our new Go service with Docker
+deployment" can generate production-ready workflows with proper caching,
+security scanning, and multi-platform Docker builds.
diff --git a/_examples/github-actions/project.yaml b/_examples/github-actions/project.yaml
new file mode 100644
index 0000000..ae6efe3
--- /dev/null
+++ b/_examples/github-actions/project.yaml
@@ -0,0 +1,49 @@
+name: myservice
+language: go
+goVersion: "1.22"
+nodeVersion: "20"
+
+# Repository settings
+defaultBranch: main
+protectedBranches:
+ - main
+ - release/*
+
+# Build settings
+buildCommand: make build
+testCommand: make test
+lintCommand: make lint
+
+# Docker settings
+docker:
+ enabled: true
+ registry: ghcr.io
+ imageName: myorg/myservice
+ platforms:
+ - linux/amd64
+ - linux/arm64
+ dockerfile: Dockerfile
+
+# Feature flags
+features:
+ caching: true
+ dependabot: true
+ codeql: true
+ releaseAutomation: true
+
+# Deployment
+deploy:
+ enabled: true
+ environments:
+ - name: staging
+ url: https://staging.example.com
+ requiresApproval: false
+ - name: production
+ url: https://example.com
+ requiresApproval: true
+
+# Notifications
+notifications:
+ slack:
+ enabled: true
+ channel: "#deployments"
diff --git a/_examples/github-actions/render.cmd b/_examples/github-actions/render.cmd
new file mode 100644
index 0000000..b1eb1d4
--- /dev/null
+++ b/_examples/github-actions/render.cmd
@@ -0,0 +1,27 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo Generating GitHub Actions workflows...
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%project.yaml" ^
+ -o "%OUTPUT_DIR%\.github\workflows"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul
diff --git a/_examples/github-actions/render.sh b/_examples/github-actions/render.sh
new file mode 100755
index 0000000..5f13cbc
--- /dev/null
+++ b/_examples/github-actions/render.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "Generating GitHub Actions workflows..."
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/project.yaml" \
+ -o "${OUTPUT_DIR}/.github/workflows"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+find "$OUTPUT_DIR" -type f
diff --git a/_examples/github-actions/templates/ci.yaml.tmpl b/_examples/github-actions/templates/ci.yaml.tmpl
new file mode 100644
index 0000000..1e42c93
--- /dev/null
+++ b/_examples/github-actions/templates/ci.yaml.tmpl
@@ -0,0 +1,156 @@
+name: CI
+
+on:
+ push:
+ branches: [{{ .defaultBranch }}]
+ pull_request:
+ branches: [{{ .defaultBranch }}]
+
+permissions:
+ contents: read
+ pull-requests: write
+{{ if .features.codeql }} security-events: write{{ end }}
+
+concurrency:
+ group: ${{ "{{" }} github.workflow {{ "}}" }}-${{ "{{" }} github.ref {{ "}}" }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+{{ if eq .language "go" }}
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "{{ .goVersion }}"
+{{ if .features.caching }} cache: true{{ end }}
+
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v4
+ with:
+ version: latest
+{{ else if eq .language "node" }}
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "{{ .nodeVersion }}"
+{{ if .features.caching }} cache: npm{{ end }}
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+{{ end }}
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+{{ if eq .language "go" }}
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "{{ .goVersion }}"
+{{ if .features.caching }} cache: true{{ end }}
+
+ - name: Run tests
+ run: {{ .testCommand }}
+
+ - name: Upload coverage
+ uses: codecov/codecov-action@v4
+ with:
+ files: coverage.out
+{{ else if eq .language "node" }}
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "{{ .nodeVersion }}"
+{{ if .features.caching }} cache: npm{{ end }}
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test -- --coverage
+{{ end }}
+
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ needs: [lint, test]
+ steps:
+ - uses: actions/checkout@v4
+
+{{ if eq .language "go" }}
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "{{ .goVersion }}"
+{{ if .features.caching }} cache: true{{ end }}
+
+ - name: Build
+ run: {{ .buildCommand }}
+{{ end }}
+{{ if .docker.enabled }}
+ docker:
+ name: Build Docker Image
+ runs-on: ubuntu-latest
+ needs: [lint, test]
+ if: github.event_name == 'push'
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: {{ .docker.registry }}
+ username: ${{ "{{" }} github.actor {{ "}}" }}
+ password: ${{ "{{" }} secrets.GITHUB_TOKEN {{ "}}" }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: {{ .docker.dockerfile }}
+ platforms: {{ .docker.platforms | join "," }}
+ push: true
+ tags: |
+ {{ .docker.registry }}/{{ .docker.imageName }}:${{ "{{" }} github.sha {{ "}}" }}
+ {{ .docker.registry }}/{{ .docker.imageName }}:latest
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+{{ end }}
+{{ if .features.codeql }}
+ security:
+ name: Security Scan
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: {{ .language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+{{ end }}
diff --git a/_examples/github-actions/templates/release.yaml.tmpl b/_examples/github-actions/templates/release.yaml.tmpl
new file mode 100644
index 0000000..f166b39
--- /dev/null
+++ b/_examples/github-actions/templates/release.yaml.tmpl
@@ -0,0 +1,77 @@
+{{ if .features.releaseAutomation -}}
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+ packages: write
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+{{ if eq .language "go" }}
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "{{ .goVersion }}"
+ cache: true
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v5
+ with:
+ version: latest
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ "{{" }} secrets.GITHUB_TOKEN {{ "}}" }}
+{{ end }}
+{{ if .docker.enabled }}
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: {{ .docker.registry }}
+ username: ${{ "{{" }} github.actor {{ "}}" }}
+ password: ${{ "{{" }} secrets.GITHUB_TOKEN {{ "}}" }}
+
+ - name: Extract version
+ id: version
+ run: echo "version=${{ "{{" }} github.ref_name {{ "}}" }}" >> $GITHUB_OUTPUT
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: {{ .docker.dockerfile }}
+ platforms: {{ .docker.platforms | join "," }}
+ push: true
+ tags: |
+ {{ .docker.registry }}/{{ .docker.imageName }}:${{ "{{" }} steps.version.outputs.version {{ "}}" }}
+ {{ .docker.registry }}/{{ .docker.imageName }}:latest
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+{{ end }}
+{{ if .notifications.slack.enabled }}
+ - name: Notify Slack
+ uses: slackapi/slack-github-action@v1
+ with:
+ channel-id: "{{ .notifications.slack.channel }}"
+ slack-message: "🚀 Released {{ .name }} ${{ "{{" }} github.ref_name {{ "}}" }}"
+ env:
+ SLACK_BOT_TOKEN: ${{ "{{" }} secrets.SLACK_BOT_TOKEN {{ "}}" }}
+{{ end }}
+{{- end }}
diff --git a/_examples/go-cli-scaffold/README.md b/_examples/go-cli-scaffold/README.md
new file mode 100644
index 0000000..5dba93a
--- /dev/null
+++ b/_examples/go-cli-scaffold/README.md
@@ -0,0 +1,67 @@
+# Go CLI Application Scaffold
+
+This example demonstrates generating a complete CLI application scaffold using
+Cobra and Viper. Define your commands, flags, and structure in YAML and generate
+a production-ready CLI.
+
+## Why Use Render?
+
+Building a CLI manually involves:
+- Creating the root command with proper initialization
+- Adding subcommands with consistent patterns
+- Setting up configuration with environment variable support
+- Wiring flags to configuration values
+
+With `render`, define the CLI structure declaratively and generate it all at once.
+
+## Structure Generated
+
+```
+{app}/
+├── main.go
+├── cmd/
+│ ├── root.go # Root command with config setup
+│ ├── version.go # Version command
+│ └── {command}/
+│ └── {command}.go # Subcommand implementation
+├── internal/
+│ └── config/
+│ └── config.go # Configuration struct
+└── .{app}.yaml # Example config file
+```
+
+## Usage
+
+This example demonstrates a **two-pass** approach:
+
+```bash
+# Pass 1: Generate base structure (directory mode)
+render templates cli.yaml -o output
+
+# Pass 2: Generate one file per command (each mode)
+render command-template/command.go.tmpl commands.yaml -o "output/cmd/{{.name}}.go"
+
+# Or run both with the script
+./render.sh
+```
+
+## Template Features Demonstrated
+
+- **Two-Pass Rendering**: Base structure + per-item files
+- **Directory Mode**: Generates main.go, go.mod, cmd/root.go, cmd/version.go
+- **Each Mode**: Generates cmd/get.go, cmd/create.go, cmd/delete.go, cmd/describe.go
+- **Separate Data Files**: cli.yaml for base, commands.yaml for commands array
+
+## CLI Features Generated
+
+- Cobra command structure with help and completion
+- Viper configuration with file, env, and flag binding
+- Persistent and local flags per command
+- Version command with build info
+- Config file support with sensible defaults
+
+## Real-World Use Case
+
+An AI assistant asked to "create a CLI for managing Kubernetes resources with
+get, create, delete, and describe commands" can generate a complete, consistent
+CLI scaffold in seconds.
diff --git a/_examples/go-cli-scaffold/cli.yaml b/_examples/go-cli-scaffold/cli.yaml
new file mode 100644
index 0000000..5d30a47
--- /dev/null
+++ b/_examples/go-cli-scaffold/cli.yaml
@@ -0,0 +1,86 @@
+name: myctl
+module: github.com/example/myctl
+description: A powerful CLI for managing resources
+version: 0.1.0
+
+# Global configuration options
+config:
+ - name: verbose
+ type: bool
+ default: false
+ env: MYCTL_VERBOSE
+ description: Enable verbose output
+
+ - name: config
+ type: string
+ default: ""
+ env: MYCTL_CONFIG
+ description: Path to config file
+
+ - name: output
+ type: string
+ default: text
+ env: MYCTL_OUTPUT
+ description: Output format (text, json, yaml)
+
+# Commands
+commands:
+ - name: get
+ short: Get resources
+ long: Retrieve and display one or more resources from the server
+ flags:
+ - name: all
+ short: a
+ type: bool
+ default: false
+ description: Show all resources including hidden ones
+ - name: namespace
+ short: n
+ type: string
+ default: default
+ description: Namespace to query
+ subcommands:
+ - name: pods
+ short: List all pods
+ - name: services
+ short: List all services
+ - name: deployments
+ short: List all deployments
+
+ - name: create
+ short: Create a resource
+ long: Create a new resource from a file or inline specification
+ flags:
+ - name: file
+ short: f
+ type: string
+ required: true
+ description: Path to resource definition file
+ - name: dry-run
+ type: bool
+ default: false
+ description: Preview without creating
+
+ - name: delete
+ short: Delete resources
+ long: Delete one or more resources by name or selector
+ flags:
+ - name: force
+ type: bool
+ default: false
+ description: Force deletion without confirmation
+ - name: selector
+ short: l
+ type: string
+ description: Label selector to filter resources
+
+ - name: describe
+ short: Describe a resource
+ long: Show detailed information about a specific resource
+ args:
+ - name: resource
+ required: true
+ description: Resource type (pod, service, deployment)
+ - name: name
+ required: true
+ description: Resource name
diff --git a/_examples/go-cli-scaffold/command-template/command.go.tmpl b/_examples/go-cli-scaffold/command-template/command.go.tmpl
new file mode 100644
index 0000000..61410fa
--- /dev/null
+++ b/_examples/go-cli-scaffold/command-template/command.go.tmpl
@@ -0,0 +1,50 @@
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var {{ .name }}Cmd = &cobra.Command{
+ Use: "{{ .name }}{{ range .args }} <{{ .name }}>{{ end }}",
+ Short: "{{ .short }}",
+ Long: `{{ .long }}`,
+{{- if .args }}
+ Args: cobra.ExactArgs({{ len .args }}),
+{{- end }}
+ RunE: func(cmd *cobra.Command, args []string) error {
+{{- range $i, $arg := .args }}
+ {{ $arg.name }} := args[{{ $i }}]
+ _ = {{ $arg.name }} // TODO: use {{ $arg.name }}
+{{- end }}
+
+ // TODO: Implement {{ .name }} command
+ fmt.Println("{{ .name }} called")
+ return nil
+ },
+}
+
+func init() {
+{{- range .flags }}
+{{- if .short }}
+ {{ $.name }}Cmd.Flags().{{ .type | title }}P("{{ .name }}", "{{ .short }}", {{ if eq .type "bool" }}{{ .default }}{{ else if eq .type "string" }}"{{ .default }}"{{ else }}{{ .default }}{{ end }}, "{{ .description }}")
+{{- else }}
+ {{ $.name }}Cmd.Flags().{{ .type | title }}("{{ .name }}", {{ if eq .type "bool" }}{{ .default }}{{ else if eq .type "string" }}"{{ .default }}"{{ else }}{{ .default }}{{ end }}, "{{ .description }}")
+{{- end }}
+{{- if .required }}
+ {{ $.name }}Cmd.MarkFlagRequired("{{ .name }}")
+{{- end }}
+{{- end }}
+{{ range .subcommands }}
+ {{ $.name }}Cmd.AddCommand(&cobra.Command{
+ Use: "{{ .name }}",
+ Short: "{{ .short }}",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: Implement {{ $.name }} {{ .name }}
+ fmt.Println("{{ $.name }} {{ .name }} called")
+ return nil
+ },
+ })
+{{- end }}
+}
diff --git a/_examples/go-cli-scaffold/commands.yaml b/_examples/go-cli-scaffold/commands.yaml
new file mode 100644
index 0000000..0e45eec
--- /dev/null
+++ b/_examples/go-cli-scaffold/commands.yaml
@@ -0,0 +1,59 @@
+- name: get
+ short: Get resources
+ long: Retrieve and display one or more resources from the server
+ flags:
+ - name: all
+ short: a
+ type: bool
+ default: false
+ description: Show all resources including hidden ones
+ - name: namespace
+ short: n
+ type: string
+ default: default
+ description: Namespace to query
+ subcommands:
+ - name: pods
+ short: List all pods
+ - name: services
+ short: List all services
+ - name: deployments
+ short: List all deployments
+
+- name: create
+ short: Create a resource
+ long: Create a new resource from a file or inline specification
+ flags:
+ - name: file
+ short: f
+ type: string
+ required: true
+ description: Path to resource definition file
+ - name: dry-run
+ type: bool
+ default: false
+ description: Preview without creating
+
+- name: delete
+ short: Delete resources
+ long: Delete one or more resources by name or selector
+ flags:
+ - name: force
+ type: bool
+ default: false
+ description: Force deletion without confirmation
+ - name: selector
+ short: l
+ type: string
+ description: Label selector to filter resources
+
+- name: describe
+ short: Describe a resource
+ long: Show detailed information about a specific resource
+ args:
+ - name: resource
+ required: true
+ description: Resource type (pod, service, deployment)
+ - name: name
+ required: true
+ description: Resource name
diff --git a/_examples/go-cli-scaffold/render.cmd b/_examples/go-cli-scaffold/render.cmd
new file mode 100644
index 0000000..54e8a43
--- /dev/null
+++ b/_examples/go-cli-scaffold/render.cmd
@@ -0,0 +1,32 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo === Pass 1: Generating base CLI structure ===
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%cli.yaml" ^
+ -o "%OUTPUT_DIR%"
+
+echo.
+echo === Pass 2: Generating command files ===
+"%RENDER%" "%SCRIPT_DIR%command-template\command.go.tmpl" "%SCRIPT_DIR%commands.yaml" ^
+ -o "%OUTPUT_DIR%\cmd\{{.name}}.go"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul
diff --git a/_examples/go-cli-scaffold/render.sh b/_examples/go-cli-scaffold/render.sh
new file mode 100755
index 0000000..52676a4
--- /dev/null
+++ b/_examples/go-cli-scaffold/render.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "=== Pass 1: Generating base CLI structure ==="
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/cli.yaml" \
+ -o "${OUTPUT_DIR}"
+
+echo ""
+echo "=== Pass 2: Generating command files ==="
+# Extract commands array and render each command
+# The commands array needs to be at the root for each mode
+"$RENDER" "${SCRIPT_DIR}/command-template/command.go.tmpl" "${SCRIPT_DIR}/commands.yaml" \
+ -o "${OUTPUT_DIR}/cmd/{{.name}}.go"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+find "$OUTPUT_DIR" -type f | sort
diff --git a/_examples/go-cli-scaffold/templates/cmd/root.go.tmpl b/_examples/go-cli-scaffold/templates/cmd/root.go.tmpl
new file mode 100644
index 0000000..c323ced
--- /dev/null
+++ b/_examples/go-cli-scaffold/templates/cmd/root.go.tmpl
@@ -0,0 +1,75 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var cfgFile string
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+ Use: "{{ .name }}",
+ Short: "{{ .description }}",
+ Long: `{{ .description }}
+
+Complete documentation is available at https://github.com/example/{{ .name }}`,
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return initConfig()
+ },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+func Execute() error {
+ return rootCmd.Execute()
+}
+
+func init() {
+ // Global flags
+ rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .name }}.yaml)")
+{{- range .config }}
+{{- if ne .name "config" }}
+ rootCmd.PersistentFlags().{{ .type | title }}("{{ .name }}", {{ if eq .type "bool" }}{{ .default }}{{ else if eq .type "string" }}"{{ .default }}"{{ else }}{{ .default }}{{ end }}, "{{ .description }}")
+ viper.BindPFlag("{{ .name }}", rootCmd.PersistentFlags().Lookup("{{ .name }}"))
+{{- end }}
+{{- end }}
+
+ // Add subcommands
+{{- range .commands }}
+ rootCmd.AddCommand({{ .name }}Cmd)
+{{- end }}
+}
+
+func initConfig() error {
+ if cfgFile != "" {
+ viper.SetConfigFile(cfgFile)
+ } else {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+
+ viper.AddConfigPath(home)
+ viper.AddConfigPath(".")
+ viper.SetConfigType("yaml")
+ viper.SetConfigName(".{{ .name }}")
+ }
+
+ // Environment variables
+ viper.SetEnvPrefix("{{ .name | upper }}")
+ viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
+ viper.AutomaticEnv()
+
+ // Read config file
+ if err := viper.ReadInConfig(); err != nil {
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+ return fmt.Errorf("error reading config: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/_examples/go-cli-scaffold/templates/cmd/version.go.tmpl b/_examples/go-cli-scaffold/templates/cmd/version.go.tmpl
new file mode 100644
index 0000000..8efb6fd
--- /dev/null
+++ b/_examples/go-cli-scaffold/templates/cmd/version.go.tmpl
@@ -0,0 +1,62 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "runtime"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var (
+ version = "dev"
+ commit = "unknown"
+ buildTime = "unknown"
+)
+
+// SetVersionInfo sets the version information from build flags
+func SetVersionInfo(v, c, bt string) {
+ version = v
+ commit = c
+ buildTime = bt
+}
+
+var versionCmd = &cobra.Command{
+ Use: "version",
+ Short: "Print version information",
+ Long: `Print version, commit hash, and build time information.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ info := struct {
+ Version string `json:"version"`
+ Commit string `json:"commit"`
+ BuildTime string `json:"buildTime"`
+ GoVersion string `json:"goVersion"`
+ OS string `json:"os"`
+ Arch string `json:"arch"`
+ }{
+ Version: version,
+ Commit: commit,
+ BuildTime: buildTime,
+ GoVersion: runtime.Version(),
+ OS: runtime.GOOS,
+ Arch: runtime.GOARCH,
+ }
+
+ switch viper.GetString("output") {
+ case "json":
+ data, _ := json.MarshalIndent(info, "", " ")
+ fmt.Println(string(data))
+ default:
+ fmt.Printf("{{ .name }} version %s\n", info.Version)
+ fmt.Printf(" commit: %s\n", info.Commit)
+ fmt.Printf(" built: %s\n", info.BuildTime)
+ fmt.Printf(" go version: %s\n", info.GoVersion)
+ fmt.Printf(" os/arch: %s/%s\n", info.OS, info.Arch)
+ }
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(versionCmd)
+}
diff --git a/_examples/go-cli-scaffold/templates/config.yaml.tmpl b/_examples/go-cli-scaffold/templates/config.yaml.tmpl
new file mode 100644
index 0000000..b337f33
--- /dev/null
+++ b/_examples/go-cli-scaffold/templates/config.yaml.tmpl
@@ -0,0 +1,7 @@
+# {{ .name }} configuration file
+# Place this file at ~/.{{ .name }}.yaml or specify with --config flag
+{{ range .config }}
+# {{ .description }}
+{{ if .env }}# Environment variable: {{ .env }}{{ end }}
+{{ .name }}: {{ if eq .type "string" }}"{{ .default }}"{{ else }}{{ .default }}{{ end }}
+{{ end }}
diff --git a/_examples/go-cli-scaffold/templates/go.mod.tmpl b/_examples/go-cli-scaffold/templates/go.mod.tmpl
new file mode 100644
index 0000000..10c30ab
--- /dev/null
+++ b/_examples/go-cli-scaffold/templates/go.mod.tmpl
@@ -0,0 +1,8 @@
+module {{ .module }}
+
+go 1.22
+
+require (
+ github.com/spf13/cobra v1.8.0
+ github.com/spf13/viper v1.18.2
+)
diff --git a/_examples/go-cli-scaffold/templates/main.go.tmpl b/_examples/go-cli-scaffold/templates/main.go.tmpl
new file mode 100644
index 0000000..b937117
--- /dev/null
+++ b/_examples/go-cli-scaffold/templates/main.go.tmpl
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "os"
+
+ "{{ .module }}/cmd"
+)
+
+// Build information set by ldflags
+var (
+ version = "dev"
+ commit = "unknown"
+ buildTime = "unknown"
+)
+
+func main() {
+ cmd.SetVersionInfo(version, commit, buildTime)
+ if err := cmd.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
diff --git a/_examples/go-driver/README.md b/_examples/go-driver/README.md
new file mode 100644
index 0000000..eb416a3
--- /dev/null
+++ b/_examples/go-driver/README.md
@@ -0,0 +1,71 @@
+# Go Driver Interface Pattern
+
+This example demonstrates how to generate a driver-based architecture in Go,
+following the pattern used by `database/sql`. This pattern enables pluggable
+implementations while maintaining a consistent API.
+
+## Why Use Render?
+
+The driver pattern requires multiple coordinated files:
+- The public facade (user-facing API)
+- The driver interface (implementor contract)
+- A registration mechanism
+- Multiple driver implementations
+
+With `render`, generate a complete driver architecture using two passes.
+
+## Structure Generated
+
+```
+pkg/storage/
+├── storage.go # Public facade API
+├── driver.go # Driver interface definition
+├── register.go # Driver registration mechanism
+└── drivers/
+ ├── s3/
+ │ └── s3.go # S3 driver implementation
+ ├── gcs/
+ │ └── gcs.go # GCS driver implementation
+ ├── azure/
+ │ └── azure.go # Azure driver implementation
+ ├── filesystem/
+ │ └── filesystem.go # Local filesystem driver
+ └── memory/
+ └── memory.go # In-memory driver for testing
+```
+
+## Usage
+
+This example demonstrates a **two-pass** approach:
+
+```bash
+# Pass 1: Generate core package (directory mode)
+render templates drivers.yaml -o pkg/storage
+
+# Pass 2: Generate driver implementations (each mode)
+render driver-template/driver_impl.go.tmpl drivers-list.yaml \
+ -o "pkg/storage/drivers/{{.name}}/{{.name}}.go"
+
+# Or run both with the script
+./render.sh
+```
+
+## Template Features Demonstrated
+
+- **Two-Pass Rendering**: Core package + per-driver implementations
+- **Directory Mode**: Generates facade, interface, and registration
+- **Each Mode**: Generates one driver per entry in drivers-list.yaml
+- **Separate Data Files**: drivers.yaml for core, drivers-list.yaml for each mode
+
+## Pattern Benefits
+
+1. **Decoupling**: Users depend on interface, not implementation
+2. **Extensibility**: Add new drivers without changing core code
+3. **Testability**: Mock drivers for unit testing
+4. **Init-time Registration**: Drivers register via `init()` functions
+
+## Real-World Use Case
+
+An AI assistant asked to "create a storage abstraction supporting S3, GCS, and
+local filesystem" can generate the complete driver architecture in seconds,
+ensuring all implementations follow the same contract.
diff --git a/_examples/go-driver/driver-template/driver_impl.go.tmpl b/_examples/go-driver/driver-template/driver_impl.go.tmpl
new file mode 100644
index 0000000..7f6d4c4
--- /dev/null
+++ b/_examples/go-driver/driver-template/driver_impl.go.tmpl
@@ -0,0 +1,75 @@
+// Package {{ .name }} provides an {{ .description | lower }} for the storage package.
+package {{ .name }}
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "storage"
+)
+
+func init() {
+ storage.Register("{{ .name }}", &connector{})
+}
+
+// Config holds the configuration for the {{ .name }} driver.
+type Config struct {
+{{- range .configFields }}
+ {{ .name }} {{ .type }}
+{{- end }}
+}
+
+type connector struct{}
+
+func (c *connector) Open(config any) (storage.Driver, error) {
+ cfg, ok := config.(*Config)
+ if !ok {
+ return nil, fmt.Errorf("{{ .name }}: invalid config type, expected *Config")
+ }
+ return newDriver(cfg)
+}
+
+type driver struct {
+ config *Config
+}
+
+func newDriver(cfg *Config) (*driver, error) {
+ // TODO: Validate configuration and establish connection
+ return &driver{config: cfg}, nil
+}
+
+func (d *driver) Close() error {
+ // TODO: Clean up resources
+ return nil
+}
+
+// Get retrieves an object by key.
+func (d *driver) Get(ctx context.Context, key string) (io.ReadCloser, error) {
+ // TODO: Implement Get
+ return nil, fmt.Errorf("{{ .name }}: Get not implemented")
+}
+
+// Put stores an object with the given key.
+func (d *driver) Put(ctx context.Context, key string, data io.Reader) error {
+ // TODO: Implement Put
+ return fmt.Errorf("{{ .name }}: Put not implemented")
+}
+
+// Delete deletes an object by key.
+func (d *driver) Delete(ctx context.Context, key string) error {
+ // TODO: Implement Delete
+ return fmt.Errorf("{{ .name }}: Delete not implemented")
+}
+
+// List lists objects matching the prefix.
+func (d *driver) List(ctx context.Context, prefix string) ([]string, error) {
+ // TODO: Implement List
+ return nil, fmt.Errorf("{{ .name }}: List not implemented")
+}
+
+// Exists checks if an object exists.
+func (d *driver) Exists(ctx context.Context, key string) (bool, error) {
+ // TODO: Implement Exists
+ return false, fmt.Errorf("{{ .name }}: Exists not implemented")
+}
diff --git a/_examples/go-driver/drivers-list.yaml b/_examples/go-driver/drivers-list.yaml
new file mode 100644
index 0000000..0428532
--- /dev/null
+++ b/_examples/go-driver/drivers-list.yaml
@@ -0,0 +1,45 @@
+- name: s3
+ description: Amazon S3 driver
+ configFields:
+ - name: Region
+ type: string
+ - name: Bucket
+ type: string
+ - name: AccessKeyID
+ type: string
+ - name: SecretAccessKey
+ type: string
+
+- name: gcs
+ description: Google Cloud Storage driver
+ configFields:
+ - name: Project
+ type: string
+ - name: Bucket
+ type: string
+ - name: CredentialsFile
+ type: string
+
+- name: azure
+ description: Azure Blob Storage driver
+ configFields:
+ - name: AccountName
+ type: string
+ - name: AccountKey
+ type: string
+ - name: Container
+ type: string
+
+- name: filesystem
+ description: Local filesystem driver
+ configFields:
+ - name: BasePath
+ type: string
+ - name: CreateDirs
+ type: bool
+
+- name: memory
+ description: In-memory driver for testing
+ configFields:
+ - name: MaxSize
+ type: int64
diff --git a/_examples/go-driver/drivers.yaml b/_examples/go-driver/drivers.yaml
new file mode 100644
index 0000000..d8737c6
--- /dev/null
+++ b/_examples/go-driver/drivers.yaml
@@ -0,0 +1,107 @@
+package: storage
+description: Cloud storage abstraction with pluggable drivers
+
+# The facade API methods
+methods:
+ - name: Get
+ args:
+ - name: ctx
+ type: context.Context
+ - name: key
+ type: string
+ returns:
+ - type: io.ReadCloser
+ - type: error
+ description: Retrieves an object by key
+
+ - name: Put
+ args:
+ - name: ctx
+ type: context.Context
+ - name: key
+ type: string
+ - name: data
+ type: io.Reader
+ returns:
+ - type: error
+ description: Stores an object with the given key
+
+ - name: Delete
+ args:
+ - name: ctx
+ type: context.Context
+ - name: key
+ type: string
+ returns:
+ - type: error
+ description: Deletes an object by key
+
+ - name: List
+ args:
+ - name: ctx
+ type: context.Context
+ - name: prefix
+ type: string
+ returns:
+ - type: "[]string"
+ - type: error
+ description: Lists objects matching the prefix
+
+ - name: Exists
+ args:
+ - name: ctx
+ type: context.Context
+ - name: key
+ type: string
+ returns:
+ - type: bool
+ - type: error
+ description: Checks if an object exists
+
+# Driver implementations
+drivers:
+ - name: s3
+ description: Amazon S3 driver
+ configFields:
+ - name: Region
+ type: string
+ - name: Bucket
+ type: string
+ - name: AccessKeyID
+ type: string
+ - name: SecretAccessKey
+ type: string
+
+ - name: gcs
+ description: Google Cloud Storage driver
+ configFields:
+ - name: Project
+ type: string
+ - name: Bucket
+ type: string
+ - name: CredentialsFile
+ type: string
+
+ - name: azure
+ description: Azure Blob Storage driver
+ configFields:
+ - name: AccountName
+ type: string
+ - name: AccountKey
+ type: string
+ - name: Container
+ type: string
+
+ - name: filesystem
+ description: Local filesystem driver
+ configFields:
+ - name: BasePath
+ type: string
+ - name: CreateDirs
+ type: bool
+
+ - name: memory
+ description: In-memory driver for testing
+ configFields:
+ - name: MaxSize
+ type: int64
diff --git a/_examples/go-driver/render.cmd b/_examples/go-driver/render.cmd
new file mode 100644
index 0000000..7494b3f
--- /dev/null
+++ b/_examples/go-driver/render.cmd
@@ -0,0 +1,32 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo === Pass 1: Generating core package (facade, interface, registration) ===
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%drivers.yaml" ^
+ -o "%OUTPUT_DIR%\pkg\storage"
+
+echo.
+echo === Pass 2: Generating driver implementations ===
+"%RENDER%" "%SCRIPT_DIR%driver-template\driver_impl.go.tmpl" "%SCRIPT_DIR%drivers-list.yaml" ^
+ -o "%OUTPUT_DIR%\pkg\storage\drivers\{{.name}}\{{.name}}.go"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul
diff --git a/_examples/go-driver/render.sh b/_examples/go-driver/render.sh
new file mode 100755
index 0000000..ed4d0ff
--- /dev/null
+++ b/_examples/go-driver/render.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "=== Pass 1: Generating core package (facade, interface, registration) ==="
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/drivers.yaml" \
+ -o "${OUTPUT_DIR}/pkg/storage"
+
+echo ""
+echo "=== Pass 2: Generating driver implementations ==="
+"$RENDER" "${SCRIPT_DIR}/driver-template/driver_impl.go.tmpl" "${SCRIPT_DIR}/drivers-list.yaml" \
+ -o "${OUTPUT_DIR}/pkg/storage/drivers/{{.name}}/{{.name}}.go"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+find "$OUTPUT_DIR" -type f | sort
diff --git a/_examples/go-driver/templates/driver.go.tmpl b/_examples/go-driver/templates/driver.go.tmpl
new file mode 100644
index 0000000..f71f429
--- /dev/null
+++ b/_examples/go-driver/templates/driver.go.tmpl
@@ -0,0 +1,21 @@
+package {{ .package }}
+
+import (
+ "context"
+ "io"
+)
+
+// Driver is the interface that must be implemented by storage drivers.
+type Driver interface {
+{{- range .methods }}
+ // {{ .name }} {{ .description | lower }}.
+ {{ .name }}({{ range $i, $arg := .args }}{{ if $i }}, {{ end }}{{ $arg.name }} {{ $arg.type }}{{ end }}) ({{ range $i, $ret := .returns }}{{ if $i }}, {{ end }}{{ $ret.type }}{{ end }})
+{{- end }}
+}
+
+// DriverConnector is implemented by drivers that can open connections.
+type DriverConnector interface {
+ // Open opens a connection using the provided configuration.
+ // The config type depends on the driver implementation.
+ Open(config any) (Driver, error)
+}
diff --git a/_examples/go-driver/templates/register.go.tmpl b/_examples/go-driver/templates/register.go.tmpl
new file mode 100644
index 0000000..a03ac7a
--- /dev/null
+++ b/_examples/go-driver/templates/register.go.tmpl
@@ -0,0 +1,48 @@
+package {{ .package }}
+
+import (
+ "fmt"
+ "sort"
+ "sync"
+)
+
+var (
+ driversMu sync.RWMutex
+ drivers = make(map[string]DriverConnector)
+)
+
+// Register makes a storage driver available by the provided name.
+// If Register is called twice with the same name or if driver is nil,
+// it panics.
+func Register(name string, driver DriverConnector) {
+ driversMu.Lock()
+ defer driversMu.Unlock()
+
+ if driver == nil {
+ panic("{{ .package }}: Register driver is nil")
+ }
+ if _, dup := drivers[name]; dup {
+ panic("{{ .package }}: Register called twice for driver " + name)
+ }
+ drivers[name] = driver
+}
+
+// UnregisterAllDrivers is used in tests to reset the driver registry.
+func UnregisterAllDrivers() {
+ driversMu.Lock()
+ defer driversMu.Unlock()
+ drivers = make(map[string]DriverConnector)
+}
+
+// RegisteredDrivers returns a sorted list of registered driver names.
+func RegisteredDrivers() []string {
+ driversMu.RLock()
+ defer driversMu.RUnlock()
+
+ names := make([]string, 0, len(drivers))
+ for name := range drivers {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
diff --git a/_examples/go-driver/templates/storage.go.tmpl b/_examples/go-driver/templates/storage.go.tmpl
new file mode 100644
index 0000000..e5bbc0f
--- /dev/null
+++ b/_examples/go-driver/templates/storage.go.tmpl
@@ -0,0 +1,77 @@
+// Package {{ .package }} provides a unified interface for cloud storage operations.
+// {{ .description }}
+//
+// Usage:
+//
+// import (
+// "{{ .package }}"
+// _ "{{ .package }}/drivers/s3" // Register S3 driver
+// )
+//
+// func main() {
+// client, err := {{ .package }}.Open("s3", config)
+// if err != nil {
+// log.Fatal(err)
+// }
+// defer client.Close()
+//
+// data, err := client.Get(ctx, "my-key")
+// // ...
+// }
+package {{ .package }}
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sync"
+)
+
+// Client provides a unified interface for storage operations.
+type Client struct {
+ driver Driver
+}
+
+// Open creates a new Client using the named driver.
+// The driver must have been registered via Register.
+func Open(driverName string, config any) (*Client, error) {
+ driversMu.RLock()
+ driver, ok := drivers[driverName]
+ driversMu.RUnlock()
+
+ if !ok {
+ return nil, fmt.Errorf("{{ .package }}: unknown driver %q (forgotten import?)", driverName)
+ }
+
+ d, err := driver.Open(config)
+ if err != nil {
+ return nil, fmt.Errorf("{{ .package }}: failed to open driver %q: %w", driverName, err)
+ }
+
+ return &Client{driver: d}, nil
+}
+
+// Close closes the client and releases any resources.
+func (c *Client) Close() error {
+ if closer, ok := c.driver.(io.Closer); ok {
+ return closer.Close()
+ }
+ return nil
+}
+{{ range .methods }}
+// {{ .name }} {{ .description | lower }}.
+func (c *Client) {{ .name }}({{ range $i, $arg := .args }}{{ if $i }}, {{ end }}{{ $arg.name }} {{ $arg.type }}{{ end }}) ({{ range $i, $ret := .returns }}{{ if $i }}, {{ end }}{{ $ret.type }}{{ end }}) {
+ return c.driver.{{ .name }}({{ range $i, $arg := .args }}{{ if $i }}, {{ end }}{{ $arg.name }}{{ end }})
+}
+{{ end }}
+// Drivers returns a list of registered driver names.
+func Drivers() []string {
+ driversMu.RLock()
+ defer driversMu.RUnlock()
+
+ names := make([]string, 0, len(drivers))
+ for name := range drivers {
+ names = append(names, name)
+ }
+ return names
+}
diff --git a/_examples/go-workspace-grpc/README.md b/_examples/go-workspace-grpc/README.md
new file mode 100644
index 0000000..78da189
--- /dev/null
+++ b/_examples/go-workspace-grpc/README.md
@@ -0,0 +1,70 @@
+# Go Workspace with gRPC Services
+
+This example demonstrates how to quickly scaffold a Go workspace containing multiple
+modules, each with a complete gRPC service architecture.
+
+## Why Use Render?
+
+When an AI assistant needs to create a multi-module Go workspace, writing each file
+individually is slow and error-prone. With `render`:
+
+1. **Speed**: Generate dozens of files in a single command
+2. **Consistency**: All services follow the same structure
+3. **Maintainability**: Update the template once, regenerate all services
+4. **Customization**: Each service gets its own name, port, and configuration
+
+## Structure Generated
+
+For each service defined in `services.yaml`, render creates:
+
+```
+services/
+└── {service-name}/
+ ├── go.mod
+ ├── proto/
+ │ └── {service}.proto
+ ├── cmd/
+ │ ├── server/
+ │ │ └── main.go
+ │ └── cli/
+ │ └── main.go
+ ├── internal/
+ │ ├── server/
+ │ │ └── server.go
+ │ └── client/
+ │ └── client.go
+ ├── api/
+ │ └── bff/
+ │ └── main.go
+ └── web/
+ ├── admin/
+ │ └── index.html
+ └── frontend/
+ └── index.html
+```
+
+## Usage
+
+```bash
+# Generate all services
+render templates services.yaml -o "services/{{.name | kebabCase}}"
+
+# Preview what would be generated
+render templates services.yaml -o "services/{{.name | kebabCase}}" --dry-run
+```
+
+## Template Features Demonstrated
+
+- **Each Mode**: Generates a complete directory structure per service
+- **Path Transformation**: Uses `.render.yaml` for dynamic file naming
+- **Casing Functions**: `kebabCase`, `pascalCase`, `snakeCase` for idiomatic naming
+- **Nested Data**: Services with ports, descriptions, and custom settings
+
+## Real-World Use Case
+
+An AI assistant asked to "create a microservices architecture with user, order, and
+payment services" can:
+
+1. Define the services in a YAML file
+2. Run a single render command
+3. Have a complete, consistent workspace ready for implementation
diff --git a/_examples/go-workspace-grpc/render.cmd b/_examples/go-workspace-grpc/render.cmd
new file mode 100644
index 0000000..eeca23f
--- /dev/null
+++ b/_examples/go-workspace-grpc/render.cmd
@@ -0,0 +1,27 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo Generating Go workspace with gRPC services...
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%services.yaml" ^
+ -o "%OUTPUT_DIR%\services\{{.name | kebabCase}}"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul | findstr /n "^" | findstr "^[1-9]:" | findstr /v "^[2-9][0-9]:"
diff --git a/_examples/go-workspace-grpc/render.sh b/_examples/go-workspace-grpc/render.sh
new file mode 100755
index 0000000..e8c0662
--- /dev/null
+++ b/_examples/go-workspace-grpc/render.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "Generating Go workspace with gRPC services..."
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/services.yaml" \
+ -o "${OUTPUT_DIR}/services/{{.name | kebabCase}}"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+find "$OUTPUT_DIR" -type f | head -20
diff --git a/_examples/go-workspace-grpc/services.yaml b/_examples/go-workspace-grpc/services.yaml
new file mode 100644
index 0000000..3e5264b
--- /dev/null
+++ b/_examples/go-workspace-grpc/services.yaml
@@ -0,0 +1,45 @@
+- name: user
+ port: 50051
+ httpPort: 8081
+ description: User management and authentication service
+ entities:
+ - name: User
+ fields:
+ - name: id
+ type: string
+ - name: email
+ type: string
+ - name: name
+ type: string
+
+- name: order
+ port: 50052
+ httpPort: 8082
+ description: Order processing and management service
+ entities:
+ - name: Order
+ fields:
+ - name: id
+ type: string
+ - name: userId
+ type: string
+ - name: total
+ type: double
+ - name: status
+ type: string
+
+- name: payment
+ port: 50053
+ httpPort: 8083
+ description: Payment processing service
+ entities:
+ - name: Payment
+ fields:
+ - name: id
+ type: string
+ - name: orderId
+ type: string
+ - name: amount
+ type: double
+ - name: method
+ type: string
diff --git a/_examples/go-workspace-grpc/templates/.render.yaml b/_examples/go-workspace-grpc/templates/.render.yaml
new file mode 100644
index 0000000..9c3f3ea
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/.render.yaml
@@ -0,0 +1,2 @@
+paths:
+ "proto/service.proto.tmpl": "proto/{{ .name }}.proto"
diff --git a/_examples/go-workspace-grpc/templates/api/bff/main.go.tmpl b/_examples/go-workspace-grpc/templates/api/bff/main.go.tmpl
new file mode 100644
index 0000000..740f9a4
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/api/bff/main.go.tmpl
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "log"
+ "net/http"
+)
+
+var (
+ port = flag.Int("port", {{ .httpPort }}, "The HTTP server port")
+)
+
+func main() {
+ flag.Parse()
+
+ mux := http.NewServeMux()
+
+ // Health check endpoint
+ mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
+ })
+{{ range .entities }}
+ // {{ .name }} endpoints
+ mux.HandleFunc("GET /api/{{ $.name }}/{{ .name | lower }}s", list{{ .name }}sHandler)
+ mux.HandleFunc("GET /api/{{ $.name }}/{{ .name | lower }}s/{id}", get{{ .name }}Handler)
+ mux.HandleFunc("POST /api/{{ $.name }}/{{ .name | lower }}s", create{{ .name }}Handler)
+ mux.HandleFunc("PUT /api/{{ $.name }}/{{ .name | lower }}s/{id}", update{{ .name }}Handler)
+ mux.HandleFunc("DELETE /api/{{ $.name }}/{{ .name | lower }}s/{id}", delete{{ .name }}Handler)
+{{ end }}
+ log.Printf("{{ .name | pascalCase }} BFF listening on :%d", *port)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), mux))
+}
+{{ range .entities }}
+func list{{ .name }}sHandler(w http.ResponseWriter, r *http.Request) {
+ // TODO: Call gRPC service and return JSON
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode([]interface{}{})
+}
+
+func get{{ .name }}Handler(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ // TODO: Call gRPC service and return JSON
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"id": id})
+}
+
+func create{{ .name }}Handler(w http.ResponseWriter, r *http.Request) {
+ // TODO: Parse body, call gRPC service, return JSON
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(map[string]string{"status": "created"})
+}
+
+func update{{ .name }}Handler(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ // TODO: Parse body, call gRPC service, return JSON
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"id": id, "status": "updated"})
+}
+
+func delete{{ .name }}Handler(w http.ResponseWriter, r *http.Request) {
+ // TODO: Call gRPC service
+ w.WriteHeader(http.StatusNoContent)
+}
+{{ end }}
diff --git a/_examples/go-workspace-grpc/templates/cmd/cli/main.go.tmpl b/_examples/go-workspace-grpc/templates/cmd/cli/main.go.tmpl
new file mode 100644
index 0000000..7115419
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/cmd/cli/main.go.tmpl
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+ "time"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+var (
+ addr = flag.String("addr", "localhost:{{ .port }}", "the address to connect to")
+)
+
+func main() {
+ flag.Parse()
+
+ conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf("did not connect: %v", err)
+ }
+ defer conn.Close()
+
+ // TODO: Create {{ .name | pascalCase }}Service client
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ _ = ctx // Use ctx for RPC calls
+ log.Printf("Connected to {{ .name }} service at %s", *addr)
+}
diff --git a/_examples/go-workspace-grpc/templates/cmd/server/main.go.tmpl b/_examples/go-workspace-grpc/templates/cmd/server/main.go.tmpl
new file mode 100644
index 0000000..6d29079
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/cmd/server/main.go.tmpl
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net"
+
+ "google.golang.org/grpc"
+)
+
+var (
+ port = flag.Int("port", {{ .port }}, "The server port")
+)
+
+func main() {
+ flag.Parse()
+
+ lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
+ if err != nil {
+ log.Fatalf("failed to listen: %v", err)
+ }
+
+ s := grpc.NewServer()
+ // TODO: Register {{ .name | pascalCase }}Service server
+
+ log.Printf("{{ .name | pascalCase }} service listening at %v", lis.Addr())
+ if err := s.Serve(lis); err != nil {
+ log.Fatalf("failed to serve: %v", err)
+ }
+}
diff --git a/_examples/go-workspace-grpc/templates/go.mod.tmpl b/_examples/go-workspace-grpc/templates/go.mod.tmpl
new file mode 100644
index 0000000..8dd34f6
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/go.mod.tmpl
@@ -0,0 +1,8 @@
+module github.com/example/{{ .name }}-service
+
+go 1.22
+
+require (
+ google.golang.org/grpc v1.62.0
+ google.golang.org/protobuf v1.32.0
+)
diff --git a/_examples/go-workspace-grpc/templates/internal/client/client.go.tmpl b/_examples/go-workspace-grpc/templates/internal/client/client.go.tmpl
new file mode 100644
index 0000000..367532c
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/internal/client/client.go.tmpl
@@ -0,0 +1,41 @@
+package client
+
+import (
+ "context"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+// {{ .name | pascalCase }}Client provides a client for the {{ .name | pascalCase }}Service.
+type {{ .name | pascalCase }}Client struct {
+ conn *grpc.ClientConn
+}
+
+// New{{ .name | pascalCase }}Client creates a new client connected to the specified address.
+func New{{ .name | pascalCase }}Client(addr string) (*{{ .name | pascalCase }}Client, error) {
+ conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ return nil, err
+ }
+ return &{{ .name | pascalCase }}Client{conn: conn}, nil
+}
+
+// Close closes the client connection.
+func (c *{{ .name | pascalCase }}Client) Close() error {
+ return c.conn.Close()
+}
+{{ range .entities }}
+// Get{{ .name }} retrieves a {{ .name }} by ID.
+func (c *{{ $.name | pascalCase }}Client) Get{{ .name }}(ctx context.Context, id string) (*{{ .name }}, error) {
+ // TODO: Implement gRPC call
+ return nil, nil
+}
+
+// {{ .name }} represents a {{ .name }} entity.
+type {{ .name }} struct {
+{{- range .fields }}
+ {{ .name | pascalCase }} {{ if eq .type "double" }}float64{{ else }}{{ .type }}{{ end }}
+{{- end }}
+}
+{{ end }}
diff --git a/_examples/go-workspace-grpc/templates/internal/server/server.go.tmpl b/_examples/go-workspace-grpc/templates/internal/server/server.go.tmpl
new file mode 100644
index 0000000..43a9686
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/internal/server/server.go.tmpl
@@ -0,0 +1,53 @@
+package server
+
+import (
+ "context"
+)
+
+// {{ .name | pascalCase }}Server implements the {{ .name | pascalCase }}Service.
+type {{ .name | pascalCase }}Server struct {
+ // TODO: Add dependencies (database, cache, etc.)
+}
+
+// New{{ .name | pascalCase }}Server creates a new {{ .name | pascalCase }}Server.
+func New{{ .name | pascalCase }}Server() *{{ .name | pascalCase }}Server {
+ return &{{ .name | pascalCase }}Server{}
+}
+{{ range .entities }}
+// Get{{ .name }} retrieves a {{ .name }} by ID.
+func (s *{{ $.name | pascalCase }}Server) Get{{ .name }}(ctx context.Context, id string) (*{{ .name }}, error) {
+ // TODO: Implement Get{{ .name }}
+ return nil, nil
+}
+
+// List{{ .name }}s returns a paginated list of {{ .name }}s.
+func (s *{{ $.name | pascalCase }}Server) List{{ .name }}s(ctx context.Context, pageSize int32, pageToken string) ([]*{{ .name }}, string, error) {
+ // TODO: Implement List{{ .name }}s
+ return nil, "", nil
+}
+
+// Create{{ .name }} creates a new {{ .name }}.
+func (s *{{ $.name | pascalCase }}Server) Create{{ .name }}(ctx context.Context, entity *{{ .name }}) (*{{ .name }}, error) {
+ // TODO: Implement Create{{ .name }}
+ return nil, nil
+}
+
+// Update{{ .name }} updates an existing {{ .name }}.
+func (s *{{ $.name | pascalCase }}Server) Update{{ .name }}(ctx context.Context, entity *{{ .name }}) (*{{ .name }}, error) {
+ // TODO: Implement Update{{ .name }}
+ return nil, nil
+}
+
+// Delete{{ .name }} deletes a {{ .name }} by ID.
+func (s *{{ $.name | pascalCase }}Server) Delete{{ .name }}(ctx context.Context, id string) error {
+ // TODO: Implement Delete{{ .name }}
+ return nil
+}
+
+// {{ .name }} represents a {{ .name }} entity.
+type {{ .name }} struct {
+{{- range .fields }}
+ {{ .name | pascalCase }} {{ if eq .type "double" }}float64{{ else }}{{ .type }}{{ end }}
+{{- end }}
+}
+{{ end }}
diff --git a/_examples/go-workspace-grpc/templates/proto/service.proto.tmpl b/_examples/go-workspace-grpc/templates/proto/service.proto.tmpl
new file mode 100644
index 0000000..bbda6c1
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/proto/service.proto.tmpl
@@ -0,0 +1,51 @@
+syntax = "proto3";
+
+package {{ .name }};
+
+option go_package = "github.com/example/{{ .name }}-service/gen/{{ .name }}pb";
+
+// {{ .description }}
+service {{ .name | pascalCase }}Service {
+{{- range .entities }}
+ rpc Get{{ .name }}(Get{{ .name }}Request) returns ({{ .name }}) {}
+ rpc List{{ .name }}s(List{{ .name }}sRequest) returns (List{{ .name }}sResponse) {}
+ rpc Create{{ .name }}(Create{{ .name }}Request) returns ({{ .name }}) {}
+ rpc Update{{ .name }}(Update{{ .name }}Request) returns ({{ .name }}) {}
+ rpc Delete{{ .name }}(Delete{{ .name }}Request) returns (Delete{{ .name }}Response) {}
+{{- end }}
+}
+{{ range .entities }}
+message {{ .name }} {
+{{- range $i, $f := .fields }}
+ {{ $f.type }} {{ $f.name }} = {{ add $i 1 }};
+{{- end }}
+}
+
+message Get{{ .name }}Request {
+ string id = 1;
+}
+
+message List{{ .name }}sRequest {
+ int32 page_size = 1;
+ string page_token = 2;
+}
+
+message List{{ .name }}sResponse {
+ repeated {{ .name }} {{ .name | lower }}s = 1;
+ string next_page_token = 2;
+}
+
+message Create{{ .name }}Request {
+ {{ .name }} {{ .name | lower }} = 1;
+}
+
+message Update{{ .name }}Request {
+ {{ .name }} {{ .name | lower }} = 1;
+}
+
+message Delete{{ .name }}Request {
+ string id = 1;
+}
+
+message Delete{{ .name }}Response {}
+{{ end }}
diff --git a/_examples/go-workspace-grpc/templates/web/admin/index.html.tmpl b/_examples/go-workspace-grpc/templates/web/admin/index.html.tmpl
new file mode 100644
index 0000000..dd11bac
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/web/admin/index.html.tmpl
@@ -0,0 +1,64 @@
+
+
+
+
+
+ {{ .name | pascalCase }} Service - Admin
+
+
+
+
+ {{ .name | pascalCase }} Service Administration
+
+
+
{{ .description }}
+ {{ range .entities }}
+
+
{{ .name }} Management
+
+
+
+ {{- range .fields }}
+ | {{ .name | title }} |
+ {{- end }}
+ Actions |
+
+
+
+
+
+
+
+
+ {{ end }}
+
+
+
+
diff --git a/_examples/go-workspace-grpc/templates/web/frontend/index.html.tmpl b/_examples/go-workspace-grpc/templates/web/frontend/index.html.tmpl
new file mode 100644
index 0000000..6c827a1
--- /dev/null
+++ b/_examples/go-workspace-grpc/templates/web/frontend/index.html.tmpl
@@ -0,0 +1,42 @@
+
+
+
+
+
+ {{ .name | pascalCase }} Service
+
+
+
+
+
{{ .name | pascalCase }} Service
+
{{ .description }}
+
+
+
Available Resources
+ {{ range .entities }}
+
+
{{ .name }}
+
Manage {{ .name | lower }} resources with full CRUD operations.
+
+ {{- range .fields }}
+ - {{ .name }} ({{ .type }})
+ {{- end }}
+
+
+ {{ end }}
+
+
+
+
diff --git a/_examples/go-workspace-makefile/README.md b/_examples/go-workspace-makefile/README.md
new file mode 100644
index 0000000..fcd0bd2
--- /dev/null
+++ b/_examples/go-workspace-makefile/README.md
@@ -0,0 +1,61 @@
+# Go Workspace Makefile Generator
+
+This example demonstrates generating a comprehensive Makefile for a Go workspace
+with multiple modules. The Makefile includes targets for building, testing, linting,
+and managing all modules consistently.
+
+## Why Use Render?
+
+Manually maintaining a Makefile for a multi-module Go workspace is tedious:
+- Each module needs similar targets
+- Dependencies between modules must be tracked
+- Adding a new module requires updating multiple places
+
+With `render`, regenerate the Makefile whenever modules change.
+
+## Generated Targets
+
+For a workspace with modules `api`, `cli`, and `worker`:
+
+```makefile
+# Global targets
+make all # Build all modules
+make test # Test all modules
+make lint # Lint all modules
+make clean # Clean all modules
+make tidy # Tidy all modules
+
+# Per-module targets
+make build-api # Build api module
+make test-api # Test api module
+make lint-api # Lint api module
+
+make build-cli # Build cli module
+make build-worker # Build worker module
+# ... etc
+```
+
+## Usage
+
+```bash
+# Generate Makefile
+render templates/Makefile.tmpl workspace.yaml -o Makefile
+
+# Preview
+render templates/Makefile.tmpl workspace.yaml -o Makefile --dry-run
+```
+
+## Template Features Demonstrated
+
+- **File Mode**: Single template to single output file
+- **Loops**: Generate targets per module
+- **Conditionals**: Different build flags per module type
+- **String Functions**: Path manipulation, naming conventions
+
+## Real-World Use Case
+
+An AI assistant asked to "add a new service module to the workspace" can:
+
+1. Add the module to `workspace.yaml`
+2. Regenerate the Makefile
+3. Have all standard targets immediately available
diff --git a/_examples/go-workspace-makefile/render.cmd b/_examples/go-workspace-makefile/render.cmd
new file mode 100644
index 0000000..bc16255
--- /dev/null
+++ b/_examples/go-workspace-makefile/render.cmd
@@ -0,0 +1,27 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo Generating Makefile for Go workspace...
+"%RENDER%" "%SCRIPT_DIR%templates\Makefile.tmpl" "%SCRIPT_DIR%workspace.yaml" ^
+ -o "%OUTPUT_DIR%\Makefile"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir "%OUTPUT_DIR%"
diff --git a/_examples/go-workspace-makefile/render.sh b/_examples/go-workspace-makefile/render.sh
new file mode 100755
index 0000000..8b64ee2
--- /dev/null
+++ b/_examples/go-workspace-makefile/render.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "Generating Makefile for Go workspace..."
+"$RENDER" "${SCRIPT_DIR}/templates/Makefile.tmpl" "${SCRIPT_DIR}/workspace.yaml" \
+ -o "${OUTPUT_DIR}/Makefile"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+ls -la "$OUTPUT_DIR"
diff --git a/_examples/go-workspace-makefile/templates/Makefile.tmpl b/_examples/go-workspace-makefile/templates/Makefile.tmpl
new file mode 100644
index 0000000..16a288f
--- /dev/null
+++ b/_examples/go-workspace-makefile/templates/Makefile.tmpl
@@ -0,0 +1,153 @@
+# Makefile for {{ .name }} Go workspace
+# Generated by render - do not edit manually
+
+.PHONY: all build test lint clean tidy fmt vet help
+.PHONY:{{ range .modules }} build-{{ .name }} test-{{ .name }} lint-{{ .name }} clean-{{ .name }}{{ end }}
+{{ if .dockerRegistry }}.PHONY:{{ range .modules }}{{ if .hasDocker }} docker-{{ .name }}{{ end }}{{ end }}{{ end }}
+
+# Go settings
+GO := go
+GOFLAGS := -v
+LDFLAGS := {{ .ldflags }}
+{{ if .buildTags }}BUILD_TAGS := -tags {{ .buildTags }}{{ end }}
+
+# Version info
+VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
+COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
+LDFLAGS += -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildTime=$(BUILD_TIME)
+
+# Docker settings
+{{ if .dockerRegistry -}}
+DOCKER_REGISTRY := {{ .dockerRegistry }}
+DOCKER_TAG ?= $(VERSION)
+{{- end }}
+
+# Directories
+BIN_DIR := bin
+DIST_DIR := dist
+
+#==============================================================================
+# Default target
+#==============================================================================
+
+all: build
+
+#==============================================================================
+# Global targets
+#==============================================================================
+
+build:{{ range .modules }} build-{{ .name }}{{ end }}
+
+test:{{ range .modules }} test-{{ .name }}{{ end }}
+
+lint:{{ range .modules }} lint-{{ .name }}{{ end }}
+ @echo "All modules linted successfully"
+
+clean:{{ range .modules }} clean-{{ .name }}{{ end }}
+ rm -rf $(BIN_DIR) $(DIST_DIR)
+
+tidy:
+ @for dir in {{ range .modules }}{{ .path }} {{ end }}; do \
+ echo "Tidying $$dir..."; \
+ (cd $$dir && $(GO) mod tidy); \
+ done
+
+fmt:
+ @gofmt -s -w .
+
+vet:
+ @$(GO) vet ./...
+
+#==============================================================================
+# Per-module targets
+#==============================================================================
+{{ range .modules }}
+# {{ .name }} ({{ .type }})
+build-{{ .name }}:
+{{- if eq .type "library" }}
+ @echo "Checking {{ .name }}..."
+ @(cd {{ .path }} && $(GO) build ./...)
+{{- else if eq .type "proto" }}
+ @echo "Generating {{ .name }}..."
+ @(cd {{ .path }} && buf generate)
+{{- else }}
+ @echo "Building {{ .name }}..."
+ @mkdir -p $(BIN_DIR)
+ @(cd {{ .path }} && $(GO) build $(GOFLAGS) $(BUILD_TAGS) -ldflags "$(LDFLAGS)" -o ../../$(BIN_DIR)/{{ .binary }} .)
+{{- end }}
+
+test-{{ .name }}:
+ @echo "Testing {{ .name }}..."
+ @(cd {{ .path }} && $(GO) test -race -cover ./...)
+
+lint-{{ .name }}:
+ @echo "Linting {{ .name }}..."
+ @(cd {{ .path }} && golangci-lint run)
+
+clean-{{ .name }}:
+{{- if .binary }}
+ @rm -f $(BIN_DIR)/{{ .binary }}
+{{- end }}
+ @(cd {{ .path }} && $(GO) clean)
+{{ if .hasDocker }}
+docker-{{ .name }}:
+ @echo "Building Docker image for {{ .name }}..."
+ @docker build -t $(DOCKER_REGISTRY)/{{ .name }}:$(DOCKER_TAG) -f {{ .path }}/Dockerfile .
+{{ end }}
+{{- end }}
+
+#==============================================================================
+# Development targets
+#==============================================================================
+{{ range .modules }}{{ if .port }}
+run-{{ .name }}: build-{{ .name }}
+ @echo "Running {{ .name }} on port {{ .port }}..."
+ @$(BIN_DIR)/{{ .binary }}
+{{ end }}{{ end }}
+
+#==============================================================================
+# CI/CD targets
+#==============================================================================
+
+ci: lint test build
+
+install-tools:
+ @echo "Installing development tools..."
+ @go install github.com/golangci/golangci-lint/cmd/golangci-lint@{{ .golangciVersion }}
+{{ if .dockerRegistry }}
+docker-push:{{ range .modules }}{{ if .hasDocker }} docker-push-{{ .name }}{{ end }}{{ end }}
+
+{{ range .modules }}{{ if .hasDocker -}}
+docker-push-{{ .name }}: docker-{{ .name }}
+ @docker push $(DOCKER_REGISTRY)/{{ .name }}:$(DOCKER_TAG)
+{{ end }}{{ end }}
+{{- end }}
+
+#==============================================================================
+# Help
+#==============================================================================
+
+help:
+ @echo "{{ .name }} Makefile"
+ @echo ""
+ @echo "Global targets:"
+ @echo " all Build all modules (default)"
+ @echo " build Build all modules"
+ @echo " test Test all modules"
+ @echo " lint Lint all modules"
+ @echo " clean Clean all modules and build artifacts"
+ @echo " tidy Run go mod tidy on all modules"
+ @echo " fmt Format all Go files"
+ @echo " vet Run go vet on all packages"
+ @echo ""
+ @echo "Module targets:"
+{{- range .modules }}
+ @echo " build-{{ .name }} Build {{ .name }} module"
+ @echo " test-{{ .name }} Test {{ .name }} module"
+ @echo " lint-{{ .name }} Lint {{ .name }} module"
+{{- end }}
+ @echo ""
+ @echo "Development:"
+ @echo " install-tools Install development tools"
+ @echo " ci Run full CI pipeline"
diff --git a/_examples/go-workspace-makefile/workspace.yaml b/_examples/go-workspace-makefile/workspace.yaml
new file mode 100644
index 0000000..5e49644
--- /dev/null
+++ b/_examples/go-workspace-makefile/workspace.yaml
@@ -0,0 +1,41 @@
+name: myproject
+goVersion: "1.22"
+defaultBranch: main
+
+modules:
+ - name: api
+ path: services/api
+ type: service
+ binary: api-server
+ port: 8080
+ hasDocker: true
+
+ - name: worker
+ path: services/worker
+ type: service
+ binary: worker
+ hasDocker: true
+
+ - name: cli
+ path: cmd/cli
+ type: cli
+ binary: myctl
+
+ - name: shared
+ path: pkg/shared
+ type: library
+
+ - name: proto
+ path: pkg/proto
+ type: proto
+
+# Build settings
+ldflags: "-s -w"
+buildTags: ""
+
+# Docker settings
+dockerRegistry: ghcr.io/myorg
+dockerBaseImage: gcr.io/distroless/static:nonroot
+
+# Linting
+golangciVersion: v1.56.0
diff --git a/_examples/kubernetes-manifests/README.md b/_examples/kubernetes-manifests/README.md
new file mode 100644
index 0000000..a9209ea
--- /dev/null
+++ b/_examples/kubernetes-manifests/README.md
@@ -0,0 +1,64 @@
+# Kubernetes Manifests Generator
+
+This example demonstrates generating Kubernetes manifests for multiple services
+across multiple environments. Define your services once and generate consistent
+deployments, services, and configmaps for each environment.
+
+## Why Use Render?
+
+Managing Kubernetes manifests for multiple environments involves:
+- Copy-pasting YAML with subtle differences
+- Risk of configuration drift between environments
+- Difficulty keeping all environments in sync
+
+With `render`, maintain a single template and generate environment-specific manifests.
+
+## Structure Generated
+
+```
+k8s/
+├── base/
+│ └── namespace.yaml
+└── overlays/
+ ├── dev/
+ │ └── {service}/
+ │ ├── deployment.yaml
+ │ ├── service.yaml
+ │ └── configmap.yaml
+ ├── staging/
+ │ └── {service}/
+ │ └── ...
+ └── prod/
+ └── {service}/
+ └── ...
+```
+
+## Usage
+
+```bash
+# Generate manifests for all environments
+render templates cluster.yaml -o k8s
+
+# Generate for a specific environment (filter data first)
+yq '.environments[] | select(.name == "dev")' cluster.yaml | \
+ render templates/env - -o k8s/overlays/dev
+```
+
+## Template Features Demonstrated
+
+- **Directory Mode**: Complete Kubernetes structure
+- **Nested Loops**: Services × Environments
+- **Path Transformation**: Environment and service in paths
+- **Conditionals**: Different resource limits per environment
+
+## Environment-Specific Configuration
+
+- **Dev**: Lower resources, single replica, debug logging
+- **Staging**: Moderate resources, 2 replicas, standard config
+- **Prod**: High resources, 3+ replicas, production settings
+
+## Real-World Use Case
+
+An AI assistant asked to "deploy our services to a new staging environment"
+can generate all required manifests with environment-appropriate settings in
+a single command.
diff --git a/_examples/kubernetes-manifests/cluster.yaml b/_examples/kubernetes-manifests/cluster.yaml
new file mode 100644
index 0000000..27b5756
--- /dev/null
+++ b/_examples/kubernetes-manifests/cluster.yaml
@@ -0,0 +1,61 @@
+namespace: myapp
+project: example-project
+
+services:
+ - name: api
+ image: ghcr.io/example/api
+ port: 8080
+ healthPath: /health
+ env:
+ - name: LOG_LEVEL
+ dev: debug
+ staging: info
+ prod: warn
+
+ - name: worker
+ image: ghcr.io/example/worker
+ port: 9090
+ healthPath: /ready
+ env:
+ - name: QUEUE_URL
+ dev: redis://redis:6379
+ staging: redis://redis.staging:6379
+ prod: redis://redis.prod:6379
+
+ - name: frontend
+ image: ghcr.io/example/frontend
+ port: 3000
+ healthPath: /
+ env:
+ - name: API_URL
+ dev: http://api:8080
+ staging: https://api.staging.example.com
+ prod: https://api.example.com
+
+environments:
+ - name: dev
+ replicas: 1
+ resources:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 200m
+ memory: 256Mi
+
+ - name: staging
+ replicas: 2
+ resources:
+ cpu: 200m
+ memory: 256Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+
+ - name: prod
+ replicas: 3
+ resources:
+ cpu: 500m
+ memory: 512Mi
+ limits:
+ cpu: "1"
+ memory: 1Gi
diff --git a/_examples/kubernetes-manifests/render.cmd b/_examples/kubernetes-manifests/render.cmd
new file mode 100644
index 0000000..2f6cdae
--- /dev/null
+++ b/_examples/kubernetes-manifests/render.cmd
@@ -0,0 +1,30 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo Generating Kubernetes manifests...
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%cluster.yaml" ^
+ -o "%OUTPUT_DIR%"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+echo Note: Service-specific manifests (deployment, service, configmap)
+echo require combining service and environment data. See README.md.
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul
diff --git a/_examples/kubernetes-manifests/render.sh b/_examples/kubernetes-manifests/render.sh
new file mode 100755
index 0000000..f6c2447
--- /dev/null
+++ b/_examples/kubernetes-manifests/render.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "Generating Kubernetes manifests..."
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/cluster.yaml" \
+ -o "${OUTPUT_DIR}"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+echo "Note: Service-specific manifests (deployment, service, configmap)"
+echo "require combining service and environment data. See README.md."
+echo ""
+find "$OUTPUT_DIR" -type f
diff --git a/_examples/kubernetes-manifests/service-template/configmap.yaml.tmpl b/_examples/kubernetes-manifests/service-template/configmap.yaml.tmpl
new file mode 100644
index 0000000..377367a
--- /dev/null
+++ b/_examples/kubernetes-manifests/service-template/configmap.yaml.tmpl
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .service.name }}-config
+ namespace: {{ .namespace }}-{{ .env.name }}
+ labels:
+ app: {{ .service.name }}
+ environment: {{ .env.name }}
+data:
+{{- range .service.env }}
+ {{ .name }}: "{{ index . $.env.name }}"
+{{- end }}
diff --git a/_examples/kubernetes-manifests/service-template/deployment.yaml.tmpl b/_examples/kubernetes-manifests/service-template/deployment.yaml.tmpl
new file mode 100644
index 0000000..59d2848
--- /dev/null
+++ b/_examples/kubernetes-manifests/service-template/deployment.yaml.tmpl
@@ -0,0 +1,46 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .service.name }}
+ namespace: {{ .namespace }}-{{ .env.name }}
+ labels:
+ app: {{ .service.name }}
+ environment: {{ .env.name }}
+spec:
+ replicas: {{ .env.replicas }}
+ selector:
+ matchLabels:
+ app: {{ .service.name }}
+ template:
+ metadata:
+ labels:
+ app: {{ .service.name }}
+ environment: {{ .env.name }}
+ spec:
+ containers:
+ - name: {{ .service.name }}
+ image: {{ .service.image }}:latest
+ ports:
+ - containerPort: {{ .service.port }}
+ resources:
+ requests:
+ cpu: {{ .env.resources.cpu }}
+ memory: {{ .env.resources.memory }}
+ limits:
+ cpu: {{ .env.limits.cpu }}
+ memory: {{ .env.limits.memory }}
+ livenessProbe:
+ httpGet:
+ path: {{ .service.healthPath }}
+ port: {{ .service.port }}
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: {{ .service.healthPath }}
+ port: {{ .service.port }}
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ envFrom:
+ - configMapRef:
+ name: {{ .service.name }}-config
diff --git a/_examples/kubernetes-manifests/service-template/service.yaml.tmpl b/_examples/kubernetes-manifests/service-template/service.yaml.tmpl
new file mode 100644
index 0000000..71ae786
--- /dev/null
+++ b/_examples/kubernetes-manifests/service-template/service.yaml.tmpl
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .service.name }}
+ namespace: {{ .namespace }}-{{ .env.name }}
+ labels:
+ app: {{ .service.name }}
+ environment: {{ .env.name }}
+spec:
+ type: ClusterIP
+ ports:
+ - port: {{ .service.port }}
+ targetPort: {{ .service.port }}
+ protocol: TCP
+ name: http
+ selector:
+ app: {{ .service.name }}
diff --git a/_examples/kubernetes-manifests/templates/base/namespace.yaml.tmpl b/_examples/kubernetes-manifests/templates/base/namespace.yaml.tmpl
new file mode 100644
index 0000000..766ecd1
--- /dev/null
+++ b/_examples/kubernetes-manifests/templates/base/namespace.yaml.tmpl
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: {{ .namespace }}
+ labels:
+ app.kubernetes.io/part-of: {{ .project }}
+ app.kubernetes.io/managed-by: render
diff --git a/_examples/maven-multimodule-grpc/README.md b/_examples/maven-multimodule-grpc/README.md
new file mode 100644
index 0000000..b0dd0a6
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/README.md
@@ -0,0 +1,62 @@
+# Maven Multi-Module Project with gRPC Services
+
+This example demonstrates scaffolding a Maven multi-module project where each module
+contains a complete gRPC service with CLI, admin UI, BFF, and browser frontend.
+
+## Why Use Render?
+
+Creating a Maven multi-module project manually involves:
+- Writing parent POM with dependency management
+- Creating module POMs with proper parent references
+- Setting up consistent directory structures
+- Configuring protobuf compilation plugins
+
+With `render`, generate the entire structure from a single data file.
+
+## Structure Generated
+
+```
+{project-name}/
+├── pom.xml # Parent POM
+└── modules/
+ └── {service-name}/
+ ├── pom.xml # Module POM
+ ├── src/main/proto/
+ │ └── {service}.proto
+ ├── src/main/java/.../
+ │ ├── server/
+ │ │ └── {Service}Server.java
+ │ ├── client/
+ │ │ └── {Service}Client.java
+ │ └── bff/
+ │ └── {Service}BffApplication.java
+ └── src/main/resources/
+ └── static/
+ ├── admin/
+ │ └── index.html
+ └── frontend/
+ └── index.html
+```
+
+## Usage
+
+```bash
+# Generate the complete project
+render templates project.yaml -o output
+
+# With custom package paths
+render templates project.yaml -o output --force
+```
+
+## Template Features Demonstrated
+
+- **Directory Mode**: Single invocation generates complete structure
+- **Path Transformation**: `.render.yaml` maps Java package to directory structure
+- **Nested Loops**: Services containing multiple entities
+- **String Functions**: Package name to path conversion with `replace`
+
+## Real-World Use Case
+
+An AI assistant asked to "create a Spring Boot microservices project with three
+services" can generate a production-ready Maven structure in seconds, including
+proper dependency versions and plugin configurations.
diff --git a/_examples/maven-multimodule-grpc/project.yaml b/_examples/maven-multimodule-grpc/project.yaml
new file mode 100644
index 0000000..4371169
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/project.yaml
@@ -0,0 +1,54 @@
+name: ecommerce-platform
+groupId: com.example.ecommerce
+version: 1.0.0-SNAPSHOT
+javaVersion: 21
+springBootVersion: 3.2.2
+grpcVersion: 1.62.0
+
+services:
+ - name: catalog
+ package: com.example.ecommerce.catalog
+ port: 9090
+ httpPort: 8080
+ description: Product catalog management
+ entities:
+ - name: Product
+ fields:
+ - name: id
+ type: String
+ - name: name
+ type: String
+ - name: price
+ type: BigDecimal
+ - name: category
+ type: String
+
+ - name: inventory
+ package: com.example.ecommerce.inventory
+ port: 9091
+ httpPort: 8081
+ description: Inventory and stock management
+ entities:
+ - name: Stock
+ fields:
+ - name: productId
+ type: String
+ - name: quantity
+ type: Integer
+ - name: warehouse
+ type: String
+
+ - name: checkout
+ package: com.example.ecommerce.checkout
+ port: 9092
+ httpPort: 8082
+ description: Shopping cart and checkout processing
+ entities:
+ - name: Cart
+ fields:
+ - name: id
+ type: String
+ - name: customerId
+ type: String
+ - name: items
+ type: List
diff --git a/_examples/maven-multimodule-grpc/render.cmd b/_examples/maven-multimodule-grpc/render.cmd
new file mode 100644
index 0000000..a9b7fb8
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/render.cmd
@@ -0,0 +1,27 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set SCRIPT_DIR=%~dp0
+set RENDER=%SCRIPT_DIR%..\..\render.exe
+set OUTPUT_DIR=%SCRIPT_DIR%output
+
+:: Build render if needed
+if not exist "%RENDER%" (
+ echo Building render...
+ pushd %SCRIPT_DIR%..\..
+ go build -o render.exe ./cmd/render
+ popd
+)
+
+:: Clean and create output directory
+if exist "%OUTPUT_DIR%" rmdir /s /q "%OUTPUT_DIR%"
+mkdir "%OUTPUT_DIR%"
+
+echo Generating Maven multi-module project...
+"%RENDER%" "%SCRIPT_DIR%templates" "%SCRIPT_DIR%project.yaml" ^
+ -o "%OUTPUT_DIR%"
+
+echo.
+echo Output generated in: %OUTPUT_DIR%
+echo.
+dir /s /b "%OUTPUT_DIR%" 2>nul
diff --git a/_examples/maven-multimodule-grpc/render.sh b/_examples/maven-multimodule-grpc/render.sh
new file mode 100755
index 0000000..4ca8aaa
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/render.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RENDER="${SCRIPT_DIR}/../../render"
+OUTPUT_DIR="${SCRIPT_DIR}/output"
+
+# Build render if needed
+if [ ! -f "$RENDER" ]; then
+ echo "Building render..."
+ (cd "${SCRIPT_DIR}/../.." && go build -o render ./cmd/render)
+fi
+
+# Clean and create output directory
+rm -rf "$OUTPUT_DIR"
+mkdir -p "$OUTPUT_DIR"
+
+echo "Generating Maven multi-module project..."
+"$RENDER" "${SCRIPT_DIR}/templates" "${SCRIPT_DIR}/project.yaml" \
+ -o "${OUTPUT_DIR}"
+
+echo ""
+echo "Output generated in: ${OUTPUT_DIR}"
+echo ""
+find "$OUTPUT_DIR" -type f
diff --git a/_examples/maven-multimodule-grpc/service-template/pom.xml.tmpl b/_examples/maven-multimodule-grpc/service-template/pom.xml.tmpl
new file mode 100644
index 0000000..5c80326
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/service-template/pom.xml.tmpl
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+
+ {{ .groupId }}
+ {{ .projectName }}
+ {{ .version }}
+ ../../pom.xml
+
+
+ {{ .name }}-service
+ {{ .name | title }} Service
+ {{ .description }}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
diff --git a/_examples/maven-multimodule-grpc/service-template/src/main/java/bff/BffApplication.java.tmpl b/_examples/maven-multimodule-grpc/service-template/src/main/java/bff/BffApplication.java.tmpl
new file mode 100644
index 0000000..20ae948
--- /dev/null
+++ b/_examples/maven-multimodule-grpc/service-template/src/main/java/bff/BffApplication.java.tmpl
@@ -0,0 +1,61 @@
+package {{ .package }}.bff;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Backend-for-Frontend application for {{ .name | pascalCase }} service.
+ * Provides REST API endpoints that communicate with the gRPC service.
+ */
+@SpringBootApplication
+@RestController
+@RequestMapping("/api/{{ .name }}")
+public class {{ .name | pascalCase }}BffApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run({{ .name | pascalCase }}BffApplication.class, args);
+ }
+
+ @GetMapping("/health")
+ public Map health() {
+ return Map.of("status", "healthy", "service", "{{ .name }}");
+ }
+{{ range .entities }}
+ @GetMapping("/{{ .name | lower }}s")
+ public List