From c43c70ea44abfad9417346bdf63e079ab97c218e Mon Sep 17 00:00:00 2001 From: Max F Date: Wed, 20 May 2026 10:47:31 -0400 Subject: [PATCH] update --- README.md | 19 +++-- scripts/install.sh | 137 +++++++++++++++++++----------------- scripts/uninstall.sh | 19 +++-- tests/test_install_smoke.py | 68 ++++++++++-------- 4 files changed, 133 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 4c4a776..c3e6a85 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,21 @@ Secrets Vault gateway. - Python 3.10 or later - A running Distributed Secrets Vault server -- `curl` (for install/uninstall scripts) +- `curl` and `sh` (for install/uninstall scripts) ## Install -Install directly from GitHub: +Install directly from GitHub (works whether your login shell is bash or zsh): -```bash -curl -fsSL https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main/scripts/install.sh | bash +```sh +curl -fsSL https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main/scripts/install.sh | sh +``` + +To download the script first instead of piping: + +```sh +curl -fsSL https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main/scripts/install.sh -o /tmp/install-dsvc.sh +sh /tmp/install-dsvc.sh ``` The installer: @@ -185,8 +192,8 @@ python3 -m unittest discover -s tests -p "test_*.py" ## Uninstall -```bash -curl -fsSL https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main/scripts/uninstall.sh | bash +```sh +curl -fsSL https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main/scripts/uninstall.sh | sh ``` The uninstall script removes: diff --git a/scripts/install.sh b/scripts/install.sh index 3e583a3..0dc2ea3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,10 +1,11 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +(set -o pipefail) 2>/dev/null && set -o pipefail RUNTIME_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/dsvc" SRC_DIR="$RUNTIME_ROOT/src" RUNTIME_BIN_DIR="$RUNTIME_ROOT/bin" -if [[ -w /usr/local/bin ]]; then +if [ -w /usr/local/bin ]; then BIN_DIR="/usr/local/bin" else BIN_DIR="$HOME/.local/bin" @@ -13,6 +14,9 @@ CONFIG_DIR="$HOME/.dsv_client" CONFIG_FILE="$CONFIG_DIR/config.json" RAW_BASE_URL="https://raw.githubusercontent.com/S26-Distributed-Capstone/DSVClient/main" +cfg_read_source="" +cfg_prompt_out="/dev/stderr" + trim_whitespace() { printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' } @@ -24,59 +28,57 @@ require_cmd() { fi } -download_python_sources() { - local files=("cli.py" "client.py" "config.py") - local file="" +prompt_and_read() { + _prompt="$1" + _resultvar="$2" + _value="" + printf "%s" "$_prompt" >"$cfg_prompt_out" + if [ "$cfg_read_source" = "tty" ]; then + if ! IFS= read -r _value < /dev/tty; then + _value="" + fi + elif ! IFS= read -r _value; then + _value="" + fi + eval "$_resultvar=\$_value" +} +download_python_sources() { mkdir -p "$SRC_DIR" - for file in "${files[@]}"; do + for file in cli.py client.py config.py; do curl -fsSL "${RAW_BASE_URL}/${file}" -o "${SRC_DIR}/${file}" done } configure_client() { - local default_base_url="http://localhost:8080" - local existing_base_url="" - local existing_username="" - local entered_base_url="" - local entered_username="" - local entered_username_trimmed="" - local prompt_fd=0 - local has_prompt_tty=0 - local prompt_out="/dev/stderr" - - prompt_and_read() { - local prompt="$1" - local __resultvar="$2" - local value="" - - printf "%s" "$prompt" > "$prompt_out" - if ! IFS= read -r -u "$prompt_fd" value; then - value="" - fi - printf -v "$__resultvar" '%s' "$value" - } - - # When installer is piped (curl | bash), stdin is not interactive. + default_base_url="http://localhost:8080" + existing_base_url="" + existing_username="" + entered_base_url="" + entered_username="" + entered_username_trimmed="" + cfg_has_prompt_tty=0 + + # When installer is piped (curl | sh), stdin is not interactive. # Use the controlling terminal directly if available. - if [[ -t 0 ]]; then - has_prompt_tty=1 - elif [[ -r /dev/tty ]]; then - if exec 3/dev/null; then - prompt_fd=3 - prompt_out="/dev/tty" - has_prompt_tty=1 - fi + if [ -t 0 ]; then + cfg_has_prompt_tty=1 + cfg_read_source="stdin" + elif [ -e /dev/tty ] && { printf '' > /dev/tty; } 2>/dev/null; then + cfg_has_prompt_tty=1 + cfg_read_source="tty" + cfg_prompt_out="/dev/tty" fi - if [[ "$has_prompt_tty" -eq 0 ]]; then + if [ "$cfg_has_prompt_tty" -eq 0 ]; then echo "No interactive terminal detected." echo "Please run install again in an interactive terminal." exit 1 fi - if [[ -f "$CONFIG_FILE" ]]; then - read -r existing_base_url existing_username < <(python3 - <<'PY' "$CONFIG_FILE" + if [ -f "$CONFIG_FILE" ]; then + IFS= read -r existing_base_url existing_username <&2 echo "Unable to fetch expected sources from GitHub." >&2 exit 1 @@ -162,8 +169,7 @@ main() { mkdir -p "$RUNTIME_BIN_DIR" cat > "$RUNTIME_BIN_DIR/dsvc" </dev/null && set -o pipefail RUNTIME_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/dsvc" DEFAULT_USER_BIN="$HOME/.local/bin" CONFIG_DIR="$HOME/.dsv_client" remove_launchers() { - local -a candidates=("/usr/local/bin/dsvc" "${DEFAULT_USER_BIN}/dsvc") - local candidate="" - local removed_count=0 + removed_count=0 - for candidate in "${candidates[@]}"; do - if [[ -e "$candidate" || -L "$candidate" ]]; then + for candidate in /usr/local/bin/dsvc "$DEFAULT_USER_BIN/dsvc"; do + if [ -e "$candidate" ] || [ -L "$candidate" ]; then rm -f "$candidate" && { echo "Removed launcher: $candidate" removed_count=$((removed_count + 1)) @@ -19,13 +18,13 @@ remove_launchers() { fi done - if (( removed_count == 0 )); then + if [ "$removed_count" -eq 0 ]; then echo "Launcher not found in expected paths." fi } remove_config() { - if [[ -d "$CONFIG_DIR" ]]; then + if [ -d "$CONFIG_DIR" ]; then rm -rf "$CONFIG_DIR" echo "Removed config directory: $CONFIG_DIR" else @@ -36,7 +35,7 @@ remove_config() { main() { remove_launchers - if [[ -d "$RUNTIME_ROOT" ]]; then + if [ -d "$RUNTIME_ROOT" ]; then rm -rf "$RUNTIME_ROOT" echo "Removed runtime: $RUNTIME_ROOT" else diff --git a/tests/test_install_smoke.py b/tests/test_install_smoke.py index 5e19397..38d4ef1 100644 --- a/tests/test_install_smoke.py +++ b/tests/test_install_smoke.py @@ -1,5 +1,6 @@ import json import os +import shutil import stat import subprocess import tempfile @@ -11,6 +12,37 @@ class InstallSmokeTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.repo_root = Path(__file__).resolve().parents[1] + cls.install_shells = ["sh"] + dash = shutil.which("dash") + if dash: + cls.install_shells.append(dash) + + def _run_install(self, env: dict) -> subprocess.CompletedProcess: + last_result = None + for shell in self.install_shells: + with self.subTest(shell=shell): + last_result = subprocess.run( + [shell, "scripts/install.sh"], + cwd=self.repo_root, + env=env, + text=True, + capture_output=True, + check=False, + ) + self.assertNotEqual( + 0, + last_result.returncode, + msg=last_result.stderr or last_result.stdout, + ) + self.assertIn( + "No interactive terminal detected.", + last_result.stdout, + ) + self.assertIn( + "Please run install again in an interactive terminal.", + last_result.stdout, + ) + return last_result def test_install_fails_without_interactive_terminal_even_with_existing_config(self): with tempfile.TemporaryDirectory() as temp_dir: @@ -34,18 +66,7 @@ def test_install_fails_without_interactive_terminal_even_with_existing_config(se encoding="utf-8", ) - result = subprocess.run( - ["bash", "scripts/install.sh"], - cwd=self.repo_root, - env=env, - text=True, - capture_output=True, - check=False, - ) - - self.assertNotEqual(0, result.returncode, msg=result.stderr or result.stdout) - self.assertIn("No interactive terminal detected.", result.stdout) - self.assertIn("Please run install again in an interactive terminal.", result.stdout) + self._run_install(env) def test_install_fails_without_interactive_terminal_no_existing_config(self): with tempfile.TemporaryDirectory() as temp_dir: @@ -63,27 +84,16 @@ def test_install_fails_without_interactive_terminal_no_existing_config(self): env["DSVC_TEST_REPO_ROOT"] = str(self.repo_root) env["PATH"] = f"{fake_bin}:{env.get('PATH', '')}" - result = subprocess.run( - ["bash", "scripts/install.sh"], - cwd=self.repo_root, - env=env, - text=True, - capture_output=True, - check=False, - ) - - self.assertNotEqual(0, result.returncode, msg=result.stderr or result.stdout) - self.assertIn("No interactive terminal detected.", result.stdout) - self.assertIn("Please run install again in an interactive terminal.", result.stdout) + self._run_install(env) @staticmethod def _create_mock_curl(path: Path) -> None: - script = """#!/usr/bin/env bash -set -euo pipefail + script = """#!/bin/sh +set -eu output="" url="" -while [[ $# -gt 0 ]]; do +while [ $# -gt 0 ]; do case "$1" in -o) output="$2" @@ -99,7 +109,7 @@ def _create_mock_curl(path: Path) -> None: esac done -if [[ -z "$output" || -z "$url" ]]; then +if [ -z "$output" ] || [ -z "$url" ]; then echo "mock curl: missing output or url" >&2 exit 1 fi @@ -107,7 +117,7 @@ def _create_mock_curl(path: Path) -> None: file_name="${url##*/}" src="${DSVC_TEST_REPO_ROOT}/${file_name}" -if [[ ! -f "$src" ]]; then +if [ ! -f "$src" ]; then echo "mock curl: source file not found: $src" >&2 exit 1 fi