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