feat: initialize OmniClaw skills registry
This commit is contained in:
105
tools/validate.mjs
Executable file
105
tools/validate.mjs
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/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.");
|
||||
|
||||
Reference in New Issue
Block a user