// generate-cli-docs.js const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const OUTPUT_DIR = path.join(__dirname, 'content', 'shared', 'influxdb3-cli'); const BASE_CMD = 'influxdb3'; const DEBUG = true; // Set to true for verbose logging // Debug logging function function debug(message, data) { if (DEBUG) { console.log(`DEBUG: ${message}`); if (data) console.log(JSON.stringify(data, null, 2)); } } // Function to remove ANSI escape codes function stripAnsiCodes(str) { // Regular expression to match ANSI escape codes // eslint-disable-next-line no-control-regex return str.replace(/[›][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); } // Ensure output directories exist function ensureDirectoryExistence(filePath) { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { return true; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); } // Get all available commands and subcommands function getCommands() { try { debug('Getting base commands'); let helpOutput = execSync(`${BASE_CMD} --help`).toString(); helpOutput = stripAnsiCodes(helpOutput); // Strip ANSI codes debug('Cleaned help output received', helpOutput); // Find all command sections (Common Commands, Resource Management, etc.) const commandSections = helpOutput.match(/^[A-Za-z\s]+:\s*$([\s\S]+?)(?=^[A-Za-z\s]+:\s*$|\n\s*$|\n[A-Z]|\n\n|$)/gm); if (!commandSections || commandSections.length === 0) { debug('No command sections found in help output'); return []; } debug(`Found ${commandSections.length} command sections`); let commands = []; // Process each section to extract commands commandSections.forEach(section => { // Extract command lines (ignoring section headers) const cmdLines = section.split('\n') .slice(1) // Skip the section header .map(line => line.trim()) .filter(line => line && !line.startsWith('-') && !line.startsWith('#')); // Skip empty lines, flags and comments debug('Command lines in section', cmdLines); // Extract command names and descriptions cmdLines.forEach(line => { // Handle commands with aliases (like "query, q") const aliasMatch = line.match(/^\s*([a-zA-Z0-9_,-\s]+?)\s{2,}(.+)$/); if (aliasMatch) { // Get primary command and any aliases const commandParts = aliasMatch[1].split(',').map(cmd => cmd.trim()); const primaryCmd = commandParts[0]; // Use the first as primary const description = aliasMatch[2].trim(); commands.push({ cmd: primaryCmd, description: description }); debug(`Added command: ${primaryCmd} - ${description}`); } }); }); debug('Extracted commands', commands); return commands; } catch (error) { console.error('Error getting commands:', error.message); if (DEBUG) console.error(error.stack); return []; } } // Get subcommands for a specific command function getSubcommands(cmd) { try { debug(`Getting subcommands for: ${cmd}`); let helpOutput = execSync(`${BASE_CMD} ${cmd} --help`).toString(); helpOutput = stripAnsiCodes(helpOutput); // Strip ANSI codes debug(`Cleaned help output for ${cmd} received`, helpOutput); // Look for sections containing commands (similar to top-level help) // First try to find a dedicated Commands: section let subcommands = []; // Try to find a dedicated "Commands:" section first const commandsMatch = helpOutput.match(/Commands:\s+([\s\S]+?)(?=^[A-Za-z\s]+:\s*$|\n\s*$|\n[A-Z]|\n\n|$)/m); if (commandsMatch) { debug(`Found dedicated Commands section for ${cmd}`); const cmdLines = commandsMatch[1].split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('-') && !line.startsWith('#')); // Skip empty lines, flags, comments cmdLines.forEach(line => { const match = line.match(/^\s*([a-zA-Z0-9_,-\s]+?)\s{2,}(.+)$/); if (match) { // Get primary command name (before any commas for aliases) const commandName = match[1].split(',')[0].trim(); const description = match[2].trim(); subcommands.push({ cmd: `${cmd} ${commandName}`, description: description }); debug(`Added subcommand: ${cmd} ${commandName} - ${description}`); } }); } else { // Look for sections like "Common Commands:", "Resource Management:", etc. const sectionMatches = helpOutput.match(/^[A-Za-z\s]+:\s*$([\s\S]+?)(?=^[A-Za-z\s]+:\s*$|\n\s*$|\n[A-Z]|\n\n|$)/gm); if (sectionMatches) { debug(`Found ${sectionMatches.length} sections with potential commands for ${cmd}`); sectionMatches.forEach(section => { const cmdLines = section.split('\n') .slice(1) // Skip the section header .map(line => line.trim()) .filter(line => line && !line.startsWith('-') && !line.startsWith('#')); // Skip empty lines, flags, comments cmdLines.forEach(line => { const match = line.match(/^\s*([a-zA-Z0-9_,-\s]+?)\s{2,}(.+)$/); if (match) { // Get primary command name (before any commas for aliases) const commandName = match[1].split(',')[0].trim(); const description = match[2].trim(); subcommands.push({ cmd: `${cmd} ${commandName}`, description: description }); debug(`Added subcommand from section: ${cmd} ${commandName} - ${description}`); } }); }); } } debug(`Extracted ${subcommands.length} subcommands for ${cmd}`, subcommands); return subcommands; } catch (error) { debug(`Error getting subcommands for ${cmd}:`, error.message); return []; } } // Helper functions to generate descriptions for different command types function getQueryDescription(cmd, fullCmd) { return ` executes a query against a running {{< product-name >}} server.`; } function getWriteDescription(cmd, fullCmd) { return ` writes data to a running {{< product-name >}} server.`; } function getShowDescription(cmd, fullCmd) { const cmdParts = cmd.split(' '); const resourceType = cmdParts.length > 1 ? cmdParts[1] : 'resources'; return ` lists ${resourceType} in your {{< product-name >}} server.`; } function getCreateDescription(cmd, fullCmd) { const cmdParts = cmd.split(' '); const createType = cmdParts.length > 1 ? cmdParts[1] : 'resources'; return ` creates ${createType} in your {{< product-name >}} server.`; } function getDeleteDescription(cmd, fullCmd) { const cmdParts = cmd.split(' '); const deleteType = cmdParts.length > 1 ? cmdParts[1] : 'resources'; return ` deletes ${deleteType} from your {{< product-name >}} server.`; } function getServeDescription(cmd, fullCmd) { return ` starts the {{< product-name >}} server.`; } function getDefaultDescription(cmd, fullCmd) { return `.`; } // Helper functions to generate examples for different command types function getQueryExample(cmd) { return { title: 'Query data using SQL', code: `${BASE_CMD} ${cmd} --database DATABASE_NAME "SELECT * FROM home"` }; } function getWriteExample(cmd) { return { title: 'Write data from a file', code: `${BASE_CMD} ${cmd} --database DATABASE_NAME --file data.lp` }; } function getShowExample(cmd) { const cmdParts = cmd.split(' '); const resourceType = cmdParts.length > 1 ? cmdParts[1] : 'resources'; return { title: `List ${resourceType}`, code: `${BASE_CMD} ${cmd}` }; } function getCreateExample(cmd) { const cmdParts = cmd.split(' '); const resourceType = cmdParts.length > 1 ? cmdParts[1] : 'resource'; return { title: `Create a new ${resourceType}`, code: `${BASE_CMD} ${cmd} --name new-${resourceType}-name` }; } function getDeleteExample(cmd) { const cmdParts = cmd.split(' '); const resourceType = cmdParts.length > 1 ? cmdParts[1] : 'resource'; return { title: `Delete a ${resourceType}`, code: `${BASE_CMD} ${cmd} --name ${resourceType}-to-delete` }; } function getServeExample(cmd) { return { title: 'Start the InfluxDB server', code: `${BASE_CMD} serve --node-id my-node --object-store file --data-dir ~/.influxdb3_data` }; } function getDefaultExample(fullCmd, cmd) { return { title: `Run the ${fullCmd} command`, code: `${BASE_CMD} ${cmd}` }; } // Generate frontmatter for a command function generateFrontmatter(cmd) { const parts = cmd.split(' '); const lastPart = parts[parts.length - 1]; const fullCmd = cmd === '' ? BASE_CMD : `${BASE_CMD} ${cmd}`; // Determine a good description based on the command let description = ''; if (cmd === '') { description = `The ${BASE_CMD} CLI runs and interacts with the {{< product-name >}} server.`; } else { const cmdParts = cmd.split(' '); const lastCmd = cmdParts[cmdParts.length - 1]; // Use the description helper functions for consistency switch (lastCmd) { case 'query': case 'q': description = `The \`${fullCmd}\` command${getQueryDescription(cmd, fullCmd)}`; break; case 'write': case 'w': description = `The \`${fullCmd}\` command${getWriteDescription(cmd, fullCmd)}`; break; case 'show': description = `The \`${fullCmd}\` command${getShowDescription(cmd, fullCmd)}`; break; case 'create': description = `The \`${fullCmd}\` command${getCreateDescription(cmd, fullCmd)}`; break; case 'delete': description = `The \`${fullCmd}\` command${getDeleteDescription(cmd, fullCmd)}`; break; case 'serve': description = `The \`${fullCmd}\` command${getServeDescription(cmd, fullCmd)}`; break; default: description = `The \`${fullCmd}\` command${getDefaultDescription(cmd, fullCmd)}`; } } // Create the frontmatter let frontmatter = `--- title: ${fullCmd} description: > ${description} `; // Add source attribute for shared files if (cmd !== '') { // Build the path relative to the /content/shared/influxdb3-cli/ directory const relativePath = cmd.split(' ').join('/'); frontmatter += `source: /shared/influxdb3-cli/${relativePath === '' ? '_index' : relativePath}.md `; } // Close the frontmatter frontmatter += `--- `; return frontmatter; } // Generate Markdown for a command function generateCommandMarkdown(cmd) { try { debug(`Generating markdown for command: ${cmd}`); const fullCmd = cmd === '' ? BASE_CMD : `${BASE_CMD} ${cmd}`; let helpOutput = execSync(`${fullCmd} --help`).toString(); helpOutput = stripAnsiCodes(helpOutput); // Strip ANSI codes debug(`Cleaned help output for ${fullCmd} received`, helpOutput); // Extract sections from help output const usageMatch = helpOutput.match(/Usage:\s+([\s\S]+?)(?:\n\n|$)/); const usage = usageMatch ? usageMatch[1].trim() : ''; const argsMatch = helpOutput.match(/Arguments:\s+([\s\S]+?)(?:\n\n|$)/); const args = argsMatch ? argsMatch[1].trim() : ''; // Store option sections separately const optionSections = {}; const optionSectionRegex = /^([A-Za-z\s]+ Options?|Required):\s*$([\s\S]+?)(?=\n^[A-Za-z\s]+:|^$|\n\n)/gm; let sectionMatch; while ((sectionMatch = optionSectionRegex.exec(helpOutput)) !== null) { const sectionTitle = sectionMatch[1].trim(); const sectionContent = sectionMatch[2].trim(); debug(`Found option section: ${sectionTitle}`); optionSections[sectionTitle] = sectionContent; } // Fallback if no specific sections found if (Object.keys(optionSections).length === 0) { const flagsMatch = helpOutput.match(/(?:Flags|Options):\s+([\s\S]+?)(?:\n\n|$)/); if (flagsMatch) { debug('Using fallback Flags/Options section'); optionSections['Options'] = flagsMatch[1].trim(); } } debug('Extracted option sections', optionSections); // Format flags as a table, processing sections and handling duplicates/multi-lines let flagsTable = ''; const addedFlags = new Set(); // Track added long flags const tableRows = []; const sectionOrder = ['Required', ...Object.keys(optionSections).filter(k => k !== 'Required')]; // Prioritize Required for (const sectionTitle of sectionOrder) { if (!optionSections[sectionTitle]) continue; const sectionContent = optionSections[sectionTitle]; const lines = sectionContent.split('\n'); let i = 0; while (i < lines.length) { const line = lines[i]; // Regex to capture flag and start of description const flagMatch = line.match(/^\s+(?:(-\w),\s+)?(--[\w-]+(?:[=\s]<[^>]+>)?)?\s*(.*)/); if (flagMatch) { const shortFlag = flagMatch[1] || ''; const longFlagRaw = flagMatch[2] || ''; // Might be empty if only short flag exists (unlikely here) const longFlag = longFlagRaw.split(/[=\s]/)[0]; // Get only the flag name, e.g., --cluster-id from --cluster-id let description = flagMatch[3].trim(); // Check for multi-line description (indented lines following) let j = i + 1; while (j < lines.length && lines[j].match(/^\s{4,}/)) { // Look for lines with significant indentation description += ' ' + lines[j].trim(); j++; } i = j; // Move main index past the multi-line description // Clean description description = description .replace(/\s+\[default:.*?\]/g, '') .replace(/\s+\[env:.*?\]/g, '') .replace(/\s+\[possible values:.*?\]/g, '') .trim(); // Check if required based on section const isRequired = sectionTitle === 'Required'; // Add to table if not already added if (longFlag && !addedFlags.has(longFlag)) { // Use longFlagRaw which includes the placeholder for display tableRows.push(`| \`${shortFlag}\` | \`${longFlagRaw.trim()}\` | ${isRequired ? '_({{< req >}})_ ' : ''}${description} |`); addedFlags.add(longFlag); debug(`Added flag: ${longFlag} (Required: ${isRequired})`); } else if (!longFlag && shortFlag && !addedFlags.has(shortFlag)) { // Handle case where only short flag might exist (though unlikely for this CLI) tableRows.push(`| \`${shortFlag}\` | | ${isRequired ? '_({{< req >}})_ ' : ''}${description} |`); addedFlags.add(shortFlag); // Use short flag for tracking if no long flag debug(`Added flag: ${shortFlag} (Required: ${isRequired})`); } else if (longFlag) { debug(`Skipping duplicate flag: ${longFlag}`); } else { debug(`Skipping flag line with no long or short flag found: ${line}`); } } else { debug(`Could not parse flag line in section "${sectionTitle}": ${line}`); i++; // Move to next line if current one doesn't match } } } if (tableRows.length > 0) { // Sort rows alphabetically by long flag, putting required flags first tableRows.sort((a, b) => { const isARequired = a.includes('_({{< req >}})_'); const isBRequired = b.includes('_({{< req >}})_'); if (isARequired && !isBRequired) return -1; if (!isARequired && isBRequired) return 1; // Extract long flag for sorting (second column content between backticks) const longFlagA = (a.match(/\|\s*`.*?`\s*\|\s*`(--[\w-]+)/) || [])[1] || ''; const longFlagB = (b.match(/\|\s*`.*?`\s*\|\s*`(--[\w-]+)/) || [])[1] || ''; return longFlagA.localeCompare(longFlagB); }); flagsTable = `| Short | Long | Description |\n| :---- | :--- | :---------- |\n${tableRows.join('\n')}`; } // Extract description from help text (appears before Usage section or other sections) let descriptionText = ''; // Updated regex to stop before any known section header const descMatches = helpOutput.match(/^([\s\S]+?)(?=Usage:|Common Commands:|Examples:|Options:|Flags:|Required:|Arguments:|$)/); if (descMatches && descMatches[1]) { descriptionText = descMatches[1].trim(); } // Example commands const examples = []; // Updated regex to stop before any known section header const exampleMatch = helpOutput.match(/(?:Example|Examples):\s*([\s\S]+?)(?=\n\n|Usage:|Options:|Flags:|Required:|Arguments:|$)/i); if (exampleMatch) { // Found examples in help output, use them const exampleBlocks = exampleMatch[1].trim().split(/\n\s*#\s+/); // Split by lines starting with # (section comments) exampleBlocks.forEach((block, index) => { const lines = block.trim().split('\n'); const titleLine = lines[0].startsWith('#') ? lines[0].substring(1).trim() : `Example ${index + 1}`; const codeLines = lines.slice(titleLine === `Example ${index + 1}` ? 0 : 1) // Skip title line if we extracted it .map(line => line.replace(/^\s*\d+\.\s*/, '').trim()) // Remove numbering like "1. " .filter(line => line); if (codeLines.length > 0) { examples.push({ title: titleLine, code: codeLines.join('\n') }); } }); } else { // Fallback example generation if (cmd === '') { // ... (existing base command examples) ... } else { // ... (existing command-specific example generation using helpers) ... } } // Construct markdown content const frontmatter = generateFrontmatter(cmd); let markdown = frontmatter; markdown += `The \`${fullCmd}\` command`; // Use extracted description if available, otherwise fallback if (descriptionText) { markdown += ` ${descriptionText.toLowerCase().replace(/\.$/, '')}.`; } else if (cmd === '') { markdown += ` runs and interacts with the {{< product-name >}} server.`; } else { // Fallback description generation using helpers const cmdParts = cmd.split(' '); const lastCmd = cmdParts[cmdParts.length - 1]; switch (lastCmd) { case 'query': case 'q': markdown += getQueryDescription(cmd, fullCmd); break; case 'write': case 'w': markdown += getWriteDescription(cmd, fullCmd); break; case 'show': markdown += getShowDescription(cmd, fullCmd); break; case 'create': markdown += getCreateDescription(cmd, fullCmd); break; case 'delete': markdown += getDeleteDescription(cmd, fullCmd); break; case 'serve': markdown += getServeDescription(cmd, fullCmd); break; default: markdown += getDefaultDescription(cmd, fullCmd); } } markdown += `\n\n## Usage\n\n\n\n\`\`\`bash\n${usage}\n\`\`\`\n\n`; if (args) { markdown += `## Arguments\n\n${args}\n\n`; } if (flagsTable) { markdown += `## Options\n\n${flagsTable}\n\n`; } if (examples.length > 0) { markdown += `## Examples\n\n`; examples.forEach(ex => { markdown += `### ${ex.title}\n\n\n\n\`\`\`bash\n${ex.code}\n\`\`\`\n\n`; }); } return markdown; } catch (error) { console.error(`Error generating markdown for '${cmd}':`, error.message); if (DEBUG) console.error(error.stack); return null; } } // Generate reference page with proper frontmatter that imports from shared content function generateReferencePage(cmd, product) { // Skip the base command since it's not typically needed as a reference if (cmd === '') { return null; } const parts = cmd.split(' '); const fullCmd = cmd === '' ? BASE_CMD : `${BASE_CMD} ${cmd}`; // Build the appropriate menu path let menuParent; if (parts.length === 1) { menuParent = 'influxdb3'; // Top-level command } else { // For nested commands, the parent is the command's parent command menuParent = `influxdb3 ${parts.slice(0, -1).join(' ')}`; } // Determine a good description let description; const lastCmd = parts.length > 0 ? parts[parts.length - 1] : ''; switch (lastCmd) { case 'query': case 'q': description = `Use the ${fullCmd} command to query data in your {{< product-name >}} instance.`; break; case 'write': case 'w': description = `Use the ${fullCmd} command to write data to your {{< product-name >}} instance.`; break; case 'show': const showType = parts.length > 1 ? parts[1] : 'resources'; description = `Use the ${fullCmd} command to list ${showType} in your {{< product-name >}} instance.`; break; case 'create': const createType = parts.length > 1 ? parts[1] : 'resources'; description = `Use the ${fullCmd} command to create ${createType} in your {{< product-name >}} instance.`; break; case 'delete': const deleteType = parts.length > 1 ? parts[1] : 'resources'; description = `Use the ${fullCmd} command to delete ${deleteType} from your {{< product-name >}} instance.`; break; case 'serve': description = `Use the ${fullCmd} command to start and run your {{< product-name >}} server.`; break; default: description = `Use the ${fullCmd} command.`; } // Build the path to the shared content const sharedPath = parts.join('/'); // Create the frontmatter for the reference page const frontmatter = `--- title: ${fullCmd} description: > ${description} menu: ${product}: parent: ${menuParent} name: ${fullCmd} weight: 400 source: /shared/influxdb3-cli/${sharedPath}.md --- `; return frontmatter; } // Create the reference page files for different product variants async function createReferencePages(cmd) { if (cmd === '') return; // Skip the base command // Define the InfluxDB products that use this CLI const products = [ { id: 'influxdb3_core', path: 'influxdb3/core' }, { id: 'influxdb3_enterprise', path: 'influxdb3/enterprise' } ]; // Generate reference pages for each product for (const product of products) { const frontmatter = generateReferencePage(cmd, product.id); if (!frontmatter) continue; const parts = cmd.split(' '); const cmdPath = parts.join('/'); // Create the directory path for the reference file const refDirPath = path.join(__dirname, '..', 'content', product.path, 'reference', 'cli', 'influxdb3', ...parts.slice(0, -1)); const refFilePath = path.join(refDirPath, `${parts[parts.length - 1]}.md`); // Create directory if it doesn't exist ensureDirectoryExistence(refFilePath); // Write the reference file fs.writeFileSync(refFilePath, frontmatter); console.log(`Generated reference page: ${refFilePath}`); } } // Process a command and its subcommands recursively async function processCommand(cmd = '', depth = 0) { debug(`Processing command: "${cmd}" at depth ${depth}`); // Generate markdown for this command const markdown = generateCommandMarkdown(cmd); if (!markdown) { console.error(`Failed to generate markdown for command: ${cmd}`); return; } // Create file path and write content let filePath; if (cmd === '') { // Base command filePath = path.join(OUTPUT_DIR, '_index.md'); } else { const parts = cmd.split(' '); const dirPath = path.join(OUTPUT_DIR, ...parts.slice(0, -1)); const fileName = parts[parts.length - 1] === '' ? '_index.md' : `${parts[parts.length - 1]}.md`; filePath = path.join(dirPath, fileName); // For commands with subcommands, also create an index file if (depth < 3) { // Limit recursion depth try { const subcommandOutput = execSync(`${BASE_CMD} ${cmd} --help`).toString(); if (subcommandOutput.includes('Commands:')) { const subDirPath = path.join(OUTPUT_DIR, ...parts); const indexFilePath = path.join(subDirPath, '_index.md'); ensureDirectoryExistence(indexFilePath); fs.writeFileSync(indexFilePath, markdown); debug(`Created index file: ${indexFilePath}`); } } catch (error) { debug(`Error checking for subcommands: ${error.message}`); } } } ensureDirectoryExistence(filePath); fs.writeFileSync(filePath, markdown); console.log(`Generated: ${filePath}`); // Create reference pages for this command await createReferencePages(cmd); // Get and process subcommands if (depth < 3) { // Limit recursion depth const subcommands = getSubcommands(cmd); debug(`Found ${subcommands.length} subcommands for "${cmd}"`); for (const subCmd of subcommands) { await processCommand(subCmd.cmd, depth + 1); } } } // Main function async function main() { try { debug('Starting documentation generation'); // Process base command await processCommand(); // Get top-level commands const commands = getCommands(); debug(`Found ${commands.length} top-level commands`); if (commands.length === 0) { console.warn('Warning: No commands were found. Check the influxdb3 CLI help output format.'); } // Process each top-level command for (const { cmd } of commands) { await processCommand(cmd, 1); } console.log('Documentation generation complete!'); } catch (error) { console.error('Error in main execution:', error.message); if (DEBUG) console.error(error.stack); } } // Run the script main();