Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/build-agents.ts
1#!/usr/bin/env npx ts-node23/**4* Build script for generating AGENTS.md from individual rule files5*6* Usage: npx ts-node scripts/build-agents.ts7*8* This script:9* 1. Reads all rule files from the rules/ directory10* 2. Parses YAML frontmatter for metadata11* 3. Groups rules by category based on filename prefix12* 4. Generates a consolidated AGENTS.md file13*/1415import * as fs from 'fs';16import * as path from 'path';17import { fileURLToPath } from 'url';18import { dirname } from 'path';1920const __filename = fileURLToPath(import.meta.url);21const __dirname = dirname(__filename);2223// Category definitions with ordering and metadata24const CATEGORIES = [25{ prefix: 'arch-', name: 'Architecture', impact: 'CRITICAL', section: 1 },26{ prefix: 'di-', name: 'Dependency Injection', impact: 'CRITICAL', section: 2 },27{ prefix: 'error-', name: 'Error Handling', impact: 'HIGH', section: 3 },28{ prefix: 'security-', name: 'Security', impact: 'HIGH', section: 4 },29{ prefix: 'perf-', name: 'Performance', impact: 'HIGH', section: 5 },30{ prefix: 'test-', name: 'Testing', impact: 'MEDIUM-HIGH', section: 6 },31{ prefix: 'db-', name: 'Database & ORM', impact: 'MEDIUM-HIGH', section: 7 },32{ prefix: 'api-', name: 'API Design', impact: 'MEDIUM', section: 8 },33{ prefix: 'micro-', name: 'Microservices', impact: 'MEDIUM', section: 9 },34{ prefix: 'devops-', name: 'DevOps & Deployment', impact: 'LOW-MEDIUM', section: 10 },35];3637interface RuleFrontmatter {38title: string;39impact: string;40impactDescription: string;41tags: string[];42}4344interface Rule {45filename: string;46frontmatter: RuleFrontmatter;47content: string;48category: string;49categorySection: number;50}5152function parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | null; body: string } {53const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;54const match = content.match(frontmatterRegex);5556if (!match) {57return { frontmatter: null, body: content };58}5960const frontmatterStr = match[1];61const body = match[2];6263// Simple YAML parsing for our expected format64const frontmatter: Partial<RuleFrontmatter> = {};65const lines = frontmatterStr.split('\n');66let currentKey = '';67let inArray = false;68const arrayItems: string[] = [];6970for (const line of lines) {71if (line.match(/^[a-zA-Z]+:/)) {72// Save previous array if we were collecting one73if (inArray && currentKey === 'tags') {74frontmatter.tags = arrayItems;75}76inArray = false;77arrayItems.length = 0;7879const [key, ...valueParts] = line.split(':');80const value = valueParts.join(':').trim();81currentKey = key.trim();8283if (value === '') {84// Might be start of array85inArray = true;86} else {87(frontmatter as any)[currentKey] = value;88}89} else if (inArray && line.trim().startsWith('-')) {90arrayItems.push(line.trim().replace(/^-\s*/, ''));91}92}9394// Save final array if needed95if (inArray && currentKey === 'tags') {96frontmatter.tags = arrayItems;97}9899return {100frontmatter: frontmatter as RuleFrontmatter,101body: body.trim()102};103}104105function getCategoryForFile(filename: string): { name: string; section: number } | null {106for (const cat of CATEGORIES) {107if (filename.startsWith(cat.prefix)) {108return { name: cat.name, section: cat.section };109}110}111return null;112}113114function readMetadata(): any {115const metadataPath = path.join(__dirname, '..', 'metadata.json');116return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));117}118119function readRules(): Rule[] {120const rulesDir = path.join(__dirname, '..', 'rules');121const files = fs.readdirSync(rulesDir)122.filter(f => f.endsWith('.md') && !f.startsWith('_'));123124const rules: Rule[] = [];125126for (const file of files) {127const filePath = path.join(rulesDir, file);128const content = fs.readFileSync(filePath, 'utf-8');129const { frontmatter, body } = parseFrontmatter(content);130131if (!frontmatter) {132console.warn(`Warning: No frontmatter found in ${file}`);133continue;134}135136const category = getCategoryForFile(file);137if (!category) {138console.warn(`Warning: Unknown category for ${file}`);139continue;140}141142rules.push({143filename: file,144frontmatter,145content: body,146category: category.name,147categorySection: category.section148});149}150151return rules;152}153154function generateTableOfContents(rulesByCategory: Map<string, Rule[]>): string {155let toc = '## Table of Contents\n\n';156157for (const cat of CATEGORIES) {158const rules = rulesByCategory.get(cat.name);159if (!rules || rules.length === 0) continue;160161// Section anchor format: #1-architecture162const sectionAnchor = `${cat.section}-${cat.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;163toc += `${cat.section}. [${cat.name}](#${sectionAnchor}) — **${cat.impact}**\n`;164165for (let i = 0; i < rules.length; i++) {166const rule = rules[i];167// Rule anchor format: #11-rule-title168const ruleNum = `${cat.section}${i + 1}`;169const anchor = `${ruleNum}-${rule.frontmatter.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;170toc += ` - ${cat.section}.${i + 1} [${rule.frontmatter.title}](#${anchor})\n`;171}172}173174return toc;175}176177function generateAgentsMd(rules: Rule[], metadata: any): string {178// Group rules by category179const rulesByCategory = new Map<string, Rule[]>();180181for (const rule of rules) {182if (!rulesByCategory.has(rule.category)) {183rulesByCategory.set(rule.category, []);184}185rulesByCategory.get(rule.category)!.push(rule);186}187188// Sort rules within each category alphabetically189for (const [category, categoryRules] of rulesByCategory) {190categoryRules.sort((a, b) => a.filename.localeCompare(b.filename));191}192193// Build document194let doc = `# NestJS Best Practices195196**Version ${metadata.version}**197${metadata.organization}198${metadata.date}199200> **Note:**201> This document is mainly for agents and LLMs to follow when maintaining,202> generating, or refactoring NestJS codebases. Humans may also find it203> useful, but guidance here is optimized for automation and consistency204> by AI-assisted workflows.205206---207208## Abstract209210${metadata.abstract}211212---213214`;215216// Add table of contents217doc += generateTableOfContents(rulesByCategory);218doc += '\n---\n\n';219220// Add rules by category221for (const cat of CATEGORIES) {222const categoryRules = rulesByCategory.get(cat.name);223if (!categoryRules || categoryRules.length === 0) continue;224225doc += `## ${cat.section}. ${cat.name}\n\n`;226doc += `**Section Impact: ${cat.impact}**\n\n`;227228for (let i = 0; i < categoryRules.length; i++) {229const rule = categoryRules[i];230const ruleNumber = `${cat.section}.${i + 1}`;231232// Add rule header with number (anchor will be auto-generated as #11-title)233doc += `### ${ruleNumber} ${rule.frontmatter.title}\n\n`;234doc += `**Impact: ${rule.frontmatter.impact}** — ${rule.frontmatter.impactDescription}\n\n`;235236// Add rule content (skip the first header since we already added it)237let ruleContent = rule.content;238// Remove the first h1 or h2 header if it matches the title239ruleContent = ruleContent.replace(/^#{1,2}\s+.*\n+/, '');240// Remove the impact line if present (we already added it)241ruleContent = ruleContent.replace(/^\*\*Impact:.*\*\*.*\n+/, '');242243doc += ruleContent;244doc += '\n\n---\n\n';245}246}247248// Add references footer249doc += `## References250251`;252for (const ref of metadata.references) {253doc += `- ${ref}\n`;254}255256doc += `257---258259*Generated by build-agents.ts on ${new Date().toISOString().split('T')[0]}*260`;261262return doc;263}264265function main() {266console.log('Building AGENTS.md...\n');267268const metadata = readMetadata();269console.log(`Version: ${metadata.version}`);270console.log(`Organization: ${metadata.organization}\n`);271272const rules = readRules();273console.log(`Found ${rules.length} rules\n`);274275// Count by category276const counts = new Map<string, number>();277for (const rule of rules) {278counts.set(rule.category, (counts.get(rule.category) || 0) + 1);279}280281console.log('Rules by category:');282for (const cat of CATEGORIES) {283const count = counts.get(cat.name) || 0;284if (count > 0) {285console.log(` ${cat.name}: ${count}`);286}287}288console.log('');289290const agentsMd = generateAgentsMd(rules, metadata);291292const outputPath = path.join(__dirname, '..', 'AGENTS.md');293fs.writeFileSync(outputPath, agentsMd);294295console.log(`Generated AGENTS.md (${agentsMd.length} bytes)`);296console.log(`Output: ${outputPath}`);297}298299main();300