do/main
2025-06-25 14:34:20 -04:00

1295 lines
No EOL
48 KiB
Bash

#!/bin/bash
# ----------------------------------------------------
# Command: _do
# Description: A collection of useful command-line utilities for managing WordPress sites.
# Author: Austin Ginder
# License: MIT
# ----------------------------------------------------
# --- Global Variables ---
CAPTAINCORE_DO_VERSION="1.1"
GUM_VERSION="0.14.4"
CWEBP_VERSION="1.5.0"
RCLONE_VERSION="1.69.3"
GIT_VERSION="2.50.0"
GUM_CMD=""
CWEBP_CMD=""
IDENTIFY_CMD=""
WP_CLI_CMD=""
RESTIC_CMD=""
# --- Helper Functions ---
# ----------------------------------------------------
# Intelligently finds or creates a private directory.
# Sets a global variable CAPTAINCORE_PRIVATE_DIR and echoes the path.
# ----------------------------------------------------
function _get_private_dir() {
# Return immediately if already found
if [[ -n "$CAPTAINCORE_PRIVATE_DIR" ]]; then
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
local wp_root=""
local parent_dir=""
# --- Tier 1: Preferred WP-CLI Method (if in a WP directory) ---
# Check if wp-cli is set up and if we are in a WP installation.
if setup_wp_cli && "$WP_CLI_CMD" core is-installed --quiet 2>/dev/null; then
local wp_config_path
wp_config_path=$("$WP_CLI_CMD" config path --quiet 2>/dev/null)
if [ -n "$wp_config_path" ] && [ -f "$wp_config_path" ]; then
wp_root=$(dirname "$wp_config_path")
parent_dir=$(dirname "$wp_root")
# --- WPE Specific Checks ---
# A. Check for _wpeprivate inside the WP root directory first (most common).
if [ -d "${wp_root}/_wpeprivate" ]; then
CAPTAINCORE_PRIVATE_DIR="${wp_root}/_wpeprivate"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# B. Check for _wpeprivate in the parent directory.
if [ -d "${parent_dir}/_wpeprivate" ]; then
CAPTAINCORE_PRIVATE_DIR="${parent_dir}/_wpeprivate"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# --- Standard Checks (relative to WP root's parent) ---
# Check for a standard ../private directory
if [ -d "${parent_dir}/private" ]; then
CAPTAINCORE_PRIVATE_DIR="${parent_dir}/private"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# Try to create a ../private directory, suppressing errors
if mkdir -p "${parent_dir}/private" 2>/dev/null; then
CAPTAINCORE_PRIVATE_DIR="${parent_dir}/private"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# Fallback to ../tmp if it exists
if [ -d "${parent_dir}/tmp" ]; then
CAPTAINCORE_PRIVATE_DIR="${parent_dir}/tmp"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
fi
fi
# --- Tier 2: Manual Fallback (if WP-CLI fails or not in a WP install) ---
local current_dir
current_dir=$(pwd)
# WPE check in current directory
if [ -d "${current_dir}/_wpeprivate" ]; then
CAPTAINCORE_PRIVATE_DIR="${current_dir}/_wpeprivate"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# Relative private check
if [ -d "../private" ]; then
CAPTAINCORE_PRIVATE_DIR=$(cd ../private && pwd)
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# Attempt to create relative private, suppressing errors
if mkdir -p "../private" 2>/dev/null; then
CAPTAINCORE_PRIVATE_DIR=$(cd ../private && pwd)
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# Relative tmp check
if [ -d "../tmp" ]; then
CAPTAINCORE_PRIVATE_DIR=$(cd ../tmp && pwd)
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
# --- Tier 3: Last Resort Fallback to Home Directory ---
# Suppress errors in case $HOME is not writable
if mkdir -p "$HOME/private" 2>/dev/null; then
CAPTAINCORE_PRIVATE_DIR="$HOME/private"
echo "$CAPTAINCORE_PRIVATE_DIR"
return 0
fi
echo "Error: Could not find or create a suitable private, writable directory." >&2
return 1
}
# ----------------------------------------------------
# Checks for and installs 'gum' if not present. Sets GUM_CMD on success.
# ----------------------------------------------------
function setup_gum() {
# Return if already found
if [[ -n "$GUM_CMD" ]]; then return 0; fi
# If gum is already in the PATH, we're good to go.
if command -v gum &> /dev/null; then
GUM_CMD="gum"
return 0
fi
# Find the private directory for storing tools
local private_dir
if ! private_dir=$(_get_private_dir); then
echo "Error: Cannot find a writable directory to install gum." >&2
return 1
fi
# Find the executable inside the private directory if it's already installed
local existing_executable
existing_executable=$(find "$private_dir" -name gum -type f 2>/dev/null | head -n 1)
if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" --version &> /dev/null; then
GUM_CMD="$existing_executable"
return 0
fi
echo "Required tool 'gum' not found. Installing to '${private_dir}'..." >&2
local original_dir; original_dir=$(pwd)
cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; }
local gum_dir_name="gum_${GUM_VERSION}_Linux_x86_64"
local gum_tarball="${gum_dir_name}.tar.gz"
if ! curl -sSL "https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/${gum_tarball}" -o "${gum_tarball}"; then
echo "Error: Failed to download gum." >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
# Find the path of the 'gum' binary WITHIN the tarball before extracting
local gum_path_in_tar
gum_path_in_tar=$(tar -tf "${gum_tarball}" | grep '/gum$' | head -n 1)
if [ -z "$gum_path_in_tar" ]; then
echo "Error: Could not find 'gum' executable within the downloaded tarball." >&2
rm -f "${gum_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
# Now extract the tarball
if ! tar -xf "${gum_tarball}"; then
echo "Error: Failed to extract gum from tarball." >&2
rm -f "${gum_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
rm -f "${gum_tarball}"
# The full path to the executable is the private dir + the path from the tarball
local gum_executable="${private_dir}/${gum_path_in_tar}"
if [ -f "$gum_executable" ]; then
chmod +x "$gum_executable"
else
echo "Error: gum executable not found at expected path after extraction: ${gum_executable}" >&2
cd "$original_dir" > /dev/null 2>&1; return 1;
fi
# Final check
if [ -x "$gum_executable" ] && "$gum_executable" --version &> /dev/null; then
echo "'gum' installed successfully." >&2
GUM_CMD="$gum_executable"
else
echo "Error: gum installation failed. The binary at ${gum_executable} might not be executable or compatible." >&2
cd "$original_dir" > /dev/null 2>&1; return 1;
fi
cd "$original_dir" > /dev/null 2>&1; return 0;
}
# ----------------------------------------------------
# Checks for and installs 'cwebp' if not present. Sets CWEBP_CMD on success.
# ----------------------------------------------------
function setup_cwebp() {
# Return if already found
if [[ -n "$CWEBP_CMD" ]]; then return 0; fi
if command -v cwebp &> /dev/null; then CWEBP_CMD="cwebp"; return 0; fi
local private_dir
if ! private_dir=$(_get_private_dir); then
echo "Error: Cannot find a writable directory to install cwebp." >&2; return 1;
fi
local existing_executable
existing_executable=$(find "$private_dir" -name cwebp -type f 2>/dev/null | head -n 1)
if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" -version &> /dev/null; then
CWEBP_CMD="$existing_executable"; return 0;
fi
echo "Required tool 'cwebp' not found. Installing to '${private_dir}'..." >&2
local original_dir; original_dir=$(pwd)
cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; }
local cwebp_dir_name="libwebp-${CWEBP_VERSION}-linux-x86-64"
local cwebp_tarball="${cwebp_dir_name}.tar.gz"
if ! curl -sSL "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/${cwebp_tarball}" -o "${cwebp_tarball}"; then
echo "Error: Failed to download cwebp." >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
local cwebp_path_in_tar
cwebp_path_in_tar=$(tar -tf "${cwebp_tarball}" | grep '/bin/cwebp$' | head -n 1)
if [ -z "$cwebp_path_in_tar" ]; then
echo "Error: Could not find 'cwebp' executable within the downloaded tarball." >&2
rm -f "${cwebp_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
if ! tar -xzf "${cwebp_tarball}"; then
echo "Error: Failed to extract cwebp." >&2; rm -f "${cwebp_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
rm -f "${cwebp_tarball}"
local cwebp_executable="${private_dir}/${cwebp_path_in_tar}"
if [ -f "$cwebp_executable" ]; then
chmod +x "$cwebp_executable";
else
echo "Error: cwebp executable not found at expected path: ${cwebp_executable}" >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
if [ -x "$cwebp_executable" ] && "$cwebp_executable" -version &> /dev/null; then
echo "'cwebp' installed successfully." >&2; CWEBP_CMD="$cwebp_executable";
else
echo "Error: cwebp installation failed." >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
cd "$original_dir" > /dev/null 2>&1; return 0;
}
# ----------------------------------------------------
# Checks for and installs ImageMagick if not present. Sets IDENTIFY_CMD on success.
# ----------------------------------------------------
function setup_imagemagick() {
# Return if already found
if [[ -n "$IDENTIFY_CMD" ]]; then
return 0
fi
# If identify is already in the PATH, we're good to go.
if command -v identify &> /dev/null; then
IDENTIFY_CMD="identify"
return 0
fi
local private_dir
if ! private_dir=$(_get_private_dir); then
# Error message is handled by the helper function
return 1
fi
# Define the path where the extracted binary should be
local identify_executable="${private_dir}/squashfs-root/usr/bin/identify"
# Check if we have already extracted it
if [ -f "$identify_executable" ] && "$identify_executable" -version &> /dev/null; then
IDENTIFY_CMD="$identify_executable"
return 0
fi
# If not found, download and extract the AppImage
echo "Required tool 'identify' not found. Sideloading via AppImage extraction..." >&2
local imagemagick_appimage_path="${private_dir}/ImageMagick.AppImage"
# Let's use the 'gcc' version as it's a common compiler toolchain for Linux
local appimage_url="https://github.com/ImageMagick/ImageMagick/releases/download/7.1.1-47/ImageMagick-82572af-gcc-x86_64.AppImage"
echo "Downloading from ${appimage_url}..." >&2
if ! wget --quiet "$appimage_url" -O "$imagemagick_appimage_path"; then
echo "Error: Failed to download the ImageMagick AppImage." >&2
rm -f "$imagemagick_appimage_path" # Clean up partial download
return 1
fi
chmod +x "$imagemagick_appimage_path"
# --- EXTRACTION STEP ---
# This is the key change to work around the FUSE error.
echo "Extracting AppImage..." >&2
# Change into the private directory to contain the extraction
cd "$private_dir" || { echo "Error: Could not enter private directory." >&2; return 1; }
# Run the extraction. This creates a 'squashfs-root' directory.
if ! ./ImageMagick.AppImage --appimage-extract >/dev/null; then
echo "Error: Failed to extract the ImageMagick AppImage." >&2
# Clean up on failure
rm -f "ImageMagick.AppImage"
rm -rf "squashfs-root"
cd - > /dev/null
return 1
fi
# We don't need the AppImage file anymore after extraction
rm -f "ImageMagick.AppImage"
# Return to the original directory
cd - > /dev/null
# Final check
if [ -f "$identify_executable" ] && "$identify_executable" -version &> /dev/null; then
echo "'identify' binary extracted successfully to ${private_dir}/squashfs-root/" >&2
IDENTIFY_CMD="$identify_executable"
else
echo "Error: ImageMagick extraction failed. Could not find the 'identify' executable." >&2
return 1
fi
}
# ----------------------------------------------------
# Checks for and installs 'rclone' if not present. Sets RCLONE_CMD on success.
# ----------------------------------------------------
function setup_rclone() {
# Return if already found
if [[ -n "$RCLONE_CMD" ]]; then return 0; fi
if command -v rclone &> /dev/null; then RCLONE_CMD="rclone"; return 0; fi
if ! command -v unzip &>/dev/null; then echo "Error: 'unzip' command is required for rclone installation." >&2; return 1; fi
local private_dir
if ! private_dir=$(_get_private_dir); then
echo "Error: Cannot find a writable directory to install rclone." >&2; return 1;
fi
local existing_executable
existing_executable=$(find "$private_dir" -name rclone -type f 2>/dev/null | head -n 1)
if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" --version &> /dev/null; then
RCLONE_CMD="$existing_executable"; return 0;
fi
echo "Required tool 'rclone' not found. Installing to '${private_dir}'..." >&2
local original_dir; original_dir=$(pwd)
cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; }
local rclone_zip="rclone-v${RCLONE_VERSION}-linux-amd64.zip"
if ! curl -sSL "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/${rclone_zip}" -o "${rclone_zip}"; then
echo "Error: Failed to download rclone." >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
local rclone_path_in_zip
rclone_path_in_zip=$(unzip -l "${rclone_zip}" | grep '/rclone$' | awk '{print $4}' | head -n 1)
if [ -z "$rclone_path_in_zip" ]; then
echo "Error: Could not find 'rclone' executable within the downloaded zip." >&2
rm -f "${rclone_zip}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
if ! unzip -q -o "${rclone_zip}"; then
echo "Error: Failed to extract rclone." >&2; rm -f "${rclone_zip}"; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
rm -f "${rclone_zip}"
local rclone_executable="${private_dir}/${rclone_path_in_zip}"
if [ -f "$rclone_executable" ]; then
chmod +x "$rclone_executable";
else
echo "Error: rclone executable not found at expected path: ${rclone_executable}" >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
if [ -x "$rclone_executable" ] && "$rclone_executable" --version &> /dev/null; then
echo "'rclone' installed successfully." >&2; RCLONE_CMD="$rclone_executable";
else
echo "Error: rclone installation failed." >&2; cd "$original_dir" > /dev/null 2>&1; return 1;
fi
cd "$original_dir" > /dev/null 2>&1; return 0;
}
# ----------------------------------------------------
# Checks for and installs 'restic' if not present.
# Sets RESTIC_CMD on success.
# ----------------------------------------------------
function setup_restic() {
# Return if already found
if [[ -n "$RESTIC_CMD" ]]; then return 0; fi
# If restic is already in the PATH, we're good.
if command -v restic &> /dev/null; then
RESTIC_CMD="restic"
return 0
fi
# Check for local installation in private dir
local restic_executable="$HOME/private/restic"
if [ -f "$restic_executable" ] && "$restic_executable" version &> /dev/null; then
RESTIC_CMD="$restic_executable"
return 0
fi
# If not found, download it
echo "Required tool 'restic' not found. Installing..." >&2
if ! command -v bunzip2 &>/dev/null; then echo "Error: 'bunzip2' command is required for installation." >&2; return 1; fi
mkdir -p "$HOME/private"
cd "$HOME/private" || { echo "Error: Could not enter ~/private." >&2; return 1; }
local restic_version="0.18.0"
local restic_archive="restic_${restic_version}_linux_amd64.bz2"
if ! curl -sL "https://github.com/restic/restic/releases/download/v${restic_version}/${restic_archive}" -o "${restic_archive}"; then
echo "Error: Failed to download restic." >&2
cd - > /dev/null
return 1
fi
# Decompress and extract the binary
bunzip2 -c "${restic_archive}" > restic_temp && mv restic_temp restic
rm -f "${restic_archive}"
chmod +x restic
# Final check
if [ -f "$restic_executable" ] && "$restic_executable" version &> /dev/null; then
echo "'restic' installed successfully." >&2
RESTIC_CMD="$restic_executable"
else
echo "Error: restic installation failed." >&2
cd - > /dev/null
return 1
fi
cd - > /dev/null
}
# ----------------------------------------------------
# Checks for and installs 'git' if not present. Sets GIT_CMD on success.
# ----------------------------------------------------
function setup_git() {
# Return if already found
if [[ -n "$GIT_CMD" ]]; then
return 0
fi
# If git is already in the PATH, we're good to go.
if command -v git &> /dev/null; then
GIT_CMD="git"
return 0
fi
# --- Sideloading Logic ---
echo "Required tool 'git' not found. Attempting to sideload..." >&2
local private_dir
if ! private_dir=$(_get_private_dir); then
return 1
fi
local git_executable="${private_dir}/git/usr/bin/git"
# Check if git has already been sideloaded
if [ -f "$git_executable" ] && "$git_executable" --version &> /dev/null; then
echo "'git' found in private directory." >&2
GIT_CMD="$git_executable"
return 0
fi
# Check for wget and dpkg-deb, which are required for sideloading
if ! command -v wget &> /dev/null || ! command -v dpkg-deb &> /dev/null; then
echo "❌ Error: 'wget' and 'dpkg-deb' are required to sideload git." >&2
return 1
fi
# Determine OS distribution and version
if [ -f /etc/os-release ]; then
. /etc/os-release
else
echo "❌ Error: Cannot determine OS distribution. /etc/os-release not found." >&2
return 1
fi
if [[ "$ID" != "ubuntu" ]]; then
echo "❌ Error: Sideloading git is currently only supported on Ubuntu." >&2
return 1
fi
# Construct the download URL for the git package
# This example uses a recent stable version. You may need to update the version number periodically.
local git_version="2.47.1-0ppa1~ubuntu16.04.1"
local git_deb_url="https://launchpad.net/~git-core/+archive/ubuntu/candidate/+build/29298725/+files/git_${git_version}_amd64.deb"
local git_deb_file="${private_dir}/git_latest.deb"
echo "Downloading git from ${git_deb_url}..." >&2
if ! wget -q -O "$git_deb_file" "$git_deb_url"; then
echo "❌ Error: Failed to download git .deb package." >&2
rm -f "$git_deb_file"
return 1
fi
echo "Extracting git package..." >&2
local extract_dir="${private_dir}/git"
mkdir -p "$extract_dir"
if ! dpkg-deb -x "$git_deb_file" "$extract_dir"; then
echo "❌ Error: Failed to extract git .deb package." >&2
rm -rf "$extract_dir"
rm -f "$git_deb_file"
return 1
fi
# Clean up the downloaded .deb file
rm -f "$git_deb_file"
# Final check
if [ -f "$git_executable" ] && "$git_executable" --version &> /dev/null; then
echo "'git' sideloaded successfully." >&2
GIT_CMD="$git_executable"
return 0
else
echo "❌ Error: git sideloading failed. The git binary is not available after extraction." >&2
return 1
fi
}
# ----------------------------------------------------
# Checks for and finds the 'wp' command. Sets WP_CLI_CMD on success.
# ----------------------------------------------------
function setup_wp_cli() {
# Return if already found
if [[ -n "$WP_CLI_CMD" ]]; then return 0; fi
# 1. Check if 'wp' is already in the PATH (covers interactive shells)
if command -v wp &> /dev/null; then
WP_CLI_CMD="wp"
return 0
fi
# 2. If not in PATH, check common absolute paths for cron environments
local common_paths=(
"/usr/local/bin/wp"
"$HOME/bin/wp"
"/opt/wp-cli/wp"
)
for path in "${common_paths[@]}"; do
if [ -x "$path" ]; then
WP_CLI_CMD="$path"
return 0
fi
done
# 3. If still not found, error out
echo "❌ Error: 'wp' command not found. Please ensure WP-CLI is installed and in your PATH." >&2
return 1
}
# ----------------------------------------------------
# (Helper) Uses PHP to check if a file is a WebP image.
# ----------------------------------------------------
function _is_webp_php() {
local file_path="$1"
if [ -z "$file_path" ]; then
return 1 # Return false if no file path is provided
fi
# IMAGETYPE_WEBP has a constant value of 18 in PHP.
# We will embed the file_path directly into the PHP string.
local php_code="
\$file_to_check = '${file_path}';
if (!file_exists(\$file_to_check)) {
// Silently exit if file doesn't exist, to avoid warnings.
exit(1);
}
if (function_exists('exif_imagetype')) {
// The @ suppresses warnings for unsupported file types.
\$image_type = @exif_imagetype(\$file_to_check);
if (\$image_type === 18) { // 18 is the constant for IMAGETYPE_WEBP
exit(0); // Exit with success code (true)
}
}
exit(1); // Exit with failure code (false)
"
if ! setup_wp_cli; then
if command -v php &> /dev/null; then
php -r "\$file_path='${file_path}'; ${php_code}"
return $?
fi
return 1
fi
# Execute 'wp eval' with the self-contained code. No extra arguments are needed.
"$WP_CLI_CMD" eval "$php_code"
return $?
}
# ----------------------------------------------------
# (Primary Checker) Checks if a file is WebP, using identify or PHP fallback.
# ----------------------------------------------------
function _is_webp() {
# Determine which method to use, but only do it once.
if [[ -z "$IDENTIFY_METHOD" ]]; then
if command -v identify &> /dev/null; then
echo "Using 'identify' command for image type checking." >&2
IDENTIFY_METHOD="identify"
else
echo "Warning: 'identify' command not found. Falling back to PHP check." >&2
IDENTIFY_METHOD="php"
fi
fi
# Execute the chosen method
if [[ "$IDENTIFY_METHOD" == "identify" ]]; then
if [[ "$(identify -format "%m" "$1")" == "WEBP" ]]; then
return 0 # It is a WebP file
else
return 1 # It is not a WebP file
fi
else # Fallback to PHP
if _is_webp_php "$1"; then
return 0 # It is a WebP file
else
return 1 # It is not a WebP file
fi
fi
}
# ----------------------------------------------------
# Displays detailed help for a specific command.
# ----------------------------------------------------
function show_command_help() {
local cmd="$1"
# If no command is specified, show the general usage.
if [ -z "$cmd" ]; then
show_usage
return
fi
# Display help text based on the command provided.
case "$cmd" in
backup)
echo "Creates a full backup (files + DB) of a WordPress site."
echo
echo "Usage: _do backup <folder>"
;;
checkpoint)
echo "Manages versioned checkpoints of a WordPress installation's manifest."
echo
echo "Usage: _do checkpoint <subcommand> [arguments]"
echo
echo "Subcommands:"
echo " create Creates a new checkpoint of the current plugin/theme/core manifest."
echo " list Lists available checkpoints from the generated list to inspect."
echo " list-generate Generates a detailed list of all checkpoints for fast viewing."
echo " revert [<hash>] Reverts the site to the specified checkpoint hash."
echo " show <hash> Retrieves the details for a specific checkpoint hash."
echo " latest Gets the hash of the most recent checkpoint."
;;
clean)
echo "Cleans up unused WordPress components or analyzes disk usage."
echo
echo "Usage: _do clean <subcommand>"
echo
echo "Subcommands:"
echo " themes Deletes all inactive themes except for the latest default WordPress theme."
echo " disk Provides an interactive disk usage analysis for the current directory."
;;
cron)
echo "Manages scheduled tasks (cron jobs) for this script."
echo
echo "Usage: _do cron <subcommand> [arguments]"
echo
echo "Subcommands:"
echo " enable Adds a job to the system crontab to run '_do cron run' every 10 minutes."
echo " list Lists all scheduled commands."
echo " run Executes any scheduled commands that are due."
echo " add \"<cmd>\" \"<time>\" \"<freq>\" Adds a new command to the schedule."
echo " delete <id> Deletes a command from the schedule."
echo
echo "Arguments for 'add':"
echo " <cmd> (Required) The _do command to run, in quotes (e.g., \"update all\")."
echo " <time> (Required) The next run time, in quotes (e.g., \"4am\", \"tomorrow 2pm\", \"+2 hours\")."
echo " <freq> (Required) The frequency, in quotes (e.g., \"1 day\", \"1 week\", \"12 hours\")."
echo
echo "Example:"
echo " _do cron add \"update all\" \"4am\" \"1 day\""
;;
db)
echo "Performs various database operations."
echo
echo "Usage: _do db <subcommand>"
echo
echo "Subcommands:"
echo " backup Performs a DB-only backup to a secure private directory."
echo " check-autoload Checks the size and top 25 largest autoloaded options in the DB."
echo " optimize Converts tables to InnoDB, reports large tables, and cleans transients."
;;
dump)
echo "Dumps the content of files matching a pattern into a single text file."
echo
echo "Usage: _do dump \"<pattern>\" [-x <exclude_pattern_1>] [-x <exclude_pattern_2>]..."
echo
echo "Arguments:"
echo " <pattern> (Required) The path and file pattern to search for, enclosed in quotes."
echo
echo "Flags:"
echo " -x <pattern> (Optional) A file or directory pattern to exclude. Can be used multiple times."
echo " To exclude a directory, the pattern MUST end with a forward slash (e.g., 'my-dir/')."
echo
echo "Examples:"
echo " _do dump \"wp-content/plugins/my-plugin/**/*.php\""
echo " _do dump \"*\" -x \"*.log\" -x \"node_modules/\""
;;
convert-to-webp)
echo "Finds and converts large images (JPG, PNG) to WebP format."
echo
echo "Usage: _do convert-to-webp [--all]"
echo
echo "Flags:"
echo " --all Convert all images, regardless of size. Defaults to images > 1MB."
;;
install)
echo "Installs helper or premium plugins."
echo
echo "Usage: _do install <subcommand> [--flags]"
echo
echo "Subcommands:"
echo " kinsta-mu Installs the Kinsta MU plugin. Use --force to install outside a Kinsta environment."
echo " helper Installs the CaptainCore Helper plugin."
echo " events-calendar-pro Installs The Events Calendar and its Pro version after prompting for a license."
;;
migrate)
echo "Migrates a site from a backup snapshot."
echo
echo "Usage: _do migrate --url=<backup-url> [--update-urls]"
echo
echo " --update-urls Update urls to destination WordPress site. Default will keep source urls."
;;
monitor)
echo "Monitors server access logs or errors in real-time."
echo
echo "Usage: _do monitor <subcommand> [--flags]"
echo
echo "Subcommands:"
echo " traffic Analyzes and monitors top hits from access logs."
echo " errors Monitors logs for HTTP 500 and PHP fatal errors."
echo " access.log Provides a real-time stream of the access log."
echo " error.log Provides a real-time stream of the error log."
echo
echo "Flags for 'traffic':"
echo " --top=<number> The number of top IP/Status combinations to show. Default is 25."
echo " --now Start processing from the end of the log file instead of the beginning."
;;
php-tags)
echo "Finds outdated or invalid PHP opening tags in PHP files."
echo
echo "Usage: _do php-tags [directory]"
echo
echo "Arguments:"
echo " [directory] (Optional) The directory to search in. Defaults to 'wp-content/'."
;;
reset-wp)
echo "Resets the WordPress installation to a default state."
echo
echo "Usage: _do reset-wp --admin_user=<username> [--admin_email=<email>]"
echo
echo "Flags:"
echo " --admin_user=<username> (Required) The username for the new administrator."
echo " --admin_email=<email> (Optional) The email for the new administrator."
echo " Defaults to the current site's admin email."
;;
reset-permissions)
echo "Resets file and folder permissions to defaults (755 for dirs, 644 for files)."
echo
echo "Usage: _do reset-permissions"
;;
slow-plugins)
echo "Identifies plugins that may be slowing down WP-CLI."
echo
echo "Usage: _do slow-plugins"
;;
suspend)
echo "Activates or deactivates a suspend message shown to visitors."
echo
echo "Usage: _do suspend <subcommand> [flags]"
echo
echo "Subcommands:"
echo " activate Activates the suspend message. Requires --name and --link flags."
echo " deactivate Deactivates the suspend message."
echo
echo "Flags for 'activate':"
echo " --name=<business-name> (Required) The name of the business to display."
echo " --link=<business-link> (Required) The contact link for the business."
echo " --wp-content=<path> (Optional) Path to wp-content directory. Defaults to 'wp-content'."
echo
echo "Flags for 'deactivate':"
echo " --wp-content=<path> (Optional) Path to wp-content directory. Defaults to 'wp-content'."
;;
update)
echo "Handles WordPress core, theme, and plugin updates."
echo
echo "Usage: _do update <subcommand>"
echo
echo "Subcommands:"
echo " all Creates a 'before' checkpoint, runs all updates, creates an"
echo " 'after' checkpoint, and logs the changes."
echo " list Shows a list of past updates to inspect from the generated list."
echo " list-generate Generates a detailed list of all updates for fast viewing."
;;
upgrade)
echo "Upgrades the _do script to the latest version."
echo
echo "Usage: _do upgrade"
;;
vault)
echo "Manages secure, full site snapshots in a remote Restic repository."
echo
echo "Usage: _do vault <subcommand>"
echo
echo "Subcommands:"
echo " create Creates a new snapshot of the current site."
echo " snapshots Lists available snapshots."
echo " mount Mounts the entire repository to a local folder for browsing."
echo " info Displays statistics about the repository."
echo " prune Removes unnecessary data from the repository."
echo
echo "Authentication:"
echo " This command requires B2 and Restic credentials, provided either by"
echo " environment variables (B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_PASSWORD,"
echo " B2_BUCKET, B2_PATH) or by piping a 5-line secrets file via stdin."
echo
echo "Example (stdin):"
echo " cat secrets.txt | _do vault snapshots"
;;
version)
echo "Displays the current version of the _do script."
echo
echo "Usage: _do version"
;;
wpcli)
echo "Checks for and identifies sources of WP-CLI warnings."
echo
echo "Usage: _do wpcli <subcommand>"
echo
echo "Subcommands:"
echo " check Runs a check to find themes or plugins causing WP-CLI warnings."
;;
*)
echo "Error: Unknown command '$cmd' for help." >&2
echo ""
show_usage
exit 1
;;
esac
}
# ----------------------------------------------------
# Displays the main help and usage information.
# ----------------------------------------------------
function show_usage() {
echo "CaptainCore _do (v$CAPTAINCORE_DO_VERSION)"
echo "--------------------------"
echo "A collection of useful command-line utilities for managing WordPress sites."
echo ""
echo "Usage:"
echo " _do <command> [arguments] [--flags]"
echo ""
echo "Available Commands:"
echo " backup Creates a full backup (files + DB) of a WordPress site."
echo " checkpoint Manages versioned checkpoints of the site's manifest."
echo " clean Removes unused items like inactive themes or analyzes disk usage."
echo " convert-to-webp Finds and converts large images (JPG, PNG) to WebP format."
echo " cron Manages cron jobs and schedules tasks to run at specific times."
echo " db Performs various database operations (backup, check-autoload, optimize)."
echo " dump Dumps the content of files matching a pattern into a single text file."
echo " install Installs helper plugins or premium plugins."
echo " migrate Migrates a site from a backup URL or local file."
echo " monitor Monitors server logs or errors in real-time."
echo " php-tags Finds outdated or invalid PHP opening tags."
echo " reset-wp Resets the WordPress installation to a default state."
echo " reset-permissions Resets file and folder permissions to defaults."
echo " slow-plugins Identifies plugins that may be slowing down WP-CLI."
echo " suspend Activates or deactivates a suspend message shown to visitors."
echo " update Runs WordPress updates and logs the changes."
echo " upgrade Upgrades this script to the latest version."
echo " vault Manages secure snapshots in a remote Restic repository."
echo " version Displays the current version of the _do script."
echo " wpcli Checks for and identifies sources of WP-CLI warnings."
echo ""
echo "Run '_do help <command>' for more information on a specific command."
}
# --- Main Entry Point and Argument Parser ---
function main() {
# If no arguments are provided, show usage and exit.
if [ $# -eq 0 ]; then
show_usage
exit 0
fi
# --- Help Flag Handling ---
# Detect 'help <command>' pattern
if [[ "$1" == "help" ]]; then
show_command_help "$2"
exit 0
fi
# Detect '<command> --help' pattern
for arg in "$@"; do
if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
# The first non-flag argument is the command we need help for.
local help_for_cmd=""
for inner_arg in "$@"; do
# Find the first argument that doesn't start with a hyphen.
if [[ ! "$inner_arg" =~ ^- ]]; then
help_for_cmd="$inner_arg"
break
fi
done
show_command_help "$help_for_cmd"
exit 0
fi
done
# --- Centralized Argument Parser ---
# This loop separates flags from commands.
local url_flag=""
local top_flag=""
local name_flag=""
local link_flag=""
local wp_content_flag=""
local update_urls_flag=""
local now_flag=""
local admin_user_flag=""
local admin_email_flag=""
local path_flag=""
local all_files_flag=""
local force_flag=""
local exclude_patterns=()
local positional_args=()
while [[ $# -gt 0 ]]; do
case $1 in
--url=*)
url_flag="${1#*=}"
shift
;;
--top=*)
top_flag="${1#*=}"
shift
;;
--now)
now_flag=true
shift
;;
--name=*)
name_flag="${1#*=}"
shift
;;
--link=*)
link_flag="${1#*=}"
shift
;;
--wp-content=*)
wp_content_flag="${1#*=}"
shift
;;
--update-urls)
update_urls_flag=true
shift
;;
--admin_user=*)
admin_user_flag="${1#*=}"
shift
;;
--admin_email=*)
admin_email_flag="${1#*=}"
shift
;;
--all)
all_files_flag=true
shift
;;
--force)
force_flag=true
shift
;;
--path=*)
path_flag="${1#*=}"
shift
;;
-x) # Exclude flag
if [[ -n "$2" ]]; then
exclude_patterns+=("$2")
shift 2 # past flag and value
else
echo "Error: -x flag requires an argument." >&2
exit 1
fi
;;
-*)
# This will catch unknown flags like --foo
echo "Error: Unknown flag: $1" >&2
show_usage
exit 1
;;
*)
# It's a command or a positional argument
positional_args+=("$1")
shift # past argument
;;
esac
done
# --- Global Path Handling ---
# If a path is provided, change to that directory first.
# This allows commands to be run from anywhere for a specific site.
if [[ -n "$path_flag" ]]; then
if [ -d "$path_flag" ]; then
cd "$path_flag" || { echo "❌ Error: Could not change directory to '$path_flag'." >&2; exit 1; }
else
echo "❌ Error: Provided path '$path_flag' does not exist." >&2
exit 1
fi
fi
# The first positional argument is the main command.
local command="${positional_args[0]}"
# --- Command Router ---
# This routes to the correct function based on the parsed command.
case "$command" in
backup)
full_backup "${positional_args[1]}"
;;
checkpoint)
local subcommand="${positional_args[1]}"
case "$subcommand" in
create)
checkpoint_create
;;
list)
checkpoint_list
;;
list-generate)
checkpoint_list_generate
;;
revert)
local hash="${positional_args[2]}"
checkpoint_revert "$hash"
;;
show)
local hash="${positional_args[2]}"
checkpoint_show "$hash"
;;
latest)
checkpoint_latest
;;
*)
show_command_help "checkpoint"
exit 0
;;
esac
;;
clean)
local arg1="${positional_args[1]}"
case "$arg1" in
themes)
clean_themes
;;
disk)
clean_disk
;;
*)
show_command_help "clean"
exit 0
;;
esac
;;
cron)
local subcommand="${positional_args[1]}"
case "$subcommand" in
enable)
cron_enable
;;
list)
cron_list
;;
run)
cron_run
;;
add)
cron_add "${positional_args[2]}" "${positional_args[3]}" "${positional_args[4]}"
;;
delete)
cron_delete "${positional_args[2]}"
;;
*)
show_command_help "cron"
exit 0
;;
esac
;;
db)
local arg1="${positional_args[1]}"
case "$arg1" in
backup)
db_backup # Originally from backup-db command
;;
check-autoload)
db_check_autoload # Originally from db-check-autoload command
;;
optimize)
db_optimize # Originally from db-optimize command
;;
*)
show_command_help "db"
exit 0
;;
esac
;;
convert-to-webp)
convert_to_webp "$all_files_flag"
;;
dump)
# There should be exactly 2 positional args total: 'dump' and the pattern.
if [ ${#positional_args[@]} -ne 2 ]; then
echo -e "Error: Incorrect number of arguments for 'dump'. It's likely your pattern was expanded by the shell." >&2
echo "Please wrap the input pattern in double quotes." >&2
echo -e "\n Usage: _do dump \"<pattern>\" [-x <exclude1>...]" >&2
return 1
fi
run_dump "${positional_args[1]}" "${exclude_patterns[@]}"
;;
install)
local subcommand="${positional_args[1]}"
case "$subcommand" in
kinsta-mu)
install_kinsta_mu "$force_flag"
;;
helper)
install_helper
;;
events-calendar-pro)
install_events_calendar_pro
;;
*)
show_command_help "install"
exit 0
;;
esac
;;
migrate)
if [[ -z "$url_flag" ]]; then
echo "Error: The 'migrate' command requires the --url=<...> flag." >&2
show_command_help "migrate"
exit 1
fi
migrate_site "$url_flag" "$update_urls_flag"
;;
monitor)
local arg1="${positional_args[1]}"
case "$arg1" in
traffic)
monitor_traffic "$top_flag" "$now_flag"
;;
errors)
monitor_errors
;;
access.log)
monitor_access_log
;;
error.log)
monitor_error_log
;;
*)
show_command_help "monitor"
exit 0
;;
esac
;;
php-tags)
find_outdated_php_tags "${positional_args[1]}"
;;
reset-wp)
if [[ -z "$admin_user_flag" ]]; then
echo "Error: The 'reset-wp' command requires the --admin_user=<...> flag." >&2
show_command_help "reset-wp"
exit 1
fi
reset_site "$admin_user_flag" "$admin_email_flag"
;;
reset-permissions)
reset_permissions
;;
slow-plugins)
identify_slow_plugins
;;
suspend)
local arg1="${positional_args[1]}"
case "$arg1" in
activate)
suspend_activate "$name_flag" "$link_flag" "$wp_content_flag"
;;
deactivate)
suspend_deactivate "$wp_content_flag"
;;
*)
show_command_help "suspend"
exit 0
;;
esac
;;
update)
local subcommand="${positional_args[1]}"
case "$subcommand" in
all)
run_update_all
;;
list)
update_list
;;
list-generate)
update_list_generate
;;
*)
show_command_help "update"
exit 1
;;
esac
;;
upgrade)
run_upgrade
;;
vault)
local subcommand="${positional_args[1]}" # Default to snapshots
case "$subcommand" in
create)
vault_create
;;
snapshots)
vault_snapshots
;;
mount)
vault_mount
;;
info)
vault_info
;;
prune)
vault_prune
;;
*)
show_command_help "vault" >&2
exit 0
;;
esac
;;
version|--version|-v)
show_version
;;
wpcli)
local subcommand="${positional_args[1]}"
case "$subcommand" in
check)
wpcli_check
;;
*)
show_command_help "wpcli"
exit 0
;;
esac
;;
*)
echo "Error: Unknown command '$command'." >&2
show_usage
exit 1
;;
esac
}
# Pass all script arguments to the main function.
main "$@"