From f99acc36a23c454bf5aa872daa354fd3e0c8b264 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 6 Aug 2026 04:01:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20nested=20pos=20subcommands=20=E2=80=94?= =?UTF-8?q?=20POS=5FSUBCMDS=20header,=20nested=20category=20help=20+=20com?= =?UTF-8?q?pletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT_TODO.md | 5 +++ DOC/AGENT_Context_Project.md | 12 +++--- DOC/DEV.md | 4 +- bin/pos | 60 +++++++++++++++++++++++++--- bin/pos-communication-telegram | 1 + bin/pos-docker-compose | 1 + bin/pos-docker-vbox | 1 + completions/pos.bash | 71 +++++++++++++++++++++++----------- scripts/gen-docs.sh | 38 ++++++++++++++---- 9 files changed, 150 insertions(+), 43 deletions(-) diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 996db8f..5be6c14 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -47,6 +47,11 @@ summary (newest last). ## Done (summary, newest last) +- 2026-08-06: Nested `pos` subcommands — `# POS_SUBCMDS:` header annotation (telegram, + docker-compose, docker-vbox) + `make gen` emits a `_pos_subcmds` completion map; + nested tools (`telegram listener`) auto-list under their parent instead of as a + flat sibling (`telegram-listener`) in `pos ` and tab-completion; generic + tool-level completion (subcommands + flags + `--help`). - 2026-08-06: Telegram **listener** — `pos communication telegram listener`: interactive `/command` → bash map editor + owner-only polling daemon as a systemd user service (map in `~/.config/linux_post_install/telegram_commands.env`, diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index f68cc7f..b9a2d05 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -484,7 +484,7 @@ System-wide flag store at `/usr/local/share/linux_post_install/flags/`: ### Adding a New Tool 1. Create `bin/pos--` from `templates/pos-tool.sh` — must be executable (`100755`); it auto-appears in `pos --help` (filename-derived, no registration) -2. Add the `# POS: ` header right after the shebang (plus `# POS_FLAGS: ...` for flag-style tools) — this is the single source of truth for the generated docs +2. Add the `# POS: ` header right after the shebang (plus `# POS_FLAGS: ...` for flag-style tools and `# POS_SUBCMDS: ...` for multi-command tools) — this is the single source of truth for the generated docs; nested tools (`pos---`) auto-list under their parent tool 3. Add to `INTERACTIVE_CMDS` in `bin/pos` if it reads stdin 4. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed); non-apt/manual installers → `command -v` guard in the tool instead 5. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets); runtime tool config → `~/.config/linux_post_install/.env` (600) @@ -532,13 +532,13 @@ 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) | -| `bin/pos` | 219 | CLI dispatcher with smart arg matching + logging + category help | +| `bin/pos` | 267 | CLI dispatcher with smart arg matching + logging + category help | | `bin/pos-communication-telegram-listener` | 408 | Telegram bot listener: map /command → bash, run them on chat messages | -| `bin/pos-communication-telegram` | 274 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | -| `bin/pos-docker-compose` | 364 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | +| `bin/pos-communication-telegram` | 275 | 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-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` | 157 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | +| `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-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) | | `bin/pos-entertainment-enable` | 50 | Enable an auto-trigger for a plugin on a schedule | @@ -557,7 +557,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `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` | 190 | Dynamic bash completion | +| `completions/pos.bash` | 215 | Dynamic bash completion | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | diff --git a/DOC/DEV.md b/DOC/DEV.md index cc798be..be35872 100644 --- a/DOC/DEV.md +++ b/DOC/DEV.md @@ -125,8 +125,10 @@ esac ```bash # POS: — one-line description rendered by `make gen` # POS_FLAGS: --flag1 --flag2 # ONLY for flag-style tools + # POS_SUBCMDS: sub1 sub2 # ONLY for multi-command tools ``` - The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion in `completions/pos.bash`. Both update via `make gen`. + The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). +- Nested tools (e.g. `bin/pos-communication-telegram-listener`) are auto-detected from filenames: the trailing segment (`listener`) is offered as a subcommand of the parent tool (`communication-telegram`) in `pos --help` and tab-completion, instead of appearing as a flat sibling (`telegram-listener`). The flat dash-form (`pos communication telegram-listener`) still dispatches. - Optionally add an EXAMPLES line in `bin/pos` `usage()` to showcase the tool in `pos --help`. - If the command **reads stdin** (prompts/selection), add it to `INTERACTIVE_CMDS` in `bin/pos` — see [The `pos` CLI](#the-pos-cli). diff --git a/bin/pos b/bin/pos index e01dda2..5698280 100755 --- a/bin/pos +++ b/bin/pos @@ -26,6 +26,8 @@ _pos_commands() { } # ── 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 @@ -36,10 +38,19 @@ _pos_category_list() { [ "$cat" = "$cmd" ] && continue cats["$cat"]+="${cmd#*-} " done - local c line subs joined + local c subs joined s s2 top=() is_nested for c in $(printf '%s\n' "${!cats[@]}" | sort); do subs=(${cats[$c]}) - joined="$(IFS='|'; echo "${subs[*]}")" + 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 } @@ -55,11 +66,36 @@ _pos_category_exists() { } _pos_category_help() { - local cat="$1" sub subs=() - local f + local cat="$1" f + local files=() s d for f in "$self"/pos-"$cat"-*; do [ -x "$f" ] || continue - subs+=("${f##*/pos-$cat-}") + 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--- 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" @@ -68,7 +104,19 @@ _pos_category_help() { echo " pos $cat [args]" echo echo "COMMANDS" - printf " %s\n" "${subs[@]}" + 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 --help' for details on a command." exit 0 diff --git a/bin/pos-communication-telegram b/bin/pos-communication-telegram index 56da6c1..7c375f8 100755 --- a/bin/pos-communication-telegram +++ b/bin/pos-communication-telegram @@ -2,6 +2,7 @@ set -euo pipefail # POS: communication telegram — Send Telegram messages/files/links/stickers via Bot API (send, test, config set) # POS_FLAGS: --send --type --caption --parse-mode --no-preview --token --chat-id +# POS_SUBCMDS: send test config CONFIG_DIR="$HOME/.config/linux_post_install" CONFIG_FILE="$CONFIG_DIR/telegram.env" diff --git a/bin/pos-docker-compose b/bin/pos-docker-compose index 3022496..107563d 100755 --- a/bin/pos-docker-compose +++ b/bin/pos-docker-compose @@ -1,6 +1,7 @@ #!/usr/bin/env bash 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 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" diff --git a/bin/pos-docker-vbox b/bin/pos-docker-vbox index 145681d..1166397 100755 --- a/bin/pos-docker-vbox +++ b/bin/pos-docker-vbox @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail # POS: docker vbox — Disposable Docker-based VMs (create/enter/start/stop/rm/ls) +# POS_SUBCMDS: create enter stop start rm ls source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" usage() { diff --git a/completions/pos.bash b/completions/pos.bash index 0640a18..ea95f07 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -11,6 +11,12 @@ _pos_flags[system-backup]="--service" _pos_flags[system-health]="--send --markdown" _pos_flags[usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version" # GEN:END posflags +# GEN:START possubcmds +declare -A _pos_subcmds +_pos_subcmds[communication-telegram]="send test config listener" +_pos_subcmds[docker-compose]="ls installed up down restart logs update config" +_pos_subcmds[docker-vbox]="create enter stop start rm ls" +# GEN:END possubcmds _pos() { local cur prev words cword @@ -39,11 +45,29 @@ _pos() { done # ── Build category→subcommand map ────────────────────────── + # Nested sub-tools (pos--- where pos-- exists) are + # offered under their parent tool, not at the category level. local -A cat_cmds + local -A nested + local cmd2 c2 cat2 sub2 for cmd in "${all_cmds[@]}"; do local cat="${cmd%%-*}" local sub="${cmd#*-}" if [ "$cat" != "$cmd" ]; then + for cmd2 in "${all_cmds[@]}"; do + cat2="${cmd2%%-*}" + sub2="${cmd2#*-}" + if [ "$cat2" = "$cat" ] && [ "$sub2" != "$sub" ] && [[ "$sub" == "$sub2-"* ]]; then + nested["$cmd"]=1 + break + fi + done + fi + done + for cmd in "${all_cmds[@]}"; do + local cat="${cmd%%-*}" + local sub="${cmd#*-}" + if [ "$cat" != "$cmd" ] && [ -z "${nested[$cmd]:-}" ]; then cat_cmds["$cat"]+="${sub} " fi done @@ -58,6 +82,26 @@ _pos() { COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-} --help" -- "$cur")) } + # Complete subcommands + flags + --help for a tool chain, walking up to + # the nearest tool that declares anything (e.g. "telegram send" → telegram flags). + _pos_complete_tool() { + local key="$1" k opts + k="$key" + while [ -n "$k" ]; do + if [ -n "${_pos_subcmds[$k]:-}" ] || [ -n "${_pos_flags[$k]:-}" ]; then + if [ "$k" = "$key" ]; then + opts="${_pos_subcmds[$k]:-} ${_pos_flags[$k]:-} --help" + else + opts="${_pos_flags[$k]:-} --help" + fi + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + return + fi + k="${k%-*}" + done + COMPREPLY=($(compgen -W "--help" -- "$cur")) + } + _pos_complete_compose_services() { local scale_dir="/usr/local/share/linux_post_install/scale-tail/services" if [ -d "$scale_dir" ]; then @@ -83,11 +127,6 @@ _pos() { COMPREPLY=($(compgen -W "$names" -- "$cur")) } - _pos_complete_flags() { - local tool="$1" - COMPREPLY=($(compgen -W "${_pos_flags[$tool]:-} --help" -- "$cur")) - } - _pos_entertainment_plugins() { local d f name out=() for d in /usr/local/bin "$pos_dir/../entertainment"; do @@ -124,26 +163,14 @@ _pos() { docker-vbox) _pos_complete_docker_vbox_cmds ;; - usb-server) - _pos_complete_flags usb-server - ;; - communication-telegram) - _pos_complete_flags communication-telegram - ;; - entertainment-send) - _pos_complete_flags entertainment-send - ;; entertainment-enable|entertainment-disable) _pos_entertainment_plugins ;; entertainment-config) COMPREPLY=($(compgen -W "set --help" -- "$cur")) ;; - network-hotspot) - _pos_complete_flags network-hotspot - ;; - system-backup) - _pos_complete_flags system-backup + *) + _pos_complete_tool "${words[1]}-${words[2]}" ;; esac ;; @@ -163,10 +190,8 @@ _pos() { ;; esac ;; - communication-telegram) - case "${words[3]}" in - send|--send) _pos_complete_flags communication-telegram ;; - esac + *) + _pos_complete_tool "${words[1]}-${words[2]}-${words[3]}" ;; esac ;; diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index cc1f7ac..9da31a3 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -9,6 +9,7 @@ set -euo pipefail # - bin/pos-* filenames → category, subcommand # - "# POS:" header line → one-line description # - "# POS_FLAGS:" line → flag completion list (flag-style tools only) +# - "# POS_SUBCMDS:" line → subcommand completion list (multi-command tools) root="$(cd "$(dirname "$0")/.." && pwd)" mode="write" @@ -17,7 +18,7 @@ mode="write" ctx="$root/DOC/AGENT_Context_Project.md" comp="$root/completions/pos.bash" -# ── Collect tools: "cat|sub|desc|flags" ──────────────────────── +# ── Collect tools: "cat|sub|desc|flags|subcmds" ──────────────── tools=() for f in "$root"/bin/pos-*; do [ -x "$f" ] || continue @@ -28,7 +29,8 @@ for f in "$root"/bin/pos-*; do [ -n "$desc" ] || { echo "gen-docs: no '# POS:' header in $f" >&2; exit 1; } desc="${desc#*— }" flags="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f")" - tools+=("$cat|$sub|$desc|$flags") + subcmds="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")" + tools+=("$cat|$sub|$desc|$flags|$subcmds") done mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort) @@ -36,12 +38,12 @@ mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort) gen_tree() { local width=0 cat sub desc flags name t for t in "${tools[@]}"; do - IFS='|' read -r cat sub desc flags <<<"$t" + IFS='|' read -r cat sub desc flags subcmds <<<"$t" name="pos-$cat-$sub" [ ${#name} -gt "$width" ] && width=${#name} done for t in "${tools[@]}"; do - IFS='|' read -r cat sub desc flags <<<"$t" + IFS='|' read -r cat sub desc flags subcmds <<<"$t" name="pos-$cat-$sub" printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc" done @@ -50,7 +52,7 @@ gen_tree() { gen_dispatch() { local cat sub desc flags t for t in "${tools[@]}"; do - IFS='|' read -r cat sub desc flags <<<"$t" + IFS='|' read -r cat sub desc flags subcmds <<<"$t" printf '| %s | %s | `pos-%s-%s` | %s |\n' "$cat" "$sub" "$cat" "$sub" "$desc" done } @@ -72,7 +74,7 @@ gen_filetable() { local cat sub desc flags name t 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 <<<"$t" + IFS='|' read -r cat sub desc flags subcmds <<<"$t" name="bin/pos-$cat-$sub" printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc" done @@ -83,12 +85,33 @@ gen_posflags() { local cat sub desc flags t echo "declare -A _pos_flags" for t in "${tools[@]}"; do - IFS='|' read -r cat sub desc flags <<<"$t" + IFS='|' read -r cat sub desc flags subcmds <<<"$t" [ -n "$flags" ] || continue printf '_pos_flags[%s-%s]="%s"\n' "$cat" "$sub" "$flags" done } +gen_possubcmds() { + # Subcommand completion: "# POS_SUBCMDS:" list + nested sub-tools from + # filenames (pos--- → "extra" completes under -). + local cat sub desc flags subcmds rest f t + echo "declare -A _pos_subcmds" + 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 + [ -x "$f" ] || continue + rest="${f##*/pos-$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" + done +} + # Section index of AGENT_Context itself: maps each "## " heading to its # line range. Excludes the "Document Map" heading (this block). gen_docmap() { @@ -155,6 +178,7 @@ regen_block "$ctx" tree regen_block "$ctx" dispatch regen_block "$ctx" selfcontained regen_block "$comp" posflags +regen_block "$comp" possubcmds regen_block "$ctx" filetable # docmap is self-referential: its own block size shifts the section line