BMAD-METHOD/tools/build-docs.js

410 lines
13 KiB
JavaScript

/**
* BMAD Documentation Build Pipeline
*
* Consolidates docs from multiple sources, generates LLM-friendly files,
* creates downloadable bundles, and builds the Docusaurus site.
*
* Build outputs:
* build/consolidated/ - Merged docs from all sources
* build/artifacts/ - With llms.txt, llms-full.txt, ZIPs
* build/site/ - Final Docusaurus output (deployable)
*/
const { execSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const archiver = require('archiver');
// =============================================================================
// Configuration
// =============================================================================
const PROJECT_ROOT = path.dirname(__dirname);
const BUILD_DIR = path.join(PROJECT_ROOT, 'build');
const SITE_URL = process.env.SITE_URL || 'https://bmad-code-org.github.io/BMAD-METHOD';
const REPO_URL = 'https://github.com/bmad-code-org/BMAD-METHOD';
const LLM_MAX_CHARS = 600_000;
const LLM_WARN_CHARS = 500_000;
const LLM_EXCLUDE_PATTERNS = ['changelog', 'ide-info/', 'v4-to-v6-upgrade', 'downloads/', 'faq'];
// =============================================================================
// Main Entry Point
// =============================================================================
async function main() {
console.log();
printBanner('BMAD Documentation Build Pipeline');
console.log();
console.log(`Project root: ${PROJECT_ROOT}`);
console.log(`Build directory: ${BUILD_DIR}`);
console.log();
cleanBuildDirectory();
const docsDir = path.join(PROJECT_ROOT, 'docs');
const artifactsDir = await generateArtifacts(docsDir);
const siteDir = buildDocusaurusSite();
printBuildSummary(docsDir, artifactsDir, siteDir);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
// =============================================================================
// Pipeline Stages
// =============================================================================
async function generateArtifacts(docsDir) {
printHeader('Generating LLM files and download bundles');
const outputDir = path.join(BUILD_DIR, 'artifacts');
fs.mkdirSync(outputDir, { recursive: true });
// Generate LLM files reading from docs/, output to artifacts/
generateLlmsTxt(outputDir);
generateLlmsFullTxt(docsDir, outputDir);
await generateDownloadBundles(outputDir);
console.log();
console.log(` \u001B[32m✓\u001B[0m Artifact generation complete`);
return outputDir;
}
function buildDocusaurusSite() {
printHeader('Building Docusaurus site');
const siteDir = path.join(BUILD_DIR, 'site');
const artifactsDir = path.join(BUILD_DIR, 'artifacts');
// Build directly from docs/ - no backup/restore needed
runDocusaurusBuild(siteDir);
copyArtifactsToSite(artifactsDir, siteDir);
console.log();
console.log(` \u001B[32m✓\u001B[0m Docusaurus build complete`);
return siteDir;
}
// =============================================================================
// LLM File Generation
// =============================================================================
function generateLlmsTxt(outputDir) {
console.log(' → Generating llms.txt...');
const content = [
'# BMAD Method Documentation',
'',
'> AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.',
'',
`Documentation: ${SITE_URL}`,
`Repository: ${REPO_URL}`,
`Full docs: ${SITE_URL}/llms-full.txt`,
'',
'## Quick Start',
'',
`- **[Quick Start](${SITE_URL}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`,
`- **[Installation](${SITE_URL}/docs/getting-started/installation)** - Installation guide`,
'',
'## Core Concepts',
'',
`- **[Scale Adaptive System](${SITE_URL}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`,
`- **[Quick Flow](${SITE_URL}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`,
`- **[Party Mode](${SITE_URL}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`,
'',
'## Modules',
'',
`- **[BMM - Method](${SITE_URL}/docs/modules/bmm/quick-start)** - Core methodology module`,
`- **[BMB - Builder](${SITE_URL}/docs/modules/bmb/)** - Agent and workflow builder`,
`- **[BMGD - Game Dev](${SITE_URL}/docs/modules/bmgd/quick-start)** - Game development module`,
'',
'---',
'',
'## Quick Links',
'',
`- [Full Documentation (llms-full.txt)](${SITE_URL}/llms-full.txt) - Complete docs for AI context`,
`- [Source Bundle](${SITE_URL}/downloads/bmad-sources.zip) - Complete source code`,
`- [Prompts Bundle](${SITE_URL}/downloads/bmad-prompts.zip) - Agent prompts and workflows`,
'',
].join('\n');
const outputPath = path.join(outputDir, 'llms.txt');
fs.writeFileSync(outputPath, content, 'utf-8');
console.log(` Generated llms.txt (${content.length.toLocaleString()} chars)`);
}
function generateLlmsFullTxt(docsDir, outputDir) {
console.log(' → Generating llms-full.txt...');
const date = new Date().toISOString().split('T')[0];
const files = getAllMarkdownFiles(docsDir);
const output = [
'# BMAD Method Documentation (Full)',
'',
'> Complete documentation for AI consumption',
`> Generated: ${date}`,
`> Repository: ${REPO_URL}`,
'',
];
let fileCount = 0;
let skippedCount = 0;
for (const mdPath of files) {
if (shouldExcludeFromLlm(mdPath)) {
skippedCount++;
continue;
}
const fullPath = path.join(docsDir, mdPath);
try {
const content = readMarkdownContent(fullPath);
output.push(`<document path="${mdPath}">`, content, '</document>', '');
fileCount++;
} catch (error) {
console.error(` Warning: Could not read ${mdPath}: ${error.message}`);
}
}
const result = output.join('\n');
validateLlmSize(result);
const outputPath = path.join(outputDir, 'llms-full.txt');
fs.writeFileSync(outputPath, result, 'utf-8');
const tokenEstimate = Math.floor(result.length / 4).toLocaleString();
console.log(
` Processed ${fileCount} files (skipped ${skippedCount}), ${result.length.toLocaleString()} chars (~${tokenEstimate} tokens)`,
);
}
function getAllMarkdownFiles(dir, baseDir = dir) {
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...getAllMarkdownFiles(fullPath, baseDir));
} else if (entry.name.endsWith('.md')) {
// Return relative path from baseDir
const relativePath = path.relative(baseDir, fullPath);
files.push(relativePath);
}
}
return files;
}
function shouldExcludeFromLlm(filePath) {
return LLM_EXCLUDE_PATTERNS.some((pattern) => filePath.includes(pattern));
}
function readMarkdownContent(filePath) {
let content = fs.readFileSync(filePath, 'utf-8');
if (content.startsWith('---')) {
const end = content.indexOf('---', 3);
if (end !== -1) {
content = content.slice(end + 3).trim();
}
}
return content;
}
function validateLlmSize(content) {
const charCount = content.length;
if (charCount > LLM_MAX_CHARS) {
console.error(` ERROR: Exceeds ${LLM_MAX_CHARS.toLocaleString()} char limit`);
process.exit(1);
} else if (charCount > LLM_WARN_CHARS) {
console.warn(` \u001B[33mWARNING: Approaching ${LLM_WARN_CHARS.toLocaleString()} char limit\u001B[0m`);
}
}
// =============================================================================
// Download Bundle Generation
// =============================================================================
async function generateDownloadBundles(outputDir) {
console.log(' → Generating download bundles...');
const downloadsDir = path.join(outputDir, 'downloads');
fs.mkdirSync(downloadsDir, { recursive: true });
await generateSourcesBundle(downloadsDir);
await generatePromptsBundle(downloadsDir);
}
async function generateSourcesBundle(downloadsDir) {
const srcDir = path.join(PROJECT_ROOT, 'src');
if (!fs.existsSync(srcDir)) return;
const zipPath = path.join(downloadsDir, 'bmad-sources.zip');
await createZipArchive(srcDir, zipPath, ['__pycache__', '.pyc', '.DS_Store', 'node_modules']);
const size = (fs.statSync(zipPath).size / 1024 / 1024).toFixed(1);
console.log(` bmad-sources.zip (${size}M)`);
}
async function generatePromptsBundle(downloadsDir) {
const modulesDir = path.join(PROJECT_ROOT, 'src', 'modules');
if (!fs.existsSync(modulesDir)) return;
const zipPath = path.join(downloadsDir, 'bmad-prompts.zip');
await createZipArchive(modulesDir, zipPath, ['docs', '.DS_Store', '__pycache__', 'node_modules']);
const size = Math.floor(fs.statSync(zipPath).size / 1024);
console.log(` bmad-prompts.zip (${size}K)`);
}
// =============================================================================
// Docusaurus Build
// =============================================================================
function runDocusaurusBuild(siteDir) {
console.log(' → Running docusaurus build...');
execSync('npx docusaurus build --config website/docusaurus.config.js --out-dir ' + siteDir, {
cwd: PROJECT_ROOT,
stdio: 'inherit',
});
}
function copyArtifactsToSite(artifactsDir, siteDir) {
console.log(' → Copying artifacts to site...');
fs.copyFileSync(path.join(artifactsDir, 'llms.txt'), path.join(siteDir, 'llms.txt'));
fs.copyFileSync(path.join(artifactsDir, 'llms-full.txt'), path.join(siteDir, 'llms-full.txt'));
const downloadsDir = path.join(artifactsDir, 'downloads');
if (fs.existsSync(downloadsDir)) {
copyDirectory(downloadsDir, path.join(siteDir, 'downloads'));
}
}
// =============================================================================
// Build Summary
// =============================================================================
function printBuildSummary(docsDir, artifactsDir, siteDir) {
console.log();
printBanner('Build Complete!');
console.log();
console.log('Build artifacts:');
console.log(` Source docs: ${docsDir}`);
console.log(` Generated files: ${artifactsDir}`);
console.log(` Final site: ${siteDir}`);
console.log();
console.log(`Deployable output: ${siteDir}/`);
console.log();
listDirectoryContents(siteDir);
}
function listDirectoryContents(dir) {
const entries = fs.readdirSync(dir).slice(0, 15);
for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
const sizeStr = formatFileSize(stat.size);
console.log(` ${entry.padEnd(40)} ${sizeStr.padStart(8)}`);
} else {
console.log(` ${entry}/`);
}
}
}
function formatFileSize(bytes) {
if (bytes > 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(1)}M`;
} else if (bytes > 1024) {
return `${Math.floor(bytes / 1024)}K`;
}
return `${bytes}B`;
}
// =============================================================================
// File System Utilities
// =============================================================================
function cleanBuildDirectory() {
console.log('Cleaning previous build...');
if (fs.existsSync(BUILD_DIR)) {
fs.rmSync(BUILD_DIR, { recursive: true });
}
fs.mkdirSync(BUILD_DIR, { recursive: true });
}
function copyDirectory(src, dest, exclude = []) {
if (!fs.existsSync(src)) return false;
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
if (exclude.includes(entry.name)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirectory(srcPath, destPath, exclude);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
return true;
}
function createZipArchive(sourceDir, outputPath, exclude = []) {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', resolve);
archive.on('error', reject);
archive.pipe(output);
const baseName = path.basename(sourceDir);
archive.directory(sourceDir, baseName, (entry) => {
for (const pattern of exclude) {
if (entry.name.includes(pattern)) return false;
}
return entry;
});
archive.finalize();
});
}
// =============================================================================
// Console Output Formatting
// =============================================================================
function printHeader(title) {
console.log();
console.log('┌' + '─'.repeat(62) + '┐');
console.log(`${title.padEnd(60)}`);
console.log('└' + '─'.repeat(62) + '┘');
}
function printBanner(title) {
console.log('╔' + '═'.repeat(62) + '╗');
console.log(`${title.padStart(31 + title.length / 2).padEnd(62)}`);
console.log('╚' + '═'.repeat(62) + '╝');
}