#!/bin/bash # Krasis installer — downloads pre-built wheels from GitHub releases. # No PyPI, no pipx, no sudo. Works out of the box. # # Install / upgrade (latest stable): # curl -sSf https://raw.githubusercontent.com/brontoguana/krasis/main/install.sh | bash # krasis update # # Install latest pre-release: # curl -sSf https://raw.githubusercontent.com/brontoguana/krasis/main/install.sh | bash -s -- prerelease # krasis prerelease # # Uninstall: # curl -sSf https://raw.githubusercontent.com/brontoguana/krasis/main/install.sh | bash -s -- --uninstall set -euo pipefail # Wrap everything in a function so bash reads the entire script from the pipe # before executing anything. Without this, subprocesses can consume stdin # (which IS the pipe) and steal the rest of the script, causing silent exit. do_install() { trap 'echo -e "\n\033[0;31m\033[1mInstall failed\033[0m at line $LINENO. Run with \"bash -x\" for details." >&2' ERR REPO="brontoguana/krasis" VENV_DIR="$HOME/.krasis/venv" BIN_DIR="$HOME/.local/bin" COMMANDS="krasis krasis-chat krasis-setup" OBSOLETE_VENV_PACKAGES=( # Stale from the old SGLang/KTransformers path; Krasis no longer uses it. sglang # Not a current Krasis install dependency. Old copies constrain fsspec and # create unrelated pip resolver warnings during Krasis upgrades. datasets ) BOLD="\033[1m" DIM="\033[2m" RED="\033[0;31m" GREEN="\033[0;32m" YELLOW="\033[1;33m" CYAN="\033[0;36m" NC="\033[0m" info() { echo -e "${CYAN}${BOLD}=>${NC} $1"; } ok() { echo -e "${GREEN}${BOLD}OK${NC} $1"; } warn() { echo -e "${YELLOW}${BOLD}!!${NC} $1"; } err() { echo -e "${RED}${BOLD}ERROR${NC} $1"; exit 1; } cleanup_obsolete_venv_packages() { [[ -x "$VENV_DIR/bin/python" ]] || return 0 [[ ${#OBSOLETE_VENV_PACKAGES[@]} -gt 0 ]] || return 0 local stale=() local package for package in "${OBSOLETE_VENV_PACKAGES[@]}"; do if "$VENV_DIR/bin/python" -m pip show "$package" &>/dev/null; then stale+=("$package") fi done if [[ ${#stale[@]} -gt 0 ]]; then info "Removing obsolete packages from Krasis-managed venv: ${stale[*]}" "$VENV_DIR/bin/python" -m pip uninstall -y "${stale[@]}" >/dev/null \ || err "Failed to remove obsolete packages from $VENV_DIR." fi } # ── Channel (stable / prerelease) ──────────────────────────────────── CHANNEL="stable" if [[ "${1:-}" == "prerelease" || "${1:-}" == "--prerelease" ]]; then CHANNEL="prerelease" shift fi # ── Uninstall ────────────────────────────────────────────────────────── if [[ "${1:-}" == "--uninstall" ]]; then info "Uninstalling Krasis..." for cmd in $COMMANDS; do rm -f "$BIN_DIR/$cmd" done rm -rf "$VENV_DIR" ok "Krasis removed. Model files in ~/.krasis/models/ were kept." exit 0 fi # ── Platform check ──────────────────────────────────────────────────── [[ "$(uname -s)" == "Linux" ]] || err "Krasis only supports Linux. On Windows, use WSL2." # ── Check curl ──────────────────────────────────────────────────────── command -v curl &>/dev/null || err "curl is required but not found.\n sudo apt install curl" # ── Find all available Python 3.10+ ────────────────────────────────── # Order matches our build matrix. Newest first so users get the best # available wheel. python3 is last as a catch-all — its version is # checked, so it won't match 3.9 or older. N_PY=0 PY_BINS=() PY_VERS=() PY_DOTS=() SEEN_VERS="" for candidate in python3.13 python3.12 python3.11 python3.10 python3; do command -v "$candidate" &>/dev/null || continue ver=$("$candidate" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null) || continue major="${ver%%.*}"; minor="${ver##*.}" [[ "$major" -eq 3 && "$minor" -ge 10 ]] || continue pyver="${major}${minor}" # Deduplicate (python3 may resolve to same as python3.12) [[ "$SEEN_VERS" == *" $pyver "* ]] && continue SEEN_VERS="$SEEN_VERS $pyver " PY_BINS[$N_PY]="$candidate" PY_VERS[$N_PY]="$pyver" PY_DOTS[$N_PY]="$ver" N_PY=$((N_PY + 1)) done if [[ $N_PY -eq 0 ]]; then warn "Python 3.10+ not found." if command -v apt-get &>/dev/null; then echo -en " Install Python 3 now? [Y/n] " read -r answer < /dev/tty 2>/dev/null || answer="y" if [[ "${answer,,}" =~ ^(y|yes|)$ ]]; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) info "Installing Python 3..." "${SUDO[@]}" apt-get update -qq /dev/null "${SUDO[@]}" apt-get install -y python3 python3-venv /dev/null || continue ver=$("$candidate" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null) || continue major="${ver%%.*}"; minor="${ver##*.}" [[ "$major" -eq 3 && "$minor" -ge 10 ]] || continue pyver="${major}${minor}" [[ "$SEEN_VERS" == *" $pyver "* ]] && continue SEEN_VERS="$SEEN_VERS $pyver " PY_BINS[$N_PY]="$candidate" PY_VERS[$N_PY]="$pyver" PY_DOTS[$N_PY]="$ver" N_PY=$((N_PY + 1)) done [[ $N_PY -eq 0 ]] && err "Python 3.10+ still not found after install." else err "Python 3.10+ is required.\n Install it: sudo apt install python3" fi elif command -v dnf &>/dev/null; then echo -en " Install Python 3 now? [Y/n] " read -r answer < /dev/tty 2>/dev/null || answer="y" if [[ "${answer,,}" =~ ^(y|yes|)$ ]]; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) info "Installing Python 3..." "${SUDO[@]}" dnf install -y python3 /dev/null || continue ver=$("$candidate" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null) || continue major="${ver%%.*}"; minor="${ver##*.}" [[ "$major" -eq 3 && "$minor" -ge 10 ]] || continue pyver="${major}${minor}" [[ "$SEEN_VERS" == *" $pyver "* ]] && continue SEEN_VERS="$SEEN_VERS $pyver " PY_BINS[$N_PY]="$candidate" PY_VERS[$N_PY]="$pyver" PY_DOTS[$N_PY]="$ver" N_PY=$((N_PY + 1)) done [[ $N_PY -eq 0 ]] && err "Python 3.10+ still not found after install." else err "Python 3.10+ is required.\n Install it: sudo dnf install python3" fi else err "Python 3.10+ not found.\n Install it with your package manager (e.g. sudo apt install python3)" fi fi # Use the first Python we found for JSON parsing PARSE_PY="${PY_BINS[0]}" # ── Fetch release from GitHub ──────────────────────────────────────── ARCH=$(uname -m) if [[ "$CHANNEL" == "prerelease" ]]; then info "Checking latest pre-release..." # /releases returns all releases sorted newest first; pick the first pre-release ALL_JSON=$(curl -sSf "https://api.github.com/repos/$REPO/releases?per_page=10" 2>/dev/null) \ || err "Cannot reach GitHub API. Check your internet connection." RELEASE_JSON=$("$PARSE_PY" -c ' import json, sys releases = json.loads(sys.stdin.read()) if isinstance(releases, dict) and "message" in releases: print(json.dumps(releases)) sys.exit(0) # Sort by published_at descending — GitHub API order is unreliable releases.sort(key=lambda r: r.get("published_at", ""), reverse=True) for r in releases: if r.get("prerelease", False): print(json.dumps(r)) sys.exit(0) print(json.dumps({"message": "No pre-release found. Create one on GitHub first."})) ' <<< "$ALL_JSON") else info "Checking latest release..." RELEASE_JSON=$(curl -sSf "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null) \ || err "Cannot reach GitHub API. Check your internet connection." fi # ── Match best Python + wheel from release assets ──────────────────── # Tries each installed Python version (newest first) against the # available wheels. Falls back to sdist (build from source) if no # pre-built wheel matches any installed Python. export KRASIS_ARCH="$ARCH" export KRASIS_PY_VERS="${PY_VERS[*]}" export KRASIS_PY_BINS="${PY_BINS[*]}" export KRASIS_PY_DOTS="${PY_DOTS[*]}" RESULT=$("$PARSE_PY" -c ' import json, sys, os data = json.loads(sys.stdin.read()) # Check for API errors (rate limit, not found, etc.) if "message" in data and "assets" not in data: print("ERROR") print(data["message"]) sys.exit(0) tag = data.get("tag_name", "unknown") arch = os.environ["KRASIS_ARCH"] py_vers = os.environ["KRASIS_PY_VERS"].split() py_bins = os.environ["KRASIS_PY_BINS"].split() py_dots = os.environ["KRASIS_PY_DOTS"].split() # Try each installed Python, newest first — pick the first with a wheel for i, pyver in enumerate(py_vers): cpver = f"cp{pyver}" for asset in data.get("assets", []): name = asset["name"] parts = name.removesuffix(".whl").split("-") if (name.endswith(".whl") and len(parts) >= 5 and parts[0] == "krasis" and parts[-3] == cpver and parts[-2] == cpver and any(platform.startswith(("manylinux", "linux_")) and platform.endswith("_" + arch) for platform in parts[-1].split("."))): print("WHEEL") print(tag) print(py_bins[i]) print(pyver) print(py_dots[i]) print(asset["browser_download_url"]) sys.exit(0) # No wheel for any installed Python — try sdist (build from source) for asset in data.get("assets", []): name = asset["name"] if (name.startswith("krasis-") and name.endswith(".tar.gz") and name[len("krasis-"):][:1].isdigit()): print("SDIST") print(tag) print(py_bins[0]) print(py_vers[0]) print(py_dots[0]) print(asset["browser_download_url"]) sys.exit(0) print("NONE") print(tag) ' <<< "$RELEASE_JSON") unset KRASIS_ARCH KRASIS_PY_VERS KRASIS_PY_BINS KRASIS_PY_DOTS # ── Parse result ───────────────────────────────────────────────────── mapfile -t LINES <<< "$RESULT" RTYPE="${LINES[0]}" case "$RTYPE" in ERROR) err "GitHub API: ${LINES[1]}\n If rate-limited, wait a minute and try again." ;; WHEEL) VERSION="${LINES[1]}" PYTHON="${LINES[2]}" PY_VER="${LINES[3]}" PY_VER_DOT="${LINES[4]}" DOWNLOAD_URL="${LINES[5]}" info "Latest release: $VERSION (Python $PY_VER_DOT, $ARCH)" ;; SDIST) VERSION="${LINES[1]}" PYTHON="${LINES[2]}" PY_VER="${LINES[3]}" PY_VER_DOT="${LINES[4]}" DOWNLOAD_URL="${LINES[5]}" warn "No pre-built wheel for your Python version(s) on $ARCH." info "Building from source (requires Rust toolchain + C compiler)..." command -v cargo &>/dev/null \ || err "Rust toolchain not found. Install it first:\n curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" ;; NONE) err "No packages found in release ${LINES[1]} for $ARCH.\n Visit https://github.com/$REPO/releases" ;; *) err "Unexpected response from GitHub API." ;; esac info "Using $PYTHON (Python $PY_VER_DOT)" # ── Ensure venv module ─────────────────────────────────────────────── if ! "$PYTHON" -c "import venv; import ensurepip" &>/dev/null; then warn "Python venv module not found. Installing python${PY_VER_DOT}-venv..." if command -v apt-get &>/dev/null; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) "${SUDO[@]}" apt-get update -qq /dev/null "${SUDO[@]}" apt-get install -y "python${PY_VER_DOT}-venv" /dev/null; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) "${SUDO[@]}" dnf install -y "python3-libs" /dev/null \ || err "Failed to install python venv. Try: sudo dnf install python3-libs" else err "Python venv module not found.\n Install it for your distro (e.g. sudo apt install python${PY_VER_DOT}-venv)" fi # Verify it worked "$PYTHON" -c "import venv" &>/dev/null \ || err "Python venv module still not available after install attempt." fi # ── Setup venv ─────────────────────────────────────────────────────── # If the venv exists, check it's healthy and uses the right Python. # If Python was upgraded or removed, the venv will be broken — recreate. NEED_VENV=false if [[ -d "$VENV_DIR" ]]; then VENV_PYVER=$("$VENV_DIR/bin/python" -c \ "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')" 2>/dev/null || echo "0") if [[ "$VENV_PYVER" != "$PY_VER" ]]; then warn "Venv was Python ${VENV_PYVER:0:1}.${VENV_PYVER:1}, now using $PY_VER_DOT. Recreating..." rm -rf "$VENV_DIR" NEED_VENV=true elif ! "$VENV_DIR/bin/python" -c "import pip" &>/dev/null; then warn "Existing venv is broken. Recreating..." rm -rf "$VENV_DIR" NEED_VENV=true fi else NEED_VENV=true fi if [[ "$NEED_VENV" == true ]]; then info "Creating environment at $VENV_DIR..." mkdir -p "$(dirname "$VENV_DIR")" "$PYTHON" -m venv "$VENV_DIR" 2>&1 || { # venv creation failed — likely missing ensurepip despite our earlier check warn "venv creation failed. Attempting to install python${PY_VER_DOT}-venv..." if command -v apt-get &>/dev/null; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) "${SUDO[@]}" apt-get update -qq /dev/null "${SUDO[@]}" apt-get install -y "python${PY_VER_DOT}-venv" /dev/null; then SUDO=() [[ "$(id -u)" -ne 0 ]] && SUDO=(sudo) "${SUDO[@]}" dnf install -y "python3-libs" /dev/null \ || err "Failed to install python venv. Try: sudo dnf install python3-libs" else err "venv creation failed.\n Install: sudo apt install python${PY_VER_DOT}-venv" fi # Retry rm -rf "$VENV_DIR" "$PYTHON" -m venv "$VENV_DIR" \ || err "venv creation still failing after installing python${PY_VER_DOT}-venv." } "$VENV_DIR/bin/pip" install --upgrade pip -q \ || warn "pip upgrade failed (non-fatal, continuing...)" fi cleanup_obsolete_venv_packages # ── Create models directory ────────────────────────────────────────── mkdir -p "$HOME/.krasis/models" # ── Download and install ───────────────────────────────────────────── TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT FILENAME="${DOWNLOAD_URL##*/}" info "Downloading $FILENAME..." curl -sSfL -o "$TMPDIR/$FILENAME" "$DOWNLOAD_URL" \ || err "Download failed. Check your internet connection." info "Installing Krasis ${VERSION#v}..." PIP_INSTALL_ARGS=(install) if [[ "$CHANNEL" == "prerelease" ]]; then PIP_INSTALL_ARGS+=(--force-reinstall --no-cache-dir) fi "$VENV_DIR/bin/pip" "${PIP_INSTALL_ARGS[@]}" "$TMPDIR/$FILENAME" \ || err "pip install failed. Check the output above for details." # ── Symlink commands ───────────────────────────────────────────────── mkdir -p "$BIN_DIR" for cmd in $COMMANDS; do if [[ -f "$VENV_DIR/bin/$cmd" ]]; then ln -sf "$VENV_DIR/bin/$cmd" "$BIN_DIR/$cmd" fi done ok "Commands installed: $COMMANDS" # ── Ensure PATH ────────────────────────────────────────────────────── ensure_path() { local rc="$1" local line='export PATH="$HOME/.local/bin:$PATH"' # Skip if .local/bin is already referenced if [[ -f "$rc" ]] && grep -q '\.local/bin' "$rc" 2>/dev/null; then return fi if [[ -f "$rc" ]]; then echo "" >> "$rc" echo "# Added by Krasis installer" >> "$rc" echo "$line" >> "$rc" fi } if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then current_shell="$(basename "${SHELL:-bash}")" case "$current_shell" in zsh) ensure_path "$HOME/.zshrc" ;; bash) ensure_path "$HOME/.bashrc" [[ -f "$HOME/.profile" ]] && ensure_path "$HOME/.profile" ;; *) ensure_path "$HOME/.bashrc" ensure_path "$HOME/.profile" ;; esac # Make available in THIS session immediately export PATH="$HOME/.local/bin:$PATH" fi # ── Verify ─────────────────────────────────────────────────────────── if command -v krasis &>/dev/null; then ok "Krasis $(krasis --version 2>/dev/null || echo "${VERSION#v}") is ready!" echo "" info "Next steps:" echo -e " 1. Restart your terminal (or run ${BOLD}source ~/.bashrc${NC})" echo -e " 2. Run ${BOLD}krasis-setup${NC} — checks CUDA tools and installs CUDA PyTorch" echo -e " 3. Run ${BOLD}krasis${NC} — launch the interactive TUI" echo "" else ok "Installed Krasis ${VERSION#v}." echo "" info "To start using it in this terminal, run:" echo "" echo -e " ${BOLD}export PATH=\"\$HOME/.local/bin:\$PATH\"${NC}" echo "" info "Then:" echo -e " 1. Restart your terminal (or run the export above)" echo -e " 2. Run ${BOLD}krasis-setup${NC} — checks CUDA tools and installs CUDA PyTorch" echo -e " 3. Run ${BOLD}krasis${NC} — launch the interactive TUI" echo "" fi echo -e "${DIM}Upgrade: krasis update${NC}" echo -e "${DIM}Pre-release: krasis prerelease${NC}" echo -e "${DIM}Uninstall: curl -sSf https://raw.githubusercontent.com/brontoguana/krasis/main/install.sh | bash -s -- --uninstall${NC}" } # end do_install do_install "$@"