mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
improve postgres best practices and add evals
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
||||
validateSkillExists,
|
||||
} from "./config.js";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import type { Metadata, Rule, Section } from "./types.js";
|
||||
import { filterRulesForProfile, listProfiles, loadProfile } from "./profiles.js";
|
||||
import type { Metadata, Profile, Rule, Section } from "./types.js";
|
||||
import { validateRuleFile } from "./validate.js";
|
||||
|
||||
/**
|
||||
@@ -100,8 +101,13 @@ export function generateSectionMap(
|
||||
/**
|
||||
* Build AGENTS.md for a specific skill
|
||||
*/
|
||||
function buildSkill(paths: SkillPaths): void {
|
||||
console.log(`[${paths.name}] Building AGENTS.md...`);
|
||||
function buildSkill(paths: SkillPaths, profile?: Profile): void {
|
||||
const profileSuffix = profile ? `.${profile.name}` : "";
|
||||
const outputFile = profile
|
||||
? paths.agentsOutput.replace(".md", `${profileSuffix}.md`)
|
||||
: paths.agentsOutput;
|
||||
|
||||
console.log(`[${paths.name}] Building AGENTS${profileSuffix}.md...`);
|
||||
|
||||
// Load metadata and sections
|
||||
const metadata = loadMetadata(paths.metadataFile, paths.name);
|
||||
@@ -113,7 +119,7 @@ function buildSkill(paths: SkillPaths): void {
|
||||
if (!existsSync(paths.rulesDir)) {
|
||||
console.log(` No rules directory found. Generating empty AGENTS.md.`);
|
||||
writeFileSync(
|
||||
paths.agentsOutput,
|
||||
outputFile,
|
||||
`# ${skillTitle}\n\nNo rules defined yet.\n`,
|
||||
);
|
||||
return;
|
||||
@@ -147,10 +153,17 @@ function buildSkill(paths: SkillPaths): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter rules by profile if specified
|
||||
let filteredRules = rules;
|
||||
if (profile) {
|
||||
filteredRules = filterRulesForProfile(rules, profile);
|
||||
console.log(` Filtered to ${filteredRules.length} rules for profile "${profile.name}"`);
|
||||
}
|
||||
|
||||
// Group rules by section and assign IDs
|
||||
const rulesBySection = new Map<number, Rule[]>();
|
||||
|
||||
for (const rule of rules) {
|
||||
for (const rule of filteredRules) {
|
||||
const sectionRules = rulesBySection.get(rule.section) || [];
|
||||
sectionRules.push(rule);
|
||||
rulesBySection.set(rule.section, sectionRules);
|
||||
@@ -225,6 +238,18 @@ function buildSkill(paths: SkillPaths): void {
|
||||
output.push(`**Impact: ${rule.impact}**\n`);
|
||||
}
|
||||
|
||||
// Add prerequisites if minVersion or extensions are specified
|
||||
const prerequisites: string[] = [];
|
||||
if (rule.minVersion) {
|
||||
prerequisites.push(`PostgreSQL ${rule.minVersion}+`);
|
||||
}
|
||||
if (rule.extensions && rule.extensions.length > 0) {
|
||||
prerequisites.push(`Extension${rule.extensions.length > 1 ? "s" : ""}: ${rule.extensions.join(", ")}`);
|
||||
}
|
||||
if (prerequisites.length > 0) {
|
||||
output.push(`**Prerequisites:** ${prerequisites.join(" | ")}\n`);
|
||||
}
|
||||
|
||||
output.push(`${rule.explanation}\n`);
|
||||
|
||||
for (const example of rule.examples) {
|
||||
@@ -269,9 +294,52 @@ function buildSkill(paths: SkillPaths): void {
|
||||
}
|
||||
|
||||
// Write output
|
||||
writeFileSync(paths.agentsOutput, output.join("\n"));
|
||||
console.log(` Generated: ${paths.agentsOutput}`);
|
||||
console.log(` Total rules: ${rules.length}`);
|
||||
writeFileSync(outputFile, output.join("\n"));
|
||||
console.log(` Generated: ${outputFile}`);
|
||||
console.log(` Total rules: ${filteredRules.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CLI arguments
|
||||
*/
|
||||
function parseArgs(): { skill?: string; profile?: string; allProfiles: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
let skill: string | undefined;
|
||||
let profile: string | undefined;
|
||||
let allProfiles = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--profile" && args[i + 1]) {
|
||||
profile = args[i + 1];
|
||||
i++;
|
||||
} else if (arg === "--all-profiles") {
|
||||
allProfiles = true;
|
||||
} else if (!arg.startsWith("--")) {
|
||||
skill = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return { skill, profile, allProfiles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a skill with all available profiles
|
||||
*/
|
||||
function buildSkillWithAllProfiles(paths: SkillPaths): void {
|
||||
const profilesDir = join(paths.skillDir, "profiles");
|
||||
const profiles = listProfiles(profilesDir);
|
||||
|
||||
// Build default (no profile)
|
||||
buildSkill(paths);
|
||||
|
||||
// Build each profile variant
|
||||
for (const profileName of profiles) {
|
||||
const profile = loadProfile(profilesDir, profileName);
|
||||
if (profile) {
|
||||
buildSkill(paths, profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run build when executed directly
|
||||
@@ -280,7 +348,7 @@ const isMainModule =
|
||||
process.argv[1]?.endsWith("build.js");
|
||||
|
||||
if (isMainModule) {
|
||||
const targetSkill = process.argv[2];
|
||||
const { skill: targetSkill, profile: profileName, allProfiles } = parseArgs();
|
||||
|
||||
if (targetSkill) {
|
||||
// Build specific skill
|
||||
@@ -292,7 +360,29 @@ if (isMainModule) {
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
buildSkill(getSkillPaths(targetSkill));
|
||||
|
||||
const paths = getSkillPaths(targetSkill);
|
||||
|
||||
if (allProfiles) {
|
||||
// Build all profile variants
|
||||
buildSkillWithAllProfiles(paths);
|
||||
} else if (profileName) {
|
||||
// Build with specific profile
|
||||
const profilesDir = join(paths.skillDir, "profiles");
|
||||
const profile = loadProfile(profilesDir, profileName);
|
||||
if (!profile) {
|
||||
console.error(`Error: Profile "${profileName}" not found`);
|
||||
const available = listProfiles(profilesDir);
|
||||
if (available.length > 0) {
|
||||
console.error(`Available profiles: ${available.join(", ")}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
buildSkill(paths, profile);
|
||||
} else {
|
||||
// Build default
|
||||
buildSkill(paths);
|
||||
}
|
||||
} else {
|
||||
// Build all skills
|
||||
const skills = discoverSkills();
|
||||
@@ -303,7 +393,12 @@ if (isMainModule) {
|
||||
|
||||
console.log(`Found ${skills.length} skill(s): ${skills.join(", ")}\n`);
|
||||
for (const skill of skills) {
|
||||
buildSkill(getSkillPaths(skill));
|
||||
const paths = getSkillPaths(skill);
|
||||
if (allProfiles) {
|
||||
buildSkillWithAllProfiles(paths);
|
||||
} else {
|
||||
buildSkill(paths);
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ export function parseRuleFile(
|
||||
const examples = extractExamples(body);
|
||||
|
||||
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
|
||||
const extensions = frontmatter.extensions?.split(",").map((e) => e.trim()) || [];
|
||||
|
||||
// Validation warnings
|
||||
if (!explanation || explanation.length < 20) {
|
||||
@@ -271,6 +272,8 @@ export function parseRuleFile(
|
||||
examples,
|
||||
references: extractReferences(body),
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
minVersion: frontmatter.minVersion || undefined,
|
||||
extensions: extensions.length > 0 ? extensions : undefined,
|
||||
};
|
||||
|
||||
return { success: true, rule, errors, warnings };
|
||||
|
||||
102
packages/skills-build/src/profiles.ts
Normal file
102
packages/skills-build/src/profiles.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Profile, Rule } from "./types.js";
|
||||
|
||||
/**
|
||||
* Load a profile from the profiles directory
|
||||
*/
|
||||
export function loadProfile(profilesDir: string, profileName: string): Profile | null {
|
||||
const profileFile = join(profilesDir, `${profileName}.json`);
|
||||
if (!existsSync(profileFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(readFileSync(profileFile, "utf-8"));
|
||||
} catch (error) {
|
||||
console.error(`Error loading profile ${profileName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available profiles in the profiles directory
|
||||
*/
|
||||
export function listProfiles(profilesDir: string): string[] {
|
||||
if (!existsSync(profilesDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readdirSync(profilesDir)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => f.replace(".json", ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare version strings (e.g., "9.5", "11", "14.2")
|
||||
* Returns: negative if a < b, 0 if equal, positive if a > b
|
||||
*/
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const partsA = a.split(".").map(Number);
|
||||
const partsB = b.split(".").map(Number);
|
||||
|
||||
const maxLen = Math.max(partsA.length, partsB.length);
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const numA = partsA[i] || 0;
|
||||
const numB = partsB[i] || 0;
|
||||
if (numA !== numB) {
|
||||
return numA - numB;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a rule is compatible with a profile
|
||||
*/
|
||||
export function isRuleCompatibleWithProfile(rule: Rule, profile: Profile): boolean {
|
||||
// Check version requirement
|
||||
if (rule.minVersion) {
|
||||
if (compareVersions(rule.minVersion, profile.minVersion) > 0) {
|
||||
// Rule requires a higher version than profile supports
|
||||
return false;
|
||||
}
|
||||
if (profile.maxVersion && compareVersions(rule.minVersion, profile.maxVersion) > 0) {
|
||||
// Rule requires a version higher than profile's max
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check extension requirements
|
||||
if (rule.extensions && rule.extensions.length > 0) {
|
||||
const allExtensions = [
|
||||
...(profile.extensions.available || []),
|
||||
...(profile.extensions.installable || []),
|
||||
];
|
||||
|
||||
for (const ext of rule.extensions) {
|
||||
if (profile.extensions.unavailable?.includes(ext)) {
|
||||
// Extension is explicitly unavailable in this profile
|
||||
return false;
|
||||
}
|
||||
if (!allExtensions.includes(ext)) {
|
||||
// Extension is not available or installable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if rule is explicitly excluded
|
||||
if (profile.excludeRules?.includes(rule.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter rules based on profile constraints
|
||||
*/
|
||||
export function filterRulesForProfile(rules: Rule[], profile: Profile): Rule[] {
|
||||
return rules.filter((rule) => isRuleCompatibleWithProfile(rule, profile));
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export interface Rule {
|
||||
references?: string[];
|
||||
tags?: string[];
|
||||
supabaseNotes?: string;
|
||||
minVersion?: string; // Minimum PostgreSQL version required (e.g., "11", "14")
|
||||
extensions?: string[]; // Required PostgreSQL extensions (e.g., ["pg_stat_statements"])
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
@@ -57,3 +59,16 @@ export interface ValidationResult {
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
minVersion: string;
|
||||
maxVersion?: string;
|
||||
extensions: {
|
||||
available: string[];
|
||||
installable?: string[];
|
||||
unavailable: string[];
|
||||
};
|
||||
excludeRules?: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user