| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | BRT_BASE_URL="${BRT_BASE_URL:-https://brt.sushii.dev}" |
| 4 | BRT_INSTALL_DIR="${BRT_DIR:-${BRT_INSTALL_DIR:-${HOME}/.local/share/brt}}" |
| 5 | BRT_BIN_DIR="${BRT_BIN_DIR:-${HOME}/.local/bin}" |
| 6 | BRT_UPDATE_CHECK_MARKER='# brt auto-update check' |
| 7 | BRT_UPDATE_CHECK_INTERVAL="${BRT_UPDATE_CHECK_INTERVAL:-86400}" |
| 8 | |
| 9 | brt_shell_rc_file() { |
| 10 | case "${SHELL##*/}" in |
| 11 | zsh) printf '%s' "${HOME}/.zshrc" ;; |
| 12 | bash) |
| 13 | if [[ "$(uname -s)" == "Darwin" ]]; then |
| 14 | printf '%s' "${HOME}/.bash_profile" |
| 15 | else |
| 16 | printf '%s' "${HOME}/.bashrc" |
| 17 | fi |
| 18 | ;; |
| 19 | *) printf '%s' "${HOME}/.profile" ;; |
| 20 | esac |
| 21 | } |
| 22 | |
| 23 | brt_ensure_startup_check_hook() { |
| 24 | brt_remove_startup_check_hook |
| 25 | } |
| 26 | |
| 27 | brt_remove_startup_check_hook() { |
| 28 | local rc tmp |
| 29 | rc=$(brt_shell_rc_file) |
| 30 | [[ -f "$rc" ]] || return 0 |
| 31 | grep -qE 'brt -update-check|# brt auto-update check|e\[3J|\033\[3J|\[3J.*\[2J' "$rc" 2>/dev/null || return 0 |
| 32 | |
| 33 | tmp=$(mktemp) |
| 34 | awk -v marker="$BRT_UPDATE_CHECK_MARKER" ' |
| 35 | $0 == marker { skip=1; next } |
| 36 | /brt -update-check/ { skip=1; next } |
| 37 | /e\[3J|print -n.*3J|printf.*\\033\[3J/ { skip=1; next } |
| 38 | skip { |
| 39 | if (/disown/) { skip=0; next } |
| 40 | if (/&!/) { skip=0; next } |
| 41 | if (/nonotify|nomonitor/) next |
| 42 | if (/set \+m/) next |
| 43 | if (/setopt localoptions/) next |
| 44 | if (/\/dev\/tty/) next |
| 45 | if (/sleep 0\./) next |
| 46 | if (/print -n|printf/) next |
| 47 | if (/^[[:space:]]*[\{\(][[:space:]]*$/) next |
| 48 | if (/^[[:space:]]*\}[[:space:]]*$/) { skip=0; next } |
| 49 | if (/^[[:space:]]*$/) { skip=0; next } |
| 50 | skip=0 |
| 51 | } |
| 52 | { print } |
| 53 | ' "$rc" > "$tmp" && mv "$tmp" "$rc" |
| 54 | } |
| 55 | |
| 56 | brt_update_resolve_install_dir() { |
| 57 | local brt_bin install_dir |
| 58 | brt_bin=$(command -v brt 2>/dev/null || true) |
| 59 | [[ -n "$brt_bin" ]] || return 1 |
| 60 | install_dir=$(brt_realpath "$brt_bin" 2>/dev/null || printf '%s' "$brt_bin") |
| 61 | dirname "$install_dir" |
| 62 | } |
| 63 | |
| 64 | brt_update_check_startup() { |
| 65 | local cache_dir="${HOME}/.brt" |
| 66 | local cache_file="${cache_dir}/last-update-check" |
| 67 | local install_dir local_version remote_version newest now last=0 |
| 68 | |
| 69 | [[ "${BRT_AUTO_UPDATE:-1}" == "0" ]] && return 0 |
| 70 | command -v curl &>/dev/null || return 0 |
| 71 | |
| 72 | install_dir="${BRT_DIR:-}" |
| 73 | if [[ -z "$install_dir" || ! -f "${install_dir}/brt" ]]; then |
| 74 | install_dir=$(brt_update_resolve_install_dir 2>/dev/null || true) |
| 75 | fi |
| 76 | [[ -n "$install_dir" && -f "${install_dir}/brt" ]] || return 0 |
| 77 | |
| 78 | now=$(date +%s) |
| 79 | if [[ -f "$cache_file" ]]; then |
| 80 | last=$(tr -d '[:space:]' < "$cache_file" 2>/dev/null || echo 0) |
| 81 | [[ "$last" =~ ^[0-9]+$ ]] || last=0 |
| 82 | if (( now - last < BRT_UPDATE_CHECK_INTERVAL )); then |
| 83 | return 0 |
| 84 | fi |
| 85 | fi |
| 86 | |
| 87 | remote_version=$( |
| 88 | curl -fsSL --max-time 4 \ |
| 89 | -H 'Cache-Control: no-cache' \ |
| 90 | -H 'Pragma: no-cache' \ |
| 91 | "${BRT_BASE_URL}/version" 2>/dev/null \ |
| 92 | | tr -d '[:space:]' |
| 93 | ) || return 0 |
| 94 | [[ -n "$remote_version" ]] || return 0 |
| 95 | |
| 96 | mkdir -p "$cache_dir" |
| 97 | printf '%s\n' "$now" > "$cache_file" |
| 98 | |
| 99 | local_version=$(brt_read_version "$install_dir") |
| 100 | local_version="${local_version:-unknown}" |
| 101 | [[ "$local_version" == "unknown" ]] && return 0 |
| 102 | |
| 103 | if [[ "$local_version" == "$remote_version" ]]; then |
| 104 | return 0 |
| 105 | fi |
| 106 | |
| 107 | newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1) |
| 108 | if [[ "$newest" != "$remote_version" ]]; then |
| 109 | return 0 |
| 110 | fi |
| 111 | |
| 112 | printf '%s → %s\n' "$local_version" "$remote_version" > "${cache_dir}/update-available" |
| 113 | return 0 |
| 114 | } |
| 115 | |
| 116 | brt_update_check_manual() { |
| 117 | brt_require curl |
| 118 | |
| 119 | local local_version remote_version newest |
| 120 | local_version=$(brt_read_version "$BRT_DIR") |
| 121 | local_version="${local_version:-${BRT_VERSION}}" |
| 122 | |
| 123 | brt_info "Checking for updates at ${BRT_BASE_URL}..." |
| 124 | |
| 125 | remote_version=$( |
| 126 | curl -fsSL --max-time 10 \ |
| 127 | -H 'Cache-Control: no-cache' \ |
| 128 | -H 'Pragma: no-cache' \ |
| 129 | "${BRT_BASE_URL}/version" 2>/dev/null \ |
| 130 | | tr -d '[:space:]' |
| 131 | ) || { |
| 132 | brt_error "Could not fetch version from ${BRT_BASE_URL}/version" |
| 133 | exit 1 |
| 134 | } |
| 135 | |
| 136 | if [[ -z "$remote_version" ]]; then |
| 137 | brt_error "Remote version file is empty" |
| 138 | exit 1 |
| 139 | fi |
| 140 | |
| 141 | if [[ "$local_version" == "$remote_version" ]]; then |
| 142 | brt_ok "brt ${local_version} is up to date" |
| 143 | return 0 |
| 144 | fi |
| 145 | |
| 146 | newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1) |
| 147 | if [[ "$newest" != "$remote_version" ]]; then |
| 148 | brt_ok "brt ${local_version} is up to date (remote: ${remote_version})" |
| 149 | return 0 |
| 150 | fi |
| 151 | |
| 152 | brt_warn "Update available: ${local_version} → ${remote_version}" |
| 153 | brt_info "Run: brt -update" |
| 154 | } |
| 155 | |
| 156 | brt_update_check() { |
| 157 | case "${1:-}" in |
| 158 | --startup) |
| 159 | brt_update_check_startup |
| 160 | ;; |
| 161 | --install-hook) |
| 162 | brt_remove_startup_check_hook |
| 163 | ;; |
| 164 | *) |
| 165 | brt_update_check_manual "$@" |
| 166 | ;; |
| 167 | esac |
| 168 | } |
| 169 | |
| 170 | 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" |
| 171 | |
| 172 | UPDATE_FILES=() |
| 173 | UPDATE_DOWNLOAD=() |
| 174 | UPDATE_SKIPPED=0 |
| 175 | |
| 176 | brt_update_download_files() { |
| 177 | UPDATE_FILES=(brt) |
| 178 | local f |
| 179 | for f in ${BRT_LIB_MODULES}; do |
| 180 | UPDATE_FILES+=("lib/${f}.sh") |
| 181 | done |
| 182 | UPDATE_FILES+=(lib/access.py lib/media.py lib/browse.py version) |
| 183 | } |
| 184 | |
| 185 | brt_update_sha256_file() { |
| 186 | local file="$1" |
| 187 | [[ -f "$file" ]] || return 0 |
| 188 | if command -v sha256sum &>/dev/null; then |
| 189 | sha256sum "$file" | awk '{print $1}' |
| 190 | else |
| 191 | shasum -a 256 "$file" | awk '{print $1}' |
| 192 | fi |
| 193 | } |
| 194 | |
| 195 | brt_update_manifest_hash() { |
| 196 | local manifest="$1" |
| 197 | local path="$2" |
| 198 | awk -v p="$path" '$0 !~ /^#/ && $2 == p { print $1; exit }' "$manifest" |
| 199 | } |
| 200 | |
| 201 | brt_update_fetch_checksums() { |
| 202 | local dest="$1" |
| 203 | curl -fsSL \ |
| 204 | -H 'Cache-Control: no-cache' \ |
| 205 | -H 'Pragma: no-cache' \ |
| 206 | "${BRT_BASE_URL}/checksums" \ |
| 207 | -o "$dest" 2>/dev/null |
| 208 | } |
| 209 | |
| 210 | brt_update_remove_stale_files() { |
| 211 | local f rel keep entry |
| 212 | for f in "${BRT_INSTALL_DIR}/lib/"*.sh "${BRT_INSTALL_DIR}/lib/"*.py; do |
| 213 | [[ -e "$f" ]] || continue |
| 214 | rel="lib/$(basename "$f")" |
| 215 | keep=0 |
| 216 | for entry in "${UPDATE_FILES[@]}"; do |
| 217 | [[ "$entry" == "$rel" ]] && keep=1 && break |
| 218 | done |
| 219 | [[ "$keep" -eq 1 ]] || rm -f "$f" |
| 220 | done |
| 221 | } |
| 222 | |
| 223 | brt_update_plan_downloads() { |
| 224 | local manifest="$1" |
| 225 | local path remote_hash local_hash |
| 226 | UPDATE_DOWNLOAD=() |
| 227 | UPDATE_SKIPPED=0 |
| 228 | |
| 229 | for path in "${UPDATE_FILES[@]}"; do |
| 230 | remote_hash=$(brt_update_manifest_hash "$manifest" "$path") |
| 231 | if [[ -n "$remote_hash" && -f "${BRT_INSTALL_DIR}/${path}" ]]; then |
| 232 | local_hash=$(brt_update_sha256_file "${BRT_INSTALL_DIR}/${path}") |
| 233 | if [[ "$remote_hash" == "$local_hash" ]]; then |
| 234 | ((UPDATE_SKIPPED++)) || true |
| 235 | continue |
| 236 | fi |
| 237 | fi |
| 238 | UPDATE_DOWNLOAD+=("$path") |
| 239 | done |
| 240 | } |
| 241 | |
| 242 | brt_update_fetch() { |
| 243 | local path="$1" |
| 244 | local dest="${BRT_INSTALL_DIR}/${path}" |
| 245 | mkdir -p "$(dirname "$dest")" |
| 246 | curl -fsSL \ |
| 247 | -H 'Cache-Control: no-cache' \ |
| 248 | -H 'Pragma: no-cache' \ |
| 249 | "${BRT_BASE_URL}/${path}" \ |
| 250 | -o "$dest" || { |
| 251 | brt_error "Failed to download ${BRT_BASE_URL}/${path}" |
| 252 | exit 1 |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | brt_update_remote_brt_version() { |
| 257 | curl -fsSL "${BRT_BASE_URL}/brt" 2>/dev/null \ |
| 258 | | grep '^_BRT_EMBEDDED_VERSION=' \ |
| 259 | | head -1 \ |
| 260 | | cut -d'"' -f2 |
| 261 | } |
| 262 | |
| 263 | brt_update_install() { |
| 264 | local path total i=0 |
| 265 | local manifest use_manifest=0 |
| 266 | |
| 267 | brt_update_download_files |
| 268 | mkdir -p "${BRT_INSTALL_DIR}/lib" "${BRT_BIN_DIR}" |
| 269 | |
| 270 | manifest=$(mktemp) |
| 271 | if brt_update_fetch_checksums "$manifest"; then |
| 272 | use_manifest=1 |
| 273 | brt_update_plan_downloads "$manifest" |
| 274 | else |
| 275 | UPDATE_DOWNLOAD=("${UPDATE_FILES[@]}") |
| 276 | UPDATE_SKIPPED=0 |
| 277 | brt_warn "Remote checksums unavailable — downloading all files" |
| 278 | fi |
| 279 | rm -f "$manifest" |
| 280 | |
| 281 | total=${#UPDATE_DOWNLOAD[@]} |
| 282 | if [[ "$total" -eq 0 ]]; then |
| 283 | brt_ok "All ${#UPDATE_FILES[@]} files already up to date" |
| 284 | else |
| 285 | if [[ "$use_manifest" -eq 1 && "$UPDATE_SKIPPED" -gt 0 ]]; then |
| 286 | brt_info "Downloading ${total} changed file(s), skipped ${UPDATE_SKIPPED} unchanged" |
| 287 | else |
| 288 | brt_info "Downloading brt from ${BRT_BASE_URL}..." |
| 289 | fi |
| 290 | |
| 291 | for path in "${UPDATE_DOWNLOAD[@]}"; do |
| 292 | ((i++)) || true |
| 293 | brt_progress_bar "$i" "$total" "$path" |
| 294 | brt_update_fetch "$path" |
| 295 | done |
| 296 | fi |
| 297 | |
| 298 | brt_update_remove_stale_files |
| 299 | |
| 300 | 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" 2>/dev/null || true |
| 301 | ln -sf "${BRT_INSTALL_DIR}/brt" "${BRT_BIN_DIR}/brt" |
| 302 | } |
| 303 | |
| 304 | brt_update_confirm() { |
| 305 | printf "Install update? [Y/n] " |
| 306 | read -r ans |
| 307 | [[ -z "$ans" || "$ans" =~ ^[Yy]$ ]] |
| 308 | } |
| 309 | |
| 310 | brt_update() { |
| 311 | brt_require curl |
| 312 | |
| 313 | local local_version remote_version remote_brt_version installed_version |
| 314 | local_version=$(brt_read_version "$BRT_DIR") |
| 315 | local_version="${local_version:-${BRT_VERSION}}" |
| 316 | |
| 317 | brt_info "Checking for updates at ${BRT_BASE_URL}..." |
| 318 | |
| 319 | remote_version=$(curl -fsSL "${BRT_BASE_URL}/version" 2>/dev/null | tr -d '[:space:]') || { |
| 320 | brt_error "Could not fetch version from ${BRT_BASE_URL}/version" |
| 321 | exit 1 |
| 322 | } |
| 323 | |
| 324 | if [[ -z "$remote_version" ]]; then |
| 325 | brt_error "Remote version file is empty" |
| 326 | exit 1 |
| 327 | fi |
| 328 | |
| 329 | remote_brt_version=$(brt_update_remote_brt_version) |
| 330 | if [[ -z "$remote_brt_version" ]]; then |
| 331 | brt_error "Could not read version from ${BRT_BASE_URL}/brt" |
| 332 | exit 1 |
| 333 | fi |
| 334 | |
| 335 | if [[ "$remote_version" != "$remote_brt_version" ]]; then |
| 336 | brt_error "Site files are out of sync (version=${remote_version}, brt=${remote_brt_version})" |
| 337 | brt_info "Try again after the site redeploys, or reinstall:" |
| 338 | brt_info "$(brt_install_hint_primary)" |
| 339 | exit 1 |
| 340 | fi |
| 341 | |
| 342 | if [[ "$local_version" == "$remote_version" ]]; then |
| 343 | brt_ok "brt ${local_version} is up to date" |
| 344 | exec "${BRT_DIR}/brt" -version |
| 345 | fi |
| 346 | |
| 347 | local newest |
| 348 | newest=$(printf '%s\n' "$local_version" "$remote_version" | sort -V | tail -1) |
| 349 | |
| 350 | if [[ "$newest" != "$remote_version" ]]; then |
| 351 | brt_ok "brt ${local_version} is up to date (remote: ${remote_version})" |
| 352 | exec "${BRT_DIR}/brt" -version |
| 353 | fi |
| 354 | |
| 355 | brt_info "Update available: ${local_version} → ${remote_version}" |
| 356 | if ! brt_update_confirm; then |
| 357 | brt_info "Update cancelled" |
| 358 | return 0 |
| 359 | fi |
| 360 | |
| 361 | brt_update_install |
| 362 | |
| 363 | installed_version=$(brt_read_version "$BRT_INSTALL_DIR") |
| 364 | if [[ "$installed_version" != "$remote_version" ]]; then |
| 365 | brt_error "Update failed: expected ${remote_version}, got ${installed_version:-unknown}" |
| 366 | exit 1 |
| 367 | fi |
| 368 | |
| 369 | hash -r 2>/dev/null || rehash 2>/dev/null || true |
| 370 | |
| 371 | # shellcheck source=/dev/null |
| 372 | source "${BRT_INSTALL_DIR}/lib/deps.sh" |
| 373 | brt_install_media_dependencies || true |
| 374 | |
| 375 | brt_ok "Updated to brt ${installed_version}" |
| 376 | brt_info "Installed at: ${BRT_INSTALL_DIR}" |
| 377 | brt_ensure_startup_check_hook || true |
| 378 | exec "${BRT_INSTALL_DIR}/brt" -version |
| 379 | } |
| 380 | |