mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
Update version from 0.1.0 to 1.0.0 across all files: - skills/postgres-best-practices/AGENTS.md - skills/postgres-best-practices/SKILL.md - skills/postgres-best-practices/metadata.json - packages/postgres-best-practices-build/package.json - packages/postgres-best-practices-build/package-lock.json - packages/postgres-best-practices-build/src/build.ts Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
248 lines
7.8 KiB
TypeScript
248 lines
7.8 KiB
TypeScript
import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
import { join, basename } from "path";
|
|
import { parseRuleFile } from "./parser.js";
|
|
import { validateRuleFile } from "./validate.js";
|
|
import { RULES_DIR, AGENTS_OUTPUT, METADATA_FILE } from "./config.js";
|
|
import type { Rule, Metadata, Section } from "./types.js";
|
|
|
|
/**
|
|
* Parse section definitions from _sections.md
|
|
*/
|
|
function parseSections(): Section[] {
|
|
const sectionsFile = join(RULES_DIR, "_sections.md");
|
|
if (!existsSync(sectionsFile)) {
|
|
console.warn("Warning: _sections.md not found, using default sections");
|
|
return getDefaultSections();
|
|
}
|
|
|
|
const content = readFileSync(sectionsFile, "utf-8");
|
|
const sections: Section[] = [];
|
|
|
|
const sectionMatches = content.matchAll(
|
|
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g
|
|
);
|
|
|
|
for (const match of sectionMatches) {
|
|
sections.push({
|
|
number: parseInt(match[1], 10),
|
|
title: match[2].trim(),
|
|
prefix: match[3].trim(),
|
|
impact: match[4].trim() as Section["impact"],
|
|
description: match[5].trim(),
|
|
});
|
|
}
|
|
|
|
return sections.length > 0 ? sections : getDefaultSections();
|
|
}
|
|
|
|
/**
|
|
* Default sections if _sections.md is missing or unparseable
|
|
*/
|
|
function getDefaultSections(): Section[] {
|
|
return [
|
|
{ number: 1, title: "Query Performance", prefix: "query", impact: "CRITICAL", description: "Slow queries, missing indexes, inefficient plans" },
|
|
{ number: 2, title: "Connection Management", prefix: "conn", impact: "CRITICAL", description: "Pooling, limits, serverless strategies" },
|
|
{ number: 3, title: "Schema Design", prefix: "schema", impact: "HIGH", description: "Table design, indexes, partitioning, data types" },
|
|
{ number: 4, title: "Concurrency & Locking", prefix: "lock", impact: "MEDIUM-HIGH", description: "Transactions, isolation, deadlocks" },
|
|
{ number: 5, title: "Security & RLS", prefix: "security", impact: "MEDIUM-HIGH", description: "Row-Level Security, privileges, auth patterns" },
|
|
{ number: 6, title: "Data Access Patterns", prefix: "data", impact: "MEDIUM", description: "N+1 queries, batch operations, pagination" },
|
|
{ number: 7, title: "Monitoring & Diagnostics", prefix: "monitor", impact: "LOW-MEDIUM", description: "pg_stat_statements, EXPLAIN, metrics" },
|
|
{ number: 8, title: "Advanced Features", prefix: "advanced", impact: "LOW", description: "Full-text search, JSONB, extensions" },
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Load metadata from metadata.json
|
|
*/
|
|
function loadMetadata(): Metadata {
|
|
if (!existsSync(METADATA_FILE)) {
|
|
return {
|
|
version: "1.0.0",
|
|
organization: "Supabase",
|
|
date: new Date().toLocaleDateString("en-US", { month: "long", year: "numeric" }),
|
|
abstract: "Postgres performance optimization guide for developers.",
|
|
references: [],
|
|
};
|
|
}
|
|
|
|
return JSON.parse(readFileSync(METADATA_FILE, "utf-8"));
|
|
}
|
|
|
|
/**
|
|
* Generate anchor from title
|
|
*/
|
|
function toAnchor(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
.replace(/\s+/g, "-");
|
|
}
|
|
|
|
/**
|
|
* Build AGENTS.md from all rule files
|
|
*/
|
|
function buildAgents(): void {
|
|
console.log("Building AGENTS.md...\n");
|
|
|
|
// Load metadata and sections
|
|
const metadata = loadMetadata();
|
|
const sections = parseSections();
|
|
|
|
// Get all rule files
|
|
const ruleFiles = readdirSync(RULES_DIR)
|
|
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
|
.map((f) => join(RULES_DIR, f));
|
|
|
|
if (ruleFiles.length === 0) {
|
|
console.log("No rule files found. Generating empty AGENTS.md template.");
|
|
}
|
|
|
|
// Parse and validate all rules
|
|
const rules: Rule[] = [];
|
|
|
|
for (const file of ruleFiles) {
|
|
const validation = validateRuleFile(file);
|
|
if (!validation.valid) {
|
|
console.error(`Skipping invalid file ${basename(file)}:`);
|
|
validation.errors.forEach((e) => console.error(` - ${e}`));
|
|
continue;
|
|
}
|
|
|
|
const result = parseRuleFile(file);
|
|
if (result.success && result.rule) {
|
|
rules.push(result.rule);
|
|
}
|
|
}
|
|
|
|
// Group rules by section and assign IDs
|
|
const rulesBySection = new Map<number, Rule[]>();
|
|
|
|
for (const rule of rules) {
|
|
const sectionRules = rulesBySection.get(rule.section) || [];
|
|
sectionRules.push(rule);
|
|
rulesBySection.set(rule.section, sectionRules);
|
|
}
|
|
|
|
// Sort rules within each section and assign IDs
|
|
for (const [sectionNum, sectionRules] of rulesBySection) {
|
|
sectionRules.sort((a, b) => a.title.localeCompare(b.title));
|
|
sectionRules.forEach((rule, index) => {
|
|
rule.id = `${sectionNum}.${index + 1}`;
|
|
});
|
|
}
|
|
|
|
// Generate markdown output
|
|
const output: string[] = [];
|
|
|
|
// Header
|
|
output.push("# Postgres Best Practices\n");
|
|
output.push(`**Version ${metadata.version}**`);
|
|
output.push(`${metadata.organization}`);
|
|
output.push(`${metadata.date}\n`);
|
|
output.push("> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n");
|
|
output.push("---\n");
|
|
|
|
// Abstract
|
|
output.push("## Abstract\n");
|
|
output.push(`${metadata.abstract}\n`);
|
|
output.push("---\n");
|
|
|
|
// Table of Contents
|
|
output.push("## Table of Contents\n");
|
|
|
|
for (const section of sections) {
|
|
const sectionRules = rulesBySection.get(section.number) || [];
|
|
output.push(`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`);
|
|
|
|
for (const rule of sectionRules) {
|
|
output.push(` - ${rule.id} [${rule.title}](#${toAnchor(rule.id + "-" + rule.title)})`);
|
|
}
|
|
|
|
output.push("");
|
|
}
|
|
|
|
output.push("---\n");
|
|
|
|
// Sections and Rules
|
|
for (const section of sections) {
|
|
const sectionRules = rulesBySection.get(section.number) || [];
|
|
|
|
output.push(`## ${section.number}. ${section.title}\n`);
|
|
output.push(`**Impact: ${section.impact}**\n`);
|
|
output.push(`${section.description}\n`);
|
|
|
|
if (sectionRules.length === 0) {
|
|
output.push("*No rules defined yet. See rules/_template.md for creating new rules.*\n");
|
|
}
|
|
|
|
for (const rule of sectionRules) {
|
|
output.push(`### ${rule.id} ${rule.title}\n`);
|
|
|
|
if (rule.impactDescription) {
|
|
output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`);
|
|
} else {
|
|
output.push(`**Impact: ${rule.impact}**\n`);
|
|
}
|
|
|
|
output.push(`${rule.explanation}\n`);
|
|
|
|
for (const example of rule.examples) {
|
|
if (example.description) {
|
|
output.push(`**${example.label} (${example.description}):**\n`);
|
|
} else {
|
|
output.push(`**${example.label}:**\n`);
|
|
}
|
|
|
|
output.push("```" + (example.language || "sql"));
|
|
output.push(example.code);
|
|
output.push("```\n");
|
|
|
|
if (example.additionalText) {
|
|
output.push(`${example.additionalText}\n`);
|
|
}
|
|
}
|
|
|
|
if (rule.supabaseNotes) {
|
|
output.push(`**Supabase Note:** ${rule.supabaseNotes}\n`);
|
|
}
|
|
|
|
if (rule.references && rule.references.length > 0) {
|
|
if (rule.references.length === 1) {
|
|
output.push(`Reference: ${rule.references[0]}\n`);
|
|
} else {
|
|
output.push("References:");
|
|
for (const ref of rule.references) {
|
|
output.push(`- ${ref}`);
|
|
}
|
|
output.push("");
|
|
}
|
|
}
|
|
|
|
output.push("---\n");
|
|
}
|
|
}
|
|
|
|
// References section
|
|
if (metadata.references && metadata.references.length > 0) {
|
|
output.push("## References\n");
|
|
for (const ref of metadata.references) {
|
|
output.push(`- ${ref}`);
|
|
}
|
|
output.push("");
|
|
}
|
|
|
|
// Write output
|
|
writeFileSync(AGENTS_OUTPUT, output.join("\n"));
|
|
console.log(`Generated: ${AGENTS_OUTPUT}`);
|
|
console.log(`Total rules: ${rules.length}`);
|
|
}
|
|
|
|
// Run build when executed directly
|
|
const isMainModule = process.argv[1]?.endsWith("build.ts") || process.argv[1]?.endsWith("build.js");
|
|
|
|
if (isMainModule) {
|
|
buildAgents();
|
|
}
|
|
|
|
export { buildAgents };
|