#!/usr/bin/env bash # Gemini CLI — installeur infrawire.sh # curl -fsSL https://infrawire.sh/i/gemini-cli | bash # curl -fsSL https://infrawire.sh/i/gemini-cli | bash -s -- --api-key AIza... # # Paquet npm officiel @google/gemini-cli, installé dans ~/.local de # l'utilisateur cible (pas de npm -g en root). Node.js officiel si absent ou # trop ancien. La clé API éventuelle va dans ~/.gemini/.env (mode 600). set -euo pipefail IW_APP_SLUG=gemini-cli IW_STEPS=5 # --- infrawire.sh : bibliothèque commune (insérée dans chaque installeur par build.py) --- IW_SITE="https://infrawire.sh" IW_BRAND_URL="https://infrawire.net" IW_LOG="${IW_LOG:-/tmp/infrawire-${IW_APP_SLUG:-install}-$(date +%Y%m%d-%H%M%S).log}" : >"$IW_LOG" 2>/dev/null || IW_LOG="/dev/null" # Couleurs : seulement sur un vrai terminal, et jamais avec NO_COLOR. if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then C_BRAND=$'\033[38;2;55;93;168m'; C_ACCENT=$'\033[38;2;143;166;227m' else C_BRAND=$'\033[38;5;25m'; C_ACCENT=$'\033[38;5;111m' fi C_OK=$'\033[38;5;42m'; C_WARN=$'\033[38;5;214m'; C_ERR=$'\033[38;5;203m' C_DIM=$'\033[2m'; C_BOLD=$'\033[1m'; C_RST=$'\033[0m' IW_TTY_OUT=1 else C_BRAND=; C_ACCENT=; C_OK=; C_WARN=; C_ERR=; C_DIM=; C_BOLD=; C_RST= IW_TTY_OUT=0 fi iw_log() { printf '%s\n' "$*" >>"$IW_LOG" 2>/dev/null || true; } iw_info() { printf ' %s•%s %s\n' "$C_ACCENT" "$C_RST" "$*"; iw_log "[info] $*"; } iw_ok() { printf ' %s✔%s %s\n' "$C_OK" "$C_RST" "$*"; iw_log "[ok] $*"; } iw_warn() { printf ' %s!%s %s\n' "$C_WARN" "$C_RST" "$*"; iw_log "[warn] $*"; } iw_die() { printf '\n %s✖ %s%s\n' "$C_ERR" "$*" "$C_RST" >&2 iw_log "[error] $*" if [ "$IW_LOG" != "/dev/null" ]; then printf ' %sFull log: %s%s\n' "$C_DIM" "$IW_LOG" "$C_RST" >&2 fi printf ' %sNeed help? %s/contact%s\n\n' "$C_DIM" "$IW_BRAND_URL" "$C_RST" >&2 exit 1 } IW_STEP=0 IW_STEPS=${IW_STEPS:-5} iw_step() { IW_STEP=$((IW_STEP + 1)) printf '\n%s[%d/%d]%s %s%s%s\n' "$C_BRAND$C_BOLD" "$IW_STEP" "$IW_STEPS" "$C_RST" "$C_BOLD" "$*" "$C_RST" iw_log "=== [$IW_STEP/$IW_STEPS] $*" } # iw_run "Libellé" cmd args… : sortie dans le journal, spinner à l'écran, # 20 dernières lignes du journal affichées en cas d'échec. iw_run() { local label=$1; shift iw_log "\$ $*" if [ "$IW_TTY_OUT" = 1 ]; then "$@" >>"$IW_LOG" 2>&1 /dev/null; do printf '\r %s%s%s %s' "$C_ACCENT" "${frames:i++%10:1}" "$C_RST" "$label" sleep 0.1 done local rc=0 wait "$pid" || rc=$? printf '\r\033[K' else local rc=0 "$@" >>"$IW_LOG" 2>&1 $IW_BRAND_URL/vps" "$C_ACCENT$C_BOLD" printf ' %s└%s┘%s\n\n' "$C_BRAND" "$rule" "$C_RST" } # --- Système --- IW_SUDO="" iw_detect_privileges() { if [ "$(id -u)" -eq 0 ]; then IW_SUDO="" elif command -v sudo >/dev/null 2>&1; then IW_SUDO="sudo" else IW_SUDO="none" fi } iw_as_root() { if [ -z "$IW_SUDO" ]; then "$@" elif [ "$IW_SUDO" = "sudo" ]; then sudo "$@" else return 1 fi } IW_OS_NAME="Linux"; IW_OS_ID=""; IW_OS_LIKE=""; IW_PM="" iw_detect_os() { [ "$(uname -s)" = "Linux" ] || iw_die "This installer supports Linux only (detected: $(uname -s))." if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release IW_OS_NAME=${PRETTY_NAME:-${NAME:-Linux}} IW_OS_ID=${ID:-} IW_OS_LIKE=${ID_LIKE:-} fi for pm in apt-get dnf yum zypper pacman apk; do if command -v "$pm" >/dev/null 2>&1; then IW_PM=$pm; break; fi done IW_ARCH=$(uname -m) case "$IW_ARCH" in x86_64|amd64) IW_ARCH=x64 ;; aarch64|arm64) IW_ARCH=arm64 ;; *) iw_die "Unsupported CPU architecture: $IW_ARCH (x86_64 and arm64 are supported)." ;; esac IW_LIBC=glibc 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 IW_LIBC=musl fi IW_MEM_MB=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0) } IW_PM_UPDATED=0 iw_pkg_update() { [ "$IW_PM_UPDATED" = 1 ] && return 0 IW_PM_UPDATED=1 case "$IW_PM" in apt-get) iw_as_root env DEBIAN_FRONTEND=noninteractive apt-get update -y ;; zypper) iw_as_root zypper --non-interactive refresh ;; pacman) iw_as_root pacman -Sy --noconfirm ;; apk) iw_as_root apk update ;; *) return 0 ;; esac } iw_pkg_install() { [ $# -gt 0 ] || return 0 case "$IW_PM" in apt-get) iw_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends "$@" ;; dnf) iw_as_root dnf install -y -q "$@" ;; yum) iw_as_root yum install -y -q "$@" ;; zypper) iw_as_root zypper --non-interactive install --no-recommends "$@" ;; pacman) iw_as_root pacman -S --noconfirm --needed "$@" ;; apk) iw_as_root apk add --no-cache "$@" ;; *) return 1 ;; esac } # Paquets de base : seulement ceux qui manquent (Rocky/Alma minimal ont curl-minimal, # qui entre en conflit avec le paquet curl si on le demande quand même). iw_ensure_base_packages() { local missing=() pkgs=() cmd for cmd in curl git tar gzip xz ps; do command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") done [ -s /etc/ssl/certs/ca-certificates.crt ] || [ -s /etc/pki/tls/certs/ca-bundle.crt ] || [ -s /etc/ssl/ca-bundle.pem ] || missing+=(certs) if [ ${#missing[@]} -eq 0 ]; then iw_ok "Base packages already present (curl, git, tar, gzip, xz)" return 0 fi [ "$IW_SUDO" != "none" ] || iw_die "Missing: ${missing[*]}. Run as root or install sudo, then retry." [ -n "$IW_PM" ] || iw_die "No supported package manager found. Install manually: ${missing[*]}" for cmd in "${missing[@]}"; do case "$cmd:$IW_PM" in xz:apt-get) pkgs+=(xz-utils) ;; ps:dnf|ps:yum|ps:pacman) pkgs+=(procps-ng) ;; ps:*) pkgs+=(procps) ;; certs:*) pkgs+=(ca-certificates) ;; *) pkgs+=("$cmd") ;; esac done iw_run "Refreshing package index ($IW_PM)" iw_pkg_update || iw_die "Package index update failed." iw_run "Installing base packages (${pkgs[*]})" iw_pkg_install "${pkgs[@]}" || iw_die "Could not install base packages." } # Lit une réponse au clavier même quand le script arrive par `curl | bash`. iw_ask_yes() { local question=$1 default=${2:-y} answer="" if [ -n "${IW_YES:-}" ]; then return 0; fi if ! { : /dev/null; then [ "$default" = y ]; return; fi local hint="[Y/n]"; [ "$default" = y ] || hint="[y/N]" printf ' %s?%s %s %s ' "$C_ACCENT" "$C_RST" "$question" "$C_DIM$hint$C_RST" read -r answer /dev/null; } iw_home_of() { getent passwd "$1" 2>/dev/null | cut -d: -f6; } # Ajoute ~/.local/bin au PATH des shells de l'utilisateur, une seule fois. iw_ensure_local_bin_path() { local user=$1 home rc line='export PATH="$HOME/.local/bin:$PATH"' home=$(iw_home_of "$user"); [ -n "$home" ] || return 0 for rc in "$home/.bashrc" "$home/.profile" "$home/.zshrc"; do [ -f "$rc" ] || { [ "$rc" = "$home/.profile" ] || continue; } if ! grep -qs '\.local/bin' "$rc"; then printf '\n# Added by infrawire.sh\n%s\n' "$line" >>"$rc" [ "$(id -u)" -eq 0 ] && chown "$user": "$rc" 2>/dev/null || true fi done } iw_system_summary() { iw_info "System: ${IW_OS_NAME} (${IW_ARCH}, ${IW_LIBC})" iw_info "Package manager: ${IW_PM:-none}" iw_info "Memory: ${IW_MEM_MB} MB" iw_info "Log file: ${IW_LOG}" } # Sous sudo : demander le mot de passe une fois, au début, plutôt qu'au milieu d'un spinner. iw_sudo_warmup() { [ "$IW_SUDO" = sudo ] || return 0 iw_info "Some steps need root: sudo may ask for your password." if iw_has_tty; then sudo -v /dev/null || iw_die "sudo needs a password but no terminal is available: run as root."; fi } iw_need_root() { [ "$IW_SUDO" != none ] || iw_die "$1 needs root: run as root, or install sudo and retry." } # --- Utilisateur cible --- IW_USER=""; IW_HOME="" # Sous sudo, on installe pour l'utilisateur qui a lancé la commande, pas pour root. iw_resolve_user() { local user=${1:-} if [ -z "$user" ]; then if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then user=$SUDO_USER; else user=$(id -un); fi fi id "$user" >/dev/null 2>&1 || iw_die "User '$user' does not exist." if [ "$user" != "$(id -un)" ] && [ "$(id -u)" -ne 0 ]; then iw_die "Installing for another user ('$user') requires root: re-run with sudo." fi IW_USER=$user IW_HOME=$(iw_home_of "$user") [ -n "$IW_HOME" ] && [ -d "$IW_HOME" ] || iw_die "Home directory of '$user' not found." iw_info "Installing for user: ${C_BOLD}${IW_USER}${C_RST} (${IW_HOME})" } # iw_as_user [VAR=valeur…] cmd… : lance cmd en tant que IW_USER, depuis son # dossier personnel, avec HOME et un PATH qui voit ~/.local/bin et /usr/local/bin. iw_as_user() ( local -a envs=(HOME="$IW_HOME" USER="$IW_USER" LOGNAME="$IW_USER" PATH="$IW_HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin") while [ $# -gt 0 ] && [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do envs+=("$1"); shift; done cd "$IW_HOME" 2>/dev/null || cd / if [ "$IW_USER" = "$(id -un)" ]; then exec env "${envs[@]}" "$@" elif command -v runuser >/dev/null 2>&1; then exec runuser -u "$IW_USER" -- env "${envs[@]}" "$@" else exec sudo -u "$IW_USER" -H env "${envs[@]}" "$@" fi ) # Ajoute un dossier au PATH des shells de IW_USER (en plus de ~/.local/bin). iw_add_user_path() { local dir=$1 rc for rc in "$IW_HOME/.bashrc" "$IW_HOME/.profile" "$IW_HOME/.zshrc"; do [ -f "$rc" ] || { [ "$rc" = "$IW_HOME/.profile" ] || continue; } grep -qsF "$dir" "$rc" && continue printf '\n# Added by infrawire.sh\nexport PATH="%s:$PATH"\n' "$dir" >>"$rc" [ "$(id -u)" -eq 0 ] && chown "$IW_USER": "$rc" 2>/dev/null || true done } # --- Node.js --- # Node.js officiel dans /usr/local, somme SHA-256 vérifiée : nodejs.org en glibc, # unofficial-builds.nodejs.org en musl (Alpine). iw_node_tarball() { local major=$1 base="https://nodejs.org/dist/latest-v$1.x" flavor="linux-${IW_ARCH}" tmp file sum if [ "$IW_LIBC" = musl ]; then local root="https://unofficial-builds.nodejs.org/download/release" v flavor="linux-${IW_ARCH}-musl" v=$(curl -fsSL --proto '=https' "$root/index.tab" | awk -v m="v${major}." -v f="$flavor" 'index($1, m) == 1 && index($3, f) && !found { print $1; found = 1 }') [ -n "$v" ] || { echo "no musl Node.js ${major} build for ${IW_ARCH}"; return 1; } base="$root/$v" iw_pkg_install libstdc++ libgcc fi tmp=$(mktemp -d /tmp/iw-node.XXXXXX) curl -fsSL --proto '=https' -o "$tmp/SHASUMS256.txt" "$base/SHASUMS256.txt" file=$(grep -oE "node-v[0-9.]+-${flavor}\.tar\.xz" "$tmp/SHASUMS256.txt" | sed -n 1p) [ -n "$file" ] || { echo "no Node.js build for ${flavor}"; return 1; } curl -fsSL --proto '=https' -o "$tmp/$file" "$base/$file" sum=$(grep " $file\$" "$tmp/SHASUMS256.txt" | cut -d' ' -f1) echo "$sum $tmp/$file" | sha256sum -c - iw_as_root tar -xJf "$tmp/$file" -C /usr/local --strip-components=1 --no-same-owner \ --exclude='*/CHANGELOG.md' --exclude='*/LICENSE' --exclude='*/README.md' rm -rf "$tmp" hash -r } # Version majeure du Node.js que verra l'utilisateur (/usr/local/bin d'abord). iw_node_major() { local v v=$(PATH="/usr/local/bin:$PATH" node --version 2>/dev/null) || return 0 v=${v#v}; echo "${v%%.*}" } # iw_ensure_node MIN [MAJEURE] : installe Node.js MAJEURE (24 par défaut) si absent ou < MIN. iw_ensure_node() { local min=$1 major=${2:-24} have have=$(iw_node_major) if [ -n "$have" ] && [ "$have" -ge "$min" ]; then iw_ok "Node.js $(PATH="/usr/local/bin:$PATH" node --version) already installed" return 0 fi iw_need_root "Installing Node.js" [ -n "$have" ] && iw_info "Node.js v$have is too old (needs ${min} or newer)" iw_run "Installing official Node.js ${major} LTS into /usr/local (checksum verified)" iw_node_tarball "$major" \ || iw_die "Node.js installation failed." iw_ok "Node.js $(/usr/local/bin/node --version) ready" } # Paquet npm installé pour IW_USER dans ~/.local : ni root, ni conflit avec la distribution. iw_npm_user_install() { iw_as_user NPM_CONFIG_PREFIX="$IW_HOME/.local" NPM_CONFIG_UPDATE_NOTIFIER=false NPM_CONFIG_FUND=false \ npm install -g --no-audit --loglevel=error "$1" } # --- Secrets et fichiers de configuration --- # Secret alphanumérique. Pas de `tr | head` : head qui ferme le tube tue tr (SIGPIPE, pipefail). iw_secret() { local n=${1:-32} s="" while [ ${#s} -lt "$n" ]; do s+=$(head -c 256 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9'); done printf '%s' "${s:0:n}" } # Les .env sont écrits entre apostrophes (aucune interpolation de $ par Compose) : # une valeur ne peut donc contenir ni apostrophe ni retour à la ligne. iw_check_value() { case "$2" in *"'"*|*$'\n'*) iw_die "$1 cannot contain a single quote or a line break." ;; esac } # Relit une valeur d'un .env existant : une réinstallation garde ses secrets. iw_env_get() { iw_as_root test -f "$1" || return 0 iw_as_root sed -n "s/^$2='\(.*\)'\$/\1/p" "$1" | sed -n '$p' } # Ligne KEY='valeur' pour un .env lu par docker compose (apostrophes = littéral). iw_env_line() { printf "%s='%s'\n" "$1" "$2"; } # iw_write_root FICHIER MODE < contenu : écrit en root, sans fenêtre où le fichier serait lisible. iw_write_root() { iw_as_root sh -c 'umask 077; cat >"$1" && chmod "$2" "$1"' sh "$1" "$2" } iw_valid_domain() { [[ "$1" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]]; } iw_valid_email() { [[ "$1" =~ ^[^[:space:]@\']+@[^[:space:]@\']+\.[^[:space:]@\']+$ ]]; } iw_valid_port() { [[ "$1" =~ ^[0-9]{1,5}$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; } # --- Réseau --- iw_public_ip() { curl -4 -fsS --max-time 6 https://api.ipify.org 2>/dev/null || true; } # Avertit (sans bloquer) si le domaine ne pointe pas encore vers ce serveur. iw_check_dns() { local domain=$1 ip resolved ip=$(iw_public_ip) resolved=$(getent ahostsv4 "$domain" 2>/dev/null | awk 'NR == 1 { print $1 }') if [ -z "$resolved" ]; then iw_warn "$domain does not resolve yet. Add an A record pointing to ${ip:-this server}: HTTPS starts once it does." elif [ -n "$ip" ] && [ "$resolved" != "$ip" ]; then iw_warn "$domain points to $resolved, but this server is $ip. The HTTPS certificate will fail until DNS is fixed." else iw_ok "$domain points to this server (${resolved})" fi } # Port TCP déjà écouté ? (sans `ss | grep -q`, même piège SIGPIPE que plus haut) iw_port_busy() { if command -v ss >/dev/null 2>&1; then [ -n "$(ss -Hltn "sport = :$1" 2>/dev/null)" ] else (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null fi } # --- Docker --- iw_docker_ready() { iw_as_root docker info >/dev/null 2>&1; } iw_docker_from_get_docker() { local tmp rc=0 tmp=$(mktemp /tmp/get-docker.XXXXXX) curl -fsSL --proto '=https' -o "$tmp" https://get.docker.com || { rm -f "$tmp"; return 1; } iw_as_root sh "$tmp" || rc=$? rm -f "$tmp" return "$rc" } iw_docker_from_rhel_repo() { iw_as_root curl -fsSL --proto '=https' -o /etc/yum.repos.d/docker-ce.repo https://download.docker.com/linux/rhel/docker-ce.repo || return 1 iw_pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } # Dépôt officiel de Docker là où il existe, paquets de la distribution ailleurs. iw_docker_packages() { case "$IW_OS_ID" in ubuntu|debian|raspbian|fedora|centos) iw_docker_from_get_docker ;; rhel|rocky|almalinux|ol) iw_docker_from_rhel_repo ;; arch|archarm|manjaro|endeavouros) iw_pkg_update && iw_pkg_install docker docker-compose ;; opensuse*|sles) iw_pkg_update && iw_pkg_install docker docker-compose ;; alpine) iw_pkg_update && iw_pkg_install docker docker-cli-compose ;; *) case " $IW_OS_LIKE " in *" rhel "*|*" centos "*|*" fedora "*) iw_docker_from_rhel_repo ;; *" debian "*|*" ubuntu "*) iw_pkg_update && { iw_pkg_install docker.io docker-compose-v2 || iw_pkg_install docker.io docker-compose; } ;; *) echo "No Docker install recipe for '${IW_OS_ID:-unknown}': install Docker Engine and Compose, then re-run."; return 1 ;; esac ;; esac } iw_docker_start() { iw_docker_ready && return 0 if [ -d /run/systemd/system ]; then iw_as_root systemctl enable --now docker elif command -v rc-service >/dev/null 2>&1 && [ -d /run/openrc ]; then iw_as_root rc-update add docker default iw_as_root rc-service docker start else # Ni systemd ni OpenRC (conteneur, WSL…) : dockerd lancé directement. iw_as_root sh -c 'nohup dockerd >/var/log/dockerd.log 2>&1 &' fi local i for i in $(seq 1 45); do iw_docker_ready && return 0; sleep 1; done return 1 } iw_ensure_docker() { iw_need_root "Docker" if command -v docker >/dev/null 2>&1 && iw_as_root docker compose version >/dev/null 2>&1; then iw_ok "Docker $(docker --version | sed 's/^Docker version \([^,]*\).*/\1/') with Compose already installed" else iw_run "Installing Docker Engine and Compose (1–3 minutes)" iw_docker_packages || iw_die "Docker installation failed." iw_as_root docker compose version >/dev/null 2>&1 || iw_die "Docker Compose v2 is not available after installation." iw_ok "$(docker --version | sed 's/,.*//')" fi iw_run "Starting the Docker service" iw_docker_start || iw_die "The Docker daemon did not start (see the log)." } # iw_compose DOSSIER args… : docker compose sur le projet de DOSSIER (nom du projet = nom du dossier). iw_compose() { local dir=$1; shift iw_as_root docker compose --project-directory "$dir" -f "$dir/compose.yaml" "$@" } # Ports 80/443 libres pour Caddy, sauf s'ils sont déjà tenus par ce même projet (réinstallation). iw_check_web_ports() { local dir=$1 p [ -n "$(iw_compose "$dir" ps -q caddy 2>/dev/null)" ] && return 0 for p in 80 443; do iw_port_busy "$p" && iw_die "Port $p is already in use (another web server?). Stop it, or install without --domain and use your own reverse proxy." done return 0 } # Service Compose du proxy HTTPS : Caddy obtient et renouvelle seul le certificat Let's Encrypt. iw_caddy_service() { cat <<'EOF' caddy: image: caddy:2 restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config EOF } # iw_caddyfile DOMAINE EMAIL AMONT [directives…] iw_caddyfile() { local domain=$1 email=$2 upstream=$3; shift 3 if [ -n "$email" ]; then printf '{\n\temail %s\n}\n\n' "$email"; fi printf '%s {\n\tencode zstd gzip\n' "$domain" local line for line in "$@"; do printf '\t%s\n' "$line"; done printf '\treverse_proxy %s\n}\n' "$upstream" } # Attend qu'une commande réussisse (N secondes max). iw_wait_for() { local seconds=$1 i; shift for ((i = 0; i < seconds; i += 3)); do "$@" >/dev/null 2>&1 && return 0; sleep 3; done return 1 } # --- fin de la bibliothèque commune --- usage() { cat <<'EOF' Gemini CLI installer by infrawire.sh Usage: curl -fsSL https://infrawire.sh/i/gemini-cli | bash -s -- [options] Options: --api-key KEY Gemini API key from aistudio.google.com (saved to ~/.gemini/.env) --user NAME Install for this Linux user (default: you, or $SUDO_USER under sudo) --version VER latest | preview | nightly | x.y.z (default: latest) -y, --yes Never ask questions -h, --help Show this help EOF } main() { local target_user="" version=latest api_key="" while [ $# -gt 0 ]; do case "$1" in --api-key) api_key=${2:-}; shift 2 ;; --api-key=*) api_key=${1#*=}; shift ;; --user) target_user=${2:-}; shift 2 ;; --user=*) target_user=${1#*=}; shift ;; --version) version=${2:-}; shift 2 ;; --version=*) version=${1#*=}; shift ;; -y|--yes) IW_YES=1; shift ;; -h|--help) usage; exit 0 ;; *) iw_die "Unknown option: $1 (see --help)" ;; esac done [[ "$version" =~ ^(latest|preview|nightly|[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?)$ ]] \ || iw_die "Invalid --version: $version (latest, preview, nightly or x.y.z)" [ -z "$api_key" ] || [[ "$api_key" =~ ^[A-Za-z0-9_-]{20,}$ ]] || iw_die "Invalid --api-key format." iw_banner "Gemini CLI" iw_step "Checking your system" iw_detect_os iw_detect_privileges iw_system_summary iw_resolve_user "$target_user" iw_step "Installing Node.js" iw_ensure_base_packages [ "$IW_LIBC" != musl ] || iw_run "Installing musl runtime libraries" iw_pkg_install libgcc libstdc++ || true iw_ensure_node 20 iw_step "Installing Gemini CLI from npm" iw_run "npm install @google/gemini-cli@${version}" iw_npm_user_install "@google/gemini-cli@${version}" \ || iw_die "npm install failed (see the log above)." iw_ensure_local_bin_path "$IW_USER" iw_ok "$IW_HOME/.local/bin added to PATH (bash, zsh, profile)" iw_step "Configuring" if [ -n "$api_key" ]; then iw_as_user sh -c 'umask 077; mkdir -p "$HOME/.gemini"; f="$HOME/.gemini/.env"; touch "$f"; grep -v "^GEMINI_API_KEY=" "$f" >"$f.tmp" || true; printf "GEMINI_API_KEY=%s\n" "$1" >>"$f.tmp"; mv "$f.tmp" "$f"; chmod 600 "$f"' sh "$api_key" \ || iw_die "Could not write ~/.gemini/.env" iw_ok "API key saved to $IW_HOME/.gemini/.env (mode 600)" else iw_info "No --api-key given: Gemini CLI will offer Google login or a key on first run" fi iw_step "Verifying" local ver ver=$(iw_as_user gemini --version 2>>"$IW_LOG" | sed -n 1p) || true [ -n "$ver" ] || iw_die "gemini --version failed (see $IW_LOG)." iw_ok "gemini ${ver}" printf '\n %s%s✔ Gemini CLI is installed!%s\n\n' "$C_OK" "$C_BOLD" "$C_RST" printf ' %sNext steps%s\n' "$C_BOLD" "$C_RST" if [ "$IW_USER" != "$(id -un)" ]; then printf ' 1. Log in as %s%s%s, then open a new shell (or run: %ssource ~/.bashrc%s)\n' "$C_ACCENT" "$IW_USER" "$C_RST" "$C_ACCENT" "$C_RST" else printf ' 1. Open a new shell, or run: %ssource ~/.bashrc%s\n' "$C_ACCENT" "$C_RST" fi printf ' 2. Go to your project: %scd ~/my-project%s\n' "$C_ACCENT" "$C_RST" printf ' 3. Start Gemini CLI: %sgemini%s\n' "$C_ACCENT" "$C_RST" if [ -z "$api_key" ]; then printf ' %sOn a headless server, pick "Use Gemini API key" (free at aistudio.google.com/apikey)%s\n' "$C_DIM" "$C_RST" fi printf ' Docs: %shttps://github.com/google-gemini/gemini-cli%s · Guide: %s%s/gemini-cli%s\n' "$C_ACCENT" "$C_RST" "$C_ACCENT" "$IW_SITE" "$C_RST" iw_promo } main "$@"