BlackBytesBox.Manifested.Initialize.ps1

function Register-LocalGalleryRepository {
    <#
    .SYNOPSIS
        Registers a local PowerShell repository for gallery modules.
 
    .DESCRIPTION
        This function ensures that the specified local repository folder exists, removes any existing
        repository with the given name, and registers the repository with a Trusted installation policy.
 
    .PARAMETER RepositoryPath
        The file system path to the local repository folder. Default is "$HOME/source/gallery".
 
    .PARAMETER RepositoryName
        The name to assign to the registered repository. Default is "LocalGallery".
 
    .EXAMPLE
        Register-LocalGalleryRepository
        Registers the local repository using the default path and repository name.
 
    .EXAMPLE
        Register-LocalGalleryRepository -RepositoryPath "C:\MyRepo" -RepositoryName "MyGallery"
        Registers the repository at "C:\MyRepo" with the name "MyGallery".
    #>

    [CmdletBinding()]
    [alias("rlgr")]
    param(
        [string]$RepositoryPath = "$HOME/source/gallery",
        [string]$RepositoryName = "LocalGallery"
    )

    # Normalize the repository path by replacing forward and backslashes with the platform's directory separator.
    $RepositoryPath = $RepositoryPath -replace '[/\\]', [System.IO.Path]::DirectorySeparatorChar

    # Ensure the local repository folder exists; if not, create it.
    if (-not (Test-Path -Path $RepositoryPath)) {
        New-Item -ItemType Directory -Path $RepositoryPath | Out-Null
    }

    # If a repository with the specified name exists, unregister it.
    if (Get-PSRepository -Name $RepositoryName -ErrorAction SilentlyContinue) {
        Write-Host "Repository '$RepositoryName' already exists. Removing it." -ForegroundColor Yellow
        Unregister-PSRepository -Name $RepositoryName
    }

    # Register the local PowerShell repository with a Trusted installation policy.
    Register-PSRepository -Name $RepositoryName -SourceLocation $RepositoryPath -InstallationPolicy Trusted

    Write-Host "Local repository '$RepositoryName' registered at: $RepositoryPath" -ForegroundColor Green
}

function Update-ManifestModuleVersion {
    <#
    .SYNOPSIS
        Updates the ModuleVersion in a PowerShell module manifest (psd1) file.
 
    .DESCRIPTION
        This function reads a PowerShell module manifest file as text, uses a regular expression to update the
        ModuleVersion value while preserving the file's comments and formatting, and writes the updated content back
        to the file. If a directory path is supplied, the function recursively searches for the first *.psd1 file and uses it.
 
    .PARAMETER ManifestPath
        The file or directory path to the module manifest (psd1) file. If a directory is provided, the function will
        search recursively for the first *.psd1 file.
 
    .PARAMETER NewVersion
        The new version string to set for the ModuleVersion property.
 
    .EXAMPLE
        PS C:\> Update-ManifestModuleVersion -ManifestPath "C:\projects\MyDscModule" -NewVersion "2.0.0"
        Updates the ModuleVersion of the first PSD1 manifest found in the given directory to "2.0.0".
    #>

    [CmdletBinding()]
    [alias("ummv")]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,

        [Parameter(Mandatory = $true)]
        [string]$NewVersion
    )

    # Check if the provided path exists
    if (-not (Test-Path $ManifestPath)) {
        throw "The path '$ManifestPath' does not exist."
    }

    # If the path is a directory, search recursively for the first *.psd1 file.
    $item = Get-Item $ManifestPath
    if ($item.PSIsContainer) {
        $psd1File = Get-ChildItem -Path $ManifestPath -Filter *.psd1 -Recurse | Select-Object -First 1
        if (-not $psd1File) {
            throw "No PSD1 manifest file found in directory '$ManifestPath'."
        }
        $ManifestPath = $psd1File.FullName
    }

    Write-Verbose "Using manifest file: $ManifestPath"

    # Read the manifest file content as text using .NET method.
    $content = [System.IO.File]::ReadAllText($ManifestPath)

    # Define the regex pattern to locate the ModuleVersion value.
    $pattern = "(?<=ModuleVersion\s*=\s*')[^']+(?=')"

    # Replace the current version with the new version using .NET regex.
    $updatedContent = [System.Text.RegularExpressions.Regex]::Replace($content, $pattern, $NewVersion)

    # Write the updated content back to the manifest file.
    [System.IO.File]::WriteAllText($ManifestPath, $updatedContent)
}

