From 44b823d4f7a3f5610a765f67648ab4ec2d9dbd18 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Apr 2026 12:42:22 -0500 Subject: [PATCH] feat: add release workflow + migrate MokoCassiopeia styles on install - .gitea/workflows/release.yml: auto-bump, build ZIP, Gitea release, per-channel updates.xml targeting (same as MokoCassiopeia) - script.php: on install, detect MokoCassiopeia styles, create matching MokoOnyx style copies with same params, set default, copy user files Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/release.yml | 517 +++++++++++++++++++++++++++++++++++ src/script.php | 98 +++---- 2 files changed, 569 insertions(+), 46 deletions(-) create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..a5d5164 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,517 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoOnyx.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx +# PATH: /.gitea/workflows/release.yml +# VERSION: 01.00.00 +# BRIEF: Joomla release — build ZIP, publish to Gitea, mirror to GitHub + +name: Create Release + +on: + push: + tags: + - '[0-9][0-9].[0-9][0-9].[0-9][0-9]' + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., 01.00.00)' + required: true + type: string + prerelease: + description: 'Mark as pre-release' + required: false + type: boolean + default: false + stability: + description: 'Stability tag (development, alpha, beta, rc, stable)' + required: false + type: string + default: 'development' + +permissions: + contents: write + +env: + GITEA_URL: https://git.mokoconsulting.tech + GITEA_ORG: MokoConsulting + GITEA_REPO: MokoOnyx + EXT_ELEMENT: mokoonyx + +jobs: + build: + name: Build Release Package + runs-on: release + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + php -v + composer --version + + - name: Get version and stability + id: meta + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + STABILITY="${{ inputs.stability }}" + PRERELEASE="${{ inputs.prerelease }}" + else + VERSION=${GITHUB_REF#refs/tags/} + STABILITY="stable" + PRERELEASE="false" + fi + + # Derive suffix and tag from stability + case "$STABILITY" in + development) SUFFIX="-dev"; TAG_NAME="development" ;; + alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;; + beta) SUFFIX="-beta"; TAG_NAME="beta" ;; + rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;; + stable) SUFFIX=""; TAG_NAME="v${VERSION%%.*}" ;; + *) SUFFIX="-dev"; TAG_NAME="development" ;; + esac + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "Building: ${ZIP_NAME} (${STABILITY})" + + - name: Auto-bump patch version + id: bump + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + INPUT_VERSION: ${{ steps.meta.outputs.version }} + INPUT_STABILITY: ${{ steps.meta.outputs.stability }} + INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }} + run: | + # Read current version from README.md + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ -z "$CURRENT" ]; then + echo "No VERSION in README.md — using input version" + echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1) + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1))) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + + echo "Bumping: ${CURRENT} → ${NEW_VERSION}" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md + + # Update templateDetails.xml / manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST" + fi + + # Update only the matching stability channel in updates.xml + if [ -f "updates.xml" ]; then + export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" + python3 << 'PYEOF' + import re, os + old = os.environ["PY_OLD"] + new = os.environ["PY_NEW"] + stability = os.environ["PY_STABILITY"] + with open("updates.xml") as f: + content = f.read() + pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = block.replace(old, new) + content = content.replace(block, updated) + with open("updates.xml", "w") as f: + f.write(content) + print(f"Updated {stability} channel: {old} -> {new}") + PYEOF + fi + + # Commit bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --optimize-autoloader --no-interaction + fi + + - name: Create package + run: | + mkdir -p build/package + rsync -av \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + src/ build/package/ + + - name: Build ZIP + id: zip + run: | + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + SIZE=$(stat -c%s "${ZIP_NAME}") + + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "size=${SIZE}" >> "$GITHUB_OUTPUT" + echo "SHA-256: ${SHA256}" + echo "Size: ${SIZE} bytes" + + # ── Gitea Release (PRIMARY) ────────────────────────────────────── + - name: "Gitea: Delete existing release" + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Find and delete existing release by tag (may not exist — ignore 404) + RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') + + if [ -n "$RELEASE_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${RELEASE_ID}" || true + echo "Deleted existing release id=${RELEASE_ID}" + fi + + # Delete existing tag + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + + - name: "Gitea: Create release" + id: gitea_release + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + PRERELEASE="${{ steps.meta.outputs.prerelease }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Build release body + BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY}) + + ### SHA-256 + \`${SHA256}\`" + + # Extract changelog if available + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md) + if [ -n "$NOTES" ]; then + BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY}) + + ${NOTES} + + ### SHA-256 + \`${SHA256}\`" + fi + fi + + IS_PRE="true" + if [ "$STABILITY" = "stable" ]; then + IS_PRE="false" + fi + + RESULT=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "${{ github.ref_name }}" \ + --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^}" \ + --arg body "$BODY" \ + --argjson pre "$IS_PRE" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}' + )") + + RELEASE_ID=$(echo "$RESULT" | jq -r '.id') + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + echo "Gitea release created: id=${RELEASE_ID}, tag=${TAG}" + + - name: "Gitea: Upload ZIP" + run: | + RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Uploaded ${ZIP_NAME} to Gitea release ${RELEASE_ID}" + + # ── GitHub Mirror (BACKUP) ─────────────────────────────────────── + - name: "GitHub: Mirror release (stable/rc only)" + if: ${{ steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc' }} + continue-on-error: true + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TOKEN="${{ secrets.GH_TOKEN }}" + GH_REPO="mokoconsulting-tech/${GITEA_REPO}" + GH_API="https://api.github.com/repos/${GH_REPO}" + + IS_PRE="true" + [ "$STABILITY" = "stable" ] && IS_PRE="false" + + # Delete existing release by tag + EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') + if [ -n "$EXISTING" ]; then + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${GH_API}/releases/${EXISTING}" || true + fi + + # Delete tag + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true + + # Create release + RELEASE_ID=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${GH_API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "${{ github.sha }}" \ + --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \ + --arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \ + --argjson pre "$IS_PRE" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}' + )" | jq -r '.id') + + # Upload ZIP + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + echo "GitHub mirror: uploaded ${ZIP_NAME}" + fi + + # ── Update updates.xml ────────────────────────────────────────── + - name: "Update updates.xml for this channel" + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.bump.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag_name }}" + DATE=$(date +%Y-%m-%d) + + if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then + echo "No updates.xml or no SHA — skipping" + exit 0 + fi + + export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + stability = os.environ["PY_STABILITY"] + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + + # Map stability to the value in updates.xml + tag_map = { + "development": "development", + "alpha": "alpha", + "beta": "beta", + "rc": "rc", + "stable": "stable", + } + xml_tag = tag_map.get(stability, "development") + + with open("updates.xml", "r") as f: + content = f.read() + + # Build regex to find the specific block for this stability tag + # Use negative lookahead to avoid matching across multiple blocks + block_pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if not match: + print(f"No block found for {xml_tag}") + exit(0) + + block = match.group(1) + original_block = block + + # Update version + block = re.sub(r"[^<]*", f"{version}", block) + + # Update creation date + block = re.sub(r"[^<]*", f"{date}", block) + + # Update SHA-256 + block = re.sub(r"[^<]*", f"{sha256}", block) + + # Update Gitea download URL + gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + block = re.sub( + r"(]*>)https://git\.mokoconsulting\.tech/[^<]*()", + rf"\g<1>{gitea_url}\g<2>", + block + ) + + # Update GitHub download URL only for RC and stable (others are Gitea-only) + if stability in ("rc", "stable"): + gh_url = f"https://github.com/mokoconsulting-tech/{gitea_repo}/releases/download/{tag}/{zip_name}" + block = re.sub( + r"(]*>)https://github\.com/[^<]*()", + rf"\g<1>{gh_url}\g<2>", + block + ) + else: + # Remove any GitHub download URL for dev/alpha/beta + block = re.sub( + r"\n\s*]*>https://github\.com/[^<]*", + "", + block + ) + + content = content.replace(original_block, block) + + with open("updates.xml", "w") as f: + f.write(content) + + print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}") + PYEOF + + - name: "Commit updates.xml to current branch and main" + run: | + if git diff --quiet updates.xml 2>/dev/null; then + echo "No changes to updates.xml" + exit 0 + fi + + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.bump.outputs.version }}" + CURRENT_BRANCH="${{ github.ref_name }}" + TOKEN="${{ secrets.GA_TOKEN }}" + + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + + # Set push URL with GA_TOKEN for authenticated pushes (branch protection requires jmiller) + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + # Push to current branch + git push || true + + # Also update updates.xml on main via Gitea API (git push blocked by branch protection) + if [ "$CURRENT_BRANCH" != "main" ]; then + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + # Get current file SHA on main (required for update) + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + # Base64-encode the updates.xml content from working tree (has updated SHA) + CONTENT=$(base64 -w0 updates.xml) + + RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: update ${STABILITY} channel to ${VERSION} on main [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )") + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then + echo "updates.xml synced to main via API (HTTP ${HTTP_CODE})" + else + echo "WARNING: failed to sync updates.xml to main (HTTP ${HTTP_CODE})" + echo "$RESPONSE" | head -5 + fi + else + echo "WARNING: could not get file SHA for updates.xml on main" + fi + fi + + - name: Summary + run: | + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TAG="${{ steps.meta.outputs.tag_name }}" + + echo "### Release Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY diff --git a/src/script.php b/src/script.php index cdcabbe..878788d 100644 --- a/src/script.php +++ b/src/script.php @@ -96,13 +96,14 @@ class Tpl_MokoonyxInstallerScript } /** - * Detect MokoCassiopeia and migrate styles, params, and user files to MokoOnyx. + * Detect MokoCassiopeia and create matching MokoOnyx styles with the same params. + * Creates a MokoOnyx style copy for each MokoCassiopeia style. */ private function migrateFromCassiopeia(): void { $db = Factory::getDbo(); - // Check if MokoCassiopeia has any template styles + // Get all MokoCassiopeia styles $query = $db->getQuery(true) ->select('*') ->from('#__template_styles') @@ -115,69 +116,74 @@ class Tpl_MokoonyxInstallerScript return; } - $this->logMessage('MokoCassiopeia detected — migrating ' . count($oldStyles) . ' style(s).'); + $this->logMessage('MokoCassiopeia detected — creating ' . count($oldStyles) . ' matching MokoOnyx style(s).'); + + // Get the installer-created default MokoOnyx style (to apply params to it) + $query = $db->getQuery(true) + ->select('id') + ->from('#__template_styles') + ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) + ->where($db->quoteName('client_id') . ' = 0') + ->order($db->quoteName('id') . ' ASC'); + $defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult(); + + $firstStyle = true; - // 1. Copy template styles with params foreach ($oldStyles as $oldStyle) { $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title); $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle); - // Check if MokoOnyx already has a style with this title - $check = $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__template_styles') - ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle)); - if ((int) $db->setQuery($check)->loadResult() > 0) { - // Update existing MokoOnyx style with MokoCassiopeia's params - $params = is_string($oldStyle->params) - ? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params) - : $oldStyle->params; + $params = is_string($oldStyle->params) + ? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params) + : $oldStyle->params; + if ($firstStyle && $defaultOnyxId) { + // Update the installer-created default style with the first MokoCassiopeia style's params $update = $db->getQuery(true) ->update('#__template_styles') ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME)) - ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle)); + ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle)) + ->where('id = ' . $defaultOnyxId); $db->setQuery($update)->execute(); - $this->logMessage("Updated existing MokoOnyx style: {$newTitle}"); + + // Set as default if MokoCassiopeia was default + if ($oldStyle->home == 1) { + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 1') + ->where('id = ' . $defaultOnyxId) + )->execute(); + + $db->setQuery( + $db->getQuery(true) + ->update('#__template_styles') + ->set($db->quoteName('home') . ' = 0') + ->where('id = ' . (int) $oldStyle->id) + )->execute(); + + $this->logMessage('Set MokoOnyx as default site template.'); + } + + $this->logMessage("Updated default MokoOnyx style with params: {$newTitle}"); + $firstStyle = false; continue; } - // Create new MokoOnyx style from MokoCassiopeia style + // For additional styles: create new MokoOnyx style copies $newStyle = clone $oldStyle; unset($newStyle->id); $newStyle->template = self::NEW_NAME; $newStyle->title = $newTitle; - $newStyle->home = 0; // Don't set as default yet + $newStyle->home = 0; + $newStyle->params = $params; - if (is_string($newStyle->params)) { - $newStyle->params = str_replace(self::OLD_NAME, self::NEW_NAME, $newStyle->params); + try { + $db->insertObject('#__template_styles', $newStyle, 'id'); + $this->logMessage("Created MokoOnyx style: {$newTitle}"); + } catch (\Throwable $e) { + $this->logMessage("Failed to create style {$newTitle}: " . $e->getMessage(), 'warning'); } - - $db->insertObject('#__template_styles', $newStyle, 'id'); - $newId = $newStyle->id; - - // If the old style was the default, make the new one default - if ($oldStyle->home == 1) { - $db->setQuery( - $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 1') - ->where('id = ' . (int) $newId) - )->execute(); - - $db->setQuery( - $db->getQuery(true) - ->update('#__template_styles') - ->set($db->quoteName('home') . ' = 0') - ->where('id = ' . (int) $oldStyle->id) - )->execute(); - - $this->logMessage('Set MokoOnyx as default site template.'); - } - - $this->logMessage("Migrated style: {$oldStyle->title} → {$newTitle}"); } // 2. Copy user files (custom themes, user.css, user.js)