Public Access
feat(core): add SourceResolver for backwards-compatible src/ → source/ migration
Introduces SourceResolver utility class with source/ → src/ → htdocs/ fallback chain, replacing hardcoded src/ references across 28 files. This enables renaming root-level src/ to source/ in all repos while maintaining backwards compatibility during the transition. Phase 1: New lib/Enterprise/SourceResolver.php with resolve(), resolveAbsolute(), globSource(), findUnderSource(), warnIfLegacy() Phase 2: Updated 19 CLI/deploy tools to use SourceResolver Phase 3: Updated 7 validator/lib files (McpServerPlugin, PackageBuilder, RepositorySynchronizer, auto_detect_platform, check_dolibarr_module, check_client_theme, check_structure) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
@@ -866,11 +866,11 @@ class DeployJoomla extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
// 3-5. Fallback chain
|
||||
foreach (['src', 'htdocs'] as $candidate) {
|
||||
if (is_dir("{$repoPath}/{$candidate}")) {
|
||||
return "{$repoPath}/{$candidate}";
|
||||
}
|
||||
// 3-5. Fallback chain (source/ → src/ → htdocs/)
|
||||
$resolved = SourceResolver::resolveAbsolute($repoPath);
|
||||
if ($resolved !== null) {
|
||||
SourceResolver::warnIfLegacy($repoPath);
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
// Last resort: repo root itself
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class JoomlaBuildCli extends CliFramework
|
||||
{
|
||||
@@ -49,17 +49,12 @@ class JoomlaBuildCli extends CliFramework
|
||||
$path = realpath($path) ?: $path;
|
||||
|
||||
// ── Find source directory ──────────────────────────────────────────────
|
||||
$srcDir = null;
|
||||
foreach (['src', 'htdocs'] as $d) {
|
||||
if (is_dir("{$path}/{$d}")) {
|
||||
$srcDir = "{$path}/{$d}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', "::error::No src/ or htdocs/ directory in {$path}");
|
||||
$this->log('ERROR', "::error::No source/ or src/ directory in {$path}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
// ── Find manifest ──────────────────────────────────────────────────────
|
||||
$manifest = $this->findManifest($srcDir);
|
||||
|
||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
|
||||
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
||||
|
||||
/**
|
||||
* Joomla Release Manager
|
||||
@@ -121,11 +121,12 @@ class JoomlaRelease extends CliFramework
|
||||
$this->log('INFO', "Version: {$displayVersion} | Release tag: {$releaseTag}");
|
||||
|
||||
// ── Step 3: Build packages ────────────────────────────────────
|
||||
$srcDir = is_dir("{$path}/src") ? "{$path}/src" : (is_dir("{$path}/htdocs") ? "{$path}/htdocs" : null);
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', 'No src/ or htdocs/ directory');
|
||||
$this->log('ERROR', 'No source/ or src/ directory');
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
$prefix = $this->typePrefix($meta);
|
||||
$zipName = "{$prefix}{$meta['element']}-{$displayVersion}.zip";
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ManifestElementCli extends CliFramework
|
||||
{
|
||||
@@ -48,7 +48,7 @@ class ManifestElementCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$extManifest = null;
|
||||
$manifestFiles = array_merge(glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.xml") ?: [], glob("{$root}/*.xml") ?: []);
|
||||
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
|
||||
foreach ($manifestFiles as $file) {
|
||||
$c = file_get_contents($file);
|
||||
if (strpos($c, '<extension') !== false) {
|
||||
@@ -58,8 +58,7 @@ class ManifestElementCli extends CliFramework
|
||||
}
|
||||
$modFile = null;
|
||||
$modFiles = array_merge(
|
||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||
);
|
||||
foreach ($modFiles as $file) {
|
||||
|
||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
|
||||
@@ -123,8 +123,8 @@ class ManifestLicensingCli extends CliFramework
|
||||
|
||||
// ── 4. Find Joomla extension manifests ────────────────────────────
|
||||
$xmlFiles = array_merge(
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class PackageBuildCli extends CliFramework
|
||||
{
|
||||
@@ -56,18 +56,13 @@ class PackageBuildCli extends CliFramework
|
||||
}
|
||||
|
||||
// -- Determine source directory -----------------------------------------------
|
||||
$sourceDir = null;
|
||||
foreach (['src', 'htdocs'] as $candidate) {
|
||||
if (is_dir("{$root}/{$candidate}")) {
|
||||
$sourceDir = "{$root}/{$candidate}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||
|
||||
if ($sourceDir === null) {
|
||||
$this->log('ERROR', "No src/ or htdocs/ directory found in {$root}");
|
||||
$this->log('ERROR', "No source/ or src/ directory found in {$root}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
|
||||
// -- Determine element and type prefix from manifest --------------------------
|
||||
$extElement = $elementOverride;
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleaseCreateCli extends CliFramework
|
||||
{
|
||||
@@ -97,8 +97,8 @@ class ReleaseCreateCli extends CliFramework
|
||||
// Find extension manifest (Joomla XML)
|
||||
$extManifest = null;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $file) {
|
||||
@@ -112,8 +112,7 @@ class ReleaseCreateCli extends CliFramework
|
||||
// Find Dolibarr module file
|
||||
$modFile = null;
|
||||
$modFiles = array_merge(
|
||||
glob("{$root}/src/core/modules/mod*.class.php") ?: [],
|
||||
glob("{$root}/htdocs/core/modules/mod*.class.php") ?: [],
|
||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||
);
|
||||
foreach ($modFiles as $file) {
|
||||
|
||||
+15
-15
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleasePackageCli extends CliFramework
|
||||
{
|
||||
@@ -99,9 +99,10 @@ class ReleasePackageCli extends CliFramework
|
||||
$extFolder = '';
|
||||
$typePrefix = '';
|
||||
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
@@ -200,14 +201,12 @@ class ReleasePackageCli extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
if ($sourceDir === null && is_dir("{$root}/src")) {
|
||||
$sourceDir = "{$root}/src";
|
||||
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
|
||||
$sourceDir = "{$root}/htdocs";
|
||||
if ($sourceDir === null) {
|
||||
$sourceDir = SourceResolver::resolveAbsolute($root);
|
||||
}
|
||||
|
||||
if ($sourceDir === null) {
|
||||
echo "No src/ or htdocs/ directory found — skipping package build\n";
|
||||
echo "No source/ or src/ directory found — skipping package build\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -231,19 +230,20 @@ class ReleasePackageCli extends CliFramework
|
||||
$subZipPath = "{$outputDir}/{$subName}.zip";
|
||||
|
||||
// If sub-package is a full repo checkout (e.g. git submodule),
|
||||
// look for a src/ subdirectory containing a Joomla manifest XML
|
||||
// look for a source/ or src/ subdirectory containing a Joomla manifest XML
|
||||
// and zip that instead of the repo root.
|
||||
$subSourceDir = $pkgDir;
|
||||
$srcCandidate = "{$pkgDir}/src";
|
||||
if (is_dir($srcCandidate)) {
|
||||
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
|
||||
if ($subSrcAbs !== null) {
|
||||
$srcManifests = array_merge(
|
||||
glob("{$srcCandidate}/*.xml") ?: [],
|
||||
glob("{$srcCandidate}/pkg_*.xml") ?: []
|
||||
glob("{$subSrcAbs}/*.xml") ?: [],
|
||||
glob("{$subSrcAbs}/pkg_*.xml") ?: []
|
||||
);
|
||||
foreach ($srcManifests as $mf) {
|
||||
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
|
||||
$subSourceDir = $srcCandidate;
|
||||
echo " Sub-package {$subName}: using src/ entry-point\n";
|
||||
$subSourceDir = $subSrcAbs;
|
||||
$subSrcName = SourceResolver::resolve($pkgDir);
|
||||
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleasePromoteCli extends CliFramework
|
||||
{
|
||||
@@ -109,8 +109,8 @@ class ReleasePromoteCli extends CliFramework
|
||||
if ($to === 'stable') {
|
||||
$root = realpath($path) ?: $path;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $xmlFile) {
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ReleaseValidateCli extends CliFramework
|
||||
{
|
||||
@@ -66,8 +66,10 @@ class ReleaseValidateCli extends CliFramework
|
||||
$platform = 'generic';
|
||||
}
|
||||
}
|
||||
$hasSource = is_dir("{$root}/src") || is_dir("{$root}/htdocs");
|
||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? 'src/ or htdocs/ found' : 'No src/ or htdocs/ directory');
|
||||
$hasSource = SourceResolver::resolveAbsolute($root) !== null;
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$srcDirName = SourceResolver::resolve($root);
|
||||
$this->addVResult('Source directory', $hasSource ? 'PASS' : 'WARN', $hasSource ? "{$srcDirName}/ found" : 'No source/ or src/ directory');
|
||||
if (!file_exists("{$root}/README.md")) {
|
||||
$this->addVResult('README.md', 'FAIL', 'Not found');
|
||||
} else {
|
||||
@@ -109,7 +111,8 @@ class ReleaseValidateCli extends CliFramework
|
||||
$this->addVResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found');
|
||||
if ($platform === 'joomla') {
|
||||
$manifest = null;
|
||||
foreach (["{$root}/src", $root] as $dir) {
|
||||
$srcAbs = SourceResolver::resolveAbsolute($root);
|
||||
foreach (array_filter([$srcAbs, $root]) as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
} foreach (glob("{$dir}/*.xml") as $xmlFile) {
|
||||
@@ -156,7 +159,7 @@ class ReleaseValidateCli extends CliFramework
|
||||
}
|
||||
} elseif ($platform === 'dolibarr') {
|
||||
$modFile = null;
|
||||
foreach (['src', 'htdocs'] as $sd) {
|
||||
foreach (SourceResolver::getCandidates() as $sd) {
|
||||
$matches = glob("{$root}/{$sd}/mod*.class.php");
|
||||
if (!empty($matches)) {
|
||||
$modFile = $matches[0];
|
||||
|
||||
+4
-9
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class ThemeLintCli extends CliFramework
|
||||
{
|
||||
@@ -41,17 +41,12 @@ class ThemeLintCli extends CliFramework
|
||||
$errors = 0;
|
||||
$warnings = 0;
|
||||
|
||||
$srcDir = null;
|
||||
foreach (['src', 'htdocs'] as $d) {
|
||||
if (is_dir("{$root}/{$d}")) {
|
||||
$srcDir = "{$root}/{$d}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$srcDir = SourceResolver::resolveAbsolute($root);
|
||||
if ($srcDir === null) {
|
||||
$this->log('ERROR', "No src/ or htdocs/ directory in {$root}");
|
||||
$this->log('ERROR', "No source/ or src/ directory in {$root}");
|
||||
return 1;
|
||||
}
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
|
||||
echo "Theme Lint: {$srcDir}\n\n";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class UpdatesXmlBuildCli extends CliFramework
|
||||
{
|
||||
@@ -109,7 +109,7 @@ class UpdatesXmlBuildCli extends CliFramework
|
||||
// -- Locate Joomla manifest ---------------------------------------------------
|
||||
$manifest = null;
|
||||
|
||||
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
||||
$candidates = SourceResolver::globSource($root, 'pkg_*.xml');
|
||||
foreach ($candidates as $f) {
|
||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||
$manifest = $f;
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionBumpCli extends CliFramework
|
||||
{
|
||||
@@ -61,11 +61,12 @@ class VersionBumpCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$manifestVersion = null;
|
||||
SourceResolver::warnIfLegacy($root);
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/mokowaas.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/mokowaas.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
foreach ($manifestFiles as $xmlFile) {
|
||||
@@ -141,7 +142,8 @@ class VersionBumpCli extends CliFramework
|
||||
}
|
||||
}
|
||||
$updatedFiles = [];
|
||||
foreach (["{$root}/src/pkg_*.xml", "{$root}/src/*.xml", "{$root}/src/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
foreach (["{$root}/{$srcName}/pkg_*.xml", "{$root}/{$srcName}/*.xml", "{$root}/{$srcName}/packages/*/*.xml", "{$root}/*.xml"] as $pattern) {
|
||||
foreach (glob($pattern) ?: [] as $xmlFile) {
|
||||
$content = file_get_contents($xmlFile);
|
||||
if (strpos($content, '<extension') === false) {
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionBumpRemoteCli extends CliFramework
|
||||
{
|
||||
@@ -104,11 +104,15 @@ class VersionBumpRemoteCli extends CliFramework
|
||||
$nextVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch);
|
||||
echo "{$version} -> {$nextVersion} ({$branch})\n";
|
||||
|
||||
// Try both source/ and src/ paths for backwards compatibility with remote repos
|
||||
$manifestPaths = [];
|
||||
if ($manifestFile !== null) {
|
||||
$manifestPaths[] = "src/{$manifestFile}";
|
||||
foreach (['source', 'src'] as $srcPrefix) {
|
||||
if ($manifestFile !== null) {
|
||||
$manifestPaths[] = "{$srcPrefix}/{$manifestFile}";
|
||||
}
|
||||
$manifestPaths[] = "{$srcPrefix}/templateDetails.xml";
|
||||
$manifestPaths[] = "{$srcPrefix}/manifest.xml";
|
||||
}
|
||||
$manifestPaths = array_merge($manifestPaths, ['src/templateDetails.xml', 'src/manifest.xml']);
|
||||
$manifestUpdated = false;
|
||||
foreach ($manifestPaths as $mPath) {
|
||||
$result = $this->updateRemoteFile($apiBase, $token, $mPath, $branch, function (string $content) use ($version, $nextVersion): string {
|
||||
|
||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionCheckCli extends CliFramework
|
||||
{
|
||||
@@ -77,7 +77,8 @@ class VersionCheckCli extends CliFramework
|
||||
$versions['pyproject.toml'] = $m[1];
|
||||
}
|
||||
}
|
||||
foreach (["{$root}/src/pkg_*.xml", "{$root}/src/*.xml", "{$root}/src/packages/*/*.xml", "{$root}/*.xml"] as $glob) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
foreach (["{$root}/{$srcName}/pkg_*.xml", "{$root}/{$srcName}/*.xml", "{$root}/{$srcName}/packages/*/*.xml", "{$root}/*.xml"] as $glob) {
|
||||
foreach (glob($glob) ?: [] as $file) {
|
||||
if (basename($file) === 'updates.xml') {
|
||||
continue;
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionReadCli extends CliFramework
|
||||
{
|
||||
@@ -64,9 +64,9 @@ class VersionReadCli extends CliFramework
|
||||
// -- 3. Fallback: Joomla manifest XML --
|
||||
$manifestVersion = null;
|
||||
$manifestFiles = array_merge(
|
||||
glob("{$root}/src/pkg_*.xml") ?: [],
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||
SourceResolver::globSource($root, '*.xml'),
|
||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
class VersionSetPlatformCli extends CliFramework
|
||||
{
|
||||
@@ -110,7 +110,8 @@ class VersionSetPlatformCli extends CliFramework
|
||||
|
||||
// Dolibarr: $this->version + $this->url_last_version in mod*.class.php
|
||||
if ($platform === 'crm-module') {
|
||||
$pattern = "{$root}/src/core/modules/mod*.class.php";
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
$pattern = "{$root}/{$srcName}/core/modules/mod*.class.php";
|
||||
foreach (glob($pattern) ?: [] as $file) {
|
||||
$content = file_get_contents($file);
|
||||
|
||||
@@ -146,9 +147,10 @@ class VersionSetPlatformCli extends CliFramework
|
||||
|
||||
// Joomla: <version> in XML manifests (top-level + sub-packages)
|
||||
if (in_array($platform, ['waas-component', 'joomla'], true)) {
|
||||
$srcName = SourceResolver::resolve($root);
|
||||
$xmlFiles = array_merge(
|
||||
glob("{$root}/src/*.xml") ?: [],
|
||||
glob("{$root}/src/packages/*/*.xml") ?: [],
|
||||
glob("{$root}/{$srcName}/*.xml") ?: [],
|
||||
glob("{$root}/{$srcName}/packages/*/*.xml") ?: [],
|
||||
glob("{$root}/*.xml") ?: []
|
||||
);
|
||||
if (empty($xmlFiles)) {
|
||||
|
||||
@@ -21,7 +21,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
@@ -51,9 +51,9 @@ class DeploySftp extends CliFramework
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Deploy a repository src/ directory to a remote web server via SFTP');
|
||||
$this->setDescription('Deploy a repository source directory to a remote web server via SFTP');
|
||||
$this->addArgument('--path', 'Repository root (default: current directory)', '.');
|
||||
$this->addArgument('--src-dir', 'Source sub-directory to upload (default: src)', 'src');
|
||||
$this->addArgument('--src-dir', 'Source sub-directory to upload (default: auto-detect)', '');
|
||||
$this->addArgument('--env', 'Target environment: dev or rs', '');
|
||||
$this->addArgument('--config', 'Explicit config file path — overrides --env', '');
|
||||
$this->addArgument('--key-passphrase', 'Passphrase for the SSH private key', '');
|
||||
@@ -158,7 +158,8 @@ class DeploySftp extends CliFramework
|
||||
*/
|
||||
private function resolveSrcDir(string $repoPath): string
|
||||
{
|
||||
$sub = $this->getArgument('--src-dir', 'src');
|
||||
$sub = $this->getArgument('--src-dir', '') ?: SourceResolver::resolve($repoPath);
|
||||
SourceResolver::warnIfLegacy($repoPath);
|
||||
$dir = $repoPath . DIRECTORY_SEPARATOR . $sub;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
|
||||
@@ -147,31 +147,29 @@ class ManifestReader
|
||||
/**
|
||||
* Get the source/entry-point directory.
|
||||
*
|
||||
* Fallback chain: manifest entry-point → source/ → src/ → htdocs/ → 'source'.
|
||||
* Uses SourceResolver for the directory fallback when no entry-point is set.
|
||||
*
|
||||
* @param string $root Repository root for existence checking
|
||||
* @return string Resolved source directory path (e.g. 'src', 'htdocs')
|
||||
* @return string Resolved source directory path (e.g. 'source', 'src', 'htdocs')
|
||||
*/
|
||||
public function getSourceDir(string $root = ''): string
|
||||
{
|
||||
$entryPoint = $this->get('entry-point', '');
|
||||
if ($entryPoint !== '') {
|
||||
// Strip trailing filename (e.g. src/index.ts → src)
|
||||
// Strip trailing filename (e.g. source/index.ts → source)
|
||||
$dir = rtrim(dirname($entryPoint) === '.' ? $entryPoint : dirname($entryPoint), '/');
|
||||
if ($root === '' || is_dir("{$root}/{$dir}")) {
|
||||
return $dir;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check common directories
|
||||
// Fallback: use SourceResolver (source/ → src/ → htdocs/ → default 'source')
|
||||
if ($root !== '') {
|
||||
if (is_dir("{$root}/src")) {
|
||||
return 'src';
|
||||
}
|
||||
if (is_dir("{$root}/htdocs")) {
|
||||
return 'htdocs';
|
||||
}
|
||||
return SourceResolver::resolve($root);
|
||||
}
|
||||
|
||||
return 'src';
|
||||
return 'source';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,8 @@ class PackageBuilder
|
||||
mkdir($packageDir, 0755, true);
|
||||
mkdir($distDir, 0755, true);
|
||||
|
||||
foreach (['src', 'admin', 'site'] as $dir) {
|
||||
$srcName = SourceResolver::resolve($repoRoot);
|
||||
foreach ([$srcName, 'admin', 'site'] as $dir) {
|
||||
if (is_dir($repoRoot . '/' . $dir)) {
|
||||
self::copyDirectory($repoRoot . '/' . $dir, $packageDir . '/' . $dir);
|
||||
}
|
||||
@@ -94,15 +95,15 @@ class PackageBuilder
|
||||
/**
|
||||
* Build a Dolibarr module release package.
|
||||
*
|
||||
* Copies everything under src/ into a build staging directory and archives
|
||||
* it as dist/<MODULE_NAME>_<VERSION>.zip.
|
||||
* Copies everything under source/ (or src/) into a build staging directory
|
||||
* and archives it as dist/<MODULE_NAME>_<VERSION>.zip.
|
||||
*
|
||||
* @param string $repoRoot Absolute path to the repository root.
|
||||
* @param string $moduleName Module name (used in archive filename).
|
||||
* @param string $version Version string.
|
||||
* @param bool $dryRun When true, preview without writing.
|
||||
* @return string Path to the created archive (or would-create path in dry-run).
|
||||
* @throws \RuntimeException When src/ is absent or archive creation fails.
|
||||
* @throws \RuntimeException When source directory is absent or archive creation fails.
|
||||
*/
|
||||
public static function buildDolibarr(
|
||||
string $repoRoot,
|
||||
@@ -110,14 +111,15 @@ class PackageBuilder
|
||||
string $version,
|
||||
bool $dryRun = false
|
||||
): string {
|
||||
$srcDir = $repoRoot . '/src';
|
||||
$srcDir = SourceResolver::resolveAbsolute($repoRoot);
|
||||
$buildDir = $repoRoot . '/build';
|
||||
$distDir = $repoRoot . '/dist';
|
||||
$archivePath = $distDir . '/' . $moduleName . '_' . $version . '.zip';
|
||||
|
||||
if (!is_dir($srcDir)) {
|
||||
throw new \RuntimeException("src/ directory not found at {$srcDir}");
|
||||
if ($srcDir === null) {
|
||||
throw new \RuntimeException("source/ or src/ directory not found in {$repoRoot}");
|
||||
}
|
||||
SourceResolver::warnIfLegacy($repoRoot);
|
||||
|
||||
if ($dryRun) {
|
||||
return $archivePath;
|
||||
|
||||
@@ -20,6 +20,7 @@ declare(strict_types=1);
|
||||
namespace MokoEnterprise\Plugins;
|
||||
|
||||
use MokoEnterprise\AbstractProjectPlugin;
|
||||
use MokoEnterprise\SourceResolver;
|
||||
|
||||
/**
|
||||
* MCP Server Project Plugin
|
||||
@@ -55,10 +56,12 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$warnings = [];
|
||||
|
||||
// Check for required source files
|
||||
$requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$srcName = SourceResolver::resolve($projectPath);
|
||||
SourceResolver::warnIfLegacy($projectPath);
|
||||
$requiredSrc = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
foreach ($requiredSrc as $file) {
|
||||
if (!file_exists("{$projectPath}/{$file}")) {
|
||||
$errors[] = "Missing required source file: {$file}";
|
||||
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||
$errors[] = "Missing required source file: {$srcName}/{$file}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,37 +85,33 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$errors[] = 'Missing tsconfig.json';
|
||||
}
|
||||
|
||||
// Check for setup wizard
|
||||
if (!file_exists("{$projectPath}/scripts/setup.mjs")) {
|
||||
$warnings[] = 'Missing scripts/setup.mjs — interactive setup wizard recommended';
|
||||
}
|
||||
|
||||
// Check for config example
|
||||
if (!file_exists("{$projectPath}/config.example.json")) {
|
||||
$warnings[] = 'Missing config.example.json — example configuration recommended';
|
||||
}
|
||||
|
||||
// Check for shebang in index.ts
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||
if ($indexTs !== null) {
|
||||
$content = @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, '#!/usr/bin/env node') === false) {
|
||||
$warnings[] = 'src/index.ts should start with #!/usr/bin/env node shebang';
|
||||
$warnings[] = "{$srcName}/index.ts should start with #!/usr/bin/env node shebang";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for McpServer usage
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
if ($indexTs !== null) {
|
||||
$content = $content ?? @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, 'McpServer') === false) {
|
||||
$errors[] = 'src/index.ts must import and use McpServer from @modelcontextprotocol/sdk';
|
||||
$errors[] = "{$srcName}/index.ts must import and use McpServer from @modelcontextprotocol/sdk";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for StdioServerTransport
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
if ($indexTs !== null) {
|
||||
$content = $content ?? @file_get_contents($indexTs);
|
||||
if ($content && strpos($content, 'StdioServerTransport') === false) {
|
||||
$warnings[] = 'src/index.ts should use StdioServerTransport for Claude Code compatibility';
|
||||
$warnings[] = "{$srcName}/index.ts should use StdioServerTransport for Claude Code compatibility";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +189,13 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
$score = 100;
|
||||
|
||||
// Check for required source files
|
||||
$requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$srcName = SourceResolver::resolve($projectPath);
|
||||
$requiredSrc = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
foreach ($requiredSrc as $file) {
|
||||
if (!file_exists("{$projectPath}/{$file}")) {
|
||||
if (SourceResolver::findUnderSource($projectPath, $file) === null) {
|
||||
$issues[] = [
|
||||
'severity' => 'critical',
|
||||
'message' => "Missing required file: {$file}",
|
||||
'message' => "Missing required file: {$srcName}/{$file}",
|
||||
];
|
||||
$score -= 20;
|
||||
}
|
||||
@@ -214,14 +214,15 @@ class McpServerPlugin extends AbstractProjectPlugin
|
||||
}
|
||||
|
||||
// Check for at least one registered tool
|
||||
if (file_exists("{$projectPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$projectPath}/src/index.ts");
|
||||
$indexTs = SourceResolver::findUnderSource($projectPath, 'index.ts');
|
||||
if ($indexTs !== null) {
|
||||
$content = @file_get_contents($indexTs);
|
||||
if ($content) {
|
||||
$toolCount = substr_count($content, 'server.tool(');
|
||||
if ($toolCount === 0) {
|
||||
$issues[] = [
|
||||
'severity' => 'critical',
|
||||
'message' => 'No MCP tools registered in src/index.ts',
|
||||
'message' => "No MCP tools registered in {$srcName}/index.ts",
|
||||
];
|
||||
$score -= 25;
|
||||
} elseif ($toolCount < 5) {
|
||||
|
||||
@@ -1380,7 +1380,7 @@ class RepositorySynchronizer
|
||||
|
||||
$descriptors = array_values(array_filter(
|
||||
$paths,
|
||||
static fn(string $p): bool => (bool) preg_match('#src/core/modules/mod\w+\.class\.php$#', $p)
|
||||
static fn(string $p): bool => (bool) preg_match('#(?:source|src)/core/modules/mod\w+\.class\.php$#', $p)
|
||||
));
|
||||
|
||||
if (empty($descriptors)) {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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: MokoPlatform.Enterprise
|
||||
* INGROUP: MokoPlatform.Lib
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /lib/Enterprise/SourceResolver.php
|
||||
* BRIEF: Resolve the root-level source directory across repos (source/, src/, htdocs/)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MokoEnterprise;
|
||||
|
||||
/**
|
||||
* Source Directory Resolver
|
||||
*
|
||||
* Provides a single, consistent fallback chain for locating the root-level
|
||||
* source directory in any MokoStandards repository. The preferred directory
|
||||
* is `source/`, with legacy `src/` and `htdocs/` as fallbacks.
|
||||
*
|
||||
* This class exists because Joomla extensions use `src/` for namespace
|
||||
* autoloading (e.g. administrator/components/com_foo/src/). Renaming our
|
||||
* root-level source directory to `source/` avoids that collision. During
|
||||
* the transition period, repos may still use `src/`, so all tooling must
|
||||
* check both.
|
||||
*
|
||||
* Usage:
|
||||
* $dir = SourceResolver::resolve($repoRoot); // 'source', 'src', or 'htdocs'
|
||||
* $abs = SourceResolver::resolveAbsolute($repoRoot); // full path or null
|
||||
* $xmls = SourceResolver::globSource($repoRoot, '*.xml'); // glob under first match
|
||||
* $path = SourceResolver::findUnderSource($repoRoot, 'core/modules'); // subpath lookup
|
||||
*
|
||||
* @since 09.02.00
|
||||
*/
|
||||
class SourceResolver
|
||||
{
|
||||
/**
|
||||
* Ordered candidate directories. source/ is preferred, src/ is legacy fallback.
|
||||
*
|
||||
* When the migration is complete and all repos use source/, the 'src'
|
||||
* entry can be removed from this list.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private const CANDIDATES = ['source', 'src', 'htdocs'];
|
||||
|
||||
/**
|
||||
* Resolve the source directory name for a repository root.
|
||||
*
|
||||
* Returns the first candidate directory that exists, or 'source' as the
|
||||
* default when no candidate is found (e.g. for new repos being scaffolded).
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return string Directory name (e.g. 'source', 'src', 'htdocs').
|
||||
*/
|
||||
public static function resolve(string $root): string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
if (is_dir("{$root}/{$candidate}")) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return 'source';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the source directory as an absolute path.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return string|null Absolute path to the source directory, or null if none exists.
|
||||
*/
|
||||
public static function resolveAbsolute(string $root): ?string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$path = "{$root}/{$candidate}";
|
||||
if (is_dir($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glob for files under the source directory.
|
||||
*
|
||||
* Checks each candidate directory in order and returns matches from the
|
||||
* first candidate that produces results. This replaces patterns like:
|
||||
*
|
||||
* glob("{$root}/src/*.xml")
|
||||
*
|
||||
* With the backwards-compatible:
|
||||
*
|
||||
* SourceResolver::globSource($root, '*.xml')
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @param string $pattern Glob pattern relative to the source directory.
|
||||
* @return string[] Matched file paths (may be empty).
|
||||
*/
|
||||
public static function globSource(string $root, string $pattern): array
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$dir = "{$root}/{$candidate}";
|
||||
if (!is_dir($dir)) {
|
||||
continue;
|
||||
}
|
||||
$matches = glob("{$dir}/{$pattern}") ?: [];
|
||||
if ($matches !== []) {
|
||||
return $matches;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a subpath under any source directory candidate.
|
||||
*
|
||||
* Useful for locating platform-specific subdirectories like
|
||||
* `core/modules/` (Dolibarr) or `media/templates/` (Joomla client themes)
|
||||
* regardless of whether the repo uses `source/` or `src/`.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @param string $subpath Relative path to look for (e.g. 'core/modules', 'index.ts').
|
||||
* @return string|null Absolute path if found, null otherwise.
|
||||
*/
|
||||
public static function findUnderSource(string $root, string $subpath): ?string
|
||||
{
|
||||
foreach (self::CANDIDATES as $candidate) {
|
||||
$full = "{$root}/{$candidate}/{$subpath}";
|
||||
if (file_exists($full) || is_dir($full)) {
|
||||
return $full;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered list of candidate directory names.
|
||||
*
|
||||
* Useful for workflows or scripts that need to iterate candidates
|
||||
* themselves (e.g. building find/grep patterns).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getCandidates(): array
|
||||
{
|
||||
return self::CANDIDATES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the resolved source directory is a legacy name (src/).
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
* @return bool True if the repo uses src/ instead of source/.
|
||||
*/
|
||||
public static function isLegacy(string $root): bool
|
||||
{
|
||||
$resolved = self::resolve($root);
|
||||
|
||||
return $resolved === 'src';
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a deprecation warning to stderr if the repo still uses src/.
|
||||
*
|
||||
* CLI tools should call this after resolving the source directory so
|
||||
* that maintainers know to rename src/ → source/.
|
||||
*
|
||||
* @param string $root Absolute path to the repository root.
|
||||
*/
|
||||
public static function warnIfLegacy(string $root): void
|
||||
{
|
||||
if (self::isLegacy($root)) {
|
||||
fwrite(STDERR, "⚠ WARNING: This repo uses src/ which is deprecated. Rename to source/ per MokoStandards.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ use MokoEnterprise\{
|
||||
PluginFactory,
|
||||
PluginRegistry,
|
||||
AuditLogger,
|
||||
MetricsCollector
|
||||
MetricsCollector,
|
||||
SourceResolver
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -228,8 +229,9 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: site structure inside src/
|
||||
$siteDirs = ['src/administrator', 'src/components', 'src/plugins', 'src/templates', 'src/media'];
|
||||
// Legacy: site structure inside source/ or src/
|
||||
$srcName = SourceResolver::resolve($repoPath);
|
||||
$siteDirs = ["{$srcName}/administrator", "{$srcName}/components", "{$srcName}/plugins", "{$srcName}/templates", "{$srcName}/media"];
|
||||
$siteDirCount = 0;
|
||||
foreach ($siteDirs as $dir) {
|
||||
if (is_dir($repoPath . '/' . $dir)) {
|
||||
@@ -238,7 +240,7 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
if ($siteDirCount >= 3) {
|
||||
$score += 20;
|
||||
$indicators[] = "Joomla site structure in src/ ({$siteDirCount}/5 dirs)";
|
||||
$indicators[] = "Joomla site structure in {$srcName}/ ({$siteDirCount}/5 dirs)";
|
||||
}
|
||||
|
||||
// Negative: if there's a Joomla extension manifest (not type="file"), it's an extension
|
||||
@@ -710,17 +712,19 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
|
||||
// Check for MCP server entry point with McpServer usage
|
||||
if (file_exists("{$repoPath}/src/index.ts")) {
|
||||
$content = @file_get_contents("{$repoPath}/src/index.ts");
|
||||
$mcpEntry = SourceResolver::findUnderSource($repoPath, 'index.ts');
|
||||
if ($mcpEntry !== null) {
|
||||
$content = @file_get_contents($mcpEntry);
|
||||
$mcpSrcName = SourceResolver::resolve($repoPath);
|
||||
if ($content) {
|
||||
if (strpos($content, 'McpServer') !== false) {
|
||||
$score += 0.3;
|
||||
$indicators[] = "Found McpServer import in src/index.ts";
|
||||
$indicators[] = "Found McpServer import in {$mcpSrcName}/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";
|
||||
$indicators[] = "Found {$toolCount} tool registrations in {$mcpSrcName}/index.ts";
|
||||
}
|
||||
if (strpos($content, 'StdioServerTransport') !== false) {
|
||||
$score += 0.1;
|
||||
@@ -730,16 +734,17 @@ class AutoDetectPlatform extends CliFramework
|
||||
}
|
||||
|
||||
// Check for the standard 4-file MCP structure
|
||||
$mcpFiles = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts'];
|
||||
$mcpRequired = ['index.ts', 'client.ts', 'config.ts', 'types.ts'];
|
||||
$foundCount = 0;
|
||||
foreach ($mcpFiles as $file) {
|
||||
if (file_exists("{$repoPath}/{$file}")) {
|
||||
foreach ($mcpRequired as $file) {
|
||||
if (SourceResolver::findUnderSource($repoPath, $file) !== null) {
|
||||
$foundCount++;
|
||||
}
|
||||
}
|
||||
if ($foundCount === 4) {
|
||||
$score += 0.1;
|
||||
$indicators[] = "Found standard MCP 4-file src/ structure";
|
||||
$mcpSrcName = $mcpSrcName ?? SourceResolver::resolve($repoPath);
|
||||
$indicators[] = "Found standard MCP 4-file {$mcpSrcName}/ structure";
|
||||
}
|
||||
|
||||
// Check for setup wizard
|
||||
|
||||
@@ -30,7 +30,7 @@ use MokoEnterprise\CliFramework;
|
||||
class CheckChangelog extends CliFramework
|
||||
{
|
||||
/** Directories searched for CHANGELOG.md, relative to --path (case-insensitive match). */
|
||||
private const SEARCH_DIRS = ['', 'src', 'docs'];
|
||||
private const SEARCH_DIRS = ['', 'source', 'src', 'docs'];
|
||||
|
||||
/**
|
||||
* Configure available arguments.
|
||||
@@ -57,7 +57,7 @@ class CheckChangelog extends CliFramework
|
||||
$found = $this->findChangelog($path);
|
||||
|
||||
if ($found === null) {
|
||||
$this->status(false, 'CHANGELOG.md found (checked root, src/, docs/)');
|
||||
$this->status(false, 'CHANGELOG.md found (checked root, source/, src/, docs/)');
|
||||
$this->printSummary(0, 1, $this->elapsed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Validates client theme packages that deliver CSS, JS, and images
|
||||
@@ -44,17 +44,17 @@ class CheckClientTheme extends CliFramework
|
||||
/** Recommended XML elements. */
|
||||
private const RECOMMENDED_ELEMENTS = ['updateservers', 'scriptfile', 'description', 'fileset'];
|
||||
|
||||
/** Required theme CSS files relative to repo root. */
|
||||
/** Required theme CSS files relative to the source directory. */
|
||||
private const REQUIRED_THEME_FILES = [
|
||||
'src/media/templates/site/mokoonyx/css/theme/light.custom.css',
|
||||
'src/media/templates/site/mokoonyx/css/theme/dark.custom.css',
|
||||
'media/templates/site/mokoonyx/css/theme/light.custom.css',
|
||||
'media/templates/site/mokoonyx/css/theme/dark.custom.css',
|
||||
];
|
||||
|
||||
/** Optional but expected files. */
|
||||
/** Optional but expected files (paths prefixed with ~ are relative to source dir). */
|
||||
private const EXPECTED_FILES = [
|
||||
'src/media/templates/site/mokoonyx/css/user.css',
|
||||
'src/media/templates/site/mokoonyx/js/user.js',
|
||||
'src/script.php',
|
||||
'~media/templates/site/mokoonyx/css/user.css',
|
||||
'~media/templates/site/mokoonyx/js/user.js',
|
||||
'~script.php',
|
||||
'updates.xml',
|
||||
];
|
||||
|
||||
@@ -81,10 +81,12 @@ class CheckClientTheme extends CliFramework
|
||||
|
||||
// ── Manifest ──────────────────────────────────────────
|
||||
$this->section('Manifest validation');
|
||||
$manifest = $path . '/src/templateDetails.xml';
|
||||
$srcName = SourceResolver::resolve($path);
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
$manifest = $path . "/{$srcName}/templateDetails.xml";
|
||||
|
||||
if (!is_file($manifest)) {
|
||||
$this->status(false, 'Missing src/templateDetails.xml');
|
||||
$this->status(false, "Missing {$srcName}/templateDetails.xml");
|
||||
$this->printSummary(0, 1, $this->elapsed());
|
||||
return 1;
|
||||
}
|
||||
@@ -144,28 +146,36 @@ class CheckClientTheme extends CliFramework
|
||||
// ── Required files ────────────────────────────────────
|
||||
$this->section('Required files');
|
||||
foreach (self::REQUIRED_THEME_FILES as $file) {
|
||||
$full = $path . '/' . $file;
|
||||
$full = "{$path}/{$srcName}/{$file}";
|
||||
if (is_file($full)) {
|
||||
$this->status(true, basename($file));
|
||||
} else {
|
||||
$this->status(false, "Missing: {$file}");
|
||||
$this->status(false, "Missing: {$srcName}/{$file}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::EXPECTED_FILES as $file) {
|
||||
$full = $path . '/' . $file;
|
||||
// Paths prefixed with ~ are relative to source dir
|
||||
if (str_starts_with($file, '~')) {
|
||||
$relFile = substr($file, 1);
|
||||
$full = "{$path}/{$srcName}/{$relFile}";
|
||||
$display = "{$srcName}/{$relFile}";
|
||||
} else {
|
||||
$full = "{$path}/{$file}";
|
||||
$display = $file;
|
||||
}
|
||||
if (is_file($full)) {
|
||||
$this->status(true, basename($file));
|
||||
} else {
|
||||
$this->warning("Missing: {$file}");
|
||||
$this->warning("Missing: {$display}");
|
||||
$warns++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── PHP syntax ────────────────────────────────────────
|
||||
$this->section('PHP syntax');
|
||||
$phpFiles = glob($path . '/src/*.php') ?: [];
|
||||
$phpFiles = glob("{$path}/{$srcName}/*.php") ?: [];
|
||||
foreach ($phpFiles as $phpFile) {
|
||||
$output = [];
|
||||
$ret = 0;
|
||||
@@ -179,20 +189,20 @@ class CheckClientTheme extends CliFramework
|
||||
}
|
||||
}
|
||||
if (empty($phpFiles)) {
|
||||
$this->warning('No PHP files in src/');
|
||||
$this->warning("No PHP files in {$srcName}/");
|
||||
}
|
||||
|
||||
// ── CSS validation ────────────────────────────────────
|
||||
$this->section('CSS validation');
|
||||
$cssFiles = array_merge(
|
||||
glob($path . '/src/media/templates/site/mokoonyx/css/theme/*.css') ?: [],
|
||||
glob($path . '/src/media/templates/site/mokoonyx/css/*.css') ?: [],
|
||||
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/theme/*.css") ?: [],
|
||||
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/*.css") ?: [],
|
||||
);
|
||||
foreach ($cssFiles as $cssFile) {
|
||||
$css = (string) file_get_contents($cssFile);
|
||||
$open = substr_count($css, '{');
|
||||
$close = substr_count($css, '}');
|
||||
$name = str_replace($path . '/src/', '', $cssFile);
|
||||
$name = str_replace("{$path}/{$srcName}/", '', $cssFile);
|
||||
|
||||
if ($open !== $close) {
|
||||
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
|
||||
@@ -241,7 +251,7 @@ class CheckClientTheme extends CliFramework
|
||||
// ── Image sizes ───────────────────────────────────────
|
||||
$this->section('Image optimization');
|
||||
$largeImages = 0;
|
||||
$imageDir = $path . '/src/images';
|
||||
$imageDir = "{$path}/{$srcName}/images";
|
||||
if (is_dir($imageDir)) {
|
||||
$iter = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)
|
||||
|
||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\CliFramework;
|
||||
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||
|
||||
/**
|
||||
* Validates the required directory structure of a Dolibarr module repository.
|
||||
@@ -47,33 +47,36 @@ class CheckDolibarrModule extends CliFramework
|
||||
$failed = 0;
|
||||
|
||||
$this->section('Checking directory structure');
|
||||
$srcName = SourceResolver::resolve($path);
|
||||
SourceResolver::warnIfLegacy($path);
|
||||
|
||||
if (!is_dir($path . '/src')) {
|
||||
$this->status(false, 'src/ directory exists');
|
||||
$srcDir = SourceResolver::resolveAbsolute($path);
|
||||
if ($srcDir === null) {
|
||||
$this->status(false, 'source/ or src/ directory exists');
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, 'src/ directory exists');
|
||||
$this->status(true, "{$srcName}/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
if (!is_dir($path . '/src/core/modules')) {
|
||||
$this->status(false, 'src/core/modules/ directory exists');
|
||||
if (!is_dir($path . "/{$srcName}/core/modules")) {
|
||||
$this->status(false, "{$srcName}/core/modules/ directory exists");
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, 'src/core/modules/ directory exists');
|
||||
$this->status(true, "{$srcName}/core/modules/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
if (!is_dir($path . '/src/langs')) {
|
||||
$this->warning('Missing suggested directory: src/langs/');
|
||||
if (!is_dir($path . "/{$srcName}/langs")) {
|
||||
$this->warning("Missing suggested directory: {$srcName}/langs/");
|
||||
} else {
|
||||
$this->status(true, 'src/langs/ directory exists');
|
||||
$this->status(true, "{$srcName}/langs/ directory exists");
|
||||
$passed++;
|
||||
}
|
||||
|
||||
$this->section('Checking module descriptor');
|
||||
|
||||
$descriptors = glob($path . '/src/core/modules/mod*.class.php') ?: [];
|
||||
$descriptors = glob($path . "/{$srcName}/core/modules/mod*.class.php") ?: [];
|
||||
if (empty($descriptors)) {
|
||||
$this->status(false, 'Module descriptor found (mod*.class.php)');
|
||||
$failed++;
|
||||
|
||||
@@ -37,7 +37,7 @@ class CheckStructure extends CliFramework
|
||||
private const REQUIRED_FILES = ['README.md', 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md'];
|
||||
|
||||
/** Directories searched for CHANGELOG.md (case-insensitive), relative to repo root. */
|
||||
private const CHANGELOG_DIRS = ['', 'src', 'docs'];
|
||||
private const CHANGELOG_DIRS = ['', 'source', 'src', 'docs'];
|
||||
|
||||
/**
|
||||
* Configure available arguments.
|
||||
|
||||
Reference in New Issue
Block a user