import { readFile, readdir } from "node:fs/promises"; import { dirname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const bundleRoot = resolve(packageRoot, "knowledge"); const RESERVED_NAMES = new Set(["index.md", "log.md"]); async function markdownFiles(directory) { const files = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const path = resolve(directory, entry.name); if (entry.isDirectory()) { files.push(...(await markdownFiles(path))); } else if (entry.isFile() && entry.name.endsWith(".md")) { files.push(path); } } return files; } function parseFrontmatter(markdown) { if (!markdown.startsWith("---\n")) { return null; } const end = markdown.indexOf("\n---\n", 4); if (end === -1) { throw new Error("frontmatter is missing its closing delimiter"); } const values = new Map(); for (const line of markdown.slice(4, end).split("\n")) { const match = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/); if (match) { values.set(match[1], match[2].trim().replace(/^['"]|['"]$/g, "")); } } return values; } function localLinks(markdown) { const links = []; const withoutCode = markdown.replace(/```[\s\S]*?```/g, ""); const pattern = /(? file.endsWith("/index.md")).map(dirname)); for (const directory of [...indexedDirectories].sort()) { const indexPath = resolve(directory, "index.md"); const indexMarkdown = await readFile(indexPath, "utf8"); const targets = await directIndexTargets(directory, indexMarkdown); for (const entry of await readdir(directory, { withFileTypes: true })) { if (entry.isFile() && entry.name.endsWith(".md") && !RESERVED_NAMES.has(entry.name)) { const expected = resolve(directory, entry.name); if (!targets.has(expected)) { errors.push(`${relative(bundleRoot, indexPath)}: missing ${entry.name}`); } } if (entry.isDirectory()) { const childFiles = files.filter((file) => file.startsWith(`${resolve(directory, entry.name)}/`)); if (childFiles.length > 0 && !targets.has(resolve(directory, entry.name))) { errors.push(`${relative(bundleRoot, indexPath)}: missing ${entry.name}/`); } } } } for (const file of files) { const directory = dirname(file); if (!indexedDirectories.has(directory)) { errors.push(`${relative(bundleRoot, file)}: containing directory requires index.md`); } } if (errors.length > 0) { process.stderr.write(`OKF validation failed:\n- ${errors.join("\n- ")}\n`); process.exitCode = 1; } else { const concepts = files.filter((file) => !RESERVED_NAMES.has(file.slice(file.lastIndexOf("/") + 1))).length; process.stdout.write(`OKF v0.2 validation passed (${concepts} concepts, ${indexedDirectories.size} indexes).\n`); }