feat(installer): add guided zeroclaw installer and distro hardening (#887)

* feat(installer): add guided zeroclaw installer entrypoint

- add top-level POSIX wrapper (zeroclaw_install.sh) that ensures bash is present

- route bootstrap/install compatibility scripts through the new installer entrypoint

- improve Linux dependency handling for Alpine/Fedora/Arch, including pacman container fallback

* fix(ci): resolve dependabot config conflict and run daily

- remove duplicate docker ecosystem entry with overlapping directory/target-branch

- switch cargo, github-actions, and docker schedules from monthly to daily
This commit is contained in:
Will Sarg 2026-02-20 04:34:14 -05:00 committed by GitHub
parent a2e9c0d1e1
commit c96ea79ac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 426 additions and 73 deletions

View file

@ -4,7 +4,7 @@ updates:
- package-ecosystem: cargo - package-ecosystem: cargo
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: daily
target-branch: main target-branch: main
open-pull-requests-limit: 3 open-pull-requests-limit: 3
labels: labels:
@ -20,7 +20,7 @@ updates:
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: daily
target-branch: main target-branch: main
open-pull-requests-limit: 1 open-pull-requests-limit: 1
labels: labels:
@ -37,7 +37,7 @@ updates:
- package-ecosystem: docker - package-ecosystem: docker
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: daily
target-branch: main target-branch: main
open-pull-requests-limit: 1 open-pull-requests-limit: 1
labels: labels:
@ -50,20 +50,3 @@ updates:
update-types: update-types:
- minor - minor
- patch - patch
- package-ecosystem: docker
directory: "/"
schedule:
interval: weekly
target-branch: main
open-pull-requests-limit: 3
labels:
- "ci"
- "dependencies"
groups:
docker-minor-patch:
patterns:
- "*"
update-types:
- minor
- patch

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || pwd)"
exec "$ROOT_DIR/scripts/bootstrap.sh" "$@" exec "$ROOT_DIR/zeroclaw_install.sh" "$@"

View file

