#!/usr/bin/env bash set -euo pipefail self="$(cd "$(dirname "$0")" && pwd)" # ── Colors (auto-off when not a TTY) ─────────────────────────── if [ -t 1 ]; then BOLD=$(tput bold 2>/dev/null || true) CYAN=$(tput setaf 6 2>/dev/null || true) DIM=$(tput dim 2>/dev/null || true) RESET=$(tput sgr0 2>/dev/null || true) else BOLD=""; CYAN=""; DIM=""; RESET="" fi # ── Collect available commands ───────────────────────────────── _pos_commands() { local cmds=() local f for f in "$self"/pos-*; do [ -x "$f" ] || continue local name="${f##*/pos-}" cmds+=("$name") done echo "${cmds[*]}" } # ── Category list (sorted, filename-derived) ───────────────────── # Nested sub-tools (pos--- where pos-- exists) are # omitted from the cheat-sheet line — they are shown under their parent tool. _pos_category_list() { local -A cats=() local cmd cat f for f in "$self"/pos-*; do [ -x "$f" ] || continue cmd="${f##*/pos-}" cat="${cmd%%-*}" [ "$cat" = "$cmd" ] && continue cats["$cat"]+="${cmd#*-} " done local c subs joined s s2 top=() is_nested for c in $(printf '%s\n' "${!cats[@]}" | sort); do subs=(${cats[$c]}) top=() for s in "${subs[@]}"; do is_nested=0 for s2 in "${subs[@]}"; do [ "$s2" = "$s" ] && continue if [[ "$s" == "$s2-"* ]]; then is_nested=1; break; fi done [ "$is_nested" -eq 1 ] || top+=("$s") done joined="$(IFS='|'; echo "${top[*]}")" printf " %-14s%s\n" "$c" "$joined" done } # ── Category helpers ──────────────────────────────────────────── _pos_category_exists() { local cat="${1:-}" f [ -n "$cat" ] || return 1 for f in "$self"/pos-"$cat"-*; do [ -x "$f" ] && return 0 done return 1 } _pos_category_help() { local cat="$1" f local files=() s d for f in "$self"/pos-"$cat"-*; do [ -x "$f" ] || continue files+=("${f##*/pos-$cat-}") done mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u) local -A desc subcmds local sc for s in "${files[@]}"; do d="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)" [ -n "$d" ] && desc["$s"]="${d#*— }" sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)" [ -n "$sc" ] && subcmds["$s"]="$sc" done # Nested sub-tools: pos--- lists "b" under . local s2 extra for s in "${files[@]}"; do for s2 in "${files[@]}"; do [ "$s2" = "$s" ] && continue if [[ "$s2" == "$s-"* ]]; then extra="${s2#$s-}" case " ${subcmds[$s]:-} " in *" $extra "*) ;; *) subcmds["$s"]="${subcmds[$s]:-}${subcmds[$s]:+ }$extra" ;; esac fi done done echo "pos $cat — $cat tools" echo echo "USAGE" echo " pos $cat [args]" echo echo "COMMANDS" local is_nested c for s in "${files[@]}"; do is_nested=0 for s2 in "${files[@]}"; do [ "$s2" = "$s" ] && continue if [[ "$s" == "$s2-"* ]]; then is_nested=1; break; fi done [ "$is_nested" -eq 1 ] && continue printf ' %-28s%s\n' "$s" "${desc[$s]:-}" for c in ${subcmds[$s]:-}; do printf ' %s %s\n' "$s" "$c" done done echo echo "Run 'pos $cat --help' for details on a command." exit 0 } # ── Help text ────────────────────────────────────────────────── usage() { local cats cats="$(_pos_category_list)" cat < [args] CATEGORIES $cats EXAMPLES pos network ip Show interfaces, routes, public IP pos network checkport 10.0.0.1:80 Check if a TCP port is open pos network scan 192.168.1.0/24 Fast parallel ping sweep pos network hotspot Launch wihotspot-gui pos docker ps List containers (health, IPs, ports) pos docker compose ls List available ScaleTail services pos docker compose up jellyfin Deploy a service with Tailscale pos docker health One-glance health dashboard pos media mp3 Download audio as MP3 pos media mp4 Download video as MP4 pos system firewall Interactive UFW manager pos system backup /srv/project Encrypted (AES-256) folder snapshot pos system backup --service Pick a folder from /srv or ~/srv pos system nfs-server share /mnt/hdd Share a folder via NFS pos system nfs-client persist 10.0.0.5:/srv/data /mnt/nfs/data Persistent NFS mount (systemd) pos ssh load-keys Load all SSH keys into agent pos usb server --ls List USB devices + connected clients pos communication telegram sender send "Backup done" Send a Telegram message pos communication telegram listener Edit the /command → bash map pos communication telegram listener --enable Install the Telegram bot listener pos entertainment send weather Fetch Open-Meteo weather → Telegram pos entertainment send joke --print Preview a joke locally (no send) pos entertainment enable weather 5m Auto-send weather to Telegram every 5 min pos entertainment status Show enabled plugins + next run pos config telegram Edit Telegram config interactively pos config Pick a config scope to edit pos docker vbox create lab1 Create disposable Docker VM pos docker vbox create lab1 --dir . Create VM using current directory pos docker vbox enter lab1 Shell into a Docker VM pos docker vbox ls List Docker VMs HELP pos --help Show a category's commands pos help Show help for a command pos --help Show this help LEGACY WRAPPERS wr-ip, wr-checkport, wr-scan-ping, wr-docker, wr-compose, wr-ufw, mp3, mp4, vbox, ssh-load-all still work and forward to pos. EOF exit 0 } # ── Handle --help / -h ───────────────────────────────────────── case "${1:-}" in -h|--help|"") usage ;; esac # ── pos help ───────────────────────────────────────── if [ "${1:-}" = "help" ]; then shift [ $# -eq 0 ] && usage cmd="pos-$*" cmd="${cmd// /-}" if command -v "$cmd" &>/dev/null; then exec "$cmd" --help fi [ -x "$self/$cmd" ] && exec "$self/$cmd" --help echo "pos: unknown command '$*'" >&2 echo "Run 'pos --help' to see available commands." >&2 exit 1 fi # ── Dispatch ─────────────────────────────────────────────────── args=("$@") n=${#args[@]} # ── Category help: pos [--help] ───────────────────── if _pos_category_exists "${args[0]:-}"; then if [ "$n" -eq 1 ] || { [ "$n" -eq 2 ] && { [ "${args[1]}" = "-h" ] || [ "${args[1]}" = "--help" ]; }; }; then _pos_category_help "${args[0]}" fi fi # ── Logging setup ────────────────────────────────────────────── LOG_DIR="$HOME/.local/share/linux_post_install/logs" mkdir -p "$LOG_DIR" 2>/dev/null || true CMD_SAFE=$(echo "${args[*]}" | tr ' /' '__') LOG_FILE="$LOG_DIR/$(date +%Y%m%d_%H%M%S)_pos_${CMD_SAFE}.log" MAIN_LOG="$LOG_DIR/pos.log" log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; } # Commands that read from stdin interactively — only log invocation INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener config" for ((i=n-1; i>=0; i--)); do cmd="pos" for ((j=0; j<=i; j++)); do cmd="${cmd}-${args[$j]}" done resolved="" if command -v "$cmd" &>/dev/null; then resolved="$cmd" elif [ -x "$self/$cmd" ]; then resolved="$self/$cmd" fi [ -z "$resolved" ] && continue sub="${cmd#pos-}" if [[ " $INTERACTIVE_CMDS " == *" $sub "* ]]; then # Interactive: log invocation only, then exec normally log_cmd "pos $*" "" 0 exec "$resolved" "${args[@]:i+1}" else # Non-interactive: capture full output "$resolved" "${args[@]:i+1}" 2>&1 | tee "$LOG_FILE" rc=${PIPESTATUS[0]} log_cmd "pos $*" "$LOG_FILE" "$rc" exit "$rc" fi done echo "pos: unknown command '${args[0]}'" >&2 echo "Run 'pos --help' to see available commands." >&2 exit 1