/** * BMAD Documentation Build Pipeline * * Consolidates docs from multiple sources, generates LLM-friendly files, * and builds the Astro+Starlight site. * * Build outputs: * build/artifacts/ - With llms.txt, llms-full.txt * build/site/ - Final Astro output (deployable) */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { getSiteUrl } from '../website/src/lib/site-url.mjs'; import { translatedLocales } from '../website/src/lib/locales.mjs'; // ============================================================================= // Configuration // ============================================================================= const PROJECT_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const BUILD_DIR = path.join(PROJECT_ROOT, 'build'); const REPO_URL = 'https://github.com/bmad-code-org/BMAD-METHOD'; // DO NOT CHANGE THESE VALUES! // llms-full.txt is consumed by AI agents as context. Most LLMs have ~200k token limits. // 600k chars ≈ 150k tokens (safe margin). Exceeding this breaks AI agent functionality. const LLM_MAX_CHARS = 600_000; const LLM_WARN_CHARS = 500_000; const LLM_EXCLUDE_PATTERNS = [ 'changelog', 'ide-info/', 'v4-to-v6-upgrade', 'faq', 'reference/glossary/', 'explanation/game-dev/', 'bmgd/', // Note: Files/dirs starting with _ (like _STYLE_GUIDE.md, _archive/) are excluded in shouldExcludeFromLlm() ]; // ============================================================================= // Main Entry Point /** * Orchestrates the full BMAD documentation build pipeline. * * Executes the high-level build steps in sequence: prints headers and paths, validates internal * documentation links, cleans the build directory, generates artifacts from the `docs/` folder, * builds the Astro site, and prints a final build summary. */ async function main() { if (process.platform === 'win32') { console.error('Error: The docs build pipeline does not support Windows.'); console.error('Please build on Linux, macOS, or WSL.'); process.exit(1); } console.log(); printBanner('BMAD Documentation Build Pipeline'); console.log(); console.log(`Project root: ${PROJECT_ROOT}`); console.log(`Build directory: ${BUILD_DIR}`); console.log(); // Check for broken internal links before building checkDocLinks(); cleanBuildDirectory(); const docsDir = path.join(PROJECT_ROOT, 'docs'); const artifactsDir = await generateArtifacts(docsDir); const siteDir = buildAstroSite(); printBuildSummary(docsDir, artifactsDir, siteDir); } main().catch((error) => { console.error(error); process.exit(1); }); // ============================================================================= // Pipeline Stages /** * Generate LLM files for the documentation pipeline. * * Creates the build/artifacts directory and writes `llms.txt` and `llms-full.txt` (sourced from the provided docs directory). * * @param {string} docsDir - Path to the source docs directory containing Markdown files. * @returns {string} Path to the created artifacts directory. */ async function generateArtifacts(docsDir) { printHeader('Generating LLM files'); 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); console.log(); console.log(` \u001B[32m✓\u001B[0m Artifact generation complete`); return outputDir; } /** * Builds the Astro + Starlight site and copies generated artifacts into the site output directory. * * @returns {string} The filesystem path to the built site directory (e.g., build/site). */ function buildAstroSite() { printHeader('Building Astro + Starlight site'); const siteDir = path.join(BUILD_DIR, 'site'); const artifactsDir = path.join(BUILD_DIR, 'artifacts'); // Build Astro site (outputs to build/site via astro.config.mjs) runAstroBuild(); copyArtifactsToSite(artifactsDir, siteDir); console.log(); console.log(` \u001B[32m✓\u001B[0m Astro build complete`); return siteDir; } // ============================================================================= // LLM File Generation /** * Create a concise llms.txt summary file containing project metadata, core links, and quick navigation entries for LLM consumption. * * Writes the file to `${outputDir}/llms.txt`. * * @param {string} outputDir - Destination directory where `llms.txt` will be written. */ function generateLlmsTxt(outputDir) { console.log(' → Generating llms.txt...'); const siteUrl = getSiteUrl(); const content = [ '# BMAD Method Documentation', '', '> AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.', '', `Documentation: ${siteUrl}`, `Repository: ${REPO_URL}`, `Full docs: ${siteUrl}/llms-full.txt`, '', '## Quick Start', '', `- **[Getting Started](${siteUrl}/tutorials/getting-started/)** - Tutorial: install and learn how BMad works`, `- **[Installation](${siteUrl}/how-to/install-bmad/)** - How to install BMad Method`, '', '## Core Concepts', '', `- **[Quick Flow](${siteUrl}/explanation/quick-flow/)** - Unified quick workflow — clarify intent, plan, implement, review, present`, `- **[Party Mode](${siteUrl}/explanation/party-mode/)** - Multi-agent collaboration`, `- **[Workflow Map](${siteUrl}/reference/workflow-map/)** - Visual overview of phases and workflows`, '', '## Modules', '', `- **[Official Modules](${siteUrl}/reference/modules/)** - BMM, BMB, BMGD, and more`, '', '---', '', '## Quick Links', '', `- [Full Documentation (llms-full.txt)](${siteUrl}/llms-full.txt) - Complete docs for AI context`, '', ].join('\n'); const outputPath = path.join(outputDir, 'llms.txt'); fs.writeFileSync(outputPath, content, 'utf-8'); console.log(` Generated llms.txt (${content.length.toLocaleString()} chars)`); } /** * Builds a consolidated llms-full.txt containing all Markdown files under docsDir wrapped in tags for LLM consumption. * * Writes the generated file to outputDir/llms-full.txt. Files matching LLM_EXCLUDE_PATTERNS are skipped; read errors for individual files are logged. The combined content is validated against configured size thresholds (will exit on overflow and warn if near limit). * @param {string} docsDir - Root directory containing source Markdown files; paths in the output are relative to this directory. * @param {string} outputDir - Directory where llms-full.txt will be written. */ function generateLlmsFullTxt(docsDir, outputDir) { console.log(' → Generating llms-full.txt...'); const date = new Date().toISOString().split('T')[0]; const files = getAllMarkdownFiles(docsDir).sort(compareLlmDocs); 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(``, content, '', ''); 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 compareLlmDocs(a, b) { const aKey = getLlmSortKey(a); const bKey = getLlmSortKey(b); if (aKey !== bKey) return aKey - bKey; return a.localeCompare(b); } function getLlmSortKey(filePath) { if (filePath === 'index.md') return 0; if (filePath.startsWith(`tutorials${path.sep}`) || filePath.startsWith('tutorials/')) return 2; if (filePath.startsWith(`how-to${path.sep}`) || filePath.startsWith('how-to/')) return 3; if (filePath.startsWith(`explanation${path.sep}`) || filePath.startsWith('explanation/')) return 4; if (filePath.startsWith(`reference${path.sep}`) || filePath.startsWith('reference/')) return 5; if (filePath.startsWith(`bmgd${path.sep}`) || filePath.startsWith('bmgd/')) return 6; return 7; } /** * Collects all Markdown (.md) files under a directory and returns their paths relative to a base directory. * @param {string} dir - Directory to search for Markdown files. * @param {string} [baseDir=dir] - Base directory used to compute returned relative paths. * @returns {string[]} An array of file paths (relative to `baseDir`) for every `.md` file found under `dir`. */ 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; } /** * Determine whether a file path matches any configured LLM exclusion pattern. * Also excludes any files or directories starting with underscore. * @param {string} filePath - The file path to test. * @returns {boolean} `true` if excluded, `false` otherwise. */ function shouldExcludeFromLlm(filePath) { // Exclude if ANY path component starts with underscore // (e.g., _STYLE_GUIDE.md, _archive/file.md, dir/_STYLE_GUIDE.md) const pathParts = filePath.split(path.sep); if (pathParts.some((part) => part.startsWith('_'))) return true; // Exclude non-root locale directories (translations duplicate English content) if (translatedLocales.some((locale) => filePath.startsWith(`${locale}/`) || filePath.startsWith(`${locale}${path.sep}`))) return true; // Check configured patterns 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`); } } // ============================================================================= // Astro Build /** * Builds the Astro site to build/site (configured in astro.config.mjs). */ function runAstroBuild() { console.log(' → Running astro build...'); execSync('npx astro build --root website', { cwd: PROJECT_ROOT, stdio: 'inherit', env: { ...process.env, }, }); } /** * Copy generated artifact files into the built site directory. * * Copies llms.txt and llms-full.txt from the artifacts directory into the site directory. * * @param {string} artifactsDir - Path to the build artifacts directory containing generated files. * @param {string} siteDir - Path to the target site directory where artifacts should be placed. */ 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')); } // ============================================================================= // Build Summary /** * Prints a concise end-of-build summary and displays a sample listing of the final site directory. * * @param {string} docsDir - Path to the source documentation directory used for the build. * @param {string} artifactsDir - Path to the directory containing generated artifacts (e.g., llms.txt). * @param {string} siteDir - Path to the final built site directory whose contents will be listed. */ 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}/`); } } } /** * Format a byte count into a compact human-readable string using B, K, or M units. * @param {number} bytes - The number of bytes to format. * @returns {string} The formatted size: bytes as `N B` (e.g. `512B`), kilobytes truncated to an integer with `K` (e.g. `2K`), or megabytes with one decimal and `M` (e.g. `1.2M`). */ 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 /** * Remove any existing build output and recreate the build directory. * * Ensures the configured BUILD_DIR is empty by deleting it if present and then creating a fresh directory. */ function cleanBuildDirectory() { console.log('Cleaning previous build...'); if (fs.existsSync(BUILD_DIR)) { fs.rmSync(BUILD_DIR, { recursive: true }); } fs.mkdirSync(BUILD_DIR, { recursive: true }); } // ============================================================================= // Console Output Formatting // ============================================================================= function printHeader(title) { console.log(); console.log('┌' + '─'.repeat(62) + '┐'); console.log(`│ ${title.padEnd(60)} │`); console.log('└' + '─'.repeat(62) + '┘'); } /** * Prints a centered decorative ASCII banner to the console using the provided title. * @param {string} title - Text to display centered inside the banner. */ function printBanner(title) { console.log('╔' + '═'.repeat(62) + '╗'); console.log(`║${title.padStart(31 + title.length / 2).padEnd(62)}║`); console.log('╚' + '═'.repeat(62) + '╝'); } // ============================================================================= // Link Checking /** * Verify internal documentation links by running the link-checking script. * * Executes the Node script tools/validate-doc-links.js from the project root and * exits the process with code 1 if the check fails. */ function checkDocLinks() { printHeader('Checking documentation links'); try { execSync('node tools/validate-doc-links.js', { cwd: PROJECT_ROOT, stdio: 'inherit', }); } catch { console.error('\n \u001B[31m✗\u001B[0m Link check failed - fix broken links before building\n'); process.exit(1); } }