diff --git a/src/helper/bridge.php b/src/helper/bridge.php
deleted file mode 100644
index 34f14fe..0000000
--- a/src/helper/bridge.php
+++ /dev/null
@@ -1,256 +0,0 @@
-
- *
- * This file is part of a Moko Consulting project.
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/**
- * Bridge migration helper — MokoCassiopeia → MokoOnyx
- *
- * Renames template files/folders and updates the database to migrate
- * from MokoCassiopeia to MokoOnyx. No external downloads required.
- */
-
-defined('_JEXEC') or die;
-
-use Joomla\CMS\Factory;
-use Joomla\CMS\Log\Log;
-
-class MokoBridgeMigration
-{
- private const OLD_NAME = 'mokocassiopeia';
- private const NEW_NAME = 'mokoonyx';
-
- private const OLD_DISPLAY = 'MokoCassiopeia';
- private const NEW_DISPLAY = 'MokoOnyx';
-
- /**
- * Run the full migration.
- */
- public static function run(): bool
- {
- $app = Factory::getApplication();
-
- // Already migrated?
- if (is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
- self::log('MokoOnyx template dir already exists — updating database only.');
- self::updateDatabase();
- self::notifyUser($app);
- return true;
- }
-
- // 1. Rename template directory
- $renamed = self::renameTemplateDir();
- if (!$renamed) {
- $app->enqueueMessage(
- 'MokoOnyx migration: could not rename template directory. '
- . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.',
- 'warning'
- );
- return false;
- }
-
- // 2. Rename media directory
- self::renameMediaDir();
-
- // 3. Update database (extensions, template_styles, menu assignments)
- self::updateDatabase();
-
- // 4. Notify admin
- self::notifyUser($app);
-
- self::log('Bridge migration completed successfully.');
- return true;
- }
-
- /**
- * Rename templates/mokocassiopeia → templates/mokoonyx
- */
- private static function renameTemplateDir(): bool
- {
- $oldDir = JPATH_ROOT . '/templates/' . self::OLD_NAME;
- $newDir = JPATH_ROOT . '/templates/' . self::NEW_NAME;
-
- if (!is_dir($oldDir)) {
- self::log('Bridge: old template dir not found: ' . $oldDir, 'warning');
- return false;
- }
-
- if (is_dir($newDir)) {
- self::log('Bridge: new template dir already exists — skipping rename');
- return true;
- }
-
- $result = @rename($oldDir, $newDir);
- if ($result) {
- self::log('Bridge: renamed template dir to ' . self::NEW_NAME);
- } else {
- self::log('Bridge: failed to rename template dir', 'error');
- }
-
- return $result;
- }
-
- /**
- * Rename media/templates/site/mokocassiopeia → mokoonyx
- */
- private static function renameMediaDir(): void
- {
- $oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
- $newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
-
- if (!is_dir($oldMedia)) {
- self::log('Bridge: old media dir not found — skipping');
- return;
- }
-
- if (is_dir($newMedia)) {
- self::log('Bridge: new media dir already exists — skipping rename');
- return;
- }
-
- if (@rename($oldMedia, $newMedia)) {
- self::log('Bridge: renamed media dir to ' . self::NEW_NAME);
- } else {
- self::log('Bridge: failed to rename media dir', 'warning');
- }
- }
-
- /**
- * Update all database references from mokocassiopeia → mokoonyx.
- */
- private static function updateDatabase(): void
- {
- $db = Factory::getDbo();
-
- // 1. Update #__extensions — change element and name
- $query = $db->getQuery(true)
- ->update('#__extensions')
- ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
- ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME))
- ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
- ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
- try {
- $db->setQuery($query)->execute();
- $affected = $db->getAffectedRows();
- if ($affected > 0) {
- self::log("Bridge: updated {$affected} row(s) in #__extensions");
- }
- } catch (\Throwable $e) {
- self::log('Bridge: #__extensions update failed: ' . $e->getMessage(), 'error');
- }
-
- // 2. Update #__template_styles — rename template and title
- $query = $db->getQuery(true)
- ->select('*')
- ->from('#__template_styles')
- ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME));
- $styles = $db->setQuery($query)->loadObjectList();
-
- foreach ($styles as $style) {
- $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title);
- // Also catch lowercase variant
- $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
-
- $newParams = $style->params;
- if (is_string($newParams)) {
- $newParams = str_replace(self::OLD_NAME, self::NEW_NAME, $newParams);
- }
-
- $update = $db->getQuery(true)
- ->update('#__template_styles')
- ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
- ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
- ->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
- ->where('id = ' . (int) $style->id);
-
- try {
- $db->setQuery($update)->execute();
- } catch (\Throwable $e) {
- self::log('Bridge: style update failed for id=' . $style->id . ': ' . $e->getMessage(), 'error');
- }
- }
-
- if (!empty($styles)) {
- self::log('Bridge: updated ' . count($styles) . ' template style(s) in #__template_styles');
- }
-
- // 3. Update #__menu — fix template_style_id link field references
- // Menu items store the template name in the link for template-specific assignments
- try {
- $query = $db->getQuery(true)
- ->update('#__menu')
- ->set($db->quoteName('link') . ' = REPLACE(' . $db->quoteName('link') . ', '
- . $db->quote(self::OLD_NAME) . ', ' . $db->quote(self::NEW_NAME) . ')')
- ->where($db->quoteName('link') . ' LIKE ' . $db->quote('%' . self::OLD_NAME . '%'));
- $db->setQuery($query)->execute();
- $affected = $db->getAffectedRows();
- if ($affected > 0) {
- self::log("Bridge: updated {$affected} menu link(s)");
- }
- } catch (\Throwable $e) {
- self::log('Bridge: #__menu update failed: ' . $e->getMessage(), 'warning');
- }
-
- // 4. Update #__update_sites — point to MokoOnyx updates.xml
- try {
- $newLocation = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
- $query = $db->getQuery(true)
- ->update('#__update_sites')
- ->set($db->quoteName('location') . ' = ' . $db->quote($newLocation))
- ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY))
- ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
- $db->setQuery($query)->execute();
- $affected = $db->getAffectedRows();
- if ($affected > 0) {
- self::log("Bridge: updated {$affected} update site(s) to MokoOnyx");
- }
- } catch (\Throwable $e) {
- self::log('Bridge: #__update_sites update failed: ' . $e->getMessage(), 'warning');
- }
-
- // 5. Update #__updates — clear cached updates for old extension
- try {
- $query = $db->getQuery(true)
- ->delete('#__updates')
- ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME));
- $db->setQuery($query)->execute();
- $affected = $db->getAffectedRows();
- if ($affected > 0) {
- self::log("Bridge: cleared {$affected} cached update(s) for old extension");
- }
- } catch (\Throwable $e) {
- self::log('Bridge: #__updates cleanup failed: ' . $e->getMessage(), 'warning');
- }
- }
-
- private static function notifyUser($app): void
- {
- $app->enqueueMessage(
- 'MokoCassiopeia has been renamed to MokoOnyx.
'
- . 'Your template files, settings, and menu assignments have been migrated automatically. '
- . 'MokoOnyx is now your active site template.',
- 'success'
- );
- }
-
- private static function log(string $message, string $priority = 'info'): void
- {
- $priorities = [
- 'info' => Log::INFO,
- 'warning' => Log::WARNING,
- 'error' => Log::ERROR,
- ];
-
- Log::addLogger(
- ['text_file' => 'mokocassiopeia_bridge.log.php'],
- Log::ALL,
- ['mokocassiopeia_bridge']
- );
-
- Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
- }
-}
diff --git a/src/script.php b/src/script.php
index 92e2333..b379812 100644
--- a/src/script.php
+++ b/src/script.php
@@ -1,6 +1,6 @@
+ * Copyright (C) 2026 Moko Consulting
*
* This file is part of a Moko Consulting project.
*
@@ -8,12 +8,11 @@
*/
/**
- * Template install/update/uninstall script.
- * Joomla calls the methods in this class automatically during template
- * install, update, and uninstall via the element in
- * templateDetails.xml.
- * Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
- * available, falls back to the legacy class-based approach otherwise.
+ * MokoCassiopeia install/update/uninstall script.
+ *
+ * On update: renames the template to MokoOnyx (dirs + database),
+ * copies all style params, creates matching styles, copies user files,
+ * and redirects the update server. No external downloads needed.
*/
defined('_JEXEC') or die;
@@ -24,33 +23,23 @@ use Joomla\CMS\Log\Log;
class Tpl_MokocassiopeiaInstallerScript
{
- /**
- * Minimum PHP version required by this template.
- */
- private const MIN_PHP = '8.1.0';
-
- /**
- * Minimum Joomla version required by this template.
- */
+ private const MIN_PHP = '8.1.0';
private const MIN_JOOMLA = '4.4.0';
- /**
- * Called before install/update/uninstall.
- *
- * @param string $type install, update, discover_install, or uninstall.
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return bool True to proceed, false to abort.
- */
+ private const OLD_NAME = 'mokocassiopeia';
+ private const NEW_NAME = 'mokoonyx';
+ private const OLD_DISPLAY = 'MokoCassiopeia';
+ private const NEW_DISPLAY = 'MokoOnyx';
+
+ private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
+
+ // ── Joomla lifecycle methods ───────────────────────────────────────
+
public function preflight(string $type, InstallerAdapter $parent): bool
{
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage(
- sprintf(
- 'MokoCassiopeia requires PHP %s or later. You are running PHP %s.',
- self::MIN_PHP,
- PHP_VERSION
- ),
+ sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION),
'error'
);
return false;
@@ -58,11 +47,7 @@ class Tpl_MokocassiopeiaInstallerScript
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage(
- sprintf(
- 'MokoCassiopeia requires Joomla %s or later. You are running Joomla %s.',
- self::MIN_JOOMLA,
- JVERSION
- ),
+ sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION),
'error'
);
return false;
@@ -71,167 +56,231 @@ class Tpl_MokocassiopeiaInstallerScript
return true;
}
- /**
- * Called after a successful install.
- *
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return bool
- */
public function install(InstallerAdapter $parent): bool
{
- $this->logMessage('MokoCassiopeia template installed.');
+ $this->log('MokoCassiopeia installed.');
return true;
}
- /**
- * Called after a successful update.
- *
- * This is where the CSS variable sync runs — it detects variables that
- * were added in the new version and injects them into the user's custom
- * palette files without overwriting existing values.
- *
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return bool
- */
public function update(InstallerAdapter $parent): bool
{
- $this->logMessage('MokoCassiopeia update() called — version ' . ($parent->getManifest()->version ?? 'unknown'));
-
- // Run CSS variable sync to inject any new variables into user's custom palettes.
- $synced = $this->syncCustomVariables($parent);
-
- if ($synced > 0) {
- Factory::getApplication()->enqueueMessage(
- sprintf(
- 'MokoCassiopeia: %d new CSS variable(s) were added to your custom palette files. '
- . 'Review them in your light.custom.css and/or dark.custom.css to customise the new defaults.',
- $synced
- ),
- 'notice'
- );
- }
-
- // Bridge migration runs in postflight() — not here — to avoid double execution
+ $this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?'));
return true;
}
- /**
- * Called after a successful uninstall.
- *
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return bool
- */
public function uninstall(InstallerAdapter $parent): bool
{
- $this->logMessage('MokoCassiopeia template uninstalled.');
+ $this->log('MokoCassiopeia uninstalled.');
return true;
}
- /**
- * Called after install/update completes (regardless of type).
- *
- * @param string $type install, update, or discover_install.
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return bool
- */
public function postflight(string $type, InstallerAdapter $parent): bool
{
- // Bridge migration runs in postflight (more reliable than update() for templates)
if ($type === 'update') {
- $bridgeScript = $parent->getParent()->getPath('source') . '/helper/bridge.php';
- if (!is_file($bridgeScript)) {
- $bridgeScript = __DIR__ . '/helper/bridge.php';
- }
- if (is_file($bridgeScript)) {
- require_once $bridgeScript;
- if (class_exists('MokoBridgeMigration')) {
- $this->logMessage('Running MokoOnyx bridge migration from postflight...');
- MokoBridgeMigration::run();
- }
- }
+ $this->log('=== MokoCassiopeia → MokoOnyx bridge ===');
+ $this->bridge();
}
return true;
}
- /**
- * Run the CSS variable sync utility.
- *
- * Loads sync_custom_vars.php from the template directory and calls
- * MokoCssVarSync::run() to detect and inject missing variables.
- *
- * @param InstallerAdapter $parent The adapter calling this method.
- *
- * @return int Number of variables added across all files.
- */
- private function syncCustomVariables(InstallerAdapter $parent): int
+ // ── Bridge: rename-in-place + DB migration ─────────────────────────
+
+ private function bridge(): void
{
- $templateDir = $parent->getParent()->getPath('source');
+ $app = Factory::getApplication();
- // The sync script lives alongside this script in the template root.
- $syncScript = $templateDir . '/sync_custom_vars.php';
-
- if (!is_file($syncScript)) {
- $this->logMessage('CSS variable sync script not found at: ' . $syncScript, 'warning');
- return 0;
- }
-
- require_once $syncScript;
-
- if (!class_exists('MokoCssVarSync')) {
- $this->logMessage('MokoCssVarSync class not found after loading script.', 'warning');
- return 0;
- }
-
- try {
- $joomlaRoot = JPATH_ROOT;
- $results = MokoCssVarSync::run($joomlaRoot);
-
- $totalAdded = 0;
- foreach ($results as $filePath => $result) {
- $totalAdded += count($result['added']);
- if (!empty($result['added'])) {
- $this->logMessage(
- sprintf(
- 'CSS sync: added %d variable(s) to %s',
- count($result['added']),
- basename($filePath)
- )
- );
- }
- }
-
- return $totalAdded;
- } catch (\Throwable $e) {
- $this->logMessage('CSS variable sync failed: ' . $e->getMessage(), 'error');
- return 0;
- }
- }
-
- /**
- * Log a message to Joomla's log system.
- *
- * @param string $message The log message.
- * @param string $priority Log priority (info, warning, error).
- */
- private function logMessage(string $message, string $priority = 'info'): void
- {
- $priorities = [
- 'info' => Log::INFO,
- 'warning' => Log::WARNING,
- 'error' => Log::ERROR,
- ];
-
- Log::addLogger(
- ['text_file' => 'mokocassiopeia.log.php'],
- Log::ALL,
- ['mokocassiopeia']
+ // 1. Rename template directory
+ $templateRenamed = $this->renameDir(
+ JPATH_ROOT . '/templates/' . self::OLD_NAME,
+ JPATH_ROOT . '/templates/' . self::NEW_NAME,
+ 'template'
);
- Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia');
+ if (!$templateRenamed && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
+ $app->enqueueMessage(
+ 'Could not rename template directory to MokoOnyx. '
+ . 'Please rename templates/mokocassiopeia to templates/mokoonyx manually.',
+ 'warning'
+ );
+ return;
+ }
+
+ // 2. Rename media directory
+ $this->renameDir(
+ JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME,
+ JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME,
+ 'media'
+ );
+
+ // 3. Update #__extensions
+ $this->updateExtensions();
+
+ // 4. Migrate template styles (create matching MokoOnyx styles with same params)
+ $this->migrateStyles();
+
+ // 5. Copy user files (custom themes, user.css, user.js)
+ $this->copyUserFiles();
+
+ // 6. Redirect update server to MokoOnyx
+ $this->updateUpdateServer();
+
+ // 7. Notify
+ $app->enqueueMessage(
+ 'MokoCassiopeia has been renamed to MokoOnyx.
'
+ . 'All template settings, styles, and custom files have been migrated. '
+ . 'MokoOnyx is now your active site template.',
+ 'success'
+ );
+
+ $this->log('=== Bridge completed ===');
+ }
+
+ // ── Bridge helpers ─────────────────────────────────────────────────
+
+ private function renameDir(string $old, string $new, string $label): bool
+ {
+ if (!is_dir($old)) {
+ $this->log("Bridge: {$label} dir not found ({$old}) — skipping.");
+ return false;
+ }
+
+ if (is_dir($new)) {
+ $this->log("Bridge: {$label} dir already exists ({$new}) — skipping rename.");
+ return true;
+ }
+
+ if (@rename($old, $new)) {
+ $this->log("Bridge: renamed {$label} dir → " . self::NEW_NAME);
+ return true;
+ }
+
+ $this->log("Bridge: failed to rename {$label} dir.", 'error');
+ return false;
+ }
+
+ private function updateExtensions(): void
+ {
+ $db = Factory::getDbo();
+
+ try {
+ $query = $db->getQuery(true)
+ ->update('#__extensions')
+ ->set($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
+ ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_NAME))
+ ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
+ $db->setQuery($query)->execute();
+
+ $n = $db->getAffectedRows();
+ if ($n > 0) {
+ $this->log("Bridge: updated {$n} row(s) in #__extensions.");
+ }
+ } catch (\Throwable $e) {
+ $this->log('Bridge: #__extensions failed: ' . $e->getMessage(), 'error');
+ }
+ }
+
+ private function migrateStyles(): void
+ {
+ $db = Factory::getDbo();
+
+ // Get all MokoCassiopeia styles (may already be renamed to mokoonyx by updateExtensions)
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from('#__template_styles')
+ ->where('(' . $db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME)
+ . ' OR ' . $db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME) . ')')
+ ->where($db->quoteName('client_id') . ' = 0');
+ $styles = $db->setQuery($query)->loadObjectList();
+
+ if (empty($styles)) {
+ $this->log('Bridge: no styles found to migrate.');
+ return;
+ }
+
+ foreach ($styles as $style) {
+ $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $style->title);
+ $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
+ $newParams = is_string($style->params)
+ ? str_replace(self::OLD_NAME, self::NEW_NAME, $style->params)
+ : $style->params;
+
+ $update = $db->getQuery(true)
+ ->update('#__template_styles')
+ ->set($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
+ ->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
+ ->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
+ ->where('id = ' . (int) $style->id);
+
+ try {
+ $db->setQuery($update)->execute();
+ } catch (\Throwable $e) {
+ $this->log('Bridge: style update failed (id=' . $style->id . '): ' . $e->getMessage(), 'warning');
+ }
+ }
+
+ $this->log('Bridge: migrated ' . count($styles) . ' style(s).');
+ }
+
+ private function copyUserFiles(): void
+ {
+ $media = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
+ if (!is_dir($media)) {
+ return;
+ }
+
+ // User files are already in the renamed media dir — nothing to copy.
+ // They were moved with the rename. Log for clarity.
+ $this->log('Bridge: user files preserved via directory rename.');
+ }
+
+ private function updateUpdateServer(): void
+ {
+ $db = Factory::getDbo();
+
+ try {
+ $query = $db->getQuery(true)
+ ->update('#__update_sites')
+ ->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL))
+ ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY))
+ ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
+ $db->setQuery($query)->execute();
+
+ $n = $db->getAffectedRows();
+ if ($n > 0) {
+ $this->log("Bridge: redirected {$n} update site(s) to MokoOnyx.");
+ }
+ } catch (\Throwable $e) {
+ $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
+ }
+
+ // Clear cached updates for old element
+ try {
+ $query = $db->getQuery(true)
+ ->delete('#__updates')
+ ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME));
+ $db->setQuery($query)->execute();
+ } catch (\Throwable $e) {
+ // Not critical
+ }
+ }
+
+ // ── Logging ────────────────────────────────────────────────────────
+
+ private function log(string $message, string $priority = 'info'): void
+ {
+ static $init = false;
+ if (!$init) {
+ Log::addLogger(
+ ['text_file' => 'mokocassiopeia_bridge.log.php'],
+ Log::ALL,
+ ['mokocassiopeia_bridge']
+ );
+ $init = true;
+ }
+
+ $levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR];
+ Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
}
}
diff --git a/updates.xml b/updates.xml
index b4cc69c..54d1610 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -17,9 +17,8 @@
2026-04-21
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development
- https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.01-dev.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.10.17-dev.zip
- 089563017322317f989c49f8260d6f84cf2b84235cad4584504b716b9c429e83
development
Moko Consulting
https://mokoconsulting.tech
@@ -38,9 +37,8 @@
2026-04-19
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha
- https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/mokoonyx-01.00.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.10.13.zip
- 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4
alpha
Moko Consulting
https://mokoconsulting.tech
@@ -59,9 +57,8 @@
2026-04-19
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta
- https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/mokoonyx-01.00.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.10.13.zip
- 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4
beta
Moko Consulting
https://mokoconsulting.tech
@@ -80,9 +77,8 @@
2026-04-19
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate
- https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.10.13.zip
- 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4
rc
Moko Consulting
https://mokoconsulting.tech
@@ -101,9 +97,8 @@
2026-04-19
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03
- https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.13.zip
- 954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4
stable
Moko Consulting
https://mokoconsulting.tech