docs: Update README and documentation for lxs multi-tool across Linux and Windows platforms.
This commit is contained in:
@@ -1,30 +1,69 @@
|
||||
# LXS
|
||||
|
||||
Linux multi-tool — a Bash menu and CLI for installing common server apps and running system tools on Debian/Ubuntu.
|
||||
Multi-machine toolbox — one menu, two platforms. A Bash entrypoint for
|
||||
Debian/Ubuntu servers and a PowerShell entrypoint for Windows workstations,
|
||||
sharing the same interface, the same install flow and the same update
|
||||
mechanism.
|
||||
|
||||
## Quick install
|
||||
```
|
||||
lxs.sh → linux/ server apps, hardening, benchmarks, MOTD
|
||||
lxs.ps1 → windows/ diagnostics, cleanup, hardening, debloat, winget apps
|
||||
VERSION shared by both
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
### Linux (Debian / Ubuntu)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.sh | sudo bash -s -- setup
|
||||
```
|
||||
|
||||
This downloads the repo tarball, installs every file to `/usr/local/share/lxs/`, and creates the symlink `/usr/local/bin/lxs`. After install, sub-scripts run from disk — no network calls per command.
|
||||
Installs every file to `/usr/local/share/lxs/` and creates the symlink
|
||||
`/usr/local/bin/lxs`. After install, sub-scripts run from disk — no network
|
||||
calls per command.
|
||||
|
||||
> Avoid `sudo bash <(curl …)` — process substitution opens `/dev/fd/63` in your shell, which the root `bash` spawned by `sudo` cannot read (`bash: /dev/fd/63: No such file or directory`). Pipe through `sudo bash -s --` instead.
|
||||
> Avoid `sudo bash <(curl …)` — process substitution opens `/dev/fd/63` in your
|
||||
> shell, which the root `bash` spawned by `sudo` cannot read
|
||||
> (`bash: /dev/fd/63: No such file or directory`). Pipe through
|
||||
> `sudo bash -s --` instead.
|
||||
|
||||
Or run once without installing (no `sudo` needed for the entrypoint — sub-scripts auto-elevate when they need root):
|
||||
Or run once without installing (no `sudo` needed for the entrypoint —
|
||||
sub-scripts auto-elevate when they need root):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.sh | bash
|
||||
```
|
||||
|
||||
When a new version is published in this repo, the interactive menu shows a *"Nouvelle version disponible"* banner; run `lxs update` to pull all updated files.
|
||||
### Windows (10 1809+ / 11 / Server 2019+)
|
||||
|
||||
In PowerShell:
|
||||
|
||||
```powershell
|
||||
& ([scriptblock]::Create((irm https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.ps1))) setup
|
||||
```
|
||||
|
||||
Installs to `%LOCALAPPDATA%\lxs`, adds `%LOCALAPPDATA%\lxs\bin` to your **user**
|
||||
PATH and drops an `lxs.cmd` shim there — so the install itself never needs
|
||||
administrator rights. Open a new terminal afterwards and run `lxs`. Tools that
|
||||
change the machine request elevation on their own, in a separate window.
|
||||
|
||||
Or open the menu once without installing:
|
||||
|
||||
```powershell
|
||||
irm https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.ps1 | iex
|
||||
```
|
||||
|
||||
Windows PowerShell 5.1 (preinstalled on every supported Windows) is enough;
|
||||
PowerShell 7 works too.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
The same verbs on both platforms:
|
||||
|
||||
```
|
||||
lxs # Interactive menu (checks for updates, cached 24h)
|
||||
lxs setup # First-time install of all files to /usr/local/share/lxs
|
||||
lxs setup # First-time install
|
||||
lxs update # Update all installed files to latest
|
||||
lxs install <app> # Install an application
|
||||
lxs tool <name> [args] # Run a system tool
|
||||
@@ -33,11 +72,68 @@ lxs version # Show version
|
||||
lxs help # Show help
|
||||
```
|
||||
|
||||
When a new version is published in this repo, the interactive menu shows an
|
||||
`UPDATE_AVAILABLE` banner; run `lxs update` to pull all updated files.
|
||||
|
||||
## What is in the box
|
||||
|
||||
### Linux — `lxs install <app>`
|
||||
|
||||
| Name | What it installs |
|
||||
|---|---|
|
||||
| `coolify` | Coolify self-hosted PaaS |
|
||||
| `pterodactyl` | Pterodactyl game server panel |
|
||||
| `uptime-kuma` | Uptime Kuma monitoring |
|
||||
| `cloudpanel` | CloudPanel server control panel |
|
||||
| `proxmox` | Proxmox VE post-install tools |
|
||||
|
||||
### Linux — `lxs tool <name>`
|
||||
|
||||
| Name | What it does |
|
||||
|---|---|
|
||||
| `system` | System information and diagnostics |
|
||||
| `benchmark` | CPU, disk and network benchmark |
|
||||
| `harden` | UFW, fail2ban, unattended-upgrades |
|
||||
| `root-password` | Change the root password |
|
||||
| `update` | apt update / upgrade / autoremove |
|
||||
| `root-ssh-login` | Toggle root SSH password login |
|
||||
| `welcome`, `motd` | Custom MOTD banner |
|
||||
|
||||
### Windows — `lxs install <app>`
|
||||
|
||||
| Name | What it installs (via winget) |
|
||||
|---|---|
|
||||
| `dev` | Git, VS Code, Node.js, Python, PowerShell 7, Windows Terminal, WSL 2 |
|
||||
| `utilities` | 7-Zip, Notepad++, PowerToys, Sysinternals, Everything, ShareX |
|
||||
| `browsers` | LibreWolf, Firefox, Brave, Chrome, VLC, OBS Studio |
|
||||
| `containers` | WSL 2, Hyper-V, Docker Desktop, VirtualBox |
|
||||
| `claude-code` | Claude Code CLI (+ Git for Windows) |
|
||||
|
||||
### Windows — `lxs tool <name>`
|
||||
|
||||
| Name | What it does |
|
||||
|---|---|
|
||||
| `system` | Hardware, OS, disks, memory, event logs, SMART, activation |
|
||||
| `network` | IP config, listening ports, DNS flush, ping/traceroute, stack reset |
|
||||
| `cleanup` | Temp files, Windows Update cache, recycle bin, Prefetch, WinSxS |
|
||||
| `repair` | SFC, DISM RestoreHealth, chkdsk |
|
||||
| `restore-point` | Enable System Protection, create/list/roll back checkpoints |
|
||||
| `update` | Windows Update (built-in COM API) + `winget upgrade --all` |
|
||||
| `harden` | Firewall, SMBv1 off, UAC, Defender, LLMNR/NetBIOS, account audit |
|
||||
| `remote-access` | Remote Desktop (with NLA) and the OpenSSH server |
|
||||
| `debloat` | Remove preinstalled apps you pick, disable telemetry/ads/Copilot |
|
||||
|
||||
The Windows tools that change the system create a **restore point first**,
|
||||
show exactly what they will change and ask before doing it. `debloat` never
|
||||
removes an app you have not selected by number, and its registry, service and
|
||||
scheduled-task changes can be reverted from its own menu.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Debian or Ubuntu (other distros may work but are not tested)
|
||||
- `curl`
|
||||
- Some sub-scripts require root; they will auto-elevate via `sudo` when run.
|
||||
- **Linux**: Debian or Ubuntu (other distros may work but are not tested),
|
||||
`curl`. Some sub-scripts require root and auto-elevate via `sudo`.
|
||||
- **Windows**: Windows 10 1809+, Windows 11 or Server 2019+. `winget`
|
||||
(App Installer) is needed for the application installers only.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_cloudpanel.log"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
+19
-3
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_coolify.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
+21
-5
@@ -6,13 +6,29 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
|
||||
# Run a sibling app script. Prefers a file next to this script (installed
|
||||
# layout); falls back to downloading from LXS_RAW_BASE.
|
||||
# layout); falls back to downloading from LXS_RAW_PLATFORM_BASE.
|
||||
run_sibling() {
|
||||
local script_path=$1
|
||||
shift
|
||||
@@ -35,7 +51,7 @@ run_sibling() {
|
||||
local temp_file exit_code
|
||||
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
|
||||
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_PLATFORM_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
"${temp_file}" "$@"
|
||||
|
||||
+19
-3
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_proxmox.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_pterodactyl.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_uptime_kuma.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
+22
-17
@@ -17,8 +17,8 @@ if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
LXS_SCRIPT_DIR=$(dirname "$_lxs_resolved")
|
||||
unset _lxs_resolved
|
||||
fi
|
||||
if [ -n "$LXS_SCRIPT_DIR" ] && [ -r "${LXS_SCRIPT_DIR}/VERSION" ]; then
|
||||
LXS_VERSION=$(head -n1 "${LXS_SCRIPT_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -n "$LXS_SCRIPT_DIR" ] && [ -r "${LXS_SCRIPT_DIR}/../VERSION" ]; then
|
||||
LXS_VERSION=$(head -n1 "${LXS_SCRIPT_DIR}/../VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
LXS_VERSION="${LXS_VERSION:-dev}"
|
||||
|
||||
@@ -27,6 +27,12 @@ LXS_BRANCH="${LXS_BRANCH:-main}"
|
||||
export LXS_RAW_BASE="${LXS_RAW_BASE:-${LXS_REPO_URL}/raw/branch/${LXS_BRANCH}}"
|
||||
LXS_TARBALL_URL="${LXS_TARBALL_URL:-${LXS_REPO_URL}/archive/${LXS_BRANCH}.tar.gz}"
|
||||
|
||||
# Platform sub-tree inside the repo. Every apps/ and tools/ path is resolved
|
||||
# under it, both on disk (${LXS_INSTALL_DIR}/linux/...) and remotely
|
||||
# (${LXS_RAW_BASE}/linux/...). A future macos/ tree only needs this to change.
|
||||
LXS_PLATFORM_DIR="linux"
|
||||
export LXS_RAW_PLATFORM_BASE="${LXS_RAW_BASE}/${LXS_PLATFORM_DIR}"
|
||||
|
||||
LXS_INSTALL_DIR="${LXS_INSTALL_DIR:-/usr/local/share/lxs}"
|
||||
LXS_BIN_PATH="${LXS_BIN_PATH:-/usr/local/bin/lxs}"
|
||||
LXS_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/lxs"
|
||||
@@ -43,7 +49,7 @@ if [ -n "$LXS_SCRIPT_DIR" ] && [ -r "${LXS_SCRIPT_DIR}/lib/common.sh" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. "${LXS_SCRIPT_DIR}/lib/common.sh"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || {
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || {
|
||||
echo "Failed to fetch lib/common.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -67,12 +73,6 @@ lxs_self_path() {
|
||||
echo "$src"
|
||||
}
|
||||
|
||||
lxs_is_installed() {
|
||||
local self
|
||||
self=$(lxs_self_path) || return 1
|
||||
[ "$(dirname "$self")" = "$LXS_INSTALL_DIR" ]
|
||||
}
|
||||
|
||||
lxs_need_root_for() {
|
||||
local path=$1
|
||||
local parent
|
||||
@@ -167,9 +167,14 @@ download_and_run() {
|
||||
shift
|
||||
local script_name local_path
|
||||
script_name=$(basename "$script_path")
|
||||
local_path="${LXS_INSTALL_DIR}/${script_path}"
|
||||
|
||||
if lxs_is_installed && [ -f "$local_path" ]; then
|
||||
# Prefer the copy sitting next to this script, whatever the prefix: that
|
||||
# covers the installed layout, a git checkout, and a custom
|
||||
# LXS_INSTALL_DIR alike. Only a `curl | bash` run has no siblings on disk.
|
||||
local_path=""
|
||||
[ -n "$LXS_SCRIPT_DIR" ] && local_path="${LXS_SCRIPT_DIR}/${script_path}"
|
||||
|
||||
if [ -n "$local_path" ] && [ -f "$local_path" ]; then
|
||||
chmod +x "$local_path" 2>/dev/null || true
|
||||
echo ""
|
||||
show_separator
|
||||
@@ -194,7 +199,7 @@ download_and_run() {
|
||||
|
||||
if curl -fsSL -H "Cache-Control: no-cache" \
|
||||
-o "${temp_file}" \
|
||||
"${LXS_RAW_BASE}/${script_path}"; then
|
||||
"${LXS_RAW_PLATFORM_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
|
||||
@@ -217,7 +222,7 @@ download_and_run() {
|
||||
return $exit_code
|
||||
else
|
||||
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
|
||||
echo -e "${RED} URL: ${LXS_RAW_BASE}/${script_path}${NC}"
|
||||
echo -e "${RED} URL: ${LXS_RAW_PLATFORM_BASE}/${script_path}${NC}"
|
||||
rm -f "${temp_file}"
|
||||
return 1
|
||||
fi
|
||||
@@ -309,8 +314,8 @@ lxs_sync_install() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${work}/extracted/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then
|
||||
echo -e "${RED}[KO] Tarball is missing lxs.sh or VERSION${NC}"
|
||||
if [ ! -f "${work}/extracted/${LXS_PLATFORM_DIR}/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then
|
||||
echo -e "${RED}[KO] Tarball is missing ${LXS_PLATFORM_DIR}/lxs.sh or VERSION${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -334,8 +339,8 @@ lxs_sync_install() {
|
||||
rm -rf "${LXS_INSTALL_DIR}.old"
|
||||
fi
|
||||
|
||||
chmod +x "${LXS_INSTALL_DIR}/lxs.sh" 2>/dev/null || true
|
||||
find "${LXS_INSTALL_DIR}/apps" "${LXS_INSTALL_DIR}/tools" -name '*.sh' -exec chmod +x {} + 2>/dev/null || true
|
||||
chmod +x "${LXS_INSTALL_DIR}/lxs.sh" "${LXS_INSTALL_DIR}/${LXS_PLATFORM_DIR}/lxs.sh" 2>/dev/null || true
|
||||
find "${LXS_INSTALL_DIR}/${LXS_PLATFORM_DIR}" -name '*.sh' -exec chmod +x {} + 2>/dev/null || true
|
||||
|
||||
if [ -e "$LXS_BIN_PATH" ] || [ -L "$LXS_BIN_PATH" ]; then
|
||||
rm -f "$LXS_BIN_PATH"
|
||||
|
||||
+19
-3
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_harden.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
+21
-5
@@ -6,13 +6,29 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
|
||||
# Run a sibling tool script. Prefers a file next to this script (installed
|
||||
# layout); falls back to downloading from LXS_RAW_BASE.
|
||||
# layout); falls back to downloading from LXS_RAW_PLATFORM_BASE.
|
||||
run_sibling() {
|
||||
local script_path=$1
|
||||
shift
|
||||
@@ -36,7 +52,7 @@ run_sibling() {
|
||||
local temp_file exit_code
|
||||
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
|
||||
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_PLATFORM_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
"${temp_file}" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_root_password.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_root_ssh_login.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_benchmark.log"
|
||||
|
||||
# Always clean up benchmark artifacts on exit, even on error or Ctrl+C. Without
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_system_infos.log"
|
||||
|
||||
# Menu: System Infos
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_update_server.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -6,10 +6,26 @@
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
# Prefers the sibling ../lib/common.sh (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone via curl.
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
LXS_RAW_PLATFORM_BASE="${LXS_RAW_PLATFORM_BASE:-${LXS_RAW_BASE}/linux}"
|
||||
_lxs_lib=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_self=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_self="${BASH_SOURCE[0]}"
|
||||
_lxs_lib="$(dirname "$_lxs_self")/../lib/common.sh"
|
||||
fi
|
||||
if [ -n "$_lxs_lib" ] && [ -r "$_lxs_lib" ]; then
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$_lxs_lib"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_PLATFORM_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
unset _lxs_lib _lxs_self
|
||||
export LXS_LOG_FILE="/tmp/lxs_welcome_message.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<#
|
||||
LXS - platform dispatcher (Windows side)
|
||||
|
||||
Thin shim kept at the repo root so the documented one-liner stays short and
|
||||
stable while the real implementation lives in windows\lxs.ps1:
|
||||
|
||||
irm https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.ps1 | iex
|
||||
& ([scriptblock]::Create((irm https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.ps1))) setup
|
||||
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
License: MIT
|
||||
#>
|
||||
|
||||
$LxsShimArgs = @($args)
|
||||
|
||||
$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" }
|
||||
$LxsPlatformDir = 'windows'
|
||||
|
||||
# Running this shim under pwsh on Linux/macOS is a mistake worth catching.
|
||||
if ($PSVersionTable.PSVersion.Major -ge 6 -and
|
||||
-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
|
||||
[System.Runtime.InteropServices.OSPlatform]::Windows)) {
|
||||
Write-Error 'lxs.ps1 is the Windows entrypoint. On Linux or macOS, use lxs.sh instead.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Locate a sibling windows\lxs.ps1 (repo checkout or installed layout).
|
||||
$LxsSelfDir = $PSScriptRoot
|
||||
if (-not $LxsSelfDir -and $MyInvocation.MyCommand.Path) {
|
||||
$LxsSelfDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
}
|
||||
|
||||
if ($LxsSelfDir) {
|
||||
$local = Join-Path $LxsSelfDir "$LxsPlatformDir\lxs.ps1"
|
||||
if (Test-Path $local) {
|
||||
& $local @LxsShimArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
# No files on disk — we were piped from irm. Fetch the platform entrypoint.
|
||||
$temp = Join-Path $env:TEMP ("lxs.entrypoint.{0}.ps1" -f [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$LxsRawBase/$LxsPlatformDir/lxs.ps1" -OutFile $temp `
|
||||
-UseBasicParsing -Headers @{ 'Cache-Control' = 'no-cache' } -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error "lxs: failed to fetch $LxsRawBase/$LxsPlatformDir/lxs.ps1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$global:LASTEXITCODE = 0
|
||||
& $temp @LxsShimArgs
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
Remove-Item $temp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
exit $code
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - platform dispatcher (POSIX side)
|
||||
# Description: Thin shim kept at the repo root so the documented install URL
|
||||
# and the /usr/local/bin/lxs symlink stay stable while the real
|
||||
# implementation lives in linux/lxs.sh.
|
||||
# Repo: https://git.hyko.cx/hykocx/lxs
|
||||
# License: MIT
|
||||
|
||||
LXS_REPO_URL="${LXS_REPO_URL:-https://git.hyko.cx/hykocx/lxs}"
|
||||
LXS_BRANCH="${LXS_BRANCH:-main}"
|
||||
export LXS_RAW_BASE="${LXS_RAW_BASE:-${LXS_REPO_URL}/raw/branch/${LXS_BRANCH}}"
|
||||
|
||||
# Platform sub-tree to dispatch into. Only linux/ ships today; the darwin
|
||||
# branch is here so a macos/ tree drops in without touching this file.
|
||||
case "$(uname -s 2>/dev/null)" in
|
||||
Linux) LXS_PLATFORM_DIR="linux" ;;
|
||||
Darwin) LXS_PLATFORM_DIR="macos" ;;
|
||||
*) LXS_PLATFORM_DIR="linux" ;;
|
||||
esac
|
||||
|
||||
# Locate a sibling <platform>/lxs.sh (repo checkout or installed layout).
|
||||
_lxs_self_dir=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_resolved=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_resolved=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_resolved="${BASH_SOURCE[0]}"
|
||||
[ -f "$_lxs_resolved" ] && _lxs_self_dir=$(dirname "$_lxs_resolved")
|
||||
unset _lxs_resolved
|
||||
fi
|
||||
|
||||
if [ -n "$_lxs_self_dir" ] && [ -f "${_lxs_self_dir}/${LXS_PLATFORM_DIR}/lxs.sh" ]; then
|
||||
exec bash "${_lxs_self_dir}/${LXS_PLATFORM_DIR}/lxs.sh" "$@"
|
||||
fi
|
||||
|
||||
# No files on disk — we were piped from curl. Fetch the platform entrypoint.
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "lxs: curl is required to bootstrap ${LXS_PLATFORM_DIR}/lxs.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_lxs_tmp=$(mktemp "/tmp/lxs.entrypoint.XXXXXX.sh") || exit 1
|
||||
if ! curl -fsSL -H "Cache-Control: no-cache" \
|
||||
-o "$_lxs_tmp" "${LXS_RAW_BASE}/${LXS_PLATFORM_DIR}/lxs.sh"; then
|
||||
echo "lxs: failed to fetch ${LXS_RAW_BASE}/${LXS_PLATFORM_DIR}/lxs.sh" >&2
|
||||
rm -f "$_lxs_tmp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$_lxs_tmp" "$@"
|
||||
_lxs_rc=$?
|
||||
rm -f "$_lxs_tmp"
|
||||
exit $_lxs_rc
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user