NovaModuleTools.psm1

# Source: src/public/GetMTProjectInfo.ps1
function Get-MTProjectInfo {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Path = (Get-Location).Path
    )
    $ProjectRoot = (Resolve-Path -LiteralPath $Path).Path
    $ProjectJson = [System.IO.Path]::Join($ProjectRoot, 'project.json')
    
    if (-not (Test-Path -LiteralPath $projectJson)) {
        throw "Not a project folder. project.json not found: $projectJson"
    }
    
    $jsonData = Get-Content -LiteralPath $projectJson -Raw | ConvertFrom-Json -AsHashtable

    $Out = @{}
    $Out['ProjectJSON'] = $ProjectJson

    foreach ($key in $jsonData.Keys) {
        $Out[$key] = $jsonData[$key]
    }

    foreach ($boolKey in @('BuildRecursiveFolders', 'FailOnDuplicateFunctionNames', 'SetSourcePath')) {
        if (-not $Out.ContainsKey($boolKey)) {
            $Out[$boolKey] = $true
            continue
        }

        $Out[$boolKey] = [bool]$Out[$boolKey]
    }

    $Out.ProjectJson = $projectJson
    $Out.PSTypeName = 'MTProjectInfo'
    $ProjectName = $jsonData.ProjectName

    ## Folders
    $Out['ProjectRoot'] = $ProjectRoot
    $Out['PublicDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'public')
    $Out['PrivateDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'private')
    $Out['ClassesDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'classes')
    $Out['ResourcesDir'] = [System.IO.Path]::Join($ProjectRoot, 'src', 'resources')
    $Out['TestsDir'] = [System.IO.Path]::Join($ProjectRoot, 'tests')
    $Out['DocsDir'] = [System.IO.Path]::Join($ProjectRoot, 'docs')
    $Out['OutputDir'] = [System.IO.Path]::Join($ProjectRoot, 'dist')  
    $Out['OutputModuleDir'] = [System.IO.Path]::Join($Out.OutputDir, $ProjectName)  
    $Out['ModuleFilePSM1'] = [System.IO.Path]::Join($Out.OutputModuleDir, "$ProjectName.psm1")   
    $Out['ManifestFilePSD1'] = [System.IO.Path]::Join($Out.OutputModuleDir, "$ProjectName.psd1")  

    return [pscustomobject]$out
}

# Source: src/public/GetNovaProjectInfo.ps1
function Get-NovaProjectInfo {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Path = (Get-Location).Path,

        [switch]$Version
    )

    $projectInfo = Get-MTProjectInfo -Path $Path

    if ($Version) {
        return $projectInfo.Version
    }

    return $projectInfo
}


# Source: src/public/InvokeMTBuild.ps1
function Invoke-MTBuild {
    [CmdletBinding()]
    param (
    )
    $ErrorActionPreference = 'Stop'
    Reset-ProjectDist
    Build-Module

    $data = Get-MTProjectInfo
    if ($data.FailOnDuplicateFunctionNames) {
        Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $data
    }

    Build-Manifest
    Build-Help
    Copy-ProjectResource
}

# Source: src/public/InvokeMTTests.ps1
function Invoke-MTTest {
    [CmdletBinding()]
    param (
        [string[]]$TagFilter,
        [string[]]$ExcludeTagFilter
    )
    Test-ProjectSchema Pester | Out-Null
    $Script:data = Get-MTProjectInfo
    $pesterConfig = New-PesterConfiguration -Hashtable $data.Pester

    $testPath = if ($data.BuildRecursiveFolders) {
        $data.TestsDir
    }
    else {
        [System.IO.Path]::Join($data.TestsDir, '*.Tests.ps1')
    }

    $pesterConfig.Run.Path = $testPath
    $pesterConfig.Run.PassThru = $true
    $pesterConfig.Run.Exit = $true
    $pesterConfig.Run.Throw = $true
    $pesterConfig.Filter.Tag = $TagFilter
    $pesterConfig.Filter.ExcludeTag = $ExcludeTagFilter
    $pesterConfig.TestResult.OutputPath = './dist/TestResults.xml'
    $TestResult = Invoke-Pester -Configuration $pesterConfig
    if ($TestResult.Result -ne 'Passed') {
        Write-Error 'Tests failed' -ErrorAction Stop
        return $LASTEXITCODE
    }
}

# Source: src/public/InvokeNovaBuild.ps1
function Invoke-NovaBuild {
    [CmdletBinding()]
    param()

    Invoke-MTBuild
}


# Source: src/public/InvokeNovaCli.ps1
function Invoke-NovaCli {
    [CmdletBinding()]
    [Alias('nova')]
    param(
        [Parameter(Position = 0)]
        [ValidateSet('info', 'version', 'build', 'test', 'init', 'publish', 'bump', 'release')]
        [string]$Command,

        [Parameter(Position = 1, ValueFromRemainingArguments)]
        [string[]]$Arguments
    )

    if ( [string]::IsNullOrWhiteSpace($Command)) {
        throw 'Missing command. Use one of: info, version, build, test, init, publish, bump, release'
    }

    switch ($Command) {
        'info' {
            return Get-NovaProjectInfo
        }
        'version' {
            return Get-NovaProjectInfo -Version
        }
        'build' {
            return Invoke-NovaBuild
        }
        'test' {
            return Test-NovaBuild
        }
        'init' {
            if ($Arguments.Count -gt 0) {
                return New-NovaModule -Path $Arguments[0]
            }

            return New-NovaModule
        }
        'bump' {
            return Update-NovaModuleVersion
        }
        'publish' {
            $options = ConvertFrom-NovaCliArgument -Arguments $Arguments
            return Publish-NovaModule @options
        }
        'release' {
            $options = ConvertFrom-NovaCliArgument -Arguments $Arguments
            return Invoke-NovaRelease -PublishOption $options
        }
    }
}





# Source: src/public/InvokeNovaRelease.ps1
function Invoke-NovaRelease {
    [CmdletBinding()]
    param(
        [hashtable]$PublishOption = @{},
        [string]$Path = (Get-Location).Path
    )

    Push-Location -LiteralPath $Path
    try {
        if ($PublishOption.Local) {
            Write-Verbose 'Using local release mode.'
        }

        Invoke-NovaBuild
        Test-NovaBuild
        $versionResult = Update-NovaModuleVersion
        Invoke-NovaBuild

        if ( $PublishOption.ContainsKey('Repository')) {
            Publish-NovaBuiltModule -Repository $PublishOption.Repository -ApiKey $PublishOption.ApiKey
        }
        else {
            Publish-NovaBuiltModule -ModuleDirectoryPath $PublishOption.ModuleDirectoryPath
        }

        return $versionResult
    }
    finally {
        Pop-Location
    }
}





# Source: src/public/NewMTModule.ps1
function New-MTModule {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [string]$Path = (Get-Location).Path
    )
    $ErrorActionPreference = 'Stop'
    Push-Location
    if (-not(Test-Path $Path)) { Write-Error 'Not a valid path' }
    $Questions = [ordered]@{
        ProjectName           = @{
            Caption = 'Module Name'
            Message = 'Enter Module name of your choice, should be single word with no special characters'
            Prompt  = 'Name'
            Default = 'MANDATORY'
        }
        Description           = @{
            Caption = 'Module Description'
            Message = 'What does your module do? Describe in simple words'
            Prompt  = 'Description'
            Default = 'NovaModuleTools Module'
        }
        Version               = @{
            Caption = 'Semantic Version'
            Message = 'Starting Version of the module (Default: 0.0.1)'
            Prompt  = 'Version'
            Default = '0.0.1'
        }
        Author                = @{
            Caption = 'Module Author'
            Message = 'Enter Author or company name'
            Prompt  = 'Name'
            Default = 'PS'
        }
        PowerShellHostVersion = @{
            Caption = 'Supported PowerShell Version'
            Message = 'What is minimum supported version of PowerShell for this module (Default: 7.4)'
            Prompt  = 'Version'
            Default = '7.4'
        }
        EnableGit             = @{
            Caption = 'Git Version Control'
            Message = 'Do you want to enable version controlling using Git'
            Prompt  = 'EnableGit'
            Default = 'No'
            Choice  = @{
                Yes = 'Enable Git'
                No  = 'Skip Git initialization'
            }
        }
        EnablePester          = @{
            Caption = 'Pester Testing'
            Message = 'Do you want to enable basic Pester Testing'
            Prompt  = 'EnablePester'
            Default = 'No'
            Choice  = @{
                Yes = 'Enable pester to perform testing'
                No  = 'Skip pester testing'
            }
        }
    }
    $Answer = @{}
    $Questions.Keys | ForEach-Object {
        $Answer.$_ = Read-AwesomeHost -Ask $Questions.$_
    }

    # TODO check other components
    if ($Answer.ProjectName -notmatch '^[A-Za-z][A-Za-z0-9_.]*$') {
        Write-Error 'Module Name invalid. Module should be one word and contain only Letters,Numbers and ' 
    }
  
    $DirProject = Join-Path -Path $Path -ChildPath $Answer.ProjectName
    $DirSrc = Join-Path -Path $DirProject -ChildPath 'src'
    $DirPrivate = Join-Path -Path $DirSrc -ChildPath 'private'
    $DirPublic = Join-Path -Path $DirSrc -ChildPath 'public'
    $DirResources = Join-Path -Path $DirSrc -ChildPath 'resources'
    $DirClasses = Join-Path -Path $DirSrc -ChildPath 'classes'
    $DirTests = Join-Path -Path $DirProject -ChildPath 'tests'
    $ProjectJSONFile = Join-Path $DirProject -ChildPath 'project.json'

    if (Test-Path $DirProject) {
        Write-Error 'Project already exists, aborting' | Out-Null
    }
    # Setup Module

    Write-Message "`nStarted Module Scaffolding" -color Green
    Write-Message 'Setting up Directories'
    ($DirProject, $DirSrc, $DirPrivate, $DirPublic, $DirResources, $DirClasses) | ForEach-Object {
        'Creating Directory: {0}' -f $_ | Write-Verbose
        New-Item -ItemType Directory -Path $_ | Out-Null
    }
    if ( $Answer.EnablePester -eq 'Yes') {
        Write-Message 'Include Pester Configs'
        New-Item -ItemType Directory -Path $DirTests | Out-Null
    }
    if ( $Answer.EnableGit -eq 'Yes') {
        Write-Message 'Initialize Git Repo'
        New-InitiateGitRepo -DirectoryPath $DirProject
    }

    ## Create ProjectJSON
    $JsonData = Get-Content (Get-ResourceFilePath -FileName 'ProjectTemplate.json') -Raw | ConvertFrom-Json -AsHashtable

    $JsonData.ProjectName = $Answer.ProjectName
    $JsonData.Description = $Answer.Description
    $JsonData.Version = $Answer.version
    $JsonData.Manifest.Author = $Answer.Author
    $JsonData.Manifest.PowerShellHostVersion = $Answer.PowerShellHostVersion
    $JsonData.Manifest.GUID = (New-Guid).GUID
    if ($Answer.EnablePester -eq 'No') { $JsonData.Remove('Pester') }

    Write-Verbose $JsonData
    $JsonData | ConvertTo-Json | Out-File $ProjectJSONFile

    'Module {0} scaffolding complete' -f $Answer.ProjectName | Write-Message -color Green
}

