- Drop sender 'config'/'config set' subcommand — redundant with 'pos config telegram' (same POS_CONFIG registry: masked token input/display, chat-id validation, chmod 600); sender/listener error hints now point there. - Listener: load_map now uses a \x1f delimiter, so mapped /commands containing '|' are no longer truncated at the first pipe. - entertainment send: forward extra plugin args (previously dropped); pass '--' before the message so leading '-' plugin output isn't parsed as an option. - write_config_key/cfg_write: replace unescaped sed writes with grep-v+append so values with &, |, \ round-trip safely. - sync_systemd: daemon-reload after removing timer units. - config-ui: 'digits' validation accepts negative group/supergroup chat ids.
25 KiB
pos CLI Reference
pos is the unified command-line interface installed to /usr/local/bin/. Every tool is a small script in bin/ with a pos-<category>-<command> name. This document explains the dispatcher and every command.
The dispatcher — bin/pos
Purpose: turn pos <category> <command> [args] into a call to the matching pos-* script.
How it works
pos scans its own directory for executable pos-* files and tries variable-length argument matching, longest first. For pos docker compose up jellyfin:
tries pos-docker-compose-up-jellyfin (not found)
tries pos-docker-compose-up (not found)
tries pos-docker-compose (found) → runs with args "up jellyfin"
pos help <full command> runs that tool's --help (e.g. pos help communication telegram, pos help docker compose — the words are joined with dashes). pos <category> and pos <category> --help list that category's subcommands (derived from bin/pos-<category>-* filenames, no script execution). Running pos with no args prints the built-in usage text (which doubles as the category cheat-sheet).
Logging behavior
Every non-interactive pos invocation logs to ~/.local/share/linux_post_install/logs/:
- Per-command files:
YYYYMMDD_HHMMSS_pos_<args>.log(full stdout + stderr). pos.log: one line per invocation — command, log file, exit code.- Interactive commands (
pos system firewall,pos media mp4,pos system backup) only log the invocation, not their output.
Commands
network
| Command | File | Purpose | Configuration |
|---|---|---|---|
pos network ip |
bin/pos-network-ip |
Show interfaces, default route, public IP + location | None. Public IP via https://ifconfig.me; location via ip-api.com (5s timeouts) |
pos network checkport <ip:port> |
bin/pos-network-checkport |
Check if a TCP port is open | None. Uses /dev/tcp with a 2s timeout; exit 0/1 via OPEN/CLOSED |
pos network scan <cidr> [--full] [--retries N] |
bin/pos-network-scan |
Two-phase nmap scan | See below |
pos network hotspot [cmd] |
bin/pos-network-hotspot |
Wi-Fi hotspot via create_ap (CLI) or wihotspot-gui (GUI) |
Uses the precompiled binaries from x64_bin/; see below |
pos network scan in detail:
- Phase 1 — fast host discovery (
nmap -sn -T5), prints the live host list. - Phase 2 (only with
--full) — service/version scan (-sV -sC), plus OS detection and NSE scripts if run with privileges; shows ports, OS, SSH host keys, HTTP titles, NetBIOS/SMB info. - Accepts a bare IP (treated as
/32) or a CIDR. - Auto-raises to
sudo nmapwhen possible (root, passwordless sudo, or an interactive terminal with--full). --retries Ntunes discovery retries (default 1).
pos network hotspot in detail:
Backed by the precompiled binaries shipped in x64_bin/ (see SCRIPTS.md → x64_bin/). Needs root for the CLI commands (uses sudo):
| Command | Behavior |
|---|---|
pos network hotspot |
Launches the wihotspot-gui (GTK3 GUI) |
pos network hotspot start <wifi-iface> [<internet-iface>] <ssid> [<passphrase>] |
Asks whether to run in the background; y starts create_ap --daemon (logs to /var/log/linux_post_install_hotspot.log), n runs in the foreground (blocks until Ctrl+C) |
pos network hotspot start --foreground <wifi-iface> [<internet-iface>] <ssid> [<passphrase>] |
Skips the prompt, runs in the foreground |
pos network hotspot stop [<id>] |
Stops the running access point via create_ap --stop; <id> is an interface name or PID, auto-detected if omitted |
pos network hotspot status |
Runs create_ap --list-running |
docker
| Command | File | Purpose | Configuration |
|---|---|---|---|
pos docker ps |
bin/pos-docker-ps |
Enhanced container list: name, image, health, uptime, IPs, ports, ID, plus a healthy/unhealthy summary | None. Requires Docker + Python 3 |
pos docker health |
bin/pos-docker-health |
One-glance health dashboard; exits 1 if any container is unhealthy | None. Checks all containers including stopped ones |
pos docker compose … |
bin/pos-docker-compose |
ScaleTail service manager | See Docker Compose / ScaleTail below |
Docker Compose / ScaleTail
pos docker compose ls — list available ScaleTail service templates.
pos docker compose installed — list deployed services under $SERVICES_BASE.
pos docker compose up <service> — deploy a service:
- If not yet deployed, creates
$SERVICES_BASE/<service>/withconfig/anddata/, copies the template'scompose.yaml. - If no
.envexists, copies the template's.env(or writes a default) and fills in your global config values (TS_AUTHKEY,TZ,DNS_SERVER). - If
TS_AUTHKEYis still empty, prompts for it. - Offers to edit
.envbefore starting (default yes on first deploy). - Runs
docker compose up -d.
pos docker compose down/restart/logs <service> — stop, restart, or tail logs of a deployment.
pos docker compose update — git pull the ScaleTail templates, then refresh the compose.yaml of every deployed service. Per-service .env files are never touched.
pos docker compose config [show] — show the global config file and SERVICES_BASE.
pos docker compose config set KEY=VALUE — set/update a global default in ~/.config/linux_post_install/compose.env.
pos docker compose config edit — open the global config in $EDITOR (creates a default file first).
Configuration (three layers, most specific wins):
| Layer | File | Notes |
|---|---|---|
| Template defaults | /usr/local/share/linux_post_install/scale-tail/services/<name>/.env |
Read-only |
| Global config | ~/.config/linux_post_install/compose.env |
Edited via config set / config edit |
| Per-service | $SERVICES_BASE/<service>/.env |
Created on first up, never overwritten |
Global config keys:
| Key | Required | Default | Purpose |
|---|---|---|---|
TS_AUTHKEY |
yes | — | Tailscale auth key for the sidecar |
TZ |
no | Europe/Amsterdam |
Service timezone |
DNS_SERVER |
no | 9.9.9.9 |
DNS server |
SERVICES_BASE |
no | /srv |
Deployment root |
Docker vbox
File: bin/pos-docker-vbox
Purpose: manage disposable Docker containers as lightweight "VMs". Each container gets a bind-mounted host directory so files persist after the container is removed. Containers carry the label linux_post_install.vbox=true.
| Command | Behavior |
|---|---|
pos docker vbox create <name> [image] [--dir <path>] |
Creates a container from ubuntu:22.04 (or the given image), bind-mounting ~/<name> (or --dir, or . for cwd) as the working directory; prompts to enter immediately |
pos docker vbox enter <name> |
Shell into the container (auto-starts it if stopped); detects the working dir from the container mounts |
pos docker vbox start/stop/rm <name> |
Start, stop, or force-remove the container |
pos docker vbox ls |
List vbox containers only (label filter) |
The standalone vbox command still works and forwards to pos docker vbox (see Legacy wrappers).
media
| Command | File | Purpose | Configuration |
|---|---|---|---|
pos media mp3 <url> |
bin/pos-media-mp3 |
Download audio as MP3 via yt-dlp, with thumbnail + metadata | Output to ~/Music/%(title)s.%(ext)s, --audio-quality 0 |
pos media mp4 <url> |
bin/pos-media-mp4 |
Download video via yt-dlp with interactive format selection | Lists formats (yt-dlp -F), asks for a format ID, saves to ~/Videos/ |
system
| Command | File | Purpose | Configuration |
|---|---|---|---|
sudo pos system firewall |
bin/pos-system-firewall |
Interactive UFW ("UFW POWER") menu: add/delete rules, status, enable/disable/reset, default policies | Must run as root. Every command is previewed and confirmed before execution; supports --dry-run; keeps a history of executed commands. Executed mutating changes are announced via lib/notify.sh |
pos system backup <folder-path> |
bin/pos-system-backup |
Create a gpg-encrypted (AES-256) tar.gz snapshot of a folder and verify it |
Prompts twice for a password (never stored). Uses sudo tar; needs gnupg (in preinstall.sh PACKAGES). Artifact <name>_<date>.tar.gz.gpg in the current directory, chmod 600. Success/failure are announced via lib/notify.sh |
pos system backup --service |
bin/pos-system-backup |
Lists folders under /srv and ~/srv, lets you pick one, then runs the same backup |
Roots via BACKUP_SERVICE_ROOTS (space-separated, default /srv $HOME/srv) or ~/.config/linux_post_install/system.env |
pos system health [--send] [--markdown] |
bin/pos-system-health |
Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | --send/--markdown are notification-only: they send the summary via lib/notify.sh to every platform in NOTIFY_PLATFORM and do NOT print the dashboard (so wrappers like the Telegram listener don't echo it back — pair with the listener's @quiet marker). HEALTH_BACKUP_MAX_AGE_DAYS (default 2) and BACKUP_SERVICE_ROOTS come from ~/.config/linux_post_install/system.env; --help shows the effective values. Platform list from ~/.config/linux_post_install/notify.env |
pos system nfs-server <cmd> |
bin/pos-system-nfs-server |
Manage the NFS kernel server: status, share <path> [client], unshare <path>, list, reload, enable, disable |
Requires nfs-kernel-server (added to preinstall.sh PACKAGES). Exports live in /etc/exports; share is idempotent (replaces any existing line for the path) and runs exportfs -ra. Default client *(rw,sync,no_subtree_check) — the tool warns you to restrict it; help prints Tailscale CGNAT (100.64.0.0/10), WireGuard (10.10.0.0/24) and LAN examples. Mutating commands announce via lib/notify.sh |
pos system nfs-client <cmd> |
bin/pos-system-nfs-client |
Mount and manage NFS shares: mount <server:export> <local-dir>, unmount <local-dir>, list, persist <server:export> <local-dir>, unpersist <local-dir> |
Requires nfs-common (added to preinstall.sh PACKAGES). persist writes a systemd .mount unit (systemd-escape --path --suffix=mount) with After=network-online.target / Wants=network-online.target — mounts only once all interfaces are up, no fstab edits to break boot — then daemon-reload + enable --now. unpersist stops/disables/removes the unit. mount/persist announce via lib/notify.sh |
systemd/pos-health.service + systemd/pos-health.timer run pos system health --send --markdown daily at 08:00 as the installing user. postinstall.sh enables the timer automatically once ~/.config/linux_post_install/telegram.env exists — re-run postinstall after configuring a notify platform to pick it up. The service also loads system.env + notify.env via EnvironmentFile=.
ssh
| Command | File | Purpose | Configuration |
|---|---|---|---|
pos ssh load-keys |
bin/pos-ssh-load-keys |
Load all ~/.ssh/id_* private keys into the ssh-agent |
Uses SSH_AUTH_SOCK (default /run/ssh-agent/socket, provided by ssh-agent.service); skips .pub, known_hosts, authorized_keys, config; validates keys before adding |
usb
File: bin/pos-usb-server
Purpose: control the USB Redirector server (usbsrv) — share local USB devices over the network and manage connected clients. Requires usbsrv (manual install from incentivespro.com — not in PACKAGES).
| Command | Behavior |
|---|---|
pos usb server --ls |
List host USB devices and connected clients |
pos usb server --ls-shared |
List shared or in-use devices only |
pos usb server --share [dev-id] [client-id] |
Share a device and connect it to a client; interactive picker when IDs are omitted (-share + -connect-to CLIENT-DEV) |
pos usb server --unshare [dev-id] |
Stop sharing a device |
pos usb server --auto-share on|off |
Toggle automatic sharing of new devices |
pos usb server --callback [addr:port] |
Create a callback connection to a client |
pos usb server --close-callback [target|all] |
Close a client callback |
pos usb server --auto-connect on|off [client] |
Toggle remote auto-connect for a client |
pos usb server --disconnect [dev-id|all] |
Disconnect a device from its clients |
pos usb server --nickname [dev-id] [nick] |
Set a device nickname (empty nick removes it) |
pos usb server --timeout [dev-id] [sec] |
Set device inactivity timeout (0 disables) |
pos usb server --port [num] |
Set the TCP port (restart server to apply) |
pos usb server --info / --version |
Show server info / version |
Subcommands that need input prompt interactively when args are omitted.
communication
| Command | File | Purpose | Configuration |
|---|---|---|---|
pos communication telegram sender send "text" |
bin/pos-communication-telegram-sender |
Send a message, link, or media file (auto-detects the type) to a Telegram chat via the Bot API | Token + chat ID from ~/.config/linux_post_install/telegram.env (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, chmod 600). Precedence: --token/--chat-id flags > env > config file |
pos communication telegram listener |
bin/pos-communication-telegram-listener |
Telegram bot listener: map /command → bash commands and run them from chat; interactive editor for the map |
Same telegram.env (the bot is the owner, TELEGRAM_CHAT_ID). Map lives in ~/.config/linux_post_install/telegram_commands.env (/cmd=bash command lines, chmod 600) |
pos communication telegram sender in detail:
| Command | Behavior |
|---|---|
pos communication telegram sender send "text" |
POSTs sendMessage to the Bot API (60s timeout); prints [+] message sent to chat <id> or fails with a nonzero exit |
pos communication telegram sender send <value> |
Auto-detects the type when --type is omitted: existing file → file (except .webp → sticker, .gif → animation, images → photo, video/audio/voice extensions → their type), value starting with http:///https:///www. → link, otherwise message |
pos communication telegram sender send <path> --type file [--caption "…"] |
Uploads a file as a sendDocument via multipart (document=@path); --caption adds a caption. Path must exist and be readable |
pos communication telegram sender send <path> [--caption "…"] |
Media uploads via their Bot API endpoint: --type photo → sendPhoto, video → sendVideo, audio → sendAudio, voice → sendVoice, animation → sendAnimation, sticker (.webp) → sendSticker (captions not supported for stickers) |
pos communication telegram sender send "url" --type link [--no-preview] |
Sends a link as a message (URLs auto-linkify); --no-preview adds disable_web_page_preview=true |
pos communication telegram sender send "text" --parse-mode <mode> |
Send with Telegram formatting; <mode> is plain (default), markdown, or html (passed as parse_mode to the API — also applies to captions). Markdown/HTML use raw Telegram syntax — unescaped characters may be rejected by the API (400) |
pos communication telegram sender send … --token <t> --chat-id <id> |
One-shot override of token/chat ID |
pos communication telegram sender test |
Sends a canned test message using the current config |
send option validation: --caption is only valid with media types (file/photo/video/audio/voice/animation), --no-preview only with --type message/link, and --type only accepts message|file|link|sticker|photo|video|audio|voice|animation. An explicit --type always overrides auto-detection.
The bot token is a secret — it is stored only in ~/.config/linux_post_install/telegram.env and never in the repo. Edit TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID interactively with pos config telegram (masked input + display). Requires network access to api.telegram.org.
pos communication telegram listener in detail:
| Command | Behavior |
|---|---|
pos communication telegram listener |
Interactive editor for the /command → bash map (add / edit / remove / test / quit); test-runs run bash -n first and may execute the command live |
pos communication telegram listener --status |
Shows service state (running/autostart), config + map file paths, and the mapped commands |
pos communication telegram listener --enable |
Installs + starts a systemd user service (pos-telegram-listener.service); the daemon polls getUpdates and runs mapped commands |
pos communication telegram listener --disable |
Stops, disables, and removes the service |
pos communication telegram listener --sync-commands |
Push the mapped /commands to the bot's / menu (setMyCommands) — also run automatically after every map edit, on --enable, and at daemon start |
pos communication telegram listener --run |
Run the polling loop in the foreground (what the service executes) |
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (TELEGRAM_CHAT_ID); anyone else's message is ignored. /help lists mapped commands; an unmapped command replies "Unknown command". Commands run as your user via timeout 60 bash -c "…" (stdout + stderr are replied, truncated to ~3800 chars; empty output → OK), so sudo inside them needs a NOPASSWD rule. A map value prefixed with @quiet runs the command but does NOT reply — for commands that already send their own notification (e.g. /status=@quiet pos system health --send), avoiding a double message. --enable warns if linger is off — the service stops when you log out unless you run sudo loginctl enable-linger $(whoami).
Map entries may carry an optional description shown in the bot's / menu: /cmd::short description=bash command (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via setMyCommands, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase [a-z0-9_] names (1–32 chars) — commands like /Status or /my-cmd are skipped from the menu with a warning but still resolve when typed.
entertainment
File: bin/pos-entertainment-send
Purpose: run a public-API plugin and send its output to Telegram by default. Plugins are standalone scripts in entertainment/ that fetch a public API and print the message to stdout — that stdout is what gets sent.
| Command | Behavior |
|---|---|
pos entertainment send |
List available plugins + usage |
pos entertainment send <plugin> [--print] [--markdown] [args…] |
Run the plugin, send its output to Telegram (silent) |
pos entertainment send <plugin> --print |
Print the output locally; do not send |
pos entertainment send <plugin> --markdown |
Send with --parse-mode markdown (via pos communication telegram sender) |
pos entertainment config |
Show the config file (~/.config/linux_post_install/entertainment.env) |
pos entertainment config set KEY=VALUE… |
Set keys (any UPPER_SNAKE key; warns if no installed plugin uses it) and re-sync the schedule |
pos entertainment enable <plugin> [interval] |
Add plugin to ENABLED + schedule it as a systemd user timer |
pos entertainment disable <plugin> |
Remove plugin from ENABLED + remove its scheduled job |
pos entertainment status |
Enabled plugins + scheduler + schedule state |
Plugin lookup order: $ENTERTAINMENT_DIR → repo entertainment/ → /usr/local/bin/ (installed by install.sh Phase 2, beside the runner). A plugin name matches the file name with or without the .sh suffix.
Plugins:
| Plugin | Source API | Config |
|---|---|---|
weather |
Open-Meteo (no API key) | ~/.config/linux_post_install/entertainment.env: WEATHER_LAT, WEATHER_LON (required), WEATHER_CITY (optional label) |
joke |
icanhazdadjoke.com (no API key) | None |
gold |
goldprice.dev (no API key, anonymous free tier) | None |
Config auto-install: postinstall.sh copies the repo's config/entertainment.env (a commented template showing each key's syntax) to ~/.config/linux_post_install/entertainment.env on install — but only if you haven't already created your own (no clobber), and prints the template so you can fill in your location. Fill in WEATHER_LAT/WEATHER_LON (and optionally WEATHER_CITY) to enable the weather plugin.
Adding a plugin: drop an executable script in entertainment/ (e.g. myfeed.sh) with a # POS_PLUGIN: <name> marker on line 3 — the runner lists and validates plugins by this marker, so non-plugin .sh files in the shared /usr/local/bin are ignored. The plugin must be non-interactive and print the message to stdout; errors go to stderr (exit nonzero). If it needs coordinates/tokens, read them from ~/.config/linux_post_install/entertainment.env (chmod 600, env precedence). No registration needed. Dependencies beyond curl/jq (both in preinstall.sh PACKAGES) should be guarded with command -v … || exit 1.
Declaring config keys (pattern): document every key the plugin reads with one # POS_KEYS: line right after # POS_PLUGIN: — KEY, a description, and (required)/(optional):
# POS_PLUGIN: myfeed
# POS_KEYS: MYFEED_URL <feed url> (required)
# POS_KEYS: MYFEED_TAG <filter tag> (optional)
The plugin itself still reads the keys as plain env vars (${MYFEED_URL:-}). The declaration is what pos entertainment config uses to print its Keys section and to decide whether config set warns about an undeclared key — add the line whenever a plugin gets a new config key.
Automation (auto-trigger): enable plugins on a schedule via the ENABLED key in the config — a comma-separated list of plugin, interval pairs:
ENABLED="weather, 5m gold, 1h joke, daily"
pos entertainment enable <plugin> [interval] appends/updates one entry and re-syncs; pos entertainment disable <plugin> removes it; pos entertainment config set ENABLED="…" replaces the whole list. Scheduling uses systemd user timers (requires a reachable user systemd manager):
- One user timer per enabled plugin (
~/.config/systemd/user/pos-entertainment-<plugin>.{service,timer}), runningpos entertainment send <plugin>as your user on that schedule (OnCalendar+Persistent=true).pos entertainment enablealso triessudo loginctl enable-linger $USERonce so timers fire without login.
The job runs as you, so it reads your $HOME configs (weather location, Telegram token) natively — no Environment=HOME= hacks.
Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or a raw OnCalendar=… spec. Default when omitted: daily.
pos entertainment status shows the enabled plugins, the scheduler, and each plugin's interval + next fire time (systemctl --user list-timers).
Notes:
- The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure.
- Scheduling is per-user for the user who runs
enable; if you manage a different machine's user (e.g. viarunuser/sudo -u), run theenable/disablecommands as that user.
flags
Feature-flag management CLIs (see SCRIPTS.md → lib/flags.sh):
| Command | Purpose |
|---|---|
flag-reader |
List all flags + status (set: <name> / unset: <name>) |
flag-reader <name> |
Check one flag; exit 0 if set, 1 if not |
flag-reader --raw <name> |
Print only the stored value (script-friendly) |
flag-set <name> [value] |
Set a flag, optionally with a value (requires sudo) |
flag-clear <name> |
Unset a flag (requires sudo) |
Legacy wrappers
Thin 2-line scripts that exec pos … "$@". All of them still work:
| Wrapper | Forwards to |
|---|---|
wr-ip |
pos network ip |
wr-checkport |
pos network checkport |
wr-scan-ping |
pos network scan |
wr-docker |
pos docker |
wr-compose |
pos docker compose |
wr-ufw |
pos system firewall |
mp3 |
pos media mp3 |
mp4 |
pos media mp4 |
vbox |
pos docker vbox |
ssh-load-all |
pos ssh load-keys |