Consolidate the repository into clearer apps, tools, and layered docs areas so contributors can navigate and maintain it more reliably. Align validation, metadata sync, and CI around the same canonical workflow to reduce drift across local checks and GitHub Actions.
681 lines
13 KiB
JavaScript
681 lines
13 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const {
|
|
listSkillIdsRecursive,
|
|
readSkill,
|
|
tokenize,
|
|
unique,
|
|
} = require("../lib/skill-utils");
|
|
const { findProjectRoot } = require("../lib/project-root");
|
|
|
|
const ROOT = findProjectRoot(__dirname);
|
|
const SKILLS_DIR = path.join(ROOT, "skills");
|
|
|
|
const STOPWORDS = new Set([
|
|
"a",
|
|
"an",
|
|
"and",
|
|
"are",
|
|
"as",
|
|
"at",
|
|
"be",
|
|
"but",
|
|
"by",
|
|
"for",
|
|
"from",
|
|
"has",
|
|
"have",
|
|
"in",
|
|
"into",
|
|
"is",
|
|
"it",
|
|
"its",
|
|
"of",
|
|
"on",
|
|
"or",
|
|
"our",
|
|
"out",
|
|
"over",
|
|
"that",
|
|
"the",
|
|
"their",
|
|
"they",
|
|
"this",
|
|
"to",
|
|
"use",
|
|
"when",
|
|
"with",
|
|
"you",
|
|
"your",
|
|
"will",
|
|
"can",
|
|
"if",
|
|
"not",
|
|
"only",
|
|
"also",
|
|
"more",
|
|
"best",
|
|
"practice",
|
|
"practices",
|
|
"expert",
|
|
"specialist",
|
|
"focused",
|
|
"focus",
|
|
"master",
|
|
"modern",
|
|
"advanced",
|
|
"comprehensive",
|
|
"production",
|
|
"production-ready",
|
|
"ready",
|
|
"build",
|
|
"create",
|
|
"deliver",
|
|
"design",
|
|
"implement",
|
|
"implementation",
|
|
"strategy",
|
|
"strategies",
|
|
"patterns",
|
|
"pattern",
|
|
"workflow",
|
|
"workflows",
|
|
"guide",
|
|
"template",
|
|
"templates",
|
|
"tool",
|
|
"tools",
|
|
"project",
|
|
"projects",
|
|
"support",
|
|
"manage",
|
|
"management",
|
|
"system",
|
|
"systems",
|
|
"services",
|
|
"service",
|
|
"across",
|
|
"end",
|
|
"end-to-end",
|
|
"using",
|
|
"based",
|
|
"ensure",
|
|
"ensure",
|
|
"help",
|
|
"needs",
|
|
"need",
|
|
"focuses",
|
|
"handles",
|
|
"builds",
|
|
"make",
|
|
]);
|
|
|
|
const TAG_STOPWORDS = new Set([
|
|
"pro",
|
|
"expert",
|
|
"patterns",
|
|
"pattern",
|
|
"workflow",
|
|
"workflows",
|
|
"templates",
|
|
"template",
|
|
"toolkit",
|
|
"tools",
|
|
"tool",
|
|
"project",
|
|
"projects",
|
|
"guide",
|
|
"management",
|
|
"engineer",
|
|
"architect",
|
|
"developer",
|
|
"specialist",
|
|
"assistant",
|
|
"analysis",
|
|
"review",
|
|
"reviewer",
|
|
"automation",
|
|
"orchestration",
|
|
"scaffold",
|
|
"scaffolding",
|
|
"implementation",
|
|
"strategy",
|
|
"context",
|
|
"management",
|
|
"feature",
|
|
"features",
|
|
"smart",
|
|
"system",
|
|
"systems",
|
|
"design",
|
|
"development",
|
|
"development",
|
|
"test",
|
|
"testing",
|
|
"workflow",
|
|
]);
|
|
|
|
const CATEGORY_RULES = [
|
|
{
|
|
name: "security",
|
|
keywords: [
|
|
"security",
|
|
"sast",
|
|
"compliance",
|
|
"privacy",
|
|
"threat",
|
|
"vulnerability",
|
|
"owasp",
|
|
"pci",
|
|
"gdpr",
|
|
"secrets",
|
|
"risk",
|
|
"malware",
|
|
"forensics",
|
|
"attack",
|
|
"incident",
|
|
"auth",
|
|
"mtls",
|
|
"zero",
|
|
"trust",
|
|
],
|
|
},
|
|
{
|
|
name: "infrastructure",
|
|
keywords: [
|
|
"kubernetes",
|
|
"k8s",
|
|
"helm",
|
|
"terraform",
|
|
"cloud",
|
|
"network",
|
|
"devops",
|
|
"gitops",
|
|
"prometheus",
|
|
"grafana",
|
|
"observability",
|
|
"monitoring",
|
|
"logging",
|
|
"tracing",
|
|
"deployment",
|
|
"istio",
|
|
"linkerd",
|
|
"service",
|
|
"mesh",
|
|
"slo",
|
|
"sre",
|
|
"oncall",
|
|
"incident",
|
|
"pipeline",
|
|
"cicd",
|
|
"ci",
|
|
"cd",
|
|
"kafka",
|
|
],
|
|
},
|
|
{
|
|
name: "data-ai",
|
|
keywords: [
|
|
"data",
|
|
"database",
|
|
"db",
|
|
"sql",
|
|
"postgres",
|
|
"mysql",
|
|
"analytics",
|
|
"etl",
|
|
"warehouse",
|
|
"dbt",
|
|
"ml",
|
|
"ai",
|
|
"llm",
|
|
"rag",
|
|
"vector",
|
|
"embedding",
|
|
"spark",
|
|
"airflow",
|
|
"cdc",
|
|
"pipeline",
|
|
],
|
|
},
|
|
{
|
|
name: "development",
|
|
keywords: [
|
|
"python",
|
|
"javascript",
|
|
"typescript",
|
|
"java",
|
|
"golang",
|
|
"go",
|
|
"rust",
|
|
"csharp",
|
|
"dotnet",
|
|
"php",
|
|
"ruby",
|
|
"node",
|
|
"react",
|
|
"frontend",
|
|
"backend",
|
|
"mobile",
|
|
"ios",
|
|
"android",
|
|
"flutter",
|
|
"fastapi",
|
|
"django",
|
|
"nextjs",
|
|
"vue",
|
|
"api",
|
|
],
|
|
},
|
|
{
|
|
name: "architecture",
|
|
keywords: [
|
|
"architecture",
|
|
"c4",
|
|
"microservices",
|
|
"event",
|
|
"cqrs",
|
|
"saga",
|
|
"domain",
|
|
"ddd",
|
|
"patterns",
|
|
"decision",
|
|
"adr",
|
|
],
|
|
},
|
|
{
|
|
name: "testing",
|
|
keywords: ["testing", "tdd", "unit", "e2e", "qa", "test"],
|
|
},
|
|
{
|
|
name: "business",
|
|
keywords: [
|
|
"business",
|
|
"market",
|
|
"sales",
|
|
"finance",
|
|
"startup",
|
|
"legal",
|
|
"hr",
|
|
"product",
|
|
"customer",
|
|
"seo",
|
|
"marketing",
|
|
"kpi",
|
|
"contract",
|
|
"employment",
|
|
],
|
|
},
|
|
{
|
|
name: "workflow",
|
|
keywords: [
|
|
"workflow",
|
|
"orchestration",
|
|
"conductor",
|
|
"automation",
|
|
"process",
|
|
"collaboration",
|
|
],
|
|
},
|
|
];
|
|
|
|
const BUNDLE_RULES = {
|
|
"core-dev": {
|
|
description:
|
|
"Core development skills across languages, frameworks, and backend/frontend fundamentals.",
|
|
keywords: [
|
|
"python",
|
|
"javascript",
|
|
"typescript",
|
|
"go",
|
|
"golang",
|
|
"rust",
|
|
"java",
|
|
"node",
|
|
"frontend",
|
|
"backend",
|
|
"react",
|
|
"fastapi",
|
|
"django",
|
|
"nextjs",
|
|
"api",
|
|
"mobile",
|
|
"ios",
|
|
"android",
|
|
"flutter",
|
|
"php",
|
|
"ruby",
|
|
],
|
|
},
|
|
"security-core": {
|
|
description: "Security, privacy, and compliance essentials.",
|
|
keywords: [
|
|
"security",
|
|
"sast",
|
|
"compliance",
|
|
"threat",
|
|
"risk",
|
|
"privacy",
|
|
"secrets",
|
|
"owasp",
|
|
"gdpr",
|
|
"pci",
|
|
"vulnerability",
|
|
"auth",
|
|
],
|
|
},
|
|
"k8s-core": {
|
|
description: "Kubernetes and service mesh essentials.",
|
|
keywords: [
|
|
"kubernetes",
|
|
"k8s",
|
|
"helm",
|
|
"istio",
|
|
"linkerd",
|
|
"service",
|
|
"mesh",
|
|
],
|
|
},
|
|
"data-core": {
|
|
description: "Data engineering and analytics foundations.",
|
|
keywords: [
|
|
"data",
|
|
"database",
|
|
"sql",
|
|
"dbt",
|
|
"airflow",
|
|
"spark",
|
|
"analytics",
|
|
"etl",
|
|
"warehouse",
|
|
"postgres",
|
|
"mysql",
|
|
"kafka",
|
|
],
|
|
},
|
|
"ops-core": {
|
|
description: "Operations, observability, and delivery pipelines.",
|
|
keywords: [
|
|
"observability",
|
|
"monitoring",
|
|
"logging",
|
|
"tracing",
|
|
"prometheus",
|
|
"grafana",
|
|
"devops",
|
|
"gitops",
|
|
"deployment",
|
|
"cicd",
|
|
"pipeline",
|
|
"slo",
|
|
"sre",
|
|
"incident",
|
|
],
|
|
},
|
|
};
|
|
|
|
const CURATED_COMMON = [
|
|
"bash-pro",
|
|
"python-pro",
|
|
"javascript-pro",
|
|
"typescript-pro",
|
|
"golang-pro",
|
|
"rust-pro",
|
|
"java-pro",
|
|
"frontend-developer",
|
|
"backend-architect",
|
|
"nodejs-backend-patterns",
|
|
"fastapi-pro",
|
|
"api-design-principles",
|
|
"sql-pro",
|
|
"database-architect",
|
|
"kubernetes-architect",
|
|
"terraform-specialist",
|
|
"observability-engineer",
|
|
"security-auditor",
|
|
"sast-configuration",
|
|
"gitops-workflow",
|
|
];
|
|
|
|
function normalizeTokens(tokens) {
|
|
return unique(tokens.map((token) => token.toLowerCase())).filter(Boolean);
|
|
}
|
|
|
|
function deriveTags(skill) {
|
|
let tags = Array.isArray(skill.tags) ? skill.tags : [];
|
|
tags = tags.map((tag) => tag.toLowerCase()).filter(Boolean);
|
|
|
|
if (!tags.length) {
|
|
tags = skill.id
|
|
.split("-")
|
|
.map((tag) => tag.toLowerCase())
|
|
.filter((tag) => tag && !TAG_STOPWORDS.has(tag));
|
|
}
|
|
|
|
return normalizeTokens(tags);
|
|
}
|
|
|
|
function detectCategory(skill, tags) {
|
|
const haystack = normalizeTokens([
|
|
...tags,
|
|
...tokenize(skill.name),
|
|
...tokenize(skill.description),
|
|
]);
|
|
const haystackSet = new Set(haystack);
|
|
|
|
for (const rule of CATEGORY_RULES) {
|
|
for (const keyword of rule.keywords) {
|
|
if (haystackSet.has(keyword)) {
|
|
return rule.name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return "general";
|
|
}
|
|
|
|
function buildTriggers(skill, tags) {
|
|
const tokens = tokenize(`${skill.name} ${skill.description}`).filter(
|
|
(token) => token.length >= 2 && !STOPWORDS.has(token),
|
|
);
|
|
return unique([...tags, ...tokens]).slice(0, 12);
|
|
}
|
|
|
|
/** Common typo aliases (e.g. em dash — instead of hyphen -) for skill lookup. */
|
|
const TYPO_ALIASES = {
|
|
"shopify—development": "shopify-development",
|
|
};
|
|
|
|
function buildAliases(skills) {
|
|
const existingIds = new Set(skills.map((skill) => skill.id));
|
|
const aliases = {};
|
|
const used = new Set();
|
|
|
|
for (const skill of skills) {
|
|
if (skill.name && skill.name !== skill.id) {
|
|
const alias = skill.name.toLowerCase();
|
|
if (!existingIds.has(alias) && !used.has(alias)) {
|
|
aliases[alias] = skill.id;
|
|
used.add(alias);
|
|
}
|
|
}
|
|
|
|
const tokens = skill.id.split("-").filter(Boolean);
|
|
if (skill.id.length < 28 || tokens.length < 4) continue;
|
|
|
|
const deduped = [];
|
|
const tokenSeen = new Set();
|
|
for (const token of tokens) {
|
|
if (tokenSeen.has(token)) continue;
|
|
tokenSeen.add(token);
|
|
deduped.push(token);
|
|
}
|
|
|
|
const aliasTokens =
|
|
deduped.length > 3
|
|
? [deduped[0], deduped[1], deduped[deduped.length - 1]]
|
|
: deduped;
|
|
const alias = unique(aliasTokens).join("-");
|
|
|
|
if (!alias || alias === skill.id) continue;
|
|
if (existingIds.has(alias) || used.has(alias)) continue;
|
|
|
|
aliases[alias] = skill.id;
|
|
used.add(alias);
|
|
}
|
|
|
|
for (const [typo, canonicalId] of Object.entries(TYPO_ALIASES)) {
|
|
if (existingIds.has(canonicalId) && !aliases[typo]) {
|
|
aliases[typo] = canonicalId;
|
|
}
|
|
}
|
|
|
|
return aliases;
|
|
}
|
|
|
|
function buildBundles(skills) {
|
|
const bundles = {};
|
|
const skillTokens = new Map();
|
|
|
|
for (const skill of skills) {
|
|
const tokens = normalizeTokens([
|
|
...skill.tags,
|
|
...tokenize(skill.name),
|
|
...tokenize(skill.description),
|
|
]);
|
|
skillTokens.set(skill.id, new Set(tokens));
|
|
}
|
|
|
|
for (const [bundleName, rule] of Object.entries(BUNDLE_RULES)) {
|
|
const bundleSkills = [];
|
|
const keywords = rule.keywords.map((keyword) => keyword.toLowerCase());
|
|
|
|
for (const skill of skills) {
|
|
const tokenSet = skillTokens.get(skill.id) || new Set();
|
|
if (keywords.some((keyword) => tokenSet.has(keyword))) {
|
|
bundleSkills.push(skill.id);
|
|
}
|
|
}
|
|
|
|
bundles[bundleName] = {
|
|
description: rule.description,
|
|
skills: bundleSkills.sort(),
|
|
};
|
|
}
|
|
|
|
const common = CURATED_COMMON.filter((skillId) => skillTokens.has(skillId));
|
|
|
|
return { bundles, common };
|
|
}
|
|
|
|
function truncate(value, limit) {
|
|
if (!value || value.length <= limit) return value || "";
|
|
return `${value.slice(0, limit - 3)}...`;
|
|
}
|
|
|
|
function renderCatalogMarkdown(catalog) {
|
|
const lines = [];
|
|
lines.push("# Skill Catalog");
|
|
lines.push("");
|
|
lines.push(`Generated at: ${catalog.generatedAt}`);
|
|
lines.push("");
|
|
lines.push(`Total skills: ${catalog.total}`);
|
|
lines.push("");
|
|
|
|
const categories = Array.from(
|
|
new Set(catalog.skills.map((skill) => skill.category)),
|
|
).sort();
|
|
for (const category of categories) {
|
|
const grouped = catalog.skills.filter(
|
|
(skill) => skill.category === category,
|
|
);
|
|
lines.push(`## ${category} (${grouped.length})`);
|
|
lines.push("");
|
|
lines.push("| Skill | Description | Tags | Triggers |");
|
|
lines.push("| --- | --- | --- | --- |");
|
|
|
|
for (const skill of grouped) {
|
|
const description = truncate(skill.description, 160).replace(
|
|
/\|/g,
|
|
"\\|",
|
|
);
|
|
const tags = skill.tags.join(", ");
|
|
const triggers = skill.triggers.join(", ");
|
|
lines.push(
|
|
`| \`${skill.id}\` | ${description} | ${tags} | ${triggers} |`,
|
|
);
|
|
}
|
|
|
|
lines.push("");
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function buildCatalog() {
|
|
const skillRelPaths = listSkillIdsRecursive(SKILLS_DIR);
|
|
const skills = skillRelPaths.map((relPath) => readSkill(SKILLS_DIR, relPath));
|
|
const catalogSkills = [];
|
|
|
|
for (const skill of skills) {
|
|
const tags = deriveTags(skill);
|
|
const category = detectCategory(skill, tags);
|
|
const triggers = buildTriggers(skill, tags);
|
|
|
|
catalogSkills.push({
|
|
id: skill.id,
|
|
name: skill.name,
|
|
description: skill.description,
|
|
category,
|
|
tags,
|
|
triggers,
|
|
// Normalize separators for deterministic cross-platform output.
|
|
path: path.relative(ROOT, skill.path).split(path.sep).join("/"),
|
|
});
|
|
}
|
|
|
|
const catalog = {
|
|
generatedAt: process.env.SOURCE_DATE_EPOCH
|
|
? new Date(process.env.SOURCE_DATE_EPOCH * 1000).toISOString()
|
|
: "2026-02-08T00:00:00.000Z",
|
|
total: catalogSkills.length,
|
|
skills: catalogSkills.sort((a, b) =>
|
|
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
),
|
|
};
|
|
|
|
const aliases = buildAliases(catalog.skills);
|
|
const bundleData = buildBundles(catalog.skills);
|
|
|
|
const catalogPath = path.join(ROOT, "data", "catalog.json");
|
|
const catalogMarkdownPath = path.join(ROOT, "CATALOG.md");
|
|
const bundlesPath = path.join(ROOT, "data", "bundles.json");
|
|
const aliasesPath = path.join(ROOT, "data", "aliases.json");
|
|
|
|
fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
|
|
fs.writeFileSync(catalogMarkdownPath, renderCatalogMarkdown(catalog));
|
|
fs.writeFileSync(
|
|
bundlesPath,
|
|
JSON.stringify(
|
|
{ generatedAt: catalog.generatedAt, ...bundleData },
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
fs.writeFileSync(
|
|
aliasesPath,
|
|
JSON.stringify({ generatedAt: catalog.generatedAt, aliases }, null, 2),
|
|
);
|
|
|
|
return catalog;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const catalog = buildCatalog();
|
|
console.log(`Generated catalog for ${catalog.total} skills.`);
|
|
}
|
|
|
|
module.exports = {
|
|
buildCatalog,
|
|
};
|