Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4761df3f6 | |||
| e6fa0a4ee9 | |||
| 4306a53fef | |||
| 300b742ac8 | |||
| 59935dc5ef | |||
| e969234ca5 | |||
| 4fd3c37c40 | |||
| 5d7407e30f | |||
| 6566c8343d | |||
| 9f289ba31b | |||
| a5c19e842d | |||
| d84a35efce | |||
| 9564880ebf | |||
| d0299d3f98 | |||
| c1f1c4109f | |||
| 710b626f47 | |||
| 1c19c0de59 | |||
| e0c9ba384a | |||
| 7ae2e77a44 | |||
| 88ea660891 |
@@ -14,8 +14,8 @@ CRITICAL: real guidance lives in DOC/. When you encounter a reference below, use
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **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, 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).
|
||||
- **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.
|
||||
|
||||
@@ -134,3 +134,7 @@ 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** — 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-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 60–120 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.
|
||||
- **2026-08-27** — Critical fix: paste injection + multiline paste in `pos ai alias`'s Insert Prompt (root cause: `menu_ask_value` → plain line-oriented `read -rp`; a multi-line Ctrl+V paste floods the tty queue, `read` consumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified `$(whoami)`/`; ls`/`sudo apt update` behavior). New `menu_read_value()` in `lib/menu-lib.sh` (169→362): raw-mode (`stty -icanon -echo -isig min 1 time 0`) bracketed-paste-aware value reader — `\e[?2004h/l` markers, text inside `[200~…[201~` inserted LITERALLY (embedded newlines/CR are data), Enter submits only outside a paste, Backspace/DEL/Left/Right/Home/End/Delete/Ctrl-U edit, Ctrl-D-on-empty + Ctrl-C/Z/\ cancel (terminal restored first); bytes read chunk-wise via `dd bs=4096|od -tx1|tr` — NOT bash's `read` builtin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmed `read -erp` (readline) atomically consumes a paste but returns only its first line, so a custom reader was required. `bin/pos-ai-alias` (712→760): `_alias_prompt_encode/_decode` (backslash→`\\`, newline→`\n`; literal `[ = ]` comparisons — bash `case` patterns don't match a single backslash), `_alias_prompt_truncate` newline-safe + max-length arg; load/save encode/decode the prompt field; edit wizard shows a truncated display default but Enter restores the FULL original prompt (fixes pre-existing silent truncation of >80-char prompts), empty-original Enter continues. Verified: pty harnesses (`/tmp/pty_{menulib,cancel,e2e_alias}.py`, `/tmp/roundtrip_test.sh`) — bracketed multiline paste captured verbatim incl. `C:\temp\note`/`$(whoami)`/`; ls`/`echo test`/`sudo apt update`, nothing executed, clean exit; single-line paste; Ctrl-D and Ctrl-C both cancel cleanly (CANCELLED→DONE, terminal restored); full create→list→show→edit E2E with decode round-trip and Enter-keeps-full; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||
- **2026-08-27** — Configurable AI-bridge trigger word for the Telegram listener: the hard-coded `ai ` prefix in `pos-communication-telegram-listener` became `TELEGRAM_AI_PREFIX` (default `ai`) — messages starting with `<prefix> ` (case-insensitive, literal match) are forwarded to Gemini. New `prefix` verb: `pos communication telegram listener prefix` shows the current word, `prefix <word>` sets it (validated `[A-Za-z0-9][A-Za-z0-9_-]*`, writes `TELEGRAM_AI_PREFIX` to `telegram.env` chmod 600); also editable via `pos config telegram` (field added to the sender's `# POS_CONFIG:` telegram scope — registry-driven, no code in config-ui). Matching is per-message hot-reloaded (like the command map — no daemon restart), via scoped `shopt -s nocasematch` + quoted-literal `=~` prefix (bash `case` patterns can't do literal-then-whitespace + case-insensitivity in one test); `ai_bridge_prefix()` precedence: telegram.env > env from load_config > default `ai`. `--status` shows the current prefix; usage + `# POS_SUBCMDS: prefix` added (completions regenerate). Preserved edge: bare `ai` (no trailing space) never matched the old regex, so it still falls through to "Unknown command". Docs: POS.md listener rows/paragraph, howto/ai.md Telegram section + troubleshooting (also corrected a stale claim that AI errors reply with a `pos config ai` hint — code replies `AI error: …` only). Verified: function-level routing harness (`/tmp/ai_prefix_routing_test.sh` — extraction of the real listener functions + PATH stub `pos`): default `ai`/`AI` routes, bare-prefix and unknown-command fallthrough, `ai /reset` and custom-`bot` `/reset` reset the session, custom `bot`/`BOT` routes and old `ai` no longer routes, per-message hot-reload after removing the var; CLI verb tests (show/set/invalid rc 1/leading-digit/`--status`); dispatch smoke `pos communication telegram listener prefix` + flat form; `pos config telegram` render shows the field; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||
- **2026-08-29** — Generic text-prefix map for the Telegram listener (user's clarification superseding the scalar `TELEGRAM_AI_PREFIX` setter): `bin/pos-communication-telegram-listener` (623→782) now routes any non-command message `<word> <text>` to a mapped command with `<text>` appended as ONE quoted argument — `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`. New map file `telegram_prefixes.env` (chmod 600, re-read per message, `@quiet` values, 120s cap, empty→`OK`, `exit <rc>` reply, syntax-checked on save, first-file-match wins, case-insensitive, word must be space-delimited so bare `<word>` still falls through). Routing order: text-prefix map → built-in Gemini `ai` bridge → `/command` map → Unknown (a mapped `ai` shadows the bridge). `prefix` verb reworked: bare = list map + bridge word; `prefix <word> <command...>` = map (validated `[A-Za-z0-9][A-Za-z0-9_-]*`, `bash -n` via check_syntax); `prefix <word>` = show one; `prefix -r <word>` = remove; the AI-bridge word itself is now set ONLY via `pos config telegram` (`TELEGRAM_AI_PREFIX`, default `ai` — `--status` + bare `prefix` still display it). `run_and_reply()` extracted to share `/command`-map (60s) and prefix (120s) execution semantics; dispatch passes `"${@:2}"`. Docs: POS.md listener rows/paragraph, howto/communication.md bullet, howto/ai.md (prefix-map + shadowing), usage(), `# POS:` header, AGENT_Context regen. Verified: routing harness `/tmp/prefix_map_routing_test.sh` 27/27 (ai-bridge regression incl. `/reset`, opencode remainder=ONE arg, case-insensitivity, bare/trailing-space fallthrough, shadowing, no partial-prefix false match, exit/OK/@quiet/env-expansion, /command-map regression via run_and_reply); CLI verb suite (set/show/remove/missing rc 1/invalid word rc 1/invalid cmd rc 1 — fixed latent `set -e` cmdsubst abort on syntax errors); dispatch smoke nested + flat + `--status`; `pos config telegram` render; `bash -n`, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–198 |
|
||||
| ## 3. Installation Flow | 199–252 |
|
||||
| ## 4. The `pos` CLI System | 253–328 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 329–360 |
|
||||
| ## 6. Docker Compose / ScaleTail | 361–403 |
|
||||
| ## 7. Optional Apps (`apps/`) | 404–433 |
|
||||
| ## 8. Entertainment Module | 434–447 |
|
||||
| ## 9. Systemd Services | 448–459 |
|
||||
| ## 10. Configuration Files | 460–486 |
|
||||
| ## 11. Coding Conventions | 487–519 |
|
||||
| ## 12. Development Workflow | 520–572 |
|
||||
| ## 13. Key File Quick Reference | 573–641 |
|
||||
| ## 14. Common Tasks for Agents | 642–675 |
|
||||
| ## 2. Directory Structure | 44–204 |
|
||||
| ## 3. Installation Flow | 205–258 |
|
||||
| ## 4. The `pos` CLI System | 259–336 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 337–368 |
|
||||
| ## 6. Docker Compose / ScaleTail | 369–411 |
|
||||
| ## 7. Optional Apps (`apps/`) | 412–441 |
|
||||
| ## 8. Entertainment Module | 442–455 |
|
||||
| ## 9. Systemd Services | 456–467 |
|
||||
| ## 10. Configuration Files | 468–494 |
|
||||
| ## 11. Coding Conventions | 495–527 |
|
||||
| ## 12. Development Workflow | 528–580 |
|
||||
| ## 13. Key File Quick Reference | 581–652 |
|
||||
| ## 14. Common Tasks for Agents | 653–686 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -61,16 +61,18 @@ Linux_post_install/
|
||||
├── bin/ # CLI tools — installed to /usr/local/bin/
|
||||
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
||||
<!-- GEN:START tree -->
|
||||
│ ├── pos-ai-alias # manage AI agent aliases
|
||||
│ ├── pos-ai-gemini # Forward to pos ai --provider gemini (backward compat)
|
||||
│ ├── pos-ai-openrouter # Forward to pos ai --provider openrouter (backward compat)
|
||||
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
|
||||
│ ├── pos-communication-matrix-sender # Send messages to a Matrix room via the client-server API (send, test, login)
|
||||
│ ├── pos-communication-scrcpy # Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
|
||||
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
||||
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages
|
||||
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
|
||||
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
||||
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||
│ │ [deps: docker]
|
||||
│ ├── 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-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location)
|
||||
@@ -81,9 +83,11 @@ Linux_post_install/
|
||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||
│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select)
|
||||
│ ├── 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-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)
|
||||
│ │ [deps: aria2c jq curl]
|
||||
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
|
||||
│ ├── pos-network-scan # Parallel ping sweep of CIDR
|
||||
@@ -94,9 +98,11 @@ Linux_post_install/
|
||||
│ ├── 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-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||
│ │ [deps: tar]
|
||||
│ ├── 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-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently
|
||||
│ ├── pos-system-uninstall # Remove pos toolkit binaries, services, shell integration, config, and data
|
||||
│ ├── pos-ai # AI assistant: ask, chat, sessions, capture, models, providers
|
||||
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
|
||||
@@ -206,7 +212,7 @@ User runs: ./install.sh [--apps|--full|--feature|--dry-run|--skip <phase>|--step
|
||||
│
|
||||
├─ Phase 2: install.sh (requires root)
|
||||
│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)
|
||||
│ └─ Copies lib/*.sh (common, flags, notify, entertainment-lib,
|
||||
│ └─ Copies lib/*.sh (common, flags, notify, registry, entertainment-lib,
|
||||
│ scheduler-lib, config-ui, user-timers-lib, entertainment-plugin-lib,
|
||||
│ usb-lib, share-lib, menu-lib) → /usr/local/bin/ (chmod 644)
|
||||
│ └─ Copies x64_bin/* → /usr/local/bin/ on x86_64 (arm64_bin/ on aarch64)
|
||||
@@ -266,48 +272,50 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
|
||||
### Available Commands
|
||||
|
||||
| Category | Command | Script | Description |
|
||||
|----------|---------|--------|-------------|
|
||||
<!-- GEN:START dispatch -->
|
||||
| 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) |
|
||||
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages |
|
||||
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
||||
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
| docker | stack | `pos-docker-stack` | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped |
|
||||
| docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||
| entertainment | config | `pos-entertainment-config` | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
|
||||
| entertainment | disable | `pos-entertainment-disable` | Disable a plugin's auto-trigger (remove it from ENABLED) |
|
||||
| entertainment | enable | `pos-entertainment-enable` | Enable an auto-trigger for a plugin on a schedule |
|
||||
| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via the configured notify platforms |
|
||||
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state |
|
||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
||||
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) |
|
||||
| media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) |
|
||||
| media | ytsync | `pos-media-ytsync` | Incrementally sync YouTube channels/playlists into ~/Videos |
|
||||
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||
| network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location |
|
||||
| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |
|
||||
| share | nfs-client | `pos-share-nfs-client` | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| share | nfs-server | `pos-share-nfs-server` | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
|
||||
| share | smb-client | `pos-share-smb-client` | Mount SMB/CIFS 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 | usb-server | `pos-share-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent |
|
||||
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| system | firewall | `pos-system-firewall` | Interactive UFW management |
|
||||
| system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| | 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 |
|
||||
| Category | Command | Script | Description | Deps | Examples |
|
||||
|----------|---------|--------|-------------|------|----------|
|
||||
| ai | alias | `pos-ai-alias` | manage AI agent aliases | | |
|
||||
| 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) | | |
|
||||
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages | | |
|
||||
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) | | |
|
||||
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) | | |
|
||||
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages | | |
|
||||
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) | | |
|
||||
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | |
|
||||
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | |
|
||||
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) | docker | |
|
||||
| docker | stack | `pos-docker-stack` | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | |
|
||||
| docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | |
|
||||
| entertainment | config | `pos-entertainment-config` | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | |
|
||||
| entertainment | disable | `pos-entertainment-disable` | Disable a plugin's auto-trigger (remove it from ENABLED) | | |
|
||||
| entertainment | enable | `pos-entertainment-enable` | Enable an auto-trigger for a plugin on a schedule | | |
|
||||
| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via the configured notify platforms | | |
|
||||
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | |
|
||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | |
|
||||
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | | |
|
||||
| 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 |
|
||||
| media | ytsync | `pos-media-ytsync` | Incrementally sync YouTube channels/playlists into ~/Videos | | |
|
||||
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | |
|
||||
| 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 | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui | | |
|
||||
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location | | |
|
||||
| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR | | |
|
||||
| share | nfs-client | `pos-share-nfs-client` | Mount NFS shares (ephemeral or persistent systemd mount units) | | |
|
||||
| share | nfs-server | `pos-share-nfs-server` | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | |
|
||||
| share | smb-client | `pos-share-smb-client` | Mount SMB/CIFS 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 | usb-server | `pos-share-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) | | |
|
||||
| ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent | | |
|
||||
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) | tar | |
|
||||
| system | firewall | `pos-system-firewall` | Interactive UFW management | | |
|
||||
| system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | |
|
||||
| system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | |
|
||||
| system | uninstall | `pos-system-uninstall` | Remove pos toolkit binaries, services, shell integration, config, and data | | |
|
||||
| | 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 -->
|
||||
|
||||
### Legacy Wrappers
|
||||
@@ -586,24 +594,26 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
|
||||
| `lib/usb-lib.sh` | 205 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`; EFI system partitions excluded; picker shows size/label/fs) — used by `pos system backup` + `pos media sync` |
|
||||
| `lib/share-lib.sh` | 318 | Domain layer for the share suite (usbsrv/smbclient record parsers, folder+mountpoint candidates, remote listings, service/firewall advisories; EOF-safe) + compat shims to `lib/menu-lib.sh` — used by all five `pos share *` tools |
|
||||
| `lib/menu-lib.sh` | 169 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
|
||||
| `lib/menu-lib.sh` | 362 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default via raw-mode bracketed-paste-safe `menu_read_value`; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
|
||||
| `lib/registry.sh` | 199 | Shared query API for POS tool metadata headers (`# POS_*:`) — `reg_scan`/`reg_list`/`reg_lookup`/`reg_each`/config scope helpers; used by `pos-tree` and `gen-docs.sh` |
|
||||
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||
| `bin/flag-clear` | 21 | Unset a flag |
|
||||
| `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) |
|
||||
<!-- 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` | 760 | manage AI agent aliases |
|
||||
| `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-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
|
||||
| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||
| `bin/pos-communication-scrcpy` | 254 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| `bin/pos-communication-telegram-listener` | 566 | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| `bin/pos-communication-telegram-listener` | 782 | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages |
|
||||
| `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-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-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) |
|
||||
@@ -613,10 +623,10 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `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-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-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-ip` | 69 | Show interfaces, routes, public IP + location |
|
||||
| `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR |
|
||||
@@ -626,14 +636,15 @@ 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-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-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-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-ai` | 642 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-system-uninstall` | 435 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||
| `bin/pos-ai` | 692 | 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-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 305 | Dynamic bash completion |
|
||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 308 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
|
||||
+4
-2
@@ -31,7 +31,7 @@ Each phase is independent and runs only if the corresponding script exists.
|
||||
|-----------|---------|-------------|
|
||||
| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |
|
||||
| `apps/<category>/` | Optional desktop app installers | run on demand |
|
||||
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
|
||||
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `registry.sh` (shared query API for POS tool metadata headers), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
|
||||
| `config/` | Gitignored user config files | `~/.config/<app>/` (via postinstall) |
|
||||
| `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/bin` (via install.sh Phase 2) |
|
||||
| `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |
|
||||
@@ -128,8 +128,10 @@ esac
|
||||
# POS: <category> <command> — one-line description rendered by `make gen`
|
||||
# POS_FLAGS: --flag1 --flag2 # ONLY for flag-style tools
|
||||
# POS_SUBCMDS: sub1 sub2 # ONLY for multi-command tools
|
||||
# POS_DEPS: binary1 binary2 # Optional: runtime deps (space-separated binary names)
|
||||
# POS_EXAMPLES: pos <tool> <args> | Description # Optional: usage examples
|
||||
```
|
||||
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `make gen` only reads the text after the first `— ` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
|
||||
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `POS_DEPS` lists runtime binary names that `command -v` would check — use when the tool requires specific binaries beyond what `preinstall.sh` installs. `POS_EXAMPLES` provides curated usage examples (one per line, pipe-delimited `command | description`) shown in `pos tree` and future help views. Both are optional and degrade gracefully when absent. `make gen` only reads the text after the first `— ` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
|
||||
- **Category-less vs categorized:** most tools are `bin/pos-<category>-<command>`. Use category-less `bin/pos-<cmd>` (e.g. `pos-config`, `pos-tree`) only for dispatcher/dev-level commands that fit no category — they dispatch and document like any tool but show with an empty category in the generated tables.
|
||||
- Nested tools (e.g. `bin/pos-communication-telegram-listener`) are auto-detected from filenames: the trailing segment (`listener`) is offered as a subcommand of the parent tool (`communication-telegram`) in `pos <category> --help` and tab-completion, instead of appearing as a flat sibling (`telegram-listener`). The flat dash-form (`pos communication telegram-listener`) still dispatches.
|
||||
- Optionally add an EXAMPLES line in `bin/pos` `usage()` to showcase the tool in `pos --help`.
|
||||
|
||||
+20
-3
@@ -70,6 +70,14 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
|
||||
| `pos ai providers` | Lists available providers, their config status, and the active provider |
|
||||
| `pos ai --model <id> …` | Overrides the model for one invocation |
|
||||
| `pos ai --provider <name> …` | Selects the provider for one invocation (gemini\|openrouter) |
|
||||
| `pos ai alias` | Interactive alias manager (`bin/pos-ai-alias`): menu loop (create / edit / remove / list) that shows the alias table (Name/Provider/Session/Prompt, prompts truncated) between picks |
|
||||
| `pos ai alias create [name]` | Interactive 4-step wizard: alias name (leading letter, then letters/digits/-/_; unique across aliases), provider pick (from installed `lib/ai-providers/*.sh` adapters), session name (defaults to the alias name), optional system prompt (must not contain `\|`; warns above 500 chars); confirm defaults to yes, then the alias is saved |
|
||||
| `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 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 wrapper path and the resolved command: `pos ai <provider> ask --session <session>[ --system '<prompt>']` |
|
||||
|
||||
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`.
|
||||
|
||||
@@ -252,6 +260,7 @@ reported as "N videos require sign-in — skipped" (escape hatch:
|
||||
| `pos system backup --service` | `bin/pos-system-backup` | Lists folders under `/srv` and `~/srv`, lets you pick one, then runs the same backup | Roots via `BACKUP_SERVICE_ROOTS` (space-separated, default `/srv $HOME/srv`) or `~/.config/linux_post_install/system.env` |
|
||||
| `pos system health` | `bin/pos-system-health` | Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | Console-only reporter — health itself never sends notifications; forward the output with a wrapper (e.g. the Telegram/Matrix listener map `/status=pos system health`) or schedule it via `pos system schedule` with a `NOTIFY` policy. `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2) and `BACKUP_SERVICE_ROOTS` come from `~/.config/linux_post_install/system.env`; `--help` shows the effective values |
|
||||
| `pos system schedule <cmd>` | `bin/pos-system-schedule` | Scheduled jobs — run a command on a timer, notify (or stay silent): `run [name\|all]`, `list`, `config`, `enable [name\|all]`, `disable [name\|all]`, `status`, `migrate`. Each job is a file in `~/.config/linux_post_install/schedule.d/<name>.env` with `INTERVAL` (`5m…59m`, `1h…23h`, `hourly`, `daily`, `weekly`, `OnCalendar=…`), `NOTIFY` policy, optional `MSG`, `RULE` (threshold only), and `COMMAND` = the literal rest of the line (pipes/quotes/`sudo` fine). Policies: `always` (full output every run), `onchange` (send when output differs from the last run; first run always sends), `onerror` (non-zero exit or empty output), `threshold` (first numeric output vs `RULE`, alert on false→true + one recovery — the old event-trigger behavior), `never` (side-effect jobs, no notify) | One systemd **user** timer pair per job (`pos-schedule-<name>.timer` + oneshot `.service`, `Persistent=true`), reconciled on `enable`/`disable`; the legacy single `pos-event-trigger` timer is auto-removed. `migrate` converts a pre-existing `event.env` rule set into `schedule.d/rule-N.env` threshold jobs. `config` is an interactive editor (add/edit/remove/enable/disable, validates interval + threshold); alerts via `lib/notify.sh`; `--dry-run` previews runs/writes/sends; jobs are arbitrary shell commands (chmod 600, same trust model as the Telegram map); starter jobs in `config/schedule.d/` auto-installed no-clobber by postinstall. Bare invocation on a terminal (or the `menu` subcommand) opens an interactive hub over these verbs (list, timer status, run-now, enable, disable, config editor) — a menu run-now asks y/N first and goes through the same `run <name>` path the systemd timers use |
|
||||
| `pos system uninstall` | `bin/pos-system-uninstall` | Safe, interactive uninstaller for the pos toolkit — scans and removes binaries, services, shell integration, config, and data in three tiers | Tier 1 (always): binaries in `/usr/local/bin/` (pos, pos-*, libs, ai-providers, entertainment plugins, prebuilt, features), systemd services (disable+remove), shell integration in `~/.bashrc` (PATH, completion, pos-ai-hook source), completion file. Tier 2 (`--config`): `~/.config/linux_post_install/` (.env files, schedule.d/, authorized_keys, rclone.conf). Tier 3 (`--data`): `~/.local/share/linux_post_install/` (ai sessions, logs, captured output). Flags: `--yes` (skip prompts, tier 1 only), `--config` (include tier 2), `--data` (include tier 3). Combine all three for nuclear removal. Git repo is never removed |
|
||||
|
||||
A scheduled job is the recommended way to run the health dashboard on a timer: a `daily` job with `COMMAND=pos system health` and `NOTIFY=always` sends the dashboard output as the alert — no separate systemd unit needed (the old `pos-health.{service,timer}` units are gone; a legacy install may still have them failed/leftover — disable and remove them).
|
||||
|
||||
@@ -298,7 +307,7 @@ Subcommands that need input prompt interactively when args are omitted. Bare inv
|
||||
| Command | File | Purpose | Configuration |
|
||||
|---------|------|---------|---------------|
|
||||
| `pos communication telegram sender send "text"` | `bin/pos-communication-telegram-sender` | Send a message, link, or media file (auto-detects the type) to a Telegram chat via the Bot API | Token + chat ID from `~/.config/linux_post_install/telegram.env` (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, chmod 600). Precedence: `--token`/`--chat-id` flags > env > config file |
|
||||
| `pos communication telegram listener` | `bin/pos-communication-telegram-listener` | Telegram bot listener: map `/command` → bash commands and run them from chat; interactive editor for the map | Same `telegram.env` (the bot is the owner, `TELEGRAM_CHAT_ID`). Map lives in `~/.config/linux_post_install/telegram_commands.env` (`/cmd=bash command` lines, chmod 600) |
|
||||
| `pos communication telegram listener` | `bin/pos-communication-telegram-listener` | Telegram bot listener: map `/command` → bash commands and `<prefix>` → apps, run them from chat; interactive editor for the map | Same `telegram.env` (the bot is the owner, `TELEGRAM_CHAT_ID`). Map lives in `~/.config/linux_post_install/telegram_commands.env` (`/cmd=bash command` lines); text-prefix app map in `telegram_prefixes.env` (`<word>=command` lines) — both chmod 600 |
|
||||
| `pos communication matrix sender send "text"` | `bin/pos-communication-matrix-sender` | Send a text message (plain or `--markdown`) to a Matrix room via the client-server API; also `login` (password → access token) and `test` | Homeserver + room from `~/.config/linux_post_install/matrix.env` (`MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID`, chmod 600, secrets masked by `pos config matrix`). Precedence: `--room` flag > env > config file |
|
||||
| `pos communication matrix listener` | `bin/pos-communication-matrix-listener` | Matrix listener: map `/command` → bash commands and run them from room messages; interactive editor for the map | Same `matrix.env` (reacts to `MATRIX_USER_ID`'s own messages; watches `MATRIX_ROOM_ID` or all joined rooms). Map lives in `~/.config/linux_post_install/matrix_commands.env` (`/cmd=bash command` lines, chmod 600) |
|
||||
| `pos communication scrcpy [cmd]` | `bin/pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb: `devices`, `record`, `tcpip`, `connect`, `push`, `pull`, `screenshot`, `info` (bare = mirror) | `scrcpy.env` (`SCRCPY_SERIAL`, `SCRCPY_MAX_SIZE`, `SCRCPY_MAX_FPS`, `SCRCPY_BIT_RATE`, `SCRCPY_FULLSCREEN`, `SCRCPY_RECORD_DIR`, `SCRCPY_PUSH_TARGET`, `SCRCPY_EXTRA_FLAGS`) via `pos config scrcpy` |
|
||||
@@ -326,12 +335,18 @@ The bot token is a secret — it is stored only in `~/.config/linux_post_install
|
||||
|---------|----------|
|
||||
| `pos communication telegram listener` | Interactive editor for the `/command` → bash map (`a`dd / `e`dit / `r`emove / `t`est / `q`uit); test-runs run `bash -n` first and may execute the command live |
|
||||
| `pos communication telegram listener --status` | Shows service state (running/autostart), config + map file paths, and the mapped commands |
|
||||
| `pos communication telegram listener --enable` | Installs + starts a systemd **user** service (`pos-telegram-listener.service`); the daemon polls `getUpdates` and runs mapped commands |
|
||||
| `pos communication telegram listener --enable` | Installs + starts a systemd **user** service (`pos-telegram-listener.service`); the daemon polls `getUpdates` and runs mapped commands + text-prefix apps |
|
||||
| `pos communication telegram listener --disable` | Stops, disables, and removes the service |
|
||||
| `pos communication telegram listener --sync-commands` | Push the mapped `/commands` to the bot's `/` menu (`setMyCommands`) — also run automatically after every map edit, on `--enable`, and at daemon start |
|
||||
| `pos communication telegram listener --run` | Run the polling loop in the foreground (what the service executes) |
|
||||
| `pos communication telegram listener prefix` | List the text-prefix map (`telegram_prefixes.env`: `<word>=command` lines) plus the built-in Gemini bridge word |
|
||||
| `pos communication telegram listener prefix <word>` | Show one mapping, or map `<word>` to a command with `prefix <word> <command...>` — non-command messages `<word> <text>` run the command with `<text>` appended as ONE quoted argument (e.g. `prefix opencode opencode` → "opencode check cpu" runs `opencode "check cpu"`). `prefix -r <word>` removes. Mapped words shadow the Gemini bridge; the bridge word itself (`TELEGRAM_AI_PREFIX`, default `ai`) is set via `pos config telegram` |
|
||||
|
||||
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` and the answer is replied verbatim; an AI failure replies the error plus a `pos config ai` hint. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `pos system backup` self-notifies, so `/backup=@quiet pos system backup $HOME/Documents` avoids a double message). `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command".
|
||||
|
||||
**Text-prefix map** (`telegram_prefixes.env`, managed via the `prefix` verb): for apps, not bash snippets — a non-command message `<word> <text>` runs the mapped command with `<text>` appended as ONE quoted argument, e.g. `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`. First match wins (file order), matching is case-insensitive and the word must be space-delimited (bare `<word>` with no trailing space replies Unknown command). Values are syntax-checked on save; `@quiet ` values suppress the reply; the 120s timeout + empty→`OK` + `exit <rc>` reply mirror the command map. Routing order on every non-command message: text-prefix map → AI bridge → `/command` map → "Unknown command".
|
||||
|
||||
**AI bridge**: non-command text starting with `<word> ` — default `ai `, configurable via `pos config telegram` → `TELEGRAM_AI_PREFIX` (a text-prefix entry with the same word shadows it) — is forwarded to Gemini via `pos ai gemini ask` (case-insensitive, e.g. `ai what is Nvidia` or `BOT what is Nvidia` with prefix `bot`) and the answer is replied verbatim; an AI failure replies the error. Commands run as your user via `timeout bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `pos system backup` self-notifies, so `/backup=@quiet pos system backup $HOME/Documents` avoids a double message). `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
|
||||
Map entries may carry an optional **description** shown in the bot's `/` menu: `/cmd::short description=bash command` (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via `setMyCommands`, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase `[a-z0-9_]` names (1–32 chars) — commands like `/Status` or `/my-cmd` are skipped from the menu with a warning but still resolve when typed.
|
||||
|
||||
@@ -476,6 +491,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).
|
||||
|
||||
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 |
|
||||
|---------|---------|
|
||||
| `pos config` | Scope picker (on a TTY), otherwise the scope list |
|
||||
|
||||
+16
-1
@@ -12,6 +12,12 @@ Everything that runs during the bootstrap install: `install.sh`, `preinstall.sh`
|
||||
- [lib/common.sh — shared library](#libcommonsh--shared-library)
|
||||
- [lib/flags.sh — feature flags](#libflagssh--feature-flags)
|
||||
- [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/usb-automount.sh — USB automount feature](#featuresusb-automountsh--usb-automount-feature)
|
||||
- [x64_bin/ — precompiled binaries](#x64_bin--precompiled-binaries)
|
||||
@@ -38,7 +44,7 @@ The phases:
|
||||
| # | Phase | Script/action |
|
||||
|---|-------|----------------|
|
||||
| 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 |
|
||||
| 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) |
|
||||
@@ -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
|
||||
|
||||
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
|
||||
|
||||
+21
-6
@@ -150,6 +150,19 @@ you: ai what is Nvidia
|
||||
bot: NVIDIA is a company best known for GPUs...
|
||||
```
|
||||
|
||||
The trigger word is configurable via `pos config telegram` →
|
||||
`TELEGRAM_AI_PREFIX` (default `ai`); it takes effect immediately, so with
|
||||
prefix `bot` you'd message `bot what is Nvidia`. `pos communication telegram
|
||||
listener prefix` shows the current value.
|
||||
|
||||
The listener also has a generic **text-prefix map** (`telegram_prefixes.env`,
|
||||
managed with `pos communication telegram listener prefix <word> <command...>`)
|
||||
that runs any app with the rest of the message as one argument — e.g.
|
||||
`prefix opencode opencode` turns the message `opencode check cpu` into
|
||||
`opencode "check cpu"`. Routing order per non-command message: text-prefix
|
||||
map → AI bridge → `/command` map, so mapping a word in the prefix map
|
||||
shadows the Gemini bridge for that word.
|
||||
|
||||
The bridge lives in the Telegram listener's `handle_message` (it calls
|
||||
`pos ai ask`); only the owner chat is served, so your key stays private.
|
||||
Set a different model per message:
|
||||
@@ -162,13 +175,14 @@ you: ai --model gemini-2.5-flash explain a Raft consensus log
|
||||
|
||||
Each chat has its own persistent session (`telegram-<chat id>` — independent
|
||||
of your terminal's `default` session), so the model
|
||||
remembers the conversation; `ai /reset` clears it. The listener passes a system
|
||||
remembers the conversation; `<prefix> /reset` clears it (with the default
|
||||
prefix that's `ai /reset`). The listener passes a system
|
||||
prompt telling the model it is answering in a Telegram chat — so it uses emojis
|
||||
and stays lively — and strips markdown (`**x**`, backticks, `#`, links…) from
|
||||
the reply before sending it, since messages go out as plain text.
|
||||
|
||||
Replying to a message before `ai …` makes that message part of the prompt, so
|
||||
the model can answer about it:
|
||||
Replying to a message before `<prefix> …` makes that message part of the
|
||||
prompt, so the model can answer about it:
|
||||
|
||||
```
|
||||
you: /status → bot: (system health output…)
|
||||
@@ -237,9 +251,10 @@ truncated). To disable: `unset __POS_CAPTURE_ACTIVE`.
|
||||
model's context window; check `pos ai models`.
|
||||
- `API error 429` → rate limit (free tier); wait and retry, or use a different
|
||||
model.
|
||||
- Nothing in Telegram for `ai …` → the listener daemon must be running
|
||||
(`pos communication telegram listener --status`); the bot token and owner
|
||||
chat id must match `pos config telegram`.
|
||||
- Nothing in Telegram for `<prefix> …` (default `ai`) → the listener daemon
|
||||
must be running (`pos communication telegram listener --status`); the bot
|
||||
token and owner chat id must match `pos config telegram`. Check the current
|
||||
trigger word with `pos communication telegram listener prefix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -97,6 +97,21 @@ pos communication telegram listener --disable # remove it
|
||||
- **Runs as you:** mapped commands execute as your user with a 60s timeout,
|
||||
stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`).
|
||||
`sudo` inside a command needs a NOPASSWD rule.
|
||||
- **Text-prefix map (apps):** `~/.config/linux_post_install/telegram_prefixes.env`
|
||||
(chmod 600), one `<word>=command` per line — a non-command message
|
||||
`<word> <text>` runs the app with `<text>` appended as ONE quoted argument,
|
||||
e.g. `opencode=opencode` turns "opencode check cpu" into `opencode "check cpu"`.
|
||||
First match wins (file order), case-insensitive, word must be space-delimited.
|
||||
Manage it with `pos communication telegram listener prefix <word> <command...>`
|
||||
(bare `prefix` lists, `prefix <word>` shows one, `prefix -r <word>` removes):
|
||||
```bash
|
||||
pos communication telegram listener prefix opencode opencode
|
||||
# then message: opencode check cpu → runs opencode "check cpu"
|
||||
pos communication telegram listener prefix ai "pos ai gemini ask --session telegram-\$TELEGRAM_CHAT_ID"
|
||||
# overrides the built-in Gemini bridge for the word 'ai'
|
||||
```
|
||||
Routing order per non-command message: text-prefix map → AI bridge →
|
||||
`/command` map → "Unknown command".
|
||||
- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but
|
||||
does NOT reply — for commands that already send their own notification, so
|
||||
you don't get it twice. `pos system backup` self-notifies, so
|
||||
|
||||
+33
-2
@@ -1,13 +1,14 @@
|
||||
# How-To: `pos system`
|
||||
|
||||
Host care: encrypted backups, firewall, and the health dashboard. Tools:
|
||||
`backup`, `firewall`, `health`.
|
||||
Host care: encrypted backups, firewall, health dashboard, and uninstall. Tools:
|
||||
`backup`, `firewall`, `health`, `uninstall`.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `pos system health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker) |
|
||||
| `pos system backup` | gpg-encrypted (AES-256) folder snapshots |
|
||||
| `pos system firewall` | Interactive UFW ("UFW POWER") management |
|
||||
| `pos system uninstall` | Safe, interactive uninstaller for the pos toolkit |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,6 +205,36 @@ not).
|
||||
`sudo ufw allow 22/tcp`, then `sudo ufw reload`.
|
||||
- `ufw reset` requires typing `RESET` — deliberate.
|
||||
|
||||
## `pos system uninstall` — remove the pos toolkit
|
||||
|
||||
```bash
|
||||
pos system uninstall # interactive scan + confirm tier 1
|
||||
pos system uninstall --yes # non-interactive, tier 1 only
|
||||
pos system uninstall --yes --config --data # remove everything (nuclear option)
|
||||
```
|
||||
|
||||
Scans the system for installed pos components and removes them in three tiers:
|
||||
|
||||
| Tier | What it removes | How to include |
|
||||
|------|----------------|----------------|
|
||||
| **Tier 1** | Binaries (`/usr/local/bin/pos*`, libs, entertainment plugins, prebuilt, features), systemd services (disable+remove), shell integration (`~/.bashrc` PATH/completion/pos-ai-hook entries), completion file | Always (default) |
|
||||
| **Tier 2** | Config files (`~/.config/linux_post_install/` — `.env` files, `schedule.d/`, `authorized_keys`, `rclone.conf`) | `--config` flag |
|
||||
| **Tier 3** | Session/log data (`~/.local/share/linux_post_install/` — AI sessions, logs, captured output) | `--data` flag |
|
||||
|
||||
The default mode is interactive: it shows what will be removed and asks for
|
||||
confirmation. The git repo is **never** removed — delete it manually if desired.
|
||||
|
||||
**Recipes:**
|
||||
- Quick cleanup: `pos system uninstall --yes`
|
||||
- Full wipe: `pos system uninstall --yes --config --data`
|
||||
- Safe preview: run `pos system uninstall` without `--yes` to see the plan first
|
||||
|
||||
**Troubleshooting:**
|
||||
- "Nothing to remove" → pos toolkit is not installed (or already removed)
|
||||
- After uninstall, run `source ~/.bashrc` or restart your shell
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- Reference: [DOC/POS.md → system](../POS.md)
|
||||
|
||||
@@ -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.
|
||||
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)
|
||||
- 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.
|
||||
|
||||
@@ -66,21 +66,27 @@ _pos_category_exists() {
|
||||
}
|
||||
|
||||
_pos_category_help() {
|
||||
local cat="$1" f
|
||||
local files=() s d
|
||||
for f in "$self"/pos-"$cat"-*; do
|
||||
[ -x "$f" ] || continue
|
||||
files+=("${f##*/pos-$cat-}")
|
||||
done
|
||||
local cat="$1" s s2 extra d sc deps
|
||||
# Lazy registry load — plain dispatch paths never pay the scan cost.
|
||||
source "$self/../lib/registry.sh" 2>/dev/null || source "$self/registry.sh"
|
||||
reg_scan "$self"
|
||||
|
||||
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)
|
||||
|
||||
local -A desc subcmds
|
||||
local sc
|
||||
local -A desc subcmds _deps
|
||||
for s in "${files[@]}"; do
|
||||
d="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)"
|
||||
[ -n "$d" ] && desc["$s"]="${d#*— }"
|
||||
sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$self/pos-$cat-$s" 2>/dev/null)"
|
||||
d="$(reg_lookup "$cat-$s" desc)"
|
||||
[ -n "$d" ] && desc["$s"]="$d"
|
||||
sc="$(reg_lookup "$cat-$s" subcmds)"
|
||||
[ -n "$sc" ] && subcmds["$s"]="$sc"
|
||||
deps="$(reg_lookup "$cat-$s" deps)"
|
||||
[ -n "$deps" ] && _deps["$s"]="$deps"
|
||||
done
|
||||
|
||||
# Nested sub-tools: pos-<cat>-<a>-<b> lists "b" under <a>.
|
||||
@@ -113,6 +119,7 @@ _pos_category_help() {
|
||||
done
|
||||
[ "$is_nested" -eq 1 ] && continue
|
||||
printf ' %-28s%s\n' "$s" "${desc[$s]:-}"
|
||||
[ -n "${_deps[$s]:-}" ] && printf ' [deps: %s]\n' "${_deps[$s]}"
|
||||
for c in ${subcmds[$s]:-}; do
|
||||
printf ' %s %s\n' "$s" "$c"
|
||||
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"; }
|
||||
|
||||
# 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 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
|
||||
cmd="pos"
|
||||
|
||||
+73
-26
@@ -2,8 +2,8 @@
|
||||
set -euo pipefail
|
||||
# POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers
|
||||
# POS_SUBCMDS: ask chat sessions capture models providers
|
||||
# POS_FLAGS: --provider --model --session --system --full --last
|
||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | AI_API_KEY=secret:API key for the active provider | AI_MODEL=:Model id (default per provider) | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset)
|
||||
# POS_FLAGS: --provider --model --session --system --full --last --trust
|
||||
# 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"
|
||||
|
||||
@@ -37,7 +37,7 @@ LEGACY_OPENROUTER_CONFIG="$HOME/.config/linux_post_install/ai-openrouter.env"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos ai [subcommand] [--provider <name>] [--model <id>] [--session <name>] [--system <text>] [--full] [--last]
|
||||
Usage: pos ai [subcommand] [--provider <name>] [--model <id>] [--session <name>] [--system <text>] [--full] [--last] [--trust]
|
||||
|
||||
AI assistant with pluggable providers (gemini, openrouter).
|
||||
|
||||
@@ -72,17 +72,16 @@ Options:
|
||||
order: (1) newest pos log, (2) captured output from
|
||||
'capture'. Notes on stderr which source was attached and
|
||||
its age; warns when stale (>60 min).
|
||||
--trust Auto-execute agent-detected commands without confirmation.
|
||||
Used by trusted alias wrappers — do NOT pass manually
|
||||
unless you fully trust the agent's output.
|
||||
-h|--help This help.
|
||||
|
||||
Config: $CONFIG_FILE (edit with 'pos config ai')
|
||||
AI_PROVIDER Provider to use (gemini|openrouter, default gemini)
|
||||
AI_API_KEY API key for the active provider (secret)
|
||||
AI_MODEL Model id (default depends on provider)
|
||||
AI_SYSTEM_PROMPT Custom system prompt (overrides built-in; empty to reset)
|
||||
AI_GEMINI_API_KEY Legacy: Gemini API key fallback
|
||||
AI_GEMINI_MODEL Legacy: Gemini model fallback
|
||||
OPENROUTER_API_KEY Legacy: OpenRouter API key fallback
|
||||
OPENROUTER_MODEL Legacy: OpenRouter model fallback
|
||||
Provider keys: auto-discovered from lib/ai-providers/*.sh
|
||||
(AI_GEMINI_API_KEY, OPENROUTER_API_KEY, etc.)
|
||||
|
||||
Notes:
|
||||
ask is terse by default: a built-in system instruction tells the model to
|
||||
@@ -164,16 +163,11 @@ load_config() {
|
||||
resolve_key() {
|
||||
load_config
|
||||
local p="${PROVIDER:-gemini}"
|
||||
# 1. Unified AI_API_KEY
|
||||
if [ -n "${AI_API_KEY:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
# 2. Provider-specific fallback
|
||||
# Each provider has its own API key — set AI_API_KEY internally for adapters
|
||||
case "$p" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && export AI_API_KEY="$AI_GEMINI_API_KEY" && return 0 ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && export AI_API_KEY="$OPENROUTER_API_KEY" && return 0 ;;
|
||||
esac
|
||||
# 3. No key found — return 1 (require_key handles the error message)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -181,10 +175,10 @@ require_key() {
|
||||
if ! resolve_key >/dev/null 2>&1; then
|
||||
local p="${PROVIDER:-gemini}"
|
||||
case "$p" in
|
||||
gemini) err "No Gemini API key — run 'pos config ai' or set AI_API_KEY / AI_GEMINI_API_KEY" ;;
|
||||
openrouter) err "No OpenRouter API key — run 'pos config ai' or set AI_API_KEY / OPENROUTER_API_KEY" ;;
|
||||
gemini) err "No Gemini API key — run 'pos config ai' and set AI_GEMINI_API_KEY" ;;
|
||||
openrouter) err "No OpenRouter API key — run 'pos config ai' and set OPENROUTER_API_KEY" ;;
|
||||
esac
|
||||
err "No API key for provider '$p' — run 'pos config ai' or set AI_API_KEY"
|
||||
err "No API key for provider '$p' — run 'pos config ai'"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -362,6 +356,52 @@ render_markdown() {
|
||||
printf '\n%s\n' "$rendered"
|
||||
}
|
||||
|
||||
# ── Command extraction from AI responses ────────────────────────
|
||||
_extract_commands() {
|
||||
local text="$1"
|
||||
printf '%s' "$text" | awk '
|
||||
/^```(bash|sh|shell)/ { in_block=1; next }
|
||||
/^```/ { if (in_block) in_block=0; next }
|
||||
in_block && NF > 0 { lines[++n] = $0 }
|
||||
END {
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (i > 1) printf "\n"
|
||||
printf "%s", lines[i]
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# ── Interactive prompt to run extracted commands ─────────────────
|
||||
_prompt_run_command() {
|
||||
local cmd="$1" trusted="${2:-0}"
|
||||
# Only prompt on interactive terminals with a controlling tty
|
||||
[ -w /dev/tty ] || return 0
|
||||
printf '\n%s\n' "Command detected:" >&2
|
||||
printf ' %s\n\n' "$cmd" >&2
|
||||
if [ "$trusted" -eq 1 ]; then
|
||||
printf '[trusted] Auto-executing (no confirmation)\n\n' >&2
|
||||
printf '%s\n' "$cmd"
|
||||
run eval "$cmd"
|
||||
return
|
||||
fi
|
||||
printf 'Run this command? [Y/n] ' >&2
|
||||
local choice
|
||||
IFS= read -r choice </dev/tty || choice=""
|
||||
case "${choice,,}" in
|
||||
n|N)
|
||||
# Add to shell history so user can press ↑ to recall, edit, run
|
||||
history -s "$cmd" 2>/dev/null || true
|
||||
printf '%s\n' "Command added to history — press ↑ to recall, edit, and run." >&2
|
||||
;;
|
||||
*)
|
||||
# Y or Enter: execute
|
||||
printf '%s\n' "$cmd"
|
||||
run eval "$cmd"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Machine context appended to the built-in default prompt ─────
|
||||
mc_clean() {
|
||||
sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g' \
|
||||
@@ -480,6 +520,10 @@ cmd_ask() {
|
||||
messages="$(session_push "$messages" assistant "$out")"
|
||||
session_save "$messages"
|
||||
render_markdown "$out"
|
||||
# Command execution prompt: extract commands from response and offer to run
|
||||
local _cmd
|
||||
_cmd="$(_extract_commands "$out")"
|
||||
[ -n "$_cmd" ] && _prompt_run_command "$_cmd" "$TRUST_MODE"
|
||||
}
|
||||
|
||||
cmd_chat() {
|
||||
@@ -513,6 +557,10 @@ cmd_chat() {
|
||||
session_save "$messages"
|
||||
printf '\n'
|
||||
render_markdown "$answer"
|
||||
# Command execution prompt: extract commands from response and offer to run
|
||||
local _cmd
|
||||
_cmd="$(_extract_commands "$answer")"
|
||||
[ -n "$_cmd" ] && _prompt_run_command "$_cmd" "$TRUST_MODE"
|
||||
printf '\n\n'
|
||||
done
|
||||
echo
|
||||
@@ -569,14 +617,10 @@ cmd_providers() {
|
||||
[ -n "$pmodel" ] || pmodel="unknown"
|
||||
# Check if API key exists for this provider
|
||||
configured="not configured"
|
||||
if [ -n "${AI_API_KEY:-}" ]; then
|
||||
configured="configured"
|
||||
else
|
||||
case "$name" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && configured="configured" ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && configured="configured" ;;
|
||||
esac
|
||||
fi
|
||||
case "$name" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && configured="configured" ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && configured="configured" ;;
|
||||
esac
|
||||
current=""
|
||||
[ "$name" = "$active" ] && current=" ← active"
|
||||
printf ' %-16s %s (model: %s)%s\n' "$name" "$configured" "$pmodel" "$current"
|
||||
@@ -587,6 +631,7 @@ cmd_providers() {
|
||||
MODEL_OVERRIDE=""
|
||||
FULL_MODE=0
|
||||
LAST_MODE=0
|
||||
TRUST_MODE=0
|
||||
PROVIDER=""
|
||||
cmd=""
|
||||
args=()
|
||||
@@ -609,6 +654,8 @@ while [ $# -gt 0 ]; do
|
||||
FULL_MODE=1; shift ;;
|
||||
--last)
|
||||
LAST_MODE=1; shift ;;
|
||||
--trust)
|
||||
TRUST_MODE=1; shift ;;
|
||||
-*) err "Unknown option '$1' (see --help)" ;;
|
||||
*)
|
||||
if [ -z "$cmd" ]; then
|
||||
|
||||
Executable
+760
@@ -0,0 +1,760 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai alias — manage AI agent aliases
|
||||
# POS_SUBCMDS: create edit remove list show
|
||||
|
||||
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"
|
||||
|
||||
# ── Paths & constants ──────────────────────────────────────────
|
||||
ENV_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.env"
|
||||
SH_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.sh"
|
||||
|
||||
# Provider discovery — same pattern as bin/pos-ai (lines 17-18)
|
||||
PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers"
|
||||
[ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers"
|
||||
|
||||
# ── Core helpers ───────────────────────────────────────────────
|
||||
|
||||
_alias_load() {
|
||||
_ALIAS_NAMES=(); _ALIAS_PROVIDERS=(); _ALIAS_SESSIONS=(); _ALIAS_PROMPTS=()
|
||||
_ALIAS_TRUSTED=()
|
||||
[ -f "$ENV_FILE" ] || return 0
|
||||
# NOTE: loop vars use _l* prefix to avoid dynamic-scope collision with
|
||||
# callers that declare 'local name' (bash read clobbers the nearest
|
||||
# matching variable up the call chain).
|
||||
local _ln _lp _ls _lp2 _lr
|
||||
while IFS='|' read -r _ln _lp _ls _lp2 _lr; do
|
||||
[[ "$_ln" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${_ln// /}" ]] && continue
|
||||
_ln="${_ln## }"; _ln="${_ln%% }"
|
||||
[[ "$_ln" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
||||
_lp="${_lp## }"; _lp="${_lp%% }"
|
||||
_ls="${_ls## }"; _ls="${_ls%% }"
|
||||
_ALIAS_NAMES+=("$_ln")
|
||||
_ALIAS_PROVIDERS+=("$_lp")
|
||||
_ALIAS_SESSIONS+=("$_ls")
|
||||
_ALIAS_PROMPTS+=("$(_alias_prompt_decode "$_lp2")")
|
||||
_ALIAS_TRUSTED+=("${_lr:-0}")
|
||||
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
||||
}
|
||||
|
||||
_alias_save() {
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
{
|
||||
printf '%s\n' "# AI aliases — managed by pos ai alias (do not hand-edit)"
|
||||
printf '%s\n' "# Format: alias_name|provider|session_name|system_prompt|trusted"
|
||||
printf '%s\n' "#"
|
||||
local i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
printf '%s|%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||
"${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_encode "${_ALIAS_PROMPTS[$i]}")" "${_ALIAS_TRUSTED[$i]:-0}"
|
||||
done
|
||||
} >"$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
}
|
||||
|
||||
# Build the pos-ai command for an alias with the prompt safely quoted as
|
||||
# ONE shell word (printf %q) — shared by regen (stored form) and show
|
||||
# (copy-pasteable display form). Empty prompt → no --system fragment.
|
||||
_alias_quote_cmd() {
|
||||
local provider="$1" session="$2" prompt="${3:-}" trusted="${4:-0}" q_prompt
|
||||
printf -v q_prompt '%q' "$prompt"
|
||||
printf 'pos ai %s ask --session %s' "$provider" "$session"
|
||||
[ "$trusted" -eq 1 ] && printf ' --trust'
|
||||
[ -n "$prompt" ] && printf ' --system %s' "$q_prompt"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Activation artifacts (Option B) ────────────────────────────
|
||||
# ENV stays the single source of truth; each alias is materialized as an
|
||||
# executable wrapper script at ~/.local/bin/<name>. Every invocation re-reads
|
||||
# current bytes, so a stale snapshot (the old sourced-alias failure mode) is
|
||||
# impossible by construction. No shell sourcing of any kind.
|
||||
|
||||
_wrapper_path() {
|
||||
printf '%s/.local/bin/%s' "$HOME" "$1"
|
||||
}
|
||||
|
||||
# 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 [trusted]).
|
||||
# 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:-}" trusted="${5:-0}"
|
||||
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" "$trusted") "\$@"
|
||||
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 [trusted]
|
||||
local path="$(_wrapper_path "$1")" tmp
|
||||
tmp="$(mktemp "${HOME}/.local/bin/.pos-alias.XXXXXX")"
|
||||
_wrapper_render "$1" "$2" "$3" "$4" "${5:-0}" >"$tmp"
|
||||
if cmp -s "$tmp" "$path"; then
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
if ! bash -n "$tmp" 2>/dev/null; then
|
||||
warn "Wrapper for '$1' failed syntax check — keeping previous version" >&2
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
mv "$tmp" "$path"
|
||||
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]}" "${_ALIAS_TRUSTED[$i]:-0}" || :
|
||||
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() {
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
if [ ${#providers[@]} -eq 0 ]; then
|
||||
err "No AI providers installed — run 'pos ai' setup first"
|
||||
fi
|
||||
menu_pick "Pick provider" "${providers[@]}"
|
||||
}
|
||||
|
||||
_alias_find() {
|
||||
local name="$1" i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" = "$name" ]; then
|
||||
echo "$i"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "-1"
|
||||
return 0
|
||||
}
|
||||
|
||||
_alias_name_valid() {
|
||||
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
|
||||
}
|
||||
|
||||
_alias_prompt_truncate() {
|
||||
local p="${1//$'\n'/\\n}" max="${2:-42}"
|
||||
if [ ${#p} -gt "$max" ]; then
|
||||
printf '%s…' "${p:0:max}"
|
||||
else
|
||||
printf '%s' "$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Prompt encoding for the line-based env file ────────────────
|
||||
# menu-lib's reader accepts literal multiline prompts; the ENV file is one
|
||||
# record per line, so prompts are escaped on save and unescaped on load:
|
||||
# backslash → \\ newline → \n
|
||||
# ('|' is already rejected at input, so | never needs escaping.)
|
||||
_alias_prompt_encode() {
|
||||
local s="$1" out="" c="" i
|
||||
for ((i = 0; i < ${#s}; i++)); do
|
||||
c="${s:i:1}"
|
||||
if [ "$c" = '\' ]; then
|
||||
out+='\\'
|
||||
elif [ "$c" = $'\n' ]; then
|
||||
out+='\n'
|
||||
else
|
||||
out+="$c"
|
||||
fi
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# Decode is order-safe: \\n (escaped newline text) is backslash + n, which
|
||||
# decodes to literal '\n' only after both escapes are consumed in order.
|
||||
_alias_prompt_decode() {
|
||||
local s="$1" out="" c="" n="" i=0
|
||||
while [ "$i" -lt "${#s}" ]; do
|
||||
c="${s:i:1}"
|
||||
if [ "$c" = '\' ] && [ $((i + 1)) -lt "${#s}" ]; then
|
||||
n="${s:i+1:1}"
|
||||
if [ "$n" = 'n' ]; then
|
||||
out+=$'\n'; i=$((i + 2)); continue
|
||||
elif [ "$n" = '\' ]; then
|
||||
out+='\'; i=$((i + 2)); continue
|
||||
fi
|
||||
fi
|
||||
out+="$c"
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# ── Non-interactive output ─────────────────────────────────────
|
||||
|
||||
# SINGLE alias-table renderer — used by `list` (stdout) and the menu
|
||||
# pre-render (inside its stderr display block). One source of truth for the
|
||||
# grid so the two contexts can never drift or duplicate each other.
|
||||
_alias_table() {
|
||||
local count=${#_ALIAS_NAMES[@]} i
|
||||
[ "$count" -eq 0 ] && return 0
|
||||
printf ' %-12s %-12s %-12s %-5s %s\n' "Name" "Provider" "Session" "Trust" "Prompt"
|
||||
printf ' %-12s %-12s %-12s %-5s %s\n' "------------" "------------" "------------" "-----" \
|
||||
"------------------------------------------"
|
||||
for ((i = 0; i < count; i++)); do
|
||||
local _tmark="—"
|
||||
[ "${_ALIAS_TRUSTED[$i]:-0}" = "1" ] && _tmark="yes"
|
||||
printf ' %-12s %-12s %-12s %-5s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||
"${_ALIAS_SESSIONS[$i]}" "$_tmark" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")"
|
||||
done
|
||||
}
|
||||
|
||||
_alias_list() {
|
||||
printf 'Aliases (%d):\n' "${#_ALIAS_NAMES[@]}"
|
||||
_alias_table
|
||||
}
|
||||
|
||||
_alias_show() {
|
||||
local idx
|
||||
idx="$(_alias_find "$1")"
|
||||
[ "$idx" = "-1" ] && err "Alias '$1' not found"
|
||||
local name="${_ALIAS_NAMES[$idx]}" provider="${_ALIAS_PROVIDERS[$idx]}"
|
||||
local session="${_ALIAS_SESSIONS[$idx]}" prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
local trusted="${_ALIAS_TRUSTED[$idx]:-0}"
|
||||
[ -z "$session" ] && session="$name"
|
||||
printf ' %-12s %s\n' "Alias:" "$name"
|
||||
printf ' %-12s %s\n' "Provider:" "$provider"
|
||||
printf ' %-12s %s\n' "Session:" "$session"
|
||||
if _alias_check_path; then
|
||||
printf ' %-12s %s\n' "Wrapper:" "$(_wrapper_path "$name")"
|
||||
else
|
||||
printf ' %-12s %s\n' "Wrapper:" "(not installed — ~/.local/bin not on PATH)"
|
||||
fi
|
||||
printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}"
|
||||
printf ' %-12s %s\n' "Trusted:" "$([ "$trusted" = "1" ] && echo "yes (auto-executes commands)" || echo "no (prompts before running)")"
|
||||
# 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" "$trusted")"
|
||||
}
|
||||
|
||||
# ── Interactive: main menu ─────────────────────────────────────
|
||||
|
||||
_alias_menu() {
|
||||
menu_guard || return 1
|
||||
while true; do
|
||||
{
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}"
|
||||
else
|
||||
_alias_table
|
||||
printf ' %d alias(es)\n' "${#_ALIAS_NAMES[@]}"
|
||||
fi
|
||||
echo >&2
|
||||
} >&2
|
||||
local choice
|
||||
choice="$(menu_run "AI Agent Aliases" "Create new alias" "Edit existing alias" \
|
||||
"Remove alias" "List aliases")" || return 0
|
||||
case "$choice" in
|
||||
1) _alias_create ;;
|
||||
2) _alias_edit ;;
|
||||
3) _alias_remove ;;
|
||||
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
|
||||
done
|
||||
}
|
||||
|
||||
# ── Interactive: create ────────────────────────────────────────
|
||||
|
||||
_alias_create() {
|
||||
local preset_name="${1:-}"
|
||||
section "Create AI Agent Alias" >&2
|
||||
|
||||
# Step 1: Alias name
|
||||
local name="$preset_name"
|
||||
while true; do
|
||||
if [ -z "$name" ]; then
|
||||
step 1 4 "Alias Name" >&2
|
||||
name="$(menu_ask_value "Alias name" "")" || return 0
|
||||
fi
|
||||
[ -z "$name" ] && { warn "Alias name cannot be empty" >&2; name=""; continue; }
|
||||
if ! _alias_name_valid "$name"; then
|
||||
warn "Invalid name '$name' — use letters, digits, hyphens, underscores (start with a letter)" >&2
|
||||
name=""; continue
|
||||
fi
|
||||
_alias_load
|
||||
local existing
|
||||
existing="$(_alias_find "$name")"
|
||||
if [ "$existing" != "-1" ]; then
|
||||
warn "Alias '$name' already exists — use 'pos ai alias edit $name' instead" >&2
|
||||
[ -n "$preset_name" ] && return 1
|
||||
name=""; continue
|
||||
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
|
||||
done
|
||||
|
||||
# Step 2: Provider
|
||||
step 2 4 "Provider" >&2
|
||||
local pidx
|
||||
pidx="$(_alias_provider_pick)" || return 0
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
local provider="${providers[$((pidx - 1))]}"
|
||||
|
||||
# Step 3: Session name
|
||||
local session=""
|
||||
while true; do
|
||||
step 3 5 "Session Name" >&2
|
||||
session="$(menu_ask_value "Session name" "$name")" || return 0
|
||||
if [ -n "$session" ] && ! _alias_name_valid "$session"; then
|
||||
warn "Invalid session '$session' — use letters, digits, hyphens, underscores" >&2
|
||||
session=""; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
[ -z "$session" ] && session="$name"
|
||||
|
||||
# Step 4: System prompt
|
||||
local prompt=""
|
||||
while true; do
|
||||
step 4 5 "System Prompt" >&2
|
||||
prompt="$(menu_ask_value "System prompt (empty = use built-in)" "")" || return 0
|
||||
if [[ "$prompt" == *'|'* ]]; then
|
||||
warn "System prompt must not contain '|' characters" >&2
|
||||
prompt=""; continue
|
||||
fi
|
||||
if [ ${#prompt} -gt 500 ]; then
|
||||
warn "Prompt is ${#prompt} chars — consider keeping it concise" >&2
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# Step 5: Trust level
|
||||
local trusted="0"
|
||||
while true; do
|
||||
step 5 5 "Trust Level" >&2
|
||||
{
|
||||
echo " TRUSTED aliases auto-execute commands from the agent"
|
||||
echo " WITHOUT asking for confirmation."
|
||||
echo ""
|
||||
echo " Only enable this for aliases you fully trust with"
|
||||
echo " unrestricted shell access on this machine."
|
||||
} >&2
|
||||
local trust_ans
|
||||
trust_ans="$(menu_ask_value "Trust this alias? (y/N)" "N")" || return 0
|
||||
case "${trust_ans,,}" in
|
||||
y|yes) trusted="1"; break ;;
|
||||
n|no|"") trusted="0"; break ;;
|
||||
*) warn "Please answer y or n" >&2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Confirmation
|
||||
{
|
||||
echo "────────────────────────────────────────────"
|
||||
printf ' Create alias '\''%s'\''?\n' "$name"
|
||||
printf ' Provider: %s\n' "$provider"
|
||||
printf ' Session: %s\n' "$session"
|
||||
local dp="$(_alias_prompt_truncate "$prompt" 50)"
|
||||
printf ' Prompt: %s\n' "${dp:-<built-in>}"
|
||||
printf ' Trusted: %s\n' "$([ "$trusted" = "1" ] && echo "yes (auto-execute)" || echo "no (confirm before run)")"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Create alias '$name'?" y; then
|
||||
log "Aborted." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_NAMES+=("$name")
|
||||
_ALIAS_PROVIDERS+=("$provider")
|
||||
_ALIAS_SESSIONS+=("$session")
|
||||
_ALIAS_PROMPTS+=("$prompt")
|
||||
_ALIAS_TRUSTED+=("$trusted")
|
||||
_alias_save
|
||||
_alias_sync
|
||||
log "Alias '$name' created." >&2
|
||||
log "Available immediately: $(_wrapper_path "$name")" >&2
|
||||
}
|
||||
|
||||
# ── Interactive: edit ──────────────────────────────────────────
|
||||
|
||||
_alias_edit() {
|
||||
local preset_name="${1:-}"
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
warn "No aliases to edit — create one first" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name="$preset_name"
|
||||
if [ -z "$name" ]; then
|
||||
section "Edit AI Agent Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local p="${_ALIAS_PROMPTS[$i]}"
|
||||
if [ ${#p} -gt 30 ]; then
|
||||
p="${p:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
|
||||
done
|
||||
local picked
|
||||
picked="$(menu_pick "Pick alias to edit" "${display_items[@]}")" || return 0
|
||||
name="${_ALIAS_NAMES[$((picked - 1))]}"
|
||||
fi
|
||||
|
||||
local idx
|
||||
idx="$(_alias_find "$name")"
|
||||
if [ "$idx" = "-1" ]; then
|
||||
err "Alias '$name' not found"
|
||||
fi
|
||||
|
||||
# Show current values
|
||||
{
|
||||
echo " Current values for '$name':"
|
||||
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
|
||||
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
|
||||
local cp="${_ALIAS_PROMPTS[$idx]}"
|
||||
[ -z "$cp" ] && cp="(default)"
|
||||
printf ' Prompt: %s\n' "$cp"
|
||||
printf ' Trusted: %s\n' "$([ "${_ALIAS_TRUSTED[$idx]:-0}" = "1" ] && echo "yes" || echo "no")"
|
||||
echo >&2
|
||||
} >&2
|
||||
|
||||
local new_provider="${_ALIAS_PROVIDERS[$idx]}"
|
||||
local new_session="${_ALIAS_SESSIONS[$idx]}"
|
||||
local new_prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
local new_trusted="${_ALIAS_TRUSTED[$idx]:-0}"
|
||||
local changed=0
|
||||
|
||||
# Edit provider
|
||||
step 1 4 "Provider" >&2
|
||||
local pidx
|
||||
pidx="$(_alias_provider_pick)" || return 0
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
local picked_provider="${providers[$((pidx - 1))]}"
|
||||
if [ "$picked_provider" != "$new_provider" ]; then
|
||||
new_provider="$picked_provider"
|
||||
changed=1
|
||||
fi
|
||||
|
||||
# Edit session
|
||||
local tmp_session=""
|
||||
while true; do
|
||||
step 2 4 "Session Name" >&2
|
||||
tmp_session="$(menu_ask_value "Session name" "$new_session")" || return 0
|
||||
if [ -n "$tmp_session" ] && ! _alias_name_valid "$tmp_session"; then
|
||||
warn "Invalid session '$tmp_session' — use letters, digits, hyphens, underscores" >&2
|
||||
tmp_session=""; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
[ -n "$tmp_session" ] && new_session="$tmp_session"
|
||||
[ "$new_session" != "${_ALIAS_SESSIONS[$idx]}" ] && changed=1
|
||||
|
||||
# Edit prompt
|
||||
local tmp_prompt=""
|
||||
local default_prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
# Display-safe default (newlines → \n, truncated); Enter on it keeps the
|
||||
# FULL original prompt — a long/multiline prompt must never be silently
|
||||
# replaced by its truncated display form.
|
||||
local default_display="$(_alias_prompt_truncate "$default_prompt" 80)"
|
||||
while true; do
|
||||
step 3 4 "System Prompt" >&2
|
||||
if ! tmp_prompt="$(menu_ask_value "System prompt" "$default_display")"; then
|
||||
# EOF/cancel: empty-answer abort only when there IS a default;
|
||||
# Enter on an empty original prompt keeps it empty and continues.
|
||||
[ -z "$default_prompt" ] && tmp_prompt="" || return 0
|
||||
fi
|
||||
if [ "$tmp_prompt" = "$default_display" ]; then
|
||||
tmp_prompt="$default_prompt" # Enter → keep the full original
|
||||
fi
|
||||
if [[ "$tmp_prompt" == *'|'* ]]; then
|
||||
warn "System prompt must not contain '|' characters" >&2
|
||||
tmp_prompt=""; continue
|
||||
fi
|
||||
if [ ${#tmp_prompt} -gt 500 ]; then
|
||||
warn "Prompt is ${#tmp_prompt} chars — consider keeping it concise" >&2
|
||||
fi
|
||||
break
|
||||
done
|
||||
# Keep full current if user pressed Enter (tmp_prompt = default_prompt value)
|
||||
if [ -n "$tmp_prompt" ]; then
|
||||
new_prompt="$tmp_prompt"
|
||||
fi
|
||||
[ "$new_prompt" != "${_ALIAS_PROMPTS[$idx]}" ] && changed=1
|
||||
|
||||
# Edit trust
|
||||
step 4 4 "Trust Level" >&2
|
||||
local cur_trust_label="no"
|
||||
[ "$new_trusted" = "1" ] && cur_trust_label="yes"
|
||||
local trust_ans
|
||||
trust_ans="$(menu_ask_value "Trust this alias? (y/N)" "$cur_trust_label")" || return 0
|
||||
case "${trust_ans,,}" in
|
||||
y|yes) new_trusted="1" ;;
|
||||
n|no|"") new_trusted="$new_trusted" ;;
|
||||
*) warn "Please answer y or n" >&2 ;;
|
||||
esac
|
||||
[ "$new_trusted" != "${_ALIAS_TRUSTED[$idx]:-0}" ] && changed=1
|
||||
|
||||
# No changes?
|
||||
if [ "$changed" -eq 0 ]; then
|
||||
log "No changes — nothing to save." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Show diff summary
|
||||
{
|
||||
echo "────────────────────────────────────────────"
|
||||
printf ' Save changes to '\''%s'\''?\n' "$name"
|
||||
local tag_p tag_s tag_pr tag_t
|
||||
[ "$new_provider" = "${_ALIAS_PROVIDERS[$idx]}" ] && tag_p="(unchanged)" || tag_p="(changed)"
|
||||
[ "$new_session" = "${_ALIAS_SESSIONS[$idx]}" ] && tag_s="(unchanged)" || tag_s="(changed)"
|
||||
[ "$new_prompt" = "${_ALIAS_PROMPTS[$idx]}" ] && tag_pr="(unchanged)" || tag_pr="(changed)"
|
||||
[ "$new_trusted" = "${_ALIAS_TRUSTED[$idx]:-0}" ] && tag_t="(unchanged)" || tag_t="(changed)"
|
||||
printf ' Provider: %-12s %s\n' "$new_provider" "$tag_p"
|
||||
printf ' Session: %-12s %s\n' "$new_session" "$tag_s"
|
||||
local dp="$(_alias_prompt_truncate "$new_prompt" 40)"
|
||||
[ -z "$dp" ] && dp="<built-in>"
|
||||
printf ' Prompt: %s %s\n' "$dp" "$tag_pr"
|
||||
printf ' Trusted: %-12s %s\n' "$([ "$new_trusted" = "1" ] && echo "yes" || echo "no")" "$tag_t"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Save changes to '$name'?" y; then
|
||||
log "Discarded." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_PROVIDERS[$idx]="$new_provider"
|
||||
_ALIAS_SESSIONS[$idx]="$new_session"
|
||||
_ALIAS_PROMPTS[$idx]="$new_prompt"
|
||||
_ALIAS_TRUSTED[$idx]="$new_trusted"
|
||||
_alias_save
|
||||
_alias_sync
|
||||
log "Alias '$name' updated — the change is live on next invocation." >&2
|
||||
}
|
||||
|
||||
# ── Interactive: remove ────────────────────────────────────────
|
||||
|
||||
_alias_remove() {
|
||||
local preset_name="${1:-}"
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
warn "No aliases to remove" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name="$preset_name"
|
||||
if [ -z "$name" ]; then
|
||||
section "Remove AI Agent Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local p="${_ALIAS_PROMPTS[$i]}"
|
||||
if [ ${#p} -gt 30 ]; then
|
||||
p="${p:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
|
||||
done
|
||||
local picked
|
||||
picked="$(menu_pick "Pick alias to remove" "${display_items[@]}")" || return 0
|
||||
name="${_ALIAS_NAMES[$((picked - 1))]}"
|
||||
fi
|
||||
|
||||
local idx
|
||||
idx="$(_alias_find "$name")"
|
||||
if [ "$idx" = "-1" ]; then
|
||||
err "Alias '$name' not found"
|
||||
fi
|
||||
|
||||
# Show alias detail
|
||||
{
|
||||
echo " Alias: $name"
|
||||
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
|
||||
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
|
||||
printf ' Prompt: %s\n' "${_ALIAS_PROMPTS[$idx]:-<built-in>}"
|
||||
echo >&2
|
||||
} >&2
|
||||
|
||||
if ! confirm "Remove alias '$name'? This cannot be undone." n; then
|
||||
log "Cancelled." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
local new_names=() new_providers=() new_sessions=() new_prompts=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" != "$name" ]; then
|
||||
new_names+=("${_ALIAS_NAMES[$i]}")
|
||||
new_providers+=("${_ALIAS_PROVIDERS[$i]}")
|
||||
new_sessions+=("${_ALIAS_SESSIONS[$i]}")
|
||||
new_prompts+=("${_ALIAS_PROMPTS[$i]}")
|
||||
fi
|
||||
done
|
||||
_ALIAS_NAMES=("${new_names[@]+"${new_names[@]}"}")
|
||||
_ALIAS_PROVIDERS=("${new_providers[@]+"${new_providers[@]}"}")
|
||||
_ALIAS_SESSIONS=("${new_sessions[@]+"${new_sessions[@]}"}")
|
||||
_ALIAS_PROMPTS=("${new_prompts[@]+"${new_prompts[@]}"}")
|
||||
_alias_save
|
||||
_alias_sync
|
||||
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> ───────────────────────────────────────────────
|
||||
|
||||
# (defined above as _alias_show)
|
||||
|
||||
# ── Usage ──────────────────────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos ai alias [subcommand] [args]
|
||||
|
||||
Manage named AI agent aliases — create, edit, remove, list, and show
|
||||
configured aliases. Each alias maps a name to a provider, session,
|
||||
optional system prompt, and a trust level.
|
||||
|
||||
Trusted aliases auto-execute the agent's commands without asking for
|
||||
confirmation. Only enable for aliases you fully trust with shell access.
|
||||
|
||||
Subcommands:
|
||||
(no args) Interactive menu
|
||||
create [name] Create a new alias (interactive prompts for each field)
|
||||
edit [name] Edit an existing alias (interactive, Enter = keep)
|
||||
remove [name] Remove an alias (interactive, default = no)
|
||||
list List all aliases (non-interactive, machine-readable)
|
||||
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:
|
||||
-h|--help Show this help.
|
||||
|
||||
Examples:
|
||||
pos ai alias # interactive menu
|
||||
pos ai alias list # show all aliases
|
||||
pos ai alias create # interactive create
|
||||
pos ai alias create mybot # create 'mybot' alias
|
||||
pos ai alias edit mybot # edit the 'mybot' alias
|
||||
pos ai alias remove mybot # remove 'mybot' (with confirm)
|
||||
pos ai alias show mybot # show alias details
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Main dispatch ──────────────────────────────────────────────
|
||||
# Every subcommand syncs first: artifacts always equal ENV truth before any
|
||||
# subcommand logic runs (migration, healing, retraction — all automatic).
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
create) shift; _alias_sync; _alias_create "${1:-}" ;;
|
||||
edit) shift; _alias_sync; _alias_edit "${1:-}" ;;
|
||||
remove) shift; _alias_sync; _alias_remove "${1:-}" ;;
|
||||
list) _alias_sync; _alias_list ;;
|
||||
show)
|
||||
[ -n "${2:-}" ] || err "Usage: pos ai alias show <name>"
|
||||
_alias_sync
|
||||
_alias_show "$2"
|
||||
;;
|
||||
"") _alias_sync; _alias_menu ;;
|
||||
*) err "Unknown subcommand '$1' (use -h for help)" ;;
|
||||
esac
|
||||
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: communication telegram-listener — Telegram bot listener: map /command → bash, run them on chat messages
|
||||
# POS: communication telegram-listener — Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages
|
||||
# POS_FLAGS: --enable --disable --status --sync-commands --run
|
||||
# POS_SUBCMDS: prefix
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
MAP_FILE="$CONFIG_DIR/telegram_commands.env"
|
||||
PREFIX_FILE="$CONFIG_DIR/telegram_prefixes.env"
|
||||
API="https://api.telegram.org"
|
||||
SERVICE="pos-telegram-listener.service"
|
||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
|
||||
# System prompt for the "ai " bridge: replies are posted straight into the
|
||||
# chat, so ask for concise, emoji-friendly Telegram-style answers.
|
||||
# System prompt for the "<prefix> " AI bridge (default prefix: "ai"): replies
|
||||
# are posted straight into the chat, so ask for concise, emoji-friendly
|
||||
# Telegram-style answers. The trigger word is configurable via
|
||||
# TELEGRAM_AI_PREFIX in telegram.env ('pos config telegram', default 'ai');
|
||||
# the text-prefix map checked before it can override any word.
|
||||
AI_SYSTEM="You are a friendly assistant chatting in a Telegram chat. Keep replies concise, use emojis and light formatting to make them lively, and never claim to send messages yourself."
|
||||
|
||||
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
@@ -32,10 +37,25 @@ Commands:
|
||||
--sync-commands
|
||||
Push the mapped /commands to the bot's "/" menu (setMyCommands)
|
||||
--run Run the polling loop in the foreground (used by the service)
|
||||
prefix [word [command...]]
|
||||
Manage the text-prefix map (telegram_prefixes.env): any
|
||||
non-command message '<prefix> <text>' runs the mapped command
|
||||
with <text> as ONE argument. Bare: list; <word>: show one;
|
||||
<word> <command...>: map (e.g. 'prefix opencode opencode' →
|
||||
"opencode check cpu" runs 'opencode "check cpu"');
|
||||
-r <word>: remove. Built-in Gemini bridge word (default 'ai')
|
||||
is set via 'pos config telegram' (TELEGRAM_AI_PREFIX).
|
||||
|
||||
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID — edit with 'pos config telegram')
|
||||
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_AI_PREFIX — edit with 'pos config telegram')
|
||||
Map: $MAP_FILE — '/cmd=bash command' per line (optional
|
||||
'/cmd::short description=bash command' shown in the bot menu)
|
||||
Prefix map: $PREFIX_FILE — '<word>=command' per line: a non-command
|
||||
message starting with '<word> <text>' runs the command with <text>
|
||||
appended as ONE quoted argument. First match wins (file order),
|
||||
case-insensitive; a mapped word shadows the built-in Gemini 'ai'
|
||||
bridge. Routing order: text-prefix map → AI bridge → /command map →
|
||||
'Unknown command'. Manage it with the 'prefix' verb.
|
||||
|
||||
Prefix a map value with '@quiet ' to run the command without replying —
|
||||
for commands that already send their own notification, so you don't get it
|
||||
@@ -241,6 +261,105 @@ strip_quiet() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── text-prefix map (PREFIX_FILE) ───────────────────────────────
|
||||
# Lines: <word>=command. Unlike the /command map (exact match on the whole
|
||||
# message), a matching <word> at the START of a non-command message routes
|
||||
# the REST of the message to the mapped command as ONE quoted argument:
|
||||
# 'opencode=opencode' + message "opencode check cpu" → 'opencode "check cpu"'
|
||||
# The map is re-read per message (edits apply without restarting), matching
|
||||
# is case-insensitive, the FIRST matching line wins (file order), a bare
|
||||
# <word> with no trailing space does NOT match, and a mapped word shadows the
|
||||
# built-in Gemini bridge in handle_message.
|
||||
|
||||
prefix_map_find() {
|
||||
[ -f "$PREFIX_FILE" ] || return 1
|
||||
local text="$1" line word cmd rem
|
||||
shopt -s nocasematch
|
||||
while IFS= read -r line; do
|
||||
case "$line" in \#*|'') continue ;; esac
|
||||
word="${line%%=*}"
|
||||
cmd="${line#*=}"
|
||||
word="${word# }"
|
||||
cmd="${cmd# }"
|
||||
[[ -n "$word" && -n "$cmd" ]] || continue
|
||||
if [[ "$text" =~ ^"$word"[[:space:]](.*)$ ]]; then
|
||||
rem="${BASH_REMATCH[1]}"
|
||||
[ -n "$rem" ] || continue
|
||||
shopt -u nocasematch
|
||||
printf '%s\x1f%s\n' "$cmd" "$rem"
|
||||
return 0
|
||||
fi
|
||||
done < "$PREFIX_FILE"
|
||||
shopt -u nocasematch
|
||||
return 1
|
||||
}
|
||||
|
||||
prefix_map_set() {
|
||||
local word="$1" cmd="$2"
|
||||
if ! [[ "$word" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
|
||||
err "invalid prefix '$word' — use one word of letters, digits, '-' or '_' (no spaces)"
|
||||
fi
|
||||
local errs
|
||||
errs="$(check_syntax "$cmd" 2>&1)" || err "invalid command for '$word': $errs"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
touch "$PREFIX_FILE"
|
||||
chmod 600 "$PREFIX_FILE"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v k="$word" 'index($0, k "=") != 1 { print }' "$PREFIX_FILE" > "$tmp"
|
||||
printf '%s=%s\n' "$word" "$cmd" >> "$tmp"
|
||||
mv "$tmp" "$PREFIX_FILE"
|
||||
chmod 600 "$PREFIX_FILE"
|
||||
}
|
||||
|
||||
prefix_map_del() {
|
||||
[ -f "$PREFIX_FILE" ] || return 1
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v k="$1" 'index($0, k "=") != 1 { print }' "$PREFIX_FILE" > "$tmp"
|
||||
if cmp -s "$tmp" "$PREFIX_FILE"; then
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
mv "$tmp" "$PREFIX_FILE"
|
||||
chmod 600 "$PREFIX_FILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
prefix_map_show() {
|
||||
local word="$1" line key
|
||||
[ -f "$PREFIX_FILE" ] || return 1
|
||||
while IFS= read -r line; do
|
||||
case "$line" in \#*|'') continue ;; esac
|
||||
key="${line%%=*}"
|
||||
if [ "$key" = "$word" ]; then
|
||||
printf '%s -> %s\n' "$key" "${line#*=}"
|
||||
return 0
|
||||
fi
|
||||
done < "$PREFIX_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run a mapped command line and reply with its output: empty output → "OK",
|
||||
# non-zero exit → "exit <rc>" + output; quiet=1 suppresses the reply (for
|
||||
# '@quiet ' entries that self-notify). Used by the /command map (60s cap)
|
||||
# and the text-prefix bridge (120s cap for app calls).
|
||||
run_and_reply() {
|
||||
local cmdline="$1" msg_id="$2" tmo="${3:-120}" quiet="${4:-0}" output rc
|
||||
if output="$(timeout "$tmo" bash -c "$cmdline" 2>&1)"; then
|
||||
rc=0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
[ "$quiet" -eq 1 ] && return
|
||||
[ -n "$output" ] || output="OK"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
reply "$(printf 'exit %s\n%s' "$rc" "$output")" "$msg_id" "$rc"
|
||||
else
|
||||
reply "$output" "$msg_id"
|
||||
fi
|
||||
}
|
||||
|
||||
ui_run_command() {
|
||||
local value="$1" output rc
|
||||
value="$(strip_quiet "$value")"
|
||||
@@ -417,12 +536,95 @@ status() {
|
||||
fi
|
||||
echo "config: $CONFIG_FILE"
|
||||
echo "map file: $MAP_FILE"
|
||||
echo "prefix map: $PREFIX_FILE"
|
||||
load_config
|
||||
echo "ai prefix: ${TELEGRAM_AI_PREFIX:-ai}"
|
||||
load_map
|
||||
echo "commands: $MAP_N mapped"
|
||||
local i
|
||||
for ((i=1; i<=MAP_N; i++)); do
|
||||
printf ' %-16s -> %s%s\n' "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$i]})}"
|
||||
done
|
||||
if [ -f "$PREFIX_FILE" ] && [ -s "$PREFIX_FILE" ]; then
|
||||
local pline pword pcmd pn=0
|
||||
echo "prefixes: ('<prefix> <text>' routes to the mapped app)"
|
||||
while IFS= read -r pline; do
|
||||
case "$pline" in \#*|'') continue ;; esac
|
||||
pword="${pline%%=*}" pcmd="${pline#*=}"
|
||||
pn=$((pn + 1))
|
||||
printf ' %-16s -> %s\n' "$pword" "$pcmd"
|
||||
done < "$PREFIX_FILE"
|
||||
[ "$pn" -gt 0 ] || echo " (none)"
|
||||
else
|
||||
echo "prefixes: (none)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── text-prefix map + built-in Gemini bridge word (prefix verb) ──
|
||||
# 'prefix' manages PREFIX_FILE (word → command). The built-in Gemini bridge
|
||||
# word (TELEGRAM_AI_PREFIX, default 'ai') is set via 'pos config telegram';
|
||||
# it is only reached when no text-prefix entry matches first.
|
||||
prefix_cmd() {
|
||||
local arg="${1:-}" rest="${*:2}"
|
||||
if [ -z "$arg" ]; then
|
||||
load_config
|
||||
echo "Text-prefix map ($PREFIX_FILE): messages like '<prefix> <text>' run the"
|
||||
echo "mapped command with <text> passed as ONE argument. First match wins"
|
||||
echo "(file order), case-insensitive; a mapped word shadows the built-in"
|
||||
echo "Gemini bridge. A bare <prefix> with no trailing space does not match."
|
||||
echo
|
||||
if [ -f "$PREFIX_FILE" ] && [ -s "$PREFIX_FILE" ]; then
|
||||
local line word cmd
|
||||
while IFS= read -r line; do
|
||||
case "$line" in \#*|'') continue ;; esac
|
||||
word="${line%%=*}" cmd="${line#*=}"
|
||||
printf ' %-16s -> %s\n' "$word" "$cmd"
|
||||
done < "$PREFIX_FILE"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo
|
||||
echo "Built-in Gemini bridge word: ${TELEGRAM_AI_PREFIX:-ai} (set via 'pos config telegram')"
|
||||
echo
|
||||
echo "Usage:"
|
||||
echo " prefix list this map + the Gemini bridge word"
|
||||
echo " prefix <word> show one mapping"
|
||||
echo " prefix <word> <command...> map <word> to a command"
|
||||
echo " prefix -r <word> remove a mapping"
|
||||
echo "Examples:"
|
||||
echo " pos communication telegram listener prefix opencode opencode"
|
||||
echo " # then sending 'opencode check cpu' runs: opencode \"check cpu\""
|
||||
return 0
|
||||
fi
|
||||
case "$arg" in
|
||||
-r|--remove)
|
||||
[ -n "$rest" ] || err "usage: prefix -r <word>"
|
||||
if prefix_map_del "$rest"; then
|
||||
log "removed prefix mapping '$rest'"
|
||||
else
|
||||
warn "no prefix mapping for '$rest'"
|
||||
fi
|
||||
return 0 ;;
|
||||
esac
|
||||
if [ -z "$rest" ]; then
|
||||
prefix_map_show "$arg" && return 0
|
||||
warn "no prefix mapping for '$arg'"
|
||||
echo "Map one with: prefix <word> <command...> (e.g. prefix opencode opencode)"
|
||||
echo "The built-in Gemini bridge word is set via 'pos config telegram' (TELEGRAM_AI_PREFIX)."
|
||||
return 1
|
||||
fi
|
||||
prefix_map_set "$arg" "$rest"
|
||||
log "prefix '$arg' -> '$rest' — sending '<$arg> <text>' runs: $rest \"<text>\" (takes effect immediately, no restart)"
|
||||
}
|
||||
|
||||
# Current AI-bridge trigger word. Like the command map, re-read per message so
|
||||
# 'prefix <word>' edits apply without restarting the daemon. Precedence:
|
||||
# telegram.env > env var from load_config (--run) > default 'ai'.
|
||||
ai_bridge_prefix() {
|
||||
local v=""
|
||||
[ -f "$CONFIG_FILE" ] && v="$(grep -E '^TELEGRAM_AI_PREFIX=' "$CONFIG_FILE" | tail -1 | sed 's/^[^=]*=//; s/^["'\'']//; s/["'\'']$//')" || true
|
||||
[ -n "$v" ] || v="${TELEGRAM_AI_PREFIX:-ai}"
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
# ── polling daemon ──────────────────────────────────────────────
|
||||
@@ -454,20 +656,45 @@ strip_markdown() {
|
||||
}
|
||||
|
||||
handle_message() {
|
||||
local text="$1" msg_id="$2" reply_text="${3:-}" value output rc quiet=0
|
||||
local text="$1" msg_id="$2" reply_text="${3:-}" value quiet=0
|
||||
case "$text" in
|
||||
/help|/start)
|
||||
reply "Mapped commands: $(map_cmds_list)" "$msg_id"
|
||||
return ;;
|
||||
esac
|
||||
# AI bridge: non-command text starting with "ai " (case-insensitive) is
|
||||
# forwarded to Gemini; the model's answer is replied verbatim. Each chat
|
||||
# gets its own persistent memory session ("telegram-<chat_id>"); the exact
|
||||
# prompt "ai /reset" clears it. Future non-command intents (e.g. reminders)
|
||||
# slot in as more case arms here.
|
||||
if [[ "$text" != /* && "$text" =~ ^[Aa][Ii][[:space:]](.*)$ ]]; then
|
||||
# Text-prefix bridge: '<prefix> <text>' runs the mapped command with
|
||||
# <text> passed as ONE quoted argument (telegram_prefixes.env), e.g.
|
||||
# 'opencode=opencode' → sending "opencode check cpu" runs
|
||||
# 'opencode "check cpu"'. Re-read per message, first match wins (file
|
||||
# order), case-insensitive; a mapped word shadows the built-in Gemini
|
||||
# bridge below. A bare <prefix> with no trailing space does not match.
|
||||
local pv cmd t qtext
|
||||
if pv="$(prefix_map_find "$text")"; then
|
||||
cmd="${pv%%$'\x1f'*}"
|
||||
t="${pv#*$'\x1f'}"
|
||||
if [ "${cmd#"$QUIET_PREFIX "}" != "$cmd" ]; then
|
||||
quiet=1
|
||||
cmd="${cmd#"$QUIET_PREFIX "}"
|
||||
fi
|
||||
qtext="$(printf '%q' "$t")"
|
||||
log "prefix: $text"
|
||||
run_and_reply "$cmd $qtext" "$msg_id" 120 "$quiet"
|
||||
return
|
||||
fi
|
||||
# AI bridge: non-command text starting with "<prefix> " (default "ai",
|
||||
# case-insensitive, configurable via TELEGRAM_AI_PREFIX in 'pos config
|
||||
# telegram') is forwarded to Gemini; the model's answer is replied
|
||||
# verbatim. Each chat gets its own persistent memory session
|
||||
# ("telegram-<chat_id>"); the exact prompt "<prefix> /reset" clears it.
|
||||
# Future non-command intents (e.g. reminders) slot in as more case arms
|
||||
# here.
|
||||
local prefix
|
||||
prefix="$(ai_bridge_prefix)"
|
||||
shopt -s nocasematch
|
||||
if [[ "$text" != /* && "$text" =~ ^"$prefix"[[:space:]](.*)$ ]]; then
|
||||
shopt -u nocasematch
|
||||
local prompt="${BASH_REMATCH[1]}" answer session
|
||||
[ -n "$prompt" ] || { reply "Usage: ai <prompt> — e.g. 'ai what is Nvidia'" "$msg_id"; return; }
|
||||
[ -n "$prompt" ] || { reply "Usage: $prefix <prompt> — e.g. '$prefix what is Nvidia'" "$msg_id"; return; }
|
||||
session="telegram-${TELEGRAM_CHAT_ID}"
|
||||
if [[ "$prompt" =~ ^/?reset[[:space:]]*$ ]]; then
|
||||
if pos ai gemini sessions reset "$session" >/dev/null 2>&1; then
|
||||
@@ -477,7 +704,7 @@ handle_message() {
|
||||
fi
|
||||
return
|
||||
fi
|
||||
log "ai: $prompt"
|
||||
log "$prefix: $prompt"
|
||||
if [ -n "$reply_text" ]; then
|
||||
prompt="[Reply context — the message you are replying to]\n${reply_text}\n\n${prompt}"
|
||||
fi
|
||||
@@ -489,6 +716,7 @@ handle_message() {
|
||||
fi
|
||||
return
|
||||
fi
|
||||
shopt -u nocasematch
|
||||
value="$(map_get "$text")"
|
||||
if [ -z "$value" ]; then
|
||||
reply "Unknown command: $text (send /help)" "$msg_id"
|
||||
@@ -499,20 +727,7 @@ handle_message() {
|
||||
value="${value#"$QUIET_PREFIX "}"
|
||||
fi
|
||||
log "exec: $text"
|
||||
if output="$(timeout 60 bash -c "$value" 2>&1)"; then
|
||||
rc=0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
[ "$quiet" -eq 1 ] && return
|
||||
if [ -z "$output" ]; then
|
||||
output="OK"
|
||||
fi
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
reply "$(printf 'exit %s\n%s' "$rc" "$output")" "$msg_id" "$rc"
|
||||
else
|
||||
reply "$output" "$msg_id"
|
||||
fi
|
||||
run_and_reply "$value" "$msg_id" 60 "$quiet"
|
||||
}
|
||||
|
||||
run_daemon() {
|
||||
@@ -561,6 +776,7 @@ case "${1:-}" in
|
||||
--status) status ;;
|
||||
--sync-commands) sync_bot_commands ;;
|
||||
--run) run_daemon ;;
|
||||
prefix) prefix_cmd "${@:2}" ;;
|
||||
"") ui ;;
|
||||
*) err "Unknown option '$1' (see --help)" ;;
|
||||
esac
|
||||
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
# POS: communication telegram-sender — Send Telegram messages/files/links/stickers via Bot API (send, test)
|
||||
# POS_FLAGS: --type --caption --parse-mode --no-preview --token --chat-id --markdown
|
||||
# POS_SUBCMDS: send test
|
||||
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot
|
||||
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot | TELEGRAM_AI_PREFIX=:AI-bridge trigger word in the telegram listener (default ai)::ai
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# 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"
|
||||
|
||||
usage() {
|
||||
|
||||
@@ -3,6 +3,9 @@ set -euo pipefail
|
||||
# POS: media sync — Incremental Music → USB sync (mp3/mp4, add/update only)
|
||||
# POS_FLAGS: --mp3 --mp4 --source --dry-run
|
||||
# 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/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
|
||||
@@ -3,6 +3,10 @@ set -euo pipefail
|
||||
# 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_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/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
|
||||
|
||||
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
# POS_FLAGS: --service --no-encrypt
|
||||
# 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_DEPS: tar
|
||||
|
||||
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"
|
||||
|
||||
Executable
+435
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: system uninstall — Remove pos toolkit binaries, services, shell integration, config, and data
|
||||
# POS_FLAGS: --yes --config --data
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos system uninstall [--yes] [--config] [--data]
|
||||
|
||||
Remove the pos toolkit from this machine. Scans for installed components
|
||||
and removes them interactively (or non-interactively with --yes).
|
||||
|
||||
Tiers:
|
||||
Tier 1 (always): binaries, entertainment plugins, systemd services, shell integration
|
||||
Tier 2 (--config): config files in ~/.config/linux_post_install/
|
||||
Tier 3 (--data): session/log/capture data in ~/.local/share/linux_post_install/
|
||||
|
||||
Flags:
|
||||
--yes Skip confirmation prompts (removes defaults only; combine with --config/--data for more)
|
||||
--config Include config files (Tier 2) in removal
|
||||
--data Include session/log data (Tier 3) in removal
|
||||
|
||||
Examples:
|
||||
pos system uninstall # interactive, tier 1 only
|
||||
pos system uninstall --yes # non-interactive, tier 1 only
|
||||
pos system uninstall --yes --config --data # nuclear option
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Parse flags ─────────────────────────────────────────────────
|
||||
YES_MODE=0
|
||||
DEL_CONFIG=0
|
||||
DEL_DATA=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--yes) YES_MODE=1; shift ;;
|
||||
--config) DEL_CONFIG=1; shift ;;
|
||||
--data) DEL_DATA=1; shift ;;
|
||||
-*) err "Unknown option '$1'" ;;
|
||||
*) err "Unexpected argument: $1 (no subcommands)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Scan functions ──────────────────────────────────────────────
|
||||
scan_tier1() {
|
||||
local found=()
|
||||
|
||||
# ── Binaries in /usr/local/bin ──
|
||||
# Main dispatcher
|
||||
[ -f /usr/local/bin/pos ] && found+=("/usr/local/bin/pos")
|
||||
|
||||
# All pos-* tools (sorted for deterministic display)
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true)
|
||||
|
||||
# Lib files shipped by install.sh
|
||||
for f in common.sh menu-lib.sh share-lib.sh; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# AI providers subdirectory
|
||||
[ -d /usr/local/bin/ai-providers ] && found+=("/usr/local/bin/ai-providers/")
|
||||
|
||||
# Entertainment plugins installed by install.sh
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(for ep in weather.sh gold.sh joke.sh; do
|
||||
[ -f "/usr/local/bin/$ep" ] && echo "/usr/local/bin/$ep"
|
||||
done | sort)
|
||||
|
||||
# Legacy forwarders
|
||||
for f in wr-* mp3 mp4 vbox ssh-load-all; do
|
||||
if compgen -G "/usr/local/bin/$f" >/dev/null 2>&1; then
|
||||
while IFS= read -r lf; do
|
||||
found+=("$lf")
|
||||
done < <(compgen -G "/usr/local/bin/$f" 2>/dev/null)
|
||||
fi
|
||||
done
|
||||
|
||||
# Prebuilt binaries (hotspot)
|
||||
for f in wihotspot wihotspot-gui create_ap; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# Feature scripts installed by install.sh
|
||||
for f in autostart.sh usb-automount.sh; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# User-local binaries
|
||||
[ -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
|
||||
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash")
|
||||
|
||||
# ~/.bash_completion entries
|
||||
if [ -f "$HOME/.bash_completion" ]; then
|
||||
while IFS= read -r line; do
|
||||
found+=("~/.bash_completion: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)")
|
||||
done < <(grep -n 'pos' "$HOME/.bash_completion" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── Systemd services ──
|
||||
for svc in autostart.service ssh-agent.service usb-automount.service; do
|
||||
if systemctl is-enabled "$svc" &>/dev/null 2>&1; then
|
||||
found+=("service: $svc")
|
||||
elif [ -f "/etc/systemd/system/$svc" ]; then
|
||||
found+=("service: $svc")
|
||||
fi
|
||||
done
|
||||
# Catch any other linux_post_install-related services
|
||||
while IFS= read -r line; do
|
||||
local svc_name
|
||||
svc_name=$(echo "$line" | awk '{print $1}')
|
||||
# Skip already-listed services
|
||||
local already=0
|
||||
for listed in autostart.service ssh-agent.service usb-automount.service; do
|
||||
[ "$svc_name" = "$listed" ] && already=1 && break
|
||||
done
|
||||
[ "$already" -eq 0 ] && found+=("service: $svc_name")
|
||||
done < <(systemctl list-unit-files --type=service 2>/dev/null | grep -i 'linux_post_install\|pos-' || true)
|
||||
|
||||
# ── Shell integration (~/.bashrc) ──
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
while IFS= read -r line; do
|
||||
found+=("~/.bashrc: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)")
|
||||
done < <(grep -n 'source.*pos-ai-hook\|linux_post_install.*PATH\|source.*pos\.bash\|pos completion' "$HOME/.bashrc" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
scan_tier2() {
|
||||
local found=()
|
||||
local cfg="$HOME/.config/linux_post_install"
|
||||
if [ -d "$cfg" ]; then
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null | sort)
|
||||
fi
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
scan_tier3() {
|
||||
local found=()
|
||||
local data="$HOME/.local/share/linux_post_install"
|
||||
if [ -d "$data" ]; then
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(find "$data" -maxdepth 2 \( -type f -o -type d \) 2>/dev/null | sort)
|
||||
fi
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
# ── Display function ────────────────────────────────────────────
|
||||
display_plan() {
|
||||
local -a t1=() t2=() t3=()
|
||||
# Collect non-empty entries from scan output
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t1+=("$line")
|
||||
done <<< "${1:-}"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t2+=("$line")
|
||||
done <<< "${2:-}"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t3+=("$line")
|
||||
done <<< "${3:-}"
|
||||
|
||||
echo
|
||||
echo "${BOLD}pos uninstall — what will be removed:${RESET}"
|
||||
echo
|
||||
|
||||
local n=1
|
||||
|
||||
if [ "${#t1[@]}" -gt 0 ]; then
|
||||
echo "${CYAN}Tier 1 (always):${RESET}"
|
||||
for item in "${t1[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${CYAN}Tier 1 (always):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ "${#t2[@]}" -gt 0 ]; then
|
||||
echo "${YELLOW}Tier 2 (--config to include):${RESET}"
|
||||
for item in "${t2[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${YELLOW}Tier 2 (--config to include):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ "${#t3[@]}" -gt 0 ]; then
|
||||
echo "${YELLOW}Tier 3 (--data to include):${RESET}"
|
||||
for item in "${t3[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${YELLOW}Tier 3 (--data to include):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# ── Removal functions ───────────────────────────────────────────
|
||||
remove_tier1() {
|
||||
local count=0
|
||||
|
||||
# ── Binaries ──
|
||||
# Main dispatcher
|
||||
[ -f /usr/local/bin/pos ] && { rm -f /usr/local/bin/pos && count=$((count+1)); }
|
||||
|
||||
# All pos-* tools
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true)
|
||||
|
||||
# Lib files
|
||||
for f in /usr/local/bin/common.sh /usr/local/bin/menu-lib.sh /usr/local/bin/share-lib.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# AI providers directory
|
||||
if [ -d /usr/local/bin/ai-providers ]; then
|
||||
rm -rf /usr/local/bin/ai-providers && count=$((count+1))
|
||||
fi
|
||||
|
||||
# Entertainment plugins
|
||||
for f in /usr/local/bin/weather.sh /usr/local/bin/gold.sh /usr/local/bin/joke.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# Legacy forwarders
|
||||
for pat in 'wr-*' mp3 mp4 vbox ssh-load-all; do
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(compgen -G "/usr/local/bin/$pat" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# Prebuilt binaries
|
||||
for f in /usr/local/bin/wihotspot /usr/local/bin/wihotspot-gui /usr/local/bin/create_ap; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# Feature scripts
|
||||
for f in /usr/local/bin/autostart.sh /usr/local/bin/usb-automount.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# User-local binaries
|
||||
[ -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
|
||||
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && { rm -f /usr/local/share/bash-completion/completions/pos.bash && count=$((count+1)); }
|
||||
|
||||
# ── Systemd services ──
|
||||
for svc in autostart.service ssh-agent.service usb-automount.service; do
|
||||
if systemctl is-enabled "$svc" &>/dev/null 2>&1; then
|
||||
systemctl disable --now "$svc" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/$svc"
|
||||
count=$((count+1))
|
||||
elif [ -f "/etc/systemd/system/$svc" ]; then
|
||||
rm -f "/etc/systemd/system/$svc"
|
||||
count=$((count+1))
|
||||
fi
|
||||
done
|
||||
# Additional linux_post_install services
|
||||
while IFS= read -r svc_file; do
|
||||
local svc_name
|
||||
svc_name=$(basename "$svc_file" .service)
|
||||
# Skip already-handled services
|
||||
local already=0
|
||||
for listed in autostart ssh-agent usb-automount; do
|
||||
[ "$svc_name" = "$listed" ] && already=1 && break
|
||||
done
|
||||
if [ "$already" -eq 0 ]; then
|
||||
systemctl disable --now "$svc_name.service" 2>/dev/null || true
|
||||
rm -f "$svc_file"
|
||||
count=$((count+1))
|
||||
fi
|
||||
done < <(find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*' 2>/dev/null || true)
|
||||
|
||||
# Reload daemon after service changes
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
|
||||
# ── Shell integration (~/.bashrc) ──
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
local before
|
||||
before=$(wc -l < "$HOME/.bashrc")
|
||||
sed -i '/source.*pos-ai-hook/d' "$HOME/.bashrc"
|
||||
sed -i '/linux_post_install.*PATH/d' "$HOME/.bashrc"
|
||||
sed -i '/source.*pos\.bash/d' "$HOME/.bashrc"
|
||||
local after
|
||||
after=$(wc -l < "$HOME/.bashrc")
|
||||
local removed=$((before - after))
|
||||
count=$((count + removed))
|
||||
fi
|
||||
|
||||
# ── Shell completion (~/.bash_completion) ──
|
||||
if [ -f "$HOME/.bash_completion" ]; then
|
||||
local before
|
||||
before=$(wc -l < "$HOME/.bash_completion")
|
||||
sed -i '/pos/d' "$HOME/.bash_completion"
|
||||
local after
|
||||
after=$(wc -l < "$HOME/.bash_completion")
|
||||
local removed=$((before - after))
|
||||
count=$((count + removed))
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 1)"
|
||||
}
|
||||
|
||||
remove_tier2() {
|
||||
local cfg="$HOME/.config/linux_post_install"
|
||||
local count=0
|
||||
|
||||
if [ -d "$cfg" ]; then
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
rm -f "$f" && count=$((count+1))
|
||||
done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null)
|
||||
|
||||
# Remove empty directory tree
|
||||
rmdir "$cfg/schedule.d" 2>/dev/null || true
|
||||
rmdir "$cfg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 2)"
|
||||
}
|
||||
|
||||
remove_tier3() {
|
||||
local data="$HOME/.local/share/linux_post_install"
|
||||
local count=0
|
||||
|
||||
if [ -d "$data" ]; then
|
||||
# Remove files first
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
rm -f "$f" && count=$((count+1))
|
||||
done < <(find "$data" -maxdepth 2 -type f 2>/dev/null)
|
||||
|
||||
# Remove directories bottom-up
|
||||
while IFS= read -r d; do
|
||||
rmdir "$d" 2>/dev/null && count=$((count+1)) || true
|
||||
done < <(find "$data" -mindepth 1 -depth -type d 2>/dev/null)
|
||||
|
||||
# Remove top-level directory if empty
|
||||
rmdir "$data" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 3)"
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────
|
||||
main() {
|
||||
section "pos system uninstall"
|
||||
|
||||
# Scan all tiers
|
||||
local scan1 scan2 scan3
|
||||
scan1="$(scan_tier1)"
|
||||
scan2="$(scan_tier2)"
|
||||
scan3="$(scan_tier3)"
|
||||
|
||||
# Display the plan
|
||||
display_plan "$scan1" "$scan2" "$scan3"
|
||||
|
||||
# Nothing to do at all?
|
||||
if [ -z "$scan1" ] && [ -z "$scan2" ] && [ -z "$scan3" ]; then
|
||||
ok "Nothing to remove — pos toolkit does not appear to be installed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Tier 1: always remove (confirm unless --yes) ──
|
||||
if [ -n "$scan1" ]; then
|
||||
if [ "$YES_MODE" -eq 1 ]; then
|
||||
log "Removing tier 1 items (--yes)..."
|
||||
else
|
||||
confirm "Remove tier 1 items (binaries, services, shell integration)?" || return 0
|
||||
fi
|
||||
remove_tier1
|
||||
fi
|
||||
|
||||
# ── Tier 2: config files ──
|
||||
if [ -n "$scan2" ]; then
|
||||
if [ "$DEL_CONFIG" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then
|
||||
confirm "Also remove config files (tier 2)?" && DEL_CONFIG=1 || true
|
||||
fi
|
||||
[ "$DEL_CONFIG" -eq 1 ] && remove_tier2
|
||||
fi
|
||||
|
||||
# ── Tier 3: session/log data ──
|
||||
if [ -n "$scan3" ]; then
|
||||
if [ "$DEL_DATA" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then
|
||||
confirm "Also remove session/log data (tier 3)?" && DEL_DATA=1 || true
|
||||
fi
|
||||
[ "$DEL_DATA" -eq 1 ] && remove_tier3
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Uninstall complete."
|
||||
echo " The git repo was NOT removed — delete it manually if desired."
|
||||
echo " Restart your shell or run: source ~/.bashrc"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+12
-6
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
# POS_FLAGS: --depth
|
||||
|
||||
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() {
|
||||
cat <<EOF
|
||||
@@ -44,12 +45,17 @@ add() {
|
||||
meta["$path"]="$type|$desc"
|
||||
}
|
||||
|
||||
for f in "$self"/pos-*; do
|
||||
[ -x "$f" ] || continue
|
||||
name="${f##*/pos-}"
|
||||
desc="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f")"
|
||||
desc="${desc#*— }"
|
||||
sc="$(sed -n '/^# POS_SUBCMDS: /{s/^# POS_SUBCMDS: //;p;q}' "$f")"
|
||||
reg_scan "$self"
|
||||
|
||||
for tool_key in $(reg_list); do
|
||||
cat="$(reg_lookup "$tool_key" cat)"
|
||||
desc="$(reg_lookup "$tool_key" desc)"
|
||||
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//-/ })
|
||||
parent="/"
|
||||
for ((i=0; i<${#words[@]}; i++)); do
|
||||
|
||||
@@ -19,15 +19,18 @@ _pos_flags[network-hotspot]="--foreground"
|
||||
_pos_flags[share-usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version menu"
|
||||
_pos_flags[system-backup]="--service --no-encrypt"
|
||||
_pos_flags[system-schedule]="--dry-run"
|
||||
_pos_flags[ai]="--provider --model --session --system --full --last"
|
||||
_pos_flags[system-uninstall]="--yes --config --data"
|
||||
_pos_flags[ai]="--provider --model --session --system --full --last --trust"
|
||||
_pos_flags[tree]="--depth"
|
||||
# GEN:END posflags
|
||||
# GEN:START possubcmds
|
||||
declare -A _pos_subcmds
|
||||
_pos_subcmds[ai-alias]="create edit remove list show"
|
||||
_pos_subcmds[ai-gemini]="ask chat models sessions capture"
|
||||
_pos_subcmds[ai-openrouter]="ask chat sessions capture"
|
||||
_pos_subcmds[communication-matrix-sender]="send test login"
|
||||
_pos_subcmds[communication-scrcpy]="devices record tcpip connect push pull screenshot info"
|
||||
_pos_subcmds[communication-telegram-listener]="prefix"
|
||||
_pos_subcmds[communication-telegram-sender]="send test"
|
||||
_pos_subcmds[docker-compose]="ls installed up down restart logs update config menu"
|
||||
_pos_subcmds[docker-vbox]="create enter stop start rm ls menu"
|
||||
@@ -40,7 +43,7 @@ _pos_subcmds[share-smb-client]="mount unmount list persist unpersist menu"
|
||||
_pos_subcmds[share-smb-server]="status share unshare list adduser deluser reload enable disable menu"
|
||||
_pos_subcmds[system-backup]="menu"
|
||||
_pos_subcmds[system-schedule]="run list config enable disable status migrate menu"
|
||||
_pos_subcmds[ai]="ask chat sessions capture models providers gemini openrouter"
|
||||
_pos_subcmds[ai]="ask chat sessions capture models providers alias gemini openrouter"
|
||||
# GEN:END possubcmds
|
||||
# GEN:START posconfigscopes
|
||||
declare -a _pos_config_scopes=(ai compose entertainment matrix notify scrcpy system telegram ytsync)
|
||||
|
||||
+12
-9
@@ -2,14 +2,17 @@
|
||||
# Copied to ~/.config/linux_post_install/ai.env on install (no clobber).
|
||||
# Edit with: pos config ai
|
||||
#
|
||||
# Syntax:
|
||||
# Provider selection:
|
||||
# AI_PROVIDER=gemini # provider: gemini|openrouter (default gemini)
|
||||
# AI_API_KEY=<key> # API key for the active provider (secret)
|
||||
# AI_MODEL=<model> # model id (default depends on provider)
|
||||
# AI_SYSTEM_PROMPT=<prompt> # custom system prompt (overrides built-in; empty to reset)
|
||||
#
|
||||
# Legacy provider-specific keys (still supported as fallback):
|
||||
# AI_GEMINI_API_KEY=<key> # Gemini API key (used when AI_API_KEY is empty)
|
||||
# AI_GEMINI_MODEL=<model> # Gemini model id (used when AI_MODEL is empty)
|
||||
# OPENROUTER_API_KEY=<key> # OpenRouter API key (used when AI_API_KEY is empty)
|
||||
# OPENROUTER_MODEL=<model> # OpenRouter model id (used when AI_MODEL is empty)
|
||||
# API keys (each provider needs its own):
|
||||
# AI_GEMINI_API_KEY=<key> # Gemini API key from aistudio.google.com
|
||||
# OPENROUTER_API_KEY=<key> # OpenRouter API key from openrouter.ai
|
||||
#
|
||||
# Model overrides (optional, defaults per provider):
|
||||
# AI_GEMINI_MODEL=<model> # Gemini model id (default: gemini-2.5-flash)
|
||||
# OPENROUTER_MODEL=<model> # OpenRouter model id (default: openrouter/auto)
|
||||
# AI_MODEL=<model> # Override for all providers (takes priority)
|
||||
#
|
||||
# System prompt:
|
||||
# AI_SYSTEM_PROMPT=<prompt> # Custom system prompt (overrides built-in; empty to reset)
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ if should_run 2 scripts; then
|
||||
done
|
||||
lib_count=0
|
||||
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"
|
||||
lib_count=$((lib_count + 1))
|
||||
lib_names+="$lf "
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
# Provider-specific: API call, auth, response parsing, models list
|
||||
# Part of the R8 provider-agnostic architecture (lib/ai-providers/).
|
||||
|
||||
# Provider-specific config variables (auto-discovered by pos config ai):
|
||||
# PROVIDER_CONFIG: AI_GEMINI_API_KEY=secret:Gemini API key from aistudio.google.com
|
||||
# PROVIDER_CONFIG: AI_GEMINI_MODEL=:Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
provider_name() { printf 'Google Gemini'; }
|
||||
provider_default_model() { printf 'gemini-2.5-flash'; }
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
# Provider-specific: API call, auth, response parsing, models list
|
||||
# Part of the R8 provider-agnostic architecture (lib/ai-providers/).
|
||||
|
||||
# Provider-specific config variables (auto-discovered by pos config ai):
|
||||
# PROVIDER_CONFIG: OPENROUTER_API_KEY=secret:OpenRouter API key from openrouter.ai
|
||||
# PROVIDER_CONFIG: OPENROUTER_MODEL=:OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
provider_name() { printf 'OpenRouter'; }
|
||||
provider_default_model() { printf 'openrouter/auto'; }
|
||||
|
||||
|
||||
+274
-34
@@ -5,12 +5,22 @@
|
||||
# (mirrors lib/notify.sh). Sourced opt-in by bin/pos-config.
|
||||
#
|
||||
# Header grammar — one "# POS_CONFIG:" line per scope a tool exposes:
|
||||
# # POS_CONFIG: <scope> | <env-file> | <KEY>=<flags>:<desc>[::<example>] | ... | *plugins
|
||||
# # POS_CONFIG: <scope> | <env-file> | <field> | ... | *plugins
|
||||
# <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
|
||||
# <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
|
||||
# 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:
|
||||
# # 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 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)
|
||||
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 ─────────────────────────────────────────────────
|
||||
# Repo layout: lib/config-ui.sh → tools live in ../bin.
|
||||
@@ -86,6 +106,29 @@ cfg_scope_envfile() {
|
||||
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).
|
||||
# The optional example is "desc::example" — a literal "::" separates the
|
||||
# value-format hint from the description.
|
||||
@@ -136,6 +179,85 @@ _cfg_plugin_keys() {
|
||||
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
|
||||
# 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() {
|
||||
local want_tag="${1:-}"
|
||||
local pdir line key desc flags
|
||||
# Repo layout: lib/config-ui.sh → ../lib/ai-providers/
|
||||
# Installed layout: /usr/local/bin/config-ui.sh → ./ai-providers/
|
||||
pdir=""
|
||||
local candidate
|
||||
for candidate in \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/../lib/ai-providers" \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/ai-providers"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
pdir="$(cd "$candidate" 2>/dev/null && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -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
|
||||
[ -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:' "$pdir"/*.sh 2>/dev/null | sed 's/^.*# PROVIDER_CONFIG:[[:space:]]*//' || true)
|
||||
return 0
|
||||
}
|
||||
|
||||
# Declared keys for a scope: "KEY|flags|description" lines, deduped.
|
||||
cfg_scope_keys() {
|
||||
local scope="$1" dir line s keystring field
|
||||
@@ -148,14 +270,33 @@ cfg_scope_keys() {
|
||||
[ "$s" = "$scope" ] || continue
|
||||
keystring="${line#*|}"
|
||||
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
|
||||
field="${field#"${field%%[![:space:]]*}"}"
|
||||
field="${field%"${field##*[![:space:]]}"}"
|
||||
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
|
||||
*plugins*) _cfg_plugin_keys ;;
|
||||
*plugins*) _cfg_plugin_keys ;;
|
||||
*providers*)
|
||||
local ptag=""
|
||||
[[ "$field" == *"="* ]] && ptag="${field#*=}"
|
||||
_cfg_provider_keys "$ptag" ;;
|
||||
esac
|
||||
else
|
||||
_cfg_key_line "$field"
|
||||
@@ -294,53 +435,152 @@ _cfg_edit_one() {
|
||||
_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.
|
||||
#
|
||||
# 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() {
|
||||
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; }
|
||||
file="$CONFIG_DIR/$envfile"
|
||||
_cfg_scope="$scope"
|
||||
|
||||
local -a keys=()
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
keys+=("$line")
|
||||
fi
|
||||
done < <(cfg_scope_keys "$scope")
|
||||
if [ ${#keys[@]} -eq 0 ]; then
|
||||
# Collect records: KEY|flags|desc|example for keys, >|cond|caption| for captions
|
||||
local -a recs=() nums=()
|
||||
mapfile -t recs < <(cfg_scope_keys "$scope")
|
||||
if [ ${#recs[@]} -eq 0 ]; then
|
||||
warn "no config keys declared for scope '$scope'"
|
||||
return 1
|
||||
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 60–120 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
|
||||
echo
|
||||
echo "pos config — ${scope} (${envfile})"
|
||||
echo "------------------------------------"
|
||||
i=0
|
||||
for line in "${keys[@]}"; do
|
||||
i=$((i + 1))
|
||||
IFS='|' read -r k f d e <<<"$line"
|
||||
v="$(cfg_value "$file" "$k")"
|
||||
printf ' %2d) %-28s %s\n' "$i" "$k" "$(cfg_display "$v" "$f")"
|
||||
if [ -n "$d" ]; then
|
||||
printf ' %s\n' "$d"
|
||||
fi
|
||||
if [ -n "$e" ]; then
|
||||
printf ' e.g. %s\n' "$e"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
read -rp "Variable number [q to quit]: " choice || { echo; return 0; }
|
||||
{
|
||||
echo
|
||||
echo "${BOLD}pos config — ${scope} (${envfile})${RESET}"
|
||||
echo "${CYAN}${rule}${RESET}"
|
||||
n=0; dim=0; pend_cap=""; pend_cond=""
|
||||
for idx in "${!recs[@]}"; do
|
||||
# Caption records use \x1f (unit separator) to avoid collision
|
||||
# with | in alternation syntax; key records use | as before.
|
||||
if [[ "${recs[$idx]}" == ">"* ]]; then
|
||||
# Caption record: >\x1fcond\x1fcaption\x1f
|
||||
# Strip leading > and first \x1f, then split on next \x1f
|
||||
pend_cond="${recs[$idx]#>}"
|
||||
pend_cond="${pend_cond#$_CS}"
|
||||
pend_cond="${pend_cond%%$_CS*}"
|
||||
pend_cap="${recs[$idx]#>}"
|
||||
pend_cap="${pend_cap#$_CS}"
|
||||
pend_cap="${pend_cap#*$_CS}"
|
||||
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
|
||||
q|Q|quit|exit) echo; return 0 ;;
|
||||
r|R|refresh) continue ;;
|
||||
"") continue ;;
|
||||
*)
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#keys[@]} )); then
|
||||
_cfg_edit_one "$file" "${keys[$((choice - 1))]}"
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#nums[@]} )); then
|
||||
_cfg_edit_one "$file" "${recs[${nums[$((choice - 1))]}]}"
|
||||
else
|
||||
warn "invalid number '$choice' (1-${#keys[@]})"
|
||||
warn "invalid number '$choice' (1-${#nums[@]})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
+198
-5
@@ -25,6 +25,8 @@
|
||||
# menu_run <title> <item...> numbered menu loop → chosen index
|
||||
# menu_pick <prompt> <item...> type-to-filter picker → chosen index
|
||||
# menu_ask_value <label> [default] prompted value → entered text
|
||||
# menu_read_value <label> raw-mode bracketed-paste reader
|
||||
# menu_redraw internal redraw (menu_read_value only)
|
||||
|
||||
# ── Colors (guarded fallbacks; a sourced common.sh wins) ──────
|
||||
CYAN="${CYAN:-}"
|
||||
@@ -150,15 +152,206 @@ menu_pick() {
|
||||
done
|
||||
}
|
||||
|
||||
# ── Raw-mode value reader (bracketed-paste safe) ───────────────
|
||||
# Reads ONE value from the terminal in raw mode with bracketed paste enabled,
|
||||
# so a multi-line CTRL+V paste is inserted LITERALLY — embedded newlines are
|
||||
# data, never line terminators — and can never leak into the shell or a later
|
||||
# prompt as leftover keystrokes. A plain bash `read` is line-oriented: it
|
||||
# consumes only the first pasted line and the remaining lines sit in the tty
|
||||
# queue, where the next prompt (or the shell after this script exits) treats
|
||||
# them as input/commands. That is the paste bug this reader exists to prevent.
|
||||
#
|
||||
# Editing (single-line typing behaves like a normal prompt):
|
||||
# Enter submit the value (outside a paste)
|
||||
# Backspace/DEL delete the character before the cursor
|
||||
# Left/Right move the cursor; Home/End jump to start/end
|
||||
# Delete delete the character at the cursor
|
||||
# Ctrl-U clear the whole value
|
||||
# Ctrl-D (empty) EOF — cancel · Ctrl-C/Z/\ — cancel · Up/Down — ignored
|
||||
# Inside a bracketed paste the above are inert: text (incl. newlines) is
|
||||
# inserted verbatim until the paste-end marker; a real Enter then submits.
|
||||
#
|
||||
# Display goes to stderr so callers may command-substitute the result:
|
||||
# rc 0 value on stdout · rc 1 cancel/EOF/non-tty.
|
||||
menu_read_value() {
|
||||
local label="$1"
|
||||
local val="" state="" chunk="" ch="" esc="" seq="" esc_c=""
|
||||
local paste=0 pos=0 submit=0 i=0 n=0
|
||||
|
||||
if ! state="$(stty -g 2>/dev/null)"; then
|
||||
# not a terminal — plain stdin read; no paste protection is possible
|
||||
IFS= read -r val || return 1
|
||||
[ -n "$val" ] && printf '%s' "$val"
|
||||
return 0
|
||||
fi
|
||||
if ! stty -icanon -echo -isig min 1 time 0 2>/dev/null; then
|
||||
stty "$state" 2>/dev/null
|
||||
IFS= read -r val || return 1
|
||||
[ -n "$val" ] && printf '%s' "$val"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local restore
|
||||
restore() {
|
||||
stty "$state" 2>/dev/null
|
||||
printf '\033[?2004l' >&2
|
||||
}
|
||||
trap 'restore; trap - INT TERM; return 1' INT TERM
|
||||
|
||||
printf '\033[?2004h' >&2
|
||||
printf '%s: ' "$label" >&2
|
||||
|
||||
# Next input byte as a 2-hex-digit string, returned via nameref. Uses
|
||||
# dd|od, NOT bash's read builtin: read's tty path self-interrupts on an ETX
|
||||
# byte even with ISIG disabled (SIGINTs the whole script on Ctrl-C, killing
|
||||
# a cmdsubst caller). One dd per input burst (VMIN=1 returns all queued
|
||||
# bytes), so pastes cost O(chunks), not O(per-byte forks). Runs in-place
|
||||
# (never in a $( ) subshell) so its chunk/offset state persists.
|
||||
# byte <hexvar> — rc 0 = byte in hexvar, rc 1 = EOF/short.
|
||||
local byte
|
||||
byte() {
|
||||
local -n _hex="$1"
|
||||
if [ "$i" -ge "$n" ]; then
|
||||
chunk="$(dd bs=4096 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
||||
[ -n "$chunk" ] || return 1
|
||||
n=${#chunk}
|
||||
i=0
|
||||
fi
|
||||
_hex="${chunk:i:2}"
|
||||
i=$((i + 2))
|
||||
return 0
|
||||
}
|
||||
|
||||
while byte ch; do
|
||||
case "$ch" in
|
||||
1b)
|
||||
seq=""
|
||||
while byte esc; do
|
||||
printf -v esc_c '%b' "\\x$esc"
|
||||
seq+="$esc_c"
|
||||
case "$esc_c" in
|
||||
[A-Za-z~]) break ;;
|
||||
esac
|
||||
done
|
||||
case "$seq" in
|
||||
'[200~') paste=1 ;;
|
||||
'[201~') paste=0 ;;
|
||||
'[C') [ "$pos" -lt "${#val}" ] && { pos=$((pos + 1)); menu_redraw "$label" "$val" "$pos"; } ;;
|
||||
'[D') [ "$pos" -gt 0 ] && { pos=$((pos - 1)); menu_redraw "$label" "$val" "$pos"; } ;;
|
||||
'[H' | '[1~') pos=0; menu_redraw "$label" "$val" "$pos" ;;
|
||||
'[F' | '[4~') pos=${#val}; menu_redraw "$label" "$val" "$pos" ;;
|
||||
'[3~')
|
||||
if [ "$pos" -lt "${#val}" ]; then
|
||||
val="${val:0:pos}${val:pos+1}"
|
||||
menu_redraw "$label" "$val" "$pos"
|
||||
fi
|
||||
;;
|
||||
'[A' | '[B') : ;; # up/down: no history — ignore
|
||||
esac
|
||||
;;
|
||||
0a | 0d)
|
||||
if [ "$paste" -eq 1 ]; then
|
||||
# newline inside a paste is literal data (paste as text);
|
||||
# echo the line break so CRLF pastes render at col 0
|
||||
printf -v ch '%b' "\\x$ch"
|
||||
val="${val:0:pos}${ch}${val:pos}"
|
||||
pos=$((pos + 1))
|
||||
printf '%s' "$ch" >&2
|
||||
else
|
||||
submit=1
|
||||
break
|
||||
fi
|
||||
;;
|
||||
7f | 08) # Backspace/DEL
|
||||
if [ "$pos" -gt 0 ]; then
|
||||
val="${val:0:pos-1}${val:pos}"
|
||||
pos=$((pos - 1))
|
||||
menu_redraw "$label" "$val" "$pos"
|
||||
fi
|
||||
;;
|
||||
03 | 1a | 1c) # Ctrl-C / Ctrl-Z / Ctrl-\ — cancel
|
||||
submit=0
|
||||
break
|
||||
;;
|
||||
04) # Ctrl-D: EOF on empty → cancel
|
||||
if [ -z "$val" ]; then
|
||||
submit=0
|
||||
break
|
||||
fi
|
||||
;;
|
||||
15) # Ctrl-U: clear
|
||||
val=""; pos=0
|
||||
menu_redraw "$label" "$val" "$pos"
|
||||
;;
|
||||
*)
|
||||
printf -v ch '%b' "\\x$ch"
|
||||
val="${val:0:pos}${ch}${val:pos}"
|
||||
pos=$((pos + 1))
|
||||
if [ "$pos" -eq "${#val}" ]; then
|
||||
printf '%s' "$ch" >&2 # append in place — fast path
|
||||
else
|
||||
menu_redraw "$label" "$val" "$pos"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
trap - INT TERM
|
||||
restore
|
||||
printf '\n' >&2
|
||||
if [ "$submit" -eq 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$val"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Internal: redraw the whole input block (menu_read_value only) ──
|
||||
# The value may span several terminal rows (multiline paste); redraw clears
|
||||
# below the block start and reprints label + value, then repositions the
|
||||
# cursor to (row, col) of $3. Columns are counted in characters — wide CJK
|
||||
# glyphs can be off by one column (display-only; the stored value is exact).
|
||||
menu_redraw() {
|
||||
local label="$1" val="$2" pos="$3"
|
||||
local nl="" r="" c="" last="" ec="" d="" up=""
|
||||
nl="${val//[^$'\n']/}"
|
||||
[ "${#nl}" -gt 0 ] && printf '\033[%dA' "${#nl}" >&2
|
||||
printf '\r\033[J' >&2
|
||||
printf '%s: ' "$label" >&2
|
||||
printf '%s' "$val" >&2
|
||||
# target row/col of the cursor
|
||||
last="${val:0:pos}"
|
||||
r="${last//[^$'\n']/}"; r="${#r}"
|
||||
last="${last##*$'\n'}"
|
||||
c="${#last}"
|
||||
# current cursor (end of block): end row = nl count; end col = after last
|
||||
# newline (or 0 when the value ends with a newline)
|
||||
ec=0; last="${val##*$'\n'}"
|
||||
case "$val" in
|
||||
*$'\n') ec=0 ;;
|
||||
*) ec="${#last}" ;;
|
||||
esac
|
||||
[ "${#nl}" -gt "$r" ] && printf '\033[%dA' $(( ${#nl} - r )) >&2
|
||||
d=$(( c - ec ))
|
||||
if [ "$d" -gt 0 ]; then
|
||||
printf '\033[%dC' "$d" >&2
|
||||
elif [ "$d" -lt 0 ]; then
|
||||
printf '\033[%dD' $(( -d )) >&2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Prompted value with optional default ───────────────────────
|
||||
# Prints "<label> [<default>]: " (read -p sends prompts to stderr) and echoes
|
||||
# the entered value or the default when the answer is empty.
|
||||
# rc 0 value on stdout · rc 1 EOF, or empty answer with no default.
|
||||
# Prints "<label> [<default>]: " and echoes the entered value or the default
|
||||
# when the answer is empty. Uses the bracketed-paste-safe reader, so pasting
|
||||
# text — including multi-line pastes — inserts it literally instead of letting
|
||||
# leftover lines escape to the shell as commands.
|
||||
# rc 0 value on stdout · rc 1 EOF/cancel, or empty answer with no default.
|
||||
menu_ask_value() {
|
||||
local label="$1" def="${2:-}" val pr="$1"
|
||||
[ -n "$def" ] && pr="$pr [$def]"
|
||||
if ! read -rp "${pr}: " val; then
|
||||
return 1 # EOF — cancel
|
||||
if ! val="$(menu_read_value "$pr")"; then
|
||||
return 1 # EOF / cancel
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
[ -n "$def" ] || return 1
|
||||
|
||||
+199
@@ -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}" ]
|
||||
}
|
||||
@@ -39,6 +39,7 @@ PACKAGES=(
|
||||
python3 python3-pip rclone
|
||||
ffmpeg
|
||||
libqrencode4 libgtk-3-0 adb
|
||||
xdotool xclip
|
||||
)
|
||||
|
||||
spawn "apt update" sudo apt update
|
||||
|
||||
+42
-16
@@ -10,6 +10,8 @@ set -euo pipefail
|
||||
# - "# POS:" header line → one-line description
|
||||
# - "# POS_FLAGS:" line → flag completion list (flag-style tools only)
|
||||
# - "# POS_SUBCMDS:" line → subcommand completion list (multi-command tools)
|
||||
# - "# POS_DEPS:" line → runtime binary dependencies (optional)
|
||||
# - "# POS_EXAMPLES:" line → curated usage examples (optional, multi-line)
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
mode="write"
|
||||
@@ -22,7 +24,7 @@ export LC_ALL=C
|
||||
ctx="$root/DOC/AGENT_Context_Project.md"
|
||||
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.
|
||||
# tooldisp <cat> <sub> → display name (pos-config / pos-communication-telegram-sender).
|
||||
tooldisp() { printf 'pos-%s%s' "${1:+$1-}" "$2"; }
|
||||
@@ -42,31 +44,55 @@ for f in "$root"/bin/pos-*; do
|
||||
desc="${desc#*— }"
|
||||
flags="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;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
|
||||
mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort)
|
||||
|
||||
# ── 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() {
|
||||
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
|
||||
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} -gt "$width" ] && width=${#name}
|
||||
done
|
||||
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")"
|
||||
printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc"
|
||||
if [ -n "$deps" ]; then
|
||||
printf '│ %*s│ [deps: %s]\n' "" "" "$deps"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
gen_dispatch() {
|
||||
local cat sub desc flags t
|
||||
for t in "${tools[@]}"; do
|
||||
IFS='|' read -r cat sub desc flags subcmds <<<"$t"
|
||||
printf '| %s | %s | `%s` | %s |\n' "$cat" "$sub" "$(tooldisp "$cat" "$sub")" "$desc"
|
||||
done
|
||||
local cat sub desc flags t deps examples
|
||||
if [ "$_has_deps_examples" -eq 1 ]; then
|
||||
printf '| Category | Command | Script | Description | Deps | Examples |\n'
|
||||
printf '|----------|---------|--------|-------------|------|----------|\n'
|
||||
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() {
|
||||
@@ -83,10 +109,10 @@ gen_selfcontained() {
|
||||
}
|
||||
|
||||
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")"
|
||||
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")"
|
||||
printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc"
|
||||
done
|
||||
@@ -94,10 +120,10 @@ gen_filetable() {
|
||||
}
|
||||
|
||||
gen_posflags() {
|
||||
local cat sub desc flags t
|
||||
local cat sub desc flags t deps examples
|
||||
echo "declare -A _pos_flags"
|
||||
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
|
||||
printf '_pos_flags[%s]="%s"\n' "$(tooldisp "$cat" "$sub" | sed 's/^pos-//')" "$flags"
|
||||
done
|
||||
@@ -106,10 +132,10 @@ gen_posflags() {
|
||||
gen_possubcmds() {
|
||||
# Subcommand completion: "# POS_SUBCMDS:" list + nested sub-tools from
|
||||
# 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"
|
||||
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:-}"
|
||||
for f in "$root"/bin/"$(tooldisp "$cat" "$sub")"-*; do
|
||||
[ -x "$f" ] || continue
|
||||
|
||||
@@ -9,6 +9,9 @@ set -euo pipefail
|
||||
# lines (single source of truth for generated docs):
|
||||
# # POS: <category> <command> — one-line description
|
||||
# # 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>
|
||||
# 4. If it reads stdin (password/selection prompts), add it to
|
||||
# INTERACTIVE_CMDS in bin/pos or its prompt breaks under the log tee.
|
||||
|
||||
Reference in New Issue
Block a user