feat: pos tree — print the CLI command tree

pos tree derives the hierarchy live from bin/pos-* filenames + # POS:
/# POS_SUBCMDS: headers, so it always matches what the dispatcher can
run. Category-less tool (like pos-config); --depth N limits nesting.

Also sharpen the agent-facing docs that the work exposed:
- AGENTS.md: drop the stale "one category-less exception" claim; document
  the category-less pattern (pos-config, pos-tree) and that gen only reads
  the text after the first em-dash in # POS: headers
- DEV.md: category-less vs categorized rule + header extraction note
- AGENT_Context "Adding a New Tool": new step 0 — pin the exact CLI verb
  and runtime context (dev/repo-only vs installed) before writing code
- POS.md: new config + tree sections; category-less tools get their own
  sections
- README: pos tree bullet following the pos X convention
This commit is contained in:
Your Name
2026-08-09 12:50:52 +00:00
parent 4d8f70d770
commit 52413a6a68
9 changed files with 162 additions and 18 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ CRITICAL: real guidance lives in DOC/. When you encounter a reference below, use
## Quick facts
- **Tool model:** `bin/pos-<category>-<command>` (one category-less exception: `pos-config`). `bin/pos` dispatches by longest-prefix arg matching. New tools are auto-discovered but must be executable (`100755`) and carry a `# POS: <cat> <cmd> — <desc>` header right after the shebang; `# POS_FLAGS:` / `# POS_SUBCMDS:` / `# POS_CONFIG:` headers feed tab-completion and the `pos config` scope registry. A missing `# POS:` header hard-fails `make gen`.
- **Tool model:** `bin/pos-<category>-<command>`, or **category-less** `bin/pos-<cmd>` for dispatcher/dev-level commands that fit no category (`pos-config`, `pos-tree`) — they dispatch like any tool and show with an empty category in the generated tables. `bin/pos` dispatches by longest-prefix arg matching. New tools are auto-discovered but must be executable (`100755`) and carry a `# POS: <cat> <cmd> — <desc>` header right after the shebang; `make gen` only uses the text after the first `— ` (the leading words are convention-only), so keep the one-line description concise. `# POS_FLAGS:` / `# POS_SUBCMDS:` / `# POS_CONFIG:` headers feed tab-completion and the `pos config` scope registry. A missing `# POS:` header hard-fails `make gen`.
- **Generated code:** blocks between `GEN:START`/`GEN:END` markers in `DOC/AGENT_Context_Project.md` (tree, dispatch, selfcontained, filetable, docmap) and `completions/pos.bash` (flags, subcmds, config scopes) are `make gen` output — never hand-edit them. After touching `bin/pos-*`, run `make gen` then `make check` (bash -n + exec-bit check + doc-sync gate + dispatch smoke; definition of done). Hand-maintained, not gen-checked: `DOC/POS.md`, the line-count rows above the filetable marker (e.g. `lib/common.sh`), `bin/pos` usage() EXAMPLES, root README.
- **Stdin gotcha:** any tool that reads stdin must be added to `INTERACTIVE_CMDS` in `bin/pos` — otherwise the logging `tee` pipe hangs on (or swallows) the prompt.
- **Deps:** apt packages → `PACKAGES` array in `preinstall.sh`; non-apt/manual installers (e.g. `usbsrv`) → `command -v <bin> || err "…"` guard inside the tool, never in PACKAGES.
+2
View File
@@ -16,6 +16,8 @@ summary (newest last).
## Done
- **2026-08-09** — `pos tree`: prints the live `pos` command tree (categories → commands → subcommands) by deriving the hierarchy from `bin/pos-*` filenames + `# POS:` / `# POS_SUBCMDS:` headers, so it always matches what the dispatcher can run. Category-less like `pos-config`; `--depth N` limit; `pos help tree` works. Docs: POS.md `tree` section, `bin/pos` usage example, `make gen` regenerated the AGENT_Context tree/dispatch/filetable + `_pos_flags[tree]`.
- **2026-08-09** — Telegram sender `config` / `config set` removed — redundant with `pos config telegram` (same `# POS_CONFIG:` registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped `/command` values containing `|` are no longer truncated (`load_map` switched from a `|` to a `\x1f` delimiter — previously `/up=echo hi | head` silently ran `echo hi `); `pos entertainment send <plugin> [args…]` actually forwards the extra args (every arg was `shift`ed in the flag loop, so `$@` was empty) and passes `--` before the message so leading-`-` plugin output isn't parsed as an option; `write_config_key` (entertainment-lib) and `cfg_write` (config-ui) replaced unescaped `sed -i "s|^K=.*|K=\"$v\"|"` with grep-v+append so values with `&`/`|`/`\` no longer mangle (also the path all telegram config now flows through); `sync_systemd` daemon-reloads after removing timer units; `digits` config validation accepts negative group/supergroup chat ids (`-100…`).
- **2026-08-09** — Fixed telegram listener editor crash on remove/edit/test: `ui_pick` printed its menu listing to **stdout**, so `idx="$(ui_pick)"` captured the menu *and* the number, and `MAP_CMDS[$idx]` (arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the `::desc` work), exposed by the description column.
+20 -16
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44180 |
| ## 3. Installation Flow | 181233 |
| ## 4. The `pos` CLI System | 234295 |
| ## 5. Shared Library — `lib/common.sh` | 296327 |
| ## 6. Docker Compose / ScaleTail | 328370 |
| ## 7. Optional Apps (`apps/`) | 371400 |
| ## 8. Entertainment Module | 401414 |
| ## 9. Systemd Services | 415427 |
| ## 10. Configuration Files | 428453 |
| ## 11. Coding Conventions | 454486 |
| ## 12. Development Workflow | 487538 |
| ## 13. Key File Quick Reference | 539586 |
| ## 14. Common Tasks for Agents | 587611 |
| ## 2. Directory Structure | 44181 |
| ## 3. Installation Flow | 182234 |
| ## 4. The `pos` CLI System | 235297 |
| ## 5. Shared Library — `lib/common.sh` | 298329 |
| ## 6. Docker Compose / ScaleTail | 330372 |
| ## 7. Optional Apps (`apps/`) | 373402 |
| ## 8. Entertainment Module | 403416 |
| ## 9. Systemd Services | 417429 |
| ## 10. Configuration Files | 430455 |
| ## 11. Coding Conventions | 456488 |
| ## 12. Development Workflow | 489541 |
| ## 13. Key File Quick Reference | 542590 |
| ## 14. Common Tasks for Agents | 591615 |
<!-- GEN:END docmap -->
## 1. Project Overview
@@ -82,6 +82,7 @@ Linux_post_install/
│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
│ ├── pos-system-nfs-client # Mount NFS shares (ephemeral or persistent systemd mount units)
│ ├── pos-system-nfs-server # Manage the NFS kernel server (status, share/unshare exports, enable/disable)
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
│ ├── pos-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted)
<!-- GEN:END tree -->
│ ├── flag-reader # Inspect feature flags (list/status/--raw)
@@ -274,6 +275,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
| system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
| system | nfs-client | `pos-system-nfs-client` | Mount NFS shares (ephemeral or persistent systemd mount units) |
| system | nfs-server | `pos-system-nfs-server` | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
| | tree | `pos-tree` | Show the pos CLI command tree: categories, commands, and subcommands |
| usb | server | `pos-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) |
<!-- GEN:END dispatch -->
@@ -502,7 +504,8 @@ System-wide flag store at `/usr/local/share/linux_post_install/flags/`:
### Adding a New Tool
1. Create `bin/pos-<category>-<command>` from `templates/pos-tool.sh` — must be executable (`100755`); it auto-appears in `pos <category> --help` (filename-derived, no registration)
0. Define the exact CLI verb (`pos <category> <command> [<subcommand>]`) and its runtime context before writing code: a **dev/repo-only** tool (e.g. reads repo files, like `pos tree`) or a **runtime** tool that must work from `/usr/local/bin` after the repo is deleted. Category-less `bin/pos-<cmd>` is for dispatcher/dev-level commands that fit no category (`pos-config`, `pos-tree`); everything else goes in a category.
1. Create `bin/pos-<category>-<command>` (or `bin/pos-<cmd>` for category-less) from `templates/pos-tool.sh` — must be executable (`100755`); it auto-appears in `pos <category> --help` (filename-derived, no registration)
2. Add the `# POS: <cat> <cmd> — <one-line description>` 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-<cat>-<a>-<b>`) 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
@@ -552,7 +555,7 @@ 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` | 270 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos` | 272 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos-communication-telegram-listener` | 509 | Telegram bot listener: map /command → bash, run them on chat messages |
| `bin/pos-communication-telegram-sender` | 220 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
@@ -577,8 +580,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-system-health` | 209 | 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-tree` | 110 | Show the pos CLI command tree: categories, commands, and subcommands |
| `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
| `completions/pos.bash` | 279 | Dynamic bash completion |
| `completions/pos.bash` | 280 | Dynamic bash completion |
<!-- GEN:END filetable -->
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
+2 -1
View File
@@ -127,7 +127,8 @@ esac
# 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 and `POS_SUBCMDS` feeds subcommand 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`). `make gen` only reads the text after the first `` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
- **Category-less vs categorized:** most tools are `bin/pos-<category>-<command>`. Use category-less `bin/pos-<cmd>` (e.g. `pos-config`, `pos-tree`) only for dispatcher/dev-level commands that fit no category — they dispatch and document like any tool but show with an empty category in the generated tables.
- 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 <category> --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).
+23
View File
@@ -14,6 +14,8 @@
- [communication](#communication)
- [entertainment](#entertainment)
- [flags](#flags)
- [config](#config)
- [tree](#tree)
- [Legacy wrappers](#legacy-wrappers)
---
@@ -48,6 +50,8 @@ Every non-interactive `pos` invocation logs to `~/.local/share/linux_post_instal
## Commands
Category-less tools (`config`, `tree`) live outside any category and are documented in their own `###` sections below.
### network
| Command | File | Purpose | Configuration |
@@ -301,6 +305,25 @@ Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libfl
| `flag-set <name> [value]` | Set a flag, optionally with a value (requires sudo) |
| `flag-clear <name>` | Unset a flag (requires sudo) |
### config
`pos config` is the interactive editor for the tools' runtime config (see [DEV.md](DEV.md#config-files) and §10 of AGENT_Context). Every tool exposes its configuration by declaring a `# POS_CONFIG:` header; `pos config` reads those at runtime — it knows nothing about the variables themselves. Values live in `~/.config/linux_post_install/<scope>.env` (chmod 600).
| Command | Purpose |
|---------|---------|
| `pos config` | Scope picker (on a TTY), otherwise the scope list |
| `pos config <scope>` | Edit that scope's variables (masked secrets, validation, `-` to clear) |
| `pos config <scope> set KEY=VALUE` | Set a value non-interactively (each tool's `config set` form) |
### tree
`pos tree` prints the `pos` command tree — every category, command, and subcommand the dispatcher can reach, annotated with each tool's `# POS:` description. Data is derived live from the `bin/pos-*` filenames and their `# POS_SUBCMDS:` headers, so it always matches what `pos` can actually run.
| Command | Purpose |
|---------|---------|
| `pos tree` | Full command tree |
| `pos tree --depth N` | Limit nesting depth (1 = root only) |
---
## Legacy wrappers
+1
View File
@@ -13,6 +13,7 @@ It is a **personal toolkit** — a bootstrap script, a unified `pos` CLI for eve
- 25+ system packages installed automatically
- The `pos` CLI: network, Docker (compose + vbox VMs), media, system, SSH, USB, communication (Telegram), and entertainment (public-API plugins → Telegram) tools
- Wi-Fi hotspot tools (`create_ap`, `wihotspot-gui`) via `pos network hotspot`
- `pos tree` — browse the whole CLI: categories, commands, and subcommands
- 15 optional desktop apps (VS Code, Brave, OBS, Tailscale, …) — pick what you want
- 119+ self-hosted services with Tailscale access (Jellyfin, Home Assistant, …)
- systemd services for SSH agent, boot-time automation, and a daily Telegram health digest (`pos system health`)
+2
View File
@@ -176,6 +176,8 @@ EXAMPLES
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
Executable
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: tree — Show the pos CLI command tree: categories, commands, and subcommands
# POS_FLAGS: --depth
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
usage() {
cat <<EOF
Usage: pos tree [--depth N]
Print the `pos` command tree: every category, command, and subcommand the
dispatcher can reach, annotated with each tool's # POS: description.
pos tree Full command tree
pos tree --depth N Limit nesting depth (1 = root only)
pos tree --help This help
EOF
exit 0
}
depth=""
case "${1:-}" in
-h|--help) usage ;;
--depth)
[ $# -ge 2 ] || err "--depth requires a number"
depth="$2"
[[ "$depth" =~ ^[0-9]+$ ]] || err "--depth must be a number"
shift 2
;;
-*) err "unknown flag: $1" ;;
esac
self="$(cd "$(dirname "$0")" && pwd)"
declare -A children meta
maxw=0
# add <parent> <word> <type> <desc> — parent "/" is the root.
add() {
local parent="$1" word="$2" type="$3" desc="$4" path
if [ "$parent" = "/" ]; then path="$word"; else path="$parent/$word"; fi
children["$parent"]+=" $word"
meta["$path"]="$type|$desc"
}
for f in "$self"/pos-*; do
[ -x "$f" ] || continue
name="${f##*/pos-}"
desc="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f")"
desc="${desc#*— }"
sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")"
words=(${name//-/ })
parent="/"
for ((i=0; i<${#words[@]}; i++)); do
word="${words[$i]}"
if [ "$parent" = "/" ]; then path="$word"; else path="$parent/$word"; fi
if [ $((i + 1)) -eq ${#words[@]} ]; then
add "$parent" "$word" "tool" "$desc"
else
[ -n "${meta[$path]:-}" ] || add "$parent" "$word" "cat" ""
fi
parent="$path"
done
for s in $sc; do
add "$parent" "$s" "sub" ""
done
done
# pad = widest "pos <full path>" leaf, so descriptions align
for p in "${!meta[@]}"; do
case "${meta[$p]%%|*}" in
tool|sub)
len=$(( ${#p} + 4 ))
[ "$len" -gt "$maxw" ] && maxw=$len
;;
esac
done
render() {
local parent="$1" prefix="$2" lvl="$3"
if [ -n "$depth" ] && [ "$lvl" -ge "$depth" ]; then return; fi
local -a kids
mapfile -t kids < <(printf '%s\n' ${children[$parent]:-} | sort -u)
local i word path type desc conn nextpref last
for ((i=0; i<${#kids[@]}; i++)); do
word="${kids[$i]}"
if [ "$parent" = "/" ]; then path="$word"; else path="$parent/$word"; fi
last=$(( i == ${#kids[@]} - 1 ))
if [ "$last" -eq 1 ]; then conn="└── "; nextpref="$prefix "; else conn="├── "; nextpref="$prefix│ "; fi
type="${meta[$path]%%|*}"
desc="${meta[$path]#*|}"
case "$type" in
cat)
printf '%s%s%s/\n' "$prefix" "$conn" "$word"
render "$path" "$nextpref" $((lvl+1))
;;
tool)
printf '%s%s%-*s# %s\n' "$prefix" "$conn" "$((maxw+1))" "pos ${path//\// }" "$desc"
[ -n "${children[$path]:-}" ] && render "$path" "$nextpref" $((lvl+1))
;;
sub)
printf '%s%s%s\n' "$prefix" "$conn" "pos ${path//\// }"
;;
esac
done
}
printf 'pos\n'
render "/" "" 1
+1
View File
@@ -8,6 +8,7 @@ _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-pr
_pos_flags[entertainment-send]="--print --markdown"
_pos_flags[network-hotspot]="--foreground"
_pos_flags[system-backup]="--service"
_pos_flags[tree]="--depth"
_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