From a00cbf7d926629bb512c1f43d6c53a13aaeb505e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 18:20:57 -0500 Subject: [PATCH] feat: add --set support and auto-migration to metadata_read.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --set field=value (comma-separated) to update metadata XML fields - FIELD_MAP defines all first-class fields with XML section/element paths - Validates field names and refuses unknown fields - Auto-migrates manifest.xml → metadata.xml on first read (copies + deletes old) - Legacy .mokoplatform format remains read-only with migration warning --- cli/metadata_read.php | 313 +++++++++++++++++++++++++++++++----------- 1 file changed, 230 insertions(+), 83 deletions(-) diff --git a/cli/metadata_read.php b/cli/metadata_read.php index 988b39c..2f37a23 100644 --- a/cli/metadata_read.php +++ b/cli/metadata_read.php @@ -9,9 +9,9 @@ * DEFGROUP: mokoplatform.CLI * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform - * PATH: /cli/manifest_read.php - * VERSION: 09.26.00 - * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption + * PATH: /cli/metadata_read.php + * VERSION: 09.27.00 + * BRIEF: Read and set metadata fields in .mokogitea/metadata.xml (or manifest.xml) */ declare(strict_types=1); @@ -20,13 +20,39 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; -class ManifestReadCli extends 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('Parse manifest.xml and output requested field(s) for CI consumption'); + $this->setDescription('Read or set metadata fields in .mokogitea/metadata.xml'); $this->addArgument('--path', 'Repository root path', '.'); - $this->addArgument('--field', 'Single field name to output', ''); + $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); @@ -34,13 +60,203 @@ class ManifestReadCli extends CliFramework protected function run(): int { - $path = $this->getArgument('--path'); - $field = $this->getArgument('--field'); + $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'); - // Determine mode + $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) { @@ -51,81 +267,13 @@ class ManifestReadCli extends CliFramework $mode = 'field'; } - // -- Locate manifest -- - $root = realpath($path) ?: $path; - $manifestFile = null; - - // Priority: manifest.xml (current standard) - $candidates = [ - "{$root}/.mokogitea/manifest.xml", - "{$root}/.mokogitea/.manifest.xml", // legacy (dot-prefixed) - "{$root}/.mokogitea/.mokoplatform", // legacy v4 - ]; - - foreach ($candidates as $candidate) { - if (file_exists($candidate)) { - $manifestFile = $candidate; - break; - } - } - - if ($manifestFile === null) { - $this->log('ERROR', "No manifest found in {$root}"); - return 1; - } - - // -- Parse XML -- - $xml = @simplexml_load_file($manifestFile); - - if ($xml === false) { - // Fallback: try YAML format (.mokostandards legacy) - $content = file_get_contents($manifestFile); - $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\"'"); - } - } else { - // Register namespace for XPath (optional, simple path works without) - $fields = [ - 'name' => (string)($xml->identity->name ?? ''), - 'display-name' => (string)($xml->identity->{"display-name"} ?? ''), - 'org' => (string)($xml->identity->org ?? ''), - 'description' => (string)($xml->identity->description ?? ''), - 'license' => (string)($xml->identity->license ?? ''), - 'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''), - 'platform' => (string)($xml->governance->platform ?? ''), - 'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''), - 'standards-source' => (string)($xml->governance->{"standards-source"} ?? ''), - 'language' => (string)($xml->build->language ?? ''), - 'package-type' => (string)($xml->build->{"package-type"} ?? ''), - 'entry-point' => (string)($xml->build->{"entry-point"} ?? ''), - 'version' => (string)($xml->identity->version ?? ''), - 'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''), - 'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''), - 'excludes' => (string)($xml->deploy->excludes ?? ''), - 'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''), - 'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''), - 'manifest-file' => $manifestFile, - ]; - } - - // Strip empty values for cleaner output - $fields = array_filter($fields, fn($v) => $v !== ''); - - // -- Output -- switch ($mode) { case 'field': if ($field === '') { - $this->log('ERROR', "Usage: manifest_read.php --path --field "); - $this->log('ERROR', " manifest_read.php --path --all"); - $this->log('ERROR', " manifest_read.php --path --json"); - $this->log('ERROR', " manifest_read.php --path --github-output"); + $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"; @@ -146,7 +294,6 @@ class ManifestReadCli extends CliFramework if ($outputFile === false || $outputFile === '') { $this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead'); foreach ($fields as $k => $v) { - // Convert field-name to FIELD_NAME for env var style $envKey = str_replace('-', '_', $k); echo "{$envKey}={$v}\n"; } @@ -166,5 +313,5 @@ class ManifestReadCli extends CliFramework } } -$app = new ManifestReadCli(); +$app = new MetadataReadCli(); exit($app->execute());