#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export WP_CLI_STRICT_ARGS_MODE=1 # Load machine/user defaults if [[ -f "$HOME/.wp-bootstraprc" ]]; then # shellcheck disable=SC1090 source "$HOME/.wp-bootstraprc" fi # Sensible fallbacks if config missing HERD_WORKSPACE="${HERD_WORKSPACE:-$HOME/Herd}" MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" MYSQL_PORT="${MYSQL_PORT:-3306}" MYSQL_ROOT_USER="${MYSQL_ROOT_USER:-root}" MYSQL_ROOT_PASS="${MYSQL_ROOT_PASS:-}" DEFAULT_ADMIN_EMAIL="${DEFAULT_ADMIN_EMAIL:-dev@vincentdesign.ca}" DEFAULT_ADMIN_USER="${DEFAULT_ADMIN_USER:-vdidev}" THEME_STARTER_REPO="${THEME_STARTER_REPO:-https://github.com/WordPress/twentytwentyfour.git}" DEFAULT_THEME_REMOTE_ORIGIN="${DEFAULT_THEME_REMOTE_ORIGIN:-}" LOCAL_TLD="${LOCAL_TLD:-test}" require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1"; exit 1; } } PHP_BIN="${WP_PHP_BIN:-php}" for bin in git curl openssl mysql composer npm; do require "$bin" done require "$PHP_BIN" if command -v wp >/dev/null 2>&1; then WP_CLI_CMD=(wp) else WP_CLI_PHAR_DEFAULT="$SCRIPT_DIR/wp-cli.phar" if [[ ! -f "$WP_CLI_PHAR_DEFAULT" && -f "$SCRIPT_DIR/windows/wp-cli.phar" ]]; then WP_CLI_PHAR_DEFAULT="$SCRIPT_DIR/windows/wp-cli.phar" fi WP_CLI_PHAR="${WP_CLI_PHAR:-$WP_CLI_PHAR_DEFAULT}" if [[ -f "$WP_CLI_PHAR" ]]; then WP_CLI_CMD=("$PHP_BIN" -f "$WP_CLI_PHAR" --) else echo "Missing dependency: wp (command) or wp-cli.phar (set WP_CLI_PHAR)." >&2 exit 1 fi fi invoke_wp() { local args=("$@") WP_CLI_STRICT_ARGS_MODE=1 "${WP_CLI_CMD[@]}" --path="$PROJECT_DIR" "${args[@]}" } usage() { cat <<'USAGE' Usage: wp-bootstrap.sh --project-name "Example Site" [options] Options: --project-name NAME Human-friendly project name (required) --admin-user USER WordPress admin username --admin-email EMAIL WordPress admin email --theme-starter-repo URL Git URL for the starter theme --herd-workspace PATH Herd workspace directory --local-tld TLD Local development TLD (e.g., test) --mysql-host HOST MySQL host --mysql-port PORT MySQL port --mysql-root-user USER MySQL root username --mysql-root-pass PASS MySQL root password --help Show this help and exit USAGE } slugify() { # lower, spaces->-, strip non [a-z0-9-] echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g;s/^-+|-+$//g' } randpass() { # 24 char base64, strip non-url-safe chars openssl rand -base64 24 | tr -d '\n' | tr -d '=/+' | cut -c1-24 } prompt() { local q def ans q="$1"; def="${2:-}" if [[ -n "$def" ]]; then read -r -p "$q [$def]: " ans || true echo "${ans:-$def}" else read -r -p "$q: " ans || true echo "$ans" fi } setup_pages_and_reading() { echo "Setting up default pages and reading options..." HOME_ID=$(invoke_wp post list --post_type=page --name='home' --field=ID --format=ids) if [[ -z "$HOME_ID" ]]; then HOME_ID=$(invoke_wp post create --post_type=page --post_status=publish --post_title="Home" --porcelain) fi NEWS_ID=$(invoke_wp post list --post_type=page --name='news' --field=ID --format=ids) if [[ -z "$NEWS_ID" ]]; then NEWS_ID=$(invoke_wp post create --post_type=page --post_status=publish --post_title="News" --porcelain) fi for TITLE in "Page Not Found (Error 404)" "Contact Us"; do SLUG="$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g;s/^-+|-+$//g')" ID=$(invoke_wp post list --post_type=page --name="$SLUG" --field=ID --format=ids) [[ -z "$ID" ]] && invoke_wp post create --post_type=page --post_status=publish --post_title="$TITLE" --porcelain >/dev/null done invoke_wp option update show_on_front 'page' invoke_wp option update page_on_front "$HOME_ID" invoke_wp option update page_for_posts "$NEWS_ID" invoke_wp post delete 1 --force >/dev/null 2>&1 || true invoke_wp post delete 2 --force >/dev/null 2>&1 || true invoke_wp rewrite structure '/%postname%/' invoke_wp rewrite flush --hard echo "Default pages and reading options configured." } install_and_activate_plugins() { local LOG="wp-content/plugin-bootstrap.log" echo "=== $(date -u '+%F %T') :: Plugin bootstrap start ===" | tee -a "$LOG" local plugins_path="" if [[ -f "$PROJECT_DIR/plugins.json" ]]; then plugins_path="$PROJECT_DIR/plugins.json" elif [[ -f "$SCRIPT_DIR/plugins.json" ]]; then plugins_path="$SCRIPT_DIR/plugins.json" fi if [[ -n "$plugins_path" ]]; then local pretty_path="$plugins_path" [[ "$pretty_path" == "$PROJECT_DIR/"* ]] && pretty_path="plugins.json" echo "Installing plugins from $pretty_path..." | tee -a "$LOG" if command -v jq >/dev/null 2>&1; then jq -c '.[]' "$plugins_path" | while read -r item; do ZIP=$(echo "$item" | jq -r '.zip // empty') SLUG=$(echo "$item" | jq -r '.slug // empty') VER=$(echo "$item" | jq -r '.version // empty') ACT=$(echo "$item" | jq -r '.activate // false') if [[ -n "$ZIP" ]]; then echo "Installing from zip: $ZIP" | tee -a "$LOG" local args=(plugin install "$ZIP" --force) [[ "$ACT" == "true" ]] && args+=(--activate) if invoke_wp "${args[@]}"; then echo "OK zip: $ZIP (activate=$ACT)" | tee -a "$LOG" else echo "FAIL zip: $ZIP" | tee -a "$LOG" fi elif [[ -n "$SLUG" ]]; then local args=(plugin install "$SLUG" --force) [[ -n "$VER" ]] && args+=("--version=$VER") [[ "$ACT" == "true" ]] && args+=(--activate) echo "Installing slug: $SLUG ${VER:+(v$VER)}" | tee -a "$LOG" if invoke_wp "${args[@]}"; then echo "OK slug: $SLUG (activate=$ACT)" | tee -a "$LOG" else echo "FAIL slug: $SLUG" | tee -a "$LOG" fi else echo "SKIP item with no slug/zip: $item" | tee -a "$LOG" fi done else echo "$pretty_path found but jq is not installed; skipping plugin install." | tee -a "$LOG" fi else echo "No plugins.json present; skipping plugin install." | tee -a "$LOG" fi echo "=== $(date -u '+%F %T') :: Plugin bootstrap end ===" | tee -a "$LOG" } PROJECT_NAME_CLI="" ADMIN_USER_CLI="" ADMIN_EMAIL_CLI="" THEME_STARTER_REPO_FLAG=0 while [[ $# -gt 0 ]]; do case "$1" in --project-name) [[ $# -lt 2 ]] && { echo "Missing value for --project-name" >&2; usage >&2; exit 1; } PROJECT_NAME_CLI="$2" shift 2 ;; --admin-user) [[ $# -lt 2 ]] && { echo "Missing value for --admin-user" >&2; usage >&2; exit 1; } ADMIN_USER_CLI="$2" shift 2 ;; --admin-email) [[ $# -lt 2 ]] && { echo "Missing value for --admin-email" >&2; usage >&2; exit 1; } ADMIN_EMAIL_CLI="$2" shift 2 ;; --theme-starter-repo) [[ $# -lt 2 ]] && { echo "Missing value for --theme-starter-repo" >&2; usage >&2; exit 1; } THEME_STARTER_REPO="$2" THEME_STARTER_REPO_FLAG=1 shift 2 ;; --herd-workspace) [[ $# -lt 2 ]] && { echo "Missing value for --herd-workspace" >&2; usage >&2; exit 1; } HERD_WORKSPACE="$2" shift 2 ;; --local-tld) [[ $# -lt 2 ]] && { echo "Missing value for --local-tld" >&2; usage >&2; exit 1; } LOCAL_TLD="$2" shift 2 ;; --mysql-host) [[ $# -lt 2 ]] && { echo "Missing value for --mysql-host" >&2; usage >&2; exit 1; } MYSQL_HOST="$2" shift 2 ;; --mysql-port) [[ $# -lt 2 ]] && { echo "Missing value for --mysql-port" >&2; usage >&2; exit 1; } MYSQL_PORT="$2" shift 2 ;; --mysql-root-user) [[ $# -lt 2 ]] && { echo "Missing value for --mysql-root-user" >&2; usage >&2; exit 1; } MYSQL_ROOT_USER="$2" shift 2 ;; --mysql-root-pass) [[ $# -lt 2 ]] && { echo "Missing value for --mysql-root-pass" >&2; usage >&2; exit 1; } MYSQL_ROOT_PASS="$2" shift 2 ;; --help|-h) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done echo "Easy WP Bootstrap (Herd + DBngin)" PROJECT_NAME="${PROJECT_NAME_CLI:-}" if [[ -z "$PROJECT_NAME" ]]; then PROJECT_NAME="$(prompt 'Project name (Human-readable)')" fi [[ -z "$PROJECT_NAME" ]] && { echo "Project name is required."; exit 1; } PROJECT_SLUG="$(slugify "$PROJECT_NAME")" FOLDER_NAME="$PROJECT_SLUG" PROJECT_DIR="$HERD_WORKSPACE/$FOLDER_NAME" LOCAL_URL="http://$PROJECT_SLUG.$LOCAL_TLD" echo "Derived:" echo " slug: $PROJECT_SLUG" echo " path: $PROJECT_DIR" echo " local URL: $LOCAL_URL" echo if [[ -n "$ADMIN_USER_CLI" ]]; then ADMIN_USER="$ADMIN_USER_CLI" else ADMIN_USER="$(prompt 'Admin username' "$DEFAULT_ADMIN_USER")" fi if [[ -n "$ADMIN_EMAIL_CLI" ]]; then ADMIN_EMAIL="$ADMIN_EMAIL_CLI" else ADMIN_EMAIL="$(prompt 'Admin email' "$DEFAULT_ADMIN_EMAIL")" fi ADMIN_PASS="$(randpass)" DB_NAME="wp_${PROJECT_SLUG}" DB_USER="$DB_NAME" DB_PASS="$(randpass)" if (( THEME_STARTER_REPO_FLAG )); then THEME_REPO_URL="$THEME_STARTER_REPO" else THEME_REPO_URL="$(prompt 'Theme starter repo URL' "$THEME_STARTER_REPO")" fi THEME_DIR="wp-content/themes/${PROJECT_SLUG}-theme" THEME_REMOTE_ORIGIN="$(prompt 'New theme repo remote (origin) URL (leave blank to skip push)' "$DEFAULT_THEME_REMOTE_ORIGIN")" echo echo "Creating project directory..." mkdir -p "$PROJECT_DIR" cd "$PROJECT_DIR" echo "Downloading WordPress core..." invoke_wp core download --force echo "Creating database and user in MySQL ($MYSQL_HOST:$MYSQL_PORT)..." MYSQL_AUTH=(-h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_ROOT_USER") if [[ -n "$MYSQL_ROOT_PASS" ]]; then MYSQL_AUTH+=(-p"$MYSQL_ROOT_PASS") fi if ! mysql "${MYSQL_AUTH[@]}" -e "SELECT VERSION();" >/dev/null 2>&1; then echo "Cannot connect to MySQL as root (${MYSQL_ROOT_USER}@${MYSQL_HOST}:${MYSQL_PORT})." >&2 exit 1 fi mysql "${MYSQL_AUTH[@]}" </dev/null 2>&1; then echo "New MySQL user '$DB_USER' cannot access database '$DB_NAME' at ${MYSQL_HOST}:${MYSQL_PORT}." >&2 exit 1 fi echo "Generating wp-config.php..." invoke_wp config create \ --dbname="$DB_NAME" \ --dbuser="$DB_USER" \ --dbpass="$DB_PASS" \ --dbhost="${MYSQL_HOST}:${MYSQL_PORT}" \ --force if invoke_wp config shuffle-salts >/dev/null 2>&1; then echo "Salt keys shuffled." else echo "Fetching salts from api.wordpress.org..." SALTS="$(curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/)" invoke_wp config set AUTH_KEY "dummy" --type=constant --raw >/dev/null 2>&1 || true "$PHP_BIN" -r 'file_put_contents("wp-config.php", preg_replace("/\\?>\\s*$/","",file_get_contents("wp-config.php"))."\n".'"'"$SALTS"'"'."\n");' fi echo "Installing WordPress..." invoke_wp core install \ --url="$LOCAL_URL" \ --title="$PROJECT_NAME" \ --admin_user="$ADMIN_USER" \ --admin_password="$ADMIN_PASS" \ --admin_email="$ADMIN_EMAIL" invoke_wp option update siteurl "$LOCAL_URL" invoke_wp option update home "$LOCAL_URL" setup_pages_and_reading HTACCESS_FILE="$PROJECT_DIR/.htaccess" if [[ ! -f "$HTACCESS_FILE" ]]; then cat >"$HTACCESS_FILE" <<'HTACCESS' # BEGIN WordPress RewriteEngine On RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # END WordPress HTACCESS echo "Created default .htaccess" fi echo "Cloning theme starter (history will be stripped)..." TMP_DIR=".starter-tmp" rm -rf "$TMP_DIR" git clone --depth=1 "$THEME_REPO_URL" "$TMP_DIR" mkdir -p "$(dirname "$THEME_DIR")" rm -rf "$THEME_DIR" mv "$TMP_DIR" "$THEME_DIR" rm -rf "$THEME_DIR/.git" if [[ -f "$THEME_DIR/style.css" ]]; then sed -i.bak "s/Theme Name:.*/Theme Name: ${PROJECT_NAME}/" "$THEME_DIR/style.css" || true sed -i.bak "s/Text Domain:.*/Text Domain: ${PROJECT_SLUG}-theme/" "$THEME_DIR/style.css" || true rm -f "$THEME_DIR/style.css.bak" fi echo "Installing theme dependencies and building assets..." pushd "$THEME_DIR" >/dev/null composer install npm install npm run build popd >/dev/null echo "Activating theme..." invoke_wp theme activate "${PROJECT_SLUG}-theme" || { echo "Activation failed (maybe theme slug mismatch). Listing themes:" invoke_wp theme list } install_and_activate_plugins echo "Initializing theme repo..." pushd "$THEME_DIR" >/dev/null git init -b main git add -A git commit -m "feat: bootstrap ${PROJECT_NAME} theme from starter" if [[ -n "$THEME_REMOTE_ORIGIN" ]]; then git remote add origin "$THEME_REMOTE_ORIGIN" git push -u origin main fi popd >/dev/null create_wpengine_staging() { if [[ -z "${WPE_API_TOKEN:-}" ]]; then echo "WPE_API_TOKEN not set; skipping staging creation." return fi echo "Stub: Add WPE API call here to create staging for ${PROJECT_SLUG}." } create_wpengine_staging SUMMARY_FILE="$PROJECT_DIR/bootstrap-summary.txt" cat > "$SUMMARY_FILE" <} Log files: - Plugin install log: wp-content/plugin-bootstrap.log Next steps: - If Herd doesn’t auto-serve the folder, link it via Herd UI/CLI and open $LOCAL_URL - Remove legacy activation.php from the starter theme if present (now handled by bootstrap) - Wire CI/CD as needed (never push DB to production) TXT echo echo "Bootstrap complete." echo "Summary saved to: $SUMMARY_FILE" echo cat "$SUMMARY_FILE"