#!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: mokocli.CLI * INGROUP: mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * PATH: /cli/manifest_read.php * VERSION: 09.33.00 * BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoCli\CliFramework; class ManifestReadCli extends CliFramework { /** Joomla extension XML element names searched in root and source/ dirs. */ private const JOOMLA_XML_ROOTS = ['extension', 'install']; protected function configure(): void { $this->setDescription('Read repo metadata from Gitea API with auto-detection fallback'); $this->addArgument('--path', 'Repository root path', '.'); $this->addArgument('--field', 'Single field name to output', ''); $this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false); $this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false); $this->addArgument('--json', 'Output all fields as JSON', false); } protected function run(): int { $path = $this->getArgument('--path'); $field = $this->getArgument('--field'); $showAll = $this->getArgument('--all'); $ghOut = $this->getArgument('--github-output'); $jsonMode = $this->getArgument('--json'); $mode = match (true) { (bool) $ghOut => 'github-output', (bool) $showAll => 'all', (bool) $jsonMode => 'json', default => 'field', }; $root = realpath($path) ?: $path; // ── 1. Resolve org/repo ────────────────────────────────────────── [$org, $repo] = $this->resolveOrgRepo($root); // ── 2. Primary: Gitea manifest API ─────────────────────────────── $fields = null; if ($org !== '' && $repo !== '') { $fields = $this->fetchFromApi($org, $repo); } // ── 3. Fallback: auto-detect from source tree ──────────────────── if ($fields === null) { $this->log('INFO', 'API unavailable — falling back to source-tree detection'); $fields = $this->autoDetect($root, $repo); } if (empty($fields)) { $this->log('ERROR', "Could not resolve metadata for {$root}"); return 1; } // Provide backward-compatible aliases (hyphenated → underscore) $fields = $this->addAliases($fields); // Strip empty values $fields = array_filter($fields, fn($v) => $v !== '' && $v !== null); // ── 4. Output ──────────────────────────────────────────────────── return $this->outputFields($fields, $mode, $field); } // ── Gitea manifest API ─────────────────────────────────────────────── private function fetchFromApi(string $org, string $repo): ?array { $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: ''; $baseUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; $baseUrl = rtrim($baseUrl, '/'); if ($token === '') { return null; } $url = "{$baseUrl}/api/v1/repos/{$org}/{$repo}/manifest"; $ctx = stream_context_create([ 'http' => [ 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", 'timeout' => 10, 'ignore_errors' => true, ], ]); $body = @file_get_contents($url, false, $ctx); if ($body === false) { return null; } // Check HTTP status from response headers $status = 0; if (isset($http_response_header[0])) { preg_match('/\d{3}/', $http_response_header[0], $m); $status = (int) ($m[0] ?? 0); } if ($status < 200 || $status >= 300) { return null; } $data = json_decode($body, true); if (!is_array($data) || empty($data)) { return null; } $this->log('INFO', "Loaded metadata from Gitea manifest API ({$org}/{$repo})"); return $data; } // ── Auto-detection fallback ────────────────────────────────────────── private function autoDetect(string $root, string $repoName): array { $fields = [ 'name' => $repoName ?: basename($root), 'org' => 'MokoConsulting', ]; // Resolve source directory (source/ or src/) $srcDir = null; foreach (['source', 'src'] as $candidate) { if (is_dir("{$root}/{$candidate}")) { $srcDir = $candidate; break; } } // ── Try Joomla detection ───────────────────────────────────── $joomlaResult = $this->detectJoomla($root, $srcDir); if ($joomlaResult !== null) { $fields = array_merge($fields, $joomlaResult); $this->log('INFO', "Auto-detected platform: joomla ({$fields['extension_type']} — {$fields['element_name']})"); return $fields; } // ── Try Dolibarr detection ─────────────────────────────────── $dolibarrResult = $this->detectDolibarr($root); if ($dolibarrResult !== null) { $fields = array_merge($fields, $dolibarrResult); $this->log('INFO', "Auto-detected platform: dolibarr"); return $fields; } // ── Generic fallback ───────────────────────────────────────── $fields['platform'] = $this->detectGenericPlatform($root); $fields['element_name'] = strtolower($fields['name']); $fields['extension_type'] = 'application'; $fields['language'] = $this->detectLanguage($root); if ($srcDir !== null) { $fields['entry_point'] = "{$srcDir}/"; } $this->log('INFO', "Auto-detected platform: {$fields['platform']}"); return $fields; } /** * Detect Joomla platform by scanning for extension XML manifests. * * Searches root and source/ dirs for XML files containing . * Extracts element name from the filename (pkg_*, com_*, mod_*, plg_*, tpl_*) or * from the tag inside the manifest. */ private function detectJoomla(string $root, ?string $srcDir): ?array { $searchDirs = [$root]; if ($srcDir !== null) { $searchDirs[] = "{$root}/{$srcDir}"; } foreach ($searchDirs as $dir) { $xmlFiles = glob("{$dir}/*.xml") ?: []; foreach ($xmlFiles as $xmlFile) { $content = @file_get_contents($xmlFile); if ($content === false) { continue; } // Match if (!preg_match('/]*type="([^"]+)"/', $content, $typeMatch)) { // Also try legacy if (!preg_match('/]*type="([^"]+)"/', $content, $typeMatch)) { continue; } } $extType = strtolower($typeMatch[1]); $basename = pathinfo($xmlFile, PATHINFO_FILENAME); // Try to extract element name from XML tag $xml = @simplexml_load_string($content); $element = ''; if ($xml !== false) { // Package manifests have element // Component/module manifests have or use filename $element = (string) ($xml->element ?? ''); if ($element === '') { $element = strtolower($basename); } } else { $element = strtolower($basename); } // Derive display name $displayName = (string) ($xml->name ?? ucfirst(str_replace('_', ' ', $basename))); return [ 'platform' => 'joomla', 'extension_type' => $extType, 'element_name' => $element, 'display_name' => $displayName, 'language' => 'PHP', 'entry_point' => ($srcDir ?? '.') . '/', ]; } // Also check for pkg_*.xml pattern specifically $pkgFiles = glob("{$dir}/pkg_*.xml") ?: []; if (!empty($pkgFiles)) { $basename = pathinfo($pkgFiles[0], PATHINFO_FILENAME); return [ 'platform' => 'joomla', 'extension_type' => 'package', 'element_name' => strtolower($basename), 'display_name' => ucfirst(str_replace('_', ' ', $basename)), 'language' => 'PHP', 'entry_point' => ($srcDir ?? '.') . '/', ]; } } // Check for com_*/manifest.xml pattern (component subdirectory) $comDirs = glob("{$root}/com_*", GLOB_ONLYDIR) ?: []; foreach ($comDirs as $comDir) { $comManifest = glob("{$comDir}/*.xml") ?: []; foreach ($comManifest as $xmlFile) { $content = @file_get_contents($xmlFile); if ($content && preg_match('/]*type="component"/', $content)) { return [ 'platform' => 'joomla', 'extension_type' => 'component', 'element_name' => strtolower(basename($comDir)), 'display_name' => ucfirst(str_replace('com_', '', basename($comDir))), 'language' => 'PHP', 'entry_point' => ($srcDir ?? '.') . '/', ]; } } } return null; } /** * Detect Dolibarr platform by scanning for module descriptor files. */ private function detectDolibarr(string $root): ?array { // Look for mod*.class.php containing DolibarrModules $searchPaths = [ "{$root}/core/modules/mod*.class.php", "{$root}/*/core/modules/mod*.class.php", ]; foreach ($searchPaths as $pattern) { $files = glob($pattern) ?: []; foreach ($files as $file) { $content = @file_get_contents($file); if ($content && str_contains($content, 'DolibarrModules')) { $modName = pathinfo($file, PATHINFO_FILENAME); // modMyModule.class → mymodule $element = strtolower(preg_replace('/^mod/', '', str_replace('.class', '', $modName))); return [ 'platform' => 'dolibarr', 'extension_type' => 'module', 'element_name' => $element, 'display_name' => ucfirst($element), 'language' => 'PHP', 'entry_point' => './', ]; } } } // Secondary: check for update.txt (Dolibarr marker) if (file_exists("{$root}/update.txt")) { return [ 'platform' => 'dolibarr', 'extension_type' => 'module', 'element_name' => strtolower(basename($root)), 'display_name' => basename($root), 'language' => 'PHP', 'entry_point' => './', ]; } return null; } /** * Detect generic platform type (php, nodejs, python, etc.) from project files. */ private function detectGenericPlatform(string $root): string { if (file_exists("{$root}/composer.json")) { return 'php'; } if (file_exists("{$root}/package.json")) { return 'nodejs'; } if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) { return 'python'; } if (file_exists("{$root}/go.mod")) { return 'go'; } if (file_exists("{$root}/Cargo.toml")) { return 'rust'; } return 'generic'; } /** * Detect primary language from project files. */ private function detectLanguage(string $root): string { if (file_exists("{$root}/composer.json")) { return 'PHP'; } if (file_exists("{$root}/tsconfig.json")) { return 'TypeScript'; } if (file_exists("{$root}/package.json")) { return 'JavaScript'; } if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) { return 'Python'; } return ''; } // ── Org/repo resolution ────────────────────────────────────────────── /** * Resolve org and repo name from environment or git remote. * * @return array{0: string, 1: string} [org, repo] */ private function resolveOrgRepo(string $root): array { // 1. GITHUB_REPOSITORY env (set in Gitea Actions / GitHub Actions) $envRepo = getenv('GITHUB_REPOSITORY') ?: ''; if ($envRepo !== '' && str_contains($envRepo, '/')) { return explode('/', $envRepo, 2); } // 2. Parse git remote origin URL $remoteUrl = trim((string) shell_exec( 'git -C ' . escapeshellarg($root) . ' remote get-url origin 2>/dev/null' )); if ($remoteUrl !== '') { // SSH: git@host:Org/Repo.git or HTTPS: https://host/Org/Repo.git if (preg_match('#[/:]([^/]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) { return [$m[1], $m[2]]; } } return ['', basename($root)]; } // ── Backward-compatible aliases ────────────────────────────────────── /** * Add hyphenated aliases for underscore fields (backward compat with old manifest.xml consumers). * Also map old field names to new ones. */ private function addAliases(array $fields): array { // Map API field names → old manifest.xml hyphenated names $aliases = [ 'display_name' => 'display-name', 'license_spdx' => 'license-spdx', 'license_name' => 'license', 'standards_version' => 'standards-version', 'standards_source' => 'standards-source', 'extension_type' => 'package-type', 'entry_point' => 'entry-point', 'element_name' => 'name', ]; foreach ($aliases as $newKey => $oldKey) { if (isset($fields[$newKey]) && !isset($fields[$oldKey])) { $fields[$oldKey] = $fields[$newKey]; } } return $fields; } // ── Output ─────────────────────────────────────────────────────────── private function outputFields(array $fields, string $mode, string $field): int { switch ($mode) { case 'field': if ($field === '') { $this->log('ERROR', "Usage: manifest:read --path --field "); $this->log('ERROR', " manifest:read --path --all"); $this->log('ERROR', " manifest:read --path --json"); $this->log('ERROR', " manifest:read --path --github-output"); return 2; } echo ($fields[$field] ?? '') . "\n"; break; case 'all': foreach ($fields as $k => $v) { echo "{$k}={$v}\n"; } break; case 'json': echo json_encode($fields, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; break; case 'github-output': $outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: ''; $lines = []; foreach ($fields as $k => $v) { $envKey = str_replace('-', '_', $k); $lines[$envKey] = "{$envKey}={$v}\n"; } // Deduplicate (aliases may collide after underscore conversion) $output = implode('', $lines); if ($outputFile === '') { $this->log('WARNING', 'GITHUB_OUTPUT not set — printing to stdout'); echo $output; } else { file_put_contents($outputFile, $output, FILE_APPEND); $this->log('INFO', "Wrote " . count($lines) . " fields to GITHUB_OUTPUT"); } break; } return 0; } } $app = new ManifestReadCli(); exit($app->execute());