From 48c27f697fac5659f7507994f7416e3b30ed75b6 Mon Sep 17 00:00:00 2001 From: Matt White Date: Mon, 4 May 2026 18:10:17 -0600 Subject: [PATCH 1/2] =?UTF-8?q?fix:=201.0=20polish=20=E2=80=94=20setup=20w?= =?UTF-8?q?izard=20UX,=20prompt=20cancel,=20project=20depth,=20docker=20sp?= =?UTF-8?q?in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundled post-launch fixes uncovered by a real-user dry-run: setup customize picker (Issue 1, 2) - Header now reads "Pick keys to customize (SPACE to mark, ENTER to confirm)" so the multi-select contract is obvious — same hazard as PR #41's sync picker. Users were habitually pressing ENTER on the first row (single- select muscle memory) and silently advancing with no overrides. - Detected-configuration table now carries a DESCRIPTION column so users can read what each key does without entering the picker. Polished the schema descriptions so bool keys state what `true` means explicitly (the user's reported confusion: "what is aliases_auto_source"). setup sync between existing accounts (Issue 3) - Cross-account sync was offered only after the wizard added a NEW account. Re-running `ckipper setup` on an established multi-account install never surfaced the sync feature. Added a top-level offer that fires when accounts >= 2. docker image build via setup (Issue 4) - `_core_prompt_spin` wraps `gum spin -- "$@"`, which execs argv as a binary. Passing a shell function failed with "executable file not found in $PATH". Drop the spinner — the build streams its own progress over ~5 min, which the user wants to see anyway. `_core_prompt_input` cancel propagation (Issue 5a) - `gum input` exits non-zero with empty stdout on Esc / Ctrl-C, but the helper substituted the default in both the cancel and empty-submit cases. The launcher branch prompt then created a worktree on `feature/dev` even when the user pressed Ctrl-X to back out. - Now propagates rc: cancel → return non-zero, no stdout. Empty submit → rc=0, default echoed (existing contract preserved). - Updated setup/dispatcher.zsh callers (account-name, customize-loop) to skip on cancellation rather than commit empty values. launcher project autodetect depth (Issue 5b) - Maxdepth was 3, but the existing comment claimed support for the `///` layout — `.git` sits one level deeper than the repo root, so depth 4 is needed. The user had a real project (~/Developer/AFF/happyhippo/hippo-vmail) hidden by the off-by-one. ~5 ms cost on the author's tree. Tests - 510/510 shell tests pass, including the new regressions: * _core_prompt_input returns non-zero with no stdout on EOF * _ckipper_setup_offer_existing_sync skips < 2 accounts * _ckipper_setup_offer_existing_sync invokes / skips sync_dispatch --- lib/core/prompt.zsh | 15 ++++++++-- lib/core/prompt_test.bats | 14 ++++++++++ lib/core/schema.zsh | 24 ++++++++-------- lib/launcher/menu.zsh | 10 ++++--- lib/setup/dispatcher.zsh | 45 ++++++++++++++++++++++++------ lib/setup/dispatcher_test.bats | 51 ++++++++++++++++++++++++++++++++++ lib/setup/prompts.zsh | 24 +++++++++++----- 7 files changed, 149 insertions(+), 34 deletions(-) diff --git a/lib/core/prompt.zsh b/lib/core/prompt.zsh index f6ccf9f..b6f4169 100644 --- a/lib/core/prompt.zsh +++ b/lib/core/prompt.zsh @@ -25,18 +25,27 @@ _core_prompt_use_gum() { # Prompt for a free-form string. Returns the entered value, or the supplied # default when input is empty. # +# Distinguishes "user submitted empty" (rc=0, value=default) from "user +# cancelled" (rc != 0, no stdout). `gum input` exits non-zero on Esc/Ctrl-C, +# and we propagate that so callers can distinguish cancellation. Previously +# we collapsed both into "echo default", which meant a cancelled launcher +# branch prompt silently created a worktree on the default branch name. +# # Args: $1 — label shown to the user; $2 — default value used on empty input. -# Returns: 0 always; prints the resolved value to stdout. +# Returns: 0 on submit (including empty submit); non-zero on cancellation. +# Prints the resolved value to stdout on success; nothing on cancel. _core_prompt_input() { local label="$1" default="$2" if _core_prompt_use_gum; then - local out + local out rc out=$(gum input --placeholder "$default" --prompt "$label > ") + rc=$? + (( rc != 0 )) && return $rc echo "${out:-$default}" return 0 fi local val="" - read -r "val?$label [$default]: " + read -r "val?$label [$default]: " || return $? echo "${val:-$default}" } diff --git a/lib/core/prompt_test.bats b/lib/core/prompt_test.bats index 814f171..ecec9f1 100644 --- a/lib/core/prompt_test.bats +++ b/lib/core/prompt_test.bats @@ -46,6 +46,20 @@ _run_prompt() { [ "$output" = "thedefault" ] } +# Regression: previously cancellation (EOF / Ctrl-C / Esc on the gum form) +# was indistinguishable from "user submitted empty" because both echoed +# the default. The launcher's branch prompt then created a worktree on +# `feature/dev` even when the user pressed Ctrl-X to back out. The fix +# propagates rc from gum / read so callers can distinguish cancel via rc. +@test "_core_prompt_input returns non-zero with no stdout when read sees EOF" { + run env CKIPPER_NO_GUM=1 PATH="$PATH" \ + zsh -c "source \"$REPO_ROOT/lib/core/prompt.zsh\"; \ + _core_prompt_input \"Q\" \"thedefault\" 2>/dev/null" `, `~/Developer//`, -# `~/Developer///`) without scanning entire user homedirs. -readonly _CKIPPER_LAUNCHER_PROJECTS_MAXDEPTH=3 +# `find -maxdepth` value for project discovery. The repository's `.git` +# directory sits one level below the repo root, so to surface +# `////.git` we need depth 4. Anything +# deeper than that is almost always a vendored sub-repo or a demo project, +# so depth 4 is the sweet spot between coverage and scan time. +readonly _CKIPPER_LAUNCHER_PROJECTS_MAXDEPTH=4 # Print the launcher banner: a styled "Ckipper" header, the product tagline, # and a trailing blank line that separates the banner from whatever prompt or diff --git a/lib/setup/dispatcher.zsh b/lib/setup/dispatcher.zsh index b408e29..229e9c0 100644 --- a/lib/setup/dispatcher.zsh +++ b/lib/setup/dispatcher.zsh @@ -4,8 +4,7 @@ # # Depends on: # - lib/core/style.zsh (`_core_style_header`) -# - lib/core/prompt.zsh (`_core_prompt_confirm`, `_core_prompt_input`, -# `_core_prompt_spin`) +# - lib/core/prompt.zsh (`_core_prompt_confirm`, `_core_prompt_input`) # - lib/setup/prereqs.zsh (`_ckipper_setup_prereqs`) # - lib/setup/prompts.zsh (`_ckipper_setup_prompts_*`) # - lib/setup/apply.zsh (`_ckipper_setup_apply_global`, @@ -37,10 +36,27 @@ _ckipper_setup() { echo "Using current values." fi _ckipper_setup_offer_account + _ckipper_setup_offer_existing_sync _ckipper_setup_offer_image_build _ckipper_setup_print_completion_summary } +# Offer a between-accounts sync when the user has 2+ accounts already and +# the wizard's add-account step did not just run one (the post-add path +# already offers the sync inline). Without this, a re-run of `ckipper setup` +# on an established multi-account install never surfaces the sync feature. +# +# Returns: 0 always. +_ckipper_setup_offer_existing_sync() { + local count + count=$(jq -r '.accounts | length' "$CKIPPER_REGISTRY" 2>/dev/null || echo 0) + (( count < 2 )) && return 0 + if ! _core_prompt_confirm "Sync settings between two existing accounts?"; then + return 0 + fi + _ckipper_account_sync_dispatch +} + # Print the post-setup hint block: review-settings command, two ways to launch # Claude (per-account aliases or `ckipper run`), and the bare-`ck` menu. # Extracted so `_ckipper_setup` stays under the 25-line cap. @@ -89,10 +105,16 @@ _ckipper_setup_run_customize_loop() { local -a picked picked=( ${(f)"$(_ckipper_setup_prompts_pick_keys)"} ) typeset -A updates - local key + local key value for key in "${picked[@]}"; do [[ -z "$key" ]] && continue - updates[$key]=$(_ckipper_setup_prompts_one_key "$key") + # If the per-key prompt returns non-zero the user cancelled it + # (Esc/Ctrl-C on gum). Skip the key rather than writing an empty + # override, which would silently blank out the value. + if ! value=$(_ckipper_setup_prompts_one_key "$key"); then + continue + fi + updates[$key]="$value" done _ckipper_setup_apply_global updates } @@ -128,7 +150,9 @@ _ckipper_setup_offer_account() { # Returns: 0 always (per-step failures are surfaced via the underlying calls). _ckipper_setup_add_account() { local name - name=$(_core_prompt_input "Account name" "$_CKIPPER_SETUP_DEFAULT_ACCOUNT_NAME") + if ! name=$(_core_prompt_input "Account name" "$_CKIPPER_SETUP_DEFAULT_ACCOUNT_NAME"); then + return 0 + fi _ckipper_account_add "$name" || return 0 typeset -A prefs _ckipper_setup_collect_account_prefs "$name" @@ -189,13 +213,16 @@ _ckipper_setup_collect_account_prefs() { "Forward host ~/.ssh into '$account' containers?" } -# Offer to build/rebuild the ckipper-dev Docker image now. Wraps the build in -# a gum spinner when available; the underlying helper streams docker output -# directly when running without gum. +# Offer to build/rebuild the ckipper-dev Docker image now. +# +# We invoke the build helper directly rather than wrapping it in a spinner. +# `gum spin -- ` execs its argv as a binary, so passing a shell function +# fails with "executable file not found in $PATH". The build also streams +# its own progress over ~5 min, which the user wants to see. # # Returns: 0 always. _ckipper_setup_offer_image_build() { if _core_prompt_confirm "Build the Docker image now? (slow; ~5 min)"; then - _core_prompt_spin "Building ckipper-dev image" _ckipper_worktree_build_image + _ckipper_worktree_build_image fi } diff --git a/lib/setup/dispatcher_test.bats b/lib/setup/dispatcher_test.bats index 9f8ebd4..bce5545 100644 --- a/lib/setup/dispatcher_test.bats +++ b/lib/setup/dispatcher_test.bats @@ -116,3 +116,54 @@ JSON [ "$status" -eq 0 ] [[ "$output" == *"STUB-BUILD"* ]] } + +# Regression: setup previously offered cross-account sync only after the +# user added a NEW account in the wizard. A user with 2+ existing accounts +# who declined "Add another?" never saw the sync feature surfaced. The +# behavioral signal we can assert (prompts written by zsh's `read "ans?…"` +# are suppressed when stdin is non-TTY, so we can't grep the label) is that +# the dispatch helper IS invoked on accept and SKIPPED otherwise. +@test "_ckipper_setup_offer_existing_sync skips when fewer than 2 accounts" { + cat >"$CKIPPER_REGISTRY" <<'JSON' +{"version":2,"default":"a","accounts":{"a":{"config_dir":"/x","keychain_service":null,"registered_at":"t","preferences":{}}}} +JSON + + _run_setup $'y\n' ' + _ckipper_account_sync_dispatch() { echo "STUB-SYNC"; } + _ckipper_setup_offer_existing_sync' + + [ "$status" -eq 0 ] + [[ "$output" != *"STUB-SYNC"* ]] +} + +@test "_ckipper_setup_offer_existing_sync invokes sync_dispatch on yes" { + cat >"$CKIPPER_REGISTRY" <<'JSON' +{"version":2,"default":"a","accounts":{ + "a":{"config_dir":"/x","keychain_service":null,"registered_at":"t","preferences":{}}, + "b":{"config_dir":"/y","keychain_service":null,"registered_at":"t","preferences":{}} +}} +JSON + + _run_setup $'y\n' ' + _ckipper_account_sync_dispatch() { echo "STUB-SYNC"; } + _ckipper_setup_offer_existing_sync' + + [ "$status" -eq 0 ] + [[ "$output" == *"STUB-SYNC"* ]] +} + +@test "_ckipper_setup_offer_existing_sync skips sync_dispatch on no" { + cat >"$CKIPPER_REGISTRY" <<'JSON' +{"version":2,"default":"a","accounts":{ + "a":{"config_dir":"/x","keychain_service":null,"registered_at":"t","preferences":{}}, + "b":{"config_dir":"/y","keychain_service":null,"registered_at":"t","preferences":{}} +}} +JSON + + _run_setup $'n\n' ' + _ckipper_account_sync_dispatch() { echo "STUB-SYNC"; } + _ckipper_setup_offer_existing_sync' + + [ "$status" -eq 0 ] + [[ "$output" != *"STUB-SYNC"* ]] +} diff --git a/lib/setup/prompts.zsh b/lib/setup/prompts.zsh index a6199c9..1f4d9cc 100644 --- a/lib/setup/prompts.zsh +++ b/lib/setup/prompts.zsh @@ -30,15 +30,24 @@ readonly _CKIPPER_SETUP_PROMPTS_NO_GUM_SENTINEL="1" # Header rendered above the summary table. readonly _CKIPPER_SETUP_PROMPTS_HEADER="Detected configuration" +# Header for the multi-select picker. Mentions SPACE explicitly because the +# preceding y/N "customize?" prompt and the source-account picker are both +# single-select Enter — without the hint, users press Enter on the first +# row and silently advance with no overrides. +readonly _CKIPPER_SETUP_PROMPTS_PICKER_HEADER="Pick keys to customize (SPACE to mark, ENTER to confirm)" + # Pipe-separated row builder for the summary table. Resolves the effective -# value via _core_config_get and the source marker via _core_config_read_global -# (empty return ⇒ default; otherwise ⇒ user override). +# value via _core_config_get, the source marker via _core_config_read_global +# (empty return ⇒ default; otherwise ⇒ user override), and the description +# from the schema. Including the description here so the user can scan the +# table and know what each key does without having to drill into a picker +# for it (e.g. `aliases_auto_source` is opaque without context). # # Args: $1 — schema key. -# Returns: 0 always; prints "||" to stdout. +# Returns: 0 always; prints "|||" to stdout. _ckipper_setup_prompts_summary_row() { local key="$1" - local value source raw + local value source raw description value=$(_core_config_get "$key") raw=$(_core_config_read_global "$key") if [[ -z "$raw" ]]; then @@ -46,7 +55,8 @@ _ckipper_setup_prompts_summary_row() { else source="$_CKIPPER_SETUP_PROMPTS_SOURCE_USER" fi - printf '%s|%s|%s\n' "$key" "$value" "$source" + description="${_CKIPPER_SCHEMA_DESCRIPTION[$key]}" + printf '%s|%s|%s|%s\n' "$key" "$value" "$source" "$description" } # Print every global-scoped key one per line in lexical order. Used by the @@ -74,7 +84,7 @@ _ckipper_setup_prompts_summary() { while IFS= read -r key; do _ckipper_setup_prompts_summary_row "$key" done < <(_ckipper_setup_prompts_global_keys) - } | _core_style_table SETTING VALUE SOURCE + } | _core_style_table SETTING VALUE SOURCE DESCRIPTION _core_style_divider } @@ -107,7 +117,7 @@ _ckipper_setup_prompts_pick_keys_fallback() { _ckipper_setup_prompts_pick_keys() { if _ckipper_setup_prompts_use_gum; then _ckipper_setup_prompts_global_keys \ - | gum choose --no-limit --header "Pick keys to customize" + | gum choose --no-limit --header "$_CKIPPER_SETUP_PROMPTS_PICKER_HEADER" return 0 fi _ckipper_setup_prompts_pick_keys_fallback From 6dbc92bbadf1a81240961e0beaec5afcb5a325cd Mon Sep 17 00:00:00 2001 From: Matt White Date: Mon, 4 May 2026 18:29:21 -0600 Subject: [PATCH 2/2] fix(setup,config,core): address PR #43 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups uncovered by self-review: 1. Detected-config DESCRIPTION column was unrenderable (Critical) The 4th column added in this PR overflowed the fixed-width table (`%-22s` does not truncate; descriptions run 90+ chars), making the table effectively unreadable. Render descriptions on the line below each row instead — keeps the three-column alignment intact and gives the description unlimited room. Picker labels also enriched with `key — description` so the user sees what each setting does while choosing what to customize, with awk extracting the bare key on the way out. 2. `lib/config/set.zsh` missed `_core_prompt_input` cancel audit (Important). With the new rc-propagating contract, an Esc/Ctrl-C during `ckipper config set ` (no value arg) would silently blank the key. Added `if ! value=$(...); then return 1` guard and a regression test that pipes EOF and asserts the writer is bypassed. 3. `_core_prompt_spin` was orphaned (Important). The only caller (setup) was removed in this PR. Per CLAUDE.md "no half-finished implementations" — the function and its two tests are gone. Its `gum spin -- $@` design was incompatible with shell functions, which is the predominant pattern in this codebase, so re-using it later would require redesigning it anyway. Tests: 510/510 pass (net unchanged: -2 spin tests, +1 description-row, +1 config-set cancel). Lint clean. --- lib/config/dispatcher_test.bats | 36 +++++++++++++++++++++++++++++ lib/config/set.zsh | 7 +++++- lib/core/prompt.zsh | 17 -------------- lib/core/prompt_test.bats | 12 ---------- lib/setup/prompts.zsh | 41 +++++++++++++++++++-------------- lib/setup/prompts_test.bats | 13 +++++++++++ 6 files changed, 79 insertions(+), 47 deletions(-) diff --git a/lib/config/dispatcher_test.bats b/lib/config/dispatcher_test.bats index 1045ea5..f5b9567 100644 --- a/lib/config/dispatcher_test.bats +++ b/lib/config/dispatcher_test.bats @@ -78,6 +78,42 @@ _run_config_dispatch() { [ "$status" -ne 0 ] } +# Regression: when `ckipper config set ` is invoked WITHOUT a value, +# the handler prompts via _core_prompt_input. Cancellation (Esc/Ctrl-C on +# gum, EOF on the read fallback) must abort the write rather than commit +# an empty value — otherwise the user's only out is to silently blank the +# key. _core_prompt_input now returns non-zero on cancel; this test pins +# the abort-on-cancel behavior in `_ckipper_config_set`. +@test "config set aborts the write when the value prompt is cancelled" { + run env HOME="$TMP_HOME" \ + CKIPPER_DIR="$CKIPPER_DIR" \ + CKIPPER_REGISTRY="$CKIPPER_REGISTRY" \ + CKIPPER_REGISTRY_VERSION="${CKIPPER_REGISTRY_VERSION:-2}" \ + CKIPPER_NO_GUM=1 \ + PATH="$PATH" \ + zsh -c " + source \"$REPO_ROOT/lib/core/schema.zsh\" + source \"$REPO_ROOT/lib/core/config.zsh\" + source \"$REPO_ROOT/lib/core/registry.zsh\" + source \"$REPO_ROOT/lib/core/fuzzy.zsh\" + source \"$REPO_ROOT/lib/core/style.zsh\" + source \"$REPO_ROOT/lib/core/help.zsh\" + source \"$REPO_ROOT/lib/core/prompt.zsh\" + source \"$REPO_ROOT/lib/config/set.zsh\" + source \"$REPO_ROOT/lib/config/get.zsh\" + _ckipper_config_set default_branch 2>/dev/null + echo \"set_rc=\$?\" + _ckipper_config_get default_branch + " /dev/null || return 1; } if [[ "$_CKIPPER_CONFIG_SET_HAS_VALUE" != "true" ]]; then local prompt_label="Value for $key (${_CKIPPER_SCHEMA_TYPE[$key]})" - value=$(_core_prompt_input "$prompt_label" "") + # Cancellation must abort the write, not commit an empty value. + # _core_prompt_input returns non-zero on Esc/Ctrl-C; without this + # check the user pressing cancel would silently blank the key. + if ! value=$(_core_prompt_input "$prompt_label" ""); then + return 1 + fi fi _core_config_set "$key" "$value" "$account" } diff --git a/lib/core/prompt.zsh b/lib/core/prompt.zsh index b6f4169..df7f0ba 100644 --- a/lib/core/prompt.zsh +++ b/lib/core/prompt.zsh @@ -101,20 +101,3 @@ _core_prompt_choose() { (( choice >= 1 && choice <= $# )) || return 1 echo "${@[choice]}" } - -# Run a command with a spinner indicator. Forwards the command's exit status. -# -# Args: $1 — label shown alongside the spinner; $2..$N — command and args to -# execute. The label is consumed by this function and never reaches the -# wrapped command. -# Returns: the exit status of the wrapped command. -_core_prompt_spin() { - local label="$1" - shift - if _core_prompt_use_gum; then - gum spin --spinner dot --title "$label" -- "$@" - return $? - fi - echo "$label..." >&2 - "$@" -} diff --git a/lib/core/prompt_test.bats b/lib/core/prompt_test.bats index ecec9f1..0618e2f 100644 --- a/lib/core/prompt_test.bats +++ b/lib/core/prompt_test.bats @@ -121,15 +121,3 @@ _run_prompt() { [ "$status" -eq 1 ] } - -@test "_core_prompt_spin runs the command and forwards exit status 0" { - _run_prompt "" '_core_prompt_spin "Working" true' - - [ "$status" -eq 0 ] -} - -@test "_core_prompt_spin forwards non-zero exit status" { - _run_prompt "" '_core_prompt_spin "Working" false' - - [ "$status" -eq 1 ] -} diff --git a/lib/setup/prompts.zsh b/lib/setup/prompts.zsh index 1f4d9cc..f988f3a 100644 --- a/lib/setup/prompts.zsh +++ b/lib/setup/prompts.zsh @@ -37,17 +37,14 @@ readonly _CKIPPER_SETUP_PROMPTS_HEADER="Detected configuration" readonly _CKIPPER_SETUP_PROMPTS_PICKER_HEADER="Pick keys to customize (SPACE to mark, ENTER to confirm)" # Pipe-separated row builder for the summary table. Resolves the effective -# value via _core_config_get, the source marker via _core_config_read_global -# (empty return ⇒ default; otherwise ⇒ user override), and the description -# from the schema. Including the description here so the user can scan the -# table and know what each key does without having to drill into a picker -# for it (e.g. `aliases_auto_source` is opaque without context). +# value via _core_config_get and the source marker via _core_config_read_global +# (empty return ⇒ default; otherwise ⇒ user override). # # Args: $1 — schema key. -# Returns: 0 always; prints "|||" to stdout. +# Returns: 0 always; prints "||" to stdout. _ckipper_setup_prompts_summary_row() { local key="$1" - local value source raw description + local value source raw value=$(_core_config_get "$key") raw=$(_core_config_read_global "$key") if [[ -z "$raw" ]]; then @@ -55,8 +52,7 @@ _ckipper_setup_prompts_summary_row() { else source="$_CKIPPER_SETUP_PROMPTS_SOURCE_USER" fi - description="${_CKIPPER_SCHEMA_DESCRIPTION[$key]}" - printf '%s|%s|%s|%s\n' "$key" "$value" "$source" "$description" + printf '%s|%s|%s\n' "$key" "$value" "$source" } # Print every global-scoped key one per line in lexical order. Used by the @@ -79,12 +75,19 @@ _ckipper_setup_prompts_global_keys() { # Returns: 0 always. _ckipper_setup_prompts_summary() { _core_style_header "$_CKIPPER_SETUP_PROMPTS_HEADER" - local key - { - while IFS= read -r key; do - _ckipper_setup_prompts_summary_row "$key" - done < <(_ckipper_setup_prompts_global_keys) - } | _core_style_table SETTING VALUE SOURCE DESCRIPTION + _core_style_table_print_row "SETTING|VALUE|SOURCE" + # Per-key block: aligned three-column row + indented description on the + # next line. We render this manually rather than feeding a 4-column row + # to `_core_style_table` because the schema descriptions can run 90+ + # characters and the fixed-width column padding (22 chars) would leave + # them overflowing across the screen and breaking column alignment for + # every other column. + local key description + while IFS= read -r key; do + _core_style_table_print_row "$(_ckipper_setup_prompts_summary_row "$key")" + description="${_CKIPPER_SCHEMA_DESCRIPTION[$key]}" + [[ -n "$description" ]] && echo " $description" + done < <(_ckipper_setup_prompts_global_keys) _core_style_divider } @@ -116,8 +119,12 @@ _ckipper_setup_prompts_pick_keys_fallback() { # keys, one per line, in schema order. _ckipper_setup_prompts_pick_keys() { if _ckipper_setup_prompts_use_gum; then - _ckipper_setup_prompts_global_keys \ - | gum choose --no-limit --header "$_CKIPPER_SETUP_PROMPTS_PICKER_HEADER" + local key + while IFS= read -r key; do + printf '%s — %s\n' "$key" "${_CKIPPER_SCHEMA_DESCRIPTION[$key]}" + done < <(_ckipper_setup_prompts_global_keys) \ + | gum choose --no-limit --header "$_CKIPPER_SETUP_PROMPTS_PICKER_HEADER" \ + | awk '{print $1}' return 0 fi _ckipper_setup_prompts_pick_keys_fallback diff --git a/lib/setup/prompts_test.bats b/lib/setup/prompts_test.bats index 81ff702..6ac4f8e 100644 --- a/lib/setup/prompts_test.bats +++ b/lib/setup/prompts_test.bats @@ -67,6 +67,19 @@ _run_prompts() { [[ "$output" != *"ssh_forward"* ]] } +# Regression: descriptions used to live in a 4th column, which overflowed +# the fixed-width table because zsh's `printf '%-22s'` does not truncate. +# They now render on the line below each row, indented two spaces. +@test "_ckipper_setup_prompts_summary renders each schema description below its row" { + _run_prompts "" "_ckipper_setup_prompts_summary" + + [ "$status" -eq 0 ] + # Each schema description should appear verbatim somewhere in the output. + [[ "$output" == *"Bool. true = installer auto-adds the per-account aliases source line"* ]] + [[ "$output" == *"Path. Base directory containing your git projects."* ]] + [[ "$output" == *"Comma-separated int list. Container ports to forward to the host."* ]] +} + @test "_ckipper_setup_prompts_summary marks set values as (your config) and unset as (default)" { echo 'CKIPPER_NOTIFY_BELL="false"' >"$CKIPPER_DIR/docker/ckipper-config.zsh"