From 7e4d88eea59519db5842701e4ef51fb600a8a5a7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 29 Jun 2026 11:36:05 -0500 Subject: [PATCH 1/2] feat(cli): add theme_vars_check.php for client MokoOnyx themes Validates a client theme package: required CSS variables defined in both light.custom.css and dark.custom.css, required files present, and templateDetails.xml sanity (version, dynamic update server not raw/branch, dlid). Standalone (no CliFramework dependency). Authored-by: Moko Consulting --- cli/theme_vars_check.php | 141 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 cli/theme_vars_check.php diff --git a/cli/theme_vars_check.php b/cli/theme_vars_check.php new file mode 100644 index 0000000..47a091e --- /dev/null +++ b/cli/theme_vars_check.php @@ -0,0 +1,141 @@ +#!/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: Verify a client MokoOnyx theme defines all required CSS variables + * (light + dark) and passes basic theme-package sanity checks. + * + * Standalone (no CliFramework dependency) so it runs even when the shared + * framework autoloader is unavailable. + * + * Usage: php theme_vars_check.php --path . [--github-output] [--strict] + */ + +declare(strict_types=1); + +$opts = getopt('', ['path:', 'github-output', 'strict']); +$path = $opts['path'] ?? '.'; +if (!is_string($path) || $path === '') { $path = '.'; } +$gh = array_key_exists('github-output', $opts); +$root = realpath($path); +if ($root === false) { $root = $path; } + +// Locate the package source directory (src/ preferred, source/ legacy). +$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"; }; + +$cssDir = "$src/media/templates/site/mokoonyx/css"; +$themeDir = "$cssDir/theme"; +$light = "$themeDir/light.custom.css"; +$dark = "$themeDir/dark.custom.css"; +$manifest = "$src/templateDetails.xml"; + +echo "MokoOnyx theme validation: $src\n\n"; + +// 1) Required files ------------------------------------------------------- +echo "=== Required files ===\n"; +$requiredFiles = [ + 'templateDetails.xml' => $manifest, + 'theme/light.custom.css' => $light, + 'theme/dark.custom.css' => $dark, + 'user.css' => "$cssDir/user.css", +]; +foreach ($requiredFiles as $label => $file) { + is_file($file) ? $ok("$label present") : $fail("missing required file: $label"); +} + +// 2) Required CSS variables (must be defined in BOTH light and dark) ------ +$required = [ + // accent + Bootstrap contextual colors + '--color-primary', '--primary', '--secondary', '--success', '--info', + '--warning', '--danger', '--light', '--dark', + // rgb triplets (utilities rely on these) + '--primary-rgb', '--secondary-rgb', '--success-rgb', '--info-rgb', + '--warning-rgb', '--danger-rgb', '--light-rgb', '--dark-rgb', + // 5.3 emphasis + subtle tokens (theme-aware utilities) + '--primary-text-emphasis', '--success-text-emphasis', '--info-text-emphasis', + '--warning-text-emphasis', '--danger-text-emphasis', + '--primary-bg-subtle', '--success-bg-subtle', '--info-bg-subtle', + '--warning-bg-subtle', '--danger-bg-subtle', + // body + links + '--body-bg', '--body-color', '--body-bg-rgb', '--body-color-rgb', + '--color-link', '--color-hover', +]; + +echo "\n=== MokoOnyx CSS variables (light + dark) ===\n"; +foreach (['light' => $light, 'dark' => $dark] as $mode => $file) { + if (!is_file($file)) { continue; } // already reported as missing file + $css = (string) file_get_contents($file); + $missing = []; + foreach ($required as $var) { + if (!preg_match('/' . preg_quote($var, '/') . '\s*:/', $css)) { + $missing[] = $var; + } + } + if ($missing) { + $fail("$mode mode is missing " . count($missing) . ' variable(s): ' . implode(', ', $missing)); + } else { + $ok("$mode mode defines all " . count($required) . ' required 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 (…///updates.xml)'); + } else { + $ok("update server: $server"); + } + + isset($xml->dlid) ? $ok(' license-key field present') + : $fail(' is missing'); + } +} + +// 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); -- 2.52.0 From b1f2c391cb43859ebb359e5c25ff03e5d1d71321 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 29 Jun 2026 11:45:09 -0500 Subject: [PATCH 2/2] feat(cli): derive required vars from MokoOnyx standard theme + repo metadata - Read the required CSS variable set dynamically from the MokoOnyx standard theme (--reference checkout) instead of a hardcoded list, so it auto-syncs. Verifies client light/dark custom CSS defines every standard variable. - Add optional repo-metadata check via Gitea API (--api-base/--repo/--token): description, website, topics, default branch. - Drop unused --strict flag. Authored-by: Moko Consulting --- cli/theme_vars_check.php | 146 ++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 48 deletions(-) diff --git a/cli/theme_vars_check.php b/cli/theme_vars_check.php index 47a091e..428b448 100644 --- a/cli/theme_vars_check.php +++ b/cli/theme_vars_check.php @@ -10,25 +10,29 @@ * INGROUP: mokocli * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli * PATH: /cli/theme_vars_check.php - * BRIEF: Verify a client MokoOnyx theme defines all required CSS variables - * (light + dark) and passes basic theme-package sanity checks. + * 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 . [--github-output] [--strict] + * Usage: + * php theme_vars_check.php --path . --reference /tmp/mokoonyx [--github-output] + * [--api-base --repo --token ] */ declare(strict_types=1); -$opts = getopt('', ['path:', 'github-output', 'strict']); -$path = $opts['path'] ?? '.'; -if (!is_string($path) || $path === '') { $path = '.'; } -$gh = array_key_exists('github-output', $opts); -$root = realpath($path); +$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; } -// Locate the package source directory (src/ preferred, source/ legacy). $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"); @@ -42,60 +46,73 @@ $fail = function (string $msg) use (&$errors, &$summary): void { $summary[] = "FAIL: $msg"; $errors++; }; -$ok = function (string $msg): void { echo " [ok] $msg\n"; }; +$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"; -$light = "$themeDir/light.custom.css"; -$dark = "$themeDir/dark.custom.css"; $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' => $light, - 'theme/dark.custom.css' => $dark, - 'user.css' => "$cssDir/user.css", + '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) Required CSS variables (must be defined in BOTH light and dark) ------ -$required = [ - // accent + Bootstrap contextual colors - '--color-primary', '--primary', '--secondary', '--success', '--info', - '--warning', '--danger', '--light', '--dark', - // rgb triplets (utilities rely on these) - '--primary-rgb', '--secondary-rgb', '--success-rgb', '--info-rgb', - '--warning-rgb', '--danger-rgb', '--light-rgb', '--dark-rgb', - // 5.3 emphasis + subtle tokens (theme-aware utilities) - '--primary-text-emphasis', '--success-text-emphasis', '--info-text-emphasis', - '--warning-text-emphasis', '--danger-text-emphasis', - '--primary-bg-subtle', '--success-bg-subtle', '--info-bg-subtle', - '--warning-bg-subtle', '--danger-bg-subtle', - // body + links - '--body-bg', '--body-color', '--body-bg-rgb', '--body-color-rgb', - '--color-link', '--color-hover', -]; - -echo "\n=== MokoOnyx CSS variables (light + dark) ===\n"; -foreach (['light' => $light, 'dark' => $dark] as $mode => $file) { - if (!is_file($file)) { continue; } // already reported as missing file - $css = (string) file_get_contents($file); - $missing = []; - foreach ($required as $var) { - if (!preg_match('/' . preg_quote($var, '/') . '\s*:/', $css)) { - $missing[] = $var; +// 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'); } - } - if ($missing) { - $fail("$mode mode is missing " . count($missing) . ' variable(s): ' . implode(', ', $missing)); - } else { - $ok("$mode mode defines all " . count($required) . ' required variables'); } } @@ -113,7 +130,7 @@ if (is_file($manifest)) { 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 (…///updates.xml)'); + $fail('update server uses a legacy raw/branch URL; use the dynamic MokoGitea feed'); } else { $ok("update server: $server"); } @@ -123,6 +140,39 @@ if (is_file($manifest)) { } } +// 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'))) { -- 2.52.0