#!/usr/bin/env npx tsx /** * Lume Documentation Generator * * Generates MDX documentation files from Lume's dump-docs command output. * This ensures documentation stays synchronized with the source code. * * Usage: * npx tsx scripts/docs-generators/lume.ts # Generate docs * npx tsx scripts/docs-generators/lume.ts --check # Check for drift (CI mode) */ import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; // ============================================================================ // Types // ============================================================================ export interface CLIDocumentation { name: string; version: string; abstract: string; commands: CommandDoc[]; } export interface CommandDoc { name: string; abstract: string; discussion?: string; arguments: ArgumentDoc[]; options: OptionDoc[]; flags: FlagDoc[]; subcommands: CommandDoc[]; } export interface ArgumentDoc { name: string; help: string; type: string; is_optional: boolean; } export interface OptionDoc { name: string; short_name?: string; help: string; type: string; default_value?: string; is_optional: boolean; } export interface FlagDoc { name: string; short_name?: string; help: string; default_value: boolean; } export interface HTTPAPIDocumentation { base_path: string; version: string; description: string; endpoints: APIEndpointDoc[]; } export interface APIEndpointDoc { method: string; path: string; description: string; category: string; path_parameters: APIParameterDoc[]; query_parameters: APIParameterDoc[]; request_body?: APIRequestBodyDoc; response_body: APIResponseDoc; status_codes: APIStatusCodeDoc[]; } export interface APIParameterDoc { name: string; type: string; required: boolean; description: string; } export interface APIRequestBodyDoc { content_type: string; description: string; fields: APIFieldDoc[]; } export interface APIResponseDoc { content_type: string; description: string; fields?: APIFieldDoc[]; } export interface APIFieldDoc { name: string; type: string; required: boolean; description: string; default_value?: string; } export interface APIStatusCodeDoc { code: number; description: string; } // ============================================================================ // Configuration // ============================================================================ const ROOT_DIR = path.resolve(__dirname, '../..'); const LUME_DIR = path.join(ROOT_DIR, 'libs', 'lume'); const DOCS_OUTPUT_DIR = path.join(ROOT_DIR, 'docs', 'content', 'docs', 'reference', 'lume'); const TAG_PREFIX = 'lume-v'; // ============================================================================ // Version Discovery // ============================================================================ interface VersionInfo { version: string; href: string; isCurrent: boolean; } /** * Get the latest released version from git tags. */ export function getLatestReleasedVersion(): string { try { const output = execSync(`git tag | grep "^${TAG_PREFIX}" | sort -V | tail -1`, { encoding: 'utf-8', cwd: ROOT_DIR, }).trim(); if (output) { return output.replace(TAG_PREFIX, ''); } } catch { // Fall through } return '0.0.0'; } /** * Discover available versioned doc folders and build version list. */ export function discoverVersions(currentVersion: string): VersionInfo[] { const versions: VersionInfo[] = []; const currentMajorMinor = currentVersion.split('.').slice(0, 2).join('.'); // Add current version (latest) versions.push({ version: currentMajorMinor, href: '/reference/lume/cli-reference', isCurrent: true, }); // Discover versioned folders (v0.2, v0.1, etc.) if (fs.existsSync(DOCS_OUTPUT_DIR)) { const entries = fs.readdirSync(DOCS_OUTPUT_DIR, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('v')) { const version = entry.name.substring(1); if (version === currentMajorMinor) continue; versions.push({ version, href: `/reference/lume/${entry.name}/cli-reference`, isCurrent: false, }); } } } // Sort descending versions.sort((a, b) => { const partsA = a.version.split('.').map((x) => parseInt(x, 10) || 0); const partsB = b.version.split('.').map((x) => parseInt(x, 10) || 0); for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { const partA = partsA[i] || 0; const partB = partsB[i] || 0; if (partA !== partB) return partB - partA; } return 0; }); return versions; } // ============================================================================ // Main // ============================================================================ async function main() { const args = process.argv.slice(2); const checkOnly = args.includes('--check') || args.includes('--check-only'); console.log('šŸ”§ Lume Documentation Generator'); console.log('================================\n'); // Step 1: Build Lume if needed console.log('šŸ“¦ Building Lume...'); try { execSync('swift build -c release', { cwd: LUME_DIR, stdio: 'inherit', }); } catch (error) { console.error('āŒ Failed to build Lume'); process.exit(1); } // Step 2: Get CLI documentation console.log('\nšŸ“– Extracting CLI documentation...'); const cliDocsJson = execSync('.build/release/lume dump-docs --type cli', { cwd: LUME_DIR, encoding: 'utf-8', }); const cliDocs: CLIDocumentation = JSON.parse(cliDocsJson); console.log(` Found ${cliDocs.commands.length} commands`); // Step 3: Get API documentation console.log('šŸ“– Extracting API documentation...'); const apiDocsJson = execSync('.build/release/lume dump-docs --type api', { cwd: LUME_DIR, encoding: 'utf-8', }); const apiDocs: HTTPAPIDocumentation = JSON.parse(apiDocsJson); console.log(` Found ${apiDocs.endpoints.length} endpoints`); // Step 4: Generate MDX files console.log('\nšŸ“ Generating documentation files...'); const cliMdx = generateCLIReferenceMDX(cliDocs); const apiMdx = generateHTTPAPIMDX(apiDocs); const cliPath = path.join(DOCS_OUTPUT_DIR, 'cli-reference.mdx'); const apiPath = path.join(DOCS_OUTPUT_DIR, 'http-api.mdx'); if (checkOnly) { // Check mode: compare with existing files console.log('\nšŸ” Checking for documentation drift...'); let hasDrift = false; if (fs.existsSync(cliPath)) { const existingCli = fs.readFileSync(cliPath, 'utf-8'); if (existingCli !== cliMdx) { console.error('āŒ cli-reference.mdx is out of sync with source code'); hasDrift = true; } else { console.log('āœ… cli-reference.mdx is up to date'); } } else { console.error('āŒ cli-reference.mdx does not exist'); hasDrift = true; } if (fs.existsSync(apiPath)) { const existingApi = fs.readFileSync(apiPath, 'utf-8'); if (existingApi !== apiMdx) { console.error('āŒ http-api.mdx is out of sync with source code'); hasDrift = true; } else { console.log('āœ… http-api.mdx is up to date'); } } else { console.error('āŒ http-api.mdx does not exist'); hasDrift = true; } if (hasDrift) { console.error("\nšŸ’” Run 'npx tsx scripts/docs-generators/lume.ts' to update documentation"); process.exit(1); } console.log('\nāœ… All Lume documentation is up to date!'); } else { // Generate mode: write files fs.writeFileSync(cliPath, cliMdx); console.log(` āœ… Generated ${path.relative(ROOT_DIR, cliPath)}`); fs.writeFileSync(apiPath, apiMdx); console.log(` āœ… Generated ${path.relative(ROOT_DIR, apiPath)}`); console.log('\nāœ… Lume documentation generated successfully!'); } } // ============================================================================ // CLI Reference Generator // ============================================================================ export function generateCLIReferenceMDX(docs: CLIDocumentation): string { const lines: string[] = []; const documentedVersion = docs.version || getLatestReleasedVersion(); // Header - frontmatter MUST be at the very beginning of the file lines.push('---'); lines.push('title: CLI Reference'); lines.push('description: Command Line Interface reference for Lume'); lines.push('---'); lines.push(''); lines.push(`{/* AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY Generated by: npx tsx scripts/docs-generators/lume.ts Source: lume dump-docs --type cli Version: ${documentedVersion} */}`); lines.push(''); lines.push(`${docs.abstract}`); lines.push(''); lines.push( `Documented against Lume **${documentedVersion}**. Run \`lume --version\` for your installed version.` ); lines.push(''); lines.push('For installation steps, see [Install Lume](/how-to-guides/lume/install-lume).'); lines.push(''); const groups = [ { title: 'VM Management', commands: ['create', 'run', 'stop', 'delete', 'clone'] }, { title: 'VM Information and Configuration', commands: ['ls', 'get', 'set'] }, { title: 'Image Management', commands: ['images', 'pull', 'push', 'convert', 'ipsw', 'prune'], }, { title: 'Guest Access and Security', commands: ['ssh', 'setup', 'sip'] }, { title: 'Configuration and Server', commands: ['config', 'serve', 'logs', 'check-update', 'update'], }, { title: 'Developer Tools', commands: ['dump-docs'] }, ]; const assignedCommands = groups.flatMap((group) => group.commands); const duplicateAssignments = assignedCommands.filter( (name, index) => assignedCommands.indexOf(name) !== index ); const ungroupedCommands = docs.commands .map((command) => command.name) .filter((name) => !assignedCommands.includes(name)); if (duplicateAssignments.length > 0 || ungroupedCommands.length > 0) { throw new Error( `CLI command grouping mismatch. Ungrouped: ${[...new Set(ungroupedCommands)].sort().join(', ') || 'none'}; duplicated: ${[...new Set(duplicateAssignments)].sort().join(', ') || 'none'}` ); } for (const group of groups) { lines.push(`## ${group.title}`); lines.push(''); for (const commandName of group.commands) { const command = docs.commands.find((candidate) => candidate.name === commandName); if (command) { lines.push(...generateCommandDoc(command, '###')); } } } // Global options lines.push('## Global Options'); lines.push(''); lines.push('These options are available for all commands:'); lines.push(''); lines.push('- `--help` - Show help information'); lines.push('- `--version` - Show version number'); lines.push(''); return lines.join('\n'); } export function generateCommandDoc(cmd: CommandDoc, heading: string): string[] { const lines: string[] = []; lines.push(`${heading} lume ${cmd.name}`); lines.push(''); lines.push(cmd.abstract); lines.push(''); // Arguments if (cmd.arguments.length > 0) { lines.push('**Arguments:**'); lines.push(''); lines.push('| Name | Type | Required | Description |'); lines.push('| ---- | ---- | -------- | ----------- |'); for (const arg of cmd.arguments) { const required = arg.is_optional ? 'No' : 'Yes'; lines.push(`| \`<${arg.name}>\` | ${arg.type} | ${required} | ${arg.help} |`); } lines.push(''); } // Options if (cmd.options.length > 0) { lines.push('**Options:**'); lines.push(''); lines.push('| Name | Type | Default | Description |'); lines.push('| ---- | ---- | ------- | ----------- |'); for (const opt of cmd.options) { const shortFlag = opt.short_name ? `-${opt.short_name}, ` : ''; const defaultVal = opt.default_value || '-'; lines.push(`| \`${shortFlag}--${opt.name}\` | ${opt.type} | ${defaultVal} | ${opt.help} |`); } lines.push(''); } // Flags if (cmd.flags.length > 0) { lines.push('**Flags:**'); lines.push(''); lines.push('| Name | Default | Description |'); lines.push('| ---- | ------- | ----------- |'); for (const flag of cmd.flags) { const shortFlag = flag.short_name ? `-${flag.short_name}, ` : ''; lines.push(`| \`${shortFlag}--${flag.name}\` | ${flag.default_value} | ${flag.help} |`); } lines.push(''); } // Subcommands if (cmd.subcommands.length > 0) { lines.push('**Subcommands:**'); lines.push(''); for (const sub of cmd.subcommands) { lines.push(`- \`lume ${cmd.name} ${sub.name}\` - ${sub.abstract}`); // Show subcommand details if (sub.arguments.length > 0 || sub.options.length > 0) { for (const arg of sub.arguments) { lines.push(` - \`<${arg.name}>\` - ${arg.help}`); } for (const opt of sub.options) { const shortFlag = opt.short_name ? `-${opt.short_name}, ` : ''; lines.push(` - \`${shortFlag}--${opt.name}\` - ${opt.help}`); } } // Nested subcommands if (sub.subcommands.length > 0) { for (const nested of sub.subcommands) { lines.push(` - \`lume ${cmd.name} ${sub.name} ${nested.name}\` - ${nested.abstract}`); } } } lines.push(''); } return lines; } // ============================================================================ // HTTP API Reference Generator // ============================================================================ export function generateHTTPAPIMDX(docs: HTTPAPIDocumentation): string { const lines: string[] = []; const documentedVersion = docs.version || getLatestReleasedVersion(); // Header - frontmatter MUST be at the very beginning of the file lines.push('---'); lines.push('title: API Reference'); lines.push('description: HTTP API reference for Lume server'); lines.push('---'); lines.push(''); lines.push(`{/* AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY Generated by: npx tsx scripts/docs-generators/lume.ts Source: lume dump-docs --type api Version: ${documentedVersion} */}`); lines.push(''); lines.push("import { Tabs, Tab } from 'fumadocs-ui/components/tabs';"); lines.push(''); // Introduction lines.push(docs.description); lines.push(''); lines.push( `Documented against Lume **${documentedVersion}**. Run \`lume --version\` for your installed version.` ); lines.push(''); lines.push('## Default URL'); lines.push(''); lines.push('```'); lines.push('http://localhost:7777'); lines.push('```'); lines.push(''); lines.push( 'Start the server with `lume serve` or specify a custom port with `lume serve --port `.' ); lines.push(''); // Group endpoints by category const categories = [...new Set(docs.endpoints.map((e) => e.category))]; for (const category of categories) { lines.push(`## ${category}`); lines.push(''); const categoryEndpoints = docs.endpoints.filter((e) => e.category === category); for (const endpoint of categoryEndpoints) { lines.push(...generateEndpointDoc(endpoint)); } } return lines.join('\n'); } export function generateEndpointDoc(endpoint: APIEndpointDoc): string[] { const lines: string[] = []; lines.push(`### ${endpoint.description}`); lines.push(''); lines.push(endpoint.description); lines.push(''); lines.push(`\`${endpoint.method}: ${endpoint.path}\``); lines.push(''); // Parameters table (path + query) const allParams = [ ...endpoint.path_parameters.map((p) => ({ ...p, location: 'path' })), ...endpoint.query_parameters.map((p) => ({ ...p, location: 'query' })), ]; if (allParams.length > 0) { lines.push('#### Parameters'); lines.push(''); lines.push('| Name | Type | Required | Description |'); lines.push('| ---- | ---- | -------- | ----------- |'); for (const param of allParams) { const required = param.required ? 'Yes' : 'No'; lines.push(`| ${param.name} | ${param.type} | ${required} | ${param.description} |`); } lines.push(''); } // Request body if (endpoint.request_body) { lines.push('#### Request Body'); lines.push(''); lines.push('| Name | Type | Required | Description |'); lines.push('| ---- | ---- | -------- | ----------- |'); for (const field of endpoint.request_body.fields) { const required = field.required ? 'Yes' : 'No'; const defaultStr = field.default_value ? ` (default: ${field.default_value})` : ''; lines.push( `| ${field.name} | ${field.type} | ${required} | ${field.description}${defaultStr} |` ); } lines.push(''); } // Example request lines.push('#### Example Request'); lines.push(''); lines.push(""); // Generate curl example lines.push(' '); lines.push('```bash'); lines.push(generateCurlExample(endpoint)); lines.push('```'); lines.push(' '); // Generate Python example lines.push(' '); lines.push('```python'); lines.push(generatePythonExample(endpoint)); lines.push('```'); lines.push(' '); // Generate TypeScript example lines.push(' '); lines.push('```typescript'); lines.push(generateTypeScriptExample(endpoint)); lines.push('```'); lines.push(' '); lines.push(''); lines.push(''); // Status codes lines.push('#### Response'); lines.push(''); for (const status of endpoint.status_codes) { lines.push(`- **${status.code}**: ${status.description}`); } lines.push(''); lines.push('---'); lines.push(''); return lines; } function generateCurlExample(endpoint: APIEndpointDoc): string { const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name)); const url = `http://localhost:7777${path}`; if (endpoint.method === 'GET' || endpoint.method === 'DELETE') { if (endpoint.method === 'DELETE') { return `curl -X DELETE "${url}"`; } return `curl "${url}"`; } // POST/PATCH with body if (endpoint.request_body && endpoint.request_body.fields.length > 0) { const bodyObj: Record = {}; for (const field of endpoint.request_body.fields) { if (field.required) { bodyObj[field.name] = getExampleValue(field); } } const bodyJson = JSON.stringify(bodyObj, null, 2); return `curl -X ${endpoint.method} "http://localhost:7777${path}" \\ -H "Content-Type: application/json" \\ -d '${bodyJson}'`; } return `curl -X ${endpoint.method} "${url}"`; } function generatePythonExample(endpoint: APIEndpointDoc): string { const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name)); const method = endpoint.method.toLowerCase(); const lines: string[] = []; lines.push('import requests'); lines.push(''); if ( endpoint.request_body && endpoint.request_body.fields.length > 0 && (endpoint.method === 'POST' || endpoint.method === 'PATCH') ) { lines.push('data = {'); for (const field of endpoint.request_body.fields) { if (field.required) { const value = getExampleValuePython(field); lines.push(` "${field.name}": ${value},`); } } lines.push('}'); lines.push(''); lines.push(`response = requests.${method}("http://localhost:7777${path}", json=data)`); } else { lines.push(`response = requests.${method}("http://localhost:7777${path}")`); } lines.push('print(response.json())'); return lines.join('\n'); } function generateTypeScriptExample(endpoint: APIEndpointDoc): string { const path = endpoint.path.replace(/:(\w+)/g, (_, name) => getExamplePathValue(name)); const lines: string[] = []; if ( endpoint.request_body && endpoint.request_body.fields.length > 0 && (endpoint.method === 'POST' || endpoint.method === 'PATCH') ) { lines.push(`const response = await fetch(\`http://localhost:7777${path}\`, {`); lines.push(` method: "${endpoint.method}",`); lines.push(' headers: { "Content-Type": "application/json" },'); lines.push(' body: JSON.stringify({'); for (const field of endpoint.request_body.fields) { if (field.required) { const value = getExampleValueTS(field); lines.push(` ${field.name}: ${value},`); } } lines.push(' }),'); lines.push('});'); } else if (endpoint.method === 'DELETE') { lines.push(`const response = await fetch(\`http://localhost:7777${path}\`, {`); lines.push(` method: "DELETE",`); lines.push('});'); } else { lines.push(`const response = await fetch(\`http://localhost:7777${path}\`);`); } lines.push('const data = await response.json();'); return lines.join('\n'); } function getExampleValue(field: APIFieldDoc): unknown { switch (field.type) { case 'string': if (field.name === 'name') return 'my-vm'; if (field.name === 'os') return 'macOS'; if (field.name === 'memory') return '8GB'; if (field.name === 'diskSize') return '50GB'; if (field.name === 'display') return '1024x768'; if (field.name === 'image') return 'macos-tahoe-vanilla:latest'; if (field.name === 'path') return '/path/to/storage'; return 'example'; case 'integer': if (field.name === 'cpu') return 4; return 1; case 'boolean': return false; case 'array': if (field.name === 'tags') return ['latest']; return []; default: return 'value'; } } function getExampleValuePython(field: APIFieldDoc): string { const val = getExampleValue(field); if (typeof val === 'string') return `"${val}"`; if (typeof val === 'boolean') return val ? 'True' : 'False'; if (Array.isArray(val)) return JSON.stringify(val); return String(val); } function getExampleValueTS(field: APIFieldDoc): string { const val = getExampleValue(field); if (typeof val === 'string') return `"${val}"`; if (Array.isArray(val)) return JSON.stringify(val); return String(val); } function getExamplePathValue(name: string): string { if (name === 'name') return 'my-vm'; if (name === 'id') return 'example-id'; return `example-${name}`; } // ============================================================================ // Run // ============================================================================ main().catch((error) => { console.error('Error:', error); process.exit(1); });