464ebb1a25
New validator for client theme packages (Joomla type="file"): - Manifest: required elements, type="file", method="upgrade", version format - Required files: light.custom.css, dark.custom.css - PHP syntax check on script.php - CSS brace balance + BOM detection - Version consistency (manifest vs updates.xml vs CHANGELOG) - Image size warnings (>1MB) Also update auto_detect_platform.php to recognise type="file" manifests as client repos alongside legacy sftp-config detection. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
941 lines
34 KiB
PHP
Executable File
941 lines
34 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Scripts.Validate
|
|
* INGROUP: MokoStandards
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /validate/auto_detect_platform.php
|
|
* BRIEF: Automatic platform detection and validation - PHP implementation
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\{
|
|
CLIApp,
|
|
ProjectTypeDetector,
|
|
PluginFactory,
|
|
PluginRegistry,
|
|
AuditLogger,
|
|
MetricsCollector
|
|
};
|
|
|
|
/**
|
|
* Automatic Platform Detection and Validation
|
|
*
|
|
* Detects whether a repository is a Joomla/WaaS component, Dolibarr/CRM module,
|
|
* or generic repository, then validates against appropriate schema
|
|
*/
|
|
class AutoDetectPlatform extends CLIApp
|
|
{
|
|
private const DETECTION_THRESHOLD = 0.5; // 50% confidence required
|
|
|
|
private ProjectTypeDetector $typeDetector;
|
|
private PluginFactory $pluginFactory;
|
|
|
|
private array $detectionResults = [
|
|
'client' => ['score' => 0, 'indicators' => []],
|
|
'joomla' => ['score' => 0, 'indicators' => []],
|
|
'dolibarr' => ['score' => 0, 'indicators' => []],
|
|
'nodejs' => ['score' => 0, 'indicators' => []],
|
|
'python' => ['score' => 0, 'indicators' => []],
|
|
'terraform' => ['score' => 0, 'indicators' => []],
|
|
'wordpress' => ['score' => 0, 'indicators' => []],
|
|
'mobile' => ['score' => 0, 'indicators' => []],
|
|
'api' => ['score' => 0, 'indicators' => []],
|
|
'mcp-server' => ['score' => 0, 'indicators' => []],
|
|
'documentation' => ['score' => 0, 'indicators' => []],
|
|
'generic' => ['score' => 0, 'indicators' => []],
|
|
];
|
|
|
|
private string $detectedPlatform = 'generic';
|
|
private string $schemaFile = '';
|
|
private ?object $detectedPlugin = null;
|
|
|
|
protected function setupArguments(): array
|
|
{
|
|
return [
|
|
'repo-path:' => 'Path to repository to analyze (default: current directory)',
|
|
'schema-dir:' => 'Path to schema definitions directory (default: definitions/default)',
|
|
'output-dir:' => 'Directory for output reports (default: var/logs/validation)',
|
|
];
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$repoPath = $this->getOption('repo-path', '.');
|
|
$schemaDir = $this->getOption('schema-dir', 'definitions/default');
|
|
$outputDir = $this->getOption('output-dir', 'var/logs/validation');
|
|
|
|
// Make paths absolute
|
|
$repoPath = $this->getAbsolutePath($repoPath);
|
|
$schemaDir = $this->getAbsolutePath($schemaDir);
|
|
$outputDir = $this->getAbsolutePath($outputDir);
|
|
|
|
if (!is_dir($repoPath)) {
|
|
$this->log("Repository path not found: {$repoPath}", 'ERROR');
|
|
return 3;
|
|
}
|
|
|
|
if (!is_dir($schemaDir)) {
|
|
$this->log("Schema directory not found: {$schemaDir}", 'ERROR');
|
|
return 3;
|
|
}
|
|
|
|
$this->log("Analyzing repository: {$repoPath}", 'INFO');
|
|
|
|
// Initialize plugin system
|
|
$logger = new AuditLogger('auto_detect_platform');
|
|
$metrics = new MetricsCollector();
|
|
$this->pluginFactory = new PluginFactory($logger, $metrics);
|
|
$this->typeDetector = new ProjectTypeDetector($logger);
|
|
|
|
// Use the new plugin system for detection
|
|
$this->log("Using ProjectTypeDetector for platform detection", 'INFO');
|
|
$detectionResult = $this->typeDetector->detectProjectType($repoPath);
|
|
|
|
if (!empty($detectionResult['type'])) {
|
|
$this->detectedPlatform = $detectionResult['type'];
|
|
$this->log("Detected platform via plugin system: {$this->detectedPlatform}", 'INFO');
|
|
|
|
// Try to get the plugin for this type
|
|
$this->detectedPlugin = $this->pluginFactory->createForProject($repoPath);
|
|
|
|
if ($this->detectedPlugin) {
|
|
$this->log("Loaded plugin: {$this->detectedPlugin->getPluginName()}", 'INFO');
|
|
|
|
// Update detection results with plugin info
|
|
$this->detectionResults[$this->detectedPlatform] = [
|
|
'score' => $detectionResult['confidence'] ?? 1.0,
|
|
'indicators' => $detectionResult['indicators'] ?? [],
|
|
];
|
|
}
|
|
} else {
|
|
// Fallback to legacy detection if plugin system doesn't detect anything
|
|
$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);
|
|
$this->detectPython($repoPath);
|
|
$this->detectTerraform($repoPath);
|
|
$this->detectWordPress($repoPath);
|
|
$this->detectMobile($repoPath);
|
|
$this->detectAPI($repoPath);
|
|
$this->detectMcpServer($repoPath);
|
|
|
|
// Determine platform
|
|
$this->determinePlatform();
|
|
}
|
|
|
|
// Map to schema file
|
|
$this->schemaFile = $this->mapPlatformToSchema($schemaDir);
|
|
|
|
if (!file_exists($this->schemaFile)) {
|
|
$this->log("Schema file not found: {$this->schemaFile}", 'ERROR');
|
|
return 3;
|
|
}
|
|
|
|
// Output results
|
|
if ($this->jsonOutput) {
|
|
$this->outputJson();
|
|
} else {
|
|
$this->displayResults();
|
|
}
|
|
|
|
// Generate reports
|
|
$this->generateReports($outputDir, $repoPath);
|
|
|
|
$this->log("Platform detection completed: {$this->detectedPlatform}", 'INFO');
|
|
$this->log("Schema file: {$this->schemaFile}", 'INFO');
|
|
|
|
if ($this->detectedPlugin) {
|
|
$this->log("Plugin available for validation and health checks", 'INFO');
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Detect client site repository.
|
|
* Client repos have either:
|
|
* (a) src/ with Joomla site structure + deployment configs (legacy)
|
|
* (b) src/templateDetails.xml with type="file" (theme package)
|
|
* They are NOT Joomla extensions (component/module/plugin/template).
|
|
*/
|
|
private function detectClient(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Strong indicator: type="file" manifest (client theme package)
|
|
$manifests = glob($repoPath . '/src/*.xml') ?: [];
|
|
$isFilePackage = false;
|
|
foreach ($manifests as $xml) {
|
|
$content = @file_get_contents($xml);
|
|
if ($content && preg_match('/<extension\s+[^>]*type="file"/', $content)) {
|
|
$score += 60;
|
|
$indicators[] = 'Found Joomla type="file" manifest (theme package)';
|
|
$isFilePackage = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Theme package files
|
|
$themeMarkers = [
|
|
'src/media/templates/site/mokoonyx/css/theme/light.custom.css' => 15,
|
|
'src/media/templates/site/mokoonyx/css/theme/dark.custom.css' => 15,
|
|
'src/script.php' => 10,
|
|
'updates.xml' => 10,
|
|
];
|
|
foreach ($themeMarkers as $path => $weight) {
|
|
$full = $repoPath . '/' . $path;
|
|
if (is_file($full)) {
|
|
$score += $weight;
|
|
$indicators[] = "Found: {$path} (+{$weight})";
|
|
}
|
|
}
|
|
|
|
// Legacy 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})";
|
|
}
|
|
}
|
|
|
|
// Legacy: site structure inside src/
|
|
$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 extension manifest (not type="file"), it's an extension
|
|
if (!$isFilePackage) {
|
|
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;
|
|
$indicators = [];
|
|
|
|
// Look for Joomla manifest files
|
|
$manifests = $this->findFiles($repoPath, '*.xml', 3);
|
|
foreach ($manifests as $manifest) {
|
|
$content = @file_get_contents($manifest);
|
|
if ($content && (
|
|
strpos($content, '<extension') !== false ||
|
|
strpos($content, '<install') !== false
|
|
)) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found Joomla manifest: " . basename($manifest);
|
|
}
|
|
}
|
|
|
|
// Check for Joomla directory structure
|
|
$joomlaDirs = ['site', 'admin', 'administrator', 'language', 'media'];
|
|
foreach ($joomlaDirs as $dir) {
|
|
if (is_dir("{$repoPath}/{$dir}")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found Joomla directory: {$dir}/";
|
|
}
|
|
}
|
|
|
|
// Check for index.html files (Joomla security pattern)
|
|
$indexCount = count($this->findFiles($repoPath, 'index.html', 2));
|
|
if ($indexCount > 2) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found {$indexCount} index.html files (Joomla pattern)";
|
|
}
|
|
|
|
$this->detectionResults['joomla'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectDolibarr(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Look for Dolibarr module descriptor
|
|
$descriptors = $this->findFiles($repoPath, 'mod*.class.php', 3);
|
|
foreach ($descriptors as $descriptor) {
|
|
$content = @file_get_contents($descriptor);
|
|
if ($content && strpos($content, 'DolibarrModules') !== false) {
|
|
$score += 0.4;
|
|
$indicators[] = "Found Dolibarr module descriptor: " . basename($descriptor);
|
|
}
|
|
}
|
|
|
|
// Check for Dolibarr-specific code patterns
|
|
$phpFiles = $this->findFiles($repoPath, '*.php', 3);
|
|
$dolibarrPatterns = ['dol_include_once', '$this->numero', 'DoliDB', 'Translate'];
|
|
|
|
foreach ($phpFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if (!$content) continue;
|
|
|
|
foreach ($dolibarrPatterns as $pattern) {
|
|
if (strpos($content, $pattern) !== false) {
|
|
$score += 0.05;
|
|
$indicators[] = "Found Dolibarr pattern '{$pattern}' in " . basename($file);
|
|
break; // Only count once per file
|
|
}
|
|
}
|
|
|
|
if ($score >= 0.8) break; // Stop early if confident
|
|
}
|
|
|
|
// Check for Dolibarr directory structure
|
|
$dolibarrDirs = ['core/modules', 'sql', 'class', 'lib', 'langs'];
|
|
foreach ($dolibarrDirs as $dir) {
|
|
if (is_dir("{$repoPath}/{$dir}")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found Dolibarr directory: {$dir}/";
|
|
}
|
|
}
|
|
|
|
// Check for SQL files in sql/ directory
|
|
if (is_dir("{$repoPath}/sql")) {
|
|
$sqlFiles = $this->findFiles("{$repoPath}/sql", '*.sql', 1);
|
|
if (count($sqlFiles) > 0) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found " . count($sqlFiles) . " SQL files in sql/";
|
|
}
|
|
}
|
|
|
|
$this->detectionResults['dolibarr'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectNodeJS(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for package.json
|
|
if (file_exists("{$repoPath}/package.json")) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found package.json";
|
|
|
|
$content = @file_get_contents("{$repoPath}/package.json");
|
|
if ($content) {
|
|
if (strpos($content, '"typescript"') !== false || strpos($content, '"@types/') !== false) {
|
|
$score += 0.1;
|
|
$indicators[] = "TypeScript dependencies detected";
|
|
}
|
|
if (strpos($content, '"react"') !== false || strpos($content, '"vue"') !== false ||
|
|
strpos($content, '"angular"') !== false || strpos($content, '"express"') !== false) {
|
|
$score += 0.1;
|
|
$indicators[] = "Node.js framework detected";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for node_modules and lock files
|
|
if (is_dir("{$repoPath}/node_modules")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found node_modules directory";
|
|
}
|
|
|
|
if (file_exists("{$repoPath}/package-lock.json") || file_exists("{$repoPath}/yarn.lock") ||
|
|
file_exists("{$repoPath}/pnpm-lock.yaml") || file_exists("{$repoPath}/bun.lockb")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found package lock file";
|
|
}
|
|
|
|
// Check for TypeScript config
|
|
if (file_exists("{$repoPath}/tsconfig.json")) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found tsconfig.json";
|
|
}
|
|
|
|
$this->detectionResults['nodejs'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectPython(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for Python package files
|
|
if (file_exists("{$repoPath}/setup.py") || file_exists("{$repoPath}/pyproject.toml")) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found Python package configuration";
|
|
}
|
|
|
|
if (file_exists("{$repoPath}/requirements.txt")) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found requirements.txt";
|
|
}
|
|
|
|
if (file_exists("{$repoPath}/Pipfile") || file_exists("{$repoPath}/poetry.lock")) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found Python dependency manager config";
|
|
}
|
|
|
|
// Check for Python files
|
|
$pyFiles = $this->findFiles($repoPath, '*.py', 2);
|
|
if (count($pyFiles) > 0) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found " . count($pyFiles) . " Python files";
|
|
}
|
|
|
|
// Check for virtual environment directories
|
|
$venvDirs = ['venv', '.venv', 'env', '.env'];
|
|
foreach ($venvDirs as $dir) {
|
|
if (is_dir("{$repoPath}/{$dir}")) {
|
|
$score += 0.05;
|
|
$indicators[] = "Found virtual environment: {$dir}/";
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->detectionResults['python'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectTerraform(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for Terraform files
|
|
$tfFiles = $this->findFiles($repoPath, '*.tf', 3);
|
|
if (count($tfFiles) > 0) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found " . count($tfFiles) . " Terraform files";
|
|
}
|
|
|
|
// Check for terraform.tfvars or *.tfvars
|
|
$tfvarsFiles = $this->findFiles($repoPath, '*.tfvars', 2);
|
|
if (count($tfvarsFiles) > 0) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found Terraform variables files";
|
|
}
|
|
|
|
// Check for .terraform directory
|
|
if (is_dir("{$repoPath}/.terraform")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found .terraform directory";
|
|
}
|
|
|
|
// Check for terraform.lock.hcl
|
|
if (file_exists("{$repoPath}/.terraform.lock.hcl")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found Terraform lock file";
|
|
}
|
|
|
|
// Check for main.tf, variables.tf, outputs.tf (common pattern)
|
|
$commonFiles = ['main.tf', 'variables.tf', 'outputs.tf'];
|
|
$foundCommon = 0;
|
|
foreach ($commonFiles as $file) {
|
|
if (file_exists("{$repoPath}/{$file}")) {
|
|
$foundCommon++;
|
|
}
|
|
}
|
|
if ($foundCommon >= 2) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found standard Terraform structure";
|
|
}
|
|
|
|
$this->detectionResults['terraform'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectWordPress(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for plugin header
|
|
$phpFiles = $this->findFiles($repoPath, '*.php', 2);
|
|
foreach ($phpFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && (strpos($content, 'Plugin Name:') !== false ||
|
|
strpos($content, 'Theme Name:') !== false)) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found WordPress plugin/theme header in " . basename($file);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for WordPress functions
|
|
$wpFunctions = ['add_action', 'add_filter', 'wp_enqueue_script', 'register_activation_hook'];
|
|
foreach ($phpFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if (!$content) continue;
|
|
|
|
foreach ($wpFunctions as $func) {
|
|
if (strpos($content, $func) !== false) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found WordPress function '{$func}'";
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for WordPress directory structure
|
|
$wpDirs = ['includes', 'templates', 'assets'];
|
|
foreach ($wpDirs as $dir) {
|
|
if (is_dir("{$repoPath}/{$dir}")) {
|
|
$score += 0.05;
|
|
$indicators[] = "Found WordPress directory: {$dir}/";
|
|
}
|
|
}
|
|
|
|
$this->detectionResults['wordpress'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectMobile(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for React Native
|
|
if (file_exists("{$repoPath}/package.json")) {
|
|
$content = @file_get_contents("{$repoPath}/package.json");
|
|
if ($content && strpos($content, '"react-native"') !== false) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found React Native in package.json";
|
|
}
|
|
}
|
|
|
|
// Check for Flutter
|
|
if (file_exists("{$repoPath}/pubspec.yaml")) {
|
|
$content = @file_get_contents("{$repoPath}/pubspec.yaml");
|
|
if ($content && strpos($content, 'flutter:') !== false) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found Flutter in pubspec.yaml";
|
|
}
|
|
}
|
|
|
|
// Check for iOS project
|
|
$xcodeFiles = $this->findFiles($repoPath, '*.xcodeproj', 2);
|
|
if (count($xcodeFiles) > 0) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found Xcode project";
|
|
}
|
|
|
|
// Check for Android project
|
|
if (file_exists("{$repoPath}/build.gradle") || file_exists("{$repoPath}/app/build.gradle")) {
|
|
$content = @file_get_contents("{$repoPath}/build.gradle") ?: @file_get_contents("{$repoPath}/app/build.gradle");
|
|
if ($content && strpos($content, 'com.android.application') !== false) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found Android application gradle";
|
|
}
|
|
}
|
|
|
|
// Check for mobile directories
|
|
$mobileDirs = ['ios', 'android', 'lib'];
|
|
$foundCount = 0;
|
|
foreach ($mobileDirs as $dir) {
|
|
if (is_dir("{$repoPath}/{$dir}")) {
|
|
$foundCount++;
|
|
}
|
|
}
|
|
if ($foundCount >= 2) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found mobile platform directories";
|
|
}
|
|
|
|
$this->detectionResults['mobile'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectAPI(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for API documentation files
|
|
$apiDocs = ['openapi.yaml', 'openapi.json', 'swagger.yaml', 'swagger.json', 'api.yaml'];
|
|
foreach ($apiDocs as $doc) {
|
|
if (file_exists("{$repoPath}/{$doc}")) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found API documentation: {$doc}";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for GraphQL schema
|
|
$graphqlFiles = $this->findFiles($repoPath, '*.graphql', 2);
|
|
if (count($graphqlFiles) > 0 || file_exists("{$repoPath}/schema.graphql")) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found GraphQL schema";
|
|
}
|
|
|
|
// Check for gRPC proto files
|
|
$protoFiles = $this->findFiles($repoPath, '*.proto', 2);
|
|
if (count($protoFiles) > 0) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found Protocol Buffer definitions";
|
|
}
|
|
|
|
// Check for Dockerfile (common in microservices)
|
|
if (file_exists("{$repoPath}/Dockerfile")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found Dockerfile";
|
|
}
|
|
|
|
// Check for docker-compose.yml
|
|
if (file_exists("{$repoPath}/docker-compose.yml") || file_exists("{$repoPath}/docker-compose.yaml")) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found docker-compose configuration";
|
|
}
|
|
|
|
// Check for API patterns in code
|
|
$apiFiles = array_merge(
|
|
$this->findFiles($repoPath, '*.js', 2),
|
|
$this->findFiles($repoPath, '*.ts', 2),
|
|
$this->findFiles($repoPath, '*.py', 2)
|
|
);
|
|
|
|
$apiPatterns = [
|
|
'@app.route' => 'Flask route',
|
|
'@api_view' => 'Django REST framework',
|
|
'express()' => 'Express.js',
|
|
'fastapi' => 'FastAPI',
|
|
'@Controller' => 'NestJS controller',
|
|
];
|
|
|
|
foreach ($apiFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if (!$content) continue;
|
|
|
|
foreach ($apiPatterns as $pattern => $name) {
|
|
if (stripos($content, $pattern) !== false) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found {$name} pattern";
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->detectionResults['api'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function detectMcpServer(string $repoPath): void
|
|
{
|
|
$score = 0;
|
|
$indicators = [];
|
|
|
|
// Check for MCP SDK in package.json
|
|
if (file_exists("{$repoPath}/package.json")) {
|
|
$content = @file_get_contents("{$repoPath}/package.json");
|
|
if ($content && strpos($content, '@modelcontextprotocol/sdk') !== false) {
|
|
$score += 0.5;
|
|
$indicators[] = "Found @modelcontextprotocol/sdk in package.json";
|
|
}
|
|
}
|
|
|
|
// Check for MCP server entry point with McpServer usage
|
|
if (file_exists("{$repoPath}/src/index.ts")) {
|
|
$content = @file_get_contents("{$repoPath}/src/index.ts");
|
|
if ($content) {
|
|
if (strpos($content, 'McpServer') !== false) {
|
|
$score += 0.3;
|
|
$indicators[] = "Found McpServer import in src/index.ts";
|
|
}
|
|
if (strpos($content, 'server.tool(') !== false) {
|
|
$score += 0.1;
|
|
$toolCount = substr_count($content, 'server.tool(');
|
|
$indicators[] = "Found {$toolCount} tool registrations in src/index.ts";
|
|
}
|
|
if (strpos($content, 'StdioServerTransport') !== false) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found StdioServerTransport (stdio MCP transport)";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for the standard 4-file MCP structure
|
|
$mcpFiles = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
|
$foundCount = 0;
|
|
foreach ($mcpFiles as $file) {
|
|
if (file_exists("{$repoPath}/{$file}")) {
|
|
$foundCount++;
|
|
}
|
|
}
|
|
if ($foundCount === 4) {
|
|
$score += 0.1;
|
|
$indicators[] = "Found standard MCP 4-file src/ structure";
|
|
}
|
|
|
|
// Check for setup wizard
|
|
if (file_exists("{$repoPath}/scripts/setup.mjs")) {
|
|
$score += 0.05;
|
|
$indicators[] = "Found interactive setup wizard";
|
|
}
|
|
|
|
// 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);
|
|
if ($content && stripos($content, 'mcp-server') !== false) {
|
|
$score += 0.2;
|
|
$indicators[] = "Found explicit mcp-server platform declaration";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->detectionResults['mcp-server'] = [
|
|
'score' => min(1.0, $score),
|
|
'indicators' => $indicators,
|
|
];
|
|
}
|
|
|
|
private function determinePlatform(): void
|
|
{
|
|
// Find platform with highest score above threshold
|
|
$maxScore = 0;
|
|
$selectedPlatform = 'generic';
|
|
|
|
foreach ($this->detectionResults as $platform => $data) {
|
|
if ($data['score'] >= self::DETECTION_THRESHOLD && $data['score'] > $maxScore) {
|
|
$maxScore = $data['score'];
|
|
$selectedPlatform = $platform;
|
|
}
|
|
}
|
|
|
|
$this->detectedPlatform = $selectedPlatform;
|
|
}
|
|
|
|
private function mapPlatformToSchema(string $schemaDir): string
|
|
{
|
|
$mapping = [
|
|
'joomla' => 'waas-component.tf',
|
|
'dolibarr' => 'crm-module.tf',
|
|
'nodejs' => 'nodejs-repository.tf',
|
|
'python' => 'python-repository.tf',
|
|
'terraform' => 'terraform-repository.tf',
|
|
'wordpress' => 'wordpress-repository.tf',
|
|
'mobile' => 'mobile-app-repository.tf',
|
|
'api' => 'api-repository.tf',
|
|
'mcp-server' => 'mcp-server.tf',
|
|
'documentation' => 'documentation-repository.tf',
|
|
'standards' => 'standards-repository.tf',
|
|
'generic' => 'default-repository.tf',
|
|
];
|
|
|
|
return $schemaDir . '/' . $mapping[$this->detectedPlatform];
|
|
}
|
|
|
|
private function displayResults(): void
|
|
{
|
|
echo "\n=== Platform Detection Results ===\n\n";
|
|
|
|
echo "Platform: {$this->detectedPlatform}\n";
|
|
echo "Schema: {$this->schemaFile}\n\n";
|
|
|
|
echo "Detection Scores:\n";
|
|
foreach ($this->detectionResults as $platform => $data) {
|
|
$percentage = round($data['score'] * 100, 1);
|
|
$status = ($data['score'] >= self::DETECTION_THRESHOLD) ? '✅' : '❌';
|
|
echo sprintf(" %s %s: %.1f%%\n", $status, ucfirst($platform), $percentage);
|
|
}
|
|
|
|
echo "\nDetection Indicators:\n";
|
|
$indicators = $this->detectionResults[$this->detectedPlatform]['indicators'];
|
|
if (empty($indicators)) {
|
|
echo " No specific indicators found (generic repository)\n";
|
|
} else {
|
|
foreach ($indicators as $indicator) {
|
|
echo " • {$indicator}\n";
|
|
}
|
|
}
|
|
|
|
echo "\n";
|
|
}
|
|
|
|
private function outputJson(): void
|
|
{
|
|
$output = [
|
|
'platform' => $this->detectedPlatform,
|
|
'schema' => $this->schemaFile,
|
|
'detection_results' => $this->detectionResults,
|
|
'threshold' => self::DETECTION_THRESHOLD,
|
|
'timestamp' => date('c'),
|
|
'plugin_available' => $this->detectedPlugin !== null,
|
|
];
|
|
|
|
if ($this->detectedPlugin) {
|
|
$output['plugin_info'] = [
|
|
'name' => $this->detectedPlugin->getPluginName(),
|
|
'version' => $this->detectedPlugin->getPluginVersion(),
|
|
'type' => $this->detectedPlugin->getProjectType(),
|
|
];
|
|
}
|
|
|
|
echo json_encode($output, JSON_PRETTY_PRINT) . PHP_EOL;
|
|
}
|
|
|
|
private function generateReports(string $outputDir, string $repoPath): void
|
|
{
|
|
// Ensure output directory exists
|
|
if (!is_dir($outputDir)) {
|
|
@mkdir($outputDir, 0755, true);
|
|
}
|
|
|
|
$timestamp = date('Ymd_His');
|
|
|
|
// Generate detection report
|
|
$detectionReport = $outputDir . "/detection_report_{$timestamp}.md";
|
|
$this->writeDetectionReport($detectionReport, $repoPath);
|
|
|
|
// Generate summary report
|
|
$summaryReport = $outputDir . "/SUMMARY_{$timestamp}.md";
|
|
$this->writeSummaryReport($summaryReport, $repoPath);
|
|
|
|
$this->log("Reports generated in: {$outputDir}", 'INFO');
|
|
}
|
|
|
|
private function writeDetectionReport(string $file, string $repoPath): void
|
|
{
|
|
$content = "# Platform Detection Report\n\n";
|
|
$content .= "**Generated**: " . date('Y-m-d H:i:s') . "\n";
|
|
$content .= "**Repository**: {$repoPath}\n\n";
|
|
|
|
$content .= "## Detected Platform\n\n";
|
|
$content .= "**Type**: " . strtoupper($this->detectedPlatform) . "\n";
|
|
$content .= "**Confidence**: " . round($this->detectionResults[$this->detectedPlatform]['score'] * 100, 1) . "%\n";
|
|
$content .= "**Schema**: {$this->schemaFile}\n\n";
|
|
|
|
$content .= "## Detection Indicators\n\n";
|
|
foreach ($this->detectionResults[$this->detectedPlatform]['indicators'] as $indicator) {
|
|
$content .= "- {$indicator}\n";
|
|
}
|
|
|
|
$content .= "\n## All Platform Scores\n\n";
|
|
foreach ($this->detectionResults as $platform => $data) {
|
|
$percentage = round($data['score'] * 100, 1);
|
|
$content .= "- **" . ucfirst($platform) . "**: {$percentage}%\n";
|
|
}
|
|
|
|
@file_put_contents($file, $content);
|
|
}
|
|
|
|
private function writeSummaryReport(string $file, string $repoPath): void
|
|
{
|
|
$content = "# Platform Detection Summary\n\n";
|
|
$content .= "| Property | Value |\n";
|
|
$content .= "|----------|-------|\n";
|
|
$content .= "| Repository | {$repoPath} |\n";
|
|
$content .= "| Platform | " . strtoupper($this->detectedPlatform) . " |\n";
|
|
$content .= "| Confidence | " . round($this->detectionResults[$this->detectedPlatform]['score'] * 100, 1) . "% |\n";
|
|
$content .= "| Schema | " . basename($this->schemaFile) . " |\n";
|
|
$content .= "| Timestamp | " . date('Y-m-d H:i:s') . " |\n\n";
|
|
|
|
$content .= "## Next Steps\n\n";
|
|
$content .= "1. Review detection indicators\n";
|
|
$content .= "2. Validate repository against schema: {$this->schemaFile}\n";
|
|
$content .= "3. Address any validation errors or warnings\n";
|
|
|
|
@file_put_contents($file, $content);
|
|
}
|
|
|
|
private function findFiles(string $dir, string $pattern, int $maxDepth = 1): array
|
|
{
|
|
$files = [];
|
|
$pattern = str_replace('*', '.*', $pattern);
|
|
$pattern = str_replace('.', '\.', $pattern);
|
|
|
|
try {
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
$iterator->setMaxDepth($maxDepth);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile() && preg_match("/{$pattern}$/", $file->getFilename())) {
|
|
$files[] = $file->getPathname();
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Directory not accessible
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
private function getAbsolutePath(string $path): string
|
|
{
|
|
if (strlen($path) > 0 && $path[0] === '/') {
|
|
return $path;
|
|
}
|
|
|
|
return getcwd() . '/' . $path;
|
|
}
|
|
}
|
|
|
|
// Run the application
|
|
$app = new AutoDetectPlatform('auto_detect_platform', 'Automatically detect platform type and validate repository');
|
|
exit($app->execute());
|