Functions/GenXdev.Helpers/Invoke-OnEachGenXdevModule.ps1

################################################################################
<#
.SYNOPSIS
Executes a script block on each GenXdev module in the workspace.
 
.DESCRIPTION
This function iterates through GenXdev modules in the workspace and executes
a provided script block against each module. It can filter modules by name
pattern, exclude local modules, include only published modules, or process
scripts instead of modules. The function automatically navigates to the
correct module directory before executing the script block.
 
.PARAMETER Script
The script block to execute for each GenXdev module. The module object is
passed as an argument to the script block.
 
.PARAMETER ModuleName
Filter to apply to module names. Supports wildcards and multiple patterns.
Defaults to 'GenXdev*' to include all GenXdev modules.
 
.PARAMETER NoLocal
Excludes local development modules from processing.
 
.PARAMETER OnlyPublished
Includes only published modules that have LICENSE and README.md files.
 
.PARAMETER FromScripts
Process scripts directory instead of module directories.
 
.PARAMETER IncludeScripts
Includes the scripts directory in addition to regular modules.
 
.PARAMETER IncludeGenXdevMainModule
Includes the main GenXdev module in addition to sub-modules.
 
.EXAMPLE
Invoke-OnEachGenXdevModule -Script { Write-Host $args[0].Name }
 
Lists all GenXdev module names.
 
.EXAMPLE
foreach-genxdev-module-do {
    param($ModuleObj, $isScriptsFolder, $isSubModule, $subModuleName)
    Get-ChildItem
} -ModuleName "GenXdev.AI"
 
Uses alias to list contents of the GenXdev.AI module directory.
#>

