#!/usr/bin/env sh # Okibi installer for macOS and Linux. # # curl -fsSL https://okibi.ai/install | sh # # Installs the PUBLIC okibi thin client (an API client for the Okibi # platform — no engine, no keys; see docs/architecture.md). The script # itself and everything it fetches are public: there is no password gate. # # It detects OS/arch, fetches latest.json from the release host, downloads # the matching tarball, SHA-256 verifies it, extracts to # ~/.local/share/okibi/versions//, and symlinks okibi (+ okibi-tui) # into ~/.local/bin so they land on PATH. Idempotent — re-running upgrades # to whatever latest.json now points at. # # Env overrides: # OKIBI_RELEASE_BASE Release-host base URL (default baked in at serve time). # OKIBI_INSTALL_ROOT Install dir (default ~/.local/share/okibi). # OKIBI_BIN_DIR Symlink dir (default ~/.local/bin). set -eu # https://releases.okibi.ai is substituted by the /install route with the # deployment's configured release base; the fallback keeps a hand-piped # copy of this script working. DEFAULT_RELEASE_BASE='https://releases.okibi.ai' RELEASE_BASE="${OKIBI_RELEASE_BASE:-$DEFAULT_RELEASE_BASE}" INSTALL_ROOT="${OKIBI_INSTALL_ROOT:-$HOME/.local/share/okibi}" BIN_DIR="${OKIBI_BIN_DIR:-$HOME/.local/bin}" err() { printf 'install: %s\n' "$*" >&2; exit 1; } info() { printf 'install: %s\n' "$*"; } command -v curl >/dev/null 2>&1 || err 'curl is required' command -v tar >/dev/null 2>&1 || err 'tar is required' detect_platform() { os='' arch='' case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) err "unsupported OS: $(uname -s) (okibi ships macOS and Linux builds)" ;; esac case "$(uname -m)" in arm64 | aarch64) arch=arm64 ;; x86_64 | amd64) arch=x64 ;; *) err "unsupported architecture: $(uname -m)" ;; esac printf '%s-%s' "$os" "$arch" } # Bare-bones JSON extraction — avoids requiring jq. Pulls `field` from the # artifact object whose "platform" matches. Fragile by design: latest.json # is a release-CI artifact with a fixed shape. extract_json_field() { platform="$1" field="$2" file="$3" tr -d '\n' <"$file" \ | grep -o "{[^{}]*\"platform\"[[:space:]]*:[[:space:]]*\"$platform\"[^{}]*}" \ | grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ | head -1 \ | sed -E "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\\1/" } verify_sha256() { file="$1" expected="$2" 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 err 'no sha256sum or shasum available to verify the download' fi [ "$actual" = "$expected" ] \ || err "checksum mismatch: expected $expected, got $actual" } PLATFORM="$(detect_platform)" info "platform: $PLATFORM" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT MANIFEST="$TMP/latest.json" info "fetching $RELEASE_BASE/latest.json" curl -fsSL "$RELEASE_BASE/latest.json" -o "$MANIFEST" \ || err "could not reach the release host at $RELEASE_BASE" VERSION="$(tr -d '\n' <"$MANIFEST" \ | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \ | head -1 | sed -E 's/.*"([^"]+)"[[:space:]]*$/\1/')" [ -n "$VERSION" ] || err 'latest.json is missing a version field' # The version becomes a path segment that is rm -rf'd below, so a hostile or # corrupt manifest must not be able to smuggle in path separators or "..". case "$VERSION" in *[!A-Za-z0-9._+-]* | *..*) err "latest.json has an unsafe version string: $VERSION" ;; esac URL="$(extract_json_field "$PLATFORM" url "$MANIFEST")" SHA="$(extract_json_field "$PLATFORM" sha256 "$MANIFEST")" [ -n "$URL" ] || err "no okibi build published for $PLATFORM yet" [ -n "$SHA" ] || err "latest.json has no checksum for $PLATFORM" # Relative URLs resolve against the release base. case "$URL" in http://* | https://*) ;; *) URL="$RELEASE_BASE/$URL" ;; esac TARBALL="$TMP/okibi.tar.gz" info "downloading okibi $VERSION" curl -fsSL "$URL" -o "$TARBALL" || err 'download failed' verify_sha256 "$TARBALL" "$SHA" info 'checksum verified' VERSION_DIR="$INSTALL_ROOT/versions/$VERSION" rm -rf "$VERSION_DIR" mkdir -p "$VERSION_DIR" # The archive wraps everything in one okibi-client--/ dir; # strip it so okibi, okibi-tui, release.json land directly in VERSION_DIR. tar -xzf "$TARBALL" -C "$VERSION_DIR" --strip-components=1 [ -f "$VERSION_DIR/okibi" ] || err 'archive did not contain an okibi binary' chmod +x "$VERSION_DIR/okibi" 2>/dev/null || true [ -f "$VERSION_DIR/okibi-tui" ] && chmod +x "$VERSION_DIR/okibi-tui" 2>/dev/null || true info "installed to $VERSION_DIR" mkdir -p "$BIN_DIR" ln -sfn "$VERSION_DIR/okibi" "$BIN_DIR/okibi" info "symlinked $BIN_DIR/okibi" # okibi-tui is the interactive frontend; okibi finds it via OKIBI_TUI_BIN or # a sibling on PATH, so keep it alongside. if [ -f "$VERSION_DIR/okibi-tui" ]; then ln -sfn "$VERSION_DIR/okibi-tui" "$BIN_DIR/okibi-tui" fi # Record the host so re-running the installer (today's upgrade path) and a # future `okibi upgrade` both know where to re-fetch. Unlike an auth-gated # host, nothing secret is stored — the release host is public. CONFIG_DIR="$HOME/.okibi" mkdir -p "$CONFIG_DIR" printf '%s\n' "$RELEASE_BASE" >"$CONFIG_DIR/installed-from" case ":$PATH:" in *":$BIN_DIR:"*) ;; *) info '' info "NOTE: $BIN_DIR is not on your PATH. Add this to your shell profile:" info '' info " export PATH=\"$BIN_DIR:\$PATH\"" info '' ;; esac # Post-install tail. Swap the copy here if the wording changes; keep every # command real (verify against the client's `okibi --help`). info "done: okibi $VERSION is installed" info '' info 'two steps to your first CLI:' info ' 1. okibi auth login log in (opens your browser)' info ' 2. okibi start okibi in the project you want a CLI for' info ' it walks you through the rest'