Last active 6 days ago

Interactive developer environment bootstrapper using official upstream downloads and signed OS-vendor packages.

Revision 886fce71935ec889aece0c7db3fe309859cafdc2

README.md Raw

Strict Developer Environment Installer

A robust, idempotent, and interactive bash script to bootstrap your macOS, Linux, or WSL development environment.

This script follows a "Strict Official Source" philosophy. It fetches binaries and installers directly from official maintainers, or from the operating system vendor's signed repositories when upstream does not publish native binaries for that platform.

✨ Key Features

  • Zero-Brew Dependency: By default, it bypasses Homebrew entirely, using official APIs, release pages, and signed OS repositories (Homebrew is available as an optional install).
  • Idempotent: Safe to run multiple times. It intelligently checks if a tool is already installed before attempting to download it.
  • Dynamic Versioning: Uses official APIs (like GitHub Releases or go.dev/VERSION) when maintainers publish versioned binaries. OS-packaged tools track the supported distribution version and security updates.
  • Auto-Path Management: Consistently writes installed-tool paths to ~/.zshrc, regardless of which shell launches the installer.
  • Architecture Aware: Automatically detects if you are running on Intel (x86_64) or Apple Silicon/ARM (aarch64/arm64) and downloads the correct binaries.

🚀 Usage

You can launch the interactive installer directly from your terminal with a single command:

bash -c "$(curl -fsSL https://tinyurl.com/4rskc9e7)"

Once the menu loads, simply follow the interactive prompts. You can select multiple tools at once by entering space-separated numbers (e.g., 1 3 8 12).

Choose 19) Update All Installed Software to update every detected tool in the menu from its official source. Tools that are not installed are skipped.

🛠️ Included Tools

Essentials & Package Managers

  • Build Tools: GCC, Make, Git, Curl, Unzip, and core libraries.
  • Homebrew: (Optional) Installed officially.

Languages & Runtimes

  • NVM (Node Version Manager): Includes interactive prompt to install LTS or Latest Node.js immediately.
  • Python 3: Fetches Python.org's latest stable .pkg for macOS. Linux and WSL install precompiled Python, pip, and venv from the signed OS-vendor repository without source compilation or third-party repositories.
  • Go: Fetches the latest tarball and installs system-wide to /usr/local/go.
  • SDKMAN: For managing Java, Kotlin, and Gradle.
  • .NET: Installed via Microsoft's official script.

Mobile Development

  • Android SDK: Installs command-line tools, platform tools, emulator, and current Android platform/build tools.

Containers & Infrastructure

  • Docker: Downloads the official .dmg (macOS) or runs the official get.docker.com script (Linux) with auto-group assignment.
  • Portainer: Deploys Portainer CE as a Docker container with persistent storage.

Cloud & App Platforms

  • AWS CLI: Fetches the official binaries directly from Amazon.
  • Google Cloud CLI (gcloud): Official Google setup script, installed at /opt/google-cloud-sdk.
  • Firebase CLI: Installs firebase-tools via NPM, so Node.js/NPM is required.

Developer Productivity

  • GitHub CLI (gh): Downloaded directly from GitHub Releases to /usr/local/bin.

Secrets & Security

  • Infisical CLI: Installs @infisical/cli via NPM for secret management, so Node.js/NPM is required.

AI & Agentic Tools

  • Claude Code CLI: Anthropic's official agent.
  • OpenAI Codex CLI: Required NVM/Node.js to run.
  • OpenCode: AI agent orchestration installed from the official OpenCode installer.
  • Augment Code CLI (Auggie): Installed from Augment's official NPM package; requires Node.js 20 or later.
  • CodeRabbit CLI: Local AI code reviews installed from CodeRabbit's official installer.

⚠️ Important Notes & Troubleshooting

  • Restart Your Shell: After running the script for the first time, restart your terminal or run source ~/.zshrc to ensure all new PATH variables (like NVM, Go, or .NET) take effect.
  • Docker Permissions (Linux): The script automatically adds your user to the docker group. You will need to log out and log back in for this to take effect.
  • AWS CLI on Linux: Ensure you install Build Tools (Option 1) first if you are on a fresh Linux install, as the AWS installer requires unzip to extract the payload.
  • Python Version on Linux: Python follows the version supported by your distribution rather than the newest python.org patch release. This keeps installation precompiled, vendor-supported, and eligible for normal OS security updates.

Built for developers who want total control over their toolchain.

