Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
#
# Brokoli one-liner installer.
#
# Usage:
#   curl -fsSL https://brokoli.orkestri.site/install.sh | sh
#
# Environment variables:
#   BROKOLI_VERSION      Install a specific release tag (default: latest).
#   BROKOLI_INSTALL_DIR  Override the install directory (default: /usr/local/bin
#                        if writable, else $HOME/.local/bin).
#   BROKOLI_NO_SETUP=1   Skip the interactive admin-user / start-server prompt.
#   BROKOLI_YES=1        Answer yes to every prompt non-interactively.
#
# The downloaded archive is checked against the release's checksums.txt
# before anything is extracted. Any failure to verify stops the install.
#
# This script is POSIX-sh (no bashisms) and works with either curl or wget.
#
set -eu

# ───────────────── output helpers ─────────────────
RED='' GREEN='' YELLOW='' BOLD='' DIM='' RESET=''
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
    RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3)
    BOLD=$(tput bold);   DIM=$(tput dim);       RESET=$(tput sgr0)
fi
info() { printf '%s==>%s %s\n' "$GREEN" "$RESET" "$1"; }
warn() { printf '%s!! %s%s\n'  "$YELLOW" "$1" "$RESET" >&2; }
die()  { printf '%serror:%s %s\n' "$RED" "$RESET" "$1" >&2; exit 1; }
step() { printf '    %s%s%s\n' "$DIM" "$1" "$RESET"; }

# Node is an optional, resolved runtime for TypeScript code nodes. Report its
# status without installing or requiring it: the single-binary install remains
# useful for Python and declarative pipelines on hosts that have no Node.
report_node_runtime() {
    if ! command -v node >/dev/null 2>&1; then
        info "Node.js not found (optional; install Node.js >= 20 only for TypeScript code nodes)"
        return
    fi

    NODE_VERSION=$(node --version 2>/dev/null || true)
    NODE_MAJOR=$(printf '%s\n' "$NODE_VERSION" | sed -n 's/^v\([0-9][0-9]*\).*/\1/p')
    if [ -n "$NODE_MAJOR" ] && [ "$NODE_MAJOR" -ge 20 ]; then
        info "Node.js $NODE_VERSION found (available for TypeScript code nodes)"
    elif [ -n "$NODE_VERSION" ]; then
        warn "Node.js $NODE_VERSION found; TypeScript code nodes require Node.js >= 20 (optional)"
    else
        warn "Node.js was found but its version could not be read; TypeScript code nodes require Node.js >= 20"
    fi
}

# sha256_of prints a file's SHA-256 in lower case, using whichever tool the
# platform has: sha256sum on Linux, shasum on macOS, openssl as a fallback.
# It returns non-zero when none of them exists.
sha256_of() {
    if command -v sha256sum >/dev/null 2>&1; then
        _sum=$(sha256sum "$1" | awk '{print $1}')
    elif command -v shasum >/dev/null 2>&1; then
        _sum=$(shasum -a 256 "$1" | awk '{print $1}')
    elif command -v openssl >/dev/null 2>&1; then
        _sum=$(openssl dgst -sha256 "$1" | awk '{print $NF}')
    else
        return 1
    fi
    printf '%s\n' "$_sum" | tr 'A-F' 'a-f'
}

# ───────────────── preflight ─────────────────
REPO="Tnsor-Labs/brokoli"
VERSION="${BROKOLI_VERSION:-latest}"

# Require curl OR wget.
if command -v curl >/dev/null 2>&1; then
    HTTP_GET='curl -fsSL'
    HTTP_HEAD='curl -fsSLI'
elif command -v wget >/dev/null 2>&1; then
    HTTP_GET='wget -qO-'
    HTTP_HEAD='wget -q --spider -S'
else
    die "neither curl nor wget found — install one of them and retry."
fi

command -v tar >/dev/null 2>&1 || die "tar not found — install it and retry."

# ───────────────── platform detection ─────────────────
OS=$(uname -s)
ARCH=$(uname -m)

case "$OS" in
    Linux)   OS_ID="Linux"   ;;
    Darwin)  OS_ID="Darwin"  ;;
    *)       die "unsupported OS: $OS (Brokoli supports Linux and macOS; Windows users should use WSL)" ;;
esac

case "$ARCH" in
    x86_64|amd64)  ARCH_ID="x86_64" ;;
    arm64|aarch64) ARCH_ID="arm64"  ;;
    *)             die "unsupported architecture: $ARCH" ;;
esac

info "Detected platform: ${OS_ID}_${ARCH_ID}"

