#!/bin/sh # Install a verified current build, or compile its matching source snapshot. set -eu fail() { printf 'i1.is: %s\n' "$*" >&2; exit 1; } show_help() { cat <<'HELP' Smart Tree / current full-feature builds Usage: curl -fsSL https://i1.is/tools/smart-tree | sh -s -- [options] --precompiled Download the current full-feature binaries (fastest) --compile Compile the matching source with Rust's full feature set --version BUILD Pin a version (8.1.0) or an immutable build ID --prefix DIRECTORY Install here (default: ~/.local/bin) --help Show this help without downloading anything Both choices include the TUI, daemon, MEM8, and local LLM support. Compilation uses --features full. Google integration and voice are unfinished features and are excluded. Local LLM models are downloaded separately on use. A terminal offers both choices; automation defaults to precompiled binaries. Environment: I1_MODE, I1_VERSION, I1_INSTALL_DIR, I1_BUILD_CACHE. HELP } I1_MODE=${I1_MODE:-} I1_VERSION=${I1_VERSION:-} I1_INSTALL_DIR=${I1_INSTALL_DIR:-${INSTALL_DIR:-$HOME/.local/bin}} while [ "$#" -gt 0 ]; do case "$1" in --precompiled) I1_MODE=precompiled ;; --compile|--source) I1_MODE=compile ;; --version) [ "$#" -ge 2 ] || fail '--version needs a value.'; I1_VERSION=$2; shift ;; --prefix) [ "$#" -ge 2 ] || fail '--prefix needs a directory.'; I1_INSTALL_DIR=$2; shift ;; --help|-h) show_help; exit 0 ;; *) fail "Unknown option: $1 (see --help)." ;; esac shift done case "$I1_MODE" in ''|precompiled|compile) ;; *) fail 'I1_MODE must be precompiled or compile.' ;; esac [ -n "$I1_INSTALL_DIR" ] || fail 'The install directory cannot be empty.' for I1_COMMAND in curl tar awk; do command -v "$I1_COMMAND" >/dev/null 2>&1 || fail "Please install $I1_COMMAND first." done case "$(uname -s)" in Linux) I1_OS=unknown-linux-gnu ;; Darwin) I1_OS=apple-darwin ;; *) fail 'This installer supports Linux, macOS, and WSL.' ;; esac case "$(uname -m)" in x86_64|amd64) I1_ARCH=x86_64 ;; arm64|aarch64) I1_ARCH=aarch64 ;; *) fail 'This installer supports x86-64 and ARM64 systems.' ;; esac if command -v sha256sum >/dev/null 2>&1; then I1_SHA=sha256sum elif command -v shasum >/dev/null 2>&1; then I1_SHA=shasum else fail 'A SHA-256 utility (sha256sum or shasum) is required.' fi fetch() { curl --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 600 --retry 2 -fsSL "$1" -o "$2"; } check_name() { case "$1" in ''|.*|-*|*[!A-Za-z0-9._-]*) fail 'Invalid version or build ID.' ;; esac [ "${#1}" -le 100 ] || fail 'Version or build ID is too long.' } I1_TMP=$(mktemp -d) I1_STAGE= cleanup() { rm -rf "$I1_TMP" if [ -n "$I1_STAGE" ]; then rm -rf "$I1_STAGE"; fi } trap cleanup EXIT trap 'exit 1' HUP INT TERM I1_ROOT=https://i1.is/releases/smart-tree if [ -z "$I1_VERSION" ]; then fetch "$I1_ROOT/latest.txt" "$I1_TMP/build" || fail 'Could not determine the current Smart Tree build.' I1_BUILD=$(cat "$I1_TMP/build") else I1_VERSION=${I1_VERSION#v} check_name "$I1_VERSION" case "$I1_VERSION" in *-*) I1_BUILD=$I1_VERSION ;; *) fetch "$I1_ROOT/versions/$I1_VERSION.txt" "$I1_TMP/build" || fail 'That version is not available on i1.is.' I1_BUILD=$(cat "$I1_TMP/build") ;; esac fi check_name "$I1_BUILD" I1_BASE="$I1_ROOT/$I1_BUILD" fetch "$I1_BASE/version.txt" "$I1_TMP/version" || fail 'This build is not available.' I1_VERSION=$(cat "$I1_TMP/version") check_name "$I1_VERSION" printf '\nSmart Tree %s / build %s\n' "$I1_VERSION" "$I1_BUILD" if [ -z "$I1_MODE" ]; then if [ -t 1 ] && ( : /dev/null; then printf '\n 1 Precompiled — full features, ready in seconds\n 2 Compile — matching source, full features\n 0 Cancel\n\nChoose [1]: ' IFS= read -r I1_CHOICE "$I1_BIN/$I1_NAME" || fail "The archive is missing $I1_NAME." chmod 755 "$I1_BIN/$I1_NAME" done else command -v cc >/dev/null 2>&1 || fail 'Install a C/C++ compiler first: macOS: xcode-select --install; Debian/Ubuntu: sudo apt install build-essential pkg-config libdbus-1-dev.' if [ "$I1_OS" = unknown-linux-gnu ]; then command -v pkg-config >/dev/null 2>&1 && pkg-config --exists dbus-1 || fail 'Install build dependencies first: Debian/Ubuntu: sudo apt install build-essential pkg-config libdbus-1-dev; Arch: sudo pacman -S base-devel dbus.' fi printf 'Downloading the matching source (Cargo.lock included)…\n' fetch "$I1_BASE/source.tar.gz" "$I1_TMP/source.tar.gz" || fail 'Could not download this source snapshot.' verify "$I1_BASE/source.tar.gz" "$I1_TMP/source.tar.gz" # Reject escaping paths, symlinks, and special files before extracting source. tar -tzf "$I1_TMP/source.tar.gz" > "$I1_TMP/source-files" awk '$0 !~ /^smart-tree\// || $0 ~ /(^|\/)\.\.(\/|$)/ {bad=1} END {exit bad}' "$I1_TMP/source-files" || fail 'Unsafe source archive paths.' tar -tvzf "$I1_TMP/source.tar.gz" > "$I1_TMP/source-types" awk 'substr($0,1,1) != "-" && substr($0,1,1) != "d" {bad=1} END {exit bad}' "$I1_TMP/source-types" || fail 'Unsafe source archive entries.' mkdir "$I1_TMP/source" tar -xzf "$I1_TMP/source.tar.gz" -C "$I1_TMP/source" I1_SOURCE="$I1_TMP/source/smart-tree" [ "$(cat "$I1_SOURCE/BUILD_ID")" = "$I1_BUILD" ] || fail 'Source build ID did not match.' # Snapshot timestamps are deterministic; refresh them for Cargo's shared cache. find "$I1_SOURCE" -type f -exec touch {} + if ! command -v cargo >/dev/null 2>&1 && [ -x "$HOME/.cargo/bin/cargo" ]; then PATH="$HOME/.cargo/bin:$PATH"; export PATH fi if ! command -v cargo >/dev/null 2>&1; then printf 'Installing the minimal Rust toolchain for this user…\n' fetch https://sh.rustup.rs "$I1_TMP/rustup.sh" sh "$I1_TMP/rustup.sh" -y --profile minimal --no-modify-path PATH="$HOME/.cargo/bin:$PATH"; export PATH fi I1_BUILD_CACHE=${I1_BUILD_CACHE:-$HOME/.cache/i1/smart-tree/target} printf 'Compiling with --features full. The first build can take several minutes.\n' ST_BUILD_ID="$I1_BUILD" cargo build --manifest-path "$I1_SOURCE/Cargo.toml" \ --locked --release --features full --bin st --bin std --bin m8 --bin n8x \ --target-dir "$I1_BUILD_CACHE" I1_BIN="$I1_BUILD_CACHE/release" fi "$I1_BIN/st" --help >/dev/null || fail 'This build could not run on this system.' mkdir -p "$I1_INSTALL_DIR" I1_STAGE=$(mktemp -d "$I1_INSTALL_DIR/.smart-tree-install.XXXXXX") for I1_NAME in st std m8 n8x; do [ ! -d "$I1_INSTALL_DIR/$I1_NAME" ] || fail "The destination $I1_NAME is a directory." cp "$I1_BIN/$I1_NAME" "$I1_STAGE/$I1_NAME" chmod 755 "$I1_STAGE/$I1_NAME" done for I1_NAME in st std m8 n8x; do mv -f "$I1_STAGE/$I1_NAME" "$I1_INSTALL_DIR/$I1_NAME"; done printf '\nInstalled Smart Tree %s (%s): st, std, m8, n8x\nLocation: %s\n' "$I1_VERSION" "$I1_MODE" "$I1_INSTALL_DIR" case ":$PATH:" in *":$I1_INSTALL_DIR:"*) ;; *) printf 'Add %s to your PATH, or run the executables by their full paths.\n' "$I1_INSTALL_DIR" ;; esac printf 'Try: st --version\nArchive and recall: https://8s.is\n'