From 13cc065ec014b86369dfe4ad60d1a43fa06fd251 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Thu, 17 Sep 2026 19:45:23 +0100 Subject: dotfiles Shell, editor and terminal configuration, the tools that go with them, and szecret, an age-based secret manager whose store lives elsewhere. History restarts here. What came before was not written to be read by anyone else, and carried machine names, addresses and an encrypted secret store; it is kept privately instead. What is left is meant to be safe on any machine, including one whose owner is not me: see CLAUDE.md for the rule it is written to. --- extra/install-claude.sh | 165 +++++++++++++++++++++++ extra/install-cursor.sh | 151 +++++++++++++++++++++ extra/install-kimi.sh | 185 +++++++++++++++++++++++++ extra/install-xai.sh | 349 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 850 insertions(+) create mode 100644 extra/install-claude.sh create mode 100644 extra/install-cursor.sh create mode 100644 extra/install-kimi.sh create mode 100644 extra/install-xai.sh (limited to 'extra') 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= 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 -- cgit v1.2.3