chore: restore files unintentionally deleted in 6c61a4e

This commit is contained in:
yeuimu
2026-08-26 17:54:35 +08:00
parent 6c61a4e871
commit 005ab5b585
486 changed files with 114058 additions and 0 deletions
@@ -0,0 +1,299 @@
#!/usr/bin/env npx ts-node
/**
* Build script for generating AGENTS.md from individual rule files
*
* Usage: npx ts-node scripts/build-agents.ts
*
* This script:
* 1. Reads all rule files from the rules/ directory
* 2. Parses YAML frontmatter for metadata
* 3. Groups rules by category based on filename prefix
* 4. Generates a consolidated AGENTS.md file
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Category definitions with ordering and metadata
const CATEGORIES = [
{ prefix: 'arch-', name: 'Architecture', impact: 'CRITICAL', section: 1 },
{ prefix: 'di-', name: 'Dependency Injection', impact: 'CRITICAL', section: 2 },
{ prefix: 'error-', name: 'Error Handling', impact: 'HIGH', section: 3 },
{ prefix: 'security-', name: 'Security', impact: 'HIGH', section: 4 },
{ prefix: 'perf-', name: 'Performance', impact: 'HIGH', section: 5 },
{ prefix: 'test-', name: 'Testing', impact: 'MEDIUM-HIGH', section: 6 },
{ prefix: 'db-', name: 'Database & ORM', impact: 'MEDIUM-HIGH', section: 7 },
{ prefix: 'api-', name: 'API Design', impact: 'MEDIUM', section: 8 },
{ prefix: 'micro-', name: 'Microservices', impact: 'MEDIUM', section: 9 },
{ prefix: 'devops-', name: 'DevOps & Deployment', impact: 'LOW-MEDIUM', section: 10 },
];
interface RuleFrontmatter {
title: string;
impact: string;
impactDescription: string;
tags: string[];
}
interface Rule {
filename: string;
frontmatter: RuleFrontmatter;
content: string;
category: string;
categorySection: number;
}
function parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | null; body: string } {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { frontmatter: null, body: content };
}
const frontmatterStr = match[1];
const body = match[2];
// Simple YAML parsing for our expected format
const frontmatter: Partial<RuleFrontmatter> = {};
const lines = frontmatterStr.split('\n');
let currentKey = '';
let inArray = false;
const arrayItems: string[] = [];
for (const line of lines) {
if (line.match(/^[a-zA-Z]+:/)) {
// Save previous array if we were collecting one
if (inArray && currentKey === 'tags') {
frontmatter.tags = arrayItems;
}
inArray = false;
arrayItems.length = 0;
const [key, ...valueParts] = line.split(':');
const value = valueParts.join(':').trim();
currentKey = key.trim();
if (value === '') {
// Might be start of array
inArray = true;
} else {
(frontmatter as any)[currentKey] = value;
}
} else if (inArray && line.trim().startsWith('-')) {
arrayItems.push(line.trim().replace(/^-\s*/, ''));
}
}
// Save final array if needed
if (inArray && currentKey === 'tags') {
frontmatter.tags = arrayItems;
}
return {
frontmatter: frontmatter as RuleFrontmatter,
body: body.trim()
};
}
function getCategoryForFile(filename: string): { name: string; section: number } | null {
for (const cat of CATEGORIES) {
if (filename.startsWith(cat.prefix)) {
return { name: cat.name, section: cat.section };
}
}
return null;
}
function readMetadata(): any {
const metadataPath = path.join(__dirname, '..', 'metadata.json');
return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
}
function readRules(): Rule[] {
const rulesDir = path.join(__dirname, '..', 'rules');
const files = fs.readdirSync(rulesDir)
.filter(f => f.endsWith('.md') && !f.startsWith('_'));
const rules: Rule[] = [];
for (const file of files) {
const filePath = path.join(rulesDir, file);
const content = fs.readFileSync(filePath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
if (!frontmatter) {
console.warn(`Warning: No frontmatter found in ${file}`);
continue;
}
const category = getCategoryForFile(file);
if (!category) {
console.warn(`Warning: Unknown category for ${file}`);
continue;
}
rules.push({
filename: file,
frontmatter,
content: body,
category: category.name,
categorySection: category.section
});
}
return rules;
}
function generateTableOfContents(rulesByCategory: Map<string, Rule[]>): string {
let toc = '## Table of Contents\n\n';
for (const cat of CATEGORIES) {
const rules = rulesByCategory.get(cat.name);
if (!rules || rules.length === 0) continue;
// Section anchor format: #1-architecture
const sectionAnchor = `${cat.section}-${cat.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
toc += `${cat.section}. [${cat.name}](#${sectionAnchor}) — **${cat.impact}**\n`;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
// Rule anchor format: #11-rule-title
const ruleNum = `${cat.section}${i + 1}`;
const anchor = `${ruleNum}-${rule.frontmatter.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
toc += ` - ${cat.section}.${i + 1} [${rule.frontmatter.title}](#${anchor})\n`;
}
}
return toc;
}
function generateAgentsMd(rules: Rule[], metadata: any): string {
// Group rules by category
const rulesByCategory = new Map<string, Rule[]>();
for (const rule of rules) {
if (!rulesByCategory.has(rule.category)) {
rulesByCategory.set(rule.category, []);
}
rulesByCategory.get(rule.category)!.push(rule);
}
// Sort rules within each category alphabetically
for (const [category, categoryRules] of rulesByCategory) {
categoryRules.sort((a, b) => a.filename.localeCompare(b.filename));
}
// Build document
let doc = `# NestJS Best Practices
**Version ${metadata.version}**
${metadata.organization}
${metadata.date}
> **Note:**
> This document is mainly for agents and LLMs to follow when maintaining,
> generating, or refactoring NestJS codebases. Humans may also find it
> useful, but guidance here is optimized for automation and consistency
> by AI-assisted workflows.
---
## Abstract
${metadata.abstract}
---
`;
// Add table of contents
doc += generateTableOfContents(rulesByCategory);
doc += '\n---\n\n';
// Add rules by category
for (const cat of CATEGORIES) {
const categoryRules = rulesByCategory.get(cat.name);
if (!categoryRules || categoryRules.length === 0) continue;
doc += `## ${cat.section}. ${cat.name}\n\n`;
doc += `**Section Impact: ${cat.impact}**\n\n`;
for (let i = 0; i < categoryRules.length; i++) {
const rule = categoryRules[i];
const ruleNumber = `${cat.section}.${i + 1}`;
// Add rule header with number (anchor will be auto-generated as #11-title)
doc += `### ${ruleNumber} ${rule.frontmatter.title}\n\n`;
doc += `**Impact: ${rule.frontmatter.impact}** — ${rule.frontmatter.impactDescription}\n\n`;
// Add rule content (skip the first header since we already added it)
let ruleContent = rule.content;
// Remove the first h1 or h2 header if it matches the title
ruleContent = ruleContent.replace(/^#{1,2}\s+.*\n+/, '');
// Remove the impact line if present (we already added it)
ruleContent = ruleContent.replace(/^\*\*Impact:.*\*\*.*\n+/, '');
doc += ruleContent;
doc += '\n\n---\n\n';
}
}
// Add references footer
doc += `## References
`;
for (const ref of metadata.references) {
doc += `- ${ref}\n`;
}
doc += `
---
*Generated by build-agents.ts on ${new Date().toISOString().split('T')[0]}*
`;
return doc;
}
function main() {
console.log('Building AGENTS.md...\n');
const metadata = readMetadata();
console.log(`Version: ${metadata.version}`);
console.log(`Organization: ${metadata.organization}\n`);
const rules = readRules();
console.log(`Found ${rules.length} rules\n`);
// Count by category
const counts = new Map<string, number>();
for (const rule of rules) {
counts.set(rule.category, (counts.get(rule.category) || 0) + 1);
}
console.log('Rules by category:');
for (const cat of CATEGORIES) {
const count = counts.get(cat.name) || 0;
if (count > 0) {
console.log(` ${cat.name}: ${count}`);
}
}
console.log('');
const agentsMd = generateAgentsMd(rules, metadata);
const outputPath = path.join(__dirname, '..', 'AGENTS.md');
fs.writeFileSync(outputPath, agentsMd);
console.log(`Generated AGENTS.md (${agentsMd.length} bytes)`);
console.log(`Output: ${outputPath}`);
}
main();
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Build script for generating AGENTS.md
# Usage: ./build.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Check if ts-node is available
if command -v npx &> /dev/null; then
echo "Running build with ts-node..."
npx ts-node build-agents.ts
else
echo "Error: npx not found. Please install Node.js."
exit 1
fi
@@ -0,0 +1,237 @@
{
"name": "nestjs-best-practices-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nestjs-best-practices-scripts",
"version": "1.0.0",
"devDependencies": {
"@types/node": "^20.0.0",
"ts-node": "^10.9.0",
"typescript": "^5.0.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
}
}
}
@@ -0,0 +1,15 @@
{
"name": "nestjs-best-practices-scripts",
"version": "1.0.0",
"type": "module",
"description": "Build scripts for NestJS Best Practices skillset",
"scripts": {
"build": "npx ts-node build-agents.ts",
"build:watch": "npx nodemon --watch ../rules --ext md --exec 'npx ts-node build-agents.ts'"
},
"devDependencies": {
"typescript": "^5.0.0",
"ts-node": "^10.9.0",
"@types/node": "^20.0.0"
}
}