function Update-ModuleIfNewer {
    <#
    .SYNOPSIS
        Installs or updates a module from a repository only if a newer version is available.
 
    .DESCRIPTION
        This function uses Find-Module to search for a module (default repository is PSGallery) and compares the
        remote version with the locally installed version (if any) using Get-InstalledModule. If the module is not installed
        or the remote version is newer, it then installs the module using Install-Module. This prevents forcing a download
        when the installed module is already up to date.
 
    .PARAMETER ModuleName
        The name of the module to check and install/update.
 
    .PARAMETER Repository
        The repository from which to search for the module. Defaults to 'PSGallery'.
 
    .EXAMPLE
        PS C:\> Update-ModuleIfNewer -ModuleName 'STROM.NANO.PSWH.CICD'
        Searches PSGallery for the module 'STROM.NANO.PSWH.CICD' and installs it only if it is not installed or if a newer version is available.
    #>

    [CmdletBinding()]
    [alias("umn")]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ModuleName,

        [Parameter(Mandatory = $false)]
        [string]$Repository = 'PSGallery'
    )

    try {
        Write-Verbose "Searching for module '$ModuleName' in repository '$Repository'..."
        $remoteModule = Find-Module -Name $ModuleName -Repository $Repository -ErrorAction Stop

        if (-not $remoteModule) {
            Write-Error "Module '$ModuleName' not found in repository '$Repository'."
            return
        }

        $remoteVersion = [version]$remoteModule.Version

        # Check if the module is installed locally.
        $localModule = Get-InstalledModule -Name $ModuleName -ErrorAction SilentlyContinue

        if ($localModule) {
            $localVersion = [version]$localModule.Version
            if ($remoteVersion -gt $localVersion) {
                Write-Host "A newer version ($remoteVersion) is available (local version: $localVersion). Installing update..."
                Install-Module -Name $ModuleName -Repository $Repository -Force
            }
            else {
                Write-Host "The installed module ($localVersion) is up to date."
            }
        }
        else {
            Write-Host "Module '$ModuleName' is not installed. Installing version $remoteVersion..."
            Install-Module -Name $ModuleName -Repository $Repository -Force
        }
    }
    catch {
        Write-Error "An error occurred: $_"
    }
}

function Remove-OldModuleVersions {
    <#
    .SYNOPSIS
        Removes older versions of an installed PowerShell module, keeping only the latest version.
 
    .DESCRIPTION
        This function retrieves all installed versions of a specified module, sorts them by version in descending
        order (so that the latest version is first), and removes all versions except the latest one.
        It helps clean up local installations accumulated from repeated updates.
 
    .PARAMETER ModuleName
        The name of the module for which to remove older versions. Only versions beyond the latest one are removed.
 
    .EXAMPLE
        PS C:\> Remove-OldModuleVersions -ModuleName 'STROM.NANO.PSWH.CICD'
        Removes all installed versions of 'STROM.NANO.PSWH.CICD' except for the latest version.
    #>

    [CmdletBinding()]
    [alias("romv")]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    try {
        # Retrieve all installed versions of the module.
        $installedModules = Get-InstalledModule -Name $Name -AllVersions -ErrorAction SilentlyContinue

        if (-not $installedModules) {
            Write-Host "No installed module found with the name '$Name'." -ForegroundColor Yellow
            return
        }

        # Sort installed versions descending; latest version comes first.
        $sortedModules = $installedModules | Sort-Object -Property Version -Descending

        # Retain the latest version (first item) and select all older versions.
        $latestModule = $sortedModules[0]
        $oldModules = $sortedModules | Select-Object -Skip 1

        if (-not $oldModules) {
            Write-Host "No older versions of '$Name' to remove." -ForegroundColor Green
            return
        }

        foreach ($module in $oldModules) {
            Write-Host "Removing $Name version $($module.Version)..." -ForegroundColor Cyan
            Uninstall-Module -Name $Name -RequiredVersion $module.Version -Force
        }

        Write-Host "Cleaned up '$Name'; latest ($($latestModule.Version)) kept."
        
    }
    catch {
        Write-Error "An error occurred while removing old versions: $_"
    }
}