# Source: src/public/NewNovaModule.ps1
function New-NovaModule {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [string]$Path = (Get-Location).Path
    )

    New-MTModule -Path $Path
}


# Source: src/public/PublishMTLocal.ps1
function Publish-MTLocal {
    [CmdletBinding()]
    param(
        [string]$ModuleDirectoryPath
    )

    if ($ModuleDirectoryPath) {
        if (-not (Test-Path $ModuleDirectoryPath -PathType Container)) {
            New-Item $ModuleDirectoryPath -ItemType Directory -Force | Out-Null
        }
    } else {
        $ModuleDirectoryPath = Get-LocalModulePath
    }

    Write-Verbose "Using $ModuleDirectoryPath as path"

    $ProjectInfo = Get-MTProjectInfo

    # Ensure module is locally built and ready
    if (-not (Test-Path $ProjectInfo.OutputModuleDir)) {
        throw 'Dist folder is empty, build the module before running publish command'
    }

    # Cleanup old files
    $OldModule = Join-Path -Path $ModuleDirectoryPath -ChildPath $ProjectInfo.ProjectName
    if (Test-Path -Path $OldModule) {
        Write-Verbose 'Removing old module files'
        Remove-Item -Recurse $OldModule -Force
    }

    # Copy New Files
    Write-Verbose 'Copying new Files'
    Copy-Item -Path $ProjectInfo.OutputModuleDir -Destination $ModuleDirectoryPath -Recurse -ErrorAction Stop
    Write-Verbose 'Module copy to local path complete, Refresh session or import module manually'
}

