#!/usr/bin/env node /** * Documentation file opener * Opens existing documentation pages in your default editor * * Usage: * docs edit # Non-blocking (default) * docs edit --wait # Wait for editor to close * docs edit --list # List files without opening * docs edit --editor vim # Use specific editor */ import { parseArgs } from 'node:util'; import process from 'node:process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { existsSync } from 'fs'; import { parseDocumentationURL, urlToFilePaths } from '../lib/url-parser.js'; import { getSourceFromFrontmatter } from '../lib/content-utils.js'; import { resolveEditor } from './lib/editor-resolver.js'; import { spawnEditor, shouldWait } from './lib/process-manager.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Repository root const REPO_ROOT = join(__dirname, '..', '..'); // Colors for console output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', cyan: '\x1b[36m', }; /** * Print colored output */ function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** * Parse command line arguments */ function parseArguments() { const { values, positionals } = parseArgs({ options: { help: { type: 'boolean', default: false }, list: { type: 'boolean', default: false }, wait: { type: 'boolean', default: false }, editor: { type: 'string' }, }, allowPositionals: true, }); // First positional argument is the URL if (positionals.length > 0 && !values.url) { values.url = positionals[0]; } return values; } /** * Print usage information */ function printUsage() { console.log(` ${colors.bright}Documentation File Opener${colors.reset} ${colors.bright}Usage:${colors.reset} docs edit Open in editor (non-blocking) docs edit --wait Wait for editor to close docs edit --list List files without opening docs edit --editor Use specific editor ${colors.bright}Arguments:${colors.reset} Documentation URL or path ${colors.bright}Options:${colors.reset} --list List matching files without opening --wait Block until editor closes (for interactive use) --editor Override default editor --help Show this help message ${colors.bright}Examples:${colors.reset} # Quick edit (CLI exits immediately, editor runs in background) docs edit https://docs.influxdata.com/influxdb3/core/admin/databases/ docs edit /influxdb3/core/admin/databases/ # Interactive edit (CLI waits for you to close editor) docs edit /influxdb3/core/admin/databases/ --wait # Use specific editor docs edit /influxdb3/core/admin/databases/ --editor nano # List files only (useful for scripting) docs edit /influxdb3/core/admin/databases/ --list ${colors.bright}Editor Configuration:${colors.reset} The editor is selected in this order: 1. --editor flag 2. DOCS_EDITOR environment variable 3. VISUAL environment variable 4. EDITOR environment variable 5. System default (vim, nano, etc.) Examples: export EDITOR=vim export EDITOR=nano export DOCS_EDITOR="code --wait" ${colors.bright}Notes:${colors.reset} - Default behavior is non-blocking (agent-friendly) - Use --wait flag for interactive editing sessions - Multiple files may open if content is shared across products `); } /** * Find matching files for a URL */ function findFiles(url) { try { // Parse URL const parsed = parseDocumentationURL(url); log(`\nšŸ” Analyzing URL: ${url}`, 'bright'); log(` Product: ${parsed.namespace}/${parsed.product || 'N/A'}`, 'cyan'); log(` Section: ${parsed.section || 'N/A'}`, 'cyan'); // Get potential file paths const potentialPaths = urlToFilePaths(parsed); const foundFiles = []; for (const relativePath of potentialPaths) { const fullPath = join(REPO_ROOT, relativePath); if (existsSync(fullPath)) { foundFiles.push(relativePath); } } return { parsed, foundFiles }; } catch (error) { log(`\nāœ— Error parsing URL: ${error.message}`, 'red'); process.exit(1); } } /** * Check if file uses shared content * @param {string} filePath - Relative path from repo root * @returns {string|null} Path to shared source file or null */ function checkSharedContent(filePath) { const fullPath = join(REPO_ROOT, filePath); return getSourceFromFrontmatter(fullPath); } /** * Open files in editor */ function openInEditor(files, options = {}) { try { // Resolve editor command const editor = resolveEditor({ editor: options.editor }); const wait = shouldWait(options.wait); log(`\nšŸ“ Opening ${files.length} file(s) in ${editor}...`, 'bright'); if (!wait) { log(' Editor will open in background (CLI exits immediately)', 'cyan'); log(' Use --wait flag to block until editor closes', 'cyan'); } // Convert to absolute paths const absolutePaths = files.map((f) => join(REPO_ROOT, f)); // Spawn editor spawnEditor(editor, absolutePaths, { wait, onError: (error) => { log(`\nāœ— Failed to open editor: ${error.message}`, 'red'); log('\nTroubleshooting:', 'yellow'); log(' 1. Set EDITOR environment variable:', 'cyan'); log(' export EDITOR=vim', 'cyan'); log(' 2. Or use --editor flag:', 'cyan'); log(' docs edit --editor nano', 'cyan'); process.exit(1); }, }); // In non-blocking mode, give process time to spawn before exit if (!wait) { setTimeout(() => { log('āœ“ Editor launched\n', 'green'); process.exit(0); }, 100); } } catch (error) { log(`\nāœ— ${error.message}`, 'red'); process.exit(1); } } /** * Main entry point */ async function main() { const options = parseArguments(); // Show help if (options.help || !options.url) { printUsage(); process.exit(0); } // Find files const { foundFiles } = findFiles(options.url); if (foundFiles.length === 0) { log('\nāœ— No files found for this URL', 'red'); log('\nThe page may not exist yet. To create new content, use:', 'yellow'); log(' docs create --products ', 'cyan'); process.exit(1); } // Display found files log('\nāœ“ Found files:', 'green'); const allFiles = new Set(); for (const file of foundFiles) { allFiles.add(file); log(` • ${file}`, 'cyan'); // Check for shared content const sharedSource = checkSharedContent(file); if (sharedSource) { if (existsSync(join(REPO_ROOT, sharedSource))) { allFiles.add(sharedSource); log( ` • ${sharedSource} ${colors.yellow}(shared source)${colors.reset}`, 'cyan' ); } } } const filesToOpen = Array.from(allFiles); // List only mode if (options.list) { log(`\nāœ“ Found ${filesToOpen.length} file(s)`, 'green'); process.exit(0); } // Open in editor openInEditor(filesToOpen, { wait: options.wait, editor: options.editor, }); } // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { log(`\nFatal error: ${error.message}`, 'red'); console.error(error.stack); process.exit(1); }); } export { findFiles, openInEditor };