Public/New-PowerShellProject.ps1

function New-PowerShellProject {
    <#
    .SYNOPSIS
    Creates a PowerShell module project.

    .DESCRIPTION
    Creates a conventional PowerShell module layout with a valid manifest,
    module root, README, gitignore, and Pester import test.

    .PARAMETER Name
    Project directory name.

    .PARAMETER Path
    Parent directory in which the project is created.

    .PARAMETER ModuleName
    PowerShell module name. Defaults to Name.

    .PARAMETER Description
    Module description used in the manifest and README.md.

    .PARAMETER Force
    Allows an existing project directory and overwrites generated files.

    .EXAMPLE
    mkpsproj ExampleTools
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [Alias('mkpsproj')]
    [OutputType([System.IO.DirectoryInfo])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9._-]*$')]
        [string]$Name,

        [Parameter()]
        [string]$Path = (Get-Location).Path,

        [Parameter()]
        [string]$ModuleName,

        [Parameter()]
        [string]$Description = 'PowerShell module generated by DevXToolkit.',

        [switch]$Force
    )

    if ([string]::IsNullOrWhiteSpace($ModuleName)) {
        $ModuleName = $Name
    }
    if ($ModuleName -notmatch '^[A-Za-z][A-Za-z0-9._-]*$') {
        throw "ModuleName '$ModuleName' must start with a letter and contain only letters, numbers, dots, underscores, or hyphens."
    }

    $projectRoot = Resolve-DtContainedPath -BasePath $Path -ChildName $Name
    if ((Test-Path -LiteralPath $projectRoot) -and -not $Force) {
        throw "Project already exists. Use -Force to update generated files: $projectRoot"
    }
    if (-not $PSCmdlet.ShouldProcess($projectRoot, 'Create PowerShell module project')) {
        return
    }

    $moduleRoot = Join-Path -Path $projectRoot -ChildPath "src/$ModuleName"
    $testsRoot = Join-Path -Path $projectRoot -ChildPath 'tests'
    New-DtDirectory -Path $projectRoot -Confirm:$false | Out-Null
    New-DtDirectory -Path $moduleRoot -Confirm:$false | Out-Null
    New-DtDirectory -Path $testsRoot -Confirm:$false | Out-Null

    $safeTitle = ConvertTo-DtSafeMarkdownText -Text $Name
    $safeDescription = ConvertTo-DtSafeMarkdownText -Text $Description
    $manifestDescription = $safeDescription.Replace("'", "''")
    $moduleGuid = [guid]::NewGuid().ToString()
    $manifest = @"
@{
    RootModule = '$ModuleName.psm1'
    ModuleVersion = '0.1.0'
    GUID = '$moduleGuid'
    Author = 'Generated by DevXToolkit'
    Description = '$manifestDescription'
    PowerShellVersion = '5.1'
    FunctionsToExport = @()
    CmdletsToExport = @()
    VariablesToExport = @()
    AliasesToExport = @()
}
"@

    $moduleContent = @"
# $ModuleName

Set-StrictMode -Version Latest
"@

    $testContent = @"
BeforeAll {
    Import-Module (Join-Path `$PSScriptRoot '../src/$ModuleName/$ModuleName.psd1') -Force
}

Describe '$ModuleName' {
    It 'imports successfully' {
        Get-Module '$ModuleName' | Should -Not -BeNullOrEmpty
    }
}
"@

    $readme = @"
# $safeTitle

$safeDescription

## Test

``````powershell
Invoke-Pester ./tests
``````
"@

    $gitignore = @"
artifacts/
TestResults/
*.nupkg
"@


    Set-DtFileContent -Path (Join-Path $moduleRoot "$ModuleName.psd1") -Content $manifest -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $moduleRoot "$ModuleName.psm1") -Content $moduleContent -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $testsRoot "$ModuleName.Tests.ps1") -Content $testContent -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $projectRoot 'README.md') -Content $readme -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $projectRoot '.gitignore') -Content $gitignore -Force:$Force -Confirm:$false | Out-Null

    Get-Item -LiteralPath $projectRoot
}