# Source: src/public/PublishNovaModule.ps1
function Publish-NovaModule {
    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param(
        [Parameter(ParameterSetName = 'Local')]
        [switch]$Local,

        [Parameter(ParameterSetName = 'Repository', Mandatory)]
        [string]$Repository,

        [string]$ModuleDirectoryPath,
        [string]$ApiKey
    )

    Invoke-NovaBuild
    Test-NovaBuild

    if ($Local) {
        Write-Verbose 'Using local publish mode.'
    }

    if ($PSCmdlet.ParameterSetName -eq 'Repository') {
        Publish-NovaBuiltModule -Repository $Repository -ApiKey $ApiKey
        return
    }

    Publish-NovaBuiltModule -ModuleDirectoryPath $ModuleDirectoryPath
}




# Source: src/public/TestNovaBuild.ps1
function Test-NovaBuild {
    [CmdletBinding()]
    param(
        [string[]]$TagFilter,
        [string[]]$ExcludeTagFilter
    )

    Invoke-MTTest -TagFilter $TagFilter -ExcludeTagFilter $ExcludeTagFilter
}


# Source: src/public/UpdateModVersion.ps1
function Update-MTModuleVersion {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$Label = 'Patch',
        [switch]$PreviewRelease,
        [switch]$StableRelease
    )
    Write-Verbose 'Running Version Update'

    $data = Get-MTProjectInfo
    $jsonContent = Get-Content -Path $data.ProjectJSON | ConvertFrom-Json

    [semver]$CurrentVersion = $jsonContent.Version
    $Major = $CurrentVersion.Major
    $Minor = $CurrentVersion.Minor
    
    if ($Label -eq 'Major') {
        $Major = $CurrentVersion.Major + 1
        $Minor = 0
        $Patch = 0
    } elseif ($Label -eq 'Minor') {
        $Minor = $CurrentVersion.Minor + 1
        $Patch = 0
    } elseif ($Label -eq 'Patch') {
        $Patch = $CurrentVersion.Patch + 1
    }

    if ($PreviewRelease) {
        $ReleaseType = 'preview' 
    } elseif ($StableRelease) { 
        $ReleaseType = $null
    } else {
        $ReleaseType = $CurrentVersion.PreReleaseLabel
    }
    
    $newVersion = [semver]::new($Major, $Minor, $Patch, $ReleaseType, $null)

    # Update the version in the JSON object
    $jsonContent.Version = $newVersion.ToString()
    Write-Host "Version bumped to : $newVersion"

    # Convert the JSON object back to JSON format
    $newJsonContent = $jsonContent | ConvertTo-Json

    # Write the updated JSON back to the file
    $newJsonContent | Set-Content -Path $data.ProjectJSON
}

