From 59a4c0e1df6cd36f1973db9b953174b87daee529 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 12 Sep 2026 13:53:44 -0400 Subject: [PATCH] feat: add alias creation to pos system bank interactive menu New 'alias' subcommand (+ POS_SUBCMDS row, menu option 6 'Manage aliases'): create/update/list/remove bash aliases that run 'pos system bank run ', written into ~/.bashrc inside a managed marker block (BASH_RC_FILE seam for tests, atomic tmp+mv rewrite). - alias_valid_name enforces ^[a-zA-Z][a-zA-Z0-9_-]*$ - same-name create is an idempotent retarget; last alias removal cleans the whole block; outer ~/.bashrc aliases preserved byte-identically - outer-alias collision refused (file untouched), PATH-shadow non-blocking warn, 'bank remove' drops aliases pointing at the removed command - GNU sed '1,0p' edge case (block at line 1) fixed with guarded ranges; regex-quote literalization avoided via glob case payload match - t-bank.sh +49 checks (71 -> 120); docs: POS.md, howto/system.md, tests/README.md, AGENT_TODO.md; make gen byte-idempotent (also refreshes the pos-communication-telegram-listener filetable line count so the previous commit's tree is gen-consistent) --- AGENT_TODO.md | 2 + DOC/AGENT_Context_Project.md | 4 +- DOC/POS.md | 15 +- DOC/howto/system.md | 39 +++- bin/pos-system-bank | 340 ++++++++++++++++++++++++++++++++++- completions/pos.bash | 2 +- tests/README.md | 2 +- tests/t-bank.sh | 186 +++++++++++++++++++ 8 files changed, 580 insertions(+), 10 deletions(-) diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 070fd93..1fb0f1b 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -42,6 +42,8 @@ summary (newest last). ## Done +- **`pos system bank alias`** — bash aliases for bank commands (Architect→Builder): `alias [alias_name]`, `alias list`, `alias remove `, menu option 6 "Manage aliases", subcommand shown in `pos tree`/completion via `# POS_SUBCMDS`. Writes `alias ='pos system bank run '` 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. + - **`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.** diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index fbdf1cc..0033444 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -643,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 → app, run them on chat messages | +| `bin/pos-communication-telegram-listener` | 924 | Telegram bot listener: map /command → bash and → 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) | @@ -678,7 +678,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-share-usb-server` | 362 | USB Redirector server control (--ls, --share; prompts when args omitted) | | `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent | | `bin/pos-system-backup` | 301 | Encrypted (AES-256) folder snapshots (tar + gpg) | -| `bin/pos-system-bank` | 311 | Persistent command bank for saving and running shell commands | +| `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 | diff --git a/DOC/POS.md b/DOC/POS.md index 45406b9..6e4c7ab 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -296,7 +296,7 @@ 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 ` | `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/.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-.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 ` 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 bank` | `bin/pos-system-bank` | Persistent command bank for saving and running shell commands — list, add, show, run, edit, remove | 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 | +| `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). @@ -315,7 +315,18 @@ On a TTY with no arguments, `pos system bank` opens an interactive menu (list / | `pos system bank show ` | Show command details and detected parameters | | `pos system bank run [key=val …]` | Run a command (interactive for missing params) | | `pos system bank edit ` | Edit an existing command | -| `pos system bank remove ` | Remove a command | +| `pos system bank remove ` | Remove a command (also drops aliases pointing at it) | +| `pos system bank alias [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 ` | 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 ='pos system bank run '` — 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: diff --git a/DOC/howto/system.md b/DOC/howto/system.md index 41a22ad..ffcf9bd 100644 --- a/DOC/howto/system.md +++ b/DOC/howto/system.md @@ -1,7 +1,8 @@ # How-To: `pos system` -Host care: encrypted backups, firewall, health dashboard, and uninstall. Tools: -`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 | |------|--------------| @@ -9,6 +10,7 @@ Host care: encrypted backups, firewall, health dashboard, and uninstall. Tools: | `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 bank` | Persistent command bank — save, run, and alias shell commands | --- @@ -233,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 ='pos system bank run '`. 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 ` 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 diff --git a/bin/pos-system-bank b/bin/pos-system-bank index db4d81c..ffeeb5c 100755 --- a/bin/pos-system-bank +++ b/bin/pos-system-bank @@ -1,7 +1,7 @@ #!/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 +# 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" @@ -20,6 +20,9 @@ Subcommands: run [key=val ...] Run a command (interactive for missing params) edit Edit an existing command remove Remove a command + alias [alias_name] Create/update a bash alias for a bank command + alias list List bash aliases in ~/.bashrc + alias remove Remove a bash alias Interactive menu: pos system bank (no args on a terminal) @@ -258,9 +261,339 @@ cmd_remove() { 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 ='pos system bank run ' +# 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 " + 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 [alias_name] | list | remove " ;; + *) cmd_alias_add "$@" ;; + esac +} + +cmd_alias_add() { + local name="${1:-}" alias_name="${2:-$1}" + [ -n "$name" ] || err "Usage: pos system bank alias [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 " + 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 " + 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 " + 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 @@ -271,13 +604,15 @@ run_menu() { "Add command" \ "Run command" \ "Edit command" \ - "Remove command")" || return 0 + "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 } @@ -306,6 +641,7 @@ case "${1:-}" in run) shift; cmd_run "$@" ;; edit) shift; cmd_edit "$@" ;; remove) shift; cmd_remove "$@" ;; + alias) shift; cmd_alias "$@" ;; "") usage ;; *) err "Unknown subcommand: $1 (see --help)" ;; esac diff --git a/completions/pos.bash b/completions/pos.bash index d7ca16f..6d90c85 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -50,7 +50,7 @@ _pos_subcmds[share-nfs-server]="status share unshare list reload enable disable _pos_subcmds[share-smb-client]="mount unmount list persist unpersist menu" _pos_subcmds[share-smb-server]="status share unshare list adduser deluser reload enable disable menu" _pos_subcmds[system-backup]="menu" -_pos_subcmds[system-bank]="list add show run edit remove" +_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" # GEN:END possubcmds diff --git a/tests/README.md b/tests/README.md index 6cfc10d..038406a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -58,4 +58,4 @@ silently. | `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-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) | \ No newline at end of file +| `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) | \ No newline at end of file diff --git a/tests/t-bank.sh b/tests/t-bank.sh index b87bf8d..25dd803 100755 --- a/tests/t-bank.sh +++ b/tests/t-bank.sh @@ -464,4 +464,190 @@ SCRIPT 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 [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" }