# ───────────────── resolve version ─────────────────
if [ "$VERSION" = "latest" ]; then
    step "Looking up latest release…"
    # /releases/latest redirects to /releases/tag/vX.Y.Z — grab the final URL.
    if command -v curl >/dev/null 2>&1; then
        RESOLVED=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
            "https://github.com/${REPO}/releases/latest")
    else
        # No --max-redirect: busybox wget rejects it, and both GNU and
        # busybox wget follow redirects anyway. The Location header is
        # indented in busybox's -S output and unindented in GNU's
        # "[following]" line, so match the first field, not the line start.
        RESOLVED=$(wget --spider -S \
            "https://github.com/${REPO}/releases/latest" 2>&1 \
            | awk 'tolower($1) == "location:" {print $2}' | tail -1)
    fi
    VERSION=$(printf '%s\n' "$RESOLVED" | sed -E 's|.*/tag/(v[^/]+).*|\1|')
    [ -n "$VERSION" ] || die "could not resolve latest version (check network / GitHub status)"
fi
info "Installing Brokoli $VERSION"

# ───────────────── pick install dir ─────────────────
if [ -n "${BROKOLI_INSTALL_DIR:-}" ]; then
    INSTALL_DIR="$BROKOLI_INSTALL_DIR"
elif [ -w /usr/local/bin ] 2>/dev/null; then
    INSTALL_DIR="/usr/local/bin"
else
    INSTALL_DIR="$HOME/.local/bin"
fi
# Always ensure the install dir exists — applies to both the
# BROKOLI_INSTALL_DIR override and the $HOME/.local/bin fallback.
mkdir -p "$INSTALL_DIR" 2>/dev/null || die "could not create $INSTALL_DIR"

case ":$PATH:" in
    *":$INSTALL_DIR:"*) PATH_HINT="" ;;
    *) PATH_HINT="  (not on \$PATH — add 'export PATH=\"$INSTALL_DIR:\$PATH\"' to your shell config)" ;;
esac

# ───────────────── download + extract ─────────────────
ASSET="brokoli_${OS_ID}_${ARCH_ID}.tar.gz"
URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}"
TMP=$(mktemp -d 2>/dev/null || mktemp -d -t brokoli-install)
trap 'rm -rf "$TMP"' EXIT INT TERM

step "Downloading $ASSET…"
if command -v curl >/dev/null 2>&1; then
    curl -fSL --progress-bar "$URL" -o "$TMP/$ASSET" \
        || die "download failed: $URL"
else
    # --show-progress exists only in GNU wget; busybox wget, the usual
    # wget on Alpine, rejects it and the download fails. Pass it only when
    # this wget advertises it. `wget --help` exits non-zero on busybox.
    WGET_PROGRESS=''
    case "$(wget --help 2>&1 || true)" in
        *--show-progress*) WGET_PROGRESS='--show-progress' ;;
    esac
    # shellcheck disable=SC2086  # empty on purpose when unsupported
    wget -q $WGET_PROGRESS "$URL" -O "$TMP/$ASSET" \
        || die "download failed: $URL"
fi

# ───────────────── verify ─────────────────
# Every release publishes checksums.txt, one "<sha256>  <file>" line per
# archive. The archive is checked against it before it is extracted or
# anything is installed, and every way of failing to verify stops here:
# no checksums file, no entry for this archive, no hash tool, or a
# mismatch. None of them falls back to installing unverified.
#
# What this proves: the archive is byte-for-byte the one the release build
# produced, so a truncated download, a corrupting proxy or an altered
# mirror is caught. What it does not prove: that the release itself is
# genuine. checksums.txt is published next to the archive, so anyone able
# to replace one could replace both. That needs a signature.
SUMS_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt"
step "Verifying checksum…"
if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$SUMS_URL" -o "$TMP/checksums.txt" \
        || die "could not download $SUMS_URL; refusing to install a binary that cannot be verified"
else
    wget -q "$SUMS_URL" -O "$TMP/checksums.txt" \
        || die "could not download $SUMS_URL; refusing to install a binary that cannot be verified"
fi
EXPECTED=$(awk -v f="$ASSET" '$2 == f {print $1}' "$TMP/checksums.txt" | tr 'A-F' 'a-f')
[ -n "$EXPECTED" ] \
    || die "checksums.txt for $VERSION has no entry for $ASSET; refusing to install a binary that cannot be verified"
ACTUAL=$(sha256_of "$TMP/$ASSET") \
    || die "no SHA-256 tool found (need sha256sum, shasum or openssl); refusing to install a binary that cannot be verified"
[ "$ACTUAL" = "$EXPECTED" ] \
    || die "checksum mismatch for $ASSET (expected $EXPECTED, got $ACTUAL). The download is corrupt or has been altered. Nothing was installed."
step "Checksum OK ($ACTUAL)"

step "Extracting…"
tar -xzf "$TMP/$ASSET" -C "$TMP" || die "extract failed"
[ -f "$TMP/brokoli" ] || die "archive did not contain a 'brokoli' binary"
chmod +x "$TMP/brokoli"

step "Installing to $INSTALL_DIR/brokoli"
mv "$TMP/brokoli" "$INSTALL_DIR/brokoli" \
    || die "could not write to $INSTALL_DIR — set BROKOLI_INSTALL_DIR or run with sudo"

