feat: add pos bank — persistent command bank with parameterized templates
gates / consistency-and-conventions (push) Successful in 29s
gates / consistency-and-conventions (push) Successful in 29s
This commit is contained in:
@@ -33,3 +33,6 @@ dream.md
|
|||||||
|
|
||||||
# Agent process reports — local-only shared memory (not tracked)
|
# Agent process reports — local-only shared memory (not tracked)
|
||||||
AgentsReport/
|
AgentsReport/
|
||||||
|
|
||||||
|
# Task breakdown notes — never committed
|
||||||
|
.tasks/
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
|
|
||||||
<!-- GEN:START docmap -->
|
<!-- GEN:START docmap -->
|
||||||
| ## 1. Project Overview | 28–43 |
|
| ## 1. Project Overview | 28–43 |
|
||||||
| ## 2. Directory Structure | 44–217 |
|
| ## 2. Directory Structure | 44–218 |
|
||||||
| ## 3. Installation Flow | 218–276 |
|
| ## 3. Installation Flow | 219–277 |
|
||||||
| ## 4. The `pos` CLI System | 277–365 |
|
| ## 4. The `pos` CLI System | 278–367 |
|
||||||
| ## 5. Shared Library — `lib/common.sh` | 366–397 |
|
| ## 5. Shared Library — `lib/common.sh` | 368–399 |
|
||||||
| ## 6. Docker Compose / ScaleTail | 398–440 |
|
| ## 6. Docker Compose / ScaleTail | 400–442 |
|
||||||
| ## 7. Optional Apps (`apps/`) | 441–470 |
|
| ## 7. Optional Apps (`apps/`) | 443–472 |
|
||||||
| ## 8. Entertainment Module | 471–484 |
|
| ## 8. Entertainment Module | 473–486 |
|
||||||
| ## 9. Systemd Services | 485–496 |
|
| ## 9. Systemd Services | 487–498 |
|
||||||
| ## 10. Configuration Files | 497–523 |
|
| ## 10. Configuration Files | 499–525 |
|
||||||
| ## 11. Coding Conventions | 524–556 |
|
| ## 11. Coding Conventions | 526–558 |
|
||||||
| ## 12. Development Workflow | 557–609 |
|
| ## 12. Development Workflow | 559–611 |
|
||||||
| ## 13. Key File Quick Reference | 610–693 |
|
| ## 13. Key File Quick Reference | 612–696 |
|
||||||
| ## 14. Common Tasks for Agents | 694–727 |
|
| ## 14. Common Tasks for Agents | 697–730 |
|
||||||
<!-- GEN:END docmap -->
|
<!-- GEN:END docmap -->
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -117,6 +117,7 @@ Linux_post_install/
|
|||||||
│ ├── pos-system-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently
|
│ ├── pos-system-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently
|
||||||
│ ├── pos-system-uninstall # Remove pos toolkit binaries, services, shell integration, config, and data
|
│ ├── pos-system-uninstall # Remove pos toolkit binaries, services, shell integration, config, and data
|
||||||
│ ├── pos-ai # AI assistant: ask, chat, sessions, capture, models, providers
|
│ ├── 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-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||||
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
|
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
|
||||||
<!-- GEN:END tree -->
|
<!-- GEN:END tree -->
|
||||||
@@ -343,6 +344,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
|||||||
| system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | |
|
| 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 | | |
|
| 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 | | |
|
| | 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) | | |
|
| | 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 | | |
|
| | tree | `pos-tree` | Show the pos CLI command tree: categories, commands, and subcommands | | |
|
||||||
<!-- GEN:END dispatch -->
|
<!-- GEN:END dispatch -->
|
||||||
@@ -683,9 +685,10 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||||
| `bin/pos-system-uninstall` | 517 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
| `bin/pos-system-uninstall` | 517 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||||
| `bin/pos-ai` | 714 | AI assistant: ask, chat, sessions, capture, models, providers |
|
| `bin/pos-ai` | 714 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||||
|
| `bin/pos-bank` | 313 | 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-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||||
| `completions/pos.bash` | 317 | Dynamic bash completion |
|
| `completions/pos.bash` | 318 | Dynamic bash completion |
|
||||||
<!-- GEN:END filetable -->
|
<!-- GEN:END filetable -->
|
||||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||||
|
|
||||||
|
|||||||
+24
@@ -534,6 +534,30 @@ 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-set <name> [value]` | Set a flag, optionally with a value (requires sudo) |
|
||||||
| `flag-clear <name>` | Unset a flag (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
|
### config
|
||||||
|
|
||||||
**File:** `bin/pos-config`
|
**File:** `bin/pos-config`
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
|||||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||||
|
|
||||||
# Commands that read from stdin interactively — only log invocation
|
# Commands that read from stdin interactively — only log invocation
|
||||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-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="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"
|
||||||
|
|
||||||
for ((i=n-1; i>=0; i--)); do
|
for ((i=n-1; i>=0; i--)); do
|
||||||
cmd="pos"
|
cmd="pos"
|
||||||
|
|||||||
Executable
+313
@@ -0,0 +1,313 @@
|
|||||||
|
#!/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
|
||||||
|
local entry
|
||||||
|
entry="$(bank_get "$name")" || err "Command not found: $name"
|
||||||
|
local cmd_name cmd_desc cmd_cmd
|
||||||
|
cmd_name="$(printf '%s' "$entry" | cut -f1)"
|
||||||
|
cmd_desc="$(printf '%s' "$entry" | cut -f2)"
|
||||||
|
cmd_cmd="$(printf '%s' "$entry" | cut -f3)"
|
||||||
|
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="" -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
|
||||||
|
local entry
|
||||||
|
entry="$(bank_get "$name")" || err "Command not found: $name"
|
||||||
|
local cmd_name cmd_desc cmd_cmd
|
||||||
|
cmd_name="$(printf '%s' "$entry" | cut -f1)"
|
||||||
|
cmd_desc="$(printf '%s' "$entry" | cut -f2)"
|
||||||
|
cmd_cmd="$(printf '%s' "$entry" | cut -f3)"
|
||||||
|
# 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
|
||||||
|
local entry
|
||||||
|
entry="$(bank_get "$name")" || err "Command not found: $name"
|
||||||
|
local old_name old_desc old_cmd
|
||||||
|
old_name="$(printf '%s' "$entry" | cut -f1)"
|
||||||
|
old_desc="$(printf '%s' "$entry" | cut -f2)"
|
||||||
|
old_cmd="$(printf '%s' "$entry" | cut -f3)"
|
||||||
|
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
|
||||||
@@ -53,6 +53,7 @@ _pos_subcmds[system-alias]="create edit remove list show"
|
|||||||
_pos_subcmds[system-backup]="menu"
|
_pos_subcmds[system-backup]="menu"
|
||||||
_pos_subcmds[system-schedule]="run list config enable disable status migrate menu"
|
_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[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:END possubcmds
|
||||||
# GEN:START posconfigscopes
|
# GEN:START posconfigscopes
|
||||||
declare -a _pos_config_scopes=(ai compose entertainment grab matrix notify scrcpy system telegram ytsync)
|
declare -a _pos_config_scopes=(ai compose entertainment grab matrix notify scrcpy system telegram ytsync)
|
||||||
|
|||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# Storage: ~/.config/linux_post_install/bank.env
|
||||||
|
# Format: name|description|command
|
||||||
|
#
|
||||||
|
# Contracts:
|
||||||
|
# * 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).
|
||||||
|
|
||||||
|
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||||
|
BANK_FILE="${BANK_FILE:-${CONFIG_DIR}/bank.env}"
|
||||||
|
|
||||||
|
# ── Load ────────────────────────────────────────────────────────
|
||||||
|
# Populate parallel arrays: BANK_NAMES, BANK_DESCS, BANK_CMDS.
|
||||||
|
bank_load() {
|
||||||
|
BANK_NAMES=(); BANK_DESCS=(); BANK_CMDS=()
|
||||||
|
[ -f "$BANK_FILE" ] || return 0
|
||||||
|
|
||||||
|
local line name desc cmd
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
[[ -z "${line// /}" ]] && continue
|
||||||
|
IFS='|' read -r name desc cmd <<< "$line"
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
BANK_NAMES+=("$name")
|
||||||
|
BANK_DESCS+=("${desc:-}")
|
||||||
|
BANK_CMDS+=("${cmd:-}")
|
||||||
|
done < "$BANK_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Save ────────────────────────────────────────────────────────
|
||||||
|
# Atomic overwrite from parallel arrays.
|
||||||
|
bank_save() {
|
||||||
|
local dir
|
||||||
|
dir="$(dirname "$BANK_FILE")"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp "${dir}/.bank.XXXXXX")"
|
||||||
|
{
|
||||||
|
printf '%s\n' "# Command Bank — managed by pos bank (do not hand-edit)"
|
||||||
|
printf '%s\n' "# Format: name|description|command"
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
printf '%s|%s|%s\n' "${BANK_NAMES[$i]}" "${BANK_DESCS[$i]}" "${BANK_CMDS[$i]}"
|
||||||
|
done
|
||||||
|
} > "$tmp"
|
||||||
|
chmod 600 "$tmp"
|
||||||
|
mv "$tmp" "$BANK_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Add ─────────────────────────────────────────────────────────
|
||||||
|
bank_add() {
|
||||||
|
local name="$1" desc="$2" cmd="$3"
|
||||||
|
bank_load
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
[[ "${BANK_NAMES[$i]}" == "$name" ]] && return 1
|
||||||
|
done
|
||||||
|
BANK_NAMES+=("$name")
|
||||||
|
BANK_DESCS+=("$desc")
|
||||||
|
BANK_CMDS+=("$cmd")
|
||||||
|
bank_save
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Remove ──────────────────────────────────────────────────────
|
||||||
|
bank_remove() {
|
||||||
|
local name="$1"
|
||||||
|
bank_load
|
||||||
|
local found=0 i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
if [[ "${BANK_NAMES[$i]}" == "$name" ]]; then
|
||||||
|
unset 'BANK_NAMES[i]'
|
||||||
|
unset 'BANK_DESCS[i]'
|
||||||
|
unset 'BANK_CMDS[i]'
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "$found" -eq 1 ]] || return 1
|
||||||
|
# Re-index arrays (unset leaves gaps)
|
||||||
|
BANK_NAMES=("${BANK_NAMES[@]}")
|
||||||
|
BANK_DESCS=("${BANK_DESCS[@]}")
|
||||||
|
BANK_CMDS=("${BANK_CMDS[@]}")
|
||||||
|
bank_save
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Update ──────────────────────────────────────────────────────
|
||||||
|
bank_update() {
|
||||||
|
local name="$1" desc="$2" cmd="$3"
|
||||||
|
bank_load
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
if [[ "${BANK_NAMES[$i]}" == "$name" ]]; then
|
||||||
|
BANK_DESCS[$i]="$desc"
|
||||||
|
BANK_CMDS[$i]="$cmd"
|
||||||
|
bank_save
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Find (by name) ──────────────────────────────────────────────
|
||||||
|
# Sets BANK_IDX. Returns 0 if found, 1 if not.
|
||||||
|
bank_find() {
|
||||||
|
local name="$1"
|
||||||
|
bank_load
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
if [[ "${BANK_NAMES[$i]}" == "$name" ]]; then
|
||||||
|
BANK_IDX=$i
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Get (by name) ───────────────────────────────────────────────
|
||||||
|
# Outputs: name\tdescription\tcommand (tab-separated).
|
||||||
|
bank_get() {
|
||||||
|
local name="$1"
|
||||||
|
bank_find "$name" || return 1
|
||||||
|
printf '%s\t%s\t%s\n' "${BANK_NAMES[$BANK_IDX]}" "${BANK_DESCS[$BANK_IDX]}" "${BANK_CMDS[$BANK_IDX]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── List names ──────────────────────────────────────────────────
|
||||||
|
bank_list_names() {
|
||||||
|
bank_load
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
|
||||||
|
printf '%s\n' "${BANK_NAMES[$i]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Count ───────────────────────────────────────────────────────
|
||||||
|
bank_count() {
|
||||||
|
bank_load
|
||||||
|
echo "${#BANK_NAMES[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Validate name ───────────────────────────────────────────────
|
||||||
|
bank_valid_name() {
|
||||||
|
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Extract {param} names from a command template ───────────────
|
||||||
|
_extract_params() {
|
||||||
|
local cmd="$1"
|
||||||
|
grep -oE '\{[a-zA-Z_][a-zA-Z0-9_]*\}' <<< "$cmd" 2>/dev/null | \
|
||||||
|
sed 's/[{}]//g' | \
|
||||||
|
awk '!seen[$0]++' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Substitute {param} with quoted values ───────────────────────
|
||||||
|
# Usage: _substitute_params "cmd template" "key1=val1" "key2=val2" ...
|
||||||
|
# Uses awk to build quoted replacements safely, avoiding bash quote-nesting.
|
||||||
|
_substitute_params() {
|
||||||
|
local cmd="$1"; shift
|
||||||
|
# Build a sed expression: for each key=val, replace {key} with "val"
|
||||||
|
local sed_expr=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
local key="${1%%=*}"
|
||||||
|
local val="${1#*=}"
|
||||||
|
# Escape sed special chars in the value
|
||||||
|
val="${val//\\/\\\\}"
|
||||||
|
val="${val//\//\\/}"
|
||||||
|
sed_expr+="s/{${key}}/\"${val}\"/g; "
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[ -n "$sed_expr" ] && printf '%s' "$cmd" | sed "$sed_expr" || printf '%s' "$cmd"
|
||||||
|
}
|
||||||
+2
-1
@@ -56,4 +56,5 @@ silently.
|
|||||||
| `t-install-version.sh` | install.sh version gate: match→skip, mismatch→proceed, --force bypass, dry-run variant, flag write, numeric comparison |
|
| `t-install-version.sh` | install.sh version gate: match→skip, mismatch→proceed, --force bypass, dry-run variant, flag write, numeric comparison |
|
||||||
| `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-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-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-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) + pos-bank CLI integration (help, list, add, show, remove, params, invalid name) |
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# tests/bank-test-helper.sh — minimal shim that sources bank-lib.sh for unit
|
||||||
|
# testing. Provides a stub `err()` so bank-lib.sh can load without the full
|
||||||
|
# common.sh / menu-lib.sh dependency chain.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Stub err() — bank-lib.sh is contractually "never exits" but bank_valid_name
|
||||||
|
# callers sometimes chain via `|| err …`. This version returns 1 so the test
|
||||||
|
# harness can assert rc.
|
||||||
|
err() { printf 'ERROR: %s\n' "$*" >&2; return 1; }
|
||||||
|
|
||||||
|
# Source the library under test.
|
||||||
|
source "$ROOT/lib/bank-lib.sh"
|
||||||
Executable
+337
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# t-bank.sh — Command Bank feature: bank-lib.sh unit tests + pos-bank CLI
|
||||||
|
# integration tests.
|
||||||
|
#
|
||||||
|
# Storage contract: pipe-delimited name|description|command in $BANK_FILE.
|
||||||
|
# Parameter syntax: {param} placeholders in command templates.
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local sandbox helper
|
||||||
|
sandbox="$(mksandbox bank)"
|
||||||
|
helper="$sandbox/helper.sh"
|
||||||
|
|
||||||
|
# ── Build a sourced helper that gives us bank-lib functions ──
|
||||||
|
cat > "$helper" <<'HELPER'
|
||||||
|
source "$ROOT/tests/bank-test-helper.sh"
|
||||||
|
HELPER
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Part A: bank-lib.sh unit tests
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# A1: bank_add + bank_load — add two entries, verify arrays
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a1.bank.env"
|
||||||
|
: > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_add "deploy" "Deploy to prod" "ssh prod 'cd /app && git pull'"
|
||||||
|
bank_add "backup" "Backup database" "pg_dump mydb | gzip > /tmp/db.sql.gz"
|
||||||
|
bank_load
|
||||||
|
# Check arrays
|
||||||
|
[ "${#BANK_NAMES[@]}" -eq 2 ] || { echo "FAIL A1: expected 2 names, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||||
|
[ "${BANK_NAMES[0]}" = "deploy" ] || { echo "FAIL A1: name[0] mismatch"; exit 1; }
|
||||||
|
[ "${BANK_DESCS[0]}" = "Deploy to prod" ] || { echo "FAIL A1: desc[0] mismatch"; exit 1; }
|
||||||
|
[ "${BANK_CMDS[1]}" = "pg_dump mydb | gzip > /tmp/db.sql.gz" ] || { echo "FAIL A1: cmd[1] mismatch"; exit 1; }
|
||||||
|
echo "PASS A1: bank_add + bank_load populates arrays correctly"
|
||||||
|
) && printf ' PASS bank_add + bank_load populates arrays\n' \
|
||||||
|
|| printf ' FAIL bank_add + bank_load populates arrays\n'
|
||||||
|
|
||||||
|
# A2: bank_add duplicate — returns rc 1
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a2.bank.env"
|
||||||
|
printf 'old|Old cmd|echo old\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_add "old" "New desc" "echo new"
|
||||||
|
exit $?
|
||||||
|
)
|
||||||
|
check_rc "bank_add duplicate returns rc 1" 1 $?
|
||||||
|
|
||||||
|
# A3: bank_remove — remove entry, verify gone
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a3.bank.env"
|
||||||
|
printf 'aaa|desc a|cmd a\nbbb|desc b|cmd b\nccc|desc c|cmd c\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_remove "bbb"
|
||||||
|
bank_load
|
||||||
|
[ "${#BANK_NAMES[@]}" -eq 2 ] || { echo "FAIL A3: expected 2, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||||
|
[ "${BANK_NAMES[0]}" = "aaa" ] || { echo "FAIL A3: name[0] mismatch"; exit 1; }
|
||||||
|
[ "${BANK_NAMES[1]}" = "ccc" ] || { echo "FAIL A3: name[1] mismatch"; exit 1; }
|
||||||
|
# Verify file on disk
|
||||||
|
grep -q '^bbb|' "$BANK_FILE" && { echo "FAIL A3: bbb still in file"; exit 1; }
|
||||||
|
echo "PASS A3: bank_remove"
|
||||||
|
) && printf ' PASS bank_remove removes entry\n' \
|
||||||
|
|| printf ' FAIL bank_remove removes entry\n'
|
||||||
|
|
||||||
|
# A4: bank_remove missing — returns rc 1
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a4.bank.env"
|
||||||
|
printf 'xxx|desc|cmd\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_remove "nonexistent"
|
||||||
|
exit $?
|
||||||
|
)
|
||||||
|
check_rc "bank_remove missing returns rc 1" 1 $?
|
||||||
|
|
||||||
|
# A5: bank_update — update entry, verify changed
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a5.bank.env"
|
||||||
|
printf 'mycmd|old desc|old cmd\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_update "mycmd" "new desc" "new cmd"
|
||||||
|
bank_load
|
||||||
|
[ "${BANK_DESCS[0]}" = "new desc" ] || { echo "FAIL A5: desc mismatch"; exit 1; }
|
||||||
|
[ "${BANK_CMDS[0]}" = "new cmd" ] || { echo "FAIL A5: cmd mismatch"; exit 1; }
|
||||||
|
# Verify on disk
|
||||||
|
grep -q 'new desc|new cmd' "$BANK_FILE" || { echo "FAIL A5: not persisted"; exit 1; }
|
||||||
|
echo "PASS A5: bank_update"
|
||||||
|
) && printf ' PASS bank_update updates entry\n' \
|
||||||
|
|| printf ' FAIL bank_update updates entry\n'
|
||||||
|
|
||||||
|
# A6: bank_update missing — returns rc 1
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a6.bank.env"
|
||||||
|
printf 'exists|desc|cmd\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_update "nope" "desc" "cmd"
|
||||||
|
exit $?
|
||||||
|
)
|
||||||
|
check_rc "bank_update missing returns rc 1" 1 $?
|
||||||
|
|
||||||
|
# A7: bank_find — found sets BANK_IDX, not-found returns 1
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a7.bank.env"
|
||||||
|
printf 'alpha|d1|c1\nbeta|d2|c2\ngamma|d3|c3\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
bank_find "beta"
|
||||||
|
[ "${BANK_IDX:-}" = "1" ] || { echo "FAIL A7: expected BANK_IDX=1, got ${BANK_IDX:-unset}"; exit 1; }
|
||||||
|
bank_find "nonexistent" && { echo "FAIL A7: should return 1 for missing"; exit 1; }
|
||||||
|
echo "PASS A7: bank_find"
|
||||||
|
) && printf ' PASS bank_find sets BANK_IDX / returns 1\n' \
|
||||||
|
|| printf ' FAIL bank_find sets BANK_IDX / returns 1\n'
|
||||||
|
|
||||||
|
# A8: bank_get — returns tab-separated output
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a8.bank.env"
|
||||||
|
printf 'mytool|My Tool|echo hello\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
local out
|
||||||
|
out="$(bank_get "mytool")"
|
||||||
|
[ "$out" = "mytool My Tool echo hello" ] || { echo "FAIL A8: unexpected output [$out]"; exit 1; }
|
||||||
|
echo "PASS A8: bank_get"
|
||||||
|
) && printf ' PASS bank_get returns tab-separated output\n' \
|
||||||
|
|| printf ' FAIL bank_get returns tab-separated output\n'
|
||||||
|
|
||||||
|
# A9: bank_list_names — lists all names
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a9.bank.env"
|
||||||
|
printf 'one|x|y\ntwo|a|b\nthree|c|d\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
local names
|
||||||
|
names="$(bank_list_names)"
|
||||||
|
local expected
|
||||||
|
expected="$(printf 'one\ntwo\nthree')"
|
||||||
|
[ "$names" = "$expected" ] || { echo "FAIL A9: expected [$expected] got [$names]"; exit 1; }
|
||||||
|
echo "PASS A9: bank_list_names"
|
||||||
|
) && printf ' PASS bank_list_names lists all names\n' \
|
||||||
|
|| printf ' FAIL bank_list_names lists all names\n'
|
||||||
|
|
||||||
|
# A10: bank_count — returns correct count
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a10.bank.env"
|
||||||
|
printf 'a|1|2\nb|3|4\nc|5|6\nd|7|8\n' > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
local cnt
|
||||||
|
cnt="$(bank_count)"
|
||||||
|
[ "$cnt" -eq 4 ] || { echo "FAIL A10: expected 4, got $cnt"; exit 1; }
|
||||||
|
echo "PASS A10: bank_count"
|
||||||
|
) && printf ' PASS bank_count returns correct count\n' \
|
||||||
|
|| printf ' FAIL bank_count returns correct count\n'
|
||||||
|
|
||||||
|
# A11: bank_count — empty file returns 0
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a11.bank.env"
|
||||||
|
: > "$BANK_FILE"
|
||||||
|
source "$helper"
|
||||||
|
local cnt
|
||||||
|
cnt="$(bank_count)"
|
||||||
|
[ "$cnt" -eq 0 ] || { echo "FAIL A11: expected 0, got $cnt"; exit 1; }
|
||||||
|
echo "PASS A11: bank_count empty"
|
||||||
|
) && printf ' PASS bank_count returns 0 for empty file\n' \
|
||||||
|
|| printf ' FAIL bank_count returns 0 for empty file\n'
|
||||||
|
|
||||||
|
# A12: bank_valid_name — valid names pass
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local fail=0
|
||||||
|
for n in "deploy" "my-cmd" "test_123" "A" "z9"; do
|
||||||
|
bank_valid_name "$n" || { fail=1; printf 'FAIL A12: %s should be valid\n' "$n" >&2; }
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
|
echo "PASS A12: bank_valid_name accepts valid names"
|
||||||
|
) && printf ' PASS bank_valid_name accepts valid names\n' \
|
||||||
|
|| printf ' FAIL bank_valid_name accepts valid names\n'
|
||||||
|
|
||||||
|
# A13: bank_valid_name — invalid names fail
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local fail=0
|
||||||
|
for n in "" "1abc" "-start" "_under" "has space" "special!"; do
|
||||||
|
bank_valid_name "$n" && { fail=1; printf 'FAIL A13: %s should be invalid\n' "$n" >&2; }
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
|
echo "PASS A13: bank_valid_name rejects invalid names"
|
||||||
|
) && printf ' PASS bank_valid_name rejects invalid names\n' \
|
||||||
|
|| printf ' FAIL bank_valid_name rejects invalid names\n'
|
||||||
|
|
||||||
|
# A14: _extract_params — extracts {param} names from template
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local params
|
||||||
|
params="$(_extract_params "echo {name} {path}")"
|
||||||
|
local expected
|
||||||
|
expected="$(printf 'name\npath')"
|
||||||
|
[ "$params" = "$expected" ] || { echo "FAIL A14: expected [$expected] got [$params]"; exit 1; }
|
||||||
|
echo "PASS A14: _extract_params"
|
||||||
|
) && printf ' PASS _extract_params extracts param names\n' \
|
||||||
|
|| printf ' FAIL _extract_params extracts param names\n'
|
||||||
|
|
||||||
|
# A15: _extract_params — no params returns empty
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local params
|
||||||
|
params="$(_extract_params "echo hello world")"
|
||||||
|
[ -z "$params" ] || { echo "FAIL A15: expected empty, got [$params]"; exit 1; }
|
||||||
|
echo "PASS A15: _extract_params empty"
|
||||||
|
) && printf ' PASS _extract_params returns empty for no params\n' \
|
||||||
|
|| printf ' FAIL _extract_params returns empty for no params\n'
|
||||||
|
|
||||||
|
# A16: _extract_params — deduplicates repeated params
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local params
|
||||||
|
params="$(_extract_params "cp {src} {dst} && ls {src}")"
|
||||||
|
local count
|
||||||
|
count="$(printf '%s' "$params" | grep -c '^src$' || true)"
|
||||||
|
[ "$count" -eq 1 ] || { echo "FAIL A16: expected 1 occurrence of src, got $count"; exit 1; }
|
||||||
|
echo "PASS A16: _extract_params deduplicates"
|
||||||
|
) && printf ' PASS _extract_params deduplicates\n' \
|
||||||
|
|| printf ' FAIL _extract_params deduplicates\n'
|
||||||
|
|
||||||
|
# A17: _substitute_params — replaces {param} with quoted values
|
||||||
|
(
|
||||||
|
source "$helper"
|
||||||
|
local result
|
||||||
|
result="$(_substitute_params "scp {file} user@host:{dest}" "file=/tmp/data.csv" "dest=/var/data")"
|
||||||
|
local expected='scp "/tmp/data.csv" user@host:"/var/data"'
|
||||||
|
[ "$result" = "$expected" ] || { echo "FAIL A17: expected [$expected] got [$result]"; exit 1; }
|
||||||
|
echo "PASS A17: _substitute_params"
|
||||||
|
) && printf ' PASS _substitute_params replaces params\n' \
|
||||||
|
|| printf ' FAIL _substitute_params replaces params\n'
|
||||||
|
|
||||||
|
# A18: bank_load — comments and blank lines are skipped
|
||||||
|
(
|
||||||
|
BANK_FILE="$sandbox/a18.bank.env"
|
||||||
|
cat > "$BANK_FILE" <<'BANK'
|
||||||
|
# Command Bank — managed by pos bank (do not hand-edit)
|
||||||
|
# Format: name|description|command
|
||||||
|
|
||||||
|
real|Real command|echo real
|
||||||
|
# another comment
|
||||||
|
|
||||||
|
# indented comment
|
||||||
|
|
||||||
|
BANK
|
||||||
|
source "$helper"
|
||||||
|
bank_load
|
||||||
|
[ "${#BANK_NAMES[@]}" -eq 1 ] || { echo "FAIL A18: expected 1, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||||
|
[ "${BANK_NAMES[0]}" = "real" ] || { echo "FAIL A18: name mismatch"; exit 1; }
|
||||||
|
echo "PASS A18: bank_load skips comments and blanks"
|
||||||
|
) && printf ' PASS bank_load skips comments and blank lines\n' \
|
||||||
|
|| printf ' FAIL bank_load skips comments and blank lines\n'
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Part B: pos-bank CLI integration tests
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
local pos_bank="$ROOT/bin/pos-bank"
|
||||||
|
|
||||||
|
# B1: pos 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"
|
||||||
|
|
||||||
|
# B2: pos 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"
|
||||||
|
|
||||||
|
# B3: pos 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"
|
||||||
|
# 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'
|
||||||
|
else
|
||||||
|
printf ' FAIL pos bank add did not persist to bank.env\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# B4: pos 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"
|
||||||
|
|
||||||
|
# B5: pos 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"
|
||||||
|
|
||||||
|
# B6: pos 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"
|
||||||
|
|
||||||
|
# B7: pos 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"
|
||||||
|
|
||||||
|
# B8: pos 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"
|
||||||
|
# Verify gone
|
||||||
|
if grep -q '^disk-usage|' "$sandbox/b3.bank.env"; then
|
||||||
|
printf ' FAIL pos bank remove did not delete from bank.env\n'
|
||||||
|
else
|
||||||
|
printf ' PASS pos bank remove deletes from bank.env\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# B9: pos 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"
|
||||||
|
|
||||||
|
# B10: pos 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"
|
||||||
|
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
|
||||||
|
: > "$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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user