Files
Linux_post_install/bin/pos-system-uninstall
T
Your Name 73d15a26b4
gates / consistency-and-conventions (push) Successful in 19s
fix: register bank-lib.sh (and yt-lib.sh) in install/uninstall lib manifests
lib/bank-lib.sh was added for pos bank but never registered in install.sh's
phase-2 lib copy list, so it never reached /usr/local/bin and pos bank failed
after install. Also restored yt-lib.sh to POS_LIBS (pre-existing gap: uninstall
left it behind). Symmetry gate tests/t-uninstall-manifest.sh now passes.
2026-09-12 03:28:15 -04:00

519 lines
19 KiB
Bash
Executable File

#!/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 "$@"