Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

1373 lines
34 KiB
Bash

#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || {
printf '%s\n' "Error: install.sh requires bash. Run 'bash install.sh' or pipe it into bash." >&2
exit 1
}
set -euo pipefail
if [ -t 1 ]; then
COLOR_RESET=$'\033[0m'
COLOR_BOLD=$'\033[1m'
COLOR_DIM=$'\033[2m'
COLOR_RED=$'\033[31m'
COLOR_GREEN=$'\033[32m'
COLOR_YELLOW=$'\033[33m'
COLOR_CYAN=$'\033[36m'
SUCCESS_MARK="✓"
else
COLOR_RESET=""
COLOR_BOLD=""
COLOR_DIM=""
COLOR_RED=""
COLOR_GREEN=""
COLOR_YELLOW=""
COLOR_CYAN=""
SUCCESS_MARK="Success:"
fi
REPO="${OPENSRE_INSTALL_REPO:-Tracer-Cloud/opensre}"
DEFAULT_INSTALL_DIR="${HOME}/.local/bin"
USER_INSTALL_DIR_CANDIDATES="${OPENSRE_USER_INSTALL_DIR_CANDIDATES:-$HOME/.local/bin:$HOME/bin}"
SYSTEM_INSTALL_DIR_CANDIDATES="${OPENSRE_SYSTEM_INSTALL_DIR_CANDIDATES:-/opt/homebrew/bin:/usr/local/bin:/opt/local/bin}"
INSTALL_DIR="${OPENSRE_INSTALL_DIR:-}"
INSTALL_DIR_OVERRIDE=0
INSTALL_CHANNEL="${OPENSRE_INSTALL_CHANNEL:-main}"
INSTALL_CHANNEL_EXPLICIT=0
[ -n "${OPENSRE_INSTALL_CHANNEL:-}" ] && INSTALL_CHANNEL_EXPLICIT=1
MAIN_RELEASE_TAG="${OPENSRE_MAIN_RELEASE_TAG:-main-build}"
BIN_NAME="opensre"
PROGRESS_PID=""
requested_version="${OPENSRE_VERSION:-}"
[ -n "$INSTALL_DIR" ] && INSTALL_DIR_OVERRIDE=1
requested_version="${requested_version#v}"
log() {
printf '%s\n' "$*"
}
warn() {
printf '%sWarning:%s %s\n' "${COLOR_YELLOW:-}" "${COLOR_RESET:-}" "$*" >&2
}
die() {
printf '%sError:%s %s\n' "${COLOR_RED:-}" "${COLOR_RESET:-}" "$*" >&2
exit 1
}
success() {
printf '%s%s %s%s\n' "${COLOR_GREEN:-}" "${SUCCESS_MARK:-Success:}" "$*" "${COLOR_RESET:-}"
}
step() {
printf '%s%s%s\n' "${COLOR_CYAN:-}" "$*" "${COLOR_RESET:-}"
}
install_verbose() {
case "${OPENSRE_INSTALL_VERBOSE:-}" in
1|true|TRUE|yes|YES)
return 0
;;
*)
return 1
;;
esac
}
is_interactive_terminal() {
[ -t 1 ] && [ "${TERM:-}" != "dumb" ] && ! install_verbose
}
terminal_supports_unicode() {
local locale_value="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
case "$locale_value" in
*UTF-8*|*utf-8*|*UTF8*|*utf8*)
return 0
;;
esac
case "${TERM_PROGRAM:-}" in
Apple_Terminal|iTerm.app|vscode|WezTerm)
return 0
;;
esac
return 1
}
terminal_columns() {
local cols=""
if command -v tput >/dev/null 2>&1; then
cols="$(tput cols 2>/dev/null || true)"
fi
if [ -z "$cols" ]; then
cols="${COLUMNS:-}"
fi
case "$cols" in
''|*[!0-9]*)
cols=80
;;
esac
if [ "$cols" -lt 20 ]; then
cols=20
fi
printf '%s\n' "$cols"
}
truncate_text() {
local value="$1"
local max_width="$2"
value="${value//$'\r'/ }"
value="${value//$'\n'/ }"
if [ "$max_width" -le 0 ]; then
return
fi
if [ "${#value}" -le "$max_width" ]; then
printf '%s' "$value"
return
fi
if [ "$max_width" -le 3 ]; then
printf '%.*s' "$max_width" "$value"
return
fi
printf '%.*s...' "$((max_width - 3))" "$value"
}
friendly_progress_label() {
local label="$1"
case "$label" in
*Fetching\ latest\ main\ build\ metadata*|*Fetching\ latest\ release\ version*|*Fetching\ release\ metadata*)
printf 'fetching metadata'
;;
*Preparing\ opensre*)
printf 'resolving build'
;;
*Downloading\ release\ archive*)
printf 'downloading archive'
;;
*Downloading\ and\ verifying\ checksum*|*Verifying\ release\ archive*)
printf 'verifying checksum'
;;
*Extracting\ and\ verifying\ binary*)
printf 'verifying binary'
;;
*Installing\ *opensre*)
printf 'installing binary'
;;
*)
label="${label#*\] }"
printf '%s' "$label"
;;
esac
}
progress_frame() {
local step_count="$1"
if terminal_supports_unicode; then
case $((step_count % 10)) in
0) printf '⠋' ;;
1) printf '⠙' ;;
2) printf '⠹' ;;
3) printf '⠸' ;;
4) printf '⠼' ;;
5) printf '⠴' ;;
6) printf '⠦' ;;
7) printf '⠧' ;;
8) printf '⠇' ;;
*) printf '⠏' ;;
esac
return
fi
case $((step_count % 4)) in
0) printf '-' ;;
1) printf '\\' ;;
2) printf '|' ;;
*) printf '/' ;;
esac
}
draw_progress() {
local label="$1"
local step_count="$2"
local title="${3:-Installing OpenSRE}"
local frame
local columns
local reserve
local available
local width
local label_width
local short_label
local trail=8
local head
local bar=""
local i=0
columns="$(terminal_columns)"
if [ "$columns" -lt 56 ]; then
title="OpenSRE"
fi
reserve=$((2 + 1 + 1 + 1 + ${#title} + 1))
available=$((columns - reserve))
if [ "$available" -lt 12 ]; then
width=4
else
width=$((available / 2))
if [ "$width" -gt 28 ]; then
width=28
fi
if [ "$width" -lt 8 ]; then
width=8
fi
fi
label_width=$((columns - reserve - width))
if [ "$label_width" -lt 8 ] && [ "$width" -gt 4 ]; then
width=$((columns - reserve - 8))
if [ "$width" -lt 4 ]; then
width=4
fi
label_width=$((columns - reserve - width))
fi
if [ "$label_width" -lt 0 ]; then
label_width=0
fi
short_label="$(truncate_text "$(friendly_progress_label "$label")" "$label_width")"
frame="$(progress_frame "$step_count")"
head=$((step_count % (width + trail)))
while [ "$i" -lt "$width" ]; do
local age=$((head - i))
if [ "$age" -ge 0 ] && [ "$age" -lt "$trail" ]; then
if terminal_supports_unicode; then
case "$age" in
0|1) bar="${bar}${COLOR_GREEN:-}${COLOR_RESET:-}" ;;
2|3) bar="${bar}${COLOR_CYAN:-}${COLOR_RESET:-}" ;;
4|5) bar="${bar}${COLOR_RED:-}${COLOR_RESET:-}" ;;
*) bar="${bar}${COLOR_YELLOW:-}${COLOR_RESET:-}" ;;
esac
else
bar="${bar}#"
fi
else
if terminal_supports_unicode; then
bar="${bar}${COLOR_DIM:-}${COLOR_RESET:-}"
else
bar="${bar}-"
fi
fi
i=$((i + 1))
done
printf '\r\033[K %s%s%s %s %s%s%s %s' \
"${COLOR_YELLOW:-}" "$frame" "${COLOR_RESET:-}" \
"$bar" "${COLOR_BOLD:-}" "$title" "${COLOR_RESET:-}" "$short_label"
}
animate_progress() {
local label="$1"
local step_count=0
while :; do
draw_progress "$label" "$step_count"
step_count=$((step_count + 1))
sleep 0.08
done
}
finish_progress() {
local progress_pid="${1:-}"
if [ -n "$progress_pid" ]; then
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
fi
printf '\r\033[K\033[?25h'
}
run_with_progress() {
local label="$1"
shift
if ! is_interactive_terminal; then
step "$label"
"$@"
return
fi
local log_file
local command_pid
local status
log_file="$(mktemp "${TMPDIR:-/tmp}/opensre-install-progress.XXXXXX")"
"$@" >"$log_file" 2>&1 &
command_pid=$!
printf '\033[?25l'
animate_progress "$label" &
PROGRESS_PID=$!
trap 'kill "$command_pid" 2>/dev/null || true; finish_progress "$PROGRESS_PID"; rm -f "$log_file"; exit 130' INT TERM
if wait "$command_pid"; then
status=0
else
status=$?
fi
finish_progress "$PROGRESS_PID"
PROGRESS_PID=""
trap - INT TERM
if [ "$status" -ne 0 ]; then
printf '%sError:%s %s failed (exit %s).%s\n' "${COLOR_RED:-}" "${COLOR_RESET:-}" "$label" "$status" "${COLOR_RESET:-}" >&2
if [ "$status" -gt 128 ]; then
printf 'Process was terminated by signal %s (e.g. killed by the OS, possibly out-of-memory).\n' "$((status - 128))" >&2
fi
if [ -s "$log_file" ]; then
cat "$log_file" >&2
else
printf '(no output was captured before the process ended)\n' >&2
fi
rm -f "$log_file"
return "$status"
fi
rm -f "$log_file"
printf ' %s%s%s %s\n' "${COLOR_GREEN:-}" "${SUCCESS_MARK:-ok}" "${COLOR_RESET:-}" "$label"
}
capture_with_progress() {
local __result_var="$1"
local label="$2"
shift 2
if ! is_interactive_terminal; then
step "$label"
local captured
local status
if captured="$("$@")"; then
printf -v "$__result_var" '%s' "$captured"
return
else
status=$?
fi
if [ -n "$captured" ]; then
printf '%s\n' "$captured" >&2
fi
return "$status"
fi
local stdout_file
local stderr_file
local command_pid
local status
stdout_file="$(mktemp "${TMPDIR:-/tmp}/opensre-install-stdout.XXXXXX")"
stderr_file="$(mktemp "${TMPDIR:-/tmp}/opensre-install-stderr.XXXXXX")"
"$@" >"$stdout_file" 2>"$stderr_file" &
command_pid=$!
printf '\033[?25l'
animate_progress "$label" &
PROGRESS_PID=$!
trap 'kill "$command_pid" 2>/dev/null || true; finish_progress "$PROGRESS_PID"; rm -f "$stdout_file" "$stderr_file"; exit 130' INT TERM
if wait "$command_pid"; then
status=0
else
status=$?
fi
finish_progress "$PROGRESS_PID"
PROGRESS_PID=""
trap - INT TERM
if [ "$status" -ne 0 ]; then
printf '%sError:%s %s failed (exit %s).%s\n' "${COLOR_RED:-}" "${COLOR_RESET:-}" "$label" "$status" "${COLOR_RESET:-}" >&2
if [ "$status" -gt 128 ]; then
printf 'Process was terminated by signal %s (e.g. killed by the OS, possibly out-of-memory).\n' "$((status - 128))" >&2
fi
if [ ! -s "$stdout_file" ] && [ ! -s "$stderr_file" ]; then
printf '(no output was captured before the process ended)\n' >&2
else
cat "$stdout_file" >&2
cat "$stderr_file" >&2
fi
rm -f "$stdout_file" "$stderr_file"
return "$status"
fi
printf -v "$__result_var" '%s' "$(cat "$stdout_file")"
rm -f "$stdout_file" "$stderr_file"
printf ' %s%s%s %s\n' "${COLOR_GREEN:-}" "${SUCCESS_MARK:-ok}" "${COLOR_RESET:-}" "$label"
}
print_installer_header() {
if ! is_interactive_terminal; then
return
fi
log "${COLOR_BOLD:-}${COLOR_CYAN:-}OpenSRE Installer${COLOR_RESET:-}"
log "${COLOR_BOLD:-}Installing the OpenSRE CLI${COLOR_RESET:-}"
log ""
}
usage() {
cat <<'EOF'
Usage: install.sh [--main] [--release] [--version <version>] [--install-dir <path>]
Installs the OpenSRE CLI.
Options:
--main Install the latest build published from the main branch (default).
--release Install the latest versioned release instead of main.
--version <version> Install a specific versioned release (for example 2026.4.29).
--install-dir <path> Install into a specific directory.
-h, --help Show this help text.
Examples:
curl -fsSL https://install.opensre.com | bash
curl -fsSL https://install.opensre.com | bash -s -- --main
curl -fsSL https://install.opensre.com | bash -s -- --version 2026.4.29
EOF
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--main)
INSTALL_CHANNEL="main"
INSTALL_CHANNEL_EXPLICIT=1
;;
--release)
INSTALL_CHANNEL="release"
INSTALL_CHANNEL_EXPLICIT=1
;;
--version)
[ "$#" -ge 2 ] || die "--version requires a value."
requested_version="${2#v}"
shift
;;
--install-dir)
[ "$#" -ge 2 ] || die "--install-dir requires a value."
INSTALL_DIR="$2"
INSTALL_DIR_OVERRIDE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
shift
done
case "$INSTALL_CHANNEL" in
release|main) ;;
*)
die "Unsupported install channel: ${INSTALL_CHANNEL}"
;;
esac
if [ -n "$requested_version" ] && [ "$INSTALL_CHANNEL" = "main" ] && [ "$INSTALL_CHANNEL_EXPLICIT" -eq 0 ]; then
INSTALL_CHANNEL="release"
fi
if [ "$INSTALL_CHANNEL" = "main" ] && [ -n "$requested_version" ]; then
die "--version cannot be combined with --main."
fi
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "'$1' is required but was not found in PATH."
}
require_prerequisites() {
need_cmd curl
need_cmd grep
need_cmd sed
need_cmd tr
need_cmd uname
}
CURL_FLAGS=(
--fail
--silent
--show-error
--location
--retry 3
--retry-delay 1
)
download_to() {
local url="$1"
local destination="$2"
curl "${CURL_FLAGS[@]}" -o "$destination" "$url"
}
download_text() {
local url="$1"
curl "${CURL_FLAGS[@]}" \
-H "Accept: application/vnd.github+json" \
-H "User-Agent: opensre-install-script" \
"$url"
}
fetch_release_json() {
local version="${1:-}"
local api_url
if [ "$INSTALL_CHANNEL" = "main" ]; then
api_url="https://api.github.com/repos/${REPO}/releases/tags/${MAIN_RELEASE_TAG}"
elif [ -n "$version" ]; then
api_url="https://api.github.com/repos/${REPO}/releases/tags/v${version}"
else
api_url="https://api.github.com/repos/${REPO}/releases/latest"
fi
download_text "$api_url"
}
extract_tag_name() {
local release_json="$1"
printf '%s\n' "$release_json" | sed -n '/"tag_name"[[:space:]]*:/{
s/.*"tag_name":[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p
q
}'
}
release_has_asset() {
local release_json="$1"
local asset_name="$2"
printf '%s' "$release_json" | tr -d '\r\n\t ' | grep -F "\"name\":\"${asset_name}\"" >/dev/null 2>&1
}
build_archive_name() {
local version="$1"
local asset_arch="$2"
local archive_version="$version"
if [ "$INSTALL_CHANNEL" = "main" ]; then
archive_version="main"
fi
if [ "$platform" = "windows" ]; then
printf 'opensre_%s_windows-%s.zip\n' "$archive_version" "$asset_arch"
return
fi
printf 'opensre_%s_%s-%s.tar.gz\n' "$archive_version" "$platform" "$asset_arch"
}
path_has_dir() {
case ":$PATH:" in
*":$1:"*)
return 0
;;
esac
return 1
}
is_candidate_dir_writable() {
local dir="$1"
local parent_dir
if [ -d "$dir" ]; then
[ -w "$dir" ]
return
fi
parent_dir="${dir%/*}"
[ -n "$parent_dir" ] || parent_dir="/"
[ -d "$parent_dir" ] && [ -w "$parent_dir" ]
}
select_writable_path_candidate_from_list() {
local candidate_list="$1"
local old_ifs="$IFS"
local dir
IFS=':'
for dir in $candidate_list; do
case "$dir" in
/*) ;;
*) continue ;;
esac
if path_has_dir "$dir" && is_candidate_dir_writable "$dir"; then
printf '%s\n' "$dir"
IFS="$old_ifs"
return 0
fi
done
IFS="$old_ifs"
return 1
}
resolve_install_dir() {
local existing_bin=""
local existing_dir=""
if [ -n "$INSTALL_DIR" ]; then
return
fi
if [ "$platform" = "windows" ]; then
INSTALL_DIR="$DEFAULT_INSTALL_DIR"
return
fi
if command -v opensre >/dev/null 2>&1; then
existing_bin="$(command -v opensre || true)"
existing_dir="${existing_bin%/*}"
if [ -n "$existing_dir" ] && path_has_dir "$existing_dir" && is_candidate_dir_writable "$existing_dir"; then
INSTALL_DIR="$existing_dir"
return
fi
fi
if INSTALL_DIR="$(select_writable_path_candidate_from_list "$USER_INSTALL_DIR_CANDIDATES")"; then
return
fi
if INSTALL_DIR="$(select_writable_path_candidate_from_list "$SYSTEM_INSTALL_DIR_CANDIDATES")"; then
return
fi
INSTALL_DIR="$DEFAULT_INSTALL_DIR"
}
ps_escape() {
printf '%s' "$1" | sed "s/'/''/g"
}
to_windows_path() {
local posix_path="$1"
if command -v cygpath >/dev/null 2>&1; then
cygpath -w "$posix_path"
return
fi
die "PowerShell archive extraction requires 'cygpath' when 'unzip' is unavailable."
}
extract_zip() {
local archive_path="$1"
local destination_dir="$2"
local archive_for_ps
local destination_for_ps
if command -v unzip >/dev/null 2>&1; then
unzip -q "$archive_path" -d "$destination_dir"
return
fi
archive_for_ps="$(ps_escape "$(to_windows_path "$archive_path")")"
destination_for_ps="$(ps_escape "$(to_windows_path "$destination_dir")")"
if command -v powershell.exe >/dev/null 2>&1; then
powershell.exe -NoLogo -NoProfile -NonInteractive -Command \
"Expand-Archive -LiteralPath '$archive_for_ps' -DestinationPath '$destination_for_ps' -Force" \
>/dev/null
return
fi
if command -v pwsh >/dev/null 2>&1; then
pwsh -NoLogo -NoProfile -NonInteractive -Command \
"Expand-Archive -LiteralPath '$archive_for_ps' -DestinationPath '$destination_for_ps' -Force" \
>/dev/null
return
fi
die "A zip extractor is required on Windows. Install 'unzip' or run the PowerShell installer."
}
extract_archive() {
local archive_path="$1"
local destination_dir="$2"
if [ "$platform" = "windows" ]; then
extract_zip "$archive_path" "$destination_dir"
return
fi
need_cmd tar
tar -xzf "$archive_path" -C "$destination_dir"
}
verify_checksum() {
local checksum_path="$1"
local archive_path="$2"
local archive_dir
local checksum_name
local normalized_checksum_path
local expected
local actual
archive_dir="${archive_path%/*}"
checksum_name="${checksum_path##*/}"
normalized_checksum_path="${checksum_path}.normalized"
tr -d '\r' < "$checksum_path" > "$normalized_checksum_path"
checksum_path="$normalized_checksum_path"
checksum_name="${checksum_path##*/}"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$archive_dir" && sha256sum -c "$checksum_name") >/dev/null \
|| die "Checksum verification failed for '${archive_path##*/}'."
return
fi
if command -v shasum >/dev/null 2>&1; then
(cd "$archive_dir" && shasum -a 256 -c "$checksum_name") >/dev/null \
|| die "Checksum verification failed for '${archive_path##*/}'."
return
fi
if command -v openssl >/dev/null 2>&1; then
expected="$(sed -n 's/^\([0-9A-Fa-f]\{64\}\)[[:space:]][[:space:]]*.*/\1/p' "$checksum_path")"
[ -n "$expected" ] || die "Checksum file '${checksum_name}' is malformed."
actual="$(openssl dgst -sha256 "$archive_path" | sed 's/^.*= //')"
[ "$expected" = "$actual" ] || die "Checksum verification failed for '${archive_path##*/}'."
return
fi
warn "No checksum verifier found (sha256sum, shasum, or openssl). Skipping checksum verification."
}
binary_app_root() {
local binary_path="$1"
local binary_dir
binary_dir="${binary_path%/*}"
if [ -d "${binary_dir}/_internal" ]; then
printf '%s\n' "$binary_dir"
return 0
fi
return 1
}
install_binary() {
local source_path="$1"
local destination_path="$2"
if command -v install >/dev/null 2>&1; then
install -m 0755 "$source_path" "$destination_path"
return
fi
cp "$source_path" "$destination_path"
chmod 0755 "$destination_path" 2>/dev/null || true
}
install_binary_app() {
local app_root="$1"
local destination_path="$2"
local app_destination_dir="${INSTALL_DIR}/.${BIN_NAME}-app"
local app_tmp_dir="${app_destination_dir}.new.$$"
local app_old_dir="${app_destination_dir}.old.$$"
rm -rf "$app_tmp_dir" "$app_old_dir"
cp -R "$app_root" "$app_tmp_dir"
chmod -R u+rwX,go+rX "$app_tmp_dir" 2>/dev/null || true
if [ -e "$app_destination_dir" ]; then
mv "$app_destination_dir" "$app_old_dir"
fi
mv "$app_tmp_dir" "$app_destination_dir"
rm -rf "$app_old_dir"
rm -f "$destination_path"
ln -s "$app_destination_dir/${BIN_NAME}" "$destination_path"
}
install_verified_binary() {
local source_path="$1"
local destination_path="$2"
local app_root=""
mkdir -p "$INSTALL_DIR"
if [ "$platform" != "windows" ] && app_root="$(binary_app_root "$source_path")"; then
install_binary_app "$app_root" "$destination_path"
return
fi
install_binary "$source_path" "$destination_path"
}
download_and_verify_checksum() {
local checksum_url="$1"
local checksum_path="$2"
local archive_path="$3"
download_to "$checksum_url" "$checksum_path"
verify_checksum "$checksum_path" "$archive_path"
}
extract_and_verify_binary() {
local archive_path="$1"
local extraction_dir="$2"
local extracted_binary_path
local extracted_version
local extract_status
printf 'Extracting %s into %s...\n' "$archive_path" "$extraction_dir" >&2
set +e
extract_archive "$archive_path" "$extraction_dir"
extract_status=$?
set -e
if [ "$extract_status" -ne 0 ]; then
printf 'Archive extraction failed (exit %s).\n' "$extract_status" >&2
return "$extract_status"
fi
printf 'Extraction finished, locating %s binary...\n' "$BIN_NAME" >&2
extracted_binary_path="$(get_binary_path_from_archive "$extraction_dir" "$BIN_NAME")"
printf 'Found binary at %s, verifying it runs...\n' "$extracted_binary_path" >&2
if [ "$INSTALL_CHANNEL" = "main" ]; then
extracted_version="$(verify_binary_version "$extracted_binary_path")" || return "$?"
else
extracted_version="$(verify_binary_version "$extracted_binary_path" "$version")" || return "$?"
fi
printf '%s\n%s\n' "$extracted_binary_path" "$extracted_version"
}
get_binary_path_from_archive() {
local extraction_root="$1"
local binary_name="$2"
local direct_binary_path
local binary_candidates=()
local binary_locations
direct_binary_path="${extraction_root}/${binary_name}"
if [ -f "$direct_binary_path" ]; then
printf '%s\n' "$direct_binary_path"
return
fi
need_cmd find
while IFS= read -r candidate; do
binary_candidates+=("$candidate")
done < <(find "$extraction_root" -type f -name "$binary_name")
case "${#binary_candidates[@]}" in
1)
printf '%s\n' "${binary_candidates[0]}"
;;
0)
die "Archive did not contain '${binary_name}'."
;;
*)
binary_locations="$(printf '%s, ' "${binary_candidates[@]}")"
binary_locations="${binary_locations%, }"
die "Found multiple '${binary_name}' files after extraction: ${binary_locations}"
;;
esac
}
verify_binary_version() {
local binary_path="$1"
local expected_version="${2:-}"
local version_output
local version_status
local actual_version
set +e
version_output="$("$binary_path" --version 2>&1)"
version_status=$?
set -e
if [ "$version_status" -ne 0 ]; then
printf 'Failed to execute %s --version (exit %s).\n' "${binary_path##*/}" "$version_status" >&2
if [ -n "$version_output" ]; then
printf 'Command output:\n%s\n' "$version_output" >&2
else
printf 'Command output: <empty>\n' >&2
fi
print_binary_diagnostics "$binary_path"
return 1
fi
actual_version="$(printf '%s\n' "$version_output" | sed -n 's/.*\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -n 1)"
if [ -z "$expected_version" ]; then
if [ -n "$actual_version" ]; then
printf '%s\n' "$actual_version"
else
printf 'main\n'
fi
return
fi
case "$version_output" in
*"$expected_version"*)
printf '%s\n' "$expected_version"
;;
*)
if [ -n "$requested_version" ] || [ -z "$actual_version" ]; then
die "Downloaded binary version mismatch. Expected '${expected_version}' but got: ${version_output}"
fi
warn "Latest release metadata reports v${expected_version}, but the downloaded binary reports v${actual_version}. Installing the verified binary anyway."
printf '%s\n' "$actual_version"
;;
esac
}
print_binary_diagnostics() {
local binary_path="$1"
printf 'Binary diagnostics:\n' >&2
printf ' path: %s\n' "$binary_path" >&2
if command -v uname >/dev/null 2>&1; then
printf ' system: %s\n' "$(uname -a 2>/dev/null || true)" >&2
fi
if command -v ls >/dev/null 2>&1; then
ls -l "$binary_path" >&2 2>/dev/null || true
fi
if command -v file >/dev/null 2>&1; then
file "$binary_path" >&2 2>/dev/null || true
fi
if [ "$platform" = "linux" ] && command -v ldd >/dev/null 2>&1; then
ldd "$binary_path" >&2 2>/dev/null || true
fi
}
configure_path() {
case ":$PATH:" in
*":${INSTALL_DIR}:"*)
return
;;
esac
if [ "$platform" = "windows" ]; then
warn "'${INSTALL_DIR}' is not in PATH for this shell. Add it to Git Bash or Windows PATH to run ${BIN_NAME:-opensre} from any terminal."
return
fi
local rc_file=""
local path_line=""
local shell_name
shell_name="${SHELL##*/}"
case "$shell_name" in
zsh)
rc_file="${HOME}/.zshrc"
path_line="export PATH=\"${INSTALL_DIR}:\$PATH\""
;;
bash)
if [ "$platform" = "darwin" ]; then
rc_file="${HOME}/.bash_profile"
else
rc_file="${HOME}/.bashrc"
fi
path_line="export PATH=\"${INSTALL_DIR}:\$PATH\""
;;
fish)
rc_file="${HOME}/.config/fish/config.fish"
path_line="fish_add_path \"${INSTALL_DIR}\""
;;
*)
log "Add the following line to your shell profile to use ${BIN_NAME:-opensre}:"
log " export PATH=\"${INSTALL_DIR}:\$PATH\""
return
;;
esac
local rc_dir="${rc_file%/*}"
[ "$rc_dir" != "$rc_file" ] && [ ! -d "$rc_dir" ] && mkdir -p "$rc_dir"
if [ -f "$rc_file" ] && grep -qF "${INSTALL_DIR}" "$rc_file"; then
return
fi
local marker="# Added by opensre installer"
if [ -f "$rc_file" ] && grep -qF "$marker" "$rc_file" && grep -qF "${INSTALL_DIR}" "$rc_file"; then
return
fi
printf '\n%s\n%s\n' "$marker" "$path_line" >> "$rc_file"
log ""
log "${BIN_NAME:-opensre} has been added to PATH in ${rc_file}."
log "To apply now, run: source \"${rc_file}\""
log "Or open a new terminal."
}
ensure_on_path() {
# A child process cannot edit the parent shell's PATH, so when the install
# dir is not already on PATH, link the binary into a user-writable directory
# that is — no sudo, and `opensre` works in this terminal immediately. Only
# when no PATH directory is writable fall back to the shell rc update.
local link_dir
if [ "$platform" != "windows" ] && ! path_has_dir "$INSTALL_DIR"; then
if link_dir="$(select_writable_path_candidate_from_list "$PATH")" \
&& mkdir -p "$link_dir" 2>/dev/null \
&& ln -sf "${INSTALL_DIR}/${BIN_NAME}" "${link_dir}/${BIN_NAME}" 2>/dev/null; then
success "Linked ${BIN_NAME} into ${link_dir} — ready to run in this terminal."
return
fi
fi
configure_path
}
print_success_screen() {
local version="$1"
local sep="────────────────────────────────────────────"
if [ ! -t 1 ]; then
sep="--------------------------------------------"
fi
log ""
log "$sep"
success "Welcome to OpenSRE"
if [ "$version" = "main" ]; then
log " ${COLOR_BOLD:-}opensre (main build) installed successfully${COLOR_RESET:-}"
else
log " ${COLOR_BOLD:-}opensre v${version} installed successfully${COLOR_RESET:-}"
fi
log "$sep"
log ""
log "Next steps:"
log " 1. Run ${BIN_NAME:-opensre} onboard"
log " Set up your LLM provider and add your observability integrations."
log ""
log " 2. Run ${BIN_NAME:-opensre} (no subcommand)"
log " From a normal interactive terminal this starts the interactive shell — type a"
log " prompt or incident description at the prompt to investigate."
log ""
log " 3. Optional — one-shot RCA from a file:"
log " ${BIN_NAME:-opensre} investigate -i path/to/alert.json"
log ""
log "Docs: https://www.opensre.com/docs"
log ""
}
auto_launch_disabled() {
case "${OPENSRE_AUTO_LAUNCH:-}" in
0|false|FALSE|no|NO|off|OFF)
return 0
;;
*)
return 1
;;
esac
}
controlling_tty_usable() {
# The onboarding wizard needs a terminal it can actually control: it calls
# tcgetattr/tcsetattr on its stdin. `stty -g` performs the same tcgetattr,
# so where it fails, launching the wizard would die with a "terminal I/O
# error" mid-render (issue #3273). Only offer /dev/tty when it passes.
command -v stty >/dev/null 2>&1 || return 1
[ -r /dev/tty ] && [ -w /dev/tty ] || return 1
stty -g </dev/tty >/dev/null 2>&1
}
run_onboarding() {
# Uses the caller's `installed_binary` (bash dynamic scoping); any redirect
# on the call applies to the wizard.
log "Launching ${BIN_NAME} onboard..."
"$installed_binary" onboard || \
warn "Onboarding exited before completion. Run '${BIN_NAME} onboard' to retry."
}
launch_onboarding_after_install() {
local installed_binary="${INSTALL_DIR}/${BIN_NAME}"
# The full-screen wizard needs a real terminal on stdout to render.
if auto_launch_disabled || [ ! -t 1 ]; then
return
fi
if [ ! -x "$installed_binary" ]; then
warn "Could not auto-launch onboarding; ${installed_binary} is not executable."
return
fi
if [ -t 0 ]; then
run_onboarding
elif [ "$platform" != "windows" ] && controlling_tty_usable; then
# Piped install (the documented `curl … | bash`): stdin is the curl pipe,
# so hand the wizard the controlling terminal instead. Git Bash on Windows
# emulates /dev/tty in ways the wizard cannot control, so the piped
# auto-launch stays darwin/linux-only.
run_onboarding </dev/tty
fi
}
cleanup() {
if [ -n "${tmp_dir:-}" ] && [ -d "$tmp_dir" ]; then
rm -rf "$tmp_dir"
fi
}
detect_platform() {
local os
local arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux)
platform="linux"
;;
Darwin)
platform="darwin"
;;
MINGW*|MSYS*|CYGWIN*)
platform="windows"
BIN_NAME="opensre.exe"
log "Detected Windows environment (${os})."
;;
*)
die "Unsupported operating system: $os"
;;
esac
case "$arch" in
x86_64|amd64)
target_arch="x64"
;;
arm64|aarch64)
target_arch="arm64"
;;
*)
die "Unsupported architecture: $arch"
;;
esac
}
resolve_release_metadata() {
version="$requested_version"
release_tag=""
if [ "$INSTALL_CHANNEL" = "main" ]; then
metadata_step="[1/6] Fetching latest main build metadata"
elif [ -n "$version" ]; then
metadata_step="[1/6] Fetching release metadata for v${version}"
else
metadata_step="[1/6] Fetching latest release version"
fi
capture_with_progress release_json "$metadata_step" fetch_release_json "$version" || {
if [ "$INSTALL_CHANNEL" = "main" ]; then
die "Failed to query main build metadata from GitHub."
fi
die "Failed to query release metadata from GitHub."
}
if [ "$INSTALL_CHANNEL" = "main" ]; then
release_tag="$(extract_tag_name "$release_json")"
else
if [ -z "$version" ]; then
version="$(extract_tag_name "$release_json")"
fi
release_tag="v${version}"
fi
if [ "$INSTALL_CHANNEL" = "main" ]; then
[ -n "$release_tag" ] || die "Failed to determine the main build tag."
else
[ -n "$version" ] || die "Failed to determine the release version."
fi
}
select_archive_asset() {
local fallback_archive
asset_arch="$target_arch"
archive="$(build_archive_name "$version" "$asset_arch")"
if [ "$platform" = "windows" ] && [ "$target_arch" = "arm64" ] && ! release_has_asset "$release_json" "$archive"; then
fallback_archive="$(build_archive_name "$version" "x64")"
if release_has_asset "$release_json" "$fallback_archive"; then
asset_arch="x64"
archive="$fallback_archive"
warn "Windows ARM64 artifact is not published for v${version}; falling back to the x64 build."
fi
fi
if release_has_asset "$release_json" "$archive"; then
return
fi
if [ "$INSTALL_CHANNEL" = "main" ]; then
die "Main build release does not include asset '${archive}'."
fi
die "Release v${version} does not include asset '${archive}'."
}
prepare_download() {
download_url="https://github.com/${REPO}/releases/download/${release_tag}/${archive}"
checksum_asset="${archive}.sha256"
checksum_url="${download_url}.sha256"
if [ "$INSTALL_CHANNEL" = "main" ]; then
step "[2/6] Preparing opensre main build (${platform}/${target_arch})"
else
step "[2/6] Preparing opensre v${version} (${platform}/${target_arch})"
fi
if [ "$asset_arch" != "$target_arch" ]; then
log "Using release asset built for ${platform}/${asset_arch}."
fi
if install_verbose; then
log " ${download_url}"
fi
}
dir_available_kib() {
local dir="$1"
local avail
command -v df >/dev/null 2>&1 || return 1
avail="$(df -Pk "$dir" 2>/dev/null | awk 'NR==2 { print $4 }')"
[ -n "$avail" ] || return 1
printf '%s\n' "$avail"
}
select_temp_base_dir() {
local min_kib="$1"
local candidate
local avail
for candidate in "${TMPDIR:-}" "${HOME:-}/.cache" /var/tmp /tmp; do
[ -n "$candidate" ] || continue
[ -d "$candidate" ] || continue
is_candidate_dir_writable "$candidate" || continue
avail="$(dir_available_kib "$candidate")" || continue
if [ "$avail" -ge "$min_kib" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
create_temp_workspace() {
local temp_base_dir
local min_kib=524288
need_cmd mktemp
if temp_base_dir="$(select_temp_base_dir "$min_kib")"; then
tmp_dir="$(mktemp -d "${temp_base_dir%/}/opensre-install.XXXXXX")"
else
warn "Could not find a temp directory with at least 512MB free; falling back to the system default and hoping for the best."
tmp_dir="$(mktemp -d)"
fi
trap cleanup EXIT
}
download_release_archive() {
archive_path="${tmp_dir}/${archive}"
run_with_progress "[3/6] Downloading release archive (${archive})" download_to "$download_url" "$archive_path" \
|| die "Failed to download '${archive}'."
}
verify_release_checksum() {
local checksum_path
if release_has_asset "$release_json" "$checksum_asset"; then
checksum_path="${tmp_dir}/${checksum_asset}"
run_with_progress "[4/6] Downloading and verifying checksum (${checksum_asset})" \
download_and_verify_checksum "$checksum_url" "$checksum_path" "$archive_path" \
|| die "Failed to download or verify checksum '${checksum_asset}'."
return
fi
if [ "$INSTALL_CHANNEL" = "main" ]; then
warn "Main build release is missing checksum asset '${checksum_asset}'."
else
warn "Release v${version} is missing checksum asset '${checksum_asset}'."
fi
}
extract_release_binary() {
local verified_binary
capture_with_progress verified_binary "[5/6] Extracting and verifying binary" extract_and_verify_binary "$archive_path" "$tmp_dir"
binary_path="${verified_binary%%$'\n'*}"
installed_version="${verified_binary#*$'\n'}"
}
install_release_binary() {
run_with_progress "[6/6] Installing ${BIN_NAME} to ${INSTALL_DIR}" install_verified_binary "$binary_path" "${INSTALL_DIR}/${BIN_NAME}"
}
print_install_confirmation() {
if [ "$INSTALL_CHANNEL" = "main" ]; then
if [ "$installed_version" = "main" ]; then
success "Installed ${BIN_NAME} main build to ${INSTALL_DIR}/${BIN_NAME}"
else
success "Installed ${BIN_NAME} main build (${installed_version}) to ${INSTALL_DIR}/${BIN_NAME}"
fi
else
success "Installed ${BIN_NAME} v${installed_version} to ${INSTALL_DIR}/${BIN_NAME}"
fi
}
finish_install() {
print_install_confirmation
ensure_on_path
print_success_screen "$installed_version"
launch_onboarding_after_install
}
main() {
parse_args "$@"
require_prerequisites
detect_platform
resolve_install_dir
print_installer_header
resolve_release_metadata
select_archive_asset
prepare_download
create_temp_workspace
download_release_archive
verify_release_checksum
extract_release_binary
install_release_binary
finish_install
}
main "$@"