<# 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 Install an application bundle lxs tool [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 / 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