# Source: src/public/UpdateNovaModuleVersion.ps1
function Update-NovaModuleVersion {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [string]$Path = (Get-Location).Path
    )

    $projectRoot = (Resolve-Path -LiteralPath $Path).Path
    $before = Get-MTProjectInfo -Path $projectRoot
    $commitMessages = Get-GitCommitMessageForVersionBump -ProjectRoot $projectRoot
    $label = Get-VersionLabelFromCommitSet -Messages $commitMessages

    Push-Location -LiteralPath $projectRoot
    try {
        Update-MTModuleVersion -Label $label
        $after = Get-MTProjectInfo
    }
    finally {
        Pop-Location
    }

    return [pscustomobject]@{
        PreviousVersion = $before.Version
        NewVersion = $after.Version
        Label = $label
        CommitCount = $commitMessages.Count
    }
}



# Source: src/private/AddScriptFileContentToModuleBuilder.ps1
function Add-ScriptFileContentToModuleBuilder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][System.Text.StringBuilder]$Builder,
        [Parameter(Mandatory)][pscustomobject]$ProjectInfo,
        [Parameter(Mandatory)][System.IO.FileInfo]$File
    )

    if ($ProjectInfo.SetSourcePath) {
        $relativePath = Get-NormalizedRelativePath -Root $ProjectInfo.ProjectRoot -FullName $File.FullName
        $Builder.AppendLine("# Source: $relativePath") | Out-Null
    }

    $Builder.AppendLine([IO.File]::ReadAllText($File.FullName)) | Out-Null
    $Builder.AppendLine() | Out-Null
}


# Source: src/private/AssertBuiltModuleHasNoDuplicateFunctionNames.ps1
function Assert-BuiltModuleHasNoDuplicateFunctionName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$ProjectInfo
    )

    $psm1Path = $ProjectInfo.ModuleFilePSM1
    if (-not (Test-Path -LiteralPath $psm1Path)) {
        throw "Built module file not found: $psm1Path"
    }

    $parsed = Get-PowerShellAstFromFile -Path $psm1Path
    if ($parsed.Errors -and $parsed.Errors.Count -gt 0) {
        $messages = @($parsed.Errors | ForEach-Object { $_.Message }) -join '; '
        throw "Built module contains parse errors and cannot be validated for duplicates. File: $psm1Path. Errors: $messages"
    }

    $topLevelFunctions = Get-TopLevelFunctionAst -Ast $parsed.Ast
    $duplicates = Get-DuplicateFunctionGroup -FunctionAst $topLevelFunctions

    if (-not $duplicates) {
        return
    }

    $sourceFiles = Get-ProjectScriptFile -ProjectInfo $ProjectInfo
    $sourceIndex = Get-FunctionSourceIndex -File $sourceFiles

    $errorText = Format-DuplicateFunctionErrorMessage -Psm1Path $psm1Path -DuplicateGroup $duplicates -SourceIndex $sourceIndex
    throw $errorText
}


# Source: src/private/BuildHelp.ps1
function Build-Help {
    [CmdletBinding()]
    param(
    )
    Write-Verbose 'Running Help update'

    $data = Get-MTProjectInfo
    $helpMarkdownFiles = Get-ChildItem -Path $data.DocsDir -Filter '*.md' -Recurse 

    if (-not $helpMarkdownFiles) {
        Write-Verbose 'No help markdown files in docs directory, skipping building help' 
        return
    }
    
    if (-not (Get-Module -Name Microsoft.PowerShell.PlatyPS -ListAvailable)) {
        throw 'The module Microsoft.PowerShell.PlatyPS must be installed for Markdown documentation to be generated.'
    }

    $AllCommandHelpFiles = $helpMarkdownFiles | Measure-PlatyPSMarkdown | Where-Object FileType -Match CommandHelp

    # Export to Dist folder
    $AllCommandHelpFiles | Import-MarkdownCommandHelp -Path { $_.FilePath } |
    Export-MamlCommandHelp -OutputFolder $data.OutputModuleDir | Out-Null

    # Rename the directory to match locale
    $HelpDirOld = Join-Path $data.OutputModuleDir $Data.ProjectName
    #TODO: hardcoded locale to en-US, change it based on Doc type
    $languageLocale = 'en-US'
    $HelpDirNew = Join-Path $data.OutputModuleDir $languageLocale
    Write-Verbose "Renamed folder to locale: $languageLocale"

    Rename-Item -Path $HelpDirOld -NewName $HelpDirNew
}

