#!/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/.grok/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 binary_tmp="${binary_path}.tmp.$$" rm -f "$binary_tmp" 2>/dev/null || true echo " Downloading grok ${version}..." >&2 if [ "$os" = "windows" ]; then if ! download_file_parallel "${artifact_base}.exe" "$binary_tmp"; then if ! download_file_parallel "$artifact_base" "$binary_tmp"; then rm -f "$binary_tmp" 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_tmp"; then rm -f "$binary_tmp" 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 mv -f "$binary_tmp" "$binary_path" # 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 agent.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 and $BIN_DIR/agent.exe." >&2 else chmod +x "$binary_tmp" if ! "$binary_tmp" --version /dev/null 2>&1; then echo "Error: downloaded grok failed to run; keeping the existing install." >&2 rm -f "$binary_tmp" exit 1 fi mv -f "$binary_tmp" "$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" ln -sf "$link_target" "$BIN_DIR/agent" echo " Binary linked to $BIN_DIR/grok and $BIN_DIR/agent." >&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" ln -sf "$BIN_DIR/agent" "$candidate/agent" SYMLINK_CREATED="$candidate" echo " Symlinked $candidate/grok -> $BIN_DIR/grok" >&2 echo " Symlinked $candidate/agent -> $BIN_DIR/agent" >&2 break fi done fi # Also update shell config so ~/.grok/bin is on PATH for future sessions user_shell="$(basename "${SHELL:-}")" config_file="" case "$user_shell" in bash) config_file="$HOME/.bashrc" ;; zsh) config_file="$HOME/.zshrc" ;; fish) config_file="$HOME/.config/fish/config.fish" ;; esac if [ -n "$config_file" ]; then mkdir -p "$(dirname "$config_file")" # Build the new installer block if [ "$user_shell" = "fish" ]; then new_block='# >>> grok installer >>> fish_add_path $HOME/.grok/bin # <<< grok installer <<<' elif [ "$user_shell" = "zsh" ]; then new_block='# >>> grok installer >>> export PATH="$HOME/.grok/bin:$PATH" fpath=(~/.grok/completions/zsh $fpath) autoload -Uz compinit && compinit -C # <<< grok installer <<<' else new_block='# >>> grok installer >>> export PATH="$HOME/.grok/bin:$PATH" [[ -r "$HOME/.grok/completions/bash/grok.bash" ]] && source "$HOME/.grok/completions/bash/grok.bash" # <<< grok installer <<<' fi if grep -qs "grok installer" "$config_file" 2>/dev/null; then # Replace existing block in-place (strip old >>> to <<< lines, insert new) tmp="$config_file.tmp.$$" awk ' /# >>> grok installer >>>/ { skip=1; next } /# <<< grok installer << "$tmp" && mv "$tmp" "$config_file" else [ -f "$config_file" ] && cp "$config_file" "$config_file.bak.$(date +%s)" # macOS bash: ensure bash_profile sources bashrc if [ "$user_shell" = "bash" ] && [ "$(uname -s)" = "Darwin" ]; then if [ -f "$HOME/.bash_profile" ] && ! grep -qs "source ~/.bashrc" "$HOME/.bash_profile"; then printf '\n[[ -r ~/.bashrc ]] && source ~/.bashrc\n' >> "$HOME/.bash_profile" fi fi fi printf '\n%s\n' "$new_block" >> "$config_file" echo " Updated $BIN_DIR in PATH in $config_file." >&2 fi echo "" >&2 if path_has_dir "$BIN_DIR" || [ -n "$SYMLINK_CREATED" ]; then echo "Run 'grok' or 'agent' to get started!" >&2 elif [ -n "$config_file" ]; then echo "Restart your terminal, then run 'grok' or 'agent' to get started!" >&2 else echo "Add $BIN_DIR to your PATH, then run 'grok' or 'agent' 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