feat: pos config — interactive config editor driven by the POS_CONFIG registry
Add bin/pos-config + lib/config-ui.sh: reads the '# POS_CONFIG:' headers across tools into a unified runtime config (~/.config/linux_post_install/ <scope>.env, chmod 600). Secret masking with show/hide, digits:/num:/url: validation, '-' clears, blank keeps; '*plugins' expands entertainment plugin vars from entertainment-lib.sh. pos config with no scope opens a picker (scope list when not a TTY); completion offers scopes from the registry at runtime. gen-docs.sh now handles category-less tools (pos-config) instead of emitting pos-config-config; fix set -e/pipefail abort that truncated the header registry mid-read. install.sh ships config-ui.sh.
This commit is contained in:
@@ -16,6 +16,8 @@ summary (newest last).
|
||||
|
||||
## Done
|
||||
|
||||
- **2026-08-09** — `pos config <scope>` interactive config editor: reads the `# POS_CONFIG:` registry across tools into a single runtime config (`~/.config/linux_post_install/*.env`, one file per scope, chmod 600); secret masking with show/hide toggle, `digits:`/`num:`/`url:` validation, `-` to clear, blank keeps; `*plugins` marker expands plugin vars (entertainment) from `entertainment-lib.sh`; gen-docs now handles category-less tools (`pos-config`), fixed a `set -e`+`pipefail` bug that truncated the header registry.
|
||||
|
||||
- **2026-08-09** — `pos-communication-telegram` → `pos-communication-telegram-sender`: one canonical `send` (dropped the legacy `--send` flag, which duplicated the `send` subcommand in completion). `pos communication telegram <TAB>` now completes to just `sender listener`. `lib/notify.sh` maps platform `telegram` → `telegram-sender` via `notify_sender_name()`; entertainment-send + health `--send` check updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed).
|
||||
|
||||
- **2026-08-09** — Structure/convention audit fix: `--dry-run` now truly dry (`spawn()` honors `DRY_RUN`, install.sh exports it to child phases, postinstall mutations run-wrapped); `gen-docs.sh` no longer chmods regenerated files to 0600; `make check` now syntax-checks apps/entertainment/features/templates; `.gitignore` protects `config/authorized_keys` + `config/rclone.conf`; honest `--send` confirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS).
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–179 |
|
||||
| ## 3. Installation Flow | 180–232 |
|
||||
| ## 4. The `pos` CLI System | 233–293 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 294–325 |
|
||||
| ## 6. Docker Compose / ScaleTail | 326–368 |
|
||||
| ## 7. Optional Apps (`apps/`) | 369–398 |
|
||||
| ## 8. Entertainment Module | 399–412 |
|
||||
| ## 9. Systemd Services | 413–425 |
|
||||
| ## 10. Configuration Files | 426–451 |
|
||||
| ## 11. Coding Conventions | 452–484 |
|
||||
| ## 12. Development Workflow | 485–536 |
|
||||
| ## 13. Key File Quick Reference | 537–583 |
|
||||
| ## 14. Common Tasks for Agents | 584–608 |
|
||||
| ## 2. Directory Structure | 44–180 |
|
||||
| ## 3. Installation Flow | 181–233 |
|
||||
| ## 4. The `pos` CLI System | 234–295 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 296–327 |
|
||||
| ## 6. Docker Compose / ScaleTail | 328–370 |
|
||||
| ## 7. Optional Apps (`apps/`) | 371–400 |
|
||||
| ## 8. Entertainment Module | 401–414 |
|
||||
| ## 9. Systemd Services | 415–427 |
|
||||
| ## 10. Configuration Files | 428–453 |
|
||||
| ## 11. Coding Conventions | 454–486 |
|
||||
| ## 12. Development Workflow | 487–538 |
|
||||
| ## 13. Key File Quick Reference | 539–586 |
|
||||
| ## 14. Common Tasks for Agents | 587–611 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -60,6 +60,7 @@ Linux_post_install/
|
||||
<!-- GEN:START tree -->
|
||||
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
||||
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test, config set)
|
||||
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
||||
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||
@@ -251,6 +252,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
<!-- GEN:START dispatch -->
|
||||
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) |
|
||||
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
@@ -550,14 +552,15 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/flag-clear` | 21 | Unset a flag |
|
||||
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
||||
<!-- GEN:START filetable -->
|
||||
| `bin/pos` | 267 | CLI dispatcher with smart arg matching + logging + category help |
|
||||
| `bin/pos` | 270 | CLI dispatcher with smart arg matching + logging + category help |
|
||||
| `bin/pos-communication-telegram-listener` | 433 | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| `bin/pos-communication-telegram-sender` | 269 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) |
|
||||
| `bin/pos-docker-compose` | 365 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| `bin/pos-communication-telegram-sender` | 270 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| `bin/pos-docker-ps` | 128 | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
| `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||
| `bin/pos-entertainment-config` | 98 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
|
||||
| `bin/pos-entertainment-config` | 99 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
|
||||
| `bin/pos-entertainment-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) |
|
||||
| `bin/pos-entertainment-enable` | 49 | Enable an auto-trigger for a plugin on a schedule |
|
||||
| `bin/pos-entertainment-send` | 93 | Run a public-API plugin and send its output via Telegram (default sender) |
|
||||
@@ -571,11 +574,11 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||
| `bin/pos-system-backup` | 125 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-firewall` | 291 | Interactive UFW management |
|
||||
| `bin/pos-system-health` | 273 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| `bin/pos-system-health` | 275 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| `bin/pos-system-nfs-client` | 138 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-system-nfs-server` | 134 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
|
||||
| `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| `completions/pos.bash` | 250 | Dynamic bash completion |
|
||||
| `completions/pos.bash` | 267 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
|
||||
@@ -173,6 +173,9 @@ EXAMPLES
|
||||
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
|
||||
@@ -231,7 +234,7 @@ 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"
|
||||
INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
||||
# POS: communication telegram-sender — Send Telegram messages/files/links/stickers via Bot API (send, test, config set)
|
||||
# POS_FLAGS: --type --caption --parse-mode --no-preview --token --chat-id --markdown
|
||||
# POS_SUBCMDS: send test config
|
||||
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: config — Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos config [<scope>]
|
||||
|
||||
Interactive editor for the tools' runtime config. Every tool exposes its
|
||||
configuration by declaring a "# POS_CONFIG:" header; pos config reads those
|
||||
at runtime — the UI knows nothing about the variables themselves.
|
||||
|
||||
pos config Scope picker (on a TTY), otherwise the scope list
|
||||
pos config <scope> Edit that scope's variables
|
||||
pos config --help This help
|
||||
|
||||
Scopes:
|
||||
$(cfg_scopes | sed 's/^/ /')
|
||||
|
||||
How a tool registers a scope (one "# POS_CONFIG:" header per scope):
|
||||
|
||||
# POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<description> | ... | *plugins
|
||||
|
||||
<env-file> config file under ~/.config/linux_post_install/ (chmod 600)
|
||||
<flags> secret (masked display + hidden input) | digits | num | float
|
||||
*plugins also list the keys declared by installed plugins' # POS_KEYS:
|
||||
headers (the entertainment scope's keys are dynamic)
|
||||
|
||||
Editing rules: Enter keeps the current value, "-" clears it, q quits.
|
||||
Values can also be set non-interactively with each tool's 'config set KEY=VALUE'.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
pick_scope() {
|
||||
local scopes=() s i env
|
||||
while IFS= read -r s; do
|
||||
if [ -n "$s" ]; then
|
||||
scopes+=("$s")
|
||||
fi
|
||||
done < <(cfg_scopes)
|
||||
if [ ${#scopes[@]} -eq 0 ]; then
|
||||
err "no config scopes found — no tool declares a '# POS_CONFIG:' header"
|
||||
fi
|
||||
echo
|
||||
echo "pos config — pick a scope"
|
||||
echo "--------------------------"
|
||||
for ((i = 1; i <= ${#scopes[@]}; i++)); do
|
||||
env="$(cfg_scope_envfile "${scopes[$((i - 1))]}" || true)"
|
||||
printf ' %2d) %-16s %s\n' "$i" "${scopes[$((i - 1))]}" "${env:-}"
|
||||
done
|
||||
echo
|
||||
local idx
|
||||
read -rp "Scope number: " idx || { echo; exit 1; }
|
||||
if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#scopes[@]} )); then
|
||||
err "invalid number '$idx'"
|
||||
fi
|
||||
cfg_ui "${scopes[$((idx - 1))]}"
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
if [ -t 0 ]; then
|
||||
pick_scope
|
||||
else
|
||||
usage
|
||||
fi
|
||||
else
|
||||
scope="${1:-}"
|
||||
case " $(cfg_scopes | tr '\n' ' ') " in
|
||||
*" $scope "*) cfg_ui "$scope" ;;
|
||||
*) err "unknown config scope '$scope' (try: $(cfg_scopes | tr '\n' ' '))" ;;
|
||||
esac
|
||||
fi
|
||||
@@ -2,6 +2,7 @@
|
||||
set -euo pipefail
|
||||
# POS: docker compose — Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||
# POS_SUBCMDS: ls installed up down restart logs update config
|
||||
# POS_CONFIG: compose | compose.env | TS_AUTHKEY=secret:Tailscale auth key for the sidecar | TZ=:Service timezone (default Europe/Amsterdam) | DNS_SERVER=:Custom DNS server (default 9.9.9.9) | SERVICES_BASE=:Deployment root (default /srv) | TEST=:Test variable
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: entertainment config — Show or edit the entertainment config (ENABLED auto-trigger list, weather location)
|
||||
# POS_CONFIG: entertainment | entertainment.env | ENABLED=:Auto-trigger list (plugin, interval pairs) | *plugins
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/entertainment-lib.sh" 2>/dev/null || source "$(dirname "$0")/entertainment-lib.sh"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
set -euo pipefail
|
||||
# POS: system health — Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
|
||||
# POS_FLAGS: --send --markdown
|
||||
# POS_CONFIG: system | system.env | BACKUP_SERVICE_ROOTS=:Roots scanned by backup --service and the health backup-age check (default: /srv $HOME/srv) | HEALTH_BACKUP_MAX_AGE_DAYS=num:Max backup age in days before health warns (default 2)
|
||||
# POS_CONFIG: notify | notify.env | NOTIFY_PLATFORM=:Comma-separated notify platforms (default telegram)
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
|
||||
|
||||
+19
-2
@@ -74,7 +74,7 @@ _pos() {
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────
|
||||
_pos_complete_categories() {
|
||||
COMPREPLY=($(compgen -W "${!cat_cmds[*]}" -- "$cur"))
|
||||
COMPREPLY=($(compgen -W "${!cat_cmds[*]} config" -- "$cur"))
|
||||
}
|
||||
|
||||
_pos_complete_subcats() {
|
||||
@@ -158,13 +158,30 @@ _pos() {
|
||||
COMPREPLY=($(compgen -W "${out[*]}" -- "$cur"))
|
||||
}
|
||||
|
||||
# Dynamic scope completion for `pos config <TAB>` — greps the
|
||||
# "# POS_CONFIG:" headers in the tools dir (the config registry).
|
||||
_pos_config_scopes() {
|
||||
local scopes=() f line scope
|
||||
for f in "$pos_dir"/pos-*; do
|
||||
[ -x "$f" ] || continue
|
||||
while IFS= read -r line; do
|
||||
scope="$(sed 's/^# POS_CONFIG:[[:space:]]*//' <<<"$line" | cut -d'|' -f1 | tr -d ' ')"
|
||||
[ -n "$scope" ] && scopes+=("$scope")
|
||||
done < <(grep '^# POS_CONFIG:' "$f" 2>/dev/null)
|
||||
done
|
||||
COMPREPLY=($(compgen -W "$(printf '%s\n' "${scopes[@]}" | sort -u | tr '\n' ' ') --help" -- "$cur"))
|
||||
}
|
||||
|
||||
# ── Dispatch ───────────────────────────────────────────────
|
||||
case "${#words[@]}" in
|
||||
2)
|
||||
_pos_complete_categories
|
||||
;;
|
||||
3)
|
||||
_pos_complete_subcats
|
||||
case "${words[1]}" in
|
||||
config) _pos_config_scopes ;;
|
||||
*) _pos_complete_subcats ;;
|
||||
esac
|
||||
;;
|
||||
4)
|
||||
case "${words[1]}-${words[2]}" in
|
||||
|
||||
@@ -117,6 +117,7 @@ if should_run 2 scripts; then
|
||||
run sudo install -m 644 lib/flags.sh /usr/local/bin/flags.sh
|
||||
run sudo install -m 644 lib/notify.sh /usr/local/bin/notify.sh
|
||||
run sudo install -m 644 lib/entertainment-lib.sh /usr/local/bin/entertainment-lib.sh
|
||||
run sudo install -m 644 lib/config-ui.sh /usr/local/bin/config-ui.sh
|
||||
|
||||
# ── Entertainment plugins ────────────────────────────────
|
||||
# Installed into /usr/local/bin so the repo can be deleted afterwards.
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# lib/config-ui.sh — interactive, self-describing config UI for the pos tools.
|
||||
# Reads each tool's "# POS_CONFIG:" header — the config registry — and renders
|
||||
# a numbered menu per scope to view/edit values in the matching env file.
|
||||
# Self-contained: uses common.sh helpers when available, with guarded fallbacks
|
||||
# (mirrors lib/notify.sh). Sourced opt-in by bin/pos-config.
|
||||
#
|
||||
# Header grammar — one "# POS_CONFIG:" line per scope a tool exposes:
|
||||
# # POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<desc> | ... | *plugins
|
||||
# <env-file> basename of the config file under ~/.config/linux_post_install/
|
||||
# <flags> secret (masked display + stty -echo input) | digits | num | float
|
||||
# *plugins marker: also list every key declared by the installed
|
||||
# entertainment plugins' "# POS_KEYS:" headers (dynamic)
|
||||
# Example:
|
||||
# # POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token | TELEGRAM_CHAT_ID=digits:Numeric chat id
|
||||
#
|
||||
# Usage (opt-in):
|
||||
# source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
# cfg_scopes # list all declared scopes (deduped, sorted)
|
||||
# cfg_ui <scope> # interactive numbered-menu editor for one scope
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
|
||||
# common.sh helpers (guarded so the lib is safe if common.sh wasn't loaded)
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||
declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||
|
||||
_cfg_scope="" # scope being edited (drives the post-write hook)
|
||||
declare -A _cfg_seen=() # key dedupe registry for cfg_scope_keys
|
||||
|
||||
# ── tool directory ─────────────────────────────────────────────────
|
||||
# Repo layout: lib/config-ui.sh → tools live in ../bin.
|
||||
# Installed layout: /usr/local/bin/config-ui.sh → ../bin == /usr/local/bin.
|
||||
cfg_tools_dir() {
|
||||
local dir f found=0
|
||||
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" 2>/dev/null && pwd)"
|
||||
if [ -n "$dir" ]; then
|
||||
for f in "$dir"/pos-*; do
|
||||
[ -x "$f" ] && { found=1; break; }
|
||||
done
|
||||
fi
|
||||
[ "$found" -eq 1 ] && { echo "$dir"; return 0; }
|
||||
echo "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
}
|
||||
|
||||
# All "# POS_CONFIG:" header lines (prefix stripped), one per line.
|
||||
# `|| true` keeps pipefail happy when a tool has no such header (grep exits 1).
|
||||
cfg_headers() {
|
||||
local dir="$1" f
|
||||
for f in "$dir"/pos-*; do
|
||||
[ -f "$f" ] && [ -x "$f" ] || continue
|
||||
grep '^# POS_CONFIG:' "$f" 2>/dev/null | sed 's/^# POS_CONFIG:[[:space:]]*//' || true
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# All declared scopes (deduped, sorted).
|
||||
cfg_scopes() {
|
||||
local dir line scope
|
||||
dir="$(cfg_tools_dir)"
|
||||
cfg_headers "$dir" | while IFS= read -r line; do
|
||||
scope="${line%%|*}"
|
||||
scope="${scope// }"
|
||||
if [ -n "$scope" ]; then
|
||||
echo "$scope"
|
||||
fi
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
# Env-file basename for a scope (first header declaring it wins). Exit 1 if none.
|
||||
cfg_scope_envfile() {
|
||||
local scope="$1" dir line s rest env
|
||||
dir="$(cfg_tools_dir)"
|
||||
while IFS= read -r line; do
|
||||
s="${line%%|*}"
|
||||
s="${s// }"
|
||||
[ "$s" = "$scope" ] || continue
|
||||
rest="${line#*|}"
|
||||
env="${rest%%|*}"
|
||||
env="${env// }"
|
||||
echo "$env"
|
||||
return 0
|
||||
done < <(cfg_headers "$dir")
|
||||
return 1
|
||||
}
|
||||
|
||||
# One key field → "KEY|flags|description" (deduped via _cfg_seen).
|
||||
_cfg_key_line() {
|
||||
local field="$1" key flags desc rest
|
||||
key="${field%%=*}"
|
||||
if [[ "$field" == *"="* ]]; then
|
||||
rest="${field#*=}"
|
||||
if [[ "$rest" == *":"* ]]; then
|
||||
flags="${rest%%:*}"
|
||||
desc="${rest#*:}"
|
||||
else
|
||||
flags="$rest"
|
||||
fi
|
||||
fi
|
||||
[ -n "$key" ] || return 0
|
||||
[ -n "${_cfg_seen[$key]:-}" ] && return 0
|
||||
_cfg_seen[$key]=1
|
||||
printf '%s|%s|%s\n' "$key" "$flags" "$desc"
|
||||
}
|
||||
|
||||
# "*plugins" expansion: keys declared by the installed entertainment
|
||||
# plugins' "# POS_KEYS:" headers (via lib/entertainment-lib.sh, sourced lazily).
|
||||
_cfg_plugin_keys() {
|
||||
declare -F config_keys >/dev/null 2>&1 || {
|
||||
local lib
|
||||
for lib in \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/../lib/entertainment-lib.sh" \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/entertainment-lib.sh"; do
|
||||
if [ -f "$lib" ]; then source "$lib"; break; fi
|
||||
done
|
||||
}
|
||||
declare -F config_keys >/dev/null 2>&1 || return 0
|
||||
local pdir line plugin key desc req
|
||||
pdir="$(plugin_dir)"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
IFS='|' read -r plugin key desc req <<<"$line"
|
||||
[ -n "$key" ] || continue
|
||||
[ -n "${_cfg_seen[$key]:-}" ] && continue
|
||||
_cfg_seen[$key]=1
|
||||
printf '%s|%s|%s (%s) [%s]\n' "$key" "" "$desc" "$req" "$plugin"
|
||||
done < <(config_keys "$pdir")
|
||||
return 0
|
||||
}
|
||||
|
||||
# Declared keys for a scope: "KEY|flags|description" lines, deduped.
|
||||
cfg_scope_keys() {
|
||||
local scope="$1" dir line s keystring field
|
||||
local -a fields=()
|
||||
dir="$(cfg_tools_dir)"
|
||||
_cfg_seen=()
|
||||
while IFS= read -r line; do
|
||||
s="${line%%|*}"
|
||||
s="${s// }"
|
||||
[ "$s" = "$scope" ] || continue
|
||||
keystring="${line#*|}"
|
||||
keystring="${keystring#*|}" # drop the env-file field
|
||||
IFS='|' read -r -a fields <<<"$keystring"
|
||||
for field in "${fields[@]}"; do
|
||||
field="${field#"${field%%[![:space:]]*}"}"
|
||||
field="${field%"${field##*[![:space:]]}"}"
|
||||
if [ -n "$field" ]; then
|
||||
if [[ "$field" == "*"* ]]; then
|
||||
case "$field" in
|
||||
*plugins*) _cfg_plugin_keys ;;
|
||||
esac
|
||||
else
|
||||
_cfg_key_line "$field"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done < <(cfg_headers "$dir")
|
||||
return 0
|
||||
}
|
||||
|
||||
# Current value of a key in an env file (file is the source of truth, never sourced).
|
||||
cfg_value() {
|
||||
local file="$1" key="$2" v
|
||||
[ -f "$file" ] || return 0
|
||||
v="$(sed -n "s|^${key}=||p" "$file" | tail -1)"
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
# Write KEY="value" (append or replace) with mkdir -p + chmod 600; a value of
|
||||
# "-" removes the key's line. Same semantics as write_config_key().
|
||||
cfg_write() {
|
||||
local file="$1" key="$2" val="$3" tmp
|
||||
mkdir -p "$(dirname "$file")"
|
||||
if [ "$val" = "-" ]; then
|
||||
[ -f "$file" ] || return 0
|
||||
tmp="$(mktemp)"
|
||||
grep -v "^${key}=" "$file" >"$tmp" || true
|
||||
mv "$tmp" "$file"
|
||||
chmod 600 "$file"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$file" ] && grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=\"${val}\"|" "$file"
|
||||
else
|
||||
echo "${key}=\"${val}\"" >>"$file"
|
||||
fi
|
||||
chmod 600 "$file"
|
||||
}
|
||||
|
||||
# Display a value: masked for secret flags, "(not set)" when empty.
|
||||
cfg_display() {
|
||||
local val="$1" flags="$2" n
|
||||
if [ -z "$val" ]; then
|
||||
echo "(not set)"
|
||||
elif [[ ",$flags," == *,secret,* ]]; then
|
||||
n=${#val}
|
||||
if [ "$n" -le 12 ]; then
|
||||
echo "${val:0:4}... (${n} chars)"
|
||||
else
|
||||
echo "${val:0:6}...${val: -4} (${n} chars)"
|
||||
fi
|
||||
else
|
||||
echo "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate a value against a key's flags. Prints an error, returns 1 on failure.
|
||||
cfg_validate() {
|
||||
local flags="$1" val="$2"
|
||||
case ",$flags," in
|
||||
*,digits,*) [[ "$val" =~ ^[0-9]+$ ]] || { echo "must be digits only"; return 1; } ;;
|
||||
esac
|
||||
case ",$flags," in
|
||||
*,num,*) [[ "$val" =~ ^-?[0-9]+$ ]] || { echo "must be an integer"; return 1; } ;;
|
||||
esac
|
||||
case ",$flags," in
|
||||
*,float,*) [[ "$val" =~ ^-?[0-9]+([.][0-9]+)?$ ]] || { echo "must be a number"; return 1; } ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# Read a value with echo off (secret flags). Falls back to plain read when
|
||||
# stdin is not a TTY (e.g. piped menu input).
|
||||
cfg_read_secret() {
|
||||
local val echo_off=0
|
||||
if [ -t 0 ]; then
|
||||
stty -echo 2>/dev/null && echo_off=1
|
||||
fi
|
||||
read -r val || true
|
||||
if [ "$echo_off" -eq 1 ]; then
|
||||
stty echo 2>/dev/null || true
|
||||
echo
|
||||
fi
|
||||
printf '%s' "$val"
|
||||
}
|
||||
|
||||
# After a successful write: the entertainment scope re-syncs its auto-trigger
|
||||
# timers when the ENABLED list changes (mirrors `pos entertainment config set`).
|
||||
_cfg_post_write() {
|
||||
local key="$1"
|
||||
[ "$_cfg_scope" = "entertainment" ] && [ "$key" = "ENABLED" ] || return 0
|
||||
declare -F sync_timers >/dev/null 2>&1 && sync_timers
|
||||
}
|
||||
|
||||
# Edit one key: Enter keeps the current value, "-" clears it.
|
||||
_cfg_edit_one() {
|
||||
local file="$1" keystr="$2"
|
||||
local key flags desc cur val errmsg
|
||||
IFS='|' read -r key flags desc <<<"$keystr"
|
||||
cur="$(cfg_value "$file" "$key")"
|
||||
|
||||
if [[ ",$flags," == *,secret,* ]]; then
|
||||
printf ' %s [%s] (input hidden): ' "$key" "$(cfg_display "$cur" "$flags")"
|
||||
val="$(cfg_read_secret)"
|
||||
else
|
||||
read -rp " ${key} [${cur:-unset}]: " val || return 0
|
||||
fi
|
||||
|
||||
if [ -z "$val" ]; then
|
||||
log "kept current value for $key"
|
||||
return 0
|
||||
fi
|
||||
if [ "$val" = "-" ]; then
|
||||
cfg_write "$file" "$key" "-"
|
||||
log "cleared $key from $file"
|
||||
_cfg_post_write "$key"
|
||||
return 0
|
||||
fi
|
||||
if ! errmsg="$(cfg_validate "$flags" "$val")"; then
|
||||
warn "$key: $errmsg — not saved"
|
||||
return 0
|
||||
fi
|
||||
cfg_write "$file" "$key" "$val"
|
||||
ok "$key saved to $file"
|
||||
_cfg_post_write "$key"
|
||||
}
|
||||
|
||||
# Interactive numbered-menu editor for one scope. q quits; r re-renders.
|
||||
cfg_ui() {
|
||||
local scope="$1" envfile file line
|
||||
envfile="$(cfg_scope_envfile "$scope")" || { warn "unknown config scope '$scope'"; return 1; }
|
||||
file="$CONFIG_DIR/$envfile"
|
||||
_cfg_scope="$scope"
|
||||
|
||||
local -a keys=()
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
keys+=("$line")
|
||||
fi
|
||||
done < <(cfg_scope_keys "$scope")
|
||||
if [ ${#keys[@]} -eq 0 ]; then
|
||||
warn "no config keys declared for scope '$scope'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local choice i k f d v
|
||||
while true; do
|
||||
echo
|
||||
echo "pos config — ${scope} (${envfile})"
|
||||
echo "------------------------------------"
|
||||
i=0
|
||||
for line in "${keys[@]}"; do
|
||||
i=$((i + 1))
|
||||
IFS='|' read -r k f d <<<"$line"
|
||||
v="$(cfg_value "$file" "$k")"
|
||||
printf ' %2d) %-28s %s\n' "$i" "$k" "$(cfg_display "$v" "$f")"
|
||||
if [ -n "$d" ]; then
|
||||
printf ' %s\n' "$d"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
read -rp "Variable number [q to quit]: " choice || { echo; return 0; }
|
||||
case "$choice" in
|
||||
q|Q|quit|exit) echo; return 0 ;;
|
||||
r|R|refresh) continue ;;
|
||||
"") continue ;;
|
||||
*)
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#keys[@]} )); then
|
||||
_cfg_edit_one "$file" "${keys[$((choice - 1))]}"
|
||||
else
|
||||
warn "invalid number '$choice' (1-${#keys[@]})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
+18
-10
@@ -19,12 +19,20 @@ ctx="$root/DOC/AGENT_Context_Project.md"
|
||||
comp="$root/completions/pos.bash"
|
||||
|
||||
# ── Collect tools: "cat|sub|desc|flags|subcmds" ────────────────
|
||||
# Category-less tools (pos-<cat>, e.g. pos-config) get an empty cat.
|
||||
# tooldisp <cat> <sub> → display name (pos-config / pos-communication-telegram-sender).
|
||||
tooldisp() { printf 'pos-%s%s' "${1:+$1-}" "$2"; }
|
||||
tools=()
|
||||
for f in "$root"/bin/pos-*; do
|
||||
[ -x "$f" ] || continue
|
||||
name="${f##*/pos-}"
|
||||
cat="${name%%-*}"
|
||||
sub="${name#*-}"
|
||||
if [[ "$name" == *-* ]]; then
|
||||
cat="${name%%-*}"
|
||||
sub="${name#*-}"
|
||||
else
|
||||
cat=""
|
||||
sub="$name"
|
||||
fi
|
||||
desc="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f")"
|
||||
[ -n "$desc" ] || { echo "gen-docs: no '# POS:' header in $f" >&2; exit 1; }
|
||||
desc="${desc#*— }"
|
||||
@@ -39,12 +47,12 @@ gen_tree() {
|
||||
local width=0 cat sub desc flags name t
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
name="pos-$cat-$sub"
|
||||
name="$(tooldisp "$cat" "$sub")"
|
||||
[ ${#name} -gt "$width" ] && width=${#name}
|
||||
done
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
name="pos-$cat-$sub"
|
||||
name="$(tooldisp "$cat" "$sub")"
|
||||
printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc"
|
||||
done
|
||||
}
|
||||
@@ -53,7 +61,7 @@ gen_dispatch() {
|
||||
local cat sub desc flags t
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
printf '| %s | %s | `pos-%s-%s` | %s |\n' "$cat" "$sub" "$cat" "$sub" "$desc"
|
||||
printf '| %s | %s | `%s` | %s |\n' "$cat" "$sub" "$(tooldisp "$cat" "$sub")" "$desc"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -75,7 +83,7 @@ gen_filetable() {
|
||||
printf '| `bin/pos` | %s | CLI dispatcher with smart arg matching + logging + category help |\n' "$(wc -l < "$root/bin/pos")"
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
name="bin/pos-$cat-$sub"
|
||||
name="bin/$(tooldisp "$cat" "$sub")"
|
||||
printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc"
|
||||
done
|
||||
printf '| `completions/pos.bash` | %s | Dynamic bash completion |\n' "$(wc -l < "$comp")"
|
||||
@@ -87,7 +95,7 @@ gen_posflags() {
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
[ -n "$flags" ] || continue
|
||||
printf '_pos_flags[%s-%s]="%s"\n' "$cat" "$sub" "$flags"
|
||||
printf '_pos_flags[%s]="%s"\n' "$(tooldisp "$cat" "$sub" | sed 's/^pos-//')" "$flags"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -99,16 +107,16 @@ gen_possubcmds() {
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
subcmds="${subcmds:-}"
|
||||
for f in "$root"/bin/pos-"$cat"-"$sub"-*; do
|
||||
for f in "$root"/bin/"$(tooldisp "$cat" "$sub")"-*; do
|
||||
[ -x "$f" ] || continue
|
||||
rest="${f##*/pos-$cat-$sub-}"
|
||||
rest="${f##*/$(tooldisp "$cat" "$sub")-}"
|
||||
case " $subcmds " in
|
||||
*" $rest "*) ;;
|
||||
*) subcmds="${subcmds:+$subcmds }$rest" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$subcmds" ] || continue
|
||||
printf '_pos_subcmds[%s-%s]="%s"\n' "$cat" "$sub" "$subcmds"
|
||||
printf '_pos_subcmds[%s]="%s"\n' "$(tooldisp "$cat" "$sub" | sed 's/^pos-//')" "$subcmds"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user