diff options
Diffstat (limited to 'extra')
| -rw-r--r-- | extra/install-claude.sh | 165 | ||||
| -rw-r--r-- | extra/install-cursor.sh | 151 | ||||
| -rw-r--r-- | extra/install-kimi.sh | 185 | ||||
| -rw-r--r-- | extra/install-xai.sh | 349 |
4 files changed, 850 insertions, 0 deletions
diff --git a/extra/install-claude.sh b/extra/install-claude.sh new file mode 100644 index 0000000..41313f3 --- /dev/null +++ b/extra/install-claude.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +set -e + +# Parse command line arguments +TARGET="$1" # Optional target parameter + +# Validate target if provided +if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then + echo "Usage: $0 [stable|latest|VERSION]" >&2 + exit 1 +fi + +DOWNLOAD_BASE_URL="https://downloads.claude.ai/claude-code-releases" +DOWNLOAD_DIR="$HOME/.claude/downloads" + +# Check for required dependencies +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "Either curl or wget is required but neither is installed" >&2 + exit 1 +fi + +# Check if jq is available (optional) +HAS_JQ=false +if command -v jq >/dev/null 2>&1; then + HAS_JQ=true +fi + +# Download function that works with both curl and wget +download_file() { + local url="$1" + local output="$2" + + if [ "$DOWNLOADER" = "curl" ]; then + if [ -n "$output" ]; then + curl -fsSL -o "$output" "$url" + else + curl -fsSL "$url" + fi + elif [ "$DOWNLOADER" = "wget" ]; then + if [ -n "$output" ]; then + wget -q -O "$output" "$url" + else + wget -q -O - "$url" + fi + else + return 1 + fi +} + +# Simple JSON parser for extracting checksum when jq is not available +get_checksum_from_manifest() { + local json="$1" + local platform="$2" + + # Normalize JSON to single line and extract checksum + json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g') + + # Extract checksum for platform using bash regex + if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + return 1 +} + +# Detect platform +case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;; + *) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac, +# download the native arm64 binary instead of the x64 one +if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi +fi + +# Check for musl on Linux and adjust platform accordingly +if [ "$os" = "linux" ]; then + if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then + platform="linux-${arch}-musl" + else + platform="linux-${arch}" + fi +else + platform="${os}-${arch}" +fi +mkdir -p "$DOWNLOAD_DIR" + +# Always download latest version (which has the most up-to-date installer) +version=$(download_file "$DOWNLOAD_BASE_URL/latest") + +# Reject non-version content (e.g. an HTML error page) before it reaches the manifest URL +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "Failed to get a valid version from downloads.claude.ai (got unexpected content)." >&2 + echo "This can happen if the download service is unreachable or not available in your region - see https://www.anthropic.com/supported-countries" >&2 + exit 1 +fi + +# Download manifest and extract checksum +manifest_json=$(download_file "$DOWNLOAD_BASE_URL/$version/manifest.json") + +# Use jq if available, otherwise fall back to pure bash parsing +if [ "$HAS_JQ" = true ]; then + checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty") +else + checksum=$(get_checksum_from_manifest "$manifest_json" "$platform") +fi + +# Validate checksum format (SHA256 = 64 hex characters) +if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then + echo "Platform $platform not found in manifest" >&2 + exit 1 +fi + +# Download and verify +binary_path="$DOWNLOAD_DIR/claude-$version-$platform" +if ! download_file "$DOWNLOAD_BASE_URL/$version/$platform/claude" "$binary_path"; then + echo "Download failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +# Pick the right checksum tool +if [ "$os" = "darwin" ]; then + actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1) +else + actual=$(sha256sum "$binary_path" | cut -d' ' -f1) +fi + +if [ "$actual" != "$checksum" ]; then + echo "Checksum verification failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +chmod +x "$binary_path" + +# Run claude install to set up launcher and shell integration +echo "Setting up Claude Code..." +"$binary_path" install ${TARGET:+"$TARGET"} + +# Clean up downloaded file +rm -f "$binary_path" + +echo "" +echo "✅ Installation complete!" +echo "" diff --git a/extra/install-cursor.sh b/extra/install-cursor.sh new file mode 100644 index 0000000..fb9ca2c --- /dev/null +++ b/extra/install-cursor.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +# Color support: check NO_COLOR or non-TTY (stdout) +if [ -n "${NO_COLOR+x}" ] || [ ! -t 1 ]; then + CYAN='' + GREEN='' + YELLOW='' + BLUE='' + PURPLE='' + RED='' + BOLD='' + DIM='' + NC='' +else + CYAN='\033[0;36m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + PURPLE='\033[0;35m' + RED='\033[0;31m' + BOLD='\033[1m' + DIM='\033[2m' + NC='\033[0m' +fi + +# Fancy header +echo "" +echo -e "${BOLD}Cursor Agent Installer${NC}" +echo "" + +# Function to print steps with style +print_step() { + echo -e "${BLUE}▸${NC} ${1}" +} + +# Function to print success +print_success() { + # Move cursor up one line and clear it + echo -ne "\033[1A\033[2K" + echo -e "${GREEN}✓${NC} ${1}" +} + +# Function to print error +print_error() { + echo -e "${RED}✗${NC} ${1}" +} + +# Detect OS and Architecture +print_step "Detecting system architecture..." + +# Detect OS +OS="$(uname -s)" +case "${OS}" in + Linux*) OS="linux";; + Darwin*) OS="darwin";; + *) + print_error "Unsupported operating system: ${OS}" + exit 1 + ;; +esac + +# Detect Architecture +ARCH="$(uname -m)" +case "${ARCH}" in + x86_64|amd64) ARCH="x64";; + arm64|aarch64) ARCH="arm64";; + *) + print_error "Unsupported architecture: ${ARCH}" + exit 1 + ;; +esac + +print_success "Detected ${OS}/${ARCH}" + +# Installation steps +print_step "Creating installation directory..." +# Create temporary directory for atomic download inside versions folder +TEMP_EXTRACT_DIR="$HOME/.local/share/cursor-agent/versions/.tmp-2026.05.27-fe9a6e2-$(date +%s)" +mkdir -p "${TEMP_EXTRACT_DIR}" + +print_success "Directory created" + + +print_step "Downloading Cursor Agent package..." +DOWNLOAD_URL="https://downloads.cursor.com/lab/2026.05.27-fe9a6e2/${OS}/${ARCH}/agent-cli-package.tar.gz" +echo -e "${DIM} Download URL: ${DOWNLOAD_URL}${NC}" + +# Cleanup function +cleanup() { + command rm -rf "${TEMP_EXTRACT_DIR}" +} +trap cleanup EXIT + +# Download with progress bar and better error handling +if curl -fSL --progress-bar "${DOWNLOAD_URL}" \ + | tar --strip-components=1 -xzf - -C "${TEMP_EXTRACT_DIR}"; then + echo -ne "\033[1A\033[2K" + echo -ne "\033[1A\033[2K" + echo -ne "\033[1A\033[2K" + print_success "Package downloaded and extracted" +else + print_error "Download failed. Please check your internet connection and try again." + print_error "If the problem persists, the package might not be available for ${OS}/${ARCH}." + cleanup + exit 1 +fi + +print_step "Finalizing installation..." +# Atomically move from temp to final destination +FINAL_DIR="$HOME/.local/share/cursor-agent/versions/2026.05.27-fe9a6e2" +command rm -rf "${FINAL_DIR}" +if mv "${TEMP_EXTRACT_DIR}" "${FINAL_DIR}"; then + print_success "Package installed successfully" +else + print_error "Failed to install package. Please check permissions." + cleanup + exit 1 +fi + + +print_step "Creating bin directory..." +mkdir -p ~/.local/bin +print_success "Bin directory ready" + + +print_step "Creating symlinks to agent executable..." +# Remove any existing symlink or file +command rm -f ~/.local/bin/cursor-agent +# Create symlinks to the Cursor Agent executable +ln -s ~/.local/share/cursor-agent/versions/2026.05.27-fe9a6e2/cursor-agent ~/.local/bin/cursor-agent +print_success "Symlink created" + +# Success message +echo "" +echo -e "${BOLD}${GREEN}✨ Installation Complete! ${NC}" +echo "" +echo "" + +# Check if ~/.local/bin is already in PATH +if [[ ":${PATH}:" == *":${HOME}/.local/bin:"* ]]; then + # PATH already configured, just show usage + echo -e "${BOLD}Start using Cursor Agent:${NC}" + echo -e " ${BOLD}cursor-agent${NC}" +else + echo -e "${BOLD}Add ~/.local/bin to PATH:${NC}" +fi + +echo "" +echo "" +echo -e "${BOLD}${CYAN}Happy coding! 🚀${NC}" +echo "" diff --git a/extra/install-kimi.sh b/extra/install-kimi.sh new file mode 100644 index 0000000..d1013ca --- /dev/null +++ b/extra/install-kimi.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# +# kimi-code installer for macOS and Linux. +# +# Usage: +# curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +# KIMI_VERSION=0.5.0 curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +# KIMI_INSTALL_DIR=/usr/local curl -fsSL https://code.kimi.com/kimi-code/install.sh | sudo -E bash +# +# Optional env: +# KIMI_VERSION Explicit version; if unset, fetch from https://code.kimi.com/kimi-code/latest +# KIMI_INSTALL_DIR Installation directory, default $HOME/.kimi-code + +set -euo pipefail + +KIMI_DOWNLOAD_BASE="https://code.kimi.com/kimi-code" + +KIMI_VERSION="${KIMI_VERSION:-}" +KIMI_INSTALL_DIR="${KIMI_INSTALL_DIR:-$HOME/.local/bin}" + +# ---------- helpers ---------- + +_have() { command -v "$1" >/dev/null 2>&1; } + +_log() { + if [ -t 1 ]; then + printf '\033[1;36m==>\033[0m %s\n' "$*" + else + printf '==> %s\n' "$*" + fi +} + +_err() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +_detect_target() { + local os arch + case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) + _err "Windows is not supported by install.sh — use install.ps1 (PowerShell)" + ;; + *) _err "unsupported OS: $(uname -s)" ;; + esac + case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) _err "unsupported architecture: $(uname -m)" ;; + esac + + # Rosetta 2: when running under an x64 shell on an ARM Mac, switch back to the native arm64 binary + if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi + fi + + # musl detection (Alpine, etc.) — we currently only ship glibc binaries, so failing early is better than a runtime dlopen error + if [ "$os" = "linux" ]; then + if [ -f "/lib/libc.musl-x86_64.so.1" ] || \ + [ -f "/lib/libc.musl-aarch64.so.1" ] || \ + ldd /bin/ls 2>&1 | grep -q musl; then + _err "Alpine / musl Linux is not currently supported. Use Node.js + 'npm install -g @moonshot-ai/kimi-code' instead." + fi + fi + + echo "${os}-${arch}" +} + +_download() { + local url="$1" dest="${2:-}" + if _have curl; then + if [ -n "$dest" ]; then + if [ -t 1 ]; then + curl --fail --location --progress-bar -o "$dest" "$url" + else + curl --fail --location --silent -o "$dest" "$url" + fi + else + curl --fail --location --silent "$url" + fi + elif _have wget; then + if [ -n "$dest" ]; then + wget -q -O "$dest" "$url" + else + wget -q -O - "$url" + fi + else + _err "curl or wget is required" + fi +} + +# Prefer jq; otherwise parse a single manifest field using pure bash regex +_manifest_field() { + local manifest_json="$1" target="$2" field="$3" + if _have jq; then + printf '%s' "$manifest_json" | jq -er ".platforms[\"$target\"].$field // empty" + else + local one_line + one_line="$(printf '%s' "$manifest_json" | tr -d '\n\r\t' | sed 's/ \+/ /g')" + if [[ $one_line =~ \"$target\"[^}]*\"$field\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then + printf '%s' "${BASH_REMATCH[1]}" + fi + fi +} + +_sha256_check() { + local file="$1" expected="$2" + local actual + if _have shasum; then + actual="$(shasum -a 256 "$file" | cut -d' ' -f1)" + elif _have sha256sum; then + actual="$(sha256sum "$file" | cut -d' ' -f1)" + else + _err "shasum or sha256sum required to verify download" + fi + if [ "$actual" != "$expected" ]; then + _err "checksum mismatch: expected $expected, got $actual" + fi +} + +# ---------- main ---------- + +TMPDIR_INSTALL="" +_cleanup() { + if [ -n "$TMPDIR_INSTALL" ] && [ -d "$TMPDIR_INSTALL" ]; then + rm -rf "$TMPDIR_INSTALL" + fi +} +trap _cleanup EXIT + +main() { + local target version manifest_url manifest filename checksum binary_url tmpdir + + target="$(_detect_target)" + _log "Detected target: $target" + + # 1. Resolve version + if [ -n "$KIMI_VERSION" ]; then + version="$KIMI_VERSION" + _log "Using pinned version $version" + else + _log "Resolving latest version from ${KIMI_DOWNLOAD_BASE}/latest" + version="$(_download "${KIMI_DOWNLOAD_BASE}/latest" | tr -d '[:space:]')" + [ -n "$version" ] || _err "could not resolve latest version" + _log "Latest version: $version" + fi + + # 2. Fetch manifest + manifest_url="${KIMI_DOWNLOAD_BASE}/${version}/manifest.json" + _log "Fetching manifest ${manifest_url}" + manifest="$(_download "$manifest_url")" + [ -n "$manifest" ] || _err "manifest is empty or unreachable: $manifest_url" + + # 3. Find current platform entry + filename="$(_manifest_field "$manifest" "$target" "filename")" + checksum="$(_manifest_field "$manifest" "$target" "checksum")" + [ -n "$filename" ] || _err "platform $target not found in manifest" + [[ "$checksum" =~ ^[a-f0-9]{64}$ ]] || _err "invalid checksum for $target: $checksum" + + # 4. Download binary + TMPDIR_INSTALL="$(mktemp -d 2>/dev/null || mktemp -d -t kimi-install)" + tmpdir="$TMPDIR_INSTALL" + binary_url="${KIMI_DOWNLOAD_BASE}/${version}/${filename}" + _log "Downloading ${binary_url}" + _download "$binary_url" "${tmpdir}/${filename}" + + # 5. Verify checksum + _log "Verifying checksum" + _sha256_check "${tmpdir}/${filename}" "$checksum" + + # 6. Install + chmod +x "${tmpdir}/${filename}" + mkdir -p "${KIMI_INSTALL_DIR}" + install -m 0755 "${tmpdir}/${filename}" "${KIMI_INSTALL_DIR}/kimi" + _log "Installed to ${KIMI_INSTALL_DIR}/kimi" + + + _log "Done. Run: kimi --version" +} + +main "$@" diff --git a/extra/install-xai.sh b/extra/install-xai.sh new file mode 100644 index 0000000..0f2e1f4 --- /dev/null +++ b/extra/install-xai.sh @@ -0,0 +1,349 @@ +#!/bin/bash +# +# Grok CLI installer — https://x.ai/cli/install.sh +# +# Auth: GROK_DEPLOYMENT_KEY (takes precedence) or ~/.grok/auth.json from `grok login`. +# Env: GROK_CHANNEL (stable|alpha|enterprise, default: stable), GROK_BIN_DIR, GROK_PROXY_URL +# +# Usage: +# curl -fsSL https://x.ai/cli/install.sh | bash # latest stable +# curl -fsSL https://x.ai/cli/install.sh | bash -s 0.1.42 # specific version +# GROK_DEPLOYMENT_KEY=<key> bash <(curl -fsSL https://x.ai/cli/install.sh) +# +# Windows: run under Git for Windows / MSYS2 Bash (same curl | bash flow); WSL +# uses the Linux binary. + +set -e + +TARGET="$1" + +if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?$ ]]; then + echo "Invalid version format: $TARGET (expected X.Y.Z or X.Y.Z-suffix)" >&2 + exit 1 +fi + +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "Either curl or wget is required but neither is installed" >&2 + exit 1 +fi + +download_file() { + local url="$1" output="$2" + if [ "$DOWNLOADER" = "curl" ]; then + if [ -n "$output" ]; then + curl -fsSL -o "$output" "$url" + else + curl -fsSL "$url" + fi + else + if [ -n "$output" ]; then + wget -q -O "$output" "$url" + else + wget -q -O - "$url" + fi + fi +} + +# Parallel byte-range download. Falls back to single-connection download_file +# whenever HEAD lacks Content-Length, the file is small (<16 MiB), curl is +# unavailable, or any chunk fetch / concat fails. +download_file_parallel() { + local url="$1" output="$2" + if [ "$DOWNLOADER" != "curl" ]; then + download_file "$url" "$output" + return + fi + local size + size=$(curl -fsSL --head "$url" 2>/dev/null | awk -F'[: \r\n]+' 'tolower($1)=="content-length"{print $2; exit}') + if [ -z "$size" ] || ! [ "$size" -ge 16777216 ] 2>/dev/null; then + download_file "$url" "$output" + return + fi + local n=8 + local chunk_size=$(( (size + n - 1) / n )) + local tmpdir + tmpdir=$(mktemp -d 2>/dev/null) || { download_file "$url" "$output"; return; } + local pids=() i start end + for i in $(seq 0 $((n - 1))); do + start=$((i * chunk_size)) + end=$((start + chunk_size - 1)) + [ $end -ge $size ] && end=$((size - 1)) + curl -fsSL -r "${start}-${end}" -o "${tmpdir}/$(printf 'chunk.%03d' "$i")" "$url" & + pids+=($!) + done + local all_ok=true pid + for pid in "${pids[@]}"; do + wait "$pid" || all_ok=false + done + if [ "$all_ok" = true ] && cat "${tmpdir}"/chunk.* > "$output" 2>/dev/null; then + rm -rf "$tmpdir" + return 0 + fi + rm -rf "$tmpdir" + download_file "$url" "$output" +} + +# Return 0 if a HEAD request for the URL gets HTTP 404. +is_not_found() { + local url="$1" code + if [ "$DOWNLOADER" = "curl" ]; then + code=$(curl -o /dev/null -sSL -w '%{http_code}' --head "$url" 2>/dev/null) || true + else + code=$(wget --server-response --spider "$url" 2>&1 | awk '/HTTP\//{print $2}' | tail -1) || true + fi + [ "$code" = "404" ] +} + +# JSON field extractor — extract a top-level string value using sed. +json_get() { + local json="$1" field="$2" + # Extract value (handling \" inside strings), then unescape JSON sequences. + printf '%s' "$json" | sed -n -E 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' | head -1 \ + | sed -e 's/\\"/"/g' -e 's/\\n/\'$'\n''/g' -e 's/\\t/\'$'\t''/g' -e 's/\\\\/\\/g' +} + +# Read a token from ~/.grok/auth.json for the given scope key. +# Format: {"scope_url": {"key": "token"}, ...} +read_grok_token() { + local auth_file="$HOME/.grok/auth.json" + local scope="$1" + [ -f "$auth_file" ] || return 1 + # Flatten to one line then extract: find the scope, then the "key" value after it + tr -d '\n' < "$auth_file" | sed -n 's|.*"'"$scope"'"[[:space:]]*:[[:space:]]*{[^}]*"key"[[:space:]]*:[[:space:]]*"\([^"]*\)".*|\1|p' | head -1 +} + +# Resolve auth: GROK_DEPLOYMENT_KEY > OIDC token > legacy token +OIDC_SCOPE="https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828" +LEGACY_SCOPE="https://accounts.x.ai/sign-in" +AUTH_SOURCE="" + +if [ -n "$GROK_DEPLOYMENT_KEY" ]; then + AUTH_SOURCE="deployment key" + echo "Auth: using deployment key." >&2 +else + OIDC_TOKEN=$(read_grok_token "$OIDC_SCOPE" 2>/dev/null) || true + LEGACY_TOKEN=$(read_grok_token "$LEGACY_SCOPE" 2>/dev/null) || true + if [ -n "$OIDC_TOKEN" ]; then + AUTH_SOURCE="auth.json (oidc)" + echo "Auth: using OIDC token from ~/.grok/auth.json." >&2 + elif [ -n "$LEGACY_TOKEN" ]; then + AUTH_SOURCE="auth.json (legacy)" + echo "Auth: using legacy token from ~/.grok/auth.json." >&2 + fi +fi + +case "$(uname -s)" in + Darwin) os="macos" ;; + Linux) os="linux" ;; + # Git for Windows / MSYS2 / Cygwin host — native Windows builds + MINGW* | MSYS* | CYGWIN*) os="windows" ;; + *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64|AMD64) arch="x86_64" ;; + arm64|aarch64|ARM64) arch="aarch64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +BASE_URL_PRIMARY="https://x.ai/cli" +BASE_URL_FALLBACK="https://storage.googleapis.com/grok-build-public-artifacts/cli" +DOWNLOAD_DIR="$HOME/.grok/downloads" +BIN_DIR="${GROK_BIN_DIR:-$HOME/.local/bin}" +mkdir -p "$DOWNLOAD_DIR" "$BIN_DIR" + +platform="${os}-${arch}" +CHANNEL="${GROK_CHANNEL:-stable}" + +# Pick a working BASE_URL: try Cloudflare-fronted x.ai first, fall back to +# direct GCS if it's unreachable. The probe doubles as the channel-pointer +# fetch when no explicit TARGET was passed, so the happy path costs zero +# extra HTTP requests. +if [ -z "$TARGET" ]; then echo "Fetching latest ${CHANNEL} version..." >&2; fi +probe_result=$(download_file "${BASE_URL_PRIMARY}/${CHANNEL}" 2>/dev/null) || true +if [ -n "$probe_result" ]; then + BASE_URL="$BASE_URL_PRIMARY" +else + echo "Note: ${BASE_URL_PRIMARY} unreachable, falling back to direct GCS." >&2 + BASE_URL="$BASE_URL_FALLBACK" + probe_result=$(download_file "${BASE_URL}/${CHANNEL}" 2>/dev/null) || true +fi + +if [ -n "$TARGET" ]; then + version="$TARGET" +else + version="$probe_result" + if [ -z "$version" ]; then + echo "Error: failed to fetch latest version from ${BASE_URL_PRIMARY}/${CHANNEL} and ${BASE_URL_FALLBACK}/${CHANNEL}" >&2 + exit 1 + fi +fi + +if [ -n "$AUTH_SOURCE" ]; then + echo "Installing Grok $version ($platform, $AUTH_SOURCE)..." >&2 +else + echo "Installing Grok $version ($platform)..." >&2 +fi + +binary_path="$DOWNLOAD_DIR/grok-$platform" +artifact_base="${BASE_URL}/grok-${version}-${platform}" + +if [ "$os" = "windows" ]; then + binary_path="${binary_path}.exe" +fi + +echo " Downloading grok ${version}..." >&2 +if [ "$os" = "windows" ]; then + if ! download_file_parallel "${artifact_base}.exe" "$binary_path"; then + if ! download_file_parallel "$artifact_base" "$binary_path"; then + rm -f "$binary_path" + if is_not_found "${artifact_base}.exe"; then + echo "Error: Grok is not yet available for your system ($platform)." >&2 + else + echo "Error: binary download failed (${artifact_base}.exe and ${artifact_base})" >&2 + fi + exit 1 + fi + fi +elif ! download_file_parallel "$artifact_base" "$binary_path"; then + rm -f "$binary_path" + if is_not_found "$artifact_base"; then + echo "Error: Grok is not yet available for your system ($platform)." >&2 + else + echo "Error: binary download failed from ${artifact_base}" >&2 + fi + exit 1 +fi + +if [ "$os" = "windows" ]; then + # Symlinks require Developer Mode on Windows; copy instead. + # If the exe is locked by a running process, rename it aside then retry. + for bin_name in grok.exe ; do + rm -f "$BIN_DIR/$bin_name.old" 2>/dev/null || true # stale backup from prior update + if ! cp -f "$binary_path" "$BIN_DIR/$bin_name" 2>/dev/null; then + mv -f "$BIN_DIR/$bin_name" "$BIN_DIR/$bin_name.old" 2>/dev/null || true + if ! cp -f "$binary_path" "$BIN_DIR/$bin_name" 2>/dev/null; then + # Rollback: restore the old binary so the install isn't broken. + mv -f "$BIN_DIR/$bin_name.old" "$BIN_DIR/$bin_name" 2>/dev/null || true + echo "Error: failed to install $bin_name" >&2 + exit 1 + fi + fi + done + echo " Binary installed to $BIN_DIR/grok.exe" >&2 +else + chmod +x "$binary_path" + # Use relative symlinks when BIN_DIR and DOWNLOAD_DIR share a parent + # (default layout: ~/.grok/bin and ~/.grok/downloads are siblings). + # Relative symlinks survive Docker bind-mounts with a different $HOME. + if [ "$(dirname "$BIN_DIR")" = "$(dirname "$DOWNLOAD_DIR")" ]; then + link_target="../$(basename "$DOWNLOAD_DIR")/$(basename "$binary_path")" + else + link_target="$binary_path" + fi + ln -sf "$link_target" "$BIN_DIR/grok" + echo " Binary linked to $BIN_DIR/grok" >&2 +fi + +# # Generate shell completions (best-effort) +# mkdir -p "$HOME/.grok/completions/bash" "$HOME/.grok/completions/zsh" +# "$BIN_DIR/grok" completions bash > "$HOME/.grok/completions/bash/grok.bash" 2>/dev/null || true +# "$BIN_DIR/grok" completions zsh > "$HOME/.grok/completions/zsh/_grok" 2>/dev/null || true +# # Fish: write to the auto-loaded completions dir so it works immediately +# if mkdir -p "$HOME/.config/fish/completions" 2>/dev/null; then +# "$BIN_DIR/grok" completions fish > "$HOME/.config/fish/completions/grok.fish" 2>/dev/null || true +# fi + +# Persist installer source and channel to config +CONFIG_FILE="$HOME/.grok/config.toml" +CLI_BLOCK="installer = \"internal\"" +if [ "$CHANNEL" != "stable" ]; then + CLI_BLOCK="${CLI_BLOCK}\nchannel = \"${CHANNEL}\"" +fi +if [ ! -f "$CONFIG_FILE" ]; then + printf '[cli]\n%b\n' "$CLI_BLOCK" > "$CONFIG_FILE" +elif grep -q '^\[cli\]' "$CONFIG_FILE"; then + tmp="$CONFIG_FILE.tmp.$$" + awk -v block="$CLI_BLOCK" ' + /^\[cli\][[:space:]]*(#.*)?$/ { print; printf "%s\n", block; in_cli=1; next } + /^\[.*\][[:space:]]*(#.*)?$/ { in_cli=0 } + in_cli && /^[[:space:]]*(installer|channel)[[:space:]]*=/ { next } + { print } + ' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" +else + printf '\n[cli]\n%b\n' "$CLI_BLOCK" >> "$CONFIG_FILE" +fi + +# Fetch managed_config.toml + requirements.toml from server (deployment key only). +if [ -n "$GROK_DEPLOYMENT_KEY" ]; then + PROXY_URL="${GROK_PROXY_URL:-https://cli-chat-proxy.grok.com/v1}" + echo " Fetching deployment config..." >&2 + DEPLOY_RESPONSE=$(curl -sS -f \ + -H "Authorization: Bearer ${GROK_DEPLOYMENT_KEY}" \ + "${PROXY_URL}/deployment/config" 2>/dev/null) || DEPLOY_RESPONSE="" + if [ -z "$DEPLOY_RESPONSE" ]; then + echo " Warning: failed to fetch deployment config from ${PROXY_URL}/deployment/config" >&2 + fi + if [ -n "$DEPLOY_RESPONSE" ]; then + MANAGED_CONFIG=$(json_get "$DEPLOY_RESPONSE" "managed_config") + REQUIREMENTS=$(json_get "$DEPLOY_RESPONSE" "requirements") + if [ -n "$MANAGED_CONFIG" ] && [ "$MANAGED_CONFIG" != "null" ]; then + printf '%s\n' "$MANAGED_CONFIG" > "$HOME/.grok/managed_config.toml" + echo " Managed config applied." >&2 + else + rm -f "$HOME/.grok/managed_config.toml" + fi + if [ -n "$REQUIREMENTS" ] && [ "$REQUIREMENTS" != "null" ]; then + printf '%s\n' "$REQUIREMENTS" > "$HOME/.grok/requirements.toml" + echo " Requirements applied." >&2 + else + rm -f "$HOME/.grok/requirements.toml" + fi + fi +fi + +if [ "$os" = "windows" ]; then + echo "Grok $version installed to $BIN_DIR/grok.exe" >&2 +else + echo "Grok $version installed to $BIN_DIR/grok" >&2 +fi + +# --- Ensure grok is on PATH --- + +path_has_dir() { + case ":$PATH:" in *":$1:"*) return 0 ;; *) return 1 ;; esac +} + +# Try to symlink into a directory already on PATH so grok works immediately +# without restarting the shell. Candidate dirs in preference order. +SYMLINK_CREATED="" +if [ "$os" != "windows" ] && ! path_has_dir "$BIN_DIR"; then + for candidate in "$HOME/.local/bin" "/usr/local/bin"; do + if path_has_dir "$candidate" && [ -d "$candidate" ] && [ -w "$candidate" ]; then + ln -sf "$BIN_DIR/grok" "$candidate/grok" + SYMLINK_CREATED="$candidate" + echo " Symlinked $candidate/grok -> $BIN_DIR/grok" >&2 + break + fi + done +fi + + +echo "" >&2 +if path_has_dir "$BIN_DIR" || [ -n "$SYMLINK_CREATED" ]; then + echo "Run 'grok' to get started!" >&2 +elif [ -n "$config_file" ]; then + echo "Restart your terminal, then run 'grok' to get started!" >&2 +else + echo "Add $BIN_DIR to your PATH, then run 'grok' to get started:" >&2 + echo ' export PATH="$HOME/.grok/bin:$PATH"' >&2 +fi + +if [ "$os" = "windows" ]; then + echo "To use grok from cmd.exe or PowerShell, add %USERPROFILE%\\.grok\\bin to your PATH." >&2 +fi |
