#!/usr/bin/env bash # ============================================================================ # Koodos Installer — macOS / Linux # ============================================================================ # Turns the bare `pipx install` into a guided, no-prerequisite setup: # • checks for Python 3.8+ (and points you at a fix if it's missing) # • bootstraps pipx # • installs the koodos CLI from the official tarball # • registers THIS computer as a Koodos device (background agent) # # Usage (no key needed - it pairs, like connecting a remote control): # curl -fsSL https://api.kprcli.com/dl/install.sh | bash # # ...prints a short code, you approve it from a device you're already on. # Unattended installs can still hand it a key up front: # curl -fsSL https://api.kprcli.com/dl/install.sh | bash -s -- # # The device key can also come from the environment: # KOODOS_KEY=kood_xxx curl -fsSL https://api.kprcli.com/dl/install.sh | bash # # Flags: # --dry-run / --no-register do everything EXCEPT registering the device # (prints the command it would have run) # -h / --help show this help # ============================================================================ set -euo pipefail # ---------------------------------------------------------------------------- # Options / arguments # ---------------------------------------------------------------------------- KOODOS_KEY="${KOODOS_KEY:-${KOODOS_API_KEY:-}}" DRY_RUN=false TARBALL="https://api.kprcli.com/dl/koodos.tar.gz" DASHBOARD="https://kprcli.com" LOCAL_APP="http://localhost:6045" while [ $# -gt 0 ]; do case "$1" in --dry-run|--no-register) DRY_RUN=true; shift ;; -h|--help) sed -n '2,25p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' exit 0 ;; -*) echo "Unknown option: $1" >&2; exit 1 ;; *) # First bare argument is the device key. KOODOS_KEY="$1"; shift ;; esac done # ---------------------------------------------------------------------------- # Terminal capabilities: color + unicode detection (with graceful fallback) # ---------------------------------------------------------------------------- USE_COLOR=true USE_UNICODE=true # No color when: NO_COLOR set, dumb terminal, or stdout is not a tty AND we # aren't being piped from curl into an interactive shell. We still colorize # `curl | bash` because stdout there is usually the user's terminal. if [ -n "${NO_COLOR:-}" ] || [ "${TERM:-}" = "dumb" ]; then USE_COLOR=false fi case "${LANG:-}${LC_ALL:-}${LC_CTYPE:-}" in *UTF-8*|*utf8*|*UTF8*) : ;; *) USE_UNICODE=false ;; esac if [ "$USE_COLOR" = true ]; then # Matrix / Koodos green (#3fb950) via truecolor when supported, else 256, # else plain green. Keeps the same visual language everywhere. if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then GREEN='\033[38;2;63;185;80m' DIM_GREEN='\033[38;2;46;125;50m' else GREEN='\033[38;5;40m' DIM_GREEN='\033[38;5;28m' fi CYAN='\033[38;5;44m' YELLOW='\033[0;33m' RED='\033[0;31m' DIM='\033[2m' BOLD='\033[1m' NC='\033[0m' else GREEN=''; DIM_GREEN=''; CYAN=''; YELLOW=''; RED=''; DIM=''; BOLD=''; NC='' fi # Log helpers — one consistent visual language for every line. if [ "$USE_UNICODE" = true ]; then S_ARROW='→'; S_OK='✓'; S_WARN='⚠'; S_ERR='✗' else S_ARROW='>'; S_OK='[OK]'; S_WARN='[!]'; S_ERR='[X]' fi step() { printf "${CYAN}%s${NC} %b\n" "$S_ARROW" "$1"; } ok() { printf "${GREEN}%s${NC} %b\n" "$S_OK" "$1"; } warn() { printf "${YELLOW}%s${NC} %b\n" "$S_WARN" "$1"; } err() { printf "${RED}%s${NC} %b\n" "$S_ERR" "$1" >&2; } plain() { printf " %b\n" "$1"; } # ---------------------------------------------------------------------------- # Banner # ---------------------------------------------------------------------------- print_banner() { printf "\n" if [ "$USE_UNICODE" = true ]; then printf "${GREEN}${BOLD}" cat <<'BANNER' ██╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ██║ ██╔╝██╔═══██╗██╔═══██╗██╔══██╗██╔═══██╗██╔════╝ █████╔╝ ██║ ██║██║ ██║██║ ██║██║ ██║███████╗ ██╔═██╗ ██║ ██║██║ ██║██║ ██║██║ ██║╚════██║ ██║ ██╗╚██████╔╝╚██████╔╝██████╔╝╚██████╔╝███████║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ BANNER printf "${NC}" printf "${DIM_GREEN} your personal AI agent · kprcli.com${NC}\n\n" else printf "${GREEN}${BOLD}" printf "+-----------------------------------------------+\n" printf "| K O O D O S |\n" printf "| your personal AI agent |\n" printf "+-----------------------------------------------+\n" printf "${NC}\n" fi } # ---------------------------------------------------------------------------- # OS detection # ---------------------------------------------------------------------------- detect_os() { case "$(uname -s)" in Darwin*) OS="macos" ;; Linux*) OS="linux" ;; CYGWIN*|MINGW*|MSYS*) err "This looks like Windows." plain "Please use the PowerShell installer instead:" plain " ${BOLD}irm https://api.kprcli.com/dl/install.ps1 | iex${NC}" exit 1 ;; *) OS="unknown" ;; esac ok "Detected ${BOLD}$OS${NC}" } # ---------------------------------------------------------------------------- # Python 3.8+ # ---------------------------------------------------------------------------- PY="" find_python() { step "Checking for Python 3.8+" local candidate for candidate in python3 python; do if command -v "$candidate" >/dev/null 2>&1; then if "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3,8) else 1)' 2>/dev/null; then PY="$candidate" ok "Python $("$candidate" -c 'import platform;print(platform.python_version())') ($candidate)" return 0 fi fi done warn "Python 3.8+ was not found." plain "Install it, then re-run this installer:" if [ "$OS" = "macos" ]; then plain " ${BOLD}brew install python${NC} (Homebrew)" plain " or download from ${CYAN}https://www.python.org/downloads/${NC}" else plain " ${BOLD}sudo apt install -y python3 python3-pip python3-venv${NC} (Debian/Ubuntu)" plain " ${BOLD}sudo dnf install -y python3 python3-pip${NC} (Fedora)" plain " or download from ${CYAN}https://www.python.org/downloads/${NC}" fi exit 1 } # ---------------------------------------------------------------------------- # pipx # ---------------------------------------------------------------------------- PIPX="" ensure_pipx() { step "Ensuring pipx is available" if command -v pipx >/dev/null 2>&1; then PIPX="pipx" ok "pipx already installed" return 0 fi if "$PY" -m pipx --version >/dev/null 2>&1; then PIPX="$PY -m pipx" ok "pipx available via '$PY -m pipx'" return 0 fi step "Installing pipx (user-level, no sudo)" if ! "$PY" -m pip install --user --upgrade pipx >/dev/null 2>&1; then # pip itself might be missing an ensurepip bootstrap; try that once. "$PY" -m ensurepip --upgrade >/dev/null 2>&1 || true "$PY" -m pip install --user --upgrade pipx >/dev/null 2>&1 || { err "Could not install pipx automatically." plain "Try: ${BOLD}$PY -m pip install --user pipx${NC}" exit 1 } fi # Make sure pipx's bin dir is on PATH for THIS run and future shells. "$PY" -m pipx ensurepath >/dev/null 2>&1 || true export PATH="$HOME/.local/bin:$PATH" PIPX="$PY -m pipx" ok "pipx installed" } # ---------------------------------------------------------------------------- # koodos # ---------------------------------------------------------------------------- install_koodos() { # Note what's already here so a second run reports an update, not a first # install. PREV_VER="" if command -v koodos >/dev/null 2>&1; then PREV_VER="$(koodos version 2>/dev/null | sed 's/^koodos //')" step "Updating koodos (found ${BOLD}$PREV_VER${NC})" else step "Installing koodos from ${DIM}$TARBALL${NC}" fi # Already current? Then don't reinstall at all - a reinstall over a running # agent is pointless here and outright fails on Windows (locked venv). LATEST="$(curl -fsSL --max-time 10 https://api.kprcli.com/v1/version 2>/dev/null \ | sed -n 's/.*"latest"[^"]*"\([^"]*\)".*/\1/p')" if [ -n "$PREV_VER" ] && [ -n "$LATEST" ] && [ "$PREV_VER" = "$LATEST" ]; then ok "koodos $LATEST - already the latest" return 0 fi # An upgrade IS needed: stop the running agent first so nothing holds the # install open. `koodos link` starts it again at the end. if [ -n "$PREV_VER" ]; then koodos agent --stop >/dev/null 2>&1 || true sleep 1 fi # --force makes this fully idempotent: safe to re-run to upgrade in place. if ! $PIPX install --force "$TARBALL" >/dev/null 2>&1; then err "koodos install failed." plain "Re-run for details: ${BOLD}$PIPX install --force $TARBALL${NC}" exit 1 fi export PATH="$HOME/.local/bin:$PATH" if command -v koodos >/dev/null 2>&1; then NEW_VER="$(koodos version 2>/dev/null | sed 's/^koodos //')" if [ -n "$PREV_VER" ] && [ "$PREV_VER" = "$NEW_VER" ]; then ok "koodos $NEW_VER - already the latest" elif [ -n "$PREV_VER" ]; then ok "koodos updated ${BOLD}$PREV_VER${NC} -> ${BOLD}$NEW_VER${NC}" else ok "koodos $NEW_VER installed" fi else ok "koodos installed" warn "The 'koodos' command isn't on PATH yet - open a new terminal, or run:" plain " ${BOLD}export PATH=\"\$HOME/.local/bin:\$PATH\"${NC}" fi } # ---------------------------------------------------------------------------- # Register this device # ---------------------------------------------------------------------------- prompt_for_key() { # Only prompt when a real terminal is actually reachable. curl|bash has no # tty on stdin; /dev/tty often still works, but a bare `[ -r /dev/tty ]` # lies — the device node can be readable while open() fails with ENXIO # (no controlling terminal). Probe by really opening it. if ! { : < /dev/tty; } 2>/dev/null; then return 0 fi printf "${CYAN}%s${NC} Paste your device key (or press Enter to skip): " "$S_ARROW" > /dev/tty 2>/dev/null || return 0 IFS= read -r KOODOS_KEY < /dev/tty 2>/dev/null || KOODOS_KEY="" KOODOS_KEY="$(printf '%s' "$KOODOS_KEY" | tr -d '[:space:]')" } register_device() { # No key? Pair instead of asking anyone to handle a secret. `koodos link` # prints a short code and waits for you to approve it from a device you are # already on; the key is minted server-side and never typed here. if [ -z "$KOODOS_KEY" ]; then if [ "$DRY_RUN" = true ]; then warn "dry-run: NOT pairing. Would run: koodos link" REGISTERED=false return 0 fi if ! { : < /dev/tty; } 2>/dev/null; then warn "No device key and no terminal to show a pairing code on." plain "Run this on that machine when you're at it: ${BOLD}koodos link${NC}" REGISTERED=false return 0 fi step "Pairing this device" if koodos link < /dev/tty > /dev/tty 2>&1; then ok "Device paired and background agent installed" REGISTERED=true else warn "Pairing didn't finish - you can retry any time with:" plain " ${BOLD}koodos link${NC}" REGISTERED=false fi return 0 fi step "Registering this device" if [ "$DRY_RUN" = true ]; then warn "dry-run: NOT registering. Would run:" plain " ${BOLD}KOODOS_API_KEY=${KOODOS_KEY} koodos agent --install${NC}" REGISTERED=false return 0 fi # koodos agent --install reads the key from KOODOS_API_KEY (or saved config) # and self-installs the background autostart (one elevation prompt where the # platform needs it). Idempotent — re-running just refreshes the key/task. if KOODOS_API_KEY="$KOODOS_KEY" koodos agent --install; then ok "Device registered and background agent installed" REGISTERED=true else warn "Registration command returned an error - you can retry with:" plain " ${BOLD}KOODOS_API_KEY=$KOODOS_KEY koodos agent --install${NC}" REGISTERED=false fi } # ---------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------- print_summary() { printf "\n" local line if [ "$USE_UNICODE" = true ]; then printf "${GREEN}┌───────────────────────────────────────────────────────────┐${NC}\n" printf "${GREEN}│${NC} ${BOLD}Koodos is installed.${NC} ${GREEN}│${NC}\n" if [ "${REGISTERED:-false}" = true ]; then printf "${GREEN}│${NC} ${GREEN}%s${NC} running in the background · it auto-updates itself ${GREEN}│${NC}\n" "$S_OK" else printf "${GREEN}│${NC} ${DIM}not paired yet — run 'koodos link' to finish${NC} ${GREEN}│${NC}\n" fi printf "${GREEN}│${NC} ${GREEN}│${NC}\n" printf "${GREEN}│${NC} dashboard ${CYAN}%-44s${NC} ${GREEN}│${NC}\n" "$DASHBOARD" # The local app is served BY the agent process, so advertising it while # the agent isn't running just sends people to a dead port. if [ "${REGISTERED:-false}" = true ]; then printf "${GREEN}│${NC} local app ${CYAN}%-44s${NC} ${GREEN}│${NC}\n" "$LOCAL_APP" fi printf "${GREEN}└───────────────────────────────────────────────────────────┘${NC}\n" else printf "${GREEN}+-----------------------------------------------------------+${NC}\n" printf " Koodos is installed.\n" if [ "${REGISTERED:-false}" = true ]; then printf " %s running in the background - it auto-updates itself\n" "$S_OK" else printf " not paired yet - run 'koodos link' to finish\n" fi printf " dashboard %s\n" "$DASHBOARD" if [ "${REGISTERED:-false}" = true ]; then printf " local app %s\n" "$LOCAL_APP" fi printf "${GREEN}+-----------------------------------------------------------+${NC}\n" fi printf "\n" if [ "${REGISTERED:-false}" = true ]; then plain "${BOLD}Next:${NC} open ${CYAN}$DASHBOARD${NC} or talk to it locally at ${CYAN}$LOCAL_APP${NC}" else plain "${BOLD}Next:${NC} run ${BOLD}koodos link${NC} to connect this machine to your fleet." fi printf "\n" } # ---------------------------------------------------------------------------- # Main # ---------------------------------------------------------------------------- main() { print_banner detect_os find_python ensure_pipx install_koodos register_device print_summary } main