Private/ConvertTo-ParamHashtable.ps1

<#
.SYNOPSIS
    Parses a flat argument list into named and positional parts for splatting.
.OUTPUTS
    PSCustomObject with:
        Named [hashtable] - named params (e.g. @{Feature='login'; Description='foo'})
        Positional [object[]] - positional args (no leading dash)
#>

function ConvertTo-ParamHashtable {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $ArgumentList
    )

    $named      = @{}
    $positional = [System.Collections.Generic.List[object]]::new()

    for ($i = 0; $i -lt $ArgumentList.Count; $i++) {
        $arg = $ArgumentList[$i]

        if ($arg -is [string] -and $arg -match '^-([A-Za-z][A-Za-z0-9_]*)$') {
            $paramName = $Matches[1]
            $nextArg   = if ($i + 1 -lt $ArgumentList.Count) { $ArgumentList[$i + 1] } else { $null }

            if ($null -ne $nextArg -and -not ($nextArg -is [string] -and $nextArg -match '^-[A-Za-z]')) {
                $named[$paramName] = $nextArg
                $i++
            }
            else {
                $named[$paramName] = $true
            }
        }
        else {
            $positional.Add($arg)
        }
    }

    return [PSCustomObject]@{
        Named      = $named
        Positional = $positional.ToArray()
    }
}