diff --git a/ubuntu-desktop-prune.sh b/ubuntu-desktop-prune.sh
new file mode 100644
index 0000000..9ace7db
--- /dev/null
+++ b/ubuntu-desktop-prune.sh
@@ -0,0 +1,510 @@
+#!/usr/bin/env bash
+# ubuntu-desktop-prune.sh
+#
+# Copyright (C) 2026 LINUXexpert.org
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see .
+
+set -euo pipefail
+
+# =========================
+# Version
+# =========================
+SCRIPT_VERSION="1.1"
+
+# =========================
+# Globals / Defaults
+# =========================
+LOG_DIR="/var/log/ubuntu-to-mint"
+mkdir -p "$LOG_DIR"
+LOG_FILE="${LOG_DIR}/ubuntu-desktop-prune-$(date +%Y%m%d-%H%M%S).log"
+exec > >(tee -a "$LOG_FILE") 2>&1
+
+CMD=""
+ASSUME_YES="no"
+WITH_RECOMMENDS="no"
+SKIP_DM_FIX="no"
+ROLLBACK_DIR=""
+
+# For safety gates
+MAX_REMOVALS_DEFAULT=75
+
+# =========================
+# Pretty output
+# =========================
+is_tty() { [[ -t 1 ]]; }
+
+if is_tty && command -v tput >/dev/null 2>&1; then
+ RED="$(tput setaf 1)"
+ GREEN="$(tput setaf 2)"
+ YELLOW="$(tput setaf 3)"
+ BLUE="$(tput setaf 4)"
+ BOLD="$(tput bold)"
+ RESET="$(tput sgr0)"
+else
+ RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; RESET=""
+fi
+
+info() { echo "${BLUE}INFO:${RESET} $*"; }
+ok() { echo "${GREEN}OK:${RESET} $*"; }
+warn() { echo "${YELLOW}WARN:${RESET} $*"; }
+die() { echo "${RED}ERROR:${RESET} $*"; exit 1; }
+
+have_cmd() { command -v "$1" >/dev/null 2>&1; }
+
+need_root() {
+ if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
+ die "This must be run as root. Use: sudo bash $0 ..."
+ fi
+}
+
+on_err() {
+ local rc=$?
+ local line="${BASH_LINENO[0]:-unknown}"
+ echo "ERROR: FAILED at line ${line} (exit ${rc})."
+ echo "ERROR: Command: ${BASH_COMMAND}"
+ echo "ERROR: Log: ${LOG_FILE}"
+ exit "$rc"
+}
+trap on_err ERR
+
+usage() {
+ cat </dev/null 2>&1; then
+ die "APT/dpkg lock is held (lock file: $l). Close package managers and try again."
+ fi
+ done
+}
+
+apt_fix_broken() {
+ info "Attempting basic dpkg/apt remediation (best-effort)..."
+ DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true
+ DEBIAN_FRONTEND=noninteractive apt-get -y -f install || true
+}
+
+apt_opts_common() {
+ local -a opts
+ opts=(-y -o Dpkg::Use-Pty=0 -o Acquire::Retries=3)
+ if [[ "$WITH_RECOMMENDS" != "yes" ]]; then
+ opts+=(--no-install-recommends)
+ fi
+ echo "${opts[@]}"
+}
+
+# =========================
+# OS / environment checks
+# =========================
+detect_os_minimal() {
+ [[ -r /etc/os-release ]] || die "Cannot read /etc/os-release"
+ # shellcheck disable=SC1091
+ . /etc/os-release
+
+ info "Script v${SCRIPT_VERSION}"
+ info "Log: ${LOG_FILE}"
+ info "Detected OS: ${NAME:-unknown} (ID=${ID:-?}, VERSION_ID=${VERSION_ID:-?}, CODENAME=${VERSION_CODENAME:-?})"
+
+ # We allow Ubuntu base systems (including “Ubuntu-with-Mint-repos” setups).
+ if [[ "${ID:-}" != "ubuntu" && "${ID:-}" != "linuxmint" ]]; then
+ warn "OS ID is not ubuntu/linuxmint. Proceeding anyway, but this script is designed for Ubuntu base systems."
+ fi
+
+ if ! have_cmd apt-get || ! have_cmd dpkg; then
+ die "apt-get/dpkg not found. This script requires Debian/Ubuntu-style package management."
+ fi
+}
+
+# =========================
+# Backup / rollback (limited)
+# =========================
+backup_state() {
+ local backup_dir="/root/ubuntu-desktop-prune-backup-$(date +%Y%m%d-%H%M%S)"
+ mkdir -p "$backup_dir"
+
+ info "Creating backup at: ${backup_dir}"
+ mkdir -p "${backup_dir}/etc"
+ cp -a /etc/apt "${backup_dir}/etc/" || true
+ cp -a /etc/lightdm 2>/dev/null "${backup_dir}/etc/" || true
+ cp -a /etc/X11/default-display-manager 2>/dev/null "${backup_dir}/etc/" || true
+ cp -a /etc/systemd/system/display-manager.service 2>/dev/null "${backup_dir}/etc/" || true
+
+ dpkg-query -W -f='${Package}\t${Version}\n' > "${backup_dir}/dpkg-packages.tsv" || true
+ apt-mark showmanual > "${backup_dir}/apt-manual.txt" || true
+ apt-mark showhold > "${backup_dir}/apt-holds.txt" || true
+ systemctl list-unit-files --state=enabled > "${backup_dir}/enabled-services.txt" 2>/dev/null || true
+
+ ok "Backup complete."
+ echo "$backup_dir"
+}
+
+rollback() {
+ need_root
+ local dir="${1:-}"
+ [[ -n "$dir" ]] || die "rollback requires a backup directory argument"
+ [[ -d "$dir" ]] || die "backup directory not found: $dir"
+ [[ -d "$dir/etc/apt" ]] || die "backup does not contain etc/apt: $dir/etc/apt"
+
+ info "Restoring /etc/apt from: $dir/etc/apt"
+ rm -rf /etc/apt
+ cp -a "$dir/etc/apt" /etc/apt
+
+ if [[ -d "$dir/etc/lightdm" ]]; then
+ info "Restoring /etc/lightdm from backup..."
+ rm -rf /etc/lightdm
+ cp -a "$dir/etc/lightdm" /etc/lightdm
+ fi
+
+ if [[ -f "$dir/etc/default-display-manager" ]]; then
+ info "Restoring /etc/X11/default-display-manager from backup..."
+ mkdir -p /etc/X11
+ cp -a "$dir/etc/default-display-manager" /etc/X11/default-display-manager
+ fi
+
+ if [[ -f "$dir/etc/display-manager.service" ]]; then
+ info "Restoring /etc/systemd/system/display-manager.service from backup..."
+ mkdir -p /etc/systemd/system
+ cp -a "$dir/etc/display-manager.service" /etc/systemd/system/display-manager.service
+ systemctl daemon-reload >/dev/null 2>&1 || true
+ fi
+
+ ok "Rollback completed (APT + DM configs)."
+ warn "Rollback does NOT automatically reinstall removed packages."
+ info "Now run:"
+ echo " sudo apt-get update"
+ echo " sudo apt-get -f install"
+}
+
+# =========================
+# Display manager hardening (noninteractive-safe)
+# =========================
+ensure_lightdm_default_noninteractive() {
+ if [[ "$SKIP_DM_FIX" == "yes" ]]; then
+ warn "--skip-dm-fix set; skipping LightDM default selection."
+ return 0
+ fi
+
+ if [[ ! -d /run/systemd/system ]]; then
+ warn "systemd not detected; skipping LightDM enablement."
+ return 0
+ fi
+
+ info "Ensuring LightDM is default display manager (debconf preseed + dpkg-reconfigure)..."
+
+ DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) install lightdm slick-greeter debconf-utils >/dev/null 2>&1 || true
+
+ # Preseed the DM selection so noninteractive installs switch away from gdm3.
+ # Key: shared/default-x-display-manager
+ echo "lightdm shared/default-x-display-manager select lightdm" | debconf-set-selections || true
+
+ # Apply selection
+ DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive lightdm >/dev/null 2>&1 || true
+
+ # Debian/Ubuntu mechanism
+ mkdir -p /etc/X11
+ echo "/usr/sbin/lightdm" > /etc/X11/default-display-manager || true
+
+ if have_cmd update-alternatives; then
+ update-alternatives --set x-display-manager /usr/sbin/lightdm >/dev/null 2>&1 || true
+ fi
+
+ # If gdm3 exists, disable it so it can't take control
+ if systemctl list-unit-files | awk '{print $1}' | grep -qx 'gdm3.service'; then
+ systemctl disable --now gdm3 >/dev/null 2>&1 || true
+ systemctl mask gdm3 >/dev/null 2>&1 || true
+ fi
+
+ # Ensure LightDM enabled
+ systemctl unmask lightdm >/dev/null 2>&1 || true
+ systemctl set-default graphical.target >/dev/null 2>&1 || true
+ systemctl enable lightdm >/dev/null 2>&1 || true
+ systemctl daemon-reload >/dev/null 2>&1 || true
+
+ # Best-effort start
+ systemctl restart lightdm >/dev/null 2>&1 || true
+
+ ok "LightDM default selection/enablement applied."
+}
+
+# =========================
+# Target selection
+# =========================
+pkg_installed() {
+ dpkg-query -W -f='${Status}\n' "$1" 2>/dev/null | grep -q "install ok installed"
+}
+
+build_prune_list() {
+ # Conservative list: remove Ubuntu GNOME session + Ubuntu desktop meta packages + GDM.
+ # We intentionally avoid removing libs/toolkit packages that Cinnamon/Xfce/MATE may still rely on.
+ local -a candidates=(
+ ubuntu-desktop
+ ubuntu-desktop-minimal
+ ubuntu-session
+ ubuntu-session-minimal
+ gdm3
+ gnome-shell
+ gnome-shell-common
+ gnome-shell-extension-ubuntu-dock
+ yaru-theme-gnome
+ yaru-theme-gtk
+ yaru-theme-icon
+ yaru-theme-sound
+ ubuntu-wallpapers
+ ubuntu-wallpapers-noble
+ ubuntu-wallpapers-jammy
+ gnome-software
+ snap-store
+ )
+
+ local -a selected=()
+ local p
+ for p in "${candidates[@]}"; do
+ if pkg_installed "$p"; then
+ selected+=("$p")
+ fi
+ done
+
+ printf '%s\n' "${selected[@]}"
+}
+
+show_target_summary() {
+ local -a pkgs=("$@")
+ if [[ ${#pkgs[@]} -eq 0 ]]; then
+ warn "No target packages from the conservative prune list are currently installed."
+ warn "Nothing to do."
+ return 1
+ fi
+
+ info "Prune targets (installed):"
+ printf ' - %s\n' "${pkgs[@]}"
+ return 0
+}
+
+# =========================
+# Simulation + safety gates
+# (FIXED: exact-match critical package detection)
+# =========================
+apt_simulate_purge() {
+ local -a pkgs=("$@")
+ local sim="/tmp/ubuntu-desktop-prune-sim.$$.txt"
+
+ info "Simulating purge (no changes)..."
+ set +e
+ DEBIAN_FRONTEND=noninteractive apt-get -s purge --autoremove "${pkgs[@]}" 2>&1 | tee "$sim"
+ local rc=${PIPESTATUS[0]}
+ set -e
+
+ [[ $rc -eq 0 ]] || die "APT simulation failed (exit $rc). See: $sim"
+
+ # Build an exact package list from the simulation.
+ # APT prints: "Remv :amd64 ..." or "Purg ..."
+ mapfile -t sim_removed < <(
+ awk '/^(Remv|Purg)[[:space:]]+/{print $2}' "$sim" \
+ | sed -E 's/:[a-z0-9]+$//' \
+ | sort -u
+ )
+
+ # Hard stops: ONLY if these exact packages are removed.
+ # NOTE: do NOT treat "systemd-*" as a hard stop.
+ local -a critical_exact=(
+ sudo
+ systemd
+ systemd-sysv
+ network-manager
+ openssh-server
+ linux-image-generic
+ linux-image-amd64
+ )
+
+ local c r
+ for c in "${critical_exact[@]}"; do
+ for r in "${sim_removed[@]}"; do
+ if [[ "$r" == "$c" ]]; then
+ die "Simulation shows removal of critical package '${c}'. Aborting. See: $sim"
+ fi
+ done
+ done
+
+ local remv_count
+ remv_count="$(grep -cE '^(Remv|Purg)[[:space:]]' "$sim" || true)"
+ info "Simulation removal count: ${remv_count}"
+
+ if [[ "${ASSUME_YES}" != "yes" && "${remv_count}" -gt "${MAX_REMOVALS_DEFAULT}" ]]; then
+ die "Simulation wants to remove ${remv_count} packages (too many for 'gentle'). Re-run with --yes only if you reviewed $sim."
+ fi
+
+ ok "Simulation looks acceptable. Review: $sim"
+}
+
+# =========================
+# Commands
+# =========================
+doctor() {
+ need_root
+ ensure_no_apt_locks
+ detect_os_minimal
+
+ info "Doctor checks..."
+ apt_fix_broken
+
+ local holds
+ holds="$(apt-mark showhold || true)"
+ if [[ -n "$holds" ]]; then
+ warn "Held packages detected (may affect pruning):"
+ echo "$holds"
+ else
+ ok "No held packages detected."
+ fi
+
+ local -a pkgs=()
+ mapfile -t pkgs < <(build_prune_list || true)
+
+ if show_target_summary "${pkgs[@]}"; then
+ ok "Doctor complete."
+ else
+ ok "Doctor complete (nothing obvious to prune)."
+ fi
+}
+
+plan() {
+ need_root
+ ensure_no_apt_locks
+ detect_os_minimal
+ apt_fix_broken
+
+ local -a pkgs=()
+ mapfile -t pkgs < <(build_prune_list || true)
+ show_target_summary "${pkgs[@]}" || return 0
+
+ # LightDM check (plan-time warning)
+ if [[ "$SKIP_DM_FIX" != "yes" ]]; then
+ if ! pkg_installed lightdm; then
+ warn "lightdm is not installed. prune will attempt to install/activate it first."
+ fi
+ if [[ -f /etc/X11/default-display-manager ]]; then
+ info "Current default-display-manager: $(cat /etc/X11/default-display-manager || true)"
+ fi
+ fi
+
+ apt_simulate_purge "${pkgs[@]}"
+}
+
+prune() {
+ need_root
+ ensure_no_apt_locks
+ detect_os_minimal
+
+ if [[ "$ASSUME_YES" != "yes" ]]; then
+ die "prune is destructive and requires --yes"
+ fi
+
+ apt_fix_broken
+
+ local -a pkgs=()
+ mapfile -t pkgs < <(build_prune_list || true)
+ show_target_summary "${pkgs[@]}" || return 0
+
+ # Ensure DM is safe before removing gdm3/gnome session packages
+ ensure_lightdm_default_noninteractive
+
+ # Re-run simulation immediately before changes
+ apt_simulate_purge "${pkgs[@]}"
+
+ local backup_dir
+ backup_dir="$(backup_state)"
+ info "Backup dir: ${backup_dir}"
+
+ info "Proceeding with purge..."
+ DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) purge --autoremove "${pkgs[@]}"
+
+ info "Post-purge remediation..."
+ apt_fix_broken
+ DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) autoremove --purge || true
+ DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) clean || true
+
+ ok "Prune completed."
+ info "Log: ${LOG_FILE}"
+ info "Backup (limited): ${backup_dir}"
+ info "Next:"
+ echo " 1) Reboot"
+ echo " 2) Confirm you can login to your desired session"
+ echo " 3) If you hit issues, restore via Timeshift/snapshot (preferred) or rollback APT/DM configs:"
+ echo " sudo bash $0 rollback ${backup_dir}"
+}
+
+# =========================
+# Arg parsing (command-first OR options-first)
+# =========================
+parse_args_any_order() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ doctor|plan|prune|rollback)
+ [[ -z "$CMD" ]] || die "Multiple commands specified: '${CMD}' and '$1'"
+ CMD="$1"
+ shift 1
+ if [[ "$CMD" == "rollback" ]]; then
+ if [[ $# -gt 0 && "${1:-}" != --* ]]; then
+ ROLLBACK_DIR="$1"
+ shift 1
+ fi
+ fi
+ ;;
+ --yes) ASSUME_YES="yes"; shift 1;;
+ --with-recommends) WITH_RECOMMENDS="yes"; shift 1;;
+ --skip-dm-fix) SKIP_DM_FIX="yes"; shift 1;;
+ -h|--help|help) usage; exit 0;;
+ *) die "Unknown argument: $1" ;;
+ esac
+ done
+}
+
+main() {
+ parse_args_any_order "$@"
+ [[ -n "$CMD" ]] || { usage; exit 1; }
+
+ case "$CMD" in
+ doctor) doctor ;;
+ plan) plan ;;
+ prune) prune ;;
+ rollback)
+ [[ -n "$ROLLBACK_DIR" ]] || die "rollback requires a directory argument"
+ rollback "$ROLLBACK_DIR"
+ ;;
+ *) die "Unknown command: $CMD" ;;
+ esac
+}
+
+main "$@"