feat: add client platform type with detection and structure definition

Client repos (WaaS) are managed Joomla sites — not extensions. They have
src/ with site structure PLUS deployment configs (sftp-config, monitoring,
sync scripts). Detection prioritizes client over joomla when deployment
markers are present.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-09 17:40:20 -05:00
parent 0e82802f0c
commit 395921282e
2 changed files with 223 additions and 194 deletions
+158 -192
View File
@@ -1,6 +1,10 @@
/**
* Client Joomla Site Structure Definition
* Standard repository structure for client Joomla site projects
* Client Repository Structure Definition
* Standard repository structure for managed Joomla client sites (WaaS)
*
* This is NOT a Joomla extension — it's a full managed client site with
* deployment configs, monitoring, SFTP settings, and sync workflows.
* The src/ directory mirrors the Joomla site's public_html.
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
@@ -11,231 +15,193 @@
locals {
repository_structure = {
metadata = {
name = "Client Joomla Site"
description = "Standard repository structure for client Joomla site projects (overrides, media, configuration)"
name = "Client Site"
description = "Managed Joomla client site — full site structure, not an extension"
repository_type = "client"
platform = "mokowaas"
last_updated = "2026-05-04T00:00:00Z"
platform = "client"
last_updated = "2026-05-09T00:00:00Z"
maintainer = "Moko Consulting"
version = "01.00.00"
schema_version = "1.0"
template_repo = "MokoConsulting/MokoStandards-Template-Client"
}
detection_hints = [
"scripts/sftp-config/",
"scripts/sync-dev-to-live.sh",
"monitoring/grafana/",
"src/administrator/",
"src/components/",
"src/plugins/",
"src/templates/",
"src/media/templates/site/mokoonyx/"
]
root_files = [
{
name = "README.md"
description = "Client project documentation"
extension = "md"
description = "Client site overview and deployment info"
required = true
always_overwrite = false
protected = true
},
{
name = "CHANGELOG.md"
extension = "md"
description = "Release history"
required = true
always_overwrite = false
},
{
name = "LICENSE"
description = "License file (GPL-3.0-or-later)"
required = true
},
{
name = "CHANGELOG.md"
description = "Version history and changes"
required = true
},
{
name = "SECURITY.md"
description = "Security policy and vulnerability reporting"
required = true
always_overwrite = true
},
{
name = "CODE_OF_CONDUCT.md"
description = "Community code of conduct"
required = true
always_overwrite = true
},
{
name = "CONTRIBUTING.md"
description = "Contribution guidelines"
extension = ""
description = "GPL-3.0-or-later license file"
required = true
always_overwrite = true
template = "templates/docs/required/LICENSE"
},
{
name = "Makefile"
description = "Build automation"
extension = ""
description = "Build and deployment targets (includes minify)"
required = true
always_overwrite = true
always_overwrite = false
},
{
name = "composer.json"
description = "PHP dependency management"
extension = "json"
description = "PHP dependencies"
required = true
always_overwrite = false
},
{
name = "phpstan.neon"
description = "PHPStan static analysis config"
required = true
always_overwrite = true
},
{
name = "codeception.yml"
description = "Codeception test framework config"
required = false
always_overwrite = false
},
{
name = ".editorconfig"
description = "Editor configuration for consistent coding style"
required = true
always_overwrite = true
},
{
name = ".gitignore"
description = "Git ignore patterns for client site projects"
extension = ""
description = "Git ignore rules (must include *.min.css, *.min.js, TODO.md)"
required = true
always_overwrite = false
},
{
name = "renovate.json"
description = "Renovate dependency management configuration"
required = true
always_overwrite = false
},
}
]
// NOTE: Client sites do NOT have updates.xml — they are not installable extensions
subdirectories = [
directories = [
{
name = "src"
description = "Site source files — template overrides, custom code, and configuration"
required = true
files = []
},
{
name = "src/images"
description = "Site images — branding, headers, event photos, profiles"
required = true
files = []
},
{
name = "src/media"
description = "Media assets — uploaded files, documents"
required = true
files = []
},
{
name = "docs"
description = "Client-specific documentation — brand reference, deployment, migration plans"
required = true
files = []
},
{
name = "scripts"
description = "Deployment and maintenance scripts"
required = false
files = []
},
{
name = "tests"
description = "Acceptance and unit tests"
required = false
files = []
},
{
name = ".gitea/workflows"
description = "Gitea Actions CI/CD workflows (10 workflows — no update-server)"
required = true
files = [
{
name = "auto-release.yml"
description = "Stable release on PR merge to main"
required = true
always_overwrite = true
},
{
name = "pre-release.yml"
description = "Manual pre-release for dev/alpha/beta/rc channels"
required = true
always_overwrite = true
},
{
name = "ci-joomla.yml"
description = "PHP lint, PHPStan, coding standards"
required = true
always_overwrite = true
},
{
name = "pr-check.yml"
description = "PR gate — validates code quality before merge"
required = true
always_overwrite = true
},
{
name = "deploy-manual.yml"
description = "Manual SFTP deploy to selected environment"
required = true
always_overwrite = true
},
{
name = "repo-health.yml"
description = "Repository health checks"
required = true
always_overwrite = true
},
{
name = "security-audit.yml"
description = "Dependency vulnerability scanning"
required = true
always_overwrite = true
},
{
name = "notify.yml"
description = "ntfy push notifications on release success or failure"
required = true
always_overwrite = true
},
{
name = "cleanup.yml"
description = "Weekly merged branch + old run cleanup"
required = true
always_overwrite = true
},
{
name = "sync-media.yml"
description = "Bidirectional SFTP sync for images/, files/, media/ between dev and production"
required = true
always_overwrite = true
},
{
name = "cascade-dev.yml"
description = "Forward-merge main to all open branches (dev, rc/*, beta/*, alpha/*) on push to main"
required = true
always_overwrite = true
},
{
name = "gitleaks.yml"
description = "Secret scanning — detect leaked credentials, API keys, and tokens using Gitleaks"
required = true
always_overwrite = true
},
name = "src"
path = "src"
description = "Joomla site public_html mirror — deployed via SFTP"
required = true
purpose = "Contains the full Joomla site directory structure"
subdirectories = [
{ name = "administrator", path = "src/administrator", description = "Joomla admin", required = true },
{ name = "components", path = "src/components", description = "Frontend components", required = true },
{ name = "plugins", path = "src/plugins", description = "Plugins", required = true },
{ name = "modules", path = "src/modules", description = "Modules", required = true },
{ name = "templates", path = "src/templates", description = "Templates", required = true },
{ name = "media", path = "src/media", description = "Media assets", required = true },
{ name = "images", path = "src/images", description = "Site images", required = false },
{ name = "language", path = "src/language", description = "Language files", required = false },
{ name = "libraries", path = "src/libraries", description = "Libraries", required = false },
{ name = "layouts", path = "src/layouts", description = "Layouts", required = false }
]
},
]
// Per-repo variables required for sync-media.yml
required_variables = [
{ name = "DEV_SYNC_HOST", description = "Dev server hostname" },
{ name = "DEV_SYNC_PORT", description = "Dev SSH port (default 22)" },
{ name = "DEV_SYNC_USERNAME", description = "Dev server username" },
{ name = "DEV_SYNC_PATH", description = "Base path on dev server" },
{ name = "PROD_SYNC_HOST", description = "Production server hostname" },
{ name = "PROD_SYNC_PORT", description = "Production SSH port (default 22)" },
{ name = "PROD_SYNC_USERNAME", description = "Production server username" },
{ name = "PROD_SYNC_PATH", description = "Base path on production server" },
]
required_secrets = [
{ name = "DEV_SYNC_KEY", description = "SSH private key for dev server" },
{ name = "PROD_SYNC_KEY", description = "SSH private key for production server" },
{
name = "scripts"
path = "scripts"
description = "Deployment, sync, and monitoring scripts"
required = true
purpose = "Contains SFTP configs, sync scripts, and monitoring"
subdirectories = [
{
name = "sftp-config"
path = "scripts/sftp-config"
description = "SFTP connection configs (dev + live)"
required = true
files = [
{
name = "sftp-config.dev.json"
extension = "json"
description = "Dev server SFTP connection"
required = true
always_overwrite = false
},
{
name = "sftp-config.rs.json"
extension = "json"
description = "Live/release server SFTP connection"
required = true
always_overwrite = false
}
]
}
]
},
{
name = "monitoring"
path = "monitoring"
description = "Grafana dashboard templates"
required = true
purpose = "Contains Panopticon-style Grafana dashboard JSON"
subdirectories = [
{
name = "grafana"
path = "monitoring/grafana"
description = "Grafana dashboard JSON templates"
required = true
files = [
{
name = "client-joomla-dashboard.json"
extension = "json"
description = "Panopticon-style Grafana dashboard template"
required = true
always_overwrite = true
template = "templates/monitoring/client-joomla-dashboard.json"
}
]
}
]
},
{
name = ".gitea"
path = ".gitea"
description = "Gitea configuration"
required = true
purpose = "Contains Gitea Actions workflows"
subdirectories = [
{
name = "workflows"
path = ".gitea/workflows"
description = "Gitea Actions CI/CD workflows"
required = true
files = [
{
name = "auto-release.yml"
extension = "yml"
description = "Auto-release on merge to main"
required = true
always_overwrite = true
},
{
name = "deploy.yml"
extension = "yml"
description = "Deploy src/ to servers via SFTP"
required = true
always_overwrite = true
},
{
name = "add-endpoint.yml"
extension = "yml"
description = "Add monitoring endpoint to sites.json"
required = true
always_overwrite = true
}
]
}
]
}
]
}
}
output "client_structure" {
description = "Client site repository structure definition"
value = local.repository_structure
}
+65 -2
View File
@@ -44,6 +44,7 @@ class AutoDetectPlatform extends CLIApp
private PluginFactory $pluginFactory;
private array $detectionResults = [
'client' => ['score' => 0, 'indicators' => []],
'joomla' => ['score' => 0, 'indicators' => []],
'dolibarr' => ['score' => 0, 'indicators' => []],
'nodejs' => ['score' => 0, 'indicators' => []],
@@ -124,6 +125,9 @@ class AutoDetectPlatform extends CLIApp
$this->log("Plugin system did not detect type, using legacy detection", 'WARNING');
// Run platform detection using legacy methods
// Client must run BEFORE Joomla — client repos contain Joomla dirs
// but are NOT Joomla extensions
$this->detectClient($repoPath);
$this->detectJoomla($repoPath);
$this->detectDolibarr($repoPath);
$this->detectNodeJS($repoPath);
@@ -166,6 +170,65 @@ class AutoDetectPlatform extends CLIApp
return 0;
}
/**
* Detect client site repository.
* Client repos have src/ with Joomla site structure PLUS deployment
* configs (sftp-config/, monitoring/). They are NOT Joomla extensions.
*/
private function detectClient(string $repoPath): void
{
$score = 0;
$indicators = [];
// Strong indicators: deployment/monitoring configs
$clientMarkers = [
'scripts/sftp-config' => 30,
'scripts/sftp-config/sftp-config.dev.json' => 10,
'scripts/sftp-config/sftp-config.rs.json' => 10,
'monitoring/grafana' => 20,
'scripts/sync-dev-to-live.sh' => 15,
'scripts/joomla-monitor.sh' => 10,
'scripts/joomla-monitor.sites.conf' => 10,
];
foreach ($clientMarkers as $path => $weight) {
$full = $repoPath . '/' . $path;
if (is_dir($full) || is_file($full)) {
$score += $weight;
$indicators[] = "Found: {$path} (+{$weight})";
}
}
// Site structure inside src/ (not at root — that would be a Joomla extension)
$siteDirs = ['src/administrator', 'src/components', 'src/plugins', 'src/templates', 'src/media'];
$siteDirCount = 0;
foreach ($siteDirs as $dir) {
if (is_dir($repoPath . '/' . $dir)) {
$siteDirCount++;
}
}
if ($siteDirCount >= 3) {
$score += 20;
$indicators[] = "Joomla site structure in src/ ({$siteDirCount}/5 dirs)";
}
// Negative: if there's a Joomla manifest XML in src/, it's an extension not a client
$manifests = glob($repoPath . '/src/*.xml');
foreach ($manifests ?: [] as $xml) {
$content = @file_get_contents($xml);
if ($content && preg_match('/<extension\s+type="(component|module|plugin|template|package)"/', $content)) {
$score -= 50;
$indicators[] = "Has Joomla extension manifest — likely extension, not client";
break;
}
}
$this->detectionResults['client'] = [
'score' => max(0, $score),
'indicators' => $indicators,
];
}
private function detectJoomla(string $repoPath): void
{
$score = 0;
@@ -639,8 +702,8 @@ class AutoDetectPlatform extends CLIApp
$indicators[] = "Found interactive setup wizard";
}
// Check for .mokostandards platform declaration
$mokoFiles = ["{$repoPath}/.gitea/.mokostandards", "{$repoPath}/.github/.mokostandards"];
// Check for .moko-platform platform declaration
$mokoFiles = ["{$repoPath}/.gitea/.moko-platform", "{$repoPath}/.github/.moko-platform"];
foreach ($mokoFiles as $mokoFile) {
if (file_exists($mokoFile)) {
$content = @file_get_contents($mokoFile);