#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import process from "node:process"; const required = [ "README.md", "SECURITY.md", "CONTRIBUTING.md", "LICENSE", "PUBLIC_SOURCE_EXPORT.md", "PUBLIC_SOURCE_MANIFEST.json", "MANIFEST.json", "ItWorx.KnowledgeHub.slnx", "docker-compose.yml", "deploy/docker/Dockerfile", "docs/api/openapi.yaml", "src/frontend/package.json" ]; const missing = required.filter((entry) => !fs.existsSync(path.join(process.cwd(), entry))); if (missing.length > 0) { console.error(`Missing public source files: ${missing.join(", ")}`); process.exit(1); } for (const privatePath of [".claude", ".agents", ".codex", "artifacts", "docs/deployments", "docs/v2", "n8n"]) { if (fs.existsSync(path.join(process.cwd(), privatePath))) { console.error(`Private-only path present in public export: ${privatePath}`); process.exit(1); } } const manifest = JSON.parse(fs.readFileSync("PUBLIC_SOURCE_MANIFEST.json", "utf8")); if (!Array.isArray(manifest.files) || manifest.files.length === 0) { console.error("PUBLIC_SOURCE_MANIFEST.json contains no files."); process.exit(1); } const markdownFiles = []; function walkMarkdown(directory) { for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { if ([".git", "node_modules", "__pycache__"].includes(entry.name)) continue; const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) walkMarkdown(fullPath); else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) markdownFiles.push(fullPath); } } function stripCode(markdown) { const withoutFences = markdown.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, (block) => block.replace(/[^\n]/g, " ")); return withoutFences.replace(/`[^`\n]*`/g, (span) => " ".repeat(span.length)); } walkMarkdown(process.cwd()); const errors = []; const linkPattern = /\[[^\]]+\]\(([^)]+)\)/g; for (const markdownFile of markdownFiles) { const content = stripCode(fs.readFileSync(markdownFile, "utf8")); for (const match of content.matchAll(linkPattern)) { let target = match[1].trim(); if (target.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue; target = target.split("#", 1)[0].split("?", 1)[0]; if (!target) continue; try { target = decodeURIComponent(target); } catch { errors.push(`Could not URL-decode link in ${path.relative(process.cwd(), markdownFile)}: ${target}`); continue; } const resolved = path.resolve(path.dirname(markdownFile), target); if (!fs.existsSync(resolved)) { errors.push(`Broken local Markdown link in ${path.relative(process.cwd(), markdownFile)}: ${match[1]}`); } } } if (errors.length > 0) { console.error("Public source validation failed:"); for (const error of errors) console.error(`- ${error}`); process.exit(1); } console.log(`Public source structure passed (${manifest.files.length} allowlisted files, ${markdownFiles.length} Markdown files, local links resolved).`);