#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const template = fs.readFileSync(path.join(repoRoot, 'website', 'template.html'), 'utf8'); const app = fs.readFileSync(path.join(repoRoot, 'website', 'app.js'), 'utf8'); const langInit = fs.readFileSync(path.join(repoRoot, 'website', 'lang-init.js'), 'utf8'); const demoCss = fs.readFileSync(path.join(repoRoot, 'website', 'styles', 'demo.css'), 'utf8'); const landingPrimaryCss = fs.readFileSync(path.join(repoRoot, 'website', 'styles', 'landing-primary.css'), 'utf8'); const legalCss = fs.readFileSync(path.join(repoRoot, 'website', 'styles', 'legal.css'), 'utf8'); const joinPage = fs.readFileSync(path.join(repoRoot, 'website', 'join.html'), 'utf8'); const helpPage = fs.readFileSync(path.join(repoRoot, 'website', 'help.html'), 'utf8'); const siteAccessHelpPage = fs.readFileSync(path.join(repoRoot, 'website', 'site-access-help.html'), 'utf8'); const websiteBuild = fs.readFileSync(path.join(repoRoot, 'website', 'build.cjs'), 'utf8'); const imprintPage = fs.readFileSync(path.join(repoRoot, 'website', 'imprint.html'), 'utf8'); const germanImprintPage = fs.readFileSync(path.join(repoRoot, 'website', 'impressum-de.html'), 'utf8'); const privacyPage = fs.readFileSync(path.join(repoRoot, 'website', 'privacy.html'), 'utf8'); const germanPrivacyPage = fs.readFileSync(path.join(repoRoot, 'website', 'datenschutz-de.html'), 'utf8'); const alternativesIndex = fs.readFileSync(path.join(repoRoot, 'website', 'alternatives', 'index.html'), 'utf8'); const alternativesCss = fs.readFileSync(path.join(repoRoot, 'website', 'styles', 'alternatives.css'), 'utf8'); const llmsText = fs.readFileSync(path.join(repoRoot, 'website', 'llms.txt'), 'utf8'); const websiteVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'website', 'version.json'), 'utf8')).version; const mockupStart = template.indexOf('
'); const mockupEnd = template.indexOf('
]+id="demo-video-select"[^>]+>/], ['sync target', /]+class="mock-input"[^>]+title="Choose sync target"[^>]*>/], ['WebSocket status', /\s*\{\{MOCK_27\}\}/] ]; for (const [label, pattern] of themeSensitiveControls) { const match = mockup.match(pattern); if (!match) throw new Error(`Could not locate ${label} in the extension mockup`); if (!/color:\s*var\(--text\)/.test(match[0])) { throw new Error(`${label} must use var(--text) so it remains readable in both themes`); } } const landingStylesheets = [...template.matchAll(/]*\brel="stylesheet"[^>]*\bhref="\{\{ASSET_PATH\}\}landing[^">]*"[^>]*>/g)]; if (landingStylesheets.length !== 1) { throw new Error(`Landing must load exactly one render-blocking stylesheet; found ${landingStylesheets.length}`); } const llmsLinks = [...template.matchAll(/]*\brel="alternate"[^>]*\btype="text\/markdown"[^>]*\bhref="https:\/\/sync\.koalastuff\.net\/llms\.txt"[^>]*>/g)]; if (llmsLinks.length !== 1) { throw new Error(`Landing head must link exactly one canonical llms.txt document; found ${llmsLinks.length}`); } const requiredLlmsSections = [ '## Quick fit assessment', '## How it works', '## Privacy and security', '## Supported environments', '## Install KoalaSync', '## Technical information' ]; for (const section of requiredLlmsSections) { if (!llmsText.includes(section)) throw new Error(`llms.txt is missing ${section}`); } if (/\bWebRTC\b|\bport 54000\b|peer-to-peer by design/i.test(llmsText)) { throw new Error('llms.txt must describe the current WebSocket relay architecture without obsolete WebRTC or port claims'); } if (!llmsText.includes('- built-in voice, video or webcam chat') || llmsText.includes('- built-in text, voice or webcam chat')) { throw new Error('llms.txt must distinguish built-in encrypted text chat from unsupported voice, video or webcam chat'); } if (!llmsText.includes(`Current website release: ${websiteVersion}`)) { throw new Error(`llms.txt release must match website/version.json (${websiteVersion})`); } const requiredSupportSocialMetadata = [ '', 'name="twitter:card" content="summary_large_image"', 'name="twitter:title"', 'name="twitter:description"', 'name="twitter:image"', '"datePublished"', '"dateModified"', '"mainEntityOfPage"' ]; for (const metadata of requiredSupportSocialMetadata) { if (!siteAccessHelpPage.includes(metadata)) { throw new Error(`site-access-help.html is missing SEO metadata: ${metadata}`); } } const requiredHelpMetadata = [ '', 'type="text/markdown" href="https://sync.koalastuff.net/llms.txt"', 'name="twitter:card" content="summary_large_image"', 'property="og:image:alt"', 'name="twitter:image:alt"', '"@type": ["CollectionPage", "FAQPage"]', '"@type": "BreadcrumbList"', '"datePublished"', '"dateModified"' ]; for (const metadata of requiredHelpMetadata) { if (!helpPage.includes(metadata)) { throw new Error(`help.html is missing SEO/AEO metadata: ${metadata}`); } } const helpStructuredDataMatch = helpPage.match(/