InstallModule.ps1

[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateSet('CurrentUser', 'AllUsers', 'Custom')]
    [string] $Scope = 'CurrentUser',

    [string] $DestinationPath,

    [string] $ModuleName,

    [switch] $Mirror
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Get-ModuleNameFromRepo {
    $manifest = Get-ChildItem -LiteralPath $PSScriptRoot -Filter '*.psd1' -File | Select-Object -First 1
    if (-not $manifest) {
        throw "No module manifest (*.psd1) found in '$PSScriptRoot'."
    }

    # Avoid parsing the manifest here (it may declare PowerShellVersion=7.x and fail under Windows PowerShell 5.1).
    return $manifest.BaseName
}

function Get-PSModuleRoot {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string] $Scope
    )

    $paths = $env:PSModulePath -split [System.IO.Path]::PathSeparator | Where-Object { $_ }

    $userProfile = [System.Environment]::GetFolderPath('UserProfile')
    $psHomePath = $PSHOME

    if ($Scope -eq 'CurrentUser') {
        $resolved = ($paths | Where-Object {
                $_ -like "$userProfile*" -and $_ -notlike "$psHomePath*"
            } | Select-Object -First 1)

        if ($resolved) { return $resolved }

        $docs = [Environment]::GetFolderPath('MyDocuments')
        if ($docs) {
            # Windows PowerShell typically uses WindowsPowerShell; PowerShell 7 uses PowerShell.
            $candidates = @(
                (Join-Path $docs 'PowerShell\Modules')
                (Join-Path $docs 'WindowsPowerShell\Modules')
            )
            return ($candidates | Select-Object -First 1)
        }

        return $null
    }

    $resolved = ($paths | Where-Object {
            $_ -notlike "$userProfile*" -and $_ -notlike "$psHomePath*"
        } | Select-Object -First 1)

    if ($resolved) { return $resolved }

    $programFiles = $env:ProgramFiles
    if ($programFiles) {
        $candidates = @(
            (Join-Path $programFiles 'PowerShell\Modules')
            (Join-Path $programFiles 'WindowsPowerShell\Modules')
        )
        return ($candidates | Select-Object -First 1)
    }

    return $null
}

if (-not $ModuleName) {
    $ModuleName = Get-ModuleNameFromRepo
}

$repoRoot = $PSScriptRoot

if ($Scope -eq 'Custom') {
    if (-not $DestinationPath) {
        throw "-DestinationPath is required when -Scope Custom."
    }
    $moduleInstallPath = Join-Path -Path $DestinationPath -ChildPath $ModuleName
} else {
    $moduleRoot = Get-PSModuleRoot -Scope $Scope
    if (-not $moduleRoot) {
        throw "Unable to resolve a PSModulePath entry for scope '$Scope'."
    }
    $moduleInstallPath = Join-Path -Path $moduleRoot -ChildPath $ModuleName
}

$excludeDirs = @(
    '.git'
    '.github'
    '.vscode'
    'CI'
    '__tests__'
    'data'
    'docs'
    'mdHelp'
    'spikes'
)

$excludeFiles = @(
    '.gitattributes'
    '.gitignore'
    'azure-pipelines.yml'
    'appveyor.yml'
    'InstallModule.ps1'
    'PublishToGallery.ps1'
    'filelist.txt'
)

if ($PSCmdlet.ShouldProcess($moduleInstallPath, "Install module '$ModuleName' from '$repoRoot'")) {
    New-Item -ItemType Directory -Path $moduleInstallPath -Force | Out-Null

    Push-Location $repoRoot
    try {
        $roboArgs = @(
            $repoRoot
            $moduleInstallPath
            '/E'          # copy subdirs incl empty
            '/NFL'        # no file list
            '/NDL'        # no dir list
            '/NJH'        # no job header
            '/NJS'        # no job summary
            '/NP'         # no progress
            '/R:2'
            '/W:1'
        )

        if ($Mirror) {
            $roboArgs += '/MIR'
        }

        $roboArgs += '/XD'
        $roboArgs += $excludeDirs

        $roboArgs += '/XF'
        $roboArgs += $excludeFiles

        & robocopy @roboArgs | Out-Null

        if ($LASTEXITCODE -ge 8) {
            throw "Robocopy failed with exit code $LASTEXITCODE."
        }
    } finally {
        Pop-Location
    }

    Get-ChildItem -LiteralPath $moduleInstallPath -Force | Out-String | Write-Verbose
}