|
Clear-Host |
|
|
|
function Show-MultiSelectMenu { |
|
[CmdletBinding()] |
|
param( |
|
[Parameter(Mandatory)] [string[]]$Options, |
|
[string]$Prompt = 'Use Up/Down to navigate, Space to toggle, Enter to confirm' |
|
) |
|
$selected = New-Object bool[] $Options.Count |
|
$current = 0 |
|
$done = $false |
|
[Console]::CursorVisible = $false |
|
try { |
|
while (-not $done) { |
|
Clear-Host |
|
Write-Host $Prompt -ForegroundColor Yellow |
|
Write-Host '' |
|
for ($i = 0; $i -lt $Options.Count; $i++) { |
|
$marker = if ($i -eq $current) { '>' } else { ' ' } |
|
$checkbox = if ($selected[$i]) { '[X]' } else { '[ ]' } |
|
$line = "{0} {1} {2}" -f $marker, $checkbox, $Options[$i] |
|
if ($i -eq $current) { |
|
Write-Host $line -ForegroundColor Cyan |
|
} else { |
|
Write-Host $line |
|
} |
|
} |
|
$key = [Console]::ReadKey($true) |
|
switch ($key.Key) { |
|
'UpArrow' { if (--$current -lt 0) { $current = $Options.Count - 1 } } |
|
'DownArrow' { if (++$current -ge $Options.Count) { $current = 0 } } |
|
'Spacebar' { $selected[$current] = -not $selected[$current] } |
|
'Enter' { $done = $true } |
|
} |
|
} |
|
} finally { |
|
[Console]::CursorVisible = $true |
|
Clear-Host |
|
} |
|
$result = @() |
|
for ($i = 0; $i -lt $Options.Count; $i++) { |
|
if ($selected[$i]) { $result += $Options[$i] } |
|
} |
|
return $result |
|
} |
|
|
|
function Show-SingleSelectMenu { |
|
[CmdletBinding()] |
|
param( |
|
[Parameter(Mandatory)] [string[]]$Options, |
|
[string]$Prompt = 'Use Up/Down to navigate, Enter to select' |
|
) |
|
$current = 0 |
|
$done = $false |
|
[Console]::CursorVisible = $false |
|
try { |
|
while (-not $done) { |
|
Clear-Host |
|
Write-Host $Prompt -ForegroundColor Yellow |
|
Write-Host '' |
|
for ($i = 0; $i -lt $Options.Count; $i++) { |
|
$marker = if ($i -eq $current) { '>' } else { ' ' } |
|
$line = "{0} {1}" -f $marker, $Options[$i] |
|
if ($i -eq $current) { |
|
Write-Host $line -ForegroundColor Cyan |
|
} else { |
|
Write-Host $line |
|
} |
|
} |
|
$key = [Console]::ReadKey($true) |
|
switch ($key.Key) { |
|
'UpArrow' { if (--$current -lt 0) { $current = $Options.Count - 1 } } |
|
'DownArrow' { if (++$current -ge $Options.Count) { $current = 0 } } |
|
'Enter' { $done = $true } |
|
} |
|
} |
|
} finally { |
|
[Console]::CursorVisible = $true |
|
Clear-Host |
|
} |
|
return $Options[$current] |
|
} |
|
|
|
function Select-PythonVersion { |
|
$data = Invoke-RestMethod -Uri 'https://www.python.org/ftp/python/index-windows.json' -UseBasicParsing |
|
$stable = $data.versions | Where-Object { $_.'sort-version' -match '^[0-9]+(\.[0-9]+)*$' } |
|
$latest = $stable[0].'sort-version' |
|
$lm = ($latest -split '\.')[0..1] -join '.' |
|
$seen = @() |
|
$opts = @("$latest [LTS]") |
|
foreach ($e in $stable) { |
|
$v = $e.'sort-version' |
|
$minor = ($v -split '\.')[0..1] -join '.' |
|
if ($minor -eq $lm) { continue } |
|
if ($seen -notcontains $minor) { |
|
$seen += $minor |
|
$opts += $v |
|
} |
|
if ($seen.Count -ge 5) { break } |
|
} |
|
return Show-SingleSelectMenu -Options $opts -Prompt 'Select Python version to install' |
|
} |
|
|
|
function Select-NodeVersion { |
|
$vers = Invoke-RestMethod -Uri 'https://nodejs.org/dist/index.json' -UseBasicParsing |
|
$latest = ($vers | Where-Object { $_.lts } | Select-Object -First 1).version.TrimStart('v') |
|
$lm = $latest.Split('.')[0] |
|
$seen = @() |
|
$opts = @("v$latest [LTS]") |
|
foreach ($i in $vers) { |
|
$v = $i.version.TrimStart('v') |
|
$maj = $v.Split('.')[0] |
|
if ($maj -eq $lm) { continue } |
|
if ($seen -notcontains $maj) { |
|
$seen += $maj |
|
$opts += "v$v" |
|
} |
|
if ($seen.Count -ge 5) { break } |
|
} |
|
return Show-SingleSelectMenu -Options $opts -Prompt 'Select Node.js version to install' |
|
} |
|
|
|
function Select-JavaVersion { |
|
$opts = @('Java 10','Java 14','Java 17','Java 20','Java 23') |
|
return Show-SingleSelectMenu -Options $opts -Prompt 'Select Java version to install' |
|
} |
|
|
|
function Install-Python { |
|
param([string]$Version) |
|
$Version = $Version -replace ' \[LTS\]$','' |
|
Write-Host "Installing Python $Version..." -ForegroundColor Green |
|
$suffix = if ([Environment]::Is64BitOperatingSystem) { '-amd64' } else { '' } |
|
$fileName = "python-$Version$suffix.exe" |
|
$url = "https://www.python.org/ftp/python/$Version/$fileName" |
|
$outPath = "$env:TEMP\$fileName" |
|
|
|
Write-Host "Downloading $fileName..." -NoNewline |
|
Invoke-WebRequest -Uri $url -OutFile $outPath -UseBasicParsing |
|
Write-Host ' OK' |
|
|
|
Write-Host "Running installer..." -NoNewline |
|
Start-Process -FilePath $outPath -ArgumentList '/passive InstallAllUsers=1 PrependPath=1' -Wait |
|
Write-Host ' OK' |
|
|
|
Remove-Item $outPath -Force |
|
} |
|
|
|
function Install-NodeJs { |
|
param([string]$Version) |
|
$Version = $Version -replace ' \[LTS\]$','' |
|
Write-Host "Installing Node.js $Version..." -ForegroundColor Green |
|
$installer = "$env:TEMP\node-v$Version-x64.msi" |
|
$url = "https://nodejs.org/dist/$Version/node-$Version-x64.msi" |
|
|
|
Write-Host "Downloading Node.js $Version..." -NoNewline |
|
Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing |
|
Write-Host ' OK' |
|
|
|
Write-Host "Running installer..." -NoNewline |
|
Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i `"$installer`" /passive /qr /norestart" -Wait |
|
Write-Host ' OK' |
|
|
|
Remove-Item $installer -Force |
|
} |
|
|
|
function Install-Java { |
|
param([string]$Version) |
|
$Version = $Version -replace ' \[LTS\]$','' |
|
Write-Host "Installing $Version..." -ForegroundColor Green |
|
switch ($Version) { |
|
'Java 10' { $url = 'https://download.java.net/openjdk/jdk10/ri/jdk-10+44_windows-x64_bin_ri.tar.gz' } |
|
'Java 14' { $url = 'https://download.java.net/openjdk/jdk14/ri/openjdk-14+36_windows-x64_bin.zip' } |
|
'Java 17' { $url = 'https://download.java.net/openjdk/jdk17.0.0.1/ri/openjdk-17.0.0.1+2_windows-x64_bin.zip' } |
|
'Java 20' { $url = 'https://download.java.net/openjdk/jdk20/ri/openjdk-20+36_windows-x64_bin.zip' } |
|
'Java 23' { $url = 'https://download.java.net/openjdk/jdk23/ri/openjdk-23+37_windows-x64_bin.zip' } |
|
} |
|
$fileName = Split-Path $url -Leaf |
|
$outPath = "$env:TEMP\$fileName" |
|
|
|
Write-Host "Downloading $fileName..." -NoNewline |
|
Invoke-WebRequest -Uri $url -OutFile $outPath -UseBasicParsing |
|
Write-Host ' OK' |
|
|
|
$installDir = "C:\Program Files\Java\$Version" |
|
New-Item -ItemType Directory -Path $installDir -Force | Out-Null |
|
|
|
Write-Host "Extracting..." -NoNewline |
|
if ($fileName -match '\.zip$') { |
|
Expand-Archive -Path $outPath -DestinationPath $installDir -Force |
|
} else { |
|
tar -xzf $outPath -C $installDir |
|
} |
|
|
|
$subs = Get-ChildItem -Directory -Path $installDir |
|
if ($subs.Count -eq 1) { |
|
$inner = $subs[0].FullName |
|
Get-ChildItem -Path $inner -Force | Move-Item -Destination $installDir -Force |
|
Remove-Item -Path $inner -Recurse -Force |
|
} |
|
Write-Host ' OK' |
|
Remove-Item $outPath -Force |
|
|
|
Write-Host "Updating PATH..." -NoNewline |
|
& setx PATH "$($env:Path);$installDir\bin" -m | Out-Null |
|
Write-Host ' OK' |
|
} |
|
|
|
function Install-Git { |
|
Write-Host 'Installing Git...' -ForegroundColor Green |
|
$rel = Invoke-RestMethod -Uri 'https://api.github.com/repos/git-for-windows/git/releases/latest' -Headers @{ 'User-Agent' = 'PowerShell' } |
|
$asset = $rel.assets | Where-Object { $_.name -match '64-bit\.exe$' } | Select-Object -First 1 |
|
$installer = "$env:TEMP\$($asset.name)" |
|
|
|
Write-Host "Downloading Git..." -NoNewline |
|
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $installer -UseBasicParsing |
|
Write-Host ' OK' |
|
|
|
Write-Host "Running installer..." -NoNewline |
|
Start-Process -FilePath $installer -ArgumentList '/SP','/SUPPRESSMSGBOXES','/SILENT','/NORESTART' -Wait |
|
Write-Host ' OK' |
|
|
|
Remove-Item $installer -Force |
|
} |
|
|
|
function Install-VCRedist { |
|
Write-Host 'Installing Visual C++ Redistributables...' -ForegroundColor Green |
|
$temp = $env:TEMP |
|
$x86 = Join-Path $temp 'vc_redist.x86.exe' |
|
$x64 = Join-Path $temp 'vc_redist.x64.exe' |
|
|
|
Invoke-WebRequest 'https://aka.ms/vs/17/release/vc_redist.x86.exe' -OutFile $x86 -UseBasicParsing |
|
Start-Process -FilePath $x86 -ArgumentList '/install','/passive','/norestart' -Wait |
|
|
|
if ([Environment]::Is64BitOperatingSystem) { |
|
Invoke-WebRequest 'https://aka.ms/vs/17/release/vc_redist.x64.exe' -OutFile $x64 -UseBasicParsing |
|
Start-Process -FilePath $x64 -ArgumentList '/install','/passive','/norestart' -Wait |
|
Remove-Item $x64 -Force |
|
} |
|
Remove-Item $x86 -Force |
|
} |
|
|
|
function Install-VSCode { |
|
Write-Host 'Installing Visual Studio Code...' -ForegroundColor Green |
|
$out = "$env:TEMP\VSCodeSetup-x64.exe" |
|
Invoke-WebRequest 'https://code.visualstudio.com/sha/download?build=stable&os=win32-x64' -OutFile $out -UseBasicParsing |
|
Start-Process -FilePath $out -ArgumentList '/SILENT','/MERGETASKS=!runcode,addcontextmenufiles,addcontextmenufolders,associatewithfiles,addtopath' -Wait |
|
Remove-Item $out -Force |
|
} |
|
|
|
$allOpts = @( |
|
'Install Python', |
|
'Install Node.js', |
|
'Install Java', |
|
'Install Git', |
|
'Install Visual C++ Redistributables', |
|
'Install Visual Studio Code' |
|
) |
|
$selected = Show-MultiSelectMenu -Options $allOpts |
|
if (-not $selected) { |
|
Write-Host 'No option selected.' -ForegroundColor Red |
|
exit 1 |
|
} |
|
|
|
$versMap = @{} |
|
if ($selected -contains 'Install Python') { $versMap['Install Python'] = Select-PythonVersion } |
|
if ($selected -contains 'Install Node.js') { $versMap['Install Node.js'] = Select-NodeVersion } |
|
if ($selected -contains 'Install Java') { $versMap['Install Java'] = Select-JavaVersion } |
|
|
|
foreach ($opt in $selected) { |
|
switch ($opt) { |
|
'Install Python' { Install-Python -Version $versMap[$opt] } |
|
'Install Node.js' { Install-NodeJs -Version $versMap[$opt] } |
|
'Install Java' { Install-Java -Version $versMap[$opt] } |
|
'Install Git' { Install-Git } |
|
'Install Visual C++ Redistributables' { Install-VCRedist } |
|
'Install Visual Studio Code' { Install-VSCode } |
|
} |
|
} |
|
|
|
Write-Host "`nAll installations complete." -ForegroundColor Green |