#!/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" s s2 extra d sc deps # Lazy registry load — plain dispatch paths never pay the scan cost. source "$self/../lib/registry.sh" 2>/dev/null || source "$self/registry.sh" reg_scan "$self" local files=() local t while IFS= read -r t; do [ -n "$t" ] || continue files+=("${t#"$cat"-}") done < <(reg_tools_in "$cat") mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u) local -A desc subcmds _deps for s in "${files[@]}"; do d="$(reg_lookup "$cat-$s" desc)" [ -n "$d" ] && desc["$s"]="$d" sc="$(reg_lookup "$cat-$s" subcmds)" [ -n "$sc" ] && subcmds["$s"]="$sc" deps="$(reg_lookup "$cat-$s" deps)" [ -n "$deps" ] && _deps["$s"]="$deps" 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]:-}" [ -n "${_deps[$s]:-}" ] && printf ' [deps: %s]\n' "${_deps[$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 network download add Enqueue an aria2 download (--tmux = live view) 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 docker stack Containers grouped by compose stack pos docker stack -a Include stopped/exited containers pos media mp3 Download audio as MP3 pos media mp4 Download video as MP4 pos media ytsync sync Incremental YouTube channel sync 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 share nfs server share /mnt/hdd Share a folder via NFS pos share nfs client persist 10.0.0.5:/srv/data /mnt/nfs/data Persistent NFS mount (systemd) pos share smb server share /mnt/hdd media --users bob Share a folder via SMB (Samba) pos share smb client persist //10.0.0.5/media /mnt/smb/media bob Persistent SMB mount (automount) pos ssh load-keys Load all SSH keys into agent pos share 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 communication matrix sender send "Backup done" Send a Matrix room message pos communication matrix sender login --user @me:example.org Fetch a Matrix access token pos communication matrix listener Edit the /command → bash map 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 system schedule list Show scheduled jobs + last results pos system schedule enable Start all job timers (per-job intervals) pos system schedule migrate Convert old event.env rules to jobs pos ai gemini ask "Explain DNS in one line" Ask Google Gemini a one-shot question pos ai gemini chat Interactive multi-turn chat pos ai gemini models List available Gemini models pos config telegram Edit Telegram config interactively pos config Pick a config scope to edit pos tree Show the command tree (categories + subcommands) 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="bank docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-yt-mp4 media-sync system-backup system-uninstall share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter ai-llamacpp ai-alias system-alias system-schedule entertainment-config 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