diff --git a/cli/wiki_sync.php b/cli/wiki_sync.php new file mode 100644 index 0000000..4b55e0f --- /dev/null +++ b/cli/wiki_sync.php @@ -0,0 +1,282 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: moko-platform.CLI + * INGROUP: moko-platform + * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + * PATH: /cli/wiki_sync.php + * VERSION: 01.00.00 + * BRIEF: Sync select wiki pages from moko-platform to all template repos + */ + +declare(strict_types=1); + +final class WikiSync +{ + private string $giteaUrl = 'https://git.mokoconsulting.tech'; + private string $token = ''; + private string $org = 'MokoConsulting'; + private string $sourceRepo = 'moko-platform'; + private array $targetRepos = []; + private array $pages = []; + private bool $dryRun = false; + private bool $allTemplates = false; + + private int $synced = 0; + private int $created = 0; + private int $skipped = 0; + private int $errors = 0; + + public function run(): int + { + $this->parseArgs(); + + if ($this->token === '') { + $this->log('ERROR: --token is required.'); + $this->printUsage(); + return 1; + } + + if (empty($this->pages) && !$this->allTemplates) { + $this->log('ERROR: --page or --all-standards is required.'); + $this->printUsage(); + return 1; + } + + // Discover template repos if --all-templates + if ($this->allTemplates || empty($this->targetRepos)) { + $this->targetRepos = $this->discoverTemplateRepos(); + } + + if (empty($this->targetRepos)) { + $this->log('No target repos found.'); + return 0; + } + + // If --all-standards, get all pages that start with uppercase + if (empty($this->pages)) { + $this->pages = $this->getStandardsPages(); + } + + $this->log("Syncing " . count($this->pages) . " page(s) to " . count($this->targetRepos) . " repo(s)"); + if ($this->dryRun) { + $this->log("[DRY RUN] No changes will be made.\n"); + } + + foreach ($this->pages as $pageName) { + $this->log("\n--- Page: {$pageName} ---"); + $sourceContent = $this->getWikiPage($this->sourceRepo, $pageName); + if ($sourceContent === null) { + $this->log(" WARNING: page not found in {$this->sourceRepo}"); + $this->errors++; + continue; + } + + foreach ($this->targetRepos as $repo) { + $existing = $this->getWikiPage($repo, $pageName); + if ($existing !== null && $existing === $sourceContent) { + $this->log(" {$repo}: IDENTICAL (skipped)"); + $this->skipped++; + continue; + } + + if ($this->dryRun) { + $action = $existing !== null ? 'WOULD UPDATE' : 'WOULD CREATE'; + $this->log(" {$repo}: {$action}"); + continue; + } + + if ($existing !== null) { + $ok = $this->updateWikiPage($repo, $pageName, $sourceContent); + $this->log(" {$repo}: " . ($ok ? 'UPDATED' : 'ERROR')); + $ok ? $this->synced++ : $this->errors++; + } else { + $ok = $this->createWikiPage($repo, $pageName, $sourceContent); + $this->log(" {$repo}: " . ($ok ? 'CREATED' : 'ERROR')); + $ok ? $this->created++ : $this->errors++; + } + } + } + + $this->log("\nDone: {$this->synced} updated, {$this->created} created, {$this->skipped} skipped, {$this->errors} error(s)"); + return $this->errors > 0 ? 1 : 0; + } + + private function discoverTemplateRepos(): array + { + $repos = $this->apiGet("/orgs/{$this->org}/repos?limit=100"); + $templates = []; + foreach ($repos as $repo) { + if (str_starts_with($repo['name'], 'Template-') && !($repo['archived'] ?? false)) { + $templates[] = $repo['name']; + } + } + sort($templates); + $this->log("Found template repos: " . implode(', ', $templates)); + return $templates; + } + + private function getStandardsPages(): array + { + $pages = $this->apiGet("/repos/{$this->org}/{$this->sourceRepo}/wiki/pages"); + $standards = []; + foreach ($pages as $page) { + $title = $page['title'] ?? ''; + // Sync pages that are all-caps with underscores (standards pages) + if (preg_match('/^[A-Z][A-Z0-9_-]+$/', $title)) { + $standards[] = $title; + } + } + sort($standards); + $this->log("Found " . count($standards) . " standards pages: " . implode(', ', $standards)); + return $standards; + } + + private function getWikiPage(string $repo, string $pageName): ?string + { + $data = $this->apiGet("/repos/{$this->org}/{$repo}/wiki/page/{$pageName}"); + if ($data === null || !isset($data['content_base64'])) { + return null; + } + return base64_decode($data['content_base64']); + } + + private function createWikiPage(string $repo, string $pageName, string $content): bool + { + $payload = json_encode([ + 'title' => $pageName, + 'content_base64' => base64_encode($content), + ]); + return $this->apiPost("/repos/{$this->org}/{$repo}/wiki/new", $payload) !== null; + } + + private function updateWikiPage(string $repo, string $pageName, string $content): bool + { + $payload = json_encode([ + 'title' => $pageName, + 'content_base64' => base64_encode($content), + ]); + return $this->apiPatch("/repos/{$this->org}/{$repo}/wiki/page/{$pageName}", $payload) !== null; + } + + private function apiGet(string $endpoint): ?array + { + $url = "{$this->giteaUrl}/api/v1{$endpoint}"; + $opts = [ + 'http' => [ + 'method' => 'GET', + 'header' => "Authorization: token {$this->token}\r\nAccept: application/json\r\n", + 'ignore_errors' => true, + ], + ]; + $ctx = stream_context_create($opts); + $result = @file_get_contents($url, false, $ctx); + if ($result === false) return null; + $data = json_decode($result, true); + return is_array($data) ? $data : null; + } + + private function apiPost(string $endpoint, string $payload): ?array + { + return $this->apiWrite('POST', $endpoint, $payload); + } + + private function apiPatch(string $endpoint, string $payload): ?array + { + return $this->apiWrite('PATCH', $endpoint, $payload); + } + + private function apiWrite(string $method, string $endpoint, string $payload): ?array + { + $url = "{$this->giteaUrl}/api/v1{$endpoint}"; + $opts = [ + 'http' => [ + 'method' => $method, + 'header' => "Authorization: token {$this->token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n", + 'content' => $payload, + 'ignore_errors' => true, + ], + ]; + $ctx = stream_context_create($opts); + $result = @file_get_contents($url, false, $ctx); + if ($result === false) return null; + $data = json_decode($result, true); + return is_array($data) ? $data : null; + } + + private function parseArgs(): void + { + global $argv; + $args = $argv; + for ($i = 1; $i < count($args); $i++) { + switch ($args[$i]) { + case '--token': + $this->token = $args[++$i] ?? ''; + break; + case '--org': + $this->org = $args[++$i] ?? ''; + break; + case '--source': + $this->sourceRepo = $args[++$i] ?? ''; + break; + case '--target': + $this->targetRepos[] = $args[++$i] ?? ''; + break; + case '--page': + $this->pages[] = $args[++$i] ?? ''; + break; + case '--all-standards': + $this->pages = []; // will be populated from source wiki + $this->allTemplates = true; + break; + case '--all-templates': + $this->allTemplates = true; + break; + case '--dry-run': + $this->dryRun = true; + break; + case '--help': + case '-h': + $this->printUsage(); + exit(0); + default: + $this->log("WARNING: Unknown argument: {$args[$i]}"); + break; + } + } + } + + private function printUsage(): void + { + $this->log('Usage: wiki_sync.php --token [options]'); + $this->log(''); + $this->log('Sync wiki pages from moko-platform to template repos.'); + $this->log(''); + $this->log('Options:'); + $this->log(' --token Gitea API token (required)'); + $this->log(' --org Organization (default: MokoConsulting)'); + $this->log(' --source Source repo (default: moko-platform)'); + $this->log(' --target Target repo (can repeat; default: all Template-* repos)'); + $this->log(' --page Page to sync (can repeat)'); + $this->log(' --all-standards Sync all UPPERCASE standards pages'); + $this->log(' --all-templates Target all Template-* repos'); + $this->log(' --dry-run Show what would be done'); + $this->log(' --help, -h Show this help'); + $this->log(''); + $this->log('Examples:'); + $this->log(' php wiki_sync.php --token xxx --page MANIFEST_STANDARD --all-templates'); + $this->log(' php wiki_sync.php --token xxx --all-standards --all-templates --dry-run'); + $this->log(' php wiki_sync.php --token xxx --page WORKFLOW_STANDARDS --target Template-Joomla'); + } + + private function log(string $msg): void + { + fwrite(STDERR, $msg . "\n"); + } +} + +(new WikiSync())->run(); diff --git a/definitions/manifest-schema.xsd b/definitions/manifest-schema.xsd new file mode 100644 index 0000000..c8ca0c2 --- /dev/null +++ b/definitions/manifest-schema.xsd @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +