diff --git a/scripts/install.sh b/scripts/install.sh index e179b558e..9f4b0a0dc 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -99,33 +99,35 @@ print_message() { echo -e "${color}${message}${NC}" } +describe_removal() { + local path=$1 + + if [[ "$path" == *".local/bin"* ]] && command -v pipx >/dev/null 2>&1; then + echo -e "${MUTED} It looks like a pipx installation. To remove it: ${NC}pipx uninstall strix-agent" + else + echo -e "${MUTED} To remove it: ${NC}rm $path" + fi +} + check_existing_installation() { + UNMANAGED_INSTALLATIONS=() + local found_paths=() while IFS= read -r -d '' path; do found_paths+=("$path") done < <(which -a strix 2>/dev/null | tr '\n' '\0' || true) - if [ ${#found_paths[@]} -gt 0 ]; then - for path in "${found_paths[@]}"; do - if [[ ! -e "$path" ]] || [[ "$path" == "$INSTALL_DIR/strix"* ]]; then - continue - fi + for path in "${found_paths[@]:-}"; do + if [[ -z "$path" ]] || [[ ! -e "$path" ]] || [[ "$path" == "$INSTALL_DIR/strix"* ]]; then + continue + fi - if [[ -n "$path" ]]; then - echo -e "${MUTED}Found existing strix at: ${NC}$path" + UNMANAGED_INSTALLATIONS+=("$path") + echo -e "${MUTED}Found another strix at: ${NC}$path" + done - if [[ "$path" == *".local/bin"* ]]; then - echo -e "${MUTED}Removing old pipx installation...${NC}" - if command -v pipx >/dev/null 2>&1; then - pipx uninstall strix-agent 2>/dev/null || true - fi - rm -f "$path" 2>/dev/null || true - elif [[ -L "$path" || -f "$path" ]]; then - echo -e "${MUTED}Removing old installation...${NC}" - rm -f "$path" 2>/dev/null || true - fi - fi - done + if [ ${#UNMANAGED_INSTALLATIONS[@]} -gt 0 ]; then + echo -e "${MUTED}This installer only manages ${NC}$INSTALL_DIR${MUTED}, so it is left alone.${NC}" fi } @@ -296,15 +298,10 @@ verify_installation() { if [[ "$which_strix" != "$INSTALL_DIR/strix" && "$which_strix" != "$INSTALL_DIR/strix.exe" ]]; then if [[ -n "$which_strix" ]]; then - echo -e "${YELLOW}⚠ Found conflicting strix at: ${NC}$which_strix" - echo -e "${MUTED}Attempting to remove...${NC}" - - if rm -f "$which_strix" 2>/dev/null; then - echo -e "${GREEN}✓ Removed conflicting installation${NC}" - else - echo -e "${YELLOW}Could not remove automatically.${NC}" - echo -e "${MUTED}Please remove manually: ${NC}rm $which_strix" - fi + echo -e "${YELLOW}⚠ Another strix wins PATH resolution: ${NC}$which_strix" + echo -e "${MUTED}The version just installed lives in ${NC}$INSTALL_DIR${MUTED} and will not run until that changes.${NC}" + echo -e "${MUTED}Put ${NC}$INSTALL_DIR${MUTED} earlier on your PATH, or remove the other executable yourself.${NC}" + describe_removal "$which_strix" fi fi diff --git a/tests/test_install_script.py b/tests/test_install_script.py index 67ca48437..e099e3348 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -152,3 +152,46 @@ def test_installer_rejects_unsupported_architecture(tmp_path: Path) -> None: assert "Unsupported OS/Arch: linux/riscv64" in result.stdout assert not curl_log_path.exists() assert not (home_path / ".strix").exists() + + +def _install_with_decoy(tmp_path: Path, decoy_directory_name: str) -> tuple[Path, Path, Path]: + """Run the installer with an unrelated `strix` ahead of it on `PATH`.""" + repository_root = Path(__file__).resolve().parents[1] + archive_path = _create_release_archive(tmp_path) + mock_bin = _create_mock_commands(tmp_path, machine="aarch64") + + pipx_log_path = tmp_path / "pipx.log" + _write_executable( + mock_bin / "pipx", + f'#!/bin/sh\nprintf \'%s\n\' "$*" >> "{pipx_log_path}"\n', + ) + + decoy_directory = tmp_path / decoy_directory_name + decoy_directory.mkdir(parents=True) + decoy_path = decoy_directory / "strix" + _write_executable(decoy_path, "#!/bin/sh\nprintf 'other-strix 1.2.3\n'\n") + + environment, home_path, _ = _create_installer_environment(tmp_path, archive_path, mock_bin) + environment["PATH"] = f"{decoy_directory}:{environment['PATH']}" + + result = _run_installer(repository_root, environment) + assert result.returncode == 0, result.stderr + + return decoy_path, pipx_log_path, home_path + + +def test_installer_leaves_unrelated_strix_executables_alone(tmp_path: Path) -> None: + decoy_path, pipx_log_path, home_path = _install_with_decoy(tmp_path, "other-bin") + + assert decoy_path.exists() + assert decoy_path.read_text(encoding="utf-8") == "#!/bin/sh\nprintf 'other-strix 1.2.3\n'\n" + assert not pipx_log_path.exists() + assert (home_path / ".strix/bin/strix").exists() + + +def test_installer_does_not_uninstall_a_pipx_managed_strix(tmp_path: Path) -> None: + decoy_path, pipx_log_path, home_path = _install_with_decoy(tmp_path, ".local/bin") + + assert decoy_path.exists() + assert not pipx_log_path.exists() + assert (home_path / ".strix/bin/strix").exists()