function Install-UserModule {
    <#
    .SYNOPSIS
        Installs one or more modules for the *current* user.
     
    .DESCRIPTION
        Thin wrapper around Install‑Module that forces `-Scope CurrentUser`
        but otherwise behaves just like the original cmdlet.
     
    .PARAMETER Name
        The module name(s) to install.
     
    .PARAMETER Force
        Suppresses all prompts, mirroring Install‑Module’s -Force switch.
     
    .EXAMPLE
        Install-UserModule -Name Pester -Force
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string[]] $Name,
    
        [switch] $Force
    )
    
    # Inject / override the scope and forward everything else
    $PSBoundParameters['Scope'] = 'CurrentUser'
    Install-Module @PSBoundParameters
}   
    
function Initialize-DotNet {
    <#
    .SYNOPSIS
        Installs specified .NET channels and sets environment variables for both the current session and the user profile.
 
    .DESCRIPTION
        This function performs the following actions:
         
          1. For each provided channel (defaulting to 8.0 and 9.0 if none are specified):
             - Sets TLS12 as the security protocol.
             - Downloads and executes the dotnet-install.ps1 script using Invoke-WebRequest with RawContent,
               and passes the -channel parameter.
           
          2. Sets the DOTNET_ROOT environment variable to "$HOME\.dotnet" for the user and current session.
          3. Updates the user's PATH environment variable to include both DOTNET_ROOT and the tools folder
             ("$HOME\.dotnet\tools") and updates the current session PATH accordingly.
 
    .PARAMETER Channels
        An array of .NET channels to install. If omitted, the function defaults to installing channels 8.0 and 9.0.
 
    .EXAMPLE
        PS C:\> Initialize-DotNet
        Installs .NET channels 8.0 and 9.0, and configures the environment variables for immediate and persistent use.
 
    .EXAMPLE
        PS C:\> Initialize-DotNet -Channels @("2.1","2.2","3.0","3.1","5.0", "6.0", "7.0", "8.0", "9.0")
        Installs the specified .NET channels and configures the environment variables.
    #>

    [CmdletBinding()]
    [alias("idot")]
    param(
        [string[]]$Channels = @("8.0", "9.0")
    )

    $dotnetInstallUrl = 'https://dot.net/v1/dotnet-install.ps1'

    foreach ($channel in $Channels) {
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Write-Host "Installing .NET channel $channel..." -ForegroundColor Cyan
        & ([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing $dotnetInstallUrl))) -channel $channel -InstallDir "$HOME\.dotnet"
    }

    # Set DOTNET_ROOT environment variable for both persistent and current session.
    $dotnetRoot = "$HOME\.dotnet"
    [Environment]::SetEnvironmentVariable('DOTNET_ROOT', $dotnetRoot, 'User')
    $env:DOTNET_ROOT = $dotnetRoot
    Write-Host "DOTNET_ROOT set to $dotnetRoot" -ForegroundColor Green
    [Environment]::SetEnvironmentVariable('DOTNET_CLI_TELEMETRY_OPTOUT', 'true', 'User')
    $env:DOTNET_CLI_TELEMETRY_OPTOUT = 'true'
    Write-Host "DOTNET_CLI_TELEMETRY_OPTOUT set to true" -ForegroundColor Green

    # Define the tools folder.
    $toolsFolder = "$dotnetRoot\tools"

    # Update PATH to include DOTNET_ROOT and the tools folder for persistent storage.
    $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
    $pathsToAdd = @()

    if (-not $currentPath.ToLower().Contains($dotnetRoot.ToLower())) {
        $pathsToAdd += $dotnetRoot
    }
    if (-not $currentPath.ToLower().Contains($toolsFolder.ToLower())) {
        $pathsToAdd += $toolsFolder
    }
    if ($pathsToAdd.Count -gt 0) {
        $newPath = "$currentPath;" + ($pathsToAdd -join ';')
        [Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
        # Also update the current session's PATH immediately.
        $env:PATH = $newPath
        Write-Host "PATH updated to include: $($pathsToAdd -join ', ')" -ForegroundColor Green
    }
    else {
        Write-Host "PATH already contains DOTNET_ROOT and tools folder." -ForegroundColor Yellow
    }
}

