mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
93a4de9d97
* Add flags-sdk skill synced from vercel/flags
Co-authored-by: Vincent Derks <vincent-derks@users.noreply.github.com>
* Use vercel flags create instead of the add alias
Co-authored-by: Vincent Derks <vincent-derks@users.noreply.github.com>
* Revert "Use vercel flags create instead of the add alias"
This reverts commit 274dcbc4d9.
* Fix dead docs sitemap URL (vercel.com/sitemap/docs.xml soft-404s)
Co-authored-by: Vincent Derks <vincent-derks@users.noreply.github.com>
* Rename Edge Config to Global Config (product renamed July 2026)
Co-authored-by: Vincent Derks <vincent-derks@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Vincent Derks <vincent-derks@users.noreply.github.com>
298 lines
8.5 KiB
TypeScript
298 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 flags @flags-sdk/vercel",
|
|
"vercel flags create my-feature",
|
|
"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);
|
|
});
|