docs: Update README and documentation for lxs multi-tool across Linux and Windows platforms.

This commit is contained in:
2026-09-09 15:32:05 -04:00
parent 3fda22d3d5
commit 1df5cdb0bb
37 changed files with 4811 additions and 75 deletions
+40
View File
@@ -0,0 +1,40 @@
<#
LXS - Browsers and media (Windows)
Description: Browsers and media players installed through winget.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_browsers.log'
if (-not (Assert-LxsWindows)) { exit 1 }
$LxsBrowserPackages = @(
@{ Name = 'LibreWolf'; Id = 'LibreWolf.LibreWolf' }
@{ Name = 'Firefox'; Id = 'Mozilla.Firefox' }
@{ Name = 'Brave'; Id = 'Brave.Brave' }
@{ Name = 'Chrome'; Id = 'Google.Chrome' }
@{ Name = 'VLC'; Id = 'VideoLAN.VLC' }
@{ Name = 'OBS Studio'; Id = 'OBSProject.OBSStudio' }
)
Invoke-LxsPackageMenu -Title 'BROWSERS & MEDIA' -Packages $LxsBrowserPackages `
-Note 'LibreWolf is a hardened Firefox fork with telemetry stripped out.' | Out-Null
exit 0
+202
View File
@@ -0,0 +1,202 @@
<#
LXS - Claude Code (Windows)
Description: Install and check the Claude Code CLI.
Two supported install methods, both official:
- winget (Anthropic.ClaudeCode) — does not auto-update
- the native installer from claude.ai — updates itself in the background
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_claude_code.log'
if (-not (Assert-LxsWindows)) { exit 1 }
$LxsClaudeInstallerUrl = 'https://claude.ai/install.ps1'
function Get-LxsClaudeCommand {
return (Get-Command claude -ErrorAction SilentlyContinue)
}
function Show-LxsClaudeStatus {
Clear-Host
Show-LxsBoxTop -Title 'CLAUDE CODE STATUS'
Write-Host ''
$cmd = Get-LxsClaudeCommand
if (-not $cmd) {
Write-Host " $($script:Gray)claude is not on your PATH$($script:NC)"
Write-Host ''
Write-Host "$($script:Gray)If you just installed it, open a new terminal so the PATH change$($script:NC)"
Write-Host "$($script:Gray)takes effect.$($script:NC)"
Write-Host ''
return
}
Write-LxsOk "Found: $($cmd.Source)"
Write-Host ''
& claude --version
Write-Host ''
Show-LxsSeparator
Write-Host ''
Write-LxsInfo 'Running claude doctor (read-only diagnostics)...'
Write-Host ''
& claude doctor
}
function Install-LxsClaudeWinget {
Clear-Host
Show-LxsBoxTop -Title 'INSTALL VIA WINGET'
Write-Host ''
Write-Host 'Installs the Anthropic.ClaudeCode package.'
Write-Host ''
Write-Host "$($script:Gray)winget installs do not update themselves — run option 5, or$($script:NC)"
Write-Host "$($script:Gray)$('winget upgrade Anthropic.ClaudeCode'), to get new versions.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Install Claude Code with winget?' -DefaultYes)) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
if (Install-LxsWingetPackage -Id 'Anthropic.ClaudeCode' -Name 'Claude Code') {
Write-Host ''
Write-LxsOk 'Open a new terminal, then run: claude'
}
}
function Install-LxsClaudeNative {
Clear-Host
Show-LxsBoxTop -Title 'INSTALL VIA THE NATIVE INSTALLER'
Write-Host ''
Write-Host 'This is the install method Anthropic documents as recommended.'
Write-Host 'It downloads and runs a script from claude.ai:'
Write-Host ''
Write-Host " $($script:White)irm $LxsClaudeInstallerUrl | iex$($script:NC)"
Write-Host ''
Write-Host "$($script:Gray)It installs into your user profile (no admin needed) and keeps$($script:NC)"
Write-Host "$($script:Gray)itself updated in the background.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Download and run that installer now?')) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
Write-LxsInfo "Fetching $LxsClaudeInstallerUrl..."
try {
$installer = Invoke-RestMethod -Uri $LxsClaudeInstallerUrl -UseBasicParsing -ErrorAction Stop
} catch {
Write-LxsErr "Download failed: $($_.Exception.Message)"
return
}
Write-LxsOk 'Installer downloaded'
Write-Host ''
Show-LxsSeparator
Write-Host ''
try {
& ([scriptblock]::Create($installer))
} catch {
Write-LxsErr "Installer failed: $($_.Exception.Message)"
return
}
Write-Host ''
Show-LxsSeparator
Write-Host ''
Write-LxsOk 'Open a new terminal, then run: claude'
}
function Install-LxsGitForWindows {
Clear-Host
Show-LxsBoxTop -Title 'GIT FOR WINDOWS'
Write-Host ''
Write-Host 'Optional but recommended alongside Claude Code: with Git for Windows'
Write-Host 'installed, Claude Code uses Git Bash for its Bash tool. Without it,'
Write-Host 'it falls back to the PowerShell tool.'
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Install Git for Windows?' -DefaultYes)) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
Install-LxsWingetPackage -Id 'Git.Git' -Name 'Git for Windows' | Out-Null
}
function Update-LxsClaudeCode {
Clear-Host
Show-LxsBoxTop -Title 'UPDATE CLAUDE CODE'
Write-Host ''
if (-not (Get-LxsClaudeCommand)) {
Write-LxsErr 'claude is not installed (or not on your PATH).'
return
}
# A native install updates itself; a winget install needs winget. Trying
# `claude update` first covers the native case and reports the right thing
# for the winget case.
Write-LxsInfo 'Asking Claude Code to update itself...'
Write-Host ''
& claude update
Write-Host ''
Show-LxsSeparator
Write-Host ''
if ((Test-LxsWinget) -and (Test-LxsPackageInstalled -Id 'Anthropic.ClaudeCode')) {
Write-LxsInfo 'This looks like a winget install; upgrading through winget too...'
Write-Host ''
& winget upgrade --id Anthropic.ClaudeCode --exact --silent `
--accept-package-agreements --accept-source-agreements
Write-Host ''
Write-Host "$($script:Gray)A winget upgrade can fail while Claude Code is running, because$($script:NC)"
Write-Host "$($script:Gray)Windows locks the executable. Close it and retry if that happens.$($script:NC)"
}
}
function Show-LxsClaudeMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'CLAUDE CODE'
Write-Host ''
Show-LxsMenuItem '1' 'Show status' 'version + claude doctor'
Show-LxsMenuItem '2' 'Install (native)' 'auto-updating, recommended'
Show-LxsMenuItem '3' 'Install (winget)' 'manual updates'
Show-LxsMenuItem '4' 'Install Git for Windows' 'enables the Bash tool'
Show-LxsMenuItem '5' 'Update Claude Code'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
Write-Host "$($script:Gray) Claude Code needs a Pro, Max, Team, Enterprise or Console account.$($script:NC)"
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsClaudeStatus; Read-LxsEnter }
'2' { Install-LxsClaudeNative; Read-LxsEnter }
'3' { Install-LxsClaudeWinget; Read-LxsEnter }
'4' { Install-LxsGitForWindows; Read-LxsEnter }
'5' { Update-LxsClaudeCode; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-5.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsClaudeMenu
exit 75
+151
View File
@@ -0,0 +1,151 @@
<#
LXS - Containers and virtualization (Windows)
Description: WSL 2, Hyper-V, Docker Desktop and VirtualBox.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_containers.log'
if (-not (Assert-LxsWindows)) { exit 1 }
$LxsContainerPackages = @(
@{ Name = 'Docker Desktop'; Id = 'Docker.DockerDesktop' }
@{ Name = 'VirtualBox'; Id = 'Oracle.VirtualBox' }
)
function Show-LxsVirtualizationStatus {
Clear-Host
Show-LxsBoxTop -Title 'VIRTUALIZATION STATUS'
Write-Host ''
try {
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
if ($cs.HypervisorPresent) {
Write-LxsOk 'A hypervisor is running (Hyper-V, WSL 2 or another VMM)'
} else {
$proc = Get-CimInstance Win32_Processor -ErrorAction Stop | Select-Object -First 1
if ($proc.VirtualizationFirmwareEnabled) {
Write-LxsOk 'Hardware virtualization is enabled in firmware, no hypervisor running'
} else {
Write-LxsWarn 'Hardware virtualization looks DISABLED in the BIOS/UEFI.'
Write-Host "$($script:Gray) Enable Intel VT-x / AMD-V there, or Docker and WSL 2 will not start.$($script:NC)"
}
}
} catch {
Write-LxsWarn 'Could not read the virtualization state.'
}
Write-Host ''
foreach ($feature in @('Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform', 'Microsoft-Hyper-V-All')) {
try {
$f = Get-WindowsOptionalFeature -Online -FeatureName $feature -ErrorAction Stop
$color = if ($f.State -eq 'Enabled') { $script:Cyan } else { $script:Gray }
Write-Host (" {0,-38} $color{1}$($script:NC)" -f $feature, $f.State)
} catch {
Write-Host (" {0,-38} $($script:Gray)not available on this edition$($script:NC)" -f $feature)
}
}
Write-Host ''
if (Get-Command wsl.exe -ErrorAction SilentlyContinue) {
Write-Host "$($script:Cyan)Installed WSL distributions:$($script:NC)"
& wsl.exe --list --verbose 2>&1 | Out-String | Write-Host
}
}
function Install-LxsWsl {
Clear-Host
Show-LxsBoxTop -Title 'WSL 2'
Write-Host ''
Write-Host 'Enables WSL and the Virtual Machine Platform, then installs the'
Write-Host "default distribution. A $($script:Bold)reboot is required$($script:NC) afterwards."
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Install WSL 2?')) { Write-LxsInfo 'Cancelled.'; return }
if (-not (Test-LxsAdmin)) {
Write-LxsErr 'Installing WSL requires administrator rights.'
return
}
Write-Host ''
& wsl.exe --install
if ($LASTEXITCODE -eq 0) {
Write-LxsOk 'WSL install started — reboot to finish'
} else {
Write-LxsWarn "wsl --install exited with code $LASTEXITCODE"
}
}
function Enable-LxsHyperV {
Clear-Host
Show-LxsBoxTop -Title 'HYPER-V'
Write-Host ''
Write-Host 'Enables the Hyper-V platform and management tools.'
Write-Host ''
Write-Host "$($script:Gray)Hyper-V is only on Pro, Enterprise and Education editions. Turning it$($script:NC)"
Write-Host "$($script:Gray)on takes over the hardware virtualization extensions, which can stop$($script:NC)"
Write-Host "$($script:Gray)VirtualBox and VMware from running their own VMs.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Enable Hyper-V?')) { Write-LxsInfo 'Cancelled.'; return }
if (-not (Test-LxsAdmin)) {
Write-LxsErr 'Enabling Hyper-V requires administrator rights.'
return
}
Write-Host ''
try {
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -All -NoRestart -ErrorAction Stop | Out-Null
Write-LxsOk 'Hyper-V enabled — reboot to finish'
} catch {
Write-LxsErr "Could not enable Hyper-V: $($_.Exception.Message)"
}
}
function Show-LxsContainersMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'CONTAINERS & VM'
Write-Host ''
Show-LxsMenuItem '1' 'Show virtualization status'
Show-LxsMenuItem '2' 'Install WSL 2' 'needs admin + reboot'
Show-LxsMenuItem '3' 'Enable Hyper-V' 'needs admin + reboot'
Show-LxsMenuItem '4' 'Install Docker / VirtualBox'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsVirtualizationStatus; Read-LxsEnter }
'2' { Install-LxsWsl; Read-LxsEnter }
'3' { Enable-LxsHyperV; Read-LxsEnter }
'4' {
Invoke-LxsPackageMenu -Title 'CONTAINERS' -Packages $LxsContainerPackages `
-Note 'Docker Desktop needs WSL 2 or Hyper-V enabled first.' | Out-Null
Read-LxsEnter
}
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-4.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsContainersMenu
exit 75
+93
View File
@@ -0,0 +1,93 @@
<#
LXS - Dev Pack (Windows)
Description: The usual development toolchain, installed through winget.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_dev_pack.log'
if (-not (Assert-LxsWindows)) { exit 1 }
$LxsDevPackages = @(
@{ Name = 'Git'; Id = 'Git.Git' }
@{ Name = 'VS Code'; Id = 'Microsoft.VisualStudioCode' }
@{ Name = 'Node.js LTS'; Id = 'OpenJS.NodeJS.LTS' }
@{ Name = 'Python 3'; Id = 'Python.Python.3.12' }
@{ Name = 'PowerShell 7'; Id = 'Microsoft.PowerShell' }
@{ Name = 'Windows Terminal'; Id = 'Microsoft.WindowsTerminal' }
)
function Enable-LxsWsl {
Clear-Host
Show-LxsBoxTop -Title 'WSL 2'
Write-Host ''
Write-Host 'Installs the Windows Subsystem for Linux (WSL 2) and the default'
Write-Host 'Ubuntu distribution. Enables the Virtual Machine Platform feature,'
Write-Host "so a $($script:Bold)reboot is required$($script:NC) before WSL works."
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Install WSL 2?')) {
Write-LxsInfo 'Cancelled.'
return
}
if (-not (Test-LxsAdmin)) {
Write-LxsErr 'Installing WSL requires administrator rights.'
Write-Host "$($script:Gray) Re-run lxs from an elevated terminal.$($script:NC)"
return
}
Write-Host ''
& wsl.exe --install
if ($LASTEXITCODE -eq 0) {
Write-LxsOk 'WSL install started'
Write-LxsWarn 'Reboot to finish, then run: wsl --install -d Ubuntu'
} else {
Write-LxsWarn "wsl --install exited with code $LASTEXITCODE"
}
}
function Show-LxsDevMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'DEV PACK'
Write-Host ''
Show-LxsMenuItem '1' 'Install dev tools' 'pick from the list'
Show-LxsMenuItem '2' 'Install WSL 2' 'needs admin + reboot'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' {
Invoke-LxsPackageMenu -Title 'DEV PACK' -Packages $LxsDevPackages `
-Note 'Everything here installs per-machine and lands on your PATH.' | Out-Null
Read-LxsEnter
}
'2' { Enable-LxsWsl; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-2.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsDevMenu
exit 75
+64
View File
@@ -0,0 +1,64 @@
<#
LXS - Apps index (Windows)
Description: Interactive menu listing the installers in windows\apps
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
function Show-LxsAppsMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'APPLICATIONS' -Right 'APP_REPOSITORY'
Write-Host ''
Show-LxsMenuItem '01' 'Dev Pack' 'git, vscode, node, python'
Show-LxsMenuItem '02' 'Utilities' '7zip, powertoys, sysinternals'
Show-LxsMenuItem '03' 'Browsers & Media' 'librewolf, firefox, vlc, obs'
Show-LxsMenuItem '04' 'Containers & VM' 'wsl2, docker, hyper-v'
Show-LxsMenuItem '05' 'Claude Code' 'anthropic cli'
Show-LxsMenuItem '00' 'BACK' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
$script = switch -Regex ($choice) {
'^0?1$' { 'apps\dev-pack.ps1' }
'^0?2$' { 'apps\utilities.ps1' }
'^0?3$' { 'apps\browsers-media.ps1' }
'^0?4$' { 'apps\containers.ps1' }
'^0?5$' { 'apps\claude-code.ps1' }
'^0?0$' { 'BACK' }
default { $null }
}
if ($script -eq 'BACK') { return }
if (-not $script) {
Write-LxsErr 'Invalid protocol. Select 0-5.'
Start-Sleep -Seconds 1
continue
}
$code = Invoke-LxsSibling -RelativePath $script -SelfDirectory $PSScriptRoot
if ($code -ne 75) { Read-LxsEnter }
}
}
Show-LxsAppsMenu
exit 75
+40
View File
@@ -0,0 +1,40 @@
<#
LXS - Utilities (Windows)
Description: Everyday utilities installed through winget.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_utilities.log'
if (-not (Assert-LxsWindows)) { exit 1 }
$LxsUtilityPackages = @(
@{ Name = '7-Zip'; Id = '7zip.7zip' }
@{ Name = 'Notepad++'; Id = 'Notepad++.Notepad++' }
@{ Name = 'PowerToys'; Id = 'Microsoft.PowerToys' }
@{ Name = 'Sysinternals'; Id = 'Microsoft.Sysinternals' }
@{ Name = 'Everything'; Id = 'voidtools.Everything' }
@{ Name = 'ShareX'; Id = 'ShareX.ShareX' }
)
Invoke-LxsPackageMenu -Title 'UTILITIES' -Packages $LxsUtilityPackages `
-Note 'Search, screenshots, archives and the Sysinternals suite.' | Out-Null
exit 0
+773
View File
@@ -0,0 +1,773 @@
<#
LXS - Common library (Windows)
Sourced by windows\lxs.ps1 and every sub-script. Mirrors linux/lib/common.sh:
colors, UI helpers, loggers, spinner, guards (admin, disk, winget) and shared
utilities (public IP, password generation, restore point, registry writes).
Compatible with Windows PowerShell 5.1 and PowerShell 7+.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# ═══════════════════════════════════════════════════════════════════════════
# Console setup — UTF-8 output (box drawing chars) and VT sequences (colors).
# Both are best-effort: a console that refuses either falls back to plain text.
# ═══════════════════════════════════════════════════════════════════════════
$script:LxsColorEnabled = $false
$script:LxsUnicodeEnabled = $false
function Initialize-LxsConsole {
[CmdletBinding()]
param()
try {
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
$script:LxsUnicodeEnabled = $true
} catch {
# Redirected output or a locked-down host — fall back to ASCII rules.
$script:LxsUnicodeEnabled = $false
}
# PowerShell 7 renders VT natively; older hosts need ENABLE_VIRTUAL_TERMINAL_PROCESSING.
if ($PSVersionTable.PSVersion.Major -ge 6) {
$script:LxsColorEnabled = $true
} else {
try {
if (-not ('Lxs.NativeConsole' -as [type])) {
Add-Type -Namespace Lxs -Name NativeConsole -MemberDefinition @'
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
'@
}
$handle = [Lxs.NativeConsole]::GetStdHandle(-11)
$mode = 0
if ([Lxs.NativeConsole]::GetConsoleMode($handle, [ref]$mode)) {
if ([Lxs.NativeConsole]::SetConsoleMode($handle, $mode -bor 0x0004)) {
$script:LxsColorEnabled = $true
}
}
} catch {
$script:LxsColorEnabled = $false
}
}
if ($env:LXS_NO_COLOR) { $script:LxsColorEnabled = $false }
$e = [char]0x1B
if ($script:LxsColorEnabled) {
$script:Red = "$e[38;2;255;64;64m" # errors, destructive actions
$script:Cyan = "$e[38;2;0;229;255m" # accents, titles, OK, info
$script:White = "$e[38;2;240;240;240m" # primary text
$script:Gray = "$e[38;2;140;140;140m" # secondary text, separators
$script:NC = "$e[0m"
$script:Bold = "$e[1m"
$script:Dim = "$e[2m"
} else {
$script:Red = ''; $script:Cyan = ''; $script:White = ''; $script:Gray = ''
$script:NC = ''; $script:Bold = ''; $script:Dim = ''
}
}
Initialize-LxsConsole
# ═══════════════════════════════════════════════════════════════════════════
# UI helpers — title + horizontal rule, no closed box. Mirrors common.sh.
#
# Show-LxsBoxTop "TITLE" "RIGHT" → TITLE [ RIGHT ]
# ──────────────────────────────────
# Show-LxsBoxMid "SECTION" → ─ SECTION ────────────────────────
# Show-LxsBoxBottom → ──────────────────────────────────
# Show-LxsMenuItem "01" "LABEL" "desc" → [01] LABEL // desc
# Show-LxsPrompt → >
# ═══════════════════════════════════════════════════════════════════════════
function Get-LxsTermWidth {
$cols = 80
try {
$raw = $Host.UI.RawUI.WindowSize.Width
if ($raw -gt 0) { $cols = $raw }
} catch {
$cols = 80
}
if ($cols -gt 100) { $cols = 100 }
if ($cols -lt 60) { $cols = 60 }
return $cols
}
function Get-LxsRule {
param([int]$Width = 0)
if ($Width -le 0) { $Width = Get-LxsTermWidth }
$char = if ($script:LxsUnicodeEnabled) { [char]0x2500 } else { '-' }
return (New-Object string @($char, $Width))
}
function Show-LxsBoxTop {
param(
[Parameter(Mandatory = $true)][string]$Title,
[string]$Right = ''
)
$cols = Get-LxsTermWidth
if ($Right) {
$padLen = $cols - 2 - $Title.Length - $Right.Length - 4
if ($padLen -lt 1) { $padLen = 1 }
$pad = ' ' * $padLen
Write-Host " $($script:Cyan)$($script:Bold)$Title$($script:NC)$pad$($script:Gray)[ $($script:Cyan)$Right$($script:Gray) ]$($script:NC)"
} else {
Write-Host " $($script:Cyan)$($script:Bold)$Title$($script:NC)"
}
Write-Host "$($script:Gray)$(Get-LxsRule $cols)$($script:NC)"
}
function Show-LxsBoxMid {
param([Parameter(Mandatory = $true)][string]$Title)
$cols = Get-LxsTermWidth
$fillLen = $cols - $Title.Length - 4
if ($fillLen -lt 2) { $fillLen = 2 }
Write-Host "$($script:Gray)$(Get-LxsRule 1) $($script:Cyan)$($script:Bold)$Title$($script:NC) $($script:Gray)$(Get-LxsRule $fillLen)$($script:NC)"
}
function Show-LxsBoxBottom { Write-Host "$($script:Gray)$(Get-LxsRule)$($script:NC)" }
function Show-LxsSeparator { Write-Host "$($script:Gray)$(Get-LxsRule)$($script:NC)" }
function Show-LxsTitle {
param(
[Parameter(Mandatory = $true)][string]$Title,
[string]$Subtitle = ''
)
Write-Host ''
Show-LxsBoxTop -Title $Title -Right $Subtitle
}
# Render a menu line. -Exit colors the key red (used for BACK / quit entries).
function Show-LxsMenuItem {
param(
[Parameter(Mandatory = $true)][string]$Key,
[Parameter(Mandatory = $true)][string]$Label,
[string]$Description = '',
[switch]$Exit
)
$keyColor = if ($Exit) { $script:Red } else { $script:Cyan }
if ($Description) {
$paddedLabel = $Label.PadRight(18)
Write-Host " $keyColor[$Key]$($script:NC) $($script:White)$($script:Bold)$paddedLabel$($script:NC) $($script:Gray)// $Description$($script:NC)"
} else {
Write-Host " $keyColor[$Key]$($script:NC) $($script:White)$($script:Bold)$Label$($script:NC)"
}
}
function Show-LxsPrompt { Write-Host " $($script:Cyan)>$($script:NC) " -NoNewline }
# Read a menu selection. Returns the trimmed string (never $null).
function Read-LxsChoice {
Show-LxsPrompt
$answer = Read-Host
if ($null -eq $answer) { return '' }
return $answer.Trim()
}
function Read-LxsEnter {
param([string]$Message = 'Press Enter to continue...')
Write-Host ''
Read-Host -Prompt $Message | Out-Null
}
# ═══════════════════════════════════════════════════════════════════════════
# Loggers
# ═══════════════════════════════════════════════════════════════════════════
function Write-LxsInfo { param([string]$Message) Write-Host "$($script:Cyan)[..]$($script:NC) $Message" }
function Write-LxsOk { param([string]$Message) Write-Host "$($script:Cyan)[OK]$($script:NC) $Message" }
function Write-LxsWarn { param([string]$Message) Write-Host "$($script:Cyan)[!!]$($script:NC) $Message" }
function Write-LxsErr { param([string]$Message) Write-Host "$($script:Red)[KO]$($script:NC) $Message" }
# ═══════════════════════════════════════════════════════════════════════════
# Option parsing
#
# Sub-scripts read $args by hand, exactly like the bash `for arg in "$@"` loop.
# A param([switch]) block cannot be used: LXS invokes sub-scripts with a
# splatted string[] (& $script @Arguments), and PowerShell binds every element
# of a splatted array positionally — '-Yes' would land in $args as a value and
# the switch would silently stay $false.
#
# $opts = Read-LxsFlags -Arguments $args -Known @('-Yes', '-y', '-Help')
# if ($opts.Unknown.Count -gt 0) { ... }
# $assumeYes = Test-LxsFlag $opts @('-Yes', '-y')
# ═══════════════════════════════════════════════════════════════════════════
function Read-LxsFlags {
param(
[string[]]$Arguments = @(),
[string[]]$Known = @()
)
$present = @{}
$unknown = @()
foreach ($a in $Arguments) {
if ([string]::IsNullOrWhiteSpace($a)) { continue }
$match = $null
foreach ($k in $Known) {
if ($k -ieq $a) { $match = $k; break }
}
if ($match) { $present[$match.ToLower()] = $true } else { $unknown += $a }
}
return [pscustomobject]@{ Present = $present; Unknown = $unknown }
}
function Test-LxsFlag {
param(
[Parameter(Mandatory = $true)]$Parsed,
[Parameter(Mandatory = $true)][string[]]$Name
)
foreach ($n in $Name) {
if ($Parsed.Present.ContainsKey($n.ToLower())) { return $true }
}
return $false
}
# Print the unknown options and return $true when there were any.
function Show-LxsUnknownFlags {
param([Parameter(Mandatory = $true)]$Parsed)
if ($Parsed.Unknown.Count -eq 0) { return $false }
foreach ($u in $Parsed.Unknown) { Write-LxsErr "Unknown option: $u" }
return $true
}
# Yes/No confirmation. Default is No unless -DefaultYes is passed.
function Confirm-LxsAction {
param(
[Parameter(Mandatory = $true)][string]$Question,
[switch]$DefaultYes,
[switch]$AssumeYes
)
if ($AssumeYes) { return $true }
$suffix = if ($DefaultYes) { '[Y/n]' } else { '[y/N]' }
$answer = Read-Host -Prompt "$Question $suffix"
if ([string]::IsNullOrWhiteSpace($answer)) { return [bool]$DefaultYes }
return $answer.Trim().ToLower() -in @('y', 'yes', 'o', 'oui')
}
# ═══════════════════════════════════════════════════════════════════════════
# Progress helpers
#
# Invoke-LxsSpinner — external command, output captured to a log file,
# animated spinner (mirrors run_spinner in common.sh).
# Invoke-LxsStep — inline PowerShell work; prints [..] then [OK]/[KO].
#
# Both return $true on success. Log file: $env:LXS_LOG_FILE or %TEMP%\lxs.log
# ═══════════════════════════════════════════════════════════════════════════
function Get-LxsTempDir {
if ($env:TEMP) { return $env:TEMP }
return [IO.Path]::GetTempPath()
}
function Get-LxsLogFile {
if ($env:LXS_LOG_FILE) { return $env:LXS_LOG_FILE }
return (Join-Path (Get-LxsTempDir) 'lxs.log')
}
function Invoke-LxsSpinner {
param(
[Parameter(Mandatory = $true)][string]$Message,
[Parameter(Mandatory = $true)][string]$FilePath,
[string[]]$ArgumentList = @(),
[int[]]$SuccessCodes = @(0)
)
$log = Get-LxsLogFile
$errLog = "$log.err"
$spinner = '|/-\'.ToCharArray()
$i = 0
Write-Host "$($script:Cyan)[..]$($script:NC) $Message" -NoNewline
$startArgs = @{
FilePath = $FilePath
NoNewWindow = $true
PassThru = $true
RedirectStandardOutput = $log
RedirectStandardError = $errLog
}
if ($ArgumentList.Count -gt 0) { $startArgs['ArgumentList'] = $ArgumentList }
try {
$proc = Start-Process @startArgs
} catch {
Write-Host "`r$($script:Red)[KO]$($script:NC) $Message "
Write-LxsErr $_.Exception.Message
return $false
}
while (-not $proc.HasExited) {
Write-Host "`r$($script:Cyan)[$($spinner[$i % 4]).]$($script:NC) $Message" -NoNewline
$i++
Start-Sleep -Milliseconds 150
}
$proc.WaitForExit()
if (Test-Path $errLog) {
Get-Content $errLog -ErrorAction SilentlyContinue | Add-Content $log -ErrorAction SilentlyContinue
Remove-Item $errLog -Force -ErrorAction SilentlyContinue
}
if ($SuccessCodes -contains $proc.ExitCode) {
Write-Host "`r$($script:Cyan)[OK]$($script:NC) $Message "
return $true
}
Write-Host "`r$($script:Red)[KO]$($script:NC) $Message (exit $($proc.ExitCode)) "
Write-Host "$($script:Gray) Log: $log$($script:NC)"
return $false
}
function Invoke-LxsStep {
param(
[Parameter(Mandatory = $true)][string]$Message,
[Parameter(Mandatory = $true)][scriptblock]$ScriptBlock
)
Write-Host "$($script:Cyan)[..]$($script:NC) $Message" -NoNewline
try {
& $ScriptBlock | Out-Null
Write-Host "`r$($script:Cyan)[OK]$($script:NC) $Message "
return $true
} catch {
Write-Host "`r$($script:Red)[KO]$($script:NC) $Message "
Write-Host "$($script:Gray) $($_.Exception.Message)$($script:NC)"
return $false
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Guards
# ═══════════════════════════════════════════════════════════════════════════
function Test-LxsAdmin {
try {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal $identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch {
return $false
}
}
# Re-launch the calling script elevated, preserving its arguments. Mirrors
# require_root in common.sh. Call near the top of a sub-script:
# Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
# Returns only when already elevated; otherwise it starts a new elevated
# window and exits the current process.
function Assert-LxsAdmin {
param(
[string]$ScriptPath = $PSCommandPath,
[string[]]$Arguments = @()
)
if (Test-LxsAdmin) { return }
if (-not $ScriptPath -or -not (Test-Path $ScriptPath)) {
Write-LxsErr 'Administrator rights are required. Re-run this from an elevated terminal.'
exit 1
}
Write-LxsWarn 'Administrator rights required; opening an elevated window...'
$psExe = (Get-Process -Id $PID).Path
if (-not $psExe) { $psExe = 'powershell.exe' }
# The elevated window is a *new* console: without a trailing pause it would
# close the moment the script ends and take all its output with it.
$quote = { param($v) "'" + ("$v" -replace "'", "''") + "'" }
$inner = "& $(& $quote $ScriptPath)"
foreach ($a in $Arguments) { $inner += " $(& $quote $a)" }
$inner += "; Write-Host ''; Read-Host 'Press Enter to close this window'"
# Single pre-quoted string: Start-Process does not quote array elements,
# and $inner contains spaces. It uses only single quotes internally, so
# wrapping it in double quotes needs no further escaping.
$cmdLine = '-NoProfile -ExecutionPolicy Bypass -Command "' + $inner + '"'
try {
Start-Process -FilePath $psExe -ArgumentList $cmdLine -Verb RunAs | Out-Null
} catch {
Write-LxsErr 'Elevation was refused.'
exit 1
}
Write-LxsInfo 'Continuing in the elevated window.'
exit 0
}
# Windows 10 1809 / Server 2019 (build 17763) is the documented floor.
function Assert-LxsWindows {
param([int]$MinimumBuild = 17763)
# Not named $IsWindows: PowerShell 7 defines that as a read-only automatic
# variable, and assigning to it throws on every call.
$onWindows = $true
if ($PSVersionTable.PSVersion.Major -ge 6) {
$onWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows)
}
if (-not $onWindows) {
Write-LxsErr 'This script only runs on Windows. Use lxs.sh on Linux.'
return $false
}
try {
$build = [int](Get-CimInstance Win32_OperatingSystem).BuildNumber
if ($build -lt $MinimumBuild) {
Write-LxsWarn "Windows build $build is older than the supported floor ($MinimumBuild). Some tools may fail."
}
} catch {
# Not fatal — CIM can be unavailable in stripped-down images.
}
return $true
}
# Mirrors require_disk_space: fail early rather than half-install.
# Test-LxsDiskSpace -MinimumMB 1024 -Paths @('C:\')
function Test-LxsDiskSpace {
param(
[int]$MinimumMB = 500,
[string[]]$Paths = @($env:SystemDrive)
)
$ok = $true
$seen = @{}
foreach ($path in $Paths) {
if ([string]::IsNullOrWhiteSpace($path)) { continue }
$root = try { [System.IO.Path]::GetPathRoot((Resolve-Path $path -ErrorAction Stop).Path) } catch { $null }
if (-not $root) { $root = [System.IO.Path]::GetPathRoot($path) }
if (-not $root) {
Write-LxsErr "Cannot read disk usage for $path"
$ok = $false
continue
}
if ($seen.ContainsKey($root)) { continue }
$seen[$root] = $true
try {
$drive = New-Object System.IO.DriveInfo $root
$freeMB = [math]::Floor($drive.AvailableFreeSpace / 1MB)
} catch {
Write-LxsErr "Cannot read disk usage for $root"
$ok = $false
continue
}
if ($freeMB -lt $MinimumMB) {
Write-LxsErr "Not enough disk space on ${root}: ${freeMB}MB free, ${MinimumMB}MB required."
$ok = $false
}
}
return $ok
}
# ═══════════════════════════════════════════════════════════════════════════
# winget helpers
# ═══════════════════════════════════════════════════════════════════════════
function Test-LxsWinget {
return [bool](Get-Command winget -ErrorAction SilentlyContinue)
}
function Assert-LxsWinget {
if (Test-LxsWinget) { return $true }
Write-LxsErr 'winget (App Installer) is not available on this machine.'
Write-Host "$($script:Gray) Install it from the Microsoft Store ('App Installer') or from$($script:NC)"
Write-Host "$($script:Gray) https://github.com/microsoft/winget-cli/releases, then re-run.$($script:NC)"
return $false
}
# `winget list` is slow (a second or more per call), and a package menu asks
# about a dozen ids at once — so the full listing is fetched once and reused.
$script:LxsWingetListCache = $null
function Get-LxsWingetListText {
param([switch]$Refresh)
if ($Refresh) { $script:LxsWingetListCache = $null }
if ($null -ne $script:LxsWingetListCache) { return $script:LxsWingetListCache }
try {
$script:LxsWingetListCache = (& winget list --accept-source-agreements 2>&1 | Out-String)
} catch {
$script:LxsWingetListCache = ''
}
return $script:LxsWingetListCache
}
function Clear-LxsWingetListCache { $script:LxsWingetListCache = $null }
function Test-LxsPackageInstalled {
param([Parameter(Mandatory = $true)][string]$Id)
$listing = Get-LxsWingetListText
if (-not $listing) { return $false }
return ($listing -match [regex]::Escape($Id))
}
# Install one winget package. Idempotent: an already-installed package is
# reported and skipped. Returns $true when the package ends up installed.
function Install-LxsWingetPackage {
param(
[Parameter(Mandatory = $true)][string]$Id,
[string]$Name = '',
[switch]$Force
)
if (-not $Name) { $Name = $Id }
if (-not $Force -and (Test-LxsPackageInstalled -Id $Id)) {
Write-LxsOk "$Name is already installed"
return $true
}
# winget.exe is an App Execution Alias; resolve the real path so output
# redirection in Invoke-LxsSpinner works reliably.
$wingetPath = (Get-Command winget -ErrorAction SilentlyContinue).Source
if (-not $wingetPath) { $wingetPath = 'winget.exe' }
# 0 = installed. -1978335135 (0x8A150061) = "no applicable update", which
# winget also returns when the package is already present at that version.
$wingetArgs = @(
'install', '--id', $Id, '--exact',
'--silent',
'--accept-package-agreements', '--accept-source-agreements',
'--disable-interactivity'
)
$installed = Invoke-LxsSpinner -Message "Installing $Name" -FilePath $wingetPath `
-ArgumentList $wingetArgs -SuccessCodes @(0, -1978335135)
Clear-LxsWingetListCache
if (-not $installed) {
Write-Host "$($script:Gray) Package id: $Id — check it with: winget search --id $Id$($script:NC)"
}
return $installed
}
# Install a list of @{ Id = '...'; Name = '...' } entries, reporting a summary.
function Install-LxsWingetPackageSet {
param(
[Parameter(Mandatory = $true)][array]$Packages
)
$failed = @()
foreach ($pkg in $Packages) {
if (-not (Install-LxsWingetPackage -Id $pkg.Id -Name $pkg.Name)) {
$failed += $pkg.Name
}
}
Write-Host ''
if ($failed.Count -eq 0) {
Write-LxsOk "All $($Packages.Count) package(s) are installed"
} else {
Write-LxsWarn "$($failed.Count) of $($Packages.Count) failed: $($failed -join ', ')"
}
return ($failed.Count -eq 0)
}
# Interactive package picker shared by every apps\ script: lists the bundle,
# marks what is already installed, and installs "all" or the numbers picked.
# Returns $true when the user actually installed something.
function Invoke-LxsPackageMenu {
param(
[Parameter(Mandatory = $true)][string]$Title,
[Parameter(Mandatory = $true)][array]$Packages,
[string]$Note = ''
)
if (-not (Assert-LxsWinget)) { return $false }
Clear-Host
Show-LxsBoxTop -Title $Title -Right 'WINGET'
Write-Host ''
if ($Note) {
Write-Host "$($script:Gray)$Note$($script:NC)"
Write-Host ''
}
Write-LxsInfo 'Checking what is already installed...'
Get-LxsWingetListText -Refresh | Out-Null
Write-Host ''
for ($i = 0; $i -lt $Packages.Count; $i++) {
$pkg = $Packages[$i]
$state = if (Test-LxsPackageInstalled -Id $pkg.Id) { "$($script:Cyan)[installed]$($script:NC)" } else { '' }
Write-Host (" $($script:Cyan)[{0,2}]$($script:NC) $($script:White)$($script:Bold){1,-24}$($script:NC) $($script:Gray){2,-34}$($script:NC) {3}" -f `
($i + 1), $pkg.Name, $pkg.Id, $state)
}
Write-Host ''
Write-Host "$($script:Gray)Enter the numbers to install, separated by commas (e.g. 1,3,5),$($script:NC)"
Write-Host "$($script:Gray)or 'all' for everything above. Empty cancels.$($script:NC)"
Write-Host ''
$selection = Read-Host -Prompt 'Selection'
if ([string]::IsNullOrWhiteSpace($selection)) {
Write-LxsInfo 'Cancelled.'
return $false
}
$targets = @()
if ($selection.Trim() -ieq 'all') {
$targets = $Packages
} else {
foreach ($part in ($selection -split ',')) {
$part = $part.Trim()
if ($part -notmatch '^\d+$') { Write-LxsErr "Not a number: $part"; return $false }
$idx = [int]$part - 1
if ($idx -lt 0 -or $idx -ge $Packages.Count) { Write-LxsErr "Out of range: $part"; return $false }
$targets += $Packages[$idx]
}
}
Write-Host ''
Show-LxsSeparator
Write-Host ''
Install-LxsWingetPackageSet -Packages $targets | Out-Null
return $true
}
# ═══════════════════════════════════════════════════════════════════════════
# Misc utilities
# ═══════════════════════════════════════════════════════════════════════════
function Get-LxsPublicIP {
foreach ($url in @('https://ifconfig.me/ip', 'https://icanhazip.com', 'https://ipinfo.io/ip')) {
try {
$ip = (Invoke-RestMethod -Uri $url -TimeoutSec 5 -ErrorAction Stop)
if ($ip) { return ("$ip").Trim() }
} catch {
continue
}
}
try {
$local = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } |
Select-Object -First 1 -ExpandProperty IPAddress
if ($local) { return $local }
} catch {
# no network
}
return 'unknown'
}
function New-LxsPassword {
param([int]$Length = 32)
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
$bytes = New-Object byte[] $Length
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
$sb = New-Object System.Text.StringBuilder
foreach ($b in $bytes) { [void]$sb.Append($chars[$b % $chars.Length]) }
return $sb.ToString()
}
# Create a System Restore point, enabling System Protection first if needed.
# Used as a safety net before debloat/hardening. Requires admin.
function New-LxsRestorePoint {
param([string]$Description = 'LXS checkpoint')
if (-not (Test-LxsAdmin)) {
Write-LxsErr 'A restore point requires administrator rights.'
return $false
}
$drive = "$env:SystemDrive\"
try {
Enable-ComputerRestore -Drive $drive -ErrorAction Stop
} catch {
Write-LxsWarn "Could not enable System Protection on ${drive}: $($_.Exception.Message)"
}
# Windows rate-limits restore points to one per 24h unless this is relaxed.
try {
New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore' `
-Name 'SystemRestorePointCreationFrequency' -Value 0 -PropertyType DWord -Force -ErrorAction Stop | Out-Null
} catch {
# Non-fatal: the checkpoint may be skipped if one was made recently.
}
try {
Checkpoint-Computer -Description $Description -RestorePointType 'MODIFY_SETTINGS' -ErrorAction Stop
Write-LxsOk "Restore point created: $Description"
return $true
} catch {
Write-LxsErr "Restore point failed: $($_.Exception.Message)"
return $false
}
}
# Write a registry value, creating the key path when it does not exist.
function Set-LxsRegistryValue {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)]$Value,
[ValidateSet('String', 'ExpandString', 'Binary', 'DWord', 'MultiString', 'QWord')]
[string]$Type = 'DWord'
)
try {
if (-not (Test-Path $Path)) { New-Item -Path $Path -Force -ErrorAction Stop | Out-Null }
New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $Type -Force -ErrorAction Stop | Out-Null
return $true
} catch {
Write-LxsErr "Registry write failed ($Path\$Name): $($_.Exception.Message)"
return $false
}
}
# Append a directory to the *user* PATH (no admin needed). Idempotent.
function Add-LxsUserPath {
param([Parameter(Mandatory = $true)][string]$Directory)
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($null -eq $current) { $current = '' }
# Windows paths are case-insensitive and a trailing backslash is
# meaningless, so compare normalized — otherwise every run appends a
# near-duplicate entry.
$normalized = $Directory.TrimEnd('\')
foreach ($entry in ($current -split ';')) {
if ($entry -and ($entry.Trim().TrimEnd('\') -ieq $normalized)) { return $false }
}
$updated = if ($current.TrimEnd(';')) { "$($current.TrimEnd(';'));$Directory" } else { $Directory }
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
$env:Path = "$env:Path;$Directory"
return $true
}
# Run a sibling script from the same directory. Prefers the local file
# (installed layout or repo checkout), falls back to the remote raw URL.
# Mirrors run_sibling() in linux/tools/index.sh.
function Invoke-LxsSibling {
param(
[Parameter(Mandatory = $true)][string]$RelativePath, # e.g. tools\system-info.ps1
[string[]]$Arguments = @(),
[string]$SelfDirectory = ''
)
$name = Split-Path $RelativePath -Leaf
if (-not $SelfDirectory) { $SelfDirectory = $PSScriptRoot }
$local = Join-Path $SelfDirectory $name
if (Test-Path $local) {
$global:LASTEXITCODE = 0
& $local @Arguments
return $LASTEXITCODE
}
$base = $env:LXS_RAW_PLATFORM_BASE
if (-not $base) { $base = 'https://git.hyko.cx/hykocx/lxs/raw/branch/main/windows' }
$url = "$base/$($RelativePath -replace '\\', '/')"
$temp = Join-Path (Get-LxsTempDir) ("lxs.{0}.{1}.ps1" -f [IO.Path]::GetFileNameWithoutExtension($name), [guid]::NewGuid().ToString('N').Substring(0, 8))
Write-LxsInfo "Fetching $($script:Bold)$name$($script:NC)..."
try {
Invoke-WebRequest -Uri $url -OutFile $temp -UseBasicParsing -Headers @{ 'Cache-Control' = 'no-cache' } -ErrorAction Stop
} catch {
Write-LxsErr "Failed to download $RelativePath"
Write-Host "$($script:Gray) URL: $url$($script:NC)"
return 1
}
Write-LxsOk 'Payload acquired'
try {
$global:LASTEXITCODE = 0
& $temp @Arguments
return $LASTEXITCODE
} finally {
Remove-Item $temp -Force -ErrorAction SilentlyContinue
}
}
+502
View File
@@ -0,0 +1,502 @@
<#
LXS - Windows multi-tool
Description: Centralized workstation management and deployment toolkit
Repo: https://git.hyko.cx/hykocx/lxs
License: MIT
Mirrors linux/lxs.sh: same verbs, same menu, same update flow.
Compatible with Windows PowerShell 5.1 and PowerShell 7+.
#>
# Raw $args pass-through: sub-scripts get their own flags verbatim
# (e.g. `lxs tool harden -Yes`) without this script trying to bind them.
$LxsArgs = @($args)
$LxsCommand = if ($LxsArgs.Count -gt 0) { [string]$LxsArgs[0] } else { '' }
$LxsRest = if ($LxsArgs.Count -gt 1) { $LxsArgs[1..($LxsArgs.Count - 1)] } else { @() }
# ═══════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════
$LxsScriptDir = $PSScriptRoot
if (-not $LxsScriptDir) { $LxsScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$LxsVersion = 'dev'
if ($LxsScriptDir) {
$versionFile = Join-Path $LxsScriptDir '..\VERSION'
if (Test-Path $versionFile) {
$raw = (Get-Content $versionFile -TotalCount 1 -ErrorAction SilentlyContinue)
if ($raw) { $LxsVersion = $raw.Trim() }
}
}
$LxsRepoUrl = 'https://git.hyko.cx/hykocx/lxs'
$LxsBranch = if ($env:LXS_BRANCH) { $env:LXS_BRANCH } else { 'main' }
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { "$LxsRepoUrl/raw/branch/$LxsBranch" }
# Platform sub-tree: every apps\ and tools\ path resolves under it.
$LxsPlatformDir = 'windows'
$env:LXS_RAW_BASE = $LxsRawBase
$env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/$LxsPlatformDir"
$LxsZipUrl = if ($env:LXS_ZIP_URL) { $env:LXS_ZIP_URL } else { "$LxsRepoUrl/archive/$LxsBranch.zip" }
# LOCALAPPDATA is normally set; fall back so a service/SYSTEM context that
# lacks it still resolves to a writable directory instead of erroring out.
$LxsLocalAppData = $env:LOCALAPPDATA
if (-not $LxsLocalAppData) {
$LxsLocalAppData = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE 'AppData\Local' } else { [IO.Path]::GetTempPath() }
}
$LxsInstallDir = if ($env:LXS_INSTALL_DIR) { $env:LXS_INSTALL_DIR } else { Join-Path $LxsLocalAppData 'lxs' }
$LxsBinDir = Join-Path $LxsInstallDir 'bin'
$LxsShimPath = Join-Path $LxsBinDir 'lxs.cmd'
$LxsCacheDir = Join-Path $LxsInstallDir 'cache'
$LxsVersionCache = Join-Path $LxsCacheDir 'remote_version'
$LxsVersionTtl = 86400
$script:LxsUpdateAvailable = $false
$script:LxsRemoteVersion = ''
$script:LxsPublicIpCache = $null
# ═══════════════════════════════════════════════════════════════════════════
# Load common library
# Prefers a sibling lib\common.ps1 (installed or repo checkout); falls back to
# fetching it from the remote when run via `irm ... | iex`.
# ═══════════════════════════════════════════════════════════════════════════
$libPath = if ($LxsScriptDir) { Join-Path $LxsScriptDir 'lib\common.ps1' } else { $null }
if ($libPath -and (Test-Path $libPath)) {
. $libPath
} else {
try {
$libSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($libSource))
}
# ═══════════════════════════════════════════════════════════════════════════
# Core helpers
# ═══════════════════════════════════════════════════════════════════════════
function Get-LxsCachedPublicIP {
if ($null -eq $script:LxsPublicIpCache) {
$script:LxsPublicIpCache = Get-LxsPublicIP
}
return $script:LxsPublicIpCache
}
# Compare two dotted versions. Returns $true when $Candidate is newer.
function Test-LxsNewerVersion {
param([string]$Current, [string]$Candidate)
try {
return ([version]$Candidate -gt [version]$Current)
} catch {
return ($Candidate -ne $Current)
}
}
function Test-LxsRemoteVersion {
$script:LxsUpdateAvailable = $false
$script:LxsRemoteVersion = ''
try {
if (-not (Test-Path $LxsCacheDir)) { New-Item -ItemType Directory -Path $LxsCacheDir -Force | Out-Null }
} catch {
return
}
$age = [double]::MaxValue
if (Test-Path $LxsVersionCache) {
$age = ((Get-Date) - (Get-Item $LxsVersionCache).LastWriteTime).TotalSeconds
}
if ($age -ge $LxsVersionTtl) {
# Refresh inline but on a short leash — a slow repo must not stall the menu.
try {
$remote = Invoke-RestMethod -Uri "$LxsRawBase/VERSION" -TimeoutSec 3 `
-Headers @{ 'Cache-Control' = 'no-cache' } -UseBasicParsing -ErrorAction Stop
("$remote").Trim() | Set-Content -Path $LxsVersionCache -Encoding ASCII
} catch {
# Offline or unreachable: fall through to whatever is cached.
}
}
if (-not (Test-Path $LxsVersionCache)) { return }
$cached = (Get-Content $LxsVersionCache -TotalCount 1 -ErrorAction SilentlyContinue)
if (-not $cached) { return }
$cached = $cached.Trim()
if (Test-LxsNewerVersion -Current $LxsVersion -Candidate $cached) {
$script:LxsUpdateAvailable = $true
$script:LxsRemoteVersion = $cached
}
}
function Show-LxsLogo {
Show-LxsBoxTop -Title "LXS // v$LxsVersion" -Right 'WINDOWS'
}
function Show-LxsHeader {
Clear-Host
Show-LxsLogo
if ($script:LxsUpdateAvailable) {
Write-Host ''
Write-Host " $($script:Cyan)[!!] UPDATE_AVAILABLE$($script:NC) $($script:White)v$($script:LxsRemoteVersion)$($script:NC) $($script:Gray)// run ``lxs update``$($script:NC)"
}
$hostname = $env:COMPUTERNAME
$osName = 'Windows'
$build = ''
$uptime = 'unknown'
$totalMem = 0.0
$usedMem = 0.0
try {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$osName = ($os.Caption -replace '^Microsoft ', '').Trim()
$build = "$($os.Version) ($($os.BuildNumber))"
$span = (Get-Date) - $os.LastBootUpTime
$uptime = "{0}d {1}h {2}m" -f $span.Days, $span.Hours, $span.Minutes
$totalMem = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$usedMem = [math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / 1MB, 1)
} catch {
# CIM unavailable — the header degrades but the menu still works.
}
$cores = $env:NUMBER_OF_PROCESSORS
$ip = Get-LxsCachedPublicIP
$disk = 'unknown'
try {
$sys = New-Object System.IO.DriveInfo "$env:SystemDrive\"
$totalGB = [math]::Round($sys.TotalSize / 1GB, 1)
$usedGB = [math]::Round(($sys.TotalSize - $sys.AvailableFreeSpace) / 1GB, 1)
$pct = if ($sys.TotalSize -gt 0) { [math]::Round(100 * ($sys.TotalSize - $sys.AvailableFreeSpace) / $sys.TotalSize) } else { 0 }
$disk = "${usedGB}G/${totalGB}G (${pct}%)"
} catch {
# No access to the system drive info.
}
Write-Host ''
Write-Host (" $($script:Cyan)NODE$($script:NC) $($script:White){0,-26}$($script:NC) $($script:Cyan)BUILD$($script:NC) $($script:White){1}$($script:NC)" -f $hostname, $build)
Write-Host (" $($script:Cyan)ADDR$($script:NC) $($script:White){0,-26}$($script:NC) $($script:Cyan)UP$($script:NC) $($script:White){1}$($script:NC)" -f $ip, $uptime)
Write-Host (" $($script:Cyan)OS$($script:NC) $($script:White){0,-26}$($script:NC) $($script:Cyan)CPU$($script:NC) $($script:White){1} cores$($script:NC)" -f $osName, $cores)
Write-Host (" $($script:Cyan)DISK$($script:NC) $($script:White){0,-26}$($script:NC) $($script:Cyan)MEM$($script:NC) $($script:White){1} / {2} GB$($script:NC)" -f $disk, $usedMem, $totalMem)
Write-Host ''
}
# Run a sub-script (tools\... or apps\...). Prefers the installed copy;
# falls back to fetching it. Mirrors download_and_run() in linux/lxs.sh.
function Invoke-LxsScript {
param(
[Parameter(Mandatory = $true)][string]$RelativePath,
[string[]]$Arguments = @()
)
$name = Split-Path $RelativePath -Leaf
$localPath = $null
if ($LxsScriptDir) { $localPath = Join-Path $LxsScriptDir $RelativePath }
$target = $null
$temp = $null
if ($localPath -and (Test-Path $localPath)) {
$target = $localPath
} else {
$url = "$env:LXS_RAW_PLATFORM_BASE/$($RelativePath -replace '\\', '/')"
$temp = Join-Path (Get-LxsTempDir) ("lxs.{0}.{1}.ps1" -f [IO.Path]::GetFileNameWithoutExtension($name), [guid]::NewGuid().ToString('N').Substring(0, 8))
Write-Host ''
Write-LxsInfo "Fetching $($script:Bold)$name$($script:NC)..."
try {
Invoke-WebRequest -Uri $url -OutFile $temp -UseBasicParsing `
-Headers @{ 'Cache-Control' = 'no-cache' } -ErrorAction Stop
} catch {
Write-LxsErr "Failed to download $RelativePath"
Write-Host "$($script:Gray) URL: $url$($script:NC)"
return 1
}
Write-LxsOk 'Payload acquired'
$target = $temp
}
Write-Host ''
Show-LxsSeparator
Write-Host ''
$global:LASTEXITCODE = 0
try {
& $target @Arguments
$code = $LASTEXITCODE
} finally {
if ($temp) { Remove-Item $temp -Force -ErrorAction SilentlyContinue }
}
if ($null -eq $code) { $code = 0 }
Write-Host ''
Show-LxsSeparator
if ($code -eq 0 -or $code -eq 75) {
Write-LxsOk 'Script completed successfully'
} else {
Write-LxsWarn "Script exited with code: $code"
}
return $code
}
# ═══════════════════════════════════════════════════════════════════════════
# CLI commands
# ═══════════════════════════════════════════════════════════════════════════
function Invoke-LxsCmdVersion { Write-Host "lxs $LxsVersion" }
function Invoke-LxsCmdHelp {
Write-Host @"
LXS - Windows multi-tool (v$LxsVersion)
Usage:
lxs Interactive menu (browse apps and tools)
lxs setup Install lxs to $LxsInstallDir and add it to PATH
lxs update Update all installed files to latest
lxs install <app> Install an application bundle
lxs tool <name> [args] Run a system tool
lxs info Show system info
lxs version Show version
lxs help Show this help
Applications:
dev Git, VS Code, Node.js, Python, PowerShell 7, Windows Terminal
utilities 7-Zip, Notepad++, PowerToys, Sysinternals, Everything, ShareX
browsers Firefox, LibreWolf, Chrome, Brave, VLC, OBS Studio
containers WSL2, Hyper-V, Docker Desktop, VirtualBox
claude-code Claude Code CLI (+ Git for Windows)
Tools:
system System information and diagnostics
network Network diagnostics and repair
cleanup Free disk space (temp, update cache, recycle bin, WinSxS)
repair SFC / DISM / chkdsk integrity repair
restore-point Create, list or roll back to a System Restore point
update Windows Update + winget upgrade
harden Baseline security hardening
remote-access Remote Desktop and OpenSSH server
debloat Remove preinstalled apps and disable telemetry
Source: $LxsRepoUrl
"@
}
function Invoke-LxsCmdInstall {
param([string]$App, [string[]]$Arguments = @())
switch ($App) {
'dev' { return (Invoke-LxsScript 'apps\dev-pack.ps1' $Arguments) }
'utilities' { return (Invoke-LxsScript 'apps\utilities.ps1' $Arguments) }
'browsers' { return (Invoke-LxsScript 'apps\browsers-media.ps1' $Arguments) }
'containers' { return (Invoke-LxsScript 'apps\containers.ps1' $Arguments) }
'claude-code' { return (Invoke-LxsScript 'apps\claude-code.ps1' $Arguments) }
'' { Write-LxsErr 'Missing app name. Try: lxs help'; return 1 }
default { Write-LxsErr "Unknown app: $App. Try: lxs help"; return 1 }
}
}
function Invoke-LxsCmdTool {
param([string]$Tool, [string[]]$Arguments = @())
switch ($Tool) {
'system' { return (Invoke-LxsScript 'tools\system-info.ps1' $Arguments) }
'network' { return (Invoke-LxsScript 'tools\net-diag.ps1' $Arguments) }
'cleanup' { return (Invoke-LxsScript 'tools\cleanup.ps1' $Arguments) }
'repair' { return (Invoke-LxsScript 'tools\repair.ps1' $Arguments) }
'restore-point' { return (Invoke-LxsScript 'tools\restore-point.ps1' $Arguments) }
'update' { return (Invoke-LxsScript 'tools\update-windows.ps1' $Arguments) }
'harden' { return (Invoke-LxsScript 'tools\harden.ps1' $Arguments) }
'remote-access' { return (Invoke-LxsScript 'tools\remote-access.ps1' $Arguments) }
'debloat' { return (Invoke-LxsScript 'tools\debloat.ps1' $Arguments) }
'' { Write-LxsErr 'Missing tool name. Try: lxs help'; return 1 }
default { Write-LxsErr "Unknown tool: $Tool. Try: lxs help"; return 1 }
}
}
# ═══════════════════════════════════════════════════════════════════════════
# setup / update — download the repo zip and mirror it into $LxsInstallDir.
# Everything lands under %LOCALAPPDATA%, so no elevation is needed here;
# individual tools elevate themselves when they touch the machine.
# ═══════════════════════════════════════════════════════════════════════════
function Invoke-LxsSync {
param([ValidateSet('setup', 'update')][string]$Action = 'update')
$work = Join-Path (Get-LxsTempDir) ("lxs.install.{0}" -f [guid]::NewGuid().ToString('N').Substring(0, 8))
try {
New-Item -ItemType Directory -Path $work -Force | Out-Null
} catch {
Write-LxsErr 'Failed to create temp dir'
return 1
}
try {
$zip = Join-Path $work 'lxs.zip'
Write-LxsInfo "Fetching $LxsZipUrl..."
try {
$progressBackup = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $LxsZipUrl -OutFile $zip -UseBasicParsing `
-Headers @{ 'Cache-Control' = 'no-cache' } -ErrorAction Stop
$ProgressPreference = $progressBackup
} catch {
Write-LxsErr "Download failed: $($_.Exception.Message)"
return 1
}
$extracted = Join-Path $work 'extracted'
try {
Expand-Archive -Path $zip -DestinationPath $extracted -Force -ErrorAction Stop
} catch {
Write-LxsErr "Extraction failed: $($_.Exception.Message)"
return 1
}
# Gitea wraps the tree in a single <repo>/ directory: strip it.
$root = $extracted
$entries = @(Get-ChildItem -Path $extracted -Force)
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $root = $entries[0].FullName }
if (-not (Test-Path (Join-Path $root "$LxsPlatformDir\lxs.ps1")) -or
-not (Test-Path (Join-Path $root 'VERSION'))) {
Write-LxsErr "Archive is missing $LxsPlatformDir\lxs.ps1 or VERSION"
return 1
}
# Replace the payload directories wholesale (so files deleted upstream
# really disappear) but keep bin\ and cache\, so the shim on PATH and
# the version cache survive an update. The old tree is moved aside
# rather than deleted, and put back if the copy fails.
if (-not (Test-Path $LxsInstallDir)) { New-Item -ItemType Directory -Path $LxsInstallDir -Force | Out-Null }
$backup = Join-Path $work 'previous'
New-Item -ItemType Directory -Path $backup -Force | Out-Null
foreach ($dir in @('linux', 'windows')) {
$dest = Join-Path $LxsInstallDir $dir
if (Test-Path $dest) { Move-Item -Path $dest -Destination (Join-Path $backup $dir) -Force -ErrorAction SilentlyContinue }
}
try {
Copy-Item -Path (Join-Path $root '*') -Destination $LxsInstallDir -Recurse -Force -ErrorAction Stop
} catch {
Write-LxsErr "Copy failed: $($_.Exception.Message)"
Write-LxsInfo 'Restoring the previous install...'
foreach ($dir in @('linux', 'windows')) {
$saved = Join-Path $backup $dir
if (Test-Path $saved) {
$dest = Join-Path $LxsInstallDir $dir
if (Test-Path $dest) { Remove-Item $dest -Recurse -Force -ErrorAction SilentlyContinue }
Move-Item -Path $saved -Destination $dest -Force -ErrorAction SilentlyContinue
}
}
return 1
}
# Command shim on PATH. -ExecutionPolicy Bypass so a restricted policy
# (the default on desktop SKUs) does not block the installed scripts.
if (-not (Test-Path $LxsBinDir)) { New-Item -ItemType Directory -Path $LxsBinDir -Force | Out-Null }
$shim = @"
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\$LxsPlatformDir\lxs.ps1" %*
exit /b %ERRORLEVEL%
"@
Set-Content -Path $LxsShimPath -Value $shim -Encoding ASCII -Force
try {
if (Add-LxsUserPath -Directory $LxsBinDir) {
Write-LxsOk "Added $LxsBinDir to your user PATH (open a new terminal to use ``lxs``)"
}
} catch {
# A blocked registry write must not fail an otherwise good install.
Write-LxsWarn "Could not update your user PATH: $($_.Exception.Message)"
Write-Host "$($script:Gray) Run lxs directly: $LxsShimPath$($script:NC)"
}
Remove-Item $LxsVersionCache -Force -ErrorAction SilentlyContinue
$newVersion = (Get-Content (Join-Path $LxsInstallDir 'VERSION') -TotalCount 1 -ErrorAction SilentlyContinue)
if ($newVersion) { $newVersion = $newVersion.Trim() }
Write-LxsOk "lxs $newVersion installed in $LxsInstallDir"
Write-LxsOk "Command: $LxsShimPath"
return 0
} finally {
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Interactive menu
# ═══════════════════════════════════════════════════════════════════════════
function Invoke-LxsMainMenu {
if (-not (Assert-LxsWindows)) { exit 1 }
Test-LxsRemoteVersion
while ($true) {
Show-LxsHeader
Show-LxsBoxMid 'SELECT'
Write-Host ''
Show-LxsMenuItem '01' 'APPLICATIONS' 'deploy stacks'
Show-LxsMenuItem '02' 'TOOLS' 'windows toolbox'
Show-LxsMenuItem 'UU' 'FETCH_UPDATE' 'sync remote'
Show-LxsMenuItem '00' 'JACK_OUT' 'exit shell' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
switch -Regex ($choice) {
'^0?1$' { Invoke-LxsScript 'apps\index.ps1' | Out-Null; continue }
'^0?2$' { Invoke-LxsScript 'tools\index.ps1' | Out-Null; continue }
'^[uU]{1,2}$' {
if ((Invoke-LxsSync -Action update) -eq 0) {
$reinstalled = Join-Path $LxsInstallDir "$LxsPlatformDir\lxs.ps1"
if (Test-Path $reinstalled) {
Read-LxsEnter 'Press Enter to reload with the new version...'
& $reinstalled
exit 0
}
}
Read-LxsEnter
continue
}
'^0?0$' {
Clear-Host
Show-LxsLogo
Write-Host "$($script:Cyan)JACK_OUT$($script:NC) $($script:Gray)// session terminated$($script:NC)"
Write-Host ''
exit 0
}
default {
Write-LxsErr 'Invalid protocol.'
Start-Sleep -Seconds 1
}
}
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Entrypoint
# ═══════════════════════════════════════════════════════════════════════════
$exitCode = 0
switch ($LxsCommand) {
'install' { $exitCode = Invoke-LxsCmdInstall -App ([string]($LxsRest | Select-Object -First 1)) -Arguments @($LxsRest | Select-Object -Skip 1) }
'tool' { $exitCode = Invoke-LxsCmdTool -Tool ([string]($LxsRest | Select-Object -First 1)) -Arguments @($LxsRest | Select-Object -Skip 1) }
'info' { Show-LxsHeader }
'update' { $exitCode = Invoke-LxsSync -Action update }
'setup' { $exitCode = Invoke-LxsSync -Action setup }
'install-self' { $exitCode = Invoke-LxsSync -Action setup }
'version' { Invoke-LxsCmdVersion }
'-v' { Invoke-LxsCmdVersion }
'--version' { Invoke-LxsCmdVersion }
'help' { Invoke-LxsCmdHelp }
'-h' { Invoke-LxsCmdHelp }
'--help' { Invoke-LxsCmdHelp }
'' { Invoke-LxsMainMenu }
default {
Write-LxsErr "Unknown command: $LxsCommand"
Invoke-LxsCmdHelp
$exitCode = 1
}
}
exit $exitCode
+227
View File
@@ -0,0 +1,227 @@
<#
LXS - Disk Cleanup (Windows)
Description: Reclaim disk space (temp files, Windows Update cache, recycle
bin, Prefetch, error reports, WinSxS component store).
Repo: https://git.hyko.cx/hykocx/lxs
Usage: cleanup.ps1 [-Yes] [-NoUpdateCache] [-NoRecycleBin] [-NoPrefetch]
[-NoComponentCleanup] [-Help]
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_cleanup.log'
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
'-Yes', '-y', '-NoUpdateCache', '-NoRecycleBin', '-NoPrefetch',
'-NoComponentCleanup', '-Help', '-h'
)
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
$Yes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
$NoUpdateCache = Test-LxsFlag $LxsOpts @('-NoUpdateCache')
$NoRecycleBin = Test-LxsFlag $LxsOpts @('-NoRecycleBin')
$NoPrefetch = Test-LxsFlag $LxsOpts @('-NoPrefetch')
$NoComponentCleanup = Test-LxsFlag $LxsOpts @('-NoComponentCleanup')
$Help = Test-LxsFlag $LxsOpts @('-Help', '-h')
if ($Help) {
Write-Host @'
Usage: cleanup.ps1 [options]
Options:
-Yes Skip the confirmation prompt
-NoUpdateCache Skip the Windows Update download cache
-NoRecycleBin Skip emptying the recycle bin
-NoPrefetch Skip the Prefetch folder
-NoComponentCleanup Skip DISM /StartComponentCleanup (the slow one)
-Help Show this help
'@
exit 0
}
function Get-LxsFreeBytes {
try { return (New-Object System.IO.DriveInfo "$env:SystemDrive\").AvailableFreeSpace } catch { return 0 }
}
function Format-LxsSize {
param([double]$Bytes)
if ($Bytes -ge 1GB) { return ('{0:N2} GB' -f ($Bytes / 1GB)) }
if ($Bytes -ge 1MB) { return ('{0:N1} MB' -f ($Bytes / 1MB)) }
return ('{0:N0} KB' -f ($Bytes / 1KB))
}
function Get-LxsFolderSize {
param([string]$Path)
if (-not (Test-Path $Path)) { return 0 }
try {
$sum = (Get-ChildItem -Path $Path -Recurse -Force -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
if ($null -eq $sum) { return 0 }
return $sum
} catch {
return 0
}
}
# Delete the *contents* of a folder, never the folder itself. Files locked by a
# running process are skipped silently — that is expected on a live system.
function Clear-LxsFolderContents {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Label
)
if (-not (Test-Path $Path)) {
Write-Host "$($script:Gray) $Label — not present, skipped$($script:NC)"
return 0
}
$before = Get-LxsFolderSize -Path $Path
$skipped = 0
Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue | ForEach-Object {
try {
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction Stop
} catch {
$skipped++
}
}
$after = Get-LxsFolderSize -Path $Path
$freed = [math]::Max(0, $before - $after)
$suffix = if ($skipped -gt 0) { " ($skipped item(s) in use, skipped)" } else { '' }
Write-LxsOk "$Label$(Format-LxsSize $freed) reclaimed$suffix"
return $freed
}
# ═══════════════════════════════════════════════════════════════════════════
# Plan
# ═══════════════════════════════════════════════════════════════════════════
if (-not (Assert-LxsWindows)) { exit 1 }
$isAdmin = Test-LxsAdmin
$doUpdateCache = (-not $NoUpdateCache) -and $isAdmin
$doPrefetch = (-not $NoPrefetch) -and $isAdmin
$doComponent = (-not $NoComponentCleanup) -and $isAdmin
Clear-Host
Show-LxsBoxTop -Title 'DISK CLEANUP'
Write-Host ''
Write-Host 'The following locations will be cleaned:'
Write-Host ' - User temp folder'
if ($isAdmin) { Write-Host ' - System temp folder (C:\Windows\Temp)' }
if (-not $NoRecycleBin) { Write-Host ' - Recycle bin (all drives)' }
if ($doUpdateCache) { Write-Host ' - Windows Update download cache' }
if ($doPrefetch) { Write-Host ' - Prefetch folder' }
if ($isAdmin) { Write-Host ' - Windows Error Reporting queue and Delivery Optimization cache' }
if ($doComponent) { Write-Host ' - WinSxS component store (DISM /StartComponentCleanup) - slow' }
Write-Host ''
if (-not $isAdmin) {
Write-LxsWarn 'Not running as administrator: system-wide locations are skipped.'
Write-Host "$($script:Gray) Re-run from an elevated terminal to clean them.$($script:NC)"
Write-Host ''
}
Show-LxsSeparator
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Proceed with the cleanup?' -DefaultYes -AssumeYes:$Yes)) {
Write-LxsInfo 'Cancelled.'
exit 0
}
# ═══════════════════════════════════════════════════════════════════════════
# Run
# ═══════════════════════════════════════════════════════════════════════════
$freeBefore = Get-LxsFreeBytes
Write-Host ''
Write-LxsInfo "Free space before: $(Format-LxsSize $freeBefore)"
Write-Host ''
Clear-LxsFolderContents -Path (Get-LxsTempDir) -Label 'User temp' | Out-Null
if ($isAdmin) {
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Temp') -Label 'System temp' | Out-Null
Clear-LxsFolderContents -Path (Join-Path $env:ProgramData 'Microsoft\Windows\WER\ReportQueue') -Label 'Error reports' | Out-Null
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\DeliveryOptimization') -Label 'Delivery Optimization' | Out-Null
}
if ($doPrefetch) {
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Prefetch') -Label 'Prefetch' | Out-Null
}
if (-not $NoRecycleBin) {
try {
Clear-RecycleBin -Force -ErrorAction Stop
Write-LxsOk 'Recycle bin emptied'
} catch {
# Clear-RecycleBin throws when the bin is already empty.
Write-Host "$($script:Gray) Recycle bin — already empty or not accessible$($script:NC)"
}
}
if ($doUpdateCache) {
# The download cache can only be cleared with the update services stopped;
# they are restarted afterwards whether or not the deletion succeeded.
$services = @('wuauserv', 'bits')
$stopped = @()
foreach ($svc in $services) {
try {
$s = Get-Service -Name $svc -ErrorAction Stop
if ($s.Status -eq 'Running') {
Stop-Service -Name $svc -Force -ErrorAction Stop
$stopped += $svc
}
} catch {
Write-LxsWarn "Could not stop $svc — the update cache may be partially locked."
}
}
try {
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\Download') -Label 'Windows Update cache' | Out-Null
} finally {
foreach ($svc in $stopped) {
try { Start-Service -Name $svc -ErrorAction Stop } catch { Write-LxsWarn "Failed to restart $svc — start it manually." }
}
}
}
if ($doComponent) {
Write-Host ''
Write-LxsInfo 'Running DISM component cleanup (several minutes, no output until done)...'
$ok = Invoke-LxsSpinner -Message 'DISM /StartComponentCleanup' -FilePath 'dism.exe' `
-ArgumentList @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
if (-not $ok) { Write-LxsWarn 'Component cleanup did not complete; see the log above.' }
}
# Windows.old is reported, never auto-deleted: removing it forfeits the
# ability to roll back a feature update.
$windowsOld = Join-Path $env:SystemDrive 'Windows.old'
if (Test-Path $windowsOld) {
Write-Host ''
Write-LxsWarn "$windowsOld exists ($(Format-LxsSize (Get-LxsFolderSize -Path $windowsOld)))."
Write-Host "$($script:Gray) It holds your previous Windows install. Removing it blocks rollback;$($script:NC)"
Write-Host "$($script:Gray) use Settings > System > Storage > Temporary files if you want it gone.$($script:NC)"
}
$freeAfter = Get-LxsFreeBytes
Write-Host ''
Show-LxsSeparator
Write-Host ''
Write-LxsOk "Free space after: $(Format-LxsSize $freeAfter)"
Write-LxsOk "Reclaimed: $(Format-LxsSize ([math]::Max(0, $freeAfter - $freeBefore)))"
Write-Host ''
exit 0
+483
View File
@@ -0,0 +1,483 @@
<#
LXS - Debloat and privacy (Windows)
Description: Remove preinstalled apps you pick, and turn off telemetry,
Cortana, ads, tracking tasks, Copilot and Recall.
Safety rules this script follows:
- a restore point is created before the first change of a session
- no app is removed unless you explicitly select it
- the registry/service/task tweaks can be undone from the menu (option 8)
- apps you remove come back only by reinstalling them from the Store
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_debloat.log'
if (-not (Assert-LxsWindows)) { exit 1 }
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
$script:LxsCheckpointDone = $false
# One restore point per session, created lazily before the first real change.
function Assert-LxsCheckpoint {
if ($script:LxsCheckpointDone) { return $true }
Write-Host ''
Write-LxsInfo 'Creating a restore point before changing anything...'
$ok = New-LxsRestorePoint -Description 'LXS before debloat'
if (-not $ok) {
Write-Host ''
Write-LxsWarn 'The restore point could not be created.'
if (-not (Confirm-LxsAction -Question 'Continue anyway, without a rollback point?')) {
return $false
}
}
$script:LxsCheckpointDone = $true
return $true
}
# ═══════════════════════════════════════════════════════════════════════════
# Preinstalled apps
#
# Only packages on this list are ever offered. Anything not listed here —
# including the Store, App Installer (winget), Terminal, Defender UI, the
# VCLibs/.NET runtime frameworks and every driver package — is left alone,
# because removing those breaks Windows or LXS itself.
# ═══════════════════════════════════════════════════════════════════════════
$LxsRemovableApps = @(
@{ Pattern = 'Microsoft.3DBuilder'; Label = '3D Builder' }
@{ Pattern = 'Microsoft.549981C3F5F10'; Label = 'Cortana' }
@{ Pattern = 'Microsoft.BingFinance'; Label = 'Bing Finance' }
@{ Pattern = 'Microsoft.BingNews'; Label = 'Bing News' }
@{ Pattern = 'Microsoft.BingSports'; Label = 'Bing Sports' }
@{ Pattern = 'Microsoft.BingWeather'; Label = 'Weather' }
@{ Pattern = 'Microsoft.BingSearch'; Label = 'Web Search (Bing)' }
@{ Pattern = 'Microsoft.GetHelp'; Label = 'Get Help' }
@{ Pattern = 'Microsoft.Getstarted'; Label = 'Tips / Get Started' }
@{ Pattern = 'Microsoft.Messaging'; Label = 'Messaging' }
@{ Pattern = 'Microsoft.Microsoft3DViewer'; Label = '3D Viewer' }
@{ Pattern = 'Microsoft.MicrosoftJournal'; Label = 'Journal' }
@{ Pattern = 'Microsoft.MicrosoftOfficeHub'; Label = 'Office Hub' }
@{ Pattern = 'Microsoft.MicrosoftSolitaireCollection'; Label = 'Solitaire Collection' }
@{ Pattern = 'Microsoft.MixedReality.Portal'; Label = 'Mixed Reality Portal' }
@{ Pattern = 'Microsoft.NetworkSpeedTest'; Label = 'Network Speed Test' }
@{ Pattern = 'Microsoft.News'; Label = 'News' }
@{ Pattern = 'Microsoft.Office.OneNote'; Label = 'OneNote (Store version)' }
@{ Pattern = 'Microsoft.Office.Sway'; Label = 'Sway' }
@{ Pattern = 'Microsoft.OneConnect'; Label = 'Mobile Plans' }
@{ Pattern = 'Microsoft.People'; Label = 'People' }
@{ Pattern = 'Microsoft.PowerAutomateDesktop'; Label = 'Power Automate Desktop' }
@{ Pattern = 'Microsoft.Print3D'; Label = 'Print 3D' }
@{ Pattern = 'Microsoft.SkypeApp'; Label = 'Skype' }
@{ Pattern = 'Microsoft.Todos'; Label = 'Microsoft To Do' }
@{ Pattern = 'Microsoft.Wallet'; Label = 'Wallet' }
@{ Pattern = 'Microsoft.WindowsAlarms'; Label = 'Alarms & Clock' }
@{ Pattern = 'Microsoft.WindowsFeedbackHub'; Label = 'Feedback Hub' }
@{ Pattern = 'Microsoft.WindowsMaps'; Label = 'Maps' }
@{ Pattern = 'Microsoft.WindowsSoundRecorder'; Label = 'Sound Recorder' }
@{ Pattern = 'Microsoft.Xbox.TCUI'; Label = 'Xbox TCUI' }
@{ Pattern = 'Microsoft.XboxApp'; Label = 'Xbox Console Companion' }
@{ Pattern = 'Microsoft.XboxGameOverlay'; Label = 'Xbox Game Overlay' }
@{ Pattern = 'Microsoft.XboxGamingOverlay'; Label = 'Xbox Game Bar' }
@{ Pattern = 'Microsoft.XboxIdentityProvider'; Label = 'Xbox Identity Provider' }
@{ Pattern = 'Microsoft.XboxSpeechToTextOverlay'; Label = 'Xbox Speech To Text' }
@{ Pattern = 'Microsoft.YourPhone'; Label = 'Phone Link' }
@{ Pattern = 'Microsoft.ZuneMusic'; Label = 'Media Player / Groove Music' }
@{ Pattern = 'Microsoft.ZuneVideo'; Label = 'Movies & TV' }
@{ Pattern = 'MicrosoftTeams'; Label = 'Teams (personal / chat)' }
@{ Pattern = 'MSTeams'; Label = 'Teams (new client)' }
@{ Pattern = 'Clipchamp.Clipchamp'; Label = 'Clipchamp' }
@{ Pattern = 'Microsoft.Copilot'; Label = 'Copilot app' }
@{ Pattern = 'Microsoft.WindowsFamilySafety'; Label = 'Family Safety' }
@{ Pattern = 'SpotifyAB.SpotifyMusic'; Label = 'Spotify (OEM stub)' }
@{ Pattern = 'Disney.37853FC22B2CE'; Label = 'Disney+ (OEM stub)' }
)
function Get-LxsInstalledBloat {
$found = @()
foreach ($entry in $LxsRemovableApps) {
try {
$pkgs = @(Get-AppxPackage -Name $entry.Pattern -ErrorAction SilentlyContinue)
} catch {
$pkgs = @()
}
if ($pkgs.Count -gt 0) {
$found += [pscustomobject]@{
Label = $entry.Label
Pattern = $entry.Pattern
Name = $pkgs[0].Name
Version = $pkgs[0].Version
}
}
}
return $found
}
function Remove-LxsSelectedApps {
Clear-Host
Show-LxsBoxTop -Title 'REMOVE PREINSTALLED APPS'
Write-Host ''
Write-LxsInfo 'Scanning installed packages...'
$found = @(Get-LxsInstalledBloat)
Write-Host ''
if ($found.Count -eq 0) {
Write-LxsOk 'None of the known preinstalled apps are present.'
return
}
for ($i = 0; $i -lt $found.Count; $i++) {
Write-Host (" $($script:Cyan)[{0,2}]$($script:NC) $($script:White){1,-32}$($script:NC) $($script:Gray){2}$($script:NC)" -f `
($i + 1), $found[$i].Label, $found[$i].Name)
}
Write-Host ''
Write-Host "$($script:Gray)Enter the numbers to remove, separated by commas (e.g. 1,4,7),$($script:NC)"
Write-Host "$($script:Gray)or 'all' for every entry above. Empty cancels — nothing is removed$($script:NC)"
Write-Host "$($script:Gray)unless you name it here.$($script:NC)"
Write-Host ''
$selection = Read-Host -Prompt 'Selection'
if ([string]::IsNullOrWhiteSpace($selection)) {
Write-LxsInfo 'Cancelled — nothing removed.'
return
}
$targets = @()
if ($selection.Trim() -ieq 'all') {
$targets = $found
} else {
foreach ($part in ($selection -split ',')) {
$part = $part.Trim()
if ($part -notmatch '^\d+$') {
Write-LxsErr "Not a number: $part"
return
}
$idx = [int]$part - 1
if ($idx -lt 0 -or $idx -ge $found.Count) {
Write-LxsErr "Out of range: $part"
return
}
$targets += $found[$idx]
}
}
$targets = @($targets | Sort-Object Name -Unique)
Write-Host ''
Write-Host 'These will be removed for the current user:'
foreach ($t in $targets) { Write-Host " - $($t.Label)" }
Write-Host ''
Write-Host "$($script:Gray)They can be reinstalled later from the Microsoft Store.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question "Remove $($targets.Count) app(s)?")) {
Write-LxsInfo 'Cancelled.'
return
}
if (-not (Assert-LxsCheckpoint)) { return }
$alsoProvisioned = Confirm-LxsAction -Question 'Also remove them from the image, so new user accounts do not get them?'
Write-Host ''
foreach ($t in $targets) {
try {
Get-AppxPackage -Name $t.Pattern -ErrorAction Stop | Remove-AppxPackage -ErrorAction Stop
Write-LxsOk "$($t.Label) removed"
} catch {
Write-LxsWarn "$($t.Label): $($_.Exception.Message)"
}
if ($alsoProvisioned) {
try {
$prov = @(Get-AppxProvisionedPackage -Online -ErrorAction Stop |
Where-Object { $_.DisplayName -like $t.Pattern })
foreach ($p in $prov) {
Remove-AppxProvisionedPackage -Online -PackageName $p.PackageName -ErrorAction Stop | Out-Null
Write-Host "$($script:Gray) also removed from the image$($script:NC)"
}
} catch {
Write-LxsWarn "$($t.Label): could not remove the provisioned package."
}
}
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Privacy tweaks
#
# Every entry carries the value to set and the value to restore, so option 8
# can undo exactly what option 2-6 did. Default = $null means "delete the
# value", which is what returns a policy key to its Windows default.
# ═══════════════════════════════════════════════════════════════════════════
$LxsTweaks = @(
# --- telemetry ---
@{ Group = 'telemetry'; Label = 'Diagnostic data set to the minimum';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'; Name = 'AllowTelemetry'; Value = 0; Default = $null }
@{ Group = 'telemetry'; Label = 'Do not send device names with telemetry';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'; Name = 'AllowDeviceNameInTelemetry'; Value = 0; Default = $null }
@{ Group = 'telemetry'; Label = 'Advertising ID disabled';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\AdvertisingInfo'; Name = 'Enabled'; Value = 0; Default = 1 }
@{ Group = 'telemetry'; Label = 'App launch tracking disabled';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'; Name = 'Start_TrackProgs'; Value = 0; Default = 1 }
@{ Group = 'telemetry'; Label = 'Feedback requests disabled';
Path = 'HKCU:\SOFTWARE\Microsoft\Siuf\Rules'; Name = 'NumberOfSIUFInPeriod'; Value = 0; Default = $null }
# --- cortana / search ---
@{ Group = 'cortana'; Label = 'Cortana disabled';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'AllowCortana'; Value = 0; Default = $null }
@{ Group = 'cortana'; Label = 'Web results in Start disabled';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'DisableWebSearch'; Value = 1; Default = $null }
@{ Group = 'cortana'; Label = 'Connected web search disabled';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'ConnectedSearchUseWeb'; Value = 0; Default = $null }
@{ Group = 'cortana'; Label = 'Search highlights disabled';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\SearchSettings'; Name = 'IsDynamicSearchBoxEnabled'; Value = 0; Default = 1 }
# --- ads and suggestions ---
@{ Group = 'ads'; Label = 'Start menu app suggestions off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SystemPaneSuggestionsEnabled'; Value = 0; Default = 1 }
@{ Group = 'ads'; Label = 'Silent install of promoted apps off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SilentInstalledAppsEnabled'; Value = 0; Default = 1 }
@{ Group = 'ads'; Label = 'Lock screen spotlight ads off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'RotatingLockScreenOverlayEnabled'; Value = 0; Default = 1 }
@{ Group = 'ads'; Label = 'Windows tips and tricks off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-338389Enabled'; Value = 0; Default = 1 }
@{ Group = 'ads'; Label = 'Suggested content in Settings off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-338393Enabled'; Value = 0; Default = 1 }
@{ Group = 'ads'; Label = 'Welcome experience after updates off';
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-310093Enabled'; Value = 0; Default = 1 }
# --- copilot / recall ---
@{ Group = 'ai'; Label = 'Windows Copilot turned off';
Path = 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot'; Name = 'TurnOffWindowsCopilot'; Value = 1; Default = $null }
@{ Group = 'ai'; Label = 'Windows Copilot turned off (machine-wide)';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot'; Name = 'TurnOffWindowsCopilot'; Value = 1; Default = $null }
@{ Group = 'ai'; Label = 'Recall snapshots disabled';
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI'; Name = 'DisableAIDataAnalysis'; Value = 1; Default = $null }
@{ Group = 'ai'; Label = 'Recall snapshots disabled (per user)';
Path = 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI'; Name = 'DisableAIDataAnalysis'; Value = 1; Default = $null }
)
$LxsTrackingTasks = @(
'\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraisal',
'\Microsoft\Windows\Application Experience\ProgramDataUpdater',
'\Microsoft\Windows\Application Experience\StartupAppTask',
'\Microsoft\Windows\Customer Experience Improvement Program\Consolidator',
'\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip',
'\Microsoft\Windows\Autochk\Proxy',
'\Microsoft\Windows\Feedback\Siuf\DmClient',
'\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload'
)
$LxsTrackingServices = @(
@{ Name = 'DiagTrack'; Label = 'Connected User Experiences and Telemetry'; Default = 'Automatic' },
@{ Name = 'dmwappushservice'; Label = 'WAP Push Message Routing'; Default = 'Manual' }
)
function Invoke-LxsTweakGroup {
param(
[Parameter(Mandatory = $true)][string]$Group,
[Parameter(Mandatory = $true)][string]$Title
)
$entries = @($LxsTweaks | Where-Object { $_.Group -eq $Group })
Clear-Host
Show-LxsBoxTop -Title $Title
Write-Host ''
Write-Host 'The following settings will be changed:'
foreach ($e in $entries) { Write-Host " - $($e.Label)" }
if ($Group -eq 'telemetry') {
Write-Host ' - Telemetry services disabled (DiagTrack, dmwappushservice)'
Write-Host ' - Tracking scheduled tasks disabled'
}
Write-Host ''
Write-Host "$($script:Gray)All of this is reversible from menu option 8.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Apply?')) {
Write-LxsInfo 'Cancelled.'
return
}
if (-not (Assert-LxsCheckpoint)) { return }
Write-Host ''
foreach ($e in $entries) {
if (Set-LxsRegistryValue -Path $e.Path -Name $e.Name -Value $e.Value) {
Write-LxsOk $e.Label
}
}
if ($Group -ne 'telemetry') { return }
Write-Host ''
foreach ($svc in $LxsTrackingServices) {
try {
$s = Get-Service -Name $svc.Name -ErrorAction Stop
if ($s.Status -eq 'Running') { Stop-Service -Name $svc.Name -Force -ErrorAction SilentlyContinue }
Set-Service -Name $svc.Name -StartupType Disabled -ErrorAction Stop
Write-LxsOk "$($svc.Label) service disabled"
} catch {
Write-LxsWarn "$($svc.Label): $($_.Exception.Message)"
}
}
Write-Host ''
$disabled = 0
foreach ($taskPath in $LxsTrackingTasks) {
$leaf = Split-Path $taskPath -Leaf
$parent = (Split-Path $taskPath -Parent) + '\'
try {
Disable-ScheduledTask -TaskName $leaf -TaskPath $parent -ErrorAction Stop | Out-Null
$disabled++
} catch {
# Absent on this edition/build — nothing to disable.
}
}
Write-LxsOk "$disabled tracking scheduled task(s) disabled"
}
function Restore-LxsTweaks {
Clear-Host
Show-LxsBoxTop -Title 'REVERT PRIVACY TWEAKS'
Write-Host ''
Write-Host 'This restores every registry value, service and scheduled task that'
Write-Host 'this script changes, back to the Windows default.'
Write-Host ''
Write-Host "$($script:Gray)Apps you removed are NOT restored — reinstall them from the Store.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Revert the privacy tweaks?')) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
foreach ($e in $LxsTweaks) {
if ($null -eq $e.Default) {
try {
if (Test-Path $e.Path) {
Remove-ItemProperty -Path $e.Path -Name $e.Name -ErrorAction Stop
}
Write-LxsOk "$($e.Label) — policy removed"
} catch {
# Already absent: that is the default state we wanted.
}
} else {
if (Set-LxsRegistryValue -Path $e.Path -Name $e.Name -Value $e.Default) {
Write-LxsOk "$($e.Label) — restored to default"
}
}
}
Write-Host ''
foreach ($svc in $LxsTrackingServices) {
try {
Set-Service -Name $svc.Name -StartupType $svc.Default -ErrorAction Stop
Write-LxsOk "$($svc.Label) service restored to $($svc.Default)"
} catch {
Write-LxsWarn "$($svc.Label): $($_.Exception.Message)"
}
}
Write-Host ''
$enabled = 0
foreach ($taskPath in $LxsTrackingTasks) {
$leaf = Split-Path $taskPath -Leaf
$parent = (Split-Path $taskPath -Parent) + '\'
try {
Enable-ScheduledTask -TaskName $leaf -TaskPath $parent -ErrorAction Stop | Out-Null
$enabled++
} catch {
# Not present on this build.
}
}
Write-LxsOk "$enabled scheduled task(s) re-enabled"
Write-Host ''
Write-LxsWarn 'Sign out and back in for the per-user settings to take effect.'
}
function Show-LxsDebloatStatus {
Clear-Host
Show-LxsBoxTop -Title 'CURRENT STATE'
Write-Host ''
$installed = @(Get-LxsInstalledBloat)
Write-Host " Known preinstalled apps still present : $($installed.Count) / $($LxsRemovableApps.Count)"
$applied = 0
foreach ($e in $LxsTweaks) {
try {
$current = (Get-ItemProperty -Path $e.Path -Name $e.Name -ErrorAction Stop).($e.Name)
if ($current -eq $e.Value) { $applied++ }
} catch {
# Value absent -> tweak not applied.
}
}
Write-Host " Privacy tweaks applied : $applied / $($LxsTweaks.Count)"
foreach ($svc in $LxsTrackingServices) {
try {
$s = Get-Service -Name $svc.Name -ErrorAction Stop
Write-Host " $($svc.Name.PadRight(38)): $($s.Status) / $($s.StartType)"
} catch {
Write-Host " $($svc.Name.PadRight(38)): not present"
}
}
Write-Host ''
}
function Show-LxsDebloatMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'DEBLOAT & PRIVACY' -Right 'ADMIN'
Write-Host ''
Show-LxsMenuItem '1' 'Show current state'
Show-LxsMenuItem '2' 'Remove preinstalled apps' 'you pick each one'
Show-LxsMenuItem '3' 'Disable telemetry' 'policy, services, tasks'
Show-LxsMenuItem '4' 'Disable Cortana & web search'
Show-LxsMenuItem '5' 'Disable ads & suggestions'
Show-LxsMenuItem '6' 'Disable Copilot & Recall'
Show-LxsMenuItem '7' 'Create a restore point' 'before doing anything'
Show-LxsMenuItem '8' 'Revert privacy tweaks' 'undo 3-6'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsDebloatStatus; Read-LxsEnter }
'2' { Remove-LxsSelectedApps; Read-LxsEnter }
'3' { Invoke-LxsTweakGroup -Group 'telemetry' -Title 'DISABLE TELEMETRY'; Read-LxsEnter }
'4' { Invoke-LxsTweakGroup -Group 'cortana' -Title 'DISABLE CORTANA & WEB SEARCH'; Read-LxsEnter }
'5' { Invoke-LxsTweakGroup -Group 'ads' -Title 'DISABLE ADS & SUGGESTIONS'; Read-LxsEnter }
'6' { Invoke-LxsTweakGroup -Group 'ai' -Title 'DISABLE COPILOT & RECALL'; Read-LxsEnter }
'7' {
if (New-LxsRestorePoint -Description 'LXS manual checkpoint') { $script:LxsCheckpointDone = $true }
Read-LxsEnter
}
'8' { Restore-LxsTweaks; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsDebloatMenu
exit 75
+327
View File
@@ -0,0 +1,327 @@
<#
LXS - Harden Windows
Description: Apply a baseline security posture (firewall, SMBv1, UAC,
Defender, LLMNR/NetBIOS) and audit local accounts.
Mirror of linux/tools/harden.sh.
Repo: https://git.hyko.cx/hykocx/lxs
Usage: harden.ps1 [-Yes] [-NoFirewall] [-NoSmb] [-NoUac] [-NoDefender]
[-NoNameResolution] [-NoRestorePoint] [-Help]
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_harden.log'
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
'-Yes', '-y', '-NoFirewall', '-NoSmb', '-NoUac', '-NoDefender',
'-NoNameResolution', '-NoRestorePoint', '-Help', '-h'
)
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
$AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
$DoFirewall = -not (Test-LxsFlag $LxsOpts @('-NoFirewall'))
$DoSmb = -not (Test-LxsFlag $LxsOpts @('-NoSmb'))
$DoUac = -not (Test-LxsFlag $LxsOpts @('-NoUac'))
$DoDefender = -not (Test-LxsFlag $LxsOpts @('-NoDefender'))
$DoNameResolution = -not (Test-LxsFlag $LxsOpts @('-NoNameResolution'))
$DoRestorePoint = -not (Test-LxsFlag $LxsOpts @('-NoRestorePoint'))
if (Test-LxsFlag $LxsOpts @('-Help', '-h')) {
Write-Host @'
Usage: harden.ps1 [options]
Options:
-Yes, -y Skip the confirmation prompt
-NoFirewall Skip the Windows Firewall configuration
-NoSmb Skip disabling SMBv1
-NoUac Skip raising the UAC prompt level
-NoDefender Skip the Microsoft Defender settings
-NoNameResolution Skip disabling LLMNR and NetBIOS-over-TCP/IP
-NoRestorePoint Do not create a restore point first
-Help, -h Show this help
'@
exit 0
}
if (-not (Assert-LxsWindows)) { exit 1 }
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
# ═══════════════════════════════════════════════════════════════════════════
# Plan
# ═══════════════════════════════════════════════════════════════════════════
Clear-Host
Show-LxsBoxTop -Title 'HARDEN WINDOWS' -Right 'ADMIN'
Write-Host ''
Write-Host 'The following changes will be applied to this machine:'
if ($DoFirewall) {
Write-Host ' - Firewall: enabled on Domain/Private/Public, inbound blocked by default'
}
if ($DoSmb) {
Write-Host ' - SMBv1: client and server disabled (legacy, exploited by WannaCry/EternalBlue)'
}
if ($DoUac) {
Write-Host ' - UAC: prompt for consent on the secure desktop, never silently elevate'
}
if ($DoDefender) {
Write-Host ' - Defender: real-time protection, PUA blocking, cloud protection, network protection'
}
if ($DoNameResolution) {
Write-Host ' - LLMNR and NetBIOS-over-TCP/IP disabled (blocks classic LAN credential relay)'
}
Write-Host ' - Audit: local administrators, blank passwords, Guest account (report only)'
Write-Host ''
if ($DoNameResolution) {
Write-Host "$($script:Gray)Note: disabling LLMNR/NetBIOS can break flat-name resolution on small$($script:NC)"
Write-Host "$($script:Gray)LANs without a DNS server. Pass -NoNameResolution to keep them.$($script:NC)"
Write-Host ''
}
Show-LxsSeparator
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Apply this hardening baseline?' -AssumeYes:$AssumeYes)) {
Write-LxsInfo 'Cancelled.'
exit 0
}
if ($DoRestorePoint) {
Write-Host ''
Write-LxsInfo 'Creating a restore point first...'
New-LxsRestorePoint -Description 'LXS before harden' | Out-Null
}
$changes = 0
$problems = 0
# ═══════════════════════════════════════════════════════════════════════════
# Firewall
# ═══════════════════════════════════════════════════════════════════════════
if ($DoFirewall) {
Write-Host ''
Show-LxsBoxMid 'FIREWALL'
Write-Host ''
try {
Set-NetFirewallProfile -Profile Domain, Private, Public -Enabled True `
-DefaultInboundAction Block -DefaultOutboundAction Allow -ErrorAction Stop
Write-LxsOk 'Firewall enabled on all profiles, inbound blocked by default'
$changes++
} catch {
Write-LxsErr "Firewall configuration failed: $($_.Exception.Message)"
$problems++
}
try {
Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object {
Write-Host " $($_.Name.PadRight(8)) enabled=$($_.Enabled) inbound=$($_.DefaultInboundAction) outbound=$($_.DefaultOutboundAction)"
}
} catch {
# Reporting only.
}
}
# ═══════════════════════════════════════════════════════════════════════════
# SMBv1
# ═══════════════════════════════════════════════════════════════════════════
if ($DoSmb) {
Write-Host ''
Show-LxsBoxMid 'SMBv1'
Write-Host ''
try {
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force -ErrorAction Stop
Write-LxsOk 'SMBv1 server protocol disabled'
$changes++
} catch {
Write-LxsWarn "Could not disable the SMBv1 server: $($_.Exception.Message)"
$problems++
}
try {
$feature = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop
if ($feature.State -eq 'Enabled') {
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction Stop | Out-Null
Write-LxsOk 'SMBv1 client feature disabled (reboot to complete)'
$changes++
} else {
Write-LxsOk 'SMBv1 client feature was already disabled'
}
} catch {
Write-LxsWarn "Could not query or disable the SMB1Protocol feature: $($_.Exception.Message)"
}
}
# ═══════════════════════════════════════════════════════════════════════════
# UAC
# ═══════════════════════════════════════════════════════════════════════════
if ($DoUac) {
Write-Host ''
Show-LxsBoxMid 'UAC'
Write-Host ''
$uacPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
$uacOk = $true
# EnableLUA=1 keeps UAC on, ConsentPromptBehaviorAdmin=2 always prompts on
# the secure desktop, PromptOnSecureDesktop=1 makes that desktop mandatory.
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'EnableLUA' -Value 1) -and $uacOk
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'ConsentPromptBehaviorAdmin' -Value 2) -and $uacOk
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'PromptOnSecureDesktop' -Value 1) -and $uacOk
if ($uacOk) {
Write-LxsOk 'UAC set to prompt for consent on the secure desktop'
$changes++
} else {
$problems++
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Defender
# ═══════════════════════════════════════════════════════════════════════════
if ($DoDefender) {
Write-Host ''
Show-LxsBoxMid 'DEFENDER'
Write-Host ''
if (-not (Get-Command Set-MpPreference -ErrorAction SilentlyContinue)) {
Write-LxsWarn 'Microsoft Defender cmdlets are not available (third-party AV installed?). Skipped.'
} else {
$settings = @(
@{ Name = 'Real-time protection'; Args = @{ DisableRealtimeMonitoring = $false } },
@{ Name = 'PUA blocking'; Args = @{ PUAProtection = 1 } },
@{ Name = 'Cloud protection'; Args = @{ MAPSReporting = 2 } },
@{ Name = 'Sample submission'; Args = @{ SubmitSamplesConsent = 1 } },
@{ Name = 'Network protection'; Args = @{ EnableNetworkProtection = 1 } },
@{ Name = 'Script scanning'; Args = @{ DisableScriptScanning = $false } },
@{ Name = 'Archive scanning'; Args = @{ DisableArchiveScanning = $false } }
)
foreach ($s in $settings) {
try {
$mpArgs = $s.Args
Set-MpPreference @mpArgs -ErrorAction Stop
Write-LxsOk "$($s.Name) enabled"
$changes++
} catch {
# Tamper Protection blocks these writes by design; that is a
# stronger guarantee than what we were trying to set.
Write-LxsWarn "$($s.Name): $($_.Exception.Message)"
}
}
try {
$status = Get-MpComputerStatus -ErrorAction Stop
Write-Host ''
Write-Host " Antivirus enabled : $($status.AntivirusEnabled)"
Write-Host " Real-time protection : $($status.RealTimeProtectionEnabled)"
Write-Host " Tamper protection : $($status.IsTamperProtected)"
Write-Host " Signature age (days) : $($status.AntivirusSignatureAge)"
} catch {
# Reporting only.
}
}
}
# ═══════════════════════════════════════════════════════════════════════════
# LLMNR + NetBIOS
# ═══════════════════════════════════════════════════════════════════════════
if ($DoNameResolution) {
Write-Host ''
Show-LxsBoxMid 'NAME RESOLUTION'
Write-Host ''
if (Set-LxsRegistryValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' `
-Name 'EnableMulticast' -Value 0) {
Write-LxsOk 'LLMNR disabled'
$changes++
} else {
$problems++
}
# NetbiosOptions: 0 = DHCP default, 1 = enabled, 2 = disabled.
$nbPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces'
try {
$interfaces = @(Get-ChildItem $nbPath -ErrorAction Stop)
foreach ($iface in $interfaces) {
Set-ItemProperty -Path $iface.PSPath -Name 'NetbiosOptions' -Value 2 -ErrorAction SilentlyContinue
}
Write-LxsOk "NetBIOS-over-TCP/IP disabled on $($interfaces.Count) interface(s)"
$changes++
} catch {
Write-LxsWarn "Could not disable NetBIOS-over-TCP/IP: $($_.Exception.Message)"
}
}
# ═══════════════════════════════════════════════════════════════════════════
# Account audit — reports only, changes nothing.
# ═══════════════════════════════════════════════════════════════════════════
Write-Host ''
Show-LxsBoxMid 'ACCOUNT AUDIT'
Write-Host ''
try {
$admins = @(Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop)
Write-Host "$($script:Cyan)Local administrators:$($script:NC)"
foreach ($a in $admins) { Write-Host " - $($a.Name) $($script:Gray)($($a.ObjectClass), $($a.PrincipalSource))$($script:NC)" }
if ($admins.Count -gt 2) {
Write-Host ''
Write-LxsWarn "$($admins.Count) accounts have administrator rights — review whether they all need it."
}
} catch {
Write-LxsWarn "Could not enumerate the Administrators group: $($_.Exception.Message)"
}
Write-Host ''
try {
$locals = @(Get-LocalUser -ErrorAction Stop)
$enabledNoExpiry = @($locals | Where-Object { $_.Enabled -and $_.PasswordNeverExpires })
$noPassword = @($locals | Where-Object { $_.Enabled -and -not $_.PasswordLastSet -and $_.Name -ne 'DefaultAccount' })
$guest = $locals | Where-Object { $_.Name -eq 'Guest' }
if ($noPassword.Count -gt 0) {
Write-LxsWarn "Enabled accounts that have never set a password: $($noPassword.Name -join ', ')"
} else {
Write-LxsOk 'No enabled account is missing a password'
}
if ($enabledNoExpiry.Count -gt 0) {
Write-Host "$($script:Gray) Password never expires: $($enabledNoExpiry.Name -join ', ')$($script:NC)"
}
if ($guest -and $guest.Enabled) {
Write-LxsWarn 'The Guest account is ENABLED — disable it unless you rely on it.'
} else {
Write-LxsOk 'Guest account is disabled'
}
} catch {
Write-LxsWarn "Could not enumerate local users: $($_.Exception.Message)"
}
# ═══════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════
Write-Host ''
Show-LxsSeparator
Write-Host ''
Write-LxsOk "$changes hardening change(s) applied"
if ($problems -gt 0) {
Write-LxsWarn "$problems change(s) failed — see the messages above."
}
if ($DoSmb) {
Write-LxsWarn 'Reboot to finish removing the SMBv1 client feature.'
}
Write-Host ''
exit 0
+75
View File
@@ -0,0 +1,75 @@
<#
LXS - Tools index (Windows)
Description: Interactive menu listing the scripts in windows\tools
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
function Show-LxsToolsMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'TOOLS' -Right 'WIN_TOOLBOX'
Write-Host ''
Show-LxsMenuItem '01' 'System Infos' 'hardware, os, processes'
Show-LxsMenuItem '02' 'Network Diag' 'ip, dns, ports, repair'
Show-LxsMenuItem '03' 'Disk Cleanup' 'temp, update cache, winsxs'
Show-LxsMenuItem '04' 'System Repair' 'sfc, dism, chkdsk'
Show-LxsMenuItem '05' 'Restore Point' 'create, list, roll back'
Show-LxsMenuItem '06' 'Update Windows' 'windows update + winget'
Show-LxsMenuItem '07' 'Harden Windows' 'firewall, smb, defender'
Show-LxsMenuItem '08' 'Remote Access' 'rdp + openssh server'
Show-LxsMenuItem '09' 'Debloat' 'remove apps, telemetry off'
Show-LxsMenuItem '00' 'BACK' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
$script = switch -Regex ($choice) {
'^0?1$' { 'tools\system-info.ps1' }
'^0?2$' { 'tools\net-diag.ps1' }
'^0?3$' { 'tools\cleanup.ps1' }
'^0?4$' { 'tools\repair.ps1' }
'^0?5$' { 'tools\restore-point.ps1' }
'^0?6$' { 'tools\update-windows.ps1' }
'^0?7$' { 'tools\harden.ps1' }
'^0?8$' { 'tools\remote-access.ps1' }
'^0?9$' { 'tools\debloat.ps1' }
'^0?0$' { 'BACK' }
default { $null }
}
if ($script -eq 'BACK') { return }
if (-not $script) {
Write-LxsErr 'Invalid protocol. Select 0-9.'
Start-Sleep -Seconds 1
continue
}
$code = Invoke-LxsSibling -RelativePath $script -SelfDirectory $PSScriptRoot
# Exit code 75 from a child means it already paused on its own
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
if ($code -ne 75) { Read-LxsEnter }
}
}
Show-LxsToolsMenu
exit 75
+197
View File
@@ -0,0 +1,197 @@
<#
LXS - Network Diagnostics (Windows)
Description: Inspect and repair the network stack.
Everything is read-only except option 8 (stack reset).
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_net_diag.log'
function Show-LxsIpConfiguration {
Clear-Host
Show-LxsBoxTop -Title 'IP CONFIGURATION'
Write-Host ''
try {
Get-NetIPConfiguration -Detailed -ErrorAction Stop | ForEach-Object {
Write-Host "$($script:Cyan)$($script:Bold)$($_.InterfaceAlias)$($script:NC) $($script:Gray)($($_.InterfaceDescription))$($script:NC)"
Write-Host " Status : $($_.NetAdapter.Status) | MAC: $($_.NetAdapter.MacAddress) | Speed: $($_.NetAdapter.LinkSpeed)"
Write-Host " IPv4 : $($_.IPv4Address.IPAddress -join ', ')"
Write-Host " IPv6 : $($_.IPv6Address.IPAddress -join ', ')"
Write-Host " Gateway : $($_.IPv4DefaultGateway.NextHop -join ', ')"
Write-Host " DNS : $($_.DNSServer.ServerAddresses -join ', ')"
Write-Host ''
}
} catch {
Write-LxsWarn 'Get-NetIPConfiguration unavailable; falling back to ipconfig /all.'
ipconfig /all | Out-String | Write-Host
}
}
function Show-LxsListeningPorts {
Clear-Host
Show-LxsBoxTop -Title 'LISTENING PORTS'
Write-Host ''
try {
$rows = Get-NetTCPConnection -State Listen -ErrorAction Stop | ForEach-Object {
$procName = try { (Get-Process -Id $_.OwningProcess -ErrorAction Stop).ProcessName } catch { 'unknown' }
[pscustomobject]@{
Local = "$($_.LocalAddress):$($_.LocalPort)"
Port = $_.LocalPort
PID = $_.OwningProcess
Process = $procName
}
}
$rows | Sort-Object Port | Format-Table -AutoSize Local, PID, Process | Out-String | Write-Host
} catch {
Write-LxsWarn 'Get-NetTCPConnection unavailable; falling back to netstat.'
netstat -ano | Select-String 'LISTENING' | Out-String | Write-Host
}
}
function Clear-LxsDnsCache {
Clear-Host
Show-LxsBoxTop -Title 'FLUSH DNS CACHE'
Write-Host ''
try {
Clear-DnsClientCache -ErrorAction Stop
Write-LxsOk 'DNS resolver cache flushed'
} catch {
Write-LxsWarn 'Clear-DnsClientCache failed; falling back to ipconfig /flushdns.'
ipconfig /flushdns | Out-String | Write-Host
}
}
function Invoke-LxsPing {
Clear-Host
Show-LxsBoxTop -Title 'PING'
Write-Host ''
$target = Read-Host -Prompt 'Host to ping (default: 1.1.1.1)'
if ([string]::IsNullOrWhiteSpace($target)) { $target = '1.1.1.1' }
Write-Host ''
ping.exe -n 4 $target | Out-String | Write-Host
}
function Invoke-LxsTraceroute {
Clear-Host
Show-LxsBoxTop -Title 'TRACEROUTE'
Write-Host ''
$target = Read-Host -Prompt 'Host to trace (default: 1.1.1.1)'
if ([string]::IsNullOrWhiteSpace($target)) { $target = '1.1.1.1' }
Write-Host ''
Write-LxsInfo "Tracing route to $target (this can take a minute)..."
tracert.exe -d -h 20 $target | Out-String | Write-Host
}
function Test-LxsRemotePort {
Clear-Host
Show-LxsBoxTop -Title 'TEST TCP PORT'
Write-Host ''
$target = Read-Host -Prompt 'Host'
if ([string]::IsNullOrWhiteSpace($target)) { Write-LxsErr 'No host given.'; return }
$port = Read-Host -Prompt 'Port'
if ($port -notmatch '^\d+$') { Write-LxsErr 'Port must be a number.'; return }
Write-Host ''
try {
$result = Test-NetConnection -ComputerName $target -Port ([int]$port) -WarningAction SilentlyContinue -ErrorAction Stop
if ($result.TcpTestSucceeded) {
Write-LxsOk "${target}:${port} is reachable ($($result.RemoteAddress))"
} else {
Write-LxsErr "${target}:${port} is NOT reachable"
}
} catch {
Write-LxsErr "Test failed: $($_.Exception.Message)"
}
}
function Show-LxsPublicIp {
Clear-Host
Show-LxsBoxTop -Title 'PUBLIC IP'
Write-Host ''
Write-LxsInfo 'Querying...'
Write-Host ''
Write-Host " $($script:White)$($script:Bold)$(Get-LxsPublicIP)$($script:NC)"
Write-Host ''
}
function Reset-LxsNetworkStack {
Clear-Host
Show-LxsBoxTop -Title 'RESET NETWORK STACK' -Right 'DESTRUCTIVE'
Write-Host ''
Write-Host 'This resets the Winsock catalog and the TCP/IP stack. It clears'
Write-Host 'third-party LSP entries, static IP settings and proxy configuration,'
Write-Host "and $($script:Bold)requires a reboot$($script:NC) to take effect."
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Reset the network stack now?')) {
Write-LxsInfo 'Cancelled.'
return
}
if (-not (Test-LxsAdmin)) {
Write-LxsErr 'This action requires administrator rights. Re-run lxs from an elevated terminal.'
return
}
Write-Host ''
netsh winsock reset | Out-String | Write-Host
netsh int ip reset | Out-String | Write-Host
Clear-DnsClientCache -ErrorAction SilentlyContinue
Write-Host ''
Write-LxsOk 'Network stack reset'
Write-LxsWarn 'Reboot required for the change to take effect.'
}
function Show-LxsNetDiagMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'NETWORK DIAG'
Write-Host ''
Show-LxsMenuItem '1' 'Show IP configuration'
Show-LxsMenuItem '2' 'Show listening ports'
Show-LxsMenuItem '3' 'Flush DNS cache'
Show-LxsMenuItem '4' 'Ping a host'
Show-LxsMenuItem '5' 'Traceroute a host'
Show-LxsMenuItem '6' 'Test a TCP port'
Show-LxsMenuItem '7' 'Show public IP'
Show-LxsMenuItem '8' 'Reset network stack' 'needs admin + reboot'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsIpConfiguration; Read-LxsEnter }
'2' { Show-LxsListeningPorts; Read-LxsEnter }
'3' { Clear-LxsDnsCache; Read-LxsEnter }
'4' { Invoke-LxsPing; Read-LxsEnter }
'5' { Invoke-LxsTraceroute; Read-LxsEnter }
'6' { Test-LxsRemotePort; Read-LxsEnter }
'7' { Show-LxsPublicIp; Read-LxsEnter }
'8' { Reset-LxsNetworkStack; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
}
}
}
if (-not (Assert-LxsWindows)) { exit 1 }
Show-LxsNetDiagMenu
exit 75
+300
View File
@@ -0,0 +1,300 @@
<#
LXS - Remote Access (Windows)
Description: Remote Desktop and the OpenSSH server — status, enable,
disable. The Windows counterpart of root-ssh-login.sh.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_remote_access.log'
if (-not (Assert-LxsWindows)) { exit 1 }
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
$LxsRdpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
$LxsRdpTcpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
function Get-LxsRdpState {
try {
# fDenyTSConnections 0 = RDP allowed, 1 = denied.
$deny = (Get-ItemProperty -Path $LxsRdpKey -Name 'fDenyTSConnections' -ErrorAction Stop).fDenyTSConnections
return ($deny -eq 0)
} catch {
return $false
}
}
function Get-LxsNlaState {
try {
return ((Get-ItemProperty -Path $LxsRdpTcpKey -Name 'UserAuthentication' -ErrorAction Stop).UserAuthentication -eq 1)
} catch {
return $false
}
}
function Show-LxsRemoteStatus {
Clear-Host
Show-LxsBoxTop -Title 'REMOTE ACCESS STATUS'
Write-Host ''
Write-Host "$($script:Cyan)$($script:Bold)Remote Desktop$($script:NC)"
if (Get-LxsRdpState) {
Write-LxsOk 'RDP is ENABLED'
if (Get-LxsNlaState) {
Write-LxsOk 'Network Level Authentication is on'
} else {
Write-LxsWarn 'Network Level Authentication is OFF — turn it on (option 2).'
}
try {
$port = (Get-ItemProperty -Path $LxsRdpTcpKey -Name 'PortNumber' -ErrorAction Stop).PortNumber
Write-Host " Listening port: $port"
} catch {
Write-Host ' Listening port: 3389 (default)'
}
} else {
Write-Host " $($script:Gray)RDP is disabled$($script:NC)"
}
try {
$rules = @(Get-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop | Where-Object { $_.Enabled -eq 'True' })
Write-Host " Firewall rules enabled: $($rules.Count)"
} catch {
# Group may not exist on Server Core images.
}
Write-Host ''
Write-Host "$($script:Cyan)$($script:Bold)OpenSSH Server$($script:NC)"
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
if (-not $svc) {
Write-Host " $($script:Gray)Not installed$($script:NC)"
} else {
if ($svc.Status -eq 'Running') { Write-LxsOk "sshd is running (startup: $($svc.StartType))" }
else { Write-LxsWarn "sshd is installed but $($svc.Status) (startup: $($svc.StartType))" }
try {
$shell = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -ErrorAction Stop).DefaultShell
Write-Host " Default shell : $shell"
} catch {
Write-Host " Default shell : cmd.exe (Windows default)"
}
}
Write-Host ''
}
function Enable-LxsRdp {
Clear-Host
Show-LxsBoxTop -Title 'ENABLE REMOTE DESKTOP'
Write-Host ''
Write-Host 'This allows inbound RDP connections and opens the matching'
Write-Host 'firewall rules. Network Level Authentication will be required,'
Write-Host 'so clients must authenticate before a session is created.'
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Enable Remote Desktop?')) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
if (Set-LxsRegistryValue -Path $LxsRdpKey -Name 'fDenyTSConnections' -Value 0) {
Write-LxsOk 'RDP connections allowed'
}
if (Set-LxsRegistryValue -Path $LxsRdpTcpKey -Name 'UserAuthentication' -Value 1) {
Write-LxsOk 'Network Level Authentication required'
}
try {
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop
Write-LxsOk 'Firewall rules for Remote Desktop enabled'
} catch {
Write-LxsWarn "Could not enable the firewall rules: $($_.Exception.Message)"
}
Write-Host ''
Write-LxsWarn 'Only expose RDP to the internet behind a VPN — it is a constant brute-force target.'
}
function Disable-LxsRdp {
Clear-Host
Show-LxsBoxTop -Title 'DISABLE REMOTE DESKTOP'
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Disable Remote Desktop and its firewall rules?')) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
if (Set-LxsRegistryValue -Path $LxsRdpKey -Name 'fDenyTSConnections' -Value 1) {
Write-LxsOk 'RDP connections denied'
}
try {
Disable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop
Write-LxsOk 'Firewall rules for Remote Desktop disabled'
} catch {
Write-LxsWarn "Could not disable the firewall rules: $($_.Exception.Message)"
}
}
function Install-LxsSshServer {
Clear-Host
Show-LxsBoxTop -Title 'OPENSSH SERVER'
Write-Host ''
Write-Host 'Installs the OpenSSH Server capability, starts sshd, sets it to'
Write-Host 'start automatically and opens TCP/22 in the firewall.'
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Install and enable the OpenSSH server?')) {
Write-LxsInfo 'Cancelled.'
return
}
Write-Host ''
try {
$cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' -ErrorAction Stop |
Select-Object -First 1
} catch {
Write-LxsErr "Could not query Windows capabilities: $($_.Exception.Message)"
return
}
if (-not $cap) {
Write-LxsErr 'The OpenSSH Server capability is not available on this edition.'
return
}
if ($cap.State -ne 'Installed') {
Write-LxsInfo "Installing $($cap.Name) (this can take a few minutes)..."
try {
Add-WindowsCapability -Online -Name $cap.Name -ErrorAction Stop | Out-Null
Write-LxsOk 'OpenSSH Server installed'
} catch {
Write-LxsErr "Installation failed: $($_.Exception.Message)"
return
}
} else {
Write-LxsOk 'OpenSSH Server was already installed'
}
try {
Set-Service -Name sshd -StartupType Automatic -ErrorAction Stop
Start-Service -Name sshd -ErrorAction Stop
Write-LxsOk 'sshd is running and set to start automatically'
} catch {
Write-LxsErr "Could not start sshd: $($_.Exception.Message)"
return
}
# The capability normally creates this rule; create it when it is missing.
try {
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -ErrorAction Stop | Out-Null
Write-LxsOk 'Firewall rule created for TCP/22'
} else {
Write-LxsOk 'Firewall rule for TCP/22 already present'
}
} catch {
Write-LxsWarn "Could not create the firewall rule: $($_.Exception.Message)"
}
Write-Host ''
Write-Host "$($script:Gray)Connect with: ssh $env:USERNAME@$(Get-LxsPublicIP)$($script:NC)"
Write-Host "$($script:Gray)Key-based auth: put your public key in %ProgramData%\ssh\administrators_authorized_keys$($script:NC)"
Write-Host "$($script:Gray)for admin accounts, or in %USERPROFILE%\.ssh\authorized_keys otherwise.$($script:NC)"
}
function Disable-LxsSshServer {
Clear-Host
Show-LxsBoxTop -Title 'DISABLE OPENSSH SERVER'
Write-Host ''
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
if (-not $svc) {
Write-LxsWarn 'The OpenSSH server is not installed.'
return
}
if (-not (Confirm-LxsAction -Question 'Stop sshd and set it to Disabled?')) {
Write-LxsInfo 'Cancelled.'
return
}
try {
Stop-Service -Name sshd -Force -ErrorAction Stop
Set-Service -Name sshd -StartupType Disabled -ErrorAction Stop
Write-LxsOk 'sshd stopped and disabled'
} catch {
Write-LxsErr "Failed: $($_.Exception.Message)"
}
}
function Set-LxsSshDefaultShell {
Clear-Host
Show-LxsBoxTop -Title 'SSH DEFAULT SHELL'
Write-Host ''
Write-Host 'Which shell should SSH sessions land in?'
Write-Host ''
Show-LxsMenuItem '1' 'PowerShell' 'powershell.exe'
Show-LxsMenuItem '2' 'PowerShell 7' 'pwsh.exe (must be installed)'
Show-LxsMenuItem '3' 'Command Prompt' 'cmd.exe (Windows default)'
Show-LxsMenuItem '0' 'Cancel' '' -Exit
Write-Host ''
$choice = Read-LxsChoice
$shell = switch ($choice) {
'1' { "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" }
'2' { (Get-Command pwsh -ErrorAction SilentlyContinue).Source }
'3' { "$env:SystemRoot\System32\cmd.exe" }
default { $null }
}
if (-not $shell) {
if ($choice -eq '2') { Write-LxsErr 'pwsh.exe was not found — install PowerShell 7 first.' }
else { Write-LxsInfo 'Cancelled.' }
return
}
Write-Host ''
if (Set-LxsRegistryValue -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -Value $shell -Type String) {
Write-LxsOk "SSH sessions will start: $shell"
}
}
function Show-LxsRemoteMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'REMOTE ACCESS' -Right 'ADMIN'
Write-Host ''
Show-LxsMenuItem '1' 'Show status'
Show-LxsMenuItem '2' 'Enable Remote Desktop' 'with NLA + firewall'
Show-LxsMenuItem '3' 'Disable Remote Desktop'
Show-LxsMenuItem '4' 'Install OpenSSH Server' 'service + firewall'
Show-LxsMenuItem '5' 'Disable OpenSSH Server'
Show-LxsMenuItem '6' 'Set SSH default shell'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsRemoteStatus; Read-LxsEnter }
'2' { Enable-LxsRdp; Read-LxsEnter }
'3' { Disable-LxsRdp; Read-LxsEnter }
'4' { Install-LxsSshServer; Read-LxsEnter }
'5' { Disable-LxsSshServer; Read-LxsEnter }
'6' { Set-LxsSshDefaultShell; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-6.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsRemoteMenu
exit 75
+150
View File
@@ -0,0 +1,150 @@
<#
LXS - System Repair (Windows)
Description: Integrity repair for the component store and system files
(SFC, DISM, chkdsk). Requires administrator rights.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_repair.log'
if (-not (Assert-LxsWindows)) { exit 1 }
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
# These commands stream long-running progress; running them through the
# spinner would hide it, so they write straight to the console.
function Invoke-LxsRepairCommand {
param(
[Parameter(Mandatory = $true)][string]$Title,
[Parameter(Mandatory = $true)][string]$Exe,
[Parameter(Mandatory = $true)][string[]]$CmdArgs,
[string]$Note = ''
)
Clear-Host
Show-LxsBoxTop -Title $Title
Write-Host ''
if ($Note) {
Write-Host "$($script:Gray)$Note$($script:NC)"
Write-Host ''
}
Write-LxsInfo "$Exe $($CmdArgs -join ' ')"
Show-LxsSeparator
Write-Host ''
& $Exe @CmdArgs
$code = $LASTEXITCODE
Write-Host ''
Show-LxsSeparator
if ($code -eq 0) {
Write-LxsOk "$Title completed"
} else {
Write-LxsWarn "$Title exited with code $code"
}
return $code
}
function Invoke-LxsSfc {
Invoke-LxsRepairCommand -Title 'SFC SCANNOW' -Exe 'sfc.exe' -CmdArgs @('/scannow') `
-Note 'Verifies every protected system file and repairs corrupted ones from the component store. Takes 5-15 minutes.' | Out-Null
}
function Invoke-LxsDism {
Clear-Host
Show-LxsBoxTop -Title 'DISM RESTOREHEALTH'
Write-Host ''
Write-Host "$($script:Gray)Repairs the component store itself. Run this before SFC when SFC$($script:NC)"
Write-Host "$($script:Gray)reports that it cannot fix files. Needs internet access for the$($script:NC)"
Write-Host "$($script:Gray)replacement payloads. Takes 10-30 minutes.$($script:NC)"
Write-Host ''
Show-LxsSeparator
Write-Host ''
Write-LxsInfo 'Step 1/3 - CheckHealth'
& dism.exe /Online /Cleanup-Image /CheckHealth
Write-Host ''
Write-LxsInfo 'Step 2/3 - ScanHealth (slow)'
& dism.exe /Online /Cleanup-Image /ScanHealth
Write-Host ''
Write-LxsInfo 'Step 3/3 - RestoreHealth'
& dism.exe /Online /Cleanup-Image /RestoreHealth
$code = $LASTEXITCODE
Write-Host ''
Show-LxsSeparator
if ($code -eq 0) { Write-LxsOk 'DISM completed' } else { Write-LxsWarn "DISM exited with code $code" }
}
function Invoke-LxsChkdsk {
Invoke-LxsRepairCommand -Title 'CHKDSK SCAN' -Exe 'chkdsk.exe' -CmdArgs @($env:SystemDrive, '/scan') `
-Note 'Online, read-only scan of the system volume. It reports problems without taking the volume offline; a full /f repair needs a reboot.' | Out-Null
}
function Invoke-LxsFullRepair {
Clear-Host
Show-LxsBoxTop -Title 'FULL REPAIR PASS'
Write-Host ''
Write-Host 'Runs, in the recommended order:'
Write-Host ' 1. DISM /RestoreHealth (repair the component store)'
Write-Host ' 2. SFC /scannow (repair system files from that store)'
Write-Host ' 3. chkdsk /scan (check the file system)'
Write-Host ''
Write-Host "$($script:Gray)Budget 30-60 minutes. Nothing here is destructive.$($script:NC)"
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Start the full repair pass?' -DefaultYes)) {
Write-LxsInfo 'Cancelled.'
return
}
Invoke-LxsDism
Read-LxsEnter
Invoke-LxsSfc
Read-LxsEnter
Invoke-LxsChkdsk
}
function Show-LxsRepairMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'SYSTEM REPAIR' -Right 'ADMIN'
Write-Host ''
Show-LxsMenuItem '1' 'SFC /scannow' 'repair system files'
Show-LxsMenuItem '2' 'DISM /RestoreHealth' 'repair component store'
Show-LxsMenuItem '3' 'chkdsk /scan' 'check the file system'
Show-LxsMenuItem '4' 'Full repair pass' 'DISM, then SFC, then chkdsk'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Invoke-LxsSfc; Read-LxsEnter }
'2' { Invoke-LxsDism; Read-LxsEnter }
'3' { Invoke-LxsChkdsk; Read-LxsEnter }
'4' { Invoke-LxsFullRepair; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-4.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsRepairMenu
exit 75
+177
View File
@@ -0,0 +1,177 @@
<#
LXS - System Restore Point (Windows)
Description: Enable System Protection, create checkpoints, list them and
roll back. The safety net for harden.ps1 and debloat.ps1.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_restore_point.log'
if (-not (Assert-LxsWindows)) { exit 1 }
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
function Get-LxsRestorePoints {
try {
return @(Get-ComputerRestorePoint -ErrorAction Stop)
} catch {
return @()
}
}
function Show-LxsRestorePoints {
Clear-Host
Show-LxsBoxTop -Title 'RESTORE POINTS'
Write-Host ''
$points = Get-LxsRestorePoints
if ($points.Count -eq 0) {
Write-LxsWarn 'No restore points found (System Protection may be disabled).'
return
}
$points | Sort-Object SequenceNumber -Descending |
Format-Table -AutoSize `
@{ Name = 'Seq'; Expression = { $_.SequenceNumber } },
@{ Name = 'Created'; Expression = { $_.ConvertToDateTime($_.CreationTime).ToString('yyyy-MM-dd HH:mm') } },
@{ Name = 'Type'; Expression = { $_.RestorePointType } },
@{ Name = 'Description'; Expression = { $_.Description } } |
Out-String -Width 160 | Write-Host
}
function Show-LxsProtectionStatus {
Clear-Host
Show-LxsBoxTop -Title 'SYSTEM PROTECTION'
Write-Host ''
$drive = "$env:SystemDrive\"
try {
# Volumes listed here have shadow storage configured, i.e. protection on.
$shadow = @(Get-CimInstance Win32_ShadowStorage -ErrorAction Stop)
if ($shadow.Count -gt 0) {
Write-LxsOk "System Protection appears to be ON for $($shadow.Count) volume(s)"
foreach ($s in $shadow) {
$maxGB = [math]::Round($s.MaxSpace / 1GB, 1)
$usedGB = [math]::Round($s.UsedSpace / 1GB, 2)
Write-Host " Shadow storage: $usedGB GB used of $maxGB GB allocated"
}
} else {
Write-LxsWarn "System Protection looks OFF for $drive — no restore points can be created."
}
} catch {
Write-LxsWarn 'Could not read the shadow storage configuration.'
}
Write-Host ''
Write-Host "$($script:Gray)Restore points found: $((Get-LxsRestorePoints).Count)$($script:NC)"
Write-Host ''
}
function Enable-LxsProtection {
Clear-Host
Show-LxsBoxTop -Title 'ENABLE SYSTEM PROTECTION'
Write-Host ''
$drive = "$env:SystemDrive\"
try {
Enable-ComputerRestore -Drive $drive -ErrorAction Stop
Write-LxsOk "System Protection enabled on $drive"
} catch {
Write-LxsErr "Failed to enable System Protection: $($_.Exception.Message)"
return
}
# Default shadow storage can be as low as 1%; 5% leaves room for a few points.
try {
& vssadmin.exe Resize ShadowStorage /For=$drive /On=$drive /MaxSize=5% | Out-String | Write-Host
} catch {
Write-LxsWarn 'Could not resize the shadow storage allocation.'
}
}
function New-LxsCheckpoint {
Clear-Host
Show-LxsBoxTop -Title 'CREATE RESTORE POINT'
Write-Host ''
$desc = Read-Host -Prompt 'Description (default: LXS manual checkpoint)'
if ([string]::IsNullOrWhiteSpace($desc)) { $desc = 'LXS manual checkpoint' }
Write-Host ''
Write-LxsInfo 'Creating the restore point (this can take a minute)...'
New-LxsRestorePoint -Description $desc | Out-Null
}
function Restore-LxsCheckpoint {
Clear-Host
Show-LxsBoxTop -Title 'ROLL BACK' -Right 'REBOOTS'
Write-Host ''
$points = Get-LxsRestorePoints
if ($points.Count -eq 0) {
Write-LxsWarn 'No restore points to roll back to.'
return
}
Show-LxsRestorePoints
Write-Host "$($script:Red)Restoring reboots the machine immediately and reverts system files,$($script:NC)"
Write-Host "$($script:Red)drivers, registry and installed programs to that point in time.$($script:NC)"
Write-Host "$($script:Gray)Your documents are not touched.$($script:NC)"
Write-Host ''
$seq = Read-Host -Prompt 'Sequence number to restore (empty to cancel)'
if ([string]::IsNullOrWhiteSpace($seq)) { Write-LxsInfo 'Cancelled.'; return }
if ($seq -notmatch '^\d+$') { Write-LxsErr 'Sequence number must be numeric.'; return }
if (-not ($points | Where-Object { $_.SequenceNumber -eq [int]$seq })) {
Write-LxsErr "No restore point with sequence number $seq."
return
}
if (-not (Confirm-LxsAction -Question "Restore to point $seq and reboot NOW?")) {
Write-LxsInfo 'Cancelled.'
return
}
try {
Restore-Computer -RestorePoint ([int]$seq) -Confirm:$false -ErrorAction Stop
} catch {
Write-LxsErr "Restore failed: $($_.Exception.Message)"
Write-Host "$($script:Gray) You can also use: rstrui.exe$($script:NC)"
}
}
function Show-LxsRestoreMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'RESTORE POINT' -Right 'ADMIN'
Write-Host ''
Show-LxsMenuItem '1' 'Show protection status'
Show-LxsMenuItem '2' 'Enable System Protection'
Show-LxsMenuItem '3' 'Create a restore point'
Show-LxsMenuItem '4' 'List restore points'
Show-LxsMenuItem '5' 'Roll back to a restore point' 'reboots the machine'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsProtectionStatus; Read-LxsEnter }
'2' { Enable-LxsProtection; Read-LxsEnter }
'3' { New-LxsCheckpoint; Read-LxsEnter }
'4' { Show-LxsRestorePoints; Read-LxsEnter }
'5' { Restore-LxsCheckpoint; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-5.'; Start-Sleep -Seconds 1 }
}
}
}
Show-LxsRestoreMenu
exit 75
+277
View File
@@ -0,0 +1,277 @@
<#
LXS - System Infos (Windows)
Description: Essential system monitoring and diagnostic views.
Read-only: nothing here changes the machine.
Repo: https://git.hyko.cx/hykocx/lxs
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_system_infos.log'
function Write-LxsField {
param([string]$Label, $Value)
Write-Host "$($script:Cyan)$($script:Bold)${Label}:$($script:NC)"
foreach ($line in @($Value)) { Write-Host " $line" }
Write-Host ''
}
function Show-LxsSystemInformation {
Clear-Host
Show-LxsBoxTop -Title 'SYSTEM INFORMATION'
Write-Host ''
try {
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$cpu = @(Get-CimInstance Win32_Processor)
$bios = Get-CimInstance Win32_BIOS
Write-LxsField 'Operating System' "$($os.Caption) $($os.OSArchitecture)"
Write-LxsField 'Version / Build' "$($os.Version) (build $($os.BuildNumber))"
Write-LxsField 'Installed On' $os.InstallDate
$span = (Get-Date) - $os.LastBootUpTime
Write-LxsField 'System Uptime' ("{0} days, {1} hours, {2} minutes" -f $span.Days, $span.Hours, $span.Minutes)
Write-LxsField 'Hostname' $env:COMPUTERNAME
Write-LxsField 'Manufacturer' "$($cs.Manufacturer) $($cs.Model)"
Write-LxsField 'BIOS' "$($bios.Manufacturer) $($bios.SMBIOSBIOSVersion)"
Write-LxsField 'CPU' ($cpu | ForEach-Object { "$($_.Name.Trim()) ($($_.NumberOfCores)C/$($_.NumberOfLogicalProcessors)T)" })
Write-LxsField 'Memory' ("{0} GB total" -f [math]::Round($cs.TotalPhysicalMemory / 1GB, 1))
Write-LxsField 'PowerShell' $PSVersionTable.PSVersion.ToString()
} catch {
Write-LxsErr "Could not read system information: $($_.Exception.Message)"
}
try {
$gpu = @(Get-CimInstance Win32_VideoController -ErrorAction Stop |
ForEach-Object { "$($_.Name) (driver $($_.DriverVersion))" })
if ($gpu.Count -gt 0) { Write-LxsField 'GPU' $gpu }
} catch {
# Headless or restricted WMI — not worth failing the whole view.
}
# Activation status. LicenseStatus 1 = licensed.
try {
$lic = @(Get-CimInstance SoftwareLicensingProduct -ErrorAction Stop |
Where-Object { $_.PartialProductKey -and $_.Name -like 'Windows*' } | Select-Object -First 1)
if ($lic.Count -gt 0) {
$state = switch ([int]$lic[0].LicenseStatus) {
0 { 'Unlicensed' }
1 { 'Licensed' }
2 { 'Out-of-box grace period' }
3 { 'Out-of-tolerance grace period' }
4 { 'Non-genuine grace period' }
5 { 'Notification (not activated)' }
6 { 'Extended grace' }
default { 'Unknown' }
}
Write-LxsField 'Activation' "$state$($lic[0].Name)"
}
} catch {
# SoftwareLicensingProduct is unavailable on some SKUs.
}
}
function Show-LxsDiskSpace {
Clear-Host
Show-LxsBoxTop -Title 'DISK SPACE'
Write-Host ''
Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object {
$totalGB = [math]::Round($_.Size / 1GB, 1)
$freeGB = [math]::Round($_.FreeSpace / 1GB, 1)
$usedGB = [math]::Round(($_.Size - $_.FreeSpace) / 1GB, 1)
$pct = if ($_.Size -gt 0) { [math]::Round(100 * ($_.Size - $_.FreeSpace) / $_.Size) } else { 0 }
$bar = ('#' * [math]::Round($pct / 5)).PadRight(20, '.')
$color = if ($pct -ge 90) { $script:Red } else { $script:Cyan }
Write-Host (" $($script:White)$($script:Bold){0,-4}$($script:NC) {1}[{2}]$($script:NC) {3,6} GB used / {4,6} GB free / {5,6} GB total ({6}%)" -f `
$_.DeviceID, $color, $bar, $usedGB, $freeGB, $totalGB, $pct)
}
Write-Host ''
}
function Show-LxsMemoryUsage {
Clear-Host
Show-LxsBoxTop -Title 'MEMORY USAGE'
Write-Host ''
$os = Get-CimInstance Win32_OperatingSystem
$totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
$usedGB = [math]::Round($totalGB - $freeGB, 2)
$pct = if ($totalGB -gt 0) { [math]::Round(100 * $usedGB / $totalGB) } else { 0 }
Write-LxsField 'Physical Memory' "$usedGB GB used / $freeGB GB free / $totalGB GB total ($pct%)"
$pageTotal = [math]::Round($os.SizeStoredInPagingFiles / 1MB, 2)
$pageFree = [math]::Round($os.FreeSpaceInPagingFiles / 1MB, 2)
Write-LxsField 'Paging File' "$([math]::Round($pageTotal - $pageFree, 2)) GB used / $pageTotal GB total"
Write-Host "$($script:Cyan)$($script:Bold)Top 10 processes by memory:$($script:NC)"
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10 |
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
@{ Name = 'Name'; Expression = { $_.ProcessName } },
@{ Name = 'Memory (MB)'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } } |
Out-String | Write-Host
}
function Show-LxsCpuLoad {
Clear-Host
Show-LxsBoxTop -Title 'CPU LOAD'
Write-Host ''
try {
$load = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
Write-LxsField 'Current Load' "$load%"
} catch {
Write-LxsWarn 'Could not read the CPU load counter.'
}
Write-LxsField 'Logical Processors' $env:NUMBER_OF_PROCESSORS
Write-Host "$($script:Cyan)$($script:Bold)Top 10 processes by CPU time:$($script:NC)"
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 |
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
@{ Name = 'Name'; Expression = { $_.ProcessName } },
@{ Name = 'CPU (s)'; Expression = { if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } } } |
Out-String | Write-Host
}
function Show-LxsNetwork {
Clear-Host
Show-LxsBoxTop -Title 'NETWORK'
Write-Host ''
try {
Get-NetIPConfiguration -ErrorAction Stop | ForEach-Object {
Write-Host "$($script:Cyan)$($script:Bold)$($_.InterfaceAlias)$($script:NC) $($script:Gray)($($_.InterfaceDescription))$($script:NC)"
Write-Host " IPv4 : $($_.IPv4Address.IPAddress -join ', ')"
Write-Host " Gateway : $($_.IPv4DefaultGateway.NextHop -join ', ')"
Write-Host " DNS : $($_.DNSServer.ServerAddresses -join ', ')"
Write-Host ''
}
} catch {
Write-LxsWarn 'Get-NetIPConfiguration unavailable; falling back to ipconfig.'
ipconfig /all | Out-String | Write-Host
}
Write-LxsField 'Public IP' (Get-LxsPublicIP)
}
function Show-LxsEventLogs {
Clear-Host
Show-LxsBoxTop -Title 'SYSTEM LOGS' -Right 'LAST 50'
Write-Host ''
foreach ($logName in @('System', 'Application')) {
Write-Host "$($script:Cyan)$($script:Bold)$logName — errors and warnings$($script:NC)"
try {
$events = Get-WinEvent -FilterHashtable @{ LogName = $logName; Level = 1, 2, 3 } -MaxEvents 25 -ErrorAction Stop
$events | Format-Table -AutoSize -Wrap `
@{ Name = 'Time'; Expression = { $_.TimeCreated.ToString('MM-dd HH:mm') } },
@{ Name = 'Level'; Expression = { $_.LevelDisplayName } },
@{ Name = 'Source'; Expression = { $_.ProviderName } },
@{ Name = 'Message'; Expression = { ($_.Message -split "`n")[0] } } |
Out-String -Width 160 | Write-Host
} catch {
Write-LxsWarn "No matching entries in $logName (or access denied)."
}
Write-Host ''
}
}
function Show-LxsTopProcesses {
Clear-Host
Show-LxsBoxTop -Title 'TOP PROCESSES'
Write-Host ''
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 20 |
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
@{ Name = 'Name'; Expression = { $_.ProcessName } },
@{ Name = 'Memory (MB)'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } },
@{ Name = 'CPU (s)'; Expression = { if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } } },
@{ Name = 'Threads'; Expression = { $_.Threads.Count } } |
Out-String | Write-Host
}
function Show-LxsDiskHealth {
Clear-Host
Show-LxsBoxTop -Title 'DISK HEALTH' -Right 'SMART'
Write-Host ''
try {
$disks = @(Get-PhysicalDisk -ErrorAction Stop)
} catch {
Write-LxsErr 'Get-PhysicalDisk is unavailable on this system.'
return
}
foreach ($disk in $disks) {
$sizeGB = [math]::Round($disk.Size / 1GB, 1)
$healthColor = if ($disk.HealthStatus -eq 'Healthy') { $script:Cyan } else { $script:Red }
Write-Host "$($script:White)$($script:Bold)$($disk.FriendlyName)$($script:NC) $($script:Gray)// $($disk.MediaType), $sizeGB GB$($script:NC)"
Write-Host " Health : $healthColor$($disk.HealthStatus)$($script:NC) (operational: $($disk.OperationalStatus -join ', '))"
# Reliability counters need admin; report the gap instead of failing.
try {
$rc = $disk | Get-StorageReliabilityCounter -ErrorAction Stop
if ($null -ne $rc.Temperature) { Write-Host " Temp : $($rc.Temperature) C" }
if ($null -ne $rc.Wear) { Write-Host " Wear : $($rc.Wear)%" }
if ($null -ne $rc.PowerOnHours) { Write-Host " Power on : $($rc.PowerOnHours) h" }
if ($null -ne $rc.ReadErrorsTotal) { Write-Host " Rd errors: $($rc.ReadErrorsTotal)" }
if ($null -ne $rc.WriteErrorsTotal) { Write-Host " Wr errors: $($rc.WriteErrorsTotal)" }
} catch {
if (-not (Test-LxsAdmin)) {
Write-Host "$($script:Gray) (run as administrator for SMART counters)$($script:NC)"
} else {
Write-Host "$($script:Gray) (this device exposes no reliability counters)$($script:NC)"
}
}
Write-Host ''
}
}
function Show-LxsSystemInfoMenu {
while ($true) {
Clear-Host
Show-LxsBoxTop -Title 'SYSTEM INFOS'
Write-Host ''
Show-LxsMenuItem '1' 'View system information'
Show-LxsMenuItem '2' 'Check disk space'
Show-LxsMenuItem '3' 'Check memory usage'
Show-LxsMenuItem '4' 'Check CPU load'
Show-LxsMenuItem '5' 'Check network'
Show-LxsMenuItem '6' 'View system logs (errors and warnings)'
Show-LxsMenuItem '7' 'Show top resource-consuming processes'
Show-LxsMenuItem '8' 'Check disk health (SMART)'
Show-LxsMenuItem '0' 'Back' '' -Exit
Write-Host ''
Show-LxsBoxBottom
Write-Host ''
$choice = Read-LxsChoice
Write-Host ''
switch ($choice) {
'1' { Show-LxsSystemInformation; Read-LxsEnter }
'2' { Show-LxsDiskSpace; Read-LxsEnter }
'3' { Show-LxsMemoryUsage; Read-LxsEnter }
'4' { Show-LxsCpuLoad; Read-LxsEnter }
'5' { Show-LxsNetwork; Read-LxsEnter }
'6' { Show-LxsEventLogs; Read-LxsEnter }
'7' { Show-LxsTopProcesses; Read-LxsEnter }
'8' { Show-LxsDiskHealth; Read-LxsEnter }
'0' { return }
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
}
}
}
if (-not (Assert-LxsWindows)) { exit 1 }
Show-LxsSystemInfoMenu
exit 75
+220
View File
@@ -0,0 +1,220 @@
<#
LXS - Update Windows
Description: Install pending Windows updates and upgrade every winget
package. Mirror of linux/tools/update-server.sh.
Repo: https://git.hyko.cx/hykocx/lxs
Usage: update-windows.ps1 [-Yes] [-NoWinget] [-NoWindowsUpdate] [-Help]
#>
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
# only hits the network when this script is run standalone.
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
. $LxsLibPath
} else {
try {
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
} catch {
Write-Error 'Failed to fetch lib/common.ps1'
exit 1
}
. ([scriptblock]::Create($LxsLibSource))
}
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_update_windows.log'
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
'-Yes', '-y', '-NoWinget', '-NoWindowsUpdate', '-Help', '-h'
)
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
$AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
$NoWinget = Test-LxsFlag $LxsOpts @('-NoWinget')
$NoWindowsUpdate = Test-LxsFlag $LxsOpts @('-NoWindowsUpdate')
if (Test-LxsFlag $LxsOpts @('-Help', '-h')) {
Write-Host @'
Usage: update-windows.ps1 [options]
Options:
-Yes, -y Skip the confirmation prompt
-NoWinget Skip `winget upgrade --all`
-NoWindowsUpdate Skip Windows Update
-Help, -h Show this help
'@
exit 0
}
if (-not (Assert-LxsWindows)) { exit 1 }
if (-not $NoWindowsUpdate) { Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args }
# ═══════════════════════════════════════════════════════════════════════════
# Windows Update via the built-in COM API. Deliberately not PSWindowsUpdate:
# that module has to be pulled from the PowerShell Gallery first, which fails
# on locked-down or offline machines. The COM surface ships with Windows.
# ═══════════════════════════════════════════════════════════════════════════
function Invoke-LxsWindowsUpdate {
Write-Host ''
Show-LxsBoxMid 'WINDOWS UPDATE'
Write-Host ''
try {
$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
} catch {
Write-LxsErr "Windows Update API unavailable: $($_.Exception.Message)"
return $false
}
Write-LxsInfo 'Searching for updates (this can take several minutes)...'
try {
$result = $searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")
} catch {
Write-LxsErr "Search failed: $($_.Exception.Message)"
return $false
}
$updates = @($result.Updates)
if ($updates.Count -eq 0) {
Write-LxsOk 'No pending updates'
return $true
}
Write-LxsOk "$($updates.Count) update(s) available:"
Write-Host ''
foreach ($u in $updates) {
$sizeMB = [math]::Round($u.MaxDownloadSize / 1MB, 1)
Write-Host " $($script:Gray)-$($script:NC) $($u.Title) $($script:Gray)(${sizeMB} MB)$($script:NC)"
}
Write-Host ''
if (-not (Confirm-LxsAction -Question 'Download and install these updates?' -DefaultYes -AssumeYes:$AssumeYes)) {
Write-LxsInfo 'Skipped.'
return $true
}
# EULAs must be accepted per-update before an unattended install.
$toInstall = New-Object -ComObject Microsoft.Update.UpdateColl
foreach ($u in $updates) {
if (-not $u.EulaAccepted) {
try { $u.AcceptEula() } catch { Write-LxsWarn "Could not accept the EULA for: $($u.Title)" }
}
[void]$toInstall.Add($u)
}
Write-Host ''
Write-LxsInfo 'Downloading...'
try {
$downloader = $session.CreateUpdateDownloader()
$downloader.Updates = $toInstall
$dlResult = $downloader.Download()
if ($dlResult.ResultCode -ne 2) {
Write-LxsWarn "Download finished with result code $($dlResult.ResultCode) (2 = success)."
} else {
Write-LxsOk 'Download complete'
}
} catch {
Write-LxsErr "Download failed: $($_.Exception.Message)"
return $false
}
Write-Host ''
Write-LxsInfo 'Installing...'
try {
$installer = $session.CreateUpdateInstaller()
$installer.Updates = $toInstall
$instResult = $installer.Install()
} catch {
Write-LxsErr "Install failed: $($_.Exception.Message)"
return $false
}
for ($i = 0; $i -lt $toInstall.Count; $i++) {
$code = $instResult.GetUpdateResult($i).ResultCode
$title = $toInstall.Item($i).Title
if ($code -eq 2) { Write-LxsOk $title } else { Write-LxsWarn "$title (result code $code)" }
}
Write-Host ''
if ($instResult.RebootRequired) {
Write-LxsWarn 'A reboot is required to finish installing these updates.'
} else {
Write-LxsOk 'Windows Update finished'
}
return $true
}
# ═══════════════════════════════════════════════════════════════════════════
# winget
# ═══════════════════════════════════════════════════════════════════════════
function Invoke-LxsWingetUpgrade {
Write-Host ''
Show-LxsBoxMid 'WINGET'
Write-Host ''
if (-not (Assert-LxsWinget)) { return $false }
Write-LxsInfo 'Packages with an available upgrade:'
Write-Host ''
& winget upgrade --include-unknown --accept-source-agreements | Out-String | Write-Host
if (-not (Confirm-LxsAction -Question 'Upgrade all of them?' -DefaultYes -AssumeYes:$AssumeYes)) {
Write-LxsInfo 'Skipped.'
return $true
}
Write-Host ''
& winget upgrade --all --include-unknown --silent `
--accept-package-agreements --accept-source-agreements --disable-interactivity
$code = $LASTEXITCODE
Write-Host ''
if ($code -eq 0) {
Write-LxsOk 'winget upgrade completed'
return $true
}
# -1978335189 = no applicable upgrade found; nothing actually went wrong.
if ($code -eq -1978335189) {
Write-LxsOk 'Everything is already up to date'
return $true
}
Write-LxsWarn "winget exited with code $code"
return $false
}
# ═══════════════════════════════════════════════════════════════════════════
# Run
# ═══════════════════════════════════════════════════════════════════════════
Clear-Host
Show-LxsBoxTop -Title 'UPDATE WINDOWS'
Write-Host ''
Write-Host 'The following will run on this machine:'
if (-not $NoWindowsUpdate) { Write-Host ' - Windows Update: search, download and install pending updates' }
if (-not $NoWinget) { Write-Host ' - winget: upgrade every package with a newer version' }
Write-Host ''
Show-LxsSeparator
if (-not (Confirm-LxsAction -Question 'Continue?' -DefaultYes -AssumeYes:$AssumeYes)) {
Write-LxsInfo 'Cancelled.'
exit 0
}
$failed = $false
if (-not $NoWindowsUpdate) { if (-not (Invoke-LxsWindowsUpdate)) { $failed = $true } }
if (-not $NoWinget) { if (-not (Invoke-LxsWingetUpgrade)) { $failed = $true } }
Write-Host ''
Show-LxsSeparator
Write-Host ''
if ($failed) {
Write-LxsWarn 'Update pass finished with warnings.'
exit 1
}
Write-LxsOk 'Update pass finished'
exit 0