From aed692d2941c2bbd16f8b279d182f2a2d4b1d178 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Mon, 1 Jun 2026 12:07:42 -0700 Subject: [PATCH] Add macOS first-mile bootstrap script --- CHANGELOG.md | 2 + README.md | 49 ++- bin/base-test | 1 + bootstrap.sh | 485 ++++++++++++++++++++++ cli/bash/commands/basectl/tests/repo.bats | 5 +- tests/bootstrap.bats | 198 +++++++++ 6 files changed, 730 insertions(+), 10 deletions(-) create mode 100755 bootstrap.sh create mode 100644 tests/bootstrap.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index 8952448..5b2b6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and Base versions are tracked in the repo-root `VERSION` file. logs. - Added `basectl workspace status` as the first read-only workspace-level project health summary. +- Added `bootstrap.sh` as a first-mile macOS bootstrapper for installing + Homebrew, Git, Bash, and Base before handing off to `basectl`. ### Fixed diff --git a/README.md b/README.md index 1105a8a..df32277 100644 --- a/README.md +++ b/README.md @@ -31,28 +31,40 @@ are tracked in [CHANGELOG.md](CHANGELOG.md). ## Start Here -Install Base through Homebrew: +On a new macOS machine, start with the first-mile bootstrap script: ```bash -brew install codeforester/base/base -basectl setup -basectl update-profile -exec "$SHELL" -l +curl -fsSL https://raw.githubusercontent.com/codeforester/base/master/bootstrap.sh | bash +``` + +The bootstrapper installs Homebrew, Git, and a supported Bash when needed, +chooses an existing Base install when one is present, otherwise defaults to a +source checkout at `~/work/base`, and prints the exact `basectl setup` and +`basectl update-profile` commands to finish the installation. + +Choose an install mode explicitly when needed: + +```bash +curl -fsSL https://raw.githubusercontent.com/codeforester/base/master/bootstrap.sh | bash -s -- --source +curl -fsSL https://raw.githubusercontent.com/codeforester/base/master/bootstrap.sh | bash -s -- --brew ``` For Homebrew installs, Base itself lives under Homebrew's prefix rather than in your project workspace. If your repositories live under a shared directory such -as `~/work`, set the workspace root in `~/.base.d/config.yaml`: +as `~/work`, set the workspace root in `~/.base.d/config.yaml` after running +`basectl setup`: ```yaml workspace: root: ~/work ``` -Or install from the repository: +Or install directly through Homebrew when Homebrew is already available: ```bash -curl -fsSL https://raw.githubusercontent.com/codeforester/base/master/install.sh | bash +brew install codeforester/base/base +basectl setup +basectl update-profile exec "$SHELL" -l ``` @@ -591,6 +603,27 @@ is requested on macOS, Base warns if `osascript` is not available. ## Installation Details +For a blank macOS machine, use `bootstrap.sh`: + +```bash +curl -fsSL https://raw.githubusercontent.com/codeforester/base/master/bootstrap.sh | bash +``` + +The bootstrapper is intentionally small. It verifies macOS, installs Homebrew +when missing, installs Git and Bash through Homebrew when needed, then installs +Base through either a source checkout or Homebrew. It does not edit shell startup +files automatically. Instead, it prints the exact follow-up commands, typically: + +```bash +~/work/base/bin/basectl setup +~/work/base/bin/basectl update-profile +exec "$SHELL" -l +``` + +Pass `--source` or `--brew` with `bash -s --` to choose the route explicitly. +Without an explicit choice, the bootstrapper preserves an existing Homebrew Base +install, then an existing source checkout, and otherwise defaults to source mode. + Base can be installed through its Homebrew tap: ```bash diff --git a/bin/base-test b/bin/base-test index 3891831..70bfd0f 100755 --- a/bin/base-test +++ b/bin/base-test @@ -23,5 +23,6 @@ bats \ lib/bash/version/tests/lib_version.bats \ lib/shell/completions/tests/completions.bats \ tests/base_init.bats \ + tests/bootstrap.bats \ tests/install.bats \ tests/integration/base_workflows.bats diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..26a40fa --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,485 @@ +#!/usr/bin/env bash + +set -euo pipefail + +bootstrap_usage() { + cat <<'EOF' +Usage: + bootstrap.sh [options] + +Options: + --source Install or update Base from a Git checkout. + --brew Install Base through Homebrew. + --install-dir Source checkout path. Defaults to ~/work/base. + --repo-url Git repository URL for source mode. + --branch Clone a specific branch for a new source checkout. + --no-homebrew-install Fail instead of installing Homebrew when missing. + --dry-run Print planned actions without making changes. + -h, --help Show this help text. + +Mode selection uses this precedence: + command-line flag, BASE_BOOTSTRAP_MODE, existing Homebrew install, + existing source checkout, then source mode. +EOF +} + +bootstrap_log() { + printf '%s\n' "$*" +} + +bootstrap_die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +bootstrap_expand_path() { + local path="$1" + + case "$path" in + "~") printf '%s\n' "$HOME" ;; + "~/"*) printf '%s/%s\n' "$HOME" "${path#"~/"}" ;; + *) printf '%s\n' "$path" ;; + esac +} + +bootstrap_parent_dir() { + local path="${1%/}" + + case "$path" in + */*) printf '%s\n' "${path%/*}" ;; + *) printf '.\n' ;; + esac +} + +bootstrap_run() { + if [[ "${BASE_BOOTSTRAP_DRY_RUN:-false}" == "true" ]]; then + printf '[DRY-RUN] Would run:' + printf ' %q' "$@" + printf '\n' + return 0 + fi + + "$@" +} + +bootstrap_uname() { + if [[ -n "${BASE_BOOTSTRAP_TEST_OS:-}" ]]; then + printf '%s\n' "$BASE_BOOTSTRAP_TEST_OS" + return 0 + fi + + uname -s +} + +bootstrap_require_macos() { + local os_name + + os_name="$(bootstrap_uname)" + [[ "$os_name" == "Darwin" ]] || bootstrap_die "bootstrap.sh currently supports macOS only." +} + +BOOTSTRAP_BREW_BIN="" + +bootstrap_find_brew() { + local candidate + local candidates="${BASE_BOOTSTRAP_BREW_CANDIDATES:-/opt/homebrew/bin/brew:/usr/local/bin/brew}" + local old_ifs + + if [[ -n "${BASE_BOOTSTRAP_BREW_BIN:-}" && -x "${BASE_BOOTSTRAP_BREW_BIN:-}" ]]; then + printf '%s\n' "$BASE_BOOTSTRAP_BREW_BIN" + return 0 + fi + + if command -v brew >/dev/null 2>&1; then + command -v brew + return 0 + fi + + old_ifs="$IFS" + IFS=: + for candidate in $candidates; do + IFS="$old_ifs" + [[ -x "$candidate" ]] || continue + printf '%s\n' "$candidate" + return 0 + done + IFS="$old_ifs" + + return 1 +} + +bootstrap_refresh_brew() { + local brew_bin + local brew_dir + + brew_bin="$(bootstrap_find_brew || true)" + [[ -n "$brew_bin" ]] || return 1 + + BOOTSTRAP_BREW_BIN="$brew_bin" + brew_dir="$(bootstrap_parent_dir "$brew_bin")" + case ":$PATH:" in + *":$brew_dir:"*) ;; + *) PATH="$brew_dir:$PATH"; export PATH ;; + esac + return 0 +} + +bootstrap_install_homebrew() { + local installer_url="${BASE_BOOTSTRAP_HOMEBREW_INSTALLER_URL:-https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh}" + + bootstrap_log "Installing Homebrew." + if [[ "${BASE_BOOTSTRAP_DRY_RUN:-false}" == "true" ]]; then + bootstrap_log "[DRY-RUN] Would run: /bin/bash -c " + BOOTSTRAP_BREW_BIN=brew + return 0 + fi + + command -v curl >/dev/null 2>&1 || bootstrap_die "curl is required to install Homebrew." + /bin/bash -c "$(curl -fsSL "$installer_url")" +} + +bootstrap_ensure_homebrew() { + local allow_install="$1" + + if bootstrap_refresh_brew; then + bootstrap_log "Homebrew is available at '$BOOTSTRAP_BREW_BIN'." + return 0 + fi + + [[ "$allow_install" == "true" ]] || bootstrap_die "Homebrew is required. Install Homebrew from https://brew.sh/ or rerun without --no-homebrew-install." + + bootstrap_install_homebrew + if [[ "${BASE_BOOTSTRAP_DRY_RUN:-false}" == "true" ]]; then + return 0 + fi + + bootstrap_refresh_brew || bootstrap_die "Homebrew installation completed, but 'brew' was not found." + bootstrap_log "Homebrew is available at '$BOOTSTRAP_BREW_BIN'." +} + +bootstrap_git_usable() { + command -v git >/dev/null 2>&1 || return 1 + git --version >/dev/null 2>&1 +} + +bootstrap_ensure_git() { + if bootstrap_git_usable; then + bootstrap_log "Git is available." + return 0 + fi + + bootstrap_log "Installing Git through Homebrew." + bootstrap_run "$BOOTSTRAP_BREW_BIN" install git + if [[ "${BASE_BOOTSTRAP_DRY_RUN:-false}" == "true" ]]; then + return 0 + fi + + bootstrap_git_usable || bootstrap_die "Git was installed, but 'git --version' still does not work." +} + +bootstrap_bash_version_number() { + printf '%s\n' "${BASE_BOOTSTRAP_TEST_BASH_VERSION:-${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}}" +} + +bootstrap_find_supported_bash() { + local candidate + local candidates="${BASE_BOOTSTRAP_BASH_CANDIDATES:-/opt/homebrew/bin/bash:/usr/local/bin/bash}" + local current_version + local old_ifs + + current_version="$(bootstrap_bash_version_number)" + if [[ "$current_version" -ge 42 ]]; then + printf '%s\n' "${BASH:-bash}" + return 0 + fi + + old_ifs="$IFS" + IFS=: + for candidate in $candidates; do + IFS="$old_ifs" + [[ -x "$candidate" ]] || continue + printf '%s\n' "$candidate" + return 0 + done + IFS="$old_ifs" + + return 1 +} + +bootstrap_ensure_supported_bash() { + if bootstrap_find_supported_bash >/dev/null 2>&1; then + bootstrap_log "Bash 4.2+ is available for Base." + return 0 + fi + + bootstrap_log "Installing Bash 4.2+ through Homebrew." + bootstrap_run "$BOOTSTRAP_BREW_BIN" install bash + if [[ "${BASE_BOOTSTRAP_DRY_RUN:-false}" == "true" ]]; then + return 0 + fi + + bootstrap_find_supported_bash >/dev/null 2>&1 || bootstrap_die "Bash was installed, but a supported Bash was not found." +} + +bootstrap_source_checkout_present() { + local install_dir="$1" + + [[ -d "$install_dir/.git" || -x "$install_dir/bin/basectl" ]] +} + +bootstrap_brew_base_installed() { + local formula="$1" + + [[ -n "$BOOTSTRAP_BREW_BIN" ]] || return 1 + "$BOOTSTRAP_BREW_BIN" list --formula "$formula" >/dev/null 2>&1 +} + +bootstrap_validate_mode() { + local mode="$1" + + case "$mode" in + ""|source|brew) return 0 ;; + *) bootstrap_die "Invalid bootstrap mode '$mode'. Use --source, --brew, or BASE_BOOTSTRAP_MODE=source|brew." ;; + esac +} + +bootstrap_select_mode() { + local requested_mode="$1" + local install_dir="$2" + local formula="$3" + + if [[ -n "$requested_mode" ]]; then + printf '%s\n' "$requested_mode" + return 0 + fi + + if bootstrap_brew_base_installed "$formula"; then + printf 'brew\n' + return 0 + fi + + if bootstrap_source_checkout_present "$install_dir"; then + printf 'source\n' + return 0 + fi + + printf 'source\n' +} + +bootstrap_install_source() { + local repo_url="$1" + local install_dir="$2" + local branch="$3" + local parent_dir + + if [[ -d "$install_dir/.git" ]]; then + bootstrap_log "Updating existing Base source checkout at '$install_dir'." + bootstrap_run git -C "$install_dir" pull --ff-only + return 0 + fi + + if [[ -e "$install_dir" ]]; then + bootstrap_die "Install path '$install_dir' exists but is not a Git checkout." + fi + + bootstrap_log "Cloning Base into '$install_dir'." + parent_dir="$(bootstrap_parent_dir "$install_dir")" + bootstrap_run mkdir -p "$parent_dir" + if [[ -n "$branch" ]]; then + bootstrap_run git clone --branch "$branch" "$repo_url" "$install_dir" + else + bootstrap_run git clone "$repo_url" "$install_dir" + fi +} + +bootstrap_install_brew_base() { + local formula="$1" + + if bootstrap_brew_base_installed "$formula"; then + bootstrap_log "Base Homebrew formula '$formula' is already installed." + return 0 + fi + + bootstrap_log "Installing Base with Homebrew formula '$formula'." + bootstrap_run "$BOOTSTRAP_BREW_BIN" install "$formula" +} + +bootstrap_find_homebrew_basectl() { + local active_basectl + local candidate + local prefix + + if [[ -n "$BOOTSTRAP_BREW_BIN" && "$BOOTSTRAP_BREW_BIN" != "brew" ]]; then + prefix="$("$BOOTSTRAP_BREW_BIN" --prefix 2>/dev/null || true)" + if [[ -n "$prefix" ]]; then + candidate="$prefix/bin/basectl" + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + fi + + active_basectl="$(command -v basectl 2>/dev/null || true)" + case "$active_basectl" in + /opt/homebrew/*|/usr/local/*) + printf '%s\n' "$active_basectl" + return 0 + ;; + esac + + return 1 +} + +bootstrap_print_provenance() { + local active_basectl + local brew_basectl + local install_dir="$1" + local mode="$2" + + brew_basectl="$(bootstrap_find_homebrew_basectl || true)" + active_basectl="$(command -v basectl 2>/dev/null || true)" + + bootstrap_log "" + bootstrap_log "Base provenance:" + if [[ "$mode" == "source" || -e "$install_dir" ]]; then + bootstrap_log " source checkout: $install_dir" + else + bootstrap_log " source checkout: not found at $install_dir" + fi + if [[ -n "$brew_basectl" ]]; then + bootstrap_log " Homebrew basectl: $brew_basectl" + else + bootstrap_log " Homebrew basectl: not found on PATH" + fi + if [[ -n "$active_basectl" ]]; then + bootstrap_log " active basectl: $active_basectl" + else + bootstrap_log " active basectl: not found on PATH" + fi +} + +bootstrap_brew_basectl_command() { + local brew_basectl + local prefix + + brew_basectl="$(bootstrap_find_homebrew_basectl || true)" + if [[ -n "$brew_basectl" ]]; then + printf '%s\n' "$brew_basectl" + return 0 + fi + + if [[ -n "$BOOTSTRAP_BREW_BIN" && "$BOOTSTRAP_BREW_BIN" != "brew" ]]; then + prefix="$("$BOOTSTRAP_BREW_BIN" --prefix 2>/dev/null || true)" + if [[ -n "$prefix" ]]; then + printf '%s\n' "$prefix/bin/basectl" + return 0 + fi + fi + + printf 'basectl\n' +} + +bootstrap_print_next_steps() { + local basectl_command + local install_dir="$1" + local mode="$2" + + if [[ "$mode" == "source" ]]; then + basectl_command="$install_dir/bin/basectl" + else + basectl_command="$(bootstrap_brew_basectl_command)" + fi + + bootstrap_log "" + bootstrap_log "Run these commands to finish Base setup and shell integration:" + bootstrap_log " $basectl_command setup" + bootstrap_log " $basectl_command update-profile" + bootstrap_log " exec \"\$SHELL\" -l" +} + +bootstrap_main() { + local allow_homebrew_install="${BASE_BOOTSTRAP_HOMEBREW_INSTALL:-true}" + local branch="${BASE_BOOTSTRAP_BRANCH:-}" + local formula="${BASE_BOOTSTRAP_BREW_FORMULA:-codeforester/base/base}" + local install_dir="${BASE_BOOTSTRAP_INSTALL_DIR:-${BASE_HOME:-$HOME/work/base}}" + local mode="${BASE_BOOTSTRAP_MODE:-}" + local repo_url="${BASE_BOOTSTRAP_REPO_URL:-https://github.com/codeforester/base.git}" + + BASE_BOOTSTRAP_DRY_RUN="${BASE_BOOTSTRAP_DRY_RUN:-false}" + + while (($# > 0)); do + case "$1" in + -h|--help) + bootstrap_usage + return 0 + ;; + --source) + mode=source + shift + ;; + --brew) + mode=brew + shift + ;; + --install-dir|--dir) + [[ -n "${2:-}" ]] || bootstrap_die "Option '$1' requires an argument." + install_dir="$2" + shift 2 + ;; + --repo-url) + [[ -n "${2:-}" ]] || bootstrap_die "Option '--repo-url' requires an argument." + repo_url="$2" + shift 2 + ;; + --branch) + [[ -n "${2:-}" ]] || bootstrap_die "Option '--branch' requires an argument." + branch="$2" + shift 2 + ;; + --no-homebrew-install) + allow_homebrew_install=false + shift + ;; + --dry-run) + BASE_BOOTSTRAP_DRY_RUN=true + shift + ;; + *) + bootstrap_usage >&2 + bootstrap_die "Unknown option '$1'." + ;; + esac + done + + bootstrap_validate_mode "$mode" + install_dir="$(bootstrap_expand_path "$install_dir")" + + bootstrap_log "Base bootstrap" + bootstrap_require_macos + bootstrap_ensure_homebrew "$allow_homebrew_install" + bootstrap_ensure_git + bootstrap_ensure_supported_bash + + mode="$(bootstrap_select_mode "$mode" "$install_dir" "$formula")" + bootstrap_log "Install mode: $mode" + + case "$mode" in + source) + bootstrap_log "Repository: $repo_url" + bootstrap_log "Install path: $install_dir" + bootstrap_install_source "$repo_url" "$install_dir" "$branch" + ;; + brew) + bootstrap_log "Formula: $formula" + bootstrap_install_brew_base "$formula" + ;; + esac + + bootstrap_print_provenance "$install_dir" "$mode" + bootstrap_print_next_steps "$install_dir" "$mode" +} + +if [[ "${BASE_BOOTSTRAP_TESTING:-false}" != "true" ]]; then + bootstrap_main "$@" +fi diff --git a/cli/bash/commands/basectl/tests/repo.bats b/cli/bash/commands/basectl/tests/repo.bats index fd1c188..6d41f18 100644 --- a/cli/bash/commands/basectl/tests/repo.bats +++ b/cli/bash/commands/basectl/tests/repo.bats @@ -49,15 +49,16 @@ load ./basectl_helpers.bash @test "basectl repo init falls back to BASE_HOME parent when workspace root is not configured" { local nested_dir="$TEST_TMPDIR/nested/current" + local repo_name="base-fallback-${BATS_TEST_NUMBER}" local workspace_root local repo_dir workspace_root="$(cd "$BASE_REPO_ROOT/.." && pwd -P)" - repo_dir="$workspace_root/base-demo" + repo_dir="$workspace_root/$repo_name" mkdir -p "$nested_dir" cd "$nested_dir" - run_basectl repo init base-demo --dry-run + run_basectl repo init "$repo_name" --dry-run [ "$status" -eq 0 ] [[ "$output" == *"[DRY-RUN] Would create '$repo_dir/README.md'."* ]] diff --git a/tests/bootstrap.bats b/tests/bootstrap.bats new file mode 100644 index 0000000..b25e783 --- /dev/null +++ b/tests/bootstrap.bats @@ -0,0 +1,198 @@ +#!/usr/bin/env bats + +load ../lib/bash/tests/test_helper.sh +bats_require_minimum_version 1.5.0 + +setup() { + setup_test_tmpdir + TEST_HOME="$TEST_TMPDIR/home" + TEST_MOCKBIN="$TEST_TMPDIR/mockbin" + TEST_COMMAND_LOG="$TEST_TMPDIR/bootstrap-commands" + mkdir -p "$TEST_HOME" "$TEST_MOCKBIN" +} + +run_bootstrap() { + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_BOOTSTRAP_TEST_OS=Darwin \ + BASE_BOOTSTRAP_TEST_BASH_VERSION=32 \ + BASE_BOOTSTRAP_BASH_CANDIDATES="$TEST_TMPDIR/missing-bash" \ + BASE_BOOTSTRAP_BREW_CANDIDATES="$TEST_TMPDIR/missing-brew" \ + "$BASH" "$BASE_REPO_ROOT/bootstrap.sh" "$@" +} + +create_unusable_git_stub() { + cat > "$TEST_MOCKBIN/git" <<'EOF' +#!/bin/sh +exit 1 +EOF + chmod +x "$TEST_MOCKBIN/git" +} + +create_git_stub() { + cat > "$TEST_MOCKBIN/git" <<'EOF' +#!/bin/sh +printf 'git %s\n' "$*" >> "${BASE_BOOTSTRAP_TEST_COMMAND_LOG:?}" +if [ "${1:-}" = "--version" ]; then + printf 'git version 2.0.0\n' + exit 0 +fi +exit 0 +EOF + chmod +x "$TEST_MOCKBIN/git" +} + +create_brew_stub() { + cat > "$TEST_MOCKBIN/brew" <<'EOF' +#!/bin/sh +printf 'brew %s\n' "$*" >> "${BASE_BOOTSTRAP_TEST_COMMAND_LOG:?}" +case "${1:-}" in + --prefix) + printf '%s\n' "${BASE_BOOTSTRAP_TEST_BREW_PREFIX:?}" + exit 0 + ;; + list) + if [ "${BASE_BOOTSTRAP_TEST_BREW_BASE_INSTALLED:-false}" = "true" ]; then + exit 0 + fi + exit 1 + ;; + install) + exit 0 + ;; +esac +exit 0 +EOF + chmod +x "$TEST_MOCKBIN/brew" +} + +create_supported_bash_candidate() { + local bash_path="$1" + + mkdir -p "$(dirname "$bash_path")" + cat > "$bash_path" <<'EOF' +#!/bin/sh +exit 0 +EOF + chmod +x "$bash_path" +} + +@test "bootstrap prints help" { + run_bootstrap --help + + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"--source"* ]] + [[ "$output" == *"--brew"* ]] +} + +@test "bootstrap source dry-run installs first-mile prerequisites and prints handoff commands" { + local install_dir="$TEST_HOME/work/base" + + create_unusable_git_stub + + run_bootstrap --dry-run --source --install-dir "$install_dir" --repo-url https://example.test/base.git + + [ "$status" -eq 0 ] + [[ "$output" == *"Base bootstrap"* ]] + [[ "$output" == *"Installing Homebrew."* ]] + [[ "$output" == *"[DRY-RUN] Would run: /bin/bash -c "* ]] + [[ "$output" == *"Installing Git through Homebrew."* ]] + [[ "$output" == *"[DRY-RUN] Would run: brew install git"* ]] + [[ "$output" == *"Installing Bash 4.2+ through Homebrew."* ]] + [[ "$output" == *"[DRY-RUN] Would run: brew install bash"* ]] + [[ "$output" == *"Install mode: source"* ]] + [[ "$output" == *"Repository: https://example.test/base.git"* ]] + [[ "$output" == *"[DRY-RUN] Would run: git clone https://example.test/base.git $install_dir"* ]] + [[ "$output" == *"$install_dir/bin/basectl setup"* ]] + [[ "$output" == *"$install_dir/bin/basectl update-profile"* ]] + [[ "$output" == *"exec \"\$SHELL\" -l"* ]] +} + +@test "bootstrap brew dry-run installs Base through Homebrew and prints basectl handoff" { + create_unusable_git_stub + + run_bootstrap --dry-run --brew + + [ "$status" -eq 0 ] + [[ "$output" == *"Install mode: brew"* ]] + [[ "$output" == *"Formula: codeforester/base/base"* ]] + [[ "$output" == *"[DRY-RUN] Would run: brew install codeforester/base/base"* ]] + [[ "$output" == *" basectl setup"* ]] + [[ "$output" == *" basectl update-profile"* ]] +} + +@test "bootstrap defaults to an existing Homebrew Base install" { + local brew_prefix="$TEST_TMPDIR/homebrew" + local supported_bash="$brew_prefix/bin/bash" + + mkdir -p "$brew_prefix/bin" + create_brew_stub + create_git_stub + create_supported_bash_candidate "$supported_bash" + touch "$brew_prefix/bin/basectl" + touch "$TEST_MOCKBIN/basectl" + chmod +x "$brew_prefix/bin/basectl" + chmod +x "$TEST_MOCKBIN/basectl" + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_BOOTSTRAP_TEST_OS=Darwin \ + BASE_BOOTSTRAP_TEST_BASH_VERSION=32 \ + BASE_BOOTSTRAP_BASH_CANDIDATES="$supported_bash" \ + BASE_BOOTSTRAP_TEST_BREW_BASE_INSTALLED=true \ + BASE_BOOTSTRAP_TEST_BREW_PREFIX="$brew_prefix" \ + BASE_BOOTSTRAP_TEST_COMMAND_LOG="$TEST_COMMAND_LOG" \ + "$BASH" "$BASE_REPO_ROOT/bootstrap.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Homebrew is available at '$TEST_MOCKBIN/brew'."* ]] + [[ "$output" == *"Git is available."* ]] + [[ "$output" == *"Bash 4.2+ is available for Base."* ]] + [[ "$output" == *"Install mode: brew"* ]] + [[ "$output" == *"Base Homebrew formula 'codeforester/base/base' is already installed."* ]] + [[ "$output" == *"Homebrew basectl: $brew_prefix/bin/basectl"* ]] + [[ "$output" == *"active basectl: $TEST_MOCKBIN/basectl"* ]] + [[ "$output" == *"$brew_prefix/bin/basectl setup"* ]] + [[ "$output" == *"$brew_prefix/bin/basectl update-profile"* ]] + ! grep -Fqx "brew install codeforester/base/base" "$TEST_COMMAND_LOG" +} + +@test "bootstrap command-line mode overrides BASE_BOOTSTRAP_MODE" { + create_unusable_git_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_BOOTSTRAP_TEST_OS=Darwin \ + BASE_BOOTSTRAP_TEST_BASH_VERSION=32 \ + BASE_BOOTSTRAP_BASH_CANDIDATES="$TEST_TMPDIR/missing-bash" \ + BASE_BOOTSTRAP_BREW_CANDIDATES="$TEST_TMPDIR/missing-brew" \ + BASE_BOOTSTRAP_MODE=brew \ + "$BASH" "$BASE_REPO_ROOT/bootstrap.sh" --dry-run --source + + [ "$status" -eq 0 ] + [[ "$output" == *"Install mode: source"* ]] + [[ "$output" != *"Formula: codeforester/base/base"* ]] +} + +@test "bootstrap can refuse to install missing Homebrew" { + run_bootstrap --no-homebrew-install + + [ "$status" -eq 1 ] + [[ "$output" == *"Base bootstrap"* ]] + [[ "$output" == *"Homebrew is required"* ]] +} + +@test "bootstrap rejects non-macOS systems" { + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_BOOTSTRAP_TEST_OS=Linux \ + "$BASH" "$BASE_REPO_ROOT/bootstrap.sh" --dry-run + + [ "$status" -eq 1 ] + [[ "$output" == *"bootstrap.sh currently supports macOS only"* ]] +}