# Source: src/private/BuildManifest.ps1
function Build-Manifest {
    Write-Verbose 'Building psd1 data file Manifest'
    $data = Get-MTProjectInfo

    ## TODO - DO schema check

    $PubFunctionFiles = Get-ChildItem -Path $data.PublicDir -Filter *.ps1
    $functionToExport = @()
    $aliasToExport = @()
    $PubFunctionFiles.FullName | ForEach-Object {
        $functionToExport += Get-FunctionNameFromFile -filePath $_
        $aliasToExport += Get-AliasInFunctionFromFile -filePath $_
    }

    ## Import Format.ps1xml (if any)
    $FormatsToProcess = @()
    Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Format.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
        if ($data.copyResourcesToModuleRoot) { 
            $FormatsToProcess += $_.Name
        } else {
            $FormatsToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
        }
    }

    ## Import Types.ps1xml1 (if any)
    $TypesToProcess = @()
    Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Types.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
        if ($data.copyResourcesToModuleRoot) { 
            $TypesToProcess += $_.Name
        } else {
            $TypesToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
        }
    }

    $ManfiestAllowedParams = (Get-Command New-ModuleManifest).Parameters.Keys
    $sv = [semver]$data.Version
    $ParmsManifest = @{
        Path              = $data.ManifestFilePSD1
        Description       = $data.Description
        FunctionsToExport = $functionToExport
        AliasesToExport   = $aliasToExport
        RootModule        = "$($data.ProjectName).psm1"
        ModuleVersion     = [version]$sv
        FormatsToProcess  = $FormatsToProcess
        TypesToProcess    = $TypesToProcess
    }
      
    ## Release lable
    if ($sv.PreReleaseLabel) {
        $ParmsManifest['Prerelease'] = $sv.PreReleaseLabel 
    } 

    # Accept only valid Manifest Parameters
    $data.Manifest.Keys | ForEach-Object {
        if ( $ManfiestAllowedParams -contains $_) {
            if ($data.Manifest.$_) {
                $ParmsManifest.add($_, $data.Manifest.$_ )
            }
        } else {
            Write-Warning "Unknown parameter $_ in Manifest"
        }
    }

    try {
        New-ModuleManifest @ParmsManifest -ErrorAction Stop
    } catch {
        'Failed to create Manifest: {0}' -f $_.Exception.Message | Write-Error -ErrorAction Stop
    }
}

# Source: src/private/BuildModule.ps1
function Build-Module {
    $data = Get-MTProjectInfo
    $MTBuildVersion = (Get-Command Invoke-MTBuild).Version
    Write-Verbose "Running ModuleTols Version: $MTBuildVersion"
    Write-Verbose 'Buidling module psm1 file'
    Test-ProjectSchema -Schema Build | Out-Null

    $sb = [System.Text.StringBuilder]::new()

    $files = Get-ProjectScriptFile -ProjectInfo $data
    foreach ($file in $files) {
        Add-ScriptFileContentToModuleBuilder -Builder $sb -ProjectInfo $data -File $file
    }
    try {
        Set-Content -Path $data.ModuleFilePSM1 -Value $sb.ToString() -Encoding 'UTF8' -ErrorAction Stop # psm1 file
    } catch {
        Write-Error 'Failed to create psm1 file' -ErrorAction Stop
    }
}

# Source: src/private/CopyProjectResource.ps1
function Copy-ProjectResource {
    $data = Get-MTProjectInfo
    $resFolder = [System.IO.Path]::Join($data.ProjectRoot, 'src', 'resources')
    if (Test-Path $resFolder) {
        ## Copy to root folder instead of creating Resource Folder in module root
        if ($data.copyResourcesToModuleRoot) {
            # Copy the resources folder content to the OutputModuleDir
            $items = Get-ChildItem -Path $resFolder -ErrorAction SilentlyContinue
            if ($items) {
                Write-Verbose 'Files found in resource folder, copying resource folder content'
                foreach ($item in $items) {
                    Copy-Item -Path $item.FullName -Destination ($data.OutputModuleDir) -Recurse -Force -ErrorAction Stop
                }
            }
        } else {
            # Copy the resources folder to the OutputModuleDir
            if (Get-ChildItem $resFolder -ErrorAction SilentlyContinue) {
                Write-Verbose 'Files found in resource folder, Copying resource folder'
                Copy-Item -Path $resFolder -Destination ($data.OutputModuleDir) -Recurse -Force -ErrorAction Stop
            }
        }
    }
}

# Source: src/private/FormatDuplicateFunctionErrorMessage.ps1
function Format-DuplicateFunctionErrorMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Psm1Path,
        [Parameter(Mandatory)][object[]]$DuplicateGroup,
        [hashtable]$SourceIndex
    )

    $lines = New-Object 'System.Collections.Generic.List[string]'
    $lines.Add("Duplicate top-level function names detected in built module: $Psm1Path")

    foreach ($dup in ($DuplicateGroup | Sort-Object -Property Name)) {
        $key = '' + $dup.Name
        $displayName = $dup.Group[0].Name

        $lines.Add('')
        $lines.Add("- $displayName")

        foreach ($occurrence in ($dup.Group | Sort-Object { $_.Extent.StartLineNumber })) {
            $lines.Add((" - dist line {0}" -f $occurrence.Extent.StartLineNumber))
        }

        foreach ($sourceLine in (Get-DuplicateFunctionSourceLine -Key $key -SourceIndex $SourceIndex)) {
            $lines.Add($sourceLine)
        }
    }

    return ($lines -join "`n")
}


# Source: src/private/GetAliasNameFromFunction.ps1
<#
.SYNOPSIS
Retrieves information about alias in a given function/file so it can be added to module manifest

.DESCRIPTION
Adding alias to module manifest and exporting it will ensure that functions can be called using alias without importing explicitly
#>

function Get-AliasInFunctionFromFile {
    param($filePath)
    try {
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null)

        $functionNodes = $ast.FindAll({
                param($node)
                $node -is [System.Management.Automation.Language.FunctionDefinitionAst]
            }, $true)

        $function = $functionNodes[0]
        $paramsAttributes = $function.Body.ParamBlock.Attributes 

        $aliases = ($paramsAttributes | Where-Object { $_.TypeName -like 'Alias' } | ForEach-Object PositionalArguments).Value
        $aliases
    } catch {
        return
    }
}

