#!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: mokoplatform.CLI * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/metadata_read.php * VERSION: 09.26.01 * BRIEF: Read and set metadata fields in .mokogitea/metadata.xml (or manifest.xml) */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; /** Field name → XPath mapping into the metadata XML */ const FIELD_MAP = [ // identity 'name' => 'identity/name', 'display-name' => 'identity/display-name', 'org' => 'identity/org', 'description' => 'identity/description', 'license' => 'identity/license', 'version' => 'identity/version', // governance 'platform' => 'governance/platform', 'standards-version' => 'governance/standards-version', 'standards-source' => 'governance/standards-source', // build 'language' => 'build/language', 'package-type' => 'build/package-type', 'entry-point' => 'build/entry-point', // deploy 'source-dir' => 'deploy/source-dir', 'remote-subdir' => 'deploy/remote-subdir', 'excludes' => 'deploy/excludes', 'dev-host' => 'deploy/dev-host', 'demo-host' => 'deploy/demo-host', ]; class MetadataReadCli extends CliFramework { protected function configure(): void { $this->setDescription('Read or set metadata fields in .mokogitea/metadata.xml'); $this->addArgument('--path', 'Repository root path', '.'); $this->addArgument('--field', 'Single field name to read', ''); $this->addArgument('--set', 'Set field value (field=value), repeatable', ''); $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'); $setValue = $this->getArgument('--set'); $showAll = $this->getArgument('--all'); $ghOutput = $this->getArgument('--github-output'); $jsonMode = $this->getArgument('--json'); $root = realpath($path) ?: $path; // -- Locate metadata file -- $metadataFile = $this->findMetadataFile($root); if ($metadataFile === null) { $this->log('ERROR', "No metadata file found in {$root}"); return 1; } // -- Auto-migrate manifest.xml → metadata.xml -- $metadataFile = $this->migrateIfNeeded($metadataFile, $root); // -- Set mode -- if ($setValue !== '') { return $this->handleSet($metadataFile, $setValue); } // -- Read mode -- $xml = @simplexml_load_file($metadataFile); if ($xml === false) { // Fallback: legacy YAML format (.mokoplatform) $fields = $this->parseLegacy($metadataFile); } else { $fields = $this->parseXml($xml, $metadataFile); } $fields = array_filter($fields, fn($v) => $v !== ''); return $this->outputFields($fields, $field, $showAll, $ghOutput, $jsonMode); } private function findMetadataFile(string $root): ?string { $candidates = [ "{$root}/.mokogitea/metadata.xml", "{$root}/.mokogitea/manifest.xml", "{$root}/.mokogitea/.manifest.xml", "{$root}/.mokogitea/.mokoplatform", ]; foreach ($candidates as $candidate) { if (file_exists($candidate)) { return $candidate; } } return null; } private function migrateIfNeeded(string $metadataFile, string $root): string { $newPath = "{$root}/.mokogitea/metadata.xml"; // Already at the new location if ($metadataFile === $newPath) { return $metadataFile; } // Legacy file found — migrate if (str_ends_with($metadataFile, '.mokoplatform')) { // YAML legacy — can't auto-migrate, just warn $this->log('WARN', "Legacy .mokoplatform format detected — migrate to metadata.xml manually"); return $metadataFile; } // manifest.xml or .manifest.xml → metadata.xml copy($metadataFile, $newPath); unlink($metadataFile); $this->log('INFO', "Migrated " . basename($metadataFile) . " → metadata.xml"); return $newPath; } private function parseXml(\SimpleXMLElement $xml, string $filePath): array { $fields = []; foreach (FIELD_MAP as $name => $xpath) { $parts = explode('/', $xpath); $node = $xml; foreach ($parts as $part) { $node = $node->{$part} ?? null; if ($node === null) break; } if ($name === 'license' && $node !== null) { // Also extract spdx attribute $fields['license'] = (string)$node; $fields['license-spdx'] = (string)($node['spdx'] ?? ''); } else { $fields[$name] = $node !== null ? (string)$node : ''; } } $fields['metadata-file'] = $filePath; return $fields; } private function parseLegacy(string $filePath): array { $content = file_get_contents($filePath); $fields = []; if (preg_match('/^platform:\s*(.+)/m', $content, $m)) { $fields['platform'] = trim($m[1], " \t\n\r\"'"); } if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) { $fields['standards-version'] = trim($m[1], " \t\n\r\"'"); } if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) { $fields['name'] = trim($m[1], " \t\n\r\"'"); } return $fields; } private function handleSet(string $metadataFile, string $setValue): int { // Parse field=value pairs (comma-separated or from repeated --set) $pairs = []; foreach (explode(',', $setValue) as $pair) { $pair = trim($pair); if ($pair === '') continue; $eq = strpos($pair, '='); if ($eq === false) { $this->log('ERROR', "Invalid set format: '{$pair}' — expected field=value"); return 1; } $key = trim(substr($pair, 0, $eq)); $val = trim(substr($pair, $eq + 1)); $pairs[$key] = $val; } if (empty($pairs)) { $this->log('ERROR', 'No field=value pairs provided'); return 1; } // Validate all fields exist in FIELD_MAP foreach ($pairs as $key => $val) { if (!isset(FIELD_MAP[$key])) { $this->log('ERROR', "Unknown field: '{$key}'"); $this->log('INFO', 'Valid fields: ' . implode(', ', array_keys(FIELD_MAP))); return 1; } } // Legacy files are read-only if (str_ends_with($metadataFile, '.mokoplatform')) { $this->log('ERROR', 'Cannot set fields on legacy .mokoplatform format — migrate to metadata.xml first'); return 1; } // Load XML $xml = @simplexml_load_file($metadataFile); if ($xml === false) { $this->log('ERROR', "Failed to parse XML: {$metadataFile}"); return 1; } // Set each field foreach ($pairs as $key => $val) { $xpath = FIELD_MAP[$key]; $parts = explode('/', $xpath); $section = $parts[0]; $element = $parts[1]; if (!isset($xml->{$section})) { $this->log('ERROR', "Section <{$section}> not found in XML — cannot set '{$key}'"); return 1; } if (!isset($xml->{$section}->{$element})) { $this->log('ERROR', "Element <{$element}> not found in <{$section}> — cannot set '{$key}'"); return 1; } $old = (string)$xml->{$section}->{$element}; $xml->{$section}->{$element} = $val; $this->log('INFO', "Set {$key}: '{$old}' → '{$val}'"); } // Write back with preserved formatting $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadXML($xml->asXML()); $dom->save($metadataFile); $this->log('INFO', "Updated {$metadataFile}"); return 0; } private function outputFields(array $fields, string $field, $showAll, $ghOutput, $jsonMode): int { if ($ghOutput) { $mode = 'github-output'; } elseif ($showAll) { $mode = 'all'; } elseif ($jsonMode) { $mode = 'json'; } else { $mode = 'field'; } switch ($mode) { case 'field': if ($field === '') { $this->log('ERROR', "Usage: metadata_read.php --path --field "); $this->log('ERROR', " metadata_read.php --path --all"); $this->log('ERROR', " metadata_read.php --path --json"); $this->log('ERROR', " metadata_read.php --path --set field=value"); 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'); if ($outputFile === false || $outputFile === '') { $this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead'); foreach ($fields as $k => $v) { $envKey = str_replace('-', '_', $k); echo "{$envKey}={$v}\n"; } } else { $fh = fopen($outputFile, 'a'); foreach ($fields as $k => $v) { $envKey = str_replace('-', '_', $k); fwrite($fh, "{$envKey}={$v}\n"); } fclose($fh); $this->log('INFO', "Wrote " . count($fields) . " fields to GITHUB_OUTPUT"); } break; } return 0; } } $app = new MetadataReadCli(); exit($app->execute());