public/Install-OSDeploySoftware.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

function Install-OSDeploySoftware {
    <#
    .SYNOPSIS
        Lists, previews, downloads, or installs OSDeploy software components
 
    .DESCRIPTION
        Provides four modes for OSDeploy workstation software. With no Name, returns the
        available component list and a preview command for each item. With Name but without
        Force or DownloadOnly, returns metadata describing the selected components. Force
        invokes each selected installer. DownloadOnly downloads supported installers without
        installing them and takes precedence as the action when both switches are specified.
 
        Supported names are adk-25h2, adk-26h1, mdt, git, code, code-insiders, hyperv, and
        7zip. DownloadOnly is reported as unsupported for Hyper-V, Git, and both Visual Studio
        Code channels. Hyper-V installation is also reported as unsupported inside a VM.
        WhatIf and Confirm apply to each requested download or installation action.
 
        After validating prerequisites, initializes OSDeployCore paths and migrates supported
        legacy cache content before listing, previewing, downloading, or installing components.
 
    .PARAMETER Name
        Specifies one or more validated component identifiers. The alias is Component. When
        omitted or empty, returns the complete component list instead of taking an action.
 
    .PARAMETER Force
        Invokes the installer for each selected component instead of returning preview metadata.
 
    .PARAMETER DownloadOnly
        Downloads supported ADK, MDT, or 7-Zip installers without installing them. Hyper-V and
        winget-based components return a NotSupported result. When combined with Force, the
        supported component helpers still receive DownloadOnly.
 
    .EXAMPLE
        PS> Install-OSDeploySoftware
 
        Returns all available component names, display names, actions, and preview commands.
 
    .EXAMPLE
        PS> Install-OSDeploySoftware -Name 'adk-26h1'
 
        Returns preview metadata for Windows ADK 26H1 without downloading or installing it.
 
    .EXAMPLE
        PS> Install-OSDeploySoftware -Name 'adk-26h1' -Force
 
        Downloads and installs Windows ADK 26H1 and its Windows PE add-on.
 
    .EXAMPLE
        PS> Install-OSDeploySoftware -Name 'mdt' -DownloadOnly
 
        Downloads the MDT installer without installing it.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject. With no Name, returns objects containing
        Name, FullName, Action, and Command. Preview objects contain Name, Component, Action,
        Source, Docs, Details, Note, and Command. Action results contain component and status
        data; skipped or unsupported results include explanatory properties.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-08-28
 
        Requires Windows 11 25H2 or later, PowerShell 7.4 or later installed from MSI,
        curl.exe, and Administrator rights. Winget-based installers also assume winget is
        available.
 
        MDT is retired by Microsoft. Use it only for existing workflows that still depend on it.
 
    .LINK
        https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install
 
    .LINK
        https://learn.microsoft.com/en-us/troubleshoot/mem/configmgr/mdt/mdt-retirement
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Alias('Component')]
        [ValidateSet(
            'adk-25h2',
            'adk-26h1',
            'mdt',
            'git',
            'code',
            'code-insiders',
            'hyperv',
            '7zip'
        )]
        [string[]] $Name,

        [switch] $Force,

        [switch] $DownloadOnly
    )
    #=================================================
    # Stop before inspecting, downloading, or installing components when prerequisites are missing.
    if (-not (Test-IsWindows11)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 is required."
    }
    if (-not (Test-IsWindows1125H2)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 25H2 (build 26200) is required."
    }
    if (-not (Test-PwshVersionMin)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] PowerShell 7.4 or higher is required."
    }
    if (-not (Test-PwshPSHome)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] The MSI installation of PowerShell 7 is required."
    }
    if (-not (Test-CommandCurl)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] curl.exe is required but was not found in the current PATH. curl.exe ships with Windows 10 1803+."
    }
    if (-not (Test-IsAdministrator)) {
        throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Administrator rights are required. Re-run PowerShell as Administrator and try again."
    }
    Initialize-OSDeployCorePaths
    #=================================================
    # Auto-download pwsh installer every time unless already cached
    # TODO: re-enable when ready
    # try {
    # $pwshWinget = Get-Command -Name 'winget' -ErrorAction SilentlyContinue
    # if ($pwshWinget) {
    # $pwshSoftwarePath = Join-Path -Path $script:OSDeployCoreSoftwarePath -ChildPath 'Microsoft.PowerShell'
    # $pwshAlreadyCached = (Test-Path -Path $pwshSoftwarePath) -and
    # (Get-ChildItem -Path $pwshSoftwarePath -File -ErrorAction SilentlyContinue | Select-Object -First 1)
    # if (-not $pwshAlreadyCached) {
    # New-Item -Path $pwshSoftwarePath -ItemType Directory -Force | Out-Null
    # $pwshPackageId = if ($global:OSDeployModule -and $global:OSDeployModule.Software.pwsh.wingetid) {
    # [string]$global:OSDeployModule.Software.pwsh.wingetid
    # } else { 'Microsoft.PowerShell' }
    # Write-Host "[$(Get-Date -Format s)] [Install-OSDeploySoftware] Downloading $pwshPackageId to $pwshSoftwarePath..." -ForegroundColor DarkGray
    # & $pwshWinget.Source download --id $pwshPackageId --download-directory $pwshSoftwarePath --accept-source-agreements --accept-package-agreements
    # }
    # } else {
    # Write-Verbose "[$($MyInvocation.MyCommand.Name)] [Install-OSDeploySoftware] winget not found; skipping auto-download of pwsh."
    # }
    # } catch {
    # Write-Verbose "[$($MyInvocation.MyCommand.Name)] [Install-OSDeploySoftware] pwsh auto-download skipped: $_"
    # }

    $availableComponents = @(
        'adk-25h2'
        'adk-26h1'
        'mdt'
        'git'
        'code'
        'code-insiders'
        'hyperv'
        '7zip'
    )

    # Return the available components when the caller does not select a component.
    if (-not $PSBoundParameters.ContainsKey('Name') -or -not $Name -or $Name.Count -eq 0) {
        $componentFullName = @{
            'pwsh'          = 'PowerShell 7'
            'PowerShell 7'  = 'PowerShell 7'
            'adk-25h2'      = 'Windows ADK 25H2'
            'adk-26h1'      = 'Windows ADK 26H1'
            'mdt'           = 'Microsoft Deployment Toolkit'
            'git'           = 'Git for Windows'
            'code'          = 'Visual Studio Code'
            'code-insiders' = 'Visual Studio Code Insiders'
            'hyperv'        = 'Hyper-V'
            '7zip'          = '7-Zip'
        }

        $options = foreach ($option in $availableComponents) {
            [pscustomobject]@{
                Name      = $option
                FullName  = $componentFullName[$option]
                Action    = 'Install'
                Command   = "Install-OSDeploySoftware -Name '$option'"
            }
        }

        return $options
    }

    $componentFullName = @{
        'pwsh'          = 'PowerShell 7'
        'PowerShell 7'  = 'PowerShell 7'
        'adk-25h2'      = 'Windows ADK 25H2'
        'adk-26h1'      = 'Windows ADK 26H1'
        'mdt'           = 'Microsoft Deployment Toolkit'
        'git'           = 'Git for Windows'
        'code'          = 'Visual Studio Code'
        'code-insiders' = 'Visual Studio Code Insiders'
        'hyperv'        = 'Hyper-V'
        'Hyper-V'       = 'Hyper-V'
        '7zip'          = '7-Zip'
        '7-Zip'         = '7-Zip'
    }
    $componentMetadata = @{
        'pwsh' = @{
            Component = 'PowerShell 7'
            Source    = $global:OSDeployModule.Software.pwsh.wingetid
            Docs      = $global:OSDeployModule.Software.pwsh.docs
            Details   = $global:OSDeployModule.Software.pwsh.wiki
        }
        'adk-25h2' = @{
            Component = 'Windows ADK 25H2'
            Source    = $global:OSDeployModule.Software.adk.'25h2'.adksetup
            Docs      = $global:OSDeployModule.Software.adk.docs
            Details   = $global:OSDeployModule.Software.adk.'25h2'.wiki
        }
        'adk-26h1' = @{
            Component = 'Windows ADK 26H1'
            Source    = $global:OSDeployModule.Software.adk.'26h1'.adksetup
            Docs      = $global:OSDeployModule.Software.adk.docs
            Details   = $global:OSDeployModule.Software.adk.'26h1'.wiki
        }
        'mdt' = @{
            Component = 'Microsoft Deployment Toolkit'
            Source    = $global:OSDeployModule.Software.mdt.msi
            Docs      = $global:OSDeployModule.Software.mdt.docs
            Details   = $global:OSDeployModule.Software.mdt.retirement
        }
        'git' = @{
            Component = 'Git for Windows'
            Source    = $global:OSDeployModule.Software.git.wingetid
            Docs      = 'https://git-scm.com/download/win'
            Details   = 'https://winget.run/pkg/Git/Git'
        }
        'hyperv' = @{
            Component = 'Hyper-V'
            Source    = $global:OSDeployModule.Software.hyperv.featurename
            Docs      = $global:OSDeployModule.Software.hyperv.docs
            Details   = $global:OSDeployModule.Software.hyperv.docs
        }
        'code' = @{
            Component = 'Visual Studio Code'
            Source    = $global:OSDeployModule.Software.vscode.stable.wingetid
            Docs      = $global:OSDeployModule.Software.vscode.docs
            Details   = $global:OSDeployModule.Software.vscode.wiki
        }
        'code-insiders' = @{
            Component = 'Visual Studio Code Insiders'
            Source    = $global:OSDeployModule.Software.vscode.insiders.wingetid
            Docs      = $global:OSDeployModule.Software.vscode.docs
            Details   = $global:OSDeployModule.Software.vscode.wiki
        }
        '7zip' = @{
            Component = '7-Zip'
            Source    = $global:OSDeployModule.Software.'7zip'.id
            Docs      = $global:OSDeployModule.Software.'7zip'.releases
            Details   = $global:OSDeployModule.BootImage.winpeapps.sevenzip.standalone
        }
    }

    # Return component metadata unless an install or download action was requested.
    if (-not $Force -and -not $DownloadOnly) {
        $preview = foreach ($item in $Name) {
            $metadata = $componentMetadata[$item]

            [pscustomobject]@{
                Name      = $item
                Component = $metadata.Component
                Action    = 'Preview'
                Source    = $metadata.Source
                Docs      = $metadata.Docs
                Details   = $metadata.Details
                Note      = 'Add -Force to install or -DownloadOnly to download only.'
                Command   = "Install-OSDeploySoftware -Name '$item' -Force"
            }
        }

        return $preview
    }

    $results = [System.Collections.Generic.List[pscustomobject]]::new()

