Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(ls -1 /c/scripts/Plaster-1/Plaster/Private/*.ps1)",
"Bash(pwsh:*)",
"WebFetch(domain:pester.dev)",
"Bash(xargs head:*)",
"Bash(hugo server:*)",
"Bash(hugo --printPathWarnings)"
]
}
}
11 changes: 8 additions & 3 deletions Plaster/Plaster.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
CompanyName = 'PowerShell.org'

# Copyright statement for this module
Copyright = '(c) PowerShell.org 2016-2025. All rights reserved.'
Copyright = '(c) PowerShell.org 2016-2026. All rights reserved.'

# Description of the functionality provided by this module
Description = 'Plaster is a template-based file and project generator written in PowerShell. Create consistent PowerShell projects with customizable templates supporting both XML and JSON formats.'
Expand Down Expand Up @@ -73,10 +73,15 @@
# Functions to export from this module - explicitly list each function that should be
# exported. This improves performance of PowerShell when discovering the commands in
# module.
FunctionsToExport = '*'
FunctionsToExport = @(
'Get-PlasterTemplate',
'Invoke-Plaster',
'New-PlasterManifest',
'Test-PlasterManifest'
)

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'
CmdletsToExport = @()

# Variables to export from this module
VariablesToExport = @()
Expand Down
18 changes: 17 additions & 1 deletion Plaster/Plaster.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ data LocalizedData {
ManifestNotValid_F1=The Plaster manifest '{0}' is not valid.
ManifestNotValidVerbose_F1=The Plaster manifest '{0}' is not valid. Specify -Verbose to see the specific schema errors.
ManifestNotWellFormedXml_F2=The Plaster manifest '{0}' is not a well-formed XML file. {1}
ManifestWrongFilename_F1=The Plaster manifest filename '{0}' is not valid. The value of the Path argument must refer to a file named 'plasterManifest.xml' or 'plasterManifest_<culture>.xml'. Change the Plaster manifest filename and then try again.
ManifestWrongFilename_F1=The Plaster manifest filename '{0}' is not valid. The value of the Path argument must refer to a file named 'plasterManifest.xml', 'plasterManifest.json', or a culture-specific variant (e.g., 'plasterManifest_en-US.xml'). Change the Plaster manifest filename and then try again.
MissingParameterPrompt_F1=<Missing prompt value for parameter '{0}'>
NewModManifest_CreatingDir_F1=Creating destination directory for module manifest: {0}
OpConflict=Conflict
Expand Down Expand Up @@ -139,6 +139,22 @@ if (-not $script:XmlSchemaValidationSupported) {
# Module logging configuration
$script:LogLevel = if ($env:PLASTER_LOG_LEVEL) { $env:PLASTER_LOG_LEVEL } else { 'Information' }

# Dot-source functions when running from source (not compiled build)
# The build system (PowerShellBuild) compiles all functions into this PSM1.
# When running from source, we need to dot-source them from Private/ and Public/.
$privatePath = Join-Path $PSScriptRoot 'Private'
$publicPath = Join-Path $PSScriptRoot 'Public'
if (Test-Path $privatePath) {
foreach ($file in (Get-ChildItem -Path $privatePath -Filter '*.ps1' -Recurse)) {
. $file.FullName
}
}
if (Test-Path $publicPath) {
foreach ($file in (Get-ChildItem -Path $publicPath -Filter '*.ps1' -Recurse)) {
. $file.FullName
}
}

# Global variables and constants for Plaster 2.0

# Enhanced $TargetNamespace definition with proper scoping
Expand Down
4 changes: 2 additions & 2 deletions Plaster/Private/Get-PlasterManifestPathForCulture.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function Get-PlasterManifestPathForCulture {
return $plasterManifestPath
}

# If no manifest is found, return $null.
# TODO: Should we throw an error instead?
# If no manifest is found, return $null. Callers (Invoke-Plaster, etc.)
# handle the missing manifest case and may fall back to JSON format.
return $null
}
12 changes: 11 additions & 1 deletion Plaster/Private/New-TemplateObjectFromManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ function New-TemplateObjectFromManifest {
)

try{
$manifestXml = Test-PlasterManifest -Path $ManifestPath
$manifestResult = Test-PlasterManifest -Path $ManifestPath
# Test-PlasterManifest may return an array if extra output leaks through;
# extract the XmlDocument from the result.
$manifestXml = if ($manifestResult -is [System.Xml.XmlDocument]) {
$manifestResult
} else {
$manifestResult | Where-Object { $_ -is [System.Xml.XmlDocument] } | Select-Object -First 1
}
if ($null -eq $manifestXml) {
throw "Failed to load manifest from '$ManifestPath'"
}
$metadata = $manifestXml["plasterManifest"]["metadata"]

$manifestObj = [PSCustomObject]@{
Expand Down
3 changes: 0 additions & 3 deletions Plaster/Private/Test-JsonManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,11 @@ function Test-JsonManifest {
throw "Invalid template name: $($metadata.name). Must start with letter and contain only letters, numbers, underscore, or hyphen"
}

# Parameters validation
# Parameters validation
if ($jsonObject.PSObject.Properties['parameters'] -and $jsonObject.parameters -and $jsonObject.parameters.Count -gt 0) {
Test-JsonManifestParameters -Parameters $jsonObject.parameters
}

# Content validation
# Content validation
# Content validation
if ($jsonObject.content -and $jsonObject.content.Count -gt 0) {
Test-JsonManifestContent -Content $jsonObject.content
Expand Down
10 changes: 0 additions & 10 deletions Plaster/Private/Write-PlasterLog.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,4 @@ function Write-PlasterLog {
Write-Debug $logMessage
}
}

# Also write to host for immediate feedback during interactive sessions
if ($Level -in @('Error', 'Warning') -and $Host.Name -ne 'ServerRemoteHost') {
$color = switch ($Level) {
'Error' { 'Red' }
'Warning' { 'Yellow' }
default { 'White' }
}
Write-Host $logMessage -ForegroundColor $color
}
}
11 changes: 9 additions & 2 deletions Plaster/Public/Get-PlasterTemplate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,16 @@ function Get-PlasterTemplate {
Get-ManifestsUnderPath @getManifestsUnderPathSplat
}
} else {
# Return all templates included with Plaster
# Return all templates included with Plaster.
# When running from source this function is dot-sourced from Public/, so
# $PSScriptRoot points at Public/ and Templates/ lives one level up. The
# compiled build flattens everything to the module root, where the first path is correct.
$templatesRoot = Join-Path $PSScriptRoot 'Templates'
if (-not (Test-Path -LiteralPath $templatesRoot)) {
$templatesRoot = Join-Path (Split-Path $PSScriptRoot -Parent) 'Templates'
}
$getManifestsUnderPathSplat = @{
RootPath = "$PSScriptRoot\Templates"
RootPath = $templatesRoot
Recurse = $true
Name = $Name
Tag = $Tag
Expand Down
7 changes: 7 additions & 0 deletions Plaster/Public/Test-PlasterManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
begin {
$schemaPath = [System.IO.Path]::Combine($PSScriptRoot, "Schema", "PlasterManifest-v1.xsd")

# When running from source, this function is dot-sourced from Public/, so
# $PSScriptRoot points at Public/ and Schema/ lives one level up. The compiled
# build flattens everything to the module root, where the first path is correct.
if (-not (Test-Path -LiteralPath $schemaPath)) {
$schemaPath = [System.IO.Path]::Combine((Split-Path $PSScriptRoot -Parent), "Schema", "PlasterManifest-v1.xsd")
}

# Schema validation is not available on .NET Core - at the moment.
if ('System.Xml.Schema.XmlSchemaSet' -as [type]) {
$xmlSchemaSet = New-Object System.Xml.Schema.XmlSchemaSet
Expand Down Expand Up @@ -151,7 +158,7 @@
if ($xmlReader) { $xmlReader.Dispose() }
}

# Validate default values for choice/multichoice parameters containing 1 or more ints

Check warning on line 161 in Plaster/Public/Test-PlasterManifest.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (ints) Suggestions: (inst, nits, Inst, in's, incs)
$xpath = "//tns:parameter[@type='choice'] | //tns:parameter[@type='multichoice']"
$choiceParameters = Select-Xml -Xml $manifest -XPath $xpath -Namespace @{tns = $TargetNamespace }
foreach ($choiceParameterXmlInfo in $choiceParameters) {
Expand Down
2 changes: 1 addition & 1 deletion Plaster/en-US/Plaster.Resources.psd1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Localized PlasterResources.psd1

ConvertFrom-StringData @'
###PSLOC

Check warning on line 4 in Plaster/en-US/Plaster.Resources.psd1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (PSLOC) Suggestions: (solc, plod, plop, plot, plow)
DestPath_F1=Destination path: {0}
ErrorFailedToLoadStoreFile_F1=Failed to load the default value store file: '{0}'.
ErrorProcessingDynamicParams_F1=Failed to create dynamic parameters from the template's manifest file. Template-based dynamic parameters will not be available until the error is corrected. The error was: {0}
Expand Down Expand Up @@ -36,7 +36,7 @@
ManifestNotValid_F1=The Plaster manifest '{0}' is not valid.
ManifestNotValidVerbose_F1=The Plaster manifest '{0}' is not valid. Specify -Verbose to see the specific schema errors.
ManifestNotWellFormedXml_F2=The Plaster manifest '{0}' is not a well-formed XML file. {1}
ManifestWrongFilename_F1=The Plaster manifest filename '{0}' is not valid. The value of the Path argument must refer to a file named 'plasterManifest.xml' or 'plasterManifest_<culture>.xml'. Change the Plaster manifest filename and then try again.
ManifestWrongFilename_F1=The Plaster manifest filename '{0}' is not valid. The value of the Path argument must refer to a file named 'plasterManifest.xml', 'plasterManifest.json', or a culture-specific variant (e.g., 'plasterManifest_en-US.xml'). Change the Plaster manifest filename and then try again.
MissingParameterPrompt_F1=<Missing prompt value for parameter '{0}'>
NewModManifest_CreatingDir_F1=Creating destination directory for module manifest: {0}
OpConflict=Conflict
Expand Down Expand Up @@ -64,5 +64,5 @@
UnrecognizedParametersElement_F1=Unrecognized manifest parameters child element: {0}.
UnrecognizedParameterType_F2=Unrecognized parameter type '{0}' on parameter name '{1}'.
UnrecognizedContentElement_F1=Unrecognized manifest content child element: {0}.
###PSLOC

Check warning on line 67 in Plaster/en-US/Plaster.Resources.psd1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (PSLOC) Suggestions: (solc, plod, plop, plot, plow)
'@
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/Plaster.svg)](https://www.powershellgallery.com/packages/Plaster)
[![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/Plaster.svg)](https://www.powershellgallery.com/packages/Plaster)
[![Build Status](https://github.com/PowerShell/Plaster/workflows/CI/badge.svg)](https://github.com/PowerShell/Plaster/actions)
[![Build Status](https://github.com/PowerShellOrg/Plaster/actions/workflows/PesterReports.yml/badge.svg)](https://github.com/PowerShellOrg/Plaster/actions)

Plaster is a template-based file and project generator written in PowerShell. Its purpose is to streamline the creation of PowerShell module projects, Pester tests, DSC configurations, and more. File generation is performed using crafted templates which allow the user to fill in details and choose from options to get their desired output.

Expand Down Expand Up @@ -43,7 +43,7 @@ Install-Module -Name Plaster -Scope CurrentUser

### From Source
```powershell
git clone https://github.com/PowerShell/Plaster.git
git clone https://github.com/PowerShellOrg/Plaster.git
Import-Module .\Plaster\Plaster\Plaster.psd1
```

Expand Down Expand Up @@ -87,7 +87,7 @@ code plasterManifest.xml
### JSON Manifest Example
```json
{
"$schema": "https://raw.githubusercontent.com/PowerShell/Plaster/v2/schema/plaster-manifest-v2.json",
"$schema": "https://raw.githubusercontent.com/PowerShellOrg/Plaster/v2/schema/plaster-manifest-v2.json",
"schemaVersion": "2.0",
"metadata": {
"name": "MyTemplate",
Expand Down Expand Up @@ -259,7 +259,7 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid

### Development Setup
```powershell
git clone https://github.com/PowerShell/Plaster.git
git clone https://github.com/PowerShellOrg/Plaster.git
cd Plaster
Import-Module .\Plaster\Plaster.psd1
Invoke-Pester # Run tests
Expand All @@ -278,4 +278,4 @@ This project is licensed under the MIT License - see [LICENSE](LICENSE) for deta

---

**Plaster 2.0** - Modern template scaffolding for PowerShell with JSON support, better tooling, and enhanced developer experience. 🚀
**Plaster 2.0** - Modern template scaffolding for PowerShell with JSON support, cross-platform compatibility, and enhanced developer experience.
33 changes: 29 additions & 4 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
## What is New in Plaster 1.0.0
December 16, 2016
## What is New in Plaster 2.0.0
April 2026

- First official release shipped to the PowerShell Gallery!
### Breaking Changes
- Minimum PowerShell version updated to 5.1 (was 3.0)
- Test framework updated to Pester 5.x
- Default file encoding changed to UTF8-NoBOM

### New Features
- **JSON Manifest Support**: Create templates using JSON (`plasterManifest.json`) with full JSON Schema validation and VS Code IntelliSense
- **Cross-Platform**: Full support for Windows, Linux, and macOS on PowerShell 7.x
- **Simplified Variables**: JSON manifests use `${ParameterName}` instead of `${PLASTER_PARAM_ParameterName}`
- **Native Arrays**: Multichoice defaults use JSON arrays `[0, 1, 2]` instead of comma-separated strings
- **Format Auto-Detection**: Plaster automatically detects and processes both XML and JSON manifests
- **Enhanced Logging**: Configurable logging via `$env:PLASTER_LOG_LEVEL`

### Improvements
- Better error messages with actionable guidance
- Improved constrained runspace compatibility with PowerShell 7.x
- Platform-specific parameter store paths (XDG on Linux, standard paths on macOS/Windows)
- Optimized module loading and template processing
- Comprehensive Pester 5.x test suite

### Bug Fixes
- Fixed .NET Core XML schema validation issues
- Resolved path handling on non-Windows platforms
- Fixed constrained runspace compatibility with PowerShell 7.x
- Corrected parameter default value storage on non-Windows platforms
- Fixed variable substitution edge cases

### Feedback
Please send your feedback to http://github.com/PowerShell/Plaster/issues
Please send your feedback to https://github.com/PowerShellOrg/Plaster/issues
2 changes: 2 additions & 0 deletions demos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated by Run-Demos.ps1 / Demo4-Discovery-Authoring.ps1
output/
86 changes: 86 additions & 0 deletions demos/Demo4-Discovery-Authoring.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<#
.SYNOPSIS
Demo 4 - Template discovery and live manifest authoring.
.DESCRIPTION
Shows the three "meta" cmdlets that surround Invoke-Plaster:
* Get-PlasterTemplate - discover templates (bundled + your own folders)
* New-PlasterManifest - author a brand-new JSON manifest from a folder of files
* Test-PlasterManifest - validate a manifest before you ship it

Ends by scaffolding from the template it just authored, proving the round-trip.
#>
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$templates = Join-Path $PSScriptRoot 'templates'
$outputDir = Join-Path $PSScriptRoot 'output'

Import-Module (Join-Path $root 'Plaster\Plaster.psd1') -Force

function Write-Header($Text) {
Write-Host ''
Write-Host ('=' * 70) -ForegroundColor DarkBlue
Write-Host " $Text" -ForegroundColor Yellow
Write-Host ('=' * 70) -ForegroundColor DarkBlue
}

# ----------------------------------------------------------------------------
Write-Header 'DEMO 4a - Discover templates with Get-PlasterTemplate'

Write-Host "`n Templates that ship with Plaster:" -ForegroundColor Green
Get-PlasterTemplate | Format-Table Name, Version, Title -AutoSize

Write-Host " Your own templates (any folder, -Recurse):" -ForegroundColor Green
Get-PlasterTemplate -Path $templates -Recurse | Format-Table Name, Version, Title, Tags -AutoSize

# ----------------------------------------------------------------------------
Write-Header 'DEMO 4b - Author a new manifest with New-PlasterManifest'

# Start from a folder that already has some files we want to template.
$scratch = Join-Path $outputDir 'authored-template'
if (Test-Path $scratch) { Remove-Item $scratch -Recurse -Force }
New-Item $scratch -ItemType Directory | Out-Null
'Write-Host "Hello from <%= $PLASTER_PARAM_Thing %>"' | Set-Content (Join-Path $scratch 'thing.ps1')
'# Notes about <%= $PLASTER_PARAM_Thing %>' | Set-Content (Join-Path $scratch 'NOTES.md')

Write-Host "`n Generating plasterManifest.json (-AddContent scans the folder)..." -ForegroundColor Green
New-PlasterManifest -Path (Join-Path $scratch 'plasterManifest.json') `
-TemplateName 'MyTinyTemplate' -TemplateType Item `
-Title 'My Tiny Template' -Description 'Authored live on stage' `
-Author 'Grace Hopper' -Tags Demo, Authoring -AddContent

Write-Host " --- authored plasterManifest.json ---" -ForegroundColor DarkGray
Get-Content (Join-Path $scratch 'plasterManifest.json') | ForEach-Object { " $_" }

# ----------------------------------------------------------------------------
Write-Header 'DEMO 4c - Validate it with Test-PlasterManifest'

$manifest = Test-PlasterManifest -Path (Join-Path $scratch 'plasterManifest.json') 3>$null
if ($manifest) {
Write-Host "`n Valid. name='$($manifest.plasterManifest.metadata.name)' type='$($manifest.plasterManifest.templateType)'" -ForegroundColor Green
}

# ----------------------------------------------------------------------------
Write-Header 'DEMO 4d - Use the template we just authored'

# Two quick edits to make the round-trip meaningful:
# 1. Add a 'Thing' parameter so there is a $PLASTER_PARAM_Thing to substitute.
# 2. New-PlasterManifest -AddContent emits 'file' actions (verbatim copy). Switch
# them to 'templateFile' so the <%= ... %> placeholders actually expand.
$json = Get-Content (Join-Path $scratch 'plasterManifest.json') -Raw | ConvertFrom-Json
$json.parameters = @(
[pscustomobject]@{ name = 'Thing'; type = 'text'; prompt = 'Name the thing'; default = 'Sproket' }

Check warning on line 74 in demos/Demo4-Discovery-Authoring.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Sproket) Suggestions: (sprocket, spoke, spoked, spoken, spokes)

Check warning on line 74 in demos/Demo4-Discovery-Authoring.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (pscustomobject)
)
foreach ($action in $json.content) { $action.type = 'templateFile' }
$json | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $scratch 'plasterManifest.json')

$dst = Join-Path $outputDir '04-authored-output'
if (Test-Path $dst) { Remove-Item $dst -Recurse -Force }
Invoke-Plaster -TemplatePath $scratch -DestinationPath $dst -NoLogo -Thing 'Sproket'

Check warning on line 81 in demos/Demo4-Discovery-Authoring.ps1

View workflow job for this annotation

GitHub Actions / Continuous Integration / Run Linters

Unknown word (Sproket) Suggestions: (sprocket, spoke, spoked, spoken, spokes)

Write-Host "`n --- thing.ps1 (generated from our authored template) ---" -ForegroundColor DarkGray
Get-Content (Join-Path $dst 'thing.ps1') | ForEach-Object { " $_" }

Write-Host "`nDemo 4 complete.`n" -ForegroundColor Cyan
Loading
Loading