# Source: src/private/GetDuplicateFunctionGroup.ps1
function Get-DuplicateFunctionGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][System.Management.Automation.Language.FunctionDefinitionAst[]]$FunctionAst
    )

    return @(
        $FunctionAst |
            Group-Object -Property { ('' + $_.Name).ToLowerInvariant() } |
            Where-Object { $_.Count -gt 1 }
    )
}


# Source: src/private/GetDuplicateFunctionSourceLine.ps1
function Get-DuplicateFunctionSourceLine {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Key,
        [hashtable]$SourceIndex
    )

    if (-not $SourceIndex) {
        return @()
    }

    if (-not $SourceIndex.ContainsKey($Key)) {
        return @()
    }

    $lines = New-Object 'System.Collections.Generic.List[string]'
    $lines.Add(' - source files:')

    foreach ($src in ($SourceIndex[$Key] | Sort-Object Path, Line)) {
        $lines.Add((" - {0}:{1}" -f $src.Path, $src.Line))
    }

    return @($lines)
}


# Source: src/private/GetFunctionNameFromFile.ps1
function Get-FunctionNameFromFile {
    param($filePath)
    try {
        $moduleContent = Get-Content -Path $filePath -Raw
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($moduleContent, [ref]$null, [ref]$null)
        $functionName = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) | ForEach-Object { $_.Name } 
        return $functionName
    }
    catch { return '' }
}

# Source: src/private/GetFunctionSourceIndex.ps1
function Get-IndexableSourceFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][object[]]$File
    )

    return @($File | Where-Object { $_ -and -not [string]::IsNullOrWhiteSpace($_.FullName) })
}

function Get-IndexableFunctionAstFromFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path
    )

    return @(Get-TopLevelFunctionAstFromFile -Path $Path | Where-Object { $_ -and -not [string]::IsNullOrWhiteSpace($_.Name) })
}

function Add-FunctionSourceIndexEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Index,
        [Parameter(Mandatory)][System.IO.FileInfo]$File,
        [Parameter(Mandatory)][System.Management.Automation.Language.FunctionDefinitionAst]$FunctionAst
    )

    $key = ('' + $FunctionAst.Name).ToLowerInvariant()
    $list = Get-OrCreateHashtableList -Index $Index -Key $key
    $list.Add([pscustomobject]@{
            Path = $File.FullName
            Line = $FunctionAst.Extent.StartLineNumber
        })
}

function Add-FunctionSourceIndexEntryFromFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Index,
        [Parameter(Mandatory)][System.IO.FileInfo]$File
    )

    foreach ($fn in (Get-IndexableFunctionAstFromFile -Path $File.FullName)) {
        Add-FunctionSourceIndexEntry -Index $Index -File $File -FunctionAst $fn
    }
}

function Get-FunctionSourceIndex {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][System.IO.FileInfo[]]$File
    )

    $index = @{}

    foreach ($f in (Get-IndexableSourceFile -File $File)) {
        Add-FunctionSourceIndexEntryFromFile -Index $index -File $f
    }

    return $index
}


# Source: src/private/GetGitCommitMessagesForVersionBump.ps1
function Get-GitCommitMessageForVersionBump {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ProjectRoot
    )

    if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot '.git'))) {
        return @()
    }

    $format = '%s%n%b%n--END-COMMIT--'
    $lastTag = & git -C $ProjectRoot describe --tags --abbrev=0 2> $null

    if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($lastTag)) {
        $raw = & git -C $ProjectRoot log "$lastTag..HEAD" --format=$format 2> $null
    }
    else {
        $raw = & git -C $ProjectRoot log --format=$format 2> $null
    }

    if ($LASTEXITCODE -ne 0 -or -not $raw) {
        return @()
    }

    $text = ($raw -join [Environment]::NewLine)
    $commits = $text -split '(?m)^--END-COMMIT--\r?$'
    return @($commits | ForEach-Object {$_.Trim()} | Where-Object {-not [string]::IsNullOrWhiteSpace($_)})
}



# Source: src/private/GetLocalModulePath.ps1
function Get-LocalModulePath {
    $sep = [IO.Path]::PathSeparator
    
    $ModulePaths = $env:PSModulePath -split $sep | ForEach-Object { $_.Trim() } | Select-Object -Unique

    if ($IsWindows) {
        $MatchPattern = '\\Documents\\PowerShell\\Modules'
        $Result = $ModulePaths | Where-Object { $_ -match $MatchPattern } | Select-Object -First 1
        if ($Result -and (Test-Path $Result)) { 
            return $Result 
        } else { 
            throw "No windows module path matching $MatchPattern found" 
        }
    } else {
        # For Mac and Linux
        $MatchPattern = '/\.local/share/powershell/Modules$'
        $Result = $ModulePaths | Where-Object { $_ -match $MatchPattern } | Select-Object -First 1
        if ($Result -and (Test-Path $Result)) {
            return $Result 
        } else {
            throw "No macOS/Linux module path matching $MatchPattern found in PSModulePath."
        }
    }
}

# Source: src/private/GetNormalizedRelativePath.ps1
function Get-NormalizedRelativePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Root,
        [Parameter(Mandatory)][string]$FullName
    )

    $rel = [System.IO.Path]::GetRelativePath($Root, $FullName)
    $rel = $rel -replace '\\', '/'
    return $rel
}


