296 lines
11 KiB
Bash
Executable File
296 lines
11 KiB
Bash
Executable File
#!/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-<cat>-<a>-<b> where pos-<cat>-<a> 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-<cat>-<a>-<b> lists "b" under <a>.
|
|
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 <command> [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 <command> --help' for details on a command."
|
|
exit 0
|
|
}
|
|
|
|
# ── Help text ──────────────────────────────────────────────────
|
|
usage() {
|
|
local cats
|
|
cats="$(_pos_category_list)"
|
|
cat <<EOF
|
|
pos — Personal OS Toolkit
|
|
|
|
A unified CLI for network, docker, media, system, SSH,
|
|
USB, communication, and entertainment tools on Debian/Ubuntu.
|
|
|
|
USAGE
|
|
pos <category> <command> [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 <url> 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 <url> Download audio as MP3
|
|
pos media mp4 <url> 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 <category> --help Show a category's commands
|
|
pos help <command> 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 <command> ─────────────────────────────────────────
|
|
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 <category> [--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="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai-gemini 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
|