#!/bin/sh # BlockClaw — One-Line Installer for Sequence0 Node Runners # # Usage: # curl -fsSL https://install.sequence0.network | sh # # Installs TWO binaries to ~/.sequence0/bin/: # 1. blockclaw — Interactive CLI for managing your node # 2. sequence0-agent — The Rust agent binary (managed by BlockClaw) # SECURITY: -e halts on errors, -u treats unset variables as errors # NOTE: Checksums should ideally be served from a separate channel or signed # with a key distributed independently from the binary download server. set -eu # ── Configuration ────────────────────────────────────────────── BASE_URL="https://install.sequence0.network" INSTALL_DIR="${HOME}/.sequence0/bin" VERSION="" # ── Colors ───────────────────────────────────────────────────── if [ -t 1 ]; then ESC=$(printf '\033') BOLD="${ESC}[1m" DIM="${ESC}[2m" WHITE="${ESC}[37m" CYAN="${ESC}[36m" GREEN="${ESC}[32m" RED="${ESC}[31m" YELLOW="${ESC}[33m" BG_WHITE="${ESC}[47m" FG_BLACK="${ESC}[30m" RESET="${ESC}[0m" else BOLD='' DIM='' WHITE='' CYAN='' GREEN='' RED='' YELLOW='' BG_WHITE='' FG_BLACK='' RESET='' ESC='' fi # ── Dimensions ───────────────────────────────────────────────── # Outer frame = 80 chars: ║ + space + inner(76) + space + ║ # Inner box = 76 chars: ┌ + ─×74 + ┐ # Box content = 72 chars: │ + space + text(72) + space + │ (inside inner box) FW=80 IW=74 CW=72 LW=30 # Label width for step value alignment # ── Helpers ──────────────────────────────────────────────────── strip_ansi() { printf '%s' "$1" | sed "s/${ESC}\[[0-9;]*m//g"; } error() { printf " ${RED}[ERR]${RESET} %s\n" "$@" >&2; exit 1; } need_cmd() { command -v "$1" > /dev/null 2>&1 || error "Required command '$1' not found."; } # ── Frame-Wrapped Drawing ───────────────────────────────────── # Every line sits inside ║ ... ║ for a connected frame frame_top() { printf "╔"; printf '═%.0s' $(seq 1 $((FW-2))); printf "╗\n"; } frame_bot() { printf "╚"; printf '═%.0s' $(seq 1 $((FW-2))); printf "╝\n"; } frame_empty() { printf "║%*s║\n" $((FW-2)) ""; } # Inner box borders inside frame fbox_top() { printf "║ ┌"; printf '─%.0s' $(seq 1 $IW); printf "┐ ║\n"; } fbox_bot() { printf "║ └"; printf '─%.0s' $(seq 1 $IW); printf "┘ ║\n"; } fbox_div() { printf "║ ├"; printf '─%.0s' $(seq 1 $IW); printf "┤ ║\n"; } fbox_empty() { printf "║ │%*s│ ║\n" $IW ""; } # Content row inside inner box inside frame: ║ │ content... │ ║ fbox_row() { local c="$1" local stripped stripped=$(strip_ansi "$c") local vis_len=${#stripped} local pad=$((CW - vis_len)) [ "$pad" -lt 0 ] && pad=0 printf "║ │ %s%*s │ ║\n" "$c" "$pad" "" } # Section heading with filled white background fbox_heading() { local title title=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]') local title_text=" ${title} " local pad=$((IW - ${#title_text})) [ "$pad" -lt 0 ] && pad=0 printf "║ │${BG_WHITE}${FG_BLACK}${BOLD}%s%*s${RESET}│ ║\n" "$title_text" "$pad" "" } # Step row with aligned values: [OK] [X/5] Label Value fbox_step() { local status="$1" step="$2" total="$3" label="$4" value="$5" local label_pad=$((LW - ${#label})) [ "$label_pad" -lt 1 ] && label_pad=1 local content="${status} [${step}/${total}] ${WHITE}${label}${RESET}$(printf '%*s' $label_pad '')${DIM}${value}${RESET}" fbox_row "$content" } # Progress bar inside frame: ║ │ [3/5] Label [████░░░] 42% │ ║ fprogress() { local pct="$1" step="$2" total="$3" label="$4" local bar_w=20 local filled=$(( (pct * bar_w) / 100 )) local empty=$(( bar_w - filled )) local bar="" local i=0 while [ "$i" -lt "$filled" ]; do bar="${bar}█"; i=$((i+1)); done i=0 while [ "$i" -lt "$empty" ]; do bar="${bar}░"; i=$((i+1)); done local pct_str if [ "$pct" -lt 10 ]; then pct_str=" ${pct}"; elif [ "$pct" -lt 100 ]; then pct_str=" ${pct}"; else pct_str="${pct}"; fi local content=" [${step}/${total}] ${label} [${WHITE}${bar}${RESET}] ${pct_str}%" local stripped stripped=$(strip_ansi "$content") local vis_len=${#stripped} local pad=$((CW - vis_len)) [ "$pad" -lt 0 ] && pad=0 printf "\r\033[2K║ │ %s%*s │ ║" "$content" "$pad" "" } # ── Parse Arguments ──────────────────────────────────────────── while [ $# -gt 0 ]; do case "$1" in --version|-v) VERSION="$2"; shift 2 ;; --version=*) VERSION="${1#*=}"; shift ;; *) shift ;; esac done [ -z "$VERSION" ] && VERSION="latest" # ── Detect Platform ──────────────────────────────────────────── detect_platform() { local os arch os="$(uname -s)"; arch="$(uname -m)" case "$os" in Darwin) os="darwin" ;; Linux) os="linux" ;; MINGW*|MSYS*|CYGWIN*) error "Windows not supported. Use WSL2." ;; *) error "Unsupported OS: $os" ;; esac case "$arch" in x86_64|amd64) arch="x64" ;; aarch64|arm64) arch="arm64" ;; *) error "Unsupported architecture: $arch" ;; esac if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then if sysctl -n sysctl.proc_translated 2>/dev/null | grep -q 1 2>/dev/null; then arch="arm64" fi fi printf "%s-%s" "$os" "$arch" } # ── Download (silent + background) ──────────────────────────── download_bg() { local url="$1" output="$2" if command -v curl > /dev/null 2>&1; then curl --fail --location --silent --proto '=https' --tlsv1.2 --output "$output" "$url" & elif command -v wget > /dev/null 2>&1; then wget --secure-protocol=TLSv1_2 --quiet -O "$output" "$url" & else error "curl or wget required." fi } download_silent() { local url="$1" output="$2" if command -v curl > /dev/null 2>&1; then curl --fail --location --silent --proto '=https' --tlsv1.2 --output "$output" "$url" elif command -v wget > /dev/null 2>&1; then wget --secure-protocol=TLSv1_2 --quiet -O "$output" "$url" fi } # Download with progress bar download_step() { local step="$1" total="$2" label="$3" url="$4" output="$5" # Get expected size for progress calculation local expected_size=0 if command -v curl > /dev/null 2>&1; then expected_size=$(curl --head --fail --location --silent \ --proto '=https' --tlsv1.2 "$url" 2>/dev/null \ | grep -i 'content-length' | tail -1 | awk '{print $2}' | tr -d '\r\n ') fi [ -z "$expected_size" ] && expected_size=0 fprogress 0 "$step" "$total" "$label" download_bg "$url" "$output" local dl_pid=$! while kill -0 "$dl_pid" 2>/dev/null; do if [ -f "$output" ] && [ "$expected_size" -gt 0 ] 2>/dev/null; then local cur cur=$(wc -c < "$output" 2>/dev/null | tr -d ' ') if [ -n "$cur" ] && [ "$cur" -gt 0 ] 2>/dev/null; then local pct=$(( (cur * 100) / expected_size )) [ "$pct" -gt 99 ] && pct=99 fprogress "$pct" "$step" "$total" "$label" fi fi sleep 0.3 2>/dev/null || sleep 1 done wait "$dl_pid" local rc=$? if [ $rc -eq 0 ] && [ -f "$output" ]; then fprogress 100 "$step" "$total" "$label" sleep 0.2 2>/dev/null || true printf "\r\033[2K" local size_bytes size_display size_bytes=$(wc -c < "$output" | tr -d ' ') if [ "$size_bytes" -gt 1048576 ] 2>/dev/null; then size_display="$(( size_bytes / 1048576 ))MB" elif [ "$size_bytes" -gt 1024 ] 2>/dev/null; then size_display="$(( size_bytes / 1024 ))KB" else size_display="${size_bytes}B" fi fbox_step "${GREEN}[OK]${RESET}" "$step" "$total" "$label" "$size_display" return 0 else printf "\r\033[2K" fbox_step "${RED}[ERR]${RESET}" "$step" "$total" "$label" "failed" return 1 fi } # ── Verify Checksum ──────────────────────────────────────────── verify_checksum() { local file="$1" checksum_file="$2" [ ! -f "$checksum_file" ] && return 0 local expected actual expected=$(cat "$checksum_file" | awk '{print $1}' | tr -d '[:space:]') [ -z "$expected" ] && return 0 if command -v sha256sum > /dev/null 2>&1; then actual=$(sha256sum "$file" | awk '{print $1}') elif command -v shasum > /dev/null 2>&1; then actual=$(shasum -a 256 "$file" | awk '{print $1}') else return 0 fi [ "$expected" != "$actual" ] && error "CHECKSUM MISMATCH for $(basename $file)! Expected: $expected Got: $actual" return 0 } # ── PATH Setup ───────────────────────────────────────────────── setup_path() { case ":${PATH}:" in *":${INSTALL_DIR}:"*) return 0 ;; esac local shell_name shell_config config_line shell_name="$(basename "${SHELL:-sh}")" shell_config="" config_line="export PATH=\"\${HOME}/.sequence0/bin:\$PATH\"" case "$shell_name" in zsh) shell_config="${HOME}/.zshrc" ;; bash) [ -f "${HOME}/.bash_profile" ] && shell_config="${HOME}/.bash_profile" || shell_config="${HOME}/.bashrc" ;; fish) shell_config="${HOME}/.config/fish/config.fish"; config_line="set -gx PATH \$HOME/.sequence0/bin \$PATH" ;; esac if [ -n "$shell_config" ]; then grep -q "sequence0/bin" "$shell_config" 2>/dev/null || printf '\n# Sequence0\n%s\n' "$config_line" >> "$shell_config" fi } # ── Pre-flight ───────────────────────────────────────────────── preflight() { local available_mb=0 if command -v df > /dev/null 2>&1; then available_mb=$(df -m "$HOME" 2>/dev/null | awk 'NR==2 {print $4}' || echo "0") fi if [ "$available_mb" -lt 200 ] 2>/dev/null && [ "$available_mb" -gt 0 ] 2>/dev/null; then error "Not enough disk space. Need 200MB, have ${available_mb}MB." fi } # ── macOS Gatekeeper Fix ────────────────────────────────────── fix_gatekeeper() { [ "$(uname -s)" = "Darwin" ] && xattr -d com.apple.quarantine "$1" 2>/dev/null || true } # ══════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════ main() { printf "\n" need_cmd uname; need_cmd chmod; need_cmd mkdir preflight local platform platform="$(detect_platform)" local tmp_dir tmp_dir="$(mktemp -d)" trap "rm -rf '${tmp_dir}'" EXIT mkdir -p "${INSTALL_DIR}" local datetime datetime=$(date '+%Y-%m-%d %H:%M:%S') # ══ FRAME TOP + HEADER ══ frame_top local title="BLOCKCLAW INSTALLER" local title_len=${#title} local date_len=${#datetime} local gap=$((FW - 12 - title_len - date_len)) [ "$gap" -lt 1 ] && gap=1 printf "║ ${RED}●${RESET} ${YELLOW}●${RESET} ${GREEN}●${RESET} ${BOLD}${WHITE}%s${RESET}%*s${DIM}%s${RESET} ║\n" \ "$title" "$gap" "" "$datetime" frame_empty # ══ INSTALLING SECTION ══ fbox_top fbox_heading "Installing" fbox_div # [1/5] Platform fbox_step "${GREEN}[OK]${RESET}" 1 5 "Detecting platform" "$platform" # [2/5] Pre-flight local disk_info="" command -v df > /dev/null 2>&1 && disk_info="$(df -m "$HOME" 2>/dev/null | awk 'NR==2 {print $4}')MB free" fbox_step "${GREEN}[OK]${RESET}" 2 5 "Pre-flight checks" "$disk_info" # Handle upgrade if [ -f "${INSTALL_DIR}/blockclaw" ]; then local old_ver old_ver=$("${INSTALL_DIR}/blockclaw" --version 2>/dev/null || echo "unknown") fbox_row "${YELLOW}[UPG]${RESET} ${DIM}Upgrading from ${old_ver}...${RESET}" if pgrep -f sequence0-agent > /dev/null 2>&1; then "${INSTALL_DIR}/blockclaw" stop 2>/dev/null || true sleep 2 fi for b in blockclaw sequence0-agent; do [ -f "${INSTALL_DIR}/${b}" ] && cp "${INSTALL_DIR}/${b}" "${INSTALL_DIR}/${b}.bak" done fi # [3/5] Download BlockClaw CLI local blockclaw_url="${BASE_URL}/releases/${VERSION}/blockclaw-${platform}" download_step 3 5 "Downloading BlockClaw CLI" "$blockclaw_url" "${tmp_dir}/blockclaw" || { fbox_bot; frame_empty; frame_bot error "Failed to download BlockClaw CLI. URL: ${blockclaw_url}" } download_silent "${blockclaw_url}.sha256" "${tmp_dir}/blockclaw.sha256" 2>/dev/null || true verify_checksum "${tmp_dir}/blockclaw" "${tmp_dir}/blockclaw.sha256" chmod +x "${tmp_dir}/blockclaw" fix_gatekeeper "${tmp_dir}/blockclaw" mv "${tmp_dir}/blockclaw" "${INSTALL_DIR}/blockclaw" # [4/5] Download Agent local agent_url="${BASE_URL}/releases/${VERSION}/sequence0-agent-${platform}" download_step 4 5 "Downloading Sequence0 agent" "$agent_url" "${tmp_dir}/sequence0-agent" || { fbox_bot; frame_empty; frame_bot error "Failed to download agent. URL: ${agent_url}" } download_silent "${agent_url}.sha256" "${tmp_dir}/sequence0-agent.sha256" 2>/dev/null || true verify_checksum "${tmp_dir}/sequence0-agent" "${tmp_dir}/sequence0-agent.sha256" chmod +x "${tmp_dir}/sequence0-agent" fix_gatekeeper "${tmp_dir}/sequence0-agent" mv "${tmp_dir}/sequence0-agent" "${INSTALL_DIR}/sequence0-agent" # [5/5] PATH setup_path local shell_name shell_cfg_name shell_name="$(basename "${SHELL:-sh}")" case "$shell_name" in zsh) shell_cfg_name="~/.zshrc" ;; bash) shell_cfg_name="~/.bashrc" ;; fish) shell_cfg_name="~/.config/fish/config.fish" ;; *) shell_cfg_name="shell profile" ;; esac fbox_step "${GREEN}[OK]${RESET}" 5 5 "Configuring PATH" "$shell_cfg_name" fbox_empty fbox_bot frame_empty # ══ GET STARTED SECTION ══ fbox_top fbox_heading "Get Started" fbox_div fbox_empty fbox_row "${DIM}\$${RESET} ${BOLD}${WHITE}blockclaw setup${RESET}" fbox_empty fbox_row "${DIM}The setup wizard will walk you through:${RESET}" fbox_row " ${CYAN}●${RESET} ${DIM}Choosing a network (testnet or mainnet)${RESET}" fbox_row " ${CYAN}●${RESET} ${DIM}Starting your node${RESET}" fbox_row " ${CYAN}●${RESET} ${DIM}Creating your wallet (works on all 100+ chains)${RESET}" fbox_empty # PATH warning if needed if ! command -v blockclaw > /dev/null 2>&1; then fbox_div fbox_row "${YELLOW}[!]${RESET} ${WHITE}PATH not set in current session${RESET}" fbox_empty fbox_row "${DIM}Run this first:${RESET} ${CYAN}export PATH=\"\$HOME/.sequence0/bin:\$PATH\"${RESET}" fbox_row "${DIM}(Automatic in new terminal windows)${RESET}" fbox_empty fi fbox_bot frame_empty # ══ INFO SECTION ══ fbox_top fbox_heading "Info" fbox_div fbox_row "${DIM}Docs${RESET} ${WHITE}https://sequence0.network/docs${RESET}" fbox_row "${DIM}Uninstall${RESET} ${WHITE}rm -rf ~/.sequence0${RESET}" fbox_bot frame_empty # ══ FRAME BOTTOM ══ frame_bot printf "\n" } main "$@"