#!/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) ───────────────────── _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 line subs joined for c in $(printf '%s\n' "${!cats[@]}" | sort); do subs=(${cats[$c]}) joined="$(IFS='|'; echo "${subs[*]}")" 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" sub subs=() local f for f in "$self"/pos-"$cat"-*; do [ -x "$f" ] || continue subs+=("${f##*/pos-$cat-}") done echo "pos $cat — $cat tools" echo echo "USAGE" echo " pos $cat [args]" echo echo "COMMANDS" printf " %s\n" "${subs[@]}" 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 ssh load-keys Load all SSH keys into agent pos usb server --ls List USB devices + connected clients pos communication telegram --send "Backup done" Send a Telegram message 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" 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