diff --git a/cli/release_package.php b/cli/release_package.php index 3262def..15bbe87 100644 --- a/cli/release_package.php +++ b/cli/release_package.php @@ -157,6 +157,26 @@ function giteaUploadAsset(string $url, string $token, string $filePath): int return $httpCode; } +// ── Read platform from .mokogitea/manifest.xml ─────────────────────────────── + +$detectedPlatform = 'generic'; +$detectedEntryPoint = ''; +$mokoManifest = "{$root}/.mokogitea/manifest.xml"; +if (file_exists($mokoManifest)) { + $mokoXml = @simplexml_load_file($mokoManifest); + if ($mokoXml !== false) { + $rawPlatform = (string)($mokoXml->governance->platform ?? ''); + if ($rawPlatform !== '') { + $detectedPlatform = match ($rawPlatform) { + 'waas-component' => 'joomla', + 'crm-module' => 'dolibarr', + default => $rawPlatform, + }; + } + $detectedEntryPoint = (string)($mokoXml->build->{"entry-point"} ?? ''); + } +} + // ── Detect element metadata from manifest XML ──────────────────────────────── $extElement = ''; @@ -257,9 +277,19 @@ echo "TAR: {$baseName}.tar.gz\n"; // ── Find source directory ──────────────────────────────────────────────────── $sourceDir = null; -if (is_dir("{$root}/src")) { + +// Use entry-point from manifest.xml if available +if ($detectedEntryPoint !== '') { + $entryDir = rtrim(dirname($detectedEntryPoint) === '.' ? $detectedEntryPoint : dirname($detectedEntryPoint), '/'); + if (is_dir("{$root}/{$entryDir}")) { + $sourceDir = "{$root}/{$entryDir}"; + } +} + +// Fallback to common directories +if ($sourceDir === null && is_dir("{$root}/src")) { $sourceDir = "{$root}/src"; -} elseif (is_dir("{$root}/htdocs")) { +} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) { $sourceDir = "{$root}/htdocs"; } diff --git a/cli/updates_xml_build.php b/cli/updates_xml_build.php index 0dcd587..714f7cb 100644 --- a/cli/updates_xml_build.php +++ b/cli/updates_xml_build.php @@ -1,5 +1,6 @@ #!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later @@ -42,174 +43,255 @@ $outputFile = null; $githubOutput = false; foreach ($argv as $i => $arg) { - if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; - if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; - if ($arg === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1]; - if ($arg === '--sha' && isset($argv[$i + 1])) $sha = $argv[$i + 1]; - if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1]; - if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1]; - if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1]; - if ($arg === '--output' && isset($argv[$i + 1])) $outputFile = $argv[$i + 1]; - if ($arg === '--github-output') $githubOutput = true; + if ($arg === '--path' && isset($argv[$i + 1])) { + $path = $argv[$i + 1]; + } + if ($arg === '--version' && isset($argv[$i + 1])) { + $version = $argv[$i + 1]; + } + if ($arg === '--stability' && isset($argv[$i + 1])) { + $stability = $argv[$i + 1]; + } + if ($arg === '--sha' && isset($argv[$i + 1])) { + $sha = $argv[$i + 1]; + } + if ($arg === '--gitea-url' && isset($argv[$i + 1])) { + $giteaUrl = $argv[$i + 1]; + } + if ($arg === '--org' && isset($argv[$i + 1])) { + $org = $argv[$i + 1]; + } + if ($arg === '--repo' && isset($argv[$i + 1])) { + $repo = $argv[$i + 1]; + } + if ($arg === '--output' && isset($argv[$i + 1])) { + $outputFile = $argv[$i + 1]; + } + if ($arg === '--github-output') { + $githubOutput = true; + } } if ($version === null) { - fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n"); - exit(1); + fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n"); + exit(1); } $root = realpath($path) ?: $path; +// -- Read platform from .mokogitea/manifest.xml -------------------------------- +$detectedPlatform = 'joomla'; // default for backward compat +$detectedName = $repo; +$detectedPackageType = ''; +$mokoManifest = "{$root}/.mokogitea/manifest.xml"; +if (file_exists($mokoManifest)) { + $mokoXml = @simplexml_load_file($mokoManifest); + if ($mokoXml !== false) { + $rawPlatform = (string)($mokoXml->governance->platform ?? ''); + if ($rawPlatform !== '') { + $detectedPlatform = match ($rawPlatform) { + 'waas-component' => 'joomla', + 'crm-module' => 'dolibarr', + default => $rawPlatform, + }; + } + $detectedName = (string)($mokoXml->identity->name ?? $repo); + $detectedPackageType = (string)($mokoXml->build->{"package-type"} ?? ''); + } +} + // -- Locate Joomla manifest --------------------------------------------------- $manifest = null; // Priority: pkg_*.xml in src/ > any extension XML in src/ > any in root $candidates = glob("{$root}/src/pkg_*.xml") ?: []; foreach ($candidates as $f) { - if (strpos(file_get_contents($f), '([^<]+)<\/name>/', $xml, $m)) $extName = $m[1]; - $extType = ''; -if (preg_match('/]*type="([^"]+)"/', $xml, $m)) $extType = $m[1]; - $extElement = ''; -if (preg_match('/([^<]+)<\/element>/', $xml, $m)) $extElement = $m[1]; -// For packages, prefer to avoid pkg_pkg_ duplication -if (empty($extElement) && preg_match('/([^<]+)<\/packagename>/', $xml, $m)) $extElement = $m[1]; -if (empty($extElement) && preg_match('/plugin="([^"]+)"/', $xml, $m)) $extElement = $m[1]; -if (empty($extElement) && preg_match('/module="([^"]+)"/', $xml, $m)) $extElement = $m[1]; -if (empty($extElement)) { - $fname = strtolower(pathinfo($manifest, PATHINFO_FILENAME)); - if (in_array($fname, ['templatedetails', 'manifest'])) { - $extElement = strtolower(str_replace([' ', '-'], '', $repo ?: basename($root))); - } else { - $extElement = $fname; - } -} -// Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) -$extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement); - $extClient = ''; -if (preg_match('/]*client="([^"]+)"/', $xml, $m)) $extClient = $m[1]; - $extFolder = ''; -if (preg_match('/]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1]; - $targetPlatform = ''; -if (preg_match('/()/', $xml, $m)) $targetPlatform = $m[1]; -if (empty($targetPlatform)) { - $targetPlatform = ''; -} - $phpMinimum = ''; -if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) $phpMinimum = $m[1]; + +if ($manifest !== null) { + // Joomla manifest found — parse extension metadata from it + $xml = file_get_contents($manifest); + + if (preg_match('/([^<]+)<\/name>/', $xml, $m)) { + $extName = $m[1]; + } + if (preg_match('/]*type="([^"]+)"/', $xml, $m)) { + $extType = $m[1]; + } + if (preg_match('/([^<]+)<\/element>/', $xml, $m)) { + $extElement = $m[1]; + } + if (empty($extElement) && preg_match('/([^<]+)<\/packagename>/', $xml, $m)) { + $extElement = $m[1]; + } + if (empty($extElement) && preg_match('/plugin="([^"]+)"/', $xml, $m)) { + $extElement = $m[1]; + } + if (empty($extElement) && preg_match('/module="([^"]+)"/', $xml, $m)) { + $extElement = $m[1]; + } + if (empty($extElement)) { + $fname = strtolower(pathinfo($manifest, PATHINFO_FILENAME)); + if (in_array($fname, ['templatedetails', 'manifest'])) { + $extElement = strtolower(str_replace([' ', '-'], '', $repo ?: basename($root))); + } else { + $extElement = $fname; + } + } + $extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement); + + if (preg_match('/]*client="([^"]+)"/', $xml, $m)) { + $extClient = $m[1]; + } + if (preg_match('/]*group="([^"]+)"/', $xml, $m)) { + $extFolder = $m[1]; + } + if (preg_match('/()/', $xml, $m)) { + $targetPlatform = $m[1]; + } + if (empty($targetPlatform)) { + $targetPlatform = ''; + } + if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) { + $phpMinimum = $m[1]; + } +} else { + // Non-Joomla platform — derive metadata from .mokogitea/manifest.xml + $extName = $detectedName ?: ($repo ?: basename($root)); + $extElement = strtolower(str_replace([' ', '-'], '', $extName)); + $extType = $detectedPackageType ?: 'generic'; + $targetPlatform = ""; +} // Resolve language key names (e.g. PLG_SYSTEM_MOKOJOOMTOS) if (preg_match('/^[A-Z_]+$/', $extName)) { - $iniFiles = []; - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS) - ); - foreach ($iterator as $file) { - if (preg_match('/\.sys\.ini$/i', $file->getFilename())) { - $iniFiles[] = $file->getPathname(); - } - } - foreach ($iniFiles as $ini) { - $content = file_get_contents($ini); - if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) { - $extName = $m[1]; - break; - } - } + $iniFiles = []; + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS) + ); + foreach ($iterator as $file) { + if (preg_match('/\.sys\.ini$/i', $file->getFilename())) { + $iniFiles[] = $file->getPathname(); + } + } + foreach ($iniFiles as $ini) { + $content = file_get_contents($ini); + if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) { + $extName = $m[1]; + break; + } + } } // Fallbacks -if (empty($extName)) $extName = $repo ?: basename($root); -if (empty($extType)) $extType = 'component'; +if (empty($extName)) { + $extName = $repo ?: basename($root); +} +if (empty($extType)) { + $extType = 'component'; +} // -- Build type prefix -------------------------------------------------------- $typePrefix = ''; switch ($extType) { - case 'plugin': $typePrefix = "plg_{$extFolder}_"; break; - case 'module': $typePrefix = 'mod_'; break; - case 'component': $typePrefix = 'com_'; break; - case 'template': $typePrefix = 'tpl_'; break; - case 'library': $typePrefix = 'lib_'; break; - case 'package': $typePrefix = 'pkg_'; break; + case 'plugin': + $typePrefix = "plg_{$extFolder}_"; + break; + case 'module': + $typePrefix = 'mod_'; + break; + case 'component': + $typePrefix = 'com_'; + break; + case 'template': + $typePrefix = 'tpl_'; + break; + case 'library': + $typePrefix = 'lib_'; + break; + case 'package': + $typePrefix = 'pkg_'; + break; } // -- Export to GITHUB_OUTPUT if requested ------------------------------------- if ($githubOutput) { - $ghOutput = getenv('GITHUB_OUTPUT'); - $lines = [ - "ext_element={$extElement}", - "ext_name={$extName}", - "ext_type={$extType}", - "ext_folder={$extFolder}", - "type_prefix={$typePrefix}", - ]; - if ($ghOutput) { - file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); - fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n"); - } else { - foreach ($lines as $line) echo "{$line}\n"; - } + $ghOutput = getenv('GITHUB_OUTPUT'); + $lines = [ + "ext_element={$extElement}", + "ext_name={$extName}", + "ext_type={$extType}", + "ext_folder={$extFolder}", + "type_prefix={$typePrefix}", + ]; + if ($ghOutput) { + file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); + fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n"); + } else { + foreach ($lines as $line) { + echo "{$line}\n"; + } + } } // -- Stability suffix map ----------------------------------------------------- $stabilitySuffixMap = [ - 'stable' => '', - 'rc' => '-rc', - 'beta' => '-beta', - 'alpha' => '-alpha', - 'development' => '-dev', + 'stable' => '', + 'rc' => '-rc', + 'beta' => '-beta', + 'alpha' => '-alpha', + 'development' => '-dev', ]; // Joomla values — maps to Joomla's stabilityTagToInteger() $stabilityTagMap = [ - 'stable' => 'stable', - 'rc' => 'rc', - 'beta' => 'beta', - 'alpha' => 'alpha', - 'development' => 'dev', + 'stable' => 'stable', + 'rc' => 'rc', + 'beta' => 'beta', + 'alpha' => 'alpha', + 'development' => 'dev', ]; // Gitea release tag names (used in download/info URLs) $releaseTagMap = [ - 'stable' => 'stable', - 'rc' => 'release-candidate', - 'beta' => 'beta', - 'alpha' => 'alpha', - 'development' => 'development', + 'stable' => 'stable', + 'rc' => 'release-candidate', + 'beta' => 'beta', + 'alpha' => 'alpha', + 'development' => 'development', ]; // -- Build update entries ----------------------------------------------------- @@ -221,70 +303,78 @@ $primaryVersion = $version . $primarySuffix; // to installed extensions. Without it, extension_id=0 in #__updates. $clientTag = ''; if (!empty($extClient)) { - $clientTag = " {$extClient}"; + $clientTag = " {$extClient}"; } else { - $clientTag = ' site'; + $clientTag = ' site'; } // Build folder tag $folderTag = ''; if (!empty($extFolder) && $extType === 'plugin') { - $folderTag = " {$extFolder}"; + $folderTag = " {$extFolder}"; } // PHP minimum tag $phpTag = ''; if (!empty($phpMinimum)) { - $phpTag = " {$phpMinimum}"; + $phpTag = " {$phpMinimum}"; } // SHA tag $shaTag = ''; if (!empty($sha)) { - $shaTag = " {$sha}"; + $shaTag = " {$sha}"; } /** * Build a single entry for a given stability tag */ function buildEntry( - string $tagName, - string $entryVersion, - string $entryDownloadUrl, - string $extName, - string $extElement, - string $extType, - string $clientTag, - string $folderTag, - string $infoUrl, - string $targetPlatform, - string $phpTag, - string $shaTag + string $tagName, + string $entryVersion, + string $entryDownloadUrl, + string $extName, + string $extElement, + string $extType, + string $clientTag, + string $folderTag, + string $infoUrl, + string $targetPlatform, + string $phpTag, + string $shaTag ): string { - $lines = []; - $lines[] = ' '; - $lines[] = " {$extName}"; - $lines[] = " {$extName} update"; - // Element in updates.xml must match what Joomla stores in #__extensions - // For packages: pkg_elementname. For plugins: elementname (folder handles grouping). - $dbElement = ($extType === 'package') ? "pkg_{$extElement}" : $extElement; - $lines[] = " {$dbElement}"; - $lines[] = " {$extType}"; - $lines[] = " {$entryVersion}"; - if (!empty($clientTag)) $lines[] = $clientTag; - if (!empty($folderTag)) $lines[] = $folderTag; - $lines[] = " {$tagName}"; - $lines[] = " {$infoUrl}"; - $lines[] = ' '; - $lines[] = " {$entryDownloadUrl}"; - $lines[] = ' '; - if (!empty($shaTag)) $lines[] = $shaTag; - $lines[] = " {$targetPlatform}"; - if (!empty($phpTag)) $lines[] = $phpTag; - $lines[] = ' Moko Consulting'; - $lines[] = ' https://mokoconsulting.tech'; - $lines[] = ' '; - return implode("\n", $lines); + $lines = []; + $lines[] = ' '; + $lines[] = " {$extName}"; + $lines[] = " {$extName} update"; + // Element in updates.xml must match what Joomla stores in #__extensions + // For packages: pkg_elementname. For plugins: elementname (folder handles grouping). + $dbElement = ($extType === 'package') ? "pkg_{$extElement}" : $extElement; + $lines[] = " {$dbElement}"; + $lines[] = " {$extType}"; + $lines[] = " {$entryVersion}"; + if (!empty($clientTag)) { + $lines[] = $clientTag; + } + if (!empty($folderTag)) { + $lines[] = $folderTag; + } + $lines[] = " {$tagName}"; + $lines[] = " {$infoUrl}"; + $lines[] = ' '; + $lines[] = " {$entryDownloadUrl}"; + $lines[] = ' '; + if (!empty($shaTag)) { + $lines[] = $shaTag; + } + $lines[] = " {$targetPlatform}"; + if (!empty($phpTag)) { + $lines[] = $phpTag; + } + $lines[] = ' Moko Consulting'; + $lines[] = ' https://mokoconsulting.tech'; + $lines[] = ' '; + return implode("\n", $lines); } // -- Determine which channels to write ---------------------------------------- @@ -295,7 +385,9 @@ function buildEntry( // When dev releases, only dev is updated; everything else is preserved. $allChannels = ['development', 'alpha', 'beta', 'rc', 'stable']; $stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels); -if ($stabilityIndex === false) $stabilityIndex = 4; // default to stable +if ($stabilityIndex === false) { + $stabilityIndex = 4; // default to stable +} // Write entries for the current channel AND all lower channels (cascade down) // All cascaded entries point to the CURRENT release (the highest stability being built) @@ -306,25 +398,25 @@ $channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$giteaTag}/ $channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}"; for ($i = 0; $i <= $stabilityIndex; $i++) { - $channelName = $allChannels[$i]; - $joomlaTag = $stabilityTagMap[$channelName] ?? $channelName; - // Only attach SHA to the primary channel entry - $entrySha = ($i === $stabilityIndex) ? $shaTag : ''; + $channelName = $allChannels[$i]; + $joomlaTag = $stabilityTagMap[$channelName] ?? $channelName; + // Only attach SHA to the primary channel entry + $entrySha = ($i === $stabilityIndex) ? $shaTag : ''; - $entries[] = buildEntry( - $joomlaTag, - $channelVersion, - $channelDownloadUrl, - $extName, - $extElement, - $extType, - $clientTag, - $folderTag, - $channelInfoUrl, - $targetPlatform, - $phpTag, - $entrySha - ); + $entries[] = buildEntry( + $joomlaTag, + $channelVersion, + $channelDownloadUrl, + $extName, + $extElement, + $extType, + $clientTag, + $folderTag, + $channelInfoUrl, + $targetPlatform, + $phpTag, + $entrySha + ); } // -- Preserve existing entries for channels not being updated ----------------- @@ -332,27 +424,27 @@ $dest = $outputFile ?? "{$root}/updates.xml"; $preservedEntries = []; if (file_exists($dest)) { - $existingXml = @simplexml_load_file($dest); - if ($existingXml) { - // Joomla tags we're writing — don't preserve these - $writtenChannels = []; - for ($i = 0; $i <= $stabilityIndex; $i++) { - $writtenChannels[] = $stabilityTagMap[$allChannels[$i]] ?? $allChannels[$i]; - } - // Also match legacy/alternate tag names (e.g. 'development' = 'dev') - $writtenChannels[] = 'development'; // alias for 'dev' + $existingXml = @simplexml_load_file($dest); + if ($existingXml) { + // Joomla tags we're writing — don't preserve these + $writtenChannels = []; + for ($i = 0; $i <= $stabilityIndex; $i++) { + $writtenChannels[] = $stabilityTagMap[$allChannels[$i]] ?? $allChannels[$i]; + } + // Also match legacy/alternate tag names (e.g. 'development' = 'dev') + $writtenChannels[] = 'development'; // alias for 'dev' - foreach ($existingXml->update as $existingUpdate) { - $existingTag = ''; - if (isset($existingUpdate->tags->tag)) { - $existingTag = (string) $existingUpdate->tags->tag; - } - // Keep entries for channels we're NOT overwriting - if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) { - $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); - } - } - } + foreach ($existingXml->update as $existingUpdate) { + $existingTag = ''; + if (isset($existingUpdate->tags->tag)) { + $existingTag = (string) $existingUpdate->tags->tag; + } + // Keep entries for channels we're NOT overwriting + if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) { + $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); + } + } + } } // -- Write updates.xml -------------------------------------------------------- diff --git a/lib/Enterprise/ManifestReader.php b/lib/Enterprise/ManifestReader.php new file mode 100644 index 0000000..1631e20 --- /dev/null +++ b/lib/Enterprise/ManifestReader.php @@ -0,0 +1,196 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.Enterprise + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /lib/Enterprise/ManifestReader.php + * BRIEF: Read and parse .mokogitea/manifest.xml — shared across all CLI tools + */ + +declare(strict_types=1); + +namespace MokoEnterprise; + +/** + * Manifest Reader + * + * Parses .mokogitea/manifest.xml and provides typed access to all fields. + * Used by CLI tools and the Enterprise library to determine platform, + * build configuration, and deployment settings from the repository manifest. + * + * @since 09.01.00 + */ +class ManifestReader +{ + /** @var array Parsed manifest fields */ + private array $fields = []; + + /** @var bool Whether a manifest was found and parsed */ + private bool $loaded = false; + + /** + * Load manifest from a repository root directory. + * + * @param string $root Repository root path + * @return self + */ + public static function fromPath(string $root): self + { + $reader = new self(); + $reader->load($root); + return $reader; + } + + /** + * Load and parse the manifest file. + * + * @param string $root Repository root path + */ + public function load(string $root): void + { + $candidates = [ + "{$root}/.mokogitea/manifest.xml", + "{$root}/.mokogitea/.manifest.xml", + "{$root}/.mokogitea/.moko-platform", + ]; + + $manifestFile = null; + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + $manifestFile = $candidate; + break; + } + } + + if ($manifestFile === null) { + return; + } + + $xml = @simplexml_load_file($manifestFile); + if ($xml === false) { + // Fallback: YAML legacy format + $content = file_get_contents($manifestFile); + if (preg_match('/^platform:\s*(.+)/m', $content, $m)) { + $this->fields['platform'] = trim($m[1], " \t\n\r\"'"); + } + $this->loaded = true; + return; + } + + $this->fields = [ + 'name' => (string)($xml->identity->name ?? ''), + 'org' => (string)($xml->identity->org ?? ''), + 'description' => (string)($xml->identity->description ?? ''), + 'license' => (string)($xml->identity->license ?? ''), + 'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''), + 'version' => (string)($xml->identity->version ?? ''), + 'platform' => (string)($xml->governance->platform ?? ''), + 'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''), + 'language' => (string)($xml->build->language ?? ''), + 'package-type' => (string)($xml->build->{"package-type"} ?? ''), + 'entry-point' => (string)($xml->build->{"entry-point"} ?? ''), + 'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''), + 'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''), + 'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''), + 'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''), + ]; + + // Strip empty values + $this->fields = array_filter($this->fields, fn($v) => $v !== ''); + $this->loaded = true; + } + + /** + * Whether a manifest was found and loaded. + * + * @return bool + */ + public function isLoaded(): bool + { + return $this->loaded; + } + + /** + * Get a single field value. + * + * @param string $key Field name (e.g. 'platform', 'package-type') + * @param string $default Default value if field is missing + * @return string + */ + public function get(string $key, string $default = ''): string + { + return $this->fields[$key] ?? $default; + } + + /** + * Get the platform slug, normalized to canonical values. + * + * @return string One of: joomla, dolibarr, generic, mcp, nodejs + */ + public function getPlatform(): string + { + $raw = $this->get('platform', 'generic'); + return match ($raw) { + 'waas-component' => 'joomla', + 'crm-module' => 'dolibarr', + default => $raw, + }; + } + + /** + * Get the source/entry-point directory. + * + * @param string $root Repository root for existence checking + * @return string Resolved source directory path (e.g. 'src', 'htdocs') + */ + public function getSourceDir(string $root = ''): string + { + $entryPoint = $this->get('entry-point', ''); + if ($entryPoint !== '') { + // Strip trailing filename (e.g. src/index.ts → src) + $dir = rtrim(dirname($entryPoint) === '.' ? $entryPoint : dirname($entryPoint), '/'); + if ($root === '' || is_dir("{$root}/{$dir}")) { + return $dir; + } + } + + // Fallback: check common directories + if ($root !== '') { + if (is_dir("{$root}/src")) { + return 'src'; + } + if (is_dir("{$root}/htdocs")) { + return 'htdocs'; + } + } + + return 'src'; + } + + /** + * Get the package type for build decisions. + * + * @return string e.g. 'package', 'dolibarr', 'generic', 'mcp-server' + */ + public function getPackageType(): string + { + return $this->get('package-type', 'generic'); + } + + /** + * Get all parsed fields. + * + * @return array + */ + public function getAll(): array + { + return $this->fields; + } +}