Start-DevShell.ps1

function Get-VsInstallerPath {
    [CmdletBinding()]
    param()

    $programFilesX86 = ${env:ProgramFiles(x86)}
    if (-not $programFilesX86) {
        $programFilesX86 = [Environment]::GetFolderPath('ProgramFilesX86')
    }
    if (-not $programFilesX86) { return $null }

    Join-Path $programFilesX86 'Microsoft Visual Studio\Installer'
}

function Get-CleanLoginPath {
    [CmdletBinding()]
    param()

    $separator = [System.IO.Path]::PathSeparator
    $segments = foreach ($pathValue in @(
            [Environment]::GetEnvironmentVariable('Path', 'Machine'),
            [Environment]::GetEnvironmentVariable('Path', 'User')
        )) {
        if ($pathValue) {
            $pathValue -split [regex]::Escape($separator) | Where-Object { $_ }
        }
    }

    $installerPath = Get-VsInstallerPath
    if ($installerPath) {
        $segments += $installerPath
    }

    ($segments | Select-Object -Unique) -join $separator
}

function Invoke-DevShellProcess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Environment,

        [Parameter(Mandatory)]
        [string[]]$ArgumentList,

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

    $process = [Diagnostics.Process]::new()
    try {
        $start = [Diagnostics.ProcessStartInfo]::new()
        $start.FileName = Join-Path $PSHOME 'pwsh.exe'
        $start.UseShellExecute = $false
        $start.WorkingDirectory = $WorkingDirectory
        foreach ($argument in $ArgumentList) { $start.ArgumentList.Add($argument) }
        foreach ($key in $Environment.Keys) {
            if ($null -eq $Environment[$key]) {
                $null = $start.Environment.Remove($key)
            } else {
                $start.Environment[$key] = [string]$Environment[$key]
            }
        }
        $process.StartInfo = $start
        if (-not $process.Start()) { throw 'Could not start the Visual Studio developer shell.' }
        $process.WaitForExit()
        if ($process.ExitCode -ne 0) {
            throw "Visual Studio developer shell exited with code $($process.ExitCode)."
        }
    } finally {
        $process.Dispose()
    }
}

function Get-VsCleanChildEnvironment {
    [CmdletBinding()]
    param()

    $environment = @{}
    foreach ($entry in (Get-VsEnvironmentSnapshot).GetEnumerator()) {
        if ($entry.Key -match '^(?:VSCMD_|__VSCMD_|VSINSTALLDIR$|VCINSTALLDIR$|VCTools|DevEnvDir$|VisualStudioVersion$|WindowsSDK|WindowsSdk|UniversalCRTSdkDir$|UCRTVersion$|ExtensionSdkDir$|NETFXSDKDir$|FrameworkDir|FrameworkVersion|Framework40Version$|DevShellId$|INCLUDE$|LIB$|LIBPATH$|__DOTNET_)') {
            $value = [Environment]::GetEnvironmentVariable($entry.Key, 'User')
            if ($null -eq $value) { $value = [Environment]::GetEnvironmentVariable($entry.Key, 'Machine') }
            $environment[$entry.Key] = $value
        }
    }
    $originals = Get-Variable -Name VsEnvironmentOriginals -Scope Script -ValueOnly -ErrorAction SilentlyContinue
    if ($originals) {
        foreach ($key in $originals.Keys) { $environment[$key] = $originals[$key] }
    }
    $environment['PATH'] = Get-CleanLoginPath
    $environment
}

function Get-VsDevShellArguments {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        $Installation,
        [Parameter(Mandatory)]
        [string]$Architecture,
        [Parameter(Mandatory)]
        [string]$HostArchitecture,
        [Parameter(Mandatory)]
        [string]$WorkingDirectory
    )

    $request = @{
        Installation     = $Installation
        Architecture     = $Architecture
        HostArchitecture = $HostArchitecture
        WorkingDirectory = $WorkingDirectory
        ModuleManifest   = Join-Path $PSScriptRoot 'Shmuelie.VisualStudio.psd1'
    } | ConvertTo-Json -Depth 5 -Compress
    $data = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($request))
    $bootstrap = @'
