aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
96 lines (85 loc) • 2.94 kB
JavaScript
// Validates SKILL.md frontmatter across the repo. Closes #1014 — the
// existing MetadataValidator's recursive walker only picks up manifest.md
// and BEHAVIOR.md, so SKILL.md frontmatter bugs (invalid YAML, missing
// required fields) reach main without CI catching them.
//
// Checks per file:
// 1. Frontmatter delimiters present
// 2. YAML parses in strict mode
// 3. Required fields present: name, namespace, description, platforms
//
// Usage:
// node tools/linters/skill-frontmatter-linter.mjs [path...]
// (default paths: agentic/code, .claude)
import { readdir, readFile, stat } from 'node:fs/promises';
import { join, relative } from 'node:path';
import yaml from 'js-yaml';
const REQUIRED = ['name', 'namespace', 'description', 'platforms'];
const DEFAULT_ROOTS = ['agentic/code', '.claude'];
async function* walk(root) {
let entries;
try {
entries = await readdir(root, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
if (e.name === 'node_modules' || e.name.startsWith('.git')) continue;
const p = join(root, e.name);
if (e.isDirectory()) yield* walk(p);
else if (e.isFile() && e.name === 'SKILL.md') yield p;
}
}
function extractFrontmatter(text) {
const m = text.match(/^---\n([\s\S]*?)\n---/);
return m ? m[1] : null;
}
async function check(file) {
const text = await readFile(file, 'utf8');
const fmText = extractFrontmatter(text);
if (!fmText) return [{ field: '<frontmatter>', msg: 'missing or malformed --- delimiters' }];
let fm;
try {
fm = yaml.load(fmText, { schema: yaml.DEFAULT_SCHEMA });
} catch (e) {
return [{ field: '<yaml>', msg: `parse error: ${e.message.split('\n')[0]}` }];
}
if (!fm || typeof fm !== 'object') return [{ field: '<yaml>', msg: 'frontmatter is not an object' }];
const errs = [];
for (const k of REQUIRED) {
if (fm[k] === undefined || fm[k] === null || fm[k] === '') {
errs.push({ field: k, msg: 'missing required field' });
}
}
return errs;
}
async function main() {
const roots = process.argv.slice(2).length ? process.argv.slice(2) : DEFAULT_ROOTS;
const cwd = process.cwd();
const files = [];
for (const r of roots) {
try {
const s = await stat(r);
if (s.isFile() && r.endsWith('SKILL.md')) files.push(r);
else if (s.isDirectory()) for await (const f of walk(r)) files.push(f);
} catch {
console.error(`warn: cannot access ${r}`);
}
}
let failed = 0;
for (const f of files) {
const errs = await check(f);
if (errs.length) {
failed++;
console.error(`✗ ${relative(cwd, f)}`);
for (const e of errs) console.error(` [${e.field}] ${e.msg}`);
}
}
console.log(`\nSKILL.md frontmatter check: ${files.length} file(s), ${failed} failed.`);
if (failed > 0) process.exit(1);
}
main().catch((e) => {
console.error('linter crashed:', e);
process.exit(2);
});