PS-TaskFile.psm1

#Requires -Version 7.4
Set-StrictMode -Version 3.0

# Path of this file. Engine runspaces import the .psm1 directly (see Private/Host.ps1).
$script:ModuleFile = $PSCommandPath
$script:ModuleVersion = (Import-PowerShellDataFile -LiteralPath (Join-Path $PSScriptRoot 'PS-TaskFile.psd1')).ModuleVersion

# PowerShell cannot replace the classes of a module that a process has already loaded: when
# the files change (an update, a checkout), newly loaded classes end up calling old ones and
# fail in obscure ways - Import-Module -Force does not help. The first load in a process
# records what the files looked like; every later load and every run compares
# (Invoke-TaskEngine) and asks for a new session instead.
$script:ModuleChanged = 'the files of the PS-TaskFile module changed after it was loaded into this PowerShell process. ' +
    'PowerShell cannot replace the classes of a loaded module: start a new PowerShell session.'

function Get-TaskModuleStamp {
    <#
    .SYNOPSIS
        Identifies the script files of the module as they are on disk: path, size and write time.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()
    $root = Split-Path -Parent $script:ModuleFile
    $files = [System.IO.Directory]::GetFiles($root, '*.ps*1', [System.IO.SearchOption]::AllDirectories)
    [array]::Sort($files, [StringComparer]::Ordinal)
    $parts = foreach ($path in $files) {
        $info = [System.IO.FileInfo]::new($path)
        '{0}|{1}|{2}' -f $path, $info.Length, $info.LastWriteTimeUtc.Ticks
    }
    $parts -join "`n"
}

function Test-TaskModuleChanged {
    <#
    .SYNOPSIS
        Tells whether the module files differ from the ones this process loaded first.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param()
    $stamp = Get-TaskModuleStamp
    $loaded = [AppDomain]::CurrentDomain.GetData('PS-TaskFile.ModuleStamp')
    if ($null -eq $loaded) {
        [AppDomain]::CurrentDomain.SetData('PS-TaskFile.ModuleStamp', $stamp)
        return $false
    }
    return [string]$loaded -cne $stamp
}

if (Test-TaskModuleChanged) { throw "PS-TaskFile: $script:ModuleChanged" }

# Private files are loaded in dependency order: PowerShell classes must be defined before
# another file's classes reference them.
$privateOrder = @(
    'Yaml'
    'Template'
    'Model'
    'Dotenv'
    'Fingerprint'
    'Runner'
    'Output'
    'Compiler'
    'Executor'
    'Host'
)
$public = @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'Public') -Filter '*.ps1' -ErrorAction Ignore | Sort-Object Name)

foreach ($name in $privateOrder) {
    . (Join-Path $PSScriptRoot 'Private' "$name.ps1")
}
foreach ($file in $public) {
    . $file.FullName
}

# Get-Task objects are shown as a compact table by default.
Update-TypeData -TypeName 'PS-TaskFile.TaskInfo' -DefaultDisplayPropertySet Name, Description, Aliases -Force

# Tab completion of task names.
Register-ArgumentCompleter -CommandName Invoke-Task, Watch-Task, Get-Task, Get-TaskSummary, Test-Task -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    Get-TaskCompletion -WordToComplete $wordToComplete -BoundParameters $fakeBoundParameters
}

Export-ModuleMember -Function @($public | ForEach-Object BaseName)