feat: command registry, alias wrapper scripts, config-ui readability
gates / consistency-and-conventions (push) Successful in 1m28s

- lib/registry.sh: shared query API over POS_* headers (reg_scan, reg_list,
  reg_lookup, reg_tools_in, reg_each, reg_config_scopes/keys). Replaces
  per-consumer sed/grep header parsing.

- bin/pos-tree + bin/pos _pos_category_help(): migrated to registry API.
  Category help now shows [deps: ...] annotations. Tree output preserved.

- New optional headers # POS_DEPS: and # POS_EXAMPLES: in tool metadata.
  Added to pos-network-download (aria2c jq curl), pos-media-sync (lsblk jq),
  pos-system-backup (tar), pos-docker-ps (docker) as initial adopters.

- scripts/gen-docs.sh: extended tools array with deps/examples fields;
  conditional column rendering in gen_dispatch; deps annotation in gen_tree.
  Fixed URL-unsafe // joiner (→ middle dot ·) and \x1f caption delimiter
  collision in config-ui.

- bin/pos-ai-alias: rewrote activation from bash aliases (source-time-frozen)
  to executable wrapper scripts at ~/.local/bin. Staleness eliminated:
  edits apply on next invocation with no shell reload. _alias_sync()
  reconciliation on every subcommand, marker-guarded lifecycle, collision
  refusal, legacy .sh retirement. Fixed dup-table bug (option 4 no-op).

- lib/config-ui.sh: @caption/@[KEY=alt] conditional captions, *providers=<tag>
  tagged wildcards, uniform typography tier (bold/cyan/dim), honest prompt.
  Active provider keys bold, inactive dimmed with reason. Backward-compatible.

- bin/pos-system-uninstall: marker-scan for wrapper script cleanup.

- Docs synced: AGENTS.md (new headers + registry), DOC/SCRIPTS.md (registry
  section + lib list), DOC/POS.md (alias wrapper activation), MAINTENANCE.md
  (M-024). Lint fixed: pos-ai-alias registered in INTERACTIVE_CMDS.

