#!/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_CURRENT_BASE = 'https://sickn33.github.io/agentic-awesome-skills/'; const DEFAULT_LEGACY_BASE = 'https://sickn33.github.io/antigravity-awesome-skills/'; const DEFAULT_EXPECTED_ROUTES = 46; const SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/; function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); } function htmlEscape(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } 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(/\s*([^<]+?)\s*<\/loc>/gi)].map((match) => xmlUnescape(match[1].trim())); if (!rawLocations.length) throw new Error('sitemap contains no URLs'); if (new Set(rawLocations).size !== rawLocations.length) throw new Error('sitemap contains duplicate URLs'); return rawLocations; } 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 (segment === '.' || segment === '..' || !SAFE_SEGMENT.test(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 (segment === '.' || segment === '..' || !SAFE_SEGMENT.test(segment)) { throw new Error('legacy base contains an unsafe path segment'); } } return path.posix.join(...baseSegments, relativeRoute, 'index.html'); } function redirectHtml(destination) { const escaped = htmlEscape(destination); return ` Agentic Awesome Skills has moved

Agentic Awesome Skills has moved

Continue to ${escaped}.

`; } function legacySitemap(redirects) { const entries = redirects.map(({ from }) => ` ${xmlEscape(from)}`).join('\n'); return ` ${entries} `; } 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, manifest, legacyBase) { for (const redirect of redirects) { const filePath = path.join(stagingDirectory, ...redirect.output_file.split('/')); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, redirectHtml(redirect.to), 'utf8'); } const legacyDirectory = path.join(stagingDirectory, ...legacyBase.pathname.split('/').filter(Boolean)); fs.mkdirSync(legacyDirectory, { recursive: true }); fs.writeFileSync(path.join(legacyDirectory, 'sitemap.xml'), legacySitemap(redirects), 'utf8'); 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); 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'); assertSafeOutput(outputDirectory, repoRoot); const sitemapSource = fs.readFileSync(sitemapPath, 'utf8'); const locations = parseSitemap(sitemapSource); if (locations.length !== expectedRoutes) { throw new Error(`sitemap route count ${locations.length} does not match locked expectation ${expectedRoutes}`); } const redirects = locations.map((location) => { const currentUrl = new URL(location); const relativeRoute = safeRelativeRoute(currentUrl, currentBase); const from = new URL(relativeRoute ? `${relativeRoute}/` : '', legacyBase).toString(); const to = currentUrl.toString(); return { from, to, output_file: outputRelativePath(legacyBase, relativeRoute), }; }); 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 manifest = { schema_version: 1, deployment_scope: 'separate GitHub Pages user-site subdirectory', not_for_current_project_pages: true, source_sitemap_sha256: sha256(sitemapSource), current_base: currentBase.toString(), legacy_base: legacyBase.toString(), route_count: redirects.length, legacy_sitemap: legacySitemapPath, 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, manifest, legacyBase); 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', '--current-base': 'currentBase', '--legacy-base': 'legacyBase', '--expected-routes': 'expectedRoutes', }; 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] [--expected-routes 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, parseSitemap, redirectHtml, };