menu.sh Raw
1#!/usr/bin/env bash
2
3# =======================================================
4# 1. BOOTSTRAPPER: PREFER ZSH, FALLBACK TO BASH
5# =======================================================
6if [ -z "${_PREFER_ZSH_BOOTSTRAPPED:-}" ]; then
7 export _PREFER_ZSH_BOOTSTRAPPED=1
8
9 # Only attempt to re-execute if $0 is an actual file on disk.
10 # This prevents the 'can't open input file' error when running
11 # from memory via `bash -c "$(curl ...)"` or `zsh -c`.
12 if [ -f "$0" ]; then
13 if command -v zsh >/dev/null 2>&1; then
14 exec zsh "$0" "$@"
15 elif command -v bash >/dev/null 2>&1; then
16 exec bash "$0" "$@"
17 else
18 echo "Error: Neither Zsh nor Bash is installed. Exiting."
19 exit 1
20 fi
21 fi
22fi
23
24set -u
25
26# =======================================================
27# 2. SHELL NORMALIZATION
28# =======================================================
29PROFILE_FILE="$HOME/.zshrc"
30
31if [ -n "${ZSH_VERSION:-}" ]; then
32 # Zsh: Enable Bash-like word splitting for unquoted variables (menu input)
33 setopt shwordsplit 2>/dev/null || true
34 CURRENT_SHELL="zsh"
35elif [ -n "${BASH_VERSION:-}" ]; then
36 CURRENT_SHELL="bash"
37else
38 CURRENT_SHELL="sh"
39fi
40
41# =======================================================
42# COLORS & LOGGING
43# =======================================================
44GREEN="\033[0;32m"
45RED="\033[0;31m"
46YELLOW="\033[1;33m"
47BLUE="\033[0;34m"
48NC="\033[0m"
49
50log() { printf "${GREEN}▶ %s${NC}\n" "$*"; }
51warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; }
52err() { printf "${RED}✖ %s${NC}\n" "$*"; }
53info() { printf "${BLUE}ℹ %s${NC}\n" "$*"; }
54
55# =======================================================
56# HELPERS & SYSTEM DETECTION
57# =======================================================
58require_cmd() { command -v "$1" >/dev/null 2>&1; }
59
60add_to_path_config() {
61 local label=$1
62 local path_line=$2
63 local target_file="${3:-$PROFILE_FILE}"
64
65 if [[ -f "$target_file" ]]; then
66 if ! grep -q "$label" "$target_file"; then
67 printf "\n# %s\n%s\n" "$label" "$path_line" >> "$target_file"
68 log "Added $label to $target_file"
69 fi
70 else
71 printf "\n# %s\n%s\n" "$label" "$path_line" >> "$PROFILE_FILE"
72 log "Added $label to shell profile ($PROFILE_FILE)."
73 fi
74
75 # Instantly evaluate the path line so the current script session
76 # can immediately use the newly installed tool in subsequent steps.
77 eval "$path_line" 2>/dev/null || true
78}
79
80detect_os_arch() {
81 ARCH=$(uname -m)
82 case "$ARCH" in
83 x86_64|amd64) SYS_ARCH="amd64"; MAC_ARCH="x86_64"; AWS_ARCH="x86_64" ;;
84 aarch64|arm64) SYS_ARCH="arm64"; MAC_ARCH="arm64"; AWS_ARCH="aarch64" ;;
85 *) err "Unsupported architecture: $ARCH"; exit 1 ;;
86 esac
87
88 if [[ "$OSTYPE" == "darwin"* ]]; then
89 OS="macOS"
90 OS_LOWER="darwin"
91 elif grep -qi microsoft /proc/version 2>/dev/null; then
92 OS="WSL"
93 OS_LOWER="linux"
94 elif [[ -f /etc/os-release ]]; then
95 . /etc/os-release
96 [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* ]] && OS="Ubuntu" || OS="Linux"
97 OS_LOWER="linux"
98 else
99 OS="Unknown"
100 OS_LOWER="unknown"
101 fi
102 log "Detected: $OS ($ARCH) running $CURRENT_SHELL"
103}
104
105detect_os_arch
106
107# =======================================================
108# CORE RUNTIMES & MANAGERS
109# =======================================================
110
111install_build_tools() {
112 log "Installing Build Essentials & Core Dependencies..."
113 case "$OS" in
114 Ubuntu|WSL) sudo apt-get update && sudo apt-get install -y build-essential curl wget git jq unzip libssl-dev zlib1g-dev libffi-dev libsqlite3-dev ;;
115 macOS) xcode-select --install || warn "Xcode tools already installed" ;;
116 *) warn "Manual installation required for $OS." ;;
117 esac
118}
119
120install_brew() {
121 if require_cmd brew; then warn "Homebrew already installed"; return; fi
122 log "Installing Homebrew from Official Source..."
123
124 # Homebrew's installer specifically requires Bash execution, regardless of current shell
125 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
126
127 if [[ "$OS_LOWER" == "linux" ]]; then
128 add_to_path_config "HOMEBREW" 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"'
129 elif [[ "$MAC_ARCH" == "arm64" ]]; then
130 add_to_path_config "HOMEBREW" 'eval "$(/opt/homebrew/bin/brew shellenv)"'
131 else
132 add_to_path_config "HOMEBREW" 'eval "$(/usr/local/bin/brew shellenv)"'
133 fi
134}
135
136install_nvm() {
137 # 1. Temporarily disable 'unbound variable' strictness for NVM
138 set +u
139
140 export NVM_DIR="$HOME/.nvm"
141
142 if [ -d "$NVM_DIR" ]; then
143 warn "NVM is already installed."
144 else
145 log "Installing NVM via $CURRENT_SHELL..."
146 curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | $CURRENT_SHELL
147 fi
148
149 [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
150
151 printf "\n${YELLOW}Which version of Node.js would you like to install?${NC}\n"
152 printf " 1) LTS (Long Term Support - Recommended for stability)\n"
153 printf " 2) Latest (Current features - Recommended for testing new APIs)\n"
154 printf " 3) Skip installing Node.js right now\n"
155
156 printf "Select (1/2/3): "
157 read node_choice
158
159 case "$node_choice" in
160 2)
161 log "Installing Latest Node.js..."
162 nvm install node
163 nvm alias default node
164 nvm use node
165
166 log "Disabling npm logs..."
167 npm config set logs-max 0
168 ;;
169 3)
170 log "Skipping Node.js installation."
171 ;;
172 *)
173 log "Installing Latest LTS Node.js..."
174 nvm install --lts
175 nvm alias default 'lts/*'
176 nvm use --lts
177
178 log "Disabling npm logs..."
179 npm config set logs-max 0
180 ;;
181 esac
182
183 # 2. Re-enable 'unbound variable' strictness for the rest of your script
184 set -u
185}
186
187install_python() {
188 if [[ "$OS" == "macOS" ]]; then
189 log "Fetching latest stable Python version..."
190 LATEST_PY=$(
191 curl -fsSL --compressed https://www.python.org/downloads/ |
192 sed -nE 's/.*Download Python ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' |
193 head -1
194 )
195 if [[ -z "$LATEST_PY" ]]; then
196 err "Could not detect the latest stable Python version from python.org."
197 return 1
198 fi
199
200 log "Detected Stable Version: $LATEST_PY"
201 log "Downloading official macOS package..."
202 PKG_URL="https://www.python.org/ftp/python/${LATEST_PY}/python-${LATEST_PY}-macos11.pkg"
203 curl -fsSL --compressed -o /tmp/python.pkg "$PKG_URL" || {
204 err "Failed to download Python macOS package from $PKG_URL"
205 return 1
206 }
207
208 log "Running installer..."
209 sudo installer -pkg /tmp/python.pkg -target / || {
210 err "Python macOS installer failed."
211 rm -f /tmp/python.pkg
212 return 1
213 }
214 rm /tmp/python.pkg
215 else
216 log "Installing precompiled Python from the official OS repository..."
217 if require_cmd apt-get; then
218 sudo apt-get update && \
219 sudo apt-get install -y python3 python3-pip python3-venv
220 elif require_cmd dnf; then
221 sudo dnf install -y python3 python3-pip
222 elif require_cmd zypper; then
223 sudo zypper --non-interactive install python3 python3-pip
224 elif require_cmd pacman; then
225 sudo pacman -Sy --needed python python-pip
226 elif require_cmd apk; then
227 sudo apk add python3 py3-pip py3-virtualenv
228 else
229 err "No supported official OS package manager found for Python."
230 return 1
231 fi
232 fi
233
234 require_cmd python3 || { err "python3 was not installed successfully."; return 1; }
235 python3 -m pip --version >/dev/null 2>&1 || {
236 err "pip is unavailable for the installed Python."
237 return 1
238 }
239 python3 -m venv --help >/dev/null 2>&1 || {
240 err "venv is unavailable for the installed Python."
241 return 1
242 }
243
244 log "$(python3 --version) installed with pip and venv support."
245}
246
247install_go() {
248 log "Fetching latest Go version from official source..."
249 LATEST_GO=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n 1) || {
250 err "Could not determine the latest Go version."
251 return 1
252 }
253 TAR_FILE="${LATEST_GO}.${OS_LOWER}-${SYS_ARCH}.tar.gz"
254
255 if [[ -x /usr/local/go/bin/go ]] && [[ "$(/usr/local/go/bin/go version)" == "go version $LATEST_GO "* ]]; then
256 warn "$LATEST_GO is already installed."
257 add_to_path_config "GO_BIN" 'export PATH="$PATH:/usr/local/go/bin"'
258 return 0
259 fi
260
261 log "Downloading $TAR_FILE..."
262 curl -fsSL -o /tmp/go.tar.gz "https://go.dev/dl/$TAR_FILE" || {
263 err "Failed to download $TAR_FILE."
264 rm -f /tmp/go.tar.gz
265 return 1
266 }
267
268 log "Installing to /usr/local/go..."
269 if ! sudo rm -rf /usr/local/go || ! sudo tar -C /usr/local -xzf /tmp/go.tar.gz; then
270 err "Go installation failed. Run this installer from an interactive terminal so sudo can authenticate."
271 rm -f /tmp/go.tar.gz
272 return 1
273 fi
274 rm -f /tmp/go.tar.gz
275 add_to_path_config "GO_BIN" 'export PATH="$PATH:/usr/local/go/bin"'
276 log "$(/usr/local/go/bin/go version) installed successfully."
277}
278
279install_sdkman() {
280 if [ -d "$HOME/.sdkman" ]; then warn "SDKMAN already exists"; return; fi
281
282 # SDKMAN explicitly blocks macOS's default Bash 3.2.
283 # If we are trapped in Bash < 4, force the installer to use Zsh.
284 if [[ "$CURRENT_SHELL" == "bash" && "${BASH_VERSINFO[0]:-0}" -lt 4 ]]; then
285 log "Outdated Bash detected. Installing SDKMAN via Zsh..."
286 curl -s "https://get.sdkman.io" | zsh
287 else
288 log "Installing SDKMAN via $CURRENT_SHELL..."
289 curl -s "https://get.sdkman.io" | $CURRENT_SHELL
290 fi
291}
292
293install_dotnet() {
294 log "Installing .NET from official script..."
295 curl -fsSL https://dot.net/v1/dotnet-install.sh | $CURRENT_SHELL
296 add_to_path_config "DOTNET_TOOLS" 'export PATH="$PATH:$HOME/.dotnet/tools"'
297}
298
299# Prints the major Java version of the given java binary, or fails.
300# Handles both "21.0.4" and legacy "1.8.0_xxx" style version strings
301# (legacy ones report as 1, which correctly counts as too old).
302java_major() {
303 local out
304 out=$("$1" -version 2>&1) || return 1
305 printf '%s\n' "$out" | sed -nE 's/.*version "([0-9]+).*/\1/p' | head -1
306}
307
308# The Android command line tools (sdkmanager, avdmanager) are Java programs
309# and need JDK 17 or newer. On macOS, /usr/bin/java is a stub that exists even
310# when no JDK is installed, so `command -v java` is not enough: we must run it.
311ensure_android_java() {
312 local major home jbr
313
314 # Pick up an SDKMAN managed JDK if SDKMAN is installed.
315 if [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]]; then
316 set +u; source "$HOME/.sdkman/bin/sdkman-init.sh"; set -u
317 fi
318
319 major=$(java_major java 2>/dev/null)
320 if [[ -n "$major" ]] && (( major >= 17 )); then
321 log "Using Java $major for the Android SDK tools."
322 return 0
323 fi
324
325 if [[ "$OS" == "macOS" ]]; then
326 # A JDK registered with macOS (Temurin, Zulu, Oracle .pkg installers).
327 home=$(/usr/libexec/java_home -v 17+ 2>/dev/null || true)
328 if [[ -n "$home" && -x "$home/bin/java" ]]; then
329 export JAVA_HOME="$home"
330 export PATH="$JAVA_HOME/bin:$PATH"
331 log "Using the JDK at $JAVA_HOME"
332 return 0
333 fi
334
335 # The JDK bundled inside Android Studio (not registered with macOS).
336 jbr="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
337 if [[ -x "$jbr/bin/java" ]]; then
338 add_to_path_config "ANDROID_STUDIO_JBR" 'export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
339export PATH="$JAVA_HOME/bin:$PATH"'
340 log "Using the JDK bundled with Android Studio."
341 return 0
342 fi
343 fi
344
345 # Nothing usable: provision a JDK through SDKMAN.
346 if [[ ! -s "$HOME/.sdkman/bin/sdkman-init.sh" ]]; then
347 log "No Java 17+ found. Installing SDKMAN to provide a JDK..."
348 install_sdkman
349 fi
350
351 if [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]]; then
352 log "Installing the default Temurin JDK through SDKMAN..."
353 set +u
354 source "$HOME/.sdkman/bin/sdkman-init.sh"
355 yes | sdk install java
356 # Re-source so the freshly installed JDK lands on PATH and JAVA_HOME.
357 source "$HOME/.sdkman/bin/sdkman-init.sh"
358 set -u
359
360 major=$(java_major java 2>/dev/null)
361 if [[ -n "$major" ]] && (( major >= 17 )); then
362 log "Using Java $major for the Android SDK tools."
363 return 0
364 fi
365 fi
366
367 err "Java 17 or newer is required by the Android SDK tools, but none could be set up."
368 err "Install a JDK (for example: sdk install java), open a new terminal, then rerun Option 8."
369 return 1
370}
371
372# Prints the download URL of the newest Android command line tools zip.
373# Tries the Android Studio download page first, then falls back to Google's
374# SDK repository manifest, which is a stable machine readable source.
375latest_cmdline_tools_url() {
376 local tools_os=$1 file
377
378 file=$(curl -fsSL --compressed https://developer.android.com/studio 2>/dev/null \
379 | grep -oE "commandlinetools-${tools_os}-[0-9]+_latest\.zip" \
380 | head -1)
381
382 if [[ -z "$file" ]]; then
383 file=$(curl -fsSL --compressed https://dl.google.com/android/repository/repository2-3.xml 2>/dev/null \
384 | grep -oE "commandlinetools-${tools_os}-[0-9]+_latest\.zip" \
385 | sort -t- -k3 -n \
386 | tail -1)
387 fi
388
389 [[ -n "$file" ]] && printf 'https://dl.google.com/android/repository/%s\n' "$file"
390}
391
392install_android() {
393 log "Installing Android SDK command-line tools..."
394
395 if ! require_cmd unzip; then
396 err "unzip is required. Please install 'Build Tools' (Option 1) first."
397 return 1
398 fi
399
400 if [[ "$OS_LOWER" == "linux" && "$SYS_ARCH" != "amd64" ]]; then
401 err "Google's Android command-line tools installer supports Linux x86_64 only."
402 return 1
403 fi
404
405 if [[ "$OS" == "macOS" ]]; then
406 TOOLS_OS="mac"
407 elif [[ "$OS_LOWER" == "linux" ]]; then
408 TOOLS_OS="linux"
409 else
410 err "Android SDK installation is not supported for $OS."
411 return 1
412 fi
413
414 # sdkmanager is a Java program, so fail fast before downloading anything.
415 ensure_android_java || return 1
416
417 ANDROID_HOME="$HOME/Android/Sdk"
418 ANDROID_SDK_ROOT="$ANDROID_HOME"
419 export ANDROID_HOME ANDROID_SDK_ROOT
420
421 mkdir -p "$ANDROID_HOME/cmdline-tools"
422
423 log "Finding latest Android command-line tools from Google..."
424 TOOLS_URL=$(latest_cmdline_tools_url "$TOOLS_OS")
425
426 if [[ -z "$TOOLS_URL" ]]; then
427 err "Could not find the Android command-line tools download URL."
428 return 1
429 fi
430
431 log "Downloading $TOOLS_URL..."
432 rm -rf /tmp/android-cmdline-tools /tmp/android-cmdline-tools.zip
433 mkdir -p /tmp/android-cmdline-tools
434 if ! curl -fsSL -o /tmp/android-cmdline-tools.zip "$TOOLS_URL" || \
435 ! unzip -q /tmp/android-cmdline-tools.zip -d /tmp/android-cmdline-tools; then
436 err "Failed to download or extract the Android command-line tools."
437 rm -rf /tmp/android-cmdline-tools /tmp/android-cmdline-tools.zip
438 return 1
439 fi
440
441 rm -rf "$ANDROID_HOME/cmdline-tools/latest"
442 mkdir -p "$ANDROID_HOME/cmdline-tools/latest"
443 mv /tmp/android-cmdline-tools/cmdline-tools/* "$ANDROID_HOME/cmdline-tools/latest/"
444 rm -rf /tmp/android-cmdline-tools /tmp/android-cmdline-tools.zip
445
446 local tools_bin="$ANDROID_HOME/cmdline-tools/latest/bin"
447
448 if [[ -x "$tools_bin/android" ]]; then
449 # Current cmdline-tools ship the Android CLI ('android'), and
450 # 'android sdk' replaces the deprecated sdkmanager. It is left
451 # interactive on purpose so any license prompt reaches you.
452 log "Installing Android SDK packages with Android CLI..."
453 if ! "$tools_bin/android" --sdk="$ANDROID_HOME" sdk install \
454 cmdline-tools/latest \
455 platform-tools \
456 emulator \
457 platforms/android-36 \
458 build-tools/36.0.0; then
459 err "Android CLI failed to install the SDK packages. See the error above."
460 return 1
461 fi
462 elif [[ -x "$tools_bin/sdkmanager" ]]; then
463 # Older cmdline-tools without the Android CLI: use sdkmanager.
464 warn "Android CLI not found in cmdline-tools. Falling back to sdkmanager."
465 SDKMANAGER="$tools_bin/sdkmanager"
466
467 # stdout is silenced, stderr is kept so Java or network errors stay visible.
468 log "Accepting Android SDK licenses..."
469 if ! yes | "$SDKMANAGER" --sdk_root="$ANDROID_HOME" --licenses >/dev/null; then
470 err "sdkmanager could not run. See the error above."
471 return 1
472 fi
473
474 log "Installing Android SDK packages..."
475 if ! "$SDKMANAGER" --sdk_root="$ANDROID_HOME" \
476 "cmdline-tools;latest" \
477 "platform-tools" \
478 "emulator" \
479 "platforms;android-36" \
480 "build-tools;36.0.0"; then
481 err "Failed to install Android SDK packages."
482 return 1
483 fi
484 else
485 err "Neither Android CLI nor sdkmanager was found in $tools_bin."
486 return 1
487 fi
488
489 add_to_path_config "ANDROID_SDK" 'export ANDROID_HOME="$HOME/Android/Sdk"
490export ANDROID_SDK_ROOT="$ANDROID_HOME"
491export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
492
493 log "Android SDK installation sequence finished!"
494}
495
496# =======================================================
497# CLOUD & INFRASTRUCTURE
498# =======================================================
499
500install_docker() {
501 log "Checking Docker status..."
502
503 # 1. Install if missing
504 if ! require_cmd docker; then
505 log "Installing Docker from official source..."
506 if [[ "$OS" == "macOS" ]]; then
507 [[ "$SYS_ARCH" == "arm64" ]] && DOCKER_MAC_ARCH="arm64" || DOCKER_MAC_ARCH="amd64"
508 DMG_URL="https://desktop.docker.com/mac/main/${DOCKER_MAC_ARCH}/Docker.dmg"
509 curl -fsSL -o /tmp/Docker.dmg "$DMG_URL"
510 hdiutil attach /tmp/Docker.dmg -nobrowse -mountpoint /Volumes/Docker
511 sudo cp -a /Volumes/Docker/Docker.app /Applications/
512 hdiutil detach /Volumes/Docker
513 rm /tmp/Docker.dmg
514
515 log "Starting Docker Desktop..."
516 open /Applications/Docker.app
517 log "Please complete the setup in the Docker Desktop UI."
518 else
519 curl -fsSL https://get.docker.com | sudo sh
520 fi
521 else
522 warn "Docker is already installed. Enforcing permissions..."
523 fi
524
525 # 2. Aggressively enforce Linux permissions
526 if [[ "$OS" != "macOS" ]]; then
527 log "Applying strict Linux post-install actions..."
528
529 # Ensure docker group exists and user is added
530 sudo groupadd -f docker
531 sudo usermod -aG docker "$USER"
532
533 # Ensure services are enabled and running
534 if command -v systemctl >/dev/null 2>&1; then
535 sudo systemctl enable --now docker.service
536 sudo systemctl enable --now docker.socket containerd.service 2>/dev/null || true
537 elif [[ "$OS" == "WSL" ]]; then
538 sudo service docker start
539 fi
540
541 # Give the system a second to generate the socket file
542 sleep 2
543
544 # Lock in ownership and permissions on the socket
545 if [[ -S /var/run/docker.sock ]]; then
546 log "Securing /var/run/docker.sock..."
547 sudo chown root:docker /var/run/docker.sock
548 sudo chmod 660 /var/run/docker.sock
549 else
550 warn "Docker socket not found. The service may have failed to start."
551 fi
552
553 log "Added $USER to the docker group and secured the socket."
554 fi
555}
556
557install_portainer() {
558 log "Installing Portainer CE..."
559
560 if ! require_cmd docker; then
561 err "Docker is required. Please install Docker (Option 9) first."
562 return 1
563 fi
564
565 if ! docker info >/dev/null 2>&1; then
566 err "Docker is installed but the daemon is not running. Start Docker, then retry Portainer."
567 return 1
568 fi
569
570 if docker ps -a --format '{{.Names}}' | grep -Fxq portainer; then
571 warn "Portainer container already exists."
572 if ! docker ps --format '{{.Names}}' | grep -Fxq portainer; then
573 log "Starting existing Portainer container..."
574 docker start portainer
575 fi
576 log "Portainer is available at https://localhost:9443"
577 return 0
578 fi
579
580 docker volume create portainer_data || {
581 err "Failed to create the Portainer data volume."
582 return 1
583 }
584
585 if ! docker run -d \
586 -p 8000:8000 \
587 -p 9443:9443 \
588 --name portainer \
589 --restart=always \
590 -v /var/run/docker.sock:/var/run/docker.sock \
591 -v portainer_data:/data \
592 portainer/portainer-ce:sts; then
593 err "Failed to start the Portainer container."
594 return 1
595 fi
596
597 log "Portainer is available at https://localhost:9443"
598}
599
600install_aws() {
601 if require_cmd aws; then warn "AWS CLI exists"; return; fi
602 log "Installing AWS CLI directly from Amazon..."
603
604 if [[ "$OS" == "macOS" ]]; then
605 curl -fsSL -o /tmp/AWSCLIV2.pkg "https://awscli.amazonaws.com/AWSCLIV2.pkg"
606 sudo installer -pkg /tmp/AWSCLIV2.pkg -target /
607 rm /tmp/AWSCLIV2.pkg
608 else
609 if ! require_cmd unzip; then
610 err "unzip is required. Please install 'Build Tools' (Option 1) first."
611 return 1
612 fi
613 curl -fsSL -o /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip"
614 cd /tmp && unzip -q awscliv2.zip && sudo ./aws/install
615 rm -rf /tmp/awscliv2.zip /tmp/aws
616 fi
617}
618
619install_gcloud() {
620 local gcloud_dir="/opt/google-cloud-sdk"
621
622 if [[ -x "$gcloud_dir/bin/gcloud" ]]; then warn "Google Cloud CLI exists at $gcloud_dir"; return; fi
623 log "Installing Google Cloud CLI to $gcloud_dir..."
624 curl -fsSL https://sdk.cloud.google.com | sudo bash -s -- --disable-prompts --install-dir=/opt
625 sudo chown -R "$(id -u):$(id -g)" "$gcloud_dir"
626
627 if [[ "$CURRENT_SHELL" == "zsh" ]]; then
628 add_to_path_config "GCLOUD" '[ -f "/opt/google-cloud-sdk/path.zsh.inc" ] && source "/opt/google-cloud-sdk/path.zsh.inc"'
629 else
630 add_to_path_config "GCLOUD" '[ -f "/opt/google-cloud-sdk/path.bash.inc" ] && source "/opt/google-cloud-sdk/path.bash.inc"'
631 fi
632}
633
634install_firebase() {
635 if require_cmd firebase; then warn "Firebase CLI exists"; return; fi
636 log "Installing Firebase CLI via NPM to ensure ARM64 compatibility..."
637
638 if ! require_cmd npm; then
639 err "NPM not found. Please install NVM (Option 3) first."
640 return 1
641 fi
642
643 npm install -g firebase-tools
644}
645
646# =======================================================
647# DEV TOOLS & SECURITY
648# =======================================================
649
650install_gh() {
651 if require_cmd gh; then warn "GitHub CLI exists"; return; fi
652 log "Fetching latest GitHub CLI version..."
653 LATEST_GH=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
654 TAR_NAME="gh_${LATEST_GH}_${OS_LOWER}_${SYS_ARCH}"
655
656 curl -fsSL -o /tmp/gh.tar.gz "https://github.com/cli/cli/releases/download/v${LATEST_GH}/${TAR_NAME}.tar.gz"
657 tar -xzf /tmp/gh.tar.gz -C /tmp
658 sudo mv "/tmp/${TAR_NAME}/bin/gh" /usr/local/bin/
659 sudo rm -rf "/tmp/${TAR_NAME}" /tmp/gh.tar.gz
660}
661
662install_infisical() {
663 if require_cmd infisical; then warn "Infisical CLI exists"; return; fi
664 log "Installing Infisical CLI via NPM..."
665
666 if ! require_cmd npm; then
667 err "NPM not found. Please install NVM (Option 3) first."
668 return 1
669 fi
670
671 npm install -g @infisical/cli
672}
673
674# =======================================================
675# AI & AGENTIC TOOLS
676# =======================================================
677
678install_claude() { log "Installing Claude Code..."; curl -fsSL https://claude.ai/install.sh | $CURRENT_SHELL; }
679
680install_opencode() { log "Installing OpenCode..."; curl -fsSL https://opencode.ai/install | $CURRENT_SHELL; }
681
682install_codex() {
683 log "Installing @openai/codex..."
684 if ! require_cmd npm; then err "Node.js/NPM is required. Install NVM (3) first."; return 1; fi
685 npm i -g @openai/codex
686}
687
688install_augment() {
689 if require_cmd auggie; then warn "Augment Code CLI exists"; return; fi
690 log "Installing Augment Code CLI (Auggie)..."
691 if ! require_cmd npm; then err "Node.js 20+/NPM is required. Install NVM (3) first."; return 1; fi
692 npm install -g @augmentcode/auggie
693}
694
695install_coderabbit() {
696 if require_cmd coderabbit || require_cmd cr; then warn "CodeRabbit CLI exists"; return; fi
697 log "Installing CodeRabbit CLI..."
698 curl -fsSL https://cli.coderabbit.ai/install.sh | sh
699}
700
701update_all() {
702 log "Updating all installed software from official sources..."
703
704 if require_cmd apt-get; then
705 sudo apt-get update && sudo apt-get install -y --only-upgrade \
706 build-essential curl wget git jq unzip libssl-dev zlib1g-dev libffi-dev libsqlite3-dev \
707 python3 python3-pip python3-venv
708 elif require_cmd dnf; then
709 sudo dnf upgrade -y python3 python3-pip
710 elif require_cmd zypper; then
711 sudo zypper --non-interactive update python3 python3-pip
712 elif require_cmd pacman; then
713 sudo pacman -Syu --noconfirm python python-pip
714 elif require_cmd apk; then
715 sudo apk upgrade
716 fi
717
718 if [[ "$OS" == "macOS" ]] && require_cmd python3; then
719 install_python
720 fi
721
722 require_cmd brew && { brew update && brew upgrade; }
723
724 if [[ -s "$HOME/.nvm/nvm.sh" ]]; then
725 set +u
726 export NVM_DIR="$HOME/.nvm"
727 LATEST_NVM=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
728 curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${LATEST_NVM}/install.sh" | PROFILE=/dev/null $CURRENT_SHELL
729 \. "$NVM_DIR/nvm.sh"
730 nvm install --lts --reinstall-packages-from=current
731 nvm alias default 'lts/*'
732 nvm use --lts
733 set -u
734 fi
735
736 require_cmd go && install_go
737
738 if [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]]; then
739 set +u
740 source "$HOME/.sdkman/bin/sdkman-init.sh"
741 sdk selfupdate force
742 sdk upgrade
743 set -u
744 fi
745
746 if require_cmd dotnet; then
747 curl -fsSL https://dot.net/v1/dotnet-install.sh | $CURRENT_SHELL
748 fi
749
750 local android_sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
751 local android_bin="$android_sdk/cmdline-tools/latest/bin"
752 if [[ -x "$android_bin/android" ]]; then
753 log "Updating Android SDK packages with Android CLI..."
754 "$android_bin/android" --sdk="$android_sdk" sdk update || \
755 warn "Android CLI could not update the SDK. See the error above."
756 elif [[ -x "$android_bin/sdkmanager" ]]; then
757 if ensure_android_java; then
758 yes | "$android_bin/sdkmanager" --licenses >/dev/null || true
759 "$android_bin/sdkmanager" --update
760 else
761 warn "Skipping the Android SDK update because no usable Java was found."
762 fi
763 fi
764
765 if require_cmd docker; then
766 if [[ "$OS" == "macOS" ]]; then
767 docker desktop update || warn "Update Docker Desktop from its application menu."
768 else
769 curl -fsSL https://get.docker.com | sudo sh
770 fi
771 fi
772
773 if require_cmd docker && docker ps -a --format '{{.Names}}' | grep -Fxq portainer; then
774 log "Updating Portainer CE..."
775 docker pull portainer/portainer-ce:sts && \
776 docker stop portainer && \
777 docker rm portainer && \
778 docker run -d \
779 -p 8000:8000 \
780 -p 9443:9443 \
781 --name portainer \
782 --restart=always \
783 -v /var/run/docker.sock:/var/run/docker.sock \
784 -v portainer_data:/data \
785 portainer/portainer-ce:sts
786 fi
787
788 if require_cmd aws; then
789 if [[ "$OS" == "macOS" ]]; then
790 curl -fsSL -o /tmp/AWSCLIV2.pkg https://awscli.amazonaws.com/AWSCLIV2.pkg && \
791 sudo installer -pkg /tmp/AWSCLIV2.pkg -target /
792 rm -f /tmp/AWSCLIV2.pkg
793 elif require_cmd unzip; then
794 curl -fsSL -o /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" && \
795 rm -rf /tmp/aws && unzip -q /tmp/awscliv2.zip -d /tmp && \
796 sudo /tmp/aws/install --update
797 rm -rf /tmp/aws /tmp/awscliv2.zip
798 fi
799 fi
800
801 require_cmd gcloud && gcloud components update --quiet
802 require_cmd firebase && npm install -g firebase-tools@latest
803
804 if require_cmd gh; then
805 log "Updating GitHub CLI..."
806 LATEST_GH=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
807 TAR_NAME="gh_${LATEST_GH}_${OS_LOWER}_${SYS_ARCH}"
808 curl -fsSL -o /tmp/gh.tar.gz "https://github.com/cli/cli/releases/download/v${LATEST_GH}/${TAR_NAME}.tar.gz" && \
809 tar -xzf /tmp/gh.tar.gz -C /tmp && \
810 sudo mv "/tmp/${TAR_NAME}/bin/gh" /usr/local/bin/
811 sudo rm -rf "/tmp/${TAR_NAME}" /tmp/gh.tar.gz
812 fi
813
814 require_cmd infisical && npm install -g @infisical/cli@latest
815 require_cmd claude && { curl -fsSL https://claude.ai/install.sh | $CURRENT_SHELL; }
816 require_cmd codex && npm install -g @openai/codex@latest
817 require_cmd opencode && { curl -fsSL https://opencode.ai/install | $CURRENT_SHELL; }
818 require_cmd auggie && npm install -g @augmentcode/auggie@latest
819 if require_cmd coderabbit || require_cmd cr; then
820 curl -fsSL https://cli.coderabbit.ai/install.sh | sh
821 fi
822
823 log "Finished updating installed software. Review any warnings or errors above."
824}
825
826# =======================================================
827# MENU LOGIC
828# =======================================================
829show_menu() {
830 clear
831 printf "${BLUE}==================================================${NC}\n"
832 printf "${GREEN} DEVELOPER ENVIRONMENT INSTALLER (Polyglot) ${NC}\n"
833 printf "${BLUE}==================================================${NC}\n"
834 printf "${YELLOW}--- Essentials & Package Managers ---${NC}\n"
835 printf " 1) Build Tools (GCC/Make/Git/Curl/Unzip)\n"
836 printf " 2) Homebrew (Optional)\n"
837 printf "${YELLOW}--- Languages & Runtimes ---${NC}\n"
838 printf " 3) NVM + Node.js\n"
839 printf " 4) Python 3\n"
840 printf " 5) Go\n"
841 printf " 6) SDKMAN (Java/Kotlin/Gradle)\n"
842 printf " 7) .NET\n"
843 printf "${YELLOW}--- Mobile Development ---${NC}\n"
844 printf " 8) Android SDK (CLI + Platform Tools)\n"
845 printf "${YELLOW}--- Containers & Infrastructure ---${NC}\n"
846 printf " 9) Docker\n"
847 printf " 13) Portainer (Docker UI)\n"
848 printf "${YELLOW}--- Cloud & App Platforms ---${NC}\n"
849 printf " 10) AWS CLI\n"
850 printf " 11) Google Cloud CLI (gcloud)\n"
851 printf " 12) Firebase CLI\n"
852 printf "${YELLOW}--- Developer Productivity ---${NC}\n"
853 printf " 14) GitHub CLI\n"
854 printf "${YELLOW}--- Secrets & Security ---${NC}\n"
855 printf " 15) Infisical CLI\n"
856 printf "${YELLOW}--- AI & Agentic Tools ---${NC}\n"
857 printf " 16) Claude Code CLI\n"
858 printf " 17) OpenAI Codex CLI\n"
859 printf " 18) OpenCode\n"
860 printf " 20) Augment Code CLI (Auggie)\n"
861 printf " 21) CodeRabbit CLI\n"
862 printf "${BLUE}==================================================${NC}\n"
863 printf " 19) Update All Installed Software\n"
864 printf " 99) Quit & Refresh Shell\n"
865 printf "${BLUE}==================================================${NC}\n"
866}
867
868while true; do
869 show_menu
870
871 # Bulletproof prompt for both Bash and Zsh
872 printf "Select options (space-separated): "
873 if ! read input; then
874 printf "\n"
875 warn "Input closed. Exiting installer."
876 exit 0
877 fi
878
879 # Thanks to 'setopt shwordsplit' in Zsh, this behaves perfectly across both shells
880 for choice in $input; do
881 case "$choice" in
882 1) install_build_tools ;;
883 2) install_brew ;;
884 3) install_nvm ;;
885 4) install_python ;;
886 5) install_go ;;
887 6) install_sdkman ;;
888 7) install_dotnet ;;
889 8) install_android ;;
890 9) install_docker ;;
891 10) install_aws ;;
892 11) install_gcloud ;;
893 12) install_firebase ;;
894 13) install_portainer ;;
895 14) install_gh ;;
896 15) install_infisical ;;
897 16) install_claude ;;
898 17) install_codex ;;
899 18) install_opencode ;;
900 19) update_all ;;
901 20) install_augment ;;
902 21) install_coderabbit ;;
903 99)
904 log "Installation complete! Refreshing terminal environment..."
905
906 # If on Linux, forcefully inherit the new docker group without needing a logout
907 if [[ "$OS" != "macOS" ]] && command -v sg >/dev/null 2>&1; then
908 exec sg docker -c "exec ${SHELL:-zsh}"
909 else
910 exec "${SHELL:-zsh}"
911 fi
912 ;;
913 *) warn "Option $choice not valid." ;;
914 esac
915 done
916
917 printf "Press Enter to continue..."
918 read dummy || exit 0
919done