Files
vercel__vercel-plugin/scripts/generate-catalog.ts
John Lindquist cd5f369dcb Add new skills, typecheck gate, remove arg/ms/serve skills, bump to 0.4.2
- Add 8 new skills: edge-runtime, geist, micro, ncc, satori, streamdown, styled-jsx, swr
- Remove arg, ms, serve skills (standalone utilities, not relevant to shipping apps on Vercel)
- Add typecheck script and wire into `bun run test` and pre-commit hook
- Fix StdioOptions type error in session-start-profiler
- Add hook-env module and posttooluse-shadcn-font-fix hook
- Refactor skill-map-frontmatter, patterns, pretooluse-skill-inject
- Update generated catalog and manifest (42 skills)
- Add .gitignore with .DS_Store
2026-03-06 19:04:25 -07:00

297 lines
8.5 KiB
TypeScript

#!/usr/bin/env bun
/**
* Generate a skill catalog with overlap matrix from the skills/ directory.
*
* Usage: bun run scripts/generate-catalog.ts
*
* Outputs: generated/skill-catalog.md
*/
import { stat, writeFile, mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { globToRegex } from "../hooks/patterns.mjs";
import type { SkillEntry } from "../hooks/patterns.mjs";
import { loadValidatedSkillMap } from "../src/shared/skill-map-loader.ts";
const ROOT = resolve(import.meta.dirname, "..");
const SKILLS_DIR = join(ROOT, "skills");
const OUTPUT_PATH = join(ROOT, "generated", "skill-catalog.md");
// Well-known files that many skills compete over
const OVERLAP_TARGETS = [
"vercel.json",
"apps/web/vercel.json",
"middleware.ts",
"middleware.js",
"src/middleware.ts",
"next.config.ts",
"next.config.js",
"next.config.mjs",
".env",
".env.local",
".env.production",
".mcp.json",
"turbo.json",
"app/api/chat/route.ts",
"app/api/completion/route.ts",
"app/layout.tsx",
"src/app/layout.tsx",
"pages/api/auth/[...nextauth].ts",
"instrumentation.ts",
"lib/ai/index.ts",
"lib/cache/index.ts",
"lib/workflow/index.ts",
"components.json",
"components/ui/button.tsx",
"integration.json",
"flags.ts",
"lib/flags/index.ts",
".vercel/project.json",
".github/workflows/deploy.yml",
"playwright.config.ts",
"api/hello.ts",
"pages/api/hello.ts",
"app/api/bot/route.ts",
"lib/bot/index.ts",
];
// Well-known bash commands that trigger skills
const BASH_OVERLAP_TARGETS = [
"vercel deploy",
"vercel dev",
"vercel env pull",
"vercel env add",
"vercel logs",
"vercel build",
"next dev",
"next build",
"next dev --turbo",
"npm run dev",
"pnpm dev",
"bun run dev",
"npx v0 generate",
"npx shadcn add button",
"turbo run build",
"npm install ai @ai-sdk/openai",
"pnpm add @vercel/blob",
"npm install @vercel/flags",
"npx create-next-app",
"vercel integration add",
"vercel firewall",
];
interface CatalogSkillEntry extends SkillEntry {
slug: string;
}
function matchesPath(skill: SkillEntry, filePath: string): boolean {
for (const pat of skill.pathPatterns) {
try {
const regex = globToRegex(pat);
if (regex.test(filePath)) return true;
} catch {
// skip broken patterns
}
}
return false;
}
function matchesBash(skill: SkillEntry, command: string): boolean {
for (const pat of skill.bashPatterns) {
try {
if (new RegExp(pat).test(command)) return true;
} catch {
// skip broken patterns
}
}
return false;
}
async function main() {
const { skills: skillMap, buildDiagnostics } = loadValidatedSkillMap(SKILLS_DIR);
if (buildDiagnostics.length > 0) {
console.warn(`Skill build warnings (${buildDiagnostics.length}):`);
for (const d of buildDiagnostics) {
console.warn(` - ${d}`);
}
}
const skills: CatalogSkillEntry[] = Object.entries(skillMap)
.map(([slug, config]: [string, any]) => ({
slug,
priority: config.priority,
pathPatterns: config.pathPatterns ?? [],
bashPatterns: config.bashPatterns ?? [],
importPatterns: config.importPatterns ?? [],
}))
.sort((a, b) => b.priority - a.priority || a.slug.localeCompare(b.slug));
const lines: string[] = [];
// Header
lines.push("# Skill Catalog");
lines.push("");
lines.push(`> Auto-generated by \`scripts/generate-catalog.ts\` — do not edit manually.`);
lines.push(`> Generated: ${new Date().toISOString()}`);
lines.push(`> Skills: ${skills.length}`);
lines.push("");
// Table of contents
lines.push("## Table of Contents");
lines.push("");
lines.push("- [Skill Index](#skill-index)");
lines.push("- [Path Overlap Matrix](#path-overlap-matrix)");
lines.push("- [Bash Overlap Matrix](#bash-overlap-matrix)");
lines.push("- [Skills by Priority](#skills-by-priority)");
lines.push("");
// Skill index
lines.push("## Skill Index");
lines.push("");
lines.push("| Skill | Priority | Path Patterns | Bash Patterns | Import Patterns |");
lines.push("|-------|----------|---------------|---------------|-----------------|");
for (const s of skills) {
const pathCount = s.pathPatterns.length;
const bashCount = s.bashPatterns.length;
const importCount = s.importPatterns.length;
lines.push(`| \`${s.slug}\` | ${s.priority} | ${pathCount} | ${bashCount} | ${importCount} |`);
}
lines.push("");
// Detail for each skill
lines.push("### Skill Details");
lines.push("");
for (const s of skills) {
lines.push(`#### \`${s.slug}\` (priority ${s.priority})`);
lines.push("");
if (s.pathPatterns.length > 0) {
lines.push("**Path patterns:**");
for (const p of s.pathPatterns) {
lines.push(`- \`${p}\``);
}
lines.push("");
}
if (s.bashPatterns.length > 0) {
lines.push("**Bash patterns:**");
for (const p of s.bashPatterns) {
lines.push(`- \`${p}\``);
}
lines.push("");
}
if (s.importPatterns.length > 0) {
lines.push("**Import patterns:**");
for (const p of s.importPatterns) {
lines.push(`- \`${p}\``);
}
lines.push("");
}
// Find example matches from overlap targets
const pathMatches = OVERLAP_TARGETS.filter((t) => matchesPath(s, t));
const bashMatches = BASH_OVERLAP_TARGETS.filter((t) => matchesBash(s, t));
if (pathMatches.length > 0 || bashMatches.length > 0) {
lines.push("**Matched examples:**");
for (const m of pathMatches) lines.push(`- \`${m}\` (path)`);
for (const m of bashMatches) lines.push(`- \`${m}\` (bash)`);
lines.push("");
}
}
// Path overlap matrix
lines.push("## Path Overlap Matrix");
lines.push("");
lines.push("Shows which skills compete on shared file targets.");
lines.push("");
const pathOverlaps: { target: string; skills: string[] }[] = [];
for (const target of OVERLAP_TARGETS) {
const matching = skills.filter((s) => matchesPath(s, target)).map((s) => s.slug);
if (matching.length >= 2) {
pathOverlaps.push({ target, skills: matching });
}
}
if (pathOverlaps.length === 0) {
lines.push("No path overlaps detected among tested targets.");
} else {
lines.push("| Target File | Competing Skills (priority) |");
lines.push("|-------------|---------------------------|");
for (const { target, skills: slugs } of pathOverlaps) {
const detail = slugs.map((slug) => {
const s = skills.find((sk) => sk.slug === slug)!;
return `\`${slug}\` (${s.priority})`;
}).join(", ");
lines.push(`| \`${target}\` | ${detail} |`);
}
}
lines.push("");
// Bash overlap matrix
lines.push("## Bash Overlap Matrix");
lines.push("");
lines.push("Shows which skills compete on shared bash commands.");
lines.push("");
const bashOverlaps: { target: string; skills: string[] }[] = [];
for (const target of BASH_OVERLAP_TARGETS) {
const matching = skills.filter((s) => matchesBash(s, target)).map((s) => s.slug);
if (matching.length >= 2) {
bashOverlaps.push({ target, skills: matching });
}
}
if (bashOverlaps.length === 0) {
lines.push("No bash overlaps detected among tested commands.");
} else {
lines.push("| Bash Command | Competing Skills (priority) |");
lines.push("|--------------|---------------------------|");
for (const { target, skills: slugs } of bashOverlaps) {
const detail = slugs.map((slug) => {
const s = skills.find((sk) => sk.slug === slug)!;
return `\`${slug}\` (${s.priority})`;
}).join(", ");
lines.push(`| \`${target}\` | ${detail} |`);
}
}
lines.push("");
// Skills by priority
lines.push("## Skills by Priority");
lines.push("");
const byPriority = new Map<number, string[]>();
for (const s of skills) {
const list = byPriority.get(s.priority) ?? [];
list.push(s.slug);
byPriority.set(s.priority, list);
}
for (const [pri, slugs] of [...byPriority.entries()].sort((a, b) => b[0] - a[0])) {
lines.push(`**Priority ${pri}:** ${slugs.map((s) => `\`${s}\``).join(", ")}`);
lines.push("");
}
// Write output
const outputDir = join(ROOT, "generated");
try {
await stat(outputDir);
} catch {
await mkdir(outputDir, { recursive: true });
}
const content = lines.join("\n") + "\n";
await writeFile(OUTPUT_PATH, content);
console.log(`Wrote ${OUTPUT_PATH} (${skills.length} skills, ${pathOverlaps.length} path overlaps, ${bashOverlaps.length} bash overlaps)`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});