#!/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();