Gates: make gen && make check && make lint = 0 FAIL, 0 WARN
This commit is contained in:
Your Name
2026-08-27 02:30:27 -04:00
parent 4fd3c37c40
commit e969234ca5
20 changed files with 826 additions and 204 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ CRITICAL: real guidance lives in DOC/. When you encounter a reference below, use
## Quick facts ## Quick facts
- **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`. Legacy `bin/wr-*`, `mp3`, `mp4`, `vbox`, `ssh-load-all` are thin forwarders to `pos` — keep them that way. - **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; optional `# POS_DEPS: <binary…>` declares space-separated runtime binaries the tool hard-requires via `command -v` guards, and optional `# POS_EXAMPLES: <command> | <description>` adds curated usage examples, one per line. `lib/registry.sh` is the shared query API over all `POS_*` headers — consumers source it (`reg_scan` + `reg_list`/`reg_lookup`/…) instead of re-implementing sed/grep header parsing; new consumers should prefer it. A missing `# POS:` header hard-fails `make gen`. Legacy `bin/wr-*`, `mp3`, `mp4`, `vbox`, `ssh-load-all` are thin forwarders to `pos` — keep them that way.
- **Categories:** `ai`, `communication`, `docker`, `entertainment`, `media`, `network`, `share` (usb, nfs, smb), `ssh`, `system`, plus category-less `config`/`tree`. `pos tree` (bin/pos-tree) is the authoritative structure — it derives the hierarchy from `bin/pos-*` filenames + `# POS:`/`# POS_SUBCMDS:` headers. - **Categories:** `ai`, `communication`, `docker`, `entertainment`, `media`, `network`, `share` (usb, nfs, smb), `ssh`, `system`, plus category-less `config`/`tree`. `pos tree` (bin/pos-tree) is the authoritative structure — it derives the hierarchy from `bin/pos-*` filenames + `# POS:`/`# POS_SUBCMDS:` headers, reads the metadata through `lib/registry.sh`, and annotates each command's declared `# POS_DEPS:`.
- **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. Generators must be **byte-order deterministic** (sort with `LC_ALL=C`, as `scripts/gen-docs.sh` does) or CI's `git diff --exit-code` trips on a locale that collates differently. After touching `bin/pos-*`, run `make gen`, then `make check`, then `make lint` (definition of done: check green + lint ends `0 FAIL, 0 WARN`). `make check` (`scripts/check-sync.sh`) is the self-consistency gate — bash -n + exec-bit check + doc-sync + dispatch smoke; `make lint` (`scripts/lint-conventions.sh`) is the convention gate — it enforces every rule in this file (shebang/strict-mode, exec bits, `# POS:` headers, `-h|--help` present and after deps guards, stdin-readers in `INTERACTIVE_CMDS`, POS.md coverage, plugin/app/unit/wrapper/secrets/env-seam classes — see `DOC/DEV.md → Convention Lint Gate`). Hand-maintained, not gen-checked: `DOC/POS.md`, the line-count rows above the filetable marker in `DOC/AGENT_Context_Project.md` (the non-`pos-*` files — `install.sh`, `preinstall.sh`, `postinstall.sh`, `lib/*`, `features/*`; bump a row's count only when that file's length changes), `bin/pos` usage() EXAMPLES, root README. CI (`.gitea/workflows/lint.yml`, job `gates`) runs the same four commands on every push to main and PR, then records the result as a git tag on the commit: `ci-ok/<sha>` or `ci-fail/<sha>` (pushes only — query with `scripts/ci-status.sh [--wait] [<sha>]`; exit 0 green / 1 red / 2 pending). A red run means gen drift or a gate failure and is a merge-blocker; still run the gates locally too (lint isn't in the pre-commit hook). - **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. Generators must be **byte-order deterministic** (sort with `LC_ALL=C`, as `scripts/gen-docs.sh` does) or CI's `git diff --exit-code` trips on a locale that collates differently. After touching `bin/pos-*`, run `make gen`, then `make check`, then `make lint` (definition of done: check green + lint ends `0 FAIL, 0 WARN`). `make check` (`scripts/check-sync.sh`) is the self-consistency gate — bash -n + exec-bit check + doc-sync + dispatch smoke; `make lint` (`scripts/lint-conventions.sh`) is the convention gate — it enforces every rule in this file (shebang/strict-mode, exec bits, `# POS:` headers, `-h|--help` present and after deps guards, stdin-readers in `INTERACTIVE_CMDS`, POS.md coverage, plugin/app/unit/wrapper/secrets/env-seam classes — see `DOC/DEV.md → Convention Lint Gate`). Hand-maintained, not gen-checked: `DOC/POS.md`, the line-count rows above the filetable marker in `DOC/AGENT_Context_Project.md` (the non-`pos-*` files — `install.sh`, `preinstall.sh`, `postinstall.sh`, `lib/*`, `features/*`; bump a row's count only when that file's length changes), `bin/pos` usage() EXAMPLES, root README. CI (`.gitea/workflows/lint.yml`, job `gates`) runs the same four commands on every push to main and PR, then records the result as a git tag on the commit: `ci-ok/<sha>` or `ci-fail/<sha>` (pushes only — query with `scripts/ci-status.sh [--wait] [<sha>]`; exit 0 green / 1 red / 2 pending). A red run means gen drift or a gate failure and is a merge-blocker; still run the gates locally too (lint isn't in the pre-commit hook).
- **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. - **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. Hotspot binaries (`create_ap`, `wihotspot*`) are prebuilt in `x64_bin/` (or `arm64_bin/`) and copied by `install.sh` — not apt packages. - **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. Hotspot binaries (`create_ap`, `wihotspot*`) are prebuilt in `x64_bin/` (or `arm64_bin/`) and copied by `install.sh` — not apt packages.
+1
View File
@@ -134,3 +134,4 @@ summary (newest last).
- **2026-08-23** — ytsync menu render bugfix (`bin/pos-media-ytsync`, live-box report): `cut -d'·'` at :292/:301 used U+00B7 = 2 bytes UTF-8 (GNU cut is byte-oriented → "delimiter must be a single character", masked by `|| true` so the `· last run …` suffix and LAST SYNC column never rendered); replaced with grep/tail capture + `${last%% ·*}` parameter expansion (semantics identical incl. empty-string=no-last-run); :1053 `printf '----…\n'` format starting with `-` parsed as invalid option → `printf '%s\n' '----…'`. Chain: Detective root cause (`reportAgents/2026-08-23-detective-ytsync-menu-errors.md`) → Builder 3-site fix (`-builder-ytsync-menu-fix.md`, pty probe: suffix + separator render, zero stderr noise) → Reviewer ACCEPT-WITH-NITS (`-reviewer-ytsync-menu-fix.md` delivered inline). Gates re-run by Orchestrator post-review: `make gen` idempotent, check OK, lint 0 FAIL / 0 WARN. - **2026-08-23** — ytsync menu render bugfix (`bin/pos-media-ytsync`, live-box report): `cut -d'·'` at :292/:301 used U+00B7 = 2 bytes UTF-8 (GNU cut is byte-oriented → "delimiter must be a single character", masked by `|| true` so the `· last run …` suffix and LAST SYNC column never rendered); replaced with grep/tail capture + `${last%% ·*}` parameter expansion (semantics identical incl. empty-string=no-last-run); :1053 `printf '----…\n'` format starting with `-` parsed as invalid option → `printf '%s\n' '----…'`. Chain: Detective root cause (`reportAgents/2026-08-23-detective-ytsync-menu-errors.md`) → Builder 3-site fix (`-builder-ytsync-menu-fix.md`, pty probe: suffix + separator render, zero stderr noise) → Reviewer ACCEPT-WITH-NITS (`-reviewer-ytsync-menu-fix.md` delivered inline). Gates re-run by Orchestrator post-review: `make gen` idempotent, check OK, lint 0 FAIL / 0 WARN.
- **2026-08-23** — Menu Phase 1 (user-ratified decision "b"): category-neutral menu library extracted from share-suite Pattern B + four P1 tool menus. New `lib/menu-lib.sh` (169 ln): `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value` (stderr render, /dev/tty reads, EOF fail-closed rc=1, index/value→stdout); `lib/share-lib.sh` (436→318) keeps its public names as pure delegating shims so all five `pos share *` tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (or `menu` verb, `# POS_SUBCMDS:` registered, completions regen'd) on `pos media sync` (164→216: Sync-now/Preview/mp3/mp4/source-folder items), `pos system backup` (216→292: typed/service-root/plain variants, every backup behind folder-naming y/N), `pos docker compose` (366→487: ls/up/down/restart/logs/update/config items, down/restart/update confirm-gated naming the stack), `pos system schedule` (81→151: list/status/run-now(confirm)/enable/disable/editor — timer-invoked `run <name>` verb dispatch byte-identical to HEAD). INTERACTIVE_CMDS unchanged; all CLI verbs byte-compatible. Docs: POS.md ×4 rows, DEV.md lib row, SCRIPTS.md sections, AGENT_Context rows + GEN. Chain: Explorer survey (37 tools, `reportAgents/2026-08-23-explorer-pos-menu-survey.md`) → Designer classification (`-designer-pos-menu-suitability.md`: 14 MENU-FIT / 7 CONDITIONAL / 16 NO-FIT) → Builder T1/T2/T3 (`-builder-t1-menu-lib-extraction.md`, `-t2-p1-menus-media-backup.md`, `-t3-p1-menus-compose-schedule.md`; T3 discloses a mid-verify symlink clobber restored+re-verified) → Reviewer ACCEPT_WITH_NOTES over the consolidated diff (`-reviewer-phase1-menu.md`, T3 integrity clean). Verified: bash -n ×7, pty probes (render/quit/EOF/non-tty fail-closed/destructive prompt-abort), gates green after each pass and re-run by Orchestrator post-review (`make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-server `menu` in POS_FLAGS nit (owning track). - **2026-08-23** — Menu Phase 1 (user-ratified decision "b"): category-neutral menu library extracted from share-suite Pattern B + four P1 tool menus. New `lib/menu-lib.sh` (169 ln): `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value` (stderr render, /dev/tty reads, EOF fail-closed rc=1, index/value→stdout); `lib/share-lib.sh` (436→318) keeps its public names as pure delegating shims so all five `pos share *` tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (or `menu` verb, `# POS_SUBCMDS:` registered, completions regen'd) on `pos media sync` (164→216: Sync-now/Preview/mp3/mp4/source-folder items), `pos system backup` (216→292: typed/service-root/plain variants, every backup behind folder-naming y/N), `pos docker compose` (366→487: ls/up/down/restart/logs/update/config items, down/restart/update confirm-gated naming the stack), `pos system schedule` (81→151: list/status/run-now(confirm)/enable/disable/editor — timer-invoked `run <name>` verb dispatch byte-identical to HEAD). INTERACTIVE_CMDS unchanged; all CLI verbs byte-compatible. Docs: POS.md ×4 rows, DEV.md lib row, SCRIPTS.md sections, AGENT_Context rows + GEN. Chain: Explorer survey (37 tools, `reportAgents/2026-08-23-explorer-pos-menu-survey.md`) → Designer classification (`-designer-pos-menu-suitability.md`: 14 MENU-FIT / 7 CONDITIONAL / 16 NO-FIT) → Builder T1/T2/T3 (`-builder-t1-menu-lib-extraction.md`, `-t2-p1-menus-media-backup.md`, `-t3-p1-menus-compose-schedule.md`; T3 discloses a mid-verify symlink clobber restored+re-verified) → Reviewer ACCEPT_WITH_NOTES over the consolidated diff (`-reviewer-phase1-menu.md`, T3 integrity clean). Verified: bash -n ×7, pty probes (render/quit/EOF/non-tty fail-closed/destructive prompt-abort), gates green after each pass and re-run by Orchestrator post-review (`make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-server `menu` in POS_FLAGS nit (owning track).
- **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic). - **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic).
- **2026-08-26** — `pos ai alias` activation rework (Option B) + `pos config` listing readability, per the 2026-08-26 Architect/Designer specs (`AgentsReport/{architect,designer}/2026-08-26-*.md`). **Alias activation:** the stale sourced-snapshot mechanism is gone — every `pos ai alias` invocation runs `_alias_sync()` (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at `~/.local/bin/<name>` chmod 755 via mktemp+mv with a `bash -n` pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy `ai-aliases.sh` generation stopped and generator-marker-guarded auto-removal with an `unalias <names>` remediation hint; loud PATH guidance when `~/.local/bin` is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; `show` gains the wrapper path; `pos-system-uninstall` sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single `_alias_table` renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). **Config readability** (`lib/config-ui.sh`, fully generic): new optional `# POS_CONFIG:` field types — `@Caption` / `@[KEY=v1|v2] Caption` group captions (condition evaluated per render via `cfg_value`; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and `*providers=<tag>` adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60120 cols, whole render block → stderr per menu-lib house pattern, honest prompt `Number to edit [r=refresh, q=quit]:`); masking/edit flow byte-compatible, no per-scope branches. `bin/pos-ai` line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (`HOME=/tmp/…`, `CONFIG_DIR` seam, argv-capturing `pos` shim) covering %q quoting round-trips (quotes/backticks/`$()`/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for `ai` AND old-format `system`; gates `make gen && make check && make lint` 0 FAIL / 0 WARN.
+67 -63
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap --> <!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 | | ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44200 | | ## 2. Directory Structure | 44204 |
| ## 3. Installation Flow | 201254 | | ## 3. Installation Flow | 205258 |
| ## 4. The `pos` CLI System | 255332 | | ## 4. The `pos` CLI System | 259336 |
| ## 5. Shared Library — `lib/common.sh` | 333364 | | ## 5. Shared Library — `lib/common.sh` | 337368 |
| ## 6. Docker Compose / ScaleTail | 365407 | | ## 6. Docker Compose / ScaleTail | 369411 |
| ## 7. Optional Apps (`apps/`) | 408437 | | ## 7. Optional Apps (`apps/`) | 412441 |
| ## 8. Entertainment Module | 438451 | | ## 8. Entertainment Module | 442455 |
| ## 9. Systemd Services | 452463 | | ## 9. Systemd Services | 456467 |
| ## 10. Configuration Files | 464490 | | ## 10. Configuration Files | 468494 |
| ## 11. Coding Conventions | 491523 | | ## 11. Coding Conventions | 495527 |
| ## 12. Development Workflow | 524576 | | ## 12. Development Workflow | 528580 |
| ## 13. Key File Quick Reference | 577648 | | ## 13. Key File Quick Reference | 581652 |
| ## 14. Common Tasks for Agents | 649682 | | ## 14. Common Tasks for Agents | 653686 |
<!-- GEN:END docmap --> <!-- GEN:END docmap -->
## 1. Project Overview ## 1. Project Overview
@@ -72,6 +72,7 @@ Linux_post_install/
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config) │ ├── 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-health # One-glance container health dashboard (exits 1 if unhealthy)
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime) │ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
│ │ [deps: docker]
│ ├── pos-docker-stack # Containers grouped by compose stack (project); standalone group; -a/--all includes stopped │ ├── pos-docker-stack # Containers grouped by compose stack (project); standalone group; -a/--all includes stopped
│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls) │ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
│ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location) │ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location)
@@ -82,9 +83,11 @@ Linux_post_install/
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) │ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select) │ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select)
│ ├── pos-media-sync # Incremental Music → USB sync (mp3/mp4, add/update only) │ ├── pos-media-sync # Incremental Music → USB sync (mp3/mp4, add/update only)
│ │ [deps: lsblk jq]
│ ├── pos-media-ytsync # Incrementally sync YouTube channels/playlists into ~/Videos │ ├── pos-media-ytsync # Incrementally sync YouTube channels/playlists into ~/Videos
│ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view │ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view
│ ├── pos-network-download # aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) │ ├── pos-network-download # aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
│ │ [deps: aria2c jq curl]
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui │ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
│ ├── pos-network-ip # Show interfaces, routes, public IP + location │ ├── pos-network-ip # Show interfaces, routes, public IP + location
│ ├── pos-network-scan # Parallel ping sweep of CIDR │ ├── pos-network-scan # Parallel ping sweep of CIDR
@@ -95,6 +98,7 @@ Linux_post_install/
│ ├── pos-share-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted) │ ├── pos-share-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted)
│ ├── pos-ssh-load-keys # Load all SSH keys into the agent │ ├── pos-ssh-load-keys # Load all SSH keys into the agent
│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg) │ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
│ │ [deps: tar]
│ ├── pos-system-firewall # Interactive UFW management │ ├── pos-system-firewall # Interactive UFW management
│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL │ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
│ ├── pos-system-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently │ ├── pos-system-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently
@@ -268,50 +272,50 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
### Available Commands ### Available Commands
| Category | Command | Script | Description |
|----------|---------|--------|-------------|
<!-- GEN:START dispatch --> <!-- GEN:START dispatch -->
| ai | alias | `pos-ai-alias` | manage AI agent aliases | | Category | Command | Script | Description | Deps | Examples |
| ai | gemini | `pos-ai-gemini` | Forward to pos ai --provider gemini (backward compat) | |----------|---------|--------|-------------|------|----------|
| ai | openrouter | `pos-ai-openrouter` | Forward to pos ai --provider openrouter (backward compat) | | ai | alias | `pos-ai-alias` | manage AI agent aliases | | |
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages | | ai | gemini | `pos-ai-gemini` | Forward to pos ai --provider gemini (backward compat) | | |
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) | | ai | openrouter | `pos-ai-openrouter` | Forward to pos ai --provider openrouter (backward compat) | | |
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages | | |
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages | | communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) | | |
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) | | communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | |
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages | | |
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) | | |
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) | | docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | |
| docker | stack | `pos-docker-stack` | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | |
| docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) | docker | |
| entertainment | config | `pos-entertainment-config` | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | docker | stack | `pos-docker-stack` | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | |
| entertainment | disable | `pos-entertainment-disable` | Disable a plugin's auto-trigger (remove it from ENABLED) | | docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | |
| entertainment | enable | `pos-entertainment-enable` | Enable an auto-trigger for a plugin on a schedule | | entertainment | config | `pos-entertainment-config` | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | |
| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via the configured notify platforms | | entertainment | disable | `pos-entertainment-disable` | Disable a plugin's auto-trigger (remove it from ENABLED) | | |
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | entertainment | enable | `pos-entertainment-enable` | Enable an auto-trigger for a plugin on a schedule | | |
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via the configured notify platforms | | |
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | | entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | |
| media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) | | media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | |
| media | ytsync | `pos-media-ytsync` | Incrementally sync YouTube channels/playlists into ~/Videos | | media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | | |
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) | lsblk jq | pos media sync --mp3 → Sync only MP3 files to USB · pos media sync --mp4 --dry-run → Preview MP4 sync without copying |
| network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) | | media | ytsync | `pos-media-ytsync` | Incrementally sync YouTube channels/playlists into ~/Videos | | |
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui | | network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | |
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location | | network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) | aria2c jq curl | pos network download add https://example.com/file.zip → Enqueue an HTTP download (auto-starts daemon) · pos network download status → Daemon health + global transfer stats · pos network download watch → Live progress view |
| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR | | network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui | | |
| share | nfs-client | `pos-share-nfs-client` | Mount NFS shares (ephemeral or persistent systemd mount units) | | network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location | | |
| share | nfs-server | `pos-share-nfs-server` | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | network | scan | `pos-network-scan` | Parallel ping sweep of CIDR | | |
| share | smb-client | `pos-share-smb-client` | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) | | share | nfs-client | `pos-share-nfs-client` | Mount NFS shares (ephemeral or persistent systemd mount units) | | |
| share | smb-server | `pos-share-smb-server` | Manage the Samba server (status, share/unshare exports, users, enable/disable) | | share | nfs-server | `pos-share-nfs-server` | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | |
| share | usb-server | `pos-share-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) | | share | smb-client | `pos-share-smb-client` | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) | | |
| ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent | | share | smb-server | `pos-share-smb-server` | Manage the Samba server (status, share/unshare exports, users, enable/disable) | | |
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) | | share | usb-server | `pos-share-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) | | |
| system | firewall | `pos-system-firewall` | Interactive UFW management | | ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent | | |
| system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) | tar | |
| system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | system | firewall | `pos-system-firewall` | Interactive UFW management | | |
| system | uninstall | `pos-system-uninstall` | Remove pos toolkit binaries, services, shell integration, config, and data | | system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | |
| | ai | `pos-ai` | AI assistant: ask, chat, sessions, capture, models, providers | | system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | |
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | system | uninstall | `pos-system-uninstall` | Remove pos toolkit binaries, services, shell integration, config, and data | | |
| | tree | `pos-tree` | Show the pos CLI command tree: categories, commands, and subcommands | | | ai | `pos-ai` | AI assistant: ask, chat, sessions, capture, models, providers | | |
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | |
| | tree | `pos-tree` | Show the pos CLI command tree: categories, commands, and subcommands | | |
<!-- GEN:END dispatch --> <!-- GEN:END dispatch -->
### Legacy Wrappers ### Legacy Wrappers
@@ -598,8 +602,8 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `features/autostart.sh` | 50 | Boot-time feature (moved from `bin/`, flag-gated service) | | `features/autostart.sh` | 50 | Boot-time feature (moved from `bin/`, flag-gated service) |
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) | | `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
<!-- GEN:START filetable --> <!-- GEN:START filetable -->
| `bin/pos` | 295 | CLI dispatcher with smart arg matching + logging + category help | | `bin/pos` | 302 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos-ai-alias` | 542 | manage AI agent aliases | | `bin/pos-ai-alias` | 658 | manage AI agent aliases |
| `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) | | `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) |
| `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) | | `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) |
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages | | `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
@@ -609,7 +613,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) | | `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| `bin/pos-docker-compose` | 487 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-compose` | 487 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
| `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) |
| `bin/pos-docker-ps` | 125 | Enhanced container overview (health, IPs, ports, uptime) | | `bin/pos-docker-ps` | 126 | Enhanced container overview (health, IPs, ports, uptime) |
| `bin/pos-docker-stack` | 101 | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | `bin/pos-docker-stack` | 101 | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped |
| `bin/pos-docker-vbox` | 1125 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | `bin/pos-docker-vbox` | 1125 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
| `bin/pos-entertainment-config` | 143 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | `bin/pos-entertainment-config` | 143 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
@@ -619,10 +623,10 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state | | `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state |
| `bin/pos-media-mp3` | 86 | Download audio as MP3 (yt-dlp) | | `bin/pos-media-mp3` | 86 | Download audio as MP3 (yt-dlp) |
| `bin/pos-media-mp4` | 132 | Download video as MP4 (smart/interactive format select) | | `bin/pos-media-mp4` | 132 | Download video as MP4 (smart/interactive format select) |
| `bin/pos-media-sync` | 216 | Incremental Music → USB sync (mp3/mp4, add/update only) | | `bin/pos-media-sync` | 219 | Incremental Music → USB sync (mp3/mp4, add/update only) |
| `bin/pos-media-ytsync` | 1191 | Incrementally sync YouTube channels/playlists into ~/Videos | | `bin/pos-media-ytsync` | 1191 | Incrementally sync YouTube channels/playlists into ~/Videos |
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
| `bin/pos-network-download` | 1104 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) | | `bin/pos-network-download` | 1108 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui | | `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location | | `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
| `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR | | `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR |
@@ -632,11 +636,11 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-share-smb-server` | 441 | Manage the Samba server (status, share/unshare exports, users, enable/disable) | | `bin/pos-share-smb-server` | 441 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
| `bin/pos-share-usb-server` | 362 | USB Redirector server control (--ls, --share; prompts when args omitted) | | `bin/pos-share-usb-server` | 362 | USB Redirector server control (--ls, --share; prompts when args omitted) |
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent | | `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
| `bin/pos-system-backup` | 292 | Encrypted (AES-256) folder snapshots (tar + gpg) | | `bin/pos-system-backup` | 293 | Encrypted (AES-256) folder snapshots (tar + gpg) |
| `bin/pos-system-firewall` | 325 | Interactive UFW management | | `bin/pos-system-firewall` | 325 | Interactive UFW management |
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
| `bin/pos-system-uninstall` | 415 | Remove pos toolkit binaries, services, shell integration, config, and data | | `bin/pos-system-uninstall` | 435 | Remove pos toolkit binaries, services, shell integration, config, and data |
| `bin/pos-ai` | 680 | AI assistant: ask, chat, sessions, capture, models, providers | | `bin/pos-ai` | 680 | AI assistant: ask, chat, sessions, capture, models, providers |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands | | `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
+4 -2
View File
@@ -75,9 +75,9 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
| `pos ai alias edit [name]` | Edits an existing alias (pick from list or pass the name): provider/session/prompt are re-prompted pre-filled with the current values — Enter keeps the current value; a per-field changed/unchanged summary is confirmed (default yes) before saving; nothing is written if nothing changed | | `pos ai alias edit [name]` | Edits an existing alias (pick from list or pass the name): provider/session/prompt are re-prompted pre-filled with the current values — Enter keeps the current value; a per-field changed/unchanged summary is confirmed (default yes) before saving; nothing is written if nothing changed |
| `pos ai alias remove [name]` | Removes an alias (pick from list or pass the name); the confirmation defaults to **no** and removal cannot be undone | | `pos ai alias remove [name]` | Removes an alias (pick from list or pass the name); the confirmation defaults to **no** and removal cannot be undone |
| `pos ai alias list` | Non-interactive: prints all aliases as a Name/Provider/Session/Prompt table (prompts truncated at 42 chars) | | `pos ai alias list` | Non-interactive: prints all aliases as a Name/Provider/Session/Prompt table (prompts truncated at 42 chars) |
| `pos ai alias show <name>` | Prints one alias's details including the resolved command: `pos ai <provider> ask --session <session>[ --system '<prompt>']` | | `pos ai alias show <name>` | Prints one alias's details including the wrapper path and the resolved command: `pos ai <provider> ask --session <session>[ --system '<prompt>']` |
Alias storage: records live in `~/.config/linux_post_install/ai-aliases.env` — one `name\|provider\|session\|system_prompt` line per alias, chmod 600, managed by the tool (do not hand-edit). Every create/edit/remove also rewrites `~/.config/linux_post_install/ai-aliases.sh` (chmod 644), which defines one shell alias per record — `alias <name>='pos ai <provider> ask --session <session>[ --system '<prompt>']'`. An empty session falls back to the alias name, prompts are single-quote-escaped, and the generated file is syntax-checked before it replaces the previous version. Source this file from your shell rc (e.g. `.bashrc`) to activate the aliases in new shells. Alias storage & activation: records live in `~/.config/linux_post_install/ai-aliases.env` — one `name\|provider\|session\|system_prompt` line per alias, chmod 600, managed by the tool (do not hand-edit); an empty session falls back to the alias name. **Activation needs no shell sourcing**: every `pos ai alias` invocation syncs the ENV file (the single source of truth) against executable wrapper scripts at `~/.local/bin/<name>` (chmod 755) — missing or changed wrappers are atomically rewritten, wrappers pos owns but ENV no longer lists are deleted, and hand-edited wrappers are healed. A wrapper re-reads its bytes on every run, so an edit is **live on the next invocation** (no reload), and the scripts work identically in interactive shells, scripts, cron, and non-login ssh sessions (`~/.local/bin` must stay on `PATH` — a loud warning with a copy-paste fix appears when it isn't). Create refuses name collisions: a foreign file at `~/.local/bin/<name>` and names resolving to another binary on `PATH` are never overwritten. The legacy generated `~/.config/linux_post_install/ai-aliases.sh` is no longer written; on the next invocation pos removes it automatically (marker-guarded — a foreign-content file is left untouched with a warning) and prints an `unalias <names>` remediation hint for already-running shells (or simply start a new shell).
Backward compatibility: `pos ai gemini` and `pos ai openrouter` still work as shorthands for `pos ai --provider gemini` and `pos ai --provider openrouter`. Backward compatibility: `pos ai gemini` and `pos ai openrouter` still work as shorthands for `pos ai --provider gemini` and `pos ai --provider openrouter`.
@@ -485,6 +485,8 @@ Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libfl
`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). `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).
Headers may also declare **group captions**: `@Caption` starts a visual group, and `@[KEY=v1|v2] Caption` makes the group conditional — while `KEY`'s current value matches none of the listed alternatives, the group stays visible but dimmed with a textual reason (`— inactive while KEY=…`), so row numbering never changes mid-session. Wildcards can be tagged: `*providers=<tag>` pulls keys from a single AI provider adapter instead of all of them. The listing renders uniformly for every scope (bold title/keys, dim numbers/examples/placeholders, word-wrapped descriptions); at the prompt type a number to edit, `r` to refresh, or `q` to quit.
| Command | Purpose | | Command | Purpose |
|---------|---------| |---------|---------|
| `pos config` | Scope picker (on a TTY), otherwise the scope list | | `pos config` | Scope picker (on a TTY), otherwise the scope list |
+16 -1
View File
@@ -12,6 +12,12 @@ Everything that runs during the bootstrap install: `install.sh`, `preinstall.sh`
- [lib/common.sh — shared library](#libcommonsh--shared-library) - [lib/common.sh — shared library](#libcommonsh--shared-library)
- [lib/flags.sh — feature flags](#libflagssh--feature-flags) - [lib/flags.sh — feature flags](#libflagssh--feature-flags)
- [lib/notify.sh — multi-platform alerting](#libnotifysh--multi-platform-alerting) - [lib/notify.sh — multi-platform alerting](#libnotifysh--multi-platform-alerting)
- [lib/entertainment-lib.sh — entertainment module](#libentertainmentlibsh--entertainment-module)
- [lib/user-timers-lib.sh — shared systemd user timers](#libusertimerslibsh--shared-systemd-user-timers)
- [lib/usb-lib.sh — shared USB-storage detection](#libusblibsh--shared-usb-storage-detection)
- [lib/share-lib.sh — share-suite domain layer + compat shims](#sharelibsh--share-suite-domain-layer--compat-shims)
- [lib/menu-lib.sh — category-neutral menu primitives](#libmenulibsh--category-neutral-menu-primitives)
- [lib/registry.sh — tool metadata query API](#libregistrysh--tool-metadata-query-api)
- [features/autostart.sh — boot-time feature](#featuresautostartsh--boot-time-feature) - [features/autostart.sh — boot-time feature](#featuresautostartsh--boot-time-feature)
- [features/usb-automount.sh — USB automount feature](#featuresusb-automountsh--usb-automount-feature) - [features/usb-automount.sh — USB automount feature](#featuresusb-automountsh--usb-automount-feature)
- [x64_bin/ — precompiled binaries](#x64_bin--precompiled-binaries) - [x64_bin/ — precompiled binaries](#x64_bin--precompiled-binaries)
@@ -38,7 +44,7 @@ The phases:
| # | Phase | Script/action | | # | Phase | Script/action |
|---|-------|----------------| |---|-------|----------------|
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp | | 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
| 2 | scripts | Copies `bin/*``/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` + `lib/usb-lib.sh` + `lib/share-lib.sh` + `lib/menu-lib.sh``/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) | | 2 | scripts | Copies `bin/*``/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` + `lib/usb-lib.sh` + `lib/share-lib.sh` + `lib/menu-lib.sh` + `lib/registry.sh``/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd | | 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` | | 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
| 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) | | 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) |
@@ -228,6 +234,15 @@ Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib
--- ---
## lib/registry.sh — tool metadata query API
**File:** `lib/registry.sh` (installed to `/usr/local/bin/registry.sh`)
**Purpose:** the one query API over the tools' `# POS_*:` metadata headers, so consumers source it instead of re-implementing sed/grep header scans. `reg_scan [dir]` reads every executable `pos-*` file once — sorted under `LC_ALL=C`, and cheap enough to call lazily (plain dispatch paths skip it entirely); each tool's key is its filename after `pos-` with the category split off at the first dash (category-less tools carry an empty category). The populated stores serve `reg_list`, `reg_categories`, `reg_tools_in` and `reg_lookup <tool> <field>` with fields `cat|desc|flags|subcmds|deps|examples` (`deps`/`examples` come from the optional `# POS_DEPS:` / `# POS_EXAMPLES:` headers); the multi-line `# POS_CONFIG:` registry gets its own helpers (`reg_config_scopes`, `reg_config_keys`, `reg_config_envfile`); `reg_each <callback>` iterates every tool calling `cb(category, tool_key, description)`; `reg_tool_exists` is the membership probe. Like `lib/config-ui.sh` it defines guarded `log`/`warn`/`err` fallbacks so it sources cleanly without `lib/common.sh`; no shebang and never executed (installed 644).
Sourced by `bin/pos-tree` (tree rendering incl. the `[deps: …]` annotations) and by `bin/pos` `_pos_category_help()` for `pos <category> --help` (lazy load there, so plain dispatch never pays the scan cost). `scripts/gen-docs.sh` predates the registry and keeps parsing the same headers independently for its generated blocks; new consumers should prefer the registry.
---
## features/autostart.sh — boot-time feature ## features/autostart.sh — boot-time feature
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`) **File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
+11
View File
@@ -275,6 +275,17 @@ Recommended fix: add rows to the POS.md command table (and cross-check HOWTO for
Verification: lint WARNs gone; `grep` shows each tool in POS.md. Verification: lint WARNs gone; `grep` shows each tool in POS.md.
Fix (2026-08-14): the tools were documented by command name but not by filename (the lint references basenames). Added `**File:** bin/pos-config` (config section), `**File:** bin/pos-tree` (tree section), and a file list on the entertainment section header covering `bin/pos-entertainment-{config,enable,disable,status}`. Verified: lint 0 WARN. HOWTO already covers the entertainment group via `pos entertainment *` command forms. Fix (2026-08-14): the tools were documented by command name but not by filename (the lint references basenames). Added `**File:** bin/pos-config` (config section), `**File:** bin/pos-tree` (tree section), and a file list on the entertainment section header covering `bin/pos-entertainment-{config,enable,disable,status}`. Verified: lint 0 WARN. HOWTO already covers the entertainment group via `pos entertainment *` command forms.
### M-024
Status: VERIFIED
Severity: LOW
Category: docs
Files: AGENTS.md:17-18; DOC/SCRIPTS.md (Phase-2 lib list, TOC, new lib section)
Evidence: the command-registry feature landed (`lib/registry.sh`, 199 lines; optional `# POS_DEPS:`/`# POS_EXAMPLES:` headers already codified in `templates/pos-tool.sh:13-14` and `DOC/DEV.md:126-134`) but three docs kept describing the old reality: AGENTS.md Quick facts enumerated only `POS_FLAGS`/`SUBCMDS`/`CONFIG` with no mention of the shared query API; DOC/SCRIPTS.md's Phase-2 lib list omitted `registry.sh` and had no section for it (its TOC also lacked five pre-existing lib sections).
Expected: docs describe what IS — code + `# POS:` headers are ground truth (Phase 0 rule 4).
Recommended fix: sync the three drifted docs to implemented reality; no code/template/completion changes.
Verification: `make gen` produces zero diff beyond pre-existing work; `make check` green; `make lint` 0 FAIL / 0 WARN; `grep -n "POS_DEPS"` hits AGENTS.md, DEV.md, SCRIPTS.md.
Fix (2026-08-26): template `templates/pos-tool.sh` now documents the optional `# POS_DEPS:`/`# POS_EXAMPLES:` headers (pre-existing); `lib/registry.sh` added as the shared query API over all `POS_*` headers (`reg_scan` + `reg_list`/`reg_lookup`/…) — AGENTS.md Tool-model + Categories bullets updated, DOC/SCRIPTS.md got the lib-list row (install.sh:143 order), a per-lib reference section, and a completed TOC. Consumers were already migrated (`bin/pos-tree`, `bin/pos` `_pos_category_help()`); lint unchanged (0 FAIL / 0 WARN).
### P3 — intentional / legacy (no action) ### P3 — intentional / legacy (no action)
- install.sh:123,135,155,185 — installer writes to /usr/local/bin are its purpose; no seam needed (lint excludes install scripts). - install.sh:123,135,155,185 — installer writes to /usr/local/bin are its purpose; no seam needed (lint excludes install scripts).
- network-download RPC_SECRET at :150 — generated at runtime (`/dev/urandom`), not a committed secret. - network-download RPC_SECRET at :150 — generated at runtime (`/dev/urandom`), not a committed secret.
+19 -12
View File
@@ -66,21 +66,27 @@ _pos_category_exists() {
} }
_pos_category_help() { _pos_category_help() {
local cat="$1" f local cat="$1" s s2 extra d sc deps
local files=() s d # Lazy registry load — plain dispatch paths never pay the scan cost.
for f in "$self"/pos-"$cat"-*; do source "$self/../lib/registry.sh" 2>/dev/null || source "$self/registry.sh"
[ -x "$f" ] || continue reg_scan "$self"
files+=("${f##*/pos-$cat-}")
done local files=()
local t
while IFS= read -r t; do
[ -n "$t" ] || continue
files+=("${t#"$cat"-}")
done < <(reg_tools_in "$cat")
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u) mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u)
local -A desc subcmds local -A desc subcmds _deps
local sc
for s in "${files[@]}"; do for s in "${files[@]}"; do
d="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)" d="$(reg_lookup "$cat-$s" desc)"
[ -n "$d" ] && desc["$s"]="${d#*— }" [ -n "$d" ] && desc["$s"]="$d"
sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)" sc="$(reg_lookup "$cat-$s" subcmds)"
[ -n "$sc" ] && subcmds["$s"]="$sc" [ -n "$sc" ] && subcmds["$s"]="$sc"
deps="$(reg_lookup "$cat-$s" deps)"
[ -n "$deps" ] && _deps["$s"]="$deps"
done done
# Nested sub-tools: pos-<cat>-<a>-<b> lists "b" under <a>. # Nested sub-tools: pos-<cat>-<a>-<b> lists "b" under <a>.
@@ -113,6 +119,7 @@ _pos_category_help() {
done done
[ "$is_nested" -eq 1 ] && continue [ "$is_nested" -eq 1 ] && continue
printf ' %-28s%s\n' "$s" "${desc[$s]:-}" printf ' %-28s%s\n' "$s" "${desc[$s]:-}"
[ -n "${_deps[$s]:-}" ] && printf ' [deps: %s]\n' "${_deps[$s]}"
for c in ${subcmds[$s]:-}; do for c in ${subcmds[$s]:-}; do
printf ' %s %s\n' "$s" "$c" printf ' %s %s\n' "$s" "$c"
done done
@@ -259,7 +266,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; } log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
# Commands that read from stdin interactively — only log invocation # Commands that read from stdin interactively — only log invocation
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup system-uninstall share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter system-schedule entertainment-config config" INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup system-uninstall share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter ai-alias system-schedule entertainment-config config"
for ((i=n-1; i>=0; i--)); do for ((i=n-1; i>=0; i--)); do
cmd="pos" cmd="pos"
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
# POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers # POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers
# POS_SUBCMDS: ask chat sessions capture models providers # POS_SUBCMDS: ask chat sessions capture models providers
# POS_FLAGS: --provider --model --session --system --full --last # POS_FLAGS: --provider --model --session --system --full --last
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | *providers | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset) # POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | @[AI_PROVIDER=gemini|] Gemini | *providers=gemini | @[AI_PROVIDER=openrouter] OpenRouter | *providers=openrouter | @General | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset)
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
+180 -64
View File
@@ -48,35 +48,134 @@ _alias_save() {
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"
} }
_alias_regen() { # Build the pos-ai command for an alias with the prompt safely quoted as
local tmp # ONE shell word (printf %q) — shared by regen (stored form) and show
tmp="$(mktemp)" # (copy-pasteable display form). Empty prompt → no --system fragment.
printf '%s\n' "#!/usr/bin/env bash" >"$tmp" _alias_quote_cmd() {
printf '%s\n\n' "# Auto-generated by pos ai alias — do not hand-edit." >>"$tmp" local provider="$1" session="$2" prompt="${3:-}" q_prompt
if [ -f "$ENV_FILE" ]; then printf -v q_prompt '%q' "$prompt"
while IFS='|' read -r name provider session prompt _rest; do printf 'pos ai %s ask --session %s' "$provider" "$session"
[[ "$name" =~ ^[[:space:]]*# ]] && continue [ -n "$prompt" ] && printf ' --system %s' "$q_prompt"
[[ -z "${name// /}" ]] && continue return 0
name="${name## }"; name="${name%% }" }
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
provider="${provider## }"; provider="${provider%% }" # ── Activation artifacts (Option B) ────────────────────────────
session="${session## }"; session="${session%% }" # ENV stays the single source of truth; each alias is materialized as an
[ -z "$session" ] && session="$name" # executable wrapper script at ~/.local/bin/<name>. Every invocation re-reads
local escaped_prompt="${prompt//\'/\'\\\'\'}" # current bytes, so a stale snapshot (the old sourced-alias failure mode) is
printf "alias %s='pos ai %s ask --session %s" "$name" "$provider" "$session" >>"$tmp" # impossible by construction. No shell sourcing of any kind.
if [ -n "$prompt" ]; then
printf " --system '%s'" "$escaped_prompt" >>"$tmp" _wrapper_path() {
fi printf '%s/.local/bin/%s' "$HOME" "$1"
printf "'\n" >>"$tmp" }
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
# Ownership test: line 2 must carry our generator marker. Files failing this
# test are NEVER overwritten or deleted.
_alias_owned() {
[ -f "$1" ] && sed -n '2p' "$1" 2>/dev/null | grep -q 'Managed by pos ai alias'
}
# Render one wrapper to stdout (args: name provider session prompt).
# The exec line reuses _alias_quote_cmd's double-%q mechanics so the prompt
# lands as exactly ONE shell word; "$@" passes user args through.
_wrapper_render() {
local name="$1" provider="$2" session="$3" prompt="${4:-}"
cat <<WRAPPER_EOF
#!/usr/bin/env bash
# Managed by pos ai alias — regenerated automatically; hand-edits are overwritten.
# Alias: ${name} | provider: ${provider} | session: ${session}
set -euo pipefail
exec $(_alias_quote_cmd "$provider" "$session" "$prompt") "\$@"
WRAPPER_EOF
}
# Atomically install/refresh one wrapper. Skips the write when the rendered
# content already matches (stable mtimes → sync idempotence is observable).
# Pre-commit validation: bash -n on the rendered file; failure keeps previous.
_wrapper_install() { # name provider session prompt
local path="$(_wrapper_path "$1")" tmp
tmp="$(mktemp "${HOME}/.local/bin/.pos-alias.XXXXXX")"
_wrapper_render "$1" "$2" "$3" "$4" >"$tmp"
if cmp -s "$tmp" "$path"; then
rm -f "$tmp"
return 0
fi fi
if ! bash -n "$tmp" 2>/dev/null; then if ! bash -n "$tmp" 2>/dev/null; then
warn "Generated alias file has syntax errors — keeping previous version" warn "Wrapper for '$1' failed syntax check — keeping previous version" >&2
rm -f "$tmp" rm -f "$tmp"
return 1 return 1
fi fi
mv "$tmp" "$SH_FILE" mv "$tmp" "$path"
chmod 644 "$SH_FILE" chmod 755 "$path"
}
# rc 0 iff ~/.local/bin is on PATH.
_alias_check_path() {
case ":$PATH:" in
*":$HOME/.local/bin:"*) return 0 ;;
*) return 1 ;;
esac
}
# Legacy ~/.config/.../ai-aliases.sh retirement: activation moved to wrapper
# scripts, and a stale sourced alias would shadow them (interactive bash gives
# aliases precedence over PATH lookups). Marker-guarded auto-remove only —
# foreign files are warned about and left untouched.
_alias_retire_legacy_sh() {
[ -f "$SH_FILE" ] || return 0
if ! head -n 3 "$SH_FILE" | grep -q 'Auto-generated by pos ai alias'; then
warn "$SH_FILE was not generated by pos ai alias — left untouched; review manually"
return 0
fi
local stale
stale="$(sed -n 's/^alias \([A-Za-z_][A-Za-z0-9_-]*\)=.*/\1/p' "$SH_FILE" | tr '\n' ' ')"
stale="${stale% }"
rm -f "$SH_FILE"
{
echo "[!] Alias activation moved to executable scripts in ~/.local/bin/ — legacy file removed: $SH_FILE"
[ -n "$stale" ] && echo " Stale sourced aliases shadow the new scripts until cleaned — run: unalias $stale"
echo " (or simply start a new shell)"
} >&2
return 0
}
# Two-way reconciliation on EVERY invocation:
# forward: each ENV entry → render-diff-install (first-run migration,
# create/edit/remove consistency, silent heal of hand-edited wrappers)
# reverse: owned wrappers whose name is not in ENV → deleted (covers remove,
# manual ENV edits, and the empty-set case)
# plus: legacy .sh retirement; PATH guidance when owned wrappers exist but
# ~/.local/bin is absent from PATH (wrappers are written regardless).
_alias_sync() {
_alias_load
local bin_dir="${HOME}/.local/bin" i name f base match any=0
mkdir -p "$bin_dir"
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
_wrapper_install "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}" || :
done
for f in "$bin_dir"/*; do
[ -f "$f" ] || continue
_alias_owned "$f" || continue
base="${f##*/}"
match=0
for name in ${_ALIAS_NAMES[@]+"${_ALIAS_NAMES[@]}"}; do
[ "$base" = "$name" ] && { match=1; break; }
done
[ "$match" -eq 1 ] || rm -f "$f"
done
_alias_retire_legacy_sh
if ! _alias_check_path; then
for f in "$bin_dir"/*; do
[ -f "$f" ] && _alias_owned "$f" && { any=1; break; }
done
if [ "$any" -eq 1 ]; then
warn "~/.local/bin is not on your PATH — alias scripts will not resolve by name."
warn " Fix now: export PATH=\"\$HOME/.local/bin:\$PATH\""
warn " Persist it: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.profile"
fi
fi
return 0
} }
_alias_provider_pick() { _alias_provider_pick() {
@@ -118,23 +217,27 @@ _alias_prompt_truncate() {
# ── Non-interactive output ───────────────────────────────────── # ── Non-interactive output ─────────────────────────────────────
_alias_list() { # SINGLE alias-table renderer — used by `list` (stdout) and the menu
_alias_load # pre-render (inside its stderr display block). One source of truth for the
local count=${#_ALIAS_NAMES[@]} # grid so the two contexts can never drift or duplicate each other.
printf 'Aliases (%d):\n' "$count" _alias_table() {
local count=${#_ALIAS_NAMES[@]} i
[ "$count" -eq 0 ] && return 0 [ "$count" -eq 0 ] && return 0
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt" printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt"
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \ printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------" "------------------------------------------"
local i
for ((i = 0; i < count; i++)); do for ((i = 0; i < count; i++)); do
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \ printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")" "${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")"
done done
} }
_alias_list() {
printf 'Aliases (%d):\n' "${#_ALIAS_NAMES[@]}"
_alias_table
}
_alias_show() { _alias_show() {
_alias_load
local idx local idx
idx="$(_alias_find "$1")" idx="$(_alias_find "$1")"
[ "$idx" = "-1" ] && err "Alias '$1' not found" [ "$idx" = "-1" ] && err "Alias '$1' not found"
@@ -144,14 +247,15 @@ _alias_show() {
printf ' %-12s %s\n' "Alias:" "$name" printf ' %-12s %s\n' "Alias:" "$name"
printf ' %-12s %s\n' "Provider:" "$provider" printf ' %-12s %s\n' "Provider:" "$provider"
printf ' %-12s %s\n' "Session:" "$session" printf ' %-12s %s\n' "Session:" "$session"
printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}" if _alias_check_path; then
# Show the resolved command printf ' %-12s %s\n' "Wrapper:" "$(_wrapper_path "$name")"
local cmd="pos ai $provider ask --session $session" else
if [ -n "$prompt" ]; then printf ' %-12s %s\n' "Wrapper:" "(not installed — ~/.local/bin not on PATH)"
local escaped="${prompt//\'/\'\\\'\'}"
cmd="$cmd --system '$escaped'"
fi fi
printf ' %-12s %s\n' "Command:" "$cmd" printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}"
# Show the resolved command (same quoting mechanism as the generated
# wrapper — what users copy from here pastes into a shell verbatim)
printf ' %-12s %s\n' "Command:" "$(_alias_quote_cmd "$provider" "$session" "$prompt")"
} }
# ── Interactive: main menu ───────────────────────────────────── # ── Interactive: main menu ─────────────────────────────────────
@@ -164,21 +268,8 @@ _alias_menu() {
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}" echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}"
else else
local count=${#_ALIAS_NAMES[@]} i _alias_table
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt" printf ' %d alias(es)\n' "${#_ALIAS_NAMES[@]}"
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------"
for ((i = 0; i < count; i++)); do
local p="${_ALIAS_PROMPTS[$i]}"
if [ ${#p} -gt 42 ]; then
p="${p:0:42}…"
fi
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
"${_ALIAS_SESSIONS[$i]}" "$p"
done
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
"------------------------------------------"
printf ' %d alias(es)\n' "$count"
fi fi
echo >&2 echo >&2
} >&2 } >&2
@@ -189,7 +280,10 @@ _alias_menu() {
1) _alias_create ;; 1) _alias_create ;;
2) _alias_edit ;; 2) _alias_edit ;;
3) _alias_remove ;; 3) _alias_remove ;;
4) _alias_list ;; 4) : ;; # List aliases — the loop's pre-render above IS the current
# table (single renderer, redrawn fresh every iteration);
# option 4 returns to the loop for a fresh render instead
# of printing a second copy (dup-table bug fix).
esac esac
done done
} }
@@ -220,6 +314,17 @@ _alias_create() {
[ -n "$preset_name" ] && return 1 [ -n "$preset_name" ] && return 1
name=""; continue name=""; continue
fi fi
# Collision refusals (never clobber foreign files or real binaries):
# 1. wrapper exists WITH our marker → fine, sync regenerates it
# 2. file exists WITHOUT marker → refuse
# 3. name resolves to another binary on PATH → refuse, naming it
local wpath
wpath="$(_wrapper_path "$name")"
if [ -e "$wpath" ]; then
_alias_owned "$wpath" || err "File '~/.local/bin/$name' already exists and was not created by pos ai alias — pick another name"
elif command -v "$name" >/dev/null 2>&1; then
err "'$name' already exists on PATH as $(command -v "$name") — pick another name"
fi
break break
done done
@@ -284,8 +389,9 @@ _alias_create() {
_ALIAS_SESSIONS+=("$session") _ALIAS_SESSIONS+=("$session")
_ALIAS_PROMPTS+=("$prompt") _ALIAS_PROMPTS+=("$prompt")
_alias_save _alias_save
_alias_regen _alias_sync
log "Alias '$name' created. Reload shell: source ~/.bashrc" >&2 log "Alias '$name' created." >&2
log "Available immediately: $(_wrapper_path "$name")" >&2
} }
# ── Interactive: edit ────────────────────────────────────────── # ── Interactive: edit ──────────────────────────────────────────
@@ -419,8 +525,8 @@ _alias_edit() {
_ALIAS_SESSIONS[$idx]="$new_session" _ALIAS_SESSIONS[$idx]="$new_session"
_ALIAS_PROMPTS[$idx]="$new_prompt" _ALIAS_PROMPTS[$idx]="$new_prompt"
_alias_save _alias_save
_alias_regen _alias_sync
log "Alias '$name' updated." >&2 log "Alias '$name' updated — the change is live on next invocation." >&2
} }
# ── Interactive: remove ──────────────────────────────────────── # ── Interactive: remove ────────────────────────────────────────
@@ -484,8 +590,9 @@ _alias_remove() {
_ALIAS_SESSIONS=("${new_sessions[@]+"${new_sessions[@]}"}") _ALIAS_SESSIONS=("${new_sessions[@]+"${new_sessions[@]}"}")
_ALIAS_PROMPTS=("${new_prompts[@]+"${new_prompts[@]}"}") _ALIAS_PROMPTS=("${new_prompts[@]+"${new_prompts[@]}"}")
_alias_save _alias_save
_alias_regen _alias_sync
log "Alias '$name' removed." >&2 log "Alias '$name' removed — script deleted from $(_wrapper_path "$name")." >&2
log "If the name still autocompletes stale in this shell, run: hash -r" >&2
} }
# ── show <name> ─────────────────────────────────────────────── # ── show <name> ───────────────────────────────────────────────
@@ -510,6 +617,12 @@ Subcommands:
list List all aliases (non-interactive, machine-readable) list List all aliases (non-interactive, machine-readable)
show <name> Show one alias's details show <name> Show one alias's details
Activation: every alias is materialized as an executable script at
~/.local/bin/<name>, synced automatically on every invocation — no shell
sourcing required. Changes are live on the next invocation, and the
scripts work identically in interactive shells, scripts, cron, and
non-login ssh sessions.
Options: Options:
-h|--help Show this help. -h|--help Show this help.
@@ -526,17 +639,20 @@ EOF
} }
# ── Main dispatch ────────────────────────────────────────────── # ── Main dispatch ──────────────────────────────────────────────
# Every subcommand syncs first: artifacts always equal ENV truth before any
# subcommand logic runs (migration, healing, retraction — all automatic).
case "${1:-}" in case "${1:-}" in
-h|--help) usage ;; -h|--help) usage ;;
create) shift; _alias_create "${1:-}" ;; create) shift; _alias_sync; _alias_create "${1:-}" ;;
edit) shift; _alias_edit "${1:-}" ;; edit) shift; _alias_sync; _alias_edit "${1:-}" ;;
remove) shift; _alias_remove "${1:-}" ;; remove) shift; _alias_sync; _alias_remove "${1:-}" ;;
list) _alias_list ;; list) _alias_sync; _alias_list ;;
show) show)
[ -n "${2:-}" ] || err "Usage: pos ai alias show <name>" [ -n "${2:-}" ] || err "Usage: pos ai alias show <name>"
_alias_sync
_alias_show "$2" _alias_show "$2"
;; ;;
"") _alias_menu ;; "") _alias_sync; _alias_menu ;;
*) err "Unknown subcommand '$1' (use -h for help)" ;; *) err "Unknown subcommand '$1' (use -h for help)" ;;
esac esac
+1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# POS: docker ps — Enhanced container overview (health, IPs, ports, uptime) # POS: docker ps — Enhanced container overview (health, IPs, ports, uptime)
# POS_DEPS: docker
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
usage() { usage() {
+3
View File
@@ -3,6 +3,9 @@ set -euo pipefail
# POS: media sync — Incremental Music → USB sync (mp3/mp4, add/update only) # POS: media sync — Incremental Music → USB sync (mp3/mp4, add/update only)
# POS_FLAGS: --mp3 --mp4 --source --dry-run # POS_FLAGS: --mp3 --mp4 --source --dry-run
# POS_SUBCMDS: menu # POS_SUBCMDS: menu
# POS_DEPS: lsblk jq
# POS_EXAMPLES: pos media sync --mp3 | Sync only MP3 files to USB
# POS_EXAMPLES: pos media sync --mp4 --dry-run | Preview MP4 sync without copying
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" 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" source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
+4
View File
@@ -3,6 +3,10 @@ set -euo pipefail
# POS: network download — aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) # POS: network download — aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
# POS_SUBCMDS: start stop status add torrent metalink list info files peers pause resume remove purge move limit set watch restart retry replace menu # POS_SUBCMDS: start stop status add torrent metalink list info files peers pause resume remove purge move limit set watch restart retry replace menu
# POS_FLAGS: --dir --out --split --seed --force --upload --gid --tmux # POS_FLAGS: --dir --out --split --seed --force --upload --gid --tmux
# POS_DEPS: aria2c jq curl
# POS_EXAMPLES: pos network download add https://example.com/file.zip | Enqueue an HTTP download (auto-starts daemon)
# POS_EXAMPLES: pos network download status | Daemon health + global transfer stats
# POS_EXAMPLES: pos network download watch | Live progress view
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh" source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
+1
View File
@@ -4,6 +4,7 @@ set -euo pipefail
# POS_FLAGS: --service --no-encrypt # POS_FLAGS: --service --no-encrypt
# POS_SUBCMDS: menu # POS_SUBCMDS: menu
# POS_CONFIG: notify | notify.env | NOTIFY_PLATFORM=:Comma-separated notify platforms (default telegram) — shared by backup, firewall, share nfs client/server # POS_CONFIG: notify | notify.env | NOTIFY_PLATFORM=:Comma-separated notify platforms (default telegram) — shared by backup, firewall, share nfs client/server
# POS_DEPS: tar
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" 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" source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
+20
View File
@@ -95,6 +95,15 @@ scan_tier1() {
# User-local binaries # User-local binaries
[ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && found+=("$HOME/.local/bin/pos-ai-hook.sh") [ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && found+=("$HOME/.local/bin/pos-ai-hook.sh")
# pos ai alias wrapper scripts (marker-managed)
if [ -d "$HOME/.local/bin" ]; then
local awf
for awf in "$HOME/.local/bin"/*; do
[ -f "$awf" ] || continue
grep -q 'Managed by pos ai alias' "$awf" 2>/dev/null && found+=("$awf")
done
fi
# Completion file # Completion file
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash") [ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash")
@@ -260,6 +269,17 @@ remove_tier1() {
# User-local binaries # User-local binaries
[ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && { rm -f "$HOME/.local/bin/pos-ai-hook.sh" && count=$((count+1)); } [ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && { rm -f "$HOME/.local/bin/pos-ai-hook.sh" && count=$((count+1)); }
# pos ai alias wrapper scripts (marker-managed only — foreign files untouched)
if [ -d "$HOME/.local/bin" ]; then
local arwf
for arwf in "$HOME/.local/bin"/*; do
[ -f "$arwf" ] || continue
if grep -q 'Managed by pos ai alias' "$arwf" 2>/dev/null; then
rm -f "$arwf" && count=$((count+1))
fi
done
fi
# Completion file # Completion file
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && { rm -f /usr/local/share/bash-completion/completions/pos.bash && count=$((count+1)); } [ -f /usr/local/share/bash-completion/completions/pos.bash ] && { rm -f /usr/local/share/bash-completion/completions/pos.bash && count=$((count+1)); }
+12 -6
View File
@@ -4,6 +4,7 @@ set -euo pipefail
# POS_FLAGS: --depth # POS_FLAGS: --depth
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
source "$(dirname "$0")/../lib/registry.sh" 2>/dev/null || source "$(dirname "$0")/registry.sh"
usage() { usage() {
cat <<EOF cat <<EOF
@@ -44,12 +45,17 @@ add() {
meta["$path"]="$type|$desc" meta["$path"]="$type|$desc"
} }
for f in "$self"/pos-*; do reg_scan "$self"
[ -x "$f" ] || continue
name="${f##*/pos-}" for tool_key in $(reg_list); do
desc="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f")" cat="$(reg_lookup "$tool_key" cat)"
desc="${desc#*— }" desc="$(reg_lookup "$tool_key" desc)"
sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")" sc="$(reg_lookup "$tool_key" subcmds)"
deps="$(reg_lookup "$tool_key" deps)"
name="${tool_key}"
if [ -n "$deps" ]; then
desc="$desc [deps: $deps]"
fi
words=(${name//-/ }) words=(${name//-/ })
parent="/" parent="/"
for ((i=0; i<${#words[@]}; i++)); do for ((i=0; i<${#words[@]}; i++)); do
+1 -1
View File
@@ -140,7 +140,7 @@ if should_run 2 scripts; then
done done
lib_count=0 lib_count=0
lib_names="" lib_names=""
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh; do for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh registry.sh; do
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf" run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
lib_count=$((lib_count + 1)) lib_count=$((lib_count + 1))
lib_names+="$lf " lib_names+="$lf "
+239 -36
View File
@@ -5,12 +5,22 @@
# (mirrors lib/notify.sh). Sourced opt-in by bin/pos-config. # (mirrors lib/notify.sh). Sourced opt-in by bin/pos-config.
# #
# Header grammar — one "# POS_CONFIG:" line per scope a tool exposes: # Header grammar — one "# POS_CONFIG:" line per scope a tool exposes:
# # POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<desc>[::<example>] | ... | *plugins # # POS_CONFIG: <scope> | <env-file> | <field> | ... | *plugins
# <env-file> basename of the config file under ~/.config/linux_post_install/ # <env-file> basename of the config file under ~/.config/linux_post_install/
# <field> := <KEY>=<flags>:<desc>[::<example>]
# | @<caption> group caption (unconditional)
# | @[<KEY>=<alt>[|…]] <caption> conditional group caption —
# active iff KEY's current value
# equals a listed alt; an empty
# alt segment ("gemini|") means
# "or unset (= default)"
# <flags> secret (masked display + stty -echo input) | digits | num | float # <flags> secret (masked display + stty -echo input) | digits | num | float
# <example> optional value format hint shown in the editor, e.g. "weather,5m joke,10m" # <example> optional value format hint shown in the editor, e.g. "weather,5m joke,10m"
# *plugins marker: also list every key declared by the installed # *plugins marker: also list every key declared by the installed
# entertainment plugins' "# POS_KEYS:" headers (dynamic) # entertainment plugins' "# POS_KEYS:" headers (dynamic)
# *providers[=<tag>] marker: keys from lib/ai-providers/*.sh adapters;
# with =<tag>, only from <tag>.sh (zero match → warn + the
# preceding caption is suppressed)
# Example: # Example:
# # POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token | TELEGRAM_CHAT_ID=digits:Numeric chat id # # POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token | TELEGRAM_CHAT_ID=digits:Numeric chat id
# #
@@ -27,8 +37,18 @@ declare -F warn >/dev/null || warn() { echo "[!] $*"; }
declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; } declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; }
declare -F ok >/dev/null || ok() { echo " OK $*"; } declare -F ok >/dev/null || ok() { echo " OK $*"; }
# Color tokens (guarded — mirrors lib/menu-lib.sh): degrade to plain text when
# common.sh didn't define them, never an error on standalone sourcing.
BOLD="${BOLD:-}"
DIM="${DIM:-}"
CYAN="${CYAN:-}"
RESET="${RESET:-}"
_cfg_scope="" # scope being edited (drives the post-write hook) _cfg_scope="" # scope being edited (drives the post-write hook)
declare -A _cfg_seen=() # key dedupe registry for cfg_scope_keys declare -A _cfg_seen=() # key dedupe registry for cfg_scope_keys
_CS=$'\x1f' # unit-separator for caption records — never in env
# names or alt strings, avoids collision with | in
# alternation syntax (AI_PROVIDER=gemini|)
# ── tool directory ───────────────────────────────────────────────── # ── tool directory ─────────────────────────────────────────────────
# Repo layout: lib/config-ui.sh → tools live in ../bin. # Repo layout: lib/config-ui.sh → tools live in ../bin.
@@ -86,6 +106,29 @@ cfg_scope_envfile() {
return 1 return 1
} }
# Split a POS_CONFIG keystring into fields on "|", IGNORING separators inside
# [...] condition brackets (caption conditions legitimately contain pipes,
# e.g. @[AI_PROVIDER=gemini|]). Byte-identical output to IFS='|' splitting for
# any string without brackets — fully backward compatible.
_cfg_split_fields() { # $1=keystring → one field per line
local s="$1" cur="" i ch depth=0
for ((i = 0; i < ${#s}; i++)); do
ch="${s:i:1}"
if [ "$ch" = "[" ]; then
depth=$((depth + 1))
elif [ "$ch" = "]" ] && [ "$depth" -gt 0 ]; then
depth=$((depth - 1))
fi
if [ "$ch" = "|" ] && [ "$depth" -eq 0 ]; then
printf '%s\n' "$cur"
cur=""
else
cur+="$ch"
fi
done
printf '%s\n' "$cur"
}
# One key field → "KEY|flags|description|example" (deduped via _cfg_seen). # One key field → "KEY|flags|description|example" (deduped via _cfg_seen).
# The optional example is "desc::example" — a literal "::" separates the # The optional example is "desc::example" — a literal "::" separates the
# value-format hint from the description. # value-format hint from the description.
@@ -136,13 +179,43 @@ _cfg_plugin_keys() {
return 0 return 0
} }
# Emit the "# PROVIDER_CONFIG:" keys of ONE adapter file (helper for
# _cfg_provider_keys; keeps the tag-filter path and the all-adapters path DRY).
_cfg_provider_file() {
local pfile="$1" line key desc flags rest
while IFS= read -r line; do
[ -n "$line" ] || continue
# Format: KEY=flags:description (same as POS_CONFIG key fields)
key="${line%%=*}"
[ -n "$key" ] || continue
[ -n "${_cfg_seen[$key]:-}" ] && continue
_cfg_seen[$key]=1
rest="${line#*=}" flags="" desc=""
if [[ "$rest" == *":"* ]]; then
flags="${rest%%:*}"
desc="${rest#*:}"
else
flags="$rest"
fi
printf '%s|%s|%s|\n' "$key" "$flags" "$desc"
done < <(grep '^# PROVIDER_CONFIG:' "$pfile" 2>/dev/null | sed 's/^.*# PROVIDER_CONFIG:[[:space:]]*//' || true)
return 0
}
# "*providers" expansion: keys declared by the installed AI provider # "*providers" expansion: keys declared by the installed AI provider
# adapters' "# PROVIDER_CONFIG:" headers (lib/ai-providers/*.sh). # adapters' "# PROVIDER_CONFIG:" headers (lib/ai-providers/*.sh).
# Optional <tag> argument restricts to <tag>.sh; an explicit tag matching zero
# adapters warns once (stderr) — silent emptiness would hide authoring errors,
# and the preceding caption is suppressed by cfg_ui's lazy flush. Bare
# *providers stays silent, exactly as today.
declare -A _CFG_TAG_WARNED=()
_cfg_provider_keys() { _cfg_provider_keys() {
local want_tag="${1:-}"
local pdir line key desc flags local pdir line key desc flags
# Repo layout: lib/config-ui.sh → ../lib/ai-providers/ # Repo layout: lib/config-ui.sh → ../lib/ai-providers/
# Installed layout: /usr/local/bin/config-ui.sh → ./ai-providers/ # Installed layout: /usr/local/bin/config-ui.sh → ./ai-providers/
pdir="" pdir=""
local candidate
for candidate in \ for candidate in \
"$(dirname "${BASH_SOURCE[0]}")/../lib/ai-providers" \ "$(dirname "${BASH_SOURCE[0]}")/../lib/ai-providers" \
"$(dirname "${BASH_SOURCE[0]}")/ai-providers"; do "$(dirname "${BASH_SOURCE[0]}")/ai-providers"; do
@@ -152,6 +225,20 @@ _cfg_provider_keys() {
fi fi
done done
[ -n "$pdir" ] || return 0 [ -n "$pdir" ] || return 0
if [ -n "$want_tag" ]; then
local matched=0 pfile
for pfile in "$pdir"/*.sh; do
[ -f "$pfile" ] || continue
[ "$(basename "$pfile" .sh)" = "$want_tag" ] || continue
matched=1
_cfg_provider_file "$pfile"
done
if [ "$matched" -eq 0 ] && [ -z "${_CFG_TAG_WARNED[$want_tag]:-}" ]; then
_CFG_TAG_WARNED["$want_tag"]=1
printf '[!] config scope: *providers=%s matched no adapter in %s\n' "$want_tag" "$pdir" >&2
fi
return 0
fi
while IFS= read -r line; do while IFS= read -r line; do
[ -n "$line" ] || continue [ -n "$line" ] || continue
# Format: KEY=flags:description (same as POS_CONFIG key fields) # Format: KEY=flags:description (same as POS_CONFIG key fields)
@@ -159,8 +246,7 @@ _cfg_provider_keys() {
[ -n "$key" ] || continue [ -n "$key" ] || continue
[ -n "${_cfg_seen[$key]:-}" ] && continue [ -n "${_cfg_seen[$key]:-}" ] && continue
_cfg_seen[$key]=1 _cfg_seen[$key]=1
# Parse flags and description from the rest rest="${line#*=}" flags="" desc=""
local rest="${line#*=}" flags="" desc=""
if [[ "$rest" == *":"* ]]; then if [[ "$rest" == *":"* ]]; then
flags="${rest%%:*}" flags="${rest%%:*}"
desc="${rest#*:}" desc="${rest#*:}"
@@ -184,15 +270,33 @@ cfg_scope_keys() {
[ "$s" = "$scope" ] || continue [ "$s" = "$scope" ] || continue
keystring="${line#*|}" keystring="${line#*|}"
keystring="${keystring#*|}" # drop the env-file field keystring="${keystring#*|}" # drop the env-file field
IFS='|' read -r -a fields <<<"$keystring" mapfile -t fields < <(_cfg_split_fields "$keystring")
for field in "${fields[@]}"; do for field in "${fields[@]}"; do
field="${field#"${field%%[![:space:]]*}"}" field="${field#"${field%%[![:space:]]*}"}"
field="${field%"${field##*[![:space:]]}"}" field="${field%"${field##*[![:space:]]}"}"
if [ -n "$field" ]; then if [ -n "$field" ]; then
if [[ "$field" == "*"* ]]; then if [[ "$field" == "@"* ]]; then
# Caption record (key position ">"): >|cond|caption|
# @[KEY=alt1|alt2] Caption → cond "KEY=alt1|alt2"
# @Caption → cond "" (always active)
local cond="" cap=""
if [[ "$field" == "@["*"]"* ]]; then
cond="${field:2}"
cond="${cond%%]*}"
cap="${field#*]}"
cap="${cap# }"
else
cap="${field#@}"
cap="${cap# }"
fi
printf '%s\n' ">${_CS}${cond}${_CS}${cap}${_CS}"
elif [[ "$field" == "*"* ]]; then
case "$field" in case "$field" in
*plugins*) _cfg_plugin_keys ;; *plugins*) _cfg_plugin_keys ;;
*providers*) _cfg_provider_keys ;; *providers*)
local ptag=""
[[ "$field" == *"="* ]] && ptag="${field#*=}"
_cfg_provider_keys "$ptag" ;;
esac esac
else else
_cfg_key_line "$field" _cfg_key_line "$field"
@@ -331,53 +435,152 @@ _cfg_edit_one() {
_cfg_post_write "$key" _cfg_post_write "$key"
} }
# Evaluate a caption condition against the env file: active iff KEY's current
# value equals any listed alt, or an empty alt segment is present and the value
# is unset/empty (trailing/double/leading pipe). Empty cond → always active.
_cfg_cond_active() { # file cond
[ -n "$2" ] || return 0
local key alts cur alt hit=0 has_empty=0 oldIFS
key="${2%%=*}"
alts="${2#*=}"
cur="$(cfg_value "$1" "$key")"
case "$alts" in
"|"*|*"||"*|*"|") has_empty=1 ;;
esac
oldIFS="$IFS"
IFS='|'
for alt in $alts; do
if [ -n "$alt" ] && [ "$alt" = "$cur" ]; then hit=1; break; fi
done
IFS="$oldIFS"
[ "$hit" -eq 1 ] && return 0
[ "$has_empty" -eq 1 ] && [ -z "$cur" ] && return 0
return 1
}
# Word-wrap <text> to <width> columns, prefixing EVERY line with <indent>
# (hanging indent). Breaks at spaces only, no hyphenation; over-long tokens
# pass through unbroken.
_cfg_wrap() { # text width indent
local text="$1" width="$2" indent="$3"
local line="" w
for w in $text; do
if [ -z "$line" ]; then
line="$w"
elif (( ${#line} + 1 + ${#w} <= width )); then
line="$line $w"
else
printf '%s%s\n' "$indent" "$line"
line="$w"
fi
done
[ -n "$line" ] && printf '%s%s\n' "$indent" "$line"
return 0
}
# Interactive numbered-menu editor for one scope. q quits; r re-renders. # Interactive numbered-menu editor for one scope. q quits; r re-renders.
#
# Rendering contract (menu-lib house pattern): the whole render block goes to
# stderr — display only, nothing on stdout. Caption records ('>') group keys;
# conditions are evaluated per render from the env file, so an edit flips group
# emphasis on the very next redraw. Inactive groups are dimmed with a textual
# reason — never hidden — so numbering stays stable across edits.
cfg_ui() { cfg_ui() {
local scope="$1" envfile file line local scope="$1" envfile file line idx
envfile="$(cfg_scope_envfile "$scope")" || { warn "unknown config scope '$scope'"; return 1; } envfile="$(cfg_scope_envfile "$scope")" || { warn "unknown config scope '$scope'"; return 1; }
file="$CONFIG_DIR/$envfile" file="$CONFIG_DIR/$envfile"
_cfg_scope="$scope" _cfg_scope="$scope"
local -a keys=() # Collect records: KEY|flags|desc|example for keys, >|cond|caption| for captions
while IFS= read -r line; do local -a recs=() nums=()
if [ -n "$line" ]; then mapfile -t recs < <(cfg_scope_keys "$scope")
keys+=("$line") if [ ${#recs[@]} -eq 0 ]; then
fi
done < <(cfg_scope_keys "$scope")
if [ ${#keys[@]} -eq 0 ]; then
warn "no config keys declared for scope '$scope'" warn "no config keys declared for scope '$scope'"
return 1 return 1
fi fi
# number→record map: numbers go to keys only, in static header order →
# stable across renders and provider switches
for idx in "${!recs[@]}"; do
[[ "${recs[$idx]}" == ">"* ]] || nums+=("$idx")
done
local choice i k f d e v # Wrap width clamped to 60120 cols minus the 6-column hanging indent
local W="${COLUMNS:-80}"
(( W < 60 )) && W=60
(( W > 120 )) && W=120
local wrapW=$((W - 6))
local rule
rule="$(printf '─%.0s' $(seq 1 40))"
local choice k f d e v disp n dim pend_cap="" pend_cond="" ckey cval why
while true; do while true; do
echo {
echo "pos config — ${scope} (${envfile})" echo
echo "------------------------------------" echo "${BOLD}pos config — ${scope} (${envfile})${RESET}"
i=0 echo "${CYAN}${rule}${RESET}"
for line in "${keys[@]}"; do n=0; dim=0; pend_cap=""; pend_cond=""
i=$((i + 1)) for idx in "${!recs[@]}"; do
IFS='|' read -r k f d e <<<"$line" # Caption records use \x1f (unit separator) to avoid collision
v="$(cfg_value "$file" "$k")" # with | in alternation syntax; key records use | as before.
printf ' %2d) %-28s %s\n' "$i" "$k" "$(cfg_display "$v" "$f")" if [[ "${recs[$idx]}" == ">"* ]]; then
if [ -n "$d" ]; then # Caption record: >\x1fcond\x1fcaption\x1f
printf ' %s\n' "$d" # Strip leading > and first \x1f, then split on next \x1f
fi pend_cond="${recs[$idx]#>}"
if [ -n "$e" ]; then pend_cond="${pend_cond#$_CS}"
printf ' e.g. %s\n' "$e" pend_cond="${pend_cond%%$_CS*}"
fi pend_cap="${recs[$idx]#>}"
done pend_cap="${pend_cap#$_CS}"
echo pend_cap="${pend_cap#*$_CS}"
read -rp "Variable number [q to quit]: " choice || { echo; return 0; } pend_cap="${pend_cap%%$_CS*}"
continue
fi
IFS='|' read -r k f d e <<<"${recs[$idx]}"
if [ -n "$pend_cap" ]; then
if _cfg_cond_active "$file" "$pend_cond"; then
dim=0
printf '\n%s ── %s%s\n' "$DIM" "$pend_cap" "$RESET"
else
dim=1
ckey="${pend_cond%%=*}"
cval="$(cfg_value "$file" "$ckey")"
if [ -z "$cval" ]; then why="— inactive (${ckey} not set)"
else why="— inactive while ${ckey}=${cval}"; fi
printf '\n%s ── %s %s%s\n' "$DIM" "$pend_cap" "$why" "$RESET"
fi
pend_cap=""
fi
n=$((n + 1))
v="$(cfg_value "$file" "$k")"
disp="$(cfg_display "$v" "$f")"
[ "$disp" = "(not set)" ] && disp="${DIM}(not set)${RESET}"
if [ "$dim" -eq 1 ]; then
printf '%s %2d) %-28s %s%s\n' "$DIM" "$n" "$k" "$disp" "$RESET"
else
printf ' %s%2d)%s %s%-28s%s %s\n' "$DIM" "$n" "$RESET" "$BOLD" "$k" "$RESET" "$disp"
fi
if [ -n "$d" ]; then
[ "$dim" -eq 1 ] && printf '%s' "$DIM"
_cfg_wrap "$d" "$wrapW" " "
[ "$dim" -eq 1 ] && printf '%s' "$RESET"
fi
if [ -n "$e" ]; then
printf '%s' "$DIM"
_cfg_wrap "e.g. $e" "$wrapW" " "
printf '%s' "$RESET"
fi
done
echo
read -rp "Number to edit [r=refresh, q=quit]: " choice || { echo; return 0; }
} >&2
case "$choice" in case "$choice" in
q|Q|quit|exit) echo; return 0 ;; q|Q|quit|exit) echo; return 0 ;;
r|R|refresh) continue ;; r|R|refresh) continue ;;
"") continue ;; "") continue ;;
*) *)
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#keys[@]} )); then if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#nums[@]} )); then
_cfg_edit_one "$file" "${keys[$((choice - 1))]}" _cfg_edit_one "$file" "${recs[${nums[$((choice - 1))]}]}"
else else
warn "invalid number '$choice' (1-${#keys[@]})" warn "invalid number '$choice' (1-${#nums[@]})"
fi fi
;; ;;
esac esac
+199
View File
@@ -0,0 +1,199 @@
# lib/registry.sh — shared query API for POS tool metadata headers.
# Sourced opt-in by consumers that need tool metadata.
# Populates bash arrays from "# POS_*:" headers in bin/pos-* files;
# consumers call reg_scan once, then reg_list / reg_lookup / reg_each.
#
# API:
# reg_scan [dir] scan pos-* files → populate arrays
# reg_list sorted tool keys
# reg_categories sorted unique category names
# reg_tools_in <cat> tool keys in a category
# reg_lookup <tool> <field> field: cat|desc|flags|subcmds|deps|examples
# reg_config_scopes sorted config scope names
# reg_config_keys <scope> key|flags|desc lines
# reg_config_envfile <scope> env-file basename for a scope
# reg_each <callback> cb(category, tool_key, description)
# reg_tool_exists <tool> exit 0 if registered
# ── common.sh helpers (guarded — mirrors lib/config-ui.sh) ─────
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; }
# ── tool directory detection ────────────────────────────────────
# Repo: lib/registry.sh → ../bin
# Install: /usr/local/bin/registry.sh → /usr/local/bin (same dir)
_reg_tools_dir() {
local dir
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" 2>/dev/null && pwd)"
if [ -d "$dir" ] && ls "$dir"/pos-* &>/dev/null; then
echo "$dir"
else
dirname "${BASH_SOURCE[0]}"
fi
}
# ── data stores ─────────────────────────────────────────────────
declare -a _reg_tools=()
declare -A _reg_cat=()
declare -A _reg_desc=()
declare -A _reg_flags=()
declare -A _reg_subcmds=()
declare -A _reg_deps=()
declare -A _reg_examples=()
declare -a _reg_config_scopes=()
declare -A _reg_config_keys=()
# ── reg_scan ────────────────────────────────────────────────────
reg_scan() {
local dir="${1:-$(_reg_tools_dir)}" f
local LC_ALL_PREV="${LC_ALL:-}"
export LC_ALL=C
_reg_tools=()
# Clear all associative arrays
for key in "${!_reg_cat[@]}"; do
unset "_reg_cat[$key]" "_reg_desc[$key]" "_reg_flags[$key]"
unset "_reg_subcmds[$key]" "_reg_deps[$key]" "_reg_examples[$key]"
done
_reg_config_scopes=()
for scope in "${!_reg_config_keys[@]}"; do
unset "_reg_config_keys[$scope]"
done
local -A scope_seen=()
for f in "$dir"/pos-*; do
[ -x "$f" ] || continue
local name="${f##*/pos-}"
local key cat
key="$name"
if [[ "$name" == *-* ]]; then
cat="${name%%-*}"
else
cat=""
fi
_reg_tools+=("$key")
_reg_cat["$key"]="$cat"
# POS: — description (text after first "— ")
local pos_line
pos_line="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f" 2>/dev/null)"
_reg_desc["$key"]="${pos_line#*— }"
# POS_FLAGS:
_reg_flags["$key"]="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f" 2>/dev/null)"
# POS_SUBCMDS:
_reg_subcmds["$key"]="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f" 2>/dev/null)"
# POS_DEPS:
_reg_deps["$key"]="$(sed -n '/^# POS_DEPS: /{s/^# POS_DEPS: //;p;q}' "$f" 2>/dev/null)"
# POS_EXAMPLES: (may appear multiple times — join with newlines)
local examples=""
examples="$(sed -n '/^# POS_EXAMPLES: /{s/^# POS_EXAMPLES: //;p}' "$f" 2>/dev/null)"
_reg_examples["$key"]="$examples"
# POS_CONFIG: (may appear multiple lines per file)
local line
while IFS= read -r line; do
[ -n "$line" ] || continue
line="${line#*POS_CONFIG:}"
local scope="${line%%|*}"
scope="${scope// }"
[ -n "$scope" ] || continue
_reg_config_keys["$scope"]+="${_reg_config_keys[$scope]:+$'\n'}$line"
if [ -z "${scope_seen[$scope]:-}" ]; then
scope_seen["$scope"]=1
_reg_config_scopes+=("$scope")
fi
done < <(grep '^# POS_CONFIG:' "$f" 2>/dev/null || true)
done
# Sort tools
mapfile -t _reg_tools < <(printf '%s\n' "${_reg_tools[@]}" | sort)
# Sort config scopes
mapfile -t _reg_config_scopes < <(printf '%s\n' "${_reg_config_scopes[@]}" | sort -u)
# Restore LC_ALL
if [ -n "$LC_ALL_PREV" ]; then
export LC_ALL="$LC_ALL_PREV"
else
unset LC_ALL
fi
}
# ── discovery ───────────────────────────────────────────────────
reg_list() { printf '%s\n' "${_reg_tools[@]}"; }
reg_categories() {
local -a cats=()
local t cat _rc_key
local -A _rc_seen=()
for t in "${_reg_tools[@]}"; do
cat="${_reg_cat[$t]}"
if [ -z "$cat" ]; then
_rc_key="__empty__"
else
_rc_key="$cat"
fi
if [ -z "${_rc_seen[$_rc_key]+x}" ]; then
_rc_seen["$_rc_key"]=1
cats+=("$cat")
fi
done
printf '%s\n' "${cats[@]}" | sort
}
reg_tools_in() {
local cat="$1" t
for t in "${_reg_tools[@]}"; do
[ "${_reg_cat[$t]}" = "$cat" ] && echo "$t"
done
}
# ── lookup ──────────────────────────────────────────────────────
reg_lookup() {
local tool="$1" field="$2"
case "$field" in
cat) echo "${_reg_cat[$tool]:-}" ;;
desc) echo "${_reg_desc[$tool]:-}" ;;
flags) echo "${_reg_flags[$tool]:-}" ;;
subcmds) echo "${_reg_subcmds[$tool]:-}" ;;
deps) echo "${_reg_deps[$tool]:-}" ;;
examples) echo "${_reg_examples[$tool]:-}" ;;
*) return 1 ;;
esac
}
# ── config scope helpers ────────────────────────────────────────
reg_config_scopes() { printf '%s\n' "${_reg_config_scopes[@]}"; }
reg_config_keys() {
local scope="$1"
echo "${_reg_config_keys[$scope]:-}"
}
reg_config_envfile() {
local scope="$1" line
line="$(echo "${_reg_config_keys[$scope]:-}" | head -1)"
[ -n "$line" ] || return 1
line="${line#*|}" # drop scope
local env="${line%%|*}"
echo "${env// }"
}
# ── iteration ───────────────────────────────────────────────────
reg_each() {
local cb="$1" t
for t in "${_reg_tools[@]}"; do
"$cb" "${_reg_cat[$t]}" "$t" "${_reg_desc[$t]}"
done
}
# ── convenience ─────────────────────────────────────────────────
reg_tool_exists() {
[ -n "${_reg_desc[$1]+x}" ]
}
+42 -16
View File
@@ -10,6 +10,8 @@ set -euo pipefail
# - "# POS:" header line → one-line description # - "# POS:" header line → one-line description
# - "# POS_FLAGS:" line → flag completion list (flag-style tools only) # - "# POS_FLAGS:" line → flag completion list (flag-style tools only)
# - "# POS_SUBCMDS:" line → subcommand completion list (multi-command tools) # - "# POS_SUBCMDS:" line → subcommand completion list (multi-command tools)
# - "# POS_DEPS:" line → runtime binary dependencies (optional)
# - "# POS_EXAMPLES:" line → curated usage examples (optional, multi-line)
root="$(cd "$(dirname "$0")/.." && pwd)" root="$(cd "$(dirname "$0")/.." && pwd)"
mode="write" mode="write"
@@ -22,7 +24,7 @@ export LC_ALL=C
ctx="$root/DOC/AGENT_Context_Project.md" ctx="$root/DOC/AGENT_Context_Project.md"
comp="$root/completions/pos.bash" comp="$root/completions/pos.bash"
# ── Collect tools: "cat|sub|desc|flags|subcmds" ──────────────── # ── Collect tools: "cat|sub|desc|flags|subcmds|deps|examples" ──
# Category-less tools (pos-<cat>, e.g. pos-config) get an empty cat. # 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 <cat> <sub> → display name (pos-config / pos-communication-telegram-sender).
tooldisp() { printf 'pos-%s%s' "${1:+$1-}" "$2"; } tooldisp() { printf 'pos-%s%s' "${1:+$1-}" "$2"; }
@@ -42,31 +44,55 @@ for f in "$root"/bin/pos-*; do
desc="${desc#*— }" desc="${desc#*— }"
flags="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f")" flags="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f")"
subcmds="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")" subcmds="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")"
tools+=("$cat|$sub|$desc|$flags|$subcmds") deps="$(sed -n '/^# POS_DEPS: /{s/^# POS_DEPS: //;p;q}' "$f")"
examples="$(grep '^# POS_EXAMPLES:' "$f" 2>/dev/null | sed 's/^# POS_EXAMPLES:[[:space:]]*//' | awk 'NR>1{printf " · "}{printf "%s", $0}END{print ""}' || true)"
tools+=("$cat|$sub|$desc|$flags|$subcmds|$deps|$examples")
done done
mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort) mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort)
# ── Block generators (emit inner content only, no markers) ────── # ── Block generators (emit inner content only, no markers) ──────
# Check whether any tool has non-empty deps or examples (for conditional columns)
_has_deps_examples=0
for t in "${tools[@]}"; do
IFS='|' read -r _ _ _ _ _ _tdeps _texamples <<<"$t"
if [ -n "$_tdeps" ] || [ -n "$_texamples" ]; then
_has_deps_examples=1
break
fi
done
gen_tree() { gen_tree() {
local width=0 cat sub desc flags name t local width=0 cat sub desc flags name t deps examples
for t in "${tools[@]}"; do for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds <<<"$t" IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
name="$(tooldisp "$cat" "$sub")" name="$(tooldisp "$cat" "$sub")"
[ ${#name} -gt "$width" ] && width=${#name} [ ${#name} -gt "$width" ] && width=${#name}
done done
for t in "${tools[@]}"; do for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds <<<"$t" IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
name="$(tooldisp "$cat" "$sub")" name="$(tooldisp "$cat" "$sub")"
printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc" printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc"
if [ -n "$deps" ]; then
printf '│ %*s│ [deps: %s]\n' "" "" "$deps"
fi
done done
} }
gen_dispatch() { gen_dispatch() {
local cat sub desc flags t local cat sub desc flags t deps examples
for t in "${tools[@]}"; do if [ "$_has_deps_examples" -eq 1 ]; then
IFS='|' read -r cat sub desc flags subcmds <<<"$t" printf '| Category | Command | Script | Description | Deps | Examples |\n'
printf '| %s | %s | `%s` | %s |\n' "$cat" "$sub" "$(tooldisp "$cat" "$sub")" "$desc" printf '|----------|---------|--------|-------------|------|----------|\n'
done for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
printf '| %s | %s | `%s` | %s | %s | %s |\n' "$cat" "$sub" "$(tooldisp "$cat" "$sub")" "$desc" "$deps" "$(printf '%s' "$examples" | sed 's/ | / → /g')"
done
else
for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
printf '| %s | %s | `%s` | %s |\n' "$cat" "$sub" "$(tooldisp "$cat" "$sub")" "$desc"
done
fi
} }
gen_selfcontained() { gen_selfcontained() {
@@ -83,10 +109,10 @@ gen_selfcontained() {
} }
gen_filetable() { gen_filetable() {
local cat sub desc flags name t local cat sub desc flags name t deps examples
printf '| `bin/pos` | %s | CLI dispatcher with smart arg matching + logging + category help |\n' "$(wc -l < "$root/bin/pos")" printf '| `bin/pos` | %s | CLI dispatcher with smart arg matching + logging + category help |\n' "$(wc -l < "$root/bin/pos")"
for t in "${tools[@]}"; do for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds <<<"$t" IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
name="bin/$(tooldisp "$cat" "$sub")" name="bin/$(tooldisp "$cat" "$sub")"
printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc" printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc"
done done
@@ -94,10 +120,10 @@ gen_filetable() {
} }
gen_posflags() { gen_posflags() {
local cat sub desc flags t local cat sub desc flags t deps examples
echo "declare -A _pos_flags" echo "declare -A _pos_flags"
for t in "${tools[@]}"; do for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds <<<"$t" IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
[ -n "$flags" ] || continue [ -n "$flags" ] || continue
printf '_pos_flags[%s]="%s"\n' "$(tooldisp "$cat" "$sub" | sed 's/^pos-//')" "$flags" printf '_pos_flags[%s]="%s"\n' "$(tooldisp "$cat" "$sub" | sed 's/^pos-//')" "$flags"
done done
@@ -106,10 +132,10 @@ gen_posflags() {
gen_possubcmds() { gen_possubcmds() {
# Subcommand completion: "# POS_SUBCMDS:" list + nested sub-tools from # Subcommand completion: "# POS_SUBCMDS:" list + nested sub-tools from
# filenames (pos-<cat>-<sub>-<extra> → "extra" completes under <cat>-<sub>). # filenames (pos-<cat>-<sub>-<extra> → "extra" completes under <cat>-<sub>).
local cat sub desc flags subcmds rest f t local cat sub desc flags subcmds deps examples rest f t
echo "declare -A _pos_subcmds" echo "declare -A _pos_subcmds"
for t in "${tools[@]}"; do for t in "${tools[@]}"; do
IFS='|' read -r cat sub desc flags subcmds <<<"$t" IFS='|' read -r cat sub desc flags subcmds deps examples <<<"$t"
subcmds="${subcmds:-}" subcmds="${subcmds:-}"
for f in "$root"/bin/"$(tooldisp "$cat" "$sub")"-*; do for f in "$root"/bin/"$(tooldisp "$cat" "$sub")"-*; do
[ -x "$f" ] || continue [ -x "$f" ] || continue
+3
View File
@@ -9,6 +9,9 @@ set -euo pipefail
# lines (single source of truth for generated docs): # lines (single source of truth for generated docs):
# # POS: <category> <command> — one-line description # # POS: <category> <command> — one-line description
# # POS_FLAGS: --flag1 --flag2 (flag-style tools only) # # POS_FLAGS: --flag1 --flag2 (flag-style tools only)
# # POS_SUBCMDS: sub1 sub2 (multi-command tools only)
# # POS_DEPS: binary1 binary2 (runtime deps, optional)
# # POS_EXAMPLES: pos <tool> <args> | Description (optional)
# 3. Exec bit: chmod +x bin/pos-<category>-<command> # 3. Exec bit: chmod +x bin/pos-<category>-<command>
# 4. If it reads stdin (password/selection prompts), add it to # 4. If it reads stdin (password/selection prompts), add it to
# INTERACTIVE_CMDS in bin/pos or its prompt breaks under the log tee. # INTERACTIVE_CMDS in bin/pos or its prompt breaks under the log tee.