diff --git a/.gitea/.moko-platform b/.gitea/.moko-platform
deleted file mode 100644
index 388e9f8..0000000
--- a/.gitea/.moko-platform
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- Template-Joomla
- MokoConsulting
- Unified Joomla extension scaffolding templates — plugin, template, module, component, package, library
- GNU General Public License v3
-
-
- template
- 04.07.00
- https://git.mokoconsulting.tech/MokoConsulting/moko-platform
- 2026-05-10T19:51:10+00:00
-
-
- Markdown
- template
- src/
-
-
diff --git a/.gitea/workflows/pr-branch-check.yml b/.gitea/workflows/pr-branch-check.yml
deleted file mode 100644
index 183a291..0000000
--- a/.gitea/workflows/pr-branch-check.yml
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# Enforces branch merge policy:
-# feature/* → dev only
-# fix/* → dev only
-# hotfix/* → dev or main (emergency)
-# dev → main only
-# alpha/* → dev only
-# beta/* → dev only
-# rc/* → main only
-
-name: "Universal: Branch Policy Check"
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-jobs:
- check-target:
- name: Verify merge target
- runs-on: ubuntu-latest
- steps:
- - name: Check branch policy
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- alpha/*|beta/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Pre-release branches must target 'dev', not '${BASE}'"
- fi
- ;;
- rc/*)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Release candidate branches must target 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo ""
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitea/workflows/pr-check.yml b/.gitea/workflows/pr-check.yml
deleted file mode 100644
index dc22a56..0000000
--- a/.gitea/workflows/pr-check.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.CI
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/pr-check.yml
-# VERSION: 01.00.00
-# BRIEF: PR gate — validates code quality and manifest before merge to main
-
-name: "Universal: PR Check"
-
-on:
- pull_request:
- branches:
- - main
- types: [opened, synchronize, reopened]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup PHP
- run: |
- if ! command -v php &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- run: |
- echo "=== PHP Lint ==="
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "Checked files, errors: ${ERRORS}"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate Joomla manifest
- run: |
- echo "=== Manifest Validation ==="
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
-
- # Check well-formed XML
- if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
- echo "::error::Manifest XML is malformed"
- exit 1
- fi
-
- # Check required elements
- for ELEMENT in name version description; do
- if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then
- echo "::error::Missing <${ELEMENT}> in manifest"
- exit 1
- fi
- done
- echo "Manifest valid"
-
- - name: Check updates.xml format
- run: |
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
- echo "=== updates.xml Validation ==="
- if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
- echo "::error::updates.xml is malformed"
- exit 1
- fi
- echo "updates.xml valid"
-
- - name: Verify package builds
- run: |
- echo "=== Package Build Test ==="
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- # Dry-run: ensure zip would succeed
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source contains ${FILE_COUNT} files — package will build"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
diff --git a/.gitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
similarity index 100%
rename from .gitea/workflows/auto-release.yml
rename to .mokogitea/workflows/auto-release.yml
diff --git a/.gitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
similarity index 100%
rename from .gitea/workflows/cascade-dev.yml
rename to .mokogitea/workflows/cascade-dev.yml
diff --git a/.gitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml
similarity index 100%
rename from .gitea/workflows/ci-joomla.yml
rename to .mokogitea/workflows/ci-joomla.yml
diff --git a/.gitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml
similarity index 100%
rename from .gitea/workflows/cleanup.yml
rename to .mokogitea/workflows/cleanup.yml
diff --git a/.gitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml
similarity index 100%
rename from .gitea/workflows/deploy-manual.yml
rename to .mokogitea/workflows/deploy-manual.yml
diff --git a/.gitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml
similarity index 100%
rename from .gitea/workflows/gitleaks.yml
rename to .mokogitea/workflows/gitleaks.yml
diff --git a/.gitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml
similarity index 100%
rename from .gitea/workflows/notify.yml
rename to .mokogitea/workflows/notify.yml
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
new file mode 100644
index 0000000..88e1884
--- /dev/null
+++ b/.mokogitea/workflows/pr-check.yml
@@ -0,0 +1,194 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: MokoStandards.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
+# PATH: /templates/workflows/universal/pr-check.yml.template
+# VERSION: 05.00.00
+# BRIEF: PR gate — branch policy + code validation before merge
+
+name: "Universal: PR Check"
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ # ── Branch Policy ──────────────────────────────────────────────────────
+ branch-policy:
+ name: Branch Policy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check branch merge target
+ run: |
+ HEAD="${{ github.head_ref }}"
+ BASE="${{ github.base_ref }}"
+
+ echo "PR: ${HEAD} → ${BASE}"
+
+ ALLOWED=true
+ REASON=""
+
+ case "$HEAD" in
+ feature/*|feat/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Feature branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ fix/*|bugfix/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Fix branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ hotfix/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
+ fi
+ ;;
+ alpha/*|beta/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Pre-release branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ rc/*)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Release candidate branches must target 'main', not '${BASE}'"
+ fi
+ ;;
+ dev)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ esac
+
+ if [ "$ALLOWED" = false ]; then
+ echo "::error::${REASON}"
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+
+ # ── Code Validation ────────────────────────────────────────────────────
+ validate:
+ name: Validate PR
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Detect platform
+ id: platform
+ run: |
+ PLATFORM=$(cat .moko-platform 2>/dev/null | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+
+ - name: Setup PHP
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ if ! command -v php &> /dev/null; then
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
+ fi
+
+ - name: PHP syntax check
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ ERRORS=0
+ while IFS= read -r -d '' file; do
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
+ ERRORS=$((ERRORS + 1))
+ fi
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
+ echo "PHP lint: ${ERRORS} error(s)"
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
+
+ - name: Validate platform manifest
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
+ if [ -z "$MANIFEST" ]; then
+ echo "::warning::No Joomla manifest found (WaaS site)"
+ exit 0
+ fi
+ echo "Manifest: ${MANIFEST}"
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
+ fi
+ for ELEMENT in name version description; do
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
+ done
+ echo "Joomla manifest valid"
+ ;;
+ dolibarr)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
+ if [ -z "$MOD_FILE" ]; then
+ echo "::error::No mod*.class.php found"
+ exit 1
+ fi
+ echo "Dolibarr module: ${MOD_FILE}"
+ ;;
+ *)
+ echo "Generic platform — no manifest validation"
+ ;;
+ esac
+
+ - name: Check update stream format
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ if [ -f "updates.xml" ]; then
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
+ fi
+ echo "updates.xml valid"
+ fi
+ ;;
+ dolibarr)
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
+ ;;
+ esac
+
+ - name: Verify package source
+ run: |
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::warning::No src/ or htdocs/ directory"
+ exit 0
+ fi
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
+ echo "Source: ${FILE_COUNT} files"
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
diff --git a/.gitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
similarity index 100%
rename from .gitea/workflows/pre-release.yml
rename to .mokogitea/workflows/pre-release.yml
diff --git a/.gitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
similarity index 100%
rename from .gitea/workflows/repo-health.yml
rename to .mokogitea/workflows/repo-health.yml
diff --git a/.gitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml
similarity index 100%
rename from .gitea/workflows/security-audit.yml
rename to .mokogitea/workflows/security-audit.yml
diff --git a/.gitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
similarity index 100%
rename from .gitea/workflows/update-server.yml
rename to .mokogitea/workflows/update-server.yml