From 5e25c6e77b2046c53f15e84efc62f4999ecce6a3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 17:54:58 -0500 Subject: [PATCH 1/5] feat: consolidate manifest CLI tools and template updates - manifest_detect.php: add display_name, target_version, php_minimum detection - manifest_integrity.php: new org-wide manifest validation tool (564 lines) - templates: update Joomla Makefile and composer.json with MokoSuite references --- cli/manifest_detect.php | 53 ++- cli/manifest_integrity.php | 564 +++++++++++++++++++++++++++ templates/repos/joomla/Makefile | 9 +- templates/repos/joomla/composer.json | 4 +- 4 files changed, 614 insertions(+), 16 deletions(-) create mode 100644 cli/manifest_integrity.php diff --git a/cli/manifest_detect.php b/cli/manifest_detect.php index c3251d3..c0e5e57 100644 --- a/cli/manifest_detect.php +++ b/cli/manifest_detect.php @@ -161,15 +161,18 @@ class ManifestDetectCli extends CliFramework $platform = $this->detectPlatform($root); $fields = [ - 'platform' => $platform, - 'name' => '', - 'description' => '', - 'version' => '', - 'element_name' => '', - 'package_type' => '', - 'language' => '', - 'entry_point' => '', - 'license_spdx' => '', + 'platform' => $platform, + 'name' => '', + 'description' => '', + 'version' => '', + 'element_name' => '', + 'package_type' => '', + 'language' => '', + 'entry_point' => '', + 'license_spdx' => '', + 'display_name' => '', + 'target_version' => '', + 'php_minimum' => '', ]; switch ($platform) { @@ -316,7 +319,13 @@ class ManifestDetectCli extends CliFramework ]; if (isset($prefixMap[$extType])) { $prefix = $prefixMap[$extType]; - if (strpos($element, $prefix) !== 0 && strpos($element, '_') === false) { + // Only add prefix if not already present (check all known prefixes) + $hasPrefix = false; + foreach ($prefixMap as $p) { + if (strpos($element, $p) === 0) { $hasPrefix = true; break; } + } + if (strpos($element, 'plg_') === 0) { $hasPrefix = true; } + if (!$hasPrefix) { $element = $prefix . $element; } } elseif ($extType === 'plugin') { @@ -349,6 +358,30 @@ class ManifestDetectCli extends CliFramework } } + // Display name for update feeds + if (!empty($fields['name'])) { + $name = $fields['name']; + // If name already has "Type - " prefix, use as-is + if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) { + $fields['display_name'] = $name; + } elseif (!empty($extType)) { + $fields['display_name'] = ucfirst($extType) . ' - ' . $name; + } + } + + // Target Joomla version + if (preg_match('/]*version="([^"]+)"/', $xml, $m)) { + $fields['target_version'] = trim($m[1]); + } else { + // Default for Joomla 5/6 + $fields['target_version'] = '(5|6)\..*'; + } + + // PHP minimum + if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) { + $fields['php_minimum'] = trim($m[1]); + } + // License if (preg_match('/([^<]+)<\/license>/', $xml, $m)) { $fields['license_spdx'] = $this->normalizeLicense(trim($m[1])); diff --git a/cli/manifest_integrity.php b/cli/manifest_integrity.php new file mode 100644 index 0000000..be93418 --- /dev/null +++ b/cli/manifest_integrity.php @@ -0,0 +1,564 @@ +#!/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/manifest_integrity.php + * VERSION: 09.26.00 + * BRIEF: Cross-check manifest API fields against repo contents across the org + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\CliFramework; + +class ManifestIntegrityCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Cross-check manifest fields against repo contents across the org'); + $this->addArgument('--path', 'Single repo path (local mode)', ''); + $this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting'); + $this->addArgument('--repo', 'Single repo name (remote mode)', ''); + $this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', ''); + $this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1'); + $this->addArgument('--fix', 'Push fixes for detected drift', false); + $this->addArgument('--json', 'Output as JSON', false); + $this->addArgument('--quiet', 'Only show repos with issues', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $org = $this->getArgument('--org'); + $repoName = $this->getArgument('--repo'); + $token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: ''; + $apiBase = rtrim($this->getArgument('--api-base'), '/'); + $fixMode = (bool) $this->getArgument('--fix'); + $jsonMode = (bool) $this->getArgument('--json'); + $quiet = (bool) $this->getArgument('--quiet'); + + if ($token === '') { + $this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)'); + return 1; + } + + // ── Mode selection ────────────────────────────────────────── + if ($path !== '') { + // Local mode: detect from source + compare to API + return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode); + } + + if ($repoName !== '') { + // Single remote repo + return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode); + } + + // Bulk mode: all repos in org + return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet); + } + + // ===================================================================== + // Local mode — detect from source, compare to API + // ===================================================================== + + private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int + { + $root = realpath($path) ?: $path; + if (!is_dir($root)) { + $this->log('ERROR', "Path does not exist: {$path}"); + return 1; + } + + if ($repoName === '') { + $repoName = $this->detectRepoName($root); + } + + // Run manifest_detect logic + $detected = $this->runDetect($root, $repoName); + $current = $this->fetchManifest($apiBase, $org, $repoName, $token); + + if ($current === null) { + $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); + return 1; + } + + $issues = $this->validate($current, $detected, $repoName); + + if ($json) { + echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $this->printIssues($repoName, $issues); + } + + if ($fix && !empty($issues)) { + return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); + } + + return empty($issues) ? 0 : 1; + } + + // ===================================================================== + // Remote single repo mode — fetch source files via API + // ===================================================================== + + private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int + { + $current = $this->fetchManifest($apiBase, $org, $repoName, $token); + if ($current === null) { + $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); + return 1; + } + + $issues = $this->validateManifestOnly($current, $repoName); + + if ($json) { + echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $this->printIssues($repoName, $issues); + } + + if ($fix && !empty($issues)) { + return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); + } + + return empty($issues) ? 0 : 1; + } + + // ===================================================================== + // Bulk org mode — check all repos + // ===================================================================== + + private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int + { + $repos = $this->fetchOrgRepos($apiBase, $org, $token); + if ($repos === null) { + $this->log('ERROR', "Failed to fetch repos for org {$org}"); + return 1; + } + + $this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)"); + + $allResults = []; + $totalIssues = 0; + $reposWithIssues = 0; + + foreach ($repos as $repo) { + $name = $repo['name']; + $manifest = $this->fetchManifest($apiBase, $org, $name, $token); + + if ($manifest === null) { + if (!$quiet) { + $this->log('WARN', "{$name}: no manifest"); + } + continue; + } + + $issues = $this->validateManifestOnly($manifest, $name); + + if (!empty($issues)) { + $reposWithIssues++; + $totalIssues += count($issues); + + if ($json) { + $allResults[] = ['repo' => $name, 'issues' => $issues]; + } else { + $this->printIssues($name, $issues); + } + + if ($fix) { + $this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues); + } + } elseif (!$quiet && !$json) { + $this->log('OK', "{$name}: clean"); + } + } + + if ($json) { + echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + echo "\n"; + $level = $reposWithIssues > 0 ? 'WARN' : 'OK'; + $this->log($level, sprintf( + 'Summary: %d repos checked, %d with issues (%d total issues)', + count($repos), + $reposWithIssues, + $totalIssues + )); + } + + return $reposWithIssues > 0 ? 1 : 0; + } + + // ===================================================================== + // Validation rules + // ===================================================================== + + /** + * Full validation: compare API manifest against locally-detected fields. + */ + private function validate(array $current, array $detected, string $repoName): array + { + $issues = []; + + // Required fields that should never be empty + $required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point']; + foreach ($required as $field) { + if (empty($current[$field])) { + $fix = $detected[$field] ?? null; + $issues[] = [ + 'field' => $field, + 'severity' => 'error', + 'message' => 'Missing required field', + 'current' => '', + 'fix' => $fix, + ]; + } + } + + // Drift detection: detected value differs from API + foreach ($detected as $field => $detectedValue) { + $currentValue = $current[$field] ?? ''; + if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) { + // Version drift is expected on dev branches (suffix) + if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) { + continue; // e.g., detected "02.34.50-dev" vs API "02.34.50" + } + if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) { + continue; + } + + $issues[] = [ + 'field' => $field, + 'severity' => 'warn', + 'message' => 'Drift: source differs from manifest', + 'current' => $currentValue, + 'fix' => $detectedValue, + ]; + } + } + + // Platform-specific structure validation + $platform = $current['platform'] ?? ''; + $issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName)); + + return $issues; + } + + /** + * API-only validation: check manifest fields for completeness and consistency + * without access to source files. + */ + private function validateManifestOnly(array $manifest, string $repoName): array + { + $issues = []; + + // Required fields + $required = ['platform', 'name', 'version', 'language']; + foreach ($required as $field) { + if (empty($manifest[$field])) { + $issues[] = [ + 'field' => $field, + 'severity' => 'error', + 'message' => 'Missing required field', + 'current' => '', + 'fix' => null, + ]; + } + } + + // Recommended fields + $recommended = ['package_type', 'entry_point', 'license_spdx', 'description']; + foreach ($recommended as $field) { + if (empty($manifest[$field])) { + $issues[] = [ + 'field' => $field, + 'severity' => 'info', + 'message' => 'Recommended field is empty', + 'current' => '', + 'fix' => null, + ]; + } + } + + // Platform-specific checks + $platform = $manifest['platform'] ?? ''; + $issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName)); + + return $issues; + } + + /** + * Platform-specific validation rules. + */ + private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array + { + $issues = []; + + switch ($platform) { + case 'joomla': + case 'waas-component': + // Joomla repos must have element_name + if (empty($manifest['element_name'])) { + $issues[] = [ + 'field' => 'element_name', + 'severity' => 'error', + 'message' => 'Joomla repos require element_name', + 'current' => '', + 'fix' => null, + ]; + } + // Language should be PHP + if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Joomla repos should have language=PHP', + 'current' => $manifest['language'], + 'fix' => 'PHP', + ]; + } + break; + + case 'dolibarr': + case 'crm-module': + if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Dolibarr repos should have language=PHP', + 'current' => $manifest['language'], + 'fix' => 'PHP', + ]; + } + break; + + case 'go': + if (!empty($manifest['language']) && $manifest['language'] !== 'Go') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Go repos should have language=Go', + 'current' => $manifest['language'], + 'fix' => 'Go', + ]; + } + break; + + case 'mcp': + if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'MCP repos should have language=TypeScript or JavaScript', + 'current' => $manifest['language'], + 'fix' => null, + ]; + } + break; + } + + // Version format check: should be XX.YY.ZZ + $version = $manifest['version'] ?? ''; + if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) { + // Allow semver for node/go repos + if (!in_array($platform, ['mcp', 'node', 'go'], true)) { + $issues[] = [ + 'field' => 'version', + 'severity' => 'info', + 'message' => 'Version does not match XX.YY.ZZ format', + 'current' => $version, + 'fix' => null, + ]; + } + } + + return $issues; + } + + // ===================================================================== + // Output + // ===================================================================== + + private function printIssues(string $repoName, array $issues): void + { + if (empty($issues)) { + return; + } + + $errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error')); + $warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn')); + $infos = count($issues) - $errors - $warns; + + echo "\n"; + $summary = []; + if ($errors > 0) $summary[] = "{$errors} error(s)"; + if ($warns > 0) $summary[] = "{$warns} warning(s)"; + if ($infos > 0) $summary[] = "{$infos} info"; + $this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary)); + + foreach ($issues as $issue) { + $icon = match ($issue['severity']) { + 'error' => 'ERROR', + 'warn' => 'WARN', + default => 'INFO', + }; + $msg = sprintf(' %-18s %s', $issue['field'], $issue['message']); + if ($issue['current'] !== '') { + $msg .= " (current: {$issue['current']})"; + } + if ($issue['fix'] !== null) { + $msg .= " → fix: {$issue['fix']}"; + } + $this->log($icon, $msg); + } + } + + // ===================================================================== + // Fix application + // ===================================================================== + + private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int + { + $fixes = []; + foreach ($issues as $issue) { + if ($issue['fix'] !== null && $issue['fix'] !== '') { + $fixes[$issue['field']] = $issue['fix']; + } + } + + if (empty($fixes)) { + $this->log('INFO', "{$repo}: no auto-fixable issues"); + return 0; + } + + $merged = array_merge($current, $fixes); + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $payload = json_encode($merged); + + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", + 'content' => $payload, + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) { + $this->log('ERROR', "{$repo}: failed to push fixes"); + return 1; + } + + $this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes))); + return 0; + } + + // ===================================================================== + // API helpers + // ===================================================================== + + private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array + { + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $ctx = stream_context_create([ + 'http' => [ + 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) return null; + + $data = json_decode($body, true); + return is_array($data) ? $data : null; + } + + private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array + { + $allRepos = []; + $page = 1; + $limit = 50; + + while (true) { + $url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}"; + $ctx = stream_context_create([ + 'http' => [ + 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", + 'timeout' => 15, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) return null; + + $repos = json_decode($body, true); + if (!is_array($repos) || empty($repos)) break; + + $allRepos = array_merge($allRepos, $repos); + + if (count($repos) < $limit) break; + $page++; + } + + // Filter out archived and empty repos + return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false)); + } + + // ===================================================================== + // Detection (delegates to manifest_detect logic) + // ===================================================================== + + private function runDetect(string $root, string $repoName): array + { + $script = __DIR__ . '/manifest_detect.php'; + $redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null'; + $cmd = sprintf( + 'php %s --path %s --repo %s --json --quiet %s', + escapeshellarg($script), + escapeshellarg($root), + escapeshellarg($repoName), + $redirect + ); + + $output = shell_exec($cmd) ?? ''; + + // Extract JSON object from output (skip banner/log lines) + if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) { + $data = json_decode($m[0], true); + if (is_array($data)) { + return $data; + } + } + + return []; + } + + private function detectRepoName(string $root): string + { + $gitConfig = "{$root}/.git/config"; + if (!file_exists($gitConfig)) { + return basename($root); + } + + $content = file_get_contents($gitConfig); + if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) { + return $m[1]; + } + + return basename($root); + } +} + +$app = new ManifestIntegrityCli(); +exit($app->execute()); diff --git a/templates/repos/joomla/Makefile b/templates/repos/joomla/Makefile index 15fbfde..c8b025b 100644 --- a/templates/repos/joomla/Makefile +++ b/templates/repos/joomla/Makefile @@ -2,14 +2,15 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later # -# MokoJoomGallery — Photo gallery management for Joomla +# MokoSuite — Joomla extension template +# Replace EXTENSION_NAME with your extension's element name. # ============================================================================== # CONFIGURATION - Customize these for your extension # ============================================================================== # Extension Configuration -EXTENSION_NAME := mokojoomgallery +EXTENSION_NAME := mokosuite EXTENSION_TYPE := package # Options: module, plugin, component, package, template EXTENSION_VERSION := 1.0.0 @@ -132,11 +133,11 @@ build: clean minify ## Build extension package @# --- Build the outer package ZIP --- @echo " Assembling pkg_$(EXTENSION_NAME)..." - @cp $(SRC_DIR)/pkg_mokojoomgallery.xml $(BUILD_DIR)/pkg_mokojoomgallery.xml + @cp $(SRC_DIR)/pkg_$(EXTENSION_NAME).xml $(BUILD_DIR)/pkg_$(EXTENSION_NAME).xml @cp $(SRC_DIR)/script.php $(BUILD_DIR)/script.php @[ -d "$(SRC_DIR)/language" ] && cp -r $(SRC_DIR)/language $(BUILD_DIR)/language || true @cd $(BUILD_DIR) && $(ZIP) -r "$(CURDIR)/$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" \ - pkg_mokojoomgallery.xml script.php language/ packages/ + pkg_$(EXTENSION_NAME).xml script.php language/ packages/ @echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)" @echo " Contents:" diff --git a/templates/repos/joomla/composer.json b/templates/repos/joomla/composer.json index 10afe3a..e47af58 100644 --- a/templates/repos/joomla/composer.json +++ b/templates/repos/joomla/composer.json @@ -1,6 +1,6 @@ { - "name": "mokoconsulting/mokojoomgallery", - "description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display", + "name": "mokoconsulting/mokosuite", + "description": "Joomla extension — replace with your extension description", "type": "joomla-package", "version": "01.00.00", "license": "GPL-3.0-or-later", -- 2.52.0 From e20423f323666d732261452e71d628a06800fa7e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 18:07:45 -0500 Subject: [PATCH 2/5] docs: update changelog for MCP extraction and npm publishing MCP servers extracted to standalone repos, published to npm and Gitea registry, manifest CLI tools consolidated. --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a42b72..ca2d889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,20 @@ BRIEF: Release changelog # Changelog ## [Unreleased] +### Added +- `cli/manifest_integrity.php` — org-wide manifest validation tool (564 lines) +- `manifest_detect.php` — detect `display_name`, `target_version`, `php_minimum` fields + +### Changed +- MCP servers extracted from monorepo to standalone `A:/MCP/` directories +- All 9 MCP servers published to npm (`@mokoconsulting/`) and Gitea package registry +- `.mcp.json` converted from local file paths to `npx -y @mokoconsulting/...@latest` +- `NPM_TOKEN` saved as MokoConsulting org secret for CI/CD +- Templates: Joomla Makefile and composer.json updated with MokoSuite references + +### Removed +- `mcp/servers/` directory — all MCP server source moved to `A:/MCP/mcp_*/` + ## [09.26.00] --- 2026-06-07 ### Added -- 2.52.0 From 14ffe531587c3f247e733a49f7b026c99dfc0cef Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 18:16:18 -0500 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20rename=20manifest=20=E2=86=92?= =?UTF-8?q?=20metadata=20with=20backward-compatible=20wrappers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manifest_read.php → metadata_read.php (+ wrapper) - manifest_detect.php → metadata_detect.php (+ wrapper) - manifest_element.php → metadata_element.php (+ wrapper) - manifest_integrity.php → metadata_integrity.php (+ wrapper) - manifest_licensing.php → metadata_licensing.php (+ wrapper) - .mokogitea/manifest.xml → .mokogitea/metadata.xml Old manifest_* files now require() the new metadata_* counterparts for backward compatibility with existing workflows and scripts. --- .mokogitea/metadata.xml | 0 cli/manifest_detect.php | 749 +------------------------------------ cli/manifest_element.php | 191 +--------- cli/manifest_integrity.php | 564 +--------------------------- cli/manifest_licensing.php | 280 +------------- cli/manifest_read.php | 170 +-------- cli/metadata_detect.php | 749 +++++++++++++++++++++++++++++++++++++ cli/metadata_element.php | 191 ++++++++++ cli/metadata_integrity.php | 564 ++++++++++++++++++++++++++++ cli/metadata_licensing.php | 280 ++++++++++++++ cli/metadata_read.php | 170 +++++++++ 11 files changed, 1964 insertions(+), 1944 deletions(-) create mode 100644 .mokogitea/metadata.xml create mode 100644 cli/metadata_detect.php create mode 100644 cli/metadata_element.php create mode 100644 cli/metadata_integrity.php create mode 100644 cli/metadata_licensing.php create mode 100644 cli/metadata_read.php diff --git a/.mokogitea/metadata.xml b/.mokogitea/metadata.xml new file mode 100644 index 0000000..e69de29 diff --git a/cli/manifest_detect.php b/cli/manifest_detect.php index c0e5e57..2438237 100644 --- a/cli/manifest_detect.php +++ b/cli/manifest_detect.php @@ -1,749 +1,4 @@ #!/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/manifest_detect.php - * VERSION: 09.26.00 - * BRIEF: Auto-detect manifest fields from source files and optionally push to API - */ - -declare(strict_types=1); - -require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; - -use MokoEnterprise\{CliFramework, SourceResolver}; - -class ManifestDetectCli extends CliFramework -{ - protected function configure(): void - { - $this->setDescription('Auto-detect manifest fields from source files'); - $this->addArgument('--path', 'Repository root path', '.'); - $this->addArgument('--json', 'Output as JSON', false); - $this->addArgument('--diff', 'Show diff against current manifest API values', false); - $this->addArgument('--update', 'Push detected fields to manifest API', false); - $this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', ''); - $this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1'); - $this->addArgument('--org', 'Gitea org', 'MokoConsulting'); - $this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', ''); - $this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $jsonMode = (bool) $this->getArgument('--json'); - $diffMode = (bool) $this->getArgument('--diff'); - $updateMode = (bool) $this->getArgument('--update'); - $ghOutput = (bool) $this->getArgument('--github-output'); - $token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: ''; - $apiBase = rtrim($this->getArgument('--api-base'), '/'); - $org = $this->getArgument('--org'); - $repoName = $this->getArgument('--repo'); - - $root = realpath($path) ?: $path; - - if (!is_dir($root)) { - $this->log('ERROR', "Path does not exist: {$path}"); - return 1; - } - - // Auto-detect repo name from git remote - if ($repoName === '') { - $repoName = $this->detectRepoName($root); - } - - // ── Detect all fields ─────────────────────────────────────── - $detected = $this->detectAll($root, $repoName); - - // ── Warn about missing fields ──────────────────────────────── - $expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point']; - foreach ($expected as $field) { - if (!isset($detected[$field]) || $detected[$field] === '') { - $this->log('WARN', "Could not detect: {$field}"); - } - } - - // ── Output ────────────────────────────────────────────────── - if ($diffMode || $updateMode) { - if ($token === '') { - $this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)'); - return 1; - } - if ($repoName === '') { - $this->log('ERROR', 'Could not determine repo name (use --repo)'); - return 1; - } - - $current = $this->fetchManifest($apiBase, $org, $repoName, $token); - if ($current === null) { - $this->log('ERROR', 'Failed to fetch current manifest from API'); - return 1; - } - - $changes = $this->computeDiff($current, $detected); - - if ($diffMode) { - if (empty($changes)) { - $this->log('INFO', 'No differences — manifest matches source'); - } else { - $this->sectionHeader('Manifest Drift'); - foreach ($changes as $field => $info) { - $this->log('WARN', sprintf( - '%-20s API: %-30s Detected: %s', - $field, - $info['current'] === '' ? '(empty)' : $info['current'], - $info['detected'] - )); - } - } - } - - if ($updateMode) { - if (empty($changes)) { - $this->log('INFO', 'Nothing to update'); - } else { - $update = array_map(fn($i) => $i['detected'], $changes); - $ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update); - if ($ok) { - $this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update))); - } else { - $this->log('ERROR', 'Failed to push manifest update'); - return 1; - } - } - } - - return 0; - } - - if ($ghOutput) { - $outputFile = getenv('GITHUB_OUTPUT'); - $lines = []; - foreach ($detected as $k => $v) { - $envKey = str_replace('-', '_', $k); - $lines[] = "{$envKey}={$v}"; - } - if ($outputFile !== false && $outputFile !== '') { - file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND); - $this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT'); - } else { - $this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead'); - echo implode("\n", $lines) . "\n"; - } - return 0; - } - - if ($jsonMode) { - echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - } else { - foreach ($detected as $k => $v) { - echo "{$k}={$v}\n"; - } - } - - return 0; - } - - // ===================================================================== - // Detection engine - // ===================================================================== - - private function detectAll(string $root, string $repoName): array - { - $platform = $this->detectPlatform($root); - - $fields = [ - 'platform' => $platform, - 'name' => '', - 'description' => '', - 'version' => '', - 'element_name' => '', - 'package_type' => '', - 'language' => '', - 'entry_point' => '', - 'license_spdx' => '', - 'display_name' => '', - 'target_version' => '', - 'php_minimum' => '', - ]; - - switch ($platform) { - case 'joomla': - $this->detectJoomla($root, $repoName, $fields); - break; - case 'dolibarr': - $this->detectDolibarr($root, $repoName, $fields); - break; - case 'go': - $this->detectGo($root, $repoName, $fields); - break; - case 'mcp': - $this->detectNode($root, $repoName, $fields); - break; - case 'node': - $this->detectNode($root, $repoName, $fields); - $fields['platform'] = 'node'; - break; - default: - $this->detectGeneric($root, $repoName, $fields); - break; - } - - // Fallbacks - if ($fields['name'] === '') { - $fields['name'] = $repoName ?: basename($root); - } - if ($fields['entry_point'] === '') { - $fields['entry_point'] = $this->detectEntryPoint($root); - } - if ($fields['license_spdx'] === '') { - $fields['license_spdx'] = $this->detectLicense($root); - } - // description: only from platform-specific source, never guessed - - // Strip empty values - return array_filter($fields, fn($v) => $v !== ''); - } - - // ── Platform detection ────────────────────────────────────────── - - private function detectPlatform(string $root): string - { - // Joomla: look for pkg_*.xml or extension XML in source dirs - $joomlaXmls = array_merge( - SourceResolver::globSource($root, 'pkg_*.xml'), - glob("{$root}/pkg_*.xml") ?: [] - ); - if (!empty($joomlaXmls)) { - return 'joomla'; - } - - // Check source dirs for any Joomla extension XML - foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) { - $content = file_get_contents($xmlFile); - if (strpos($content, 'findJoomlaManifest($root); - if ($extManifest === null) { - return; - } - - $xml = file_get_contents($extManifest); - - // Type - $extType = ''; - if (preg_match('/type="([^"]*)"/', $xml, $m)) { - $extType = $m[1]; - } - $fields['package_type'] = $extType; - - // Element name - $element = ''; - if (preg_match('/([^<]+)<\/element>/', $xml, $m)) { - $element = $m[1]; - } - if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) { - $element = $m[1]; - } - if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) { - $element = $m[1]; - } - if ($extType === 'package' && preg_match('/([^<]+)<\/packagename>/', $xml, $m)) { - $element = $m[1]; - } - if ($element === '') { - $element = strtolower(basename($extManifest, '.xml')); - } - - // Ensure element has type prefix (API stores full element_name like pkg_mokosuite) - $prefixMap = [ - 'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_', - 'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_', - ]; - if (isset($prefixMap[$extType])) { - $prefix = $prefixMap[$extType]; - // Only add prefix if not already present (check all known prefixes) - $hasPrefix = false; - foreach ($prefixMap as $p) { - if (strpos($element, $p) === 0) { $hasPrefix = true; break; } - } - if (strpos($element, 'plg_') === 0) { $hasPrefix = true; } - if (!$hasPrefix) { - $element = $prefix . $element; - } - } elseif ($extType === 'plugin') { - $folder = ''; - if (preg_match('/group="([^"]*)"/', $xml, $gm)) { - $folder = $gm[1]; - } - if ($folder !== '' && strpos($element, 'plg_') !== 0) { - $element = "plg_{$folder}_" . $element; - } - } - $fields['element_name'] = $element; - - // Name - if (preg_match('/([^<]+)<\/name>/', $xml, $m)) { - $fields['name'] = trim($m[1]); - } - - // Version - if (preg_match('/([^<]+)<\/version>/', $xml, $m)) { - $fields['version'] = trim($m[1]); - } - - // Description - if (preg_match('/([^<]+)<\/description>/', $xml, $m)) { - $desc = trim($m[1]); - // Skip language string keys like COM_MOKOSUITE_DESCRIPTION - if (strpos($desc, '_') === false || strlen($desc) > 60) { - $fields['description'] = $desc; - } - } - - // Display name for update feeds - if (!empty($fields['name'])) { - $name = $fields['name']; - // If name already has "Type - " prefix, use as-is - if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) { - $fields['display_name'] = $name; - } elseif (!empty($extType)) { - $fields['display_name'] = ucfirst($extType) . ' - ' . $name; - } - } - - // Target Joomla version - if (preg_match('/]*version="([^"]+)"/', $xml, $m)) { - $fields['target_version'] = trim($m[1]); - } else { - // Default for Joomla 5/6 - $fields['target_version'] = '(5|6)\..*'; - } - - // PHP minimum - if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) { - $fields['php_minimum'] = trim($m[1]); - } - - // License - if (preg_match('/([^<]+)<\/license>/', $xml, $m)) { - $fields['license_spdx'] = $this->normalizeLicense(trim($m[1])); - } - } - - private function findJoomlaManifest(string $root): ?string - { - // Priority: pkg_*.xml (package manifest) - $pkgXmls = array_merge( - SourceResolver::globSource($root, 'pkg_*.xml'), - glob("{$root}/pkg_*.xml") ?: [] - ); - if (!empty($pkgXmls)) { - return $pkgXmls[0]; - } - - // Any extension XML in source dir - foreach (SourceResolver::globSource($root, '*.xml') as $file) { - $content = file_get_contents($file); - if (strpos($content, 'findDolibarrModule($root); - if ($modFile === null) { - return; - } - - $content = file_get_contents($modFile); - - // Element name from class file - $modBasename = basename($modFile, '.class.php'); - $fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename)); - - // Name - if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { - $fields['name'] = $m[1]; - } - - // Version - if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { - $fields['version'] = $m[1]; - } - - // Description - if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { - $desc = $m[1]; - if (strpos($desc, '$') === false) { - $fields['description'] = $desc; - } - } - - // License - if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) { - $fields['license_spdx'] = $m[1]; - } - } - - private function findDolibarrModule(string $root): ?string - { - $candidates = array_merge( - SourceResolver::globSource($root, 'core/modules/mod*.class.php'), - glob("{$root}/core/modules/mod*.class.php") ?: [] - ); - foreach ($candidates as $file) { - if (strpos(file_get_contents($file), 'DolibarrModules') !== false) { - return $file; - } - } - return null; - } - - // ── Go ────────────────────────────────────────────────────────── - - private function detectGo(string $root, string $repoName, array &$fields): void - { - $fields['language'] = 'Go'; - $fields['package_type'] = 'application'; - $fields['entry_point'] = './'; - - $goMod = "{$root}/go.mod"; - if (!file_exists($goMod)) { - return; - } - - $content = file_get_contents($goMod); - - // Module path → name - if (preg_match('/^module\s+(\S+)/m', $content, $m)) { - $modulePath = $m[1]; - $parts = explode('/', $modulePath); - $fields['name'] = end($parts); - } - - // Go version - if (preg_match('/^go\s+(\S+)/m', $content, $m)) { - // This is Go language version, not the project version - // Project version comes from git tags or source files - } - - // License - $fields['license_spdx'] = $this->detectLicense($root); - } - - // ── Node / MCP ────────────────────────────────────────────────── - - private function detectNode(string $root, string $repoName, array &$fields): void - { - $pkgFile = "{$root}/package.json"; - if (!file_exists($pkgFile)) { - return; - } - - $pkg = json_decode(file_get_contents($pkgFile), true) ?? []; - - $fields['name'] = $pkg['name'] ?? ''; - // Strip npm scope - if (strpos($fields['name'], '/') !== false) { - $fields['name'] = explode('/', $fields['name'])[1]; - } - - $fields['version'] = $pkg['version'] ?? ''; - $fields['description'] = $pkg['description'] ?? ''; - $fields['license_spdx'] = $pkg['license'] ?? ''; - - // Language detection - if (file_exists("{$root}/tsconfig.json")) { - $fields['language'] = 'TypeScript'; - } else { - $fields['language'] = 'JavaScript'; - } - - // Package type - $deps = array_merge( - array_keys($pkg['dependencies'] ?? []), - array_keys($pkg['devDependencies'] ?? []) - ); - $isMcp = false; - foreach ($deps as $dep) { - if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') { - $isMcp = true; - break; - } - } - $fields['package_type'] = $isMcp ? 'mcp-server' : 'application'; - - // Entry point - if (file_exists("{$root}/dist")) { - $fields['entry_point'] = 'dist/'; - } elseif (file_exists("{$root}/src")) { - $fields['entry_point'] = 'src/'; - } else { - $fields['entry_point'] = './'; - } - } - - // ── Generic ───────────────────────────────────────────────────── - - private function detectGeneric(string $root, string $repoName, array &$fields): void - { - $fields['package_type'] = 'generic'; - - // Try to detect language from file extensions - $fields['language'] = $this->detectLanguageFromFiles($root); - $fields['license_spdx'] = $this->detectLicense($root); - } - - // ===================================================================== - // Shared detection helpers - // ===================================================================== - - private function detectEntryPoint(string $root): string - { - $abs = SourceResolver::resolveAbsolute($root); - if ($abs !== null) { - return basename($abs) . '/'; - } - if (is_dir("{$root}/dist")) return 'dist/'; - if (is_dir("{$root}/src")) return 'src/'; - return './'; - } - - private function detectLicense(string $root): string - { - // Check LICENSE file - foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) { - $file = "{$root}/{$name}"; - if (!file_exists($file)) continue; - $content = file_get_contents($file); - - // SPDX header - if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) { - return $m[1]; - } - - // Common license patterns - if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) { - if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later'; - if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later'; - } - if (strpos($content, 'MIT License') !== false) return 'MIT'; - if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0'; - } - - return ''; - } - - - private function detectLanguageFromFiles(string $root): string - { - $counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0]; - - $extensions = [ - 'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript', - 'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell', - ]; - - // Quick scan: only check top two levels - foreach (glob("{$root}/*") ?: [] as $item) { - $ext = pathinfo($item, PATHINFO_EXTENSION); - if (isset($extensions[$ext])) { - $counts[$extensions[$ext]]++; - } - if (is_dir($item) && basename($item)[0] !== '.') { - foreach (glob("{$item}/*") ?: [] as $subItem) { - $ext = pathinfo($subItem, PATHINFO_EXTENSION); - if (isset($extensions[$ext])) { - $counts[$extensions[$ext]]++; - } - } - } - } - - arsort($counts); - $top = key($counts); - return $counts[$top] > 0 ? $top : ''; - } - - private function normalizeLicense(string $license): string - { - $lower = strtolower($license); - $isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false; - if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later'; - if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later'; - if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT'; - if (strpos($lower, 'apache') !== false) return 'Apache-2.0'; - return $license; - } - - private function detectRepoName(string $root): string - { - $gitConfig = "{$root}/.git/config"; - if (!file_exists($gitConfig)) { - return basename($root); - } - - $content = file_get_contents($gitConfig); - if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) { - return $m[1]; - } - - return basename($root); - } - - // ===================================================================== - // API interaction - // ===================================================================== - - private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array - { - $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; - $ctx = stream_context_create([ - 'http' => [ - 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", - 'timeout' => 10, - ], - ]); - - $body = @file_get_contents($url, false, $ctx); - if ($body === false) return null; - - return json_decode($body, true); - } - - private function computeDiff(array $current, array $detected): array - { - // Map detected keys to API keys (underscores match) - $changes = []; - - foreach ($detected as $key => $value) { - $apiKey = $key; - $currentVal = $current[$apiKey] ?? ''; - - // Only flag as changed if detected value is non-empty and differs - if ($value !== '' && $value !== $currentVal) { - // Don't overwrite a non-empty API value with a detected value - // unless the API value is actually empty - if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) { - $changes[$key] = [ - 'current' => $currentVal, - 'detected' => $value, - ]; - } - } - } - - return $changes; - } - - private function shouldOverride(string $field, string $current, string $detected): bool - { - // Version: detected from source is authoritative - if ($field === 'version') return true; - - // These fields: source files are authoritative - if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) { - return true; - } - - // For other fields, only fill empty — don't overwrite manual edits - return false; - } - - private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool - { - $merged = array_merge($current, $update); - $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; - $payload = json_encode($merged); - - $ctx = stream_context_create([ - 'http' => [ - 'method' => 'PUT', - 'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", - 'content' => $payload, - 'timeout' => 10, - ], - ]); - - $body = @file_get_contents($url, false, $ctx); - return $body !== false; - } -} - -$app = new ManifestDetectCli(); -exit($app->execute()); +// Backward-compatibility wrapper — manifest_* renamed to metadata_* +require __DIR__ . '/metadata_detect.php'; diff --git a/cli/manifest_element.php b/cli/manifest_element.php index 3be1ee1..c2d0209 100644 --- a/cli/manifest_element.php +++ b/cli/manifest_element.php @@ -1,191 +1,4 @@ #!/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/manifest_element.php - * BRIEF: Extract element name, type, type prefix, and ZIP name from manifest - */ - -declare(strict_types=1); - -require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; - -use MokoEnterprise\{CliFramework, SourceResolver}; - -class ManifestElementCli extends CliFramework -{ - protected function configure(): void - { - $this->setDescription('Extract element name, type, type prefix, and ZIP name from manifest'); - $this->addArgument('--path', 'Repository root', '.'); - $this->addArgument('--version', 'Version string', null); - $this->addArgument('--stability', 'Stability level', 'stable'); - $this->addArgument('--repo', 'Repository name', ''); - $this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $version = $this->getArgument('--version'); - $stability = $this->getArgument('--stability'); - $repoName = $this->getArgument('--repo'); - $githubOutput = (bool) $this->getArgument('--github-output'); - $root = realpath($path) ?: $path; - $platform = 'generic'; - $manifestXml = "{$root}/.mokogitea/manifest.xml"; - if (file_exists($manifestXml)) { - $content = file_get_contents($manifestXml); - if (preg_match('/([^<]+)<\/platform>/', $content, $pm)) { - $platform = trim($pm[1]); - } - } - $extManifest = null; - $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, '([^<]+)<\/element>/', $xml, $em)) { - $extElement = $em[1]; - } - if (empty($extElement) && preg_match('/module="([^"]*)"/', $xml, $mm)) { - $extElement = $mm[1]; - } - if (empty($extElement) && preg_match('/plugin="([^"]*)"/', $xml, $pm2)) { - $extElement = $pm2[1]; - } - if ($extType === 'package' && preg_match('/([^<]+)<\/packagename>/', $xml, $pn)) { - $extElement = $pn[1]; - } - if (empty($extElement)) { - $extElement = strtolower(basename($extManifest, '.xml')); - if (in_array($extElement, ['templatedetails', 'manifest'], true)) { - $extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root))); - } - } - if (preg_match('/([^<]+)<\/name>/', $xml, $nm)) { - $extName = trim($nm[1]); - } - break; - case in_array($platform, ['dolibarr', 'crm-module'], true) && $modFile !== null: - $extType = 'dolibarr-module'; - $modBasename = basename($modFile, '.class.php'); - $extElement = strtolower(preg_replace('/^mod/', '', $modBasename)); - $modContent = file_get_contents($modFile); - if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $nm)) { - $extName = $nm[1]; - } - break; - default: - $extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root))); - $extType = 'generic'; - break; - } - $extElement = preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement); - $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; - } - $suffixMap = [ - 'development' => '-dev', - 'dev' => '-dev', - 'alpha' => '-alpha', - 'beta' => '-beta', - 'rc' => '-rc', - 'release-candidate' => '-rc', - 'stable' => '', - ]; - $suffix = $suffixMap[$stability] ?? ''; - $zipName = ''; - if ($version !== null) { - $zipName = "{$typePrefix}{$extElement}-{$version}{$suffix}.zip"; - } - if (empty($extName)) { - $extName = $repoName ?: basename($root); - } - $outputs = [ - 'platform' => $platform, - 'ext_element' => $extElement, - 'ext_type' => $extType, - 'ext_folder' => $extFolder, - 'ext_name' => $extName, - 'type_prefix' => $typePrefix, - 'zip_name' => $zipName, - ]; - if ($githubOutput) { - $ghOutput = getenv('GITHUB_OUTPUT'); - $lines = []; - foreach ($outputs as $key => $value) { - $lines[] = "{$key}={$value}"; - } - if ($ghOutput) { - file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); - } else { - foreach ($outputs as $key => $value) { - echo "::set-output name={$key}::{$value}\n"; - } - } - } else { - foreach ($outputs as $key => $value) { - echo "{$key}={$value}\n"; - } - } - return 0; - } -} - -$app = new ManifestElementCli(); -exit($app->execute()); +// Backward-compatibility wrapper — manifest_* renamed to metadata_* +require __DIR__ . '/metadata_element.php'; diff --git a/cli/manifest_integrity.php b/cli/manifest_integrity.php index be93418..67b8879 100644 --- a/cli/manifest_integrity.php +++ b/cli/manifest_integrity.php @@ -1,564 +1,4 @@ #!/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/manifest_integrity.php - * VERSION: 09.26.00 - * BRIEF: Cross-check manifest API fields against repo contents across the org - */ - -declare(strict_types=1); - -require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; - -use MokoEnterprise\CliFramework; - -class ManifestIntegrityCli extends CliFramework -{ - protected function configure(): void - { - $this->setDescription('Cross-check manifest fields against repo contents across the org'); - $this->addArgument('--path', 'Single repo path (local mode)', ''); - $this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting'); - $this->addArgument('--repo', 'Single repo name (remote mode)', ''); - $this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', ''); - $this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1'); - $this->addArgument('--fix', 'Push fixes for detected drift', false); - $this->addArgument('--json', 'Output as JSON', false); - $this->addArgument('--quiet', 'Only show repos with issues', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $org = $this->getArgument('--org'); - $repoName = $this->getArgument('--repo'); - $token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: ''; - $apiBase = rtrim($this->getArgument('--api-base'), '/'); - $fixMode = (bool) $this->getArgument('--fix'); - $jsonMode = (bool) $this->getArgument('--json'); - $quiet = (bool) $this->getArgument('--quiet'); - - if ($token === '') { - $this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)'); - return 1; - } - - // ── Mode selection ────────────────────────────────────────── - if ($path !== '') { - // Local mode: detect from source + compare to API - return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode); - } - - if ($repoName !== '') { - // Single remote repo - return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode); - } - - // Bulk mode: all repos in org - return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet); - } - - // ===================================================================== - // Local mode — detect from source, compare to API - // ===================================================================== - - private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int - { - $root = realpath($path) ?: $path; - if (!is_dir($root)) { - $this->log('ERROR', "Path does not exist: {$path}"); - return 1; - } - - if ($repoName === '') { - $repoName = $this->detectRepoName($root); - } - - // Run manifest_detect logic - $detected = $this->runDetect($root, $repoName); - $current = $this->fetchManifest($apiBase, $org, $repoName, $token); - - if ($current === null) { - $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); - return 1; - } - - $issues = $this->validate($current, $detected, $repoName); - - if ($json) { - echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - } else { - $this->printIssues($repoName, $issues); - } - - if ($fix && !empty($issues)) { - return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); - } - - return empty($issues) ? 0 : 1; - } - - // ===================================================================== - // Remote single repo mode — fetch source files via API - // ===================================================================== - - private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int - { - $current = $this->fetchManifest($apiBase, $org, $repoName, $token); - if ($current === null) { - $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); - return 1; - } - - $issues = $this->validateManifestOnly($current, $repoName); - - if ($json) { - echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - } else { - $this->printIssues($repoName, $issues); - } - - if ($fix && !empty($issues)) { - return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); - } - - return empty($issues) ? 0 : 1; - } - - // ===================================================================== - // Bulk org mode — check all repos - // ===================================================================== - - private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int - { - $repos = $this->fetchOrgRepos($apiBase, $org, $token); - if ($repos === null) { - $this->log('ERROR', "Failed to fetch repos for org {$org}"); - return 1; - } - - $this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)"); - - $allResults = []; - $totalIssues = 0; - $reposWithIssues = 0; - - foreach ($repos as $repo) { - $name = $repo['name']; - $manifest = $this->fetchManifest($apiBase, $org, $name, $token); - - if ($manifest === null) { - if (!$quiet) { - $this->log('WARN', "{$name}: no manifest"); - } - continue; - } - - $issues = $this->validateManifestOnly($manifest, $name); - - if (!empty($issues)) { - $reposWithIssues++; - $totalIssues += count($issues); - - if ($json) { - $allResults[] = ['repo' => $name, 'issues' => $issues]; - } else { - $this->printIssues($name, $issues); - } - - if ($fix) { - $this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues); - } - } elseif (!$quiet && !$json) { - $this->log('OK', "{$name}: clean"); - } - } - - if ($json) { - echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - } else { - echo "\n"; - $level = $reposWithIssues > 0 ? 'WARN' : 'OK'; - $this->log($level, sprintf( - 'Summary: %d repos checked, %d with issues (%d total issues)', - count($repos), - $reposWithIssues, - $totalIssues - )); - } - - return $reposWithIssues > 0 ? 1 : 0; - } - - // ===================================================================== - // Validation rules - // ===================================================================== - - /** - * Full validation: compare API manifest against locally-detected fields. - */ - private function validate(array $current, array $detected, string $repoName): array - { - $issues = []; - - // Required fields that should never be empty - $required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point']; - foreach ($required as $field) { - if (empty($current[$field])) { - $fix = $detected[$field] ?? null; - $issues[] = [ - 'field' => $field, - 'severity' => 'error', - 'message' => 'Missing required field', - 'current' => '', - 'fix' => $fix, - ]; - } - } - - // Drift detection: detected value differs from API - foreach ($detected as $field => $detectedValue) { - $currentValue = $current[$field] ?? ''; - if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) { - // Version drift is expected on dev branches (suffix) - if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) { - continue; // e.g., detected "02.34.50-dev" vs API "02.34.50" - } - if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) { - continue; - } - - $issues[] = [ - 'field' => $field, - 'severity' => 'warn', - 'message' => 'Drift: source differs from manifest', - 'current' => $currentValue, - 'fix' => $detectedValue, - ]; - } - } - - // Platform-specific structure validation - $platform = $current['platform'] ?? ''; - $issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName)); - - return $issues; - } - - /** - * API-only validation: check manifest fields for completeness and consistency - * without access to source files. - */ - private function validateManifestOnly(array $manifest, string $repoName): array - { - $issues = []; - - // Required fields - $required = ['platform', 'name', 'version', 'language']; - foreach ($required as $field) { - if (empty($manifest[$field])) { - $issues[] = [ - 'field' => $field, - 'severity' => 'error', - 'message' => 'Missing required field', - 'current' => '', - 'fix' => null, - ]; - } - } - - // Recommended fields - $recommended = ['package_type', 'entry_point', 'license_spdx', 'description']; - foreach ($recommended as $field) { - if (empty($manifest[$field])) { - $issues[] = [ - 'field' => $field, - 'severity' => 'info', - 'message' => 'Recommended field is empty', - 'current' => '', - 'fix' => null, - ]; - } - } - - // Platform-specific checks - $platform = $manifest['platform'] ?? ''; - $issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName)); - - return $issues; - } - - /** - * Platform-specific validation rules. - */ - private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array - { - $issues = []; - - switch ($platform) { - case 'joomla': - case 'waas-component': - // Joomla repos must have element_name - if (empty($manifest['element_name'])) { - $issues[] = [ - 'field' => 'element_name', - 'severity' => 'error', - 'message' => 'Joomla repos require element_name', - 'current' => '', - 'fix' => null, - ]; - } - // Language should be PHP - if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { - $issues[] = [ - 'field' => 'language', - 'severity' => 'warn', - 'message' => 'Joomla repos should have language=PHP', - 'current' => $manifest['language'], - 'fix' => 'PHP', - ]; - } - break; - - case 'dolibarr': - case 'crm-module': - if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { - $issues[] = [ - 'field' => 'language', - 'severity' => 'warn', - 'message' => 'Dolibarr repos should have language=PHP', - 'current' => $manifest['language'], - 'fix' => 'PHP', - ]; - } - break; - - case 'go': - if (!empty($manifest['language']) && $manifest['language'] !== 'Go') { - $issues[] = [ - 'field' => 'language', - 'severity' => 'warn', - 'message' => 'Go repos should have language=Go', - 'current' => $manifest['language'], - 'fix' => 'Go', - ]; - } - break; - - case 'mcp': - if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) { - $issues[] = [ - 'field' => 'language', - 'severity' => 'warn', - 'message' => 'MCP repos should have language=TypeScript or JavaScript', - 'current' => $manifest['language'], - 'fix' => null, - ]; - } - break; - } - - // Version format check: should be XX.YY.ZZ - $version = $manifest['version'] ?? ''; - if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) { - // Allow semver for node/go repos - if (!in_array($platform, ['mcp', 'node', 'go'], true)) { - $issues[] = [ - 'field' => 'version', - 'severity' => 'info', - 'message' => 'Version does not match XX.YY.ZZ format', - 'current' => $version, - 'fix' => null, - ]; - } - } - - return $issues; - } - - // ===================================================================== - // Output - // ===================================================================== - - private function printIssues(string $repoName, array $issues): void - { - if (empty($issues)) { - return; - } - - $errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error')); - $warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn')); - $infos = count($issues) - $errors - $warns; - - echo "\n"; - $summary = []; - if ($errors > 0) $summary[] = "{$errors} error(s)"; - if ($warns > 0) $summary[] = "{$warns} warning(s)"; - if ($infos > 0) $summary[] = "{$infos} info"; - $this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary)); - - foreach ($issues as $issue) { - $icon = match ($issue['severity']) { - 'error' => 'ERROR', - 'warn' => 'WARN', - default => 'INFO', - }; - $msg = sprintf(' %-18s %s', $issue['field'], $issue['message']); - if ($issue['current'] !== '') { - $msg .= " (current: {$issue['current']})"; - } - if ($issue['fix'] !== null) { - $msg .= " → fix: {$issue['fix']}"; - } - $this->log($icon, $msg); - } - } - - // ===================================================================== - // Fix application - // ===================================================================== - - private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int - { - $fixes = []; - foreach ($issues as $issue) { - if ($issue['fix'] !== null && $issue['fix'] !== '') { - $fixes[$issue['field']] = $issue['fix']; - } - } - - if (empty($fixes)) { - $this->log('INFO', "{$repo}: no auto-fixable issues"); - return 0; - } - - $merged = array_merge($current, $fixes); - $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; - $payload = json_encode($merged); - - $ctx = stream_context_create([ - 'http' => [ - 'method' => 'PUT', - 'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", - 'content' => $payload, - 'timeout' => 10, - ], - ]); - - $body = @file_get_contents($url, false, $ctx); - if ($body === false) { - $this->log('ERROR', "{$repo}: failed to push fixes"); - return 1; - } - - $this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes))); - return 0; - } - - // ===================================================================== - // API helpers - // ===================================================================== - - private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array - { - $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; - $ctx = stream_context_create([ - 'http' => [ - 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", - 'timeout' => 10, - ], - ]); - - $body = @file_get_contents($url, false, $ctx); - if ($body === false) return null; - - $data = json_decode($body, true); - return is_array($data) ? $data : null; - } - - private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array - { - $allRepos = []; - $page = 1; - $limit = 50; - - while (true) { - $url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}"; - $ctx = stream_context_create([ - 'http' => [ - 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", - 'timeout' => 15, - ], - ]); - - $body = @file_get_contents($url, false, $ctx); - if ($body === false) return null; - - $repos = json_decode($body, true); - if (!is_array($repos) || empty($repos)) break; - - $allRepos = array_merge($allRepos, $repos); - - if (count($repos) < $limit) break; - $page++; - } - - // Filter out archived and empty repos - return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false)); - } - - // ===================================================================== - // Detection (delegates to manifest_detect logic) - // ===================================================================== - - private function runDetect(string $root, string $repoName): array - { - $script = __DIR__ . '/manifest_detect.php'; - $redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null'; - $cmd = sprintf( - 'php %s --path %s --repo %s --json --quiet %s', - escapeshellarg($script), - escapeshellarg($root), - escapeshellarg($repoName), - $redirect - ); - - $output = shell_exec($cmd) ?? ''; - - // Extract JSON object from output (skip banner/log lines) - if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) { - $data = json_decode($m[0], true); - if (is_array($data)) { - return $data; - } - } - - return []; - } - - private function detectRepoName(string $root): string - { - $gitConfig = "{$root}/.git/config"; - if (!file_exists($gitConfig)) { - return basename($root); - } - - $content = file_get_contents($gitConfig); - if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) { - return $m[1]; - } - - return basename($root); - } -} - -$app = new ManifestIntegrityCli(); -exit($app->execute()); +// Backward-compatibility wrapper — manifest_* renamed to metadata_* +require __DIR__ . '/metadata_integrity.php'; diff --git a/cli/manifest_licensing.php b/cli/manifest_licensing.php index 416a757..628c0fb 100644 --- a/cli/manifest_licensing.php +++ b/cli/manifest_licensing.php @@ -1,280 +1,4 @@ #!/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/manifest_licensing.php - * VERSION: 09.26.00 - * BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests - */ - -declare(strict_types=1); - -require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; - -use MokoEnterprise\{CliFramework, SourceResolver}; - -/** - * Reads the block from .mokogitea/manifest.xml and ensures that the - * Joomla extension manifest contains the correct and tags. - * - * manifest.xml licensing block example: - * - * - * true - * true - * https://git.mokoconsulting.tech/{org}/{repo}/updates.xml - * MyExtension Updates - * - * - * Supports {org} and {repo} placeholders in update-server URL, resolved from - * the manifest's block or git remote. - */ -class ManifestLicensingCli extends CliFramework -{ - protected function configure(): void - { - $this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests'); - $this->addArgument('--path', 'Repository root path', '.'); - $this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false); - $this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false); - } - - protected function run(): int - { - $root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path'); - $fix = (bool) $this->getArgument('--fix'); - $ghOutput = (bool) $this->getArgument('--github-output'); - - // ── 1. Read manifest.xml ────────────────────────────────────────── - $manifestFile = "{$root}/.mokogitea/manifest.xml"; - - if (!file_exists($manifestFile)) { - $this->log('WARN', "No manifest.xml found at {$manifestFile}"); - $this->outputResult($ghOutput, 'skipped', 'No manifest.xml'); - return 0; - } - - $xml = @simplexml_load_file($manifestFile); - - if ($xml === false) { - $this->log('ERROR', "Failed to parse {$manifestFile}"); - return 1; - } - - // ── 2. Check if licensing is enabled ────────────────────────────── - if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') { - $this->log('INFO', 'Licensing not enabled in manifest.xml — skipping'); - $this->outputResult($ghOutput, 'skipped', 'Licensing not enabled'); - return 0; - } - - $licensingNode = $xml->licensing; - $dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true'; - $updateServerUrl = (string) ($licensingNode->{'update-server'} ?? ''); - $updateServerName = (string) ($licensingNode->{'update-server-name'} ?? ''); - - // ── 3. Resolve placeholders ─────────────────────────────────────── - $org = (string) ($xml->identity->org ?? ''); - $repo = (string) ($xml->identity->name ?? ''); - - // Fallback to git remote if manifest doesn't have org/name - if (empty($org) || empty($repo)) { - $remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null")); - - if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) { - if (empty($org)) { - $org = $m[1]; - } - if (empty($repo)) { - $repo = $m[2]; - } - } - } - - // Default update server URL if not specified - if (empty($updateServerUrl) && !empty($org) && !empty($repo)) { - $updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml"; - } - - // Resolve {org} and {repo} placeholders - $updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl); - - // Default server name from display-name or repo name - if (empty($updateServerName)) { - $displayName = (string) ($xml->identity->{'display-name'} ?? $repo); - $updateServerName = $displayName . ' Updates'; - } - - if (empty($updateServerUrl)) { - $this->log('ERROR', 'Cannot determine update server URL — set in manifest.xml or ensure org/repo are available'); - return 1; - } - - $this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}"); - $this->log('INFO', "Update server: {$updateServerUrl}"); - $this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no')); - - // ── 4. Find Joomla extension manifests ──────────────────────────── - $xmlFiles = array_merge( - SourceResolver::globSource($root, '*.xml'), - SourceResolver::globSource($root, 'packages/*/*.xml'), - glob("{$root}/*.xml") ?: [] - ); - - $packageManifest = null; - - foreach ($xmlFiles as $file) { - $content = file_get_contents($file); - - if (!str_contains($content, 'log('WARN', 'No Joomla extension manifest found'); - $this->outputResult($ghOutput, 'skipped', 'No extension manifest'); - return 0; - } - - $relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest)); - $this->log('INFO', "Package manifest: {$relPath}"); - - // ── 5. Check and fix the manifest ───────────────────────────────── - $content = file_get_contents($packageManifest); - $original = $content; - $changes = []; - - // --- 5a. Ensure block with correct URL --- - if (preg_match('#\s*#s', $content)) { - // Empty updateservers block — inject the server - $replacement = "\n" - . " {$updateServerUrl}\n" - . " "; - $content = preg_replace('#\s*#s', $replacement, $content); - $changes[] = 'Added update server URL to empty '; - } elseif (!str_contains($content, '')) { - // No updateservers at all — add before - $serverBlock = "\n \n" - . " {$updateServerUrl}\n" - . " \n"; - $content = str_replace('', $serverBlock . '', $content); - $changes[] = 'Added block'; - } else { - // updateservers exists — verify URL is correct - if (preg_match('#]*>([^<]+)#', $content, $m)) { - if ($m[1] !== $updateServerUrl) { - $content = preg_replace( - '#(]*>)[^<]+()#', - "\${1}{$updateServerUrl}\${2}", - $content - ); - $changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}"; - } - } - } - - // --- 5b. Ensure tag if required --- - if ($dlidEnabled) { - if (!str_contains($content, ' if present, otherwise before - $dlidTag = ' ' . "\n"; - - if (str_contains($content, '')) { - $content = str_replace('', $dlidTag . "\n ", $content); - } else { - $content = str_replace('', $dlidTag . '', $content); - } - - $changes[] = 'Added tag'; - } - } - - // --- 5c. Ensure for packages --- - if (str_contains($content, 'type="package"') && !str_contains($content, '')) { - $blockTag = ' true' . "\n"; - - if (str_contains($content, ' - $content = preg_replace( - '#(\s*\n)#', - "\${1}{$blockTag}", - $content - ); - } elseif (str_contains($content, '')) { - $content = str_replace('', $blockTag . "\n ", $content); - } else { - $content = str_replace('', $blockTag . '', $content); - } - - $changes[] = 'Added true'; - } - - // ── 6. Report and apply ─────────────────────────────────────────── - if (empty($changes)) { - $this->log('INFO', 'All licensing tags are correct — no changes needed'); - $this->outputResult($ghOutput, 'ok', 'No changes needed'); - return 0; - } - - foreach ($changes as $change) { - $this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change); - } - - if ($fix) { - file_put_contents($packageManifest, $content); - $this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)"); - $this->outputResult($ghOutput, 'fixed', implode('; ', $changes)); - } else { - $this->log('WARN', 'Run with --fix to apply changes'); - $this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes)); - return 1; - } - - return 0; - } - - /** - * Write result to $GITHUB_OUTPUT if requested. - */ - private function outputResult(bool $ghOutput, string $status, string $detail): void - { - if (!$ghOutput) { - return; - } - - $outputFile = getenv('GITHUB_OUTPUT'); - - if ($outputFile === false || $outputFile === '') { - echo "licensing_status={$status}\n"; - echo "licensing_detail={$detail}\n"; - return; - } - - $fh = fopen($outputFile, 'a'); - fwrite($fh, "licensing_status={$status}\n"); - fwrite($fh, "licensing_detail={$detail}\n"); - fclose($fh); - } -} - -$app = new ManifestLicensingCli(); -exit($app->execute()); +// Backward-compatibility wrapper — manifest_* renamed to metadata_* +require __DIR__ . '/metadata_licensing.php'; diff --git a/cli/manifest_read.php b/cli/manifest_read.php index 988b39c..2937e2b 100644 --- a/cli/manifest_read.php +++ b/cli/manifest_read.php @@ -1,170 +1,4 @@ #!/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/manifest_read.php - * VERSION: 09.26.00 - * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption - */ - -declare(strict_types=1); - -require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; - -use MokoEnterprise\CliFramework; - -class ManifestReadCli extends CliFramework -{ - protected function configure(): void - { - $this->setDescription('Parse manifest.xml and output requested field(s) for CI consumption'); - $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'); - $ghOutput = $this->getArgument('--github-output'); - $jsonMode = $this->getArgument('--json'); - - // Determine mode - if ($ghOutput) { - $mode = 'github-output'; - } elseif ($showAll) { - $mode = 'all'; - } elseif ($jsonMode) { - $mode = 'json'; - } else { - $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"); - 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) { - // Convert field-name to FIELD_NAME for env var style - $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 ManifestReadCli(); -exit($app->execute()); +// Backward-compatibility wrapper — manifest_* renamed to metadata_* +require __DIR__ . '/metadata_read.php'; diff --git a/cli/metadata_detect.php b/cli/metadata_detect.php new file mode 100644 index 0000000..c0e5e57 --- /dev/null +++ b/cli/metadata_detect.php @@ -0,0 +1,749 @@ +#!/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/manifest_detect.php + * VERSION: 09.26.00 + * BRIEF: Auto-detect manifest fields from source files and optionally push to API + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\{CliFramework, SourceResolver}; + +class ManifestDetectCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Auto-detect manifest fields from source files'); + $this->addArgument('--path', 'Repository root path', '.'); + $this->addArgument('--json', 'Output as JSON', false); + $this->addArgument('--diff', 'Show diff against current manifest API values', false); + $this->addArgument('--update', 'Push detected fields to manifest API', false); + $this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', ''); + $this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1'); + $this->addArgument('--org', 'Gitea org', 'MokoConsulting'); + $this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', ''); + $this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $jsonMode = (bool) $this->getArgument('--json'); + $diffMode = (bool) $this->getArgument('--diff'); + $updateMode = (bool) $this->getArgument('--update'); + $ghOutput = (bool) $this->getArgument('--github-output'); + $token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: ''; + $apiBase = rtrim($this->getArgument('--api-base'), '/'); + $org = $this->getArgument('--org'); + $repoName = $this->getArgument('--repo'); + + $root = realpath($path) ?: $path; + + if (!is_dir($root)) { + $this->log('ERROR', "Path does not exist: {$path}"); + return 1; + } + + // Auto-detect repo name from git remote + if ($repoName === '') { + $repoName = $this->detectRepoName($root); + } + + // ── Detect all fields ─────────────────────────────────────── + $detected = $this->detectAll($root, $repoName); + + // ── Warn about missing fields ──────────────────────────────── + $expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point']; + foreach ($expected as $field) { + if (!isset($detected[$field]) || $detected[$field] === '') { + $this->log('WARN', "Could not detect: {$field}"); + } + } + + // ── Output ────────────────────────────────────────────────── + if ($diffMode || $updateMode) { + if ($token === '') { + $this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)'); + return 1; + } + if ($repoName === '') { + $this->log('ERROR', 'Could not determine repo name (use --repo)'); + return 1; + } + + $current = $this->fetchManifest($apiBase, $org, $repoName, $token); + if ($current === null) { + $this->log('ERROR', 'Failed to fetch current manifest from API'); + return 1; + } + + $changes = $this->computeDiff($current, $detected); + + if ($diffMode) { + if (empty($changes)) { + $this->log('INFO', 'No differences — manifest matches source'); + } else { + $this->sectionHeader('Manifest Drift'); + foreach ($changes as $field => $info) { + $this->log('WARN', sprintf( + '%-20s API: %-30s Detected: %s', + $field, + $info['current'] === '' ? '(empty)' : $info['current'], + $info['detected'] + )); + } + } + } + + if ($updateMode) { + if (empty($changes)) { + $this->log('INFO', 'Nothing to update'); + } else { + $update = array_map(fn($i) => $i['detected'], $changes); + $ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update); + if ($ok) { + $this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update))); + } else { + $this->log('ERROR', 'Failed to push manifest update'); + return 1; + } + } + } + + return 0; + } + + if ($ghOutput) { + $outputFile = getenv('GITHUB_OUTPUT'); + $lines = []; + foreach ($detected as $k => $v) { + $envKey = str_replace('-', '_', $k); + $lines[] = "{$envKey}={$v}"; + } + if ($outputFile !== false && $outputFile !== '') { + file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND); + $this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT'); + } else { + $this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead'); + echo implode("\n", $lines) . "\n"; + } + return 0; + } + + if ($jsonMode) { + echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + foreach ($detected as $k => $v) { + echo "{$k}={$v}\n"; + } + } + + return 0; + } + + // ===================================================================== + // Detection engine + // ===================================================================== + + private function detectAll(string $root, string $repoName): array + { + $platform = $this->detectPlatform($root); + + $fields = [ + 'platform' => $platform, + 'name' => '', + 'description' => '', + 'version' => '', + 'element_name' => '', + 'package_type' => '', + 'language' => '', + 'entry_point' => '', + 'license_spdx' => '', + 'display_name' => '', + 'target_version' => '', + 'php_minimum' => '', + ]; + + switch ($platform) { + case 'joomla': + $this->detectJoomla($root, $repoName, $fields); + break; + case 'dolibarr': + $this->detectDolibarr($root, $repoName, $fields); + break; + case 'go': + $this->detectGo($root, $repoName, $fields); + break; + case 'mcp': + $this->detectNode($root, $repoName, $fields); + break; + case 'node': + $this->detectNode($root, $repoName, $fields); + $fields['platform'] = 'node'; + break; + default: + $this->detectGeneric($root, $repoName, $fields); + break; + } + + // Fallbacks + if ($fields['name'] === '') { + $fields['name'] = $repoName ?: basename($root); + } + if ($fields['entry_point'] === '') { + $fields['entry_point'] = $this->detectEntryPoint($root); + } + if ($fields['license_spdx'] === '') { + $fields['license_spdx'] = $this->detectLicense($root); + } + // description: only from platform-specific source, never guessed + + // Strip empty values + return array_filter($fields, fn($v) => $v !== ''); + } + + // ── Platform detection ────────────────────────────────────────── + + private function detectPlatform(string $root): string + { + // Joomla: look for pkg_*.xml or extension XML in source dirs + $joomlaXmls = array_merge( + SourceResolver::globSource($root, 'pkg_*.xml'), + glob("{$root}/pkg_*.xml") ?: [] + ); + if (!empty($joomlaXmls)) { + return 'joomla'; + } + + // Check source dirs for any Joomla extension XML + foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) { + $content = file_get_contents($xmlFile); + if (strpos($content, 'findJoomlaManifest($root); + if ($extManifest === null) { + return; + } + + $xml = file_get_contents($extManifest); + + // Type + $extType = ''; + if (preg_match('/type="([^"]*)"/', $xml, $m)) { + $extType = $m[1]; + } + $fields['package_type'] = $extType; + + // Element name + $element = ''; + if (preg_match('/([^<]+)<\/element>/', $xml, $m)) { + $element = $m[1]; + } + if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) { + $element = $m[1]; + } + if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) { + $element = $m[1]; + } + if ($extType === 'package' && preg_match('/([^<]+)<\/packagename>/', $xml, $m)) { + $element = $m[1]; + } + if ($element === '') { + $element = strtolower(basename($extManifest, '.xml')); + } + + // Ensure element has type prefix (API stores full element_name like pkg_mokosuite) + $prefixMap = [ + 'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_', + 'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_', + ]; + if (isset($prefixMap[$extType])) { + $prefix = $prefixMap[$extType]; + // Only add prefix if not already present (check all known prefixes) + $hasPrefix = false; + foreach ($prefixMap as $p) { + if (strpos($element, $p) === 0) { $hasPrefix = true; break; } + } + if (strpos($element, 'plg_') === 0) { $hasPrefix = true; } + if (!$hasPrefix) { + $element = $prefix . $element; + } + } elseif ($extType === 'plugin') { + $folder = ''; + if (preg_match('/group="([^"]*)"/', $xml, $gm)) { + $folder = $gm[1]; + } + if ($folder !== '' && strpos($element, 'plg_') !== 0) { + $element = "plg_{$folder}_" . $element; + } + } + $fields['element_name'] = $element; + + // Name + if (preg_match('/([^<]+)<\/name>/', $xml, $m)) { + $fields['name'] = trim($m[1]); + } + + // Version + if (preg_match('/([^<]+)<\/version>/', $xml, $m)) { + $fields['version'] = trim($m[1]); + } + + // Description + if (preg_match('/([^<]+)<\/description>/', $xml, $m)) { + $desc = trim($m[1]); + // Skip language string keys like COM_MOKOSUITE_DESCRIPTION + if (strpos($desc, '_') === false || strlen($desc) > 60) { + $fields['description'] = $desc; + } + } + + // Display name for update feeds + if (!empty($fields['name'])) { + $name = $fields['name']; + // If name already has "Type - " prefix, use as-is + if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) { + $fields['display_name'] = $name; + } elseif (!empty($extType)) { + $fields['display_name'] = ucfirst($extType) . ' - ' . $name; + } + } + + // Target Joomla version + if (preg_match('/]*version="([^"]+)"/', $xml, $m)) { + $fields['target_version'] = trim($m[1]); + } else { + // Default for Joomla 5/6 + $fields['target_version'] = '(5|6)\..*'; + } + + // PHP minimum + if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) { + $fields['php_minimum'] = trim($m[1]); + } + + // License + if (preg_match('/([^<]+)<\/license>/', $xml, $m)) { + $fields['license_spdx'] = $this->normalizeLicense(trim($m[1])); + } + } + + private function findJoomlaManifest(string $root): ?string + { + // Priority: pkg_*.xml (package manifest) + $pkgXmls = array_merge( + SourceResolver::globSource($root, 'pkg_*.xml'), + glob("{$root}/pkg_*.xml") ?: [] + ); + if (!empty($pkgXmls)) { + return $pkgXmls[0]; + } + + // Any extension XML in source dir + foreach (SourceResolver::globSource($root, '*.xml') as $file) { + $content = file_get_contents($file); + if (strpos($content, 'findDolibarrModule($root); + if ($modFile === null) { + return; + } + + $content = file_get_contents($modFile); + + // Element name from class file + $modBasename = basename($modFile, '.class.php'); + $fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename)); + + // Name + if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { + $fields['name'] = $m[1]; + } + + // Version + if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { + $fields['version'] = $m[1]; + } + + // Description + if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) { + $desc = $m[1]; + if (strpos($desc, '$') === false) { + $fields['description'] = $desc; + } + } + + // License + if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) { + $fields['license_spdx'] = $m[1]; + } + } + + private function findDolibarrModule(string $root): ?string + { + $candidates = array_merge( + SourceResolver::globSource($root, 'core/modules/mod*.class.php'), + glob("{$root}/core/modules/mod*.class.php") ?: [] + ); + foreach ($candidates as $file) { + if (strpos(file_get_contents($file), 'DolibarrModules') !== false) { + return $file; + } + } + return null; + } + + // ── Go ────────────────────────────────────────────────────────── + + private function detectGo(string $root, string $repoName, array &$fields): void + { + $fields['language'] = 'Go'; + $fields['package_type'] = 'application'; + $fields['entry_point'] = './'; + + $goMod = "{$root}/go.mod"; + if (!file_exists($goMod)) { + return; + } + + $content = file_get_contents($goMod); + + // Module path → name + if (preg_match('/^module\s+(\S+)/m', $content, $m)) { + $modulePath = $m[1]; + $parts = explode('/', $modulePath); + $fields['name'] = end($parts); + } + + // Go version + if (preg_match('/^go\s+(\S+)/m', $content, $m)) { + // This is Go language version, not the project version + // Project version comes from git tags or source files + } + + // License + $fields['license_spdx'] = $this->detectLicense($root); + } + + // ── Node / MCP ────────────────────────────────────────────────── + + private function detectNode(string $root, string $repoName, array &$fields): void + { + $pkgFile = "{$root}/package.json"; + if (!file_exists($pkgFile)) { + return; + } + + $pkg = json_decode(file_get_contents($pkgFile), true) ?? []; + + $fields['name'] = $pkg['name'] ?? ''; + // Strip npm scope + if (strpos($fields['name'], '/') !== false) { + $fields['name'] = explode('/', $fields['name'])[1]; + } + + $fields['version'] = $pkg['version'] ?? ''; + $fields['description'] = $pkg['description'] ?? ''; + $fields['license_spdx'] = $pkg['license'] ?? ''; + + // Language detection + if (file_exists("{$root}/tsconfig.json")) { + $fields['language'] = 'TypeScript'; + } else { + $fields['language'] = 'JavaScript'; + } + + // Package type + $deps = array_merge( + array_keys($pkg['dependencies'] ?? []), + array_keys($pkg['devDependencies'] ?? []) + ); + $isMcp = false; + foreach ($deps as $dep) { + if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') { + $isMcp = true; + break; + } + } + $fields['package_type'] = $isMcp ? 'mcp-server' : 'application'; + + // Entry point + if (file_exists("{$root}/dist")) { + $fields['entry_point'] = 'dist/'; + } elseif (file_exists("{$root}/src")) { + $fields['entry_point'] = 'src/'; + } else { + $fields['entry_point'] = './'; + } + } + + // ── Generic ───────────────────────────────────────────────────── + + private function detectGeneric(string $root, string $repoName, array &$fields): void + { + $fields['package_type'] = 'generic'; + + // Try to detect language from file extensions + $fields['language'] = $this->detectLanguageFromFiles($root); + $fields['license_spdx'] = $this->detectLicense($root); + } + + // ===================================================================== + // Shared detection helpers + // ===================================================================== + + private function detectEntryPoint(string $root): string + { + $abs = SourceResolver::resolveAbsolute($root); + if ($abs !== null) { + return basename($abs) . '/'; + } + if (is_dir("{$root}/dist")) return 'dist/'; + if (is_dir("{$root}/src")) return 'src/'; + return './'; + } + + private function detectLicense(string $root): string + { + // Check LICENSE file + foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) { + $file = "{$root}/{$name}"; + if (!file_exists($file)) continue; + $content = file_get_contents($file); + + // SPDX header + if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) { + return $m[1]; + } + + // Common license patterns + if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) { + if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later'; + if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later'; + } + if (strpos($content, 'MIT License') !== false) return 'MIT'; + if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0'; + } + + return ''; + } + + + private function detectLanguageFromFiles(string $root): string + { + $counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0]; + + $extensions = [ + 'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript', + 'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell', + ]; + + // Quick scan: only check top two levels + foreach (glob("{$root}/*") ?: [] as $item) { + $ext = pathinfo($item, PATHINFO_EXTENSION); + if (isset($extensions[$ext])) { + $counts[$extensions[$ext]]++; + } + if (is_dir($item) && basename($item)[0] !== '.') { + foreach (glob("{$item}/*") ?: [] as $subItem) { + $ext = pathinfo($subItem, PATHINFO_EXTENSION); + if (isset($extensions[$ext])) { + $counts[$extensions[$ext]]++; + } + } + } + } + + arsort($counts); + $top = key($counts); + return $counts[$top] > 0 ? $top : ''; + } + + private function normalizeLicense(string $license): string + { + $lower = strtolower($license); + $isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false; + if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later'; + if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later'; + if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT'; + if (strpos($lower, 'apache') !== false) return 'Apache-2.0'; + return $license; + } + + private function detectRepoName(string $root): string + { + $gitConfig = "{$root}/.git/config"; + if (!file_exists($gitConfig)) { + return basename($root); + } + + $content = file_get_contents($gitConfig); + if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) { + return $m[1]; + } + + return basename($root); + } + + // ===================================================================== + // API interaction + // ===================================================================== + + private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array + { + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $ctx = stream_context_create([ + 'http' => [ + 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) return null; + + return json_decode($body, true); + } + + private function computeDiff(array $current, array $detected): array + { + // Map detected keys to API keys (underscores match) + $changes = []; + + foreach ($detected as $key => $value) { + $apiKey = $key; + $currentVal = $current[$apiKey] ?? ''; + + // Only flag as changed if detected value is non-empty and differs + if ($value !== '' && $value !== $currentVal) { + // Don't overwrite a non-empty API value with a detected value + // unless the API value is actually empty + if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) { + $changes[$key] = [ + 'current' => $currentVal, + 'detected' => $value, + ]; + } + } + } + + return $changes; + } + + private function shouldOverride(string $field, string $current, string $detected): bool + { + // Version: detected from source is authoritative + if ($field === 'version') return true; + + // These fields: source files are authoritative + if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) { + return true; + } + + // For other fields, only fill empty — don't overwrite manual edits + return false; + } + + private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool + { + $merged = array_merge($current, $update); + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $payload = json_encode($merged); + + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", + 'content' => $payload, + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + return $body !== false; + } +} + +$app = new ManifestDetectCli(); +exit($app->execute()); diff --git a/cli/metadata_element.php b/cli/metadata_element.php new file mode 100644 index 0000000..3be1ee1 --- /dev/null +++ b/cli/metadata_element.php @@ -0,0 +1,191 @@ +#!/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/manifest_element.php + * BRIEF: Extract element name, type, type prefix, and ZIP name from manifest + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\{CliFramework, SourceResolver}; + +class ManifestElementCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Extract element name, type, type prefix, and ZIP name from manifest'); + $this->addArgument('--path', 'Repository root', '.'); + $this->addArgument('--version', 'Version string', null); + $this->addArgument('--stability', 'Stability level', 'stable'); + $this->addArgument('--repo', 'Repository name', ''); + $this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $version = $this->getArgument('--version'); + $stability = $this->getArgument('--stability'); + $repoName = $this->getArgument('--repo'); + $githubOutput = (bool) $this->getArgument('--github-output'); + $root = realpath($path) ?: $path; + $platform = 'generic'; + $manifestXml = "{$root}/.mokogitea/manifest.xml"; + if (file_exists($manifestXml)) { + $content = file_get_contents($manifestXml); + if (preg_match('/([^<]+)<\/platform>/', $content, $pm)) { + $platform = trim($pm[1]); + } + } + $extManifest = null; + $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, '([^<]+)<\/element>/', $xml, $em)) { + $extElement = $em[1]; + } + if (empty($extElement) && preg_match('/module="([^"]*)"/', $xml, $mm)) { + $extElement = $mm[1]; + } + if (empty($extElement) && preg_match('/plugin="([^"]*)"/', $xml, $pm2)) { + $extElement = $pm2[1]; + } + if ($extType === 'package' && preg_match('/([^<]+)<\/packagename>/', $xml, $pn)) { + $extElement = $pn[1]; + } + if (empty($extElement)) { + $extElement = strtolower(basename($extManifest, '.xml')); + if (in_array($extElement, ['templatedetails', 'manifest'], true)) { + $extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root))); + } + } + if (preg_match('/([^<]+)<\/name>/', $xml, $nm)) { + $extName = trim($nm[1]); + } + break; + case in_array($platform, ['dolibarr', 'crm-module'], true) && $modFile !== null: + $extType = 'dolibarr-module'; + $modBasename = basename($modFile, '.class.php'); + $extElement = strtolower(preg_replace('/^mod/', '', $modBasename)); + $modContent = file_get_contents($modFile); + if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $nm)) { + $extName = $nm[1]; + } + break; + default: + $extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root))); + $extType = 'generic'; + break; + } + $extElement = preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement); + $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; + } + $suffixMap = [ + 'development' => '-dev', + 'dev' => '-dev', + 'alpha' => '-alpha', + 'beta' => '-beta', + 'rc' => '-rc', + 'release-candidate' => '-rc', + 'stable' => '', + ]; + $suffix = $suffixMap[$stability] ?? ''; + $zipName = ''; + if ($version !== null) { + $zipName = "{$typePrefix}{$extElement}-{$version}{$suffix}.zip"; + } + if (empty($extName)) { + $extName = $repoName ?: basename($root); + } + $outputs = [ + 'platform' => $platform, + 'ext_element' => $extElement, + 'ext_type' => $extType, + 'ext_folder' => $extFolder, + 'ext_name' => $extName, + 'type_prefix' => $typePrefix, + 'zip_name' => $zipName, + ]; + if ($githubOutput) { + $ghOutput = getenv('GITHUB_OUTPUT'); + $lines = []; + foreach ($outputs as $key => $value) { + $lines[] = "{$key}={$value}"; + } + if ($ghOutput) { + file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); + } else { + foreach ($outputs as $key => $value) { + echo "::set-output name={$key}::{$value}\n"; + } + } + } else { + foreach ($outputs as $key => $value) { + echo "{$key}={$value}\n"; + } + } + return 0; + } +} + +$app = new ManifestElementCli(); +exit($app->execute()); diff --git a/cli/metadata_integrity.php b/cli/metadata_integrity.php new file mode 100644 index 0000000..be93418 --- /dev/null +++ b/cli/metadata_integrity.php @@ -0,0 +1,564 @@ +#!/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/manifest_integrity.php + * VERSION: 09.26.00 + * BRIEF: Cross-check manifest API fields against repo contents across the org + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\CliFramework; + +class ManifestIntegrityCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Cross-check manifest fields against repo contents across the org'); + $this->addArgument('--path', 'Single repo path (local mode)', ''); + $this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting'); + $this->addArgument('--repo', 'Single repo name (remote mode)', ''); + $this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', ''); + $this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1'); + $this->addArgument('--fix', 'Push fixes for detected drift', false); + $this->addArgument('--json', 'Output as JSON', false); + $this->addArgument('--quiet', 'Only show repos with issues', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $org = $this->getArgument('--org'); + $repoName = $this->getArgument('--repo'); + $token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: ''; + $apiBase = rtrim($this->getArgument('--api-base'), '/'); + $fixMode = (bool) $this->getArgument('--fix'); + $jsonMode = (bool) $this->getArgument('--json'); + $quiet = (bool) $this->getArgument('--quiet'); + + if ($token === '') { + $this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)'); + return 1; + } + + // ── Mode selection ────────────────────────────────────────── + if ($path !== '') { + // Local mode: detect from source + compare to API + return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode); + } + + if ($repoName !== '') { + // Single remote repo + return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode); + } + + // Bulk mode: all repos in org + return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet); + } + + // ===================================================================== + // Local mode — detect from source, compare to API + // ===================================================================== + + private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int + { + $root = realpath($path) ?: $path; + if (!is_dir($root)) { + $this->log('ERROR', "Path does not exist: {$path}"); + return 1; + } + + if ($repoName === '') { + $repoName = $this->detectRepoName($root); + } + + // Run manifest_detect logic + $detected = $this->runDetect($root, $repoName); + $current = $this->fetchManifest($apiBase, $org, $repoName, $token); + + if ($current === null) { + $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); + return 1; + } + + $issues = $this->validate($current, $detected, $repoName); + + if ($json) { + echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $this->printIssues($repoName, $issues); + } + + if ($fix && !empty($issues)) { + return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); + } + + return empty($issues) ? 0 : 1; + } + + // ===================================================================== + // Remote single repo mode — fetch source files via API + // ===================================================================== + + private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int + { + $current = $this->fetchManifest($apiBase, $org, $repoName, $token); + if ($current === null) { + $this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}"); + return 1; + } + + $issues = $this->validateManifestOnly($current, $repoName); + + if ($json) { + echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $this->printIssues($repoName, $issues); + } + + if ($fix && !empty($issues)) { + return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues); + } + + return empty($issues) ? 0 : 1; + } + + // ===================================================================== + // Bulk org mode — check all repos + // ===================================================================== + + private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int + { + $repos = $this->fetchOrgRepos($apiBase, $org, $token); + if ($repos === null) { + $this->log('ERROR', "Failed to fetch repos for org {$org}"); + return 1; + } + + $this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)"); + + $allResults = []; + $totalIssues = 0; + $reposWithIssues = 0; + + foreach ($repos as $repo) { + $name = $repo['name']; + $manifest = $this->fetchManifest($apiBase, $org, $name, $token); + + if ($manifest === null) { + if (!$quiet) { + $this->log('WARN', "{$name}: no manifest"); + } + continue; + } + + $issues = $this->validateManifestOnly($manifest, $name); + + if (!empty($issues)) { + $reposWithIssues++; + $totalIssues += count($issues); + + if ($json) { + $allResults[] = ['repo' => $name, 'issues' => $issues]; + } else { + $this->printIssues($name, $issues); + } + + if ($fix) { + $this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues); + } + } elseif (!$quiet && !$json) { + $this->log('OK', "{$name}: clean"); + } + } + + if ($json) { + echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + echo "\n"; + $level = $reposWithIssues > 0 ? 'WARN' : 'OK'; + $this->log($level, sprintf( + 'Summary: %d repos checked, %d with issues (%d total issues)', + count($repos), + $reposWithIssues, + $totalIssues + )); + } + + return $reposWithIssues > 0 ? 1 : 0; + } + + // ===================================================================== + // Validation rules + // ===================================================================== + + /** + * Full validation: compare API manifest against locally-detected fields. + */ + private function validate(array $current, array $detected, string $repoName): array + { + $issues = []; + + // Required fields that should never be empty + $required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point']; + foreach ($required as $field) { + if (empty($current[$field])) { + $fix = $detected[$field] ?? null; + $issues[] = [ + 'field' => $field, + 'severity' => 'error', + 'message' => 'Missing required field', + 'current' => '', + 'fix' => $fix, + ]; + } + } + + // Drift detection: detected value differs from API + foreach ($detected as $field => $detectedValue) { + $currentValue = $current[$field] ?? ''; + if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) { + // Version drift is expected on dev branches (suffix) + if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) { + continue; // e.g., detected "02.34.50-dev" vs API "02.34.50" + } + if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) { + continue; + } + + $issues[] = [ + 'field' => $field, + 'severity' => 'warn', + 'message' => 'Drift: source differs from manifest', + 'current' => $currentValue, + 'fix' => $detectedValue, + ]; + } + } + + // Platform-specific structure validation + $platform = $current['platform'] ?? ''; + $issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName)); + + return $issues; + } + + /** + * API-only validation: check manifest fields for completeness and consistency + * without access to source files. + */ + private function validateManifestOnly(array $manifest, string $repoName): array + { + $issues = []; + + // Required fields + $required = ['platform', 'name', 'version', 'language']; + foreach ($required as $field) { + if (empty($manifest[$field])) { + $issues[] = [ + 'field' => $field, + 'severity' => 'error', + 'message' => 'Missing required field', + 'current' => '', + 'fix' => null, + ]; + } + } + + // Recommended fields + $recommended = ['package_type', 'entry_point', 'license_spdx', 'description']; + foreach ($recommended as $field) { + if (empty($manifest[$field])) { + $issues[] = [ + 'field' => $field, + 'severity' => 'info', + 'message' => 'Recommended field is empty', + 'current' => '', + 'fix' => null, + ]; + } + } + + // Platform-specific checks + $platform = $manifest['platform'] ?? ''; + $issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName)); + + return $issues; + } + + /** + * Platform-specific validation rules. + */ + private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array + { + $issues = []; + + switch ($platform) { + case 'joomla': + case 'waas-component': + // Joomla repos must have element_name + if (empty($manifest['element_name'])) { + $issues[] = [ + 'field' => 'element_name', + 'severity' => 'error', + 'message' => 'Joomla repos require element_name', + 'current' => '', + 'fix' => null, + ]; + } + // Language should be PHP + if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Joomla repos should have language=PHP', + 'current' => $manifest['language'], + 'fix' => 'PHP', + ]; + } + break; + + case 'dolibarr': + case 'crm-module': + if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Dolibarr repos should have language=PHP', + 'current' => $manifest['language'], + 'fix' => 'PHP', + ]; + } + break; + + case 'go': + if (!empty($manifest['language']) && $manifest['language'] !== 'Go') { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'Go repos should have language=Go', + 'current' => $manifest['language'], + 'fix' => 'Go', + ]; + } + break; + + case 'mcp': + if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) { + $issues[] = [ + 'field' => 'language', + 'severity' => 'warn', + 'message' => 'MCP repos should have language=TypeScript or JavaScript', + 'current' => $manifest['language'], + 'fix' => null, + ]; + } + break; + } + + // Version format check: should be XX.YY.ZZ + $version = $manifest['version'] ?? ''; + if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) { + // Allow semver for node/go repos + if (!in_array($platform, ['mcp', 'node', 'go'], true)) { + $issues[] = [ + 'field' => 'version', + 'severity' => 'info', + 'message' => 'Version does not match XX.YY.ZZ format', + 'current' => $version, + 'fix' => null, + ]; + } + } + + return $issues; + } + + // ===================================================================== + // Output + // ===================================================================== + + private function printIssues(string $repoName, array $issues): void + { + if (empty($issues)) { + return; + } + + $errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error')); + $warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn')); + $infos = count($issues) - $errors - $warns; + + echo "\n"; + $summary = []; + if ($errors > 0) $summary[] = "{$errors} error(s)"; + if ($warns > 0) $summary[] = "{$warns} warning(s)"; + if ($infos > 0) $summary[] = "{$infos} info"; + $this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary)); + + foreach ($issues as $issue) { + $icon = match ($issue['severity']) { + 'error' => 'ERROR', + 'warn' => 'WARN', + default => 'INFO', + }; + $msg = sprintf(' %-18s %s', $issue['field'], $issue['message']); + if ($issue['current'] !== '') { + $msg .= " (current: {$issue['current']})"; + } + if ($issue['fix'] !== null) { + $msg .= " → fix: {$issue['fix']}"; + } + $this->log($icon, $msg); + } + } + + // ===================================================================== + // Fix application + // ===================================================================== + + private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int + { + $fixes = []; + foreach ($issues as $issue) { + if ($issue['fix'] !== null && $issue['fix'] !== '') { + $fixes[$issue['field']] = $issue['fix']; + } + } + + if (empty($fixes)) { + $this->log('INFO', "{$repo}: no auto-fixable issues"); + return 0; + } + + $merged = array_merge($current, $fixes); + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $payload = json_encode($merged); + + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", + 'content' => $payload, + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) { + $this->log('ERROR', "{$repo}: failed to push fixes"); + return 1; + } + + $this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes))); + return 0; + } + + // ===================================================================== + // API helpers + // ===================================================================== + + private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array + { + $url = "{$apiBase}/repos/{$org}/{$repo}/manifest"; + $ctx = stream_context_create([ + 'http' => [ + 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", + 'timeout' => 10, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) return null; + + $data = json_decode($body, true); + return is_array($data) ? $data : null; + } + + private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array + { + $allRepos = []; + $page = 1; + $limit = 50; + + while (true) { + $url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}"; + $ctx = stream_context_create([ + 'http' => [ + 'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n", + 'timeout' => 15, + ], + ]); + + $body = @file_get_contents($url, false, $ctx); + if ($body === false) return null; + + $repos = json_decode($body, true); + if (!is_array($repos) || empty($repos)) break; + + $allRepos = array_merge($allRepos, $repos); + + if (count($repos) < $limit) break; + $page++; + } + + // Filter out archived and empty repos + return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false)); + } + + // ===================================================================== + // Detection (delegates to manifest_detect logic) + // ===================================================================== + + private function runDetect(string $root, string $repoName): array + { + $script = __DIR__ . '/manifest_detect.php'; + $redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null'; + $cmd = sprintf( + 'php %s --path %s --repo %s --json --quiet %s', + escapeshellarg($script), + escapeshellarg($root), + escapeshellarg($repoName), + $redirect + ); + + $output = shell_exec($cmd) ?? ''; + + // Extract JSON object from output (skip banner/log lines) + if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) { + $data = json_decode($m[0], true); + if (is_array($data)) { + return $data; + } + } + + return []; + } + + private function detectRepoName(string $root): string + { + $gitConfig = "{$root}/.git/config"; + if (!file_exists($gitConfig)) { + return basename($root); + } + + $content = file_get_contents($gitConfig); + if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) { + return $m[1]; + } + + return basename($root); + } +} + +$app = new ManifestIntegrityCli(); +exit($app->execute()); diff --git a/cli/metadata_licensing.php b/cli/metadata_licensing.php new file mode 100644 index 0000000..416a757 --- /dev/null +++ b/cli/metadata_licensing.php @@ -0,0 +1,280 @@ +#!/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/manifest_licensing.php + * VERSION: 09.26.00 + * BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\{CliFramework, SourceResolver}; + +/** + * Reads the block from .mokogitea/manifest.xml and ensures that the + * Joomla extension manifest contains the correct and tags. + * + * manifest.xml licensing block example: + * + * + * true + * true + * https://git.mokoconsulting.tech/{org}/{repo}/updates.xml + * MyExtension Updates + * + * + * Supports {org} and {repo} placeholders in update-server URL, resolved from + * the manifest's block or git remote. + */ +class ManifestLicensingCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests'); + $this->addArgument('--path', 'Repository root path', '.'); + $this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false); + $this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false); + } + + protected function run(): int + { + $root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path'); + $fix = (bool) $this->getArgument('--fix'); + $ghOutput = (bool) $this->getArgument('--github-output'); + + // ── 1. Read manifest.xml ────────────────────────────────────────── + $manifestFile = "{$root}/.mokogitea/manifest.xml"; + + if (!file_exists($manifestFile)) { + $this->log('WARN', "No manifest.xml found at {$manifestFile}"); + $this->outputResult($ghOutput, 'skipped', 'No manifest.xml'); + return 0; + } + + $xml = @simplexml_load_file($manifestFile); + + if ($xml === false) { + $this->log('ERROR', "Failed to parse {$manifestFile}"); + return 1; + } + + // ── 2. Check if licensing is enabled ────────────────────────────── + if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') { + $this->log('INFO', 'Licensing not enabled in manifest.xml — skipping'); + $this->outputResult($ghOutput, 'skipped', 'Licensing not enabled'); + return 0; + } + + $licensingNode = $xml->licensing; + $dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true'; + $updateServerUrl = (string) ($licensingNode->{'update-server'} ?? ''); + $updateServerName = (string) ($licensingNode->{'update-server-name'} ?? ''); + + // ── 3. Resolve placeholders ─────────────────────────────────────── + $org = (string) ($xml->identity->org ?? ''); + $repo = (string) ($xml->identity->name ?? ''); + + // Fallback to git remote if manifest doesn't have org/name + if (empty($org) || empty($repo)) { + $remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null")); + + if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) { + if (empty($org)) { + $org = $m[1]; + } + if (empty($repo)) { + $repo = $m[2]; + } + } + } + + // Default update server URL if not specified + if (empty($updateServerUrl) && !empty($org) && !empty($repo)) { + $updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml"; + } + + // Resolve {org} and {repo} placeholders + $updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl); + + // Default server name from display-name or repo name + if (empty($updateServerName)) { + $displayName = (string) ($xml->identity->{'display-name'} ?? $repo); + $updateServerName = $displayName . ' Updates'; + } + + if (empty($updateServerUrl)) { + $this->log('ERROR', 'Cannot determine update server URL — set in manifest.xml or ensure org/repo are available'); + return 1; + } + + $this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}"); + $this->log('INFO', "Update server: {$updateServerUrl}"); + $this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no')); + + // ── 4. Find Joomla extension manifests ──────────────────────────── + $xmlFiles = array_merge( + SourceResolver::globSource($root, '*.xml'), + SourceResolver::globSource($root, 'packages/*/*.xml'), + glob("{$root}/*.xml") ?: [] + ); + + $packageManifest = null; + + foreach ($xmlFiles as $file) { + $content = file_get_contents($file); + + if (!str_contains($content, 'log('WARN', 'No Joomla extension manifest found'); + $this->outputResult($ghOutput, 'skipped', 'No extension manifest'); + return 0; + } + + $relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest)); + $this->log('INFO', "Package manifest: {$relPath}"); + + // ── 5. Check and fix the manifest ───────────────────────────────── + $content = file_get_contents($packageManifest); + $original = $content; + $changes = []; + + // --- 5a. Ensure block with correct URL --- + if (preg_match('#\s*#s', $content)) { + // Empty updateservers block — inject the server + $replacement = "\n" + . " {$updateServerUrl}\n" + . " "; + $content = preg_replace('#\s*#s', $replacement, $content); + $changes[] = 'Added update server URL to empty '; + } elseif (!str_contains($content, '')) { + // No updateservers at all — add before + $serverBlock = "\n \n" + . " {$updateServerUrl}\n" + . " \n"; + $content = str_replace('', $serverBlock . '', $content); + $changes[] = 'Added block'; + } else { + // updateservers exists — verify URL is correct + if (preg_match('#]*>([^<]+)#', $content, $m)) { + if ($m[1] !== $updateServerUrl) { + $content = preg_replace( + '#(]*>)[^<]+()#', + "\${1}{$updateServerUrl}\${2}", + $content + ); + $changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}"; + } + } + } + + // --- 5b. Ensure tag if required --- + if ($dlidEnabled) { + if (!str_contains($content, ' if present, otherwise before + $dlidTag = ' ' . "\n"; + + if (str_contains($content, '')) { + $content = str_replace('', $dlidTag . "\n ", $content); + } else { + $content = str_replace('', $dlidTag . '', $content); + } + + $changes[] = 'Added tag'; + } + } + + // --- 5c. Ensure for packages --- + if (str_contains($content, 'type="package"') && !str_contains($content, '')) { + $blockTag = ' true' . "\n"; + + if (str_contains($content, ' + $content = preg_replace( + '#(\s*\n)#', + "\${1}{$blockTag}", + $content + ); + } elseif (str_contains($content, '')) { + $content = str_replace('', $blockTag . "\n ", $content); + } else { + $content = str_replace('', $blockTag . '', $content); + } + + $changes[] = 'Added true'; + } + + // ── 6. Report and apply ─────────────────────────────────────────── + if (empty($changes)) { + $this->log('INFO', 'All licensing tags are correct — no changes needed'); + $this->outputResult($ghOutput, 'ok', 'No changes needed'); + return 0; + } + + foreach ($changes as $change) { + $this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change); + } + + if ($fix) { + file_put_contents($packageManifest, $content); + $this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)"); + $this->outputResult($ghOutput, 'fixed', implode('; ', $changes)); + } else { + $this->log('WARN', 'Run with --fix to apply changes'); + $this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes)); + return 1; + } + + return 0; + } + + /** + * Write result to $GITHUB_OUTPUT if requested. + */ + private function outputResult(bool $ghOutput, string $status, string $detail): void + { + if (!$ghOutput) { + return; + } + + $outputFile = getenv('GITHUB_OUTPUT'); + + if ($outputFile === false || $outputFile === '') { + echo "licensing_status={$status}\n"; + echo "licensing_detail={$detail}\n"; + return; + } + + $fh = fopen($outputFile, 'a'); + fwrite($fh, "licensing_status={$status}\n"); + fwrite($fh, "licensing_detail={$detail}\n"); + fclose($fh); + } +} + +$app = new ManifestLicensingCli(); +exit($app->execute()); diff --git a/cli/metadata_read.php b/cli/metadata_read.php new file mode 100644 index 0000000..988b39c --- /dev/null +++ b/cli/metadata_read.php @@ -0,0 +1,170 @@ +#!/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/manifest_read.php + * VERSION: 09.26.00 + * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\CliFramework; + +class ManifestReadCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Parse manifest.xml and output requested field(s) for CI consumption'); + $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'); + $ghOutput = $this->getArgument('--github-output'); + $jsonMode = $this->getArgument('--json'); + + // Determine mode + if ($ghOutput) { + $mode = 'github-output'; + } elseif ($showAll) { + $mode = 'all'; + } elseif ($jsonMode) { + $mode = 'json'; + } else { + $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"); + 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) { + // Convert field-name to FIELD_NAME for env var style + $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 ManifestReadCli(); +exit($app->execute()); -- 2.52.0 From a00cbf7d926629bb512c1f43d6c53a13aaeb505e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 18:20:57 -0500 Subject: [PATCH 4/5] 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()); -- 2.52.0 From 0a194828eef644db90d581849f3fa5ba6e0eccfc Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 11 Jun 2026 23:25:41 +0000 Subject: [PATCH 5/5] chore(version): auto-bump patch 09.26.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- cli/branch_rename.php | 2 +- cli/bulk_workflow_push.php | 2 +- cli/bulk_workflow_trigger.php | 2 +- cli/client_dashboard.php | 2 +- cli/client_inventory.php | 2 +- cli/client_provision.php | 2 +- cli/grafana_dashboard.php | 2 +- cli/joomla_build.php | 2 +- cli/metadata_detect.php | 2 +- cli/metadata_integrity.php | 2 +- cli/metadata_licensing.php | 2 +- cli/metadata_read.php | 2 +- cli/platform_detect.php | 2 +- cli/release_cascade.php | 2 +- cli/release_publish.php | 2 +- cli/scaffold_client.php | 2 +- cli/updates_xml_sync.php | 2 +- cli/version_auto_bump.php | 2 +- cli/version_check.php | 2 +- cli/wiki_sync.php | 2 +- cli/workflow_sync.php | 2 +- deploy/backup-before-deploy.php | 2 +- deploy/deploy-dolibarr.php | 2 +- deploy/health-check.php | 2 +- deploy/rollback-joomla.php | 2 +- deploy/sync-joomla.php | 2 +- mcp/servers/mokocrm_api/CONTRIBUTING.md | 2 +- mcp/servers/mokocrm_api/SECURITY.md | 2 +- mcp/servers/mokosuite_api/CONTRIBUTING.md | 2 +- mcp/servers/mokosuite_api/SECURITY.md | 2 +- .../repos/client-waas/.mokogitea/workflows/issue-branch.yml | 2 +- templates/repos/client-waas/CODE_OF_CONDUCT.md | 2 +- templates/repos/client-waas/CONTRIBUTING.md | 2 +- .../repos/dolibarr/.mokogitea/workflows/issue-branch.yml | 2 +- templates/repos/dolibarr/GOVERNANCE.md | 2 +- templates/repos/dolibarr/docs/update-server.md | 2 +- templates/repos/generic/.mokogitea/workflows/issue-branch.yml | 2 +- templates/repos/generic/CODE_OF_CONDUCT.md | 2 +- templates/repos/generic/SECURITY.md | 2 +- templates/repos/generic/docs/INSTALLATION.md | 2 +- templates/repos/generic/docs/templates/README-template.md | 2 +- templates/repos/joomla/.mokogitea/workflows/issue-branch.yml | 2 +- templates/repos/joomla/CODE_OF_CONDUCT.md | 2 +- templates/repos/joomla/GOVERNANCE.md | 2 +- templates/repos/joomla/SECURITY.md | 2 +- templates/repos/mcp/.mokogitea/workflows/issue-branch.yml | 2 +- templates/repos/mcp/CODE_OF_CONDUCT.md | 2 +- templates/repos/mcp/CONTRIBUTING.md | 2 +- templates/repos/mcp/docs/API.md | 2 +- templates/repos/mcp/docs/ARCHITECTURE.md | 2 +- templates/repos/mcp/docs/INSTALLATION.md | 2 +- tests/Unit/VersionBumpTest.php | 2 +- tests/Unit/VersionReadTest.php | 4 ++-- validate/check_file_integrity.php | 2 +- 56 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index d4329c1..f056073 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index 0ebbf9f..18ce55c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root INGROUP: MokoPlatform REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform PATH: /README.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Project overview and documentation --> diff --git a/cli/branch_rename.php b/cli/branch_rename.php index 679ba8d..fdb266e 100644 --- a/cli/branch_rename.php +++ b/cli/branch_rename.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/branch_rename.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old) */ diff --git a/cli/bulk_workflow_push.php b/cli/bulk_workflow_push.php index 691c498..542c9f6 100644 --- a/cli/bulk_workflow_push.php +++ b/cli/bulk_workflow_push.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/bulk_workflow_push.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Push a workflow file to all governed repos via the Gitea Contents API */ diff --git a/cli/bulk_workflow_trigger.php b/cli/bulk_workflow_trigger.php index 1befecc..84ec699 100644 --- a/cli/bulk_workflow_trigger.php +++ b/cli/bulk_workflow_trigger.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/bulk_workflow_trigger.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Trigger a workflow across multiple repos at once */ diff --git a/cli/client_dashboard.php b/cli/client_dashboard.php index f49f6db..a73dee4 100644 --- a/cli/client_dashboard.php +++ b/cli/client_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/client_dashboard.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Generate unified client dashboard HTML */ diff --git a/cli/client_inventory.php b/cli/client_inventory.php index dcfa339..fb15c7c 100644 --- a/cli/client_inventory.php +++ b/cli/client_inventory.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/client_inventory.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Discover and list all client-waas repos with their server configuration status */ diff --git a/cli/client_provision.php b/cli/client_provision.php index 300edd5..33cc4e7 100644 --- a/cli/client_provision.php +++ b/cli/client_provision.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/client_provision.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Provision a new client environment end-to-end */ diff --git a/cli/grafana_dashboard.php b/cli/grafana_dashboard.php index ee7580f..7ffbdee 100644 --- a/cli/grafana_dashboard.php +++ b/cli/grafana_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/grafana_dashboard.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Manage Grafana dashboards via API */ diff --git a/cli/joomla_build.php b/cli/joomla_build.php index a6cc210..eeadc88 100644 --- a/cli/joomla_build.php +++ b/cli/joomla_build.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/joomla_build.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Build a Joomla extension ZIP from manifest — all types supported * NOTE: Called by pre-release and auto-release workflows. */ diff --git a/cli/metadata_detect.php b/cli/metadata_detect.php index c0e5e57..5cb6203 100644 --- a/cli/metadata_detect.php +++ b/cli/metadata_detect.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/manifest_detect.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Auto-detect manifest fields from source files and optionally push to API */ diff --git a/cli/metadata_integrity.php b/cli/metadata_integrity.php index be93418..16fd8e5 100644 --- a/cli/metadata_integrity.php +++ b/cli/metadata_integrity.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/manifest_integrity.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Cross-check manifest API fields against repo contents across the org */ diff --git a/cli/metadata_licensing.php b/cli/metadata_licensing.php index 416a757..6f0c704 100644 --- a/cli/metadata_licensing.php +++ b/cli/metadata_licensing.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/manifest_licensing.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests */ diff --git a/cli/metadata_read.php b/cli/metadata_read.php index 2f37a23..0b5fd77 100644 --- a/cli/metadata_read.php +++ b/cli/metadata_read.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/metadata_read.php - * VERSION: 09.27.00 + * VERSION: 09.26.01 * BRIEF: Read and set metadata fields in .mokogitea/metadata.xml (or manifest.xml) */ diff --git a/cli/platform_detect.php b/cli/platform_detect.php index b9e89ab..6ed7993 100644 --- a/cli/platform_detect.php +++ b/cli/platform_detect.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/platform_detect.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Auto-detect repository platform type and optionally update manifest */ diff --git a/cli/release_cascade.php b/cli/release_cascade.php index f35bf87..a39a1d6 100644 --- a/cli/release_cascade.php +++ b/cli/release_cascade.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/release_cascade.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent. */ diff --git a/cli/release_publish.php b/cli/release_publish.php index 3b0ec69..e12b203 100644 --- a/cli/release_publish.php +++ b/cli/release_publish.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/release_publish.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Publish a release and create copies for all lesser stability streams. */ diff --git a/cli/scaffold_client.php b/cli/scaffold_client.php index 27af765..e87781b 100644 --- a/cli/scaffold_client.php +++ b/cli/scaffold_client.php @@ -12,7 +12,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/scaffold_client.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings */ diff --git a/cli/updates_xml_sync.php b/cli/updates_xml_sync.php index 5483355..dd40c10 100644 --- a/cli/updates_xml_sync.php +++ b/cli/updates_xml_sync.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/updates_xml_sync.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Sync updates.xml to target branches via Gitea API * NOTE: Called by pre-release and auto-release workflows after updates.xml * is modified on the current branch. Pushes the file to other branches diff --git a/cli/version_auto_bump.php b/cli/version_auto_bump.php index ef56670..b45f75b 100644 --- a/cli/version_auto_bump.php +++ b/cli/version_auto_bump.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/version_auto_bump.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash */ diff --git a/cli/version_check.php b/cli/version_check.php index fd1f8a9..8e93e9d 100644 --- a/cli/version_check.php +++ b/cli/version_check.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/version_check.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Validate version consistency across README, manifests, and sub-packages */ diff --git a/cli/wiki_sync.php b/cli/wiki_sync.php index 6964772..5b8e3f3 100644 --- a/cli/wiki_sync.php +++ b/cli/wiki_sync.php @@ -10,7 +10,7 @@ * INGROUP: mokoplatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /cli/wiki_sync.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Sync select wiki pages from mokoplatform to all template repos */ diff --git a/cli/workflow_sync.php b/cli/workflow_sync.php index e072fe5..db87e20 100644 --- a/cli/workflow_sync.php +++ b/cli/workflow_sync.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/workflow_sync.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform */ diff --git a/deploy/backup-before-deploy.php b/deploy/backup-before-deploy.php index c63358c..a39fa01 100644 --- a/deploy/backup-before-deploy.php +++ b/deploy/backup-before-deploy.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /deploy/backup-before-deploy.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Snapshot Joomla directories before deployment for rollback capability */ diff --git a/deploy/deploy-dolibarr.php b/deploy/deploy-dolibarr.php index 67c566b..dedeb9c 100644 --- a/deploy/deploy-dolibarr.php +++ b/deploy/deploy-dolibarr.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /deploy/deploy-dolibarr.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync */ diff --git a/deploy/health-check.php b/deploy/health-check.php index 2da0d95..fb5dc96 100644 --- a/deploy/health-check.php +++ b/deploy/health-check.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /deploy/health-check.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Post-deploy health check — verify a Joomla site is responding correctly */ diff --git a/deploy/rollback-joomla.php b/deploy/rollback-joomla.php index 2e10c3f..2d77aee 100644 --- a/deploy/rollback-joomla.php +++ b/deploy/rollback-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /deploy/rollback-joomla.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot */ diff --git a/deploy/sync-joomla.php b/deploy/sync-joomla.php index 97f2583..53daaf2 100644 --- a/deploy/sync-joomla.php +++ b/deploy/sync-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /deploy/sync-joomla.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Sync Joomla site directories between two servers via rsync over SSH */ diff --git a/mcp/servers/mokocrm_api/CONTRIBUTING.md b/mcp/servers/mokocrm_api/CONTRIBUTING.md index 7b44822..4a0acd1 100644 --- a/mcp/servers/mokocrm_api/CONTRIBUTING.md +++ b/mcp/servers/mokocrm_api/CONTRIBUTING.md @@ -14,7 +14,7 @@ DEFGROUP: dolibarr-api-mcp.Documentation INGROUP: dolibarr-api-mcp REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CONTRIBUTING.md BRIEF: Contribution guidelines for the project --> diff --git a/mcp/servers/mokocrm_api/SECURITY.md b/mcp/servers/mokocrm_api/SECURITY.md index b15d3db..e1b6254 100644 --- a/mcp/servers/mokocrm_api/SECURITY.md +++ b/mcp/servers/mokocrm_api/SECURITY.md @@ -10,7 +10,7 @@ DEFGROUP: dolibarr-api-mcp.Documentation INGROUP: dolibarr-api-mcp REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp PATH: /SECURITY.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/mcp/servers/mokosuite_api/CONTRIBUTING.md b/mcp/servers/mokosuite_api/CONTRIBUTING.md index 9785822..16ef2ea 100644 --- a/mcp/servers/mokosuite_api/CONTRIBUTING.md +++ b/mcp/servers/mokosuite_api/CONTRIBUTING.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CONTRIBUTING.md BRIEF: Contribution guidelines for the project --> diff --git a/mcp/servers/mokosuite_api/SECURITY.md b/mcp/servers/mokosuite_api/SECURITY.md index 17f6e88..170bc96 100644 --- a/mcp/servers/mokosuite_api/SECURITY.md +++ b/mcp/servers/mokosuite_api/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME] INGROUP: [PROJECT_NAME].Documentation REPO: [REPOSITORY_URL] PATH: /SECURITY.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/templates/repos/client-waas/.mokogitea/workflows/issue-branch.yml b/templates/repos/client-waas/.mokogitea/workflows/issue-branch.yml index 087a3af..382f37c 100644 --- a/templates/repos/client-waas/.mokogitea/workflows/issue-branch.yml +++ b/templates/repos/client-waas/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/templates/repos/client-waas/CODE_OF_CONDUCT.md b/templates/repos/client-waas/CODE_OF_CONDUCT.md index ec21081..6e9cd20 100644 --- a/templates/repos/client-waas/CODE_OF_CONDUCT.md +++ b/templates/repos/client-waas/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: mokoconsulting-tech/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CODE_OF_CONDUCT.md BRIEF: Contributor Covenant Code of Conduct version 1.3.0 --> diff --git a/templates/repos/client-waas/CONTRIBUTING.md b/templates/repos/client-waas/CONTRIBUTING.md index 9785822..16ef2ea 100644 --- a/templates/repos/client-waas/CONTRIBUTING.md +++ b/templates/repos/client-waas/CONTRIBUTING.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CONTRIBUTING.md BRIEF: Contribution guidelines for the project --> diff --git a/templates/repos/dolibarr/.mokogitea/workflows/issue-branch.yml b/templates/repos/dolibarr/.mokogitea/workflows/issue-branch.yml index 087a3af..382f37c 100644 --- a/templates/repos/dolibarr/.mokogitea/workflows/issue-branch.yml +++ b/templates/repos/dolibarr/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/templates/repos/dolibarr/GOVERNANCE.md b/templates/repos/dolibarr/GOVERNANCE.md index a5f0ea6..1d004c0 100644 --- a/templates/repos/dolibarr/GOVERNANCE.md +++ b/templates/repos/dolibarr/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.MokoStandards-Template-Dolibarr INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Dolibarr - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoStandards-Template-Dolibarr --> diff --git a/templates/repos/dolibarr/docs/update-server.md b/templates/repos/dolibarr/docs/update-server.md index 15e5028..818bb55 100644 --- a/templates/repos/dolibarr/docs/update-server.md +++ b/templates/repos/dolibarr/docs/update-server.md @@ -10,7 +10,7 @@ DEFGROUP: MokoStandards-Template-Dolibarr.Documentation INGROUP: MokoStandards.Templates REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-Template-Dolibarr PATH: /docs/update-server.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: How this module's update server file (update.txt) is managed --> diff --git a/templates/repos/generic/.mokogitea/workflows/issue-branch.yml b/templates/repos/generic/.mokogitea/workflows/issue-branch.yml index 087a3af..382f37c 100644 --- a/templates/repos/generic/.mokogitea/workflows/issue-branch.yml +++ b/templates/repos/generic/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/templates/repos/generic/CODE_OF_CONDUCT.md b/templates/repos/generic/CODE_OF_CONDUCT.md index ec21081..6e9cd20 100644 --- a/templates/repos/generic/CODE_OF_CONDUCT.md +++ b/templates/repos/generic/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: mokoconsulting-tech/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CODE_OF_CONDUCT.md BRIEF: Contributor Covenant Code of Conduct version 1.3.0 --> diff --git a/templates/repos/generic/SECURITY.md b/templates/repos/generic/SECURITY.md index 17f6e88..170bc96 100644 --- a/templates/repos/generic/SECURITY.md +++ b/templates/repos/generic/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME] INGROUP: [PROJECT_NAME].Documentation REPO: [REPOSITORY_URL] PATH: /SECURITY.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/templates/repos/generic/docs/INSTALLATION.md b/templates/repos/generic/docs/INSTALLATION.md index dc100dd..dd2a8df 100644 --- a/templates/repos/generic/docs/INSTALLATION.md +++ b/templates/repos/generic/docs/INSTALLATION.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION PATH: /docs/INSTALLATION.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Installation and setup instructions for [PROJECT_NAME] --> diff --git a/templates/repos/generic/docs/templates/README-template.md b/templates/repos/generic/docs/templates/README-template.md index 3466b1d..38cfa64 100644 --- a/templates/repos/generic/docs/templates/README-template.md +++ b/templates/repos/generic/docs/templates/README-template.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./README.md BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default --> diff --git a/templates/repos/joomla/.mokogitea/workflows/issue-branch.yml b/templates/repos/joomla/.mokogitea/workflows/issue-branch.yml index 087a3af..382f37c 100644 --- a/templates/repos/joomla/.mokogitea/workflows/issue-branch.yml +++ b/templates/repos/joomla/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/templates/repos/joomla/CODE_OF_CONDUCT.md b/templates/repos/joomla/CODE_OF_CONDUCT.md index ff4b6f2..917d462 100644 --- a/templates/repos/joomla/CODE_OF_CONDUCT.md +++ b/templates/repos/joomla/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: MokoStandards-Template-Joomla-Plugin INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin/ - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/templates/repos/joomla/GOVERNANCE.md b/templates/repos/joomla/GOVERNANCE.md index 96d9530..8a16157 100644 --- a/templates/repos/joomla/GOVERNANCE.md +++ b/templates/repos/joomla/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.MokoStandards-Template-Joomla-Plugin INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for MokoStandards-Template-Joomla-Plugin --> diff --git a/templates/repos/joomla/SECURITY.md b/templates/repos/joomla/SECURITY.md index 3fdebcd..02ccac0 100644 --- a/templates/repos/joomla/SECURITY.md +++ b/templates/repos/joomla/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: MokoStandards-Template-Joomla-Plugin INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Joomla-Plugin PATH: /SECURITY.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/templates/repos/mcp/.mokogitea/workflows/issue-branch.yml b/templates/repos/mcp/.mokogitea/workflows/issue-branch.yml index 087a3af..382f37c 100644 --- a/templates/repos/mcp/.mokogitea/workflows/issue-branch.yml +++ b/templates/repos/mcp/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 09.26.00 +# VERSION: 09.26.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/templates/repos/mcp/CODE_OF_CONDUCT.md b/templates/repos/mcp/CODE_OF_CONDUCT.md index ec21081..6e9cd20 100644 --- a/templates/repos/mcp/CODE_OF_CONDUCT.md +++ b/templates/repos/mcp/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: mokoconsulting-tech/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CODE_OF_CONDUCT.md BRIEF: Contributor Covenant Code of Conduct version 1.3.0 --> diff --git a/templates/repos/mcp/CONTRIBUTING.md b/templates/repos/mcp/CONTRIBUTING.md index 9785822..16ef2ea 100644 --- a/templates/repos/mcp/CONTRIBUTING.md +++ b/templates/repos/mcp/CONTRIBUTING.md @@ -14,7 +14,7 @@ DEFGROUP: INGROUP: Project.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic - VERSION: 09.26.00 + VERSION: 09.26.01 PATH: ./CONTRIBUTING.md BRIEF: Contribution guidelines for the project --> diff --git a/templates/repos/mcp/docs/API.md b/templates/repos/mcp/docs/API.md index a5d9ad9..d21ab64 100644 --- a/templates/repos/mcp/docs/API.md +++ b/templates/repos/mcp/docs/API.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION DEFGROUP: {{PROJECT_NAME}}.Documentation PATH: /docs/API.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: MCP tool reference documentation --> diff --git a/templates/repos/mcp/docs/ARCHITECTURE.md b/templates/repos/mcp/docs/ARCHITECTURE.md index 6abfa6c..74f6d4f 100644 --- a/templates/repos/mcp/docs/ARCHITECTURE.md +++ b/templates/repos/mcp/docs/ARCHITECTURE.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION DEFGROUP: {{PROJECT_NAME}}.Documentation PATH: /docs/ARCHITECTURE.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Architecture overview and design decisions --> diff --git a/templates/repos/mcp/docs/INSTALLATION.md b/templates/repos/mcp/docs/INSTALLATION.md index 0fb7751..52f8d78 100644 --- a/templates/repos/mcp/docs/INSTALLATION.md +++ b/templates/repos/mcp/docs/INSTALLATION.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION DEFGROUP: {{PROJECT_NAME}}.Documentation PATH: /docs/INSTALLATION.md -VERSION: 09.26.00 +VERSION: 09.26.01 BRIEF: Installation and setup instructions --> diff --git a/tests/Unit/VersionBumpTest.php b/tests/Unit/VersionBumpTest.php index 4962b7e..0eca056 100644 --- a/tests/Unit/VersionBumpTest.php +++ b/tests/Unit/VersionBumpTest.php @@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\nSome content\n" + "\nSome content\n" ); $this->execute(); diff --git a/tests/Unit/VersionReadTest.php b/tests/Unit/VersionReadTest.php index 95c15c6..b0a2d9e 100644 --- a/tests/Unit/VersionReadTest.php +++ b/tests/Unit/VersionReadTest.php @@ -34,7 +34,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "# Test\n\n" + "# Test\n\n" ); $this->assertSame('02.03.04', trim($this->runScript())); @@ -68,7 +68,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\n" + "\n" ); mkdir("{$this->tmpDir}/src", 0755, true); file_put_contents( diff --git a/validate/check_file_integrity.php b/validate/check_file_integrity.php index 74efd1a..ff62df2 100644 --- a/validate/check_file_integrity.php +++ b/validate/check_file_integrity.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform * PATH: /validate/check_file_integrity.php - * VERSION: 09.26.00 + * VERSION: 09.26.01 * BRIEF: Compare deployed files on a remote server against the local repository to detect drift */ -- 2.52.0