INSTALLED_VERSION=$("$INSTALL_DIR/brokoli" --version 2>/dev/null || echo "$VERSION")
info "Brokoli $INSTALLED_VERSION installed${PATH_HINT}"
report_node_runtime

# ───────────────── interactive first-run setup ─────────────────
#
# curl | sh pipes the script to stdin, so `read` from stdin fails. We read
# from /dev/tty instead, which is the real user terminal even when stdin is
# a pipe. This is the same trick rustup / bun / homebrew use.
#
setup_interactively() {
    if [ "${BROKOLI_NO_SETUP:-}" = "1" ]; then return 1; fi
    [ -e /dev/tty ] || return 1
    return 0
}

if setup_interactively; then
    printf '\n'
    printf '%sWould you like to start Brokoli now and create an admin user? [Y/n] %s' "$BOLD" "$RESET"
    if [ "${BROKOLI_YES:-}" = "1" ]; then
        ANS="y"; printf 'y\n'
    else
        read -r ANS </dev/tty || ANS="n"
    fi
    case "$ANS" in
        ''|y|Y|yes|YES|Yes) ;;
        *)
            printf '\n'
            info "Skipped setup. Start Brokoli yourself with:"
            printf '    %sbrokoli serve%s\n' "$BOLD" "$RESET"
            exit 0
            ;;
    esac

    # Check port availability before starting.
    PORT="${BROKOLI_PORT:-8080}"
    if command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q ":${PORT} "; then
        warn "port ${PORT} is already in use — skipping auto-start"
        warn "start manually on a different port: brokoli serve --port 9090"
        exit 0
    fi

    # Collect admin credentials.
    DEFAULT_USER="admin"
    printf '    admin username [%s]: ' "$DEFAULT_USER"
    read -r ADMIN_USER </dev/tty || ADMIN_USER=""
    [ -n "$ADMIN_USER" ] || ADMIN_USER="$DEFAULT_USER"

    # Password must be 10+ chars (validated by the server).
    while :; do
        printf '    admin password (10+ chars): '
        stty -echo 2>/dev/null || true
        read -r ADMIN_PASS </dev/tty || ADMIN_PASS=""
        stty echo 2>/dev/null || true
        printf '\n'
        [ ${#ADMIN_PASS} -ge 10 ] && break
        warn "password too short, try again"
    done

    # Start the server in the background.
    LOG="$HOME/.brokoli-install.log"
    info "Starting Brokoli on http://localhost:${PORT}…"
    nohup "$INSTALL_DIR/brokoli" serve --port "$PORT" >"$LOG" 2>&1 &
    SERVER_PID=$!

    # Wait up to 20s for the health endpoint.
    i=0
    while [ $i -lt 40 ]; do
        if $HTTP_GET "http://localhost:${PORT}/health" >/dev/null 2>&1; then
            break
        fi
        i=$((i + 1))
        sleep 0.5
    done
    if [ $i -ge 40 ]; then
        warn "server did not come up in 20s — check $LOG"
        exit 1
    fi

    # Create the first user via the unauthenticated setup endpoint.
    step "Creating admin user '$ADMIN_USER'…"
    PAYLOAD=$(printf '{"username":"%s","password":"%s","role":"admin"}' \
        "$ADMIN_USER" "$ADMIN_PASS")
    if command -v curl >/dev/null 2>&1; then
        RESP=$(curl -fsS -X POST "http://localhost:${PORT}/api/auth/users" \
            -H 'Content-Type: application/json' \
            -d "$PAYLOAD" 2>&1) || { warn "admin creation failed: $RESP"; exit 1; }
    else
        RESP=$(wget -qO- --post-data="$PAYLOAD" \
            --header='Content-Type: application/json' \
            "http://localhost:${PORT}/api/auth/users" 2>&1) \
            || { warn "admin creation failed: $RESP"; exit 1; }
    fi

    printf '\n'
    info "Ready to go"
    printf '    %sURL:%s       http://localhost:%s\n' "$BOLD" "$RESET" "$PORT"
    printf '    %susername:%s  %s\n'                  "$BOLD" "$RESET" "$ADMIN_USER"
    printf '    %spassword:%s  (the one you just set)\n' "$BOLD" "$RESET"
    printf '\n'
    printf '    %sserver log:%s  %s\n' "$DIM" "$RESET" "$LOG"
    printf '    %sstop with:%s   kill %s\n'           "$DIM" "$RESET" "$SERVER_PID"
    printf '\n'
    printf '    Next: open the URL and trigger your first pipeline from the\n'
    printf '    sample templates in the Pipelines tab.\n'
else
    printf '\n'
    info "Installed. Next steps:"
    printf '    %sbrokoli serve%s                # start the server on :8080\n' "$BOLD" "$RESET"
    printf '    open http://localhost:8080      # open the UI\n'
    printf '\n'
    printf '%sDocs:%s  https://docs-brokoli.orkestri.site\n' "$DIM" "$RESET"
fi