Public/New-PstFunction.ps1

function New-PstFunction {
    <#
.SYNOPSIS
    Creates a new PowerShell function from a template with configurable complexity levels.
.DESCRIPTION
    This function takes an approved verb and noun, copies a function template based on the specified
    complexity level, and renames the file and function block to the specified verb-noun combination.

    Complexity Levels:
    - Basic: Simple function with minimal structure (no error handling, basic parameters)
    - Moderate: Function with parameter validation, basic error handling, and help documentation
    - Complex: Full cmdlet features including begin/process/end blocks, pipeline support, ShouldProcess
.PARAMETER Verb
    (Required) The approved verb for the function name.
.PARAMETER Noun
    (Required) The noun for the function name.
.PARAMETER ComplexityLevel
    (Optional) The complexity level of the function template. Valid values: Basic, Moderate, Complex.
    Defaults to Complex for backward compatibility.
.PARAMETER Force
    (Optional) Forces creation even if file exists, useful in automation scenarios.
.PARAMETER IncludeProjectLayout
    (Optional) Creates a complete project structure including src/, tests/, docs/ directories and supporting files.
.EXAMPLE
    New-PstFunction -Verb Get -Noun Item
    Creates a new complex function file named Get-Item.ps1 with full cmdlet features.
.EXAMPLE
    New-PstFunction -Verb Get -Noun Item -ComplexityLevel Basic
    Creates a simple function with minimal structure for basic automation tasks.
.EXAMPLE
    New-PstFunction -Verb Set -Noun Configuration -ComplexityLevel Moderate
    Creates a moderately complex function with parameter validation and error handling.
.EXAMPLE
    New-PstFunction -Verb Get -Noun UserData -IncludeProjectLayout
    Creates a complete project structure with Get-UserData.ps1 in src/, test files, documentation, and .gitignore.
.NOTES
    Ensure that the appropriate template files exist in the Resources/Samples directory.
    Template files: Function-Basic.ps1, Function-Moderate.ps1, Function-Complex.ps1
.LINK
    For more information on approved verbs, visit: https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands
#>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true, Position = 0,
            HelpMessage = "The approved verb for the function name.")]
        [ValidateSet("Add", "Approve", "Assert", "Backup", "Block", "Checkpoint", "Clear", "Close", "Compare", "Complete", "Compress", "Confirm", "Connect", "Convert", "ConvertFrom", "ConvertTo", "Copy", "Debug", "Deny", "Disable", "Disconnect", "Dismount", "Edit", "Enable", "Enter", "Exit", "Expand", "Export", "Find", "Format", "Get", "Grant", "Group", "Hide", "Import", "Initialize", "Install", "Invoke", "Join", "Limit", "Lock", "Measure", "Merge", "Mount", "Move", "New", "Open", "Optimize", "Out", "Ping", "Pop", "Protect", "Publish", "Push", "Read", "Receive", "Redo", "Register", "Remove", "Rename", "Repair", "Request", "Reset", "Resize", "Resolve", "Restart", "Restore", "Resume", "Revoke", "Save", "Search", "Select", "Send", "Set", "Show", "Skip", "Split", "Start", "Step", "Stop", "Submit", "Suspend", "Switch", "Sync", "Test", "Trace", "Unblock", "Undo", "Uninstall", "Unlock", "Unprotect", "Unpublish", "Unregister", "Update", "Use", "Wait", "Watch", "Write")]
        [string]$Verb,

        [Parameter(Mandatory = $true, Position = 1,
            HelpMessage = "The noun for the function name.")]
        [ValidatePattern('^[A-Za-z][A-Za-z0-9]*$')]
        [string]$Noun,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Basic", "Moderate", "Complex")]
        [string]$ComplexityLevel = "Complex",

        [Parameter(Mandatory = $false)]
        [switch]$Force,

        [Parameter(Mandatory = $false)]
        [switch]$IncludeProjectLayout
    )
    begin {
        Write-Debug -Message "Begin '$($MyInvocation.MyCommand.Name)' at '$(Get-Date)'"

        # Select template based on complexity level
        $templateFileName = switch ($ComplexityLevel) {
            "Basic"     { "Function-Basic.ps1" }
            "Moderate"  { "Function-Moderate.ps1" }
            "Complex"   { "Function-Complex.ps1" }
            default     { "Function-Complex.ps1" }  # Fallback to Complex
        }

        $templatePath = $Samples[$templateFileName]
        $newFunctionName = "$Verb-$Noun"

        # Determine file path based on project layout option
        if ($IncludeProjectLayout) {
            $projectRoot = $newFunctionName
            $newFilePath = Join-Path $projectRoot "src" "$newFunctionName.ps1"
        } else {
            $newFilePath = "$newFunctionName.ps1"
        }

        Write-Verbose "Using complexity level: $ComplexityLevel"
        Write-Verbose "Selected template: $templateFileName"

        if (-not $templatePath) {
            throw "Template '$templateFileName' not found in Samples hashtable. Available templates: $($Samples.Keys -join ', ')"
        }
        if (-not (Test-Path $templatePath)) {
            throw "Template file not found: $templatePath"
        } else {
            Write-Verbose "Template file found: $templatePath"
        }
    }
    process {
        try {
            # Check if we're in automation mode - inline check to avoid scope issues
            $isAutomationMode = $Force -or
            ($Global:PstAutomationMode -eq $true) -or
            ($env:CI -eq 'true') -or
            ($env:GITHUB_ACTIONS -eq 'true') -or
            ($Host.Name -eq 'ServerRemoteHost') -or
            (-not [Environment]::UserInteractive) -or
            ($global:ConfirmPreference -eq 'None')

            # Create project structure if IncludeProjectLayout is specified
            if ($IncludeProjectLayout) {
                $projectRoot = $newFunctionName

                # Create directory structure
                $directories = @(
                    $projectRoot,
                    (Join-Path $projectRoot "src"),
                    (Join-Path $projectRoot "tests"),
                    (Join-Path $projectRoot "tests" "Unit"),
                    (Join-Path $projectRoot "tests" "Integration"),
                    (Join-Path $projectRoot "docs")
                )

                foreach ($dir in $directories) {
                    if (-not (Test-Path $dir)) {
                        if ($isAutomationMode -or $PSCmdlet.ShouldProcess($dir, "Create directory")) {
                            New-Item -Path $dir -ItemType Directory -Force | Out-Null
                            Write-Verbose "Created directory: $dir"
                        }
                    }
                }

                # Create README.md in root
                $readmeContent = @"
# $newFunctionName

PowerShell function for [brief description].

## Installation

``````powershell
Import-Module .\src\$newFunctionName.ps1
``````

## Usage

``````powershell
$newFunctionName -Parameter Value
``````

## Testing

``````powershell
Invoke-Pester -Path .\tests\
``````

## License

[Specify license]
"@

                $readmePath = Join-Path $projectRoot "README.md"
                if ($isAutomationMode -or $PSCmdlet.ShouldProcess($readmePath, "Create README.md")) {
                    $readmeContent | Set-Content -Path $readmePath
                    Write-Verbose "Created: $readmePath"
                }

                # Create .gitignore
                $gitignoreContent = @"
# Build artifacts
build/
*.nupkg

# Test results
TestResults*.xml
coverage*.xml

# Temporary files
temp/
*.tmp
*.log
"@

                $gitignorePath = Join-Path $projectRoot ".gitignore"
                if ($isAutomationMode -or $PSCmdlet.ShouldProcess($gitignorePath, "Create .gitignore")) {
                    $gitignoreContent | Set-Content -Path $gitignorePath
                    Write-Verbose "Created: $gitignorePath"
                }

                # Create docs README.md
                $docsReadmeContent = @"
# $newFunctionName Documentation

## Overview

[Function overview and purpose]

## Parameters

[Parameter documentation]

## Examples

[Usage examples]

## Notes

[Additional notes]
"@

                $docsReadmePath = Join-Path $projectRoot "docs" "README.md"
                if ($isAutomationMode -or $PSCmdlet.ShouldProcess($docsReadmePath, "Create docs README.md")) {
                    $docsReadmeContent | Set-Content -Path $docsReadmePath
                    Write-Verbose "Created: $docsReadmePath"
                }

                # Create test file
                $testContent = @"
BeforeAll {
    Import-Module "`$PSScriptRoot\..\..\src\$newFunctionName.ps1" -Force
}

Describe "$newFunctionName" {
    Context "When valid parameters are provided" {
        It "Should execute without errors" {
            # Arrange

            # Act

            # Assert
            `$true | Should -Be `$true
        }
    }

    Context "When invalid parameters are provided" {
        It "Should throw an error" {
            # Arrange & Act & Assert
            { $newFunctionName -Parameter `$null } | Should -Throw
        }
    }
}
"@

                $testFilePath = Join-Path $projectRoot "tests" "Unit" "$newFunctionName.Tests.ps1"
                if ($isAutomationMode -or $PSCmdlet.ShouldProcess($testFilePath, "Create test file")) {
                    $testContent | Set-Content -Path $testFilePath
                    Write-Verbose "Created: $testFilePath"
                }
            }

            # Check if file already exists
            if ((Test-Path $newFilePath) -and -not $isAutomationMode) {
                if (-not $PSCmdlet.ShouldProcess($newFilePath, "Overwrite existing function file")) {
                    Write-Warning "Operation cancelled by user."
                    return
                }
            }
            elseif ((Test-Path $newFilePath) -and $isAutomationMode -and -not $Force) {
                Write-Warning "File '$newFilePath' already exists. Use -Force to overwrite in automation mode."
                return
            }
            # Proceed with file creation - skip ShouldProcess in automation mode
            if ($isAutomationMode -or $PSCmdlet.ShouldProcess($newFilePath, "Create new function file")) {
                Write-Verbose "Getting file content from '$($templatePath)'"
                $templateContent = (Get-Content -Path $templatePath) -join "`n"
                Write-Verbose "Replacing '""Function Verb-Noun"" with ""Function $($newFunctionName)""' "
                $newContent = $templateContent -replace "Function Verb-Noun", "Function $newFunctionName"

                # Create parent directory if it doesn't exist (for project layout)
                $parentDir = Split-Path -Path $newFilePath -Parent
                if ($parentDir -and -not (Test-Path $parentDir)) {
                    New-Item -Path $parentDir -ItemType Directory -Force | Out-Null
                }

                Write-Verbose "Set-Content to path '$($newFilePath)'"
                $newContent | Set-Content -Path $newFilePath

                if ($IncludeProjectLayout) {
                    Write-Output "New function project created: $projectRoot"
                    Write-Output " - Function file: $newFilePath"
                    Write-Output " - Test file: tests\Unit\$newFunctionName.Tests.ps1"
                    Write-Output " - Documentation: docs\README.md"
                } else {
                    Write-Output "New function file created: $newFilePath"
                }
            }
            else {
                Write-Verbose "Operation cancelled or skipped."
            }
        }
        catch {
            if ($_.Exception -and $_.Exception.Message) {
                Write-Error "An error occurred: $($_.Exception.Message)"
            } else {
                Write-Error "An error occurred, but no additional information is available."
            }
        }
    }
    end {
        if ($?) {
            Write-Debug -Message "End '$($MyInvocation.MyCommand.Name)' at '$(Get-Date)'"
        }
    }
}