Files
Easy-WP/wp-bootstrap.sh
2025-11-01 11:06:18 -05:00

468 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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[@]}" <<SQL
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';
CREATE USER IF NOT EXISTS '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASS';
CREATE USER IF NOT EXISTS '$DB_USER'@'%' IDENTIFIED BY '$DB_PASS';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'127.0.0.1';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'%';
FLUSH PRIVILEGES;
SQL
if ! mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$DB_USER" -p"$DB_PASS" -D "$DB_NAME" -e "SELECT 1;" >/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
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# 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" <<TXT
Easy WP Bootstrap — Summary
===========================
Project: $PROJECT_NAME
Slug: $PROJECT_SLUG
Folder: $PROJECT_DIR
Local URL: $LOCAL_URL
DB Host: $MYSQL_HOST
DB Port: $MYSQL_PORT
DB Name: $DB_NAME
DB User: $DB_USER
DB Pass: $DB_PASS
WP Admin User: $ADMIN_USER
WP Admin Pass: $ADMIN_PASS
WP Admin Email: $ADMIN_EMAIL
Theme Dir: $THEME_DIR
Theme Remote: ${THEME_REMOTE_ORIGIN:-<none>}
Log files:
- Plugin install log: wp-content/plugin-bootstrap.log
Next steps:
- If Herd doesnt 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"