lib/Preflight.ps1

function Test-PathDriveAvailability {
    <#
    .SYNOPSIS
        Throws if any drive referenced in $Paths is not currently mounted.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Paths
    )

    $drives = $Paths.Values |
        ForEach-Object { [System.IO.Path]::GetPathRoot($_) } |
        Sort-Object -Unique

    foreach ($drive in $drives) {
        if (-not (Test-Path -Path $drive -PathType Container)) {
            throw "Drive '$drive' (referenced in `$Paths) is not available. Mount it or update the `$Paths configuration."
        }
    }
}

function Test-AppRunning {
    <#
    .SYNOPSIS
        Returns $true when a process matching the given name is running.
        When PathHint is supplied, the process's MainModule.FileName must
        also contain that substring (case-insensitive) - used to disambiguate
        generic names like "Code" from unrelated apps such as CodeMeter.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $ProcessName,
        [string] $PathHint
    )

    $procs = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    if (-not $procs) { return $false }
    if (-not $PathHint) { return $true }

    foreach ($proc in $procs) {
        try {
            if ($proc.MainModule.FileName -like "*$PathHint*") {
                return $true
            }
        }
        catch {
            # Get-Process can fail to read MainModule for cross-arch processes
            # (e.g. 64-bit process from 32-bit PowerShell) without elevation.
            # Fall back to assuming it counts so we err on the side of warning.
            return $true
        }
    }
    return $false
}

function Test-BlockingProcess {
    <#
    .SYNOPSIS
        Detects running applications that hold locks on cache or home
        directories and prompts the user to abort. Returns $true when it
        is safe (or the user chose) to proceed.
    #>

    [CmdletBinding()]
    param(
        [switch] $Force
    )

    $checks = @(
        @{ Name = 'Docker Desktop'; Process = 'Docker Desktop' }
        @{ Name = 'Claude';         Process = 'claude' }
        @{ Name = 'Visual Studio';  Process = 'devenv' }
        @{ Name = 'Rider';          Process = 'rider64' }
        @{ Name = 'VS Code';        Process = 'Code'; PathHint = 'Microsoft VS Code' }
    )

    $running = $checks |
        Where-Object { Test-AppRunning -ProcessName $_['Process'] -PathHint $_['PathHint'] }

    if (-not $running) { return $true }

    Write-Status -Level Warn -Message 'WARNING: The following applications are running and may lock files:'
    foreach ($r in $running) {
        Write-Status -Level Warn -Message " - $($r.Name)"
    }

    if ($Force) {
        Write-Status -Level Warn -Message ' -Force specified; continuing anyway.'
        return $true
    }

    # Skip the interactive prompt under -WhatIf; just surface the warning.
    if ($WhatIfPreference) { return $true }

    return $PSCmdlet.ShouldContinue(
        'These applications may interfere with the migration. Continue anyway?',
        'Running processes detected'
    )
}