| #!/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. |
| // |
| // 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, 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); |
| } |
| |
| // Skills whose family is NOT encoded in their name prefix. |
| const OVERRIDES = { |
| // utilities — framework meta-skills |
| "write-skill": "utilities", |
| "list-skills": "utilities", |
| "optimize-skill": "utilities", |
| // mentoring skill that keeps the pr-management- prefix |
| "pr-management-mentor": "mentoring", |
| // repo-health — not name-prefixed |
| "ci-runner-audit": "repo-health", |
| "dependency-audit": "repo-health", |
| "workflow-security-audit": "repo-health", |
| "audit-finding-fix": "repo-health", |
| "license-compliance-audit": "repo-health", |
| "flaky-test-triage": "repo-health", |
| // contributor-growth — not name-prefixed |
| "committer-onboarding": "contributor-growth", |
| "contributor-activity-sweep": "contributor-growth", |
| "contributor-nomination": "contributor-growth", |
| "contributor-to-committer": "contributor-growth", |
| "good-first-issue-author": "contributor-growth", |
| }; |
| |
| // Name-prefix -> family. Checked in order; first match wins. |
| const PREFIXES = [ |
| ["pr-management-", "pr-management"], |
| ["security-", "security"], |
| ["release-", "release-management"], |
| ["mentoring-", "mentoring"], |
| ["pairing-", "pairing"], |
| ["setup-", "setup"], |
| ["setup", "setup"], |
| ["issue-", "issue"], |
| ]; |
| |
| function classify(name) { |
| if (OVERRIDES[name]) return OVERRIDES[name]; |
| for (const [prefix, family] of PREFIXES) { |
| if (name === prefix || name.startsWith(prefix)) return family; |
| } |
| 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 = classify(name); |
| if (!family) { |
| uncategorized.push(name); |
| continue; |
| } |
| counts[family] = (counts[family] || 0) + 1; |
| } |
| |
| if (uncategorized.length) { |
| console.warn( |
| `⚠ ${uncategorized.length} skill(s) matched 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 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)}`); |