# Source: src/private/GetNovaCliOptions.ps1
function ConvertFrom-NovaCliArgument {
    [CmdletBinding()]
    param(
        [string[]]$Arguments
    )

    $options = @{}
    $index = 0

    while ($index -lt $Arguments.Count) {
        $token = $Arguments[$index]

        switch -Regex ($token) {
            '^(--local|-Local)$' {
                $options.Local = $true
            }
            '^(--repository|-Repository)$' {
                $index++
                if ($index -ge $Arguments.Count) {
                    throw 'Missing value for --repository'
                }

                $options.Repository = $Arguments[$index]
            }
            '^(--path|-Path|-ModuleDirectoryPath)$' {
                $index++
                if ($index -ge $Arguments.Count) {
                    throw 'Missing value for --path'
                }

                $options.ModuleDirectoryPath = $Arguments[$index]
            }
            '^(--apikey|-ApiKey)$' {
                $index++
                if ($index -ge $Arguments.Count) {
                    throw 'Missing value for --apikey'
                }

                $options.ApiKey = $Arguments[$index]
            }
            default {
                throw "Unknown argument: $token"
            }
        }

        $index++
    }

    return $options
}



# Source: src/private/GetOrCreateHashtableList.ps1
function Get-OrCreateHashtableList {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Index,
        [Parameter(Mandatory)][string]$Key
    )

    if (-not $Index.ContainsKey($Key)) {
        $Index[$Key] = New-Object 'System.Collections.Generic.List[object]'
    }

    return $Index[$Key]
}


# Source: src/private/GetOrderedScriptFileForDirectory.ps1
function Get-OrderedScriptFileForDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Directory,
        [Parameter(Mandatory)][string]$ProjectRoot,
        [Parameter(Mandatory)][bool]$Recurse
    )

    if (-not (Test-Path -LiteralPath $Directory)) {
        return @()
    }

    $items = if ($Recurse) {
        Get-ChildItem -Path $Directory -Filter '*.ps1' -File -Recurse -ErrorAction SilentlyContinue
    }
    else {
        Get-ChildItem -Path $Directory -Filter '*.ps1' -File -ErrorAction SilentlyContinue
    }

    $root = $ProjectRoot

    return @(
        $items |
            Sort-Object -Stable -Property @(
                @{ Expression = { (Get-NormalizedRelativePath -Root $root -FullName $_.FullName).ToLowerInvariant() } },
                @{ Expression = { $_.FullName.ToLowerInvariant() } }
            )
    )
}


# Source: src/private/GetPowerShellAstFromFile.ps1
function Get-PowerShellAstFromFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path
    )

    $tokens = $null
    $errors = $null
    $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors)

    return [pscustomobject]@{
        Ast = $ast
        Errors = $errors
    }
}


# Source: src/private/GetProjectScriptFiles.ps1
function Get-ProjectScriptFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$ProjectInfo
    )

    $recurse = [bool]$ProjectInfo.BuildRecursiveFolders

    $ordered = New-Object 'System.Collections.Generic.List[System.IO.FileInfo]'

    $root = $ProjectInfo.ProjectRoot

    foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.ClassesDir -ProjectRoot $root -Recurse:$recurse)) {
        $ordered.Add($f)
    }

    foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.PublicDir -ProjectRoot $root -Recurse:$false)) {
        $ordered.Add($f)
    }

    foreach ($f in (Get-OrderedScriptFileForDirectory -Directory $ProjectInfo.PrivateDir -ProjectRoot $root -Recurse:$recurse)) {
        $ordered.Add($f)
    }

    return @($ordered)
}


# Source: src/private/GetResourceFilePath.ps1
function Get-ResourceFilePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$FileName
    )

    $candidates = @(
        [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "resources/$FileName")),
        [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "../resources/$FileName"))
    ) | Select-Object -Unique

    foreach ($candidate in $candidates) {
        if (Test-Path -LiteralPath $candidate) {
            return $candidate
        }
    }

    throw "Resource file not found: $FileName. Checked: $($candidates -join ', ')"
}



# Source: src/private/GetTopLevelFunctionAst.ps1
function Get-TopLevelFunctionAst {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][System.Management.Automation.Language.Ast]$Ast
    )

    $all = @($Ast.FindAll({
                param($n)
                $n -is [System.Management.Automation.Language.FunctionDefinitionAst]
            }, $true))

    $top = foreach ($candidate in $all) {
        $nested = $false
        foreach ($other in $all) {
            if ($other -eq $candidate) { continue }

            if ($other.Extent.StartOffset -lt $candidate.Extent.StartOffset -and $other.Extent.EndOffset -gt $candidate.Extent.EndOffset) {
                $nested = $true
                break
            }
        }

        if (-not $nested) {
            $candidate
        }
    }

    return @($top)
}


# Source: src/private/GetTopLevelFunctionAstFromFile.ps1
function Get-TopLevelFunctionAstFromFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path
    )

    $parsed = Get-PowerShellAstFromFile -Path $Path
    if ($parsed.Errors -and $parsed.Errors.Count -gt 0) {
        return @()
    }

    return @(Get-TopLevelFunctionAst -Ast $parsed.Ast)
}


# Source: src/private/GetVersionLabelFromCommitMessages.ps1
function Get-VersionLabelFromCommitSet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]]$Messages
    )

    if ($Messages | Where-Object {$_ -match '(?im)BREAKING CHANGE|^[a-z]+(\(.+\))?!:'}) {
        return 'Major'
    }

    if ($Messages | Where-Object {$_ -match '(?im)^\s*feat(\(.+\))?:'}) {
        return 'Minor'
    }

    if ($Messages | Where-Object {$_ -match '(?im)^\s*fix(\(.+\))?:'}) {
        return 'Patch'
    }

    return 'Patch'
}



