Last active
July 19, 2026 17:22
-
-
Save Mikaayenson/3f05db683761ba604bec1575607dce0c to your computer and use it in GitHub Desktop.
Cross-platform GenAI tool installer v1.8.3 — put npm global bin on PATH. macOS/Linux/Windows/WSL.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # install-genai-tools.sh — Cross-platform installer for GenAI coding tools (LOL-GAI). | |
| # | |
| # Installs GenAI assistants, coding agents, and local model servers so you | |
| # can validate process telemetry, network artifacts, config paths, and file | |
| # behavior against detection rules. | |
| # | |
| # Platforms: macOS (Intel + Apple Silicon), Linux (x86_64 + arm64), | |
| # Windows x64 + ARM64 (Git Bash / WSL / MSYS2) | |
| # | |
| # Empirically verified on: | |
| # - macOS 15.7.5 (Apple Silicon + Intel) | |
| # - Ubuntu 22.04 x86_64 + Ubuntu 24.04 ARM64 | |
| # - Amazon Linux 2023 x86_64 (AL2023, dnf-based) | |
| # - Windows 11 Pro 10.0.26100 (ARM64) | |
| # - Windows Server 2022 Datacenter x64 (winget bootstrapped at runtime) | |
| # | |
| # Catalog IDs align with LOL-GAI yaml/**/*.yaml (123 tools). Legacy | |
| # installer aliases remain for backward compatibility. | |
| # | |
| # Usage: | |
| # curl -sSL <gist-raw-url> | bash -s -- --list | |
| # curl -sSL <gist-raw-url> | bash -s -- --tool claude-code | |
| # curl -sSL <gist-raw-url> | bash -s -- --cli | |
| # curl -sSL <gist-raw-url> | bash -s -- --all | |
| # | |
| # # Or run locally: | |
| # bash install-genai-tools.sh --list | |
| # bash install-genai-tools.sh --check | |
| # bash install-genai-tools.sh --tool cursor | |
| # bash install-genai-tools.sh --desktop --dry-run | |
| # bash install-genai-tools.sh --all | |
| # | |
| # Options: | |
| # --tool <name> Install a single tool by name (use --list to see names) | |
| # --all Install everything | |
| # --cli Install CLI agents only | |
| # --desktop Install desktop/IDE apps only | |
| # --local Install local model servers only | |
| # --vscode Install VS Code extensions only | |
| # --mcp Install MCP servers only | |
| # --research Clone research / OSS self-host daemons (bolt.diy, openclaw, nemoclaw) | |
| # --list List all supported tools and exit | |
| # --check Show what's already installed and exit | |
| # --dry-run Show what would be installed without doing it | |
| # --no-color Disable colored output | |
| # --yes Skip confirmation prompts | |
| # -h, --help Show this help | |
| # | |
| # Package managers (auto-detected): | |
| # macOS: Homebrew | |
| # Linux: apt/dnf/pacman + npm/pip | |
| # Windows: winget, scoop, or npm/pip (via Git Bash, WSL, or MSYS2) | |
| # All: npm, pip/pipx/uv (for CLI agents) | |
| # | |
| # Known platform blockers (empirically verified, 2026-04): | |
| # - Windows ARM64: aider, openhands → require Rust + MSVC build tools | |
| # (no prebuilt wheels for hf-xet, grpcio, tiktoken). | |
| # - Windows ARM64: zed → no native Windows ARM64 build. | |
| # - Windows ARM64: bito → x64-only download URL; skip on ARM64. | |
| # - Windows ARM64: koboldcpp, textgen-webui, tabby → no ARM64 binaries. | |
| # - macOS x64: junie, koboldcpp, tabby → Apple Silicon only. | |
| # - Linux ARM64: lm-studio, koboldcpp, llama.cpp prebuilt → x64 only. | |
| # - Linux RPM: replit-desktop, zenflow → vendor ships .deb only. | |
| # - Linux: windsurf, claude-desktop, cursor (deb path), trae, | |
| # pearai → no Linux build from vendor. | |
| # - Linux: ollama, lm-studio installers need sudo NOPASSWD or | |
| # SUDO_PASS env var when run from non-tty SSH. | |
| # | |
| # Setting SUDO_PASS in the environment lets the Linux installers (Ollama, | |
| # LM Studio, .deb / .rpm package installs) work over non-interactive SSH: | |
| # SUDO_PASS=hunter2 bash install-genai-tools.sh --all --yes | |
| # | |
| # Author: Mika Ayenson (@Mikaayenson) | |
| # ────────────────────────────────────────────────────────────────────── | |
| set -euo pipefail | |
| # ── globals ────────────────────────────────────────────────────────── | |
| # 1.6.0 — cross-platform install hardening (see PATCHES.md): | |
| # * mktempd helper (fixes Linux "too few X's in template") | |
| # * detect_pkg_mgr + install_local_package → apt / dnf / yum support | |
| # * _sudo + _run_with_sudo_askpass → non-tty SSH installs work | |
| # * win_run_silent (PowerShell Start-Process -Wait) for Windows installers | |
| # * Cursor JSON metadata parser (downloadUrl) + Linux AppImage path | |
| # * Windsurf / VSCodium / Ollama / LM Studio winget-pkgs manifest fallback | |
| # * github-mcp-server prebuilt-binary path (no Docker / Go required) | |
| # * has_npm_bin → recognize npm globals not yet on PATH (factory-droid, cn) | |
| # * install_via_winget_manifest for Server 2022 (catalog-broken hosts) | |
| # * ensure_winget_windows + update_vscode_if_old auto-bootstrap | |
| # * libatomic dep for LM Studio AppImage on AL2023 | |
| # * GitHub Copilot Chat conflict tolerated when bundled with VS Code | |
| # 1.6.1 — add Google Antigravity (desktop): brew --cask antigravity / winget Google.Antigravity; | |
| # registry parity check via Antigravity.app or `agy` CLI; Linux → vendor download URL. | |
| # 1.6.2 — add antigravity-cli (agy) via official curl/irm installers; | |
| # desktop google-antigravity unchanged; --cli now includes agy. | |
| # 1.7.0 — 2026 landscape refresh: | |
| # 1.7.1 — cross-OS install hardening: | |
| # 1.9.1 — Windows SSH reliability: | |
| # * tool registry field parsing without cut (Git Bash spawn storm fix) | |
| # * win_ps_file / win_run_silent via temp .ps1 (-File, not inline -Command) | |
| # * auto --yes + --no-color on non-TTY; per-check timeout on Windows --check | |
| # * --log-file for non-interactive install progress | |
| # * WoA detection via uname node (Git Bash Prism reports x86_64) | |
| # * VS Code extension list cached for --check | |
| # * documented Git Bash invocation (--noprofile --norc, redirect to file) | |
| # 1.9.0 — LOL-GAI catalog parity (123 YAML Ids) + browser-use | |
| # 1.9.2 — major vendor surface refresh: | |
| # * google-antigravity: Surfaces (desktop IDE vs standalone agy CLI); fix CLI install check | |
| # * codex-openai: merge ChatGPT desktop (com.openai.codex) with Codex CLI | |
| # * kiro: merge Kiro IDE + CLI surfaces; add install_kiro_ide | |
| # * sync/enrich guards for google-antigravity, codex-openai, kiro manual catalogs | |
| # 1.8.3 — ensure npm global bin dir on PATH after npm installs | |
| # 1.8.2 — exit non-zero when installs fail; harden python tool installs | |
| # 1.8.1 — fix check_cursor_agent missing; uv tool install --force | |
| # 1.8.0 — endpoint-scope expansion: ~100 terminal GenAI binaries | |
| # (coding agents, chat CLIs, local inference, MCP, speech, image, cloud CLIs) | |
| # + generic install_python_tool / install_npm_tool / install_go_tool helpers | |
| # + categories: chat, speech, image, cloud; --mcp/--research filter fix | |
| # 1.7.2 — define missing native_cli_os / skip_unsupported / ensure_windows_user_bins_on_path; | |
| # WSL: detect_pkg_mgr + _sudo + has_npm_bin + npm_install_g use Linux paths | |
| # * IS_WSL + native_cli_os() so WSL uses Linux curl/npm paths for CLIs | |
| # * install_* exit 2 = unsupported platform (skip, not fail) | |
| # * Warp Linux: apt/dnf repo or AppImage (not open_url stub) | |
| # * Crush: prefer winget on Windows before go/npm | |
| # * antigravity/cursor-agent: PATH refresh + post-install verify | |
| # * pi: npm_install_g --ignore-scripts; plandex/hermes work in WSL | |
| # * cursor-agent CLI (curl cursor.com/install → agent/cursor-agent) | |
| # * cline-cli standalone (npm i -g cline) distinct from VS Code ext | |
| # * warp agentic terminal (brew cask / winget Warp.Warp) | |
| # * pi coding agent (@earendil-works/pi-coding-agent) | |
| # * plandex multi-step CLI (plandex.ai/install.sh) | |
| # * opencode: un-archive — anomalyco/opencode is active (crush stays Charm) | |
| # * gemini-cli notes: consumer sunset → migrate to antigravity-cli | |
| # * windsurf notes: Cognition rebrand to Devin Desktop (OTA); keep windsurf id | |
| VERSION="1.9.2" | |
| PLATFORM="" # darwin, linux, windows | |
| IS_WSL=false # true when running under WSL (PLATFORM may still be windows) | |
| UNSUPPORTED_COUNT=0 | |
| ARCH="$(uname -m)" | |
| ARCH_KIND="" # x64, arm64 | |
| DRY_RUN=false | |
| YES=false | |
| NO_COLOR=false | |
| LOG_FILE="" | |
| WIN_CHECK_TIMEOUT="${WIN_CHECK_TIMEOUT:-8}" | |
| INSTALLED_COUNT=0 | |
| SKIPPED_COUNT=0 | |
| FAILED_COUNT=0 | |
| # Write ~/.zshenv once so zsh finds Homebrew on non-login shells (SSH | |
| # `user@host cmd`, IDE tasks, CI). ~/.zshenv is sourced for every zsh; | |
| # ~/.zprofile is skipped for non-login shells — this was breaking PATH on | |
| # Apple Silicon test VMs when brew lived under /opt/homebrew/bin. | |
| ensure_macos_homebrew_path_in_zshenv() { | |
| [[ "${PLATFORM:-}" != "darwin" ]] && return 0 | |
| local zshenv="${HOME}/.zshenv" | |
| local marker="# BEGIN install-genai-tools.sh Homebrew PATH" | |
| touch "$zshenv" | |
| if grep -qF "$marker" "$zshenv" 2>/dev/null; then | |
| return 0 | |
| fi | |
| { | |
| echo "" | |
| echo "$marker" | |
| echo "# Non-login zsh skips ~/.zprofile; keep CLI tools on PATH for SSH/automation." | |
| echo "if [[ -x /opt/homebrew/bin/brew ]]; then" | |
| echo " export PATH=\"/opt/homebrew/bin:/opt/homebrew/sbin:\${PATH}\"" | |
| echo "elif [[ -x /usr/local/bin/brew ]]; then" | |
| echo " export PATH=\"/usr/local/bin:/usr/local/sbin:\${PATH}\"" | |
| echo "fi" | |
| echo "# END install-genai-tools.sh Homebrew PATH" | |
| } >>"$zshenv" | |
| } | |
| detect_platform() { | |
| case "$(uname -s)" in | |
| Darwin) PLATFORM="darwin" ;; | |
| Linux) | |
| if grep -qi microsoft /proc/version 2>/dev/null; then | |
| # WSL: keep PLATFORM=windows so winget/desktop targets still work, | |
| # but set IS_WSL so CLI curl/npm installers use Linux paths. | |
| PLATFORM="windows" | |
| IS_WSL=true | |
| else | |
| PLATFORM="linux" | |
| fi | |
| ;; | |
| MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;; | |
| *) PLATFORM="linux" ;; | |
| esac | |
| case "$ARCH" in | |
| x86_64|amd64) ARCH_KIND="x64" ;; | |
| arm64|aarch64|arm64e) ARCH_KIND="arm64" ;; | |
| *) ARCH_KIND="$ARCH" ;; | |
| esac | |
| # Git Bash on Windows ARM64 often runs as x64 Prism (uname -m=x86_64, | |
| # PROCESSOR_ARCHITECTURE=AMD64). Detect WoA from the uname node suffix. | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| if uname -a 2>/dev/null | grep -qi 'ARM64'; then | |
| ARCH="arm64" | |
| ARCH_KIND="arm64" | |
| fi | |
| fi | |
| # Suppress macOS Gatekeeper "Are you sure you want to open this app | |
| # downloaded from the Internet?" prompts for any cask we install. This sets | |
| # the option for the duration of this script only — it does not modify the | |
| # user's shell rc files or persistent brew config. | |
| if [[ "$PLATFORM" == "darwin" ]]; then | |
| case "${HOMEBREW_CASK_OPTS:-}" in | |
| *--no-quarantine*) ;; | |
| *) export HOMEBREW_CASK_OPTS="--no-quarantine ${HOMEBREW_CASK_OPTS:-}" ;; | |
| esac | |
| # Non-interactive SSH sessions usually have a minimal PATH that excludes | |
| # /opt/homebrew/bin (Apple Silicon) and /usr/local/bin (Intel). When the | |
| # installer is driven over SSH we still want to find brew automatically | |
| # so cask-based installs don't fall back to "Download from..." stubs. | |
| for brew_bin in /opt/homebrew/bin /usr/local/bin; do | |
| if [[ -x "$brew_bin/brew" ]]; then | |
| case ":$PATH:" in | |
| *":$brew_bin:"*) ;; | |
| *) export PATH="$brew_bin:$PATH" ;; | |
| esac | |
| fi | |
| done | |
| ensure_macos_homebrew_path_in_zshenv | |
| fi | |
| # Initialize the package-manager handle on Linux so install_local_package | |
| # and other helpers don't have to redetect on every call. | |
| detect_pkg_mgr | |
| } | |
| is_windows_arm64() { | |
| [[ "$PLATFORM" == "windows" && "$ARCH_KIND" == "arm64" ]] | |
| } | |
| # OS for CLI curl/npm installers. WSL keeps PLATFORM=windows for winget/desktop | |
| # targeting, but CLIs must follow the Linux userland. | |
| native_cli_os() { | |
| if [[ "$IS_WSL" == true ]]; then | |
| echo "linux" | |
| return 0 | |
| fi | |
| echo "${PLATFORM:-linux}" | |
| } | |
| # Mark a tool as unsupported on this host. Callers should `return 2` so | |
| # do_install() increments UNSUPPORTED_COUNT instead of FAILED_COUNT. | |
| skip_unsupported() { | |
| warn "${1:-unsupported on this platform}" | |
| } | |
| # Refresh PATH after Windows (Git Bash / MSYS) installs so newly dropped | |
| # binaries are visible in the current shell. On WSL/Linux/macOS this falls | |
| # through to ~/.local/bin. | |
| ensure_windows_user_bins_on_path() { | |
| if [[ "$IS_WSL" == true ]]; then | |
| ensure_user_local_bin_on_path | |
| return 0 | |
| fi | |
| [[ "$PLATFORM" == "windows" ]] || return 0 | |
| local candidates=() | |
| # npm globals (winget/Node default) | |
| if has_cmd npm; then | |
| local pfx; pfx="$(npm config get prefix 2>/dev/null || true)" | |
| [[ -n "$pfx" ]] && candidates+=("$pfx") | |
| fi | |
| # Common user / WinGet / Cursor locations (Git Bash POSIX paths) | |
| local localappdata="${LOCALAPPDATA:-}" | |
| local appdata="${APPDATA:-}" | |
| local userprofile="${USERPROFILE:-$HOME}" | |
| # Translate Windows env if we only have cygpath-friendly HOME | |
| if [[ -z "$localappdata" ]] && has_cmd cygpath; then | |
| localappdata="$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || true)" | |
| fi | |
| if [[ -z "$appdata" ]] && has_cmd cygpath; then | |
| appdata="$(cygpath -u "$APPDATA" 2>/dev/null || true)" | |
| fi | |
| [[ -n "$localappdata" ]] && candidates+=( | |
| "$localappdata/Microsoft/WinGet/Links" | |
| "$localappdata/Programs/cursor/resources/app/bin" | |
| "$localappdata/cursor-agent" | |
| "$localappdata/Programs/antigravity" | |
| ) | |
| [[ -n "$appdata" ]] && candidates+=("$appdata/npm") | |
| candidates+=( | |
| "$userprofile/.local/bin" | |
| "$HOME/.local/bin" | |
| "$HOME/AppData/Local/Microsoft/WinGet/Links" | |
| "$HOME/AppData/Roaming/npm" | |
| ) | |
| local d | |
| for d in "${candidates[@]}"; do | |
| [[ -d "$d" ]] || continue | |
| case ":$PATH:" in | |
| *":$d:"*) ;; | |
| *) export PATH="$d:$PATH" ;; | |
| esac | |
| done | |
| } | |
| # ── colors ─────────────────────────────────────────────────────────── | |
| setup_colors() { | |
| if [[ "$NO_COLOR" == true ]] || [[ ! -t 1 ]]; then | |
| RED="" GRN="" YLW="" BLU="" MAG="" CYN="" BLD="" DIM="" RST="" | |
| else | |
| RED=$'\033[0;31m' GRN=$'\033[0;32m' YLW=$'\033[0;33m' | |
| BLU=$'\033[0;34m' MAG=$'\033[0;35m' CYN=$'\033[0;36m' | |
| BLD=$'\033[1m' DIM=$'\033[2m' RST=$'\033[0m' | |
| fi | |
| } | |
| info() { echo "${BLU}[*]${RST} $*"; } | |
| ok() { echo "${GRN}[+]${RST} $*"; } | |
| warn() { echo "${YLW}[!]${RST} $*"; } | |
| err() { echo "${RED}[-]${RST} $*" >&2; } | |
| step() { echo "${MAG}==> ${BLD}$*${RST}"; } | |
| dim() { echo "${DIM} $*${RST}"; } | |
| # ── tool registry ──────────────────────────────────────────────────── | |
| declare -a TOOLS=() | |
| register_tool() { | |
| TOOLS+=("$1|$2|$3|$4|$5") | |
| } | |
| # Parse TOOLS entries without spawning cut (Git Bash on Windows is ~300ms/cut; | |
| # 166 tools × 4 fields ≈ 3+ minutes for --list/--check over SSH). | |
| tool_name() { | |
| local e="$1" | |
| echo "${e%%|*}" | |
| } | |
| tool_category() { | |
| local e="$1" | |
| e="${e#*|}" | |
| echo "${e%%|*}" | |
| } | |
| tool_check() { | |
| local e="$1" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| echo "${e%%|*}" | |
| } | |
| tool_func() { | |
| local e="$1" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| echo "${e%%|*}" | |
| } | |
| tool_desc() { | |
| local e="$1" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| e="${e#*|}" | |
| echo "${e#*|}" | |
| } | |
| # ── prerequisite checks ───────────────────────────────────────────── | |
| has_cmd() { command -v "$1" &>/dev/null; } | |
| has_brew() { has_cmd brew; } | |
| has_npm() { has_cmd npm; } | |
| has_pip() { has_cmd pip3 || has_cmd pip; } | |
| has_code() { has_cmd code || has_cmd cursor || has_cmd codium || [[ -x "/Applications/Cursor.app/Contents/Resources/app/bin/cursor" ]]; } | |
| has_winget() { has_cmd winget; } | |
| has_scoop() { has_cmd scoop; } | |
| pip_cmd() { | |
| if has_cmd pip3; then echo pip3; elif has_cmd pip; then echo pip; else echo pip3; fi | |
| } | |
| open_url() { | |
| local url="$1" | |
| case "$PLATFORM" in | |
| darwin) open "$url" 2>/dev/null || true ;; | |
| windows) cmd.exe /c start "" "$url" 2>/dev/null || explorer.exe "$url" 2>/dev/null || true ;; | |
| linux) xdg-open "$url" 2>/dev/null || true ;; | |
| esac | |
| } | |
| check_prerequisites() { | |
| local missing=() | |
| has_cmd curl || missing+=(curl) | |
| has_cmd git || missing+=(git) | |
| if [[ ${#missing[@]} -gt 0 ]]; then | |
| err "Missing required tools: ${missing[*]}" | |
| err "Install them first, then re-run." | |
| exit 1 | |
| fi | |
| } | |
| ensure_brew() { | |
| if [[ "$PLATFORM" == "darwin" ]] && ! has_brew; then | |
| warn "Homebrew not found. Many tools require it on macOS." | |
| warn "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| ensure_npm() { | |
| if has_npm; then ensure_npm_global_bin_on_path; return 0; fi | |
| if has_brew; then | |
| warn "npm not found. Installing via Homebrew..." | |
| brew install node | |
| elif [[ "$PLATFORM" == "windows" ]] && has_winget; then | |
| warn "npm not found. Installing Node.js via winget..." | |
| winget install -e --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements | |
| else | |
| warn "npm not found. Installing Node.js from official binaries..." | |
| install_node_binary || return 1 | |
| fi | |
| has_npm | |
| } | |
| install_node_binary() { | |
| local node_ver="v22.15.0" | |
| local arch | |
| arch="$(uname -m)" | |
| local os_part="" | |
| case "$(native_cli_os)" in | |
| darwin) os_part="darwin" ;; | |
| linux) os_part="linux" ;; | |
| *) err "No automatic Node.js install for $PLATFORM (native_cli_os=$(native_cli_os))"; return 1 ;; | |
| esac | |
| case "$arch" in | |
| x86_64) arch="x64" ;; | |
| aarch64|arm64) arch="arm64" ;; | |
| *) err "Unsupported architecture: $arch"; return 1 ;; | |
| esac | |
| local tarball="node-${node_ver}-${os_part}-${arch}.tar.xz" | |
| local url="https://nodejs.org/dist/${node_ver}/${tarball}" | |
| local dest="$HOME/.local/node" | |
| info "Downloading Node.js $node_ver ($os_part $arch)..." | |
| mkdir -p "$HOME/.local/bin" "$dest" | |
| if curl -fsSL "$url" -o "/tmp/$tarball"; then | |
| tar -xJf "/tmp/$tarball" -C "$dest" --strip-components=1 | |
| ln -sf "$dest/bin/node" "$HOME/.local/bin/node" | |
| ln -sf "$dest/bin/npm" "$HOME/.local/bin/npm" | |
| ln -sf "$dest/bin/npx" "$HOME/.local/bin/npx" | |
| export PATH="$HOME/.local/bin:$dest/bin:$PATH" | |
| rm -f "/tmp/$tarball" | |
| ok "Node.js $node_ver installed to ~/.local/node" | |
| else | |
| err "Failed to download Node.js" | |
| return 1 | |
| fi | |
| } | |
| ensure_pip() { | |
| if ! has_pip; then | |
| err "pip not found. Install Python 3: https://www.python.org/downloads/" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Some `pip install --user` and `npm install -g` (with prefix=~/.local) targets | |
| # drop binaries in ~/.local/bin which is often missing from PATH on Linux. | |
| # Ensure npm's global bin directory is on PATH (critical on Linux where | |
| # prefix may be ~/.local/node and bins live in $prefix/bin, not ~/.local/bin). | |
| ensure_npm_global_bin_on_path() { | |
| has_cmd npm || return 0 | |
| local pfx bin_dir | |
| pfx="$(npm prefix -g 2>/dev/null || true)" | |
| [[ -n "$pfx" ]] || return 0 | |
| if [[ -d "$pfx/bin" ]]; then | |
| bin_dir="$pfx/bin" | |
| else | |
| bin_dir="$pfx" | |
| fi | |
| [[ -d "$bin_dir" ]] || return 0 | |
| case ":$PATH:" in | |
| *":$bin_dir:"*) ;; | |
| *) export PATH="$bin_dir:$PATH" ;; | |
| esac | |
| for rc in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bashrc" "$HOME/.bash_profile"; do | |
| [[ -f "$rc" ]] || continue | |
| grep -qsF "$bin_dir" "$rc" && continue | |
| printf '\n# Added by install-genai-tools.sh (npm global bin)\nexport PATH="%s:$PATH"\n' "$bin_dir" >> "$rc" | |
| done | |
| } | |
| ensure_user_local_bin_on_path() { | |
| local local_bin="$HOME/.local/bin" | |
| [[ -d "$local_bin" ]] || mkdir -p "$local_bin" | |
| case ":$PATH:" in | |
| *":$local_bin:"*) return 0 ;; | |
| esac | |
| export PATH="$local_bin:$PATH" | |
| for rc in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bashrc" "$HOME/.bash_profile"; do | |
| [[ -f "$rc" ]] || continue | |
| grep -qsF '$HOME/.local/bin' "$rc" && continue | |
| grep -qsF '~/.local/bin' "$rc" && continue | |
| printf '\n# Added by install-genai-tools.sh\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$rc" | |
| done | |
| dim "Added \$HOME/.local/bin to PATH (current shell + rc files)." | |
| } | |
| npm_install_g() { | |
| # On Windows (Git Bash / MSYS), `sudo` does not exist. The default npm | |
| # globals dir is %APPDATA%\npm; if it doesn't exist yet `[[ -w ... ]]` | |
| # returns false and the script falls back to sudo, which then fails. Make | |
| # sure the dir exists so the unprivileged path is always taken. | |
| # WSL uses Linux npm — skip the Windows prefix dance. | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]] && has_cmd npm; then | |
| local pfx; pfx="$(npm config get prefix 2>/dev/null)" | |
| [[ -n "$pfx" ]] && mkdir -p "$pfx/node_modules" 2>/dev/null | |
| fi | |
| if [[ -w "$(npm root -g 2>/dev/null)" ]] 2>/dev/null; then | |
| npm install -g "$@" | |
| elif [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| # Windows has no sudo; just attempt the install — npm itself may still | |
| # succeed if the prefix is user-writable. | |
| npm install -g "$@" | |
| else | |
| _sudo npm install -g "$@" | |
| fi | |
| ensure_npm_global_bin_on_path | |
| ensure_user_local_bin_on_path | |
| ensure_windows_user_bins_on_path | |
| } | |
| # ── generic cross-OS install helpers (1.8.0) ───────────────────────── | |
| ensure_go() { | |
| if has_cmd go; then return 0; fi | |
| if has_brew; then | |
| brew install go || return 1 | |
| elif [[ "$(native_cli_os)" == "linux" ]]; then | |
| warn "Go not found — install from https://go.dev/dl/ then re-run" | |
| return 1 | |
| elif [[ "$(native_cli_os)" == "windows" ]] && has_winget; then | |
| winget install -e --id GoLang.Go --accept-package-agreements --accept-source-agreements || return 1 | |
| else | |
| return 1 | |
| fi | |
| has_cmd go | |
| } | |
| ensure_gopath_bin() { | |
| local gbin="${GOPATH:-$HOME/go}/bin" | |
| mkdir -p "$gbin" | |
| case ":$PATH:" in *":$gbin:"*) ;; *) export PATH="$gbin:$PATH" ;; esac | |
| } | |
| ensure_cargo() { | |
| if has_cmd cargo; then return 0; fi | |
| warn "Rust/cargo not found — install via https://rustup.rs then re-run" | |
| return 1 | |
| } | |
| # Prefer uv tool, then pipx, then pip --user. Works on macOS/Linux/WSL/Windows. | |
| install_python_tool() { | |
| local pkg="$1" | |
| local label="${2:-$pkg}" | |
| local expect_bin="${3:-}" | |
| step "Installing $label (python: $pkg)" | |
| if has_cmd uv; then | |
| # Some SDKs ship no console scripts — uv errors with "No executables". | |
| # Prefer pipx/pip in that case so the package still lands, then callers | |
| # can write_local_shim for endpoint-visible binaries. | |
| if ! uv tool install --force "$pkg" 2>/tmp/uv-tool-install.err; then | |
| if grep -qiE 'No executables|entrypoints' /tmp/uv-tool-install.err 2>/dev/null; then | |
| warn "uv: $pkg has no console scripts — falling back to pip --user" | |
| ensure_pip || return 1 | |
| "$(pip_cmd)" install --user "$pkg" || return 1 | |
| else | |
| cat /tmp/uv-tool-install.err >&2 | |
| return 1 | |
| fi | |
| fi | |
| elif has_cmd pipx; then | |
| pipx install "$pkg" 2>/dev/null || pipx install --force "$pkg" || return 1 | |
| elif ensure_pip; then | |
| "$(pip_cmd)" install --user "$pkg" || return 1 | |
| else | |
| err "Need uv, pipx, or pip to install $pkg" | |
| return 1 | |
| fi | |
| ensure_user_local_bin_on_path | |
| ensure_windows_user_bins_on_path | |
| if [[ -n "$expect_bin" ]] && ! has_cmd "$expect_bin"; then | |
| warn "$expect_bin not on PATH after installing $pkg — caller should write_local_shim" | |
| fi | |
| return 0 | |
| } | |
| install_npm_tool() { | |
| local pkg="$1" | |
| local label="${2:-$pkg}" | |
| step "Installing $label (npm: $pkg)" | |
| ensure_npm || return 1 | |
| npm_install_g "$pkg" || return 1 | |
| ensure_user_local_bin_on_path | |
| ensure_windows_user_bins_on_path | |
| return 0 | |
| } | |
| install_go_tool() { | |
| local import_path="$1" | |
| local label="${2:-$import_path}" | |
| step "Installing $label (go install $import_path)" | |
| ensure_go || return 1 | |
| ensure_gopath_bin | |
| go install "$import_path" || return 1 | |
| ensure_gopath_bin | |
| return 0 | |
| } | |
| install_brew_formula() { | |
| local formula="$1" | |
| local label="${2:-$formula}" | |
| step "Installing $label (brew: $formula)" | |
| if ! has_brew; then | |
| return 1 | |
| fi | |
| brew install "$formula" || return 1 | |
| return 0 | |
| } | |
| # Write a small launcher shim into ~/.local/bin so npx/uvx tools appear as | |
| # endpoint-visible binaries (detection scope: terminal-accessible executables). | |
| write_local_shim() { | |
| local name="$1" | |
| shift | |
| local cmd=("$@") | |
| local dest="$HOME/.local/bin/$name" | |
| mkdir -p "$HOME/.local/bin" | |
| { | |
| echo '#!/usr/bin/env bash' | |
| echo "# Generated by install-genai-tools.sh — shim for $name" | |
| printf 'exec' | |
| local a | |
| for a in "${cmd[@]}"; do | |
| printf ' %q' "$a" | |
| done | |
| echo ' "$@"' | |
| } > "$dest" | |
| chmod +x "$dest" | |
| ensure_user_local_bin_on_path | |
| dim "Shim: $dest -> ${cmd[*]}" | |
| } | |
| install_git_clone_tool() { | |
| local repo="$1" # https://github.com/org/repo.git | |
| local dir_name="$2" | |
| local label="${3:-$dir_name}" | |
| local dest="$HOME/genai-tools/$dir_name" | |
| step "Installing $label (git clone -> $dest)" | |
| mkdir -p "$HOME/genai-tools" | |
| if [[ -d "$dest/.git" ]]; then | |
| git -C "$dest" pull --ff-only 2>/dev/null || true | |
| else | |
| git clone --depth 1 "$repo" "$dest" || return 1 | |
| fi | |
| dim "Cloned: $dest" | |
| return 0 | |
| } | |
| # ── pkg-mgr / sudo / windows helpers (added 2026-04 — see PATCHES.md) ─ | |
| # Detect Linux package manager once at startup so install functions can route | |
| # .deb / .rpm payloads correctly without hard-coding `dpkg` / `apt-get`. | |
| PKG_MGR="" | |
| detect_pkg_mgr() { | |
| # WSL sets PLATFORM=windows but runs a Linux package manager. | |
| if [[ "$PLATFORM" != "linux" && "$IS_WSL" != true ]]; then | |
| return 0 | |
| fi | |
| if command -v apt-get >/dev/null 2>&1; then PKG_MGR="apt" | |
| elif command -v dnf >/dev/null 2>&1; then PKG_MGR="dnf" | |
| elif command -v yum >/dev/null 2>&1; then PKG_MGR="yum" | |
| fi | |
| } | |
| # sudo wrapper that handles three modes: | |
| # * NOPASSWD (no prompt) — run sudo directly | |
| # * SUDO_PASS env set — pipe password via -S | |
| # * Otherwise — run sudo (may fail on non-tty SSH sessions) | |
| # On Windows there is no sudo, so just execute the command unchanged | |
| # (assumes Git Bash / MSYS already runs as Administrator). | |
| _sudo() { | |
| # Native Windows has no sudo; WSL does (Linux userland). | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| "$@" | |
| return $? | |
| fi | |
| if sudo -n true 2>/dev/null; then | |
| sudo "$@" | |
| elif [[ -n "${SUDO_PASS:-}" ]]; then | |
| echo "$SUDO_PASS" | sudo -S "$@" | |
| else | |
| sudo "$@" | |
| fi | |
| } | |
| # Run a vendor install script that uses sudo internally (Ollama, LM Studio, | |
| # Zed, etc.). Sets up an askpass helper from $SUDO_PASS if NOPASSWD isn't | |
| # configured, so curl-piped installers don't fail on `sudo: a password is required`. | |
| _run_with_sudo_askpass() { | |
| local script="$1" | |
| if sudo -n true 2>/dev/null; then | |
| bash "$script" | |
| return $? | |
| fi | |
| if [[ -n "${SUDO_PASS:-}" ]]; then | |
| local d; d="$(mktemp -d -t askpass.XXXXXX)" | |
| cat > "$d/askpass.sh" <<EOF | |
| #!/usr/bin/env bash | |
| echo "$SUDO_PASS" | |
| EOF | |
| chmod +x "$d/askpass.sh" | |
| SUDO_ASKPASS="$d/askpass.sh" bash "$script" | |
| local rc=$? | |
| rm -rf "$d" | |
| return $rc | |
| fi | |
| bash "$script" | |
| } | |
| # Install a local .deb / .rpm package using the host's package manager. | |
| # Falls back to plain rpm -i for AL2023 / RHEL where `dnf install <file>` may | |
| # refuse signed remote-only repos. | |
| install_local_package() { | |
| local pkg="$1" | |
| case "$PKG_MGR" in | |
| apt) _sudo apt-get install -y "$pkg" ;; | |
| dnf|yum) _sudo "$PKG_MGR" install -y "$pkg" || _sudo rpm -i --force "$pkg" ;; | |
| *) | |
| err "Unknown Linux package manager — cannot install $pkg" | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| # Always-safe mktemp -d wrapper. Avoids "too few X's in template" — the bare | |
| # `mktemp -d -t foo` form is rejected by GNU coreutils but accepted on macOS, | |
| # which made the original gist work locally and fail on Linux. | |
| mktempd() { mktemp -d -t "${1:-genai}.XXXXXX"; } | |
| # Escape a string for use inside PowerShell single-quoted literals. | |
| _win_ps_quote() { | |
| local s="$1" | |
| s="${s//\'/''}" | |
| printf "'%s'" "$s" | |
| } | |
| # Run a PowerShell script from a temp file (-File). Avoids nested quoting bugs | |
| # when invoked over SSH (outer shells strip $p.Id, here-string terminators, etc.). | |
| win_ps_file() { | |
| local ps1 ps1_win rc | |
| ps1="$(mktemp "${TMPDIR:-/tmp}/genai-win.XXXXXX.ps1")" | |
| cat >"$ps1" | |
| ps1_win="$(cygpath -w "$ps1" 2>/dev/null || echo "$ps1")" | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1_win" | |
| rc=$? | |
| rm -f "$ps1" | |
| return $rc | |
| } | |
| # Run a Windows installer silently using PowerShell's Start-Process -Wait. | |
| # More reliable than `cmd.exe /c start /wait` in non-interactive SSH | |
| # sessions, where the cmd window can lose its parent and hang forever. | |
| # Args: $1 = exe path (POSIX), $2..$N = installer arguments | |
| win_run_silent() { | |
| [[ "$PLATFORM" != "windows" ]] && return 1 | |
| local exe="$1"; shift | |
| local exe_win; exe_win="$(cygpath -w "$exe" 2>/dev/null || echo "$exe")" | |
| local exe_q; exe_q="$(_win_ps_quote "$exe_win")" | |
| if [[ $# -eq 0 ]]; then | |
| win_ps_file <<EOF | |
| \$ProgressPreference = 'SilentlyContinue' | |
| exit (Start-Process -FilePath $exe_q -Wait -PassThru).ExitCode | |
| EOF | |
| else | |
| local arg args_ps="" arg_q | |
| for arg in "$@"; do | |
| arg_q="$(_win_ps_quote "$arg")" | |
| if [[ -n "$args_ps" ]]; then args_ps="$args_ps, $arg_q"; else args_ps="$arg_q"; fi | |
| done | |
| win_ps_file <<EOF | |
| \$ProgressPreference = 'SilentlyContinue' | |
| \$args = @($args_ps) | |
| exit (Start-Process -FilePath $exe_q -ArgumentList \$args -Wait -PassThru).ExitCode | |
| EOF | |
| fi | |
| } | |
| # Absolute Git Bash path for Windows SSH automation (documented in --help). | |
| git_bash_bin() { | |
| local candidates=( | |
| "/c/Program Files/Git/bin/bash.exe" | |
| "/c/Program Files (x86)/Git/bin/bash.exe" | |
| ) | |
| local c | |
| for c in "${candidates[@]}"; do | |
| [[ -x "$c" ]] && { echo "$c"; return 0; } | |
| done | |
| command -v bash 2>/dev/null || echo "bash" | |
| } | |
| # Run a registry check with a timeout on native Windows (code --list-extensions, | |
| # npm config, greps over large trees can stall BatchMode SSH for minutes). | |
| run_tool_check() { | |
| local check="$1" | |
| if [[ "$PLATFORM" != "windows" || "$IS_WSL" == true ]]; then | |
| eval "$check" &>/dev/null | |
| return $? | |
| fi | |
| if [[ "$check" =~ ^has_cmd\ ]]; then | |
| eval "$check" &>/dev/null | |
| return $? | |
| fi | |
| ( | |
| eval "$check" &>/dev/null | |
| ) & | |
| local cpid=$! | |
| local waited=0 | |
| while kill -0 "$cpid" 2>/dev/null && [[ $waited -lt $WIN_CHECK_TIMEOUT ]]; do | |
| sleep 1 | |
| waited=$((waited + 1)) | |
| done | |
| if kill -0 "$cpid" 2>/dev/null; then | |
| kill "$cpid" 2>/dev/null | |
| wait "$cpid" 2>/dev/null || true | |
| return 1 | |
| fi | |
| wait "$cpid" | |
| return $? | |
| } | |
| # Pull a JSON field via Python (chosen at runtime to avoid the Microsoft | |
| # Store python3 stub on Windows). | |
| _python_bin() { | |
| # On Windows, $(command -v python3) often resolves to the WindowsApps stub | |
| # at C:\Users\X\AppData\Local\Microsoft\WindowsApps\python3.exe which prints | |
| # "Python was not found" and exits non-zero. Prefer real `python` first. | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| if command -v python >/dev/null 2>&1; then echo python; return 0; fi | |
| fi | |
| command -v python3 || command -v python || echo "" | |
| } | |
| json_get() { | |
| local key="$1" | |
| local py; py="$(_python_bin)" | |
| if [[ -z "$py" ]]; then | |
| # Fallback regex parser — works for top-level string fields only. | |
| grep -oE "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ | |
| | sed -E "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/" \ | |
| | head -1 | |
| return 0 | |
| fi | |
| "$py" -c "import json,sys | |
| try: | |
| d=json.load(sys.stdin); print(d.get('$key','')) | |
| except Exception: | |
| pass" | |
| } | |
| # Look for a binary in npm globals AND \$PATH. Used after npm install -g to | |
| # verify success even when npm globals isn't on the calling shell's PATH. | |
| has_npm_bin() { | |
| local name="$1" | |
| has_cmd "$name" && return 0 | |
| has_cmd npm || return 1 | |
| local pfx; pfx="$(npm config get prefix 2>/dev/null)" | |
| [[ -z "$pfx" ]] && return 1 | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| [[ -e "$pfx/$name.cmd" || -e "$pfx/$name.exe" || -e "$pfx/$name" ]] | |
| else | |
| [[ -e "$pfx/bin/$name" ]] | |
| fi | |
| } | |
| # Update Microsoft VS Code on Windows if the installed version is older than | |
| # the threshold required by current Roo Code / Kilo Code releases (1.95+). | |
| # On Windows Server 2022 the in-box VS Code (if any) is often 1.83.x, which | |
| # silently fails extension installs with "not compatible with current version". | |
| update_vscode_if_old() { | |
| [[ "$PLATFORM" != "windows" ]] && return 0 | |
| has_cmd code || return 0 | |
| local cur; cur="$(code --version 2>/dev/null | head -1)" | |
| [[ -z "$cur" ]] && return 0 | |
| local major | |
| major="$(echo "$cur" | awk -F. '{printf "%d%02d", $1, $2}')" | |
| [[ "$major" -ge 195 ]] && return 0 | |
| info "Updating VS Code from $cur (need >=1.95 for current Roo Code / Kilo Code)…" | |
| local arch="x64"; [[ "$ARCH_KIND" == "arm64" ]] && arch="arm64" | |
| local code_path; code_path="$(which code)" | |
| local url | |
| if [[ "$code_path" == */Program\ Files/* || "$code_path" == */c/Program\ Files/* ]]; then | |
| url="https://update.code.visualstudio.com/latest/win32-${arch}/stable" | |
| else | |
| url="https://update.code.visualstudio.com/latest/win32-${arch}-user/stable" | |
| fi | |
| local d; d="$(mktempd vscode)" | |
| local dest="$d/installer.exe" | |
| local dest_win; dest_win="$(cygpath -w "$dest")" | |
| # Use Invoke-WebRequest — curl drops the connection mid-download on some | |
| # Windows Server CDN paths; Invoke-WebRequest auto-retries. | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ | |
| "\$ProgressPreference='SilentlyContinue'; | |
| Invoke-WebRequest -Uri '$url' -OutFile '$dest_win' -UseBasicParsing -TimeoutSec 600" \ | |
| 2>&1 | tail -3 | |
| win_run_silent "$dest" "/VERYSILENT" "/NORESTART" "/MERGETASKS=!runcode" 2>&1 | tail -3 | |
| rm -rf "$d" | |
| local new_ver; new_ver="$(code --version 2>/dev/null | head -1)" | |
| ok "VS Code updated to ${new_ver:-?}" | |
| } | |
| # Bootstrap winget on Windows Server 2022 via the Microsoft.WinGet.Client | |
| # PowerShell module (handles the Microsoft.UI.Xaml + WindowsAppRuntime + | |
| # DesktopAppInstaller dependency chain that direct msixbundle installs can't). | |
| # Idempotent and best-effort — never fails the rest of the script. | |
| ensure_winget_windows() { | |
| [[ "$PLATFORM" != "windows" ]] && return 0 | |
| has_winget && return 0 | |
| info "Bootstrapping winget via Microsoft.WinGet.Client (Win Server 2022 needs this)…" | |
| win_ps_file <<'EOF' 2>&1 | tail -10 | |
| $ProgressPreference = 'SilentlyContinue' | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null | |
| Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue | |
| Install-Module -Name Microsoft.WinGet.Client -Repository PSGallery -Force -AllowClobber -Scope AllUsers | |
| try { Repair-WinGetPackageManager -AllUsers -Latest } catch { Repair-WinGetPackageManager -Latest } | |
| EOF | |
| has_winget && ok "winget installed" || warn "winget bootstrap incomplete (see Microsoft docs)" | |
| } | |
| # Install a Windows package by reading its manifest from microsoft/winget-pkgs | |
| # (https://github.com/microsoft/winget-pkgs) and downloading the InstallerUrl | |
| # directly. Used when winget itself is broken / catalog source can't sync. | |
| # Args: | |
| # $1 package id (e.g. Codeium.Windsurf) | |
| # $2 manifest path (e.g. c/Codeium/Windsurf) | |
| # $3 check dir (folder that should exist after install) | |
| # $4 installer args (default: "/VERYSILENT /NORESTART") | |
| install_via_winget_manifest() { | |
| [[ "$PLATFORM" != "windows" ]] && return 1 | |
| local pkg_id="$1" pkg_path="$2" check_dir="$3" args="${4:-/VERYSILENT /NORESTART}" | |
| local py; py="$(_python_bin)" | |
| [[ -z "$py" ]] && { err "$pkg_id: no python on PATH for manifest parse"; return 1; } | |
| # Latest version directory in winget-pkgs. | |
| local version | |
| version=$(curl -fsSL "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/$pkg_path" 2>/dev/null \ | |
| | "$py" -c 'import json,sys,re | |
| try: | |
| xs=[d["name"] for d in json.load(sys.stdin) if re.match(r"^[0-9]", d["name"])] | |
| xs.sort(key=lambda v:[int(p) for p in v.split("-")[0].split(".") if p.isdigit()]) | |
| print(xs[-1] if xs else "") | |
| except Exception: | |
| pass') | |
| [[ -z "$version" ]] && { err "$pkg_id: could not list versions"; return 1; } | |
| local manifest | |
| manifest=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/winget-pkgs/master/manifests/$pkg_path/$version/$pkg_id.installer.yaml") | |
| [[ -z "$manifest" ]] && { | |
| manifest=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/winget-pkgs/master/manifests/$pkg_path/$version/$pkg_id.yaml") | |
| } | |
| [[ -z "$manifest" ]] && { err "$pkg_id: could not fetch manifest"; return 1; } | |
| local arch="x64"; [[ "$ARCH_KIND" == "arm64" ]] && arch="arm64" | |
| # Prefer machine-scope installer; fall back to any URL for this arch. | |
| local url | |
| url=$(echo "$manifest" | awk -v arch="$arch" ' | |
| $0 ~ "Architecture: *"arch"$" { a=1; sc=0; next } | |
| /Architecture:/ && $0 !~ arch"$" { a=0; sc=0; next } | |
| a && /Scope: *machine/ { sc=1 } | |
| a && sc && /InstallerUrl:/ { sub(/^[^h]*/, "", $0); print; exit } | |
| ') | |
| if [[ -z "$url" ]]; then | |
| url=$(echo "$manifest" | awk -v arch="$arch" ' | |
| $0 ~ "Architecture: *"arch"$" { a=1; next } | |
| /Architecture:/ && $0 !~ arch"$" { a=0; next } | |
| a && /InstallerUrl:/ { sub(/^[^h]*/, "", $0); print; exit } | |
| ') | |
| fi | |
| [[ -z "$url" ]] && { err "$pkg_id: no InstallerUrl for $arch in manifest $version"; return 1; } | |
| info "Installing $pkg_id $version from $url" | |
| local dest="$HOME/$(basename "$url" | sed 's/?.*//')" | |
| local dest_win; dest_win="$(cygpath -w "$dest")" | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ | |
| "\$ProgressPreference='SilentlyContinue'; | |
| Invoke-WebRequest -Uri '$url' -OutFile '$dest_win' -UseBasicParsing -TimeoutSec 600" 2>&1 | tail -3 | |
| # shellcheck disable=SC2046 | |
| set -- $args | |
| win_run_silent "$dest" "$@" 2>&1 | tail -3 | |
| [[ -d "$check_dir" ]] | |
| } | |
| # ── macOS Gatekeeper helpers ───────────────────────────────────────── | |
| # Strip com.apple.quarantine from a freshly-installed .app bundle so the | |
| # first-launch "Are you sure you want to open this app downloaded from the | |
| # Internet?" Gatekeeper prompt is suppressed. Best-effort — never fails the | |
| # installer. | |
| strip_quarantine_macos() { | |
| [[ "$PLATFORM" == "darwin" ]] || return 0 | |
| local target="$1" | |
| [[ -z "$target" || ! -e "$target" ]] && return 0 | |
| xattr -dr com.apple.quarantine "$target" 2>/dev/null || true | |
| } | |
| # Strip quarantine from /Applications/<Name>.app (and ~/Applications fallback) | |
| # whether or not the .app exists yet. Used after install_desktop_app to cover | |
| # both brew cask and manual install paths. | |
| strip_quarantine_app() { | |
| [[ "$PLATFORM" == "darwin" ]] || return 0 | |
| local name="$1" | |
| for base in "/Applications" "$HOME/Applications"; do | |
| [[ -d "$base/${name}.app" ]] && strip_quarantine_macos "$base/${name}.app" | |
| done | |
| } | |
| # Install the shell-wrapper CLI for a VS Code-family IDE so the user does not | |
| # have to click the in-app "Install '<cmd>' command in PATH" prompt. Most brew | |
| # casks (cursor, windsurf, void, codium, zed) drop this symlink to | |
| # /opt/homebrew/bin/ as part of postinstall; manual DMG installs and a few | |
| # casks (trae, pearai) do not. This helper is idempotent and best-effort — | |
| # never fails the installer. | |
| # | |
| # Args: | |
| # $1 wrapper command name (e.g. "trae", "pearai", "void") | |
| # $2 app bundle name (e.g. "Trae", "PearAI", "Void") | |
| # $3 bundled binary basename (e.g. "trae", "code", "void") | |
| # — relative to <App>.app/Contents/Resources/app/bin/ | |
| link_macos_ide_cli() { | |
| [[ "$PLATFORM" == "darwin" ]] || return 0 | |
| local cmd="$1" app="$2" bin="$3" | |
| local app_path | |
| for base in "/Applications" "$HOME/Applications"; do | |
| [[ -d "$base/${app}.app" ]] && { app_path="$base/${app}.app"; break; } | |
| done | |
| [[ -z "${app_path:-}" ]] && return 0 | |
| local src="$app_path/Contents/Resources/app/bin/$bin" | |
| [[ -e "$src" ]] || src="$app_path/Contents/MacOS/$bin" | |
| [[ -e "$src" ]] || { dim "No bundled CLI for $app at $src — skipping shell wrapper"; return 0; } | |
| local target_dirs=() | |
| # Prefer dirs already on PATH and writable without sudo. | |
| for d in "/opt/homebrew/bin" "$HOME/.local/bin" "/usr/local/bin"; do | |
| [[ -d "$d" ]] || continue | |
| target_dirs+=("$d") | |
| done | |
| local linked=0 | |
| for d in "${target_dirs[@]}"; do | |
| local dest="$d/$cmd" | |
| if [[ -L "$dest" || -e "$dest" ]]; then | |
| dim "Shell wrapper $cmd already at $dest" | |
| linked=1; break | |
| fi | |
| if [[ -w "$d" ]]; then | |
| ln -sf "$src" "$dest" 2>/dev/null && { ok "Linked $cmd → $dest"; linked=1; break; } | |
| fi | |
| done | |
| if [[ $linked -eq 0 ]]; then | |
| # Fall back to /usr/local/bin via sudo. Only attempt if we have a non- | |
| # interactive sudo (NOPASSWD configured); otherwise just print the hint. | |
| local dest="/usr/local/bin/$cmd" | |
| if sudo -n true 2>/dev/null; then | |
| sudo mkdir -p /usr/local/bin 2>/dev/null | |
| sudo ln -sf "$src" "$dest" 2>/dev/null && { ok "Linked $cmd → $dest (via sudo)"; linked=1; } | |
| fi | |
| fi | |
| if [[ $linked -eq 0 ]]; then | |
| dim "Could not auto-link $cmd to PATH. Open $app and run command palette → 'Install '$cmd' command in PATH'." | |
| fi | |
| } | |
| # ── app detection helpers ──────────────────────────────────────────── | |
| has_app() { | |
| local name="$1" | |
| case "$PLATFORM" in | |
| darwin) | |
| [[ -d "/Applications/${name}.app" ]] || [[ -d "$HOME/Applications/${name}.app" ]] | |
| ;; | |
| windows) | |
| local lower | |
| lower="$(echo "$name" | tr '[:upper:]' '[:lower:]')" | |
| has_cmd "$lower" && return 0 | |
| # Check common Windows install paths | |
| local progfiles="${PROGRAMFILES:-/c/Program Files}" | |
| local localapp="${LOCALAPPDATA:-$HOME/AppData/Local}" | |
| [[ -d "$progfiles/$name" ]] || [[ -d "$localapp/Programs/$name" ]] || [[ -d "$localapp/$name" ]] \ | |
| || [[ -d "$progfiles/${name} CLI" ]] || [[ -d "$localapp/Programs/${name} CLI" ]] | |
| ;; | |
| linux) | |
| has_cmd "$(echo "$name" | tr '[:upper:]' '[:lower:]')" | |
| ;; | |
| esac | |
| } | |
| check_lm_studio() { has_cmd lms || has_app 'LM Studio'; } | |
| check_tabnine() { has_cmd tabnine || has_vscode_ext tabnine; } | |
| check_blackbox_ai() { has_cmd blackbox || has_cmd blackbox-ai; } | |
| check_textgen_webui() { [[ -d "$HOME/text-generation-webui" ]]; } | |
| check_zencoder() { has_cmd zencoder || has_app 'Zenflow'; } | |
| check_google_antigravity() { has_app Antigravity || has_app "Antigravity.app"; } | |
| # Standalone Antigravity CLI (curl install) — distinct from Homebrew cask agy shim → IDE binary. | |
| has_standalone_agy() { | |
| local cand | |
| for cand in "$HOME/.local/bin/agy" "$HOME/AppData/Local/agy/bin/agy.exe"; do | |
| [[ -x "$cand" ]] && return 0 | |
| done | |
| if [[ "$PLATFORM" == "windows" ]]; then | |
| local win="${LOCALAPPDATA:-$HOME/AppData/Local}/agy/bin/agy.exe" | |
| [[ -x "$win" ]] && return 0 | |
| fi | |
| return 1 | |
| } | |
| check_antigravity_cli() { has_standalone_agy; } | |
| check_kiro_ide() { has_app Kiro || has_app "Kiro.app"; } | |
| check_elastic_mcp_server() { | |
| # Considered "installed" if the npx cache has the package or the user has | |
| # configured an MCP host to launch it. We don't run a global install. | |
| has_cmd npx && { | |
| [[ -d "$HOME/.npm/_npx" ]] && grep -rq "@elastic/mcp-server" "$HOME/.npm/_npx" 2>/dev/null && return 0 | |
| } | |
| return 1 | |
| } | |
| # Cloud-leaning tools: each has a real local artifact path even though the | |
| # product is primarily hosted. We install the closest official/community | |
| # surface that puts something on disk so detection telemetry can be captured. | |
| # | |
| # bolt -> stackblitz-labs/bolt.diy (OSS sibling), pnpm install | |
| # devin -> official VS Code extension cognition.devin | |
| # grok -> grok-agent npm CLI (matches registry processNames) | |
| # replit -> official Replit desktop app from github.com/replit/desktop | |
| ensure_pnpm() { | |
| if has_cmd pnpm; then return 0; fi | |
| ensure_npm || return 1 | |
| info "Installing pnpm (required by bolt.diy)" | |
| npm_install_g pnpm || return 1 | |
| } | |
| install_bolt() { | |
| step "Installing Bolt (bolt.diy — OSS self-host of bolt.new)" | |
| ensure_npm || return 1 | |
| ensure_pnpm || return 1 | |
| local dest="$HOME/.local/share/bolt-diy" | |
| mkdir -p "$(dirname "$dest")" | |
| if [[ -d "$dest/.git" ]]; then | |
| info "Repo exists; pulling latest…" | |
| git -C "$dest" pull --ff-only || true | |
| else | |
| git clone --depth=1 https://github.com/stackblitz-labs/bolt.diy "$dest" || return 1 | |
| fi | |
| ( cd "$dest" && pnpm install --prefer-offline ) || { | |
| warn "pnpm install failed — likely native-build deps. Retry with: cd $dest && pnpm install" | |
| return 1 | |
| } | |
| if [[ -f "$dest/.env.example" && ! -f "$dest/.env.local" ]]; then | |
| cp "$dest/.env.example" "$dest/.env.local" | |
| dim "Seeded $dest/.env.local from .env.example — add your provider API keys." | |
| fi | |
| dim "Source: $dest" | |
| dim "Run: cd $dest && pnpm run dev (http://localhost:5173)" | |
| dim "Note: bolt.new itself is browser-only; this is the OSS sibling that" | |
| dim " produces local processes/configs for detection telemetry." | |
| } | |
| check_bolt() { [[ -d "$HOME/.local/share/bolt-diy/.git" && -d "$HOME/.local/share/bolt-diy/node_modules" ]]; } | |
| install_devin() { | |
| install_vscode_ext "Devin (Cognition)" "cognition.devin" || return 1 | |
| dim "Sign in: open the Devin tab in the activity bar (⌘+G to start a session)." | |
| dim "Sessions still execute in Cognition's cloud VMs; the extension is the local surface." | |
| } | |
| check_devin() { has_vscode_ext "cognition.devin"; } | |
| install_grok() { | |
| step "Installing Grok (grok-agent npm CLI — matches registry processNames)" | |
| ensure_npm || return 1 | |
| npm_install_g grok-agent || return 1 | |
| dim "Verify: grok-agent --version (or: grok-agent doctor)" | |
| dim "Auth: export XAI_API_KEY=... (https://console.x.ai/team/default/api-keys)" | |
| dim "Note: xAI does not ship an official CLI. grok-agent is the community" | |
| dim " npm package the registry tracks under processNames=[grok-agent,node]." | |
| } | |
| check_grok() { has_cmd grok-agent || has_cmd grok; } | |
| install_replit() { | |
| step "Installing Replit Desktop (github.com/replit/desktop)" | |
| case "$PLATFORM" in | |
| darwin) | |
| local dmg url | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| url="https://github.com/replit/desktop/releases/latest/download/Replit.dmg" | |
| else | |
| url="https://github.com/replit/desktop/releases/latest/download/Replit-Intel.dmg" | |
| fi | |
| dmg="$(mktempd replit-desktop)/replit.dmg" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$dmg" || return 1 | |
| # Parse hdiutil's tab-separated attach output (3 cols: device fs mountpoint). | |
| local mp | |
| mp="$(hdiutil attach "$dmg" -nobrowse -noautoopen 2>/dev/null \ | |
| | awk -F'\t' 'BEGIN{IGNORECASE=1} $NF ~ /^\/Volumes\// {mp=$NF} END{print mp}')" | |
| if [[ -z "${mp:-}" || ! -d "$mp" ]]; then | |
| err "Failed to mount $dmg" | |
| return 1 | |
| fi | |
| info "Mounted $mp" | |
| if [[ ! -d "$mp/Replit.app" ]]; then | |
| err "Replit.app not found inside $mp" | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| return 1 | |
| fi | |
| rm -rf /Applications/Replit.app 2>/dev/null || true | |
| if ! cp -R "$mp/Replit.app" /Applications/; then | |
| warn "/Applications not writable; falling back to ~/Applications/" | |
| mkdir -p "$HOME/Applications" | |
| cp -R "$mp/Replit.app" "$HOME/Applications/" || { | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| return 1 | |
| } | |
| fi | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -f "$dmg" | |
| strip_quarantine_app "Replit" | |
| ;; | |
| windows) | |
| if is_windows_arm64; then | |
| warn "Replit Desktop ships only win32-x64 (no native ARM64 build)." | |
| warn "Install will run via Prism emulation; expect mixed-arch processes." | |
| fi | |
| if has_winget; then | |
| # No winget package; fall through to direct download. | |
| : | |
| fi | |
| local exe url | |
| url="https://github.com/replit/desktop/releases/latest/download/Replit-1.0.14.Setup.exe" | |
| exe="$(mktempd replit-setup)/setup.exe" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$exe" || return 1 | |
| # Use PowerShell Start-Process -Wait — `"$exe" /S` from Git Bash can | |
| # detach in non-interactive SSH sessions and never return. | |
| win_run_silent "$exe" "/S" || { err "Setup failed"; return 1; } | |
| rm -f "$exe" | |
| ;; | |
| linux) | |
| # Replit Desktop only ships .deb artifacts (no .rpm). On AL2023 / RHEL | |
| # we have to convert with alien or skip — most non-Debian users will | |
| # rely on the web app anyway. | |
| if [[ "$PKG_MGR" != "apt" ]]; then | |
| warn "Replit Desktop ships .deb only; $PKG_MGR-based hosts not supported." | |
| warn "Use the web app at https://replit.com or install on Ubuntu/Debian." | |
| return 1 | |
| fi | |
| local deb url arch | |
| arch="amd64"; [[ "$ARCH_KIND" == "arm64" ]] && arch="arm64" | |
| url="https://github.com/replit/desktop/releases/latest/download/replit_1.0.14_${arch}.deb" | |
| deb="$(mktempd replit)/replit.deb" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$deb" || return 1 | |
| _sudo dpkg -i "$deb" || { _sudo apt-get install -fy && _sudo dpkg -i "$deb"; } || return 1 | |
| rm -f "$deb" | |
| ;; | |
| esac | |
| dim "Config: ~/Library/Application Support/Replit/ (macOS), ~/.config/Replit/ (Linux), %APPDATA%\\Replit\\ (Windows)" | |
| dim "Note: Electron wrapper around the cloud IDE. Agent inference still runs server-side." | |
| } | |
| check_replit() { has_app "Replit" || has_cmd replit; } | |
| # Research / OSS daemons that ship via git clone, not a registry. | |
| install_openclaw() { | |
| step "Installing OpenClaw (clone from GitHub)" | |
| ensure_npm || return 1 | |
| local dest="$HOME/.local/share/openclaw" | |
| mkdir -p "$(dirname "$dest")" | |
| if [[ -d "$dest/.git" ]]; then | |
| info "Repo exists; pulling latest…" | |
| git -C "$dest" pull --ff-only || true | |
| else | |
| git clone --depth=1 https://github.com/openclaw/openclaw "$dest" || return 1 | |
| fi | |
| ( cd "$dest" && npm install ) || return 1 | |
| dim "Source: $dest" | |
| dim "Run: cd $dest && npm start (or follow $dest/README.md)" | |
| dim "Note: research/red-team tool — review code before exposing the Gateway." | |
| } | |
| check_openclaw() { [[ -d "$HOME/.local/share/openclaw/.git" ]]; } | |
| install_nemoclaw() { | |
| step "Installing NemoClaw (NVIDIA enterprise wrapper for OpenClaw)" | |
| local dest="$HOME/.local/share/nemoclaw" | |
| mkdir -p "$(dirname "$dest")" | |
| if [[ -d "$dest/.git" ]]; then | |
| info "Repo exists; pulling latest…" | |
| git -C "$dest" pull --ff-only || true | |
| else | |
| git clone --depth=1 https://github.com/NVIDIA/NemoClaw "$dest" || return 1 | |
| fi | |
| dim "Source: $dest" | |
| dim "Sandbox: also clone https://github.com/NVIDIA/OpenShell for the K3s runtime" | |
| dim "Setup: follow $dest/README.md (Docker + K3s required)" | |
| } | |
| check_nemoclaw() { [[ -d "$HOME/.local/share/nemoclaw/.git" ]]; } | |
| # IDE-extension-only tools (Cline, Roo Code, Supermaven) — alias to their VS | |
| # Code extension installers so `--tool cline` works the same as `--tool ext-cline`. | |
| install_cline_alias() { install_ext_cline; } | |
| install_roo_code_alias() { install_ext_roo_code; } | |
| install_supermaven_alias() { install_ext_supermaven; } | |
| check_cline_alias() { has_vscode_ext claude-dev; } | |
| check_roo_code_alias() { has_vscode_ext roo-cline; } | |
| check_supermaven_alias() { has_vscode_ext supermaven; } | |
| # Cache IDE extension list for the duration of one --check run (Windows | |
| # `code --list-extensions` can take 10–30s; we have 8+ ext checks). | |
| _VSCODE_EXT_LIST="" | |
| _vscode_ext_list() { | |
| if [[ -n "$_VSCODE_EXT_LIST" ]]; then | |
| printf '%s' "$_VSCODE_EXT_LIST" | |
| return 0 | |
| fi | |
| local cli | |
| cli="$(find_ide_cli 2>/dev/null)" || return 1 | |
| _VSCODE_EXT_LIST="$("$cli" --list-extensions 2>/dev/null || true)" | |
| printf '%s' "$_VSCODE_EXT_LIST" | |
| } | |
| has_vscode_ext() { | |
| _vscode_ext_list | grep -qi "$1" | |
| } | |
| # ── platform install helpers ───────────────────────────────────────── | |
| # Tries the best package manager for the current platform. | |
| # Priority: brew (macOS) > winget (Windows) > scoop (Windows) > npm > manual | |
| install_with_winget() { | |
| local pkg_id="$1" | |
| if has_winget; then | |
| winget install -e --id "$pkg_id" --accept-package-agreements --accept-source-agreements | |
| return $? | |
| fi | |
| return 1 | |
| } | |
| install_desktop_app() { | |
| local name="$1" | |
| local brew_cask="${2:-}" | |
| local winget_id="${3:-}" | |
| local linux_cmd="${4:-}" | |
| local download_url="${5:-}" | |
| step "Installing $name" | |
| case "$PLATFORM" in | |
| darwin) | |
| if [[ -n "$brew_cask" ]] && has_brew; then | |
| local rc=0 | |
| brew install --cask "$brew_cask" || rc=$? | |
| # Strip Gatekeeper quarantine even on success — HOMEBREW_CASK_OPTS | |
| # already passes --no-quarantine, but some bottles set it anyway and | |
| # users running an older brew will benefit from the belt-and-braces. | |
| strip_quarantine_app "$name" | |
| # Also try the cask's canonical app name (often differs from the | |
| # display "$name", e.g. "Claude Desktop" vs "Claude"). | |
| strip_quarantine_app "$brew_cask" | |
| return $rc | |
| fi | |
| ;; | |
| windows) | |
| if [[ -n "$winget_id" ]] && has_winget; then | |
| install_with_winget "$winget_id" | |
| return $? | |
| fi | |
| ;; | |
| linux) | |
| if [[ -n "$linux_cmd" ]]; then | |
| eval "$linux_cmd" | |
| return $? | |
| fi | |
| ;; | |
| esac | |
| if [[ -n "$download_url" ]]; then | |
| info "Download from: $download_url" | |
| open_url "$download_url" | |
| return 1 | |
| fi | |
| err "No automated install method for $name on $PLATFORM" | |
| return 1 | |
| } | |
| # ── install functions: CLI agents ──────────────────────────────────── | |
| install_claude_code() { | |
| step "Installing Claude Code (Anthropic)" | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| brew install claude-code || return 1 | |
| elif [[ "$PLATFORM" == "windows" ]] && has_winget; then | |
| install_with_winget "Anthropic.ClaudeCode" || return 1 | |
| elif ensure_npm; then | |
| npm_install_g @anthropic-ai/claude-code || return 1 | |
| else | |
| return 1 | |
| fi | |
| dim "Verify: claude --version" | |
| dim "Config: ~/.claude/settings.json" | |
| } | |
| install_codex() { | |
| step "Installing Codex CLI (OpenAI)" | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| brew install --cask codex || true | |
| fi | |
| if ! has_cmd codex; then | |
| ensure_npm || return 1 | |
| npm_install_g @openai/codex || return 1 | |
| ensure_windows_user_bins_on_path | |
| fi | |
| has_cmd codex || has_npm_bin codex || { err "codex not found after install"; return 1; } | |
| dim "Verify: codex --version" | |
| } | |
| install_gemini_cli() { | |
| step "Installing Gemini CLI (Google)" | |
| warn "Gemini CLI consumer access sunsets mid-2026 — prefer antigravity-cli (agy)" | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| brew install gemini-cli || true | |
| fi | |
| if ! has_cmd gemini; then | |
| ensure_npm || return 1 | |
| npm_install_g @google/gemini-cli || return 1 | |
| ensure_windows_user_bins_on_path | |
| fi | |
| has_cmd gemini || has_npm_bin gemini || { err "gemini not found after install"; return 1; } | |
| dim "Verify: gemini --version" | |
| dim "Migrate: bash install-genai-tools.sh --tool antigravity-cli" | |
| } | |
| install_antigravity_cli() { | |
| step "Installing Antigravity CLI (Google) — agy" | |
| if has_standalone_agy; then | |
| dim "Already installed: $(command -v agy 2>/dev/null || echo ~/.local/bin/agy)" | |
| return 0 | |
| fi | |
| if has_cmd agy && ! has_standalone_agy; then | |
| warn "agy on PATH is the Antigravity IDE shim — installing standalone CLI to ~/.local/bin/agy" | |
| fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| darwin|linux) | |
| curl -fsSL https://antigravity.google/cli/install.sh | bash || return 1 | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| if command -v powershell.exe >/dev/null 2>&1; then | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ | |
| "irm https://antigravity.google/cli/install.ps1 | iex" || return 1 | |
| ensure_windows_user_bins_on_path | |
| else | |
| skip_unsupported "Antigravity CLI needs PowerShell on native Windows" | |
| return 2 | |
| fi | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| local cand | |
| for cand in "$HOME/.local/bin/agy" "/usr/local/bin/agy"; do | |
| [[ -x "$cand" ]] || continue | |
| case ":$PATH:" in *":$(dirname "$cand"):"*) ;; *) export PATH="$(dirname "$cand"):$PATH" ;; esac | |
| done | |
| has_standalone_agy || { err "standalone agy not found after install — check ~/.local/bin or %LOCALAPPDATA%\\agy\\bin"; return 1; } | |
| dim "Verify: agy --version" | |
| dim "Docs: https://antigravity.google/docs/cli/overview" | |
| dim "Note: first run may print a Google auth URL (SSH-friendly)" | |
| } | |
| install_google_antigravity() { | |
| install_desktop_app "Google Antigravity" \ | |
| "antigravity" \ | |
| "Google.Antigravity" \ | |
| "" \ | |
| "https://antigravity.google/download" || return 1 | |
| link_macos_ide_cli "agy" "Antigravity" "antigravity" | |
| dim "Verify: agy --version (macOS/Homebrew) or launch Antigravity from Start Menu (Windows)" | |
| dim "Docs: https://antigravity.google/docs/home" | |
| } | |
| install_aider() { | |
| step "Installing Aider" | |
| if is_windows_arm64 && [[ "$IS_WSL" != true ]]; then | |
| skip_unsupported "Aider has no Windows ARM64 wheels — use WSL or x64 Windows" | |
| return 2 | |
| fi | |
| if has_cmd uv; then | |
| uv tool install --force aider-chat || return 1 | |
| elif has_cmd pipx; then | |
| pipx install aider-chat || return 1 | |
| elif ensure_pip; then | |
| "$(pip_cmd)" install --user aider-chat || return 1 | |
| ensure_user_local_bin_on_path | |
| else | |
| return 1 | |
| fi | |
| dim "Verify: aider --version" | |
| dim "Config: ~/.aider.conf.yml" | |
| } | |
| install_genaiscript() { | |
| step "Installing GenAIScript (Microsoft)" | |
| ensure_npm || return 1 | |
| npm_install_g genaiscript || return 1 | |
| dim "Verify: genaiscript --version" | |
| } | |
| install_qwen_code() { | |
| step "Installing Qwen Code (Alibaba)" | |
| if has_brew; then | |
| brew install qwen-code || return 1 | |
| elif ensure_npm; then | |
| npm_install_g @qwen-code/qwen-code || return 1 | |
| else | |
| return 1 | |
| fi | |
| dim "Verify: qwen --version" | |
| } | |
| install_amp() { | |
| step "Installing Amp / Cody CLI (Sourcegraph)" | |
| ensure_npm || return 1 | |
| npm_install_g @sourcegraph/amp || return 1 | |
| dim "Verify: amp --version" | |
| } | |
| install_copilot_cli() { | |
| step "Installing GitHub Copilot CLI" | |
| if [[ "$PLATFORM" == "windows" ]] && has_winget; then | |
| install_with_winget "GitHub.Copilot" && { dim "Verify: copilot --version"; return 0; } | |
| fi | |
| ensure_npm || return 1 | |
| npm_install_g @github/copilot || return 1 | |
| dim "Verify: copilot --version" | |
| dim "Note: On Windows ARM64 the npm package ships a native @github/copilot-win32-arm64 helper signed by GitHub, Inc." | |
| } | |
| install_opencode() { | |
| step "Installing OpenCode (anomalyco / opencode.ai — model-agnostic TUI)" | |
| # Active project (anomalyco/opencode). Distinct from Charm Crush (crush binary). | |
| if has_cmd opencode; then | |
| dim "Already on PATH: $(command -v opencode)" | |
| return 0 | |
| fi | |
| local os; os="$(native_cli_os)" | |
| if [[ "$os" == "darwin" || "$os" == "linux" ]] && has_brew; then | |
| brew install anomalyco/tap/opencode 2>/dev/null \ | |
| || brew install opencode 2>/dev/null \ | |
| || true | |
| fi | |
| if ! has_cmd opencode; then | |
| case "$os" in | |
| darwin|linux) | |
| curl -fsSL https://opencode.ai/install | bash || { | |
| ensure_npm || return 1 | |
| npm_install_g opencode-ai || return 1 | |
| } | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| # scoop/choco when available; else npm | |
| if has_cmd scoop; then | |
| scoop install opencode || true | |
| fi | |
| if ! has_cmd opencode; then | |
| ensure_npm || return 1 | |
| npm_install_g opencode-ai || return 1 | |
| ensure_windows_user_bins_on_path | |
| fi | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| fi | |
| has_cmd opencode || has_npm_bin opencode || { err "opencode not found after install"; return 1; } | |
| dim "Verify: opencode --version" | |
| dim "Docs: https://opencode.ai/docs/" | |
| dim "Note: Charm Crush is a separate tool (crush); do not confuse with opencode" | |
| } | |
| install_goose() { | |
| step "Installing Goose (AAIF / Block)" | |
| case "$PLATFORM" in | |
| darwin) | |
| # Install the desktop app via cask (gets us /Applications/Goose.app on disk)… | |
| if has_brew; then | |
| brew install --cask block-goose || warn "block-goose cask install failed" | |
| fi | |
| # …and the standalone CLI via the official installer (gets us a `goose` | |
| # binary on PATH). Either path is sufficient for the registry check. | |
| if ! has_cmd goose; then | |
| info "Installing standalone goose CLI…" | |
| curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash || true | |
| ensure_user_local_bin_on_path | |
| fi | |
| ;; | |
| linux) | |
| curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash || true | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| if has_winget; then | |
| install_with_winget "Block.Goose" || return 1 | |
| else | |
| info "Download from: https://block.github.io/goose/" | |
| open_url "https://block.github.io/goose/" | |
| return 1 | |
| fi | |
| ;; | |
| esac | |
| has_cmd goose || [[ -d /Applications/Goose.app || -d "$HOME/Applications/Goose.app" ]] || { | |
| err "goose not found after install" | |
| return 1 | |
| } | |
| dim "Verify: goose --version (or open Goose.app)" | |
| dim "Config: ~/.config/goose/" | |
| } | |
| check_goose() { has_cmd goose || [[ -d /Applications/Goose.app || -d "$HOME/Applications/Goose.app" ]]; } | |
| install_kiro_cli() { | |
| step "Installing Kiro CLI (AWS)" | |
| case "$PLATFORM" in | |
| darwin|linux) | |
| # Kiro's installer reads from /dev/tty for a telemetry opt-in prompt and | |
| # crashes on headless / piped sessions. Run with stdin closed so the | |
| # prompt-handling falls back to its default ("decline telemetry") path, | |
| # and use a temp script + bash so /dev/tty isn't required. | |
| local installer="/tmp/kiro-install-$$.sh" | |
| if curl -fsSL https://cli.kiro.dev/install -o "$installer"; then | |
| bash "$installer" </dev/null || { | |
| warn "Headless install hit an interactive prompt — re-running with default opt-out." | |
| KIRO_OPT_OUT_TELEMETRY=true bash "$installer" </dev/null || true | |
| } | |
| rm -f "$installer" | |
| else | |
| curl -fsSL https://cli.kiro.dev/install | bash || true | |
| fi | |
| ensure_user_local_bin_on_path | |
| has_cmd kiro-cli || { err "kiro-cli binary not found after install"; return 1; } | |
| ;; | |
| windows) | |
| if has_winget; then | |
| install_with_winget "Amazon.KiroCLI" || return 1 | |
| else | |
| info "Windows: Run in PowerShell: irm 'https://cli.kiro.dev/install.ps1' | iex" | |
| info "Or download from: https://kiro.dev/docs/cli/installation/" | |
| open_url "https://kiro.dev/docs/cli/installation/" | |
| return 1 | |
| fi | |
| ;; | |
| esac | |
| dim "Verify: kiro-cli --version" | |
| dim "Auth: kiro-cli login" | |
| dim "Note: Native ARM64 Bun-compiled binary on Windows; signed by 'Amazon Web Services, Inc.' (AWS Kiro)." | |
| } | |
| install_kiro_ide() { | |
| step "Installing Kiro IDE (AWS)" | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew; then | |
| brew tap kirodev/tap 2>/dev/null || true | |
| brew install --cask kiro 2>/dev/null || brew install kirodev/tap/kiro || return 1 | |
| else | |
| open_url "https://kiro.dev/downloads/" | |
| return 1 | |
| fi | |
| ;; | |
| linux) | |
| info "Download .deb or universal tarball from https://kiro.dev/downloads/" | |
| open_url "https://kiro.dev/downloads/" | |
| return 1 | |
| ;; | |
| windows) | |
| if has_winget; then | |
| install_with_winget "Amazon.Kiro" 2>/dev/null || install_with_winget "Kiro.Kiro" 2>/dev/null || { | |
| open_url "https://kiro.dev/downloads/" | |
| return 1 | |
| } | |
| else | |
| open_url "https://kiro.dev/downloads/" | |
| return 1 | |
| fi | |
| ;; | |
| esac | |
| check_kiro_ide || { err "Kiro IDE not found after install"; return 1; } | |
| dim "Verify: launch Kiro from Applications / Start Menu" | |
| dim "Docs: https://kiro.dev/docs/" | |
| } | |
| install_augment() { | |
| step "Installing Augment Code (Auggie CLI)" | |
| ensure_npm || return 1 | |
| npm_install_g @augmentcode/auggie || return 1 | |
| dim "Verify: auggie --version" | |
| dim "Auth: auggie login" | |
| } | |
| install_tabnine_cli() { | |
| step "Installing Tabnine" | |
| # Tabnine's primary surface is the IDE extension (TabNine.tabnine-vscode); | |
| # the npm package `tabnine` is not the official CLI. Prefer the VS Code | |
| # extension when an IDE is present, then fall back to brew (cask). | |
| if find_ide_cli &>/dev/null; then | |
| install_vscode_ext "Tabnine" "TabNine.tabnine-vscode" && return 0 | |
| fi | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| brew install --cask tabnine 2>/dev/null && return 0 | |
| fi | |
| info "Tabnine ships primarily as an IDE plugin. Install in VS Code/JetBrains:" | |
| info " https://www.tabnine.com/install" | |
| open_url "https://www.tabnine.com/install" | |
| return 1 | |
| } | |
| install_bito() { | |
| step "Installing Bito CLI" | |
| case "$PLATFORM" in | |
| darwin|linux) | |
| # gitbito/CLI assets are named bito-{macos,linux}-{arm,x86}.tar.gz — | |
| # short arch + 'macos' (not 'darwin'). The hosted installer would drop | |
| # the binary in /usr/local/bin via sudo, which fails on non-interactive | |
| # hosts. Pull the tarball directly and install to ~/.local/bin instead. | |
| local arch_part="x86" | |
| [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]] && arch_part="arm" | |
| local os_part="linux" | |
| [[ "$PLATFORM" == "darwin" ]] && os_part="macos" | |
| local dl_url | |
| dl_url="$(curl -fsSL https://api.github.com/repos/gitbito/CLI/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url.*bito-${os_part}-${arch_part}\.tar\.gz" \ | |
| | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')" | |
| if [[ -n "$dl_url" ]]; then | |
| local tmpdir; tmpdir="$(mktemp -d)" | |
| if curl -fsSL "$dl_url" -o "$tmpdir/bito.tar.gz" && tar -xzf "$tmpdir/bito.tar.gz" -C "$tmpdir"; then | |
| mkdir -p "$HOME/.local/bin" | |
| # The tarball ships a single binary named bito-${os}-${arch}; rename | |
| # to plain `bito` for $PATH consumption. | |
| local bin_src | |
| bin_src="$(find "$tmpdir" -maxdepth 4 -type f \( -name 'bito' -o -name "bito-${os_part}-${arch_part}" \) 2>/dev/null | head -1)" | |
| if [[ -n "$bin_src" ]]; then | |
| install -m 0755 "$bin_src" "$HOME/.local/bin/bito" | |
| ensure_user_local_bin_on_path | |
| fi | |
| fi | |
| rm -rf "$tmpdir" | |
| fi | |
| if ! has_cmd bito; then | |
| warn "Direct binary install failed; falling back to interactive installer (may prompt for sudo)." | |
| curl -fsSL https://alpha.bito.ai/downloads/cli/install.sh | bash || true | |
| fi | |
| has_cmd bito || { err "bito binary not found (installer may require sudo)"; return 1; } | |
| ;; | |
| windows) | |
| if is_windows_arm64; then | |
| warn "Bito CLI ships only x64 Windows binaries; no public ARM64 build." | |
| warn "Skipping ARM64. On x64 Windows, run the official installer." | |
| fi | |
| info "Windows: Download from https://docs.bito.ai/other-bito-ai-tools/bito-cli" | |
| open_url "https://docs.bito.ai/other-bito-ai-tools/bito-cli" | |
| return 1 | |
| ;; | |
| esac | |
| dim "Verify: bito --version" | |
| dim "Config: ~/.bitoai/etc/ (also ~/.bito/ on first auth)" | |
| } | |
| install_codebuff() { | |
| step "Installing Codebuff" | |
| ensure_npm || return 1 | |
| npm_install_g codebuff || return 1 | |
| dim "Verify: codebuff --version" | |
| dim "Config: ~/.codebuff/ (binary cache: ~/.config/manicode/codebuff)" | |
| } | |
| install_junie() { | |
| step "Installing Junie (JetBrains)" | |
| if [[ "$PLATFORM" == "darwin" ]] && [[ "$ARCH" == "x86_64" ]]; then | |
| warn "Junie's macOS binary ships Apple Silicon only — x86_64 install will 404." | |
| warn "Use a JetBrains IDE plugin or run on arm64." | |
| return 1 | |
| fi | |
| if has_brew; then | |
| brew tap jetbrains/junie 2>/dev/null || true | |
| brew install junie || true | |
| fi | |
| if ! has_cmd junie; then | |
| if ensure_npm; then | |
| npm_install_g @jetbrains/junie || return 1 | |
| else | |
| curl -fsSL https://junie.jetbrains.com/install.sh | bash || return 1 | |
| fi | |
| fi | |
| ensure_user_local_bin_on_path | |
| has_cmd junie || { err "junie binary not found after install"; return 1; } | |
| dim "Verify: junie --version" | |
| dim "Config: ~/.junie/" | |
| } | |
| install_pochi() { | |
| step "Installing Pochi (TabbyML)" | |
| ensure_npm || return 1 | |
| npm_install_g pochi || return 1 | |
| dim "Verify: pochi --version" | |
| dim "Config: ~/.pochi/" | |
| } | |
| install_qodo_command() { | |
| step "Installing Qodo Command" | |
| ensure_npm || return 1 | |
| npm_install_g @qodo/command || return 1 | |
| dim "Verify: qodo --version" | |
| dim "Config: ~/.qodo/" | |
| } | |
| install_kilo_code() { | |
| step "Installing Kilo Code CLI" | |
| ensure_npm || return 1 | |
| npm_install_g @kilocode/cli || return 1 | |
| dim "Verify: kilo --version" | |
| dim "Note: Primary surface is the VS Code extension (kilocode.kilo-code)." | |
| } | |
| install_crush() { | |
| step "Installing Crush (Charmbracelet)" | |
| # Prefer platform-native package managers first. | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]] && has_winget; then | |
| install_with_winget "charmbracelet.crush" || true | |
| ensure_windows_user_bins_on_path | |
| if has_cmd crush; then | |
| dim "Verify: crush --version" | |
| dim "Config: ~/.config/crush/" | |
| return 0 | |
| fi | |
| fi | |
| if has_brew; then | |
| brew install charmbracelet/tap/crush || return 1 | |
| elif ensure_npm; then | |
| npm_install_g @charmland/crush || return 1 | |
| elif has_cmd go; then | |
| go install github.com/charmbracelet/crush@latest || return 1 | |
| ensure_user_local_bin_on_path | |
| else | |
| err "No supported install method for Crush on $PLATFORM" | |
| return 1 | |
| fi | |
| has_cmd crush || has_npm_bin crush || { err "crush not found after install"; return 1; } | |
| dim "Verify: crush --version" | |
| dim "Config: ~/.config/crush/" | |
| } | |
| install_openhands() { | |
| step "Installing OpenHands" | |
| if is_windows_arm64 && [[ "$IS_WSL" != true ]]; then | |
| skip_unsupported "OpenHands has no Windows ARM64 wheels — use WSL or x64 Windows" | |
| return 2 | |
| fi | |
| if has_cmd uv; then | |
| uv tool install openhands --python 3.12 || return 1 | |
| elif ensure_pip; then | |
| "$(pip_cmd)" install --user openhands-ai || return 1 | |
| ensure_user_local_bin_on_path | |
| else | |
| return 1 | |
| fi | |
| dim "Verify: openhands --version" | |
| dim "Config: ~/.openhands/" | |
| } | |
| install_factory_droid() { | |
| step "Installing Factory Droid CLI" | |
| if ensure_npm; then | |
| npm_install_g droid || return 1 | |
| else | |
| curl -fsSL https://app.factory.ai/cli | sh || return 1 | |
| ensure_user_local_bin_on_path | |
| fi | |
| # `has_cmd droid` returns false when the npm globals dir is not yet on the | |
| # current shell's PATH (common right after a first-time npm install). Check | |
| # the npm prefix directly so the install is recognized as successful. | |
| has_npm_bin droid || { err "droid binary not found after install"; return 1; } | |
| dim "Verify: droid --version" | |
| dim "Config: ~/.factory/" | |
| } | |
| install_jules() { | |
| step "Installing Google Jules CLI" | |
| ensure_npm || return 1 | |
| npm_install_g @google/jules || return 1 | |
| dim "Verify: jules --version" | |
| } | |
| install_gitlab_duo_cli() { | |
| step "Installing GitLab Duo CLI" | |
| ensure_npm || return 1 | |
| npm_install_g @gitlab/duo-cli || return 1 | |
| dim "Verify: duo --version" | |
| dim "Config: ~/.gitlab-duo/" | |
| } | |
| install_blackbox_ai() { | |
| step "Installing Blackbox AI CLI" | |
| ensure_npm || return 1 | |
| # The npm package was historically published as `blackboxai` but is now | |
| # `@blackbox_ai/blackbox-cli` (the legacy name returns 404). Try the new | |
| # name first and fall back to the legacy one for older registries. | |
| npm_install_g @blackbox_ai/blackbox-cli || npm_install_g blackboxai || return 1 | |
| dim "Verify: blackbox --version (or: blackbox-ai --version)" | |
| dim "Config: ~/.blackbox/" | |
| } | |
| install_rovo_dev() { | |
| step "Installing Atlassian Rovo Dev CLI (acli)" | |
| # Atlassian distributes acli as a Go binary at acli.atlassian.com. | |
| # Rovo Dev is invoked via `acli rovodev run`. | |
| # See: https://developer.atlassian.com/cloud/acli/guides/download-supported-packages/ | |
| # macOS: prefer the official Homebrew tap (atlassian/homebrew-acli). | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| brew tap atlassian/homebrew-acli 2>/dev/null | |
| if brew install acli 2>/dev/null; then | |
| dim "Verify: acli --version (then: acli rovodev run)" | |
| dim "Config: ~/.config/acli/ (per-product YAML configs)" | |
| return 0 | |
| fi | |
| fi | |
| # Windows: try winget first, then fall back to direct download. | |
| if [[ "$PLATFORM" == "windows" ]] && has_winget; then | |
| install_with_winget "Atlassian.CLI" 2>/dev/null && { | |
| dim "Verify: acli --version (then: acli rovodev run)" | |
| return 0 | |
| } | |
| fi | |
| local arch_part="" | |
| case "$ARCH_KIND" in | |
| x64) arch_part="amd64" ;; | |
| arm64) arch_part="arm64" ;; | |
| *) err "Unsupported arch for acli: $ARCH"; return 1 ;; | |
| esac | |
| mkdir -p "$HOME/.local/bin" | |
| local url="" | |
| case "$PLATFORM" in | |
| darwin) | |
| # Tarball: extract `acli` binary into ~/.local/bin | |
| url="https://acli.atlassian.com/darwin/latest/acli_darwin_${arch_part}.tar.gz" | |
| local tmp; tmp="$(mktemp -d)" | |
| if curl -fsSL "$url" -o "$tmp/acli.tar.gz" && tar -xzf "$tmp/acli.tar.gz" -C "$tmp"; then | |
| # Tarball layout is `acli` at top level (sometimes inside a folder). | |
| local found | |
| found="$(find "$tmp" -maxdepth 3 -type f -name acli -perm -u+x 2>/dev/null | head -1)" | |
| [[ -z "$found" ]] && found="$(find "$tmp" -maxdepth 3 -type f -name acli 2>/dev/null | head -1)" | |
| if [[ -n "$found" ]]; then | |
| install -m 0755 "$found" "$HOME/.local/bin/acli" | |
| rm -rf "$tmp" | |
| else | |
| err "acli binary not found in tarball ($url)" | |
| rm -rf "$tmp" | |
| return 1 | |
| fi | |
| else | |
| err "Failed to download/extract acli from $url" | |
| rm -rf "$tmp" | |
| return 1 | |
| fi | |
| ;; | |
| linux) | |
| url="https://acli.atlassian.com/linux/latest/acli_linux_${arch_part}.tar.gz" | |
| local tmp; tmp="$(mktemp -d)" | |
| if curl -fsSL "$url" -o "$tmp/acli.tar.gz" && tar -xzf "$tmp/acli.tar.gz" -C "$tmp"; then | |
| local found | |
| found="$(find "$tmp" -maxdepth 3 -type f -name acli 2>/dev/null | head -1)" | |
| if [[ -n "$found" ]]; then | |
| install -m 0755 "$found" "$HOME/.local/bin/acli" | |
| rm -rf "$tmp" | |
| else | |
| err "acli binary not found in tarball ($url)" | |
| rm -rf "$tmp" | |
| return 1 | |
| fi | |
| else | |
| err "Failed to download/extract acli from $url" | |
| rm -rf "$tmp" | |
| return 1 | |
| fi | |
| ;; | |
| windows) | |
| url="https://acli.atlassian.com/windows/latest/acli_windows_${arch_part}/acli.exe" | |
| if ! curl -fsSL "$url" -o "$HOME/.local/bin/acli.exe"; then | |
| err "Failed to download acli.exe from $url" | |
| return 1 | |
| fi | |
| chmod +x "$HOME/.local/bin/acli.exe" | |
| ;; | |
| esac | |
| ensure_user_local_bin_on_path | |
| has_cmd acli || { err "acli binary not found after install"; return 1; } | |
| dim "Verify: acli --version" | |
| dim "Run: acli rovodev run" | |
| dim "Config: ~/.config/acli/ (per-product YAML configs)" | |
| } | |
| install_hermes_agent() { | |
| step "Installing Hermes Agent (Nous Research)" | |
| # Hermes Agent is NOT on PyPI. Install via the official installer (uv + git clone). | |
| local os; os="$(native_cli_os)" | |
| if [[ "$os" == "windows" ]]; then | |
| skip_unsupported "Hermes Agent has no native Windows installer — use WSL and re-run" | |
| return 2 | |
| fi | |
| if ! has_cmd uv && ! has_cmd git; then | |
| err "Hermes Agent installer needs git (and ideally uv). Install git first." | |
| return 1 | |
| fi | |
| curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh \ | |
| | bash -s -- --skip-setup || return 1 | |
| ensure_user_local_bin_on_path | |
| has_cmd hermes || { err "hermes binary not found after install (check ~/.local/bin)"; return 1; } | |
| dim "Verify: hermes --version" | |
| dim "Setup: hermes setup (interactive provider/model setup)" | |
| dim "Config: ~/.hermes/ (config.yaml, SOUL.md, memories/, skills/, sessions/)" | |
| } | |
| install_continue_cli() { | |
| step "Installing Continue CLI (cn)" | |
| ensure_npm || return 1 | |
| npm_install_g @continuedev/cli || return 1 | |
| has_npm_bin cn || { err "cn binary not found after install"; return 1; } | |
| dim "Verify: cn --version" | |
| dim "Config: ~/.continue/ (shared with VS Code/JetBrains extensions)" | |
| } | |
| install_openai_cli() { | |
| step "Installing OpenAI CLI (openai-python SDK)" | |
| # Console script `openai` ships with the openai pip package. | |
| if has_cmd uv; then | |
| uv tool install openai || return 1 | |
| elif has_cmd pipx; then | |
| pipx install openai || return 1 | |
| elif has_cmd pip3; then | |
| pip3 install --user openai || return 1 | |
| elif has_cmd pip; then | |
| pip install --user openai || return 1 | |
| else | |
| err "Need pip / pipx / uv to install openai" | |
| return 1 | |
| fi | |
| ensure_user_local_bin_on_path | |
| dim "Verify: openai --version" | |
| dim "Auth: export OPENAI_API_KEY=..." | |
| } | |
| install_llm_cli() { | |
| step "Installing Simon Willison's llm CLI" | |
| # `llm` is a pip package; ships an `llm` console script. | |
| if has_brew; then | |
| brew install llm && { dim "Verify: llm --version"; return 0; } | |
| fi | |
| if has_cmd uv; then | |
| uv tool install llm || return 1 | |
| elif has_cmd pipx; then | |
| pipx install llm || return 1 | |
| elif has_cmd pip3; then | |
| pip3 install --user llm || return 1 | |
| elif has_cmd pip; then | |
| pip install --user llm || return 1 | |
| else | |
| err "Need pip / pipx / uv / brew to install llm" | |
| return 1 | |
| fi | |
| ensure_user_local_bin_on_path | |
| dim "Verify: llm --version" | |
| dim "Config: ~/Library/Application Support/io.datasette.llm/ (macOS), ~/.config/io.datasette.llm/ (Linux)" | |
| } | |
| install_github_mcp_release_binary() { | |
| # Map our internal PLATFORM/ARCH_KIND to the asset suffix used by | |
| # github/github-mcp-server release tarballs/zips. The release page lists | |
| # darwin_arm64.tar.gz, linux_x86_64.tar.gz, etc. | |
| local os_part="" arch_part="" ext="tar.gz" | |
| case "$PLATFORM" in | |
| darwin) os_part="Darwin" ;; | |
| linux) os_part="Linux" ;; | |
| windows) os_part="Windows"; ext="zip" ;; | |
| esac | |
| case "$ARCH_KIND" in | |
| x64) arch_part="x86_64" ;; | |
| arm64) arch_part="arm64" ;; | |
| *) return 1 ;; | |
| esac | |
| local pat="${os_part}_${arch_part}\\.${ext}\"" | |
| local url | |
| url=$(curl -fsSL https://api.github.com/repos/github/github-mcp-server/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url" \ | |
| | grep -E "$pat" \ | |
| | head -1 \ | |
| | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/') | |
| [[ -z "$url" ]] && return 1 | |
| info "Downloading prebuilt github-mcp-server: $url" | |
| local d; d="$(mktempd github-mcp)" | |
| local archive="$d/asset.$ext" | |
| curl -fsSL "$url" -o "$archive" || { rm -rf "$d"; return 1; } | |
| if [[ "$ext" == "tar.gz" ]]; then | |
| tar -xzf "$archive" -C "$d" 2>/dev/null | |
| else | |
| ( cd "$d" && unzip -q "$archive" ) | |
| fi | |
| local bin | |
| bin="$(find "$d" -maxdepth 3 -type f -name 'github-mcp-server*' ! -name '*.tar.gz' ! -name '*.zip' 2>/dev/null | head -1)" | |
| if [[ -z "$bin" ]]; then | |
| rm -rf "$d" | |
| return 1 | |
| fi | |
| mkdir -p "$HOME/.local/bin" | |
| if [[ "$PLATFORM" == "windows" ]]; then | |
| cp "$bin" "$HOME/.local/bin/github-mcp-server.exe" | |
| else | |
| install -m 0755 "$bin" "$HOME/.local/bin/github-mcp-server" | |
| fi | |
| rm -rf "$d" | |
| ensure_user_local_bin_on_path | |
| has_cmd github-mcp-server || return 1 | |
| ok "github-mcp-server installed to ~/.local/bin" | |
| dim "Verify: github-mcp-server --help" | |
| dim "Auth: export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_..." | |
| } | |
| install_github_mcp_server() { | |
| step "Installing GitHub MCP Server" | |
| # Preferred path: download the prebuilt release binary published on | |
| # https://github.com/github/github-mcp-server/releases (no Docker, no Go | |
| # toolchain required). Falls back to Docker, then to go install, then | |
| # opens the release page. | |
| install_github_mcp_release_binary && return 0 | |
| if has_cmd docker; then | |
| info "Pulling ghcr.io/github/github-mcp-server (no auth required for public image)…" | |
| if docker pull ghcr.io/github/github-mcp-server:latest >/dev/null 2>&1; then | |
| ok "Image pulled. Configure your MCP host with:" | |
| cat <<'EOF' | |
| "github": { | |
| "command": "docker", | |
| "args": ["run", "-i", "--rm", | |
| "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", | |
| "ghcr.io/github/github-mcp-server"], | |
| "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." } | |
| } | |
| EOF | |
| return 0 | |
| fi | |
| warn "Docker pull failed; falling back to go build." | |
| fi | |
| # github-mcp-server v1.0.3 requires Go 1.25+. Older toolchains (eg. apt | |
| # golang-go on Ubuntu 22.04) reject the go.mod, so version-gate on 1.25. | |
| go_is_recent() { | |
| has_cmd go || return 1 | |
| local v | |
| v=$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//') | |
| case "$v" in | |
| 1.[0-9]|1.[0-9].*|1.1[0-9]|1.1[0-9].*|1.2[0-4]|1.2[0-4].*) return 1 ;; | |
| *) return 0 ;; | |
| esac | |
| } | |
| if go_is_recent; then | |
| info "Building from source via go install…" | |
| go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest || return 1 | |
| local gobin="${GOBIN:-${GOPATH:-$HOME/go}/bin}" | |
| if [[ -x "$gobin/github-mcp-server" ]]; then | |
| mkdir -p "$HOME/.local/bin" | |
| ln -sf "$gobin/github-mcp-server" "$HOME/.local/bin/github-mcp-server" | |
| ensure_user_local_bin_on_path | |
| fi | |
| has_cmd github-mcp-server || { err "github-mcp-server not on PATH after go install"; return 1; } | |
| dim "Verify: github-mcp-server --help" | |
| dim "Auth: export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_..." | |
| return 0 | |
| fi | |
| # No Docker, stale or missing go — install a recent Go via brew (macOS) or the official | |
| # tarball (Linux). github/github-mcp-server requires Go 1.25+, which is | |
| # newer than the golang-go shipped in stable apt repos, so we always grab | |
| # the official tarball into ~/.local/go on Linux. | |
| if [[ "$PLATFORM" == "darwin" ]] && has_brew; then | |
| info "Installing Go toolchain via brew so we can build github-mcp-server…" | |
| brew install go || true | |
| elif [[ "$PLATFORM" == "linux" ]]; then | |
| info "Installing Go toolchain (1.25+) from official tarball so we can build github-mcp-server…" | |
| local go_ver="1.25.5" | |
| local arch_kind="amd64" | |
| case "$ARCH" in | |
| x86_64|amd64) arch_kind="amd64" ;; | |
| aarch64|arm64) arch_kind="arm64" ;; | |
| esac | |
| local tar_url="https://go.dev/dl/go${go_ver}.linux-${arch_kind}.tar.gz" | |
| local tmp_tar | |
| tmp_tar="$(mktemp -t go.XXXXXX.tar.gz)" | |
| if curl -fsSL "$tar_url" -o "$tmp_tar" 2>/dev/null; then | |
| mkdir -p "$HOME/.local" | |
| rm -rf "$HOME/.local/go" | |
| tar -C "$HOME/.local" -xzf "$tmp_tar" 2>/dev/null | |
| rm -f "$tmp_tar" | |
| export PATH="$HOME/.local/go/bin:$PATH" | |
| else | |
| warn "Could not download Go tarball; falling back to system 'go' if any." | |
| _sudo apt-get install -y golang-go >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| if go_is_recent; then | |
| info "Building from source via go install…" | |
| go install github.com/github/github-mcp-server/cmd/github-mcp-server@latest || return 1 | |
| local gobin="${GOBIN:-${GOPATH:-$HOME/go}/bin}" | |
| if [[ -x "$gobin/github-mcp-server" ]]; then | |
| mkdir -p "$HOME/.local/bin" | |
| ln -sf "$gobin/github-mcp-server" "$HOME/.local/bin/github-mcp-server" | |
| ensure_user_local_bin_on_path | |
| fi | |
| has_cmd github-mcp-server || { err "github-mcp-server not on PATH after go install"; return 1; } | |
| dim "Verify: github-mcp-server --help" | |
| dim "Auth: export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_..." | |
| return 0 | |
| fi | |
| warn "Could not provision Docker or a Go toolchain on this host." | |
| warn "Download a release binary manually:" | |
| open_url "https://github.com/github/github-mcp-server/releases/latest" | |
| return 1 | |
| } | |
| install_elastic_mcp_server() { | |
| step "Installing Elastic MCP Server (mcp-server-elasticsearch / Elastic Cloud)" | |
| ensure_npm || return 1 | |
| # Use npx for on-demand invocation; no global install needed for MCP hosts. | |
| if has_cmd npx; then | |
| info "Elastic MCP servers are typically launched via npx by your MCP host." | |
| info "Common configs (add to your MCP host config — Claude Desktop, Cursor, etc.):" | |
| cat <<'EOF' | |
| Elasticsearch (server-side): | |
| "elasticsearch": { | |
| "command": "npx", | |
| "args": ["-y", "@elastic/mcp-server-elasticsearch"], | |
| "env": { | |
| "ES_URL": "https://...elastic-cloud.com", | |
| "ES_API_KEY": "..." | |
| } | |
| } | |
| Elastic Cloud: | |
| "elastic-cloud": { | |
| "command": "npx", | |
| "args": ["-y", "@elastic/mcp-server-cloud"], | |
| "env": { "EC_API_KEY": "..." } | |
| } | |
| EOF | |
| # Pre-warm both packages so they end up in the npx cache. | |
| npx -y @elastic/mcp-server-elasticsearch --help >/dev/null 2>&1 || true | |
| npx -y @elastic/mcp-server-cloud --help >/dev/null 2>&1 || true | |
| dim "Reference: https://github.com/elastic/mcp-server-elasticsearch" | |
| return 0 | |
| fi | |
| err "npx is required to launch Elastic MCP servers" | |
| return 1 | |
| } | |
| install_zencoder_cli() { | |
| step "Installing Zencoder CLI (via Zenflow desktop)" | |
| # The `zencoder` CLI is bundled with the Zenflow desktop app — Zenflow | |
| # auto-installs and self-updates it. There is no standalone CLI installer. | |
| # Pull the OS-appropriate Zenflow build and unpack/install it; first launch | |
| # of Zenflow drops `zencoder` into PATH and the registry-tracked | |
| # ~/.zencoder/ config dir. | |
| case "$PLATFORM" in | |
| darwin) | |
| local arch="arm64"; [[ "$ARCH_KIND" == "x64" ]] && arch="x64" | |
| local url="https://download.zencoder.ai/zenflowapp/latest/darwin-${arch}/Zenflow.dmg" | |
| local dmg; dmg="$(mktempd zenflow)/zenflow.dmg" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$dmg" || return 1 | |
| local mp | |
| mp="$(hdiutil attach "$dmg" -nobrowse -noautoopen 2>/dev/null \ | |
| | awk -F'\t' '$NF ~ /^\/Volumes\// {mp=$NF} END{print mp}')" | |
| [[ -z "${mp:-}" || ! -d "$mp" ]] && { err "Failed to mount $dmg"; return 1; } | |
| local app_src | |
| app_src="$(find "$mp" -maxdepth 2 -name '*.app' -type d | head -1)" | |
| if [[ -z "$app_src" ]]; then | |
| err "No .app bundle found inside $mp" | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| return 1 | |
| fi | |
| rm -rf "/Applications/$(basename "$app_src")" 2>/dev/null || true | |
| cp -R "$app_src" /Applications/ || cp -R "$app_src" "$HOME/Applications/" || { | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true; return 1; } | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -f "$dmg" | |
| strip_quarantine_app "$(basename "$app_src" .app)" | |
| ;; | |
| linux) | |
| if [[ "$ARCH_KIND" != "x64" ]]; then | |
| warn "Zenflow ships only linux-x64 builds (no native ARM64)." | |
| return 1 | |
| fi | |
| # .deb only — same constraint as Replit Desktop. | |
| if [[ "$PKG_MGR" != "apt" ]]; then | |
| warn "Zenflow ships .deb only; $PKG_MGR-based hosts not supported." | |
| return 1 | |
| fi | |
| local url="https://download.zencoder.ai/zenflowapp/latest/linux-x64/Zenflow.deb" | |
| local deb; deb="$(mktempd zenflow)/zenflow.deb" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$deb" || return 1 | |
| _sudo dpkg -i "$deb" || { _sudo apt-get install -fy && _sudo dpkg -i "$deb"; } || return 1 | |
| rm -f "$deb" | |
| ;; | |
| windows) | |
| if is_windows_arm64; then | |
| warn "Zenflow only ships win32-x64 (no native ARM64)." | |
| warn "Will install via Prism emulation; expect mixed-arch processes." | |
| fi | |
| local url="https://download.zencoder.ai/zenflowapp/latest/win32-x64/Zenflow-Setup.exe" | |
| local exe; exe="$(mktempd zenflow-setup)/setup.exe" | |
| info "Downloading $url" | |
| curl -fsSL "$url" -o "$exe" || return 1 | |
| win_run_silent "$exe" "/S" || { err "Setup failed"; return 1; } | |
| rm -f "$exe" | |
| ;; | |
| esac | |
| dim "Launch Zenflow once — it will install the bundled \`zencoder\` CLI to PATH." | |
| dim "Config: ~/.zencoder/ (created on first run)" | |
| dim "Note: No standalone CLI installer exists; Zenflow is the canonical source." | |
| } | |
| # ── install functions: desktop / IDE apps ──────────────────────────── | |
| install_cursor() { | |
| step "Installing Cursor" | |
| # macOS via brew + Windows via winget take the easy path. | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew; then | |
| brew install --cask cursor || return 1 | |
| strip_quarantine_app "Cursor" | |
| link_macos_ide_cli "cursor" "Cursor" "cursor" | |
| dim "Verify: cursor --version" | |
| return 0 | |
| fi | |
| ;; | |
| windows) | |
| if has_winget && install_with_winget "Anysphere.Cursor"; then | |
| dim "Verify: cursor --version" | |
| return 0 | |
| fi | |
| ;; | |
| esac | |
| # Direct download path. The Cursor "/api/download" endpoint returns | |
| # *JSON metadata*, not the binary — the original script saved the | |
| # 638-byte response as the AppImage / Setup.exe, then chmod'd it and | |
| # it predictably crashed. We have to parse the JSON and follow | |
| # `downloadUrl` to the real CDN file. | |
| local plat="" | |
| case "$PLATFORM" in | |
| darwin) plat="darwin-universal" ;; | |
| linux) plat="linux-${ARCH_KIND}" ;; # linux-x64 / linux-arm64 | |
| windows) plat="win32-${ARCH_KIND}-system" ;; # win32-x64-system / win32-arm64-system | |
| esac | |
| local meta_url="https://www.cursor.com/api/download?platform=${plat}&releaseTrack=stable" | |
| info "Resolving Cursor download URL ($plat)…" | |
| local meta dl_url | |
| if [[ "$PLATFORM" == "windows" ]]; then | |
| # Use PowerShell on Windows — the Microsoft Store python3 stub breaks | |
| # `python3 -c` here on stock Windows Server 2022. | |
| dl_url=$(powershell.exe -NoProfile -Command \ | |
| "\$ProgressPreference='SilentlyContinue'; | |
| try { (Invoke-RestMethod -Uri '$meta_url' -UseBasicParsing).downloadUrl } | |
| catch { '' }" | tr -d '\r\n') | |
| else | |
| meta="$(curl -fsSL -H 'Accept: application/json' "$meta_url")" | |
| dl_url="$(echo "$meta" | json_get downloadUrl)" | |
| fi | |
| [[ -z "$dl_url" ]] && { err "Cursor download API returned no URL"; return 1; } | |
| info "Downloading $dl_url" | |
| case "$PLATFORM" in | |
| darwin) | |
| local d; d="$(mktempd cursor)" | |
| curl -fsSL "$dl_url" -o "$d/cursor.zip" || { rm -rf "$d"; return 1; } | |
| ( cd "$d" && unzip -q cursor.zip ) || { rm -rf "$d"; return 1; } | |
| local app; app="$(find "$d" -maxdepth 2 -name 'Cursor.app' -type d | head -1)" | |
| [[ -z "$app" ]] && { err "No Cursor.app inside zip"; rm -rf "$d"; return 1; } | |
| rm -rf /Applications/Cursor.app | |
| cp -R "$app" /Applications/ || cp -R "$app" "$HOME/Applications/" || { rm -rf "$d"; return 1; } | |
| rm -rf "$d" | |
| strip_quarantine_app "Cursor" | |
| link_macos_ide_cli "cursor" "Cursor" "cursor" | |
| ;; | |
| linux) | |
| mkdir -p "$HOME/.local/bin" "$HOME/Applications" | |
| local dest="$HOME/Applications/Cursor.AppImage" | |
| curl -fsSL "$dl_url" -o "$dest" || return 1 | |
| chmod +x "$dest" | |
| ln -sf "$dest" "$HOME/.local/bin/cursor" | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| local d; d="$(mktempd cursor)" | |
| local dest="$d/CursorSetup.exe" | |
| local dest_win; dest_win="$(cygpath -w "$dest")" | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ | |
| "\$ProgressPreference='SilentlyContinue'; | |
| Invoke-WebRequest -Uri '$dl_url' -OutFile '$dest_win' -UseBasicParsing -TimeoutSec 600" 2>&1 | tail -3 | |
| win_run_silent "$dest" "/VERYSILENT" "/NORESTART" || { err "Cursor installer failed"; rm -rf "$d"; return 1; } | |
| rm -rf "$d" | |
| ;; | |
| esac | |
| dim "Verify: cursor --version" | |
| dim "Config: ~/.cursor/hooks.json" | |
| } | |
| install_windsurf() { | |
| step "Installing Windsurf / Devin Desktop (Cognition — Windsurf rebranded Jun 2026)" | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew && brew install --cask windsurf; then | |
| strip_quarantine_app "Windsurf" | |
| link_macos_ide_cli "windsurf" "Windsurf" "windsurf" | |
| dim "Verify: windsurf --version" | |
| return 0 | |
| fi | |
| ;; | |
| windows) | |
| # Try winget first; fall back to winget-pkgs manifest direct download | |
| # because winget's catalog source can fail to sync on Server 2022. | |
| if has_winget && install_with_winget "Codeium.Windsurf"; then | |
| dim "Verify: windsurf --version" | |
| return 0 | |
| fi | |
| if install_via_winget_manifest "Codeium.Windsurf" "c/Codeium/Windsurf" \ | |
| "/c/Users/$USER/AppData/Local/Programs/Windsurf"; then | |
| dim "Verify: windsurf --version" | |
| return 0 | |
| fi | |
| err "Windsurf install failed"; return 1 | |
| ;; | |
| linux) | |
| # Codeium does not publish a Linux build. | |
| skip_unsupported "Windsurf/Devin Desktop has no Linux build (macOS/Windows only)" | |
| return 2 | |
| ;; | |
| esac | |
| err "No automated install method for Windsurf on $PLATFORM"; return 1 | |
| } | |
| install_claude_desktop() { | |
| install_desktop_app "Claude Desktop (Anthropic)" \ | |
| "claude" \ | |
| "Anthropic.Claude" \ | |
| "" \ | |
| "https://claude.ai/download" || return 1 | |
| dim "Config: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)" | |
| } | |
| install_zed() { | |
| if is_windows_arm64; then | |
| warn "Zed does not currently ship a native Windows ARM64 build." | |
| warn "winget reports 'No applicable installer found' for Zed.Zed on ARM64." | |
| warn "Skipping. Track progress at https://github.com/zed-industries/zed/issues." | |
| return 1 | |
| fi | |
| install_desktop_app "Zed" \ | |
| "zed" \ | |
| "Zed.Zed" \ | |
| "curl -fsSL https://zed.dev/install.sh | sh" \ | |
| "https://zed.dev/download" || return 1 | |
| dim "Verify: zed --version" | |
| dim "Config: ~/.config/zed/settings.json" | |
| } | |
| install_vscodium() { | |
| step "Installing VSCodium" | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew && brew install --cask vscodium; then | |
| strip_quarantine_app "VSCodium" | |
| dim "Verify: codium --version" | |
| return 0 | |
| fi | |
| ;; | |
| windows) | |
| if has_winget && install_with_winget "VSCodium.VSCodium"; then | |
| dim "Verify: codium --version" | |
| return 0 | |
| fi | |
| if install_via_winget_manifest "VSCodium.VSCodium" "v/VSCodium/VSCodium" \ | |
| "/c/Users/$USER/AppData/Local/Programs/VSCodium"; then | |
| dim "Verify: codium --version" | |
| return 0 | |
| fi | |
| ;; | |
| linux) | |
| local arch | |
| case "$ARCH" in | |
| x86_64|amd64) arch=amd64 ;; | |
| aarch64|arm64) arch=arm64 ;; | |
| *) arch="$ARCH" ;; | |
| esac | |
| local pat tmp url | |
| tmp="$(mktempd vscodium)" | |
| case "$PKG_MGR" in | |
| apt) pat="_${arch}\\.deb\"" ;; | |
| dnf|yum) | |
| # VSCodium's RPM filename arch suffix is x86_64 / aarch64, not amd64 / arm64. | |
| local rarch="$arch"; [[ "$arch" == amd64 ]] && rarch="x86_64" | |
| [[ "$arch" == arm64 ]] && rarch="aarch64" | |
| pat="\\.${rarch}\\.rpm\"" | |
| ;; | |
| *) err "Unsupported Linux package manager"; rm -rf "$tmp"; return 1 ;; | |
| esac | |
| url=$(curl -fsSL https://api.github.com/repos/VSCodium/vscodium/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url" \ | |
| | grep -E "$pat" \ | |
| | head -1 \ | |
| | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/') | |
| [[ -z "$url" ]] && { err "Could not locate VSCodium asset for $arch / $PKG_MGR"; rm -rf "$tmp"; return 1; } | |
| local file="$tmp/$(basename "$url")" | |
| if curl -fsSL "$url" -o "$file"; then | |
| install_local_package "$file" || { rm -rf "$tmp"; return 1; } | |
| rm -rf "$tmp" | |
| has_cmd codium && { dim "Verify: codium --version"; return 0; } | |
| fi | |
| rm -rf "$tmp" | |
| ;; | |
| esac | |
| err "VSCodium install failed; download manually from https://vscodium.com/#install" | |
| return 1 | |
| } | |
| install_trae() { | |
| install_desktop_app "TRAE (ByteDance)" \ | |
| "trae" \ | |
| "ByteDance.Trae" \ | |
| "" \ | |
| "https://www.trae.ai/download" || return 1 | |
| link_macos_ide_cli "trae" "Trae" "trae" | |
| dim "Config: ~/Library/Application Support/Trae/ (macOS), %APPDATA%\\Trae\\ (Windows)" | |
| dim "Note: International (trae.ai) and China (trae.cn — brew cask 'trae-cn') variants." | |
| } | |
| install_trae_solo() { | |
| step "Installing TRAE SOLO" | |
| # TRAE SOLO is ByteDance's standalone autonomous coding agent — a separate | |
| # downloadable product from TRAE IDE. Distinct bundle ID (com.trae.solo.app) | |
| # and config dir (~/Library/Application Support/TRAE SOLO/). The trae.ai | |
| # download page is a Next.js SPA whose buttons fire client-side downloads | |
| # against lf-static.traecdn.us. Hit the CDN directly. Apple Silicon only as | |
| # of April 2026; Windows on waitlist; Linux not announced. | |
| local cdn="https://lf-static.traecdn.us/obj/trae-ai-tx/pkg/app/releases/stable" | |
| local default_version="2.3.19982" | |
| local version="${TRAE_SOLO_VERSION:-$default_version}" | |
| case "$PLATFORM" in | |
| darwin) | |
| if [[ "$ARCH" != "arm64" ]]; then | |
| warn "TRAE SOLO ships Apple Silicon only (arm64); current arch=$ARCH. Skipping." | |
| return 0 | |
| fi | |
| local tmp dmg url mp app | |
| url="$cdn/$version/darwin/TRAE_SOLO-darwin-arm64.dmg" | |
| tmp="$(mktemp -d)" | |
| dmg="$tmp/TRAE_SOLO-darwin-arm64.dmg" | |
| info "Downloading TRAE SOLO DMG (~245 MB) from $url …" | |
| curl -fL "$url" -o "$dmg" || { err "Download failed: $url"; rm -rf "$tmp"; return 1; } | |
| [[ -s "$dmg" ]] || { err "Empty DMG"; rm -rf "$tmp"; return 1; } | |
| mp="$(hdiutil attach "$dmg" -nobrowse -noautoopen 2>/dev/null \ | |
| | awk -F'\t' 'BEGIN{IGNORECASE=1} $NF ~ /^\/Volumes\// {mp=$NF} END{print mp}')" | |
| if [[ -z "${mp:-}" || ! -d "$mp" ]]; then | |
| err "Failed to mount $dmg" | |
| rm -rf "$tmp"; return 1 | |
| fi | |
| info "Mounted $mp" | |
| app="" | |
| while IFS= read -r entry; do | |
| case "$entry" in *.app) app="$mp/$entry"; break ;; esac | |
| done < <(ls -1 "$mp" 2>/dev/null) | |
| [[ -z "$app" ]] && app="$(find "$mp" -maxdepth 2 -type d -name '*.app' 2>/dev/null | head -1)" | |
| if [[ -z "$app" ]]; then | |
| err "TRAE SOLO .app bundle not found inside $mp" | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp"; return 1 | |
| fi | |
| rm -rf "/Applications/$(basename "$app")" 2>/dev/null || true | |
| if ! cp -R "$app" /Applications/; then | |
| warn "/Applications not writable; falling back to ~/Applications/" | |
| mkdir -p "$HOME/Applications" | |
| cp -R "$app" "$HOME/Applications/" || { | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp"; return 1 | |
| } | |
| fi | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp" | |
| strip_quarantine_app "$(basename "$app" .app)" | |
| # TRAE SOLO is a VS Code OSS fork; bundled CLI wrapper "trae-solo" lives | |
| # in Contents/Resources/app/bin/. | |
| link_macos_ide_cli "trae-solo" "$(basename "$app" .app)" "trae-solo" | |
| ;; | |
| linux) | |
| warn "TRAE SOLO does not yet ship for Linux (as of April 2026). Skipping." | |
| return 0 | |
| ;; | |
| windows) | |
| warn "TRAE SOLO Windows build is on the waitlist (https://www.trae.ai/download). Skipping." | |
| return 0 | |
| ;; | |
| *) | |
| warn "Unsupported platform for TRAE SOLO: $PLATFORM" | |
| return 1 | |
| ;; | |
| esac | |
| dim "Config: ~/Library/Application Support/TRAE SOLO/ (macOS only)" | |
| dim "Bundle ID: com.trae.solo.app — distinct from TRAE IDE (com.trae.app)." | |
| dim "Signer: Developer ID Application: SPRING (SG) PTE. LTD. (TeamID 79M8227NKH) — same as TRAE IDE." | |
| } | |
| install_pearai() { | |
| step "Installing PearAI" | |
| # PearAI does not publish brew/winget packages and the trypear.ai/beta page is | |
| # a Next.js SPA whose download buttons are wired to a DigitalOcean Spaces CDN | |
| # at https://pearai-app.nyc3.digitaloceanspaces.com/PearAI-latest/<platform>/. | |
| # GitHub releases only ship Linux tarballs. Hit the CDN directly for darwin | |
| # and windows, and fall back to GitHub releases for linux. | |
| local cdn="https://pearai-app.nyc3.digitaloceanspaces.com/PearAI-latest" | |
| case "$PLATFORM" in | |
| darwin) | |
| local arch_seg="darwin-arm64" | |
| [[ "$ARCH" == "x86_64" ]] && arch_seg="darwin-x64" | |
| local tmp dmg url | |
| url="$cdn/$arch_seg/PearAI-Installer.dmg" | |
| tmp="$(mktemp -d)" | |
| dmg="$tmp/PearAI-Installer.dmg" | |
| info "Downloading PearAI DMG (~330 MB)…" | |
| curl -fL "$url" -o "$dmg" || { err "Download failed: $url"; rm -rf "$tmp"; return 1; } | |
| [[ -s "$dmg" ]] || { err "Empty DMG"; rm -rf "$tmp"; return 1; } | |
| local mp | |
| mp="$(hdiutil attach "$dmg" -nobrowse -noautoopen 2>/dev/null \ | |
| | awk -F'\t' 'BEGIN{IGNORECASE=1} $NF ~ /^\/Volumes\// {mp=$NF} END{print mp}')" | |
| if [[ -z "${mp:-}" || ! -d "$mp" ]]; then | |
| err "Failed to mount $dmg" | |
| rm -rf "$tmp"; return 1 | |
| fi | |
| info "Mounted $mp" | |
| local app | |
| app="$(find "$mp" -maxdepth 2 -type d -name 'PearAI*.app' 2>/dev/null | head -1)" | |
| if [[ -z "$app" ]]; then | |
| err "PearAI .app bundle not found inside $mp" | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp"; return 1 | |
| fi | |
| rm -rf "/Applications/$(basename "$app")" 2>/dev/null || true | |
| if ! cp -R "$app" /Applications/; then | |
| warn "/Applications not writable; falling back to ~/Applications/" | |
| mkdir -p "$HOME/Applications" | |
| cp -R "$app" "$HOME/Applications/" || { | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp"; return 1 | |
| } | |
| fi | |
| hdiutil detach "$mp" -quiet 2>/dev/null || true | |
| rm -rf "$tmp" | |
| strip_quarantine_app "$(basename "$app" .app)" | |
| # PearAI inherits VS Code OSS bundling — its CLI binary is named "code" | |
| # inside the bundle. Link it as `pearai` for unambiguous attribution. | |
| link_macos_ide_cli "pearai" "$(basename "$app" .app)" "code" | |
| ;; | |
| linux) | |
| local url tmp | |
| url="$(curl -fsSL https://api.github.com/repos/trypear/pearai-app/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url.*PearAI-linux\.tar\.gz" \ | |
| | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')" | |
| [[ -z "$url" ]] && { err "Could not locate PearAI Linux tarball on GitHub releases."; return 1; } | |
| tmp="$(mktemp -d)" | |
| curl -fsSL "$url" -o "$tmp/pearai.tar.gz" || { rm -rf "$tmp"; return 1; } | |
| mkdir -p "$HOME/.local/share" | |
| tar -xzf "$tmp/pearai.tar.gz" -C "$HOME/.local/share/" || { rm -rf "$tmp"; return 1; } | |
| rm -rf "$tmp" | |
| mkdir -p "$HOME/.local/bin" | |
| local bin | |
| bin="$(find "$HOME/.local/share" -maxdepth 3 -type f -name 'pearai' 2>/dev/null | head -1)" | |
| [[ -n "$bin" ]] && ln -sf "$bin" "$HOME/.local/bin/pearai" && ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| local url tmp exe | |
| url="$cdn/windows-x64/PearAISetup.exe" | |
| tmp="$(mktemp -d)" | |
| exe="$tmp/PearAISetup.exe" | |
| info "Downloading PearAI Windows installer (~454 MB)…" | |
| curl -fL "$url" -o "$exe" || { err "Download failed: $url"; rm -rf "$tmp"; return 1; } | |
| info "Launching installer…" | |
| cmd.exe /c "$(cygpath -w "$exe" 2>/dev/null || echo "$exe") /S" || true | |
| rm -f "$exe" | |
| ;; | |
| esac | |
| dim "Config: ~/Library/Application Support/PearAI/ (macOS), ~/.config/PearAI/ (Linux), %APPDATA%\\PearAI\\ (Windows)" | |
| dim "Note: VS Code fork. Combines PearAI Router + PearAI Agent (Roo/Cline lineage)." | |
| } | |
| install_void_editor() { | |
| warn "Void Editor: project status is paused (no recent commits). Existing builds still ship." | |
| install_desktop_app "Void Editor" \ | |
| "void" \ | |
| "" \ | |
| "" \ | |
| "https://voideditor.com/download-beta" || return 1 | |
| link_macos_ide_cli "void" "Void" "void" | |
| dim "Config: ~/Library/Application Support/Void/ (macOS)" | |
| dim "Releases: https://github.com/voideditor/void/releases" | |
| } | |
| # ── install functions: local model servers ─────────────────────────── | |
| install_ollama() { | |
| step "Installing Ollama" | |
| local rc=0 | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew; then | |
| brew install ollama || rc=$? | |
| else | |
| info "Download from: https://ollama.com/download" | |
| open_url "https://ollama.com/download" | |
| return 1 | |
| fi | |
| ;; | |
| linux) | |
| # The vendor install script writes to /etc/systemd, /usr/share, etc. | |
| # and uses sudo internally — without an askpass it hangs forever in | |
| # non-interactive SSH sessions. | |
| local d; d="$(mktempd ollama)" | |
| curl -fsSL https://ollama.com/install.sh -o "$d/ollama-install.sh" || { rm -rf "$d"; return 1; } | |
| _run_with_sudo_askpass "$d/ollama-install.sh" || rc=$? | |
| rm -rf "$d" | |
| ;; | |
| windows) | |
| if has_winget && install_with_winget "Ollama.Ollama"; then | |
| dim "Verify: ollama --version" | |
| return 0 | |
| fi | |
| # winget catalog hiccup on Server 2022 — pull the manifest directly. | |
| if install_via_winget_manifest "Ollama.Ollama" "o/Ollama/Ollama" \ | |
| "/c/Users/$USER/AppData/Local/Programs/Ollama" \ | |
| "/SP- /VERYSILENT /NORESTART /SUPPRESSMSGBOXES"; then | |
| dim "Verify: ollama --version" | |
| return 0 | |
| fi | |
| info "Download manually from: https://ollama.com/download" | |
| open_url "https://ollama.com/download" | |
| return 1 | |
| ;; | |
| esac | |
| [[ $rc -ne 0 ]] && return 1 | |
| dim "Verify: ollama --version" | |
| dim "Models: ~/.ollama/models/" | |
| dim "API: http://127.0.0.1:11434" | |
| } | |
| install_lm_studio() { | |
| step "Installing LM Studio" | |
| local rc=0 | |
| case "$PLATFORM" in | |
| darwin) | |
| if has_brew; then | |
| brew install --cask lm-studio || rc=$? | |
| else | |
| info "Download from: https://lmstudio.ai" | |
| open_url "https://lmstudio.ai" | |
| return 1 | |
| fi | |
| ;; | |
| linux) | |
| if [[ "$ARCH_KIND" != "x64" ]]; then | |
| warn "LM Studio publishes only Linux x64 AppImage (no native ARM64)." | |
| return 1 | |
| fi | |
| # The AppImage runtime needs libatomic; AL2023 / minimal Ubuntu cloud | |
| # images don't ship it. Install it best-effort first. | |
| case "$PKG_MGR" in | |
| dnf|yum) _sudo "$PKG_MGR" install -y libatomic >/dev/null 2>&1 || true ;; | |
| apt) _sudo apt-get install -y libatomic1 >/dev/null 2>&1 || true ;; | |
| esac | |
| local d; d="$(mktempd lmstudio)" | |
| curl -fsSL https://lmstudio.ai/install.sh -o "$d/lms-install.sh" || { rm -rf "$d"; return 1; } | |
| _run_with_sudo_askpass "$d/lms-install.sh" || rc=$? | |
| rm -rf "$d" | |
| ;; | |
| windows) | |
| if has_winget && install_with_winget "ElementLabs.LMStudio"; then | |
| dim "Verify: lms --version (after: npx lmstudio install-cli)" | |
| return 0 | |
| fi | |
| if install_via_winget_manifest "ElementLabs.LMStudio" "e/ElementLabs/LMStudio" \ | |
| "/c/Users/$USER/AppData/Local/LM-Studio" \ | |
| "/S"; then | |
| dim "Verify: lms --version (after: npx lmstudio install-cli)" | |
| return 0 | |
| fi | |
| info "Download manually from: https://lmstudio.ai" | |
| open_url "https://lmstudio.ai" | |
| return 1 | |
| ;; | |
| esac | |
| [[ $rc -ne 0 ]] && return 1 | |
| dim "Verify: lms --version (after: npx lmstudio install-cli)" | |
| dim "API: http://127.0.0.1:1234" | |
| } | |
| install_llama_cpp() { | |
| step "Installing llama.cpp" | |
| if has_brew; then | |
| brew install llama.cpp | |
| elif [[ "$PLATFORM" == "windows" ]] && has_winget; then | |
| install_with_winget "ggerganov.llama.cpp" | |
| elif [[ "$PLATFORM" == "linux" ]]; then | |
| local tmpdir | |
| tmpdir="$(mktemp -d)" | |
| info "Downloading prebuilt binary from GitHub releases..." | |
| local dl_url | |
| dl_url="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null \ | |
| | grep 'browser_download_url.*ubuntu-x64\.tar\.gz' \ | |
| | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')" | |
| if [[ -n "$dl_url" ]] && curl -fsSL "$dl_url" -o "$tmpdir/llama.tar.gz" 2>/dev/null; then | |
| mkdir -p "$tmpdir/llama-bin" | |
| tar xzf "$tmpdir/llama.tar.gz" -C "$tmpdir/llama-bin" 2>/dev/null | |
| local bin_dir | |
| bin_dir="$(find "$tmpdir/llama-bin" -name 'llama-server' -type f 2>/dev/null | head -1 | xargs dirname 2>/dev/null)" | |
| if [[ -n "$bin_dir" ]]; then | |
| _sudo cp "$bin_dir"/llama-* /usr/local/bin/ 2>/dev/null || \ | |
| { mkdir -p "$HOME/.local/bin" && cp "$bin_dir"/llama-* "$HOME/.local/bin/" 2>/dev/null; } | |
| else | |
| warn "Could not locate binaries in release archive" | |
| info "Build from source: git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp && cmake -B build && cmake --build build" | |
| rm -rf "$tmpdir" | |
| return 1 | |
| fi | |
| rm -rf "$tmpdir" | |
| else | |
| warn "Prebuilt binary download failed" | |
| info "Build from source: git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp && cmake -B build && cmake --build build" | |
| rm -rf "$tmpdir" | |
| return 1 | |
| fi | |
| else | |
| info "Build from source: https://github.com/ggml-org/llama.cpp" | |
| return 1 | |
| fi | |
| dim "Verify: llama-server --version" | |
| dim "API: http://127.0.0.1:8080" | |
| } | |
| install_jan() { | |
| install_desktop_app "Jan" \ | |
| "jan" \ | |
| "Jan.Jan" \ | |
| "" \ | |
| "https://jan.ai/download" || return 1 | |
| dim "Data: ~/jan/" | |
| } | |
| install_gpt4all() { | |
| install_desktop_app "GPT4All" \ | |
| "gpt4all" \ | |
| "NomicAI.GPT4All" \ | |
| "" \ | |
| "https://www.nomic.ai/gpt4all" || return 1 | |
| } | |
| install_koboldcpp() { | |
| step "Installing KoboldCpp (LostRuins)" | |
| # KoboldCpp publishes single-file prebuilt binaries on GitHub releases. | |
| # No brew/winget/npm/pip distribution. | |
| local asset="" | |
| case "$PLATFORM" in | |
| darwin) | |
| [[ "$ARCH_KIND" == "arm64" ]] && asset="koboldcpp-mac-arm64" || asset="" | |
| [[ -z "$asset" ]] && { warn "KoboldCpp ships only mac arm64; macOS x64 unsupported."; return 1; } | |
| ;; | |
| linux) | |
| [[ "$ARCH_KIND" == "x64" ]] && asset="koboldcpp-linux-x64" || asset="" | |
| [[ -z "$asset" ]] && { warn "KoboldCpp ships only linux x64; ARM Linux unsupported."; return 1; } | |
| ;; | |
| windows) | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| warn "KoboldCpp does not ship a Windows ARM64 binary. Skipping." | |
| return 1 | |
| fi | |
| asset="koboldcpp.exe" | |
| ;; | |
| esac | |
| local url | |
| url="$(curl -fsSL https://api.github.com/repos/LostRuins/koboldcpp/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url.*${asset}\(\"\|$\)" \ | |
| | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')" | |
| if [[ -z "$url" ]]; then | |
| err "Could not locate ${asset} on the latest KoboldCpp release." | |
| return 1 | |
| fi | |
| mkdir -p "$HOME/.local/bin" | |
| local dest="$HOME/.local/bin/${asset}" | |
| if curl -fsSL "$url" -o "$dest"; then | |
| chmod +x "$dest" | |
| [[ "$PLATFORM" != "windows" ]] && ln -sf "$dest" "$HOME/.local/bin/koboldcpp" | |
| ensure_user_local_bin_on_path | |
| else | |
| err "Failed to download KoboldCpp from $url" | |
| return 1 | |
| fi | |
| dim "Verify: koboldcpp --version" | |
| dim "API: http://127.0.0.1:5001 (no auth by default — bind to localhost only!)" | |
| } | |
| install_textgen_webui() { | |
| step "Installing Text Generation WebUI (oobabooga)" | |
| # No package manager distribution. Standard install: clone repo + run start_*.sh. | |
| if is_windows_arm64; then | |
| warn "textgen-webui has no native Windows ARM64 wheels for torch/exllama." | |
| warn "Use WSL or x64 Windows. Skipping." | |
| return 1 | |
| fi | |
| if ! has_cmd git; then | |
| err "git is required to clone the textgen-webui repository" | |
| return 1 | |
| fi | |
| local dest="$HOME/text-generation-webui" | |
| if [[ -d "$dest/.git" ]]; then | |
| info "Repo already cloned at $dest — pulling latest." | |
| git -C "$dest" pull --ff-only || true | |
| else | |
| git clone --depth 1 https://github.com/oobabooga/text-generation-webui.git "$dest" || return 1 | |
| fi | |
| case "$PLATFORM" in | |
| darwin) dim "Run: cd $dest && bash start_macos.sh" ;; | |
| linux) dim "Run: cd $dest && bash start_linux.sh" ;; | |
| windows) dim "Run: cd $dest && start_windows.bat (or start_wsl.bat)" ;; | |
| esac | |
| dim "API: http://127.0.0.1:7860 (UI), 127.0.0.1:5000 (OpenAI-compat, env OPENEDAI_PORT)" | |
| } | |
| install_tabby_server() { | |
| step "Installing Tabby (TabbyML server)" | |
| # Tabby ships prebuilt binaries on GitHub releases. Most prod deployments | |
| # run dockerized; we install the binary directly for local validation. | |
| local asset="" | |
| case "$PLATFORM" in | |
| darwin) | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| asset="tabby_aarch64-apple-darwin.tar.gz" | |
| else | |
| warn "Tabby ships only Apple Silicon on macOS; macOS x64 not supported." | |
| return 1 | |
| fi | |
| ;; | |
| linux) | |
| [[ "$ARCH_KIND" == "x64" ]] && asset="tabby_x86_64-manylinux_2_28.tar.gz" \ | |
| || { warn "Tabby Linux is x86_64 only."; return 1; } | |
| ;; | |
| windows) | |
| if has_cmd docker; then | |
| info "Use docker: docker run -d --gpus all -p 8080:8080 \\" | |
| info " -v \$HOME/.tabby:/data registry.tabbyml.com/tabbyml/tabby \\" | |
| info " serve --model StarCoder-1B --device cuda" | |
| return 1 | |
| fi | |
| warn "No native Windows binary; install Docker Desktop first." | |
| return 1 | |
| ;; | |
| esac | |
| local url | |
| url="$(curl -fsSL https://api.github.com/repos/TabbyML/tabby/releases/latest 2>/dev/null \ | |
| | grep "browser_download_url.*${asset}\(\"\|$\)" \ | |
| | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')" | |
| if [[ -z "$url" ]]; then | |
| err "Could not locate ${asset} on the latest Tabby release." | |
| return 1 | |
| fi | |
| local tmpdir; tmpdir="$(mktemp -d)" | |
| if curl -fsSL "$url" -o "$tmpdir/tabby.dl"; then | |
| mkdir -p "$HOME/.local/bin" | |
| if [[ "$asset" == *.tar.gz ]]; then | |
| tar -xzf "$tmpdir/tabby.dl" -C "$tmpdir" | |
| local bin | |
| bin="$(find "$tmpdir" -maxdepth 4 -type f -name 'tabby' 2>/dev/null | head -1)" | |
| [[ -n "$bin" ]] && install -m 0755 "$bin" "$HOME/.local/bin/tabby" | |
| else | |
| install -m 0755 "$tmpdir/tabby.dl" "$HOME/.local/bin/tabby" | |
| fi | |
| rm -rf "$tmpdir" | |
| ensure_user_local_bin_on_path | |
| else | |
| rm -rf "$tmpdir" | |
| err "Failed to download Tabby from $url" | |
| return 1 | |
| fi | |
| has_cmd tabby || { err "tabby binary not found after install"; return 1; } | |
| dim "Verify: tabby --version" | |
| dim "Run: tabby serve --model StarCoder-1B" | |
| dim "API: http://127.0.0.1:8080" | |
| dim "Config: ~/.tabby/" | |
| } | |
| # ── install functions: VS Code extensions ──────────────────────────── | |
| find_ide_cli() { | |
| if has_cmd code; then echo "code"; return; fi | |
| if has_cmd cursor; then echo "cursor"; return; fi | |
| if has_cmd codium; then echo "codium"; return; fi | |
| local app_cli="/Applications/Cursor.app/Contents/Resources/app/bin/cursor" | |
| if [[ -x "$app_cli" ]]; then echo "$app_cli"; return; fi | |
| return 1 | |
| } | |
| install_vscode_ext() { | |
| local name="$1" ext_id="$2" | |
| step "Installing $name" | |
| # Some extensions (Roo Code, Kilo Code) require a recent VS Code engine. | |
| # main() already calls update_vscode_if_old once, but if the user invoked | |
| # `--tool ext-roo-code` directly we still want the safety net here. The | |
| # function is a no-op on already-current installs. | |
| update_vscode_if_old | |
| local cli | |
| cli="$(find_ide_cli)" || { err "VS Code, Cursor, or VSCodium required"; return 1; } | |
| if "$cli" --install-extension "$ext_id" 2>&1 | tee /tmp/_ext_$$.log | grep -qE "successfully installed|is already installed"; then | |
| rm -f /tmp/_ext_$$.log | |
| return 0 | |
| fi | |
| # Cursor / VSCodium use Open VSX. Some extensions (Microsoft-proprietary like | |
| # github.copilot, vendor-only like blackboxapp.blackbox, cognition.devin) are | |
| # only on the Microsoft Marketplace. Fall back to direct VSIX side-load. | |
| if grep -q "not found" /tmp/_ext_$$.log 2>/dev/null; then | |
| rm -f /tmp/_ext_$$.log | |
| install_vscode_ext_vsix "$name" "$ext_id" | |
| return $? | |
| fi | |
| rm -f /tmp/_ext_$$.log | |
| return 1 | |
| } | |
| # Side-load a VSIX from the Microsoft Marketplace for IDEs whose default | |
| # registry is Open VSX (Cursor, VSCodium, etc.). | |
| install_vscode_ext_vsix() { | |
| local name="$1" ext_id="$2" | |
| local publisher="${ext_id%%.*}" ext_name="${ext_id#*.}" | |
| info "$name not in Open VSX; side-loading VSIX from MS Marketplace…" | |
| local cli | |
| cli="$(find_ide_cli)" || return 1 | |
| local tmp vsix url | |
| tmp="$(mktemp -d)" | |
| vsix="$tmp/${ext_id}.vsix" | |
| url="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${publisher}/vsextensions/${ext_name}/latest/vspackage" | |
| if curl -fsSL --compressed -A "VSCode 1.96.0" "$url" -o "$vsix" \ | |
| && [[ -s "$vsix" ]] \ | |
| && "$cli" --install-extension "$vsix"; then | |
| rm -rf "$tmp" | |
| return 0 | |
| fi | |
| rm -rf "$tmp" | |
| err "Could not install $name. Manual: https://marketplace.visualstudio.com/items?itemName=${ext_id}" | |
| return 1 | |
| } | |
| install_ext_cline() { install_vscode_ext "Cline (Claude Dev)" "saoudrizwan.claude-dev"; } | |
| install_ext_continue() { install_vscode_ext "Continue" "Continue.continue"; } | |
| install_ext_copilot() { | |
| install_vscode_ext "GitHub Copilot" "GitHub.copilot" || return 1 | |
| # GitHub.copilot-chat is bundled with new VS Code releases; trying to | |
| # install the marketplace version on top of the built-in often fails with | |
| # a dependency-conflict (eg. built-in 0.46.0 vs. marketplace 0.45.1). Treat | |
| # an existing extension as success regardless of the install-extension exit. | |
| if has_cmd code; then | |
| code --install-extension GitHub.copilot-chat 2>/dev/null | |
| code --list-extensions 2>/dev/null | grep -qi 'github.copilot-chat' || \ | |
| warn "GitHub Copilot Chat not detected after install (built-in may be sufficient)." | |
| fi | |
| } | |
| install_ext_tabnine() { install_vscode_ext "Tabnine" "TabNine.tabnine-vscode"; } | |
| install_ext_roo_code() { install_vscode_ext "Roo Code" "RooVeterinaryInc.roo-cline"; } | |
| install_ext_supermaven() { install_vscode_ext "Supermaven" "supermaven.supermaven"; } | |
| install_ext_kilo_code() { install_vscode_ext "Kilo Code" "kilocode.kilo-code"; } | |
| install_ext_blackbox_ai(){ install_vscode_ext "Blackbox AI" "Blackboxapp.blackbox"; } | |
| # ── register all tools ─────────────────────────────────────────────── | |
| # ── 1.7.0 additions: Cursor Agent CLI, Cline CLI, Warp, Pi, Plandex ── | |
| check_cursor_agent() { has_cmd agent || has_cmd cursor-agent; } | |
| install_cursor_agent() { | |
| step "Installing Cursor Agent CLI (agent / cursor-agent)" | |
| if check_cursor_agent; then | |
| dim "Already present: $(command -v agent 2>/dev/null || command -v cursor-agent)" | |
| return 0 | |
| fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| darwin|linux) | |
| curl -fsS https://cursor.com/install | bash || return 1 | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| if command -v powershell.exe >/dev/null 2>&1; then | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ | |
| "irm 'https://cursor.com/install?win32=true' | iex" || return 1 | |
| ensure_windows_user_bins_on_path | |
| else | |
| err "Need PowerShell for Cursor Agent CLI on native Windows" | |
| return 1 | |
| fi | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| # Common install locations if not yet on PATH | |
| local cand | |
| for cand in "$HOME/.local/bin/agent" "$HOME/.local/bin/cursor-agent" \ | |
| "$HOME/.cursor/bin/agent" "$HOME/.cursor-agent/agent"; do | |
| [[ -x "$cand" ]] || continue | |
| case ":$PATH:" in *":$(dirname "$cand"):"*) ;; *) export PATH="$(dirname "$cand"):$PATH" ;; esac | |
| done | |
| check_cursor_agent || { err "agent/cursor-agent not found after install"; return 1; } | |
| dim "Verify: agent --version" | |
| dim "Docs: https://cursor.com/docs/cli/overview" | |
| } | |
| install_cline_cli() { | |
| step "Installing Cline CLI (standalone terminal agent)" | |
| if has_cmd cline; then | |
| dim "Already on PATH: $(command -v cline)" | |
| return 0 | |
| fi | |
| ensure_npm || return 1 | |
| npm_install_g cline || return 1 | |
| ensure_windows_user_bins_on_path | |
| has_cmd cline || has_npm_bin cline || { err "cline binary not found after install"; return 1; } | |
| dim "Verify: cline --version" | |
| dim "Auth: cline auth" | |
| dim "Docs: https://docs.cline.bot/cli/cli-reference" | |
| dim "Note: VS Code extension remains --tool ext-cline / --tool cline (alias)" | |
| } | |
| install_warp() { | |
| step "Installing Warp (agentic terminal / ADE)" | |
| # Native Windows (Git Bash / MSYS) — winget | |
| if [[ "$PLATFORM" == "windows" && "$IS_WSL" != true ]]; then | |
| if has_winget; then | |
| install_with_winget "Warp.Warp" || return 1 | |
| ensure_windows_user_bins_on_path | |
| else | |
| open_url "https://www.warp.dev/download" | |
| return 1 | |
| fi | |
| check_warp || warn "Warp installed but app not detected yet — launch once" | |
| dim "Verify: Warp from Start Menu" | |
| dim "Docs: https://docs.warp.dev/" | |
| return 0 | |
| fi | |
| # macOS | |
| if [[ "$PLATFORM" == "darwin" ]]; then | |
| if has_brew; then | |
| brew install --cask warp || return 1 | |
| strip_quarantine_app "Warp" | |
| else | |
| open_url "https://www.warp.dev/download" | |
| return 1 | |
| fi | |
| check_warp || warn "Warp.app installed — launch once" | |
| dim "Verify: open Warp.app" | |
| dim "Docs: https://docs.warp.dev/" | |
| return 0 | |
| fi | |
| # Linux or WSL (Linux userland packages) | |
| detect_pkg_mgr | |
| local tmp pkg_url pkg_file | |
| tmp="$(mktempd warp)" | |
| case "$PKG_MGR" in | |
| apt) | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| pkg_url="https://app.warp.dev/download?package=deb_arm64" | |
| else | |
| pkg_url="https://app.warp.dev/download?package=deb" | |
| fi | |
| pkg_file="$tmp/warp-terminal.deb" | |
| info "Downloading Warp .deb ($ARCH_KIND)…" | |
| curl -fsSL -L "$pkg_url" -o "$pkg_file" || { rm -rf "$tmp"; return 1; } | |
| install_local_package "$pkg_file" || { rm -rf "$tmp"; return 1; } | |
| ;; | |
| dnf|yum) | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| pkg_url="https://app.warp.dev/download?package=rpm_arm64" | |
| else | |
| pkg_url="https://app.warp.dev/download?package=rpm" | |
| fi | |
| pkg_file="$tmp/warp-terminal.rpm" | |
| info "Downloading Warp .rpm ($ARCH_KIND)…" | |
| curl -fsSL -L "$pkg_url" -o "$pkg_file" || { rm -rf "$tmp"; return 1; } | |
| install_local_package "$pkg_file" || { rm -rf "$tmp"; return 1; } | |
| ;; | |
| *) | |
| if [[ "$ARCH_KIND" == "arm64" ]]; then | |
| pkg_url="https://app.warp.dev/download?package=appimage_arm64" | |
| pkg_file="$HOME/.local/bin/Warp-ARM64.AppImage" | |
| else | |
| pkg_url="https://app.warp.dev/download?package=appimage" | |
| pkg_file="$HOME/.local/bin/Warp-x64.AppImage" | |
| fi | |
| mkdir -p "$HOME/.local/bin" | |
| info "Downloading Warp AppImage ($ARCH_KIND)…" | |
| curl -fsSL -L "$pkg_url" -o "$pkg_file" || { rm -rf "$tmp"; return 1; } | |
| chmod +x "$pkg_file" | |
| ensure_user_local_bin_on_path | |
| ln -sfn "$pkg_file" "$HOME/.local/bin/warp-terminal" 2>/dev/null || true | |
| ;; | |
| esac | |
| rm -rf "$tmp" | |
| has_cmd warp-terminal || has_app Warp || warn "Warp package installed — run warp-terminal once" | |
| dim "Verify: warp-terminal" | |
| dim "Docs: https://docs.warp.dev/" | |
| } | |
| install_pi() { | |
| step "Installing Pi coding agent (Earendil / @earendil-works/pi-coding-agent)" | |
| if has_cmd pi; then | |
| dim "Already on PATH: $(command -v pi)" | |
| return 0 | |
| fi | |
| ensure_npm || return 1 | |
| # Official docs recommend --ignore-scripts for npm global installs | |
| npm_install_g --ignore-scripts @earendil-works/pi-coding-agent || return 1 | |
| has_cmd pi || has_npm_bin pi || { err "pi binary not found after install"; return 1; } | |
| dim "Verify: pi --version" | |
| dim "Docs: https://github.com/earendil-works/pi" | |
| } | |
| install_plandex() { | |
| step "Installing Plandex (multi-step coding CLI)" | |
| if has_cmd plandex || has_cmd pdx; then | |
| dim "Already on PATH: $(command -v plandex 2>/dev/null || command -v pdx)" | |
| return 0 | |
| fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| darwin|linux) | |
| curl -sL https://plandex.ai/install.sh | bash || return 1 | |
| ensure_user_local_bin_on_path | |
| ;; | |
| windows) | |
| skip_unsupported "Plandex has no native Windows build — use WSL (then re-run here)" | |
| return 2 | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| has_cmd plandex || has_cmd pdx || { err "plandex not found after install"; return 1; } | |
| dim "Verify: plandex --version (alias: pdx)" | |
| dim "Docs: https://docs.plandex.ai/" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # 1.8.0 — endpoint-scope tools (terminal GenAI binaries) | |
| # Scope: anything that installs a terminal-accessible AI/LLM executable. | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| # ── coding / agent CLIs ─────────────────────────────────────────────── | |
| install_command_code() { | |
| install_npm_tool "command-code" "Command Code" || return 1 | |
| has_cmd command-code || has_npm_bin command-code || has_cmd cmdc || return 1 | |
| dim "Verify: command-code --help" | |
| } | |
| install_codegpt() { | |
| install_npm_tool "codegpt" "CodeGPT CLI" || return 1 | |
| has_cmd codegpt || has_npm_bin codegpt || return 1 | |
| dim "Verify: codegpt --help" | |
| } | |
| install_open_interpreter() { | |
| install_python_tool "open-interpreter" "Open Interpreter" || return 1 | |
| has_cmd interpreter || { err "interpreter not on PATH"; return 1; } | |
| dim "Verify: interpreter --help" | |
| } | |
| install_vibe_cli() { | |
| # Ambiguous ecosystem; prefer the coding-oriented npm if present later. | |
| # Fallback: community vibe packages are often unrelated — skip if unknown. | |
| step "Installing Vibe CLI" | |
| if has_cmd vibe; then dim "Already on PATH"; return 0; fi | |
| ensure_npm || return 1 | |
| # Try known coding agent package names; soft-fail to unsupported | |
| if npm view @vibe/cli name &>/dev/null; then | |
| npm_install_g "@vibe/cli" || return 1 | |
| elif npm view vibe-coding-cli name &>/dev/null; then | |
| npm_install_g "vibe-coding-cli" || return 1 | |
| else | |
| skip_unsupported "No trusted Vibe coding CLI package found on npm — install manually if you have a vendor URL" | |
| return 2 | |
| fi | |
| ensure_windows_user_bins_on_path | |
| has_cmd vibe || has_npm_bin vibe || return 1 | |
| } | |
| install_repoprompt() { | |
| step "Installing RepoPrompt" | |
| if has_cmd repoprompt; then return 0; fi | |
| if has_brew && brew info repoprompt &>/dev/null; then | |
| brew install repoprompt || return 1 | |
| elif ensure_npm && npm view repoprompt name &>/dev/null; then | |
| npm_install_g repoprompt || return 1 | |
| else | |
| open_url "https://repoprompt.com" | |
| skip_unsupported "RepoPrompt: no cross-platform package — opened vendor download page" | |
| return 2 | |
| fi | |
| has_cmd repoprompt || has_npm_bin repoprompt || return 1 | |
| } | |
| install_roo_cli() { | |
| # Roo Code is primarily a VS Code extension; no official standalone `roo` CLI | |
| # as of 1.8.0. Map to extension install for endpoint coverage of the product. | |
| step "Installing Roo CLI surface (VS Code extension — no official standalone CLI)" | |
| install_ext_roo_code || return 1 | |
| write_local_shim "roo" "echo" "roo: use Roo Code VS Code extension (ext-roo-code); no official CLI binary yet" | |
| dim "Note: shim only — real surface is the VS Code extension" | |
| } | |
| # ── chat / prompt CLIs ──────────────────────────────────────────────── | |
| check_fabric() { has_cmd fabric || has_cmd fabric-ai; } | |
| install_fabric() { | |
| step "Installing Fabric (danielmiessler)" | |
| if has_cmd fabric || has_cmd fabric-ai; then return 0; fi | |
| if has_brew; then | |
| brew install fabric-ai || true | |
| if has_cmd fabric-ai && ! has_cmd fabric; then | |
| write_local_shim "fabric" "fabric-ai" | |
| fi | |
| fi | |
| if ! has_cmd fabric && ! has_cmd fabric-ai; then | |
| install_go_tool "github.com/danielmiessler/fabric/cmd/fabric@latest" "Fabric" || return 1 | |
| fi | |
| has_cmd fabric || has_cmd fabric-ai || return 1 | |
| dim "Verify: fabric --help (Homebrew may expose fabric-ai)" | |
| } | |
| install_shellgpt() { | |
| install_python_tool "shell-gpt" "ShellGPT" || return 1 | |
| has_cmd sgpt || return 1 | |
| dim "Verify: sgpt --version" | |
| } | |
| install_tgpt() { | |
| step "Installing tgpt" | |
| if has_cmd tgpt; then return 0; fi | |
| if has_brew; then | |
| brew install tgpt && return 0 | |
| fi | |
| # Go binary releases are the canonical path; npm package also exists | |
| if install_go_tool "github.com/aandrew-me/tgpt@latest" "tgpt" 2>/dev/null; then | |
| has_cmd tgpt && return 0 | |
| fi | |
| install_npm_tool "tgpt" "tgpt" || return 1 | |
| has_cmd tgpt || has_npm_bin tgpt || return 1 | |
| } | |
| install_mods() { | |
| step "Installing mods (Charmbracelet)" | |
| if has_cmd mods; then return 0; fi | |
| if has_brew; then | |
| brew install mods && return 0 | |
| fi | |
| install_go_tool "github.com/charmbracelet/mods@latest" "mods" || return 1 | |
| has_cmd mods || return 1 | |
| dim "Verify: mods --help" | |
| } | |
| install_askai() { | |
| install_python_tool "askai" "AskAI CLI" || return 1 | |
| has_cmd askai || return 1 | |
| } | |
| install_gptme() { | |
| install_python_tool "gptme" "GPTMe" || return 1 | |
| has_cmd gptme || return 1 | |
| dim "Verify: gptme --version" | |
| } | |
| install_gpterm() { | |
| install_python_tool "gpterm" "GPTerm" || return 1 | |
| has_cmd gpterm || return 1 | |
| } | |
| install_gptshell() { | |
| install_python_tool "gptshell" "GPTShell" || return 1 | |
| has_cmd gptshell || return 1 | |
| } | |
| install_chatgpt_cli() { | |
| install_npm_tool "chatgpt" "ChatGPT CLI" || return 1 | |
| has_cmd chatgpt || has_npm_bin chatgpt || return 1 | |
| } | |
| install_anthropic_cli() { | |
| # Official SDK ships `anthropic` console script in recent releases | |
| install_python_tool "anthropic" "Anthropic CLI (SDK)" || return 1 | |
| has_cmd anthropic || warn "anthropic package installed — console script may vary by version" | |
| dim "Auth: export ANTHROPIC_API_KEY=..." | |
| } | |
| install_groq_cli() { | |
| install_python_tool "groq" "Groq CLI (SDK)" || return 1 | |
| # SDK may not ship a bin; provide a thin shim that prints usage if missing | |
| if ! has_cmd groq; then | |
| write_local_shim "groq" "python3" "-c" "import groq,sys; print('groq SDK', getattr(groq,'__version__','ok')); print('Use the Python SDK — no first-party CLI binary')" | |
| fi | |
| dim "Auth: export GROQ_API_KEY=..." | |
| } | |
| install_cohere_cli() { | |
| step "Installing Cohere CLI (community plyght/cohere-cli)" | |
| if has_cmd cohere; then | |
| dim "cohere already on PATH" | |
| return 0 | |
| fi | |
| case "$PLATFORM" in | |
| macos|linux) | |
| if curl -fsSL https://raw.githubusercontent.com/plyght/cohere-cli/main/install.sh | bash; then | |
| has_cmd cohere || { err "cohere binary not found after install.sh"; return 1; } | |
| else | |
| err "cohere-cli install.sh failed" | |
| return 1 | |
| fi | |
| ;; | |
| windows) | |
| info "Windows: community cohere-cli is bash-only; use WSL or manual install from https://github.com/plyght/cohere-cli" | |
| open_url "https://github.com/plyght/cohere-cli" | |
| return 1 | |
| ;; | |
| esac | |
| dim "Verify: cohere" | |
| dim "Config: ~/.config/cohere-cli/config.env" | |
| } | |
| install_mistral_cli() { | |
| install_python_tool "mistralai" "Mistral CLI (SDK)" || return 1 | |
| if ! has_cmd mistral; then | |
| write_local_shim "mistral" "python3" "-c" "import mistralai; print('mistralai SDK loaded')" | |
| fi | |
| } | |
| install_together_cli() { | |
| install_python_tool "together" "Together CLI" || return 1 | |
| has_cmd together || warn "together installed — check PATH for console script" | |
| } | |
| install_openrouter_cli() { | |
| install_python_tool "openrouter" "OpenRouter CLI" || return 1 | |
| has_cmd openrouter || warn "openrouter SDK installed" | |
| } | |
| install_fireworks_cli() { | |
| install_python_tool "fireworks-ai" "Fireworks CLI (SDK)" || return 1 | |
| if ! has_cmd fireworks; then | |
| write_local_shim "fireworks" "python3" "-c" "import fireworks; print('fireworks-ai SDK loaded')" | |
| fi | |
| } | |
| install_deepseek_cli() { | |
| install_npm_tool "deepseek-cli" "DeepSeek CLI" || return 1 | |
| has_cmd deepseek || has_npm_bin deepseek || has_cmd ds || return 1 | |
| } | |
| install_perplexity_cli() { | |
| step "Installing Perplexity CLI (community npm)" | |
| ensure_npm || return 1 | |
| npm_install_g perplexity-cli || return 1 | |
| dim "Verify: perplexity-cli --help" | |
| dim "Config: ~/.perplexity-cli/config.json" | |
| } | |
| install_poe_cli() { | |
| install_python_tool "poe-api" "Poe CLI (community)" || return 1 | |
| if ! has_cmd poe; then | |
| write_local_shim "poe" "python3" "-c" "import poe; print('poe-api loaded')" | |
| fi | |
| dim "Note: community reverse-engineered API — treat as research tooling" | |
| } | |
| install_kimi_cli() { | |
| install_python_tool "kimi-cli" "Kimi CLI" || return 1 | |
| has_cmd kimi || has_cmd kimi-cli || warn "kimi-cli installed — verify binary name" | |
| } | |
| install_moonshot_cli() { | |
| # Moonshot / Kimi share ecosystem; prefer kimi-cli package | |
| step "Installing Moonshot CLI surface (via kimi-cli)" | |
| install_kimi_cli || return 1 | |
| if ! has_cmd moonshot; then | |
| write_local_shim "moonshot" "kimi" 2>/dev/null || write_local_shim "moonshot" "python3" "-c" "print('Use kimi-cli / Moonshot API')" | |
| fi | |
| } | |
| install_cerebras_cli() { | |
| install_python_tool "cerebras-cloud-sdk" "Cerebras CLI (SDK)" || return 1 | |
| if ! has_cmd cerebras; then | |
| write_local_shim "cerebras" "python3" "-c" "import cerebras; print('cerebras SDK loaded')" | |
| fi | |
| } | |
| install_sambanova_cli() { | |
| install_python_tool "sambanova" "SambaNova CLI (SDK)" || return 1 | |
| if ! has_cmd sncloud; then | |
| write_local_shim "sncloud" "python3" "-c" "import sambanova; print('sambanova SDK loaded')" | |
| fi | |
| } | |
| install_azure_ai_cli() { | |
| step "Installing Azure AI CLI surface" | |
| # Official path is Azure CLI + ML/AI extensions (`az` / `ai` preview varies). | |
| if has_cmd ai; then return 0; fi | |
| if has_cmd az; then | |
| az extension add -n ml -y 2>/dev/null || true | |
| write_local_shim "ai" "az" "ml" "-h" | |
| dim "Shim: ai -> az ml (Azure ML extension)" | |
| return 0 | |
| fi | |
| if [[ "$(native_cli_os)" == "windows" ]] && has_winget; then | |
| install_with_winget "Microsoft.AzureCLI" || return 1 | |
| write_local_shim "ai" "az" "ml" "-h" | |
| return 0 | |
| fi | |
| if has_brew; then | |
| brew install azure-cli || return 1 | |
| write_local_shim "ai" "az" "ml" "-h" | |
| return 0 | |
| fi | |
| skip_unsupported "Install Azure CLI (az) first, then re-run — ai shim wraps az ml" | |
| return 2 | |
| } | |
| # ── local inference ─────────────────────────────────────────────────── | |
| install_msty() { | |
| install_desktop_app "Msty" "msty" "" "msty" "https://msty.app" || return 1 | |
| } | |
| install_localai() { | |
| step "Installing LocalAI" | |
| if has_cmd local-ai || has_cmd localai; then return 0; fi | |
| if has_brew; then | |
| brew install localai && return 0 | |
| fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| linux|darwin) | |
| # Official one-liner installer when available; else clone | |
| if curl -fsSIL https://localai.io/install.sh &>/dev/null; then | |
| curl -fsSL https://localai.io/install.sh | bash || return 1 | |
| else | |
| install_git_clone_tool "https://github.com/mudler/LocalAI.git" "LocalAI" "LocalAI" || return 1 | |
| write_local_shim "local-ai" "echo" "LocalAI cloned to ~/genai-tools/LocalAI — see README for build" | |
| fi | |
| ;; | |
| windows) | |
| open_url "https://localai.io" | |
| skip_unsupported "LocalAI: use WSL or download from localai.io on native Windows" | |
| return 2 | |
| ;; | |
| esac | |
| ensure_user_local_bin_on_path | |
| } | |
| install_xinference() { | |
| install_python_tool "xinference" "Xinference" || return 1 | |
| has_cmd xinference || return 1 | |
| dim "Verify: xinference --help" | |
| } | |
| install_vllm() { | |
| step "Installing vLLM" | |
| local os; os="$(native_cli_os)" | |
| if [[ "$os" == "darwin" ]]; then | |
| skip_unsupported "vLLM is Linux/CUDA-oriented — use Linux GPU hosts or WSL2+NVIDIA" | |
| return 2 | |
| fi | |
| if [[ "$os" == "windows" && "$IS_WSL" != true ]]; then | |
| skip_unsupported "vLLM: use WSL2 + NVIDIA on Windows" | |
| return 2 | |
| fi | |
| install_python_tool "vllm" "vLLM" || return 1 | |
| has_cmd vllm || return 1 | |
| } | |
| install_tgi() { | |
| step "Installing Text Generation Inference launcher" | |
| if has_cmd text-generation-launcher; then return 0; fi | |
| if has_cmd docker; then | |
| write_local_shim "text-generation-launcher" "docker" "run" "--rm" "-it" "ghcr.io/huggingface/text-generation-inference:latest" | |
| dim "Shim uses Docker image ghcr.io/huggingface/text-generation-inference" | |
| return 0 | |
| fi | |
| skip_unsupported "TGI typically runs via Docker — install Docker then re-run" | |
| return 2 | |
| } | |
| install_exllama() { | |
| install_python_tool "exllama" "ExLlama" || return 1 | |
| if ! has_cmd exllama; then | |
| write_local_shim "exllama" "python3" "-c" "import exllama; print('exllama loaded')" | |
| fi | |
| } | |
| install_llama_swap() { | |
| step "Installing llama-swap" | |
| if has_cmd llama-swap; then return 0; fi | |
| local os; os="$(native_cli_os)" | |
| # Prefer GitHub release binary via go install if module path known | |
| if install_go_tool "github.com/mostlygeek/llama-swap/cmd/llama-swap@latest" "llama-swap" 2>/dev/null; then | |
| has_cmd llama-swap && return 0 | |
| fi | |
| install_git_clone_tool "https://github.com/mostlygeek/llama-swap.git" "llama-swap" "llama-swap" || return 1 | |
| write_local_shim "llama-swap" "echo" "llama-swap cloned to ~/genai-tools/llama-swap — build per README" | |
| } | |
| install_alpaca_cli() { | |
| # Note: alpaca-py is the trading SDK — not an LLM CLI. Skip with clarity. | |
| step "Alpaca CLI" | |
| skip_unsupported "alpaca-py is a trading SDK, not an LLM CLI — skipping to avoid false endpoint coverage" | |
| return 2 | |
| } | |
| install_open_webui() { | |
| install_python_tool "open-webui" "Open WebUI" || return 1 | |
| has_cmd open-webui || return 1 | |
| dim "Verify: open-webui serve" | |
| } | |
| install_anythingllm() { | |
| install_desktop_app "AnythingLLM" "anythingllm" "" "anythingllm" "https://anythingllm.com/download" || return 1 | |
| } | |
| install_dify_cli() { | |
| # dify-client is an API SDK; CLI surface is limited | |
| install_python_tool "dify-client" "Dify client (SDK)" || return 1 | |
| if ! has_cmd dify; then | |
| write_local_shim "dify" "python3" "-c" "import dify_client; print('dify-client SDK loaded')" | |
| fi | |
| } | |
| install_librechat() { | |
| step "Installing LibreChat (git self-host)" | |
| install_git_clone_tool "https://github.com/danny-avila/LibreChat.git" "LibreChat" "LibreChat" || return 1 | |
| write_local_shim "librechat" "echo" "LibreChat at ~/genai-tools/LibreChat — use npm/docker per README" | |
| } | |
| # ── MCP ─────────────────────────────────────────────────────────────── | |
| install_mcp_inspector() { | |
| install_npm_tool "@modelcontextprotocol/inspector" "MCP Inspector" || return 1 | |
| has_cmd mcp-inspector || has_npm_bin mcp-inspector || return 1 | |
| dim "Verify: mcp-inspector --help" | |
| } | |
| install_mcp_cli() { | |
| # Official Python MCP package with CLI extras | |
| step "Installing MCP CLI (python mcp[cli])" | |
| if has_cmd mcp; then return 0; fi | |
| if has_cmd uv; then | |
| uv tool install "mcp[cli]" || return 1 | |
| elif has_cmd pipx; then | |
| pipx install "mcp[cli]" || return 1 | |
| elif ensure_pip; then | |
| "$(pip_cmd)" install --user "mcp[cli]" || return 1 | |
| else | |
| return 1 | |
| fi | |
| ensure_user_local_bin_on_path | |
| has_cmd mcp || warn "mcp package installed — binary name may be mcp" | |
| } | |
| install_mcp_server_filesystem() { | |
| install_npm_tool "@modelcontextprotocol/server-filesystem" "MCP Filesystem" || return 1 | |
| has_cmd mcp-server-filesystem || has_npm_bin mcp-server-filesystem || return 1 | |
| } | |
| install_mcp_server_github() { | |
| install_npm_tool "@modelcontextprotocol/server-github" "MCP GitHub" || return 1 | |
| has_cmd mcp-server-github || has_npm_bin mcp-server-github || return 1 | |
| } | |
| install_mcp_server_memory() { | |
| install_npm_tool "@modelcontextprotocol/server-memory" "MCP Memory" || return 1 | |
| has_cmd mcp-server-memory || has_npm_bin mcp-server-memory || return 1 | |
| } | |
| install_mcp_server_sqlite() { | |
| install_npm_tool "mcp-server-sqlite" "MCP SQLite" || return 1 | |
| has_cmd mcp-server-sqlite || has_npm_bin mcp-server-sqlite || return 1 | |
| } | |
| install_mcp_server_postgres() { | |
| install_npm_tool "@modelcontextprotocol/server-postgres" "MCP Postgres" || return 1 | |
| has_cmd mcp-server-postgres || has_npm_bin mcp-server-postgres || return 1 | |
| } | |
| install_mcp_server_brave_search() { | |
| install_npm_tool "@modelcontextprotocol/server-brave-search" "MCP Brave Search" || return 1 | |
| has_cmd mcp-server-brave-search || has_npm_bin mcp-server-brave-search || return 1 | |
| } | |
| install_mcp_server_fetch() { | |
| # Prefer PyPI — some npm namesquats exist | |
| step "Installing MCP Fetch (PyPI mcp-server-fetch)" | |
| install_python_tool "mcp-server-fetch" "MCP Fetch" || return 1 | |
| has_cmd mcp-server-fetch || return 1 | |
| } | |
| install_mcp_server_puppeteer() { | |
| install_npm_tool "@modelcontextprotocol/server-puppeteer" "MCP Puppeteer" || return 1 | |
| has_cmd mcp-server-puppeteer || has_npm_bin mcp-server-puppeteer || return 1 | |
| } | |
| install_mcp_server_playwright() { | |
| install_npm_tool "@playwright/mcp" "MCP Playwright" || return 1 | |
| # Binary is playwright-mcp; add alias shim | |
| if has_cmd playwright-mcp || has_npm_bin playwright-mcp; then | |
| if ! has_cmd mcp-server-playwright; then | |
| write_local_shim "mcp-server-playwright" "playwright-mcp" | |
| fi | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # ── speech ──────────────────────────────────────────────────────────── | |
| install_whisper_cpp() { | |
| step "Installing whisper.cpp" | |
| if has_cmd whisper-cli || has_cmd whisper-cpp || has_cmd whisper; then return 0; fi | |
| if has_brew; then | |
| brew install whisper-cpp || return 1 | |
| # Homebrew may expose whisper-cli | |
| if has_cmd whisper-cli; then return 0; fi | |
| if has_cmd whisper-cpp && ! has_cmd whisper-cli; then | |
| write_local_shim "whisper-cli" "whisper-cpp" | |
| fi | |
| return 0 | |
| fi | |
| install_git_clone_tool "https://github.com/ggerganov/whisper.cpp.git" "whisper.cpp" "whisper.cpp" || return 1 | |
| write_local_shim "whisper-cli" "echo" "whisper.cpp at ~/genai-tools/whisper.cpp — build with make" | |
| } | |
| install_whisperx() { | |
| install_python_tool "whisperx" "WhisperX" || return 1 | |
| has_cmd whisperx || return 1 | |
| } | |
| install_faster_whisper() { | |
| install_python_tool "faster-whisper" "faster-whisper" || return 1 | |
| if ! has_cmd faster-whisper; then | |
| write_local_shim "faster-whisper" "python3" "-c" "import faster_whisper; print('faster-whisper', faster_whisper.__version__ if hasattr(faster_whisper,'__version__') else 'ok')" | |
| fi | |
| } | |
| install_elevenlabs() { | |
| step "Installing ElevenLabs Agents CLI" | |
| ensure_npm || return 1 | |
| npm_install_g @elevenlabs/cli || return 1 | |
| dim "Verify: elevenlabs --help" | |
| dim "Note: PyPI elevenlabs is a library-only Python SDK (no console script)" | |
| } | |
| install_kokoro() { | |
| install_python_tool "kokoro" "Kokoro TTS" || return 1 | |
| has_cmd kokoro || warn "kokoro installed" | |
| } | |
| install_piper() { | |
| step "Installing Piper TTS" | |
| if has_cmd piper; then return 0; fi | |
| install_python_tool "piper-tts" "Piper TTS" || return 1 | |
| has_cmd piper || warn "piper-tts installed — binary may be piper" | |
| } | |
| install_bark() { | |
| install_python_tool "suno-bark" "Bark TTS (library)" || return 1 | |
| dim "Note: suno-bark is library-only; use python -c 'from bark import generate_audio'" | |
| } | |
| # ── image / diffusion ───────────────────────────────────────────────── | |
| install_stable_diffusion_cli() { | |
| step "Installing Stable Diffusion CLI surface" | |
| # No single canonical `stable-diffusion` bin — use diffusers + shim | |
| install_python_tool "diffusers" "Diffusers (SD stack)" || return 1 | |
| write_local_shim "stable-diffusion" "python3" "-c" "import diffusers; print('diffusers', diffusers.__version__)" | |
| dim "For UIs see: comfyui / invokeai / automatic1111" | |
| } | |
| install_comfyui() { | |
| step "Installing ComfyUI" | |
| if has_cmd comfy; then return 0; fi | |
| # Official-ish: comfy-cli on pip provides `comfy` | |
| if install_python_tool "comfy-cli" "ComfyUI CLI" 2>/dev/null && has_cmd comfy; then | |
| return 0 | |
| fi | |
| install_git_clone_tool "https://github.com/comfyanonymous/ComfyUI.git" "ComfyUI" "ComfyUI" || return 1 | |
| write_local_shim "comfy" "echo" "ComfyUI at ~/genai-tools/ComfyUI — python main.py" | |
| } | |
| install_invokeai() { | |
| install_python_tool "invokeai" "InvokeAI" || return 1 | |
| has_cmd invokeai || return 1 | |
| } | |
| install_fooocus() { | |
| install_git_clone_tool "https://github.com/lllyasviel/Fooocus.git" "Fooocus" "Fooocus" || return 1 | |
| write_local_shim "fooocus" "echo" "Fooocus at ~/genai-tools/Fooocus — see launch.py" | |
| } | |
| install_automatic1111() { | |
| install_git_clone_tool "https://github.com/AUTOMATIC1111/stable-diffusion-webui.git" "stable-diffusion-webui" "A1111 WebUI" || return 1 | |
| write_local_shim "webui" "bash" "$HOME/genai-tools/stable-diffusion-webui/webui.sh" | |
| } | |
| install_diffusers_cli() { | |
| install_python_tool "diffusers" "Diffusers" || return 1 | |
| write_local_shim "diffusers-cli" "python3" "-c" "import diffusers,sys; print(diffusers.__version__)" | |
| } | |
| # ── cloud / MLOps CLIs ──────────────────────────────────────────────── | |
| install_huggingface_cli() { | |
| install_python_tool "huggingface_hub[cli]" "Hugging Face CLI" || \ | |
| install_python_tool "huggingface_hub" "Hugging Face Hub" || return 1 | |
| has_cmd huggingface-cli || has_cmd hf || return 1 | |
| dim "Verify: huggingface-cli --help (or hf)" | |
| } | |
| install_hf_transfer() { | |
| install_python_tool "hf_transfer" "HF Transfer" || return 1 | |
| # Library — provide marker binary for endpoint attribution | |
| write_local_shim "hf-transfer" "python3" "-c" "import hf_transfer; print('hf_transfer ok')" | |
| } | |
| install_modal_cli() { | |
| install_python_tool "modal" "Modal CLI" || return 1 | |
| has_cmd modal || return 1 | |
| } | |
| install_replicate_cli() { | |
| install_python_tool "replicate" "Replicate CLI (SDK)" || return 1 | |
| has_cmd replicate || warn "replicate SDK installed" | |
| } | |
| install_bentoml() { | |
| install_python_tool "bentoml" "BentoML" || return 1 | |
| has_cmd bentoml || return 1 | |
| } | |
| install_cog() { | |
| step "Installing Cog (Replicate container tool)" | |
| if has_cmd cog; then return 0; fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| darwin|linux) | |
| # Official install script | |
| if curl -fsSIL https://github.com/replicate/cog/releases/latest &>/dev/null; then | |
| curl -fsSL https://raw.githubusercontent.com/replicate/cog/main/install.sh | _sudo bash || \ | |
| install_python_tool "cog" "Cog" || return 1 | |
| else | |
| install_python_tool "cog" "Cog" || return 1 | |
| fi | |
| ;; | |
| windows) | |
| install_python_tool "cog" "Cog" || return 1 | |
| ;; | |
| esac | |
| ensure_user_local_bin_on_path | |
| has_cmd cog || return 1 | |
| } | |
| # ── LOL-GAI additions: Cursor tunnels + Browser Use ─────────────────── | |
| check_cursor_tunnel() { | |
| has_cmd cursor-tunnel || [[ -x "/Applications/Cursor.app/Contents/Resources/app/bin/cursor-tunnel" ]] || \ | |
| [[ -x "$HOME/AppData/Local/Programs/cursor/resources/app/bin/cursor-tunnel.exe" ]] || \ | |
| [[ -x "$LOCALAPPDATA/Programs/cursor/resources/app/bin/cursor-tunnel.exe" ]] | |
| } | |
| check_cursor_code_tunnel() { | |
| has_cmd code-tunnel || [[ -x "/Applications/Cursor.app/Contents/Resources/app/node_modules/@vscode/ripgrep/bin/code-tunnel" ]] || \ | |
| has_cmd cursor-tunnel | |
| } | |
| install_cursor_tunnel() { | |
| step "Ensuring Cursor (provides cursor-tunnel)" | |
| install_cursor || return $? | |
| if check_cursor_tunnel; then | |
| ok "cursor-tunnel present with Cursor install" | |
| return 0 | |
| fi | |
| warn "cursor-tunnel binary not found after Cursor install — may appear after first launch" | |
| return 0 | |
| } | |
| install_cursor_code_tunnel() { | |
| step "Ensuring Cursor (provides code-tunnel)" | |
| install_cursor || return $? | |
| ok "code-tunnel / cursor-tunnel ship with Cursor IDE" | |
| return 0 | |
| } | |
| check_browser_use_desktop() { | |
| has_app "Browser Use" || has_cmd browser-use || \ | |
| [[ -d "/Applications/Browser Use.app" ]] || \ | |
| [[ -d "$HOME/AppData/Local/Programs/Browser Use" ]] | |
| } | |
| install_browser_use_desktop() { | |
| step "Installing Browser Use Desktop" | |
| if check_browser_use_desktop; then return 0; fi | |
| local os; os="$(native_cli_os)" | |
| case "$os" in | |
| darwin) | |
| if has_brew; then | |
| brew install --cask browser-use 2>/dev/null && return 0 || true | |
| fi | |
| # Official releases — open download page if no cask | |
| open_url "https://github.com/browser-use/desktop/releases" | |
| warn "Browser Use Desktop: download from GitHub releases (no brew cask yet)" | |
| return 2 | |
| ;; | |
| linux) | |
| install_git_clone_tool "https://github.com/browser-use/desktop.git" "browser-use-desktop" "Browser Use Desktop" || return 1 | |
| write_local_shim "browser-use" "echo" "Browser Use Desktop clone at ~/genai-tools/browser-use-desktop" | |
| return 0 | |
| ;; | |
| windows) | |
| if has_winget; then | |
| winget install --id BrowserUse.BrowserUse -e --accept-package-agreements --accept-source-agreements 2>/dev/null && return 0 || true | |
| fi | |
| open_url "https://github.com/browser-use/desktop/releases" | |
| warn "Browser Use Desktop: download Windows installer from GitHub releases" | |
| return 2 | |
| ;; | |
| esac | |
| } | |
| check_browser_use_box() { | |
| [[ -d "$HOME/genai-tools/bux" ]] || [[ -d "$HOME/genai-tools/browser-use-box" ]] || has_cmd bux | |
| } | |
| install_browser_use_box() { | |
| step "Installing Browser Use Box (BUX)" | |
| if check_browser_use_box; then return 0; fi | |
| # Prefer official BUX repo; fall back to browser-use monorepo docs | |
| if install_git_clone_tool "https://github.com/browser-use/bux.git" "bux" "Browser Use Box" 2>/dev/null; then | |
| write_local_shim "bux" "echo" "BUX at ~/genai-tools/bux — see README for agent container setup" | |
| return 0 | |
| fi | |
| install_git_clone_tool "https://github.com/browser-use/browser-use.git" "browser-use" "browser-use" || return 1 | |
| write_local_shim "bux" "echo" "browser-use at ~/genai-tools/browser-use — BUX is the box/runtime surface" | |
| return 0 | |
| } | |
| register_tools() { | |
| # CLI agents — most use npm so they work on all platforms | |
| register_tool "claude-code" "cli" "has_cmd claude" "install_claude_code" "Anthropic agentic coding CLI" | |
| register_tool "codex-openai" "cli" "has_cmd codex" "install_codex" "OpenAI agentic coding CLI" | |
| register_tool "gemini-cli" "cli" "has_cmd gemini" "install_gemini_cli" "Google Gemini CLI (legacy — prefer antigravity-cli)" | |
| register_tool "antigravity-cli" "cli" "check_antigravity_cli" "install_antigravity_cli" "Google Antigravity CLI (agy — Gemini CLI successor)" | |
| register_tool "aider" "cli" "has_cmd aider" "install_aider" "OSS AI pair programming CLI" | |
| register_tool "genaiscript" "cli" "has_cmd genaiscript" "install_genaiscript" "Microsoft LLM scripting framework" | |
| register_tool "qwen-code" "cli" "has_cmd qwen" "install_qwen_code" "Alibaba terminal coding agent" | |
| register_tool "cody-amp" "cli" "has_cmd amp" "install_amp" "Sourcegraph Cody/Amp CLI" | |
| register_tool "github-copilot-copilot-cli" "cli" "has_cmd copilot" "install_copilot_cli" "GitHub Copilot CLI" | |
| register_tool "opencode" "cli" "has_cmd opencode" "install_opencode" "OpenCode TUI agent (anomalyco — model-agnostic)" | |
| register_tool "goose" "cli" "check_goose" "install_goose" "Block/AAIF extensible AI agent (41k+ stars)" | |
| register_tool "kiro" "cli" "has_cmd kiro-cli" "install_kiro_cli" "AWS Kiro CLI (fmr. Amazon Q Developer CLI)" | |
| register_tool "kiro-ide" "desktop" "check_kiro_ide" "install_kiro_ide" "AWS Kiro IDE (spec-driven VS Code fork)" | |
| register_tool "augment-code" "cli" "has_cmd auggie" "install_augment" "Augment Code agentic CLI" | |
| register_tool "tabnine" "cli" "check_tabnine" "install_tabnine_cli" "Tabnine completion (IDE plugin primary)" | |
| register_tool "bito" "cli" "has_cmd bito" "install_bito" "Bito AI code assistant CLI" | |
| register_tool "codebuff" "cli" "has_cmd codebuff" "install_codebuff" "Codebuff terminal coding agent" | |
| register_tool "junie" "cli" "has_cmd junie" "install_junie" "JetBrains autonomous coding agent" | |
| register_tool "pochi" "cli" "has_cmd pochi" "install_pochi" "TabbyML Pochi agent CLI" | |
| register_tool "qodo-command" "cli" "has_cmd qodo" "install_qodo_command" "Qodo terminal coding agent" | |
| register_tool "kilo-code" "cli" "has_cmd kilo" "install_kilo_code" "Kilo Code terminal CLI" | |
| register_tool "crush" "cli" "has_cmd crush" "install_crush" "Charmbracelet TUI agent (opencode successor)" | |
| register_tool "openhands" "cli" "has_cmd openhands" "install_openhands" "All Hands AI autonomous agent" | |
| register_tool "factory-droid" "cli" "has_cmd droid" "install_factory_droid" "Factory.ai Droid agentic CLI" | |
| register_tool "google-jules-tools" "cli" "has_cmd jules" "install_jules" "Google Jules autonomous coding CLI" | |
| register_tool "gitlab-duo-cli" "cli" "has_cmd duo" "install_gitlab_duo_cli" "GitLab Duo agentic CLI" | |
| register_tool "blackbox-ai" "cli" "check_blackbox_ai" "install_blackbox_ai" "Blackbox AI terminal coding agent" | |
| register_tool "rovo-dev-cli" "cli" "has_cmd acli" "install_rovo_dev" "Atlassian Rovo Dev CLI (acli rovodev)" | |
| register_tool "hermes-agent" "cli" "has_cmd hermes" "install_hermes_agent" "Nous Research Hermes Agent platform" | |
| register_tool "continue-dev" "cli" "has_cmd cn" "install_continue_cli" "Continue Dev CLI (cn binary)" | |
| register_tool "cursor" "desktop" "has_app Cursor" "install_cursor" "Anysphere Cursor (IDE + bundled tunnel helpers)" | |
| register_tool "cline-cline-cli" "cli" "has_cmd cline" "install_cline_cli" "Cline standalone terminal CLI (npm: cline)" | |
| register_tool "pi" "cli" "has_cmd pi" "install_pi" "Pi coding agent (Earendil — minimal harness)" | |
| register_tool "plandex" "cli" "has_cmd plandex" "install_plandex" "Plandex multi-step / large-task coding CLI" | |
| register_tool "zencoder-cli" "cli" "check_zencoder" "install_zencoder_cli" "Zencoder native CLI (bundled with Zenflow desktop)" | |
| register_tool "openai-cli" "cli" "has_cmd openai" "install_openai_cli" "OpenAI Python SDK console script" | |
| register_tool "llm-cli" "cli" "has_cmd llm" "install_llm_cli" "Simon Willison's llm CLI / plugin host" | |
| # MCP servers (spawned by MCP hosts — Claude Code, Cursor, Windsurf, etc.) | |
| register_tool "github-mcp-server" "mcp" "has_cmd github-mcp-server" "install_github_mcp_server" "GitHub's official MCP server (Go binary)" | |
| register_tool "elastic-mcp-server" "mcp" "check_elastic_mcp_server" "install_elastic_mcp_server" "Elastic MCP servers (Elasticsearch + Cloud, via npx)" | |
| # CLI installs for tools the registry tracks as cloud / API-only — we use | |
| # the closest community/official surface that puts artifacts on disk so the | |
| # detection telemetry referenced in registryData.js can be exercised. | |
| register_tool "grok" "cli" "check_grok" "install_grok" "xAI Grok via grok-agent npm CLI (community)" | |
| # Research / OSS daemons that ship via git clone. | |
| register_tool "bolt" "research" "check_bolt" "install_bolt" "Bolt (bolt.diy OSS sibling — pnpm self-host)" | |
| register_tool "openclaw" "research" "check_openclaw" "install_openclaw" "OpenClaw personal AI agent daemon (Node.js)" | |
| register_tool "nemoclaw" "research" "check_nemoclaw" "install_nemoclaw" "NVIDIA NemoClaw enterprise wrapper for OpenClaw" | |
| # Desktop / IDE apps | |
| register_tool "windsurf" "desktop" "check_windsurf_or_devin" "install_windsurf" "Windsurf / Devin Desktop (Cognition; VS Code fork)" | |
| register_tool "claude-desktop" "desktop" "has_app Claude" "install_claude_desktop" "Anthropic Claude desktop app" | |
| register_tool "zed" "desktop" "has_app Zed" "install_zed" "Zed high-performance editor" | |
| register_tool "vscodium" "desktop" "has_cmd codium" "install_vscodium" "OSS VS Code (no telemetry)" | |
| register_tool "trae" "desktop" "has_app Trae" "install_trae" "ByteDance TRAE AI-native IDE" | |
| register_tool "trae-solo" "desktop" "has_app 'TRAE SOLO'" "install_trae_solo" "ByteDance TRAE SOLO autonomous coding agent (macOS arm64 only)" | |
| register_tool "pearai" "desktop" "has_app PearAI" "install_pearai" "PearAI IDE (VS Code fork + Roo/Cline agent)" | |
| register_tool "void-editor" "desktop" "has_app Void" "install_void_editor" "Void Editor (VS Code fork — paused)" | |
| register_tool "google-antigravity" "desktop" "check_google_antigravity" "install_google_antigravity" "Google Antigravity (agent-first IDE — VS Code-derived)" | |
| register_tool "warp" "desktop" "check_warp" "install_warp" "Warp agentic terminal (ADE — brew/winget)" | |
| register_tool "replit-ghostwriter-desktop-wrapper" "desktop" "check_replit" "install_replit" "Replit Desktop (Electron wrapper for github.com/replit/desktop)" | |
| # Local model servers | |
| register_tool "ollama" "local" "has_cmd ollama" "install_ollama" "Local LLM server (llama, mistral, etc.)" | |
| register_tool "lm-studio" "local" "check_lm_studio" "install_lm_studio" "Local model GUI + API server" | |
| register_tool "llama-cpp" "local" "has_cmd llama-server" "install_llama_cpp" "ggml inference engine" | |
| register_tool "jan" "local" "has_app Jan" "install_jan" "Local AI desktop app" | |
| register_tool "gpt4all" "local" "has_app GPT4All" "install_gpt4all" "Nomic local model runner" | |
| register_tool "koboldcpp" "local" "has_cmd koboldcpp" "install_koboldcpp" "LostRuins KoboldCpp inference server" | |
| register_tool "textgen-webui" "local" "check_textgen_webui" "install_textgen_webui" "oobabooga Text Generation WebUI" | |
| register_tool "tabby-server" "local" "has_cmd tabby" "install_tabby_server" "TabbyML self-hosted Copilot alternative" | |
| # VS Code extensions — work on any platform with VS Code / Cursor / Codium | |
| register_tool "ext-cline" "vscode" "has_vscode_ext claude-dev" "install_ext_cline" "Cline (Claude Dev) extension" | |
| register_tool "ext-continue" "vscode" "has_vscode_ext continue" "install_ext_continue" "Continue dev extension" | |
| register_tool "github-copilot-ide-extension" "vscode" "has_vscode_ext copilot" "install_ext_copilot" "GitHub Copilot extension" | |
| register_tool "ext-tabnine" "vscode" "has_vscode_ext tabnine" "install_ext_tabnine" "Tabnine AI extension" | |
| register_tool "ext-roo-code" "vscode" "has_vscode_ext roo-cline" "install_ext_roo_code" "Roo Code (Cline lineage; sunsetting May 2026)" | |
| register_tool "ext-supermaven" "vscode" "has_vscode_ext supermaven" "install_ext_supermaven" "Supermaven completion + chat" | |
| register_tool "ext-kilo-code" "vscode" "has_vscode_ext kilo-code" "install_ext_kilo_code" "Kilo Code VS Code extension" | |
| register_tool "ext-blackbox-ai" "vscode" "has_vscode_ext blackbox" "install_ext_blackbox_ai" "Blackbox AI VS Code extension" | |
| # Extension-only registry tools — registered under their bare ID for parity | |
| # with the registry; the actual install is the corresponding ext-* entry. | |
| register_tool "cline-ide-extension" "vscode" "check_cline_alias" "install_cline_alias" "Cline VS Code extension (LOL-GAI Id)" | |
| register_tool "roo-code" "vscode" "check_roo_code_alias" "install_roo_code_alias" "Roo Code — alias for ext-roo-code" | |
| register_tool "supermaven" "vscode" "check_supermaven_alias" "install_supermaven_alias" "Supermaven — alias for ext-supermaven" | |
| register_tool "devin" "vscode" "check_devin" "install_devin" "Cognition Devin (cognition.devin VS Code extension)" | |
| # ── 1.8.0 endpoint-scope expansion ────────────────────────────────── | |
| register_tool "command-code" "cli" "has_cmd command-code" "install_command_code" "Command Code learning coding agent" | |
| register_tool "codegpt" "cli" "has_cmd codegpt" "install_codegpt" "CodeGPT terminal CLI" | |
| register_tool "open-interpreter" "cli" "has_cmd interpreter" "install_open_interpreter" "Open Interpreter (interpreter binary)" | |
| register_tool "vibe" "cli" "has_cmd vibe" "install_vibe_cli" "Vibe coding CLI (best-effort)" | |
| register_tool "repoprompt" "cli" "has_cmd repoprompt" "install_repoprompt" "RepoPrompt codebase prompting CLI" | |
| register_tool "roo-cli" "cli" "has_cmd roo" "install_roo_cli" "Roo Code CLI surface (ext + shim)" | |
| register_tool "fabric" "chat" "check_fabric" "install_fabric" "Fabric AI prompt patterns CLI" | |
| register_tool "shellgpt" "chat" "has_cmd sgpt" "install_shellgpt" "ShellGPT (sgpt) shell assistant" | |
| register_tool "tgpt" "chat" "has_cmd tgpt" "install_tgpt" "tgpt terminal ChatGPT client" | |
| register_tool "mods" "chat" "has_cmd mods" "install_mods" "Charmbracelet mods AI in the pipeline" | |
| register_tool "askai" "chat" "has_cmd askai" "install_askai" "AskAI terminal helper" | |
| register_tool "gptme" "chat" "has_cmd gptme" "install_gptme" "GPTMe agentic chat CLI" | |
| register_tool "gpterm" "chat" "has_cmd gpterm" "install_gpterm" "GPTerm OpenAI terminal" | |
| register_tool "gptshell" "chat" "has_cmd gptshell" "install_gptshell" "GPTShell natural-language shell" | |
| register_tool "chatgpt-cli" "chat" "has_cmd chatgpt" "install_chatgpt_cli" "ChatGPT Node CLI" | |
| register_tool "anthropic-cli" "chat" "has_cmd anthropic" "install_anthropic_cli" "Anthropic Python SDK CLI" | |
| register_tool "groq-cli" "chat" "has_cmd groq" "install_groq_cli" "Groq SDK / CLI surface" | |
| register_tool "cohere-cli" "chat" "has_cmd cohere" "install_cohere_cli" "Community Cohere terminal chat (plyght/cohere-cli)" | |
| register_tool "mistral-cli" "chat" "has_cmd mistral" "install_mistral_cli" "Mistral SDK / CLI surface" | |
| register_tool "together-cli" "chat" "has_cmd together" "install_together_cli" "Together AI CLI" | |
| register_tool "openrouter-cli" "chat" "has_cmd openrouter" "install_openrouter_cli" "OpenRouter CLI/SDK" | |
| register_tool "fireworks-cli" "chat" "has_cmd fireworks" "install_fireworks_cli" "Fireworks AI SDK surface" | |
| register_tool "deepseek-cli" "chat" "has_cmd deepseek" "install_deepseek_cli" "DeepSeek CLI" | |
| register_tool "perplexity-cli" "chat" "has_cmd perplexity-cli" "install_perplexity_cli" "Perplexity community npm CLI" | |
| register_tool "poe-cli" "chat" "has_cmd poe" "install_poe_cli" "Poe community API CLI" | |
| register_tool "kimi-cli" "chat" "has_cmd kimi" "install_kimi_cli" "Kimi / Moonshot CLI" | |
| register_tool "moonshot-cli" "chat" "has_cmd moonshot" "install_moonshot_cli" "Moonshot CLI alias" | |
| register_tool "cerebras-cli" "chat" "has_cmd cerebras" "install_cerebras_cli" "Cerebras SDK surface" | |
| register_tool "sambanova-cli" "chat" "has_cmd sncloud" "install_sambanova_cli" "SambaNova sncloud surface" | |
| register_tool "azure-ai-cli" "chat" "has_cmd ai" "install_azure_ai_cli" "Azure AI CLI surface (az ml shim)" | |
| register_tool "msty" "local" "has_app Msty" "install_msty" "Msty local AI desktop" | |
| register_tool "localai" "local" "has_cmd local-ai" "install_localai" "LocalAI OpenAI-compatible server" | |
| register_tool "xinference" "local" "has_cmd xinference" "install_xinference" "Xorbits Xinference model server" | |
| register_tool "vllm" "local" "has_cmd vllm" "install_vllm" "vLLM high-throughput inference" | |
| register_tool "tgi" "local" "has_cmd text-generation-launcher" "install_tgi" "HF Text Generation Inference" | |
| register_tool "exllama" "local" "has_cmd exllama" "install_exllama" "ExLlama inference" | |
| register_tool "llama-swap" "local" "has_cmd llama-swap" "install_llama_swap" "llama-swap multi-model proxy" | |
| register_tool "alpaca" "local" "has_cmd alpaca" "install_alpaca_cli" "Alpaca (skipped — not LLM CLI)" | |
| register_tool "open-webui" "local" "has_cmd open-webui" "install_open_webui" "Open WebUI launcher" | |
| register_tool "anythingllm" "local" "has_app AnythingLLM" "install_anythingllm" "AnythingLLM desktop" | |
| register_tool "dify" "local" "has_cmd dify" "install_dify_cli" "Dify client SDK surface" | |
| register_tool "librechat" "local" "has_cmd librechat" "install_librechat" "LibreChat self-host clone" | |
| register_tool "mcp-inspector" "mcp" "has_cmd mcp-inspector" "install_mcp_inspector" "Official MCP Inspector" | |
| register_tool "mcp-cli" "mcp" "has_cmd mcp" "install_mcp_cli" "MCP Python CLI (mcp[cli])" | |
| register_tool "mcp-server-filesystem" "mcp" "has_cmd mcp-server-filesystem" "install_mcp_server_filesystem" "MCP filesystem server" | |
| register_tool "mcp-server-github" "mcp" "has_cmd mcp-server-github" "install_mcp_server_github" "MCP GitHub server (npm)" | |
| register_tool "mcp-server-memory" "mcp" "has_cmd mcp-server-memory" "install_mcp_server_memory" "MCP memory server" | |
| register_tool "mcp-server-sqlite" "mcp" "has_cmd mcp-server-sqlite" "install_mcp_server_sqlite" "MCP SQLite server" | |
| register_tool "mcp-server-postgres" "mcp" "has_cmd mcp-server-postgres" "install_mcp_server_postgres" "MCP Postgres server" | |
| register_tool "mcp-server-brave-search" "mcp" "has_cmd mcp-server-brave-search" "install_mcp_server_brave_search" "MCP Brave Search" | |
| register_tool "mcp-server-fetch" "mcp" "has_cmd mcp-server-fetch" "install_mcp_server_fetch" "MCP Fetch (PyPI)" | |
| register_tool "mcp-server-puppeteer" "mcp" "has_cmd mcp-server-puppeteer" "install_mcp_server_puppeteer" "MCP Puppeteer" | |
| register_tool "mcp-server-playwright" "mcp" "has_cmd mcp-server-playwright" "install_mcp_server_playwright" "MCP Playwright" | |
| register_tool "whisper-cpp" "speech" "has_cmd whisper-cli" "install_whisper_cpp" "whisper.cpp (whisper-cli)" | |
| register_tool "whisperx" "speech" "has_cmd whisperx" "install_whisperx" "WhisperX ASR" | |
| register_tool "faster-whisper" "speech" "has_cmd faster-whisper" "install_faster_whisper" "faster-whisper ASR" | |
| register_tool "elevenlabs" "cli" "has_cmd elevenlabs" "install_elevenlabs" "ElevenLabs Agents CLI (@elevenlabs/cli)" | |
| register_tool "kokoro" "speech" "has_cmd kokoro" "install_kokoro" "Kokoro TTS" | |
| register_tool "piper" "speech" "has_cmd piper" "install_piper" "Piper TTS" | |
| register_tool "bark" "speech" "python3 -c 'import bark'" "install_bark" "Bark TTS (suno-bark library)" | |
| register_tool "stable-diffusion" "image" "has_cmd stable-diffusion" "install_stable_diffusion_cli" "Stable Diffusion CLI surface" | |
| register_tool "comfyui" "image" "has_cmd comfy" "install_comfyui" "ComfyUI / comfy CLI" | |
| register_tool "invokeai" "image" "has_cmd invokeai" "install_invokeai" "InvokeAI" | |
| register_tool "fooocus" "image" "has_cmd fooocus" "install_fooocus" "Fooocus launcher shim" | |
| register_tool "automatic1111" "image" "has_cmd webui" "install_automatic1111" "A1111 webui launcher" | |
| register_tool "diffusers-cli" "image" "has_cmd diffusers-cli" "install_diffusers_cli" "Hugging Face Diffusers CLI surface" | |
| register_tool "huggingface-cli" "cloud" "has_cmd huggingface-cli" "install_huggingface_cli" "Hugging Face Hub CLI" | |
| register_tool "hf-transfer" "cloud" "has_cmd hf-transfer" "install_hf_transfer" "HF Transfer accelerator" | |
| register_tool "modal" "cloud" "has_cmd modal" "install_modal_cli" "Modal cloud CLI" | |
| register_tool "replicate" "cloud" "has_cmd replicate" "install_replicate_cli" "Replicate SDK/CLI" | |
| register_tool "bentoml" "cloud" "has_cmd bentoml" "install_bentoml" "BentoML CLI" | |
| register_tool "cog" "cloud" "has_cmd cog" "install_cog" "Cog (Replicate ML containers)" | |
| # ── LOL-GAI catalog Id aliases (legacy installer names) ───────────── | |
| register_tool "augment" "cli" "has_cmd auggie" "install_augment" "(alias) Augment Code" | |
| register_tool "codex" "cli" "has_cmd codex" "install_codex" "(alias) Codex OpenAI" | |
| register_tool "amp" "cli" "has_cmd amp" "install_amp" "(alias) Cody/Amp" | |
| register_tool "continue-cli" "cli" "has_cmd cn" "install_continue_cli" "(alias) Continue Dev" | |
| register_tool "cursor-agent" "cli" "check_cursor_agent" "install_cursor_agent" "(alias) Cursor Agent CLI" | |
| register_tool "cursor-cursor-cli" "cli" "check_cursor_agent" "install_cursor_agent" "(alias) former catalog Id cursor-cursor-cli" | |
| register_tool "cursor-cursor-ide" "desktop" "has_app Cursor" "install_cursor" "(alias) former catalog Id cursor-cursor-ide" | |
| register_tool "copilot-cli" "cli" "has_cmd copilot" "install_copilot_cli" "(alias) GitHub Copilot CLI" | |
| register_tool "ext-copilot" "vscode" "has_vscode_ext copilot" "install_ext_copilot" "(alias) Copilot IDE extension" | |
| register_tool "jules" "cli" "has_cmd jules" "install_jules" "(alias) Google Jules Tools" | |
| register_tool "hermes" "cli" "has_cmd hermes" "install_hermes_agent" "(alias) Hermes Agent" | |
| register_tool "kiro-cli" "cli" "has_cmd kiro-cli" "install_kiro_cli" "(alias) Kiro" | |
| register_tool "replit" "desktop" "check_replit" "install_replit" "(alias) Replit Desktop" | |
| register_tool "rovo-dev" "cli" "has_cmd acli" "install_rovo_dev" "(alias) Rovo Dev CLI" | |
| register_tool "zencoder" "cli" "check_zencoder" "install_zencoder_cli" "(alias) Zencoder CLI" | |
| register_tool "cline-cli" "cli" "has_cmd cline" "install_cline_cli" "(alias) Cline CLI" | |
| register_tool "cline" "vscode" "check_cline_alias" "install_cline_alias" "(alias) Cline IDE extension" | |
| # Cursor tunnel helpers — installed with Cursor IDE (aliases of catalog Id `cursor`) | |
| register_tool "cursor-tunnel" "desktop" "check_cursor_tunnel" "install_cursor_tunnel" "(alias) Cursor tunnel helper" | |
| register_tool "cursor-code-tunnel" "desktop" "check_cursor_code_tunnel" "install_cursor_code_tunnel" "(alias) Cursor code-tunnel helper" | |
| # Browser Use (LOL-GAI catalog — previously missing) | |
| register_tool "browser-use-desktop" "desktop" "check_browser_use_desktop" "install_browser_use_desktop" "Browser Use Desktop (Electron)" | |
| register_tool "browser-use-box" "research" "check_browser_use_box" "install_browser_use_box" "Browser Use Box / BUX (git clone)" | |
| } | |
| # ── list / check / install logic ───────────────────────────────────── | |
| cat_label() { | |
| case "$1" in | |
| cli) echo "CLI Agents" ;; | |
| chat) echo "Chat / Provider CLIs" ;; | |
| desktop) echo "Desktop / IDE Apps" ;; | |
| local) echo "Local Model Servers" ;; | |
| vscode) echo "VS Code Extensions" ;; | |
| mcp) echo "MCP Servers" ;; | |
| speech) echo "Speech (ASR / TTS)" ;; | |
| image) echo "Image / Diffusion" ;; | |
| cloud) echo "Cloud / MLOps CLIs" ;; | |
| research) echo "Research / OSS Self-Host" ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| list_tools() { | |
| echo "" | |
| echo "${BLD}GenAI Tools Installer v${VERSION}${RST}" | |
| echo "${DIM}LOL-GAI lab installer — ${#TOOLS[@]} tools tracked${RST}" | |
| echo "" | |
| local current_cat="" | |
| for entry in "${TOOLS[@]}"; do | |
| local name cat desc | |
| name="$(tool_name "$entry")" | |
| cat="$(tool_category "$entry")" | |
| desc="$(tool_desc "$entry")" | |
| if [[ "$cat" != "$current_cat" ]]; then | |
| current_cat="$cat" | |
| echo "" | |
| echo " ${CYN}${BLD}$(cat_label "$cat")${RST}" | |
| echo " ${DIM}────────────────────────────────────────${RST}" | |
| fi | |
| printf " ${GRN}%-18s${RST} %s\n" "$name" "$desc" | |
| done | |
| echo "" | |
| echo "${DIM} Use --tool <name> to install one, or --all / --cli / --desktop / --local / --vscode / --mcp / --research for groups.${RST}" | |
| echo "" | |
| } | |
| check_tools() { | |
| echo "" | |
| echo "${BLD}GenAI Tools — Installation Status${RST}" | |
| echo "" | |
| local installed=0 not_installed=0 current_cat="" | |
| for entry in "${TOOLS[@]}"; do | |
| local name cat check desc | |
| name="$(tool_name "$entry")" | |
| cat="$(tool_category "$entry")" | |
| check="$(tool_check "$entry")" | |
| desc="$(tool_desc "$entry")" | |
| if [[ "$cat" != "$current_cat" ]]; then | |
| current_cat="$cat" | |
| echo "" | |
| echo " ${CYN}${BLD}$(cat_label "$cat")${RST}" | |
| fi | |
| if run_tool_check "$check"; then | |
| printf " ${GRN}%-18s %-4s${RST} %s\n" "$name" "[ok]" "$desc" | |
| installed=$((installed + 1)) | |
| else | |
| printf " ${DIM}%-18s %-4s${RST} %s\n" "$name" "[--]" "$desc" | |
| not_installed=$((not_installed + 1)) | |
| fi | |
| done | |
| echo "" | |
| echo " ${GRN}Installed: $installed${RST} ${DIM}Not installed: $not_installed${RST}" | |
| echo "" | |
| } | |
| do_install() { | |
| local name="$1" func="$2" check="$3" | |
| if eval "$check" &>/dev/null 2>&1; then | |
| ok "$name is already installed, skipping" | |
| SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) | |
| return 0 | |
| fi | |
| if [[ "$DRY_RUN" == true ]]; then | |
| info "[dry-run] Would install: $name" | |
| return 0 | |
| fi | |
| set +e | |
| "$func" | |
| local rc=$? | |
| set -e | |
| case "$rc" in | |
| 0) | |
| ok "$name installed successfully" | |
| INSTALLED_COUNT=$((INSTALLED_COUNT + 1)) | |
| ;; | |
| 2) | |
| info "$name unsupported on this platform — skipped" | |
| UNSUPPORTED_COUNT=$((UNSUPPORTED_COUNT + 1)) | |
| ;; | |
| *) | |
| err "$name installation failed" | |
| FAILED_COUNT=$((FAILED_COUNT + 1)) | |
| ;; | |
| esac | |
| } | |
| install_by_filter() { | |
| local filter="$1" | |
| local matched=false | |
| for entry in "${TOOLS[@]}"; do | |
| local name cat check func | |
| name="$(tool_name "$entry")" | |
| cat="$(tool_category "$entry")" | |
| check="$(tool_check "$entry")" | |
| func="$(tool_func "$entry")" | |
| case "$filter" in | |
| all) | |
| matched=true | |
| do_install "$name" "$func" "$check" | |
| ;; | |
| cli|chat|desktop|local|vscode|mcp|research|speech|image|cloud) | |
| if [[ "$cat" == "$filter" ]]; then | |
| matched=true | |
| do_install "$name" "$func" "$check" | |
| fi | |
| ;; | |
| *) | |
| if [[ "$name" == "$filter" ]]; then | |
| matched=true | |
| do_install "$name" "$func" "$check" | |
| fi | |
| ;; | |
| esac | |
| done | |
| if [[ "$matched" == false ]]; then | |
| err "No tool found matching: $filter" | |
| err "Run with --list to see available tools" | |
| return 1 | |
| fi | |
| } | |
| confirm_install() { | |
| local filter="$1" | |
| local count=0 names=() | |
| for entry in "${TOOLS[@]}"; do | |
| local name cat check | |
| name="$(tool_name "$entry")" | |
| cat="$(tool_category "$entry")" | |
| check="$(tool_check "$entry")" | |
| case "$filter" in | |
| all) ;; | |
| cli|chat|desktop|local|vscode|mcp|research|speech|image|cloud) | |
| [[ "$cat" != "$filter" ]] && continue | |
| ;; | |
| *) [[ "$name" != "$filter" ]] && continue ;; | |
| esac | |
| if ! eval "$check" &>/dev/null 2>&1; then | |
| names+=("$name") | |
| count=$((count + 1)) | |
| fi | |
| done | |
| if [[ $count -eq 0 ]]; then | |
| ok "Everything in '$filter' is already installed!" | |
| return 1 | |
| fi | |
| echo "" | |
| info "Will install $count tool(s): ${names[*]}" | |
| if [[ "$DRY_RUN" == true ]]; then | |
| dim "(dry-run mode — nothing will be installed)" | |
| return 0 | |
| fi | |
| if [[ "$YES" == true ]]; then | |
| return 0 | |
| fi | |
| if [[ ! -t 0 ]]; then | |
| return 0 | |
| fi | |
| echo "" | |
| read -rp " Continue? [Y/n] " answer | |
| case "${answer:-y}" in | |
| [Yy]*) return 0 ;; | |
| *) info "Aborted."; return 1 ;; | |
| esac | |
| } | |
| # ── banner ─────────────────────────────────────────────────────────── | |
| platform_label() { | |
| case "$PLATFORM" in | |
| darwin) echo "macOS" ;; | |
| linux) echo "Linux" ;; | |
| windows) echo "Windows" ;; | |
| *) echo "$PLATFORM" ;; | |
| esac | |
| } | |
| pkg_managers() { | |
| local mgrs="" | |
| has_brew && mgrs="${mgrs}brew " | |
| has_winget && mgrs="${mgrs}winget " | |
| has_scoop && mgrs="${mgrs}scoop " | |
| has_npm && mgrs="${mgrs}npm " | |
| has_pip && mgrs="${mgrs}pip " | |
| has_cmd uv && mgrs="${mgrs}uv " | |
| echo "${mgrs:-none detected}" | |
| } | |
| banner() { | |
| echo "" | |
| echo "${BLD} ╔══════════════════════════════════════════════╗${RST}" | |
| echo "${BLD} ║ GenAI Tools Installer v${VERSION} ║${RST}" | |
| echo "${BLD} ║ ${DIM}LOL-GAI${RST}${BLD} ║${RST}" | |
| echo "${BLD} ╚══════════════════════════════════════════════╝${RST}" | |
| echo "" | |
| echo " ${DIM}Platform: $(platform_label) (${ARCH} / ${ARCH_KIND})${RST}" | |
| echo " ${DIM}Managers: $(pkg_managers)${RST}" | |
| echo " ${DIM}Catalog: ${#TOOLS[@]} tools (cli / mcp / cloud / research / desktop / local / vscode)${RST}" | |
| echo "" | |
| } | |
| summary() { | |
| echo "" | |
| echo "${BLD}── Summary ──${RST}" | |
| [[ $INSTALLED_COUNT -gt 0 ]] && ok "Installed: $INSTALLED_COUNT" | |
| [[ $SKIPPED_COUNT -gt 0 ]] && info "Already present: $SKIPPED_COUNT" | |
| [[ $UNSUPPORTED_COUNT -gt 0 ]] && info "Unsupported on this OS: $UNSUPPORTED_COUNT" | |
| [[ $FAILED_COUNT -gt 0 ]] && err "Failed: $FAILED_COUNT" | |
| echo "" | |
| if [[ $INSTALLED_COUNT -gt 0 ]]; then | |
| dim "Next steps:" | |
| dim " 1. Launch each tool to generate initial process telemetry" | |
| dim " 2. Capture process/path/signing attribution for LOL-GAI YAML" | |
| dim " 3. Update yaml/<vendor>/<id>.yaml AttributionSignals" | |
| dim " 4. Re-run: python3 bin/validate.py && python3 bin/site.py" | |
| echo "" | |
| fi | |
| } | |
| # ── usage / help ───────────────────────────────────────────────────── | |
| usage() { | |
| echo "" | |
| echo "${BLD}Usage:${RST} install-genai-tools.sh [OPTIONS]" | |
| echo "" | |
| echo "${BLD}Options:${RST}" | |
| echo " --tool <name> Install a single tool by name" | |
| echo " --all Install everything" | |
| echo " --cli Install CLI agents only" | |
| echo " --desktop Install desktop/IDE apps only" | |
| echo " --local Install local model servers only" | |
| echo " --vscode Install VS Code extensions only" | |
| echo " --mcp Install MCP servers only" | |
| echo " --chat Install chat / provider CLIs only" | |
| echo " --speech Install speech (ASR/TTS) tools only" | |
| echo " --image Install image / diffusion tools only" | |
| echo " --cloud Install cloud / MLOps CLIs only" | |
| echo " --research Clone research / OSS self-host daemons (bolt.diy, openclaw, nemoclaw)" | |
| echo " --list List all supported tools" | |
| echo " --check Show installation status" | |
| echo " --dry-run Preview what would be installed" | |
| echo " --no-color Disable colored output" | |
| echo " --yes Skip confirmation prompts" | |
| echo " --log-file PATH Tee install output to PATH (for SSH / background runs)" | |
| echo " -h, --help Show this help" | |
| echo "" | |
| echo "${BLD}Windows SSH (BatchMode):${RST}" | |
| echo " ${DIM}scp scripts/install-genai-tools.sh user@host:install-genai-tools.sh${RST}" | |
| echo " ${DIM}ssh user@host \"\\\"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\\\" --noprofile --norc -c 'bash ~/install-genai-tools.sh --list > ~/list.out 2>&1; echo DONE:\$?'\"${RST}" | |
| echo " ${DIM}Prefer file redirect + scp over streaming long output. Use --log-file for installs.${RST}" | |
| echo "" | |
| echo "${BLD}Examples:${RST}" | |
| echo " ${DIM}# See what's available${RST}" | |
| echo " bash install-genai-tools.sh --list" | |
| echo "" | |
| echo " ${DIM}# Check what you already have${RST}" | |
| echo " bash install-genai-tools.sh --check" | |
| echo "" | |
| echo " ${DIM}# Install just Claude Code${RST}" | |
| echo " bash install-genai-tools.sh --tool claude-code" | |
| echo "" | |
| echo " ${DIM}# Install all CLI agents${RST}" | |
| echo " bash install-genai-tools.sh --cli" | |
| echo "" | |
| echo " ${DIM}# Install everything, no prompts${RST}" | |
| echo " bash install-genai-tools.sh --all --yes" | |
| echo "" | |
| echo " ${DIM}# Pipe from URL${RST}" | |
| echo " curl -sSL <url> | bash -s -- --tool cursor" | |
| echo " curl -sSL <url> | bash -s -- --all" | |
| echo "" | |
| } | |
| # ── main ───────────────────────────────────────────────────────────── | |
| main() { | |
| local action="" target="" | |
| # Non-interactive SSH / CI: never prompt; skip ANSI when stdout is not a TTY. | |
| if [[ ! -t 0 ]]; then | |
| YES=true | |
| fi | |
| if [[ ! -t 1 ]]; then | |
| NO_COLOR=true | |
| fi | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --tool) | |
| action="install"; target="${2:-}" | |
| [[ -z "$target" ]] && { err "--tool requires a name"; exit 1; } | |
| target="${target//_/-}" | |
| shift 2 ;; | |
| --all) action="install"; target="all"; shift ;; | |
| --cli) action="install"; target="cli"; shift ;; | |
| --desktop) action="install"; target="desktop"; shift ;; | |
| --local) action="install"; target="local"; shift ;; | |
| --vscode) action="install"; target="vscode"; shift ;; | |
| --mcp) action="install"; target="mcp"; shift ;; | |
| --chat) action="install"; target="chat"; shift ;; | |
| --speech) action="install"; target="speech"; shift ;; | |
| --image) action="install"; target="image"; shift ;; | |
| --cloud) action="install"; target="cloud"; shift ;; | |
| --research) action="install"; target="research"; shift ;; | |
| --list) action="list"; shift ;; | |
| --check) action="check"; shift ;; | |
| --dry-run) DRY_RUN=true; shift ;; | |
| --no-color) NO_COLOR=true; shift ;; | |
| --log-file) | |
| LOG_FILE="${2:-}" | |
| [[ -z "$LOG_FILE" ]] && { err "--log-file requires a path"; exit 1; } | |
| shift 2 ;; | |
| --yes|-y) YES=true; shift ;; | |
| -h|--help) action="help"; shift ;; | |
| *) | |
| err "Unknown option: $1" | |
| usage | |
| exit 1 ;; | |
| esac | |
| done | |
| detect_platform | |
| setup_colors | |
| register_tools | |
| if [[ -n "$LOG_FILE" && "$action" == "install" ]]; then | |
| mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| fi | |
| case "$action" in | |
| list) | |
| list_tools | |
| exit 0 | |
| ;; | |
| check) | |
| banner | |
| check_tools | |
| exit 0 | |
| ;; | |
| install) | |
| banner | |
| check_prerequisites | |
| # On Windows Server 2022 the host ships without winget; on Windows 11 | |
| # winget exists but the catalog source needs to be primed. Either way | |
| # this is a no-op on already-working hosts. Run before extension/IDE | |
| # installers that rely on `code` being recent enough (Roo Code etc.). | |
| # Skip heavy bootstrap during dry-run (winget source refresh can hang over SSH). | |
| if [[ "$DRY_RUN" != true ]]; then | |
| ensure_winget_windows | |
| update_vscode_if_old | |
| fi | |
| if confirm_install "$target"; then | |
| install_by_filter "$target" | |
| summary | |
| # Non-zero exit when any tool failed (for CI / smoke harnesses) | |
| if [[ "${FAILED_COUNT:-0}" -gt 0 ]]; then | |
| exit 1 | |
| fi | |
| fi | |
| ;; | |
| help) | |
| usage | |
| ;; | |
| "") | |
| banner | |
| usage | |
| ;; | |
| esac | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment