#!/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: 09.22.00 * BRIEF: Sync select wiki pages from moko-platform to all template repos */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class WikiSyncCli extends CliFramework { 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 $allTemplates = false; private bool $allStandards = false; private int $synced = 0; private int $created = 0; private int $skipped = 0; private int $errors = 0; protected function configure(): void { $this->setDescription('Sync wiki pages from moko-platform to template repos'); $this->addArgument('--token', 'Gitea API token (required)', ''); $this->addArgument('--org', 'Organization (default: MokoConsulting)', 'MokoConsulting'); $this->addArgument('--source', 'Source repo (default: moko-platform)', 'moko-platform'); $this->addArgument('--target', 'Target repo (can repeat)', ''); $this->addArgument('--page', 'Page to sync (can repeat)', ''); $this->addArgument('--all-standards', 'Sync all UPPERCASE standards pages', false); $this->addArgument('--all-templates', 'Target all Template-* repos', false); } protected function run(): int { $this->token = $this->getArgument('--token'); $this->org = $this->getArgument('--org'); $this->sourceRepo = $this->getArgument('--source'); $this->allStandards = (bool) $this->getArgument('--all-standards'); $this->allTemplates = (bool) $this->getArgument('--all-templates'); // Handle repeatable args from raw argv global $argv; foreach ($argv as $i => $arg) { if ($arg === '--target' && isset($argv[$i + 1])) { $this->targetRepos[] = $argv[$i + 1]; } if ($arg === '--page' && isset($argv[$i + 1])) { $this->pages[] = $argv[$i + 1]; } } if ($this->token === '') { $this->log('ERROR', '--token is required.'); return 1; } if (empty($this->pages) && !$this->allStandards) { $this->log('ERROR', '--page or --all-standards is required.'); return 1; } // Discover template repos if --all-templates if ($this->allTemplates || empty($this->targetRepos)) { $this->targetRepos = $this->discoverTemplateRepos(); } if (empty($this->targetRepos)) { $this->log('INFO', '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('INFO', "Syncing " . count($this->pages) . " page(s) to " . count($this->targetRepos) . " repo(s)"); if ($this->dryRun) { $this->log('INFO', "[DRY RUN] No changes will be made.\n"); } foreach ($this->pages as $pageName) { $this->log('INFO', "\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('INFO', " {$repo}: IDENTICAL (skipped)"); $this->skipped++; continue; } if ($this->dryRun) { $action = $existing !== null ? 'WOULD UPDATE' : 'WOULD CREATE'; $this->log('INFO', " {$repo}: {$action}"); continue; } if ($existing !== null) { $ok = $this->updateWikiPage($repo, $pageName, $sourceContent); $this->log('INFO', " {$repo}: " . ($ok ? 'UPDATED' : 'ERROR')); $ok ? $this->synced++ : $this->errors++; } else { $ok = $this->createWikiPage($repo, $pageName, $sourceContent); $this->log('INFO', " {$repo}: " . ($ok ? 'CREATED' : 'ERROR')); $ok ? $this->created++ : $this->errors++; } } } $this->log('INFO', "\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('INFO', "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('INFO', "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; } } $app = new WikiSyncCli(); exit($app->execute());