Skip to content

Instantly share code, notes, and snippets.

@Mikaayenson
Last active July 19, 2026 17:22
Show Gist options
  • Select an option

  • Save Mikaayenson/3f05db683761ba604bec1575607dce0c to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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