Terminal/SRxVSCode.psm1

$script:SRxVSCodeInstallStarted = $false

function Test-SRxVSCodeInstalled {
    [CmdletBinding()]
    param(
        [string[]]$CandidatePaths = $null,
        [switch]$SkipRegistry,
        [switch]$SkipCommand
    )

    if ($null -eq $CandidatePaths) {
        $CandidatePaths = @()
        if ($env:LOCALAPPDATA)           { $CandidatePaths += (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft VS Code\Code.exe') }
        if ($env:ProgramFiles)           { $CandidatePaths += (Join-Path $env:ProgramFiles 'Microsoft VS Code\Code.exe') }
        if (${env:ProgramFiles(x86)})    { $CandidatePaths += (Join-Path ${env:ProgramFiles(x86)} 'Microsoft VS Code\Code.exe') }
    }

    foreach ($p in $CandidatePaths) {
        if ([string]::IsNullOrEmpty($p)) { continue }
        if (Test-Path -LiteralPath $p -PathType Leaf) {
            $scope = 'Machine'
            if ($env:LOCALAPPDATA -and $p.StartsWith($env:LOCALAPPDATA, [System.StringComparison]::OrdinalIgnoreCase)) { $scope = 'User' }
            return [pscustomobject]@{ Installed = $true; Path = $p; Scope = $scope; Source = 'File' }
        }
    }

    if (-not $SkipRegistry) {
        $keys = @(
            @{ Root = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall';             Scope = 'User' },
            @{ Root = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall';             Scope = 'Machine' },
            @{ Root = 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'; Scope = 'Machine' }
        )
        foreach ($k in $keys) {
            try {
                if (-not (Test-Path $k.Root)) { continue }
                $hit = Get-ChildItem -Path $k.Root -ErrorAction SilentlyContinue | ForEach-Object {
                    try { Get-ItemProperty -Path $_.PSPath -ErrorAction Stop } catch { $null }
                } | Where-Object {
                    $_ -and $_.PSObject.Properties['DisplayName'] -and ([string]$_.DisplayName -like 'Microsoft Visual Studio Code*') -and ([string]$_.DisplayName -notlike '*Insiders*')
                } | Select-Object -First 1
                if ($hit) {
                    $exe = $null
                    if ($hit.PSObject.Properties['InstallLocation'] -and $hit.InstallLocation) {
                        $cand = Join-Path ([string]$hit.InstallLocation) 'Code.exe'
                        if (Test-Path -LiteralPath $cand) { $exe = $cand }
                    }
                    if ($exe) {
                        return [pscustomobject]@{ Installed = $true; Path = $exe; Scope = $k.Scope; Source = 'Registry' }
                    }
                }
            } catch { }
        }
    }

    if (-not $SkipCommand) {
        $cmd = Get-Command -Name 'code.cmd', 'code' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($cmd) {
            return [pscustomobject]@{ Installed = $true; Path = $cmd.Source; Scope = 'Unknown'; Source = 'Command' }
        }
    }

    return [pscustomobject]@{ Installed = $false; Path = $null; Scope = $null; Source = $null }
}

$script:SRxVSCodeInstallerScript = @'
param(
    [Parameter(Mandatory)][string]$LogPath,
    [string]$BuildEdition = 'Stable-User',
    [string]$AdditionalExtensions = '',
    [switch]$LaunchWhenDone
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
try { Start-Transcript -Path $LogPath -Append | Out-Null } catch { }
$mutex = $null
$owned = $false
$code = 1
try {
    $mutex = New-Object System.Threading.Mutex -ArgumentList $false, 'Local\SRxVSCodeUserInstall'
    try { $owned = $mutex.WaitOne(0) }
    catch {
        # a previous installer crashed while holding the mutex -> we own it now
        $inner = $_.Exception
        while ($inner.InnerException) { $inner = $inner.InnerException }
        if ($inner -is [System.Threading.AbandonedMutexException]) { $owned = $true } else { throw }
    }
    if (-not $owned) {
        Write-Output 'Another VS Code installation is already in progress - nothing to do.'
        $code = 3
    }
    else {
        [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
        try { [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials } catch { }
 
        $nuget = Get-PackageProvider -ListAvailable -Name NuGet -ErrorAction SilentlyContinue |
                 Where-Object { $_.Version -ge [version]'2.8.5.201' } | Select-Object -First 1
        if (-not $nuget) {
            Write-Output 'Installing NuGet package provider (CurrentUser)...'
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force | Out-Null
        }
 
        $installed = Get-InstalledScript -Name Install-VSCode -ErrorAction SilentlyContinue
        if (-not $installed) {
            Write-Output 'Install-Script -Name Install-VSCode -Scope CurrentUser -Force'
            Install-Script -Name Install-VSCode -Scope CurrentUser -Force -Confirm:$false
            $installed = Get-InstalledScript -Name Install-VSCode -ErrorAction Stop
        }
        $scriptPath = Join-Path $installed.InstalledLocation 'Install-VSCode.ps1'
        if (-not (Test-Path -LiteralPath $scriptPath)) { throw "Install-VSCode.ps1 not found at '$scriptPath'." }
 
        $splat = @{ BuildEdition = $BuildEdition }
        if ($AdditionalExtensions) { $splat['AdditionalExtensions'] = @($AdditionalExtensions -split ',' | Where-Object { $_ }) }
        if ($LaunchWhenDone) { $splat['LaunchWhenDone'] = $true }
        Write-Output "Running $scriptPath -BuildEdition $BuildEdition"
        & $scriptPath @splat
        $code = 0
    }
}
catch {
    Write-Output "FAILED: $($_.Exception.Message)"
    $code = 1
}
finally {
    if ($mutex) {
        if ($owned) { try { $mutex.ReleaseMutex() } catch { } }
        $mutex.Dispose()
    }
    try { Stop-Transcript | Out-Null } catch { }
}
exit $code
'@


function Get-SRxVSCodeInstallCommand {
    [CmdletBinding()]
    param(
        [string]$BuildEdition = 'Stable-User',
        [string[]]$AdditionalExtensions = @(),
        [switch]$LaunchWhenDone,
        [string]$WorkFolder = $(Join-Path $(if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { $env:TEMP }) 'SRx')
    )
    $scriptDir = Join-Path $WorkFolder 'VSCode'
    $logDir    = Join-Path $WorkFolder 'Logs'
    foreach ($d in @($scriptDir, $logDir)) { if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } }

    $scriptFile = Join-Path $scriptDir 'Invoke-SRxVSCodeInstall.ps1'
    $utf8Bom = New-Object System.Text.UTF8Encoding -ArgumentList $true
    [System.IO.File]::WriteAllText($scriptFile, $script:SRxVSCodeInstallerScript, $utf8Bom)

    $logFile = Join-Path $logDir ('Install-VSCode-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log')
    $argLine = '-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}" -LogPath "{1}" -BuildEdition {2}' -f $scriptFile, $logFile, $BuildEdition
    $ext = @($AdditionalExtensions | Where-Object { $_ })
    if ($ext.Count -gt 0) { $argLine += (' -AdditionalExtensions "{0}"' -f ($ext -join ',')) }
    if ($LaunchWhenDone)  { $argLine += ' -LaunchWhenDone' }

    return [pscustomobject]@{
        FilePath   = Join-Path $PSHOME 'powershell.exe'
        Arguments  = $argLine
        ScriptFile = $scriptFile
        LogPath    = $logFile
    }
}

function Start-SRxVSCodeUserInstall {
    [CmdletBinding()]
    param(
        [object]$Detection = $null,
        [string]$BuildEdition = 'Stable-User',
        [string[]]$AdditionalExtensions = @(),
        [switch]$LaunchWhenDone,
        [switch]$DryRun,
        [switch]$Force,
        [string]$WorkFolder = $null
    )
    if ($null -eq $Detection) { $Detection = Test-SRxVSCodeInstalled }
    if ($Detection.Installed) { return $null }
    if ($script:SRxVSCodeInstallStarted -and -not $Force -and -not $DryRun) { return $null }

    $cmdArgs = @{ BuildEdition = $BuildEdition; AdditionalExtensions = $AdditionalExtensions; LaunchWhenDone = [bool]$LaunchWhenDone }
    if ($WorkFolder) { $cmdArgs['WorkFolder'] = $WorkFolder }
    $cmd = Get-SRxVSCodeInstallCommand @cmdArgs
    if ($DryRun) {
        return [pscustomobject]@{ Process = $null; LogPath = $cmd.LogPath; Command = $cmd; DryRun = $true }
    }

    $proc = Start-Process -FilePath $cmd.FilePath -ArgumentList $cmd.Arguments -WindowStyle Hidden -PassThru
    try { $null = $proc.Handle } catch { }
    $script:SRxVSCodeInstallStarted = $true
    return [pscustomobject]@{ Process = $proc; LogPath = $cmd.LogPath; Command = $cmd; DryRun = $false }
}

function Reset-SRxVSCodeInstallState {
    [CmdletBinding()]
    param()
    $script:SRxVSCodeInstallStarted = $false
}

Export-ModuleMember -Function Test-SRxVSCodeInstalled, Get-SRxVSCodeInstallCommand, Start-SRxVSCodeUserInstall, Reset-SRxVSCodeInstallState