# Components that use Windows features or winget (which downloads to %TEMP%\WinGet) have no separate downloadable asset.
        $downloadOnlyUnsupported = @('hyperv', 'git', 'code', 'code-insiders')

    foreach ($item in $Name) {
        # Skip Hyper-V because nested installation is unsupported.
        if (Test-IsVM -and $item -eq 'hyperv') {
            $results.Add([pscustomobject]@{
                Name      = $item
                Component = $componentFullName[$item]
                Action    = 'Skipped'
                Status    = 'NotSupported'
                Note      = 'Hyper-V cannot be installed inside a virtual machine.'
            })
            continue
        }

        # Report unsupported download-only components without attempting an action.
        if ($DownloadOnly -and $item -in $downloadOnlyUnsupported) {
            $results.Add([pscustomobject]@{
                Name        = $item
                Component   = $componentFullName[$item]
                Action      = 'DownloadOnly'
                Status      = 'NotSupported'
                Note        = '-DownloadOnly is not supported for Windows feature-based components.'
            })
            continue
        }

        $resolvedComponent = if ($componentFullName.ContainsKey($item)) {
            $componentFullName[$item]
        }
        else {
            $item
        }

        # Honor WhatIf and Confirm for every component-changing action.
        switch ($resolvedComponent) {
            'Windows ADK 25H2' {
                if ($PSCmdlet.ShouldProcess('Windows ADK 25H2', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-SoftwareMicrosoftWindowsAdk25H2 -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = 'Windows ADK 25H2'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
            'Windows ADK 26H1' {
                if ($PSCmdlet.ShouldProcess('Windows ADK 26H1', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-SoftwareMicrosoftWindowsAdk26H1 -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = 'Windows ADK 26H1'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
            'Microsoft Deployment Toolkit' {
                if ($PSCmdlet.ShouldProcess('Microsoft Deployment Toolkit', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-SoftwareMicrosoftDeploymentToolkit -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = 'Microsoft Deployment Toolkit'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
            'PowerShell 7' {
                if ($PSCmdlet.ShouldProcess('PowerShell 7', 'Install')) {
                    Install-SoftwareMicrosoftPwsh
                    $results.Add([pscustomobject]@{
                        Component = 'PowerShell 7'
                        Status    = 'Installed'
                    })
                }
            }
            'Visual Studio Code' {
                if ($PSCmdlet.ShouldProcess('Visual Studio Code', 'Install')) {
                    Install-SoftwareMicrosoftVSCode
                    $results.Add([pscustomobject]@{
                        Component = 'Visual Studio Code'
                        Status    = 'Installed'
                    })
                }
            }
            'Visual Studio Code Insiders' {
                if ($PSCmdlet.ShouldProcess('Visual Studio Code Insiders', 'Install')) {
                    Install-SoftwareMicrosoftVSCodeInsiders
                    $results.Add([pscustomobject]@{
                        Component = 'Visual Studio Code Insiders'
                        Status    = 'Installed'
                    })
                }
            }
            'Git for Windows' {
                if ($PSCmdlet.ShouldProcess('Git for Windows', 'Install')) {
                    Install-SoftwareGitForWindows
                    $results.Add([pscustomobject]@{
                        Component = 'Git for Windows'
                        Status    = 'Installed'
                    })
                }
            }
            'Hyper-V' {
                if ($PSCmdlet.ShouldProcess('Hyper-V', 'Install')) {
                    $hvResult = Install-SoftwareMicrosoftHyperV
                    $results.Add([pscustomobject]@{
                        Component     = 'Hyper-V'
                        Status        = 'Installed'
                        RestartNeeded = ($hvResult.RestartNeeded -eq $true)
                    })
                }
            }
            '7-Zip' {
                if ($PSCmdlet.ShouldProcess('7-Zip', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-Software7Zip -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = '7-Zip'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
        }
    }

    $results
}