Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6976186d4 | |||
| 59a4c0e1df | |||
| fcfa2a569a | |||
| 287f0b75b7 |
+4
-2
@@ -42,9 +42,11 @@ summary (newest last).
|
||||
|
||||
## Done
|
||||
|
||||
- **`pos bank` command bank** — feature (41efc91) + lib registration fix (73d15a2) + main-menu visibility & `run` crash fix (c5d501a) + tab-completion of category-less tools (e23d57e) + multiline command storage/execution fix (11b4a67, v2 escaped format with backward compat). Done 2026-09-12.
|
||||
- **`pos system bank alias`** — bash aliases for bank commands (Architect→Builder): `alias <name> [alias_name]`, `alias list`, `alias remove <alias_name>`, menu option 6 "Manage aliases", subcommand shown in `pos tree`/completion via `# POS_SUBCMDS`. Writes `alias <alias_name>='pos system bank run <name>'` lines into `~/.bashrc` inside a managed `# >>> pos bank aliases … <<<` block (`BASH_RC_FILE` seam); same alias name retargets the line, removing the last alias removes the whole block; outer-alias collision refused (line shown), PATH-shadow non-blocking warn, `bank remove` drops aliases pointing at the removed command. Fixed during build: undeclared `declare -g -A` alias map → `unbound variable`; `[[ =~ ]]` literalizes `'` → payload matched via glob `case`; **GNU sed `1,0p` prints line 1** (0 = last-line sentinel) → guarded extraction ranges so edge blocks rewrite cleanly. New `tests/t-bank.sh` Part B2 (49 alias checks). Docs: POS.md system category + detail block, howto/system.md new bank section. Verified: `bash -n`, t-bank 120 checks, `make gen` byte-idempotent, `make check`, `make lint` 0 FAIL / 0 WARN. Done 2026-09-12.
|
||||
|
||||
- **2026-09-09** — New `pos system alias` tool (Architect→Builder→Reviewer→Writer): persistent command aliases via wrapper scripts in `~/.local/bin/`. Interactive menu (create/edit/remove/list/show), storage at `~/.config/linux_post_install/aliases.env` (pipe-delimited `name|command`), wrapper sync on every invocation, name validation (`^[a-zA-Z][a-zA-Z0-9_-]*$`), ownership markers, collision checks. Docs: POS.md system category + detail block, howto/system.md recipes section. Verified: `bash -n`, `make gen` byte-idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN.
|
||||
- **`pos bank` command bank** — feature (41efc91) + lib registration fix (73d15a2) + main-menu visibility & `run` crash fix (c5d501a) + tab-completion of category-less tools (e23d57e) + multiline command storage/execution fix (11b4a67, v2 escaped format with backward compat). Done 2026-09-12. **2026-09-12: migrated into the `system` category → `pos system bank` (`bin/pos-system-bank`); storage seam (`bank.env`, `BANK_FILE`, v2 format) unchanged.**
|
||||
|
||||
- **2026-09-09** — New `pos system alias` tool (Architect→Builder→Reviewer→Writer): persistent command aliases via wrapper scripts in `~/.local/bin/`. Interactive menu (create/edit/remove/list/show), storage at `~/.config/linux_post_install/aliases.env` (pipe-delimited `name|command`), wrapper sync on every invocation, name validation (`^[a-zA-Z][a-zA-Z0-9_-]*$`), ownership markers, collision checks. Docs: POS.md system category + detail block, howto/system.md recipes section. Verified: `bash -n`, `make gen` byte-idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN. **2026-09-12: removed per user request (feature no longer wanted); `bin/pos-system-alias` deleted, docs cleaned up.**
|
||||
|
||||
- **2026-09-09** — Telegram listener single-instance guard (Toolsmith): `bin/pos-communication-telegram-listener --run` now takes a `flock(1)` on `${XDG_RUNTIME_DIR:-/tmp}/pos-telegram-listener.lock` inside `run_daemon()` (before config load/sync/poll loop) — a second `--run` on the same token fails fast (exit 1, `ERROR: listener already running (single instance) — check: systemctl --user status pos-telegram-listener`), never racing getUpdates (Telegram 409/command stealing). Kernel auto-release → no stale-lock bookkeeping, systemd `Restart=always` restarts clean. `--status` first line now reports `listener: running (single instance lock held)` / `listener: not running` via the same `lock_held()` probe. `flock` dep guard added (`util-linux`). New regression `tests/t-telegram-listener-singleton.sh` (7 checks: first acquires+loops, second exits 1 with exact message, lock releases → third starts clean, status reports both states; stubbed curl/systemctl, sandboxed XDG_RUNTIME_DIR — hermetic, no network). Verified: `bash -n`, `make gen` ×2 byte-idempotent, `make check` OK, `make lint` 0 FAIL / 0 WARN, `make test` green, `git diff --check` clean.
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–218 |
|
||||
| ## 3. Installation Flow | 219–277 |
|
||||
| ## 4. The `pos` CLI System | 278–367 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 368–399 |
|
||||
| ## 6. Docker Compose / ScaleTail | 400–442 |
|
||||
| ## 7. Optional Apps (`apps/`) | 443–472 |
|
||||
| ## 8. Entertainment Module | 473–486 |
|
||||
| ## 9. Systemd Services | 487–498 |
|
||||
| ## 10. Configuration Files | 499–525 |
|
||||
| ## 11. Coding Conventions | 526–558 |
|
||||
| ## 12. Development Workflow | 559–611 |
|
||||
| ## 13. Key File Quick Reference | 612–697 |
|
||||
| ## 14. Common Tasks for Agents | 698–731 |
|
||||
| ## 2. Directory Structure | 44–217 |
|
||||
| ## 3. Installation Flow | 218–276 |
|
||||
| ## 4. The `pos` CLI System | 277–365 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 366–397 |
|
||||
| ## 6. Docker Compose / ScaleTail | 398–440 |
|
||||
| ## 7. Optional Apps (`apps/`) | 441–470 |
|
||||
| ## 8. Entertainment Module | 471–484 |
|
||||
| ## 9. Systemd Services | 485–496 |
|
||||
| ## 10. Configuration Files | 497–523 |
|
||||
| ## 11. Coding Conventions | 524–556 |
|
||||
| ## 12. Development Workflow | 557–609 |
|
||||
| ## 13. Key File Quick Reference | 610–694 |
|
||||
| ## 14. Common Tasks for Agents | 695–728 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -109,15 +109,14 @@ Linux_post_install/
|
||||
│ ├── pos-share-smb-server # Manage the Samba server (status, share/unshare exports, users, enable/disable)
|
||||
│ ├── 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-alias # Manage persistent command aliases (wrapper scripts in ~/.local/bin/)
|
||||
│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||
│ │ [deps: tar]
|
||||
│ ├── pos-system-bank # Persistent command bank for saving and running shell commands
|
||||
│ ├── 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-bank # Persistent command bank for saving and running shell commands
|
||||
│ ├── 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
|
||||
<!-- GEN:END tree -->
|
||||
@@ -337,14 +336,13 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| 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 | alias | `pos-system-alias` | Manage persistent command aliases (wrapper scripts in ~/.local/bin/) | | |
|
||||
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) | tar | |
|
||||
| system | bank | `pos-system-bank` | Persistent command bank for saving and running shell commands | | |
|
||||
| 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 | | |
|
||||
| | bank | `pos-bank` | Persistent command bank for saving and running shell commands | | |
|
||||
| | 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 -->
|
||||
@@ -628,7 +626,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `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` |
|
||||
| `lib/yt-lib.sh` | 50 | Shared YouTube helpers for `pos media yt *` (`yt_check_deps`, `yt_validate_url`, `yt_echo_cmd`, `classify_url`) — sourced by `yt-mp3`/`yt-mp4`/`yt-grab`/`yt-subtitles` |
|
||||
| `lib/bank-lib.sh` | 183 | Shared Command Bank storage helpers for `pos bank` (`bank_load`/`bank_save`/`bank_add`/`bank_remove`/`bank_update`, `{param}` template substitution, v2 `\n`-escaped multiline storage with backward-compat raw load; store `~/.config/linux_post_install/bank.env`) — sourced by `pos-bank` |
|
||||
| `lib/bank-lib.sh` | 183 | Shared Command Bank storage helpers for `pos system bank` (`bank_load`/`bank_save`/`bank_add`/`bank_remove`/`bank_update`, `{param}` template substitution, v2 `\n`-escaped multiline storage with backward-compat raw load; store `~/.config/linux_post_install/bank.env`) — sourced by `pos-system-bank` |
|
||||
| `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 |
|
||||
@@ -645,7 +643,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-communication-matrix-listener` | 582 | Matrix listener: map /command → bash, run them on room messages |
|
||||
| `bin/pos-communication-matrix-sender` | 215 | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||
| `bin/pos-communication-scrcpy` | 245 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| `bin/pos-communication-telegram-listener` | 889 | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages |
|
||||
| `bin/pos-communication-telegram-listener` | 924 | Telegram bot listener: map /command → bash and <prefix> → app, run them on chat messages |
|
||||
| `bin/pos-communication-telegram-sender` | 212 | 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) |
|
||||
@@ -679,17 +677,16 @@ 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-alias` | 488 | Manage persistent command aliases (wrapper scripts in ~/.local/bin/) |
|
||||
| `bin/pos-system-backup` | 301 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-bank` | 647 | Persistent command bank for saving and running shell commands |
|
||||
| `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-system-uninstall` | 518 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||
| `bin/pos-ai` | 714 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-bank` | 311 | Persistent command bank for saving and running shell commands |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 331 | Dynamic bash completion |
|
||||
| `completions/pos.bash` | 330 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
|
||||
+33
-35
@@ -296,22 +296,44 @@ reported as "N videos require sign-in — skipped" (escape hatch:
|
||||
| `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) including runtime-created `~/.config/systemd/user/pos-*` user units, ScaleTail templates + feature-flag store under `/usr/local/share/linux_post_install/`, 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 |
|
||||
| `pos system alias` | `bin/pos-system-alias` | Manage persistent command aliases — create, edit, remove, list, and show named aliases that map names to shell commands via executable wrapper scripts in `~/.local/bin/` | Aliases stored in `~/.config/linux_post_install/aliases.env` (pipe-delimited: `name\|command`). Each alias materializes as a wrapper script at `~/.local/bin/<name>` (chmod 755) that runs the mapped command with any arguments forwarded. Wrapper scripts are synced automatically on every invocation; changes are live immediately. Name validation: must start with a letter, then letters/digits/hyphens/underscores. Refuses name collisions with existing files on `~/.local/bin/` (unless pos-owned) and existing binaries on `PATH`. Requires `~/.local/bin` on `PATH` — a warning with a copy-paste fix appears when it isn't |
|
||||
| `pos system bank` | `bin/pos-system-bank` | Persistent command bank for saving and running shell commands — list, add, show, run, edit, remove, alias | Commands stored in `~/.config/linux_post_install/bank.env` (pipe-delimited: `name\|description\|command`, chmod 600, managed by the tool). Parameterized `{param}` templates are substituted at run time. Interactive menu on a TTY with no args. `BANK_FILE` env seam overrides the path. `alias` subcommand manages bash aliases in `~/.bashrc` inside a managed `# >>> pos bank aliases …` block (`BASH_RC_FILE` env seam) |
|
||||
|
||||
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).
|
||||
|
||||
`pos system alias` in detail:
|
||||
#### system bank
|
||||
|
||||
| Command | Behavior |
|
||||
|---------|----------|
|
||||
| `pos system alias` | Interactive menu: create / edit / remove / list aliases; shows the current alias table between picks |
|
||||
| `pos system alias create [name]` | Interactive 2-step wizard: alias name (must start with a letter, then letters/digits/-/_; unique — collisions with existing files on `~/.local/bin/` or binaries on `PATH` are refused), command (must not contain `\|`); confirms before saving |
|
||||
| `pos system alias edit [name]` | Edits an existing alias (pick from list or pass the name); shows current values, prompts for the new command (Enter keeps current); saves only if changed |
|
||||
| `pos system alias remove [name]` | Removes an alias (pick from list or pass the name); confirmation defaults to **no** — removal deletes the wrapper script and cannot be undone |
|
||||
| `pos system alias list` | Non-interactive: prints all aliases as a Name/Command table (commands truncated at 60 chars) |
|
||||
| `pos system alias show <name>` | Prints one alias's details: name, command, wrapper path, and how to test it |
|
||||
**File:** `bin/pos-system-bank`
|
||||
|
||||
Alias storage & activation: records live in `~/.config/linux_post_install/aliases.env` — one `name\|command` line per alias, chmod 600, managed by the tool (do not hand-edit). **Activation needs no shell sourcing**: every `pos system alias` invocation syncs the ENV file against executable wrapper scripts at `~/.local/bin/<name>` (chmod 755) — missing or changed wrappers are atomically rewritten, wrappers pos owns but the ENV no longer lists are deleted, and hand-edited wrappers are healed. Wrapper scripts re-read their 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.
|
||||
Persistent command bank for saving and running shell commands. Commands are stored in `~/.config/linux_post_install/bank.env` (pipe-delimited: `name|description|command`, chmod 600, managed by the tool). Supports parameterized templates with `{param}` placeholders that are substituted at run time (quoted for safe shell evaluation).
|
||||
|
||||
On a TTY with no arguments, `pos system bank` opens an interactive menu (list / add / run / edit / remove).
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `pos system bank list` | List all saved commands |
|
||||
| `pos system bank add <name> [desc] [cmd]` | Add a new command (interactive for missing args) |
|
||||
| `pos system bank show <name>` | Show command details and detected parameters |
|
||||
| `pos system bank run <name> [key=val …]` | Run a command (interactive for missing params) |
|
||||
| `pos system bank edit <name>` | Edit an existing command |
|
||||
| `pos system bank remove <name>` | Remove a command (also drops aliases pointing at it) |
|
||||
| `pos system bank alias <name> [alias_name]` | Add/update a bash alias for a bank command (default alias name = bank name) |
|
||||
| `pos system bank alias list` | List bash aliases from `~/.bashrc` |
|
||||
| `pos system bank alias remove <alias_name>` | Remove a bash alias |
|
||||
|
||||
Aliases are bash aliases written into `~/.bashrc` inside a managed block
|
||||
(`# >>> pos bank aliases … <<<`, `bashrc`-path seam: `BASH_RC_FILE`), as
|
||||
`alias <alias_name>='pos system bank run <name>'` — retyping the same alias
|
||||
name retargets the line; removing the last alias removes the whole block.
|
||||
An alias name already defined outside the block is refused, and aliasing a
|
||||
command that also exists on `PATH` prints a non-blocking warning (the alias
|
||||
will shadow it in interactive shells).
|
||||
|
||||
Example with parameters:
|
||||
|
||||
```
|
||||
pos system bank add convert "Convert video" "ffmpeg -i {input} -crf {quality} {output}"
|
||||
pos system bank run convert input=clip.mp4 quality=23 output=clip.mkv
|
||||
```
|
||||
|
||||
### ssh
|
||||
|
||||
@@ -534,30 +556,6 @@ Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libfl
|
||||
| `flag-set <name> [value]` | Set a flag, optionally with a value (requires sudo) |
|
||||
| `flag-clear <name>` | Unset a flag (requires sudo) |
|
||||
|
||||
### bank
|
||||
|
||||
**File:** `bin/pos-bank`
|
||||
|
||||
Persistent command bank for saving and running shell commands. Commands are stored in `~/.config/linux_post_install/bank.env` (pipe-delimited: `name|description|command`). Supports parameterized templates with `{param}` placeholders that are substituted at run time (quoted for safe shell evaluation).
|
||||
|
||||
On a TTY with no arguments, `pos bank` opens an interactive menu (list / add / run / edit / remove).
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `pos bank list` | List all saved commands |
|
||||
| `pos bank add <name> [desc] [cmd]` | Add a new command (interactive for missing args) |
|
||||
| `pos bank show <name>` | Show command details and detected parameters |
|
||||
| `pos bank run <name> [key=val …]` | Run a command (interactive for missing params) |
|
||||
| `pos bank edit <name>` | Edit an existing command |
|
||||
| `pos bank remove <name>` | Remove a command |
|
||||
|
||||
Example with parameters:
|
||||
|
||||
```
|
||||
pos bank add convert "Convert video" "ffmpeg -i {input} -crf {quality} {output}"
|
||||
pos bank run convert input=clip.mp4 quality=23 output=clip.mkv
|
||||
```
|
||||
|
||||
### config
|
||||
|
||||
**File:** `bin/pos-config`
|
||||
|
||||
+37
-120
@@ -1,131 +1,16 @@
|
||||
# How-To: `pos system`
|
||||
|
||||
Host care: encrypted backups, firewall, health dashboard, persistent aliases, and uninstall. Tools:
|
||||
`alias`, `backup`, `firewall`, `health`, `uninstall`.
|
||||
Host care: encrypted backups, firewall, health dashboard, uninstall, and the
|
||||
persistent command bank. Tools: `backup`, `firewall`, `health`, `uninstall`,
|
||||
`bank`.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `pos system alias` | Manage persistent command aliases (wrapper scripts in `~/.local/bin/`) |
|
||||
| `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 |
|
||||
|
||||
---
|
||||
|
||||
## `pos system alias` — persistent command aliases
|
||||
|
||||
Create named shortcuts for shell commands. Each alias becomes an executable
|
||||
wrapper script in `~/.local/bin/` that runs the mapped command with any
|
||||
arguments forwarded.
|
||||
|
||||
### Quick start
|
||||
|
||||
```bash
|
||||
pos system alias # interactive menu
|
||||
pos system alias list # show all aliases
|
||||
pos system alias create # interactive create wizard
|
||||
pos system alias show restart-dns # show one alias's details
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Create an alias:**
|
||||
|
||||
```bash
|
||||
pos system alias create restart-dns
|
||||
# Step 1: Alias name → restart-dns
|
||||
# Step 2: Command → sudo systemctl restart systemd-resolved
|
||||
# Confirm → [y]
|
||||
# Alias 'restart-dns' created.
|
||||
# Test it: restart-dns
|
||||
```
|
||||
|
||||
**Create more aliases:**
|
||||
|
||||
```bash
|
||||
pos system alias create exit-google
|
||||
# Command → pkill -f chrome
|
||||
|
||||
pos system alias create update-all
|
||||
# Command → sudo apt update && sudo apt upgrade -y
|
||||
|
||||
pos system alias create my-ip
|
||||
# Command → curl -s ifconfig.me
|
||||
```
|
||||
|
||||
**Use them directly** (no `pos` needed — just the alias name):
|
||||
|
||||
```bash
|
||||
restart-dns # runs: sudo systemctl restart systemd-resolved
|
||||
exit-google # runs: pkill -f chrome
|
||||
update-all # runs: sudo apt update && sudo apt upgrade -y
|
||||
my-ip # runs: curl -s ifconfig.me
|
||||
restart-dns 1.1.1.1 # arguments are forwarded to the command
|
||||
```
|
||||
|
||||
**Edit an alias:**
|
||||
|
||||
```bash
|
||||
pos system alias edit restart-dns
|
||||
# Shows current command, prompts for new value (Enter = keep current)
|
||||
```
|
||||
|
||||
**Remove an alias:**
|
||||
|
||||
```bash
|
||||
pos system alias remove restart-dns
|
||||
# Shows details, asks for confirmation (default: no)
|
||||
```
|
||||
|
||||
**List all aliases:**
|
||||
|
||||
```bash
|
||||
pos system alias list
|
||||
# Name Command
|
||||
# ---------------- ----------------------------------------
|
||||
# restart-dns sudo systemctl restart systemd-resolved
|
||||
# exit-google pkill -f chrome
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
- Aliases are stored in `~/.config/linux_post_install/aliases.env`
|
||||
(pipe-delimited: `name|command`, chmod 600).
|
||||
- Each alias is materialized as an executable wrapper at
|
||||
`~/.local/bin/<name>` (chmod 755).
|
||||
- Wrappers are synced automatically on every `pos system alias` invocation
|
||||
— edits are live on the next run.
|
||||
- Name validation: must start with a letter, then letters/digits/hyphens/
|
||||
underscores. Collisions with existing files or PATH binaries are refused.
|
||||
|
||||
### PATH requirement
|
||||
|
||||
`~/.local/bin` must be on your `PATH` for alias scripts to resolve by name.
|
||||
If it isn't, you'll see a warning with a fix:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
# Persist it:
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
|
||||
```
|
||||
|
||||
### Recipes
|
||||
|
||||
- **DNS restart shortcut:** `pos system alias create restart-dns`
|
||||
with command `sudo systemctl restart systemd-resolved`.
|
||||
- **Quick app launcher:** `pos system alias create open-code`
|
||||
with command `code ~/projects`.
|
||||
- **Custom backup alias:** `pos system alias create snap-docs`
|
||||
with command `pos system backup ~/Documents`.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- `Alias 'X' already exists` → use `pos system alias edit X` instead.
|
||||
- `File '~/.local/bin/X' already exists` → pick a different name (pos
|
||||
won't overwrite non-pos-owned files).
|
||||
- `~/.local/bin is not on your PATH` → add it to `~/.profile` (see above).
|
||||
- Alias name autocompletes stale after removal → run `hash -r`.
|
||||
| `pos system bank` | Persistent command bank — save, run, and alias shell commands |
|
||||
|
||||
---
|
||||
|
||||
@@ -350,6 +235,39 @@ confirmation. The git repo is **never** removed — delete it manually if desire
|
||||
- "Nothing to remove" → pos toolkit is not installed (or already removed)
|
||||
- After uninstall, run `source ~/.bashrc` or restart your shell
|
||||
|
||||
## `pos system bank` — persistent command bank + bash aliases
|
||||
|
||||
```bash
|
||||
pos system bank add convert "Convert video" "ffmpeg -i {input} -crf {quality} {output}"
|
||||
pos system bank list
|
||||
pos system bank run convert input=clip.mp4 quality=23 output=clip.mkv
|
||||
pos system bank alias convert conv # bash alias: conv='pos system bank run convert'
|
||||
pos system bank alias list
|
||||
```
|
||||
|
||||
The bank is a persistent store of shell commands in
|
||||
`~/.config/linux_post_install/bank.env` (pipe-delimited, chmod 600, managed by
|
||||
the tool). `{param}` placeholders are substituted at run time; on a TTY with no
|
||||
arguments the tool opens an interactive menu.
|
||||
|
||||
**Bash aliases** are written to `~/.bashrc` inside a managed block
|
||||
(`# >>> pos bank aliases … <<<`), one line per alias:
|
||||
`alias <alias_name>='pos system bank run <name>'`. Details:
|
||||
|
||||
- Default alias name is the bank name; pass a second argument for a shorter one:
|
||||
`pos system bank alias convert conv`.
|
||||
- Reusing an alias name **retargets** it to the new bank command; removing the
|
||||
last alias removes the whole managed block.
|
||||
- An alias name already defined elsewhere in `~/.bashrc` is **refused** (shown
|
||||
with its line) so the managed block never shadows your own config. Aliasing a
|
||||
command that also exists on `PATH` prints a non-blocking notice that it will
|
||||
shadow the real command in interactive shells.
|
||||
- `pos system bank remove <name>` also drops aliases pointing at the removed
|
||||
command.
|
||||
|
||||
After adding/removing an alias, run `source ~/.bashrc` (or open a new shell)
|
||||
for it to take effect.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
@@ -357,4 +275,3 @@ confirmation. The git repo is **never** removed — delete it manually if desire
|
||||
- Reference: [DOC/POS.md → system](../POS.md)
|
||||
- Notify platform config: [communication.md](communication.md)
|
||||
- Backup roots shared with health: `system.env` ([DOC/POS.md](../POS.md))
|
||||
- Alias storage: `~/.config/linux_post_install/aliases.env` ([DOC/POS.md → pos system alias](../POS.md#pos-system-alias-in-detail))
|
||||
|
||||
@@ -28,7 +28,7 @@ _pos_commands() {
|
||||
# ── Category list (sorted, filename-derived) ─────────────────────
|
||||
# Nested sub-tools (pos-<cat>-<a>-<b> where pos-<cat>-<a> exists) are
|
||||
# omitted from the cheat-sheet line — they are shown under their parent tool.
|
||||
# Category-less tools (pos-bank, pos-config, pos-tree) are emitted as their
|
||||
# Category-less tools (pos-config, pos-tree) are emitted as their
|
||||
# own line with an empty subcommand column; a name already present as a
|
||||
# category (pos-ai vs the ai category built from pos-ai-*) is NOT duplicated.
|
||||
_pos_category_list() {
|
||||
@@ -214,7 +214,7 @@ EXAMPLES
|
||||
|
||||
pos tree Show the command tree (categories + subcommands)
|
||||
|
||||
pos bank run <name> Run a saved command from the bank
|
||||
pos system bank run <name> Run a saved command from the bank
|
||||
|
||||
pos docker vbox create lab1 Create disposable Docker VM
|
||||
pos docker vbox create lab1 --dir . Create VM using current directory
|
||||
@@ -274,7 +274,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="bank docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-yt-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-llamacpp ai-alias system-alias system-schedule entertainment-config config"
|
||||
INTERACTIVE_CMDS="system-bank docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-yt-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-llamacpp ai-alias system-schedule entertainment-config config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: bank — Persistent command bank for saving and running shell commands
|
||||
# POS_SUBCMDS: list add show run edit remove
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
|
||||
source "$(dirname "$0")/../lib/bank-lib.sh" 2>/dev/null || source "$(dirname "$0")/bank-lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos bank [subcommand] [args]
|
||||
|
||||
Persistent command bank for saving and running shell commands.
|
||||
|
||||
Subcommands:
|
||||
list List all saved commands
|
||||
add <name> [desc] [cmd] Add a new command (interactive if args missing)
|
||||
show <name> Show command details
|
||||
run <name> [key=val ...] Run a command (interactive for missing params)
|
||||
edit <name> Edit an existing command
|
||||
remove <name> Remove a command
|
||||
|
||||
Interactive menu:
|
||||
pos bank (no args on a terminal)
|
||||
|
||||
Examples:
|
||||
pos bank add disk-info "Disk usage" "df -h && du -sh /mnt/12T"
|
||||
pos bank run disk-info
|
||||
pos bank run convert-video input=/path/to/video.mp4 quality=23 output=/path/out.mp4
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
# ── Interactive command picker ───────────────────────────────────
|
||||
_pick_command() {
|
||||
bank_load
|
||||
local count="${#BANK_NAMES[@]}"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
warn "Command bank is empty — add one with: pos bank add <name> \"desc\" \"cmd\""
|
||||
return 1
|
||||
fi
|
||||
local -a items=()
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
if [ -n "${BANK_DESCS[$i]}" ]; then
|
||||
items+=("${BANK_NAMES[$i]} — ${BANK_DESCS[$i]}")
|
||||
else
|
||||
items+=("${BANK_NAMES[$i]}")
|
||||
fi
|
||||
done
|
||||
local choice
|
||||
choice="$(menu_pick "Pick command" "${items[@]}")" || return 1
|
||||
echo "${BANK_NAMES[$((choice - 1))]}"
|
||||
}
|
||||
|
||||
# ── cmd_list ─────────────────────────────────────────────────────
|
||||
cmd_list() {
|
||||
bank_load
|
||||
local count="${#BANK_NAMES[@]}"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
log "Command bank is empty — add one with: pos bank add <name> \"desc\" \"cmd\""
|
||||
return 0
|
||||
fi
|
||||
echo
|
||||
echo "COMMAND BANK"
|
||||
echo
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
if [ -n "${BANK_DESCS[$i]}" ]; then
|
||||
printf ' %-25s %s\n' "${BANK_NAMES[$i]}" "${BANK_DESCS[$i]}"
|
||||
else
|
||||
printf ' %s\n' "${BANK_NAMES[$i]}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# ── cmd_add ──────────────────────────────────────────────────────
|
||||
cmd_add() {
|
||||
local name="" desc="" cmd=""
|
||||
case $# in
|
||||
0) ;;
|
||||
1) name="$1" ;;
|
||||
2) name="$1"; desc="$2" ;;
|
||||
*) name="$1"; desc="$2"; cmd="$3" ;;
|
||||
esac
|
||||
if [ -z "$name" ]; then
|
||||
name="$(menu_ask_value "Name")" || return 0
|
||||
fi
|
||||
if [ -z "$desc" ]; then
|
||||
desc="$(menu_ask_value "Description" "")" || return 0
|
||||
fi
|
||||
if [ -z "$cmd" ]; then
|
||||
cmd="$(menu_read_value "Command")" || return 0
|
||||
fi
|
||||
bank_valid_name "$name" || err "Invalid name: $name (use letters, numbers, hyphens, underscores)"
|
||||
[ -n "$cmd" ] || err "Command cannot be empty"
|
||||
bank_load
|
||||
local i
|
||||
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||
[[ "${BANK_NAMES[$i]}" == "$name" ]] && err "Command '$name' already exists (use 'pos bank edit' to modify)"
|
||||
done
|
||||
bank_add "$name" "$desc" "$cmd"
|
||||
ok "Saved: $name"
|
||||
}
|
||||
|
||||
# ── cmd_show ─────────────────────────────────────────────────────
|
||||
cmd_show() {
|
||||
[ -n "${1:-}" ] || err "Usage: pos bank show <name>"
|
||||
local name="$1"
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local cmd_name cmd_desc cmd_cmd
|
||||
cmd_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
echo
|
||||
echo "Name: $cmd_name"
|
||||
echo
|
||||
echo "Description:"
|
||||
if [ -n "$cmd_desc" ]; then
|
||||
echo "$cmd_desc"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo
|
||||
echo "Command:"
|
||||
echo
|
||||
echo "$cmd_cmd"
|
||||
local params
|
||||
params="$(_extract_params "$cmd_cmd")"
|
||||
if [ -n "$params" ]; then
|
||||
echo
|
||||
echo "Parameters:"
|
||||
echo "$params" | while IFS= read -r p; do
|
||||
echo " $p"
|
||||
done
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# ── cmd_run ──────────────────────────────────────────────────────
|
||||
cmd_run() {
|
||||
local name=""
|
||||
local -a cli_params=()
|
||||
if [ $# -eq 0 ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
else
|
||||
name="$1"; shift
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
*=*) cli_params+=("$1") ;;
|
||||
*) err "Invalid parameter format: $1 (expected key=value)" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local cmd_name cmd_desc cmd_cmd
|
||||
cmd_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
# Detect parameters
|
||||
local params_str
|
||||
params_str="$(_extract_params "$cmd_cmd")"
|
||||
local -a params=()
|
||||
if [ -n "$params_str" ]; then
|
||||
while IFS= read -r p; do
|
||||
params+=("$p")
|
||||
done <<< "$params_str"
|
||||
fi
|
||||
# Build parameter values from CLI args
|
||||
local -A param_values=()
|
||||
for p in "${cli_params[@]}"; do
|
||||
local key="${p%%=*}" val="${p#*=}"
|
||||
param_values["$key"]="$val"
|
||||
done
|
||||
# Interactively prompt for missing parameters
|
||||
local -a missing=()
|
||||
for p in "${params[@]}"; do
|
||||
[[ -z "${param_values[$p]+x}" ]] && missing+=("$p")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
for p in "${missing[@]}"; do
|
||||
local val
|
||||
val="$(menu_ask_value "$p" "")" || return 0
|
||||
param_values["$p"]="$val"
|
||||
done
|
||||
fi
|
||||
# Substitute parameters
|
||||
local final_cmd="$cmd_cmd"
|
||||
if [ ${#params[@]} -gt 0 ]; then
|
||||
local -a sub_args=()
|
||||
for p in "${params[@]}"; do
|
||||
sub_args+=("${p}=${param_values[$p]:-}")
|
||||
done
|
||||
final_cmd="$(_substitute_params "$cmd_cmd" "${sub_args[@]}")"
|
||||
fi
|
||||
echo
|
||||
log "Running: $final_cmd"
|
||||
echo
|
||||
eval "$final_cmd"
|
||||
return $?
|
||||
}
|
||||
|
||||
# ── cmd_edit ─────────────────────────────────────────────────────
|
||||
cmd_edit() {
|
||||
local name="${1:-}"
|
||||
if [ -z "$name" ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local old_name old_desc old_cmd
|
||||
old_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
old_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
old_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
echo
|
||||
log "Editing: $old_name"
|
||||
echo
|
||||
local new_name new_desc new_cmd
|
||||
new_name="$(menu_ask_value "New name" "$old_name")" || return 0
|
||||
new_desc="$(menu_ask_value "New description" "$old_desc")" || return 0
|
||||
new_cmd="$(menu_read_value "New command")" || return 0
|
||||
[ -z "$new_name" ] && new_name="$old_name"
|
||||
[ -z "$new_desc" ] && new_desc="$old_desc"
|
||||
[ -z "$new_cmd" ] && new_cmd="$old_cmd"
|
||||
if [ "$new_name" != "$old_name" ]; then
|
||||
bank_valid_name "$new_name" || err "Invalid name: $new_name"
|
||||
local i found=0
|
||||
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||
[[ "${BANK_NAMES[$i]}" == "$new_name" ]] && found=1
|
||||
done
|
||||
[[ "$found" -eq 1 ]] && err "A command named '$new_name' already exists"
|
||||
bank_remove "$old_name"
|
||||
bank_add "$new_name" "$new_desc" "$new_cmd"
|
||||
else
|
||||
bank_update "$new_name" "$new_desc" "$new_cmd"
|
||||
fi
|
||||
ok "Updated: $new_name"
|
||||
}
|
||||
|
||||
# ── cmd_remove ───────────────────────────────────────────────────
|
||||
cmd_remove() {
|
||||
local name="${1:-}"
|
||||
if [ -z "$name" ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
if [ -t 0 ]; then
|
||||
confirm "Remove '$name'?" n || { log "Cancelled"; return 0; }
|
||||
fi
|
||||
bank_remove "$name"
|
||||
ok "Removed: $name"
|
||||
}
|
||||
|
||||
# ── Interactive menu ─────────────────────────────────────────────
|
||||
run_menu() {
|
||||
menu_guard || exit 1
|
||||
while true; do
|
||||
local choice
|
||||
choice="$(menu_run "Command Bank" \
|
||||
"List commands" \
|
||||
"Add command" \
|
||||
"Run command" \
|
||||
"Edit command" \
|
||||
"Remove command")" || return 0
|
||||
case "$choice" in
|
||||
1) cmd_list ;;
|
||||
2) cmd_add ;;
|
||||
3) cmd_run ;;
|
||||
4) cmd_edit ;;
|
||||
5) cmd_remove ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────
|
||||
if [ "${1:-}" = "menu" ]; then
|
||||
run_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $# -eq 0 ] && [ -t 0 ]; then
|
||||
run_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
list) shift; cmd_list "$@" ;;
|
||||
add) shift; cmd_add "$@" ;;
|
||||
show) shift; cmd_show "$@" ;;
|
||||
run) shift; cmd_run "$@" ;;
|
||||
edit) shift; cmd_edit "$@" ;;
|
||||
remove) shift; cmd_remove "$@" ;;
|
||||
"") usage ;;
|
||||
*) err "Unknown subcommand: $1 (see --help)" ;;
|
||||
esac
|
||||
@@ -8,6 +8,9 @@ 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"
|
||||
# Persisted getUpdates offset (survives restarts so unconfirmed updates are
|
||||
# never re-delivered in a burst after a crash/restart).
|
||||
STATE_FILE="${LISTENER_STATE_FILE:-$CONFIG_DIR/telegram-listener.state}"
|
||||
API="https://api.telegram.org"
|
||||
SERVICE="pos-telegram-listener.service"
|
||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
@@ -97,6 +100,31 @@ load_config() {
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
# ── getUpdates offset persistence ────────────────────────────────
|
||||
# load_offset — read the last confirmed update offset from STATE_FILE.
|
||||
# Returns 0 when the file is missing/invalid (fresh start).
|
||||
load_offset() {
|
||||
local val
|
||||
[ -f "$STATE_FILE" ] || { printf '0'; return 0; }
|
||||
val="$(cat "$STATE_FILE" 2>/dev/null || true)"
|
||||
case "$val" in
|
||||
''|*[!0-9]*) printf '0' ;;
|
||||
*) printf '%s' "$val" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# save_offset — atomically persist the last processed update offset.
|
||||
save_offset() {
|
||||
local val="$1"
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
printf '%s\n' "$val" > "$tmp"
|
||||
chmod 600 "$tmp"
|
||||
mv "$tmp" "$STATE_FILE"
|
||||
chmod 600 "$STATE_FILE"
|
||||
}
|
||||
|
||||
# ── command map (MAP_FILE) ──────────────────────────────────────
|
||||
# Lines: /cmd=bash command, or /cmd::description=bash command. Keys keep the
|
||||
# leading slash; read via awk so values may contain '='. Entries are emitted
|
||||
@@ -347,9 +375,12 @@ prefix_map_show() {
|
||||
#
|
||||
# PID → metadata arrays (populated by run_and_reply, drained by reap_commands)
|
||||
declare -A _CMD_OUT _CMD_MSG _CMD_QUIET
|
||||
# Exit codes stored by the SIGCHLD handler (wait -n) so reap_commands can
|
||||
# retrieve them without calling blocking wait.
|
||||
declare -A _EXIT_CODES
|
||||
#
|
||||
# No SIGCHLD reaper: `wait -n`/`wait -n -p` inside a CHLD trap is unreliable
|
||||
# on bash 5.2 (it reports "no children" even when processes have exited), so
|
||||
# a trap-based reaper silently never fires. reap_commands below polls with
|
||||
# kill -0 and reaps via `wait "$pid" || rc=$?` — simple, non-blocking, and
|
||||
# exit-code-correct (see the crash this design replaces).
|
||||
|
||||
# run_and_reply <cmdline> <msg_id> [timeout] [quiet]
|
||||
# Starts the command in the background and returns immediately. The main
|
||||
@@ -376,13 +407,12 @@ reap_commands() {
|
||||
for pid in "${!_CMD_OUT[@]}"; do
|
||||
# Non-blocking: has the process exited?
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
# Retrieve exit code (SIGCHLD handler stores it; fallback to wait).
|
||||
local rc="${_EXIT_CODES[$pid]:-}"
|
||||
if [ -n "$rc" ]; then
|
||||
unset _EXIT_CODES[$pid]
|
||||
else
|
||||
wait "$pid" 2>/dev/null; rc=$?
|
||||
fi
|
||||
# Retrieve exit code. `wait "$pid"` returns immediately since the
|
||||
# process has exited. IMPORTANT: a bare `wait "$pid"` under
|
||||
# `set -e` would abort the daemon on non-zero exits (the crash
|
||||
# this code replaces), so the status is captured via `|| rc=$?`.
|
||||
local rc=0
|
||||
wait "$pid" 2>/dev/null || rc=$?
|
||||
local out_file="${_CMD_OUT[$pid]}"
|
||||
local msg_id="${_CMD_MSG[$pid]}"
|
||||
local quiet="${_CMD_QUIET[$pid]}"
|
||||
@@ -831,11 +861,15 @@ run_daemon() {
|
||||
fi
|
||||
sync_bot_commands || true
|
||||
|
||||
local offset=0
|
||||
log "listener running (chat ${TELEGRAM_CHAT_ID}, owner ${TELEGRAM_OWNER_ID:-unset}) — Ctrl+C to stop"
|
||||
# SIGCHLD: reap finished children and store their exit codes so
|
||||
# reap_commands can retrieve them without blocking.
|
||||
trap 'local _p; while _p=$(wait -n 2>/dev/null); do _EXIT_CODES[$_p]=$?; done' CHLD
|
||||
# Resume from the last persisted update offset so unconfirmed updates are
|
||||
# not re-delivered in a burst after a crash/restart (systemd Restart=always
|
||||
# used to restart from 0 and re-run duplicate commands).
|
||||
local offset
|
||||
offset="$(load_offset)"
|
||||
log "listener running (chat ${TELEGRAM_CHAT_ID}, owner ${TELEGRAM_OWNER_ID:-unset}, offset ${offset}) — Ctrl+C to stop"
|
||||
# No SIGCHLD reaper here: `wait -n` inside a CHLD trap is unreliable on
|
||||
# bash 5.2 (see the note near the _CMD_* arrays). reap_commands handles
|
||||
# finished children after each polling cycle.
|
||||
trap 'kill $(jobs -p) 2>/dev/null; rm -f /tmp/pos-cmd.* 2>/dev/null; wait 2>/dev/null; exit 0' TERM INT
|
||||
while true; do
|
||||
local resp n i
|
||||
@@ -857,6 +891,7 @@ run_daemon() {
|
||||
msg_id="$(printf '%s' "$resp" | jq -r ".result[$i].message.message_id // empty")"
|
||||
reply_text="$(printf '%s' "$resp" | jq -r ".result[$i].message.reply_to_message.text // .result[$i].message.reply_to_message.caption // empty")"
|
||||
offset=$((u + 1))
|
||||
save_offset "$offset"
|
||||
[ -n "$text" ] || continue
|
||||
if [ -z "${TELEGRAM_OWNER_ID:-}" ]; then
|
||||
warn "TELEGRAM_OWNER_ID unset — ignoring command (set it with 'pos config telegram')"
|
||||
|
||||
@@ -1,488 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: system alias — Manage persistent command aliases (wrapper scripts in ~/.local/bin/)
|
||||
# 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}/aliases.env"
|
||||
|
||||
# ── Core helpers ───────────────────────────────────────────────
|
||||
|
||||
# Load aliases from ENV_FILE into parallel arrays.
|
||||
# Loop vars use _a* prefix to avoid dynamic-scope collisions with callers.
|
||||
_alias_load() {
|
||||
_ALIAS_NAMES=(); _ALIAS_COMMANDS=()
|
||||
[ -f "$ENV_FILE" ] || return 0
|
||||
local _an _ac
|
||||
while IFS='|' read -r _an _ac; do
|
||||
[[ "$_an" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${_an// /}" ]] && continue
|
||||
_an="${_an## }"; _an="${_an%% }"
|
||||
[[ "$_an" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
||||
_ac="${_ac## }"; _ac="${_ac%% }"
|
||||
_ALIAS_NAMES+=("$_an")
|
||||
_ALIAS_COMMANDS+=("$_ac")
|
||||
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
||||
}
|
||||
|
||||
# Save parallel arrays back to ENV_FILE (atomic overwrite).
|
||||
_alias_save() {
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
{
|
||||
printf '%s\n' "# System aliases — managed by pos system alias (do not hand-edit)"
|
||||
printf '%s\n' "# Format: alias_name|command"
|
||||
local i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
printf '%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_COMMANDS[$i]}"
|
||||
done
|
||||
} >"$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
}
|
||||
|
||||
# ── Wrapper scripts ────────────────────────────────────────────
|
||||
|
||||
_wrapper_path() {
|
||||
printf '%s/.local/bin/%s' "$HOME" "$1"
|
||||
}
|
||||
|
||||
# Ownership test: line 2 must carry our generator marker.
|
||||
_alias_owned() {
|
||||
[ -f "$1" ] && sed -n '2p' "$1" 2>/dev/null | grep -q 'Managed by pos system alias'
|
||||
}
|
||||
|
||||
# Render one wrapper to stdout (args: name command).
|
||||
_wrapper_render() {
|
||||
local name="$1" command="$2"
|
||||
cat <<WRAPPER_EOF
|
||||
#!/usr/bin/env bash
|
||||
# Managed by pos system alias — regenerated automatically; hand-edits are overwritten.
|
||||
# Alias: ${name} | command: ${command}
|
||||
set -euo pipefail
|
||||
exec bash -c '${command} "\$@"' _ "\$@"
|
||||
WRAPPER_EOF
|
||||
}
|
||||
|
||||
# Atomically install/refresh one wrapper. Skips when content matches.
|
||||
# Pre-commit validation: bash -n on the rendered file; failure keeps previous.
|
||||
_wrapper_install() {
|
||||
local name="$1" command="$2" path tmp
|
||||
path="$(_wrapper_path "$name")"
|
||||
tmp="$(mktemp "${HOME}/.local/bin/.pos-alias.XXXXXX")"
|
||||
_wrapper_render "$name" "$command" >"$tmp"
|
||||
if cmp -s "$tmp" "$path" 2>/dev/null; then
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
if ! bash -n "$tmp" 2>/dev/null; then
|
||||
warn "Wrapper for '$name' 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
|
||||
}
|
||||
|
||||
# Two-way reconciliation on every invocation:
|
||||
# forward: each ENV entry → render-diff-install
|
||||
# reverse: owned wrappers whose name is not in ENV → deleted
|
||||
# plus: PATH guidance when owned wrappers exist but ~/.local/bin is absent
|
||||
_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_COMMANDS[$i]}" || :
|
||||
done
|
||||
for f in "$bin_dir"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
_alias_owned "$f" || continue
|
||||
base="${f##*/}"
|
||||
match=0
|
||||
for name in ${_ALIAS_NAMES[@]+"${_ALIAS_NAMES[@]}"}; do
|
||||
[ "$base" = "$name" ] && { match=1; break; }
|
||||
done
|
||||
[ "$match" -eq 1 ] || rm -f "$f"
|
||||
done
|
||||
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_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_-]*$ ]]
|
||||
}
|
||||
|
||||
# Truncate a command string for display (inline pipes, redirects).
|
||||
_command_truncate() {
|
||||
local s="$1" max="${2:-42}"
|
||||
s="${s//$'\n'/ }"
|
||||
if [ ${#s} -gt "$max" ]; then
|
||||
printf '%s…' "${s:0:max}"
|
||||
else
|
||||
printf '%s' "$s"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Non-interactive output ─────────────────────────────────────
|
||||
|
||||
_alias_table() {
|
||||
local count=${#_ALIAS_NAMES[@]} i
|
||||
[ "$count" -eq 0 ] && return 0
|
||||
printf ' %-16s %s\n' "Name" "Command"
|
||||
printf ' %-16s %s\n' "----------------" "----------------------------------------"
|
||||
for ((i = 0; i < count; i++)); do
|
||||
printf ' %-16s %s\n' "${_ALIAS_NAMES[$i]}" "$(_command_truncate "${_ALIAS_COMMANDS[$i]}" 60)"
|
||||
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]}" command="${_ALIAS_COMMANDS[$idx]}"
|
||||
printf ' %-12s %s\n' "Alias:" "$name"
|
||||
printf ' %-12s %s\n' "Command:" "$command"
|
||||
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' "Test:" "$name"
|
||||
}
|
||||
|
||||
# ── 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 "System 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 IS the current table
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Interactive: create ────────────────────────────────────────
|
||||
|
||||
_alias_create() {
|
||||
local preset_name="${1:-}"
|
||||
section "Create System Alias" >&2
|
||||
|
||||
# Step 1: Alias name
|
||||
local name="$preset_name"
|
||||
while true; do
|
||||
if [ -z "$name" ]; then
|
||||
step 1 2 "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 system alias edit $name' instead" >&2
|
||||
[ -n "$preset_name" ] && return 1
|
||||
name=""; continue
|
||||
fi
|
||||
# Collision: wrapper exists without our marker → refuse
|
||||
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 system alias — pick another name"
|
||||
fi
|
||||
# Collision: name resolves to another binary on PATH → refuse
|
||||
if 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: Command
|
||||
local command=""
|
||||
while true; do
|
||||
step 2 2 "Command" >&2
|
||||
command="$(menu_ask_value "Command to execute" "")" || return 0
|
||||
[ -z "$command" ] && { warn "Command cannot be empty" >&2; continue; }
|
||||
[[ "$command" == *'|'* ]] || break
|
||||
warn "Command must not contain '|' characters" >&2
|
||||
command=""
|
||||
done
|
||||
|
||||
# Confirmation
|
||||
{
|
||||
echo "────────────────────────────────────────────"
|
||||
printf ' Create alias '\''%s'\''?\n' "$name"
|
||||
printf ' Command: %s\n' "$command"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Create alias '$name'?" y; then
|
||||
log "Aborted." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_NAMES+=("$name")
|
||||
_ALIAS_COMMANDS+=("$command")
|
||||
_alias_save
|
||||
_alias_sync
|
||||
log "Alias '$name' created." >&2
|
||||
log "Test it: $name" >&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 System Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local c="${_ALIAS_COMMANDS[$i]}"
|
||||
if [ ${#c} -gt 30 ]; then
|
||||
c="${c:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} → ${c}")
|
||||
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 ' Command: %s\n' "${_ALIAS_COMMANDS[$idx]}"
|
||||
echo >&2
|
||||
} >&2
|
||||
|
||||
local new_command="${_ALIAS_COMMANDS[$idx]}"
|
||||
local changed=0
|
||||
|
||||
# Edit command
|
||||
step 1 1 "Command" >&2
|
||||
local default_display="${_ALIAS_COMMANDS[$idx]}"
|
||||
[ ${#default_display} -gt 60 ] && default_display="${default_display:0:60}…"
|
||||
local tmp_command
|
||||
tmp_command="$(menu_ask_value "Command to execute" "$default_display")" || return 0
|
||||
if [ -n "$tmp_command" ]; then
|
||||
if [[ "$tmp_command" == *'|'* ]]; then
|
||||
warn "Command must not contain '|' characters" >&2
|
||||
return 0
|
||||
fi
|
||||
if [ "$tmp_command" != "${_ALIAS_COMMANDS[$idx]}" ]; then
|
||||
new_command="$tmp_command"
|
||||
changed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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_c
|
||||
[ "$new_command" = "${_ALIAS_COMMANDS[$idx]}" ] && tag_c="(unchanged)" || tag_c="(changed)"
|
||||
printf ' Command: %s %s\n' "$new_command" "$tag_c"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Save changes to '$name'?" y; then
|
||||
log "Discarded." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_COMMANDS[$idx]="$new_command"
|
||||
_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 System Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local c="${_ALIAS_COMMANDS[$i]}"
|
||||
if [ ${#c} -gt 30 ]; then
|
||||
c="${c:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} → ${c}")
|
||||
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 ' Command: %s\n' "${_ALIAS_COMMANDS[$idx]}"
|
||||
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_commands=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" != "$name" ]; then
|
||||
new_names+=("${_ALIAS_NAMES[$i]}")
|
||||
new_commands+=("${_ALIAS_COMMANDS[$i]}")
|
||||
fi
|
||||
done
|
||||
_ALIAS_NAMES=("${new_names[@]+"${new_names[@]}"}")
|
||||
_ALIAS_COMMANDS=("${new_commands[@]+"${new_commands[@]}"}")
|
||||
_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
|
||||
}
|
||||
|
||||
# ── Usage ──────────────────────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos system alias [subcommand] [args]
|
||||
|
||||
Manage persistent command aliases — create, edit, remove, list, and show
|
||||
named aliases. Each alias maps a name to a shell command, materialized as
|
||||
an executable wrapper script in ~/.local/bin/.
|
||||
|
||||
Subcommands:
|
||||
(no args) Interactive menu
|
||||
create [name] Create a new alias (interactive prompts)
|
||||
edit [name] Edit an existing alias (interactive, Enter = keep)
|
||||
remove [name] Remove an alias (with confirmation)
|
||||
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. Changes are
|
||||
live on the next invocation.
|
||||
|
||||
Options:
|
||||
-h|--help Show this help.
|
||||
|
||||
Examples:
|
||||
pos system alias # interactive menu
|
||||
pos system alias list # show all aliases
|
||||
pos system alias create # interactive create
|
||||
pos system alias create restart-dns # create 'restart-dns' alias
|
||||
pos system alias edit restart-dns # edit the 'restart-dns' alias
|
||||
pos system alias remove restart-dns # remove 'restart-dns' (with confirm)
|
||||
pos system alias show restart-dns # show alias details
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Main dispatch ──────────────────────────────────────────────
|
||||
# Every subcommand syncs first: artifacts always equal ENV truth before any
|
||||
# subcommand logic runs.
|
||||
|
||||
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 system alias show <name>"
|
||||
_alias_sync
|
||||
_alias_show "$2"
|
||||
;;
|
||||
"") _alias_sync; _alias_menu ;;
|
||||
*) err "Unknown subcommand '$1' (use -h for help)" ;;
|
||||
esac
|
||||
Executable
+647
@@ -0,0 +1,647 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: system bank — Persistent command bank for saving and running shell commands
|
||||
# POS_SUBCMDS: list add show run edit remove alias
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
|
||||
source "$(dirname "$0")/../lib/bank-lib.sh" 2>/dev/null || source "$(dirname "$0")/bank-lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos system bank [subcommand] [args]
|
||||
|
||||
Persistent command bank for saving and running shell commands.
|
||||
|
||||
Subcommands:
|
||||
list List all saved commands
|
||||
add <name> [desc] [cmd] Add a new command (interactive if args missing)
|
||||
show <name> Show command details
|
||||
run <name> [key=val ...] Run a command (interactive for missing params)
|
||||
edit <name> Edit an existing command
|
||||
remove <name> Remove a command
|
||||
alias <name> [alias_name] Create/update a bash alias for a bank command
|
||||
alias list List bash aliases in ~/.bashrc
|
||||
alias remove <alias_name> Remove a bash alias
|
||||
|
||||
Interactive menu:
|
||||
pos system bank (no args on a terminal)
|
||||
|
||||
Examples:
|
||||
pos system bank add disk-info "Disk usage" "df -h && du -sh /mnt/12T"
|
||||
pos system bank run disk-info
|
||||
pos system bank run convert-video input=/path/to/video.mp4 quality=23 output=/path/out.mp4
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
# ── Interactive command picker ───────────────────────────────────
|
||||
_pick_command() {
|
||||
bank_load
|
||||
local count="${#BANK_NAMES[@]}"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
warn "Command bank is empty — add one with: pos system bank add <name> \"desc\" \"cmd\""
|
||||
return 1
|
||||
fi
|
||||
local -a items=()
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
if [ -n "${BANK_DESCS[$i]}" ]; then
|
||||
items+=("${BANK_NAMES[$i]} — ${BANK_DESCS[$i]}")
|
||||
else
|
||||
items+=("${BANK_NAMES[$i]}")
|
||||
fi
|
||||
done
|
||||
local choice
|
||||
choice="$(menu_pick "Pick command" "${items[@]}")" || return 1
|
||||
echo "${BANK_NAMES[$((choice - 1))]}"
|
||||
}
|
||||
|
||||
# ── cmd_list ─────────────────────────────────────────────────────
|
||||
cmd_list() {
|
||||
bank_load
|
||||
local count="${#BANK_NAMES[@]}"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
log "Command bank is empty — add one with: pos system bank add <name> \"desc\" \"cmd\""
|
||||
return 0
|
||||
fi
|
||||
echo
|
||||
echo "COMMAND BANK"
|
||||
echo
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
if [ -n "${BANK_DESCS[$i]}" ]; then
|
||||
printf ' %-25s %s\n' "${BANK_NAMES[$i]}" "${BANK_DESCS[$i]}"
|
||||
else
|
||||
printf ' %s\n' "${BANK_NAMES[$i]}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# ── cmd_add ──────────────────────────────────────────────────────
|
||||
cmd_add() {
|
||||
local name="" desc="" cmd=""
|
||||
case $# in
|
||||
0) ;;
|
||||
1) name="$1" ;;
|
||||
2) name="$1"; desc="$2" ;;
|
||||
*) name="$1"; desc="$2"; cmd="$3" ;;
|
||||
esac
|
||||
if [ -z "$name" ]; then
|
||||
name="$(menu_ask_value "Name")" || return 0
|
||||
fi
|
||||
if [ -z "$desc" ]; then
|
||||
desc="$(menu_ask_value "Description" "")" || return 0
|
||||
fi
|
||||
if [ -z "$cmd" ]; then
|
||||
cmd="$(menu_read_value "Command")" || return 0
|
||||
fi
|
||||
bank_valid_name "$name" || err "Invalid name: $name (use letters, numbers, hyphens, underscores)"
|
||||
[ -n "$cmd" ] || err "Command cannot be empty"
|
||||
bank_load
|
||||
local i
|
||||
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||
[[ "${BANK_NAMES[$i]}" == "$name" ]] && err "Command '$name' already exists (use 'pos system bank edit' to modify)"
|
||||
done
|
||||
bank_add "$name" "$desc" "$cmd"
|
||||
ok "Saved: $name"
|
||||
}
|
||||
|
||||
# ── cmd_show ─────────────────────────────────────────────────────
|
||||
cmd_show() {
|
||||
[ -n "${1:-}" ] || err "Usage: pos system bank show <name>"
|
||||
local name="$1"
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local cmd_name cmd_desc cmd_cmd
|
||||
cmd_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
echo
|
||||
echo "Name: $cmd_name"
|
||||
echo
|
||||
echo "Description:"
|
||||
if [ -n "$cmd_desc" ]; then
|
||||
echo "$cmd_desc"
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo
|
||||
echo "Command:"
|
||||
echo
|
||||
echo "$cmd_cmd"
|
||||
local params
|
||||
params="$(_extract_params "$cmd_cmd")"
|
||||
if [ -n "$params" ]; then
|
||||
echo
|
||||
echo "Parameters:"
|
||||
echo "$params" | while IFS= read -r p; do
|
||||
echo " $p"
|
||||
done
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# ── cmd_run ──────────────────────────────────────────────────────
|
||||
cmd_run() {
|
||||
local name=""
|
||||
local -a cli_params=()
|
||||
if [ $# -eq 0 ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
else
|
||||
name="$1"; shift
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
*=*) cli_params+=("$1") ;;
|
||||
*) err "Invalid parameter format: $1 (expected key=value)" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local cmd_name cmd_desc cmd_cmd
|
||||
cmd_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
# Detect parameters
|
||||
local params_str
|
||||
params_str="$(_extract_params "$cmd_cmd")"
|
||||
local -a params=()
|
||||
if [ -n "$params_str" ]; then
|
||||
while IFS= read -r p; do
|
||||
params+=("$p")
|
||||
done <<< "$params_str"
|
||||
fi
|
||||
# Build parameter values from CLI args
|
||||
local -A param_values=()
|
||||
for p in "${cli_params[@]}"; do
|
||||
local key="${p%%=*}" val="${p#*=}"
|
||||
param_values["$key"]="$val"
|
||||
done
|
||||
# Interactively prompt for missing parameters
|
||||
local -a missing=()
|
||||
for p in "${params[@]}"; do
|
||||
[[ -z "${param_values[$p]+x}" ]] && missing+=("$p")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
for p in "${missing[@]}"; do
|
||||
local val
|
||||
val="$(menu_ask_value "$p" "")" || return 0
|
||||
param_values["$p"]="$val"
|
||||
done
|
||||
fi
|
||||
# Substitute parameters
|
||||
local final_cmd="$cmd_cmd"
|
||||
if [ ${#params[@]} -gt 0 ]; then
|
||||
local -a sub_args=()
|
||||
for p in "${params[@]}"; do
|
||||
sub_args+=("${p}=${param_values[$p]:-}")
|
||||
done
|
||||
final_cmd="$(_substitute_params "$cmd_cmd" "${sub_args[@]}")"
|
||||
fi
|
||||
echo
|
||||
log "Running: $final_cmd"
|
||||
echo
|
||||
eval "$final_cmd"
|
||||
return $?
|
||||
}
|
||||
|
||||
# ── cmd_edit ─────────────────────────────────────────────────────
|
||||
cmd_edit() {
|
||||
local name="${1:-}"
|
||||
if [ -z "$name" ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local old_name old_desc old_cmd
|
||||
old_name="${BANK_NAMES[$BANK_IDX]}"
|
||||
old_desc="${BANK_DESCS[$BANK_IDX]}"
|
||||
old_cmd="${BANK_CMDS[$BANK_IDX]}"
|
||||
echo
|
||||
log "Editing: $old_name"
|
||||
echo
|
||||
local new_name new_desc new_cmd
|
||||
new_name="$(menu_ask_value "New name" "$old_name")" || return 0
|
||||
new_desc="$(menu_ask_value "New description" "$old_desc")" || return 0
|
||||
new_cmd="$(menu_read_value "New command")" || return 0
|
||||
[ -z "$new_name" ] && new_name="$old_name"
|
||||
[ -z "$new_desc" ] && new_desc="$old_desc"
|
||||
[ -z "$new_cmd" ] && new_cmd="$old_cmd"
|
||||
if [ "$new_name" != "$old_name" ]; then
|
||||
bank_valid_name "$new_name" || err "Invalid name: $new_name"
|
||||
local i found=0
|
||||
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||
[[ "${BANK_NAMES[$i]}" == "$new_name" ]] && found=1
|
||||
done
|
||||
[[ "$found" -eq 1 ]] && err "A command named '$new_name' already exists"
|
||||
bank_remove "$old_name"
|
||||
bank_add "$new_name" "$new_desc" "$new_cmd"
|
||||
else
|
||||
bank_update "$new_name" "$new_desc" "$new_cmd"
|
||||
fi
|
||||
ok "Updated: $new_name"
|
||||
}
|
||||
|
||||
# ── cmd_remove ───────────────────────────────────────────────────
|
||||
cmd_remove() {
|
||||
local name="${1:-}"
|
||||
if [ -z "$name" ]; then
|
||||
name="$(_pick_command)" || return 0
|
||||
fi
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
if [ -t 0 ]; then
|
||||
confirm "Remove '$name'?" n || { log "Cancelled"; return 0; }
|
||||
fi
|
||||
bank_remove "$name"
|
||||
# Drop bash aliases pointing at the removed command (best-effort upkeep;
|
||||
# a malformed block is a warning, never a remove blocker).
|
||||
if [ -f "$BASH_RC_FILE" ] && grep -qF "$ALIAS_START" "$BASH_RC_FILE"; then
|
||||
if alias_block_parse; then
|
||||
local -a keep=() i
|
||||
local removed_alias=0
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
if [[ "${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}" == "$name" ]]; then
|
||||
removed_alias=1
|
||||
else
|
||||
keep+=("${ALIAS_NAMES[$i]}")
|
||||
fi
|
||||
done
|
||||
if [ "$removed_alias" -eq 1 ]; then
|
||||
ALIAS_NAMES=("${keep[@]}")
|
||||
alias_block_write
|
||||
log "Removed alias pointing to '$name' from $BASH_RC_FILE"
|
||||
fi
|
||||
else
|
||||
local rc=$?
|
||||
[ "$rc" -eq 2 ] && warn "Alias block in $BASH_RC_FILE is malformed — left untouched"
|
||||
fi
|
||||
fi
|
||||
ok "Removed: $name"
|
||||
}
|
||||
|
||||
# ── Alias management ─────────────────────────────────────────────
|
||||
# Bash aliases for bank commands live in a single managed block inside
|
||||
# $BASH_RC_FILE (~/.bashrc by default), so they survive rebuilds and are easy
|
||||
# to see/undo by hand. Every line has the exact shape:
|
||||
# alias <alias_name>='pos system bank run <bank_name>'
|
||||
# The block is written atomically (tmp + mv, preserving file permissions).
|
||||
ALIAS_START='# >>> pos bank aliases (managed by pos system bank — do not hand-edit) <<<'
|
||||
ALIAS_END='# <<< pos bank aliases (managed by pos system bank) <<<'
|
||||
BASH_RC_FILE="${BASH_RC_FILE:-$HOME/.bashrc}"
|
||||
|
||||
# Valid bash alias names we manage: letters/digits/_/- (same rule as bank names).
|
||||
alias_valid_name() {
|
||||
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
|
||||
}
|
||||
|
||||
# Parse the managed block from $BASH_RC_FILE into:
|
||||
# ALIAS_NAMES — ordered array of alias names (file order, unique)
|
||||
# ALIAS_TARGETS — assoc: alias_name → bank command name
|
||||
# Returns: 0 = healthy block present, 1 = no block, 2 = malformed block.
|
||||
alias_block_parse() {
|
||||
declare -g -a ALIAS_NAMES
|
||||
declare -g -A ALIAS_TARGETS
|
||||
ALIAS_NAMES=()
|
||||
ALIAS_TARGETS=()
|
||||
[ -f "$BASH_RC_FILE" ] || return 1
|
||||
local line in_block=0 starts=0 ends=0 rc=0
|
||||
while IFS= read -r line; do
|
||||
if [ "$in_block" -eq 0 ]; then
|
||||
if [[ "$line" == "$ALIAS_START" ]]; then
|
||||
in_block=1
|
||||
starts=$((starts + 1))
|
||||
[ "$starts" -gt 1 ] && rc=2
|
||||
elif [[ "$line" == "$ALIAS_END" ]]; then
|
||||
rc=2
|
||||
fi
|
||||
elif [[ "$line" == "$ALIAS_END" ]]; then
|
||||
in_block=0
|
||||
ends=$((ends + 1))
|
||||
elif [[ "$line" =~ ^alias[[:space:]]+([a-zA-Z][a-zA-Z0-9_-]*)= ]]; then
|
||||
# NOTE: no `'` inside the =~ regex (bash treats quotes there as
|
||||
# quoting operators and would literalize the ( ) groups) — the
|
||||
# quoted payload is matched with a glob `case` instead.
|
||||
local a="${BASH_REMATCH[1]}"
|
||||
local payload="${line#*=}"
|
||||
case "$payload" in
|
||||
"'pos system bank run "[a-zA-Z][a-zA-Z0-9_-]*"'")
|
||||
local t="${payload%\'}"
|
||||
t="${t#\'pos system bank run }"
|
||||
if [[ " ${ALIAS_NAMES[*]:-} " == *" $a "* ]]; then
|
||||
rc=2
|
||||
continue
|
||||
fi
|
||||
ALIAS_NAMES+=("$a")
|
||||
ALIAS_TARGETS["$a"]="$t"
|
||||
;;
|
||||
*) rc=2 ;;
|
||||
esac
|
||||
else
|
||||
rc=2
|
||||
fi
|
||||
done < "$BASH_RC_FILE"
|
||||
[ "$in_block" -eq 1 ] && rc=2
|
||||
[ "$rc" -eq 0 ] && [ "$starts" -eq 0 ] && return 1
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Require a healthy managed block (alias commands err on missing/malformed).
|
||||
alias_block_require() {
|
||||
local rc
|
||||
if alias_block_parse; then
|
||||
return 0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
if [ "$rc" -eq 1 ]; then
|
||||
err "No alias block in $BASH_RC_FILE — add one with: pos system bank alias <name>"
|
||||
fi
|
||||
err "Alias block in $BASH_RC_FILE is malformed (markers or lines were hand-edited) — fix it manually, then retry"
|
||||
}
|
||||
|
||||
# Scan for an alias NAME defined OUTSIDE the managed block (a collision we
|
||||
# refuse to overwrite). Prints the offending line; rc 0 = found, 1 = clean.
|
||||
alias_collision() {
|
||||
local name="$1"
|
||||
[ -f "$BASH_RC_FILE" ] || return 1
|
||||
local line in_block=0
|
||||
while IFS= read -r line; do
|
||||
if [ "$in_block" -eq 0 ] && [[ "$line" == "$ALIAS_START" ]]; then
|
||||
in_block=1
|
||||
continue
|
||||
fi
|
||||
if [ "$in_block" -eq 1 ] && [[ "$line" == "$ALIAS_END" ]]; then
|
||||
in_block=0
|
||||
continue
|
||||
fi
|
||||
if [ "$in_block" -eq 0 ] && [[ "$line" =~ ^[[:space:]]*alias[[:space:]]+$name= ]]; then
|
||||
printf '%s\n' "$line"
|
||||
return 0
|
||||
fi
|
||||
done < "$BASH_RC_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Rewrite $BASH_RC_FILE from ALIAS_NAMES + ALIAS_TARGETS. Callers guarantee the
|
||||
# file is absent or has at most one healthy block. Removes the block entirely
|
||||
# when ALIAS_NAMES is empty; appends a fresh block when none exists yet.
|
||||
alias_block_write() {
|
||||
local content="" start_line=0 end_line=0 n=0 line
|
||||
local pre="" post="" dir perms tmp new_content=""
|
||||
if [ -f "$BASH_RC_FILE" ]; then
|
||||
content="$(< "$BASH_RC_FILE")"
|
||||
perms="$(stat -c %a "$BASH_RC_FILE" 2>/dev/null || printf '600')"
|
||||
while IFS= read -r line; do
|
||||
n=$((n + 1))
|
||||
if [ "$start_line" -eq 0 ] && [[ "$line" == "$ALIAS_START" ]]; then
|
||||
start_line=$n
|
||||
elif [ "$start_line" -gt 0 ] && [ "$end_line" -eq 0 ] && [[ "$line" == "$ALIAS_END" ]]; then
|
||||
end_line=$n
|
||||
fi
|
||||
done <<< "$content"
|
||||
if [ "$start_line" -gt 0 ] && [ "$end_line" -eq 0 ]; then
|
||||
err "Alias block in $BASH_RC_FILE has no end marker — fix it manually, then retry"
|
||||
fi
|
||||
else
|
||||
perms=600
|
||||
fi
|
||||
# No block and nothing to manage → never touch the file (nor create it).
|
||||
if [ "$start_line" -eq 0 ] && [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$start_line" -gt 0 ]; then
|
||||
# NOTE: sed "1,0p" is NOT empty in GNU sed (0 is a last-line sentinel,
|
||||
# it prints line 1) — guard the ranges so edge blocks rewrite cleanly.
|
||||
if [ "$start_line" -gt 1 ]; then
|
||||
pre="$(printf '%s\n' "$content" | sed -n "1,$((start_line - 1))p")"
|
||||
fi
|
||||
if [ "$end_line" -lt "$n" ]; then
|
||||
post="$(printf '%s\n' "$content" | sed -n "$((end_line + 1)),\$p")"
|
||||
fi
|
||||
else
|
||||
pre="$(printf '%s\n' "$content")"
|
||||
fi
|
||||
if [ -n "$pre" ]; then
|
||||
new_content+="$pre"
|
||||
case "$new_content" in
|
||||
*$'\n') ;;
|
||||
*) new_content+=$'\n' ;;
|
||||
esac
|
||||
fi
|
||||
if [ "${#ALIAS_NAMES[@]}" -gt 0 ]; then
|
||||
new_content+="$ALIAS_START"$'\n'
|
||||
local i
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
new_content+="alias ${ALIAS_NAMES[$i]}='pos system bank run ${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}'"$'\n'
|
||||
done
|
||||
new_content+="$ALIAS_END"$'\n'
|
||||
fi
|
||||
if [ -n "$post" ]; then
|
||||
new_content+="$post"
|
||||
case "$new_content" in
|
||||
*$'\n') ;;
|
||||
*) new_content+=$'\n' ;;
|
||||
esac
|
||||
fi
|
||||
dir="$(dirname "$BASH_RC_FILE")"
|
||||
mkdir -p "$dir" || err "Cannot create directory: $dir"
|
||||
tmp="$(mktemp "${dir}/.pos-bank-aliases.XXXXXX")" || err "Cannot create temp file in $dir"
|
||||
printf '%s' "$new_content" > "$tmp"
|
||||
chmod "$perms" "$tmp"
|
||||
mv "$tmp" "$BASH_RC_FILE"
|
||||
}
|
||||
|
||||
# ── cmd_alias ────────────────────────────────────────────────────
|
||||
cmd_alias() {
|
||||
case "${1:-}" in
|
||||
list) shift; cmd_alias_list "$@" ;;
|
||||
remove) shift; cmd_alias_remove "$@" ;;
|
||||
"") err "Usage: pos system bank alias <name> [alias_name] | list | remove <alias_name>" ;;
|
||||
*) cmd_alias_add "$@" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_alias_add() {
|
||||
local name="${1:-}" alias_name="${2:-$1}"
|
||||
[ -n "$name" ] || err "Usage: pos system bank alias <name> [alias_name]"
|
||||
bank_valid_name "$name" || err "Invalid command name: $name"
|
||||
alias_valid_name "$alias_name" || err "Invalid alias name: $alias_name (use letters, numbers, hyphens, underscores)"
|
||||
bank_load
|
||||
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
|
||||
local rc
|
||||
if alias_block_parse; then
|
||||
rc=0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
[ "$rc" -eq 2 ] && err "Alias block in $BASH_RC_FILE is malformed (hand-edited?) — fix it manually, then retry"
|
||||
if [ -f "$BASH_RC_FILE" ] && hit="$(alias_collision "$alias_name")"; then
|
||||
err "Alias '$alias_name' is already defined outside the managed block in $BASH_RC_FILE: $hit"
|
||||
fi
|
||||
if command -v "$alias_name" >/dev/null 2>&1; then
|
||||
warn "Note: '$alias_name' is also a command on PATH — the alias will shadow it in interactive shells"
|
||||
fi
|
||||
local found=0 i
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
[[ "${ALIAS_NAMES[$i]}" == "$alias_name" ]] && found=1
|
||||
done
|
||||
[ "$found" -eq 0 ] && ALIAS_NAMES+=("$alias_name")
|
||||
ALIAS_TARGETS["$alias_name"]="$name"
|
||||
alias_block_write
|
||||
ok "Alias '$alias_name' → pos system bank run $name (in $BASH_RC_FILE)"
|
||||
log "Run: source $BASH_RC_FILE (or open a new shell)"
|
||||
}
|
||||
|
||||
cmd_alias_list() {
|
||||
alias_block_require
|
||||
if [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
|
||||
log "No aliases in $BASH_RC_FILE — add one with: pos system bank alias <name>"
|
||||
return 0
|
||||
fi
|
||||
echo
|
||||
echo "BANK ALIASES (in $BASH_RC_FILE)"
|
||||
echo
|
||||
local i
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
printf ' %-25s → pos system bank run %s\n' "${ALIAS_NAMES[$i]}" "${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# ── Interactive alias picker ─────────────────────────────────────
|
||||
_pick_alias() {
|
||||
local rc
|
||||
if alias_block_parse; then
|
||||
rc=0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
[ "$rc" -eq 2 ] && err "Alias block in $BASH_RC_FILE is malformed — fix it manually"
|
||||
[ "$rc" -eq 1 ] && err "No alias block in $BASH_RC_FILE — add one with: pos system bank alias <name>"
|
||||
if [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
|
||||
warn "No aliases yet — add one first"
|
||||
return 1
|
||||
fi
|
||||
local -a items=()
|
||||
local i
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
items+=("${ALIAS_NAMES[$i]} → pos system bank run ${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}")
|
||||
done
|
||||
local choice
|
||||
choice="$(menu_pick "Pick alias" "${items[@]}")" || return 1
|
||||
echo "${ALIAS_NAMES[$((choice - 1))]}"
|
||||
}
|
||||
|
||||
cmd_alias_remove() {
|
||||
local alias_name="${1:-}"
|
||||
if [ -z "$alias_name" ]; then
|
||||
if [ -t 0 ]; then
|
||||
alias_name="$(_pick_alias)" || return 0
|
||||
else
|
||||
err "Usage: pos system bank alias remove <alias_name>"
|
||||
fi
|
||||
fi
|
||||
alias_block_require
|
||||
local found=0 i
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
[[ "${ALIAS_NAMES[$i]}" == "$alias_name" ]] && found=1
|
||||
done
|
||||
[ "$found" -eq 1 ] || err "Alias '$alias_name' not found (see: pos system bank alias list)"
|
||||
if [ -t 0 ]; then
|
||||
confirm "Remove alias '$alias_name'?" n || { log "Cancelled"; return 0; }
|
||||
fi
|
||||
local -a keep=()
|
||||
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
|
||||
[[ "${ALIAS_NAMES[$i]}" != "$alias_name" ]] && keep+=("${ALIAS_NAMES[$i]}")
|
||||
done
|
||||
ALIAS_NAMES=("${keep[@]}")
|
||||
unset "ALIAS_TARGETS[$alias_name]"
|
||||
alias_block_write
|
||||
ok "Removed alias '$alias_name' from $BASH_RC_FILE"
|
||||
log "Run: source $BASH_RC_FILE (or open a new shell)"
|
||||
}
|
||||
|
||||
alias_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice="$(menu_run "Bank Aliases" \
|
||||
"List aliases" \
|
||||
"Add alias" \
|
||||
"Remove alias")" || return 0
|
||||
case "$choice" in
|
||||
1) cmd_alias_list ;;
|
||||
2)
|
||||
local name alias_name
|
||||
name="$(_pick_command)" || return 0
|
||||
alias_name="$(menu_ask_value "Alias name" "$name")" || return 0
|
||||
[ -z "$alias_name" ] && alias_name="$name"
|
||||
cmd_alias_add "$name" "$alias_name"
|
||||
;;
|
||||
3)
|
||||
local alias_name
|
||||
alias_name="$(_pick_alias)" || return 0
|
||||
cmd_alias_remove "$alias_name"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Interactive menu ─────────────────────────────────────────────
|
||||
run_menu() {
|
||||
menu_guard || exit 1
|
||||
while true; do
|
||||
local choice
|
||||
choice="$(menu_run "Command Bank" \
|
||||
"List commands" \
|
||||
"Add command" \
|
||||
"Run command" \
|
||||
"Edit command" \
|
||||
"Remove command" \
|
||||
"Manage aliases")" || return 0
|
||||
case "$choice" in
|
||||
1) cmd_list ;;
|
||||
2) cmd_add ;;
|
||||
3) cmd_run ;;
|
||||
4) cmd_edit ;;
|
||||
5) cmd_remove ;;
|
||||
6) alias_menu ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────
|
||||
if [ "${1:-}" = "menu" ]; then
|
||||
run_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $# -eq 0 ] && [ -t 0 ]; then
|
||||
run_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
list) shift; cmd_list "$@" ;;
|
||||
add) shift; cmd_add "$@" ;;
|
||||
show) shift; cmd_show "$@" ;;
|
||||
run) shift; cmd_run "$@" ;;
|
||||
edit) shift; cmd_edit "$@" ;;
|
||||
remove) shift; cmd_remove "$@" ;;
|
||||
alias) shift; cmd_alias "$@" ;;
|
||||
"") usage ;;
|
||||
*) err "Unknown subcommand: $1 (see --help)" ;;
|
||||
esac
|
||||
@@ -49,11 +49,10 @@ _pos_subcmds[share-nfs-client]="mount unmount list persist unpersist menu"
|
||||
_pos_subcmds[share-nfs-server]="status share unshare list reload enable disable menu"
|
||||
_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-alias]="create edit remove list show"
|
||||
_pos_subcmds[system-backup]="menu"
|
||||
_pos_subcmds[system-bank]="list add show run edit remove alias"
|
||||
_pos_subcmds[system-schedule]="run list config enable disable status migrate menu"
|
||||
_pos_subcmds[ai]="ask chat sessions capture models providers llamacpp alias gemini hf openrouter server"
|
||||
_pos_subcmds[bank]="list add show run edit remove"
|
||||
# GEN:END possubcmds
|
||||
# GEN:START posconfigscopes
|
||||
declare -a _pos_config_scopes=(ai compose entertainment grab matrix notify scrcpy system telegram ytsync)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/bank-lib.sh — shared storage helpers for the Command Bank.
|
||||
# Sourced by bin/pos-bank. Uses err() from lib/common.sh.
|
||||
# Sourced by bin/pos-system-bank. Uses err() from lib/common.sh.
|
||||
#
|
||||
# Storage: ~/.config/linux_post_install/bank.env
|
||||
# Format: name|description|command
|
||||
@@ -11,7 +11,7 @@
|
||||
# * Defines ONLY bank_* functions — sourcing never clobbers a tool's helpers.
|
||||
# * Requires common.sh to be sourced by the CALLER.
|
||||
# * NEVER exits — return codes only.
|
||||
# * Performs NO interactive prompts (those stay in bin/pos-bank).
|
||||
# * Performs NO interactive prompts (those stay in bin/pos-system-bank).
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
BANK_FILE="${BANK_FILE:-${CONFIG_DIR}/bank.env}"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Failure: telegram-listener exit-254 crash loop (LattePanda, 2026-09-12)
|
||||
|
||||
**Root cause:** No webcam device on host (`/dev/video*` absent). The `/capture` command chain
|
||||
(`ffmpeg -f v4l2 -i /dev/video0 ...` from telegram_commands.env) fails fast with exit 254.
|
||||
The listener (bin/pos-communication-telegram-listener) spawns that chain as a bg child; its
|
||||
SIGCHLD trap `while _p=$(wait -n 2>/dev/null); do _EXIT_CODES[$_p]=$?; done` only stores exits
|
||||
when `wait -n` returns 0 (non-zero exit → while-condition false → body skipped → pid consumed
|
||||
but never recorded). `reap_commands()` then falls into `wait "$pid" 2>/dev/null; rc=$?`;
|
||||
a failing `wait` under `set -euo pipefail` kills the whole shell **before** `rc=$?` runs
|
||||
(`set -e` does NOT exempt a failing simple command followed by `;`). Verified: raw ffmpeg→254,
|
||||
exact chain→254, trap_full.sh (byte-identical trap/reap + real ffmpeg child)→EXIT=254.
|
||||
|
||||
**Loop persistence:** Restart=always + RestartSec=5 + every instance starts `local offset=0`
|
||||
and dies before confirming the previous getUpdates → same pending updates re-fetch and
|
||||
re-execute on every restart → /capture fails 254 again → infinite crash loop (NRestarts 80+
|
||||
in ~1h). Journal bursts byte-identical across instances (160 execs/16 starts ~10 min).
|
||||
|
||||
**Prevention:**
|
||||
1. reap fallback must tolerate non-zero child exits: `rc=0; wait "$pid" 2>/dev/null || rc=$?`
|
||||
(or capture inside if/||). Never `wait "$pid"; rc=$?` under set -e.
|
||||
2. CHLD trap should record non-zero exits: `while _p=$(wait -n 2>/dev/null); do ...; done`
|
||||
cannot observe a failing exit — use `wait -n; _p=$?`-safe pattern or `|| true` + $? capture.
|
||||
3. Command map entries should pre-check devices (`[ -e /dev/video0 ] || error`) so a
|
||||
permanently-broken command cannot crash the daemon.
|
||||
4. Persist the getUpdates offset (or confirm after processing) so replays stop on restart.
|
||||
5. Diagnose "slow bot" by checking `systemctl --user show ... -p NRestarts` / journal
|
||||
`status=254` FIRST — crash-loop downtime beats processing speed by orders of magnitude.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Lesson: `set -e` + `wait "$pid"; rc=$?` kills the shell on non-zero child exit
|
||||
|
||||
**Symptom:** a daemon loop crash-looping with status = a background child's exit code
|
||||
(set -euo pipefail shell).
|
||||
|
||||
**Debugging approach that worked:**
|
||||
1. Before theorizing, reproduce the failing command chain in isolation AND check the exact
|
||||
exit code of each link (ffmpeg on absent device returned 254 — not the "expected" 231
|
||||
from /dev/null!).
|
||||
2. Bisect the propagation path: raw command → chain wrapper → bg child spawn → CHLD trap
|
||||
reaping → reap_commands wait fallback. Using `bash -x` on a byte-identical reduced
|
||||
script (trap_full.sh) pinpointed the death at `+ wait <pid>`.
|
||||
3. Baseline shell semantics test: `bash -c 'set -e; false; echo REACHED'` → exits without
|
||||
REACHED. Proves `;` does NOT shield a failing simple command from errexit. Many devs
|
||||
wrongly assume `cmd; rc=$?` is always safe — it is NOT under `set -e`.
|
||||
4. Bash `wait -n` in a while-condition ALSO conceals non-zero child exits (loop body
|
||||
skipped) → a "reaping" trap silently loses the failure info → downstream fallbacks
|
||||
go to the dangerous path.
|
||||
|
||||
**Key insight:** with `set -euo pipefail`, ANY background failure can become a main-shell
|
||||
death if reaped via `wait "$pid"` in a plain command list. The safe capture is
|
||||
`rc=0; wait "$pid" 2>/dev/null || rc=$?` (errexit suppressed by `||`), or run reaping
|
||||
inside `if wait ...; then ... else rc=$?; fi`.
|
||||
+2
-1
@@ -57,4 +57,5 @@ silently.
|
||||
| `t-share-mountpoint.sh` | share-client `ask_mountpoint` UX: existing/new/declined/rejected paths, confirm gate, mkdir side effects, non-TTY stdin contract, static `n`→`t` guards |
|
||||
| `t-pos-media-yt.sh` | unified `pos media yt` suite: dispatcher + forwarder resolution, shared yt-lib helpers, yt-mp3/mp4/grab/subtitles flags, dry-run deps, `YT_OUT_DIR` seam, `GRAB_DEFAULT` config, negative controls (unsafe-URL no-expansion, `--lang en,ar` single arg, txt timestamp-stripping) |
|
||||
| `t-telegram-listener-singleton.sh` | Telegram listener single-instance guard: first `--run` acquires the flock, second `--run` fails fast with the exact message, lock auto-releases so the next start is clean, `--status` reports the lock state |
|
||||
| `t-bank.sh` | Command Bank: bank-lib.sh unit tests (add/remove/update/find/get/list/count/valid/extract_params/substitute_params, multiline `\n` storage round-trip, literal-`\n` escape round-trip, v1 backward compat) + pos-bank CLI integration (help, list, add, show, run, remove, params, invalid name, multiline show/run) |
|
||||
| `t-telegram-listener-reap.sh` | Telegram listener crash-loop regression: non-zero (254) child exit no longer kills the daemon, reply carries the real exit code + output, negative control proves the old `wait`-under-`set -e` idiom dies, getUpdates offset persists across restarts (resume, invalid-state fallback, empty-batch no-write) |
|
||||
| `t-bank.sh` | Command Bank: bank-lib.sh unit tests (add/remove/update/find/get/list/count/valid/extract_params/substitute_params, multiline `\n` storage round-trip, literal-`\n` escape round-trip, v1 backward compat) + pos system bank CLI integration (help, list, add, show, run, remove, params, invalid name, multiline show/run) + `alias` subcommand (managed `~/.bashrc` block, exact line format, idempotent create, retarget, list, remove, block cleaned when empty, outer collision refused w/ file untouched, PATH-shadow warn, invalid alias name, unknown command, `bank remove` drops aliases, unrelated bashrc content preserved byte-identically, malformed block, empty-block message) |
|
||||
+256
-70
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# t-bank.sh — Command Bank feature: bank-lib.sh unit tests + pos-bank CLI
|
||||
# t-bank.sh — Command Bank feature: bank-lib.sh unit tests + pos system bank CLI
|
||||
# integration tests.
|
||||
#
|
||||
# Storage contract: pipe-delimited name|description|command in $BANK_FILE.
|
||||
@@ -233,7 +233,7 @@ HELPER
|
||||
(
|
||||
BANK_FILE="$sandbox/a18.bank.env"
|
||||
cat > "$BANK_FILE" <<'BANK'
|
||||
# Command Bank — managed by pos bank (do not hand-edit)
|
||||
# Command Bank — managed by pos system bank (do not hand-edit)
|
||||
# Format: name|description|command
|
||||
|
||||
real|Real command|echo real
|
||||
@@ -295,7 +295,7 @@ SCRIPT
|
||||
(
|
||||
BANK_FILE="$sandbox/a21.bank.env"
|
||||
cat > "$BANK_FILE" <<'BANK'
|
||||
# Command Bank — managed by pos bank (do not hand-edit)
|
||||
# Command Bank — managed by pos system bank (do not hand-edit)
|
||||
# Format: name|description|command
|
||||
raw|Raw echo|echo 'a\b'
|
||||
BANK
|
||||
@@ -311,7 +311,7 @@ BANK
|
||||
(
|
||||
BANK_FILE="$sandbox/a22.bank.env"
|
||||
cat > "$BANK_FILE" <<'BANK'
|
||||
# Command Bank — managed by pos bank (do not hand-edit)
|
||||
# Command Bank — managed by pos system bank (do not hand-edit)
|
||||
# Format: name|description|command
|
||||
ts-google|Tailscale Google|tailscale status --json >/tmp/ts.json && grep -q '"ExitNodeStatus":null' /tmp/ts.json && tailscale set --exit-node=google || tailscale set --exit-node=
|
||||
BANK
|
||||
@@ -328,116 +328,116 @@ BANK
|
||||
|| printf ' FAIL old-format entry round-trips through re-save\n'
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Part B: pos-bank CLI integration tests
|
||||
# Part B: pos system bank CLI integration tests
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
local pos_bank="$ROOT/bin/pos-bank"
|
||||
local pos_bank="$ROOT/bin/pos-system-bank"
|
||||
|
||||
# B1: pos bank --help shows usage
|
||||
# B1: pos system bank --help shows usage
|
||||
test_run env BANK_FILE="$sandbox/b1.bank.env" "$pos_bank" --help
|
||||
check_rc "pos bank --help exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank --help shows Usage" "Usage:" "$TR_OUT"
|
||||
check_contains "pos bank --help mentions subcommands" "Subcommands:" "$TR_OUT"
|
||||
check_rc "pos system bank --help exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank --help shows Usage" "Usage:" "$TR_OUT"
|
||||
check_contains "pos system bank --help mentions subcommands" "Subcommands:" "$TR_OUT"
|
||||
|
||||
# B2: pos bank list on empty bank shows empty message
|
||||
# B2: pos system bank list on empty bank shows empty message
|
||||
: > "$sandbox/b2.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b2.bank.env" "$pos_bank" list
|
||||
check_rc "pos bank list empty exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank list empty message" "Command bank is empty" "$TR_OUT"
|
||||
check_rc "pos system bank list empty exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank list empty message" "Command bank is empty" "$TR_OUT"
|
||||
|
||||
# B3: pos bank add — adds a command, verify in bank.env
|
||||
# B3: pos system bank add — adds a command, verify in bank.env
|
||||
: > "$sandbox/b3.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" add "disk-usage" "Check disk usage" "df -h"
|
||||
check_rc "pos bank add exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank add confirms" "Saved: disk-usage" "$TR_OUT"
|
||||
check_rc "pos system bank add exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank add confirms" "Saved: disk-usage" "$TR_OUT"
|
||||
# Verify the file
|
||||
if grep -q '^disk-usage|Check disk usage|df -h$' "$sandbox/b3.bank.env"; then
|
||||
printf ' PASS pos bank add persists to bank.env\n'
|
||||
printf ' PASS pos system bank add persists to bank.env\n'
|
||||
else
|
||||
printf ' FAIL pos bank add did not persist to bank.env\n'
|
||||
printf ' FAIL pos system bank add did not persist to bank.env\n'
|
||||
fi
|
||||
|
||||
# B4: pos bank add duplicate — fails
|
||||
# B4: pos system bank add duplicate — fails
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" add "disk-usage" "dup" "echo dup"
|
||||
check_not_contains "pos bank add duplicate errors" "0" "$TR_RC"
|
||||
check_contains "pos bank add duplicate message" "already exists" "$TR_OUT"
|
||||
check_not_contains "pos system bank add duplicate errors" "0" "$TR_RC"
|
||||
check_contains "pos system bank add duplicate message" "already exists" "$TR_OUT"
|
||||
|
||||
# B5: pos bank show — shows command details
|
||||
# B5: pos system bank show — shows command details
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" show "disk-usage"
|
||||
check_rc "pos bank show exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank show shows name" "Name: disk-usage" "$TR_OUT"
|
||||
check_contains "pos bank show shows description" "Check disk usage" "$TR_OUT"
|
||||
check_contains "pos bank show shows command" "df -h" "$TR_OUT"
|
||||
check_rc "pos system bank show exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank show shows name" "Name: disk-usage" "$TR_OUT"
|
||||
check_contains "pos system bank show shows description" "Check disk usage" "$TR_OUT"
|
||||
check_contains "pos system bank show shows command" "df -h" "$TR_OUT"
|
||||
|
||||
# B6: pos bank show missing — fails
|
||||
# B6: pos system bank show missing — fails
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" show "nonexistent"
|
||||
check_not_contains "pos bank show missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank show missing message" "not found" "$TR_OUT"
|
||||
check_not_contains "pos system bank show missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank show missing message" "not found" "$TR_OUT"
|
||||
|
||||
# B7: pos bank list after add — shows the entry
|
||||
# B7: pos system bank list after add — shows the entry
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" list
|
||||
check_rc "pos bank list populated exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank list shows header" "COMMAND BANK" "$TR_OUT"
|
||||
check_contains "pos bank list shows entry" "disk-usage" "$TR_OUT"
|
||||
check_rc "pos system bank list populated exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank list shows header" "COMMAND BANK" "$TR_OUT"
|
||||
check_contains "pos system bank list shows entry" "disk-usage" "$TR_OUT"
|
||||
|
||||
# B8: pos bank remove — removes a command
|
||||
# B8: pos system bank remove — removes a command
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" remove "disk-usage"
|
||||
check_rc "pos bank remove exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank remove confirms" "Removed: disk-usage" "$TR_OUT"
|
||||
check_rc "pos system bank remove exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank remove confirms" "Removed: disk-usage" "$TR_OUT"
|
||||
# Verify gone
|
||||
if grep -q '^disk-usage|' "$sandbox/b3.bank.env"; then
|
||||
printf ' FAIL pos bank remove did not delete from bank.env\n'
|
||||
printf ' FAIL pos system bank remove did not delete from bank.env\n'
|
||||
else
|
||||
printf ' PASS pos bank remove deletes from bank.env\n'
|
||||
printf ' PASS pos system bank remove deletes from bank.env\n'
|
||||
fi
|
||||
|
||||
# B9: pos bank remove missing — fails
|
||||
# B9: pos system bank remove missing — fails
|
||||
: > "$sandbox/b9.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b9.bank.env" "$pos_bank" remove "ghost"
|
||||
check_not_contains "pos bank remove missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank remove missing message" "not found" "$TR_OUT"
|
||||
check_not_contains "pos system bank remove missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank remove missing message" "not found" "$TR_OUT"
|
||||
|
||||
# B10: pos bank add with parameters — show lists them
|
||||
# B10: pos system bank add with parameters — show lists them
|
||||
: > "$sandbox/b10.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b10.bank.env" "$pos_bank" add "convert" "Convert video" "ffmpeg -i {input} -q:v {quality} {output}"
|
||||
check_rc "pos bank add with params exits 0" 0 "$TR_RC"
|
||||
check_rc "pos system bank add with params exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b10.bank.env" "$pos_bank" show "convert"
|
||||
check_contains "show lists input param" "input" "$TR_OUT"
|
||||
check_contains "show lists quality param" "quality" "$TR_OUT"
|
||||
check_contains "show lists output param" "output" "$TR_OUT"
|
||||
|
||||
# B11: pos bank add invalid name — fails
|
||||
# B11: pos system bank add invalid name — fails
|
||||
: > "$sandbox/b11.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b11.bank.env" "$pos_bank" add "1bad" "desc" "cmd"
|
||||
check_not_contains "pos bank add invalid name exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank add invalid name message" "Invalid name" "$TR_OUT"
|
||||
check_not_contains "pos system bank add invalid name exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank add invalid name message" "Invalid name" "$TR_OUT"
|
||||
|
||||
# B12: pos bank run — executes a saved command (no params)
|
||||
# B12: pos system bank run — executes a saved command (no params)
|
||||
# Negative control: the old `local name="" -a cli_params=()` declaration
|
||||
# crashed at line 150 before any output (rc != 0, no "Running:" line).
|
||||
: > "$sandbox/b12.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b12.bank.env" "$pos_bank" add "greet" "Greet" "echo bank-run-ok"
|
||||
check_rc "pos bank add for run exits 0" 0 "$TR_RC"
|
||||
check_rc "pos system bank add for run exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b12.bank.env" "$pos_bank" run "greet"
|
||||
check_rc "pos bank run executes saved command" 0 "$TR_RC"
|
||||
check_contains "pos bank run logs the command" "Running: echo bank-run-ok" "$TR_OUT"
|
||||
check_contains "pos bank run executes output" "bank-run-ok" "$TR_OUT"
|
||||
check_rc "pos system bank run executes saved command" 0 "$TR_RC"
|
||||
check_contains "pos system bank run logs the command" "Running: echo bank-run-ok" "$TR_OUT"
|
||||
check_contains "pos system bank run executes output" "bank-run-ok" "$TR_OUT"
|
||||
|
||||
# B13: pos bank run missing command — fails
|
||||
# B13: pos system bank run missing command — fails
|
||||
test_run env BANK_FILE="$sandbox/b12.bank.env" "$pos_bank" run "ghost"
|
||||
check_not_contains "pos bank run missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank run missing message" "Command not found" "$TR_OUT"
|
||||
check_not_contains "pos system bank run missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank run missing message" "Command not found" "$TR_OUT"
|
||||
|
||||
# B14: pos bank run with params — CLI key=val substitution, no interactive prompt
|
||||
# B14: pos system bank run with params — CLI key=val substitution, no interactive prompt
|
||||
: > "$sandbox/b14.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b14.bank.env" "$pos_bank" add "echo-param" "Echo param" "echo hi {who}"
|
||||
check_rc "pos bank add param exits 0" 0 "$TR_RC"
|
||||
check_rc "pos system bank add param exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b14.bank.env" "$pos_bank" run "echo-param" "who=there"
|
||||
check_rc "pos bank run with params exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank run substitutes param" 'Running: echo hi "there"' "$TR_OUT"
|
||||
check_contains "pos bank run executes substituted command" "hi there" "$TR_OUT"
|
||||
check_rc "pos system bank run with params exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank run substitutes param" 'Running: echo hi "there"' "$TR_OUT"
|
||||
check_contains "pos system bank run executes substituted command" "hi there" "$TR_OUT"
|
||||
|
||||
# B15: pos bank add multiline + show — cmd_show retrieves the FULL script via arrays
|
||||
# B15: pos system bank add multiline + show — cmd_show retrieves the FULL script via arrays
|
||||
# Negative control: bank_get + cut -f3 truncated the command at the first newline.
|
||||
# (Brace-free script: {param} template detection would prompt on run in a non-TTY.)
|
||||
: > "$sandbox/b15.bank.env"
|
||||
@@ -451,17 +451,203 @@ done
|
||||
SCRIPT
|
||||
)"
|
||||
test_run env BANK_FILE="$sandbox/b15.bank.env" "$pos_bank" add "ml-demo" "Multiline demo" "$ml_script"
|
||||
check_rc "pos bank add multiline exits 0" 0 "$TR_RC"
|
||||
check_rc "pos system bank add multiline exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b15.bank.env" "$pos_bank" show "ml-demo"
|
||||
check_rc "pos bank show multiline exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank show prints script shebang" '#!/usr/bin/env bash' "$TR_OUT"
|
||||
check_contains "pos bank show prints loop line" 'while [ "$n" -lt 2 ]; do' "$TR_OUT"
|
||||
check_contains "pos bank show prints arithmetic line" 'n=$((n + 1))' "$TR_OUT"
|
||||
check_contains "pos bank show prints substitution echo" 'echo "round $n: $(printf' "$TR_OUT"
|
||||
check_rc "pos system bank show multiline exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank show prints script shebang" '#!/usr/bin/env bash' "$TR_OUT"
|
||||
check_contains "pos system bank show prints loop line" 'while [ "$n" -lt 2 ]; do' "$TR_OUT"
|
||||
check_contains "pos system bank show prints arithmetic line" 'n=$((n + 1))' "$TR_OUT"
|
||||
check_contains "pos system bank show prints substitution echo" 'echo "round $n: $(printf' "$TR_OUT"
|
||||
|
||||
# B16: pos bank run multiline — executes the WHOLE script via eval of the full command
|
||||
# B16: pos system bank run multiline — executes the WHOLE script via eval of the full command
|
||||
test_run env BANK_FILE="$sandbox/b15.bank.env" "$pos_bank" run "ml-demo"
|
||||
check_rc "pos bank run multiline exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank run multiline output line 1" "round 1: ok" "$TR_OUT"
|
||||
check_contains "pos bank run multiline output line 2" "round 2: ok" "$TR_OUT"
|
||||
check_rc "pos system bank run multiline exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank run multiline output line 1" "round 1: ok" "$TR_OUT"
|
||||
check_contains "pos system bank run multiline output line 2" "round 2: ok" "$TR_OUT"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Part B2: pos system bank alias — managed ~/.bashrc block
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
local A_START='# >>> pos bank aliases (managed by pos system bank — do not hand-edit) <<<'
|
||||
local A_END='# <<< pos bank aliases (managed by pos system bank) <<<'
|
||||
|
||||
# B17: alias create writes the managed block with the exact line format
|
||||
: > "$sandbox/b17.bank.env"
|
||||
: > "$sandbox/b17.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" add "backup" "Backup" "rsync -a src/ dst/"
|
||||
check_rc "pos system bank add for alias exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" alias "backup" "bk"
|
||||
check_rc "pos system bank alias create exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank alias create confirms" "Alias 'bk'" "$TR_OUT"
|
||||
check_contains "pos system bank alias create hints source" "source $sandbox/b17.bashrc" "$TR_OUT"
|
||||
if grep -qF "alias bk='pos system bank run backup'" "$sandbox/b17.bashrc" \
|
||||
&& grep -qF "$A_START" "$sandbox/b17.bashrc" \
|
||||
&& grep -qF "$A_END" "$sandbox/b17.bashrc"; then
|
||||
printf ' PASS alias create writes managed block with exact line\n'
|
||||
else
|
||||
printf ' FAIL alias create writes managed block with exact line\n'
|
||||
fi
|
||||
|
||||
# B18: alias create is idempotent — same alias name stays a single line
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" alias "backup" "bk"
|
||||
check_rc "pos system bank alias create idempotent exits 0" 0 "$TR_RC"
|
||||
local cnt18
|
||||
cnt18="$(grep -c '^alias bk=' "$sandbox/b17.bashrc" || true)"
|
||||
[ "$cnt18" -eq 1 ] && printf ' PASS alias create stays a single line when repeated\n' \
|
||||
|| printf ' FAIL alias create stays a single line when repeated (count=%s)\n' "$cnt18"
|
||||
|
||||
# B19: alias list — shows the header and the alias row
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" alias list
|
||||
check_rc "pos system bank alias list exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank alias list shows header" "BANK ALIASES" "$TR_OUT"
|
||||
check_contains "pos system bank alias list shows bk row" "bk" "$TR_OUT"
|
||||
check_contains "pos system bank alias list shows target" "pos system bank run backup" "$TR_OUT"
|
||||
|
||||
# B20: alias list without a block errors
|
||||
: > "$sandbox/b20.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b20.bashrc" "$pos_bank" alias list
|
||||
check_not_contains "pos system bank alias list no-block exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias list no-block message" "No alias block" "$TR_OUT"
|
||||
|
||||
# B21: alias for an unknown bank command errors
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" alias "ghost"
|
||||
check_not_contains "pos system bank alias unknown bank exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias unknown bank message" "Command not found" "$TR_OUT"
|
||||
|
||||
# B22: invalid alias name errors
|
||||
test_run env BANK_FILE="$sandbox/b17.bank.env" BASH_RC_FILE="$sandbox/b17.bashrc" "$pos_bank" alias "backup" "1bad"
|
||||
check_not_contains "pos system bank alias invalid name exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias invalid name message" "Invalid alias name" "$TR_OUT"
|
||||
|
||||
# B23: PATH-shadowed alias warns (non-blocking)
|
||||
: > "$sandbox/b23.bank.env"
|
||||
: > "$sandbox/b23.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b23.bank.env" BASH_RC_FILE="$sandbox/b23.bashrc" "$pos_bank" add "cat" "Cat" "cat file"
|
||||
check_rc "pos system bank add path-shadow cmd exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b23.bank.env" BASH_RC_FILE="$sandbox/b23.bashrc" "$pos_bank" alias "cat"
|
||||
check_rc "pos system bank alias path-shadow still exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank alias path-shadow warns" "also a command on PATH" "$TR_OUT"
|
||||
|
||||
# B24: alias colliding with an outer (unmanaged) alias is refused, file untouched
|
||||
: > "$sandbox/b24.bank.env"
|
||||
printf '%s\n' "alias ls='ls --color=auto'" "# my config" > "$sandbox/b24.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b24.bank.env" BASH_RC_FILE="$sandbox/b24.bashrc" "$pos_bank" add "ls" "Ls" "ls -la"
|
||||
check_rc "pos system bank add for collision exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b24.bank.env" BASH_RC_FILE="$sandbox/b24.bashrc" "$pos_bank" alias "ls"
|
||||
check_not_contains "pos system bank alias collision exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias collision message" "already defined outside the managed block" "$TR_OUT"
|
||||
if grep -qF "alias ls='ls --color=auto'" "$sandbox/b24.bashrc" \
|
||||
&& ! grep -qF "$A_START" "$sandbox/b24.bashrc"; then
|
||||
printf ' PASS alias collision leaves bashrc untouched\n'
|
||||
else
|
||||
printf ' FAIL alias collision leaves bashrc untouched\n'
|
||||
fi
|
||||
|
||||
# B25: retarget + multiple aliases — alias name is the key; alias <name> [alias_name]
|
||||
: > "$sandbox/b25.bank.env"
|
||||
: > "$sandbox/b25.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" add "backup" "Backup" "rsync"
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" add "df-x" "Df" "df -h"
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" alias "backup" "bk"
|
||||
check_rc "pos system bank alias first exits 0" 0 "$TR_RC"
|
||||
# retarget: same alias name bk now points at df-x via a different bank name
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" alias "df-x" "bk"
|
||||
check_rc "pos system bank alias retarget exits 0" 0 "$TR_RC"
|
||||
if grep -qF "alias bk='pos system bank run df-x'" "$sandbox/b25.bashrc" \
|
||||
&& ! grep -qF "alias bk='pos system bank run backup'" "$sandbox/b25.bashrc"; then
|
||||
printf ' PASS alias with same alias name retargets the existing line\n'
|
||||
else
|
||||
printf ' FAIL alias with same alias name retargets the existing line\n'
|
||||
fi
|
||||
# a second distinct alias for the same bank command is allowed
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" alias "backup" "bku"
|
||||
check_rc "pos system bank alias second name exits 0" 0 "$TR_RC"
|
||||
local cnt25
|
||||
cnt25="$(grep -c '^alias ' "$sandbox/b25.bashrc" || true)"
|
||||
[ "$cnt25" -eq 2 ] && printf ' PASS two aliases coexist (2 lines)\n' \
|
||||
|| printf ' FAIL two aliases coexist (lines=%s)\n' "$cnt25"
|
||||
|
||||
# B26: alias remove keeps the block when other aliases remain
|
||||
test_run env BANK_FILE="$sandbox/b25.bank.env" BASH_RC_FILE="$sandbox/b25.bashrc" "$pos_bank" alias remove "bk"
|
||||
check_rc "pos system bank alias remove exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank alias remove confirms" "Removed alias 'bk'" "$TR_OUT"
|
||||
if grep -qF "$A_START" "$sandbox/b25.bashrc" && grep -qF "$A_END" "$sandbox/b25.bashrc" \
|
||||
&& ! grep -qF "alias bk=" "$sandbox/b25.bashrc" \
|
||||
&& grep -qF "alias bku=" "$sandbox/b25.bashrc"; then
|
||||
printf ' PASS alias remove keeps block with remaining aliases\n'
|
||||
else
|
||||
printf ' FAIL alias remove keeps block with remaining aliases\n'
|
||||
fi
|
||||
|
||||
# B27: removing the last alias cleans the block from the file entirely
|
||||
: > "$sandbox/b27.bank.env"
|
||||
: > "$sandbox/b27.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b27.bank.env" BASH_RC_FILE="$sandbox/b27.bashrc" "$pos_bank" add "deploy" "Deploy" "deploy.sh"
|
||||
test_run env BANK_FILE="$sandbox/b27.bank.env" BASH_RC_FILE="$sandbox/b27.bashrc" "$pos_bank" alias "deploy" "d"
|
||||
test_run env BANK_FILE="$sandbox/b27.bank.env" BASH_RC_FILE="$sandbox/b27.bashrc" "$pos_bank" alias remove "d"
|
||||
check_rc "pos system bank alias remove last exits 0" 0 "$TR_RC"
|
||||
if ! grep -qF "$A_START" "$sandbox/b27.bashrc" && [ ! -s "$sandbox/b27.bashrc" ]; then
|
||||
printf ' PASS removing last alias cleans block and empties file\n'
|
||||
else
|
||||
printf ' FAIL removing last alias cleans block and empties file\n'
|
||||
fi
|
||||
|
||||
# B28: removing a missing alias errors (block present)
|
||||
: > "$sandbox/b28.bank.env"
|
||||
: > "$sandbox/b28.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b28.bank.env" BASH_RC_FILE="$sandbox/b28.bashrc" "$pos_bank" add "deploy" "Deploy" "deploy.sh"
|
||||
test_run env BANK_FILE="$sandbox/b28.bank.env" BASH_RC_FILE="$sandbox/b28.bashrc" "$pos_bank" alias "deploy" "d"
|
||||
test_run env BANK_FILE="$sandbox/b28.bank.env" BASH_RC_FILE="$sandbox/b28.bashrc" "$pos_bank" alias remove "nope"
|
||||
check_not_contains "pos system bank alias remove missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias remove missing message" "not found" "$TR_OUT"
|
||||
|
||||
# B29: bank remove also drops aliases pointing at the removed command
|
||||
: > "$sandbox/b29.bank.env"
|
||||
: > "$sandbox/b29.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b29.bank.env" BASH_RC_FILE="$sandbox/b29.bashrc" "$pos_bank" add "foo" "Foo" "echo foo"
|
||||
test_run env BANK_FILE="$sandbox/b29.bank.env" BASH_RC_FILE="$sandbox/b29.bashrc" "$pos_bank" alias "foo" "f"
|
||||
test_run env BANK_FILE="$sandbox/b29.bank.env" BASH_RC_FILE="$sandbox/b29.bashrc" "$pos_bank" remove "foo"
|
||||
check_rc "pos system bank remove with alias exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank remove mentions alias cleanup" "Removed alias pointing to 'foo'" "$TR_OUT"
|
||||
check_contains "pos system bank remove confirms" "Removed: foo" "$TR_OUT"
|
||||
if ! grep -qF "alias f=" "$sandbox/b29.bashrc" && ! grep -qF "$A_START" "$sandbox/b29.bashrc"; then
|
||||
printf ' PASS bank remove drops alias and cleans block\n'
|
||||
else
|
||||
printf ' FAIL bank remove drops alias and cleans block\n'
|
||||
fi
|
||||
|
||||
# B30: unrelated bashrc content survives an alias add+remove round-trip byte-identically
|
||||
: > "$sandbox/b30.bank.env"
|
||||
printf '%s\n' "# user config" "export EDITOR=vim" "PATH=/custom:\$PATH" > "$sandbox/b30.orig"
|
||||
cp "$sandbox/b30.orig" "$sandbox/b30.bashrc"
|
||||
test_run env BANK_FILE="$sandbox/b30.bank.env" BASH_RC_FILE="$sandbox/b30.bashrc" "$pos_bank" add "tool" "Tool" "tool-cmd"
|
||||
test_run env BANK_FILE="$sandbox/b30.bank.env" BASH_RC_FILE="$sandbox/b30.bashrc" "$pos_bank" alias "tool" "tl"
|
||||
check_rc "pos system bank alias on custom bashrc exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b30.bank.env" BASH_RC_FILE="$sandbox/b30.bashrc" "$pos_bank" alias remove "tl"
|
||||
check_rc "pos system bank alias remove on custom bashrc exits 0" 0 "$TR_RC"
|
||||
if cmp -s "$sandbox/b30.orig" "$sandbox/b30.bashrc"; then
|
||||
printf ' PASS unrelated bashrc content preserved byte-identically\n'
|
||||
else
|
||||
printf ' FAIL unrelated bashrc content preserved byte-identically\n'
|
||||
fi
|
||||
|
||||
# B31: malformed block (start marker without end) errors instead of rewriting
|
||||
printf '%s\n' "$A_START" "alias broken='pos system bank run junk'" > "$sandbox/b31.bashrc"
|
||||
: > "$sandbox/b31.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b31.bank.env" BASH_RC_FILE="$sandbox/b31.bashrc" "$pos_bank" alias list
|
||||
check_not_contains "pos system bank alias malformed block exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias malformed block message" "malformed" "$TR_OUT"
|
||||
|
||||
# B32: empty block lists cleanly
|
||||
printf '%s\n' "$A_START" "$A_END" > "$sandbox/b32.bashrc"
|
||||
: > "$sandbox/b32.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b32.bank.env" BASH_RC_FILE="$sandbox/b32.bashrc" "$pos_bank" alias list
|
||||
check_rc "pos system bank alias empty block exits 0" 0 "$TR_RC"
|
||||
check_contains "pos system bank alias empty block message" "No aliases" "$TR_OUT"
|
||||
|
||||
# B33: alias remove without a name on a non-tty errors with usage
|
||||
test_run env BANK_FILE="$sandbox/b27.bank.env" BASH_RC_FILE="$sandbox/b27.bashrc" "$pos_bank" alias remove
|
||||
check_not_contains "pos system bank alias remove no-arg exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos system bank alias remove no-arg usage" "pos system bank alias remove" "$TR_OUT"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# t-telegram-listener-reap.sh — regression tests for the listener crash-loop:
|
||||
# (a) a background command exiting non-zero (254) must NOT kill the daemon.
|
||||
# The old code reaped via `wait "$pid"` under `set -e`, so any non-zero
|
||||
# child exit aborted the listener and systemd Restart=always crash-looped
|
||||
# it (offset reset to 0 → duplicate re-delivery). Reply must carry the
|
||||
# real exit code.
|
||||
# (b) getUpdates offset persistence: resumed from the state file across
|
||||
# restarts (no duplicate burst after restart), invalid state falls back
|
||||
# to 0, and empty batches never rewrite the file.
|
||||
# (c) negative control: the OLD trap + `wait "$pid"` idiom still dies on 254
|
||||
# (proves the regression is real and the fix works).
|
||||
# Hermetic: stubbed curl (no network), stubbed systemctl, real jq/flock/timeout.
|
||||
|
||||
run_test() {
|
||||
require_cmd jq "telegram reap" || return 0
|
||||
require_cmd timeout "telegram reap" || return 0
|
||||
|
||||
local sandbox stubs cfg runtime home listener curl_log batch_file
|
||||
sandbox="$(mksandbox telegram-reap)"
|
||||
stubs="$sandbox/stubs"
|
||||
cfg="$sandbox/cfg"
|
||||
runtime="$sandbox/runtime"
|
||||
home="$sandbox/home"
|
||||
listener="$ROOT/bin/pos-communication-telegram-listener"
|
||||
curl_log="$sandbox/curl.log"
|
||||
mkdir -p "$stubs" "$cfg" "$runtime" "$home"
|
||||
: > "$curl_log"
|
||||
|
||||
# ── command map: two commands with non-zero exits, one with output ──
|
||||
cat > "$cfg/telegram_commands.env" <<'MAP'
|
||||
/fail254=exit 254
|
||||
/with_out=echo boom; false
|
||||
MAP
|
||||
: > "$cfg/telegram_prefixes.env"
|
||||
|
||||
# ── stub curl ──
|
||||
# Serve one batch with 2 commands, then empty batches (sleep keeps the
|
||||
# empty-poll loop from spinning while the daemon runs).
|
||||
batch_file="$sandbox/batch.json"
|
||||
cat > "$batch_file" <<'JSON'
|
||||
{"ok":true,"result":[
|
||||
{"update_id":1,"message":{"message_id":10,"from":{"id":123},"chat":{"id":456},"text":"/fail254"}},
|
||||
{"update_id":2,"message":{"message_id":11,"from":{"id":123},"chat":{"id":456},"text":"/with_out"}}
|
||||
]}
|
||||
JSON
|
||||
|
||||
cat > "$stubs/curl" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'curl %s\n' "\$*" >> "$curl_log"
|
||||
for a in "\$@"; do
|
||||
case "\$a" in
|
||||
*getUpdates*)
|
||||
if [ ! -e "$sandbox/reap.served" ]; then
|
||||
touch "$sandbox/reap.served"
|
||||
cat "$batch_file"
|
||||
else
|
||||
sleep 1
|
||||
printf '%s' '{"ok":true,"result":[]}'
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s' '{"ok":true}'
|
||||
STUB
|
||||
chmod +x "$stubs/curl"
|
||||
|
||||
printf '#!/usr/bin/env bash\nexit 1\n' > "$stubs/systemctl"
|
||||
chmod +x "$stubs/systemctl"
|
||||
|
||||
local common=(PATH="$stubs:/usr/bin:/bin" CONFIG_DIR="$cfg"
|
||||
XDG_RUNTIME_DIR="$runtime" HOME="$home"
|
||||
TELEGRAM_BOT_TOKEN=testbot TELEGRAM_CHAT_ID=456 TELEGRAM_OWNER_ID=123)
|
||||
|
||||
# ── (a) non-zero command exit must not kill the daemon ──
|
||||
# rc=124 → `timeout` killed an alive-and-polling daemon; rc=0 → clean exit.
|
||||
# With the old code the daemon itself died with 254, which is caught here.
|
||||
test_run_env "${common[@]}" -- timeout 10 "$listener" --run
|
||||
|
||||
if [ "${TR_RC:-0}" -eq 124 ] || [ "${TR_RC:-0}" -eq 0 ]; then
|
||||
printf ' PASS daemon survived child exit 254 (rc=%s)\n' "${TR_RC}"
|
||||
else
|
||||
printf ' FAIL daemon died with rc=%s (crash-loop regression)\n' "${TR_RC:-?}"
|
||||
fi
|
||||
|
||||
check_contains "/fail254 dispatched" "exec: /fail254" "${TR_OUT:-}"
|
||||
check_contains "/with_out dispatched" "exec: /with_out" "${TR_OUT:-}"
|
||||
|
||||
local curl_content
|
||||
curl_content="$(cat "$curl_log")"
|
||||
# The stub logs curl's RAW argv (--data-urlencode passes text unencoded
|
||||
# to the wire, so the log shows "text=exit 254" with a real space).
|
||||
# /fail254 has no output → reply text is "exit 254\nOK".
|
||||
check_contains "/fail254 reply carries exit 254" "text=exit 254" "$curl_content"
|
||||
# /with_out → "exit 1\nboom" — output preserved alongside the exit code.
|
||||
check_contains "/with_out reply carries exit 1" "text=exit 1" "$curl_content"
|
||||
check_contains "/with_out reply preserves output" "boom" "$curl_content"
|
||||
|
||||
# ── offset persistence: written after processing updates ──
|
||||
# update 1 → offset 2 persisted (both messages advance the offset).
|
||||
check_eq "offset persisted after processing" "3" "$(cat "$cfg/telegram-listener.state")"
|
||||
|
||||
# ── (b) offset resumed from the state file across restarts ──
|
||||
# No state file → offset starts at 0; with one present it resumes from it.
|
||||
local sb2 stubs2 cfg2 curl2
|
||||
sb2="$sandbox/resume"
|
||||
stubs2="$sb2/stubs"
|
||||
cfg2="$sb2/cfg"
|
||||
curl2="$sb2/curl.log"
|
||||
mkdir -p "$stubs2" "$cfg2"
|
||||
: > "$curl2"
|
||||
printf '99\n' > "$cfg2/telegram-listener.state"
|
||||
: > "$cfg2/telegram_commands.env"
|
||||
: > "$cfg2/telegram_prefixes.env"
|
||||
|
||||
cat > "$stubs2/curl" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
printf 'curl %s\n' "\$*" >> "$curl2"
|
||||
for a in "\$@"; do
|
||||
case "\$a" in
|
||||
*getUpdates*)
|
||||
sleep 1
|
||||
printf '%s' '{"ok":true,"result":[]}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s' '{"ok":true}'
|
||||
STUB
|
||||
chmod +x "$stubs2/curl"
|
||||
printf '#!/usr/bin/env bash\nexit 1\n' > "$stubs2/systemctl"
|
||||
chmod +x "$stubs2/systemctl"
|
||||
|
||||
local common2=(PATH="$stubs2:/usr/bin:/bin" CONFIG_DIR="$cfg2"
|
||||
XDG_RUNTIME_DIR="$sb2/rt" HOME="$sb2/home"
|
||||
TELEGRAM_BOT_TOKEN=testbot TELEGRAM_CHAT_ID=456 TELEGRAM_OWNER_ID=123)
|
||||
mkdir -p "$sb2/rt" "$sb2/home"
|
||||
|
||||
test_run_env "${common2[@]}" -- timeout 3 "$listener" --run
|
||||
check_contains "offset resumed from persisted state file" "offset=99" "$(cat "$curl2")"
|
||||
# Empty batches never rewrite the state file.
|
||||
check_eq "empty batches do not rewrite state" "99" "$(cat "$cfg2/telegram-listener.state")"
|
||||
|
||||
# invalid state file → fall back to 0
|
||||
printf 'garbage\n' > "$cfg2/telegram-listener.state"
|
||||
: > "$curl2"
|
||||
test_run_env "${common2[@]}" -- timeout 3 "$listener" --run
|
||||
check_contains "invalid state file falls back to offset 0" "offset=0" "$(cat "$curl2")"
|
||||
|
||||
# ── (c) negative control: the OLD trap + `wait "$pid"` idiom still dies ──
|
||||
local old_rc old_out
|
||||
set +e
|
||||
old_out="$(bash -s 2>&1 <<'INNER'
|
||||
set -euo pipefail
|
||||
declare -A _EXIT_CODES=()
|
||||
trap 'while _p=$(wait -n 2>/dev/null); do _EXIT_CODES[$_p]=$?; done' CHLD
|
||||
exit 254 & pid=$!
|
||||
sleep 0.3
|
||||
wait "$pid" 2>/dev/null; rc=$?
|
||||
echo "still-alive rc=$rc"
|
||||
INNER
|
||||
)"
|
||||
old_rc=$?
|
||||
set -e
|
||||
check_not_contains "old trap+wait idiom does not survive 254" "still-alive" "$old_out"
|
||||
check_rc "old trap+wait idiom dies with 254" 254 "$old_rc"
|
||||
}
|
||||
Reference in New Issue
Block a user