public/Install-OSDeploySoftware.ps1

#Requires -PSEdition Core
#Requires -Version 7.4

<#
.SYNOPSIS
Installs OS deployment prerequisite software on Windows.
 
.DESCRIPTION
Install-OSDeploySoftware is the single entry point for setting up a
Windows workstation with all tools commonly required for OS deployment work.
 
When called without -Force, the function runs in preview mode: it returns a
table showing each requested component's name, download source, documentation
link, and the exact command needed to install it. This lets you review what
will happen before committing to any change.
 
Add -Force to perform the actual installation. Administrator rights are
required for ADK, MDT, and Hyper-V installations; winget must be available for
Git for Windows and Visual Studio Code installs.
 
Supported components:
 
  adk-25h2 Windows ADK 10.1.26100.2454 (25H2) + WinPE add-on, downloaded with curl.
  adk-26h1 Windows ADK 10.1.28000.1 (26H1) + WinPE add-on, downloaded with curl.
  mdt Microsoft Deployment Toolkit 6.3.8456.1000, installed from a verified MSI.
  git Git for Windows, installed via winget with optional global identity setup.
  code Visual Studio Code (stable channel), installed via winget.
  code-insiders Visual Studio Code Insiders (pre-release channel), installed via winget.
  hyperv Hyper-V, enabled as a Windows optional feature.
 
.PARAMETER Name
One or more component names to inspect or install. Accepts the alias 'Component'.
Valid values: adk-25h2, adk-26h1, mdt, git, code, code-insiders, hyperv
 
When omitted, returns a list of all available components and the command
needed to install each one.
 
.PARAMETER Force
Performs the installation. Without this switch the command runs in preview
mode and returns source, documentation, and installation details only.
 
.PARAMETER DownloadOnly
Downloads the installer(s) to the component-named cache subfolder without
installing. For components that require curl (adk-25h2, adk-26h1, mdt),
the file is saved to C:\ProgramData\OSDeployCore\cache\downloads\<name>\
and installation is skipped. winget- and feature-based components (git, code,
code-insiders, hyperv) do not support this switch; a note object is returned.
Can be combined with -Force to re-download even when a cached file exists.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
 
Without -Force: returns one object per component with Name, Component,
Action, Source, Docs, Details, Note, and Command properties.
 
With -Force: returns one object per component with Component and Status
properties.
 
When no -Name is supplied: returns one object per available component with
Name, FullName, Action, and Command properties.
 
.INPUTS
None. This function does not accept pipeline input.
 
.EXAMPLE
Install-OSDeploySoftware
 
Returns a list of all available components, their full names, and the install
command for each. No changes are made to the system.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'adk-26h1'
 
Returns installation source and documentation details for Windows ADK 26H1.
No changes are made to the system. Add -Force to install.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'adk-26h1' -Force
 
Downloads and installs Windows ADK 10.1.28000.1 and its Windows PE add-on.
Requires Administrator rights.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'git', 'code', 'code-insiders' -Force
 
Installs Git for Windows, Visual Studio Code, and VS Code Insiders in sequence.
Requires winget (App Installer from the Microsoft Store).
 
.EXAMPLE
Install-OSDeploySoftware -Name 'mdt'
 
Returns the MDT download source, retirement notice link, and the exact
command to run to install. No changes are made to the system.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'adk-26h1' -DownloadOnly
 
Downloads adksetup.exe and adkwinpesetup.exe to
C:\ProgramData\OSDeployCore\cache\downloads\adk-26h1\ without installing.
Requires Administrator rights.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'mdt' -DownloadOnly
 
Downloads and SHA256-verifies MicrosoftDeploymentToolkit_x64.msi to
C:\ProgramData\OSDeployCore\cache\downloads\mdt\ without installing.
Requires Administrator rights.
 
.EXAMPLE
Install-OSDeploySoftware -Name 'mdt' -Force
 
Downloads, SHA256-verifies, and silently installs Microsoft Deployment
Toolkit 6.3.8456.1000. Requires Administrator rights.
 
IMPORTANT: MDT has been officially retired by Microsoft. Install it only
if your existing workflow depends on it. For new deployments consider
using Microsoft Intune or Windows Autopilot instead.
 
.NOTES
- Run PowerShell 7 as Administrator before using -Force with ADK, MDT, or Hyper-V.
- winget (App Installer) must be installed for git, code, and code-insiders.
- ADK and MDT use curl.exe for downloads; curl.exe ships with Windows 10 1803+.
- ADK installs also apply the MDT WinPE x86 MMC snap-in fix automatically.
- MDT is officially retired. See the retirement notice link in the Details property.
 
Author: David Segura
Company: Recast Software
 
.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
#>

function Install-OSDeploySoftware {
    [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
    )

    Write-OSDeployBanner

    # 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 '[Install-OSDeploySoftware] winget not found; skipping auto-download of pwsh.'
    # }
    # } catch {
    # Write-Verbose "[Install-OSDeploySoftware] pwsh auto-download skipped: $_"
    # }

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

    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
        }
    }

    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
    }

    Assert-OSDeployAdministrator

    $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) {
        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
        }

        switch ($resolvedComponent) {
            'Windows ADK 25H2' {
                if ($PSCmdlet.ShouldProcess('Windows ADK 25H2', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-MicrosoftWindowsAdk25H2 -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-MicrosoftWindowsAdk26H1 -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-MicrosoftDeploymentToolkit -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = 'Microsoft Deployment Toolkit'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
            'PowerShell 7' {
                if ($PSCmdlet.ShouldProcess('PowerShell 7', 'Install')) {
                    Install-MicrosoftPwsh
                    $results.Add([pscustomobject]@{
                        Component = 'PowerShell 7'
                        Status    = 'Installed'
                    })
                }
            }
            'Visual Studio Code' {
                if ($PSCmdlet.ShouldProcess('Visual Studio Code', 'Install')) {
                    Install-MicrosoftVSCode
                    $results.Add([pscustomobject]@{
                        Component = 'Visual Studio Code'
                        Status    = 'Installed'
                    })
                }
            }
            'Visual Studio Code Insiders' {
                if ($PSCmdlet.ShouldProcess('Visual Studio Code Insiders', 'Install')) {
                    Install-MicrosoftVSCodeInsiders
                    $results.Add([pscustomobject]@{
                        Component = 'Visual Studio Code Insiders'
                        Status    = 'Installed'
                    })
                }
            }
            'Git for Windows' {
                if ($PSCmdlet.ShouldProcess('Git for Windows', 'Install')) {
                    Install-GitForWindows
                    $results.Add([pscustomobject]@{
                        Component = 'Git for Windows'
                        Status    = 'Installed'
                    })
                }
            }
            'Hyper-V' {
                if ($PSCmdlet.ShouldProcess('Hyper-V', 'Install')) {
                    $hvResult = Install-MicrosoftHyperV
                    $results.Add([pscustomobject]@{
                        Component     = 'Hyper-V'
                        Status        = 'Installed'
                        RestartNeeded = ($hvResult.RestartNeeded -eq $true)
                    })
                }
            }
            '7-Zip' {
                if ($PSCmdlet.ShouldProcess('7-Zip', ($DownloadOnly ? 'Download' : 'Install'))) {
                    Install-7Zip -DownloadOnly:$DownloadOnly
                    $results.Add([pscustomobject]@{
                        Component = '7-Zip'
                        Status    = ($DownloadOnly ? 'Downloaded' : 'Installed')
                    })
                }
            }
        }
    }

    $results
}