diff --git a/src/helper/migrate.php b/src/helper/migrate.php
new file mode 100644
index 0000000..8d94ecc
--- /dev/null
+++ b/src/helper/migrate.php
@@ -0,0 +1,205 @@
+
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * One-time migration from MokoCassiopeia → MokoOnyx.
+ * Called from index.php on first page load. Creates a .migrated
+ * marker file so it only runs once.
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Log\Log;
+
+(function () {
+ $markerFile = __DIR__ . '/../.migrated';
+
+ // Already migrated
+ if (file_exists($markerFile)) {
+ return;
+ }
+
+ $db = Factory::getDbo();
+ $app = Factory::getApplication();
+
+ $oldName = 'mokocassiopeia';
+ $newName = 'mokoonyx';
+ $oldDisplay = 'MokoCassiopeia';
+ $newDisplay = 'MokoOnyx';
+
+ // Init logger
+ Log::addLogger(
+ ['text_file' => 'mokoonyx_migrate.log.php'],
+ Log::ALL,
+ ['mokoonyx_migrate']
+ );
+
+ $log = function (string $msg, int $level = Log::INFO) {
+ Log::add($msg, $level, 'mokoonyx_migrate');
+ };
+
+ $log('=== MokoOnyx migration started (index.php bootstrap) ===');
+
+ // Check if MokoCassiopeia has styles to migrate
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from('#__template_styles')
+ ->where($db->quoteName('template') . ' = ' . $db->quote($oldName))
+ ->where($db->quoteName('client_id') . ' = 0');
+ $oldStyles = $db->setQuery($query)->loadObjectList();
+
+ if (empty($oldStyles)) {
+ $log('No MokoCassiopeia styles found — fresh install, nothing to migrate.');
+ @file_put_contents($markerFile, date('Y-m-d H:i:s') . ' fresh install');
+ return;
+ }
+
+ $log('Found ' . count($oldStyles) . ' MokoCassiopeia style(s) to migrate.');
+
+ // Get the default MokoOnyx style (created by Joomla installer)
+ $query = $db->getQuery(true)
+ ->select('id')
+ ->from('#__template_styles')
+ ->where($db->quoteName('template') . ' = ' . $db->quote($newName))
+ ->where($db->quoteName('client_id') . ' = 0')
+ ->order($db->quoteName('id') . ' ASC');
+ $defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
+
+ $isFirst = true;
+
+ foreach ($oldStyles as $old) {
+ $newTitle = str_replace($oldDisplay, $newDisplay, $old->title);
+ $newTitle = str_replace($oldName, $newName, $newTitle);
+ $newParams = is_string($old->params)
+ ? str_replace($oldName, $newName, $old->params)
+ : $old->params;
+
+ if ($isFirst && $defaultOnyxId) {
+ // Apply params to the installer-created default MokoOnyx style
+ $update = $db->getQuery(true)
+ ->update('#__template_styles')
+ ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
+ ->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
+ ->where('id = ' . $defaultOnyxId);
+ $db->setQuery($update)->execute();
+
+ // Set as default if MokoCassiopeia was default
+ if ($old->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) $old->id)
+ )->execute();
+
+ $log('Set MokoOnyx as default site template.');
+ }
+
+ $log("Applied params to default MokoOnyx style: {$newTitle}");
+ $isFirst = false;
+ continue;
+ }
+
+ // Additional styles: check if already exists
+ $check = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from('#__template_styles')
+ ->where($db->quoteName('template') . ' = ' . $db->quote($newName))
+ ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
+ if ((int) $db->setQuery($check)->loadResult() > 0) {
+ $log("Style '{$newTitle}' already exists — skipping.");
+ continue;
+ }
+
+ // Create new MokoOnyx style copy
+ $new = clone $old;
+ unset($new->id);
+ $new->template = $newName;
+ $new->title = $newTitle;
+ $new->params = $newParams;
+ $new->home = 0;
+
+ try {
+ $db->insertObject('#__template_styles', $new, 'id');
+ $log("Created MokoOnyx style: {$newTitle}");
+ } catch (\Throwable $e) {
+ $log("Failed to create style '{$newTitle}': " . $e->getMessage(), Log::WARNING);
+ }
+ }
+
+ // Copy user files from MokoCassiopeia media
+ $oldMedia = JPATH_ROOT . '/media/templates/site/' . $oldName;
+ $newMedia = JPATH_ROOT . '/media/templates/site/' . $newName;
+
+ if (is_dir($oldMedia) && is_dir($newMedia)) {
+ $userFiles = [
+ 'css/theme/light.custom.css',
+ 'css/theme/dark.custom.css',
+ 'css/theme/light.custom.min.css',
+ 'css/theme/dark.custom.min.css',
+ 'css/user.css',
+ 'css/user.min.css',
+ 'js/user.js',
+ 'js/user.min.js',
+ ];
+
+ $copied = 0;
+ foreach ($userFiles as $rel) {
+ $src = $oldMedia . '/' . $rel;
+ $dst = $newMedia . '/' . $rel;
+ if (is_file($src) && !is_file($dst)) {
+ $dir = dirname($dst);
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+ copy($src, $dst);
+ $copied++;
+ }
+ }
+
+ if ($copied > 0) {
+ $log("Copied {$copied} user file(s) from MokoCassiopeia.");
+ }
+ }
+
+ // Update the update server
+ try {
+ $onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
+ $query = $db->getQuery(true)
+ ->update('#__update_sites')
+ ->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
+ ->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
+ ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
+ $db->setQuery($query)->execute();
+ $n = $db->getAffectedRows();
+ if ($n > 0) {
+ $log("Redirected {$n} update site(s) to MokoOnyx.");
+ }
+ } catch (\Throwable $e) {
+ $log('Update server redirect failed: ' . $e->getMessage(), Log::WARNING);
+ }
+
+ // Write marker file
+ @file_put_contents($markerFile, date('Y-m-d H:i:s') . " migrated {$oldName} → {$newName}");
+
+ $log('=== Migration completed ===');
+
+ // Enqueue message for admin
+ if ($app->isClient('administrator')) {
+ $app->enqueueMessage(
+ 'MokoOnyx has imported your MokoCassiopeia settings.
'
+ . 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
+ 'success'
+ );
+ }
+})();
diff --git a/src/index.php b/src/index.php
index 5de0bc1..add7e31 100644
--- a/src/index.php
+++ b/src/index.php
@@ -9,6 +9,11 @@
defined('_JEXEC') or die;
+// One-time migration from MokoCassiopeia (runs once, creates .migrated marker)
+if (!file_exists(__DIR__ . '/.migrated')) {
+ require_once __DIR__ . '/helper/migrate.php';
+}
+
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;