@ -15,16 +15,20 @@ error() {
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
ZeroClaw one-click bootstrap ZeroClaw installer bootstrap engine
Usage: Usage:
./bootstrap.sh [options] ./zeroclaw_install.sh [options]
./bootstrap.sh [options] # compatibility entrypoint
Modes: Modes:
Default mode installs/builds ZeroClaw only (requires existing Rust toolchain). Default mode installs/builds ZeroClaw only (requires existing Rust toolchain).
Guided mode asks setup questions and configures options interactively.
Optional bootstrap mode can also install system dependencies and Rust. Optional bootstrap mode can also install system dependencies and Rust.
Options: Options:
--guided Run interactive guided installer
--no-guided Disable guided installer
--docker Run bootstrap in Docker and launch onboarding inside the container --docker Run bootstrap in Docker and launch onboarding inside the container
--install-system-deps Install build dependencies (Linux/macOS) --install-system-deps Install build dependencies (Linux/macOS)
--install-rust Install Rust via rustup if missing --install-rust Install Rust via rustup if missing
@ -36,18 +40,22 @@ Options:
--api-key <key> API key for non-interactive onboarding --api-key <key> API key for non-interactive onboarding
--provider <id> Provider for non-interactive onboarding (default: openrouter) --provider <id> Provider for non-interactive onboarding (default: openrouter)
--model <id> Model for non-interactive onboarding (optional) --model <id> Model for non-interactive onboarding (optional)
--build-first Alias for explicitly enabling separate `cargo build --release --locked`
--skip-build Skip `cargo build --release --locked` --skip-build Skip `cargo build --release --locked`
--skip-install Skip `cargo install --path . --force --locked` --skip-install Skip `cargo install --path . --force --locked`
-h, --help Show help -h, --help Show help
Examples: Examples:
./bootstrap.sh ./zeroclaw_install.sh
./zeroclaw_install.sh --guided
./zeroclaw_install.sh --install-system-deps --install-rust
./zeroclaw_install.sh --prefer-prebuilt
./zeroclaw_install.sh --prebuilt-only
./zeroclaw_install.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
./zeroclaw_install.sh --interactive-onboard
# Compatibility entrypoint:
./bootstrap.sh --docker ./bootstrap.sh --docker
./bootstrap.sh --install-system-deps --install-rust
./bootstrap.sh --prefer-prebuilt
./bootstrap.sh --prebuilt-only
./bootstrap.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
./bootstrap.sh --interactive-onboard
# Remote one-liner # Remote one-liner
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/main/scripts/bootstrap.sh | bash curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/main/scripts/bootstrap.sh | bash
@ -60,6 +68,8 @@ Environment:
ZEROCLAW_MODEL Used when --model is not provided ZEROCLAW_MODEL Used when --model is not provided
ZEROCLAW_BOOTSTRAP_MIN_RAM_MB Minimum RAM threshold for source build preflight (default: 2048) ZEROCLAW_BOOTSTRAP_MIN_RAM_MB Minimum RAM threshold for source build preflight (default: 2048)
ZEROCLAW_BOOTSTRAP_MIN_DISK_MB Minimum free disk threshold for source build preflight (default: 6144) ZEROCLAW_BOOTSTRAP_MIN_DISK_MB Minimum free disk threshold for source build preflight (default: 6144)
ZEROCLAW_DISABLE_ALPINE_AUTO_DEPS
Set to 1 to disable Alpine auto-install of missing prerequisites
USAGE USAGE
} }
@ -227,19 +237,152 @@ run_privileged() {
fi fi
} }
is_container_runtime() {
if [[ -f /.dockerenv || -f /run/.containerenv ]]; then
return 0
fi
if [[ -r /proc/1/cgroup ]] && grep -Eq '(docker|containerd|kubepods|podman|lxc)' /proc/1/cgroup; then
return 0
fi
return 1
}
run_pacman() {
if ! have_cmd pacman; then
error "pacman is not available."
return 1
fi
if ! is_container_runtime; then
run_privileged pacman "$@"
return $?
fi
local pacman_cfg_tmp=""
local pacman_rc=0
pacman_cfg_tmp="$(mktemp /tmp/zeroclaw-pacman.XXXXXX.conf)"
cp /etc/pacman.conf "$pacman_cfg_tmp"
if ! grep -Eq '^[[:space:]]*DisableSandboxSyscalls([[:space:]]|$)' "$pacman_cfg_tmp"; then
printf '\nDisableSandboxSyscalls\n' >> "$pacman_cfg_tmp"
fi
if run_privileged pacman --config "$pacman_cfg_tmp" "$@"; then
pacman_rc=0
else
pacman_rc=$?
fi
rm -f "$pacman_cfg_tmp"
return "$pacman_rc"
}
ALPINE_PREREQ_PACKAGES=(
bash
build-base
pkgconf
git
curl
openssl-dev
perl
ca-certificates
)
ALPINE_MISSING_PKGS=()
find_missing_alpine_prereqs() {
ALPINE_MISSING_PKGS=()
if ! have_cmd apk; then
return 0
fi
local pkg=""
for pkg in "${ALPINE_PREREQ_PACKAGES[@]}"; do
if ! apk info -e "$pkg" >/dev/null 2>&1; then
ALPINE_MISSING_PKGS+=("$pkg")
fi
done
}
bool_to_word() {
if [[ "$1" == true ]]; then
echo "yes"
else
echo "no"
fi
}
prompt_yes_no() {
local question="$1"
local default_answer="$2"
local prompt=""
local answer=""
if [[ "$default_answer" == "yes" ]]; then
prompt="[Y/n]"
else
prompt="[y/N]"
fi
while true; do
if ! read -r -p "$question $prompt " answer; then
error "guided installer input was interrupted."
exit 1
fi
answer="${answer:-$default_answer}"
case "$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')" in
y|yes)
return 0
;;
n|no)
return 1
;;
*)
echo "Please answer yes or no."
;;
esac
done
}
install_system_deps() { install_system_deps() {
info "Installing system dependencies" info "Installing system dependencies"
case "$(uname -s)" in case "$(uname -s)" in
Linux) Linux)
if have_cmd apt-get; then if have_cmd apk; then
find_missing_alpine_prereqs
if [[ ${#ALPINE_MISSING_PKGS[@]} -eq 0 ]]; then
info "Alpine prerequisites already installed"
else
info "Installing Alpine prerequisites: ${ALPINE_MISSING_PKGS[*]}"
run_privileged apk add --no-cache "${ALPINE_MISSING_PKGS[@]}"
fi
elif have_cmd apt-get; then
run_privileged apt-get update -qq run_privileged apt-get update -qq
run_privileged apt-get install -y build-essential pkg-config git curl run_privileged apt-get install -y build-essential pkg-config git curl
elif have_cmd dnf; then elif have_cmd dnf; then
run_privileged dnf group install -y development-tools run_privileged dnf install -y \
run_privileged dnf install -y pkg-config git curl gcc \
gcc-c++ \
make \
pkgconf-pkg-config \
git \
curl \
openssl-devel \
perl
elif have_cmd pacman; then
run_pacman -Sy --noconfirm
run_pacman -S --noconfirm --needed \
gcc \
make \
pkgconf \
git \
curl \
openssl \
perl \
ca-certificates
else else
warn "Unsupported Linux distribution. Install compiler toolchain + pkg-config + git + curl manually." warn "Unsupported Linux distribution. Install compiler toolchain + pkg-config + git + curl + OpenSSL headers + perl manually."
fi fi
;; ;;
Darwin) Darwin)
@ -288,12 +431,125 @@ install_rust_toolchain() {
fi fi
} }
run_guided_installer() {
local os_name="$1"
local provider_input=""
local model_input=""
local api_key_input=""
echo
echo "ZeroClaw guided installer"
echo "Answer a few questions, then the installer will run automatically."
echo
if [[ "$os_name" == "Linux" ]]; then
if prompt_yes_no "Install Linux build dependencies (toolchain/pkg-config/git/curl)?" "yes"; then
INSTALL_SYSTEM_DEPS=true
fi
else
if prompt_yes_no "Install system dependencies for $os_name?" "no"; then
INSTALL_SYSTEM_DEPS=true
fi
fi
if have_cmd cargo && have_cmd rustc; then
info "Detected Rust toolchain: $(rustc --version)"
else
if prompt_yes_no "Rust toolchain not found. Install Rust via rustup now?" "yes"; then
INSTALL_RUST=true
fi
fi
if prompt_yes_no "Run a separate prebuild before install?" "yes"; then
SKIP_BUILD=false
else
SKIP_BUILD=true
fi
if prompt_yes_no "Install zeroclaw into cargo bin now?" "yes"; then
SKIP_INSTALL=false
else
SKIP_INSTALL=true
fi
if prompt_yes_no "Run onboarding after install?" "no"; then
RUN_ONBOARD=true
if prompt_yes_no "Use interactive onboarding?" "yes"; then
INTERACTIVE_ONBOARD=true
else
INTERACTIVE_ONBOARD=false
if ! read -r -p "Provider [$PROVIDER]: " provider_input; then
error "guided installer input was interrupted."
exit 1
fi
if [[ -n "$provider_input" ]]; then
PROVIDER="$provider_input"
fi
if ! read -r -p "Model [${MODEL:-leave empty}]: " model_input; then
error "guided installer input was interrupted."
exit 1
fi
if [[ -n "$model_input" ]]; then
MODEL="$model_input"
fi
if [[ -z "$API_KEY" ]]; then
if ! read -r -s -p "API key (hidden, leave empty to switch to interactive onboarding): " api_key_input; then
echo
error "guided installer input was interrupted."
exit 1
fi
echo
if [[ -n "$api_key_input" ]]; then
API_KEY="$api_key_input"
else
warn "No API key entered. Using interactive onboarding instead."
INTERACTIVE_ONBOARD=true
fi
fi
fi
fi
echo
info "Installer plan"
local install_binary=true
local build_first=false
if [[ "$SKIP_INSTALL" == true ]]; then
install_binary=false
fi
if [[ "$SKIP_BUILD" == false ]]; then
build_first=true
fi
echo " docker-mode: $(bool_to_word "$DOCKER_MODE")"
echo " install-system-deps: $(bool_to_word "$INSTALL_SYSTEM_DEPS")"
echo " install-rust: $(bool_to_word "$INSTALL_RUST")"
echo " build-first: $(bool_to_word "$build_first")"
echo " install-binary: $(bool_to_word "$install_binary")"
echo " onboard: $(bool_to_word "$RUN_ONBOARD")"
if [[ "$RUN_ONBOARD" == true ]]; then
echo " interactive-onboard: $(bool_to_word "$INTERACTIVE_ONBOARD")"
if [[ "$INTERACTIVE_ONBOARD" == false ]]; then
echo " provider: $PROVIDER"
if [[ -n "$MODEL" ]]; then
echo " model: $MODEL"
fi
fi
fi
echo
if ! prompt_yes_no "Proceed with this install plan?" "yes"; then
info "Installation canceled by user."
exit 0
fi
}
ensure_docker_ready() { ensure_docker_ready() {
if ! have_cmd docker; then if ! have_cmd docker; then
error "docker is not installed." error "docker is not installed."
cat <<'MSG' >&2 cat <<'MSG' >&2
Install Docker first, then re-run with: Install Docker first, then re-run with:
./bootstrap.sh --docker ./zeroclaw_install.sh --docker
MSG MSG
exit 1 exit 1
fi fi
@ -342,9 +598,9 @@ run_docker_bootstrap() {
Use either: Use either:
--api-key "sk-..." --api-key "sk-..."
or: or:
ZEROCLAW_API_KEY="sk-..." ./bootstrap.sh --docker ZEROCLAW_API_KEY="sk-..." ./zeroclaw_install.sh --docker
or run interactive: or run interactive:
./bootstrap.sh --docker --interactive-onboard ./zeroclaw_install.sh --docker --interactive-onboard
MSG MSG
exit 1 exit 1
fi fi
@ -373,6 +629,8 @@ SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd || pwd)" SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd || pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd || pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd || pwd)"
REPO_URL="https://github.com/zeroclaw-labs/zeroclaw.git" REPO_URL="https://github.com/zeroclaw-labs/zeroclaw.git"
ORIGINAL_ARG_COUNT=$#
GUIDED_MODE="auto"
DOCKER_MODE=false DOCKER_MODE=false
INSTALL_SYSTEM_DEPS=false INSTALL_SYSTEM_DEPS=false
@ -391,6 +649,14 @@ MODEL="${ZEROCLAW_MODEL:-}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--guided)
GUIDED_MODE="on"
shift
;;
--no-guided)
GUIDED_MODE="off"
shift
;;
--docker) --docker)
DOCKER_MODE=true DOCKER_MODE=true
shift shift
@ -448,6 +714,10 @@ while [[ $# -gt 0 ]]; do
} }
shift 2 shift 2
;; ;;
--build-first)
SKIP_BUILD=false
shift
;;
--skip-build) --skip-build)
SKIP_BUILD=true SKIP_BUILD=true
shift shift
@ -469,6 +739,24 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
OS_NAME="$(uname -s)"
if [[ "$GUIDED_MODE" == "auto" ]]; then
if [[ "$OS_NAME" == "Linux" && "$ORIGINAL_ARG_COUNT" -eq 0 && -t 0 && -t 1 ]]; then
GUIDED_MODE="on"
else
GUIDED_MODE="off"
fi
fi
if [[ "$DOCKER_MODE" == true && "$GUIDED_MODE" == "on" ]]; then
warn "--guided is ignored with --docker."
GUIDED_MODE="off"
fi
if [[ "$GUIDED_MODE" == "on" ]]; then
run_guided_installer "$OS_NAME"
fi
if [[ "$DOCKER_MODE" == true ]]; then if [[ "$DOCKER_MODE" == true ]]; then
if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then
warn "--install-system-deps is ignored with --docker." warn "--install-system-deps is ignored with --docker."
@ -477,6 +765,15 @@ if [[ "$DOCKER_MODE" == true ]]; then
warn "--install-rust is ignored with --docker." warn "--install-rust is ignored with --docker."
fi fi
else else
if [[ "$OS_NAME" == "Linux" && -z "${ZEROCLAW_DISABLE_ALPINE_AUTO_DEPS:-}" ]] && have_cmd apk; then
find_missing_alpine_prereqs
if [[ ${#ALPINE_MISSING_PKGS[@]} -gt 0 && "$INSTALL_SYSTEM_DEPS" == false ]]; then
info "Detected Alpine with missing prerequisites: ${ALPINE_MISSING_PKGS[*]}"
info "Auto-enabling system dependency installation (set ZEROCLAW_DISABLE_ALPINE_AUTO_DEPS=1 to disable)."
INSTALL_SYSTEM_DEPS=true
fi
fi
if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then
install_system_deps install_system_deps
fi fi
@ -554,8 +851,8 @@ DONE
cat <<'DONE' cat <<'DONE'
Next steps: Next steps:
./bootstrap.sh --docker --interactive-onboard ./zeroclaw_install.sh --docker --interactive-onboard
./bootstrap.sh --docker --api-key "sk-..." --provider openrouter ./zeroclaw_install.sh --docker --api-key "sk-..." --provider openrouter
DONE DONE
exit 0 exit 0
fi fi
@ -588,7 +885,7 @@ if [[ "$PREBUILT_INSTALLED" == false && ( "$SKIP_BUILD" == false || "$SKIP_INSTA
cat <<'MSG' >&2 cat <<'MSG' >&2
Install Rust first: https://rustup.rs/ Install Rust first: https://rustup.rs/
or re-run with: or re-run with:
./bootstrap.sh --install-rust ./zeroclaw_install.sh --install-rust
MSG MSG
exit 1 exit 1
fi fi
@ -633,9 +930,9 @@ if [[ "$RUN_ONBOARD" == true ]]; then
Use either: Use either:
--api-key "sk-..." --api-key "sk-..."
or: or:
ZEROCLAW_API_KEY="sk-..." ./bootstrap.sh --onboard ZEROCLAW_API_KEY="sk-..." ./zeroclaw_install.sh --onboard
or run interactive: or run interactive:
./bootstrap.sh --interactive-onboard ./zeroclaw_install.sh --interactive-onboard
MSG MSG
exit 1 exit 1
fi fi

View file

@ -2,10 +2,15 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || pwd)"
INSTALLER_LOCAL="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd || pwd)/zeroclaw_install.sh"
BOOTSTRAP_LOCAL="$SCRIPT_DIR/bootstrap.sh" BOOTSTRAP_LOCAL="$SCRIPT_DIR/bootstrap.sh"
REPO_URL="https://github.com/zeroclaw-labs/zeroclaw.git" REPO_URL="https://github.com/zeroclaw-labs/zeroclaw.git"
echo "[deprecated] scripts/install.sh -> bootstrap.sh" >&2 echo "[deprecated] scripts/install.sh -> ./zeroclaw_install.sh" >&2
if [[ -x "$INSTALLER_LOCAL" ]]; then
exec "$INSTALLER_LOCAL" "$@"
fi
if [[ -f "$BOOTSTRAP_LOCAL" ]]; then if [[ -f "$BOOTSTRAP_LOCAL" ]]; then
exec "$BOOTSTRAP_LOCAL" "$@" exec "$BOOTSTRAP_LOCAL" "$@"
@ -24,35 +29,15 @@ trap cleanup EXIT
git clone --depth 1 "$REPO_URL" "$TEMP_DIR" >/dev/null 2>&1 git clone --depth 1 "$REPO_URL" "$TEMP_DIR" >/dev/null 2>&1
if [[ -x "$TEMP_DIR/zeroclaw_install.sh" ]]; then
exec "$TEMP_DIR/zeroclaw_install.sh" "$@"
fi
if [[ -x "$TEMP_DIR/scripts/bootstrap.sh" ]]; then if [[ -x "$TEMP_DIR/scripts/bootstrap.sh" ]]; then
"$TEMP_DIR/scripts/bootstrap.sh" "$@" exec "$TEMP_DIR/scripts/bootstrap.sh" "$@"
exit 0
fi fi
echo "[deprecated] cloned revision has no bootstrap.sh; falling back to legacy source install flow" >&2 echo "error: zeroclaw_install.sh/bootstrap.sh was not found in the fetched revision." >&2
echo "Run the local bootstrap directly when possible:" >&2
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then echo " ./zeroclaw_install.sh --help" >&2
cat <<'USAGE'
Legacy install.sh fallback mode
Behavior:
- Clone repository
- cargo build --release --locked
- cargo install --path <clone> --force --locked
For the new dual-mode installer, use:
./bootstrap.sh --help
USAGE
exit 0
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "error: cargo is required for legacy install.sh fallback mode" >&2
echo "Install Rust first: https://rustup.rs/" >&2
exit 1 exit 1
fi
cargo build --release --locked --manifest-path "$TEMP_DIR/Cargo.toml"
cargo install --path "$TEMP_DIR" --force --locked
echo "Legacy source install completed." >&2

88
zeroclaw_install.sh Executable file
View file

@ -0,0 +1,88 @@
#!/usr/bin/env sh
set -eu
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
run_privileged() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif have_cmd sudo; then
sudo "$@"
else
echo "error: sudo is required to install missing dependencies." >&2
exit 1
fi
}
is_container_runtime() {
if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
return 0
fi
if [ -r /proc/1/cgroup ] && grep -Eq '(docker|containerd|kubepods|podman|lxc)' /proc/1/cgroup; then
return 0
fi
return 1
}
run_pacman() {
if ! is_container_runtime; then
run_privileged pacman "$@"
return $?
fi
PACMAN_CFG_TMP="$(mktemp /tmp/zeroclaw-pacman.XXXXXX.conf)"
cp /etc/pacman.conf "$PACMAN_CFG_TMP"
if ! grep -Eq '^[[:space:]]*DisableSandboxSyscalls([[:space:]]|$)' "$PACMAN_CFG_TMP"; then
printf '\nDisableSandboxSyscalls\n' >> "$PACMAN_CFG_TMP"
fi
if run_privileged pacman --config "$PACMAN_CFG_TMP" "$@"; then
PACMAN_RC=0
else
PACMAN_RC=$?
fi
rm -f "$PACMAN_CFG_TMP"
return "$PACMAN_RC"
}
ensure_bash() {
if have_cmd bash; then
return 0
fi
echo "==> bash not found; attempting to install it"
if have_cmd apk; then
run_privileged apk add --no-cache bash
elif have_cmd apt-get; then
run_privileged apt-get update -qq
run_privileged apt-get install -y bash
elif have_cmd dnf; then
run_privileged dnf install -y bash
elif have_cmd pacman; then
run_pacman -Sy --noconfirm
run_pacman -S --noconfirm --needed bash
else
echo "error: unsupported package manager; install bash manually and retry." >&2
exit 1
fi
}
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" >/dev/null 2>&1 && pwd || pwd)"
BOOTSTRAP_SCRIPT="$ROOT_DIR/scripts/bootstrap.sh"
if [ ! -f "$BOOTSTRAP_SCRIPT" ]; then
echo "error: scripts/bootstrap.sh not found from repository root." >&2
exit 1
fi
ensure_bash
if [ "$#" -eq 0 ]; then
exec bash "$BOOTSTRAP_SCRIPT" --guided
fi
exec bash "$BOOTSTRAP_SCRIPT" "$@"