fix: paste-safe multi-line value input in pos ai alias Insert Prompt
gates / consistency-and-conventions (push) Successful in 1m54s
gates / consistency-and-conventions (push) Successful in 1m54s
menu_ask_value used line-oriented read -rp: a multiline Ctrl+V paste flooded the tty queue, read consumed only the first line, and the rest executed as commands later (or were eaten by a later prompt). - lib/menu-lib.sh: new menu_read_value() raw-mode bracketed-paste reader (stty -icanon -echo -isig, \e[?2004h/l, literal newlines inside [200~..[201~, Enter submits outside paste, edit keys, cancel on Ctrl-D-empty/Ctrl-C/Z/\, terminal restored via trap). Bytes via dd|od|tr chunks, not bash read: read self-interrupts on ETX from a tty even with ISIG disabled. - bin/pos-ai-alias: prompt encode/decode (backslash, newline) with load/save wiring; newline-safe truncate; edit wizard Enter keeps the full original prompt (no more silent >80-char truncation). Verified via pty harnesses: multiline + single-line paste captured verbatim with nothing executed, Ctrl-D/Ctrl-C cancel cleanly, full create/list/show/edit E2E, round-trips byte-exact. Gates: make gen && make check, make lint 0 FAIL 0 WARN.
This commit is contained in:
@@ -135,3 +135,4 @@ summary (newest last).
|
|||||||
- **2026-08-23** — Menu Phase 1 (user-ratified decision "b"): category-neutral menu library extracted from share-suite Pattern B + four P1 tool menus. New `lib/menu-lib.sh` (169 ln): `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value` (stderr render, /dev/tty reads, EOF fail-closed rc=1, index/value→stdout); `lib/share-lib.sh` (436→318) keeps its public names as pure delegating shims so all five `pos share *` tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (or `menu` verb, `# POS_SUBCMDS:` registered, completions regen'd) on `pos media sync` (164→216: Sync-now/Preview/mp3/mp4/source-folder items), `pos system backup` (216→292: typed/service-root/plain variants, every backup behind folder-naming y/N), `pos docker compose` (366→487: ls/up/down/restart/logs/update/config items, down/restart/update confirm-gated naming the stack), `pos system schedule` (81→151: list/status/run-now(confirm)/enable/disable/editor — timer-invoked `run <name>` verb dispatch byte-identical to HEAD). INTERACTIVE_CMDS unchanged; all CLI verbs byte-compatible. Docs: POS.md ×4 rows, DEV.md lib row, SCRIPTS.md sections, AGENT_Context rows + GEN. Chain: Explorer survey (37 tools, `reportAgents/2026-08-23-explorer-pos-menu-survey.md`) → Designer classification (`-designer-pos-menu-suitability.md`: 14 MENU-FIT / 7 CONDITIONAL / 16 NO-FIT) → Builder T1/T2/T3 (`-builder-t1-menu-lib-extraction.md`, `-t2-p1-menus-media-backup.md`, `-t3-p1-menus-compose-schedule.md`; T3 discloses a mid-verify symlink clobber restored+re-verified) → Reviewer ACCEPT_WITH_NOTES over the consolidated diff (`-reviewer-phase1-menu.md`, T3 integrity clean). Verified: bash -n ×7, pty probes (render/quit/EOF/non-tty fail-closed/destructive prompt-abort), gates green after each pass and re-run by Orchestrator post-review (`make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-server `menu` in POS_FLAGS nit (owning track).
|
- **2026-08-23** — Menu Phase 1 (user-ratified decision "b"): category-neutral menu library extracted from share-suite Pattern B + four P1 tool menus. New `lib/menu-lib.sh` (169 ln): `menu_guard`/`menu_run`/`menu_pick`/`menu_ask_value` (stderr render, /dev/tty reads, EOF fail-closed rc=1, index/value→stdout); `lib/share-lib.sh` (436→318) keeps its public names as pure delegating shims so all five `pos share *` tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (or `menu` verb, `# POS_SUBCMDS:` registered, completions regen'd) on `pos media sync` (164→216: Sync-now/Preview/mp3/mp4/source-folder items), `pos system backup` (216→292: typed/service-root/plain variants, every backup behind folder-naming y/N), `pos docker compose` (366→487: ls/up/down/restart/logs/update/config items, down/restart/update confirm-gated naming the stack), `pos system schedule` (81→151: list/status/run-now(confirm)/enable/disable/editor — timer-invoked `run <name>` verb dispatch byte-identical to HEAD). INTERACTIVE_CMDS unchanged; all CLI verbs byte-compatible. Docs: POS.md ×4 rows, DEV.md lib row, SCRIPTS.md sections, AGENT_Context rows + GEN. Chain: Explorer survey (37 tools, `reportAgents/2026-08-23-explorer-pos-menu-survey.md`) → Designer classification (`-designer-pos-menu-suitability.md`: 14 MENU-FIT / 7 CONDITIONAL / 16 NO-FIT) → Builder T1/T2/T3 (`-builder-t1-menu-lib-extraction.md`, `-t2-p1-menus-media-backup.md`, `-t3-p1-menus-compose-schedule.md`; T3 discloses a mid-verify symlink clobber restored+re-verified) → Reviewer ACCEPT_WITH_NOTES over the consolidated diff (`-reviewer-phase1-menu.md`, T3 integrity clean). Verified: bash -n ×7, pty probes (render/quit/EOF/non-tty fail-closed/destructive prompt-abort), gates green after each pass and re-run by Orchestrator post-review (`make gen` idempotent · `make check` OK · `make lint` 0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-server `menu` in POS_FLAGS nit (owning track).
|
||||||
- **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic).
|
- **2026-08-23** — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, `lib/menu-lib.sh` exists). `pos docker vbox` (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation `menu_self` (verbs never re-enter the menu → no recursion); `enter` hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. `pos network download` (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (`menu_ask_value`, optional `--tmux`), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (`-m 3`) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through `uses_stdin`). `pos system firewall` (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr `{ … } >&2` (body byte-preserved), all **38** interactive reads → `/dev/tty` via tool-local `tty_read()` (EOF/no-tty → pointer + rc1, never hangs), `prompt_ipver` de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register `# POS_SUBCMDS:` += `menu`; POS.md rows updated; GEN regen'd. Chain: Builder T4 (`reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md`; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (`-t5-firewall-menu-migration.md`; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (`-reviewer-phase2-menu.md`, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — `make check` OK · `make lint` 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); `confirm()` EOF hits set-u unbound `yn` (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic).
|
||||||
- **2026-08-26** — `pos ai alias` activation rework (Option B) + `pos config` listing readability, per the 2026-08-26 Architect/Designer specs (`AgentsReport/{architect,designer}/2026-08-26-*.md`). **Alias activation:** the stale sourced-snapshot mechanism is gone — every `pos ai alias` invocation runs `_alias_sync()` (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at `~/.local/bin/<name>` chmod 755 via mktemp+mv with a `bash -n` pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy `ai-aliases.sh` generation stopped and generator-marker-guarded auto-removal with an `unalias <names>` remediation hint; loud PATH guidance when `~/.local/bin` is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; `show` gains the wrapper path; `pos-system-uninstall` sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single `_alias_table` renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). **Config readability** (`lib/config-ui.sh`, fully generic): new optional `# POS_CONFIG:` field types — `@Caption` / `@[KEY=v1|v2] Caption` group captions (condition evaluated per render via `cfg_value`; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and `*providers=<tag>` adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60–120 cols, whole render block → stderr per menu-lib house pattern, honest prompt `Number to edit [r=refresh, q=quit]:`); masking/edit flow byte-compatible, no per-scope branches. `bin/pos-ai` line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (`HOME=/tmp/…`, `CONFIG_DIR` seam, argv-capturing `pos` shim) covering %q quoting round-trips (quotes/backticks/`$()`/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for `ai` AND old-format `system`; gates `make gen && make check && make lint` 0 FAIL / 0 WARN.
|
- **2026-08-26** — `pos ai alias` activation rework (Option B) + `pos config` listing readability, per the 2026-08-26 Architect/Designer specs (`AgentsReport/{architect,designer}/2026-08-26-*.md`). **Alias activation:** the stale sourced-snapshot mechanism is gone — every `pos ai alias` invocation runs `_alias_sync()` (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at `~/.local/bin/<name>` chmod 755 via mktemp+mv with a `bash -n` pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy `ai-aliases.sh` generation stopped and generator-marker-guarded auto-removal with an `unalias <names>` remediation hint; loud PATH guidance when `~/.local/bin` is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; `show` gains the wrapper path; `pos-system-uninstall` sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single `_alias_table` renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). **Config readability** (`lib/config-ui.sh`, fully generic): new optional `# POS_CONFIG:` field types — `@Caption` / `@[KEY=v1|v2] Caption` group captions (condition evaluated per render via `cfg_value`; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and `*providers=<tag>` adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60–120 cols, whole render block → stderr per menu-lib house pattern, honest prompt `Number to edit [r=refresh, q=quit]:`); masking/edit flow byte-compatible, no per-scope branches. `bin/pos-ai` line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (`HOME=/tmp/…`, `CONFIG_DIR` seam, argv-capturing `pos` shim) covering %q quoting round-trips (quotes/backticks/`$()`/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for `ai` AND old-format `system`; gates `make gen && make check && make lint` 0 FAIL / 0 WARN.
|
||||||
|
- **2026-08-27** — Critical fix: paste injection + multiline paste in `pos ai alias`'s Insert Prompt (root cause: `menu_ask_value` → plain line-oriented `read -rp`; a multi-line Ctrl+V paste floods the tty queue, `read` consumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified `$(whoami)`/`; ls`/`sudo apt update` behavior). New `menu_read_value()` in `lib/menu-lib.sh` (169→362): raw-mode (`stty -icanon -echo -isig min 1 time 0`) bracketed-paste-aware value reader — `\e[?2004h/l` markers, text inside `[200~…[201~` inserted LITERALLY (embedded newlines/CR are data), Enter submits only outside a paste, Backspace/DEL/Left/Right/Home/End/Delete/Ctrl-U edit, Ctrl-D-on-empty + Ctrl-C/Z/\ cancel (terminal restored first); bytes read chunk-wise via `dd bs=4096|od -tx1|tr` — NOT bash's `read` builtin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmed `read -erp` (readline) atomically consumes a paste but returns only its first line, so a custom reader was required. `bin/pos-ai-alias` (712→760): `_alias_prompt_encode/_decode` (backslash→`\\`, newline→`\n`; literal `[ = ]` comparisons — bash `case` patterns don't match a single backslash), `_alias_prompt_truncate` newline-safe + max-length arg; load/save encode/decode the prompt field; edit wizard shows a truncated display default but Enter restores the FULL original prompt (fixes pre-existing silent truncation of >80-char prompts), empty-original Enter continues. Verified: pty harnesses (`/tmp/pty_{menulib,cancel,e2e_alias}.py`, `/tmp/roundtrip_test.sh`) — bracketed multiline paste captured verbatim incl. `C:\temp\note`/`$(whoami)`/`; ls`/`echo test`/`sudo apt update`, nothing executed, clean exit; single-line paste; Ctrl-D and Ctrl-C both cancel cleanly (CANCELLED→DONE, terminal restored); full create→list→show→edit E2E with decode round-trip and Enter-keeps-full; `bash -n` ×2, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
|
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
|
||||||
| `lib/usb-lib.sh` | 205 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`; EFI system partitions excluded; picker shows size/label/fs) — used by `pos system backup` + `pos media sync` |
|
| `lib/usb-lib.sh` | 205 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`; EFI system partitions excluded; picker shows size/label/fs) — used by `pos system backup` + `pos media sync` |
|
||||||
| `lib/share-lib.sh` | 318 | Domain layer for the share suite (usbsrv/smbclient record parsers, folder+mountpoint candidates, remote listings, service/firewall advisories; EOF-safe) + compat shims to `lib/menu-lib.sh` — used by all five `pos share *` tools |
|
| `lib/share-lib.sh` | 318 | Domain layer for the share suite (usbsrv/smbclient record parsers, folder+mountpoint candidates, remote listings, service/firewall advisories; EOF-safe) + compat shims to `lib/menu-lib.sh` — used by all five `pos share *` tools |
|
||||||
| `lib/menu-lib.sh` | 169 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
|
| `lib/menu-lib.sh` | 362 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default via raw-mode bracketed-paste-safe `menu_read_value`; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
|
||||||
| `lib/registry.sh` | 199 | Shared query API for POS tool metadata headers (`# POS_*:`) — `reg_scan`/`reg_list`/`reg_lookup`/`reg_each`/config scope helpers; used by `pos-tree` and `gen-docs.sh` |
|
| `lib/registry.sh` | 199 | Shared query API for POS tool metadata headers (`# POS_*:`) — `reg_scan`/`reg_list`/`reg_lookup`/`reg_each`/config scope helpers; used by `pos-tree` and `gen-docs.sh` |
|
||||||
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||||
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||||
@@ -603,7 +603,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
|
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
|
||||||
<!-- GEN:START filetable -->
|
<!-- GEN:START filetable -->
|
||||||
| `bin/pos` | 302 | CLI dispatcher with smart arg matching + logging + category help |
|
| `bin/pos` | 302 | CLI dispatcher with smart arg matching + logging + category help |
|
||||||
| `bin/pos-ai-alias` | 712 | manage AI agent aliases |
|
| `bin/pos-ai-alias` | 760 | manage AI agent aliases |
|
||||||
| `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) |
|
| `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) |
|
||||||
| `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) |
|
| `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) |
|
||||||
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
|
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
|
||||||
|
|||||||
+61
-13
@@ -34,7 +34,7 @@ _alias_load() {
|
|||||||
_ALIAS_NAMES+=("$_ln")
|
_ALIAS_NAMES+=("$_ln")
|
||||||
_ALIAS_PROVIDERS+=("$_lp")
|
_ALIAS_PROVIDERS+=("$_lp")
|
||||||
_ALIAS_SESSIONS+=("$_ls")
|
_ALIAS_SESSIONS+=("$_ls")
|
||||||
_ALIAS_PROMPTS+=("$_lp2")
|
_ALIAS_PROMPTS+=("$(_alias_prompt_decode "$_lp2")")
|
||||||
_ALIAS_TRUSTED+=("${_lr:-0}")
|
_ALIAS_TRUSTED+=("${_lr:-0}")
|
||||||
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ _alias_save() {
|
|||||||
local i
|
local i
|
||||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||||
printf '%s|%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
printf '%s|%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||||
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}" "${_ALIAS_TRUSTED[$i]:-0}"
|
"${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_encode "${_ALIAS_PROMPTS[$i]}")" "${_ALIAS_TRUSTED[$i]:-0}"
|
||||||
done
|
done
|
||||||
} >"$ENV_FILE"
|
} >"$ENV_FILE"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
@@ -214,14 +214,54 @@ _alias_name_valid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_alias_prompt_truncate() {
|
_alias_prompt_truncate() {
|
||||||
local p="$1"
|
local p="${1//$'\n'/\\n}" max="${2:-42}"
|
||||||
if [ ${#p} -gt 42 ]; then
|
if [ ${#p} -gt "$max" ]; then
|
||||||
printf '%s…' "${p:0:42}"
|
printf '%s…' "${p:0:max}"
|
||||||
else
|
else
|
||||||
printf '%s' "$p"
|
printf '%s' "$p"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Prompt encoding for the line-based env file ────────────────
|
||||||
|
# menu-lib's reader accepts literal multiline prompts; the ENV file is one
|
||||||
|
# record per line, so prompts are escaped on save and unescaped on load:
|
||||||
|
# backslash → \\ newline → \n
|
||||||
|
# ('|' is already rejected at input, so | never needs escaping.)
|
||||||
|
_alias_prompt_encode() {
|
||||||
|
local s="$1" out="" c="" i
|
||||||
|
for ((i = 0; i < ${#s}; i++)); do
|
||||||
|
c="${s:i:1}"
|
||||||
|
if [ "$c" = '\' ]; then
|
||||||
|
out+='\\'
|
||||||
|
elif [ "$c" = $'\n' ]; then
|
||||||
|
out+='\n'
|
||||||
|
else
|
||||||
|
out+="$c"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Decode is order-safe: \\n (escaped newline text) is backslash + n, which
|
||||||
|
# decodes to literal '\n' only after both escapes are consumed in order.
|
||||||
|
_alias_prompt_decode() {
|
||||||
|
local s="$1" out="" c="" n="" i=0
|
||||||
|
while [ "$i" -lt "${#s}" ]; do
|
||||||
|
c="${s:i:1}"
|
||||||
|
if [ "$c" = '\' ] && [ $((i + 1)) -lt "${#s}" ]; then
|
||||||
|
n="${s:i+1:1}"
|
||||||
|
if [ "$n" = 'n' ]; then
|
||||||
|
out+=$'\n'; i=$((i + 2)); continue
|
||||||
|
elif [ "$n" = '\' ]; then
|
||||||
|
out+='\'; i=$((i + 2)); continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
out+="$c"
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Non-interactive output ─────────────────────────────────────
|
# ── Non-interactive output ─────────────────────────────────────
|
||||||
|
|
||||||
# SINGLE alias-table renderer — used by `list` (stdout) and the menu
|
# SINGLE alias-table renderer — used by `list` (stdout) and the menu
|
||||||
@@ -404,8 +444,7 @@ _alias_create() {
|
|||||||
printf ' Create alias '\''%s'\''?\n' "$name"
|
printf ' Create alias '\''%s'\''?\n' "$name"
|
||||||
printf ' Provider: %s\n' "$provider"
|
printf ' Provider: %s\n' "$provider"
|
||||||
printf ' Session: %s\n' "$session"
|
printf ' Session: %s\n' "$session"
|
||||||
local dp="$prompt"
|
local dp="$(_alias_prompt_truncate "$prompt" 50)"
|
||||||
[ ${#dp} -gt 50 ] && dp="${dp:0:50}…"
|
|
||||||
printf ' Prompt: %s\n' "${dp:-<built-in>}"
|
printf ' Prompt: %s\n' "${dp:-<built-in>}"
|
||||||
printf ' Trusted: %s\n' "$([ "$trusted" = "1" ] && echo "yes (auto-execute)" || echo "no (confirm before run)")"
|
printf ' Trusted: %s\n' "$([ "$trusted" = "1" ] && echo "yes (auto-execute)" || echo "no (confirm before run)")"
|
||||||
echo "────────────────────────────────────────────"
|
echo "────────────────────────────────────────────"
|
||||||
@@ -508,12 +547,21 @@ _alias_edit() {
|
|||||||
|
|
||||||
# Edit prompt
|
# Edit prompt
|
||||||
local tmp_prompt=""
|
local tmp_prompt=""
|
||||||
|
local default_prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||||
|
# Display-safe default (newlines → \n, truncated); Enter on it keeps the
|
||||||
|
# FULL original prompt — a long/multiline prompt must never be silently
|
||||||
|
# replaced by its truncated display form.
|
||||||
|
local default_display="$(_alias_prompt_truncate "$default_prompt" 80)"
|
||||||
while true; do
|
while true; do
|
||||||
step 3 4 "System Prompt" >&2
|
step 3 4 "System Prompt" >&2
|
||||||
local default_prompt="${_ALIAS_PROMPTS[$idx]}"
|
if ! tmp_prompt="$(menu_ask_value "System prompt" "$default_display")"; then
|
||||||
[ ${#default_prompt} -gt 80 ] && default_prompt="${default_prompt:0:80}…"
|
# EOF/cancel: empty-answer abort only when there IS a default;
|
||||||
[ -z "$default_prompt" ] && default_prompt=""
|
# Enter on an empty original prompt keeps it empty and continues.
|
||||||
tmp_prompt="$(menu_ask_value "System prompt" "$default_prompt")" || return 0
|
[ -z "$default_prompt" ] && tmp_prompt="" || return 0
|
||||||
|
fi
|
||||||
|
if [ "$tmp_prompt" = "$default_display" ]; then
|
||||||
|
tmp_prompt="$default_prompt" # Enter → keep the full original
|
||||||
|
fi
|
||||||
if [[ "$tmp_prompt" == *'|'* ]]; then
|
if [[ "$tmp_prompt" == *'|'* ]]; then
|
||||||
warn "System prompt must not contain '|' characters" >&2
|
warn "System prompt must not contain '|' characters" >&2
|
||||||
tmp_prompt=""; continue
|
tmp_prompt=""; continue
|
||||||
@@ -559,9 +607,9 @@ _alias_edit() {
|
|||||||
[ "$new_trusted" = "${_ALIAS_TRUSTED[$idx]:-0}" ] && tag_t="(unchanged)" || tag_t="(changed)"
|
[ "$new_trusted" = "${_ALIAS_TRUSTED[$idx]:-0}" ] && tag_t="(unchanged)" || tag_t="(changed)"
|
||||||
printf ' Provider: %-12s %s\n' "$new_provider" "$tag_p"
|
printf ' Provider: %-12s %s\n' "$new_provider" "$tag_p"
|
||||||
printf ' Session: %-12s %s\n' "$new_session" "$tag_s"
|
printf ' Session: %-12s %s\n' "$new_session" "$tag_s"
|
||||||
local dp="$new_prompt"
|
local dp="$(_alias_prompt_truncate "$new_prompt" 40)"
|
||||||
[ -z "$dp" ] && dp="<built-in>"
|
[ -z "$dp" ] && dp="<built-in>"
|
||||||
printf ' Prompt: %s %s\n' "${dp:0:40}" "$tag_pr"
|
printf ' Prompt: %s %s\n' "$dp" "$tag_pr"
|
||||||
printf ' Trusted: %-12s %s\n' "$([ "$new_trusted" = "1" ] && echo "yes" || echo "no")" "$tag_t"
|
printf ' Trusted: %-12s %s\n' "$([ "$new_trusted" = "1" ] && echo "yes" || echo "no")" "$tag_t"
|
||||||
echo "────────────────────────────────────────────"
|
echo "────────────────────────────────────────────"
|
||||||
} >&2
|
} >&2
|
||||||
|
|||||||
+198
-5
@@ -25,6 +25,8 @@
|
|||||||
# menu_run <title> <item...> numbered menu loop → chosen index
|
# menu_run <title> <item...> numbered menu loop → chosen index
|
||||||
# menu_pick <prompt> <item...> type-to-filter picker → chosen index
|
# menu_pick <prompt> <item...> type-to-filter picker → chosen index
|
||||||
# menu_ask_value <label> [default] prompted value → entered text
|
# menu_ask_value <label> [default] prompted value → entered text
|
||||||
|
# menu_read_value <label> raw-mode bracketed-paste reader
|
||||||
|
# menu_redraw internal redraw (menu_read_value only)
|
||||||
|
|
||||||
# ── Colors (guarded fallbacks; a sourced common.sh wins) ──────
|
# ── Colors (guarded fallbacks; a sourced common.sh wins) ──────
|
||||||
CYAN="${CYAN:-}"
|
CYAN="${CYAN:-}"
|
||||||
@@ -150,15 +152,206 @@ menu_pick() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Raw-mode value reader (bracketed-paste safe) ───────────────
|
||||||
|
# Reads ONE value from the terminal in raw mode with bracketed paste enabled,
|
||||||
|
# so a multi-line CTRL+V paste is inserted LITERALLY — embedded newlines are
|
||||||
|
# data, never line terminators — and can never leak into the shell or a later
|
||||||
|
# prompt as leftover keystrokes. A plain bash `read` is line-oriented: it
|
||||||
|
# consumes only the first pasted line and the remaining lines sit in the tty
|
||||||
|
# queue, where the next prompt (or the shell after this script exits) treats
|
||||||
|
# them as input/commands. That is the paste bug this reader exists to prevent.
|
||||||
|
#
|
||||||
|
# Editing (single-line typing behaves like a normal prompt):
|
||||||
|
# Enter submit the value (outside a paste)
|
||||||
|
# Backspace/DEL delete the character before the cursor
|
||||||
|
# Left/Right move the cursor; Home/End jump to start/end
|
||||||
|
# Delete delete the character at the cursor
|
||||||
|
# Ctrl-U clear the whole value
|
||||||
|
# Ctrl-D (empty) EOF — cancel · Ctrl-C/Z/\ — cancel · Up/Down — ignored
|
||||||
|
# Inside a bracketed paste the above are inert: text (incl. newlines) is
|
||||||
|
# inserted verbatim until the paste-end marker; a real Enter then submits.
|
||||||
|
#
|
||||||
|
# Display goes to stderr so callers may command-substitute the result:
|
||||||
|
# rc 0 value on stdout · rc 1 cancel/EOF/non-tty.
|
||||||
|
menu_read_value() {
|
||||||
|
local label="$1"
|
||||||
|
local val="" state="" chunk="" ch="" esc="" seq="" esc_c=""
|
||||||
|
local paste=0 pos=0 submit=0 i=0 n=0
|
||||||
|
|
||||||
|
if ! state="$(stty -g 2>/dev/null)"; then
|
||||||
|
# not a terminal — plain stdin read; no paste protection is possible
|
||||||
|
IFS= read -r val || return 1
|
||||||
|
[ -n "$val" ] && printf '%s' "$val"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! stty -icanon -echo -isig min 1 time 0 2>/dev/null; then
|
||||||
|
stty "$state" 2>/dev/null
|
||||||
|
IFS= read -r val || return 1
|
||||||
|
[ -n "$val" ] && printf '%s' "$val"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local restore
|
||||||
|
restore() {
|
||||||
|
stty "$state" 2>/dev/null
|
||||||
|
printf '\033[?2004l' >&2
|
||||||
|
}
|
||||||
|
trap 'restore; trap - INT TERM; return 1' INT TERM
|
||||||
|
|
||||||
|
printf '\033[?2004h' >&2
|
||||||
|
printf '%s: ' "$label" >&2
|
||||||
|
|
||||||
|
# Next input byte as a 2-hex-digit string, returned via nameref. Uses
|
||||||
|
# dd|od, NOT bash's read builtin: read's tty path self-interrupts on an ETX
|
||||||
|
# byte even with ISIG disabled (SIGINTs the whole script on Ctrl-C, killing
|
||||||
|
# a cmdsubst caller). One dd per input burst (VMIN=1 returns all queued
|
||||||
|
# bytes), so pastes cost O(chunks), not O(per-byte forks). Runs in-place
|
||||||
|
# (never in a $( ) subshell) so its chunk/offset state persists.
|
||||||
|
# byte <hexvar> — rc 0 = byte in hexvar, rc 1 = EOF/short.
|
||||||
|
local byte
|
||||||
|
byte() {
|
||||||
|
local -n _hex="$1"
|
||||||
|
if [ "$i" -ge "$n" ]; then
|
||||||
|
chunk="$(dd bs=4096 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
||||||
|
[ -n "$chunk" ] || return 1
|
||||||
|
n=${#chunk}
|
||||||
|
i=0
|
||||||
|
fi
|
||||||
|
_hex="${chunk:i:2}"
|
||||||
|
i=$((i + 2))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while byte ch; do
|
||||||
|
case "$ch" in
|
||||||
|
1b)
|
||||||
|
seq=""
|
||||||
|
while byte esc; do
|
||||||
|
printf -v esc_c '%b' "\\x$esc"
|
||||||
|
seq+="$esc_c"
|
||||||
|
case "$esc_c" in
|
||||||
|
[A-Za-z~]) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "$seq" in
|
||||||
|
'[200~') paste=1 ;;
|
||||||
|
'[201~') paste=0 ;;
|
||||||
|
'[C') [ "$pos" -lt "${#val}" ] && { pos=$((pos + 1)); menu_redraw "$label" "$val" "$pos"; } ;;
|
||||||
|
'[D') [ "$pos" -gt 0 ] && { pos=$((pos - 1)); menu_redraw "$label" "$val" "$pos"; } ;;
|
||||||
|
'[H' | '[1~') pos=0; menu_redraw "$label" "$val" "$pos" ;;
|
||||||
|
'[F' | '[4~') pos=${#val}; menu_redraw "$label" "$val" "$pos" ;;
|
||||||
|
'[3~')
|
||||||
|
if [ "$pos" -lt "${#val}" ]; then
|
||||||
|
val="${val:0:pos}${val:pos+1}"
|
||||||
|
menu_redraw "$label" "$val" "$pos"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
'[A' | '[B') : ;; # up/down: no history — ignore
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
0a | 0d)
|
||||||
|
if [ "$paste" -eq 1 ]; then
|
||||||
|
# newline inside a paste is literal data (paste as text);
|
||||||
|
# echo the line break so CRLF pastes render at col 0
|
||||||
|
printf -v ch '%b' "\\x$ch"
|
||||||
|
val="${val:0:pos}${ch}${val:pos}"
|
||||||
|
pos=$((pos + 1))
|
||||||
|
printf '%s' "$ch" >&2
|
||||||
|
else
|
||||||
|
submit=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
7f | 08) # Backspace/DEL
|
||||||
|
if [ "$pos" -gt 0 ]; then
|
||||||
|
val="${val:0:pos-1}${val:pos}"
|
||||||
|
pos=$((pos - 1))
|
||||||
|
menu_redraw "$label" "$val" "$pos"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
03 | 1a | 1c) # Ctrl-C / Ctrl-Z / Ctrl-\ — cancel
|
||||||
|
submit=0
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
04) # Ctrl-D: EOF on empty → cancel
|
||||||
|
if [ -z "$val" ]; then
|
||||||
|
submit=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
15) # Ctrl-U: clear
|
||||||
|
val=""; pos=0
|
||||||
|
menu_redraw "$label" "$val" "$pos"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf -v ch '%b' "\\x$ch"
|
||||||
|
val="${val:0:pos}${ch}${val:pos}"
|
||||||
|
pos=$((pos + 1))
|
||||||
|
if [ "$pos" -eq "${#val}" ]; then
|
||||||
|
printf '%s' "$ch" >&2 # append in place — fast path
|
||||||
|
else
|
||||||
|
menu_redraw "$label" "$val" "$pos"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
trap - INT TERM
|
||||||
|
restore
|
||||||
|
printf '\n' >&2
|
||||||
|
if [ "$submit" -eq 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf '%s' "$val"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Internal: redraw the whole input block (menu_read_value only) ──
|
||||||
|
# The value may span several terminal rows (multiline paste); redraw clears
|
||||||
|
# below the block start and reprints label + value, then repositions the
|
||||||
|
# cursor to (row, col) of $3. Columns are counted in characters — wide CJK
|
||||||
|
# glyphs can be off by one column (display-only; the stored value is exact).
|
||||||
|
menu_redraw() {
|
||||||
|
local label="$1" val="$2" pos="$3"
|
||||||
|
local nl="" r="" c="" last="" ec="" d="" up=""
|
||||||
|
nl="${val//[^$'\n']/}"
|
||||||
|
[ "${#nl}" -gt 0 ] && printf '\033[%dA' "${#nl}" >&2
|
||||||
|
printf '\r\033[J' >&2
|
||||||
|
printf '%s: ' "$label" >&2
|
||||||
|
printf '%s' "$val" >&2
|
||||||
|
# target row/col of the cursor
|
||||||
|
last="${val:0:pos}"
|
||||||
|
r="${last//[^$'\n']/}"; r="${#r}"
|
||||||
|
last="${last##*$'\n'}"
|
||||||
|
c="${#last}"
|
||||||
|
# current cursor (end of block): end row = nl count; end col = after last
|
||||||
|
# newline (or 0 when the value ends with a newline)
|
||||||
|
ec=0; last="${val##*$'\n'}"
|
||||||
|
case "$val" in
|
||||||
|
*$'\n') ec=0 ;;
|
||||||
|
*) ec="${#last}" ;;
|
||||||
|
esac
|
||||||
|
[ "${#nl}" -gt "$r" ] && printf '\033[%dA' $(( ${#nl} - r )) >&2
|
||||||
|
d=$(( c - ec ))
|
||||||
|
if [ "$d" -gt 0 ]; then
|
||||||
|
printf '\033[%dC' "$d" >&2
|
||||||
|
elif [ "$d" -lt 0 ]; then
|
||||||
|
printf '\033[%dD' $(( -d )) >&2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ── Prompted value with optional default ───────────────────────
|
# ── Prompted value with optional default ───────────────────────
|
||||||
# Prints "<label> [<default>]: " (read -p sends prompts to stderr) and echoes
|
# Prints "<label> [<default>]: " and echoes the entered value or the default
|
||||||
# the entered value or the default when the answer is empty.
|
# when the answer is empty. Uses the bracketed-paste-safe reader, so pasting
|
||||||
# rc 0 value on stdout · rc 1 EOF, or empty answer with no default.
|
# text — including multi-line pastes — inserts it literally instead of letting
|
||||||
|
# leftover lines escape to the shell as commands.
|
||||||
|
# rc 0 value on stdout · rc 1 EOF/cancel, or empty answer with no default.
|
||||||
menu_ask_value() {
|
menu_ask_value() {
|
||||||
local label="$1" def="${2:-}" val pr="$1"
|
local label="$1" def="${2:-}" val pr="$1"
|
||||||
[ -n "$def" ] && pr="$pr [$def]"
|
[ -n "$def" ] && pr="$pr [$def]"
|
||||||
if ! read -rp "${pr}: " val; then
|
if ! val="$(menu_read_value "$pr")"; then
|
||||||
return 1 # EOF — cancel
|
return 1 # EOF / cancel
|
||||||
fi
|
fi
|
||||||
if [ -z "$val" ]; then
|
if [ -z "$val" ]; then
|
||||||
[ -n "$def" ] || return 1
|
[ -n "$def" ] || return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user