Private/ExtensionLoader.ps1

# Module-scoped state for loaded extensions
$script:LoadedExtensions = @()

function Register-LedgerExtension {
    <#
    .SYNOPSIS
    Registers a loaded extension in the tracking list (bookkeeping only).
    #>

    param (
        [string]$Name,
        [string]$Path,
        [string]$Source,
        [string[]]$Functions
    )

    $script:LoadedExtensions += [PSCustomObject]@{
        Name      = $Name
        Path      = $Path
        Source    = $Source
        Functions = $Functions
    }
}

function Import-LedgerExtensionRuntime {
    <#
    .SYNOPSIS
    Loads a single extension at runtime (from Set-LedgerCurrentJournal).
    Creates global functions so they are immediately callable.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [ValidateSet('Env', 'User', 'Journal')]
        [string]$Source
    )

    if (-not (Test-Path $Path -PathType Leaf)) {
        Write-Warning "PSLedger extension not found: $Path"
        return
    }

    $functionsBefore = (Get-ChildItem function:).Name

    try {
        # Dot-source in current scope (within this function)
        . $Path
    }
    catch {
        Write-Warning "PSLedger extension failed to load: $Path — $($_.Exception.Message)"
        return
    }

    $functionsAfter = (Get-ChildItem function:).Name
    $newFunctions = @($functionsAfter | Where-Object { $_ -notin $functionsBefore })

    # Promote new functions to global scope so they persist after this function returns
    foreach ($funcName in $newFunctions) {
        $funcDef = Get-Item "function:$funcName"
        Set-Item "function:global:$funcName" -Value $funcDef.ScriptBlock
    }
    $newFunctions = @($functionsAfter | Where-Object { $_ -notin $functionsBefore })

    $name = [System.IO.Path]::GetFileNameWithoutExtension($Path)
    Register-LedgerExtension -Name $name -Path $Path -Source $Source -Functions $newFunctions
}

function Remove-LedgerJournalExtensions {
    <#
    .SYNOPSIS
    Removes all extensions that were loaded from a journal source.
    #>

    [CmdletBinding()]
    param ()

    $journalExtensions = @($script:LoadedExtensions | Where-Object { $_.Source -eq 'Journal' })

    foreach ($ext in $journalExtensions) {
        foreach ($funcName in $ext.Functions) {
            if (Test-Path "function:global:$funcName") {
                Remove-Item "function:global:$funcName" -Force
            }
            if (Test-Path "function:$funcName") {
                Remove-Item "function:$funcName" -Force
            }
        }
    }

    $script:LoadedExtensions = @($script:LoadedExtensions | Where-Object { $_.Source -ne 'Journal' })
}