#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoCLI.CLI * INGROUP: MokoCLI * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * PATH: /bin/validate-module * VERSION: 04.01.00 * BRIEF: Validates a Dolibarr module structure — checks descriptor, version, module ID, required files * * USAGE * vendor/bin/validate-module --path . (from a governed Dolibarr repo) * php bin/validate-module --path /path/to/repo (standalone) */ declare(strict_types=1); // Find autoloader — works from vendor/bin/ symlink or direct execution $autoloaders = [ __DIR__ . '/../vendor/autoload.php', // direct: bin/validate-module __DIR__ . '/../autoload.php', // symlink: vendor/bin/validate-module → ../../bin/validate-module __DIR__ . '/../../autoload.php', // alternative vendor layout ]; foreach ($autoloaders as $autoloader) { if (file_exists($autoloader)) { require_once $autoloader; break; } } // Resolve --path argument $path = '.'; foreach ($argv as $i => $arg) { if ($arg === '--path' && isset($argv[$i + 1])) { $path = $argv[$i + 1]; } } $path = realpath($path) ?: $path; $errors = []; $warnings = []; $passed = []; // ── 1. Check src/ directory exists ────────────────────────────────────── if (!is_dir("{$path}/src")) { $errors[] = "Missing src/ directory"; } else { $passed[] = "src/ directory exists"; } // ── 2. Check module descriptor exists ─────────────────────────────────── $modFiles = glob("{$path}/src/core/modules/mod*.class.php") ?: []; if (empty($modFiles)) { $modFiles = glob("{$path}/core/modules/mod*.class.php") ?: []; } if (empty($modFiles)) { $errors[] = "No module descriptor found (src/core/modules/mod*.class.php)"; } else { $modFile = $modFiles[0]; $modContent = file_get_contents($modFile); $passed[] = "Module descriptor: " . basename($modFile); // ── 3. Check module number (numero) ───────────────────────────────── if (preg_match('/\$this->numero\s*=\s*(\d+)/', $modContent, $m)) { $numero = (int) $m[1]; if ($numero === 0) { $errors[] = "Module number (\$this->numero) is 0 — request one via issue template"; } else { $passed[] = "Module number: {$numero}"; } } else { $errors[] = "Module number (\$this->numero) not found in descriptor"; } // ── 4. Check version field ────────────────────────────────────────── if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $m)) { $version = $m[1]; $passed[] = "Module version: {$version}"; if ($version === 'development') { $warnings[] = "Module version is 'development' — auto-release will set the real version on merge to main"; } } else { $errors[] = "Module version (\$this->version) not found in descriptor"; } // ── 5. Check url_last_version ──────────────────────────────────────── if (preg_match('/\$this->url_last_version\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $m)) { $url = $m[1]; $passed[] = "Update URL: {$url}"; // Detect current branch $branch = trim((string) @shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null')); if ($branch === 'main' || $branch === 'master') { if (!str_contains($url, '/main/update.txt')) { $errors[] = "On main: url_last_version must point to /main/update.txt"; } else { $passed[] = "Update URL correctly points to main"; } } elseif (!empty($branch) && $branch !== 'HEAD') { if (str_contains($url, '/main/')) { $warnings[] = "url_last_version points to main (current branch: {$branch}) — auto-release handles this on merge"; } } } else { $warnings[] = "url_last_version not set — see docs/update-server.md"; } } // ── 5. Check README.md exists and has VERSION ─────────────────────────── $readme = null; foreach (['README.md', 'src/README.md'] as $candidate) { if (file_exists("{$path}/{$candidate}")) { $readme = $candidate; break; } } if ($readme === null) { $warnings[] = "No README.md found"; } else { $readmeContent = file_get_contents("{$path}/{$readme}"); if (preg_match('/VERSION:\s*(\d{2}\.\d{2}\.\d{2})/', $readmeContent, $m)) { $passed[] = "README version: {$m[1]}"; } else { $warnings[] = "README.md missing VERSION in FILE INFORMATION block"; } } // ── 6. Check CHANGELOG exists ─────────────────────────────────────────── $hasChangelog = file_exists("{$path}/CHANGELOG.md") || file_exists("{$path}/src/ChangeLog.md"); if ($hasChangelog) { $passed[] = "Changelog present"; } else { $warnings[] = "No CHANGELOG.md or ChangeLog.md found"; } // ── 7. Check LICENSE exists ───────────────────────────────────────────── if (file_exists("{$path}/LICENSE")) { $passed[] = "LICENSE present"; } else { $errors[] = "Missing LICENSE file"; } // ── Output ────────────────────────────────────────────────────────────── echo "MokoCLI Module Validation" . PHP_EOL; echo str_repeat("─", 50) . PHP_EOL; echo PHP_EOL; foreach ($passed as $p) { echo " ✅ {$p}" . PHP_EOL; } foreach ($warnings as $w) { echo " ⚠️ {$w}" . PHP_EOL; } foreach ($errors as $e) { echo " ❌ {$e}" . PHP_EOL; } echo PHP_EOL; $total = count($passed) + count($warnings) + count($errors); echo "Results: {$total} checks — " . count($passed) . " passed, " . count($warnings) . " warnings, " . count($errors) . " errors" . PHP_EOL; exit(count($errors) > 0 ? 1 : 0);