function Invoke-OnEachGenXdevModule {

    [CmdletBinding()]
    [Alias('foreach-genxdev-module-do')]

    param(
        ###############################################################################
        [Parameter(
            Mandatory = $true,
            Position = 0,
            HelpMessage = ('The script block to execute for each ' +
                          'GenXdev module')
        )]
        [Alias('ScriptBlock')]
        [scriptblock] $Script,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            Position = 1,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'Filter to apply to module names'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('Module', 'BaseModuleName', 'SubModuleName')]
        [ValidatePattern('^(GenXdev|GenXde[v]\*|GenXdev(\.\w+)+)+$')]
        [SupportsWildcards()]
        [string[]] $ModuleName = @('GenXdev*'),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Excludes local development modules from processing'
        )]
        [switch] $NoLocal,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Includes only published modules that have LICENSE ' +
                          'and README.md files')
        )]
        [switch] $OnlyPublished,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Process scripts directory instead of module directories'
        )]
        [switch] $FromScripts,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Includes the scripts directory in addition to ' +
                          'regular modules')
        )]
        [switch] $IncludeScripts,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Includes the main GenXdev module in addition to ' +
                          'sub-modules')
        )]
        [switch] $IncludeGenXdevMainModule
    )

    begin {

        # resolve the scripts directory path relative to current module
        $scriptsPath = GenXdev.FileSystem\Expand-Path `
            "$PSScriptRoot\..\..\..\..\..\Scripts\" `
            -CreateDirectory

        # resolve the modules directory path relative to current module
        $modulesPath = GenXdev.FileSystem\Expand-Path `
            "$PSScriptRoot\..\..\..\..\" `
            -CreateDirectory

        # override module name filter when processing scripts only
        if ($FromScripts) {

            $ModuleName = @('GenXdev.Scripts')
        }

        # output progress information for troubleshooting
        Microsoft.PowerShell.Utility\Write-Verbose (
            "Processing modules with base names: $($ModuleName -join ', ')")
    }

    process {

        $allGenXdevModules = @(GenXdev.FileSystem\Find-Item "$modulesPath\GenXdev*\1.238.2025\GenXdev*.psm1" -PassThru -NoRecurse | Microsoft.PowerShell.Core\ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_.FullName) }) +
            @("GenXdev.Scripts")

        $genXdevModules = $allGenXdevModules

        if ($ModuleName.Count -eq 1 -and $ModuleName[0] -eq 'GenXdev*') {

            $genXdevModules = @($allGenXdevModules | Microsoft.PowerShell.Core\ForEach-Object {

                if ((-not ($IncludeScripts -or $FromScripts)) -and ($_ -like "GenXdev.Scripts")) {

                    return;
                }

                $parts = $_.Split('.')

                if ((($parts.Length -eq 1) -and $IncludeGenXdevMainModule) -or
                    ($parts.Length -eq 2)) {

                    $_
                }
            })
        }

        # iterate through each specified module name pattern
        $uniqueModulesNames = @($ModuleName | Microsoft.PowerShell.Core\ForEach-Object {

            $moduleToFind = $_

            foreach ($module in $genXdevModules) {

                if ($module -like $moduleToFind) {

                    $module
                }
            }
        } |
        Microsoft.PowerShell.Utility\Select-Object -Unique |
        Microsoft.PowerShell.Utility\Sort-Object {

            $_.Length.ToString().PadLeft(5, '0')+"_"+$_.ToLower()
        })

        foreach ($nextModule in $uniqueModulesNames) {

            # split module name into parts for pattern matching
            $partsObj = $nextModule.Split(".")

            # output current module pattern being processed
            Microsoft.PowerShell.Utility\Write-Verbose (
                "Processing module pattern: $nextModule")

            # define internal function to process individual modules
            function processModule {

                param($ModuleObj, $parts)

                # construct expected license file path for published modules
                $licenseFilePath = (
                    "$($ModuleObj.FullName)\1.238.2025\LICENSE")

                # construct expected readme file path for published modules
                $readmeFilePath = (
                    "$($ModuleObj.FullName)\1.238.2025\README.md")

                # skip license and readme checks for scripts directory
                if ($ModuleObj.FullName -eq $scriptsPath) {

                    $licenseFilePath = $null
                    $readmeFilePath = $null
                }

                # determine if module should be processed based on all filters
                $shouldProcess = (
                    ($ModuleObj.Name -like (
                        $IncludeGenXdevMainModule ? 'GenXdev*' : 'GenXdev.*')) -and
                    ((-not $NoLocal) -or ($ModuleObj.Name -notlike '*.Local*')) -and
                    ((-not $OnlyPublished) -or (
                        ($ModuleObj.Name -notlike '*Local*') -and
                        ($ModuleObj.Name -notlike '*Local') -and
                        ($ModuleObj.Name -notlike 'GenXdev.Scripts') -and
                        [IO.File]::Exists($licenseFilePath) -and
                        [IO.File]::Exists($readmeFilePath))))

                # process module if it passes all filters
                if ($shouldProcess) {

                    # output module name being processed
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Processing module: $($ModuleObj.Name)")

                    # save current location to restore after processing
                    $originalLocation = (
                        Microsoft.PowerShell.Management\Get-Location).Path

                    try {

                        # handle scripts directory with special logic
                        if ($ModuleObj.Name -like 'GenXdev.Scripts') {

                            # navigate directly to scripts directory
                            Microsoft.PowerShell.Management\Set-Location `
                                -LiteralPath $scriptsPath
                        }
                        else {

                            # initialize version variable for parsing
                            [version] $version = $null

                            # navigate to module root directory first
                            Microsoft.PowerShell.Management\Set-Location `
                                -LiteralPath "$($ModuleObj.FullName)"

                            # find the latest version directory for the module
                            $versionDirectory = (
                                Microsoft.PowerShell.Management\Get-ChildItem `
                                    -LiteralPath '.\' `
                                    -Filter "*.*.*" `
                                    -Directory `
                                    -ErrorAction SilentlyContinue |
                                Microsoft.PowerShell.Core\Where-Object {
                                    [Version]::TryParse($_.Name, [ref]$version)
                                } |
                                Microsoft.PowerShell.Utility\Sort-Object {
                                    [Version]::new($_.Name)
                                } -Descending |
                                Microsoft.PowerShell.Utility\Select-Object `
                                    -First 1 |
                                Microsoft.PowerShell.Core\ForEach-Object {
                                    $_.FullName
                                })

                            # skip module if no valid version directory found
                            if ($null -eq $versionDirectory) {

                                # output warning for modules without version dirs
                                Microsoft.PowerShell.Utility\Write-Verbose (
                                    "No version directory found for " +
                                    "$($ModuleObj.Name)")

                                return
                            }

                            # navigate to the specific version directory
                            Microsoft.PowerShell.Management\Set-Location `
                                -LiteralPath $versionDirectory

                            # output current working directory for debugging
                            Microsoft.PowerShell.Utility\Write-Verbose (
                                "Changed to directory: $versionDirectory")
                        }

                        Microsoft.PowerShell.Utility\Write-Progress `
                            -Activity "Processing modules" `
                            -CurrentOperation "Processing $($ModuleObj.Name)" `
                            -Status "Executing script block"

                        # execute the user script with module context parameters
                        Microsoft.PowerShell.Core\Invoke-Command `
                            -ScriptBlock $Script `
                            -ArgumentList @(
                                ($ModuleObj | Microsoft.PowerShell.Utility\ConvertTo-Json | Microsoft.PowerShell.Utility\ConvertFrom-Json),
                                ($ModuleObj.Name -eq "GenXdev.Scripts"),
                                ($parts.Length -gt 2),
                                ($parts -join '.')) `
                            -NoNewScope
                    }
                    finally {

                        # always restore original working directory
                        Microsoft.PowerShell.Management\Set-Location `
                            -LiteralPath $originalLocation
                    }
                }
                else {

                    # output information about skipped modules
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Skipping module: $($ModuleObj.Name) (filtered out)")
                }
            }

            # handle scripts directory as a special case
            if ($nextModule -like 'GenXdev.Scripts') {

                # create mock module object for scripts directory
                processModule -parts $partsObj -ModuleObj @{
                    Name     = 'GenXdev.Scripts'
                    FullName = $scriptsPath
                }

                continue
            }

            try {

                # process the matching module
                processModule -parts $partsObj -ModuleObj @{
                    Name     = ($partsObj.Length -lt 2 ? $partsObj[0] : ($partsObj[0]+"."+$partsObj[1]))
                    FullName = "$modulesPath\$($partsObj.Length -lt 2 ? $partsObj[0] : ($partsObj[0]+"."+$partsObj[1]))"
                }
            }
            catch {

                # output any errors encountered during processing
                Microsoft.PowerShell.Utility\Write-Error `
                    $_.Exception.Message
            }
        }
    }

    end {

        Microsoft.PowerShell.Utility\Write-Progress `
            -Activity "Processing modules" `
            -Status "Completed" `
            -Completed
    }
}
################################################################################