Add install scope selector

This commit is contained in:
Arvin
2026-06-25 12:06:07 +08:00
parent 4539f2c7f2
commit 10bcbe1ee7
2 changed files with 239 additions and 1 deletions
+113 -1
View File
@@ -1145,8 +1145,104 @@ guard_unsafe_sudo_auto_install() {
fi
}
install_sudo_available() {
command -v sudo >/dev/null 2>&1 || return 1
sudo -n true >/dev/null 2>&1 && return 0
id -nG 2>/dev/null | tr ' ' '\n' | grep -Eq '^(sudo|wheel|admin)$'
}
install_scope_choice_tty() {
[ -t 0 ] && [ -t 2 ]
}
install_prompt_scope_choice() {
local selected=0 key
local root_label="root 安装(推荐)"
local user_label="user 安装"
local root_desc="安装到系统命令路径,支持 systemd 后端,适合长期运行、开机自启、全局可用。"
local user_desc="不写系统目录,安装到当前用户环境,适合无 root 权限或只给当前用户使用。"
if ! install_scope_choice_tty; then
warn "检测到当前用户可使用 sudo;非交互终端默认使用 user 安装"
printf 'ℹ %s\n' "如需 root 安装,请显式执行:sudo bash install.sh system" >&2
echo "user"
return 0
fi
while true; do
printf '\033[2J\033[H' >&2
echo "检测到当前用户可使用 sudo,请选择安装方式:" >&2
echo >&2
if [ "$selected" -eq 0 ]; then
echo "> $root_label" >&2
else
echo " $root_label" >&2
fi
echo " $root_desc" >&2
echo >&2
if [ "$selected" -eq 1 ]; then
echo "> $user_label" >&2
else
echo " $user_label" >&2
fi
echo " $user_desc" >&2
echo >&2
echo "使用 ↑/↓ 或 j/k 移动,Enter 确认。" >&2
IFS= read -rsn1 key || {
echo "user"
return 0
}
case "$key" in
"")
if [ "$selected" -eq 0 ]; then
echo "system"
else
echo "user"
fi
return 0
;;
j|J)
selected=1
;;
k|K)
selected=0
;;
$'\033')
IFS= read -rsn2 -t 0.1 key || key=""
case "$key" in
"[A") selected=0 ;;
"[B") selected=1 ;;
esac
;;
1)
echo "system"
return 0
;;
2)
echo "user"
return 0
;;
esac
done
}
rerun_install_as_root_system() {
local install_script
install_script="$PROJECT_DIR/install.sh"
[ -f "$install_script" ] || die "未找到安装脚本:$install_script"
echo "👉 将使用 root/system 方式重新执行安装"
exec sudo bash "$install_script" system
}
detect_install_scope() {
local requested="${1:-auto}"
local selected_scope
case "$requested" in
system)
@@ -1159,7 +1255,23 @@ detect_install_scope() {
if [ "$(id -u)" -eq 0 ]; then
INSTALL_SCOPE="system"
else
INSTALL_SCOPE="user"
if install_sudo_available; then
selected_scope="$(install_prompt_scope_choice)"
case "$selected_scope" in
system)
rerun_install_as_root_system
return $?
;;
user)
INSTALL_SCOPE="user"
;;
*)
die "未知安装选择:$selected_scope"
;;
esac
else
INSTALL_SCOPE="user"
fi
fi
;;
*)
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=scripts/core/common.sh
source "$PROJECT_DIR/scripts/core/common.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
PROJECT_DIR="$tmp_dir/project"
mkdir -p "$PROJECT_DIR"
touch "$PROJECT_DIR/install.sh"
reset_scope_state() {
INSTALL_SCOPE=""
INSTALL_HOME=""
RUNTIME_DIR=""
BIN_DIR=""
LOG_DIR=""
SELECTOR_PROMPTED="false"
RERUN_AS_ROOT="false"
MOCK_IS_ROOT="false"
MOCK_SUDO_AVAILABLE="false"
MOCK_TTY="false"
MOCK_SELECTED_SCOPE="user"
: > "$tmp_dir/prompted"
: > "$tmp_dir/rerun"
}
id() {
if [ "${1:-}" = "-u" ]; then
if [ "$MOCK_IS_ROOT" = "true" ]; then
printf '0\n'
else
printf '1000\n'
fi
return 0
fi
command id "$@"
}
install_sudo_available() {
[ "$MOCK_SUDO_AVAILABLE" = "true" ]
}
install_scope_choice_tty() {
[ "$MOCK_TTY" = "true" ]
}
install_prompt_scope_choice() {
printf 'prompted\n' >> "$tmp_dir/prompted"
if ! install_scope_choice_tty; then
printf 'user\n'
return 0
fi
printf '%s\n' "$MOCK_SELECTED_SCOPE"
}
rerun_install_as_root_system() {
printf 'rerun\n' >> "$tmp_dir/rerun"
return 77
}
assert_equal() {
local name="$1"
local actual="$2"
local expected="$3"
if [ "$actual" != "$expected" ]; then
echo "not ok - $name: got '$actual', expected '$expected'" >&2
return 1
fi
echo "ok - $name"
}
reset_scope_state
detect_install_scope user
assert_equal "explicit user selects user scope" "$INSTALL_SCOPE" "user"
assert_equal "explicit user does not prompt" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "0"
reset_scope_state
detect_install_scope system
assert_equal "explicit system selects system scope" "$INSTALL_SCOPE" "system"
assert_equal "explicit system does not prompt" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "0"
reset_scope_state
MOCK_IS_ROOT="true"
detect_install_scope auto
assert_equal "root auto selects system scope" "$INSTALL_SCOPE" "system"
assert_equal "root auto does not prompt" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "0"
reset_scope_state
detect_install_scope auto
assert_equal "non-root without sudo selects user scope" "$INSTALL_SCOPE" "user"
assert_equal "non-root without sudo does not prompt" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "0"
reset_scope_state
MOCK_SUDO_AVAILABLE="true"
detect_install_scope auto
assert_equal "non-root sudo non-tty prompts through default path" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "1"
assert_equal "non-root sudo non-tty selects user scope" "$INSTALL_SCOPE" "user"
reset_scope_state
MOCK_SUDO_AVAILABLE="true"
MOCK_TTY="true"
MOCK_SELECTED_SCOPE="user"
detect_install_scope auto
assert_equal "non-root sudo tty user choice prompts" "$(wc -l < "$tmp_dir/prompted" | tr -d ' ')" "1"
assert_equal "non-root sudo tty user choice selects user scope" "$INSTALL_SCOPE" "user"
reset_scope_state
MOCK_SUDO_AVAILABLE="true"
MOCK_TTY="true"
MOCK_SELECTED_SCOPE="system"
set +e
detect_install_scope auto
rc=$?
set -e
assert_equal "non-root sudo tty system choice calls root rerun" "$(wc -l < "$tmp_dir/rerun" | tr -d ' ')" "1"
assert_equal "non-root sudo tty system choice stops current flow" "$rc" "77"