diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 63077ab5..00000000
--- a/.editorconfig
+++ /dev/null
@@ -1,26 +0,0 @@
-# https://editorconfig.org/
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-indent_size = 4
-indent_style = space
-insert_final_newline = true
-trim_trailing_whitespace = true
-
-[*.json]
-indent_size = 2
-
-[*.{js,jsx,ts,tsx}]
-indent_size = 2
-
-[*.md]
-indent_size = unset
-trim_trailing_whitespace = false
-
-[*.py]
-indent_size = 4
-
-[{*.{yaml,yml},.yamllint}]
-indent_size = 2
diff --git a/.github/scripts/generate_wiki_docs.py b/.github/scripts/generate_wiki_docs.py
deleted file mode 100755
index c43a9b0f..00000000
--- a/.github/scripts/generate_wiki_docs.py
+++ /dev/null
@@ -1,252 +0,0 @@
-#!/usr/bin/env python3
-"""Generate GitHub Wiki documentation for module variables.
-
-This script auto-generates variable documentation in GitHub Wiki markdown format
-for all registered modules, using the latest schema version for each.
-"""
-
-import sys
-from pathlib import Path
-
-# Add project root to path (script is in .github/scripts, so go up twice)
-project_root = Path(__file__).parent.parent.parent
-sys.path.insert(0, str(project_root))
-
-# ruff: noqa: E402
-# Import all modules to register them
-import cli.modules.ansible
-import cli.modules.compose
-import cli.modules.helm
-import cli.modules.kubernetes
-import cli.modules.packer
-import cli.modules.terraform # noqa: F401
-from cli.core.registry import registry # Module import after path manipulation
-
-
-def format_value(value):
- """Format value for markdown display."""
- if value is None or value == "":
- return "_none_"
- if isinstance(value, bool):
- return "✓" if value else "✗"
- if isinstance(value, list):
- return ", ".join(f"`{v}`" for v in value)
- return f"`{value}`"
-
-
-def generate_module_docs(module_name: str, output_dir: Path): # noqa: PLR0912, PLR0915
- """Generate wiki documentation for a single module."""
- # Get module class from registry
- module_classes = dict(registry.iter_module_classes())
-
- if module_name not in module_classes:
- sys.stderr.write(f"Warning: Module '{module_name}' not found, skipping\n")
- return False
-
- module_cls = module_classes[module_name]
- schema_version = module_cls.schema_version
-
- # Get the spec for the latest schema version
- if hasattr(module_cls, "schemas") and schema_version in module_cls.schemas:
- spec = module_cls.schemas[schema_version]
- elif hasattr(module_cls, "spec"):
- spec = module_cls.spec
- else:
- sys.stderr.write(f"Warning: No spec found for module '{module_name}', skipping\n")
- return False
-
- # Generate markdown content
- lines = []
-
- # Header
- lines.append(f"# {module_name.title()} Variables")
- lines.append("")
- lines.append(f"**Module:** `{module_name}` ")
- lines.append(f"**Schema Version:** `{schema_version}` ")
- lines.append(f"**Description:** {module_cls.description}")
- lines.append("")
- lines.append("---")
- lines.append("")
- lines.append(
- "This page documents all available variables for the "
- + f"{module_name} module. Variables are organized into sections "
- + "that can be enabled/disabled based on your configuration needs."
- )
- lines.append("")
-
- # Table of contents
- lines.append("## Table of Contents")
- lines.append("")
- for section_key, section_data in spec.items():
- section_title = section_data.get("title", section_key)
- anchor = section_title.lower().replace(" ", "-").replace("/", "")
- lines.append(f"- [{section_title}](#{anchor})")
- lines.append("")
- lines.append("---")
- lines.append("")
-
- # Process each section
- for section_key, section_data in spec.items():
- section_title = section_data.get("title", section_key)
- section_desc = section_data.get("description", "")
- section_toggle = section_data.get("toggle", "")
- section_needs = section_data.get("needs", "")
- section_required = section_data.get("required", False)
- section_vars = section_data.get("vars", {})
-
- # Section header
- lines.append(f"## {section_title}")
- lines.append("")
-
- # Section metadata
- metadata = []
- if section_required:
- metadata.append("**Required:** Yes")
- if section_toggle:
- metadata.append(f"**Toggle Variable:** `{section_toggle}`")
- if section_needs:
- if isinstance(section_needs, list):
- needs_str = ", ".join(f"`{n}`" for n in section_needs)
- else:
- needs_str = f"`{section_needs}`"
- metadata.append(f"**Depends On:** {needs_str}")
-
- if metadata:
- lines.append(" \n".join(metadata))
- lines.append("")
-
- if section_desc:
- lines.append(section_desc)
- lines.append("")
-
- # Skip sections with no variables
- if not section_vars:
- lines.append("_No variables defined in this section._")
- lines.append("")
- continue
-
- # Variables table
- lines.append("| Variable | Type | Default | Description |")
- lines.append("|----------|------|---------|-------------|")
-
- for var_name, var_data in section_vars.items():
- var_type = var_data.get("type", "str")
- var_default = format_value(var_data.get("default"))
- var_description = var_data.get("description", "").replace("\n", " ")
-
- # Add extra metadata to description
- extra_parts = []
- if var_data.get("sensitive"):
- extra_parts.append("**Sensitive**")
- if var_data.get("autogenerated"):
- extra_parts.append("**Auto-generated**")
- if "options" in var_data:
- opts = ", ".join(f"`{o}`" for o in var_data["options"])
- extra_parts.append(f"**Options:** {opts}")
- if "needs" in var_data:
- extra_parts.append(f"**Needs:** `{var_data['needs']}`")
- if "extra" in var_data:
- extra_parts.append(var_data["extra"])
-
- if extra_parts:
- var_description += "
" + " • ".join(extra_parts)
-
- lines.append(f"| `{var_name}` | `{var_type}` | {var_default} | {var_description} |")
-
- lines.append("")
- lines.append("---")
- lines.append("")
-
- # Footer
- lines.append("## Notes")
- lines.append("")
- lines.append("- **Required sections** must be configured")
- lines.append("- **Toggle variables** enable/disable entire sections")
- lines.append("- **Dependencies** (`needs`) control when sections/variables are available")
- lines.append("- **Sensitive variables** are masked during prompts")
- lines.append("- **Auto-generated variables** are populated automatically if not provided")
- lines.append("")
- lines.append("---")
- lines.append("")
- lines.append(f"_Last updated: Schema version {schema_version}_")
-
- # Write to file
- output_file = output_dir / f"Variables-{module_name.title()}.md"
- output_file.write_text("\n".join(lines))
-
- sys.stdout.write(f"Generated: {output_file.name}\n")
- return True
-
-
-def generate_variables_index(modules: list[str], output_dir: Path):
- """Generate index page for all variable documentation."""
- lines = []
-
- lines.append("# Variables Documentation")
- lines.append("")
- lines.append("This section contains auto-generated documentation for all " + "available variables in each module.")
- lines.append("")
- lines.append("## Available Modules")
- lines.append("")
-
- for module_name in sorted(modules):
- lines.append(f"- [{module_name.title()}](Variables-{module_name.title()})")
-
- lines.append("")
- lines.append("---")
- lines.append("")
- lines.append("Each module page includes:")
- lines.append("")
- lines.append("- Schema version information")
- lines.append("- Complete list of sections and variables")
- lines.append("- Variable types, defaults, and descriptions")
- lines.append("- Section dependencies and toggle configurations")
- lines.append("")
- lines.append("---")
- lines.append("")
- lines.append("_This documentation is auto-generated from module schemas._")
-
- output_file = output_dir / "Variables.md"
- output_file.write_text("\n".join(lines))
-
- sys.stdout.write(f"Generated: {output_file.name}\n")
-
-
-# Minimum required arguments
-MIN_ARGS = 2
-
-
-def main():
- """Main entry point."""
- if len(sys.argv) < MIN_ARGS:
- sys.stderr.write("Usage: python3 scripts/generate_wiki_docs.py \n")
- sys.exit(1)
-
- output_dir = Path(sys.argv[1])
- output_dir.mkdir(parents=True, exist_ok=True)
-
- sys.stdout.write(f"Generating wiki documentation in: {output_dir}\n")
- sys.stdout.write("\n")
-
- # Get all registered modules
- module_classes = dict(registry.iter_module_classes())
- successful_modules = []
-
- for module_name in sorted(module_classes.keys()):
- if generate_module_docs(module_name, output_dir):
- successful_modules.append(module_name)
-
- sys.stdout.write("\n")
-
- # Generate index page
- if successful_modules:
- generate_variables_index(successful_modules, output_dir)
- sys.stdout.write("\n")
- sys.stdout.write(f"✓ Successfully generated documentation for {len(successful_modules)} module(s)\n")
- else:
- sys.stderr.write("Error: No documentation generated\n")
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/.github/workflows/docs-update-wiki.yaml b/.github/workflows/docs-update-wiki.yaml
deleted file mode 100644
index 6e45a61f..00000000
--- a/.github/workflows/docs-update-wiki.yaml
+++ /dev/null
@@ -1,98 +0,0 @@
----
-name: Docs - Update Wiki
-
-'on':
- push:
- branches:
- - main
- paths:
- - 'cli/core/schema/**/*.json' # JSON schema files
- - '.wiki/**' # Static wiki pages
- - '.github/scripts/generate_wiki_docs.py' # Wiki generation script
- - '.github/workflows/docs-update-wiki.yaml' # This workflow
- workflow_dispatch: # Allow manual trigger
-
-permissions:
- contents: write
-
-jobs:
- update-wiki:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v6
- with:
- fetch-depth: 1
-
- - name: Checkout wiki repository
- uses: actions/checkout@v6
- with:
- repository: ${{ github.repository }}.wiki
- path: wiki
- token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Set up Python
- uses: actions/setup-python@v6
- with:
- python-version: '3.14'
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e .
-
- - name: Generate variable documentation
- run: |
- python3 .github/scripts/generate_wiki_docs.py wiki/
-
- - name: Sync wiki pages from .wiki directory
- run: |
- # Copy all markdown files from .wiki/ to wiki/ (except Variables-*.md which are auto-generated)
- if [ -d ".wiki" ]; then
- echo "Syncing wiki pages from .wiki/ directory..."
- for file in .wiki/*.md; do
- filename=$(basename "$file")
- # Skip auto-generated variable documentation files
- if [[ ! "$filename" =~ ^Variables- ]]; then
- echo " Copying $filename"
- cp "$file" "wiki/$filename"
- fi
- done
- else
- echo "No .wiki directory found, skipping static wiki pages sync"
- fi
-
- - name: Check for changes
- id: changes
- working-directory: wiki
- run: |
- git add .
- if git diff --staged --quiet; then
- echo "has_changes=false" >> $GITHUB_OUTPUT
- echo "No changes detected in wiki documentation"
- else
- echo "has_changes=true" >> $GITHUB_OUTPUT
- echo "Changes detected in wiki documentation"
- fi
-
- - name: Commit and push changes
- if: steps.changes.outputs.has_changes == 'true'
- working-directory: wiki
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git commit -m "Auto-update wiki pages"
-
- # Pull with rebase to handle any remote changes, then push
- # GitHub wikis use master as default branch
- git pull --rebase origin master
- git push origin master
-
- - name: Summary
- run: |
- if [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then
- echo "Wiki variable documentation updated successfully"
- echo "View at: https://github.com/${{ github.repository }}/wiki"
- else
- echo "No changes to wiki documentation"
- fi
diff --git a/.wiki/Core-Concepts-Defaults.md b/.wiki/Core-Concepts-Defaults.md
index ff7b2ffc..d9acc55d 100644
--- a/.wiki/Core-Concepts-Defaults.md
+++ b/.wiki/Core-Concepts-Defaults.md
@@ -4,7 +4,7 @@ Save time by setting default values for variables you use frequently. This page
## What are Default Variables?
-**Default variables** are user-defined values that override module and template defaults. They allow you to:
+**Default variables** are user-defined values that override template manifest values for a module. They allow you to:
- Avoid repetitive typing during template generation
- Standardize values across multiple templates
- Customize your environment once, use everywhere
@@ -13,13 +13,13 @@ Save time by setting default values for variables you use frequently. This page
Variables are resolved in this order (lowest to highest priority):
-1. Module runtime defaults
-2. Template defaults from `template.json`
-3. **User config** (your saved defaults) ← This page
-4. `--var-file`
-5. CLI arguments (`--var`)
+1. Template manifest values from `template.json`
+2. **User config** (your saved defaults) ← This page
+3. `--var-file`
+4. CLI arguments (`--var`)
+5. Interactive prompt answers
-Your saved defaults override module and template defaults, but they can still be overridden at generation time.
+Your saved defaults override template manifest values, but they can still be overridden at generation time.
## Managing Defaults
@@ -104,7 +104,7 @@ Response:
Removed default: container_timezone
```
-The variable will now use module/template defaults again.
+The variable will now use template manifest values again.
### Clear All Defaults
@@ -123,11 +123,13 @@ Cleared all defaults for compose
## Configuration Storage
-Defaults are stored in:
+Defaults are usually stored in:
```
~/.config/boilerplates/config.yaml
```
+If a local `./config.yaml` exists in your current working directory, the CLI uses that file instead.
+
Example content:
```yaml
libraries:
@@ -153,13 +155,15 @@ defaults:
You can manually edit the config file:
```bash
-# Edit configuration
+# Edit the global configuration example
nano ~/.config/boilerplates/config.yaml
# Verify defaults
boilerplates compose defaults list
```
+If you are using a local `./config.yaml`, edit that file instead.
+
## Common Use Cases
### Timezone Configuration
@@ -251,6 +255,8 @@ Defaults don't transfer between modules—they're module-specific.
## Backup and Restore
+The examples below use the global config path. If the CLI is using a local `./config.yaml`, substitute that path instead.
+
### Backup Configuration
Save your configuration:
@@ -332,7 +338,7 @@ cat ~/.config/boilerplates/config.yaml
If config file is corrupted:
```bash
-# Validate YAML syntax
+# Validate global config YAML syntax
python3 -c "import yaml; yaml.safe_load(open('~/.config/boilerplates/config.yaml'))"
# Or remove and recreate
@@ -346,15 +352,16 @@ If wrong values appear:
```bash
# Check precedence
-# 1. Module spec
-# 2. Template spec
-# 3. User defaults ← Check here
+# 1. Template manifest values
+# 2. User defaults ← Check here
+# 3. --var-file
# 4. CLI --var
+# 5. Interactive answers
# Verify your defaults
boilerplates compose defaults list
-# Check template spec
+# Check the loaded template state
boilerplates compose show
```
diff --git a/.wiki/Core-Concepts-Libraries.md b/.wiki/Core-Concepts-Libraries.md
index 2c1bf118..67250f7e 100644
--- a/.wiki/Core-Concepts-Libraries.md
+++ b/.wiki/Core-Concepts-Libraries.md
@@ -40,14 +40,28 @@ libraries:
## Local Storage
-Git libraries are stored under:
+Git libraries are stored under the active config directory's `libraries/` folder.
+
+Global-config default:
```text
~/.config/boilerplates/libraries/
```
+If the CLI is using a local `./config.yaml`, the library checkout location becomes `./libraries/`.
+
The configured `directory` is applied inside that checkout. For the official library the directory is `.`.
+## Configuration Location
+
+By default, Boilerplates uses:
+
+```text
+~/.config/boilerplates/config.yaml
+```
+
+If a local `./config.yaml` exists in your current working directory, the CLI uses that file instead.
+
## Discovery Rules
Boilerplates discovers templates by module directory, for example:
@@ -79,11 +93,21 @@ boilerplates repo update
Add a custom Git library:
```bash
-boilerplates repo add my-templates https://github.com/user/templates \
+boilerplates repo add my-templates \
+ --library-type git \
+ --url https://github.com/user/templates \
--directory . \
--branch main
```
+Add a static library:
+
+```bash
+boilerplates repo add local \
+ --library-type static \
+ --path ~/my-templates
+```
+
Remove a library:
```bash
@@ -127,12 +151,6 @@ Templates with `metadata.draft: true` are excluded from normal listings and look
## Config File
-Library configuration lives in:
-
-```text
-~/.config/boilerplates/config.yaml
-```
-
Example:
```yaml
diff --git a/.wiki/Core-Concepts-Templates.md b/.wiki/Core-Concepts-Templates.md
index f6661401..48133e16 100644
--- a/.wiki/Core-Concepts-Templates.md
+++ b/.wiki/Core-Concepts-Templates.md
@@ -1,10 +1,8 @@
# Templates
-Templates are the core unit of Boilerplates. A template is a directory with a `template.json` manifest and a `files/` directory containing the files to render.
+Templates are the core unit of Boilerplates. A supported template is a directory with a `template.json` manifest and a `files/` directory containing every renderable output file.
-## Template Structure
-
-Every supported template looks like this:
+## Required Layout
```text
my-template/
@@ -16,11 +14,13 @@ my-template/
└── app.yaml
```
-Only `template.json` is a supported manifest format. Legacy `template.yaml` and `template.yml` manifests are not supported.
+Rules:
+- `template.json` is the only supported manifest format
+- all rendered content must live under `files/`
+- legacy `template.yaml`, `template.yml`, and top-level `.j2` layouts are incompatible with the current runtime
-## Manifest Shape
+## Top-Level Manifest Shape
-Top-level fields:
- `slug`
- `kind`
- `metadata`
@@ -35,6 +35,8 @@ Example:
"metadata": {
"name": "My Template",
"description": "Short human description",
+ "author": "Your Name",
+ "date": "2026-04-22",
"tags": ["infra", "dev"],
"icon": {
"provider": "mdi",
@@ -48,7 +50,7 @@ Example:
"source_dep_version": "1.1.0",
"source_dep_digest": "sha256:abc123def456",
"upstream_ref": "release-2026-04-22",
- "notes": "Tracks upstream container release used by this template snapshot"
+ "notes": "Tracks the tested upstream dependency snapshot"
}
},
"variables": [
@@ -68,24 +70,38 @@ Example:
}
```
+## `slug` and Template IDs
+
+`slug` is the canonical template ID exposed by the CLI.
+
+Behavior:
+- if `slug` is present, it wins over the directory name
+- if `slug` ends with `-`, that suffix is normalized away for CLI use
+- if `slug` is missing, the directory name is used
+
+Example:
+- directory: `portainer/`
+- `kind`: `compose`
+- `slug`: `portainer-compose`
+- CLI ID: `portainer`
+
## Metadata
Common metadata fields:
- `name`
- `description`
+- `author`
+- `date`
- `tags`
- `icon`
- `draft`
-- `author`
-- `date`
-- `guide`
- `version`
### Version Metadata
-`metadata.version` is optional. If present, it must be an object.
+`metadata.version` is optional, but when present it must be an object.
-Supported version fields:
+Supported fields:
- `name`
- `source_dep_name`
- `source_dep_version`
@@ -93,57 +109,34 @@ Supported version fields:
- `upstream_ref`
- `notes`
-Important rules:
-- the whole `version` object may be omitted
-- any individual `version` field may be omitted
-- the CLI uses `metadata.version.name` as the visible version label in list/show output
+Important behavior:
+- `metadata.version.name` is the user-facing version label shown in list/show output
+- the rest of the version object is for upstream dependency tracking
+- the full object may be omitted
+- individual fields inside the object may also be omitted
-This means these are all valid:
+## Variable Declarations Are Mandatory
-```json
-{
- "metadata": {
- "name": "My Template"
- }
-}
-```
+Any variable used in files under `files/` must be declared in `template.json`.
-```json
-{
- "metadata": {
- "name": "My Template",
- "version": {
- "name": "v1.1"
- }
- }
-}
-```
+If a file references an undeclared variable, the template fails validation and load/render operations surface a template error.
-```json
-{
- "metadata": {
- "name": "My Template",
- "version": {
- "source_dep_name": "ghcr.io/example/my-image",
- "source_dep_version": "1.1.0"
- }
- }
-}
-```
+Use the [Variables](Core-Concepts-Variables) page for the manifest structure.
## Files and Rendering
-All files inside `files/` are part of the template output.
+Everything under `files/` is part of the output tree.
-Rendering rules:
-- Boilerplates renders every file under `files/`
-- files without template expressions pass through unchanged
-- output paths currently match the relative file paths under `files/`
-- template discovery ignores anything without `template.json`
+Rendering behavior:
+- Boilerplates walks every file under `files/`
+- files are rendered with the custom delimiter set
+- files without template expressions still pass through the render pipeline
+- output paths currently mirror the relative paths inside `files/`
+- rendered output is sanitized to normalize blank lines and trailing whitespace
## Delimiters
-Templates use custom delimiters, not default Jinja syntax:
+Templates use custom delimiters rather than default Jinja syntax:
- variables: `<< value >>`
- blocks: `<% if condition %>`
@@ -165,17 +158,21 @@ Legacy `{{ }}`, `{% %}`, and `{# #}` delimiters are rejected.
## Includes and Imports
-Includes and imports are resolved relative to the template's `files/` directory.
-
-Example:
+Includes and imports resolve relative to the template's `files/` directory.
```jinja
<% include 'partials/header.yaml' %>
```
-## Template Discovery
+## Discovery Rules
-Templates are discovered from configured libraries. A directory is considered a template only when it contains `template.json`.
+Templates are discovered from configured libraries.
+
+A directory is considered a valid template only when:
+- `template.json` exists
+- `files/` exists
+
+In practice, Boilerplates discovers templates by module directory path such as `compose//` or `terraform//`.
Useful commands:
@@ -186,11 +183,11 @@ boilerplates compose show nginx
```
Draft templates:
-- set `metadata.draft` to `true` to hide a template from normal discovery
+- set `metadata.draft` to `true` to hide the template from normal discovery
-## Generation
+## Generation Workflow
-Typical generation flow:
+Typical workflow:
```bash
boilerplates compose show nginx
@@ -204,6 +201,7 @@ Useful flags:
- `--var` for direct CLI overrides
- `--no-interactive` for non-interactive generation
- `--dry-run` to preview generation without writing files
+- `--show-files` to print rendered files during dry-run
## Validation
@@ -219,11 +217,19 @@ Validate all templates in a module:
boilerplates compose validate
```
+Validation covers:
+- manifest structure
+- variable declaration coverage
+- delimiter compatibility
+- renderability
+- optional semantic validators for module-specific output
+
## Best Practices
- keep manifests in `template.json`
-- keep all generated content under `files/`
+- keep every generated file under `files/`
+- declare every variable that appears in rendered content
- use the custom delimiter set consistently
-- hardcode tested upstream application versions in rendered files unless a variable is truly required
+- hardcode tested upstream application versions in rendered files unless a variable is actually needed
- use `metadata.version.name` for the user-facing label
-- use the remaining `metadata.version` fields to track upstream dependency context when useful
+- use the remaining `metadata.version` fields for upstream snapshot context when useful
diff --git a/.wiki/Core-Concepts-Variables.md b/.wiki/Core-Concepts-Variables.md
index 12742fe9..0efc9207 100644
--- a/.wiki/Core-Concepts-Variables.md
+++ b/.wiki/Core-Concepts-Variables.md
@@ -1,10 +1,10 @@
# Variables
-Variables define the configurable inputs available to a template. In the current runtime they live in `template.json` under `variables`, grouped into sections with `items`.
+Variables are defined per template in `template.json` under the top-level `variables` array. There is no separate schema reference layer anymore, so the manifest is the source of truth.
-## Variable Layout
+## Runtime Shape
-Example:
+Each entry in `variables` is a group. Each group contains `items`.
```json
{
@@ -24,7 +24,7 @@ Example:
"name": "environment",
"type": "enum",
"title": "Environment",
- "default": "prod",
+ "value": "stage",
"config": {
"options": ["prod", "stage", "dev"]
}
@@ -35,15 +35,16 @@ Example:
}
```
-Each group requires:
+Group fields:
- `name`
- `title`
+- `description`
+- `toggle`
+- `needs`
- `items`
-Each item requires:
+Item fields:
- `name`
-
-Common item fields:
- `type`
- `title`
- `description`
@@ -54,11 +55,13 @@ Common item fields:
- `extra`
- `config`
-`title` is used as the prompt label. If `description` is omitted, the runtime falls back to `title`.
+Runtime mapping notes:
+- `title` is used as the prompt label
+- `description` is used as the display/help text
+- if `description` is omitted, the runtime falls back to `title`
-## Variable Types
+## Supported Types
-Supported types:
- `str`
- `int`
- `float`
@@ -69,25 +72,37 @@ Supported types:
- `hostname`
- `secret`
-### Secret Variables
+Type notes:
+- `bool` defaults to `false` when omitted
+- other types default to empty unless a `default` or `value` is provided
+- `secret` values are masked in prompts and display output
-Use `type: "secret"` for values that should be masked in prompts and output.
+## `default` vs `value`
+
+Use `default` for the normal manifest default.
+
+Use `value` when the template should pin the runtime value directly. If both are present, `value` wins.
Example:
```json
{
- "name": "api_token",
- "type": "secret",
- "title": "API token"
+ "name": "environment",
+ "type": "enum",
+ "title": "Environment",
+ "default": "prod",
+ "value": "stage",
+ "config": {
+ "options": ["prod", "stage", "dev"]
+ }
}
```
## Config Object
-Extra behavior is defined under `config`.
+Additional behavior is configured through `config`.
-Supported config fields:
+Supported fields:
- `placeholder`
- `textarea`
- `unit`
@@ -143,7 +158,7 @@ Supported config fields:
}
```
-### Autogenerated Secret Values
+### Autogenerated Secrets
`config.autogenerated` is supported only for `secret` variables.
@@ -180,22 +195,12 @@ Base64 example:
}
```
-## Default Values and Overrides
+Rules:
+- autogenerated secrets cannot define `default`
+- base64 generation uses `bytes`, not `length`
+- `autogenerated` on non-secret variables is rejected
-Value precedence is:
-
-1. module runtime defaults
-2. template defaults from `template.json`
-3. saved user defaults in `config.yaml`
-4. `--var-file`
-5. `--var`
-
-Useful notes:
-- `default` defines the standard value
-- `value` lets a template pin a value more directly in the manifest
-- autogenerated secrets cannot also define a default
-
-## Dependencies
+## Dependencies with `needs`
Both groups and items support `needs`.
@@ -207,14 +212,18 @@ Examples:
Semantics:
- semicolon means AND
-- comma means OR within the same comparison
-- `!=` means the current value must not match
+- comma means OR within a single comparison
+- `=` means the value must match
+- `!=` means the value must not match
-## Toggles
+Behavior notes:
+- group `needs` controls whether the group is considered available
+- item `needs` controls whether the item is shown and validated
+- disabled boolean items are reset to `false` unless they were explicitly set via CLI
-Groups may define `toggle`, but the toggle variable must exist as a boolean item in that same group.
+## Group Toggles
-Example:
+Groups may define `toggle`, but the referenced toggle variable must exist as a boolean item in that same group.
```json
{
@@ -237,14 +246,25 @@ Example:
}
```
-If a referenced toggle variable is not present, the runtime drops the toggle behavior.
+If the toggle variable is missing, the runtime ignores the toggle.
+
+## Override Order
+
+For a loaded template, values are applied in this order:
+
+1. `default` / `value` from `template.json`
+2. saved defaults from the active config file
+3. `--var-file`
+4. `--var`
+5. interactive prompt answers
## Inspecting Variables
-The fastest way to see the actual variables for a template is still:
+Use `show` against the template you care about:
```bash
boilerplates compose show nginx
+boilerplates terraform show cloudflare-dns-record
```
-That reflects the template as the runtime sees it, including defaults, dependencies, and optional sections.
+That view reflects the actual runtime state for that template, including defaults, dependencies, toggle state, and file structure.
diff --git a/.wiki/Home.md b/.wiki/Home.md
index fd30492f..e05d720f 100644
--- a/.wiki/Home.md
+++ b/.wiki/Home.md
@@ -20,10 +20,6 @@ The current runtime supports:
- [Libraries](Core-Concepts-Libraries) - official and custom libraries, discovery, priority, and config
- [Defaults](Core-Concepts-Defaults) - saved default values and precedence
-## Variable Discovery
-
-- [Variables](Variables) - how to inspect the variables a template actually exposes
-
## Additional Docs
- [Repository README](https://github.com/ChristianLempa/boilerplates/blob/main/README.md)
diff --git a/.wiki/Installation.md b/.wiki/Installation.md
index 2b1d40b9..a295ab43 100644
--- a/.wiki/Installation.md
+++ b/.wiki/Installation.md
@@ -283,11 +283,16 @@ Sync the default template library:
boilerplates repo update
```
-This downloads all available templates to:
+This syncs the configured Git library checkouts under the active config directory's `libraries/` folder.
+
+Global-config default:
+
```
~/.config/boilerplates/libraries/
```
+If the CLI is using a local `./config.yaml`, the sync location becomes `./libraries/`.
+
### Shell Completion (Optional)
Enable tab completion for your shell:
diff --git a/.wiki/Variables.md b/.wiki/Variables.md
deleted file mode 100644
index 4f3262b8..00000000
--- a/.wiki/Variables.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Variables Reference
-
-Boilerplates no longer publishes static schema-version variable reference pages.
-
-The current runtime is template-driven:
-- modules provide baseline variable behavior
-- each template can override defaults and add template-specific variables
-- the exact variable set depends on the template you are inspecting
-
-## How to Inspect Variables
-
-Use the CLI against the template itself:
-
-```bash
-boilerplates compose show nginx
-boilerplates terraform show cloudflare-dns-record
-boilerplates ansible show ubuntu-vm-core
-```
-
-This shows:
-- template metadata
-- rendered version label from `metadata.version.name` when present
-- the template file tree
-- the actual variable groups and items exposed by that template
-
-## Recommended Workflow
-
-1. List templates for a module.
-2. Show the template you want to use.
-3. Review defaults, dependencies, and optional sections.
-4. Generate with `--output`, `--var-file`, and `--var` as needed.
-
-## Why the Old Pages Were Removed
-
-The older wiki pages were generated from schema-version snapshots and no longer matched the current `template.json` runtime. They were removed to avoid documenting variables that may not exist, may have changed shape, or may be template-specific.
diff --git a/.wiki/_Sidebar.md b/.wiki/_Sidebar.md
index c53fbd0e..112f9ece 100644
--- a/.wiki/_Sidebar.md
+++ b/.wiki/_Sidebar.md
@@ -11,6 +11,3 @@
- [Variables](Core-Concepts-Variables)
- [Libraries](Core-Concepts-Libraries)
- [Defaults](Core-Concepts-Defaults)
-
-### Reference
-- [Variables](Variables)
diff --git a/README.md b/README.md
index ff7732db..3eede875 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,7 @@ Create reusable templates and turn them into configurable workloads for homelabs
Create reusable templates for infrastructure expertise like Docker, Kubernetes, Terraform, Ansible, Python, and more. Use the built-in *Jinja2-like* templating syntax with `<< >>` variables, `<% %>` blocks, and `<# #>` comments to keep configuration modular and conditional. Sync with Git in both directions or manage everything locally. Render templates, configure variables through a guided wizard, and wire up secrets. Copy them to remote servers and environments or any local directory.
-*Don't want to start from scratch?*
-> Explore 100+ template presets for homelabs and self-hosted infrastructure: https://github.com/ChristianLempa/boilerplates-library
+✨ Explore 100+ template presets for homelabs and self-hosted infrastructure: https://github.com/ChristianLempa/boilerplates-library
## Boilerplates CLI
@@ -15,10 +14,9 @@ The Boilerplates CLI is the main interface for working with template libraries l
It combines template-defined variables and defaults, guided interactive prompts, CLI variable overrides, and git-backed template libraries into one workflow. In practice, that means you can keep reusable boilerplates in a repository and turn them into concrete, environment-specific configurations with a single command.
-> **WARNING**
-> Boilerplates `0.2.0` introduced the new template format. Legacy `template.yaml` / `template.yml` manifests and `.j2` template files are no longer supported.
->
-> New templates must use `template.json`, keep renderable content under `files/`, and use the custom *Jinja2*-like delimiters `<< >>`, `<% %>`, and `<# #>` instead of default *Jinja2* syntax.
+⚠️ Boilerplates `0.2.0` introduced the new template format. Legacy `template.yaml` / `template.yml` manifests and `.j2` template files are no longer supported.
+
+ℹ️ New templates must use `template.json`, keep renderable content under `files/`, and use the custom *Jinja2*-like delimiters `<< >>`, `<% %>`, and `<# #>` instead of default *Jinja2* syntax.
### Installation
diff --git a/cli/core/template/template.py b/cli/core/template/template.py
index f79a20aa..0b2df883 100644
--- a/cli/core/template/template.py
+++ b/cli/core/template/template.py
@@ -203,7 +203,6 @@ class TemplateMetadata:
library: str = "unknown"
library_type: str = "git"
draft: bool = False
- guide: str = ""
icon: dict[str, Any] = field(default_factory=dict)
def __init__(
@@ -226,7 +225,6 @@ class TemplateMetadata:
self.library = library_name or "unknown"
self.library_type = library_type
self.draft = bool(metadata.get("draft", False))
- self.guide = str(metadata.get("guide", "")).rstrip("\n")
self.icon = metadata.get("icon", {}) if isinstance(metadata.get("icon"), dict) else {}