function Initialize-NugetRepositoryDotNet {
    <#
    .SYNOPSIS
        Initializes a NuGet package source using the dotnet CLI.
 
    .DESCRIPTION
        This function manages a single NuGet source using the dotnet CLI. It retrieves the currently registered
        sources via 'dotnet nuget list source' and checks if the provided source (by Name and Location) exists.
        If the source is not found, it registers it. If the source is found but is marked as [Disabled],
        it removes and then re-adds the source as enabled.
        Additionally, if the Location is a local path (not a URL), it ensures the directory exists by creating it if necessary.
 
    .EXAMPLE
        Initialize-NugetRepositoryDotNet -Name "nuget.org" -Location "https://api.nuget.org/v3/index.json"
        This will verify that the NuGet source for nuget.org is registered and enabled.
    #>

    [CmdletBinding()]
    [alias("inugetx")]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [string]$Location
    )

    # Check if the Location is a URL; if not, treat it as a local directory.
    if ($Location -notmatch '^https?://') {
        $Location = $Location -replace '[/\\]', [System.IO.Path]::DirectorySeparatorChar
        Write-Host "Provided Location '$Location' is a local path." -ForegroundColor Cyan
        if (-not (Test-Path $Location)) {
            Write-Host "Local path '$Location' does not exist. Creating directory." -ForegroundColor Cyan
            New-Item -ItemType Directory -Path $Location | Out-Null
        }
    }

    Write-Host "Retrieving registered NuGet sources using dotnet CLI..." -ForegroundColor Cyan
    $listOutput = dotnet nuget list source 2>&1
    $lines = $listOutput -split "`n"

    $foundIndex = $null
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match [regex]::Escape($Location)) {
            $foundIndex = $i
            break
        }
    }

    if ($foundIndex -ne $null) {
        # Assume the preceding line contains the name and status, e.g., " 1. nuget.org [Enabled]"
        $statusLine = if ($foundIndex -gt 0) { $lines[$foundIndex - 1] } else { "" }
        if ($statusLine -match '^\s*\d+\.\s*(?<Name>\S+)\s*\[(?<Status>\w+)\]') {
            $registeredName = $Matches["Name"]
            $status = $Matches["Status"]
            if ($status -eq "Disabled") {
                Write-Host "Source '$registeredName' ($Location) is disabled. Removing and re-adding it as enabled." -ForegroundColor Yellow
                dotnet nuget remove source $registeredName
                Write-Host "Adding source '$Name' with URL '$Location'." -ForegroundColor Green
                dotnet nuget add source $Location --name $Name
            }
            else {
                Write-Host "Source '$registeredName' with URL '$Location' is already registered and enabled. Skipping." -ForegroundColor Yellow
            }
        }
        else {
            Write-Host "Could not parse status for source with URL '$Location'. Skipping." -ForegroundColor Red
        }
    }
    else {
        Write-Host "Source '$Name' not found. Registering it." -ForegroundColor Green
        dotnet nuget add source $Location --name $Name
    }
}

function Initialize-NugetRepositories {
    <#
    .SYNOPSIS
        Initializes the default NuGet package sources.
 
    .DESCRIPTION
        This function registers the default NuGet package sources if they are not already present.
        It uses enhanced logic: if a repository with a matching URL exists but is not trusted,
        it will be re-registered with the Trusted flag. If the repository exists and is already trusted,
        it is skipped.
 
    .EXAMPLE
        Init-NugetRepositorys
        Initializes and registers the default NuGet package sources, ensuring they are trusted.
    #>

    [CmdletBinding()]
    [alias("inuget")]
    param()
    # Define the default NuGet repository sources.
    $defaultSources = @(
        [PSCustomObject]@{ Name = "nuget.org";         Location = "https://api.nuget.org/v3/index.json" },
        [PSCustomObject]@{ Name = "int.nugettest.org"; Location = "https://apiint.nugettest.org/v3/index.json" }
    )

    # Retrieve the currently registered NuGet package sources.
    $existingSources = Get-PackageSource -ProviderName NuGet -ErrorAction SilentlyContinue

    foreach ($source in $defaultSources) {
        $found = $existingSources | Where-Object { $_.Location -eq $source.Location }
        if ($found) {
            # Check if the found source is trusted.
            if (-not $($found.IsTrusted)) {
                Write-Host "Repository '$($source.Name)' exists but is not trusted. Updating trust setting." -ForegroundColor Yellow
                # Unregister the untrusted source and re-register it with the Trusted flag.
                Unregister-PackageSource -Name $found.Name -ProviderName NuGet -Force -ErrorAction SilentlyContinue
                Register-PackageSource -Name $source.Name -Location $source.Location -ProviderName NuGet -Trusted
            }
            else {
                Write-Host "Repository '$($source.Name)' with URL '$($source.Location)' is already registered and trusted. Skipping." -ForegroundColor Yellow
            }
        }
        else {
            Write-Host "Registering repository '$($source.Name)' with URL '$($source.Location)'." -ForegroundColor Green
            Register-PackageSource -Name $source.Name -Location $source.Location -ProviderName NuGet -Trusted | Out-Null
        }
    }
}

