summaryrefslogtreecommitdiff
path: root/extra/install-xai.sh
diff options
context:
space:
mode:
Diffstat (limited to 'extra/install-xai.sh')
-rw-r--r--extra/install-xai.sh349
1 files changed, 349 insertions, 0 deletions
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