pwshrun-bootstrap.ps1

<#
    The bootstrapper is receiving implicit arguments from pwshrun.psm1 New-Module script block
    - $alias : the alias to use for this task runner
    - $options : the options for the task runner
#>


# Analogous to Microsoft.Extensions.Logging.LogLevel
# Trace = 0, Debug = 1, Information = 2, Warning = 3, Error = 4, Critical = 5, None = 6
enum LogLevel {
    Trace = 0
    Debug = 1
    Information = 2
    Warning = 3
    Error = 4
    Critical = 5
    None = 6
}

if ($options.ContainsKey("settings")) {
    $settingsPath = $options.settings
} else {
    $settingsPath = "~\.pwshrun.$alias.json"
}
$config = @{
    "vars" = @{
        "PWSHRUN_HOME" = $PSScriptRoot;
        "RUNNER" = $alias;
        "LOGLEVEL" = [LogLevel]::Warning;
    };
    "bundles" = @{};
    "tasks" = @{};
    "settings" = @{};
    "types" = @{
        "LogLevel" = [LogLevel];
    };
}

function PwshRun-LoadSettings {
    Param(
        [string] $settingsPath
    )

    $combined = @{}
    $data = Get-Content $(PwshRun-ExpandVariables $settingsPath) | ConvertFrom-Json -AsHashtable
    if ($data._vars) {
        $config.vars = PwshRun-MergeHashtables $config.vars $data._vars
        $data.Remove("_vars")
    }
    if ($data._include) {
        foreach ($file in $data._include) {
            $combined = PwshRun-MergeHashtables $combined $(PwshRun-LoadSettings $file)
        }
        $data.Remove("_include")
    }

    $combined = PwshRun-MergeHashtables $combined $data

    $combined
}

<#
 .Synopsis
    Registers runnable tasks with this runner
#>

function PwshRun-RegisterTasks {
    Param(
        [string] $bundle,
        [hashtable[]] $tasks
    )

    if ($config.bundles.ContainsKey($bundle)) {
        $config.bundles[$bundle] += $tasks
    } else {
        $config.bundles[$bundle] = $tasks
    }

    $tasks | ForEach-Object {
        $config.tasks[$_.Alias] = $_;
    }
}

<#
 .Synopsis
    Gets the settings for a specific task bundle
#>

function PwshRun-GetSettings {
    Param(
        [string] $taskBundle
    )

    return $config.settings[$taskBundle]
}

<#
 .Synopsis
    Creates local variables from all elements in the given hashtable in the parent
    (caller) scope.
#>

function PwshRun-CreateVariables {
    Param(
        [hashtable] $vars
    )

    $vars.GetEnumerator() | ForEach-Object {
        New-Variable -Force -Scope 1 -Name $_.Key -Value $_.Value
    }
}

<#
 .Synopsis
    Performs string expansion with a defined set of variables
#>

function PwshRun-ExpandVariables {
    Param(
        [string] $str,
        [hashtable] $vars = $config.vars
    )

    PwshRun-CreateVariables $vars

    return $ExecutionContext.InvokeCommand.ExpandString($str)
}

function PwshRun-MergeHashtables {
    $output = @{}
    # $input is an enumerator, so we have to get the enumerator of $tables in order
    # to combine the two
    foreach ($table in ($input + $args.GetEnumerator())) {
        if ($table -is [hashtable]) {
            foreach ($key in $table.Keys) {
                if ($table.$key -is [hashtable] -and $output.$key -is [hashtable]) {
                    $output.$key = PwshRun-MergeHashtables $output.$key $table.$key
                } else {
                    $output.$key = $table.$key
                }
            }
        }
    }
    $output
}

function PwshRun-RegisterPromptHook {
    Param(
        [string] $name,
        [ScriptBlock] $block
    )

    $global:PwshRunPrompt.hooks[$name] = $block
}

function PwshRun-RemovePromptHook {
    Param(
        [string] $name
    )

    $global:PwshRunPrompt.hooks.Remove($name)
}

if (!(Test-Path -Path $settingsPath)) {
    Write-Warning "Missing settings file $settingsPath"
} else {
    $config.settings = PwshRun-LoadSettings $settingsPath
}



<#
 .Synopsis
    Invokes a PwshRun task with the given arguments
#>

$invokeName = "Invoke-PwshRunTaskOf$((Get-Culture).TextInfo.ToTitleCase($alias))"
Set-Item -Path "function:$invokeName" -Value {
    Param(
        [string] $taskName,
        [switch] $splat = $false,
        [Parameter(Mandatory=$false, ValueFromRemainingArguments=$true)] $taskArgs
    )

    $task = $config.tasks[$taskName]
    if (!$task) {
        Write-Error "Unknown task $taskName"
        return
    }

    if ($taskArgs.Length -eq 0) {
        # short circuit for 0 arguments case
        & $task.Command
        return
    }

    # PowerShell dynamic argument handling is weird ...
    if ($splat) {
        $processedArgs = $taskArgs | Select-Object -First 1
        & $task.Command @processedArgs
    } else {
        $namedArgs = @{}
        $positionalArgs = New-Object System.Collections.ArrayList
        $name = $null
        # building named and positional arguments by "guessing"
        $taskArgs | ForEach-Object {
            if ($_ -is [string] -and $_ -match "^-\w+$") {
                $name = $_.Substring(1)
            } elseif ($_ -is [string] -and $_ -match "^\+\w+$") {
                $namedArgs.Add($_.Substring(1), $true)
            } else {
                # allow _escaping_ of arguments that would normally be interpreted as the name for a
                # named argument ("-Foo") => "`-Foo" will be converted to "-Foo" as an argument _value_
                $value = if ($_ -match "^``[-+]") { $_.Substring(1) } else { $_ }

                if ($name -eq $null) {
                    $positionalArgs.Add($value) > $null
                } else {
                    $namedArgs.Add($name, $value)
                    $name = $null
                }
            }
        }
        & $task.Command @namedArgs @positionalArgs
    }
}

. "$PSScriptRoot/core-bundle.ps1"

Set-Alias $alias $invokeName
Export-ModuleMember -Function $invokeName -Alias $alias

<#
 Load runner scripts / tasks
#>

$options.load | ForEach-Object {
    $path = PwshRun-ExpandVariables $_

    if (Test-Path $path -PathType Container) {
        Get-ChildItem $path -Filter "*.ps1" | ForEach-Object {
            . $_.FullName
        }
    } else {
        . $path
    }
}