Private/Update-PSADTUninstallLogic.ps1

function Update-PSADTUninstallLogic {
    <#
    .SYNOPSIS
        Writes the Uninstall section of SupportFiles\AppConfig.json from sandbox capture data.
    .DESCRIPTION
        Parses the InstallationChanges_*.json produced by the documentation sandbox and records the
        uninstall inputs (MSI product codes, registry uninstall strings split into exe/msi
        uninstallers, and install-folder cleanup paths) as DATA in AppConfig.json. The static,
        data-driven deploy script consumes these at runtime — no PowerShell is generated here, so
        there is no injection surface and the operation is idempotent (re-running just overwrites the
        Uninstall section; there is no marker to consume).
 
        Product codes are validated as strict GUIDs (Test-Win32ToolkitProductCode). Only EXE-based
        projects call this; MSI stays on PSADT Zero-Config MSI.
 
        See knowledge-base/designs/data-driven-generation.md.
    .PARAMETER ProjectPath
        Full path to the PSADT project folder.
    .PARAMETER JsonFilePath
        Full path to the InstallationChanges_*.json capture file.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string]$ProjectPath,

        [Parameter(Mandatory)]
        [string]$JsonFilePath
    )

    try {
        $data = Get-Content -Path $JsonFilePath -Raw -Encoding UTF8 | ConvertFrom-Json

        $appName      = 'Unknown App'
        $productCodes = @()
        $uninstallers = @()
        $cleanupPaths = @()

        if ($data.NewRegistryKeys) {
            foreach ($regKey in $data.NewRegistryKeys) {
                if ($regKey.Path -notlike '*Uninstall*') { continue }

                if ($regKey.Values) {
                    if ($regKey.Values.DisplayName) { $appName = $regKey.Values.DisplayName }

                    # Prefer QuietUninstallString, then UninstallString (exe), then msiexec string.
                    $selected = $null
                    if ($regKey.Values.QuietUninstallString -and $regKey.Values.QuietUninstallString -like '*.exe*') {
                        $selected = $regKey.Values.QuietUninstallString
                    }
                    elseif ($regKey.Values.UninstallString -and $regKey.Values.UninstallString -like '*.exe*') {
                        $selected = $regKey.Values.UninstallString
                    }
                    elseif ($regKey.Values.UninstallString -and $regKey.Values.UninstallString -like '*msiexec*') {
                        $selected = $regKey.Values.UninstallString
                    }

                    if ($selected) {
                        if ($selected -like '*msiexec*') {
                            if ($selected -match '\{[A-F0-9-]{36}\}' -and (Test-Win32ToolkitProductCode $matches[0])) {
                                $uninstallers += [pscustomobject]@{ Type = 'msi'; ProductCode = $matches[0]; Path = $null; Args = $null }
                            }
                        }
                        elseif ($selected -like '*.exe*') {
                            $exePath = ''; $exeParams = ''
                            if ($selected -match '"([^"]*\.exe)"(.*)')      { $exePath = $matches[1]; $exeParams = $matches[2].Trim() }
                            elseif ($selected -match '([^\s]*\.exe)(.*)')   { $exePath = $matches[1]; $exeParams = $matches[2].Trim() }
                            else                                            { $exePath = $selected }
                            $uninstallers += [pscustomobject]@{ Type = 'exe'; ProductCode = $null; Path = $exePath; Args = $exeParams }
                        }
                    }
                }

                # Product code from the Uninstall key path itself (strict GUID only).
                if ($regKey.Path -match '\{[A-F0-9-]{36}\}') {
                    $pc = [regex]::Match($regKey.Path, '\{[A-F0-9-]{36}\}').Value
                    if ((Test-Win32ToolkitProductCode $pc) -and $pc -notin $productCodes) {
                        $productCodes += $pc
                    }
                }
            }
        }

        # Install-folder cleanup candidates (avoid the shared roots).
        if ($data.NewFiles) {
            foreach ($file in $data.NewFiles) {
                if ($file.Path -like '*Program Files*') {
                    $dir = Split-Path $file.Path -Parent
                    if ($dir -notin $cleanupPaths -and
                        $dir -notlike '*Program Files' -and $dir -notlike '*ProgramData' -and
                        $dir -ne 'C:\Program Files' -and $dir -ne 'C:\ProgramData') {
                        $cleanupPaths += $dir
                    }
                }
            }
        }

        $cfg = Get-Win32ToolkitAppConfig -ProjectPath $ProjectPath
        $cfg | Add-Member -NotePropertyName Uninstall -NotePropertyValue ([pscustomobject]@{
            AppName      = $appName
            ProductCodes = $productCodes
            Uninstallers = $uninstallers
            CleanupPaths = $cleanupPaths
        }) -Force
        Set-Win32ToolkitAppConfig -ProjectPath $ProjectPath -Config $cfg | Out-Null

        Write-Host "✓ Uninstall data written for '$appName' (product codes: $($productCodes.Count), uninstallers: $($uninstallers.Count), cleanup paths: $($cleanupPaths.Count))" -ForegroundColor Green
        return $true
    }
    catch {
        Write-Warning "Failed to write uninstall data: $($_.Exception.Message)"
        return $false
    }
}