try {
    $request = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('__REQUEST__')) | ConvertFrom-Json -ErrorAction Stop
    $module = Import-Module -Name $request.ModuleManifest -PassThru -ErrorAction Stop
    Set-Location -LiteralPath $request.WorkingDirectory -ErrorAction Stop
    & $module {
        param($request)
        Initialize-VsDeveloperEnvironment -Installation $request.Installation -Architecture $request.Architecture -HostArchitecture $request.HostArchitecture
    } $request
} catch {
    [Console]::Error.WriteLine("Cannot initialize Visual Studio developer shell: $_")
    exit 1
}
'@

    $bootstrap = $bootstrap.Replace('__REQUEST__', $data)
    @('-NoLogo', '-NoExit', '-EncodedCommand', [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($bootstrap)))
}

function Start-DevShell {
    <#
    .SYNOPSIS
        Starts a nested PowerShell session for a Visual Studio developer environment.
 
    .DESCRIPTION
        Starts pwsh inline in the current window and waits until the nested shell
        exits. The selected Visual Studio year is passed to the child process via
        VSDEV_VERSION, with VSDEV_ARCH and VSDEV_HOSTARCH carrying architecture
        choices. Normal PowerShell profiles run first; the module then initializes
        the exact selected installation using VsDevCmd.bat before interactive use.
        No PSVSEnv or custom profile initialization is required. A profile may
        use this module's Set-VS commands with the VSDEV_* values; an exactly
        matching environment is reused, while conflicting toolsets fail.
 
        The child receives a clean login PATH and restored or cleared inherited
        toolset variables. The parent's environment is unchanged. The caller's
        filesystem working directory is restored after profiles have run.
        Complete, launchable releases of Visual Studio 2017, 2019, 2022, and
        2026, including Build Tools, are supported; previews are excluded.
 
    .PARAMETER Version
        Visual Studio year to request, such as 2022 or 2026. Defaults to the
        latest year returned by Get-InstalledVsVersion.
 
    .PARAMETER Arch
        Target architecture to pass to the child session through VSDEV_ARCH.
        Defaults to amd64.
 
    .PARAMETER HostArch
        Host architecture to pass to the child session through VSDEV_HOSTARCH.
        Defaults to amd64.
 
    .EXAMPLE
        Start-DevShell
        Starts a nested PowerShell session for the latest discoverable Visual
        Studio developer environment.
 
    .EXAMPLE
        Start-DevShell 2022
        Starts a nested PowerShell session requesting Visual Studio 2022.
    .EXAMPLE
        Start-DevShell 2022 -WhatIf
        Shows the nested developer shell that would be launched without
        starting a child process.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Position = 0)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-InstalledVsVersion |
                Where-Object { "$_" -like "$wordToComplete*" } |
                ForEach-Object { "$_" }
        })]
        [int]$Version,

        [ValidateSet('x86', 'amd64', 'arm', 'arm64')]
        [string]$Arch = 'amd64',

        [ValidateSet('x86', 'amd64', 'arm64')]
        [string]$HostArch = 'amd64'
    )

    Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name

    $installations = @(Get-VsDeveloperInstallation)
    if ($installations.Count -eq 0) {
        throw 'No complete, launchable Visual Studio release installations with VsDevCmd.bat were found.'
    }

    $selectedVersion = if ($PSBoundParameters.ContainsKey('Version')) { $Version } else { $installations[0].Year }
    $installation = $installations | Where-Object Year -EQ $selectedVersion | Select-Object -First 1
    if (-not $installation) {
        $installedVersions = @($installations.Year | Sort-Object -Descending -Unique)
        throw "Visual Studio $selectedVersion is not available. Available versions: $($installedVersions -join ', ')."
    }

    $location = Get-Location
    if ($location.Provider.Name -ne 'FileSystem') {
        throw 'Start-DevShell requires a filesystem working directory.'
    }
    $target = "Visual Studio $selectedVersion ($Arch, host $HostArch) in $($location.Path)"
    if ($PSCmdlet.ShouldProcess($target, 'Start nested developer shell')) {
        $environment = Get-VsCleanChildEnvironment
        $environment['VSDEV_VERSION'] = [string]$selectedVersion
        $environment['VSDEV_ARCH'] = $Arch
        $environment['VSDEV_HOSTARCH'] = $HostArch
        $arguments = Get-VsDevShellArguments -Installation $installation -Architecture $Arch -HostArchitecture $HostArch -WorkingDirectory $location.Path
        Invoke-DevShellProcess -Environment $environment -ArgumentList $arguments -WorkingDirectory $location.Path
    }
}