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:
+158
-192
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user