function Test-IsWindows {
    <#
    .SYNOPSIS
        Returns $True if PowerShell is running on Windows (any edition or version).
 
    .DESCRIPTION
        Combines .NET RuntimeInformation (if present) with a PlatformID fallback
        to detect Windows accurately across PowerShell Desktop and Core.
 
    .OUTPUTS
        Boolean: $True on Windows, $False otherwise.
    #>

    [alias("iswin")]
    param()
    # Determine if the RuntimeInformation type exists
    if ([Type]::GetType('System.Runtime.InteropServices.RuntimeInformation', $false)) {
        # Use cross-platform API
        return [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
            [System.Runtime.InteropServices.OSPlatform]::Windows
        )
    }
    else {
        # Fallback for older Windows PowerShell: check PlatformID
        return (
            [Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT
        )
    }
}

function Write-LogInline {
    <#
    .SYNOPSIS
        Writes a timestamped, color‑coded inline log entry to the console, optionally appends to a daily log file, and optionally returns log details as JSON.
 
    .DESCRIPTION
        Formats messages with a high-precision timestamp, log-level abbreviation, and caller identifier. Color-codes console output by severity, can overwrite the previous line, and can append to a per-process daily log file.
        Use -ReturnJson to emit a JSON representation of the log details instead of returning nothing.
 
    .PARAMETER Level
        The log level. Valid values: Verbose, Debug, Information, Warning, Error, Critical.
 
    .PARAMETER MinLevel
        Minimum level to write to the console. Messages below this level are suppressed. Default: Information.
 
    .PARAMETER FileMinLevel
        Minimum level to append to the log file. Messages below this level are skipped. Default: Verbose.
 
    .PARAMETER Template
        The message template, using placeholders like {Name}.
 
    .PARAMETER Params
        Values for each placeholder in Template. Either a hashtable or an ordered object array.
 
    .PARAMETER UseBackColor
        Switch to enable background coloring in the console.
 
    .PARAMETER Overwrite
        Switch to overwrite the previous console entry rather than writing a new line.
 
    .PARAMETER InitialWrite
        Switch to output an initial blank line instead of attempting to overwrite on the first call when using -Overwrite.
 
    .PARAMETER FileAppName
        When set, enables file logging under:
        %LOCALAPPDATA%\Write-LogInline\<FileAppName>\<yyyy-MM-dd>_<PID>.log
 
    .PARAMETER ReturnJson
        Switch to return the log details as a JSON-formatted string; otherwise, no output.
 
    .EXAMPLE
        # Write a green "Hello, World!" message to the console
        Write-LogInline -Level Information `
                    -Template "{greeting}, {user}!" `
                    -Params @{ greeting = "Hello"; user = "World" }
 
    .EXAMPLE
        # Using defaults plus -ReturnJson
        $WriteLogInlineDefaults = @{
            FileMinLevel = 'Verbose'
            MinLevel = 'Information'
            UseBackColor = $false
            Overwrite = $true
            FileAppName = 'testing'
            ReturnJson = $false
        }
 
        Write-LogInline -Level Verbose `
                    -Template "{hello}-{world} number {num} at {time}!" `
                    -Params "Hello","World",1,1.2 `
                    @WriteLogInlineDefaults
 
    .NOTES
        Requires PowerShell 5.0 or later.
    #>

    [CmdletBinding()]
    [alias("wlog")]
    param(
        [ValidateSet('Verbose','Debug','Information','Warning','Error','Critical')][string]$Level,
        [ValidateSet('Verbose','Debug','Information','Warning','Error','Critical')][string]$MinLevel       = 'Information',
        [ValidateSet('Verbose','Debug','Information','Warning','Error','Critical')][string]$FileMinLevel  = 'Verbose',
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()] [string]$Template,
        [object]$Params,
        [switch]$UseBackColor,
        [switch]$Overwrite,
        [switch]$InitialWrite,
        [string]$FileAppName,
        [switch]$ReturnJson
    )

    # Normalize any non-hashtable, non-array to a one‐item array
    if ($Params -isnot [hashtable] -and $Params -isnot [object[]]) {
        $Params = @($Params)
    }

    # Now enforce flatness on arrays
    if ($Params -is [object[]] -and ($Params |
    Where-Object { $_ -is [System.Collections.IEnumerable] -and -not ($_ -is [string]) }
    )) {
        throw "Parameter -Params array must be flat (no nested collections)."
    }

    # ANSI escape
    $esc = [char]27
    if (-not $script:WLI_Caller) {
        $script:WLI_Caller = if ($MyInvocation.PSCommandPath) { Split-Path -Leaf $MyInvocation.PSCommandPath } else { 'Console' }
    }
    $caller = $script:WLI_Caller

    # Level maps
    $levelValues = @{ Verbose=0; Debug=1; Information=2; Warning=3; Error=4; Critical=5 }
    $abbrMap      = @{ Verbose='VRB'; Debug='DBG'; Information='INF'; Warning='WRN'; Error='ERR'; Critical='CRT' }

    $writeConsole = $levelValues[$Level] -ge $levelValues[$MinLevel]
    $writeToFile  = $FileAppName -and ($levelValues[$Level] -ge $levelValues[$FileMinLevel])
    if (-not ($writeConsole -or $writeToFile)) { return }

    # File path init
    if ($writeToFile) {
        $os = [int][System.Environment]::OSVersion.Platform
        switch ($os) {
            2 { $base = $env:LOCALAPPDATA } # Win32NT
            4 { $base = Join-Path $env:HOME ".local/share" } # Unix
            6 { $base = Join-Path $env:HOME ".local/share" } # MacOSX
            default { throw "Unsupported OS platform: $os" }
        }
        $root = Join-Path $base "Write-LogInline/$FileAppName"

        if (-not (Test-Path $root)) { New-Item -Path $root -ItemType Directory | Out-Null }
        $date    = Get-Date -Format 'yyyy-MM-dd'
        $logPath = Join-Path $root "${date}_${PID}.log"
    }

    # Timestamp and render
    $timeEntry = Get-Date
    $timeStr   = $timeEntry.ToString('yyyy-MM-dd HH:mm:ss:ff')
    $plMatches = [regex]::Matches($Template, '{(?<name>\w+)}')
    $keys      = $plMatches | ForEach-Object { $_.Groups['name'].Value } | Select-Object -Unique
    $wasHash    = $Params -is [hashtable]
    $paramArray = @($Params)

    if (-not $wasHash -and $paramArray.Count -lt $keys.Count) {
        throw "Insufficient parameters: expected $($keys.Count), received $($paramArray.Count)"
    }

    $keys = @($keys)
    if ($wasHash) {
        $map = $Params
    } else {
        $map = @{}
        for ($i = 0; $i -lt $keys.Count; $i++) { $map[$keys[$i]] = $paramArray[$i] }  # CHANGED: use paramArray
    }

    # Fix: cast null to empty string, avoid boolean -or misuse
    $msg = $Template
    foreach ($k in $keys) {
        $escName = [regex]::Escape($k)
        $msg = $msg -replace "\{$escName\}", [string]$map[$k]
    }
    $rawLine = "[$timeStr $($abbrMap[$Level])][$caller] $msg"

    # Write to file
    if ($writeToFile) { $rawLine | Out-File -FilePath $logPath -Append -Encoding UTF8 }

    # Console output
    if ($writeConsole) {
        if ($InitialWrite) {
            # Initial invocation: write a blank line instead of overwriting
            Write-Host ""
        }
        if ($Overwrite) {
            for ($i = 0; $i -lt $script:WLI_LastLines; $i++) {
                Write-Host -NoNewline ($esc + '[1A' + "`r" + $esc + '[K')
            }
        }
        Write-Host -NoNewline ($esc + '[?25l')

        # Color maps
        $levelMap = @{
            Verbose     = @{ Abbrev='VRB'; Fore='DarkGray' }
            Debug       = @{ Abbrev='DBG'; Fore='Cyan'     }
            Information = @{ Abbrev='INF'; Fore='Green'    }
            Warning     = @{ Abbrev='WRN'; Fore='Yellow'   }
            Error       = @{ Abbrev='ERR'; Fore='Red'      }
            Critical    = @{ Abbrev='CRT'; Fore='White'; Back='DarkRed' }
        }
        $typeColorMap = @{
            'System.String'   = 'Green';   'System.DateTime' = 'Yellow'
            'System.Int32'    = 'Cyan';    'System.Int64'     = 'Cyan'
            'System.Double'   = 'Blue';    'System.Decimal'   = 'Blue'
            'System.Boolean'  = 'Magenta'; 'Default'          = 'White'
            'System.Version'  = 'Magenta'; 'Microsoft.PackageManagement.Internal.Utility.Versions.FourPartVersion' = 'Magenta'
            'Microsoft.PowerShell.ExecutionPolicy' = 'Magenta'
            'System.Management.Automation.ActionPreference' = 'Green'
        }
        $staticFore = 'White'; $staticBack = 'Black'
        function Write-Colored { param($Text,$Fore,$Back) if ($UseBackColor -and $Back) { Write-Host -NoNewline $Text -ForegroundColor $Fore -BackgroundColor $Back } else { Write-Host -NoNewline $Text -ForegroundColor $Fore } }

        # Header
        $entry = $levelMap[$Level]
        $tag   = $entry.Abbrev
        if ($entry.ContainsKey('Back')) {
            $lvlBack = $entry.Back
        } elseif ($UseBackColor) {
            $lvlBack = $staticBack
        } else {
            $lvlBack = $null
        }
        Write-Colored '[' $staticFore $staticBack; Write-Colored $timeStr $staticFore $staticBack; Write-Colored ' ' $staticFore $staticBack
        if ($lvlBack) { Write-Host -NoNewline $tag -ForegroundColor $entry.Fore -BackgroundColor $lvlBack } else { Write-Host -NoNewline $tag -ForegroundColor $entry.Fore }
        Write-Colored '] [' $staticFore $staticBack; Write-Colored $caller $staticFore $staticBack; Write-Colored '] ' $staticFore $staticBack

        # Message parts
        $pos = 0
        foreach ($m in $plMatches) {
            if ($m.Index -gt $pos) {
                Write-Colored $Template.Substring($pos, $m.Index - $pos) $staticFore $staticBack
            }
            $val = $map[$m.Groups['name'].Value]
            $t   = $val.GetType().FullName

            if ($typeColorMap.ContainsKey($t)) {
                $f = $typeColorMap[$t]
            } else {
                $f = $typeColorMap['Default']
            }

            if ($UseBackColor) {
                $b = $staticBack
            } else {
                $b = $null
            }

            Write-Colored $val $f $b
            $pos = $m.Index + $m.Length
        }

        if ($pos -lt $Template.Length) {
            if ($UseBackColor) {
                $b = $staticBack
            } else {
                $b = $null
            }
            Write-Colored $Template.Substring($pos) $staticFore $b
        }

        Write-Host ''
        Write-Host -NoNewline ($esc + '[?25h')

        try {
            $width = $Host.UI.RawUI.WindowSize.Width
        } catch {
            $width = 80
        }

        $script:WLI_LastLines = [math]::Ceiling($rawLine.Length / ($width - 1))
    }

    # Return JSON
    $output = [PSCustomObject]@{
        DateTime   = $timeEntry
        PID        = $PID
        Level      = $Level
        Template   = $Template
        Message    = $msg
        Parameters = $map
    }

    # Return JSON only if requested
    if ($ReturnJson) {
        return $output | ConvertTo-Json -Depth 5
    }
}

