#!/usr/bin/env bash # m-dev-tools — interactive bootstrap installer. # # Recommended invocation (review first, then run): # curl -O https://raw.githubusercontent.com/m-dev-tools/.github/main/setup.sh # less ./setup.sh # bash ./setup.sh # # Or, for the convinced: # bash <(curl -fsSL https://raw.githubusercontent.com/m-dev-tools/.github/main/setup.sh) # # Flags: # -y, --yes non-interactive; accept defaults # -d, --dir PATH install root (default: ~/m-dev-tools) # -h, --help this message # # What it does: # 1. Detect OS (Linux distro / macOS) and check for required tools # (git, docker, python3.12+, uv, make). Prints install commands # for anything missing and exits — never sudo's. # 2. Verifies the Docker daemon is reachable. # 3. Clones m-cli into the chosen install root. # 4. Delegates the rest (sibling clones, venv install, engine # install + start, m doctor verification) to `make bootstrap` # inside m-cli. # 5. Prints PATH-setup advice and a "next steps" pointer to the # TDD lifecycle walkthrough. # # Idempotent — re-running on an already-installed host skips the # clones and re-verifies via `m doctor`. set -euo pipefail # ── flag parsing ───────────────────────────────────────────────────── NONINTERACTIVE=0 M_DEV_HOME_ARG="" while [[ $# -gt 0 ]]; do case "$1" in -y|--yes) NONINTERACTIVE=1; shift ;; -d|--dir) M_DEV_HOME_ARG="$2"; shift 2 ;; -h|--help) sed -n '2,/^set -/p' "$0" | sed 's/^# \{0,1\}//' | head -n -1 exit 0 ;; *) printf 'unknown flag: %s\n' "$1" >&2; exit 2 ;; esac done # ── pretty-print helpers (degrade gracefully on no-tty) ────────────── if [[ -t 1 ]]; then RED=$'\033[1;31m'; YEL=$'\033[1;33m'; GRN=$'\033[1;32m' CYA=$'\033[1;36m'; RST=$'\033[0m' else RED=""; YEL=""; GRN=""; CYA=""; RST="" fi info() { printf '%s==>%s %s\n' "$CYA" "$RST" "$*"; } warn() { printf '%sWARN%s %s\n' "$YEL" "$RST" "$*" >&2; } fail() { printf '%sFAIL%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; } ok() { printf ' %s✓%s %s\n' "$GRN" "$RST" "$*"; } ask() { # ask "prompt" "default" — read interactively, return default in -y mode. local prompt="$1" default="$2" reply if (( NONINTERACTIVE )); then printf '%s\n' "$default" return 0 fi read -r -p "$prompt [$default]: " reply printf '%s\n' "${reply:-$default}" } # ── OS detection ───────────────────────────────────────────────────── detect_os() { if [[ "$(uname -s)" == "Darwin" ]]; then printf 'macos\n' return 0 fi if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release case "${ID:-unknown}" in ubuntu|debian|linuxmint|pop) printf 'debian\n' ;; fedora|rhel|centos|rocky|almalinux) printf 'fedora\n' ;; arch|manjaro|endeavouros) printf 'arch\n' ;; *) printf 'linux-other\n' ;; esac return 0 fi printf 'unsupported\n' } OS=$(detect_os) case "$OS" in macos|debian|fedora|arch|linux-other) info "Detected OS: $OS" ;; unsupported) fail "Unsupported OS. m-dev-tools targets Linux (apt/dnf/pacman) and macOS." ;; esac install_hint() { # Print a one-line install hint for the given package on the detected OS. local pkg="$1" case "$OS" in macos) printf ' brew install %s\n' "$pkg" ;; debian) printf ' sudo apt install %s\n' "$pkg" ;; fedora) printf ' sudo dnf install %s\n' "$pkg" ;; arch) printf ' sudo pacman -S %s\n' "$pkg" ;; *) printf ' install %s via your package manager\n' "$pkg" ;; esac } # ── pre-flight ─────────────────────────────────────────────────────── info "Pre-flight checks..." missing=0 # git if command -v git >/dev/null; then ok "git present ($(git --version | awk '{print $3}'))" else warn "git not found." install_hint git missing=1 fi # docker if command -v docker >/dev/null; then ok "docker present ($(docker --version | awk '{print $3}' | tr -d ,))" else warn "docker not found." case "$OS" in debian) install_hint docker.io ;; macos) printf ' brew install --cask docker # then launch Docker Desktop\n' ;; *) install_hint docker ;; esac missing=1 fi # make if command -v make >/dev/null; then ok "make present" else warn "make not found." install_hint make missing=1 fi # python 3.12+ PYTHON="" for py in python3.12 python3.13 python3 python; do if command -v "$py" >/dev/null && \ "$py" -c 'import sys; sys.exit(0 if sys.version_info >= (3,12) else 1)' 2>/dev/null; then PYTHON="$py" break fi done if [[ -n "$PYTHON" ]]; then ok "python ≥ 3.12 present ($("$PYTHON" --version 2>&1))" else warn "python 3.12+ not found." case "$OS" in macos) printf ' brew install python@3.12\n' ;; debian) printf ' sudo apt install python3.12 python3.12-venv\n' ;; fedora) printf ' sudo dnf install python3.12\n' ;; arch) printf ' sudo pacman -S python\n' ;; *) install_hint python3.12 ;; esac missing=1 fi # uv if command -v uv >/dev/null; then ok "uv present ($(uv --version | awk '{print $2}'))" else warn "uv not found." printf ' curl -LsSf https://astral.sh/uv/install.sh | sh\n' missing=1 fi # docker daemon reachable if docker info >/dev/null 2>&1; then ok "docker daemon reachable" else warn "docker daemon not running." case "$OS" in macos) printf ' Open Docker Desktop and wait for the daemon to start.\n' ;; debian|fedora|arch) printf ' sudo systemctl start docker\n' printf ' sudo usermod -aG docker $USER # then log out / back in for group to take effect\n' ;; *) printf ' Start the Docker daemon for your platform.\n' ;; esac missing=1 fi if (( missing )); then fail "fix the prerequisites above, then re-run setup.sh." fi # ── confirm install location ───────────────────────────────────────── M_DEV_HOME=$(ask "Install m-dev-tools under" "${M_DEV_HOME_ARG:-$HOME/m-dev-tools}") info "Installing to: $M_DEV_HOME" mkdir -p "$M_DEV_HOME" cd "$M_DEV_HOME" # ── clone m-cli ────────────────────────────────────────────────────── if [[ -d m-cli/.git ]]; then info "m-cli already cloned — skipping" else info "Cloning m-cli..." git clone https://github.com/m-dev-tools/m-cli fi # ── delegate to make bootstrap ─────────────────────────────────────── info "Delegating to 'make bootstrap' inside m-cli..." cd m-cli make bootstrap # ── next steps ─────────────────────────────────────────────────────── printf '\n' ok "Setup complete." cat <