#!/usr/bin/env node import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; const root = path.resolve(new URL("..", import.meta.url).pathname); const skillsDir = path.join(root, "skills"); const apisDir = path.join(root, "apis"); const errors = []; async function exists(file) { try { await stat(file); return true; } catch { return false; } } function check(condition, message) { if (!condition) errors.push(message); } function parseSkillFrontmatter(text, file) { const match = text.match(/^---\n([\s\S]*?)\n---/); check(Boolean(match), `${file}: missing YAML frontmatter`); if (!match) return {}; const data = {}; for (const line of match[1].split("\n")) { const parts = line.split(":"); if (parts.length < 2) continue; const key = parts.shift().trim(); const value = parts.join(":").trim().replace(/^"|"$/g, ""); data[key] = value; } return data; } async function validateSkills() { if (!(await exists(skillsDir))) return; const entries = await readdir(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const name = entry.name; const dir = path.join(skillsDir, name); const skillFile = path.join(dir, "SKILL.md"); const registryFile = path.join(dir, "skill.registry.json"); const agentFile = path.join(dir, "agents", "openai.yaml"); check(await exists(skillFile), `${name}: missing SKILL.md`); check(await exists(registryFile), `${name}: missing skill.registry.json`); check(await exists(agentFile), `${name}: missing agents/openai.yaml`); if (!(await exists(skillFile)) || !(await exists(registryFile))) continue; const skillText = await readFile(skillFile, "utf8"); const frontmatter = parseSkillFrontmatter(skillText, `${name}/SKILL.md`); check(frontmatter.name === name, `${name}: frontmatter name must equal folder name`); check(Boolean(frontmatter.description), `${name}: missing frontmatter description`); let metadata; try { metadata = JSON.parse(await readFile(registryFile, "utf8")); } catch (error) { errors.push(`${name}: invalid skill.registry.json: ${error.message}`); continue; } check(metadata.name === name, `${name}: metadata name must equal folder name`); check(Boolean(metadata.version), `${name}: missing version`); check(Boolean(metadata.title), `${name}: missing title`); check(Boolean(metadata.description), `${name}: missing description`); } } async function validateApiDocs() { if (!(await exists(apisDir))) return; const products = await readdir(apisDir, { withFileTypes: true }); for (const productEntry of products) { if (!productEntry.isDirectory()) continue; const productDir = path.join(apisDir, productEntry.name); const files = (await readdir(productDir)).filter((name) => name.endsWith(".md")); const stems = new Map(); for (const file of files) { const stem = file.replace(/\.(zh|en)\.md$/, ""); if (!stems.has(stem)) stems.set(stem, new Set()); if (file.endsWith(".zh.md")) stems.get(stem).add("zh"); if (file.endsWith(".en.md")) stems.get(stem).add("en"); const text = await readFile(path.join(productDir, file), "utf8"); check(/^#\s+.+/m.test(text), `${productEntry.name}/${file}: missing H1 title`); } for (const [stem, languages] of stems) { check(languages.has("zh"), `${productEntry.name}/${stem}: missing zh doc`); check(languages.has("en"), `${productEntry.name}/${stem}: missing en doc`); } } } await validateSkills(); await validateApiDocs(); if (errors.length > 0) { console.error(errors.map((item) => `- ${item}`).join("\n")); process.exit(1); } console.log("Validation passed.");