function Enable-TemporaryUserScriptExecution {
    <#
    .SYNOPSIS
        Temporarily enables script and module execution by setting a permissive execution policy for the CurrentUser, while capturing the original policy.
    .DESCRIPTION
        Retrieves the CurrentUser execution policy. If it's not already permissive, sets it to RemoteSigned. Returns the original policy for later restoration.
    .EXAMPLE
        # Capture and enable script execution temporarily
        $originalPolicy = Enable-TemporaryUserScriptExecution
    #>

    [CmdletBinding()]
    [alias("etse")]
    param()
    # Constant list of policies that allow scripts/modules
    $allowedPolicies = @('RemoteSigned','Unrestricted','Bypass')

    $WriteLogInlineDefaults = @{
        FileMinLevel  = 'Error'
        MinLevel      = 'Information'
        UseBackColor  = $false
        Overwrite     = $false
        FileAppName   = $null
        ReturnJson    = $false
    }

    # Capture the current CurrentUser policy
    $originalPolicy = Get-ExecutionPolicy -Scope CurrentUser

            # Log the invocation attempt
    
    try {
        Write-LogInline -Level Information -Template "Attempting to enable temporary script execution for CurrentUser scope..." @WriteLogInlineDefaults

        Write-LogInline -Level Information -Template "Current CurrentUser policy is '{0}'." -Params $originalPolicy @WriteLogInlineDefaults

        if ($allowedPolicies -contains $originalPolicy) {
            Write-LogInline -Level Information -Template "Policy is already permissive; no change needed." @WriteLogInlineDefaults
        }
        else {
            $targetPolicy = $allowedPolicies[0]  # RemoteSigned
            Write-LogInline -Level Information -Template "Temporarily setting CurrentUser execution policy to '{0}'." -Params $targetPolicy @WriteLogInlineDefaults
            Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy $targetPolicy -Force
            Write-LogInline -Level Information -Template "Execution policy now '{0}' for temporary script execution." -Params $targetPolicy  @WriteLogInlineDefaults
        }
    }
    catch {
        Write-LogInline -Level Error -Template "Error enabling temporary script execution: $_" @WriteLogInlineDefaults
        throw
    }

    # Return the original for restoration
    return $originalPolicy
}

