mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
234 lines
7.6 KiB
TypeScript
234 lines
7.6 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Build-time script that generates a static skill manifest from SKILL.md
|
|
* frontmatter. The PreToolUse hook reads this manifest instead of scanning
|
|
* and parsing every SKILL.md on each invocation.
|
|
*
|
|
* Usage: bun run scripts/build-manifest.ts
|
|
* node scripts/build-manifest.ts (also works via bun shim)
|
|
*/
|
|
|
|
import { resolve, join } from "node:path";
|
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
|
|
// Import the canonical skill-map builder (ESM)
|
|
import { globToRegex, importPatternToRegex } from "../hooks/patterns.mjs";
|
|
import type { SkillEntry, ManifestSkill } from "../hooks/patterns.mjs";
|
|
import type { ChainToRule, ValidationRule } from "../hooks/skill-map-frontmatter.mjs";
|
|
import { loadValidatedSkillMap } from "../src/shared/skill-map-loader.ts";
|
|
|
|
export { buildManifest, writeManifestFile, synthesizeChainToFromValidate };
|
|
|
|
const ROOT = resolve(import.meta.dir, "..");
|
|
const SKILLS_DIR = join(ROOT, "skills");
|
|
const OUT_DIR = join(ROOT, "generated");
|
|
const OUT_FILE = join(OUT_DIR, "skill-manifest.json");
|
|
|
|
interface ManifestSkillWithBody extends ManifestSkill {
|
|
bodyPath: string;
|
|
}
|
|
|
|
interface Manifest {
|
|
generatedAt: string;
|
|
version: 2;
|
|
skills: Record<string, ManifestSkillWithBody>;
|
|
}
|
|
|
|
/**
|
|
* Compile regex sources for a skill config at build time.
|
|
* Path globs → globToRegex().source, bash patterns → RegExp source,
|
|
* import patterns → importPatternToRegex() source+flags.
|
|
*
|
|
* Returns paired arrays: patterns and regex sources stay in sync so that
|
|
* index N of pathPatterns always corresponds to index N of pathRegexSources.
|
|
* Invalid patterns are dropped from both arrays to prevent index drift.
|
|
*/
|
|
function compileRegexSources(config: SkillEntry) {
|
|
const pathPatterns: string[] = [];
|
|
const pathRegexSources: string[] = [];
|
|
for (const p of config.pathPatterns) {
|
|
try {
|
|
pathRegexSources.push(globToRegex(p).source);
|
|
pathPatterns.push(p);
|
|
} catch {
|
|
// Skip invalid — validation catches these
|
|
}
|
|
}
|
|
|
|
const bashPatterns: string[] = [];
|
|
const bashRegexSources: string[] = [];
|
|
for (const p of config.bashPatterns) {
|
|
try {
|
|
new RegExp(p); // validate
|
|
bashRegexSources.push(p);
|
|
bashPatterns.push(p);
|
|
} catch {
|
|
// Skip invalid
|
|
}
|
|
}
|
|
|
|
const importPatterns: string[] = [];
|
|
const importRegexSources: Array<{ source: string; flags: string }> = [];
|
|
for (const p of config.importPatterns) {
|
|
try {
|
|
const re = importPatternToRegex(p);
|
|
importRegexSources.push({ source: re.source, flags: re.flags });
|
|
importPatterns.push(p);
|
|
} catch {
|
|
// Skip invalid
|
|
}
|
|
}
|
|
|
|
return { pathPatterns, pathRegexSources, bashPatterns, bashRegexSources, importPatterns, importRegexSources };
|
|
}
|
|
|
|
/**
|
|
* Auto-synthesize chainTo entries from validate rules that have upgradeToSkill
|
|
* with severity "error" or "recommended", unless a matching chainTo already
|
|
* exists for that targetSkill in the same skill.
|
|
*
|
|
* Returns the number of synthesized entries added.
|
|
*/
|
|
function synthesizeChainToFromValidate(
|
|
skills: Record<string, SkillEntry>,
|
|
allSlugs: Set<string>,
|
|
): { count: number; warnings: string[] } {
|
|
let count = 0;
|
|
const warnings: string[] = [];
|
|
|
|
for (const [slug, config] of Object.entries(skills)) {
|
|
if (!config.validate?.length) continue;
|
|
|
|
// Collect existing chainTo targets for this skill
|
|
const existingTargets = new Set(
|
|
(config.chainTo ?? []).map((c: ChainToRule) => c.targetSkill),
|
|
);
|
|
|
|
for (const rule of config.validate as ValidationRule[]) {
|
|
if (!rule.upgradeToSkill) continue;
|
|
if (rule.severity !== "error" && rule.severity !== "recommended") continue;
|
|
if (existingTargets.has(rule.upgradeToSkill)) continue;
|
|
if (!allSlugs.has(rule.upgradeToSkill)) {
|
|
warnings.push(
|
|
`skill "${slug}": cannot synthesize chainTo for upgradeToSkill "${rule.upgradeToSkill}" — target skill does not exist`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const message =
|
|
rule.upgradeWhy ||
|
|
`${rule.message} — loading ${rule.upgradeToSkill} guidance.`;
|
|
|
|
const synthesized: ChainToRule = {
|
|
pattern: rule.pattern,
|
|
targetSkill: rule.upgradeToSkill,
|
|
message,
|
|
synthesized: true,
|
|
};
|
|
|
|
if (!config.chainTo) {
|
|
config.chainTo = [];
|
|
}
|
|
config.chainTo.push(synthesized);
|
|
existingTargets.add(rule.upgradeToSkill);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return { count, warnings };
|
|
}
|
|
|
|
/**
|
|
* Build the skill manifest object from the skills directory.
|
|
* Exported so validate.ts can reuse this without duplicating logic.
|
|
*/
|
|
function buildManifest(skillsDir: string): { manifest: Manifest; warnings: string[]; errors: string[] } {
|
|
const { validation, buildDiagnostics } = loadValidatedSkillMap(skillsDir);
|
|
const allWarnings: string[] = [...buildDiagnostics];
|
|
|
|
if (!validation.ok) {
|
|
return { manifest: null as any, warnings: allWarnings, errors: validation.errors };
|
|
}
|
|
|
|
if (validation.warnings?.length) {
|
|
allWarnings.push(...validation.warnings);
|
|
}
|
|
|
|
// Auto-synthesize chainTo from upgradeToSkill validate rules
|
|
const normalizedSkills = validation.normalizedSkillMap.skills as Record<string, SkillEntry>;
|
|
const allSlugs = new Set(Object.keys(normalizedSkills));
|
|
const { count: synthCount, warnings: synthWarnings } =
|
|
synthesizeChainToFromValidate(normalizedSkills, allSlugs);
|
|
allWarnings.push(...synthWarnings);
|
|
if (synthCount > 0) {
|
|
console.error(` ⤳ Synthesized ${synthCount} chainTo rule(s) from upgradeToSkill validate rules`);
|
|
}
|
|
|
|
const skills: Record<string, ManifestSkillWithBody> = {};
|
|
for (const [slug, config] of Object.entries(normalizedSkills) as [string, SkillEntry][]) {
|
|
const { pathPatterns, pathRegexSources, bashPatterns, bashRegexSources, importPatterns, importRegexSources } = compileRegexSources(config);
|
|
skills[slug] = {
|
|
priority: config.priority,
|
|
summary: config.summary,
|
|
docs: config.docs,
|
|
...(config.sitemap ? { sitemap: config.sitemap } : {}),
|
|
pathPatterns,
|
|
bashPatterns,
|
|
importPatterns,
|
|
bodyPath: `skills/${slug}/SKILL.md`,
|
|
pathRegexSources,
|
|
bashRegexSources,
|
|
importRegexSources,
|
|
...(config.validate?.length ? { validate: config.validate } : {}),
|
|
...(config.chainTo?.length ? { chainTo: config.chainTo } : {}),
|
|
...(config.promptSignals ? { promptSignals: config.promptSignals } : {}),
|
|
...(config.retrieval ? { retrieval: config.retrieval } : {}),
|
|
};
|
|
}
|
|
|
|
const manifest: Manifest = {
|
|
generatedAt: new Date().toISOString(),
|
|
version: 2,
|
|
skills,
|
|
};
|
|
|
|
return { manifest, warnings: allWarnings, errors: [] };
|
|
}
|
|
|
|
/**
|
|
* Write the manifest JSON to generated/skill-manifest.json.
|
|
* Returns the number of skills written.
|
|
*/
|
|
function writeManifestFile(manifest: Manifest, outDir = OUT_DIR, outFile = OUT_FILE): number {
|
|
mkdirSync(outDir, { recursive: true });
|
|
writeFileSync(outFile, JSON.stringify(manifest, null, 2) + "\n");
|
|
return Object.keys(manifest.skills).length;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CLI entry point (only when run directly)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function isMain() {
|
|
try {
|
|
return resolve(process.argv[1] || "") === resolve(import.meta.filename);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isMain()) {
|
|
const { manifest, warnings, errors } = buildManifest(SKILLS_DIR);
|
|
|
|
for (const w of warnings) console.warn(`[warn] ${w}`);
|
|
|
|
if (errors.length > 0) {
|
|
console.error("[error] Skill map validation failed:");
|
|
for (const e of errors) console.error(` - ${e}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const count = writeManifestFile(manifest);
|
|
console.log(`✓ Wrote ${count} skills to ${OUT_FILE}`);
|
|
}
|