410 lines
13 KiB
JavaScript
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) + '╝');
|
|
}
|