From a200f1b8fe804f1e653d1bf4b984bfe61a29274d Mon Sep 17 00:00:00 2001 From: allrude Date: Sun, 22 Feb 2026 15:55:26 +0100 Subject: [PATCH] feat: add multistore support with `mage add store` and `mage add stores` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds interactive and batch workflows for creating Magento 2 store views under a Valet local development environment. New commands: mage add store — interactive: prompts for code, name, domain, locale; creates the store view via dev/tools/create-store.php, sets base URLs, updates .valet-env.php, and runs valet link + valet secure in one step. mage add stores — batch: reads dev/tools/stores.json and creates all defined store views non-interactively; batches all valet link calls then all valet secure calls to avoid redundant nginx restarts. stores.json schema (project-agnostic — domain derived from project name): { "stores": [ { "code": "nl_nl", "name": "Dutch", "suffix": "nl", "locale": "nl_NL" } ] } Changes: src/_multistore.sh (new) - mage_valet_env_add_entry: appends a store entry to .valet-env.php using Python 3; skips duplicates safely. - mage_add_store: single-store interactive flow with auto-lowercase of store code and input validation. - mage_add_stores_from_file: batch flow; resolves stores.json from dev/tools/ -> project root -> mage bin dir; computes domain as - so stores.json is project-agnostic. src/_setup.sh - Removed hardcoded placeholder store-2 / default2 from mage_setup. - mage_setup now scaffolds dev/tools/create-store.php and dev/tools/stores.json from the mage bin directory into every new project automatically. - After scaffold, mage_setup prompts to run mage_add_stores_from_file if stores.json is non-empty (skippable with N). src/_info.sh — added help entries for `add store` and `add stores` src/_mage.sh — added dispatch cases for `add store` and `add stores` --- mage | 251 ++++++++++++++++++++++++++++++++++++++++++++- src/_info.sh | 2 + src/_mage.sh | 8 ++ src/_multistore.sh | 199 +++++++++++++++++++++++++++++++++++ src/_setup.sh | 35 ++++++- 5 files changed, 490 insertions(+), 5 deletions(-) create mode 100644 src/_multistore.sh diff --git a/mage b/mage index 05962c5..926e381 100755 --- a/mage +++ b/mage @@ -375,6 +375,8 @@ function mage_help() { mage_help_cmd "add hyva" "Add Hyva Theme" mage_help_cmd "add checkout" "Add Hyva Checkout" mage_help_cmd "add baldr" "Add Siteation Baldr" + mage_help_cmd "add store" "Create a new store view (interactive)" + mage_help_cmd "add stores" "Create all stores from dev/tools/stores.json" mage_help_cmd "add [PKG]" "Add composer package" mage_help_cmd "upd/update [PKG]" "Update composer package(s)" mage_help_cmd "del/remove [PKG]" "Remove composer package(s)" @@ -557,6 +559,206 @@ function convert_to_mage_os() { $MAGENTO_CLI s:up } +# --------------------------------------------------------------------------- +# Multistore helpers +# --------------------------------------------------------------------------- + +# Append a store entry to .valet-env.php (preserving all existing entries) +function mage_valet_env_add_entry() { + local domain="$1" + local code="$2" + + python3 - "$domain" "$code" <<'PYEOF' +import sys, re + +domain, code = sys.argv[1], sys.argv[2] +entry = ( + f"\t'{domain}' => [\n" + f"\t\t'MAGE_RUN_CODE' => '{code}',\n" + f"\t\t'MAGE_RUN_TYPE' => 'store',\n" + f"\t],\n" +) + +with open('.valet-env.php', 'r') as f: + content = f.read() + +# Skip if domain already present +if f"'{domain}'" in content: + print(f".valet-env.php already contains '{domain}', skipping.") + sys.exit(0) + +# Insert before the closing ]; +content = re.sub(r'^(\];)', entry + r'\1', content, flags=re.MULTILINE) + +with open('.valet-env.php', 'w') as f: + f.write(content) + +print(f"Updated .valet-env.php: added '{domain}' → '{code}'") +PYEOF +} + +# Add a single store view (interactive) +function mage_add_store() { + echo "" + read -e -p "Store code (e.g. nl_nl): " store_code && echo "" + read -e -p "Store name (e.g. Dutch Store): " store_name && echo "" + read -e -p "Domain name (e.g. myproject-nl, without .test): " store_domain && echo "" + read -e -p "Locale code (e.g. nl_NL, leave empty to skip): " store_locale && echo "" + + if [[ -z "$store_code" || -z "$store_name" || -z "$store_domain" ]]; then + echo "Error: store code, name and domain are required." + return 1 + fi + + # Auto-lowercase the store code + store_code=$(echo "$store_code" | tr '[:upper:]' '[:lower:]') + + if [[ ! "$store_code" =~ ^[a-z][a-z0-9_]*$ ]]; then + echo "Error: store code must be letters, digits and underscores, starting with a letter (got: ${store_code})." + return 1 + fi + + echo "Creating store group and store view in Magento..." + local php_args="--code=${store_code} --name=${store_name}" + if [[ -n "$store_locale" ]]; then + php_args="${php_args} --locale=${store_locale}" + fi + + if ! $PHP_CLI dev/tools/create-store.php $php_args; then + echo "Error: failed to create store entities." + return 1 + fi + + echo "Setting base URLs..." + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/unsecure/base_url "https://${store_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/secure/base_url "https://${store_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/secure/use_in_frontend 1 + + echo "Flushing cache..." + $MAGENTO_CLI cache:flush + + echo "Updating .valet-env.php..." + mage_valet_env_add_entry "$store_domain" "$store_code" + + echo "" + echo "Store '${store_code}' created." + + if [[ $VALET == 1 ]]; then + echo "" + echo "Linking and securing Valet domain..." + valet link "${store_domain}" + valet secure "${store_domain}" + fi + echo "" +} + +# Add all stores defined in dev/tools/stores.json (batch, non-interactive) +# Schema: { "stores": [ { "code": "nl_nl", "name": "Dutch", "suffix": "nl", "locale": "nl_NL" } ] } +# The domain is computed as - so stores.json stays project-agnostic. +function mage_add_stores_from_file() { + local stores_file="${1:-}" + + # Resolve file: dev/tools/ → project root → mage bin dir + if [[ -z "$stores_file" ]]; then + if [[ -f "dev/tools/stores.json" ]]; then + stores_file="dev/tools/stores.json" + elif [[ -f "stores.json" ]]; then + stores_file="stores.json" + elif [[ -f "$(dirname "$0")/stores.json" ]]; then + stores_file="$(dirname "$0")/stores.json" + fi + fi + + if [[ -z "$stores_file" || ! -f "$stores_file" ]]; then + echo "No stores.json found (checked dev/tools/, project root, and mage bin dir)." + return 0 + fi + + echo "Reading stores from ${stores_file}..." + + local project_name + project_name=$(basename "$(pwd)") + + # Parse JSON with Python 3 — output one line per store: code|name|suffix|locale + local stores_data + stores_data=$(python3 - "$stores_file" <<'PYEOF' +import json, sys +path = sys.argv[1] +data = json.load(open(path)) +stores = data.get("stores", []) +if not stores: + print("__empty__") + sys.exit(0) +for s in stores: + code = s.get("code", "") + name = s.get("name", "") + suffix = s.get("suffix", "") + locale = s.get("locale", "") + if not code or not name or not suffix: + print(f"__skip__: missing field in entry: {s}", file=__import__('sys').stderr) + continue + print(f"{code}|{name}|{suffix}|{locale}") +PYEOF +) + + if [[ "$stores_data" == "__empty__" ]]; then + echo "No stores defined in ${stores_file}." + return 0 + fi + + local created=0 + local new_domains=() + while IFS='|' read -r s_code s_name s_suffix s_locale; do + [[ -z "$s_code" ]] && continue + + local s_domain="${project_name}-${s_suffix}" + + echo "" + echo "→ Creating store: ${s_code} (${s_name}) at ${s_domain}.test" + + local php_args="--code=${s_code} --name=${s_name}" + if [[ -n "$s_locale" ]]; then + php_args="${php_args} --locale=${s_locale}" + fi + + if ! $PHP_CLI dev/tools/create-store.php $php_args; then + echo " Skipping ${s_code} due to error." + continue + fi + + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/unsecure/base_url "https://${s_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/secure/base_url "https://${s_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/secure/use_in_frontend 1 + + mage_valet_env_add_entry "$s_domain" "$s_code" + + new_domains+=("$s_domain") + (( created++ )) + done <<< "$stores_data" + + if [[ "$created" -gt 0 ]]; then + echo "" + echo "Flushing cache..." + $MAGENTO_CLI cache:flush + + if [[ $VALET == 1 && ${#new_domains[@]} -gt 0 ]]; then + echo "" + echo "Linking Valet domains..." + for d in "${new_domains[@]}"; do + valet link "$d" + done + echo "" + echo "Securing Valet domains..." + for d in "${new_domains[@]}"; do + valet secure "$d" + done + fi + + echo "" + echo "${created} store(s) created." + fi +} + function mage_new_in_folder() { if [[ ! -d package-source ]]; then mkdir package-source @@ -871,9 +1073,6 @@ function mage_add_sample() { touch README.md php -f $HOME/.magento-sampledata/$mversion/dev/tools/build-sample-data.php -- --ce-source="$PWD" - # Unset default styles from sample data - $MAGENTO_CLI config:set design/head/includes "" &> /dev/null - $MAGENTO_CLI setup:upgrade # Set theme to Hyva if present @@ -883,6 +1082,9 @@ function mage_add_sample() { fi fi + # Unset default styles from sample data + $MAGENTO_CLI config:set design/head/includes "" &> /dev/null + $MAGENTO_CLI indexer:reindex $MAGENTO_CLI cache:flush } @@ -1010,11 +1212,44 @@ function mage_setup() { { echo -e ' .valet-env.php fi + # Scaffold dev/tools/ with multistore helpers from mage bin dir + local _bin_dir + _bin_dir="$(dirname "$0")" + if [[ -f "${_bin_dir}/create-store.php" ]]; then + mkdir -p dev/tools + cp "${_bin_dir}/create-store.php" dev/tools/create-store.php + fi + if [[ ! -f "dev/tools/stores.json" ]] && [[ -f "${_bin_dir}/stores.json" ]]; then + mkdir -p dev/tools + cp "${_bin_dir}/stores.json" dev/tools/stores.json + fi + + # Create extra stores from stores.json (if present and non-empty) + if [[ $VALET == 1 ]]; then + _stores_file="" + if [[ -f "dev/tools/stores.json" ]]; then + _stores_file="dev/tools/stores.json" + elif [[ -f "stores.json" ]]; then + _stores_file="stores.json" + elif [[ -f "$(dirname "$0")/stores.json" ]]; then + _stores_file="$(dirname "$0")/stores.json" + fi + + if [[ -n "$_stores_file" ]]; then + _count=$(python3 -c "import json; d=json.load(open('${_stores_file}')); print(len(d.get('stores',[])))" 2>/dev/null || echo 0) + if [[ "$_count" -gt 0 ]]; then + read -p "Create ${_count} store(s) from ${_stores_file}? [Y/n] " _ADD && echo "" + if [[ ! $_ADD =~ ^[nN] ]]; then + mage_add_stores_from_file "$_stores_file" + fi + fi + fi + fi + # Cleanup root sample files mage_cleanup_sample_files } @@ -1245,6 +1480,14 @@ case "${@}" in $COMPOSER_CLI require siteation/magento2-theme-baldr ;; +"add store") + mage_add_store + ;; + +"add stores") + mage_add_stores_from_file + ;; + "add "*) $COMPOSER_CLI require "${@:2}" ;; diff --git a/src/_info.sh b/src/_info.sh index 094648c..5f763e1 100644 --- a/src/_info.sh +++ b/src/_info.sh @@ -51,6 +51,8 @@ function mage_help() { mage_help_cmd "add hyva" "Add Hyva Theme" mage_help_cmd "add checkout" "Add Hyva Checkout" mage_help_cmd "add baldr" "Add Siteation Baldr" + mage_help_cmd "add store" "Create a new store view (interactive)" + mage_help_cmd "add stores" "Create all stores from dev/tools/stores.json" mage_help_cmd "add [PKG]" "Add composer package" mage_help_cmd "upd/update [PKG]" "Update composer package(s)" mage_help_cmd "del/remove [PKG]" "Remove composer package(s)" diff --git a/src/_mage.sh b/src/_mage.sh index 14d6559..eeed511 100644 --- a/src/_mage.sh +++ b/src/_mage.sh @@ -217,6 +217,14 @@ case "${@}" in $COMPOSER_CLI require siteation/magento2-theme-baldr ;; +"add store") + mage_add_store + ;; + +"add stores") + mage_add_stores_from_file + ;; + "add "*) $COMPOSER_CLI require "${@:2}" ;; diff --git a/src/_multistore.sh b/src/_multistore.sh new file mode 100644 index 0000000..cede596 --- /dev/null +++ b/src/_multistore.sh @@ -0,0 +1,199 @@ +# --------------------------------------------------------------------------- +# Multistore helpers +# --------------------------------------------------------------------------- + +# Append a store entry to .valet-env.php (preserving all existing entries) +function mage_valet_env_add_entry() { + local domain="$1" + local code="$2" + + python3 - "$domain" "$code" <<'PYEOF' +import sys, re + +domain, code = sys.argv[1], sys.argv[2] +entry = ( + f"\t'{domain}' => [\n" + f"\t\t'MAGE_RUN_CODE' => '{code}',\n" + f"\t\t'MAGE_RUN_TYPE' => 'store',\n" + f"\t],\n" +) + +with open('.valet-env.php', 'r') as f: + content = f.read() + +# Skip if domain already present +if f"'{domain}'" in content: + print(f".valet-env.php already contains '{domain}', skipping.") + sys.exit(0) + +# Insert before the closing ]; +content = re.sub(r'^(\];)', entry + r'\1', content, flags=re.MULTILINE) + +with open('.valet-env.php', 'w') as f: + f.write(content) + +print(f"Updated .valet-env.php: added '{domain}' → '{code}'") +PYEOF +} + +# Add a single store view (interactive) +function mage_add_store() { + echo "" + read -e -p "Store code (e.g. nl_nl): " store_code && echo "" + read -e -p "Store name (e.g. Dutch Store): " store_name && echo "" + read -e -p "Domain name (e.g. myproject-nl, without .test): " store_domain && echo "" + read -e -p "Locale code (e.g. nl_NL, leave empty to skip): " store_locale && echo "" + + if [[ -z "$store_code" || -z "$store_name" || -z "$store_domain" ]]; then + echo "Error: store code, name and domain are required." + return 1 + fi + + # Auto-lowercase the store code + store_code=$(echo "$store_code" | tr '[:upper:]' '[:lower:]') + + if [[ ! "$store_code" =~ ^[a-z][a-z0-9_]*$ ]]; then + echo "Error: store code must be letters, digits and underscores, starting with a letter (got: ${store_code})." + return 1 + fi + + echo "Creating store group and store view in Magento..." + local php_args="--code=${store_code} --name=${store_name}" + if [[ -n "$store_locale" ]]; then + php_args="${php_args} --locale=${store_locale}" + fi + + if ! $PHP_CLI dev/tools/create-store.php $php_args; then + echo "Error: failed to create store entities." + return 1 + fi + + echo "Setting base URLs..." + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/unsecure/base_url "https://${store_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/secure/base_url "https://${store_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$store_code" web/secure/use_in_frontend 1 + + echo "Flushing cache..." + $MAGENTO_CLI cache:flush + + echo "Updating .valet-env.php..." + mage_valet_env_add_entry "$store_domain" "$store_code" + + echo "" + echo "Store '${store_code}' created." + + if [[ $VALET == 1 ]]; then + echo "" + echo "Linking and securing Valet domain..." + valet link "${store_domain}" + valet secure "${store_domain}" + fi + echo "" +} + +# Add all stores defined in dev/tools/stores.json (batch, non-interactive) +# Schema: { "stores": [ { "code": "nl_nl", "name": "Dutch", "suffix": "nl", "locale": "nl_NL" } ] } +# The domain is computed as - so stores.json stays project-agnostic. +function mage_add_stores_from_file() { + local stores_file="${1:-}" + + # Resolve file: dev/tools/ → project root → mage bin dir + if [[ -z "$stores_file" ]]; then + if [[ -f "dev/tools/stores.json" ]]; then + stores_file="dev/tools/stores.json" + elif [[ -f "stores.json" ]]; then + stores_file="stores.json" + elif [[ -f "$(dirname "$0")/stores.json" ]]; then + stores_file="$(dirname "$0")/stores.json" + fi + fi + + if [[ -z "$stores_file" || ! -f "$stores_file" ]]; then + echo "No stores.json found (checked dev/tools/, project root, and mage bin dir)." + return 0 + fi + + echo "Reading stores from ${stores_file}..." + + local project_name + project_name=$(basename "$(pwd)") + + # Parse JSON with Python 3 — output one line per store: code|name|suffix|locale + local stores_data + stores_data=$(python3 - "$stores_file" <<'PYEOF' +import json, sys +path = sys.argv[1] +data = json.load(open(path)) +stores = data.get("stores", []) +if not stores: + print("__empty__") + sys.exit(0) +for s in stores: + code = s.get("code", "") + name = s.get("name", "") + suffix = s.get("suffix", "") + locale = s.get("locale", "") + if not code or not name or not suffix: + print(f"__skip__: missing field in entry: {s}", file=__import__('sys').stderr) + continue + print(f"{code}|{name}|{suffix}|{locale}") +PYEOF +) + + if [[ "$stores_data" == "__empty__" ]]; then + echo "No stores defined in ${stores_file}." + return 0 + fi + + local created=0 + local new_domains=() + while IFS='|' read -r s_code s_name s_suffix s_locale; do + [[ -z "$s_code" ]] && continue + + local s_domain="${project_name}-${s_suffix}" + + echo "" + echo "→ Creating store: ${s_code} (${s_name}) at ${s_domain}.test" + + local php_args="--code=${s_code} --name=${s_name}" + if [[ -n "$s_locale" ]]; then + php_args="${php_args} --locale=${s_locale}" + fi + + if ! $PHP_CLI dev/tools/create-store.php $php_args; then + echo " Skipping ${s_code} due to error." + continue + fi + + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/unsecure/base_url "https://${s_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/secure/base_url "https://${s_domain}.test/" + $MAGENTO_CLI config:set --scope=stores --scope-code="$s_code" web/secure/use_in_frontend 1 + + mage_valet_env_add_entry "$s_domain" "$s_code" + + new_domains+=("$s_domain") + (( created++ )) + done <<< "$stores_data" + + if [[ "$created" -gt 0 ]]; then + echo "" + echo "Flushing cache..." + $MAGENTO_CLI cache:flush + + if [[ $VALET == 1 && ${#new_domains[@]} -gt 0 ]]; then + echo "" + echo "Linking Valet domains..." + for d in "${new_domains[@]}"; do + valet link "$d" + done + echo "" + echo "Securing Valet domains..." + for d in "${new_domains[@]}"; do + valet secure "$d" + done + fi + + echo "" + echo "${created} store(s) created." + fi +} diff --git a/src/_setup.sh b/src/_setup.sh index 031c88f..06f15ba 100644 --- a/src/_setup.sh +++ b/src/_setup.sh @@ -121,11 +121,44 @@ function mage_setup() { { echo -e ' .valet-env.php fi + # Scaffold dev/tools/ with multistore helpers from mage bin dir + local _bin_dir + _bin_dir="$(dirname "$0")" + if [[ -f "${_bin_dir}/create-store.php" ]]; then + mkdir -p dev/tools + cp "${_bin_dir}/create-store.php" dev/tools/create-store.php + fi + if [[ ! -f "dev/tools/stores.json" ]] && [[ -f "${_bin_dir}/stores.json" ]]; then + mkdir -p dev/tools + cp "${_bin_dir}/stores.json" dev/tools/stores.json + fi + + # Create extra stores from stores.json (if present and non-empty) + if [[ $VALET == 1 ]]; then + _stores_file="" + if [[ -f "dev/tools/stores.json" ]]; then + _stores_file="dev/tools/stores.json" + elif [[ -f "stores.json" ]]; then + _stores_file="stores.json" + elif [[ -f "$(dirname "$0")/stores.json" ]]; then + _stores_file="$(dirname "$0")/stores.json" + fi + + if [[ -n "$_stores_file" ]]; then + _count=$(python3 -c "import json; d=json.load(open('${_stores_file}')); print(len(d.get('stores',[])))" 2>/dev/null || echo 0) + if [[ "$_count" -gt 0 ]]; then + read -p "Create ${_count} store(s) from ${_stores_file}? [Y/n] " _ADD && echo "" + if [[ ! $_ADD =~ ^[nN] ]]; then + mage_add_stores_from_file "$_stores_file" + fi + fi + fi + fi + # Cleanup root sample files mage_cleanup_sample_files }