blob: e7204724629a7a71fff0100fd5c062cbfa17d2c6 [file]
#!/usr/bin/env node
// Generate per-family skill counts from a magpie `skills/` directory.
//
// The website's skill-family counts used to be hand-edited in
// ImmersiveGradientHero.tsx and drifted every time a skill landed upstream.
// This script derives the counts from the actual SKILL.md directories so they
// stay in sync with apache/magpie. It is invoked by scripts/sync-docs.sh after
// the framework repo is checked out.
//
// Family membership is read straight from each skill's `family:` frontmatter
// key — the single source of truth apache/magpie declares (skills/setup/SKILL.md
// Golden rule 8; validated against ALLOWED_FAMILIES in the skill-and-tool-
// validator). It is never inferred from the skill's name prefix: a name-prefix
// heuristic cannot express families that span prefixes (e.g. contributor-growth,
// repo-health) and silently mis-classified skills whenever upstream added one.
//
// Usage: node gen-skill-counts.mjs <skills-dir> <out-json>
//
// Output is deterministic (no timestamps) so the committed JSON only changes
// when the counts actually change.
import {
readdirSync,
statSync,
existsSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
const [, , skillsDir, outPath] = process.argv;
if (!skillsDir || !outPath) {
console.error("usage: gen-skill-counts.mjs <skills-dir> <out-json>");
process.exit(1);
}
// Read the `family:` frontmatter key from a skill's SKILL.md. Returns the
// declared family, or null when the SKILL.md is absent/unreadable or declares
// no family (mirrors read_family() in setup-status/collect_status.py). Family
// is never inferred from the name prefix.
function readFamily(name) {
let text;
try {
text = readFileSync(join(skillsDir, name, "SKILL.md"), "utf8");
} catch {
return null;
}
if (!text.startsWith("---")) return null;
const end = text.indexOf("\n---", 3);
const frontmatter = end !== -1 ? text.slice(3, end) : text;
for (const raw of frontmatter.split("\n")) {
const line = raw.trim();
if (line.startsWith("family:")) {
return line.slice("family:".length).trim() || null;
}
}
return null;
}
const dirs = readdirSync(skillsDir)
.filter((d) => {
try {
return (
statSync(join(skillsDir, d)).isDirectory() &&
existsSync(join(skillsDir, d, "SKILL.md"))
);
} catch {
return false;
}
})
.sort();
const counts = {};
const uncategorized = [];
for (const name of dirs) {
const family = readFamily(name);
if (!family) {
uncategorized.push(name);
continue;
}
counts[family] = (counts[family] || 0) + 1;
}
if (uncategorized.length) {
console.warn(
`⚠ ${uncategorized.length} skill(s) declare no family: ${uncategorized.join(", ")}`,
);
}
// Sort family keys for stable, diff-friendly output.
const sortedCounts = Object.fromEntries(
Object.keys(counts)
.sort()
.map((k) => [k, counts[k]]),
);
const out = {
_comment:
"GENERATED by scripts/gen-skill-counts.mjs — do not edit by hand. Counts derive from the `family:` frontmatter of apache/magpie skills/.",
total: dirs.length,
uncategorized,
counts: sortedCounts,
};
writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
console.log(`✓ skill counts → ${outPath}: ${JSON.stringify(sortedCounts)}`);