From af6f38b391d22eed147f849c733401756f8c647b Mon Sep 17 00:00:00 2001 From: adamw Date: Thu, 17 Sep 2026 11:18:44 +0200 Subject: [PATCH 1/2] fix(installer): read the overwrite prompt from the terminal when piped into a shell With curl ... | sh, stdin is the script itself, so the overwrite question got EOF and always aborted. Re-running the install command now works as an upgrade, as the docs already claimed. --- cli/test/installer/installer.bats | 54 +++++++++++++++++++++++++++++++ docs/installation.md | 8 +++-- install.sh | 9 +++++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/cli/test/installer/installer.bats b/cli/test/installer/installer.bats index 0e1c0477..7a26c0d3 100644 --- a/cli/test/installer/installer.bats +++ b/cli/test/installer/installer.bats @@ -464,3 +464,57 @@ FAKEID # SANDCAT_HOME dir removed since it was empty after cli/ deletion [ ! -d "$sandcat_home" ] } + +# Runs a shell command under a pseudo-terminal and types $1 on it. +# Stdin is held open briefly because macOS `script` forwards EOF to the +# terminal at once, and it could arrive before the typed answer. +_run_with_tty() { + local answer=$1 cmd=$2 + if [ "$(uname)" = "Darwin" ]; then + (printf '%s\n' "$answer"; sleep 2) | script -q /dev/null sh -c "$cmd" + else + (printf '%s\n' "$answer"; sleep 2) | script -q -e -c "$cmd" /dev/null + fi +} + +_install_fixture() { + SANDCAT_HOME="$1" \ + SANDCAT_BIN_DIR="$2" \ + SANDCAT_NON_INTERACTIVE=true \ + PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SH" +} + +@test "install.sh piped into a shell reads the overwrite answer from the terminal" { + _stub_curl_with_fixture master + + local sandcat_home="$BATS_TEST_TMPDIR/home/.local/share/sandcat" + local sandcat_bin="$BATS_TEST_TMPDIR/home/.local/bin" + _install_fixture "$sandcat_home" "$sandcat_bin" + touch "$sandcat_home/cli/stale" + + SANDCAT_HOME="$sandcat_home" \ + SANDCAT_BIN_DIR="$sandcat_bin" \ + PATH="$FAKE_BIN:$PATH" \ + run _run_with_tty y "cat '$INSTALL_SH' | bash" + assert_success + assert_output --partial "Overwrite?" + assert_output --partial "Installed sandcat" + [ ! -e "$sandcat_home/cli/stale" ] +} + +@test "install.sh piped into a shell keeps the existing install on an empty answer" { + _stub_curl_with_fixture master + + local sandcat_home="$BATS_TEST_TMPDIR/home/.local/share/sandcat" + local sandcat_bin="$BATS_TEST_TMPDIR/home/.local/bin" + _install_fixture "$sandcat_home" "$sandcat_bin" + touch "$sandcat_home/cli/stale" + + SANDCAT_HOME="$sandcat_home" \ + SANDCAT_BIN_DIR="$sandcat_bin" \ + PATH="$FAKE_BIN:$PATH" \ + run _run_with_tty "" "cat '$INSTALL_SH' | bash" + assert_success + assert_output --partial "Aborted" + [ -e "$sandcat_home/cli/stale" ] +} diff --git a/docs/installation.md b/docs/installation.md index a6283972..d71374f5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -24,9 +24,11 @@ curl -fsSL https://raw.githubusercontent.com/VirtusLab/sandcat/master/install.sh Ensure `~/.local/bin` is on your `PATH` (the installer prints a hint if it isn't), then jump to [Initialize the sandbox](getting-started/quickstart.md#2-initialize-the-sandbox-for-your-project). -**Upgrade:** re-run the same command. The installer atomically swaps the -existing install; `~/.config/sandcat/` (user settings) is never touched. -Combine with `SANDCAT_REF` to jump to a different branch/tag/commit: +**Upgrade:** re-run the same command and answer `y` when asked to overwrite +the existing install (set `SANDCAT_NON_INTERACTIVE=true` to skip the +question). The installer atomically swaps the existing install; +`~/.config/sandcat/` (user settings) is never touched. Combine with +`SANDCAT_REF` to jump to a different branch/tag/commit: ```bash curl -fsSL https://raw.githubusercontent.com/VirtusLab/sandcat/master/install.sh | SANDCAT_REF=v1.0.0 sh diff --git a/install.sh b/install.sh index 6693386c..f5c41200 100644 --- a/install.sh +++ b/install.sh @@ -214,7 +214,14 @@ prompt_yes_no() { fi printf '%s [y/N]: ' "$msg" >&2 local reply - IFS= read -r reply || reply="" + # When piped (curl ... | sh), stdin is the script itself, so ask the + # terminal directly. Without one (CI without SANDCAT_NON_INTERACTIVE), + # the answer stays empty and defaults to "no". + if [ -t 0 ]; then + IFS= read -r reply || reply="" + else + { IFS= read -r reply < /dev/tty; } 2>/dev/null || reply="" + fi case "$reply" in y|Y|yes|YES) return 0 ;; *) return 1 ;; From e32816226c42365f4be588c028ecde53b2d10f82 Mon Sep 17 00:00:00 2001 From: adamw Date: Thu, 17 Sep 2026 11:24:03 +0200 Subject: [PATCH 2/2] fix(installer): run under dash so curl ... | sh works on Debian/Ubuntu set -o pipefail and the ERR trap are bash-only. Neither is needed: no pipeline relies on pipefail, and set -e plus the EXIT trap already exit on failure and clean up. --- cli/test/installer/installer.bats | 4 ++-- install.sh | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/test/installer/installer.bats b/cli/test/installer/installer.bats index 7a26c0d3..4197747a 100644 --- a/cli/test/installer/installer.bats +++ b/cli/test/installer/installer.bats @@ -495,7 +495,7 @@ _install_fixture() { SANDCAT_HOME="$sandcat_home" \ SANDCAT_BIN_DIR="$sandcat_bin" \ PATH="$FAKE_BIN:$PATH" \ - run _run_with_tty y "cat '$INSTALL_SH' | bash" + run _run_with_tty y "cat '$INSTALL_SH' | sh" assert_success assert_output --partial "Overwrite?" assert_output --partial "Installed sandcat" @@ -513,7 +513,7 @@ _install_fixture() { SANDCAT_HOME="$sandcat_home" \ SANDCAT_BIN_DIR="$sandcat_bin" \ PATH="$FAKE_BIN:$PATH" \ - run _run_with_tty "" "cat '$INSTALL_SH' | bash" + run _run_with_tty "" "cat '$INSTALL_SH' | sh" assert_success assert_output --partial "Aborted" [ -e "$sandcat_home/cli/stale" ] diff --git a/install.sh b/install.sh index f5c41200..8d4b90aa 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,9 @@ # SANDCAT_REF branch/tag/commit to install (default: master) # SANDCAT_NON_INTERACTIVE skip prompts when set to "true" (default: false) -set -euo pipefail +# No pipefail: dash (sh on Debian/Ubuntu) lacks it, and no pipeline here +# relies on it. +set -eu # --- Logging helpers --------------------------------------------------------- @@ -36,8 +38,9 @@ cleanup() { fi } +# With set -e, a failing command exits with its status, and the EXIT trap +# then runs cleanup. (An ERR trap would not be portable to dash.) trap 'cleanup' EXIT -trap 'cleanup; exit 1' ERR # --- Argument parsing --------------------------------------------------------