#!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: mokocli.CLI * INGROUP: mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * PATH: /cli/theme_vars_check.php * BRIEF: Validate a client MokoOnyx theme package — required CSS variables * (derived dynamically from the MokoOnyx standard theme) are defined in * the client's light/dark custom CSS, required files exist, the manifest * is sane, and (optionally) the repo's Gitea metadata is set. * * Standalone (no CliFramework dependency) so it runs even when the shared * framework autoloader is unavailable. * * Usage: * php theme_vars_check.php --path . --reference /tmp/mokoonyx [--github-output] * [--api-base --repo --token ] */ declare(strict_types=1); $opts = getopt('', ['path:', 'reference:', 'github-output', 'api-base:', 'repo:', 'token:']); $path = isset($opts['path']) && is_string($opts['path']) && $opts['path'] !== '' ? $opts['path'] : '.'; $ref = isset($opts['reference']) && is_string($opts['reference']) ? $opts['reference'] : ''; $gh = array_key_exists('github-output', $opts); $root = realpath($path); if ($root === false) { $root = $path; } $src = is_dir("$root/src") ? "$root/src" : (is_dir("$root/source") ? "$root/source" : null); if ($src === null) { fwrite(STDERR, "ERROR: no src/ or source/ directory under $root\n"); exit(1); } $errors = 0; $summary = []; $fail = function (string $msg) use (&$errors, &$summary): void { echo " [FAIL] $msg\n"; $summary[] = "FAIL: $msg"; $errors++; }; $ok = function (string $msg): void { echo " [ok] $msg\n"; }; $note = function (string $msg): void { echo " [note] $msg\n"; }; /** Extract the set of CSS custom-property names DEFINED in a CSS string. */ $definedVars = static function (string $css): array { // Matches "--name:" at a declaration position (not var(--name) uses). preg_match_all('/(?:^|[\s;{])(--[a-z0-9_-]+)\s*:/i', $css, $m); return array_values(array_unique(array_map('strtolower', $m[1] ?? []))); }; /** Find the MokoOnyx standard theme file for a mode inside a reference checkout. */ $findStandard = static function (string $ref, string $mode): ?string { if ($ref === '') { return null; } foreach ([ "$ref/source/media/css/theme/$mode.standard.css", "$ref/media/templates/site/mokoonyx/css/theme/$mode.standard.css", "$ref/$mode.standard.css", ] as $cand) { if (is_file($cand)) { return $cand; } } return null; }; $cssDir = "$src/media/templates/site/mokoonyx/css"; $themeDir = "$cssDir/theme"; $manifest = "$src/templateDetails.xml"; $customs = ['light' => "$themeDir/light.custom.css", 'dark' => "$themeDir/dark.custom.css"]; echo "MokoOnyx theme validation: $src\n\n"; // 1) Required files ------------------------------------------------------- echo "=== Required files ===\n"; $requiredFiles = [ 'templateDetails.xml' => $manifest, 'theme/light.custom.css' => $customs['light'], 'theme/dark.custom.css' => $customs['dark'], 'user.css' => "$cssDir/user.css", ]; foreach ($requiredFiles as $label => $file) { is_file($file) ? $ok("$label present") : $fail("missing required file: $label"); } // 2) CSS variables — required set derived from the MokoOnyx standard theme echo "\n=== CSS variables vs MokoOnyx standard theme ===\n"; if ($ref === '') { $note('no --reference given; skipping variable parity check'); } else { foreach ($customs as $mode => $customFile) { $std = $findStandard($ref, $mode); if ($std === null) { $fail("$mode: standard theme not found under reference '$ref'"); continue; } if (!is_file($customFile)) { continue; // already reported missing } $required = $definedVars((string) file_get_contents($std)); $defined = $definedVars((string) file_get_contents($customFile)); $missing = array_values(array_diff($required, $defined)); if ($missing) { $shown = array_slice($missing, 0, 15); $more = count($missing) - count($shown); $fail("$mode mode missing " . count($missing) . '/' . count($required) . ' standard variable(s): ' . implode(', ', $shown) . ($more > 0 ? " (+$more more)" : '')); } else { $ok("$mode mode defines all " . count($required) . ' standard variables'); } } } // 3) Manifest sanity ------------------------------------------------------ echo "\n=== Manifest (templateDetails.xml) ===\n"; if (is_file($manifest)) { $xml = @simplexml_load_file($manifest); if ($xml === false) { $fail('templateDetails.xml is not well-formed XML'); } else { $version = isset($xml->version) ? trim((string) $xml->version) : ''; $version !== '' ? $ok("version $version") : $fail(' is missing or empty'); $server = isset($xml->updateservers->server) ? trim((string) $xml->updateservers->server) : ''; if ($server === '') { $fail(' is missing'); } elseif (strpos($server, '/raw/branch/') !== false) { $fail('update server uses a legacy raw/branch URL; use the dynamic MokoGitea feed'); } else { $ok("update server: $server"); } isset($xml->dlid) ? $ok(' license-key field present') : $fail(' is missing'); // Required manifest fields foreach (['name', 'element', 'author', 'creationDate'] as $field) { (isset($xml->$field) && trim((string) $xml->$field) !== '') ? $ok("<$field> present") : $fail("<$field> is missing or empty"); } // scriptfile (if declared) must exist if (isset($xml->scriptfile)) { $sf = trim((string) $xml->scriptfile); is_file("$src/$sf") ? $ok("scriptfile '$sf' present") : $fail(" references '$sf' which is not in src/"); } // Fileset integrity — every referenced file/folder must exist in src/ if (isset($xml->fileset)) { $missingFs = []; foreach ($xml->fileset->files as $files) { $folder = trim((string) ($files['folder'] ?? '')); $baseDir = $folder !== '' ? "$src/$folder" : $src; foreach ($files->filename as $fn) { $rel = ($folder !== '' ? "$folder/" : '') . trim((string) $fn); if (!is_file("$baseDir/" . trim((string) $fn))) { $missingFs[] = $rel; } } foreach ($files->folder as $fd) { $rel = ($folder !== '' ? "$folder/" : '') . trim((string) $fd) . '/'; if (!is_dir("$baseDir/" . trim((string) $fd))) { $missingFs[] = $rel; } } } if ($missingFs) { $shown = array_slice($missingFs, 0, 10); $more = count($missingFs) - count($shown); $fail('fileset references ' . count($missingFs) . ' missing path(s): ' . implode(', ', $shown) . ($more > 0 ? " (+$more more)" : '')); } else { $ok('all fileset paths exist in src/'); } } } } // 4) Repository metadata via Gitea API (optional) ------------------------- $apiBase = isset($opts['api-base']) && is_string($opts['api-base']) ? rtrim($opts['api-base'], '/') : ''; $repoSlug = isset($opts['repo']) && is_string($opts['repo']) ? trim($opts['repo']) : ''; $token = isset($opts['token']) && is_string($opts['token']) ? trim($opts['token']) : ''; if ($apiBase !== '' && $repoSlug !== '' && $token !== '') { echo "\n=== Repository metadata (Gitea API) ===\n"; $url = "$apiBase/repos/$repoSlug"; $ctx = stream_context_create(['http' => [ 'method' => 'GET', 'header' => "Authorization: token $token\r\nAccept: application/json\r\n", 'ignore_errors' => true, 'timeout' => 15, ]]); $resp = @file_get_contents($url, false, $ctx); $data = $resp !== false ? json_decode($resp, true) : null; if (!is_array($data) || !isset($data['name'])) { $fail("could not read repo metadata from Gitea API ($url)"); } else { trim((string) ($data['description'] ?? '')) !== '' ? $ok('description set') : $fail('repo description is empty'); trim((string) ($data['website'] ?? '')) !== '' ? $ok('website set: ' . $data['website']) : $fail('repo website is empty'); $topics = $data['topics'] ?? []; (is_array($topics) && count($topics) > 0) ? $ok(count($topics) . ' topic(s) set') : $fail('repo has no topics'); ((string) ($data['default_branch'] ?? '')) === 'main' ? $ok('default branch is main') : $fail("default branch is '" . ($data['default_branch'] ?? '') . "' (expected main)"); } } else { echo "\n=== Repository metadata (Gitea API) ===\n"; $note('API args not provided; skipping repo metadata check'); } // Output / exit ----------------------------------------------------------- echo "\n"; if ($gh && ($f = getenv('GITHUB_STEP_SUMMARY'))) { $md = "## MokoOnyx Theme Validation\n\n"; $md .= $errors === 0 ? "All checks passed.\n" : implode("\n", array_map(static fn ($l) => "- $l", $summary)) . "\n"; @file_put_contents($f, $md, FILE_APPEND); } if ($errors > 0) { echo "FAILED: $errors issue(s) found.\n"; exit(1); } echo "All MokoOnyx theme validation checks passed.\n"; exit(0);