Files
playbook/antigravity-awesome-skills/tools/scripts/generate-pages-redirect-bridge.js
T
2026-07-18 00:02:59 +00:00

394 lines
16 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
const DEFAULT_SITEMAP = path.join(REPO_ROOT, 'apps', 'web-app', 'public', 'sitemap.xml');
const DEFAULT_SKILLS_INDEX = path.join(REPO_ROOT, 'skills_index.json');
const GOOGLE_VERIFICATION_FILENAME = 'google5815fd8827d2319c.html';
const DEFAULT_GOOGLE_VERIFICATION = path.join(REPO_ROOT, 'apps', 'web-app', 'public', GOOGLE_VERIFICATION_FILENAME);
const BING_VERIFICATION_TOKEN = 'CAC904EB0D2DD1B22B5F2BC540CAD654';
const DEFAULT_CURRENT_BASE = 'https://sickn33.github.io/agentic-awesome-skills/';
const DEFAULT_LEGACY_BASE = 'https://sickn33.github.io/antigravity-awesome-skills/';
const DEFAULT_EXPECTED_ROUTES = 187;
const SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
function isSafeSegment(value) {
return typeof value === 'string' && value !== '.' && value !== '..' && SAFE_SEGMENT.test(value);
}
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function htmlEscape(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function xmlEscape(value) {
return htmlEscape(value);
}
function xmlUnescape(value) {
const entities = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
};
return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => entities[entity]);
}
function canonicalBase(value, label) {
let url;
try {
url = new URL(value);
} catch (_) {
throw new Error(`${label} must be an absolute URL`);
}
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
throw new Error(`${label} must be a credential-free HTTPS URL without query or fragment`);
}
if (!url.pathname.endsWith('/')) throw new Error(`${label} must end with a slash`);
return url;
}
function parseSitemap(source) {
const rawLocations = [...source.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)].map((match) => xmlUnescape(match[1].trim()));
if (!rawLocations.length) throw new Error('sitemap contains no <loc> URLs');
if (new Set(rawLocations).size !== rawLocations.length) throw new Error('sitemap contains duplicate <loc> URLs');
return rawLocations;
}
function parseSkillIds(source) {
let payload;
try {
payload = JSON.parse(source);
} catch (error) {
throw new Error(`skills index is not valid JSON: ${error.message}`);
}
if (!Array.isArray(payload) || !payload.length) throw new Error('skills index must be a non-empty array');
const ids = payload.map((entry, index) => {
const id = entry && entry.id;
if (!id || !isSafeSegment(id)) {
throw new Error(`skills index entry ${index} has an unsafe or missing id`);
}
return id;
});
if (new Set(ids).size !== ids.length) throw new Error('skills index contains duplicate ids');
return ids;
}
function safeRelativeRoute(currentUrl, currentBase) {
if (currentUrl.protocol !== 'https:' || currentUrl.origin !== currentBase.origin || currentUrl.search || currentUrl.hash) {
throw new Error(`sitemap URL is outside the current HTTPS identity: ${currentUrl.toString()}`);
}
if (!currentUrl.pathname.startsWith(currentBase.pathname) || !currentUrl.pathname.endsWith('/')) {
throw new Error(`sitemap URL is outside the current base path or lacks a trailing slash: ${currentUrl.toString()}`);
}
const relative = currentUrl.pathname.slice(currentBase.pathname.length);
const segments = relative.split('/').filter(Boolean);
for (const segment of segments) {
if (!isSafeSegment(segment)) {
throw new Error(`sitemap URL contains an unsafe path segment: ${currentUrl.toString()}`);
}
}
return segments.join('/');
}
function outputRelativePath(legacyBase, relativeRoute) {
const baseSegments = legacyBase.pathname.split('/').filter(Boolean);
for (const segment of baseSegments) {
if (!isSafeSegment(segment)) {
throw new Error('legacy base contains an unsafe path segment');
}
}
return path.posix.join(...baseSegments, relativeRoute, 'index.html');
}
function redirectHtml(destination, options = {}) {
const escaped = htmlEscape(destination);
const bingVerification = options.bingVerificationToken
? `\n <meta name="msvalidate.01" content="${htmlEscape(options.bingVerificationToken)}">`
: '';
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">${bingVerification}
<meta http-equiv="refresh" content="0; url=${escaped}">
<link rel="canonical" href="${escaped}">
<title>Agentic Awesome Skills has moved</title>
</head>
<body>
<main>
<h1>Agentic Awesome Skills has moved</h1>
<p>Continue to <a href="${escaped}">${escaped}</a>.</p>
</main>
</body>
</html>
`;
}
function legacySitemap(redirects) {
const entries = redirects.map(({ from }) => ` <url><loc>${xmlEscape(from)}</loc></url>`).join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries}
</urlset>
`;
}
function isInside(parent, candidate) {
const relative = path.relative(parent, candidate);
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
}
function physicalCandidate(candidate) {
const suffix = [];
let cursor = path.resolve(candidate);
while (!fs.existsSync(cursor)) {
const parent = path.dirname(cursor);
if (parent === cursor) throw new Error(`cannot resolve an existing ancestor for: ${candidate}`);
suffix.unshift(path.basename(cursor));
cursor = parent;
}
return path.join(fs.realpathSync(cursor), ...suffix);
}
function containsExistingSymlink(parent, candidate) {
const relative = path.relative(parent, candidate);
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) return false;
let cursor = parent;
for (const segment of relative.split(path.sep)) {
cursor = path.join(cursor, segment);
if (!fs.existsSync(cursor)) return false;
if (fs.lstatSync(cursor).isSymbolicLink()) return true;
}
return false;
}
function assertSafeOutput(outputDirectory, repoRoot) {
if (fs.existsSync(outputDirectory)) throw new Error(`output path already exists: ${outputDirectory}`);
const physicalRepoRoot = fs.realpathSync(repoRoot);
const codexDirectory = path.join(repoRoot, '.codex');
const codexIsSymlink = fs.existsSync(codexDirectory) && fs.lstatSync(codexDirectory).isSymbolicLink();
const physicalCodexDirectory = physicalCandidate(codexDirectory);
const physicalOutput = physicalCandidate(outputDirectory);
if (isInside(repoRoot, outputDirectory) && !isInside(codexDirectory, outputDirectory)) {
throw new Error('output inside the repository is allowed only under ignored .codex/');
}
if (isInside(repoRoot, outputDirectory) && containsExistingSymlink(repoRoot, path.dirname(outputDirectory))) {
throw new Error('in-repository output paths may not traverse symlinks');
}
if (isInside(physicalRepoRoot, physicalOutput) && (codexIsSymlink || !isInside(physicalCodexDirectory, physicalOutput))) {
throw new Error('physical output resolves inside the repository but outside ignored .codex/');
}
}
function writeBridge(stagingDirectory, redirects, sitemapRedirects, manifest, legacyBase, googleVerificationSource) {
for (const redirect of redirects) {
const filePath = path.join(stagingDirectory, ...redirect.output_file.split('/'));
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const isLegacyRoot = redirect.from === legacyBase.toString();
fs.writeFileSync(filePath, redirectHtml(redirect.to, {
bingVerificationToken: isLegacyRoot ? BING_VERIFICATION_TOKEN : null,
}), 'utf8');
}
const legacyDirectory = path.join(stagingDirectory, ...legacyBase.pathname.split('/').filter(Boolean));
fs.mkdirSync(legacyDirectory, { recursive: true });
fs.writeFileSync(path.join(legacyDirectory, 'sitemap.xml'), legacySitemap(sitemapRedirects), 'utf8');
fs.writeFileSync(path.join(legacyDirectory, GOOGLE_VERIFICATION_FILENAME), googleVerificationSource);
fs.writeFileSync(path.join(stagingDirectory, 'redirect-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
fs.writeFileSync(path.join(stagingDirectory, '.nojekyll'), '', 'utf8');
}
function generateBridge(options) {
const repoRoot = path.resolve(options.repoRoot || REPO_ROOT);
const sitemapPath = path.resolve(options.sitemapPath || DEFAULT_SITEMAP);
const skillsIndexPath = path.resolve(options.skillsIndexPath || DEFAULT_SKILLS_INDEX);
const googleVerificationPath = path.resolve(options.googleVerificationPath || DEFAULT_GOOGLE_VERIFICATION);
if (!options.outputDirectory) throw new Error('--output is required');
const outputDirectory = path.resolve(options.outputDirectory);
const currentBase = canonicalBase(options.currentBase || DEFAULT_CURRENT_BASE, 'current base');
const legacyBase = canonicalBase(options.legacyBase || DEFAULT_LEGACY_BASE, 'legacy base');
if (currentBase.toString() === legacyBase.toString()) throw new Error('current and legacy bases must be distinct');
const expectedRoutes = Number(options.expectedRoutes ?? DEFAULT_EXPECTED_ROUTES);
if (!Number.isSafeInteger(expectedRoutes) || expectedRoutes <= 0) throw new Error('expected route count must be a positive integer');
const expectedSkills = options.expectedSkills == null ? null : Number(options.expectedSkills);
if (expectedSkills != null && (!Number.isSafeInteger(expectedSkills) || expectedSkills <= 0)) {
throw new Error('expected skill count must be a positive integer');
}
assertSafeOutput(outputDirectory, repoRoot);
const sitemapSource = fs.readFileSync(sitemapPath, 'utf8');
const skillsIndexSource = fs.readFileSync(skillsIndexPath, 'utf8');
const googleVerificationSource = fs.readFileSync(googleVerificationPath);
const expectedGoogleVerification = `google-site-verification: ${GOOGLE_VERIFICATION_FILENAME}`;
if (googleVerificationSource.toString('utf8').trim() !== expectedGoogleVerification) {
throw new Error(`Google verification file must contain exactly: ${expectedGoogleVerification}`);
}
const locations = parseSitemap(sitemapSource);
const skillIds = parseSkillIds(skillsIndexSource);
if (locations.length !== expectedRoutes) {
throw new Error(`sitemap route count ${locations.length} does not match locked expectation ${expectedRoutes}`);
}
if (expectedSkills != null && skillIds.length !== expectedSkills) {
throw new Error(`skills index count ${skillIds.length} does not match locked expectation ${expectedSkills}`);
}
const skillIdSet = new Set(skillIds);
const sitemapRoutes = locations.map((location) => {
const currentUrl = new URL(location);
const relativeRoute = safeRelativeRoute(currentUrl, currentBase);
const segments = relativeRoute.split('/');
if (segments[0] === 'skill' && (segments.length !== 2 || !skillIdSet.has(segments[1]))) {
throw new Error(`sitemap skill route is absent from the current skills index: ${currentUrl.toString()}`);
}
return { currentUrl, relativeRoute };
});
const normalisedSitemapUrls = sitemapRoutes.map(({ currentUrl }) => currentUrl.toString());
if (new Set(normalisedSitemapUrls).size !== normalisedSitemapUrls.length) {
throw new Error('sitemap contains duplicate URLs after normalization');
}
const allRoutes = new Map(sitemapRoutes.map(({ currentUrl, relativeRoute }) => [currentUrl.toString(), { currentUrl, relativeRoute }]));
for (const id of skillIds) {
const currentUrl = new URL(`skill/${id}/`, currentBase);
allRoutes.set(currentUrl.toString(), { currentUrl, relativeRoute: `skill/${id}` });
}
const toRedirect = ({ currentUrl, relativeRoute }) => {
const from = new URL(relativeRoute ? `${relativeRoute}/` : '', legacyBase).toString();
const to = currentUrl.toString();
return {
from,
to,
output_file: outputRelativePath(legacyBase, relativeRoute),
};
};
const redirects = [...allRoutes.values()].map(toRedirect);
const sitemapRedirects = sitemapRoutes.map(toRedirect).sort((left, right) => left.from.localeCompare(right.from));
if (!redirects.some(({ to }) => to === currentBase.toString())) throw new Error('sitemap does not contain the current root route');
if (new Set(redirects.map(({ from }) => from)).size !== redirects.length) throw new Error('legacy mapping is not one-to-one');
if (new Set(redirects.map(({ output_file }) => output_file)).size !== redirects.length) throw new Error('multiple routes map to the same output file');
redirects.sort((left, right) => left.from.localeCompare(right.from));
const legacySitemapPath = path.posix.join(...legacyBase.pathname.split('/').filter(Boolean), 'sitemap.xml');
if (redirects.some(({ output_file }) => output_file.startsWith(`${legacySitemapPath}/`))) {
throw new Error(`generated route collides with reserved legacy sitemap path: ${legacySitemapPath}`);
}
const googleVerificationOutputPath = path.posix.join(
...legacyBase.pathname.split('/').filter(Boolean),
GOOGLE_VERIFICATION_FILENAME,
);
if (redirects.some(({ output_file }) => output_file.startsWith(`${googleVerificationOutputPath}/`))) {
throw new Error(`generated route collides with reserved Google verification path: ${googleVerificationOutputPath}`);
}
const manifest = {
schema_version: 3,
source_repository: 'https://github.com/sickn33/agentic-awesome-skills',
deployment_scope: 'separate GitHub Pages user-site subdirectory',
not_for_current_project_pages: true,
source_sitemap_sha256: sha256(sitemapSource),
source_skills_index_sha256: sha256(skillsIndexSource),
current_base: currentBase.toString(),
legacy_base: legacyBase.toString(),
source_sitemap_route_count: locations.length,
current_skill_route_count: skillIds.length,
route_count: redirects.length,
legacy_sitemap_route_count: sitemapRedirects.length,
legacy_sitemap_policy: 'curated current sitemap routes only',
legacy_sitemap: legacySitemapPath,
webmaster_verification: {
google: {
legacy_file: googleVerificationOutputPath,
sha256: sha256(googleVerificationSource),
},
bing: {
legacy_page: outputRelativePath(legacyBase, ''),
meta_name: 'msvalidate.01',
token: BING_VERIFICATION_TOKEN,
},
},
redirects,
};
fs.mkdirSync(path.dirname(outputDirectory), { recursive: true });
let stagingDirectory = null;
try {
stagingDirectory = fs.mkdtempSync(path.join(path.dirname(outputDirectory), `.${path.basename(outputDirectory)}.${process.pid}.`));
writeBridge(stagingDirectory, redirects, sitemapRedirects, manifest, legacyBase, googleVerificationSource);
fs.renameSync(stagingDirectory, outputDirectory);
} finally {
if (stagingDirectory && fs.existsSync(stagingDirectory)) fs.rmSync(stagingDirectory, { recursive: true, force: true });
}
return manifest;
}
function parseArgs(argv) {
const options = {};
const aliases = {
'--output': 'outputDirectory',
'--sitemap': 'sitemapPath',
'--skills-index': 'skillsIndexPath',
'--google-verification': 'googleVerificationPath',
'--current-base': 'currentBase',
'--legacy-base': 'legacyBase',
'--expected-routes': 'expectedRoutes',
'--expected-skills': 'expectedSkills',
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === '--help' || arg === '-h') {
options.help = true;
continue;
}
const key = aliases[arg];
const value = argv[index + 1];
if (!key || !value || value.startsWith('--')) throw new Error(`unknown option or missing value: ${arg}`);
options[key] = value;
index += 1;
}
return options;
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
process.stdout.write('Usage: node tools/scripts/generate-pages-redirect-bridge.js --output NEW_DIR [--sitemap FILE] [--skills-index FILE] [--google-verification FILE] [--expected-routes N] [--expected-skills N]\n');
return;
}
const manifest = generateBridge(options);
process.stdout.write(`Generated ${manifest.route_count} redirect pages in ${path.resolve(options.outputDirectory)}\n`);
}
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(`redirect bridge generation failed: ${error.message}\n`);
process.exitCode = 1;
}
}
module.exports = {
generateBridge,
htmlEscape,
parseArgs,
parseSkillIds,
parseSitemap,
redirectHtml,
BING_VERIFICATION_TOKEN,
GOOGLE_VERIFICATION_FILENAME,
};