#!/usr/bin/env bash set -euo pipefail # POS: system uninstall — Remove pos toolkit binaries, services, shell integration, config, and data # POS_FLAGS: --yes --config --data source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" usage() { cat <<'EOF' Usage: pos system uninstall [--yes] [--config] [--data] Remove the pos toolkit from this machine. Scans for installed components and removes them interactively (or non-interactively with --yes). Tiers: Tier 1 (always): binaries, entertainment plugins, systemd services, shell integration Tier 2 (--config): config files in ~/.config/linux_post_install/ Tier 3 (--data): session/log/capture data in ~/.local/share/linux_post_install/ Flags: --yes Skip confirmation prompts (removes defaults only; combine with --config/--data for more) --config Include config files (Tier 2) in removal --data Include session/log data (Tier 3) in removal Examples: pos system uninstall # interactive, tier 1 only pos system uninstall --yes # non-interactive, tier 1 only pos system uninstall --yes --config --data # nuclear option Intentionally NOT removed (user-managed): - apt packages (system packages installed by preinstall.sh) - /usr/local/bin/yt-dlp (manually installed) - ~/.config/rclone/ (rclone manages its own config) - ~/.ssh/authorized_keys additions (user SSH access) - pos-owned config files (removed by --config tier) - pos-owned data files (removed by --data tier) EOF exit 0 } # ── Parse flags ───────────────────────────────────────────────── YES_MODE=0 DEL_CONFIG=0 DEL_DATA=0 while [ $# -gt 0 ]; do case "$1" in -h|--help) usage ;; --yes) YES_MODE=1; shift ;; --config) DEL_CONFIG=1; shift ;; --data) DEL_DATA=1; shift ;; -*) err "Unknown option '$1'" ;; *) err "Unexpected argument: $1 (no subcommands)" ;; esac done # ── pos-owned installed files (single source of truth, mirrors install.sh) ── # lib/*.sh list shipped by install.sh phase 2 to /usr/local/bin. POS_LIBS=(common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh \ user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh registry.sh \ yt-lib.sh bank-lib.sh) # Exact lines postinstall.sh appends to ~/.bashrc. PATH_LINE='export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$HOME/.local/bin:$PATH"' COMPLETION_LINE='source /usr/local/share/bash-completion/completions/pos.bash 2>/dev/null || true' # Pos-ai-hook source lines are not written by the installer (user/legacy-added), # so they are matched by an anchored source-pattern, never a bare substring. HOOK_PATTERN='^[[:space:]]*source[[:space:]].*pos-ai-hook\.sh' # Entertainment plugins installed by install.sh — discovered by POS_PLUGIN marker # so future plugins are removed without touching this file. installed_plugins() { local ep for ep in /usr/local/bin/*.sh; do [ -f "$ep" ] || continue grep -q '^# POS_PLUGIN:' "$ep" 2>/dev/null && echo "$ep" done | sort } # ── Scan functions ────────────────────────────────────────────── scan_tier1() { local found=() # ── Binaries in /usr/local/bin ── # Main dispatcher [ -f /usr/local/bin/pos ] && found+=("/usr/local/bin/pos") # All pos-* tools (sorted for deterministic display) local f while IFS= read -r f; do found+=("$f") done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true) # Lib files shipped by install.sh (single source: POS_LIBS) for f in "${POS_LIBS[@]}"; do [ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f") done # AI providers subdirectory [ -d /usr/local/bin/ai-providers ] && found+=("/usr/local/bin/ai-providers/") # Entertainment plugins installed by install.sh (discovered by POS_PLUGIN marker) while IFS= read -r f; do [ -n "$f" ] && found+=("$f") done < <(installed_plugins) # Legacy forwarders for f in wr-* mp3 mp4 vbox ssh-load-all; do if compgen -G "/usr/local/bin/$f" >/dev/null 2>&1; then while IFS= read -r lf; do found+=("$lf") done < <(compgen -G "/usr/local/bin/$f" 2>/dev/null) fi done # Prebuilt binaries (hotspot) for f in wihotspot wihotspot-gui create_ap; do [ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f") done # Feature scripts installed by install.sh for f in autostart.sh usb-automount.sh; do [ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f") done # User-local binaries [ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && found+=("$HOME/.local/bin/pos-ai-hook.sh") # pos ai alias wrapper scripts (marker-managed) if [ -d "$HOME/.local/bin" ]; then local awf for awf in "$HOME/.local/bin"/*; do [ -f "$awf" ] || continue grep -q 'Managed by pos ai alias' "$awf" 2>/dev/null && found+=("$awf") done fi # Completion file [ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash") # ~/.bash_completion entries (anchored source lines only, never /pos/ substring) if [ -f "$HOME/.bash_completion" ]; then while IFS= read -r line; do found+=("~/.bash_completion: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)") done < <(grep -nE '^[[:space:]]*source[[:space:]].*pos\.bash' "$HOME/.bash_completion" 2>/dev/null || true) fi # ── Systemd services ── for svc in autostart.service ssh-agent.service usb-automount.service; do if systemctl is-enabled "$svc" &>/dev/null 2>&1; then found+=("service: $svc") elif [ -f "/etc/systemd/system/$svc" ]; then found+=("service: $svc") fi done # Catch any other linux_post_install-related services while IFS= read -r line; do local svc_name svc_name=$(echo "$line" | awk '{print $1}') # Skip already-listed services local already=0 for listed in autostart.service ssh-agent.service usb-automount.service; do [ "$svc_name" = "$listed" ] && already=1 && break done [ "$already" -eq 0 ] && found+=("service: $svc_name") done < <(systemctl list-unit-files --type=service 2>/dev/null | grep -i 'linux_post_install\|pos-' || true) # ── ScaleTail templates + feature-flag store (install.sh phase 2/4) ── [ -d /usr/local/share/linux_post_install/scale-tail ] && found+=("/usr/local/share/linux_post_install/scale-tail/") [ -d /usr/local/share/linux_post_install/flags ] && found+=("/usr/local/share/linux_post_install/flags/") # ── Runtime-created user units (pos-* in ~/.config/systemd/user) ── local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" if [ -d "$user_unit_dir" ]; then local ufile while IFS= read -r ufile; do [ -n "$ufile" ] && found+=("user-unit: $(basename "$ufile")") done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true) fi # ── Shell integration (~/.bashrc) — exact installer lines + anchored hook ── if [ -f "$HOME/.bashrc" ]; then while IFS= read -r line; do found+=("~/.bashrc: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)") done < <({ grep -nF "$PATH_LINE" "$HOME/.bashrc"; grep -nF "$COMPLETION_LINE" "$HOME/.bashrc"; grep -nE "$HOOK_PATTERN" "$HOME/.bashrc"; } 2>/dev/null | sort -u || true) fi printf '%s\n' "${found[@]}" } scan_tier2() { local found=() local cfg="$HOME/.config/linux_post_install" if [ -d "$cfg" ]; then while IFS= read -r f; do found+=("$f") done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null | sort) fi printf '%s\n' "${found[@]}" } scan_tier3() { local found=() local data="$HOME/.local/share/linux_post_install" if [ -d "$data" ]; then while IFS= read -r f; do found+=("$f") done < <(find "$data" -maxdepth 2 \( -type f -o -type d \) 2>/dev/null | sort) fi printf '%s\n' "${found[@]}" } # ── Display function ──────────────────────────────────────────── display_plan() { local -a t1=() t2=() t3=() # Collect non-empty entries from scan output while IFS= read -r line; do [ -n "$line" ] && t1+=("$line") done <<< "${1:-}" while IFS= read -r line; do [ -n "$line" ] && t2+=("$line") done <<< "${2:-}" while IFS= read -r line; do [ -n "$line" ] && t3+=("$line") done <<< "${3:-}" echo echo "${BOLD}pos uninstall — what will be removed:${RESET}" echo local n=1 if [ "${#t1[@]}" -gt 0 ]; then echo "${CYAN}Tier 1 (always):${RESET}" for item in "${t1[@]}"; do printf " %3d) %s\n" "$n" "$item" n=$((n + 1)) done else echo "${CYAN}Tier 1 (always):${RESET} (nothing found)" fi echo if [ "${#t2[@]}" -gt 0 ]; then echo "${YELLOW}Tier 2 (--config to include):${RESET}" for item in "${t2[@]}"; do printf " %3d) %s\n" "$n" "$item" n=$((n + 1)) done else echo "${YELLOW}Tier 2 (--config to include):${RESET} (nothing found)" fi echo if [ "${#t3[@]}" -gt 0 ]; then echo "${YELLOW}Tier 3 (--data to include):${RESET}" for item in "${t3[@]}"; do printf " %3d) %s\n" "$n" "$item" n=$((n + 1)) done else echo "${YELLOW}Tier 3 (--data to include):${RESET} (nothing found)" fi echo } # ── Removal functions ─────────────────────────────────────────── remove_tier1() { local count=0 # ── Binaries ── # Main dispatcher [ -f /usr/local/bin/pos ] && { rm -f /usr/local/bin/pos && count=$((count+1)); } # All pos-* tools local f while IFS= read -r f; do [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); } done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true) # Lib files (single source: POS_LIBS) for f in "${POS_LIBS[@]}"; do [ -f "/usr/local/bin/$f" ] && { rm -f "/usr/local/bin/$f" && count=$((count+1)); } done # AI providers directory if [ -d /usr/local/bin/ai-providers ]; then rm -rf /usr/local/bin/ai-providers && count=$((count+1)) fi # Entertainment plugins (discovered by POS_PLUGIN marker) while IFS= read -r f; do [ -n "$f" ] && [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); } done < <(installed_plugins) # Legacy forwarders for pat in 'wr-*' mp3 mp4 vbox ssh-load-all; do while IFS= read -r f; do [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); } done < <(compgen -G "/usr/local/bin/$pat" 2>/dev/null || true) done # Prebuilt binaries for f in /usr/local/bin/wihotspot /usr/local/bin/wihotspot-gui /usr/local/bin/create_ap; do [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); } done # Feature scripts for f in /usr/local/bin/autostart.sh /usr/local/bin/usb-automount.sh; do [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); } done # User-local binaries [ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && { rm -f "$HOME/.local/bin/pos-ai-hook.sh" && count=$((count+1)); } # pos ai alias wrapper scripts (marker-managed only — foreign files untouched) if [ -d "$HOME/.local/bin" ]; then local arwf for arwf in "$HOME/.local/bin"/*; do [ -f "$arwf" ] || continue if grep -q 'Managed by pos ai alias' "$arwf" 2>/dev/null; then rm -f "$arwf" && count=$((count+1)) fi done fi # Completion file [ -f /usr/local/share/bash-completion/completions/pos.bash ] && { rm -f /usr/local/share/bash-completion/completions/pos.bash && count=$((count+1)); } # ── Systemd services ── for svc in autostart.service ssh-agent.service usb-automount.service; do if systemctl is-enabled "$svc" &>/dev/null 2>&1; then systemctl disable --now "$svc" 2>/dev/null || true rm -f "/etc/systemd/system/$svc" count=$((count+1)) elif [ -f "/etc/systemd/system/$svc" ]; then rm -f "/etc/systemd/system/$svc" count=$((count+1)) fi done # Additional linux_post_install services while IFS= read -r svc_file; do local svc_name svc_name=$(basename "$svc_file" .service) # Skip already-handled services local already=0 for listed in autostart ssh-agent usb-automount; do [ "$svc_name" = "$listed" ] && already=1 && break done if [ "$already" -eq 0 ]; then systemctl disable --now "$svc_name.service" 2>/dev/null || true rm -f "$svc_file" count=$((count+1)) fi done < <(find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*' 2>/dev/null || true) # Reload daemon after service changes systemctl daemon-reload 2>/dev/null || true # ── Runtime-created USER systemd units (pos-* in ~/.config/systemd/user) ── # Wrapped `|| true`: no user session (e.g. running as a scheduled task) is fine. local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" if [ -d "$user_unit_dir" ]; then local ufile uniname while IFS= read -r ufile; do [ -n "$ufile" ] || continue uniname="$(basename "$ufile")" systemctl --user disable --now "$uniname" 2>/dev/null || true rm -f "$ufile" && count=$((count+1)) done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true) systemctl --user daemon-reload 2>/dev/null || true fi # ── ScaleTail templates (submodule clone — guard existence) ── if [ -d /usr/local/share/linux_post_install/scale-tail ]; then rm -rf /usr/local/share/linux_post_install/scale-tail && count=$((count+1)) fi # ── Feature-flag store ── if [ -d /usr/local/share/linux_post_install/flags ]; then rm -rf /usr/local/share/linux_post_install/flags && count=$((count+1)) fi # Clean up parent dir if empty rmdir /usr/local/share/linux_post_install 2>/dev/null || true # ── Shell integration (~/.bashrc) — exact installer lines + anchored hook ── # Only lines postinstall.sh itself added are removed by exact literal match; # pos-ai-hook lines are matched anchored (never a bare substring). if [ -f "$HOME/.bashrc" ]; then local before after removed tmp before=$(wc -l < "$HOME/.bashrc") tmp="$(mktemp)" awk -v p="$PATH_LINE" -v c="$COMPLETION_LINE" -v h="$HOOK_PATTERN" ' $0 == p || $0 == c || $0 ~ h { next } { print } ' "$HOME/.bashrc" > "$tmp" after=$(wc -l < "$tmp") removed=$((before - after)) if [ "$removed" -gt 0 ]; then chmod --reference="$HOME/.bashrc" "$tmp" mv "$tmp" "$HOME/.bashrc" count=$((count + removed)) else rm -f "$tmp" fi fi # ── Shell completion (~/.bash_completion) — anchored source lines only ── if [ -f "$HOME/.bash_completion" ]; then local before after removed tmp before=$(wc -l < "$HOME/.bash_completion") tmp="$(mktemp)" awk '/^[[:space:]]*source[[:space:]].*pos\.bash/ { next } { print }' "$HOME/.bash_completion" > "$tmp" after=$(wc -l < "$tmp") removed=$((before - after)) if [ "$removed" -gt 0 ]; then chmod --reference="$HOME/.bash_completion" "$tmp" mv "$tmp" "$HOME/.bash_completion" count=$((count + removed)) else rm -f "$tmp" fi fi ok "Removed $count items (tier 1)" } remove_tier2() { local cfg="$HOME/.config/linux_post_install" local count=0 if [ -d "$cfg" ]; then local f while IFS= read -r f; do rm -f "$f" && count=$((count+1)) done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null) # Remove empty directory tree rmdir "$cfg/schedule.d" 2>/dev/null || true rmdir "$cfg" 2>/dev/null || true fi ok "Removed $count items (tier 2)" } remove_tier3() { local data="$HOME/.local/share/linux_post_install" local count=0 if [ -d "$data" ]; then # Remove files first local f while IFS= read -r f; do rm -f "$f" && count=$((count+1)) done < <(find "$data" -maxdepth 2 -type f 2>/dev/null) # Remove directories bottom-up while IFS= read -r d; do rmdir "$d" 2>/dev/null && count=$((count+1)) || true done < <(find "$data" -mindepth 1 -depth -type d 2>/dev/null) # Remove top-level directory if empty rmdir "$data" 2>/dev/null || true fi ok "Removed $count items (tier 3)" } # ── Main ──────────────────────────────────────────────────────── main() { section "pos system uninstall" # Scan all tiers local scan1 scan2 scan3 scan1="$(scan_tier1)" scan2="$(scan_tier2)" scan3="$(scan_tier3)" # Display the plan display_plan "$scan1" "$scan2" "$scan3" # Nothing to do at all? if [ -z "$scan1" ] && [ -z "$scan2" ] && [ -z "$scan3" ]; then ok "Nothing to remove — pos toolkit does not appear to be installed." return 0 fi # ── Tier 1: always remove (confirm unless --yes) ── if [ -n "$scan1" ]; then if [ "$YES_MODE" -eq 1 ]; then log "Removing tier 1 items (--yes)..." else confirm "Remove tier 1 items (binaries, services, shell integration)?" || return 0 fi remove_tier1 fi # ── Tier 2: config files ── if [ -n "$scan2" ]; then if [ "$DEL_CONFIG" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then confirm "Also remove config files (tier 2)?" && DEL_CONFIG=1 || true fi [ "$DEL_CONFIG" -eq 1 ] && remove_tier2 fi # ── Tier 3: session/log data ── if [ -n "$scan3" ]; then if [ "$DEL_DATA" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then confirm "Also remove session/log data (tier 3)?" && DEL_DATA=1 || true fi [ "$DEL_DATA" -eq 1 ] && remove_tier3 fi echo ok "Uninstall complete." echo " The git repo was NOT removed — delete it manually if desired." echo " Restart your shell or run: source ~/.bashrc" } main "$@"