diff --git a/mcp/src/index.ts b/mcp/src/index.ts index 2cf80a2..016df77 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -399,6 +399,42 @@ server.tool( }, ); +// ── Wiki Health & Sync ────────────────────────────────────────────────── + +server.tool( + 'standards_check_wiki', + 'Check wiki health for a repository — verifies Home page exists, has MokoStandards reference, and pages are indexed', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_wiki_health.php', ['--path', repo_path, '--json']); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_sync_wikis', + 'Sync Gitea wiki repos to their GitHub mirrors. Optionally filter by repo name.', + { + repo: z.string().optional().describe('Filter to a specific repo name (e.g. "MokoOnyx"). Omit to sync all.'), + }, + async ({ repo }) => { + const scriptPath = resolve(config.apiPath, 'scripts', 'sync-wikis-to-github.sh'); + const args = repo ? [scriptPath, repo] : [scriptPath]; + const { execFile } = await import('node:child_process'); + return new Promise((resolvePromise) => { + execFile('bash', args, { + timeout: 120_000, + maxBuffer: 5 * 1024 * 1024, + env: { ...process.env, GH_TOKEN: process.env.GH_TOKEN ?? '' }, + }, (err, stdout, stderr) => { + resolvePromise(textResult(stdout || stderr || err?.message || 'No output')); + }); + }); + }, +); + // ── Server Info ───────────────────────────────────────────────────────── server.tool( diff --git a/scripts/sync-wikis-to-github.sh b/scripts/sync-wikis-to-github.sh new file mode 100644 index 0000000..0d786fe --- /dev/null +++ b/scripts/sync-wikis-to-github.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# sync-wikis-to-github.sh — Mirror Gitea wiki repos to their GitHub counterparts +# +# Clones each Gitea wiki repo, adds the GitHub wiki as a remote, and force-pushes. +# Requires GH_TOKEN env var or ~/.github-token for authentication. +# +# Usage: +# sync-wikis-to-github.sh [repo-name] # sync one repo +# sync-wikis-to-github.sh # sync all configured repos +# +# Requires: git, curl +# +set -euo pipefail + +GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" +GITHUB_ORG="${GITHUB_ORG:-mokoconsulting-tech}" +GH_TOKEN="${GH_TOKEN:-$(cat ~/.github-token 2>/dev/null || echo "")}" +GITEA_TOKEN="${GITEA_TOKEN:-$(cat ~/.gitea-token 2>/dev/null || echo "")}" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +log() { echo "[$(date '+%H:%M:%S')] $*"; } + +if [[ -z "$GH_TOKEN" ]]; then + log "ERROR: GH_TOKEN not set. Export it or create ~/.github-token" + exit 1 +fi + +# Repo mappings: gitea_owner/repo → github_repo (same name unless overridden) +declare -A REPOS=( + ["MokoConsulting/moko-platform"]="moko-platform" + ["MokoConsulting/MokoOnyx"]="MokoOnyx" + ["MokoConsulting/MokoWaaS"]="MokoWaaS" + ["MokoConsulting/monitor-mcp"]="monitor-mcp" + ["MokoConsulting/deploy-mcp"]="deploy-mcp" + ["MokoConsulting/ssh-mcp"]="ssh-mcp" + ["MokoConsulting/backup-mcp"]="backup-mcp" + ["MokoConsulting/joomla-api-mcp"]="joomla-api-mcp" + ["MokoConsulting/Template-Client-WaaS"]="Template-Client-WaaS" + ["ClarksvilleFurs/client-waas-clarksvillefurs"]="client-waas-clarksvillefurs" +) + +FILTER="${1:-}" +SYNCED=0 +FAILED=0 + +for GITEA_REPO in "${!REPOS[@]}"; do + GH_REPO="${REPOS[$GITEA_REPO]}" + + # Filter to specific repo if requested + if [[ -n "$FILTER" && "$GH_REPO" != "$FILTER" && "$GITEA_REPO" != *"$FILTER"* ]]; then + continue + fi + + log "Syncing wiki: ${GITEA_REPO} → github/${GH_REPO}" + + WORK="${TMPDIR}/${GH_REPO}.wiki" + + # Clone Gitea wiki + GITEA_WIKI_URL="${GITEA_URL}/${GITEA_REPO}.wiki.git" + if ! git clone --quiet "$GITEA_WIKI_URL" "$WORK" 2>/dev/null; then + log " SKIP: no wiki on Gitea" + continue + fi + + cd "$WORK" + + # Add GitHub wiki remote + GH_WIKI_URL="https://${GH_TOKEN}@github.com/${GITHUB_ORG}/${GH_REPO}.wiki.git" + + # Ensure GitHub wiki exists (create initial page if needed) + WIKI_CHECK=$(curl -sf -H "Authorization: token ${GH_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_ORG}/${GH_REPO}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('has_wiki', False))" 2>/dev/null || echo "False") + + if [[ "$WIKI_CHECK" != "True" ]]; then + log " SKIP: GitHub wiki not enabled for ${GH_REPO}" + FAILED=$((FAILED + 1)) + continue + fi + + git remote add github "$GH_WIKI_URL" 2>/dev/null || git remote set-url github "$GH_WIKI_URL" + + # Force push to GitHub wiki + if git push --force --quiet github master:master 2>/dev/null || git push --force --quiet github main:master 2>/dev/null; then + SYNCED=$((SYNCED + 1)) + log " OK: synced" + else + FAILED=$((FAILED + 1)) + log " FAIL: push failed" + fi + + cd "$TMPDIR" +done + +log "Done. Synced: ${SYNCED}, Failed: ${FAILED}" diff --git a/validate/check_wiki_health.php b/validate/check_wiki_health.php new file mode 100644 index 0000000..8a0d89b --- /dev/null +++ b/validate/check_wiki_health.php @@ -0,0 +1,153 @@ +#!/usr/bin/env php + + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoStandards.Validate + * INGROUP: MokoStandards + * PATH: /validate/check_wiki_health.php + * VERSION: 01.00.00 + * BRIEF: Validate wiki health — checks Home page exists, has MokoStandards link, pages are indexed + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../vendor/autoload.php'; + +use MokoEnterprise\CLIApp; + +class CheckWikiHealth extends CLIApp +{ + public function __construct() + { + parent::__construct('check-wiki-health', 'Validate wiki health for a repository', '01.00.00'); + } + + protected function setupArguments(): array + { + return [ + 'path:' => 'Repository path (default: current directory)', + 'gitea-url:' => 'Gitea base URL (default: https://git.mokoconsulting.tech)', + 'token:' => 'Gitea API token (or set GITEA_TOKEN env var)', + ]; + } + + protected function run(): int + { + $repoPath = realpath($this->getOption('path', '.')) ?: '.'; + $giteaUrl = $this->getOption('gitea-url', 'https://git.mokoconsulting.tech'); + $token = $this->getOption('token', getenv('GITEA_TOKEN') ?: ''); + + // Detect repo owner/name from git config + $configFile = $repoPath . '/.git/config'; + $remote = ''; + if (is_file($configFile)) { + $config = file_get_contents($configFile); + if (preg_match('/url\s*=\s*(.+)/', $config, $m)) { + $remote = trim($m[1]); + } + } + + if (empty($remote)) { + $this->log('Cannot determine git remote — skipping wiki check', 'WARNING'); + return 0; + } + + // Parse owner/repo from remote URL + if (preg_match('#[:/]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) { + $owner = $m[1]; + $repo = $m[2]; + } else { + $this->log("Cannot parse owner/repo from remote: {$remote}", 'WARNING'); + return 0; + } + + $this->log("Checking wiki: {$owner}/{$repo}"); + $issues = 0; + + // Check wiki pages via API + $apiUrl = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/wiki/pages"; + $headers = $token ? ["Authorization: token {$token}"] : []; + $pages = $this->apiGet($apiUrl, $headers); + + if ($pages === null) { + $this->log(' No wiki found or API error', 'WARNING'); + $issues++; + if ($this->jsonOutput) { + echo json_encode(['status' => 'no_wiki', 'issues' => $issues]); + } + return 0; + } + + $pageCount = count($pages); + $this->log(" Found {$pageCount} wiki page(s)"); + + // Check Home exists + $hasHome = false; + $pageTitles = []; + foreach ($pages as $page) { + $title = $page['title'] ?? ''; + $pageTitles[] = $title; + if (strtolower($title) === 'home') { + $hasHome = true; + } + } + + if (!$hasHome) { + $this->log(' FAIL: No Home page', 'ERROR'); + $issues++; + } else { + $this->log(' OK: Home page exists'); + } + + // Check Home has MokoStandards link + if ($hasHome) { + $homeUrl = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/wiki/page/Home"; + $home = $this->apiGet($homeUrl, $headers); + if ($home) { + $content = base64_decode($home['content_base64'] ?? ''); + if (stripos($content, 'moko-platform/wiki') !== false || stripos($content, 'MokoStandards') !== false) { + $this->log(' OK: Has MokoStandards reference'); + } else { + $this->log(' WARN: Home page missing MokoStandards reference', 'WARNING'); + $issues++; + } + } + } + + if ($this->jsonOutput) { + echo json_encode([ + 'repo' => "{$owner}/{$repo}", + 'pages' => $pageCount, + 'has_home' => $hasHome, + 'issues' => $issues, + 'page_titles' => $pageTitles, + ], JSON_PRETTY_PRINT); + } + + return $issues > 0 ? 1 : 0; + } + + private function apiGet(string $url, array $headers = []): ?array + { + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => array_merge(['Accept: application/json'], $headers), + 'timeout' => 10, + 'ignore_errors' => true, + ], + 'ssl' => ['verify_peer' => false], + ]); + + $response = @file_get_contents($url, false, $ctx); + if ($response === false) return null; + + $data = json_decode($response, true); + return is_array($data) ? $data : null; + } +} + +(new CheckWikiHealth())->execute();