function Restore-OriginalUserScriptExecution {
    <#
    .SYNOPSIS
        Restores a previously captured CurrentUser execution policy to its original state, if needed.
    .DESCRIPTION
        Compares the CurrentUser execution policy with the given original. If they differ, restores the policy. Otherwise logs that no change is needed.
    .EXAMPLE
        # Restore policy after temporary change
        Restore-OriginalUserScriptExecution -Policy $originalPolicy
    #>

    [CmdletBinding()]
    [alias("rotse")]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Policy
    )

    $WriteLogInlineDefaults = @{
        FileMinLevel  = 'Error'
        MinLevel      = 'Information'
        UseBackColor  = $false
        Overwrite     = $false
        FileAppName   = $null
        ReturnJson    = $false
    }
    
    # Get the current policy to decide if restoration is needed
    $currentPolicy = Get-ExecutionPolicy -Scope CurrentUser

    try {
        if ($currentPolicy -eq $Policy) { 
            Write-LogInline -Level Information -Template "CurrentUser policy restore is already '{0}' (desired was '{1}'); no restore needed." ` -Params $currentPolicy, $Policy @WriteLogInlineDefaults

        }
        else {
            Write-LogInline -Level Information -Template "Restoring CurrentUser execution policy to '{0}'." -Params $currentPolicy @WriteLogInlineDefaults
            Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy $currentPolicy -Force
            $currentPolicy = Get-ExecutionPolicy -Scope CurrentUser
            Write-LogInline -Level Information -Template "Execution policy restored to '{0}'." -Params $currentPolicy @WriteLogInlineDefaults
        }
    }
    catch {
        Write-LogInline -Level Error -Template "Failed to restore execution policy: $_" @WriteLogInlineDefaults
        throw
    }
}

function Invoke-IsolatedScript {
    <#
    .SYNOPSIS
        Executes a given PowerShell script block in an isolated shell and returns output and errors.
 
    .DESCRIPTION
        This function executes the provided PowerShell script block in a completely isolated and fresh PowerShell environment.
        It captures standard output and errors internally and returns them as structured results.
 
        Useful when script execution must avoid interference from existing loaded modules or persistent states.
 
    .PARAMETER ScriptBlock
        The PowerShell script block containing commands to execute.
 
    .EXAMPLE
        $result = Invoke-IsolatedScript -ScriptBlock { Remove-OldModuleVersions -Name 'Example.Module' }
        if ($result.Output) { $result.Output | ForEach-Object { Write-Host $_ } }
        if ($result.Errors) { $result.Errors | ForEach-Object { Write-Error $_ } }
 
    .RETURNS
        [PSCustomObject] containing Output and Errors arrays.
    #>

    [CmdletBinding()]
    [alias("iis")]
    param (
        [Parameter(Mandatory = $true)]
        [ScriptBlock]$ScriptBlock
    )

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = 'powershell.exe'
    $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -Command $($ScriptBlock.ToString())"
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.UseShellExecute = $false
    $psi.CreateNoWindow = $true

    $process = [System.Diagnostics.Process]::Start($psi)
    $output = $process.StandardOutput.ReadToEnd()
    $errorOutput = $process.StandardError.ReadToEnd()
    $process.WaitForExit()

    # Process and structure output
    $outputLines = $output -split "`n" | ForEach-Object { $_.TrimEnd() } | Where-Object { $_ -ne '' }
    $errorLines = $errorOutput -split "`n" | ForEach-Object { $_.TrimEnd() } | Where-Object { $_ -ne '' }

    return [PSCustomObject]@{
        Output = $outputLines
        Errors = $errorLines
    }
}