| 1 | #!/usr/bin/env bash |
| 2 | # brt installer — pipe via: curl -fsSL https://brt.sushii.dev/install.sh | sh |
| 3 | [ -n "${BASH_VERSION:-}" ] || exec bash -s "$@" |
| 4 | set -euo pipefail |
| 5 | |
| 6 | BRT_INSTALL_DIR="${BRT_INSTALL_DIR:-${HOME}/.local/share/brt}" |
| 7 | BRT_BIN_DIR="${BRT_BIN_DIR:-${HOME}/.local/bin}" |
| 8 | BRT_BASE_URL="${BRT_BASE_URL:-https://brt.sushii.dev}" |
| 9 | |
| 10 | info() { printf '\033[36m●\033[0m %s\n' "$*"; } |
| 11 | ok() { printf '\033[32m✓\033[0m %s\n' "$*"; } |
| 12 | warn() { printf '\033[33m!\033[0m %s\n' "$*" >&2; } |
| 13 | error() { printf '\033[31m✗\033[0m %s\n' "$*" >&2; exit 1; } |
| 14 | |
| 15 | _brt_installer_os_windows() { |
| 16 | [[ "${OS:-}" == "Windows_NT" ]] && return 0 |
| 17 | case "$(uname -s 2>/dev/null)" in |
| 18 | MINGW*|MSYS*|CYGWIN*) return 0 ;; |
| 19 | esac |
| 20 | return 1 |
| 21 | } |
| 22 | |
| 23 | if _brt_installer_os_windows; then |
| 24 | warn "This installer is for macOS and Linux." |
| 25 | info "On Windows, use:" |
| 26 | printf ' irm https://brt.sushii.dev/win/install.ps1 | iex\n' |
| 27 | exit 1 |
| 28 | fi |
| 29 | |
| 30 | load_progress_bar() { |
| 31 | local root="${1:-}" |
| 32 | if [[ -n "$root" && -f "${root}/lib/progress.sh" ]]; then |
| 33 | # shellcheck source=/dev/null |
| 34 | source "${root}/lib/progress.sh" |
| 35 | return 0 |
| 36 | fi |
| 37 | |
| 38 | local tmp |
| 39 | tmp=$(mktemp) |
| 40 | curl -fsSL \ |
| 41 | -H 'Cache-Control: no-cache' \ |
| 42 | -H 'Pragma: no-cache' \ |
| 43 | "${BRT_BASE_URL}/lib/progress.sh" \ |
| 44 | -o "$tmp" || error "Failed to download ${BRT_BASE_URL}/lib/progress.sh" |
| 45 | # shellcheck source=/dev/null |
| 46 | source "$tmp" |
| 47 | rm -f "$tmp" |
| 48 | } |
| 49 | |
| 50 | BRT_LIB_MODULES="common progress deps ip dns portscan ping whois trace curl post headers ssl reverse edit json encode hash url grep uuid pass run serve watch listen killport du extract sysinfo publickey timestamp timer calc env which speed weather qr image video doom browse config access update uninstall" |
| 51 | |
| 52 | build_install_files() { |
| 53 | INSTALL_FILES=(brt) |
| 54 | local f |
| 55 | for f in ${BRT_LIB_MODULES}; do |
| 56 | INSTALL_FILES+=("lib/${f}.sh") |
| 57 | done |
| 58 | INSTALL_FILES+=(lib/access.py lib/media.py lib/browse.py version) |
| 59 | } |
| 60 | |
| 61 | fetch() { |
| 62 | local path="$1" |
| 63 | local dest="${BRT_INSTALL_DIR}/${path}" |
| 64 | mkdir -p "$(dirname "$dest")" |
| 65 | curl -fsSL \ |
| 66 | -H 'Cache-Control: no-cache' \ |
| 67 | -H 'Pragma: no-cache' \ |
| 68 | "${BRT_BASE_URL}/${path}" \ |
| 69 | -o "$dest" || error "Failed to download ${BRT_BASE_URL}/${path}" |
| 70 | } |
| 71 | |
| 72 | fetch_remote_version() { |
| 73 | curl -fsSL \ |
| 74 | -H 'Cache-Control: no-cache' \ |
| 75 | -H 'Pragma: no-cache' \ |
| 76 | "${BRT_BASE_URL}/version" \ |
| 77 | | tr -d '[:space:]' |
| 78 | } |
| 79 | |
| 80 | read_installed_version() { |
| 81 | if [[ -f "${BRT_INSTALL_DIR}/version" ]]; then |
| 82 | tr -d '[:space:]' < "${BRT_INSTALL_DIR}/version" |
| 83 | else |
| 84 | grep '^_BRT_EMBEDDED_VERSION=' "${BRT_INSTALL_DIR}/brt" 2>/dev/null | head -1 | cut -d'"' -f2 |
| 85 | fi |
| 86 | } |
| 87 | |
| 88 | verify_install_version() { |
| 89 | local expected="$1" |
| 90 | local installed embedded |
| 91 | |
| 92 | installed=$(read_installed_version) |
| 93 | embedded=$(grep '^_BRT_EMBEDDED_VERSION=' "${BRT_INSTALL_DIR}/brt" 2>/dev/null | head -1 | cut -d'"' -f2) |
| 94 | |
| 95 | [[ -n "$installed" ]] || error "Install failed: version file missing" |
| 96 | [[ "$installed" == "$expected" ]] || error "Install failed: version file is ${installed}, expected ${expected}" |
| 97 | [[ "$embedded" == "$expected" ]] || error "Install failed: brt binary is ${embedded}, expected ${expected}" |
| 98 | } |
| 99 | |
| 100 | install_from_local() { |
| 101 | local root="$1" |
| 102 | local path total i=0 |
| 103 | |
| 104 | build_install_files |
| 105 | total=${#INSTALL_FILES[@]} |
| 106 | |
| 107 | info "Installing brt from local source..." |
| 108 | mkdir -p "$BRT_INSTALL_DIR/lib" "$BRT_BIN_DIR" |
| 109 | load_progress_bar "$root" |
| 110 | |
| 111 | for path in "${INSTALL_FILES[@]}"; do |
| 112 | ((i++)) || true |
| 113 | progress_bar "$i" "$total" "$path" |
| 114 | mkdir -p "$(dirname "${BRT_INSTALL_DIR}/${path}")" |
| 115 | cp "${root}/${path}" "${BRT_INSTALL_DIR}/${path}" |
| 116 | done |
| 117 | |
| 118 | chmod +x "$BRT_INSTALL_DIR/brt" "$BRT_INSTALL_DIR/lib/"*.sh "$BRT_INSTALL_DIR/lib/access.py" "$BRT_INSTALL_DIR/lib/media.py" "$BRT_INSTALL_DIR/lib/browse.py" |
| 119 | } |
| 120 | |
| 121 | install_from_site() { |
| 122 | local remote_version="$1" |
| 123 | local path total i=0 |
| 124 | |
| 125 | build_install_files |
| 126 | total=${#INSTALL_FILES[@]} |
| 127 | |
| 128 | info "Downloading brt ${remote_version} from ${BRT_BASE_URL}..." |
| 129 | |
| 130 | rm -rf "$BRT_INSTALL_DIR" |
| 131 | mkdir -p "$BRT_INSTALL_DIR/lib" "$BRT_BIN_DIR" |
| 132 | load_progress_bar |
| 133 | |
| 134 | for path in "${INSTALL_FILES[@]}"; do |
| 135 | ((i++)) || true |
| 136 | progress_bar "$i" "$total" "$path" |
| 137 | fetch "$path" |
| 138 | done |
| 139 | |
| 140 | chmod +x "$BRT_INSTALL_DIR/brt" "$BRT_INSTALL_DIR/lib/"*.sh "$BRT_INSTALL_DIR/lib/access.py" "$BRT_INSTALL_DIR/lib/media.py" "$BRT_INSTALL_DIR/lib/browse.py" |
| 141 | verify_install_version "$remote_version" |
| 142 | } |
| 143 | |
| 144 | BRT_PATH_MARKER='# brt PATH' |
| 145 | |
| 146 | shell_rc_file() { |
| 147 | case "${SHELL##*/}" in |
| 148 | zsh) printf '%s' "${HOME}/.zshrc" ;; |
| 149 | bash) |
| 150 | if [[ "$(uname -s)" == "Darwin" ]]; then |
| 151 | printf '%s' "${HOME}/.bash_profile" |
| 152 | else |
| 153 | printf '%s' "${HOME}/.bashrc" |
| 154 | fi |
| 155 | ;; |
| 156 | *) printf '%s' "${HOME}/.profile" ;; |
| 157 | esac |
| 158 | } |
| 159 | |
| 160 | ensure_shell_path() { |
| 161 | local rc path_line |
| 162 | rc=$(shell_rc_file) |
| 163 | path_line="export PATH=\"${BRT_BIN_DIR}:\$PATH\"" |
| 164 | |
| 165 | if [[ -f "$rc" ]] && grep -qF "$BRT_PATH_MARKER" "$rc" 2>/dev/null; then |
| 166 | SHELL_RC_CONFIGURED="$rc" |
| 167 | return 0 |
| 168 | fi |
| 169 | |
| 170 | mkdir -p "$(dirname "$rc")" |
| 171 | touch "$rc" |
| 172 | { |
| 173 | echo '' |
| 174 | echo "$BRT_PATH_MARKER" |
| 175 | echo "$path_line" |
| 176 | } >> "$rc" |
| 177 | SHELL_RC_CONFIGURED="$rc" |
| 178 | ok "Added ${BRT_BIN_DIR} to PATH in ${rc}" |
| 179 | } |
| 180 | |
| 181 | ensure_path_priority() { |
| 182 | mkdir -p "$BRT_BIN_DIR" |
| 183 | ln -sf "${BRT_INSTALL_DIR}/brt" "${BRT_BIN_DIR}/brt" |
| 184 | |
| 185 | local dir p target |
| 186 | IFS=':' read -ra _path_dirs <<< "$PATH" |
| 187 | for dir in "${_path_dirs[@]}"; do |
| 188 | [[ -n "$dir" && -d "$dir" ]] || continue |
| 189 | p="${dir}/brt" |
| 190 | [[ -e "$p" ]] || continue |
| 191 | [[ "$p" == "${BRT_BIN_DIR}/brt" ]] && continue |
| 192 | if [[ -L "$p" ]]; then |
| 193 | target=$(readlink "$p" 2>/dev/null || true) |
| 194 | [[ "$target" == "${BRT_INSTALL_DIR}/brt" ]] && continue |
| 195 | fi |
| 196 | if [[ -w "$dir" ]]; then |
| 197 | rm -f "$p" |
| 198 | info "Removed conflicting brt at ${p}" |
| 199 | else |
| 200 | warn "Could not remove conflicting brt at ${p}" |
| 201 | fi |
| 202 | done |
| 203 | |
| 204 | case ":${PATH}:" in |
| 205 | *":${BRT_BIN_DIR}:"*) ;; |
| 206 | *) ensure_shell_path ;; |
| 207 | esac |
| 208 | |
| 209 | case ":${PATH}:" in |
| 210 | *":${BRT_BIN_DIR}:"*) ;; |
| 211 | *) export PATH="${BRT_BIN_DIR}:${PATH}" ;; |
| 212 | esac |
| 213 | } |
| 214 | |
| 215 | ensure_startup_update_check() { |
| 216 | if [[ -x "${BRT_INSTALL_DIR}/brt" ]]; then |
| 217 | BRT_DIR="${BRT_INSTALL_DIR}" "${BRT_INSTALL_DIR}/brt" -update-check --install-hook 2>/dev/null || true |
| 218 | fi |
| 219 | } |
| 220 | |
| 221 | install_media_dependencies() { |
| 222 | # shellcheck source=/dev/null |
| 223 | source "${BRT_INSTALL_DIR}/lib/deps.sh" |
| 224 | brt_install_media_dependencies |
| 225 | } |
| 226 | |
| 227 | # Piped from curl — always install from site (never local copy) |
| 228 | SCRIPT_PATH="${BASH_SOURCE[0]:-}" |
| 229 | PIPED=false |
| 230 | [[ -z "$SCRIPT_PATH" || "$SCRIPT_PATH" == "-" || "$SCRIPT_PATH" == /dev/fd/* || "$SCRIPT_PATH" == /dev/stdin ]] && PIPED=true |
| 231 | |
| 232 | LOCAL_ROOT="" |
| 233 | if [[ "$PIPED" == false && -n "$SCRIPT_PATH" && -f "$SCRIPT_PATH" ]]; then |
| 234 | SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" |
| 235 | if [[ -f "${SCRIPT_DIR}/brt" && -d "${SCRIPT_DIR}/lib" && -f "${SCRIPT_DIR}/install.sh" ]]; then |
| 236 | LOCAL_ROOT="$SCRIPT_DIR" |
| 237 | elif [[ -f "${SCRIPT_DIR}/../brt" && -d "${SCRIPT_DIR}/../lib" ]]; then |
| 238 | LOCAL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" |
| 239 | fi |
| 240 | fi |
| 241 | |
| 242 | REMOTE_VERSION="" |
| 243 | if [[ -z "$LOCAL_ROOT" ]]; then |
| 244 | REMOTE_VERSION=$(fetch_remote_version) |
| 245 | [[ -n "$REMOTE_VERSION" ]] || error "Could not fetch latest version from ${BRT_BASE_URL}/version" |
| 246 | info "Latest version: ${REMOTE_VERSION}" |
| 247 | install_from_site "$REMOTE_VERSION" |
| 248 | else |
| 249 | install_from_local "$LOCAL_ROOT" |
| 250 | REMOTE_VERSION=$(read_installed_version) |
| 251 | fi |
| 252 | |
| 253 | ensure_path_priority |
| 254 | |
| 255 | ensure_startup_update_check |
| 256 | |
| 257 | install_media_dependencies || true |
| 258 | |
| 259 | ok "brt ${REMOTE_VERSION} installed to ${BRT_INSTALL_DIR}" |
| 260 | info "Binary: ${BRT_BIN_DIR}/brt" |
| 261 | echo |
| 262 | info "Verifying install..." |
| 263 | "${BRT_BIN_DIR}/brt" -version |
| 264 | echo |
| 265 | if [[ -n "${SHELL_RC_CONFIGURED:-}" ]]; then |
| 266 | ok "PATH configured — open a new terminal, or run: source \"${SHELL_RC_CONFIGURED}\"" |
| 267 | else |
| 268 | ok "PATH ready (${BRT_BIN_DIR})" |
| 269 | fi |
| 270 | info "Quick test: brt -help" |
| 271 | |