Private/Configure-PSADTForInstaller.ps1

function Configure-PSADTForInstaller {
    <#
    .SYNOPSIS
        Configures a scaffolded PSADT v4 project for its installer (data-driven).
    .DESCRIPTION
        Writes the App and Installer sections of SupportFiles\AppConfig.json (values are stored as
        DATA via ConvertTo-Json, never interpolated into generated code) and patches
        Invoke-AppDeployToolkit.ps1 with fixed, value-free routines that read that data at runtime
        (see Set-PSADTDataDrivenScript). MSI installers keep an empty App.Name so PSADT Zero-Config
        MSI still applies. Org-template branding is applied afterwards, unchanged.
 
        See knowledge-base/designs/data-driven-generation.md.
    .PARAMETER ProjectPath
        Full path to the PSADT project folder (contains Invoke-AppDeployToolkit.ps1 and Files\).
    .PARAMETER AppInfo
        App metadata resolved from winget (used as a fallback for Name/Version).
    .PARAMETER Architecture
        Target architecture string (x64/x86/arm64).
    #>

    [CmdletBinding()]
    param(
        [string]$ProjectPath,
        [PSCustomObject]$AppInfo,
        [string]$Architecture
    )

    try {
        $filesPath  = Join-Path $ProjectPath 'Files'
        $scriptPath = Join-Path $ProjectPath 'Invoke-AppDeployToolkit.ps1'

        if (-not (Test-Path $scriptPath)) {
            Write-Warning "PSADT script not found: $scriptPath"
            return $false
        }

        # Detect installer type
        $fileInfo = Get-InstallerFileInfo -FilesPath $filesPath
        if (-not $fileInfo.FileName) {
            Write-Warning 'No installer files detected in Files folder'
            return $false
        }
        Write-Verbose "Detected installer: $($fileInfo.FileName) ($($fileInfo.Type.ToUpper()))"

        # Winget manifest metadata (may be absent)
        $yamlInfo = Get-YAMLInstallerInfo -FilesPath $filesPath

        # ---- Resolve values (kept as DATA — never emitted as code) ----
        $isMsi   = ($fileInfo.Type -eq 'msi')
        $vendor  = if ($yamlInfo.Publisher) { $yamlInfo.Publisher } else { '' }
        $version = if ($yamlInfo.PackageVersion) { $yamlInfo.PackageVersion } elseif ($AppInfo.Version) { $AppInfo.Version } else { '' }
        # Empty AppName for MSI => Zero-Config MSI stays enabled.
        $name    = if ($isMsi) { '' } elseif ($yamlInfo.PackageName) { $yamlInfo.PackageName } else { $AppInfo.Name }
        # msix/appx never take silent switches (installed via Add-AppxPackage/provisioning) — storing
        # YAML args for them would be dead, confusing data.
        $silent  = if ($isMsi -or $fileInfo.Type -in @('msix', 'appx')) { '' }
                   elseif ($yamlInfo.SilentArgs) { $yamlInfo.SilentArgs }
                   else { '/S' }
        # DisplayName is the real product name — always populated (incl. MSI, where App.Name is empty for
        # Zero-Config). Drives the install tattoo + Intune detection key so both cover MSI.
        $display = if ($yamlInfo.PackageName) { $yamlInfo.PackageName } elseif ($AppInfo.Name) { $AppInfo.Name } else { '' }

        # ---- Write App + Installer sections into AppConfig.json (merge-preserving) ----
        $cfg = Get-Win32ToolkitAppConfig -ProjectPath $ProjectPath
        $cfg | Add-Member -NotePropertyName App -NotePropertyValue ([pscustomobject]@{
            Vendor         = $vendor
            Name           = $name
            DisplayName    = $display
            Version        = $version
            Arch           = $Architecture
            ScriptAuthor   = if ($script:OrgTemplate -and $script:OrgTemplate.AppScriptAuthor) { $script:OrgTemplate.AppScriptAuthor } else { '' }
            ScriptDate     = (Get-Date -Format 'yyyy-MM-dd')
            Description    = if ($yamlInfo -and $yamlInfo.Description)    { $yamlInfo.Description }    else { '' }
            InformationUrl = if ($yamlInfo -and $yamlInfo.InformationUrl) { $yamlInfo.InformationUrl } else { '' }
        }) -Force
        $cfg | Add-Member -NotePropertyName Installer -NotePropertyValue ([pscustomobject]@{
            Type       = $fileInfo.Type
            FileName   = $fileInfo.FileName
            SilentArgs = $silent
        }) -Force
        if (-not ($cfg.PSObject.Properties.Name -contains 'ProcessesToClose')) {
            $cfg | Add-Member -NotePropertyName ProcessesToClose -NotePropertyValue @() -Force
        }

        # ---- Org-template Intune publish defaults (D2/D3) — data only ----
        # Persist the active template's Intune defaults so Publish (which may run standalone, without an
        # org template loaded) can honor them. Written ONLY when the template supplies them, so
        # non-template projects keep no IntuneDefaults section and Publish uses its built-in fallbacks.
        if ($script:OrgTemplate -and $script:OrgTemplate.PSObject.Properties['IntuneDefaults'] -and $script:OrgTemplate.IntuneDefaults) {
            $cfg | Add-Member -NotePropertyName IntuneDefaults -NotePropertyValue (New-Win32ToolkitIntuneDefaults -Template $script:OrgTemplate) -Force
        }

        # ---- Tenant pin — the guard Publish/Sync enforce before writing anything ----
        # Persisted into the project so a STANDALONE publish (no org template loaded) is still checked.
        if ($script:OrgTemplate -and $script:OrgTemplate.PSObject.Properties['TenantId'] -and $script:OrgTemplate.TenantId) {
            $cfg | Add-Member -NotePropertyName Intune -NotePropertyValue ([pscustomobject]@{
                TenantId = [string]$script:OrgTemplate.TenantId
            }) -Force
        }

        # ---- Org documentation branding (B8) — data only, used by Export-Win32ToolkitDocumentation ----
        if ($script:OrgTemplate) {
            $company   = if ($script:OrgTemplate.PSObject.Properties['CompanyName']) { [string]$script:OrgTemplate.CompanyName } else { '' }
            $docFooter = if ($script:OrgTemplate.PSObject.Properties['DocFooter'])   { [string]$script:OrgTemplate.DocFooter }   else { '' }
            if (-not [string]::IsNullOrWhiteSpace($company) -or -not [string]::IsNullOrWhiteSpace($docFooter)) {
                $cfg | Add-Member -NotePropertyName Branding -NotePropertyValue ([pscustomobject]@{
                    CompanyName = $company
                    DocFooter   = $docFooter
                }) -Force
            }
        }

        Set-Win32ToolkitAppConfig -ProjectPath $ProjectPath -Config $cfg | Out-Null
        Write-Verbose 'Wrote SupportFiles\AppConfig.json (App + Installer)'

        # MSIX/APPX: write the identity-driven Uninstall section NOW (host-manifest-driven — it needs
        # nothing from the sandbox capture, so the uninstall works even if the capture times out).
        if ($fileInfo.Type -in @('msix', 'appx')) {
            if (-not (Update-PSADTMsixUninstallLogic -ProjectPath $ProjectPath)) {
                Write-Warning 'MSIX uninstall data could not be written — the package would have no working uninstall.'
            }
            # Same capture-independent reasoning: an MSIX writes no App Paths / Uninstall / InstallLocation
            # artifacts, so the capture-based ProcessesToClose writer finds nothing. The manifest declares
            # the app's executables outright — read them here.
            [void](Update-PSADTMsixProcessesToClose -ProjectPath $ProjectPath)
        }

        # ---- Patch the deploy script to be data-driven (fixed, value-free) ----
        if (Set-PSADTDataDrivenScript -ScriptPath $scriptPath) {
            Write-Verbose 'Deploy script patched to data-driven'
        } else {
            Write-Warning 'Data-driven patching of the deploy script did not complete cleanly'
        }

        # ---- Org template branding and dialog settings (unchanged) ----
        if ($script:OrgTemplate) {
            Write-Verbose 'Applying org template...'
            Apply-OrgTemplate -ProjectPath $ProjectPath -Template $script:OrgTemplate | Out-Null
        }

        Write-Host '✓ PSADT project configured (data-driven)!' -ForegroundColor Green
        return $true
    }
    catch {
        Write-Warning "Failed to configure PSADT: $($_.Exception.Message)"
        return $false
    }
}