# Source: src/private/InitiateGitRepo.ps1
function New-InitiateGitRepo {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$DirectoryPath
    )

    # Check if Git is installed
    if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
        Write-Warning 'Git is not installed. Please install Git and initialize repo manually' 
        return
    }
    Push-Location -StackName 'GitInit'
    # Navigate to the specified directory
    Set-Location $DirectoryPath

    # Check if a Git repository already exists
    if (Test-Path -Path '.git') {
        Write-Warning 'A Git repository already exists in this directory.'
        return
    }
    if ($PSCmdlet.ShouldProcess($DirectoryPath, ("Initiating git on $DirectoryPath"))) {
        try {
            git init | Out-Null
        } catch {
            Write-Error 'Failed to initialize Git repo'
        }
    }
    Write-Verbose 'Git repository initialized successfully'
    Pop-Location -StackName 'GitInit'
}


# Source: src/private/PublishNovaBuiltModule.ps1
function Publish-NovaBuiltModule {
    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param(
        [Parameter(ParameterSetName = 'Repository')]
        [string]$Repository,

        [string]$ModuleDirectoryPath,
        [string]$ApiKey
    )

    $projectInfo = Get-MTProjectInfo

    if (-not (Test-Path -LiteralPath $projectInfo.OutputModuleDir)) {
        throw 'Dist folder is empty, build the module before running publish command'
    }

    if ($PSCmdlet.ParameterSetName -eq 'Repository') {
        $resolvedApiKey = $ApiKey
        if ($Repository -eq 'PSGallery' -and [string]::IsNullOrWhiteSpace($resolvedApiKey)) {
            $resolvedApiKey = $env:PSGALLERY_API
        }

        $publishParams = @{
            Path = $projectInfo.OutputModuleDir
            Repository = $Repository
            Verbose = $true
        }

        if (-not [string]::IsNullOrWhiteSpace($resolvedApiKey)) {
            $publishParams.ApiKey = $resolvedApiKey
        }

        Publish-PSResource @publishParams
        return
    }

    Publish-MTLocal -ModuleDirectoryPath $ModuleDirectoryPath
}



# Source: src/private/ReadAwesomeHost.ps1
function Read-AwesomeHost {
    [CmdletBinding()]
    param (
        [Parameter()]
        [pscustomobject]
        $Ask
    )
    ## For standard questions
    if ($null -eq $Ask.Choice) {
        do {
            $response = $Host.UI.Prompt($Ask.Caption, $Ask.Message, $Ask.Prompt)
        } while ($Ask.Default -eq 'MANDATORY' -and [string]::IsNullOrEmpty($response.Values))

        if ([string]::IsNullOrEmpty($response.Values)) {
            $result = $Ask.Default
        } else {
            $result = $response.Values
        }
    }
    ## For Choice based
    if ($Ask.Choice) {
        $Cs = @()
        $Ask.Choice.Keys | ForEach-Object {
            $Cs += New-Object System.Management.Automation.Host.ChoiceDescription "&$_", $($Ask.Choice.$_)
        }
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($Cs)
        $IndexOfDefault = $Cs.Label.IndexOf('&' + $Ask.Default)
        $response = $Host.UI.PromptForChoice($Ask.Caption, $Ask.Message, $options, $IndexOfDefault)
        $result = $Cs.Label[$response] -replace '&'
    }
    return $result
}

# Source: src/private/ResetProjectDist.ps1
function Reset-ProjectDist {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
    )
    $ErrorActionPreference = 'Stop'
    $data = Get-MTProjectInfo
    try {
        Write-Verbose 'Running dist folder reset'
        if (Test-Path $data.OutputDir) {
            Remove-Item -Path $data.OutputDir -Recurse -Force
        }
        # Setup Folders
        New-Item -Path $data.OutputDir -ItemType Directory -Force | Out-Null # Dist folder
        New-Item -Path $data.OutputModuleDir -Type Directory -Force | Out-Null # Module Folder
    } catch {
        Write-Error 'Failed to reset Dist folder'
    }
}

# Source: src/private/TestProjectSchema.ps1
function Test-ProjectSchema {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('Build', 'Pester')]
        [string]
        $Schema
    )
    Write-Verbose "Running Schema test against using $Schema schema"
    $SchemaPath = @{
        Build  = Get-ResourceFilePath -FileName 'Schema-Build.json'
        Pester = Get-ResourceFilePath -FileName 'Schema-Pester.json'
    }
    $result = switch ($Schema) {
        'Build' { Test-Json -Path 'project.json' -Schema (Get-Content $SchemaPath.Build -Raw) -ErrorAction Stop }
        'Pester' { Test-Json -Path 'project.json' -Schema (Get-Content $SchemaPath.Pester -Raw) -ErrorAction Stop }
        Default { $false }
    }
    return $result
}

# Source: src/private/WriteMessage.ps1
function Write-Message {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [string]
        $Text,
        [ValidateSet('Yello', 'Blue', 'Green')]
        [string]
        $color = 'Blue'
    )
    PROCESS {
        Write-Host $Text -ForegroundColor $color
    }
}